Puppy Raffle

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

Lack of Access Control on Critical Function PuppyRaffle::selectWinner

Root + Impact

Root Cause

The contract does not enforce role‑based access control on selectWinner(). As a result, anyone can trigger winner selection at arbitrary times. This enables attackers to end raffles early, bias RNG outcomes, and compromise governance transparency. The impact is severe, honest players lose funds or fair odds, and the raffle system becomes untrustworthy.

Description

The selectWinner() function is callable by any external account without restrictions. This lack of access control allows malicious actors to prematurely end raffles, manipulate timing of winner selection, and undermine fairness. Critical lifecycle functions should be restricted to trusted roles or governance mechanisms.

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 totalAmountCollected = players.length * entranceFee;
uint256 prizePool = (totalAmountCollected * 80) / 100;
uint256 fee = (totalAmountCollected * 20) / 100;
totalFees = totalFees + uint64(fee);
uint256 tokenId = totalSupply();
// We use a different RNG calculate from the winnerIndex to determine rarity
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;
}
delete players;
raffleStartTime = block.timestamp;
previousWinner = winner;
(bool success,) = winner.call{value: prizePool}("");
require(success, "PuppyRaffle: Failed to send prize pool to winner");
_safeMint(winner, tokenId);
}

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Impact 1

  • Impact 2

Proof of Concept

  1. Contract allows anyone to call selectWinner().

  2. No onlyOwner or role‑based modifier is applied.

  3. Attacker waits until raffle has enough players, the execute the code below

  4. Result:

    • Attacker can prematurely end the raffle before intended duration.

    • Attacker can repeatedly call selectWinner() to manipulate timing and randomness.

    • Honest players lose funds or have odds skewed.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./PuppyRaffle.sol";
contract WinnerHijacker {
PuppyRaffle public raffle;
constructor(address raffleAddress) {
raffle = PuppyRaffle(raffleAddress);
}
function forceWinnerSelection() external {
raffle.selectWinner(); // anyone can call this
}
}

Recommended Mitigation

  • Restrict access to selectWinner() using onlyOwner or role‑based modifiers.

  • Enforce lifecycle checks (raffle duration, minimum players) before allowing execution.

Updates

Lead Judging Commences

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