MyCut

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

fundContest() has no "already funded" guard, allowing the owner to double-fund and permanently lock the excess tokens

Root + Impact

Description

ContestManager.fundContest() transfers totalRewards tokens from the owner into the Pot every time it is called. There is no state variable, mapping, or flag tracking whether a particular Pot has already been funded. A second call deposits another full totalRewards amount, but the Pot's remainingRewards is set once at construction — so any tokens beyond the original totalRewards can never be distributed.

// ContestManager.sol :: fundContest()
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();
}
// @> No check: was this pot already funded?
// @> Calling this twice transfers 2x totalRewards into the Pot
token.transferFrom(msg.sender, address(pot), totalRewards);
}
// Pot.sol :: constructor (relevant excerpt)
// @> remainingRewards is set once and never updated on incoming transfers
remainingRewards = totalRewards; // immutable accounting snapshot

Risk

Likelihood:

  • An owner script with a retry-on-failure pattern, a frontend double-click, or a scripting error easily triggers a second fundContest() call on the same index.

  • Because there is no on-chain feedback (no event on first fund, no isFunded view), the owner has no cheap way to verify whether funding already occurred before calling again.

Impact:

  • All tokens deposited above the original totalRewards are permanently stuck: closePot() only distributes remainingRewards (the constructor snapshot), never the actual ERC20 balance.

  • For large reward pools this can represent substantial token loss with no recovery path.

Proof of Concept

// Owner calls fundContest(0) twice for a 10_000e18 reward pool
// After call 1: Pot.balanceOf = 10_000e18, remainingRewards = 10_000e18 ✓
// After call 2: Pot.balanceOf = 20_000e18, remainingRewards = 10_000e18 ✗
// At close: only 10_000e18 is distributed (remainingRewards)
// Locked : 10_000e18 excess tokens — unrecoverable

Recommended Mitigation

+ // ContestManager.sol
+ mapping(address => bool) public isFunded;
+ error ContestManager__AlreadyFunded();
function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
+ if (isFunded[address(pot)]) revert ContestManager__AlreadyFunded();
+ isFunded[address(pot)] = true;
IERC20 token = pot.getToken();
// ... rest unchanged
}
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!