MyCut

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

`Pot::claimCut` has no deadline, so a late player can claim after the 90-day period and take the bonus meant for players who claimed in time

Root + Impact

Description

  • Players have 90 days to claim. After that, the remaining pool belongs to the manager (10%) and to "those who claimed in time" (README).

  • claimCut never checks the time, and neither closePot nor anything else marks the Pot as closed. Between day 90 and the moment the admin calls closePot, and even after that, a player who missed the deadline can still claim the full reward. This reduces remainingRewards, so the in-time claimants and the manager lose their share.

function claimCut() public {
address player = msg.sender;
uint256 reward = playersToRewards[player];
@> // no check against i_deployedAt + 90 days, no "closed" flag
if (reward <= 0) {
revert Pot__RewardNotFound();
}
playersToRewards[player] = 0;
remainingRewards -= reward;
claimants.push(player);
_transferReward(player, reward);
}

Risk

Likelihood:

  • Every time the admin does not call closePot in the same block the 90 days end. The admin is a person sending a separate transaction, so there is always a window.

  • A late player only needs to watch the mempool or the clock. Claiming right after day 90, or front-running closeContest, costs one transaction.

Impact:

  • Players who claimed in time lose their share of the leftover pool, and the manager loses the 10% cut. In the PoC both get zero.

  • The late player is also pushed into claimants, so they would additionally receive a bonus from any later closePot call.

Proof of Concept

player2 claims 35 in time. player1 misses the deadline and claims 65 on day 91, before the admin closes. closePot then finds remainingRewards == 0: player2 gets no bonus and the manager gets no cut.

function testLateClaimStealsBonus() public {
ERC20Mock token = new ERC20Mock("W", "W", admin, 0);
token.mint(admin, 100);
address[] memory players = new address[](2);
players[0] = player1; players[1] = player2;
uint256[] memory rewards = new uint256[](2);
rewards[0] = 65; rewards[1] = 35;
vm.startPrank(admin);
token.approve(address(conMan), 100);
Pot pot = Pot(conMan.createContest(players, rewards, IERC20(address(token)), 100));
conMan.fundContest(0);
vm.stopPrank();
vm.prank(player2);
pot.claimCut(); // in time
vm.warp(block.timestamp + 91 days);
vm.prank(player1);
pot.claimCut(); // late, still succeeds
assertEq(token.balanceOf(player1), 65);
vm.prank(admin);
conMan.closeContest(address(pot));
assertEq(token.balanceOf(player2), 35); // no bonus
assertEq(token.balanceOf(address(conMan)), 0); // no manager cut
}

Recommended Mitigation

Reject claims once the claim period is over.

+ error Pot__ClaimPeriodOver();
+
function claimCut() public {
+ if (block.timestamp - i_deployedAt >= 90 days) {
+ revert Pot__ClaimPeriodOver();
+ }
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!