uint64 cast on accumulated fees silently truncates, corrupting accounting and locking funds foreverSeverity: High
selectWinner accrues the protocol's 20% cut into totalFees, which withdrawFees later sends to the feeAddress.
totalFees is a uint64 and the per-round fee (a uint256) is narrowed with uint64(fee) under Solidity ^0.7.6, which has no overflow checks. When a round's fee exceeds type(uint64).max (~18.446 ETH), the cast keeps only fee mod 2^64, so totalFees records far less than the ETH actually retained.
Likelihood:
Occurs whenever a single round's 20% fee exceeds ~18.446 ETH (e.g. a 100 ETH entrance fee with 4 players yields an 80 ETH fee that truncates), and also accumulates across many rounds until the uint64 running total wraps.
Occurs silently — no revert at accrual time — so the corruption is only noticed later when withdrawal fails.
Impact:
Fee accounting diverges from reality; the mis-recorded portion of fees is unaccounted for.
Because withdrawFees requires address(this).balance == uint256(totalFees), the broken invariant makes all fees permanently unwithdrawable.
Save as test/FeeOverflowPoC.t.sol and run forge test --mt testFeeTruncatesAndLocks. A round with a 100 ETH entrance fee produces a real 80 ETH fee, but totalFees records 6213023705161793536 wei (= 80e18 mod 2^64); withdrawFees() then reverts.
Store fees as uint256 and drop the narrowing cast; prefer Solidity ^0.8 for checked arithmetic.
## 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.