MyCut

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

Pot Contract: Repeated Close Allows Extra Manager Rewards

Pot Contract: Repeated Close Allows Extra Manager Rewards

Description

  • The Pot contract is designed to distribute rewards to contest claimants for 90 days. After this claim period, the manager is entitled to claim a one-time 10% cut of the remaining unclaimed rewards, and the rest is distributed to the claimants who claimed in time.

  • The closePot() function does not enforce a single execution. As a result, the manager can call closePot() multiple times and repeatedly claim 10% of the remaining rewards. This allows the manager to extract more than the intended share, violating the protocol’s reward distribution rules.

// there is no guard preventing multiple calls of the function
// remainingRewards is never updated
function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
}

Risk

Likelihood:

  • This bug occurs whenever the pot has remaining unclaimed rewards after the 90-day claim period and the owner or manager calls the closePot() function. Each call results in the manager receiving an additional 10% of the remaining rewards. The vulnerability can be exploited repeatedly until the rewards are fully distributed.

Impact:

  • The manager can claim more than the intended 10% cut of unclaimed rewards, breaking the protocol’s reward allocation rules.

  • Contest participants may receive less than their fair share if the manager repeatedly claims the cut.

  • The protocol’s assumptions about total token distribution are compromised, potentially undermining user trust.

  • Any owner or manager account can exploit this bug without special privileges beyond normal permissions.

Proof of Concept

In the test file, paste this code and then run `forge test --mt test_closePotTwice -vvv`

function test_closePotTwice() public mintAndApproveTokens {
vm.startPrank(owner);
ContestManager cm = helper_contest();
cm.createContest(
players,
rewards,
IERC20(ERC20Mock(wethToken)),
totalRewards
);
cm.fundContest(0);
address pot = cm.getContests()[0];
console.log("totalRewards",cm.getContestTotalRewards(pot));
// Move time forward
vm.warp(block.timestamp + 91 days);
address contestManager = address(cm);
uint256 ownerBalanceBefore = ERC20Mock(wethToken).balanceOf(contestManager);
// First close
cm.closeContest(pot);
uint256 ownerBalanceAfterFirstClose =
ERC20Mock(wethToken).balanceOf(contestManager);
console.log(
"Owner balance after first close: ",
ownerBalanceAfterFirstClose
);
// Second close
cm.closeContest(pot);
uint256 ownerBalanceAfterSecondClose =
ERC20Mock(wethToken).balanceOf(contestManager);
console.log(
"Owner balance after second close: ",
ownerBalanceAfterSecondClose
);
uint256 firstManagerCut =
ownerBalanceAfterFirstClose - ownerBalanceBefore;
uint256 secondManagerCut =
ownerBalanceAfterSecondClose - ownerBalanceAfterFirstClose;
// 🔥 THIS PROVES THE INVARIANT IS BROKEN
assertGt(firstManagerCut, 0);
assertEq(firstManagerCut, secondManagerCut);
vm.stopPrank();
}

Recommended Mitigation

Intorduce a state variable like `bool`

+ bool private closed;
then add this line in the function:
+ function closePot() external onlyOwner {
+ if (closed) revert Pot__AlreadyClosed();
+ closed = true;
+ ...
+ }
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!