Puppy Raffle

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

Deterministic Randomness Allows Winner Prediction

Root + Impact

The selectWinner function uses msg.sender, block.timestamp, and block.difficulty for random number generation. These values are publicly known or can be influenced by miners/validators, allowing an attacker to predict and manipulate the winner and NFT rarity.

Description

  • The normal behavior is for the protocol to select a random winner and assign a random rarity to their NFT after the raffle duration expires.

  • The issue is that the random number is derived from msg.sender (known), block.timestamp (predictable by miners), and block.difficulty (known or influenceable), making the outcome deterministic and predictable.

// Root cause in the codebase with @> marks to highlight the relevant section
function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
@> uint256 winnerIndex =
@> uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
address winner = players[winnerIndex];
// ...
@> uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
if (rarity <= COMMON_RARITY) {
tokenIdToRarity[tokenId] = COMMON_RARITY;
} else if (rarity <= COMMON_RARITY + RARE_RARITY) {
tokenIdToRarity[tokenId] = RARE_RARITY;
} else {
tokenIdToRarity[tokenId] = LEGENDARY_RARITY;
}
}

Risk

Likelihood:

  • An attacker can call selectWinner at a specific block where they know they will win

  • Miners/validators can influence block.timestamp and block.difficulty to ensure a specific outcome

  • The attacker can also enter the raffle multiple times at indices they control to guarantee they are at the winning index

Impact:

  • Attacker can guarantee themselves a win and potentially a legendary NFT

  • Unfair raffle defeats the intended purpose of randomness

  • Economic impact from predetermined winners claiming the prize pool

Proof of Concept

// Attacker's strategy:
// 1. Enter raffle at indices they control (e.g., indices 0, 1, 2, 3)
// 2. Calculate winning index: keccak256(msg.sender, block.timestamp, block.difficulty) % players.length
// 3. Since block.timestamp is known at transaction time, calculate which block produces winning index
// 4. Call selectWinner at that specific block with known msg.sender
// 5. Win guaranteed
// Rarity can be similarly predicted/influenced

Recommended Mitigation

// Use Chainlink VRF for verifiable randomness
import {VRFConsumerBaseV2} from "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol";
import {VRFCoordinatorV2Interface} from "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
contract PuppyRaffle is ERC721, Ownable, VRFConsumerBaseV2 {
VRFCoordinatorV2Interface COORDINATOR;
uint64 s_subscriptionId;
uint256 public s_requestId;
function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
s_requestId = COORDINATOR.requestRandomWords(
keyHash,
s_subscriptionId,
requestConfirmations,
callbackGasLimit,
numWords
);
}
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
uint256 winnerIndex = randomWords[0] % players.length;
// Use randomWords[1] for rarity selection
// Complete winner selection logic...
}
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 8 hours 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!