enterRaffle should add the new entrants to the current round and reject any address that has already entered (README rule 2: "Duplicate addresses are not allowed"). Any user should be able to enter at any point before the draw, for a gas cost that does not depend on how many players entered before them.
After pushing the new entrants (L81-83), enterRaffle compares every pair in the whole storage array (L86-90). That is n(n-1)/2 iterations on every call, each one reading storage, and nothing caps n. The PoC measures about 850 gas per pair: one ticket costs 55,517 gas as player 5 and 17.27M gas as player 201, and at 265 players a one-address entry needs 30,048,477 gas. That is more than a 30M-gas transaction can carry, so nobody can enter again until selectWinner resets the array.
Likelihood:
No privilege or special setup is needed. enterRaffle is public and nothing caps the number of players, so a busy round reaches the ceiling through normal growth with no attacker: 265 players under a 30M gas limit, or 198 players under EIP-7825's 2^24 per-transaction cap.
A griefer can also reach the ceiling on purpose. The PoC does it in 14 transactions that each fit under 30M gas, about 182M gas in total. The griefer still pays entranceFee for each of its 261 tickets and loses about 20% of that stake to fees at the draw, so the deliberate path costs real money.
Cost becomes a problem well before the hard ceiling. A single ticket already costs 4.48M gas at player 101 and 17.27M gas at player 201, which discourages late entrants as the round fills.
Impact:
Entering is partly unavailable. Once the array reaches the ceiling, every enterRaffle call fails for lack of gas, so late users cannot join the round. In the PoC the honest entrant's call reverts, no entry is recorded, and the user keeps their ETH. No funds are stolen or lost.
Identical tickets cost very different amounts of gas. One ticket costs 311 times as much as player 201 as it does as player 5, and the same 100-address batch costs 6.52M gas as the first batch but 19.20M gas as the second.
The damage is limited to one round. selectWinner has no pair loop: it costs 1.74M gas with 300 players, runs delete players, and entry reopens for the next round. A griefer gains only ticket share (261 of 265), not profit. That keeps the impact at Medium.
test/PoC_PR-07.t.sol has five tests. They deploy PuppyRaffle directly and, before each measured call, cool its storage with Foundry's vm.cool cheatcode (declared through a local interface), so each figure matches a fresh transaction. The tests show that the same 100-address batch costs 6.52M gas first and 19.20M gas second, and that one ticket costs 55,517 gas as player 5 but 38.39M gas as player 301. Under a 30M cap entry closes at 265 players (198 under a 2^24 cap): the same call succeeds with unlimited gas, and the honest user keeps their ETH. A further test shows selectWinner still runs with 300 players and reopens entry. Because the capped calls give the callee the full cap with no intrinsic gas deducted, the measured ceilings are slightly generous; run the tests with forge test --match-path test/PoC_PR-07.t.sol -vv.
File test/PoC_PR-07.t.sol; run FOUNDRY_FFI=false forge test --match-path test/PoC_PR-07.t.sol -vv.
## 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); } ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.