MyCut

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

Closed pots can be re-funded allowing balance inflation and accounting inconsistencies

Root + Impact

Root Cause: Missing closed state validation in fundContest()
Impact: Closed pots can be re-funded, inflating balance and breaking accounting invariants

Description

  • When a pot is closed via closePot(), it has completed its lifecycle and distributed all rewards to claimants and the manager.

  • The fundContest() function does not verify whether a pot has already been closed before transferring funds to it, allowing additional tokens to be sent to pots that should be immutable.

function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
IERC20 token = pot.getToken();
uint256 totalRewards = contestToTotalRewards[address(pot)];
if (token.balanceOf(msg.sender) < totalRewards) {
revert ContestManager__InsufficientFunds();
}
@> token.transferFrom(msg.sender, address(pot), totalRewards);
// @audit No validation that pot is not already closed
// @audit No validation that pot has not been funded before
}

Risk

Likelihood:

  • The owner can call fundContest() at any time without restriction, including after closePot() has executed.

  • There is no on-chain enforcement preventing multiple calls to fundContest() for the same pot index.

  • Administrative errors or misunderstanding of pot state could lead to accidental re-funding.

Impact:

  • Closed pots receive additional funds that were not part of the original contest design, breaking the immutability assumption.

  • The pot's token balance becomes inflated beyond remainingRewards, creating accounting discrepancies (e.g., pot balance 1,900 but remainingRewards still 1,000).

  • Additional funds sent to closed pots have no mechanism to be claimed or distributed, as closePot() has already executed and cannot be called again.

  • Players may still have unclaimed checkCut() amounts that could now be claimed against the newly injected funds, despite the pot being closed.

Proof of Concept

The following scenario demonstrates a closed pot being re-funded:

  1. A pot is created with 1,000 tokens total rewards

  2. After 90 days, closeContest() is called, which triggers closePot()

  3. closePot() distributes 100 tokens to ContestManager (manager cut), leaving 900 tokens in the pot

  4. The owner then calls fundContest(0) on the already-closed pot

  5. An additional 1,000 tokens are transferred to the pot

// State after closePot()
Pot balance: 900 tokens
remainingRewards: 1,000 tokens (not updated from manager cut transfer)
Pot is closed, lifecycle complete
// Owner calls fundContest(0) on the closed pot
fundContest(0) executes:
- No check that pot is closed
- transferFrom(owner, pot, 1,000 tokens) ✓
// Final state
Pot balance: 1,900 tokens ❌
remainingRewards: 1,000 tokens
Accounting broken: balance > expected amount
Test fails: "managerCut + claimantsPaid + potBalance > remainingAtClose"

The pot now holds 1,900 tokens but remainingRewards is still 1,000, creating a 900 token discrepancy that breaks protocol invariants.

Recommended Mitigation

Add a closed state flag to the Pot contract and validate it in fundContest():

In Pot.sol:

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

In ContestManager.sol:

function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
+ require(!pot.isClosed(), "Cannot fund closed pot");
+
IERC20 token = pot.getToken();
uint256 totalRewards = contestToTotalRewards[address(pot)];
if (token.balanceOf(msg.sender) < totalRewards) {
revert ContestManager__InsufficientFunds();
}
token.transferFrom(msg.sender, address(pot), totalRewards);
}

Additionally, consider tracking whether a pot has already been funded to prevent double-funding before closure.

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!