Puppy Raffle

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

[High] Weak Randomness in PuppyRaffle::selectWinner allows winner prediction


[High] Quadratic Gas DoS in PuppyRaffle::enterRaffle leads to Protocol Denial of Service

Root + Impact

The enterRaffle function uses nested loops to check for duplicate addresses in the players storage array. As the array grows, the gas cost for each new entry increases quadratically ($O(n^2)$). This leads to gas exhaustion where the cost to enter exceeds the Block Gas Limit, resulting in a permanent Denial of Service (DoS) for new participants and "bricking" the raffle.

Description

Normal behavior (expected)

A scalable raffle contract should maintain a near-constant gas cost for entries. Duplicate checks should be performed using efficient data structures, such as a mapping, to ensure constant-time ($O(1)$) complexity regardless of the number of participants.

Actual behavior (bug)

The duplicate check is implemented with a nested for loop that iterates over the entire players array for every new player added in the batch.

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

As players.length increases, the number of required comparison operations grows exponentially. This causes the gas cost to surge rapidly. Once the player count reaches a certain threshold, the gas required for the transaction will exceed the Ethereum Block Gas Limit (30M gas), making it impossible for any further players to join.

Risk

Likelihood:
High. This is a fundamental flaw in the duplicate-check logic. Any raffle that successfully attracts a few hundred players will naturally trigger this gas ceiling.

Impact:
High. 1. Total Denial of Service: New users are unable to join the raffle because their transactions will consistently fail with "Out of Gas." 2. Protocol Bricking: If the raffle logic requires a minimum number of players to finish, and the gas cost to reach that number exceeds the block limit, the contract becomes permanently stuck, locking all prize funds and fees inside.

Proof of Concept

The following test demonstrates that adding 200 players consumes nearly the entire gas capacity of an Ethereum block, proving the growth.

function test_QuadraticGasDoS() public {
// 1. Enter 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));
}
puppyRaffle.enterRaffle{value: entranceFee * batchSize}(players);
// 2. Enter second 100 players (Gas cost will spike)
address[] memory players2 = new address[](batchSize);
for(uint256 i=0; i<batchSize; i++) {
players2[i] = address(uint160(i + batchSize + 1));
}
puppyRaffle.enterRaffle{value: entranceFee * batchSize}(players2);
}

Foundry Test Log

Ran 1 test for test/GasDoS.t.sol:GasDoS
[PASS] test_QuadraticGasDoS() (gas: 25556454)
Logs:
------------------------------------------
------------------------------------------
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 103.76ms (96.12ms CPU time)
Ran 1 test suite in 465.24ms (103.76ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Technical Analysis: At only 200 players, the gas consumption reached 25,556,454. Since the Ethereum Block Gas Limit is 30,000,000, adding even a small additional batch would exceed the limit and cause the transaction to fail permanently.

Recommended Mitigation

Replace the nested loop check with a mapping(address => bool) to track active players. This allows for duplicate checks to be performed in constant time.

+ mapping(address => bool) public isPlayerInRaffle;
function enterRaffle(address[] memory newPlayers) public payable {
require(msg.value == entranceFee * newPlayers.length, "PuppyRaffle: Must send enough");
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);
}
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!