The PuppyRaffle contract tracks accumulated protocol fees using a uint64 state variable called totalFees. However, the fee amount is first computed as a uint256 and then unsafely truncated with an explicit cast uint64(fee) before being added. This silent truncation causes an integer overflow whenever the computed fee exceeds type(uint64).max (approximately 18.44 ETH), wrapping the value around to a much smaller number and permanently destroying the difference.
Because Solidity 0.7.x does not include built-in overflow protection for explicit casts, this truncation happens silently with no revert and no event emitted. The lost ETH remains locked in the contract but is no longer tracked by totalFees, making it permanently unrecoverable through withdrawFees().
Additionally, the withdrawFees() function enforces a strict balance check:
This means that after an overflow event, withdrawFees() becomes permanently bricked — the owner can never withdraw any fees at all, even the small truncated amount that totalFees still tracks.
Likelihood:
The overflow threshold is type(uint64).max = 18,446,744,073,709,551,615 wei ≈ 18.44 ETH per raffle round
With an entranceFee of 1 ETH, only 93 players are needed to trigger overflow in a single round (93 * 1e18 * 20% = 18.6 ETH > 18.44 ETH)
With a lower entranceFee of 0.1 ETH, only 923 players are needed — a realistic number for a popular raffle
The overflow accumulates across rounds: even if each individual round does not overflow, totalFees is never reset to zero after withdrawFees() fails, so the cumulative sum will eventually overflow
This is a First Flight contest with intentional bugs, making it certain to be triggered
Impact
Protocol fees collected in ETH are permanently destroyed — they cannot be recovered by any function in the contract
The feeAddress wallet receives far less ETH than the protocol intends (20% of prize pool)
After overflow occurs, withdrawFees() is permanently bricked due to the strict balance equality check, meaning even correctly-tracked fees become unwithdrawable
The owner has no emergency mechanism to rescue the stuck ETH, resulting in total and irreversible loss of all accumulated fees
Formal Verification Evidence (Certora Prover):
The Certora Prover rule totalFees_no_overflow was formally verified and returned VIOLATED, providing mathematical proof that totalFees can decrease after a call to selectWinner(). This is impossible under correct arithmetic and confirms that integer overflow is occurring:
The Prover found a concrete counterexample where calling selectWinner() causes totalFees to decrease — direct mathematical proof of the overflow.
The fix requires two changes: promoting totalFees from uint64 to uint256 to match the native size of fee calculations, and removing the unsafe downcast.
This eliminates the truncation entirely. Since fee is already a uint256, no cast is needed. The withdrawFees() balance check will also begin working correctly again because totalFees will accurately reflect the true amount of ETH owed to the fee recipient.
Additionally, consider upgrading to Solidity ^0.8.0 or using OpenZeppelin's SafeCast library to prevent similar silent truncation bugs elsewhere in the codebase.
## 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.