MyCut

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

fundContest can be called multiple times on the same pot, doubling or tripling the funded amount

Root + Impact

Description

  • fundContest has no check to prevent it from being called multiple times on the same contest index. Each call transfers totalRewards tokens into the pot again. The pot tracks remainingRewards as a fixed value set at construction, so extra funded tokens are permanently locked with no way to recover them.

function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
IERC20 token = pot.getToken();
uint256 totalRewards = contestToTotalRewards[address(pot)];
// No check: has this pot already been funded?
token.transferFrom(msg.sender, address(pot), totalRewards);
}

Risk

Likelihood:

  • Owner calling fundContest twice by mistake is a realistic scenario

  • No on-chain guard prevents it

Impact:

  • Excess tokens permanently locked in the pot

  • Owner loses funds with no recovery path since closePot only distributes remainingRewards which is set at construction

Proof of Concept

The following test demonstrates that fundContest can be called twice on the same pot, sending double the tokens
into it. The extra tokens are permanently locked since remainingRewards only tracks the original totalRewards
value:
function testDoubleFundLocksTokens() public mintAndApproveTokens {
vm.startPrank(user);
contest = ContestManager(conMan).createContest(
players, rewards, IERC20(weth), 4
);
// Fund once
ContestManager(conMan).fundContest(0);
assertEq(ERC20Mock(weth).balanceOf(contest), 4);
// Fund again - no guard prevents this
ERC20Mock(weth).approve(conMan, 4);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
// Pot now has 8 tokens but remainingRewards is still 4
assertEq(ERC20Mock(weth).balanceOf(contest), 8);
assertEq(Pot(contest).getRemainingRewards(), 4);
// Extra 4 tokens permanently locked - closePot only distributes remainingRewards
vm.warp(91 days);
vm.prank(user);
ContestManager(conMan).closeContest(contest);
// Tokens still stuck in pot after close
assertGt(ERC20Mock(weth).balanceOf(contest), 0);
}

Recommended Mitigation

+ mapping(address => bool) private contestFunded;
function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
+ require(!contestFunded[address(pot)], "Contest already funded");
+ contestFunded[address(pot)] = true;
IERC20 token = pot.getToken();
uint256 totalRewards = contestToTotalRewards[address(pot)];
token.transferFrom(msg.sender, address(pot), totalRewards);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 hours 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!