MyCut

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

`Pot::closePot` can be called more than once because it has no closed flag and never resets `remainingRewards`, so repeated calls drain funds owed to players who haven't claimed

Root + Impact

Description

  • A pot should be closed only once. The leftover is distributed a single time and the pot's accounting should reflect that.

  • closePot only checks that 90 days have passed. It never records that the pot was closed and never lowers remainingRewards, so every call pays the same managerCut and claimantCut again from the Pot's balance. Those tokens belong to players who haven't claimed yet: their rewards are still recorded, but the Pot can no longer pay them.

function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) { // only check
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);
}
// remainingRewards never updated, no `closed` flag
}
}

Risk

Likelihood:

  • When the owner's close transaction is sent again, e.g. a retry after a stuck/underpriced transaction, a keeper/automation script, or a UI double submit.

  • When closeContest is called on a pot that is already closed, since nothing on-chain shows the pot is closed.

Impact:

  • Each extra call pays out funds owed to players who haven't claimed yet; their claimCut then reverts and they lose their reward.

  • Early claimants get paid several times, and more manager cut gets locked in ContestManager. getRemainingRewards keeps reporting a balance that no longer exists.

Proof of Concept

3 calls to closeContest pay player1 its 67.5 bonus 3 times and leave only 7.5 in the Pot, while remainingRewards still reports 300.

function testClosePotCanBeCalledRepeatedly() public mintAndApproveTokens {
address player3 = makeAddr("player3");
address player4 = makeAddr("player4");
address[] memory ps = new address[](4);
ps[0] = player1; ps[1] = player2; ps[2] = player3; ps[3] = player4;
uint256[] memory rs = new uint256[](4);
for (uint256 i; i < 4; i++) rs[i] = 100e18;
vm.startPrank(user);
address pot = ContestManager(conMan).createContest(ps, rs, IERC20(address(weth)), 400e18);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
vm.prank(player1);
Pot(pot).claimCut();
vm.warp(block.timestamp + 91 days);
vm.startPrank(user);
ContestManager(conMan).closeContest(pot);
ContestManager(conMan).closeContest(pot);
ContestManager(conMan).closeContest(pot);
vm.stopPrank();
// each call paid 30 (manager) + 67.5 (player1) again
assertEq(weth.balanceOf(pot), 7.5e18);
assertEq(weth.balanceOf(player1), 100e18 + 3 * 67.5e18);
assertEq(Pot(pot).getRemainingRewards(), 300e18); // never reset
}

Recommended Mitigation

+ bool private closed;
+ error Pot__AlreadyClosed();
function closePot() external onlyOwner {
+ if (closed) revert Pot__AlreadyClosed();
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
+ closed = true;
if (remainingRewards > 0) {
...
+ remainingRewards = 0;
}
}
Updates

Lead Judging Commences

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