MyCut

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

claimCut allows claiming after pot is closed — 90-day deadline is not enforced post-close when residual balance exists

Root + Impact

Description

  • closePot() distributes the remaining reward pool among claimants who acted within 90 days. The intended invariant is that any player who did not call claimCut() before closure forfeits their allocation.

  • closePot() does not set any closed-state flag and does not zero the playersToRewards entries of non-claiming players. After closure, those players still have non-zero mapping entries and can call claimCut(). If any token balance remains in the pot (truncation dust from M6, a direct transfer, or intentional re-funding), the post-close claimCut call succeeds — violating the 90-day claim window.

function claimCut() public {
address player = msg.sender;
uint256 reward = playersToRewards[player];
if (reward <= 0) { revert Pot__RewardNotFound(); }
// no check: require(!isClosed)
// no check: require(block.timestamp <= i_deployedAt + 90 days)
playersToRewards[player] = 0;
remainingRewards -= reward;
claimants.push(player);
_transferReward(player, reward);
}

Risk

Likelihood:

  • A post-close balance in the pot is uncommon but achievable — truncation dust always exists after closePot (per M6), and anyone can send tokens directly to a contract address.

  • A player who intentionally waited past the deadline can observe the pot's token balance on-chain and claim if it is non-zero, gaming the deadline by monitoring for a post-close deposit.

Impact:

  • The 90-day claim window ceases to be a hard deadline. Players who forfeited their allocation by not claiming in time can reclaim it post-closure if the contract holds any residual balance, undermining the contest's finality guarantees.

Proof of Concept

Place this test in test/ and run forge test --match-test testPostCloseClaimSucceeds. The test demonstrates that players can successfully call claimCut() after closePot() has already been executed, withdrawing tokens from a pot that has officially closed.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {Pot} from "src/Pot.sol";
import {MockERC20} from "test/mocks/MockERC20.sol";
contract L11Test is Test {
MockERC20 token;
Pot pot;
address manager = address(0x1);
address player1 = address(0x2); // claims in time
address player2 = address(0x3); // does NOT claim in time
function setUp() public {
token = new MockERC20();
address[] memory players = new address[](2);
players[0] = player1; players[1] = player2;
uint256[] memory rewards = new uint256[](2);
rewards[0] = 100e18; rewards[1] = 100e18;
token.mint(manager, 200e18);
vm.prank(manager);
pot = new Pot(players, rewards, token, 200e18);
vm.prank(manager);
token.transfer(address(pot), 200e18);
}
function testPostCloseClaimSucceeds() public {
vm.prank(player1); pot.claimCut(); // claims within window
vm.warp(block.timestamp + 91 days);
vm.prank(manager); pot.closePot();
// Ensure pot has residual balance post-close
token.transfer(address(pot), 1e18);
// player2 still has a non-zero mapping entry; claimCut succeeds post-close
vm.prank(player2);
pot.claimCut(); // succeeds — deadline not enforced
assertGt(token.balanceOf(player2), 0);
}
}

Recommended Mitigation

Add a boolean state variable isClosed and set it to true inside closePot(). Then check it at the top of claimCut() so the function reverts once the distribution cycle has ended.

function claimCut() public {
+ require(!isClosed, "Pot is closed");
address player = msg.sender;
// ...
}
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!