MyCut

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

Player can claim reward even after 90 days after contest deployment

Description

  • Players should only be able to claim their per‑player reward during the 90‑day claim window. After 90 days have elapsed since the contest was opened (or funded, depending on policy), claiming should be blocked, and only the closing/redistribution logic should run.

  • Pot.claimCut() has no time gate and no closed flag. Even when more than 90 days have passed since deployment, players can still call claimCut() and receive their original allocation. This violates the intended claim window and also corrupts post‑close redistribution (since late claimants can join after the deadline).

// Pot.sol
function claimCut() public {
address player = msg.sender;
uint256 reward = playersToRewards[player];
if (reward <= 0) {
revert Pot__RewardNotFound();
}
// @> ROOT CAUSE: No check that the claim window is still open
// @> ROOT CAUSE: No `closed` state guard
playersToRewards[player] = 0;
remainingRewards -= reward;
claimants.push(player);
_transferReward(player, reward);
}

Risk

Likelihood: Medium

  • In routine operations where the manager delays funding or close actions, time will exceed 90 days since deployment while the pot remains open; players can and will continue to claim in that period.

  • The path is consistently reachable because there is no guard in claimCut() to prevent late claims.

Impact: Medium

  • Policy breach / fairness violation: Players who missed the 90‑day window can still collect, contrary to the contest rules.

  • Accounting corruption: Late claims reduce remainingRewards after the deadline, altering the base used for the manager cut and claimant redistribution, causing under/over‑payments and stranded balances.

Proof of Concept

  • Copy the code below to TestMyCut.t.sol.

  • Run command forge test --mt testPlayerCanClaimAfter90DaysWindow -vv.

function testPlayerCanClaimAfter90DaysWindow() public mintAndApproveTokens {
// Create the contest but do not fund it immediately
vm.prank(user);
contest = ContestManager(conMan).createContest(players, rewards, IERC20(ERC20Mock(weth)), 4);
uint256 i_deployedAt = block.timestamp;
// Simulate delay in funding the contest
vm.warp(100 days);
// Now fund the contest
vm.prank(user);
ContestManager(conMan).fundContest(0);
// Check that more than 90 days have passed since deployment
assertTrue(block.timestamp - i_deployedAt > 90 days);
emit log_named_uint("Days since deployment", (block.timestamp - i_deployedAt) / 1 days);
// Player can still claim their cut, even after 90 days from deployment
uint256 player1BalanceBefore = ERC20Mock(weth).balanceOf(player1);
emit log_named_uint("Player 1 balance before claiming cut", player1BalanceBefore);
vm.prank(player1);
Pot(contest).claimCut();
uint256 player1BalanceAfter = ERC20Mock(weth).balanceOf(player1);
emit log_named_uint("Player 1 balance after claiming cut", player1BalanceAfter);
assert(player1BalanceAfter > player1BalanceBefore);
}

Output:

[⠊] Compiling...
No files changed, compilation skipped
Ran 1 test for test/TestMyCut.t.sol:TestMyCut
[PASS] testPlayerCanClaimAfter90DaysWindow() (gas: 1102317)
Logs:
User Address: 0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D
Contest Manager Address 1: 0x7BD1119CEC127eeCDBa5DCA7d1Bd59986f6d7353
Minting tokens to: 0x6CA6d1e2D5347Bfab1d91e883F1915560e09129D
Approved tokens to: 0x7BD1119CEC127eeCDBa5DCA7d1Bd59986f6d7353
Days since deployment: 99
Player 1 balance before claiming cut: 0
Player 1 balance after claiming cut: 3
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.97ms (418.20µs CPU time)
Ran 1 test suite in 13.86ms (1.97ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

  • Add a time gate and a closed flag to Pot, and block claims after the window or once closed.

- contract Pot is Ownable(msg.sender) {
+ contract Pot is Ownable {
+ bool public closed;
+ // If you’ve implemented funding tracking:
+ // bool public funded;
+ // uint256 public fundedAt;
function claimCut() public {
+ // Block claims after the 90-day window since deployment (or since funding).
+ require(block.timestamp - i_deployedAt < 90 days, "claim-window-closed");
+ require(!closed, "pot-closed");
+ // If using funded policy:
+ // require(funded && block.timestamp - fundedAt < 90 days, "claim-window-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);
}
function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
+ require(!closed, "already-closed");
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length; // fix separately
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
+ closed = true;
}
Updates

Lead Judging Commences

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