MyCut

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

Missing Re-Fund Guard in fundContest() — Excess Tokens Permanently Locked

Missing Re-Fund Guard in fundContest() — Excess Tokens Permanently Locked

Description

The ContestManager.fundContest() function is intended to be called once per contest to transfer totalRewards worth of ERC20 tokens from the admin into the Pot contract. The function should only allow a single funding operation per contest.

However, fundContest() has no state flag to prevent repeated calls. Each invocation transfers totalRewards tokens from the admin to the Pot, regardless of whether the Pot has already been funded. Since the Pot only distributes the original totalRewards amount through claimCut() and closePot(), any excess tokens transferred in subsequent calls are permanently locked in the Pot contract.

// ContestManager.sol L28-38
function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
IERC20 token = pot.getToken();
@> uint256 totalRewards = contestToTotalRewards[address(pot)]; // Always returns same value
if (token.balanceOf(msg.sender) < totalRewards) {
revert ContestManager__InsufficientFunds();
}
@> token.transferFrom(msg.sender, address(pot), totalRewards); // No re-fund guard
}

Risk

Likelihood: Medium

  • Requires the admin to call fundContest() more than once for the same contest index. While not expected behavior, there is no guard, no event emission, and no warning — an honest mistake or a UI bug could trigger this.

Impact: Medium

  • The excess transfer amount (totalRewards per extra call) is permanently locked. The Pot has no sweep function and its distribution logic only accounts for the original funded amount.

Severity: Medium

Proof of Concept

An admin creates a contest with 1000 USDC total rewards. They call fundContest(0) to fund it, then accidentally call fundContest(0) again (e.g., a UI retry after a timeout). The Pot now holds 2000 USDC, but only 1000 USDC is ever distributable through claimCut() and closePot(). The extra 1000 USDC is permanently locked.

function test_M01_double_fund() public {
// Setup: create contest with 1000 USDC
ContestManager manager = new ContestManager();
// ... create contest ...
// Fund once (correct)
token.approve(address(manager), 2000);
manager.fundContest(0);
assertEq(token.balanceOf(address(pot)), 1000);
// Fund again (accidental)
manager.fundContest(0);
assertEq(token.balanceOf(address(pot)), 2000); // Double funded
// After all claims + closePot(), 1000 USDC remains locked
// ... all players claim, close pot ...
uint256 locked = token.balanceOf(address(pot));
assertGt(locked, 0, "Excess funds permanently locked");
}

Recommended Mitigation

Adding a boolean funding guard ensures each contest can only be funded once. This is a standard pattern for one-time operations and prevents both accidental and malicious re-funding.

+ mapping(address => bool) public isFunded;
function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
+ require(!isFunded[address(pot)], "Already funded");
+ isFunded[address(pot)] = true;
IERC20 token = pot.getToken();
uint256 totalRewards = contestToTotalRewards[address(pot)];
// ...
}
Updates

Lead Judging Commences

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