Beginner FriendlyFoundryNFT
100 EXP
View results
Submission Details
Severity: medium
Valid

Unexpected exponential gas costs for PuppyRaffle::enterRaffle() can cause denial of service attack

Summary

The current implementation of the duplicate error check can cause the cost of gas for each subsequent PuppyRaffle::enterRaffle() to increase exponentially. This is considered a Denial of Service attack as it means an attacker can enter a lage quantity addresses, increasing the size of the players array massively in a single transaction, thereby making the gas price for entry prohibitively expensive for any following players.

Vulnerability Details

For loops are significant gas consumers, so the fact that there two for loops over the entire players array, would cause the gas fees to increase exponentially as the players array grows. Normally, gas optimizations are considered differently, however, this is a security vulnerability because it can become a Denial of Service. An attacker can prevent or highly discourage additional players from entering the raffle by increasing the cost of gas to be so expensive as additional players will refuse to pay. This would then give the attacker to have a disproportionate advantage of winning the raffle.

Here is a test case to demonstrate the behavior:

function testEnterRaffleGasExceedsExpectation() public {
uint startAt = 100; // start high enough not to clobber existing addresses
uint batchCount = 5;
uint256 lastGas;
for (uint bIndex = 1; bIndex <= batchCount; ++bIndex) {
address[] memory toPlay = new address[](100);
for (uint pIndex = 0; pIndex < toPlay.length; ++pIndex) {
toPlay[pIndex] = address((startAt * bIndex) + pIndex);
}
uint256 gasStart = gasleft();
puppyRaffle.enterRaffle{value: entranceFee * toPlay.length}(toPlay);
uint256 gasEnd = gasleft();
uint256 gasSpent = gasStart - gasEnd;
console.log("The cost of gas for batch %s was %s", bIndex, gasSpent);
assertLt(lastGas, gasSpent);
lastGas = gasSpent;
}
}

As you can see, after every 100 bulk entries, the gas cost for the next entry is quite large:

The cost of gas for batch 1 was 6271948
The cost of gas for batch 2 was 18068139
The cost of gas for batch 3 was 37782733
The cost of gas for batch 4 was 65387327
The cost of gas for batch 5 was 100881922

Impact

Once the players array has grown large enough, additional players will very likely find the cost of the gas to join prohibitively expensive. This would limit the number of players that would join the raffle and give an attacker a disproportionate advantage of winning the raffle.

Tools Used

Manual Review and Foundry

Recommendations

  1. Consider allowing duplicate addresses. Based on the documentation and the logic in the contract there is no clear reason why duplicates should not be allowed. No matter how many times a player wants to enter the raffle, they have to pay the entranceFee. And selecting the winner does not appear to be adversely effected should there be a single address in the array more than once.

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]);
}
-
- // Check for duplicates
- 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);
}
  1. If there is a legitimate business case to be made that is not clear, then consider using an alternative method for tracking and determining if the address is a duplicate. One method that would be far more efficient might be using a mapping to track address and a flag to indicate the address is already entered.

@@ +24,1 @@ contract PuppyRaffle is ERC721, Ownable {
+ mapping(address => bool) private existingAddress;
@@ +83,3 @@ contract PuppyRaffle is ERC721, Ownable {
+ if (!existingAddress[newPlayers[i]]) {
+ players.push(newPlayers[i]);
+ existingAddress[newPlayers[i]] = true;
@@ -88,7 @@ contract PuppyRaffle is ERC721, Ownable {
-
- // Check for duplicates
- 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");
- }
- }
@@ +101,1 @@ contract PuppyRaffle is ERC721, Ownable {
+ delete existingAddress[playerAddress];
@@ +146,3 @@ contract PuppyRaffle is ERC721, Ownable {
+ for (uint256 index = 0; index < players.length; ++index) {
+ delete existingAddress[players[index]];
+ }
Updates

Lead Judging Commences

Hamiltonite Lead Judge about 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

denial-of-service-in-enter-raffle

Support

FAQs

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

Give us feedback!