Puppy Raffle

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

uint64 truncation of totalFees permanently locks protocol fees

Root + Impact

Description

uint64 public totalFees = 0;
...
function selectWinner() external {
...
uint256 fee = (totalAmountCollected * 20) / 100;
@> totalFees = totalFees + uint64(fee); // uint64(fee) truncates when fee > type(uint64).max
...
}

Risk

Likelihood: Medium — requires a raffle to reach ~93 players (at a 1 ETH fee), so a single large fee exceeds type(uint64).max. Reachable in a successful raffle, but not in every one.

Impact: High — totalFees is corrupted, so withdrawFees() reverts on its strict balance-equality check. The protocol's fees are permanently locked with no recovery path.

Proof of Concept

This test enters 93 players, runs the draw, and shows two things: totalFees is
recorded as ~0.15 ETH instead of the real 18.6 ETH (truncation), and
withdrawFees() then reverts (locked funds). The assertLt confirms the
truncation; the vm.expectRevert confirms the fees can no longer be withdrawn.

From the trace: totalFees() returns 153255926290448384 (~0.15 ETH),
while the real fees collected are 18.6 ETH. withdrawFees() then reverts.

function test_fee_overflow_locks_withdrawals() public {
uint256 playerCount = 93;
address[] memory players = new address[](playerCount);
for (uint256 i = 0; i < playerCount; i++) {
players[i] = address(uint160(i + 1));
}
puppyRaffle.enterRaffle{value: entranceFee * playerCount}(players);
vm.warp(block.timestamp + duration + 1);
puppyRaffle.selectWinner();
​
uint256 feesRecorded = puppyRaffle.totalFees();
uint256 feesExpected = (entranceFee * playerCount * 20) / 100; // 18.6 ETH
assertLt(feesRecorded, feesExpected); // recorded << real: truncation
​
vm.expectRevert("PuppyRaffle: There are currently players active!");
puppyRaffle.withdrawFees();
}

Recommended Mitigation

Store totalFees as uint256 to remove the narrowing cast and the truncation.
Upgrading to Solidity >=0.8 adds automatic overflow checks as defense in depth.
The strict-equality require in withdrawFees() should also be reconsidered, as
it is independently fragile to forced ETH via selfdestruct.

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

Lead Judging Commences

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