Puppy Raffle

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

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

### [M-#] (ROOT + IMPACT) Looping through th eplayers array, to check the duplicates in `PuppyRaffle:enterRaffle` is a potential denial of Service(DoS) attack, incrementing gas costs for future entrants
IMPACT: MEDIUM
LIKELIHOOD: MEDIUM
**Description:**The `PuppyRaffle::enterRaffle` function loops through the `players` array to check for duplicates. However, the longer the `PuppyRaffle::players` array is more checks a new player will have to make. This means the gas costs for players who enter right when the raffle started will be dramatically lower than those who enter ;later. Every additional address in the `players` array, is an additionall check the loop will have to make.
```javascript
// @audit DoS attack
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");
}
}
```
The gas costs for the raffle entrants will greatly increase as more players enter the raffle. Discouraging the laterusers from entering and causing a rush at the start of a raffle to be one of first entrants in the queue.
An attacker might make the `PuppyRaffle::entrants` array so big. that no one else enters, guranteeing themselves win.
<details>
<summary>PoC</summary>
```javascript
function test_DoSAttack() public {
vm.txGasPrice(1);
uint256 playersNum = 100;
address[] memory players = new address[](playersNum);
// the below is how we generate 100 different player address
for(uint256 i=0; i<playersNum; i++) {
players[i] = address(i);
}
uint256 gasEnter = gasleft();
puppyRaffle.enterRaffle{value: entranceFee*players.length}(players);
uint256 gasEnd = gasleft();
uint256 gasFirstUsed = (gasEnter - gasEnd) * tx.gasprice;
console.log("Gas used of the first 100 players", gasFirstUsed);
address[] memory playersTwo = new address[](playersNum);
for(uint256 i=0; i<playersNum; i++) {
playersTwo[i] = address(i+playersNum+1);
}
uint256 gasEnter2 = gasleft();
puppyRaffle.enterRaffle{value: entranceFee*playersTwo.length}(playersTwo);
uint256 gasEnd2 = gasleft();
uint256 gasSecondUsed = (gasEnter2 - gasEnd2) * tx.gasprice;
console.log("Gas used of the second 100 players", gasSecondUsed);
// assert
assert(gasFirstUsed<gasSecondUsed);
}
``
</details>
If we have two sets of 100 players,
The gas cost of first 100 players is around ~23971625
The gas cost of the second 100 players is around ~88937365
which is a way lot higher, and more expensive
in competetie audit:

Recommended Mitigation

There are a few recommendations
1. Consider allwoing duplicates. Users can make new wallet addresses anyways, so a duplicate check doesn't prevernt the same person from entering multiple times, onlyt the same wallet address
2. Consider using a mapping to check duplicates. This would allow you to check for duplicates in constant time, rather than linear time. You could have each raffle have a uint256 id, and the mapping would be a player address mapped to the raffle Id.
```diff
+ 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 = raffleStartTime + raffleDuration, "PuppyRaffle: Raffle not over");
```
Updates

Lead Judging Commences

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