Puppy Raffle

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

DoS — `enterRaffle()` duplicate check is O(n²) over all players

Author Revealed upon completion

Root + Impact

Description

The duplicate check in enterRaffle() loops over the entire players array (all previously entered players, not just the new ones) with a nested loop — O(n²) comparisons. Gas grows quadratically with the raffle size.

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

Risk

Likelihood: High — the cost grows deterministically with every entry; no attacker is needed.

Impact: Medium — entering a single player costs 128k gas with 10 players in the raffle, 1.2M with 50, 4.5M with 100, and 17.3M with 200 (over half the ~30M block gas limit). Past ~300 players a single entry exceeds the block limit entirely and nobody can enter anymore. The raffle also becomes unfair: later players pay far more gas than early ones for the same ticket.

Proof of Concept

The test measures the gas of entering one additional player at increasing raffle sizes and asserts the cost explodes (gas at 200 players is more than 10x the gas at 10 players).

function test_M1_EnterRaffleGasGrowsQuadratically() public {
_bulkEnter(10, 1000);
uint256 gasAt10 = _measureSingleEntry(9000); // 11th player
_bulkEnter(39, 2000); // total 50
uint256 gasAt50 = _measureSingleEntry(9001); // 51st player
_bulkEnter(49, 3000); // total 100
uint256 gasAt100 = _measureSingleEntry(9002); // 101st player
_bulkEnter(99, 4000); // total 200
uint256 gasAt200 = _measureSingleEntry(9003); // 201st player
​
// measured: 128,079 / 1,238,519 / 4,503,069 / 17,287,169 gas
assertGt(gasAt200, gasAt10 * 10, "cost of entering explodes as the raffle grows");
assertGt(gasAt100, gasAt50, "cost grows with player count");
}

Recommended Mitigation

Track duplicates with a mapping(address => bool) (O(1) lookup) instead of a nested loop over the whole array.

+ mapping(address => bool) public hasEntered;
...
- 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");
- }
- }
+ for (uint256 i = 0; i < newPlayers.length; i++) {
+ require(!hasEntered[newPlayers[i]], "PuppyRaffle: Duplicate player");
+ hasEntered[newPlayers[i]] = true;
+ players.push(newPlayers[i]);
+ }

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!