Puppy Raffle

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

Unsafe uint64 cast on totalFees causes silent overflow and permanent loss of protocol fees

Root + Impact

Description

  • The totalFees variable is declared as uint64, which maxes out at approximately 18.4 ETH. In selectWinner(), the fee is calculated as a uint256 and then unsafely downcast to uint64. Since the contract runs on Solidity 0.7.6, which has no built-in overflow protection, the value silently wraps around when it exceeds the uint64 maximum. The protocol permanently loses track of its fees.

// @> uint64 can only hold ~18.4 ETH
uint64 public totalFees = 0;
// @> fee is uint256, but gets crammed into uint64
totalFees = totalFees + uint64(fee);

Risk

Likelihood:

  • This triggers every time cumulative fees cross the ~18.4 ETH boundary

For a popular raffle with a reasonable entrance fee, this threshold is reached quickly

Impact:

  • Protocol fees silently reset to an incorrect random value

The withdrawFees() strict equality check (address(this).balance == uint256(totalFees)) will also mismatch, potentially locking all fees permanently

  • Direct revenue loss for the protocol with no recovery path

Proof of Concept

This test demonstrates the overflow in a concrete scenario. Assume entranceFee is 1 ETH and 100 players enter the raffle. The total collected is 100 ETH, and the 20% fee is 20 ETH. When selectWinner() executes totalFees = totalFees + uint64(fee), the 20 ETH value (20,000,000,000,000,000,000 wei) exceeds the uint64 maximum. On Solidity 0.7.6, this silently wraps around to a small garbage value instead of reverting. The owner then calls withdrawFees(), but it reverts because address(this).balance no longer equals uint256(totalFees). The fees are permanently locked in the contract with no recovery mechanism.

// Assume entranceFee = 1 ether, 100 players enter
// totalAmountCollected = 100 ether
// fee = 20 ether (20%)
// uint64(20 ether) overflows — totalFees stores garbage value
// Owner calls withdrawFees() — reverts because balance != totalFees
// Fees are locked forever

Recommended Mitigation

Change totalFees from uint64 to uint256 so it matches the precision of the fee calculation. The uint64 type can only hold ~18.4 ETH which is an unreasonably low ceiling for a protocol collecting fees. With uint256, overflow is practically impossible (the max value is larger than the total ETH supply many times over). Alternatively, upgrading to Solidity 0.8.x would add automatic overflow checks that revert the transaction instead of wrapping silently, but changing the type is the cleaner fix since it addresses the root cause rather than just catching the symptom.

- uint64 public totalFees = 0;
+ uint256 public totalFees = 0;
// In selectWinner():
- totalFees = totalFees + uint64(fee);
+ totalFees = totalFees + fee;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-05] Typecasting from uint256 to uint64 in PuppyRaffle.selectWinner() May Lead to Overflow and Incorrect Fee Calculation

## Description ## Vulnerability Details The type conversion from uint256 to uint64 in the expression 'totalFees = totalFees + uint64(fee)' may potentially cause overflow problems if the 'fee' exceeds the maximum value that a uint64 can accommodate (2^64 - 1). ```javascript totalFees = totalFees + uint64(fee); ``` ## POC <details> <summary>Code</summary> ```javascript function testOverflow() public { uint256 initialBalance = address(puppyRaffle).balance; // This value is greater than the maximum value a uint64 can hold uint256 fee = 2**64; // Send ether to the contract (bool success, ) = address(puppyRaffle).call{value: fee}(""); assertTrue(success); uint256 finalBalance = address(puppyRaffle).balance; // Check if the contract's balance increased by the expected amount assertEq(finalBalance, initialBalance + fee); } ``` </details> In this test, assertTrue(success) checks if the ether was successfully sent to the contract, and assertEq(finalBalance, initialBalance + fee) checks if the contract's balance increased by the expected amount. If the balance didn't increase as expected, it could indicate an overflow. ## Impact This could consequently lead to inaccuracies in the computation of 'totalFees'. ## Recommendations To resolve this issue, you should change the data type of `totalFees` from `uint64` to `uint256`. This will prevent any potential overflow issues, as `uint256` can accommodate much larger numbers than `uint64`. Here's how you can do it: Change the declaration of `totalFees` from: ```javascript uint64 public totalFees = 0; ``` to: ```jasvascript uint256 public totalFees = 0; ``` And update the line where `totalFees` is updated from: ```diff - totalFees = totalFees + uint64(fee); + totalFees = totalFees + fee; ``` This way, you ensure that the data types are consistent and can handle the range of values that your contract may encounter.

Support

FAQs

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

Give us feedback!