MyCut

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

closePot() can be executed multiple times, draining rewards repeatedly

Root + Impact

Description

Normal behavior:
After the 90-day claim period has elapsed, the Pot should be closed once, where the manager takes a cut of the remaining rewards and the remainder is distributed, after which no further reward distribution should occur.

Issue:
The closePot() function does not update or lock the reward state after execution, allowing it to be called repeatedly and causing the same remaining rewards to be redistributed multiple times.

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);
}
// @> remainingRewards is never updated or reset
// @> no state variable prevents closePot() from being called again
}
}

Risk

Likelihood:

  • Reason 1: Occurs every time closePot() is called after the 90-day claim period, as no state transition restricts repeated execution

  • Reason 2: Repeated calls reuse the same remainingRewards snapshot for reward calculation

Impact:

  • Impact 1: Rewards can be distributed multiple times, exceeding the intended totalRewards invariant

  • Impact 2: The contract’s token balance can be fully drained through repeated closePot() calls

Proof of Concept

Once the claim window has elapsed, calling closePot() performs reward distribution without modifying the remainingRewards state. Because no guard or state update exists, subsequent calls execute the same logic again using the same reward snapshot, causing repeated payouts.

// Assume 90 days have passed since deployment
pot.closePot();
// Manager cut and claimant rewards are transferred
pot.closePot();
// The same remainingRewards value is reused, triggering another full distribution
pot.closePot();
// Repeated calls continue draining the contract balance

This demonstrates that reward distribution is not a one-time operation, but can be executed an arbitrary number of times.

Recommended Mitigation

The contract should transition into a finalized state after closing the Pot. This can be achieved by either introducing an explicit closed flag or by updating remainingRewards to zero after the first execution, ensuring subsequent calls cannot redistribute rewards.

function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
+ if (remainingRewards == 0) return;
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 = 0;
}

This ensures closePot() is idempotent and enforces the intended one-time reward distribution invariant.

Updates

Lead Judging Commences

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