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

The duplicates check makes the gas cost to entry exponentially higher for every new participant

Summary

The duplicates check makes the gas cost to entry exponentially higher for every new participant to the point of millions of gas for players number 100 and 101.

Vulnerability Details

The double loop used to check the duplicates condition generates extreme gas costs for each new player that joins the raffle:

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");
}

PoC

Please make the following modifications to PuppyRaffleTest.t.sol:

define the following addresses in state variables section:

address randomPlayer = makeAddr("You did not wake up today to be mediocre.");
address mAlice = makeAddr("We are what we repeatedly do. Excellence then is not an act but a habit.");

define the following function:

function addHundred() internal {
address[] memory newPlayers = new address[](100);
for (uint256 i = 1; i < 101; i++) {
address randomPlayer = address(i);
newPlayers[i - 1] = randomPlayer;
}
puppyRaffle.enterRaffle{value: entranceFee * 100}(newPlayers);
}

call the new function in setUp:

function setUp() public {
puppyRaffle = new PuppyRaffle(
entranceFee,
feeAddress,
duration
);
++ addHundred();
}

copy the following test:

function testPlayers100and101() public {
address[] memory players = new address[](2);
players[0] = randomPlayer;
players[1] = mAlice;
puppyRaffle.enterRaffle{value: entranceFee * 2}(players);
assertEq(puppyRaffle.players(100), randomPlayer);
assertEq(puppyRaffle.players(101), mAlice);
}

run with forge test --mt testPlayers100and101

Result:

Running 1 test for test/PuppyRaffleTest.t.sol:PuppyRaffleTest
[PASS] testPlayers100and101() (gas: 4368276)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 30.51ms

In today's prices gas: 4368276 = approx $1507.96

forge test --mt testPlayers100and101 --gas-report

Running 1 test for test/PuppyRaffleTest.t.sol:PuppyRaffleTest
[PASS] testPlayers100and101() (gas: 4373736)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 32.95ms
| src/PuppyRaffle.sol:PuppyRaffle contract | | | | | |
|------------------------------------------|-----------------|---------|---------|---------|---------|
| Deployment Cost | Deployment Size | | | | |
| 3229550 | 14458 | | | | |
| Function Name | min | avg | median | max | # calls |
| enterRaffle | 4346208 | 5287120 | 5287120 | 6228032 | 2 |
| players | 702 | 702 | 702 | 702 | 2 |

If we modify the addHundred function to add 150 instead of 100, adjust the checks, then we call again a function that adds 2 new players, the gas cost is over $3k.

Running 1 test for test/PuppyRaffleTest.t.sol:PuppyRaffleTest
[PASS] testPlayers100and101() (gas: 9470045)
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 64.48ms

Impact

The gas costs to join the raffle for the players that join towards the end are unjustifiably higher compared to the first joiners. It get's super expensive really fast, up to the point it reaches the gas block limit.

Tools Used

Manual review, foundry tests

Recommendations

The approach undertaken has a time complexity of O(n^2). This can be done in O(n) as follows:

++ mapping(address => bool) private activePlayers;
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");
-- }
-- }
++ address player = newPlayers[i];
++ // Check if the player already entered
++ require(!activePlayers[player], "PuppyRaffle: Player already registered");
++ // Set player active
++ activePlayers[player] = true;
++ players.push(player);
++ }
emit RaffleEnter(newPlayers);
}

We keep both the old players array and the new activePlayers because we are still going to use the players array for selecting a winner or finding the players index, but we need the mapping to ensure a faster and less resource intensive duplication check. Other parts of the code should be updated to reflect the new activePlayers mapping, for example there should be a delete activePlayers in selectWinner function.

Updates

Lead Judging Commences

Hamiltonite Lead Judge over 1 year 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.