MyCut

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

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

Root + Impact

Description

ContestManager.fundContest() transfers totalRewards tokens into a Pot every time it is called. There is no isFunded flag, no balance check on the Pot, and no revert if the Pot already holds tokens. A second call deposits another totalRewards into the same Pot, giving it 2× the intended balance.

// ContestManager.sol fundContest() — no guard against double-funding
function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
IERC20 token = pot.getToken();
uint256 totalRewards = contestToTotalRewards[address(pot)];
// @> no check: if (pot already funded) revert
token.transferFrom(msg.sender, address(pot), totalRewards);
}

Pot.remainingRewards is set once at construction to totalRewards and decremented only when players claim. It never reads the contract's actual token balance. When closePot runs it distributes only up to remainingRewards — the extra tokens deposited by the second fundContest call are never distributed and cannot be recovered.

Risk

Likelihood: Low. Requires owner error (calling fundContest twice) or a UI bug that submits the transaction twice.

Impact: Any excess tokens above i_totalRewards are permanently locked. With standard ERC20 there is no emergencyWithdraw in either Pot or ContestManager to recover them.

Proof of Concept

The test calls fundContest(0) twice on the same Pot. The second call transfers another totalRewards into the Pot because there is no guard to prevent it. After both calls the Pot holds 2× totalRewards. When closeContest runs, it distributes only up to remainingRewards (set at construction to 1× totalRewards) — the extra never moves. The final assertion confirms the excess is still locked in the Pot after close.

function testDoubleFundLocksExcess() public mintAndApproveTokens {
vm.startPrank(owner);
address pot = contestManager.createContest(players, rewards, IERC20(weth), totalRewards);
contestManager.fundContest(0); // first fund — correct
contestManager.fundContest(0); // second fund — excess tokens locked
vm.stopPrank();
// Pot now holds 2× totalRewards
assertEq(weth.balanceOf(pot), totalRewards * 2);
vm.warp(block.timestamp + 91 days);
vm.prank(owner);
contestManager.closeContest(pot);
// closePot only distributed remainingRewards (1×), excess 1× still locked
assertGt(weth.balanceOf(pot), 0, "excess tokens permanently locked");
}

Recommended Mitigation

Add an isFunded flag to Pot and revert on a second fund:

+ bool public isFunded;
+ error Pot__AlreadyFunded();
// called by ContestManager after transferFrom
+ function markFunded() external onlyOwner {
+ if (isFunded) revert Pot__AlreadyFunded();
+ isFunded = true;
+ }

Or check the Pot balance inside fundContest:

function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
IERC20 token = pot.getToken();
uint256 totalRewards = contestToTotalRewards[address(pot)];
+ require(token.balanceOf(address(pot)) == 0, "already funded");
token.transferFrom(msg.sender, address(pot), totalRewards);
}
Updates

Lead Judging Commences

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