MyCut

AI First Flight #8
Beginner FriendlyFoundry
EXP
View results
Submission Details
Impact: medium
Likelihood: medium
Invalid

Integer Division in closePot Leaves Dust With No Withdrawal Path

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

// Root ca# [L-03] Integer Division in closePot Leaves Dust With No Withdrawal Path
### Summary
Integer division in `closePot` truncates calculations for both the manager's cut and individual claimant cuts. The leftover dust from these divisions has no withdrawal path and becomes permanently stuck in the contract balance.
### Vulnerability Detail
Inside `Pot.sol`, the division math inside `closePot` truncates remainder values down:
```solidity
uint256 managerCut = remainingRewards / managerCutPercent;
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
```
Because Solidity does not support fractional numbers, any division remainder (dust) stays within the contract. Since the function sets `remainingRewards = 0` at completion and marks the pot as closed, this dust accumulates over time with no administrative fallback mechanism to extract it.
### Impact
Locked funds. Small amounts of dust accumulate across multiple pots. While individually negligible per contest, the trapped capital across many contests becomes permanently lost.
### Affected Code
* `Pot.sol`
### Proof of Concept
Add the following test case to your test suite to verify the truncation accumulation:
```solidity
function test_L03_PrecisionLossInClosePot() public {
// 7 players, 700 total tokens
address[] memory players = new address[](7);
uint256[] memory rewards = new uint256[](7);
for (uint256 i = 0; i < 7; i++) {
players[i] = address(uint160(i + 1));
rewards[i] = 100e18;
}
// 4 players claim, leaving remainingRewards at 300
for (uint256 i = 0; i < 4; i++) {
vm.prank(players[i]);
pot.claimCut();
}
vm.warp(block.timestamp + 91 days);
uint256 potBalanceBefore = weth.balanceOf(address(pot));
pot.closePot();
// Math:
// managerCut = 300 / 10 = 30
// claimantCut = (300 - 30) / 7 = 38 (270 / 7 = 38.57, truncated to 38)
// Distributed: 30 + (38 * 4) = 182
// Dust: 300 - 182 = 118 stuck forever
uint256 expectedDistributed = 30 + (38 * 4);
uint256 actualStuck = potBalanceBefore - expectedDistributed;
assertGt(actualStuck, 0); // Dust permanently locked
assertEq(weth.balanceOf(address(pot)), actualStuck);
}
```
### Tools Used
* Manual Review
### Recommendations
Sweep any remaining contract token balance to the first claimant or back to the manager after the main distribution loop completes:
```solidity
function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) revert Pot__StillOpenForClaim();
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
if (claimants.length > 0) {
uint256 claimantCut = (remainingRewards - managerCut) / claimants.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
// Distribute any remaining precision dust to the first claimant
uint256 dust = i_token.balanceOf(address(this));
if (dust > 0 && claimants.length > 0) {
_transferReward(claimants[0], dust);
}
}
remainingRewards = 0;
closed = true;
}
```
use in the codebase with @> marks to highlight the relevant section

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Reason 2

Impact:

  • Impact 1

  • Impact 2

Proof of Concept

Recommended Mitigation

- remove this code
+ add this code
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!