MyCut

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

closePot does not set remainingRewards to zero after distribution, leaving stale state that could be exploited if closePot is called again

Root + Impact

Description

  • After closePot distributes funds, remainingRewards is never reset to zero. While onlyOwner limits who can call it, if closePot is ever called a second time (possible if the contract receives more tokens), the stale remainingRewards value could trigger another distribution even though the original pot was already closed.

function closePot() external onlyOwner {
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 set to 0
}
}

Risk

Likelihood:

  • Requires owner to call closePot twice, low probability

  • But if contract receives additional tokens, second call would distribute incorrectly

Impact:

  • Double distribution if called twice with new funds

  • Stale state makes contract behavior unpredictable

Proof of Concept

The following test demonstrates that after closePot distributes funds, remainingRewards remains non-zero.
If the contract receives additional tokens and closePot is called again, it triggers another full distribution
using the stale remainingRewards value:
function testStaleRemainingRewardsAfterClose() public mintAndApproveTokens {
vm.startPrank(user);
contest = ContestManager(conMan).createContest(players, rewards, IERC20(weth), 4);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
vm.prank(player1);
Pot(contest).claimCut();
vm.warp(91 days);
vm.startPrank(user);
ContestManager(conMan).closeContest(contest);
vm.stopPrank();
// remainingRewards never zeroed - check stale state
uint256 remaining = Pot(contest).getRemainingRewards();
assertGt(remaining, 0); // should be 0 after close but isn't
// If contract receives new tokens, closePot can be called again
// distributing based on stale remainingRewards value
ERC20Mock(address(weth)).mint(contest, 100);
vm.warp(block.timestamp + 1 days);
vm.startPrank(user);
ContestManager(conMan).closeContest(contest); // second close succeeds
vm.stopPrank();
}

Recommended Mitigation

function closePot() external onlyOwner {
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
+ remainingRewards = 0;
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);
}
}
}
Updates

Lead Judging Commences

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