MyCut

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

Pot funded late results in < 90 days for claiming the rewards

Description

  • Players should have a full 90‑day window to claim their rewards starting from the moment the pot is actually funded (i.e., when tokens become available for payout). The manager should only be able to close the pot after this 90‑day funded window elapses.

  • The pot’s closing gate uses the deployment timestamp (i_deployedAt) rather than the funding timestamp. If the manager funds the pot long after creation, players effectively get less than 90 days to claim before the pot can be closed. In extreme cases, the manager could close without ever funding for most of the period, then fund shortly before closing, leaving claimants with only a few days (or even zero days) to claim.

// Pot.sol
constructor(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards) {
// ...
- remainingRewards = totalRewards;
+ remainingRewards = totalRewards; // @> Initialized at deploy (misleading if funding is later)
i_deployedAt = block.timestamp; // @> ROOT CAUSE: window starts at deploy time
// ...
}
function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) { // @> ROOT CAUSE: checks deploy-based window
revert Pot__StillOpenForClaim();
}
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
// ...
}
}

Risk

Likelihood: Medium

  • Operationally, contests are often created ahead of time and funded later (e.g., after sponsor confirmations or treasury movements). Whenever funding occurs after deployment, the shortened window manifests automatically.

  • Managers routinely close pots when the (deploy‑based) 90 days elapse, so the issue occurs in regular workflows whenever funding is delayed.

Impact: Medium

  • Fairness violation / policy breach: Claimants receive less than the promised 90 days to claim, undermining contest rules and user trust.

  • Premature closure and fund mismanagement: Closing can be triggered before 90 days since funding, potentially redistributing unclaimed funds early and creating disputes or support burden.

Proof of Concept

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

  • Run command forge test --mt testLateFundingShortensTheClaimPeriod.

function testLateFundingShortensTheClaimPeriod() public mintAndApproveTokens {
// Create the contest
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(60 days);
// Now fund the contest
vm.prank(user);
ContestManager(conMan).fundContest(0);
// At this point, only 30 days remain for claiming
// That contradicts the original 90-day claim period from deployment
assertTrue(block.timestamp - i_deployedAt < 90 days);
}

Recommended Mitigation

  • Track funding time and use it to gate closure and claims.

  • Also block claims before funding.

- contract Pot is Ownable(msg.sender) {
+ contract Pot is Ownable {
+ // Track funded status and timestamp
+ bool public funded;
+ uint256 public fundedAt;
- constructor(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards) {
+ constructor(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
+ Ownable(msg.sender)
+ {
i_players = players;
i_rewards = rewards;
i_token = token;
i_totalRewards = totalRewards;
- remainingRewards = totalRewards;
+ remainingRewards = 0; // set on funding, not at deploy
i_deployedAt = block.timestamp;
for (uint256 i = 0; i < i_players.length; i++) {
playersToRewards[i_players[i]] = i_rewards[i];
}
}
+ /// @notice Called by the owner (ContestManager) after transferring tokens to this pot.
+ function markFunded(uint256 amount) external onlyOwner {
+ require(!funded, "already-funded");
+ funded = true;
+ fundedAt = block.timestamp;
+ remainingRewards = amount;
+ }
function claimCut() public {
+ require(funded, "not-funded");
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) {
+ require(funded, "not-funded");
+ if (block.timestamp - fundedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
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);
}
}
}
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!