MyCut

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

Pot::closePot is re-callable with no closed-flag and never decrements remainingRewards, letting the owner take repeated manager cuts and re-pay claimants until the Pot is drained

Summary

Pot::closePot has no "already closed" flag and never decrements remainingRewards inside its payout loop. Once the 90-day window has passed, the function can be called repeatedly. Each call passes the time check again, sees remainingRewards > 0 again, and re-executes both the manager-cut transfer and the full claimant payout loop, draining the Pot beyond the intended single distribution.

Description

A correct close-out is a one-time operation: take the manager cut once, distribute the residue once, mark the Pot closed. closePot does neither the "once" nor the "mark closed" part. remainingRewards is the only state that could gate re-entry into the distribution, and it is never updated during close, so its value is identical before and after a close.

Risk

The owner (or anyone who can trigger closeContest) can call close multiple times after the 90-day boundary. Each repeat sends another managerCutPercent cut to the manager and pays claimants[] their claimantCut a second, third, ... time, transferring out funds that were never theirs to distribute again. The accounting view getRemainingRewards() also reports a stale, pre-close figure forever, misleading any integrator.

Vulnerability Details

src/Pot.sol::closePot (L49-62):

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);
        }
    }
}

There is no closed boolean and remainingRewards is never set to 0. The guard if (remainingRewards > 0) is the only re-entry gate, and it is never falsified by a close.

Impact

  • Double (or N-times) manager cut: first close sends 70e18; a second close sends another 70e18 (70e18 -> 140e18 in the PoC), and so on per call.

  • Repeated claimant payouts drain residue/leftover balance.

  • Stale accounting: getRemainingRewards() never reflects the distribution.

Proof of Concept

test_HH2_reclose_double_manager_cut (close, then close again after the window). Real forge output:

[PASS] test_HH2_reclose_double_manager_cut()

Assertions: manager balance after second close == 2x the single-cut amount (70e18 -> 140e18); Pot balance decreases on each re-call with no state preventing it.

Recommended Mitigation

Add a closed flag and set it at the start of closePot:

bool private closed;
...
if (closed) revert Pot__AlreadyClosed();
closed = true;

Also zero remainingRewards after distribution so the accounting view is correct and the residue cannot be re-distributed.

Tools Used

Foundry (forge), manual review.

Updates

Lead Judging Commences

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