Puppy Raffle

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

[M-04] Precision Loss in Fee Calculation Can Lock Dust ETH

Root + Impact

Description

  • The prize pool and fee calculations use integer division which can result in precision loss.

  • With certain entrance fee values, the sum of prizePool + fee may not equal totalAmountCollected, leaving dust ETH permanently locked 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; // Integer division
@> uint256 fee = (totalAmountCollected * 20) / 100; // Integer division
// prizePool + fee may not equal totalAmountCollected!
// ...
}

Risk

Likelihood: Low

  • Reason 1 // Only occurs with specific entrance fee values

  • Reason 2 // Standard 1 ETH fee divides evenly

Impact: Medium

  • Impact 1 // Small amounts of ETH permanently locked per raffle

  • Impact 2 // Accumulates over many raffles

  • Impact 3 // No mechanism to recover dust

Proof of Concept

The following demonstrates precision loss with a non-standard entrance fee.

function testPrecisionLoss() public {
// Setup with entrance fee that causes precision loss
PuppyRaffle oddRaffle = new PuppyRaffle(
0.33 ether, // Odd entrance fee
feeAddress,
duration
);
address[] memory players = new address[](4);
players[0] = playerOne;
players[1] = playerTwo;
players[2] = playerThree;
players[3] = playerFour;
oddRaffle.enterRaffle{value: 0.33 ether * 4}(players);
uint256 totalCollected = 0.33 ether * 4; // 1.32 ETH
uint256 prizePool = (totalCollected * 80) / 100; // 1.056 ETH
uint256 fee = (totalCollected * 20) / 100; // 0.264 ETH
// Check for dust
uint256 dust = totalCollected - prizePool - fee;
console.log("Dust locked:", dust); // May be > 0
}

Recommended Mitigation

Calculate fee as the remainder after prize pool to avoid dust.

function selectWinner() external {
// ...
uint256 totalAmountCollected = players.length * entranceFee;
uint256 prizePool = (totalAmountCollected * 80) / 100;
- uint256 fee = (totalAmountCollected * 20) / 100;
+ uint256 fee = totalAmountCollected - prizePool; // Remainder goes to fee
// ...
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 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!