MyCut

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

`closePot()` uses `i_players.length` as divisor but iterates over `claimants`, permanently locking residual tokens

Root + Impact

Description

In Pot.sol::closePot(), claimantCut is calculated by dividing the remaining balance (after manager cut) by i_players.length, but the distribution loop iterates over claimants. When the number of claimants is less than the number of players, each claimant receives a smaller share than intended, and the undistributed portion remains in the contract with no withdrawal mechanism.

// Root cause in the codebase with @> marks to highlight the relevant section
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);
​
@> uint256 claimantCut = (remainingRewards - managerCut) /
@> i_players.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
}

Risk

Likelihood:H

Impact:H

Proof of Concept

function test_H1_WrongDivisorLocksTokens() public {
address[] memory players = new address[](2);
players[0] = player1;
players[1] = player2;
​
uint256[] memory rewards = new uint256[](2);
rewards[0] = 500;
rewards[1] = 500;
​
vm.startPrank(owner);
address contest = conMan.createContest(players, rewards, IERC20(address(weth)), 1000);
conMan.fundContest(0);
vm.stopPrank();
​
// Only player1 claims
vm.prank(player1);
Pot(contest).claimCut();
​
// remainingRewards = 500, claimants = [player1]
// closePot: managerCut = 500/10 = 50
// claimantCut = (500-50) / i_players.length(2) = 225
// loop runs claimants.length(1) time → player1 gets 225
// total out = 50 + 225 = 275
// locked = 500 - 275 = 225 tokens
vm.warp(91 days);
vm.prank(owner);
conMan.closeContest(contest);
​
uint256 lockedBalance = weth.balanceOf(contest);
console.log("[H-1] Locked tokens in Pot after closeContest:", lockedBalance);
​
// This should be 0 in a correct implementation, but it won't be
assertEq(lockedBalance, 0, "H-1 FAIL: tokens are permanently locked in Pot");
}

Recommended Mitigation

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);
​
- uint256 claimantCut = (remainingRewards - managerCut) /
- i_players.length;
+ uint256 claimantCut = (remainingRewards - managerCut) /
+ claimants.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
}
Updates

Lead Judging Commences

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