MyCut

AI First Flight #8
Beginner FriendlyFoundry
EXP
View results
Submission Details
Severity: low
Valid

Sequential Integer Division in closePot() — Token Dust Permanently Locked

Sequential Integer Division in closePot() — Token Dust Permanently Locked

Description

The closePot() function performs two sequential integer division operations to compute the manager cut and per-claimant redistribution. In Solidity, integer division truncates toward zero, discarding any fractional remainder. The protocol expects that all remaining rewards are fully distributed during pot closure.

Because there are two division operations in sequence, precision loss compounds. The first division (remainingRewards / managerCutPercent) loses up to managerCutPercent - 1 wei. The second division (redistributable / i_players.length) loses up to i_players.length - 1 wei. The combined dust remains in the Pot contract, which has no sweep or recovery mechanism, locking it permanently.

// Pot.sol L49-61
function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
if (remainingRewards > 0) {
@> uint256 managerCut = remainingRewards / managerCutPercent; // Truncation #1
i_token.transfer(msg.sender, managerCut);
@> uint256 claimantCut = (remainingRewards - managerCut) / i_players.length; // Truncation #2
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
}

Risk

Likelihood: High

  • Integer division truncation occurs on every closePot() call unless remainingRewards happens to be exactly divisible by both managerCutPercent and the player count. This is mathematically unlikely for arbitrary reward amounts.

Impact: Low

  • Individual dust amounts are small (bounded by managerCutPercent + i_players.length wei per closure). However, the loss is systemic across all protocol Pots and accumulates over the protocol's lifetime with no recovery path.

Severity: Medium

Proof of Concept

A Pot has remainingRewards = 103 with managerCutPercent = 10 and 7 claimants. The manager cut is 103 / 10 = 10 (0.3 tokens lost to truncation). The redistributable pool is 103 - 10 = 93. Each claimant receives 93 / 7 = 13 (2 tokens lost to truncation). Total distributed: 10 + (13 × 7) = 101. The remaining 2 tokens are permanently locked.

function test_M02_precision_dust() public {
// Setup: 7 claimants, 103 tokens remaining
// managerCutPercent = 10
address[] memory players = new address[](7);
uint256[] memory rewards = new uint256[](7);
for (uint i = 0; i < 7; i++) {
players[i] = address(uint160(i + 1));
rewards[i] = 15; // 105 total, 103 remains after some claims
}
Pot pot = new Pot(players, rewards, token, 105);
token.transfer(address(pot), 105);
// 2 tokens lost to precision, all players claim
// ... claims happen, then closePot ...
vm.warp(block.timestamp + 91 days);
pot.closePot();
uint256 dust = token.balanceOf(address(pot));
assertGt(dust, 0, "Precision dust permanently locked");
}

Recommended Mitigation

The fix assigns any remaining dust to the last claimant by computing the actual total distributed and adding the difference. This ensures zero tokens are left in the contract after closure.

uint256 claimantCut = (remainingRewards - managerCut) / claimants.length;
+ uint256 totalClaimantPay = claimantCut * claimants.length;
+ uint256 dust = (remainingRewards - managerCut) - totalClaimantPay;
+
for (uint256 i = 0; i < claimants.length; i++) {
- _transferReward(claimants[i], claimantCut);
+ uint256 amount = claimantCut;
+ if (i == claimants.length - 1) {
+ amount += dust; // Last claimant sweeps remaining dust
+ }
+ _transferReward(claimants[i], amount);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 2 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[L-03] [H-03] Precision loss can lead to rewards getting stuck in the pot forever

### \[H-03] Precision loss can lead to rewards getting stuck in the pot forever **Description:** When contest manager closes the pot by calling `Pot::closePot`, 10 percent of the remaining rewards are transferred to the contest manager and the rest are distributed equally among the claimants. It does this by dividing the rewards by the manager's cut percentage which is 10. Then the remaining rewards are divided by the number of players to distribute equally among claimants. Since solidity allows only integer division this will lead to precision loss which will cause a portion of funds to be left in the pot forever. Each pot follows the same method, so as number of pots grow, the loss of funds is very significant. **Impact:** Reward tokens get stuck in the pot forever which causes loss of funds. **Proof of code:** Add the below test to `test/TestMyCut.t.sol` ```javascript function testPrecisionLoss() public mintAndApproveTokens { ContestManager cm = ContestManager(conMan); uint playersLength = 3; address[] memory p = new address[](playersLength); uint256[] memory r = new uint256[](playersLength); uint tr = 86; p[0] = makeAddr("_player1"); p[1] = makeAddr("_player2"); p[2] = makeAddr("_player3"); r[0] = 20; r[1] = 23; r[2] = 43; vm.startPrank(user); address pot = cm.createContest(p, r, weth, tr); cm.fundContest(0); vm.stopPrank(); console.log("\n\ntoken balance in pot before: ", weth.balanceOf(pot)); vm.prank(p[1]); // player 2 Pot(pot).claimCut(); vm.prank(p[0]); // player 1 Pot(pot).claimCut(); vm.prank(user); vm.warp(block.timestamp + 90 days + 1); cm.closeContest(pot); console.log( "\n\ntoken balance in pot after closing pot: ", weth.balanceOf(pot) ); assert(weth.balanceOf(pot) != 0); } ``` Run the below test command in terminal ```Solidity forge test --mt testPrecisionLoss -vv ``` Which results in the below output ```Solidity [⠒] Compiling... [⠆] Compiling 1 files with 0.8.20 [⠰] Solc 0.8.20 finished in 2.57s Compiler run successful! Ran 1 test for test/TestMyCut.t.sol:TestMyCut [PASS] testPrecisionLoss() (gas: 936926) Logs: token balance in pot before: 86 token balance in pot after closing pot: 1 Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.75ms (654.60µs CPU time) Ran 1 test suite in 261.16ms (1.75ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests) ``` If you observe the output you can see the pot still has rewards despite distributing them to claimants. **Recommended Mitigations:** Fixed-Point Arithmetic: Utilize a fixed-point arithmetic library or implement a custom solution to handle fee calculations with greater precision.

Support

FAQs

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

Give us feedback!