Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: medium
Valid

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.

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);
}
​
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.

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 14 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[M-01] `PuppyRaffle: enterRaffle` Use of gas extensive duplicate check leads to Denial of Service, making subsequent participants to spend much more gas than prev ones to enter

## Description `enterRaffle` function uses gas inefficient duplicate check that causes leads to Denial of Service, making subsequent participants to spend much more gas than previous users to enter. ## Vulnerability Details In the `enterRaffle` function, to check duplicates, it loops through the `players` array. As the `player` array grows, it will make more checks, which leads the later user to pay more gas than the earlier one. More users in the Raffle, more checks a user have to make leads to pay more gas. ## Impact As the arrays grows significantly over time, it will make the function unusable due to block gas limit. This is not a fair approach and lead to bad user experience. ## POC In existing test suit, add this test to see the difference b/w gas for users. once added run `forge test --match-test testEnterRaffleIsGasInefficient -vvvvv` in terminal. you will be able to see logs in terminal. ```solidity function testEnterRaffleIsGasInefficient() public { vm.startPrank(owner); vm.txGasPrice(1); /// First we enter 100 participants uint256 firstBatch = 100; address[] memory firstBatchPlayers = new address[](firstBatch); for(uint256 i = 0; i < firstBatchPlayers; i++) { firstBatch[i] = address(i); } uint256 gasStart = gasleft(); puppyRaffle.enterRaffle{value: entranceFee * firstBatch}(firstBatchPlayers); uint256 gasEnd = gasleft(); uint256 gasUsedForFirstBatch = (gasStart - gasEnd) * txPrice; console.log("Gas cost of the first 100 partipants is:", gasUsedForFirstBatch); /// Now we enter 100 more participants uint256 secondBatch = 200; address[] memory secondBatchPlayers = new address[](secondBatch); for(uint256 i = 100; i < secondBatchPlayers; i++) { secondBatch[i] = address(i); } gasStart = gasleft(); puppyRaffle.enterRaffle{value: entranceFee * secondBatch}(secondBatchPlayers); gasEnd = gasleft(); uint256 gasUsedForSecondBatch = (gasStart - gasEnd) * txPrice; console.log("Gas cost of the next 100 participant is:", gasUsedForSecondBatch); vm.stopPrank(owner); } ``` ## Recommendations Here are some of recommendations, any one of that can be used to mitigate this risk. 1. User a mapping to check duplicates. For this approach you to declare a variable `uint256 raffleID`, that way each raffle will have unique id. Add a mapping from player address to raffle id to keep of users for particular round. ```diff + uint256 public raffleID; + mapping (address => uint256) public usersToRaffleId; . . 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]); + usersToRaffleId[newPlayers[i]] = true; } // Check for duplicates + for (uint256 i = 0; i < newPlayers.length; i++){ + require(usersToRaffleId[i] != raffleID, "PuppyRaffle: Already a participant"); - 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); } . . . function selectWinner() external { //Existing code + raffleID = raffleID + 1; } ``` 2. Allow duplicates participants, As technically you can't stop people participants more than once. As players can use new address to enter. ```solidity 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]); } emit RaffleEnter(newPlayers); } ```

Support

FAQs

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

Give us feedback!