Puppy Raffle

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

Looping through array to check for duplicates in `PuppyRaffle::enterRaffle` is a potential Denial of Service (Dos) attack, incrementing gas costs for future entrants

Description:

An unbounded for loop in PuppyRaffle::enterRaffle leads to a potential Denial of Service. The first user who enters the raffle will pay a much lower gas cost than the 100th user, for example.
This is because the cost of entering the raffle increases as the number of participants grows.
Every time a user wants to enter the raffle, the for loop in PuppyRaffle::enterRaffle iterates over all users who have already entered the raffle.

// @audit Dos Attack
@> for (uint256 i = 0; i < players.length - 1; i++) {
// @audit Dos
for (uint256 j = i + 1; j < players.length; j++) {
require(
players[i] != players[j],
"PuppyRaffle: Duplicate player"
);
}
}

Impact: This design causes increasing gas costs for users as the raffle grows, potentially making the function prohibitively expensive to execute. In extreme cases, it may prevent users from entering the raffle once the array becomes sufficiently large, resulting in a Denial of Service (DoS).

Proof of Concept:

If we have 2 sets of 100 players enter, the gas costs will be such:

  • 1st 100 players: 6538458

  • 2nd 100 players: 19011064

This more than 3x more expensive for the second 100 players.

Proof Of Concept Place the following test into `PuppyRaffleTest.t.sol`.
function test_denialOfService() public {
uint256 gasStart1 = gasleft();
uint256 numberOfPlayers = 100;
address[] memory players = new address[](numberOfPlayers);
for (uint256 i = 0; i < numberOfPlayers; i++) {
players[i] = address(i + 1);
}
puppyRaffle.enterRaffle{value: entranceFee * players.length}(players);
uint256 gasUsed1 = gasStart1 - gasleft();
uint256 gasStart2 = gasleft();
uint256 numberOfPlayers2 = 100;
address[] memory players2 = new address[](numberOfPlayers2);
for (uint256 i = 0; i < numberOfPlayers2; i++) {
players2[i] = address(i + numberOfPlayers + 1);
}
puppyRaffle.enterRaffle{value: entranceFee * players2.length}(players2);
uint256 gasUsed2 = gasStart2 - gasleft();
assertGt(gasUsed2, gasUsed1);
console2.log(gasUsed1);
console2.log(gasUsed2);
}

Recommended Mitigation: There are a few recommendations.

  1. Consider allowing duplicates. Users can make new wallet addresses anyways, so a duplicate check doesn't prevent the same person from entering multiple times, only the same wallet address.

  2. Consider using a mapping to check for duplicates. This would allow constant time lookup of whether a user has already entered.

+ mapping(address => uint256) public addressToRaffleId;
+ uint256 public raffleId = 0;
.
.
.
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]);
+ addressToRaffleId[newPlayers[i]] = raffleId;
}
- // Check for duplicates
+ // Check for duplicates only from the new players
+ for (uint256 i = 0; i < newPlayers.length; i++) {
+ require(addressToRaffleId[newPlayers[i]] != raffleId, "PuppyRaffle: Duplicate player");
+ }
- for (uint256 i = 0; i < players.length; i++) {
- for (uint256 j = i + 1; j < players.length; j++) {
- require(players[i] != players[j], "PuppyRaffle: Duplicate player");
- }
- }
emit RaffleEnter(newPlayers);
}
.
.
.
function selectWinner() external {
+ raffleId = raffleId + 1;
require(block.timestamp >= raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
Updates

Lead Judging Commences

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