totalFees is declared as uint64, while fee is computed from totalAmountCollected as a uint256 (20% of collected entrance fees). The code adds it via an unchecked narrowing cast uint64(fee), which truncates silently (no revert) and never checks range. On solc ^0.7.6, plain arithmetic also has no built-in overflow protection.
type(uint64).max is only about 18.4467 ETH. Once the cumulative fee collected across rounds crosses that ceiling, totalFees silently wraps around modulo 2^64 - it no longer equals the ETH actually sitting in the contract. Because withdrawFees() requires address(this).balance == totalFees exactly, after the wrap that equality can never hold again through any normal contract operation, and the real fee ETH becomes permanently unreachable - there is no other function that can move it out.
This is not a single extreme-parameter edge case: ordinary, moderately-sized rounds run back-to-back (no interim withdrawFees() call, which nothing in the contract or docs requires) are enough to cross the threshold over time.
Likelihood:
Reason 1 // No attacker or special parameters are needed - purely ordinary, repeated protocol usage (many normal-sized rounds without an interim fee withdrawal) triggers the wraparound over time.
Reason 2 // Nothing in the contract enforces a cap on cumulative fees or forces periodic withdrawal, so any moderately active deployment will eventually cross the uint64 ceiling.
Impact:
Impact 1 // Real ETH already collected as protocol fees becomes permanently, irrecoverably stuck in the contract - withdrawFees() reverts forever and there is no alternate rescue path.
Impact 2 // The on-chain accounting variable silently desyncs from the contract's real balance, corrupting the protocol's own bookkeeping with no error or event to signal it happened.
Ran with forge test --match-path "test/PoC_2.t.sol" -vv: [PASS] testCumulativeFeeTruncationBricksWithdrawFeesAcrossNormalRounds(). Logs: "Rounds run before wraparound: 5", true cumulative fee owed = 20 ETH, totalFees actually stored on-chain (wrapped) = 1.553255926290448384 ETH, real contract balance permanently stuck = 20 ETH. The test uses an ordinary 1 ETH entrance fee and 20 players/round (nothing extreme), runs 5 back-to-back rounds with no interim withdrawal, then shows withdrawFees() reverting even though the contract demonstrably holds the full 20 ETH it owes.
Storing totalFees as uint256 removes the narrowing cast entirely (the storage-packing gas saving is not worth a permanent fund-lockup risk). If a smaller type must be kept for packing, explicitly require the running total stays within range before casting, and revert instead of silently truncating.
## 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.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.