MyCut

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

`ContestManager::fundContest` can be called more than once for the same Pot, and the extra tokens are locked forever

Root + Impact

Description

  • fundContest should transfer totalRewards to a Pot once.

  • Nothing records that a Pot is already funded, so every call transfers totalRewards again. The Pot accounts only for totalRewards (remainingRewards), so closePot never distributes the surplus, and neither contract can withdraw it.

function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
IERC20 token = pot.getToken();
uint256 totalRewards = contestToTotalRewards[address(pot)];
@> // no "already funded" check
if (token.balanceOf(msg.sender) < totalRewards) {
revert ContestManager__InsufficientFunds();
}
token.transferFrom(msg.sender, address(pot), totalRewards);
}

Risk

Likelihood:

  • When the admin retries a funding transaction that looked like it failed, or passes the wrong index (indices are positional, and the admin can easily confuse contests).

Impact:

  • The whole second transfer (totalRewards) is permanently lost to the admin.

Proof of Concept

The Pot is funded twice. Both players claim, and the Pot is closed. 100 tokens remain in the Pot with no way to recover them.

function testFundContestTwice() public mintAndApproveTokens {
vm.startPrank(user);
rewards = [65, 35];
totalRewards = 100;
contest = ContestManager(conMan).createContest(players, rewards, IERC20(ERC20Mock(weth)), totalRewards);
ContestManager(conMan).fundContest(0);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
assertEq(weth.balanceOf(contest), 2 * totalRewards);
assertEq(Pot(contest).getRemainingRewards(), totalRewards);
vm.prank(player1);
Pot(contest).claimCut();
vm.prank(player2);
Pot(contest).claimCut();
vm.warp(91 days);
vm.prank(user);
ContestManager(conMan).closeContest(contest);
assertEq(weth.balanceOf(contest), totalRewards); // stuck
}

The test uses the setup of the existing test/TestMyCut.t.sol.

Recommended Mitigation

Add mapping to keep funded contests for check. Add error type "ContestManager__AlreadyFunded"

+ mapping(address => bool) public contestFunded;
+ error ContestManager__AlreadyFunded();
function fundContest(uint256 index) public onlyOwner {
Pot pot = Pot(contests[index]);
+ if (contestFunded[address(pot)]) revert ContestManager__AlreadyFunded();
+ contestFunded[address(pot)] = true;
IERC20 token = pot.getToken();
Updates

Lead Judging Commences

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