Puppy Raffle

AI First Flight #1
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: low
Valid

Potential Precision Loss in Fee Calculation

Root + Impact

Description

  • 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.

// Root cause in the codebase with @> marks to highlight the relevant section
function selectWinner() external {
// ...
uint256 totalAmountCollected = players.length * entranceFee;
uint256 prizePool = (totalAmountCollected * 80) / 100; // Potential precision loss
uint256 fee = (totalAmountCollected * 20) / 100; // Potential precision loss
// Note: prizePool + fee might be less than totalAmountCollected
// ...
}

Risk

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.

Proof of Concept

This PoC demonstrates how precision loss in the original fee/prize calculation causes ETH to be permanently locked in the contract.

// Example with 3 players and entranceFee = 1 wei
// totalAmountCollected = 3 wei
// prizePool = (3 * 80) / 100 = 2 wei
// fee = (3 * 20) / 100 = 0 wei
// Total distributed = 2 wei
// Remaining 1 wei stays locked in the contract due to integer division truncation

Recommended Mitigation

To prevent precision loss and avoid locking residual ETH in the contract, apply the following mitigations:

  1. 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;

  1. Avoid dual percentage calculations

    • Do not compute both prizePool and fee independently using integer division, as rounding truncation can cause dust.

  2. 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;

  1. Add an invariant check (defensive)

    • Ensure all collected ETH is accounted for during execution.

  2. assert(prizePool + fee == totalAmountCollected);

  1. 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.

function selectWinner() external {
// ...
uint256 totalAmountCollected = players.length * entranceFee;
- uint256 prizePool = (totalAmountCollected * 80) / 100; // Potential precision loss
- uint256 fee = (totalAmountCollected * 20) / 100; // Potential precision loss
- // Note: prizePool + fee might be less than totalAmountCollected
+ uint256 fee = (totalAmountCollected * 20) / 100;
+ uint256 prizePool = totalAmountCollected - fee; // Ensure no dust remains
// ...
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[L-06] Fee should be 'totalAmountCollected-prizePool' to prevent decimal loss

## 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; ```

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!