Puppy Raffle

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

DOS while Entering the Raffle

Root + Impact

Description

  • In the Puppy Raffle FirstFlight 1 contract, the enterRaffle function prevents duplicate entries by iterating through the entire players array every time a new player is added. Because this is an O(n) operation inside a loop, the gas required to execute the transaction increases drastically as the players array grows. Once the array reaches a critical size, the gas cost to iterate through it will exceed the Ethereum block gas limit, causing the transaction to permanently fail.


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

Risk

  • Permanent Denial of Service (DoS): Once the array becomes too large, no new users will ever be able to enter the raffle, permanently halting the protocol's core functionality.

  • Griefing: A malicious actor could intentionally fill the array with enough minimum-value transactions to artificially hit the block gas limit, deliberately bricking the contract for all legitimate users.

Proof of Concept

Arrange: A batch of 100 valid user addresses is generated and successfully entered into the raffle via enterRaffle.

  • Act: A second, much larger batch of 1,500 new user addresses is generated. The test attempts to enter them into the ongoing raffle.

  • Assert: The transaction immediately crashes and throws an [OutOfGas] EvmError: OutOfGas. The for loop requires more computational fuel to check the previous entries than the EVM allows, mathematically proving the function cannot scale.

function test_DOS_If_Many_user_EnterRaffle() public {
//Arrange
uint256 batch1 = 100;
address[] memory batch1Users = new address[](batch1);
for (uint256 i = 0; i < batch1; i++) {
batch1Users[i] = address(i + 1);
}
pRaffle.enterRaffle{value: STARTING_AMOUNT * batch1}(batch1Users);
uint256 batch2 = 1500;
address[] memory batch2User = new address[](batch2);
for (uint256 i = 0; i < batch2; i++) {
batch2User[i] = address(i + 101);
}
//Act
pRaffle.enterRaffle{value: STARTING_AMOUNT * batch2}(batch2User);
//Assert
}
[FAIL: EvmError: Revert] test_DOS_If_Many_user_EnterRaffle() (gas: 1057057470)
Traces:
[9286207] TestPRaffle::setUp()
├─ [4391286] → new DeployPuppyRaffle@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ └─ ← [Return] 21598 bytes of code
├─ [4551931] DeployPuppyRaffle::run()
│ ├─ [0] VM::broadcast()
│ │ └─ ← [Return]
│ ├─ [4488167] → new PuppyRaffle@0x34A1D3fff3958843C43aD80F30b94c510645C316
│ │ ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: DefaultSender: [0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38])
│ │ └─ ← [Return] 18943 bytes of code
│ └─ ← [Return] PuppyRaffle: [0x34A1D3fff3958843C43aD80F30b94c510645C316]
├─ [186492] → new AttackPRaffle@0x2e234DAe75C793f67A35089C9d99245E1C58470b
│ └─ ← [Return] 820 bytes of code
├─ [0] VM::deal(spider: [0xB279F90f644e63EAca636d78E1d3fcC206632F63], 10000000000000000000 [1e19])
│ └─ ← [Return]
├─ [0] VM::deal(alie: [0x70E1B74cD0d17f05De348115B5Cd2772812B906F], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [0] VM::deal(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [0] VM::deal(dan: [0xb72116984E306d834a0ae638688Ef9AF1f7FE2cd], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [0] VM::deal(eli: [0xEa61F454C2B4A5A16AB556DBE8DBB176C1D02177], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [0] VM::deal(DefaultSender: [0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38], 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [0] VM::deal(AttackPRaffle: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 1000000000000000000 [1e18])
│ └─ ← [Return]
└─ ← [Stop]
[1057057470] TestPRaffle::test_DOS_If_Many_user_EnterRaffle()
├─ [6477300] PuppyRaffle::enterRaffle{value: 100000000000000000000}([0x0000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000002, 0x0000000000000000000000000000000000000003, 0x0000000000000000000000000000000000000004, 0x0000000000000000000000000000000000000005, 0x0000000000000000000000000000000000000006, 0x0000000000000000000000000000000000000007, 0x0000000000000000000000000000000000000008, 0x0000000000000000000000000000000000000009, 0x000000000000000000000000000000000000000A, 0x000000000000000000000000000000000000000b, 0x000000000000000000000000000000000000000C, 0x000000000000000000000000000000000000000d, 0x000000000000000000000000000000000000000E, 0x000000000000000000000000000000000000000F, 0x0000000000000000000000000000000000000010, 0x0000000000000000000000000000000000000011, 0x0000000000000000000000000000000000000012, 0x0000000000000000000000000000000000000013, 0x0000000000000000000000000000000000000014, 0x0000000000000000000000000000000000000015, 0x0000000000000000000000000000000000000016, 0x0000000000000000000000000000000000000017, 0x0000000000000000000000000000000000000018, 0x0000000000000000000000000000000000000019, 0x000000000000000000000000000000000000001a, 0x000000000000000000000000000000000000001B, 0x000000000000000000000000000000000000001c, 0x000000000000000000000000000000000000001D, 0x000000000000000000000000000000000000001e, 0x000000000000000000000000000000000000001F, 0x0000000000000000000000000000000000000020, 0x0000000000000000000000000000000000000021, 0x0000000000000000000000000000000000000022, 0x0000000000000000000000000000000000000023, 0x0000000000000000000000000000000000000024,
:
:
:
00000000000000000000000000000000638, 0x0000000000000000000000000000000000000639, 0x000000000000000000000000000000000000063A, 0x000000000000000000000000000000000000063B, 0x000000000000000000000000000000000000063c, 0x000000000000000000000000000000000000063D, 0x000000000000000000000000000000000000063e, 0x000000000000000000000000000000000000063F, 0x0000000000000000000000000000000000000640])
│ └─ ← [OutOfGas] EvmError: OutOfGas
└─ ← [Revert] EvmError: Revert

Recommended Mitigation

  • To completely remove the gas limit ceiling, replace the unbounded for loop with a mapping to track active players. A mapping lookup costs a fixed, predictable amount of gas (O(1)), meaning the contract will remain perfectly functional whether there are 10 players or 10,000 players.

// 1. Add a mapping to track players efficiently
mapping(address => bool) public hasEntered;
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++) {
require(newPlayers[i] != address(0), "PuppyRaffle: Cannot enter zero address");
// 2. Check the mapping in O(1) time instead of looping
require(!hasEntered[newPlayers[i]], "PuppyRaffle: Duplicate player");
// 3. Mark the player as entered
hasEntered[newPlayers[i]] = true;
players.push(newPlayers[i]);
}
emit RaffleEnter(newPlayers);
}
Updates

Lead Judging Commences

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