Puppy Raffle

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

DoS by Refunding Multiple Players to Enable Duplicate Zeros

Root + Impact

An attacker can refund themselves multiple times (creating zero addresses in the players array), then call enterRaffle to cause the duplicate check to revert when comparing zero addresses against each other, preventing legitimate entries until the next raffle.

Description

  • The normal behavior is for players to enter and optionally refund themselves, with the duplicate check preventing the same address from appearing twice.

  • The issue is that after refunding, a player's slot becomes address(0). Multiple refunds create multiple zero addresses. When new players enter, the duplicate check compares these zero addresses and reverts, blocking all entries.

// Root cause in the codebase with @> marks to highlight the relevant section
function refund(uint256 playerIndex) public {
address playerAddress = players[playerIndex];
require(playerAddress == msg.sender, "PuppyRaffle: Only the player can refund");
require(playerAddress != address(0), "PuppyRaffle: Player already refunded, or is not active");
payable(msg.sender).sendValue(entranceFee);
@> players[playerIndex] = address(0); // Creates a zero address in array
emit RaffleRefunded(playerAddress);
}
function enterRaffle(address[] memory newPlayers) public payable {
require(msg.value == entranceFee * newPlayers.length, "PuppyRaffle: Must send enough to enter raffle");
for (uint256 i = 0; i < newPlayers.length; i++) {
players.push(newPlayers[i]);
}
// Check for duplicates
@> for (uint256 i = 0; i < players.length - 1; i++) {
@> for (uint256 j = i + 1; j < players.length; j++) {
@> require(players[i] != players[j], "PuppyRaffle: Duplicate player");
@> }
@> }
emit RaffleEnter(newPlayers);
}

Risk

Likelihood:

  • An attacker can refund their own entry to create a zero address, then call refund on another player they previously entered

  • Once multiple zero addresses exist in the array, any call to enterRaffle will revert on the duplicate check when comparing address(0) == address(0)

  • This persists until the raffle winner is selected and the players array is cleared

Impact:

  • Raffle is blocked from accepting new players for an extended period

  • Denial of service until the next winner is selected

  • Loss of potential entrance fees for the protocol

Proof of Concept

// Setup: Attacker entered at index 0 and 1
// 1. Attacker calls refund(0) -> players[0] = address(0)
// 2. Attacker calls refund(1) -> players[1] = address(0)
// 3. Any call to enterRaffle(newPlayers) now iterates:
// - Compares players[0] (0x0000) with players[1] (0x0000)
// - Fails with "Duplicate player"
// - All new entries are blocked

Recommended Mitigation

// Option 1: Skip zero addresses in duplicate check
for (uint256 i = 0; i < players.length - 1; i++) {
+ if (players[i] == address(0)) continue;
for (uint256 j = i + 1; j < players.length; j++) {
+ if (players[j] == address(0)) continue;
require(players[i] != players[j], "PuppyRaffle: Duplicate player");
}
}
// Option 2: Use mapping approach (preferred)
mapping(address => bool) private isActivePlayer;
function refund(uint256 playerIndex) public {
address playerAddress = players[playerIndex];
require(playerAddress == msg.sender, "PuppyRaffle: Only the player can refund");
require(playerAddress != address(0), "PuppyRaffle: Player already refunded, or is not active");
payable(msg.sender).sendValue(entranceFee);
isActivePlayer[playerAddress] = false;
players[playerIndex] = address(0);
emit RaffleRefunded(playerAddress);
}
function enterRaffle(address[] memory newPlayers) public payable {
require(msg.value == entranceFee * newPlayers.length, "PuppyRaffle: Must send enough to enter raffle");
for (uint256 i = 0; i < newPlayers.length; i++) {
require(!isActivePlayer[newPlayers[i]], "PuppyRaffle: Duplicate player");
players.push(newPlayers[i]);
isActivePlayer[newPlayers[i]] = true;
}
emit RaffleEnter(newPlayers);
}
Updates

Lead Judging Commences

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