Puppy Raffle

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

Unbounded duplicate check in enterRaffle enables a denial of service

Root + Impact

Description

function enterRaffle(address[] memory newPlayers) public payable {
...
@> 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: Medium — an attacker can cheaply flood the array with early entries, or the array simply grows as a popular raffle fills. No special privileges are needed.

Impact: Medium — each new entry costs more than the last; past a certain size a single enterRaffle call exceeds the block gas limit (~30M) and can no longer be executed, locking out all further participation.

Proof of Concept

This test enters two batches of 100 players and measures the gas of each
enterRaffle call. The second batch — entering when 100 players are already in
the array — costs far more than the first, confirming the cost grows with the
number of existing players.

Measured gas:
first batch: 23,990,822
second batch: 88,937,355 (~3.7x the first, and above the ~30M block gas limit)

The second batch alone exceeds Ethereum's block gas limit, meaning that in
production this entry could not be mined at all.

function test_dos_entering_gets_more_expensive() public {
uint256 groupSize = 100;
​
address[] memory first = new address[](groupSize);
for (uint256 i = 0; i < groupSize; i++) {
first[i] = address(uint160(i + 1));
}
uint256 gasBefore1 = gasleft();
puppyRaffle.enterRaffle{value: entranceFee * groupSize}(first);
uint256 gasFirst = gasBefore1 - gasleft();
​
address[] memory second = new address[](groupSize);
for (uint256 i = 0; i < groupSize; i++) {
second[i] = address(uint160(i + 1 + groupSize)); // 101..200
}
uint256 gasBefore2 = gasleft();
puppyRaffle.enterRaffle{value: entranceFee * groupSize}(second);
uint256 gasSecond = gasBefore2 - gasleft();
​
assertGt(gasSecond, gasFirst);
}

Recommended Mitigation

Remove the O(n^2) duplicate check. Track entrants with a
mapping(address => bool) (or a per-raffle id) so duplicate detection is O(1)
per entry. Alternatively, allow duplicates — a user can already enter from
multiple wallets, so the check adds little protection at an unbounded gas cost.

- 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");
- }
- }
+ require(!hasEntered[newPlayers[i]], "PuppyRaffle: Duplicate player");
+ hasEntered[newPlayers[i]] = true;
Updates

Lead Judging Commences

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