Puppy Raffle

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

# [H-04] Integer Truncation in `totalFees` Leads to Permanent Lockup of Protocol Revenue

[H-02] Integer Truncation in totalFees Leads to Permanent Lockup of Protocol Revenue

Description

In normal operation, the selectWinner() function calculates a 20% protocol fee from the total raffle entry deposits and accumulates this value into totalFees so the contract owner can later withdraw protocol earnings via withdrawFees().

However, totalFees is declared as a 64-bit integer (uint64), while the newly calculated fee is a 256-bit integer (uint256). Because the contract uses Solidity 0.7.6 without explicit overflow protection, casting fee to uint64 silently truncates higher-order bits whenever cumulative or single-raffle fees exceed $2^{64} - 1$ wei ($\approx 18.446 \text{ ETH}$).

Solidity

// Root cause in the codebase:
uint256 totalAmountCollected = players.length * entranceFee;
uint256 fee = (totalAmountCollected * 20) / 100;
//@> totalFees = totalFees + uint64(fee);

Risk

Likelihood: Medium

  • Reason 1: Large raffles with total collected funds exceeding $\approx 92.23 \text{ ETH}$ generate a single-instance fee greater than $18.446 \text{ ETH}$, causing immediate bit truncation upon winner selection.

  • Reason 2: Multiple consecutive medium-sized raffles continuously accumulate fees into totalFees, inevitably pushing the aggregate balance past the type(uint64).max threshold.

Impact: High

  • Impact 1: Protocol fees above $18.446 \text{ ETH}$ suffer severe truncation, resetting totalFees to a fraction of its true value and permanently trapping the unrecorded ETH balance inside the contract.

  • Impact 2: The contract owner loses access to legitimate protocol revenues because withdrawFees() restricts withdrawals strictly to the corrupted totalFees state variable.

Proof of Concept

function test_Overflow() public {
address[] memory players = new address[](100);
for (uint256 i = 0; i < 100; i++) {
players[i] = address(uint160(i + 1));
}
puppyRaffle.enterRaffle{value: entranceFee * 100}(players);
uint256 sendedFees = entranceFee * 100;
uint256 participantsLength = players.length;
vm.warp(duration + puppyRaffle.raffleStartTime() + 1);
puppyRaffle.selectWinner();
uint256 num = puppyRaffle.totalFees();
uint256 num2 = (sendedFees *20) /100;
assert(puppyRaffle.totalFees() < num2);
console2.log("sendedFees: ", num2);
console2.log("totalFees: ", num);
}

Ran 1 test for test/PuppyRaffleTest.t.sol:PuppyRaffleTest
[PASS] test_Overflow() (gas: 22379318)
Logs:
sendedFees: 20000000000000000000
totalFees: 1553255926290448384

Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 66.42ms (38.15ms CPU time)

Recommended Mitigation

Update the type of the totalFees state variable from uint64 to uint256 to ensure it can store fee amounts exceeding $2^{64}-1$ wei without bit truncation. Additionally, remove the explicit uint64 typecast in selectWinner().

For complete safety against arithmetic overflows, consider upgrading the codebase to Solidity 0.8.x (which includes built-in overflow checks) or integrating OpenZeppelin's SafeMath library.

In PuppyRaffle::selectWinner

- uint64 public totalFees = 0;
- totalFees = totalFees + uint64(fee);
+ uint256 public totalFees = 0;
+ totalFees = totalFees + fee;
Updates

Lead Judging Commences

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