MyCut

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

closePot() never updates remainingRewards or marks the pot closed, so the same pot can be closed repeatedly and pays out the remainder multiple times

Root + Impact

closePot() reads remainingRewards to size the manager cut and the claimant cuts, but it never writes the variable (or any closed flag) back. The 90-day time check stays satisfied forever, so the owner can call closeContest on the same pot again and again. Every call pays a fresh 10% manager cut and a fresh round of claimant cuts computed from the original, stale remainingRewards, draining the leftover balance left behind by the distribution (which the divisor bug in this same function guarantees is substantial). One pot's closing distribution can be paid out three times in the verified PoC, limited only by the token balance physically remaining.

Description

function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
if (remainingRewards > 0) { // remainingRewards is NEVER decreased or zeroed here
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);
}
}
// @> // no: remainingRewards = 0; no: closed = true;
}

Risk

Likelihood: Medium - requires the trusted owner (or any automation driving it) to call close more than once, whether by mistake, by a duplicated transaction, or deliberately; the contract offers no protection at all against it.
Impact: Medium - the pot's remaining balance, which by the documented rules belongs to a single final distribution, can be paid out repeatedly to the manager and past claimants until it is gone, leaving nothing for the intended one-time settlement and corrupting the contest's final accounting.

Proof of Concept

Verified with Foundry (test passes). Same 100-token, 4-player pot, one claimant. The owner closes the pot three times. The manager cut of 7.5 is paid three times (22.5 total) and the claimant cut of 16.875 is paid three times, while getRemainingRewards() still reports the untouched 75 after all three closes.

manager.closeContest(potAddr); // close #1: pays 7.5 + 16.875
manager.closeContest(potAddr); // close #2: pays 7.5 + 16.875 again
manager.closeContest(potAddr); // close #3: pays 7.5 + 16.875 again
assertEq(token.balanceOf(address(manager)), 22.5e18);
assertEq(Pot(potAddr).getRemainingRewards(), 75e18); // never updated

Recommended Mitigation

Settle the state inside closePot: zero remainingRewards (and record a closed flag) before making the transfers, so any later call sees nothing left to distribute.

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!