MyCut

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

Missing token rescue mechanism allows funds to be permanently locked in closed pots

Root + Impact

Root Cause: Missing token rescue mechanism in Pot contract
Impact: Tokens sent after closure become permanently locked

Description

  • After closePot() executes, the pot has distributed its remaining rewards and completed its lifecycle, but the contract remains live and can still receive tokens.

  • The Pot contract lacks any rescue or sweep mechanism to recover tokens sent to it after closure, either accidentally (user error) or maliciously (grief attack), causing those funds to become permanently locked.

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);
}
}
@> // No flag marking pot as closed
@> // No mechanism to rescue tokens sent after this point
}
// @audit No rescue/sweep function exists in the entire contract

Risk

Likelihood:

  • Users may accidentally send tokens to a closed pot address thinking it is still active.

  • Malicious actors could grief the system by sending dust amounts to closed pots, permanently locking value.

  • The contract accepts ERC20 transfers at any time without validation, making accidental sends likely over the protocol's lifetime.

Impact:

  • Any tokens sent to the pot after closePot() are permanently inaccessible, as no withdrawal mechanism exists.

  • Over many pot instances, accidental transfers accumulate into significant locked value.

  • The pot owner (ContestManager) cannot recover these funds even though it owns the pot contract.

Proof of Concept

The following scenario demonstrates tokens becoming permanently locked:

  1. A pot is created with 1,000 tokens and closes after 90 days, distributing rewards properly

  2. After closure, someone accidentally sends 9,319 tokens to the pot address (via direct transfer or mint)

  3. The pot now holds 10,219 tokens but remainingRewards is still 1,000

  4. No function exists to withdraw these extra 9,219 tokens

// State after closePot()
Pot balance: 900 tokens
remainingRewards: 1,000 tokens (not updated, but this is a separate bug)
// Accidental transfer occurs
MockERC20::mint(Pot, 9,319 tokens)
// Final state
Pot balance: 10,219 tokens
No way to withdraw the extra 9,319 tokens ❌
Funds permanently locked ❌

The test assertion pot balance after close > remainingAtClose fails because the pot balance grew unexpectedly with no mechanism to extract the surplus.

Recommended Mitigation

Add a rescue function that allows the owner to withdraw tokens sent to the pot after closure. Track whether the pot has been closed to prevent abuse:

+ bool public isClosed;
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);
}
}
+ isClosed = true;
}
+ /// @notice Rescue tokens accidentally sent to the pot after closure
+ /// @param recipient Address to receive the rescued tokens
+ function rescueTokens(address recipient) external onlyOwner {
+ require(isClosed, "Pot must be closed first");
+ uint256 balance = i_token.balanceOf(address(this));
+ require(balance > 0, "No tokens to rescue");
+ i_token.transfer(recipient, balance);
+ }

This allows the protocol to recover accidentally sent tokens while preventing abuse by requiring the pot to be closed first.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 6 days 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!