Puppy Raffle

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

Quadratic gas cost in `enterRaffle` duplicate check causes denial of service

The duplicate check in enterRaffle (L96-102) uses nested loops that compare every pair of addresses in the entire players array:

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");
}
}

This performs N * (N - 1) / 2 comparisons where N = players.length. Each comparison requires two storage reads (SLOAD: 2,100 gas cold, 100 gas warm). The gas cost grows quadratically with the number of players:

  • 100 players: ~4,950 comparisons (~10-22M gas)

  • 200 players: ~19,900 comparisons (~40-87M gas, exceeds 30M block gas limit)

Once players.length exceeds approximately 100-200 entries, new players cannot enter because the transaction exceeds the block gas limit. Early entrants pay low gas costs while later entrants are priced out or blocked entirely.

Additionally, players.length - 1 at L96 underflows to type(uint256).max if players is empty and newPlayers.length == 0 (Solidity 0.7.6 has no underflow protection), causing an out-of-gas revert.

Exploit Scenario

  1. An attacker enters the raffle with a large batch of unique addresses in a single transaction (e.g., 100 addresses).

  2. Subsequent callers attempting to enter must iterate the duplicate check over 100+ existing entries plus their own.

  3. The gas cost for new entrants exceeds the block gas limit. No further players can join.

  4. The attacker's addresses dominate the player pool, increasing their odds of winning.

Recommendations

Short term: Replace the array-based duplicate check with a mapping(address => bool) that tracks whether an address has entered. This reduces the check from O(n^2) to O(1) per entry:

mapping(address => bool) public hasEntered;
// In enterRaffle:
for (uint256 i = 0; i < newPlayers.length; i++) {
require(!hasEntered[newPlayers[i]], "PuppyRaffle: Duplicate player");
hasEntered[newPlayers[i]] = true;
players.push(newPlayers[i]);
}

Long term: Clear the mapping entries when selectWinner resets the raffle, or use a round-based nonce in the mapping key to avoid clearing costs.

Updates

Lead Judging Commences

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