Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

[High] Quadratic Gas DoS in enterRaffle

[High] Denial of Service via Quadratic Gas Complexity in PuppyRaffle::enterRaffle

Description

The PuppyRaffle::enterRaffle function allows users to enter the raffle by providing an array of addresses. To maintain the integrity of the raffle, the contract performs a check to ensure that no duplicate player addresses are added to the players list.

However, this duplicate check is implemented using nested for loops. For every new player being added, the contract iterates through the entire players array. As the number of players increases, the number of operations required for each new entry grows quadratically ().

Specifically, the first player added requires 1 check, the 100th player requires 100 checks, and the 1,000th player requires 1,000 checks. When adding a batch of players, the total number of operations scales exponentially relative to the total number of participants already in the raffle.

Risk

  • Likelihood: High. This is a fundamental architectural flaw. Any successful raffle will naturally accumulate enough players to trigger this condition. It does not require a malicious actor; it will happen through normal usage.

  • Impact: High. Once the players array reaches a certain size (typically a few hundred entries), the gas cost for the enterRaffle function will exceed the block gas limit (30,000,000 on Ethereum).

    • Denial of Service (DoS): New users will be unable to join the raffle as their transactions will consistently revert due to "Out of Gas" errors.

    • Protocol Bricking: If the raffle logic requires a certain number of players to proceed to the winner selection phase, the protocol could be permanently stuck in an unfinishable state.

Root Cause

The vulnerability is caused by the nested loop structure in the duplicate check logic of src/PuppyRaffle.sol.

// src/PuppyRaffle.sol
@> for (uint256 i = 0; i < players.length; i++) {
@> for (uint256 j = i + 1; j < players.length; j++) {
require(players[i] != players[j], "PuppyRaffle: Duplicate player");
}
}

##Proof of Concept (PoC)

The following Foundry test demonstrates the exponential growth in gas costs. By comparing the gas cost of the first 100 players versus the next 100 players, we can see that the second batch is significantly more expensive, proving the complexity.

function test_QuadraticGasDoS() public {
vm.txGasPrice(1);
// 1. Enter the first 100 players
uint256 batchSize = 100;
address[] memory players = new address[](batchSize);
for(uint256 i=0; i<batchSize; i++) {
players[i] = address(uint160(i + 1));
}
uint256 gasStart = gasleft();
puppyRaffle.enterRaffle{value: 1 ether * batchSize}(players);
uint256 gasUsedFirst = gasStart - gasleft();
// 2. Enter the second 100 players (Gas cost will be much higher)
address[] memory players2 = new address[](batchSize);
for(uint256 i=0; i<batchSize; i++) {
players2[i] = address(uint160(i + batchSize + 1));
}
gasStart = gasleft();
puppyRaffle.enterRaffle{value: 1 ether * batchSize}(players2);
uint256 gasUsedSecond = gasStart - gasleft();
console.log("Gas for first 100 players: ", gasUsedFirst);
console.log("Gas for second 100 players: ", gasUsedSecond);
// Assert that the second batch cost significantly more than the first
assertTrue(gasUsedSecond > gasUsedFirst);
}

##Recommended Mitigation
To resolve this issue, the search must be replaced with an lookup. This can be achieved by using a mapping(address => bool) to track active players.

  • mapping(address => bool) public isPlayerInRaffle;

    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(!isPlayerInRaffle[newPlayers[i]], "PuppyRaffle: Duplicate player");

  • isPlayerInRaffle[newPlayers[i]] = true;
    players.push(newPlayers[i]);
    }

  • for (uint256 i = 0; i < players.length; i++) {

  • for (uint256 j = i + 1; j < players.length; j++) {

  • require(players[i] != players[j], "PuppyRaffle: Duplicate player");

  • }

  • }
    emit RaffleEnter(newPlayers);

    }

Alternatively, if the contract must be cleared between rounds, ensure the isPlayerInRaffle mapping is reset in the selectWinner function to allow players to enter the next round.
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!