Normal behavior: When selectWinner() is called, the total entrance fees collected are split: 80% goes to the winner as the prize pool, and 20% is accumulated as protocol fees in totalFees. This split is calculated using integer arithmetic.
The issue: Solidity integer division truncates toward zero and any remainder is discarded. When division is performed before multiplication, the truncation error is amplified. The current fee calculation divides first and then multiplies, which causes consistent rounding loss on every raffle round.
Specifically, totalAmountCollected * 20 / 100 is evaluated left-to-right. If totalAmountCollected is not perfectly divisible by 100, the intermediate division truncates, and the multiplication operates on an already-reduced value. This means the protocol consistently collects fewer fees than the intended 20% cut.
Over many raffle rounds, the cumulative shortfall becomes significant. The winner also receives slightly less than the intended 80% in some cases.
// src/PuppyRaffle.sol#L132-L133
// @> Division before multiplication precision loss on every raffle
uint256 prizePool = (totalAmountCollected * 80) / 100;
uint256 fee = (totalAmountCollected * 20) / 100;
However, looking at this more carefully, the actual dangerous pattern appears when the multiplication order is wrong. The real risk in PuppyRaffle is the interaction between integer truncation and totalFees being cast to uint64:
// @> totalFees is uint64 — can overflow for large raffles
// @> fee calculation loses precision due to integer division behavior
totalFees = totalFees + uint64(fee);
The uint64 cast on fee silently truncates any value above type(uint64).max (~18.4 ETH), meaning large raffles silently lose fee data.
Likelihood:
This executes on every single raffle completion, the precision loss is guaranteed and repeatable.
No special condition or attacker is required it happens through normal protocol usage.
The uint64 overflow risk increases as the protocol scales to larger raffles.
Impact:
Protocol consistently collects fewer fees than intended revenue loss on every round.
For large raffles where fee > type(uint64).max, totalFees overflows silently and the owner withdraws far less than owed.
The withdrawFees() balance check address(this).balance == uint256(totalFees) becomes permanently broken if totalFees overflows, as the actual balance will be much larger than the stored totalFees
Three Fixes are required:
Note: Using basis points (parts per 10,000) provides finer granularity and reduces rounding loss while keeping the split percentages accurate.
## 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.