The fee calculation in selectWinner() uses integer division which can lead to precision loss. When (totalAmountCollected * 80) / 100 and (totalAmountCollected * 20) / 100 are calculated, any remainder from the division is lost. Over many raffles, these small amounts accumulate as locked funds in the contract.
Likelihood:
The original implementation calculates prizePool and fee independently using integer division. Because Solidity truncates decimals, this can introduce rounding errors where:
prizePool + fee < totalAmountCollected
As a result, small amounts of ETH (“dust”) can remain permanently locked in the contract, especially when totalAmountCollected is not perfectly divisible by 100.
The fix derives prizePool from totalAmountCollected - fee instead of recalculating it with another division. This guarantees that 100% of the collected funds are accounted for (either as fees or as prize), eliminating precision loss and preventing trapped funds.
Impact:
Small amounts of ETH (1-2 wei per raffle) become permanently locked in the contract. While individually negligible, this can accumulate over time.
This PoC demonstrates how precision loss in the original fee/prize calculation causes ETH to be permanently locked in the contract.
To prevent precision loss and avoid locking residual ETH in the contract, apply the following mitigations:
Derive one value from the other (preferred)
Calculate either the fee or the prize pool using percentage math.
Derive the remaining amount by subtraction to ensure full distribution.
uint256 fee = (totalAmountCollected * 20) / 100;
uint256 prizePool = totalAmountCollected - fee;
Avoid dual percentage calculations
Do not compute both prizePool and fee independently using integer division, as rounding truncation can cause dust.
Use basis points for clarity
Express percentages in basis points to reduce rounding confusion and improve readability.
uint256 fee = (totalAmountCollected * 2000) / 10_000; // 20%
uint256 prizePool = totalAmountCollected - fee;
Add an invariant check (defensive)
Ensure all collected ETH is accounted for during execution.
assert(prizePool + fee == totalAmountCollected);
Optional: Explicit dust handling
If design requires, explicitly assign any remainder to either the prize pool or fees to avoid stranded funds.
Outcome
These mitigations guarantee that all ETH collected in a raffle round is deterministically distributed, eliminating precision-loss dust and preventing permanent fund lockup.
## Description `fee` should be 'totalAmountCollected-prizePool' to prevent decimal loss ## Vulnerability Details ``` uint256 totalAmountCollected = players.length * entranceFee; uint256 prizePool = (totalAmountCollected * 80) / 100; uint256 fee = (totalAmountCollected * 20) / 100; ``` This formula calculates `fee` should be 'totalAmountCollected-prizePool' ## Impact By calculates `fee` like the formula above can cause a loss in `totalAmountCollected' if the `prizePool` is rounded. ## Recommendations ```diff - uint256 fee = (totalAmountCollected * 20) / 100; + uint256 fee = totalAmountCollected-prizePool; ```
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.