MyCut

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

Root cause: `claimCut` never checks whether the 90 day window has expired or the pot was already closed. Impact: players who missed the deadline still take their full reward afterwards, and whoever arrives last is bricked

Root cause: claimCut never checks whether the 90 day window has expired or the pot was already closed. Impact: players who missed the deadline still take their full reward afterwards, and whoever arrives last is bricked

Description

The README puts a hard boundary on the claim period. Claimants get "90 days to claim before the manager takes a cut of the remaining pool and the remainder is distributed equally to those who claimed in time". Two things follow: after the window a player has forfeited his reward, and the leftover is settled among those who did claim in time. closePot enforces half of that deal by refusing to run before 90 days.

claimCut enforces nothing. It reads the mapping, checks the reward is non zero, and pays. There is no timestamp comparison, no flag recording that the pot has been closed, and closePot does not clear playersToRewards, so every entry survives the close untouched. A player who ignored the entire window can walk in afterwards and collect in full, which contradicts the deadline the rest of the contract is built around.

// src/Pot.sol
function claimCut() public {
address player = msg.sender;
uint256 reward = playersToRewards[player];
@> if (reward <= 0) { // the only check, nothing about time or state
revert Pot__RewardNotFound();
}
playersToRewards[player] = 0;
remainingRewards -= reward;
claimants.push(player);
_transferReward(player, reward);
}

The two periods overlap instead of meeting: at exactly 90 days closePot is already permitted while claimCut is still wide open. And because the manager cut has already left the same balance, the pot can no longer cover every outstanding reward. Late players race each other for what is left, and whoever arrives after it runs dry simply reverts.

Risk

Likelihood:

  • Any registered player who missed the window can do this at any moment after the close, with an ordinary call and no special conditions.

  • It costs nothing to attempt and there is no guard to trip, so a player watching the pot has every incentive to send the transaction the moment the close lands.

Impact:

  • Rewards the protocol treats as forfeited leave the pot anyway, so the leftover the spec reserves for punctual claimants is drained by the people who ignored the deadline.

  • The pot is short by the manager cut, so late claims become first come first served and a player loses his entire allocation to whoever transacts first. In the run below one late player takes his full 500 and the equally entitled second one is left with nothing recoverable.

Proof of Concept

Two players are registered for 500 each and the contest is funded with 1000. Nobody claims during the window, so claimants stays empty and the payout loop inside closePot never executes, which keeps this independent of how the leftover is divided. The manager closes after 91 days and his cut of 100 leaves the pot. Player one then claims his full reward three months past the deadline, and player two, who has exactly the same entitlement, is left with a pot holding 400 against a reward of 500. Run with forge test --match-test test_claimAfterClose_independentOfDivisorBug -vv.

function test_claimAfterClose_independentOfDivisorBug() public {
Pot pot = _mk(500, 500, 1000); // p1 and p2, nobody claims yet
vm.warp(block.timestamp + 91 days); // window expires
vm.prank(manager);
conMan.closeContest(address(pot)); // claimants empty, loop is a no-op
assertEq(weth.balanceOf(address(pot)), 900); // cut of 100 already gone
vm.prank(p1);
pot.claimCut(); // 91 days late, still succeeds
assertEq(weth.balanceOf(p1), 500);
assertEq(weth.balanceOf(address(pot)), 400); // not enough left for p2
vm.prank(p2);
vm.expectRevert();
pot.claimCut();
assertEq(weth.balanceOf(p2), 0);
}

Output:

p1 claimed 91 days past the deadline, p2 left with nothing

Recommended Mitigation

Give the pot an explicit closed state and refuse claims once the window is over. Storing a flag also makes the intent readable rather than leaving it implied by a timestamp:

+ error Pot__ClaimPeriodOver();
+ bool private closed;
function claimCut() public {
+ if (closed || block.timestamp - i_deployedAt >= 90 days) {
+ revert Pot__ClaimPeriodOver();
+ }
address player = msg.sender;
...
}
function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
+ closed = true;

With the guard in place both late calls revert, the forfeited rewards stay in the pot and are settled through the leftover distribution, which is the behaviour the README describes.

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!