MyCut

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

No Fallback for Zero Claimants in closePot → 90% of Rewards Permanently Locked

Description

  • After the 90-day window, closePot() sends the manager their 10% cut, then distributes the remaining 90% among the claimants array — players who previously
    called claimCut(). The loop iterating over claimants is the only distribution mechanism.

  • When no player has called claimCut() before the pot closes, claimants.length == 0 and the loop body never executes. The manager receives remainingRewards / 10
    (which is also sent to ContestManager instead of the owner — see H-01), while the remaining 90% of remainingRewards stays in the Pot contract indefinitely. There
    is no fallback path, no second close attempt, and no rescue function for the stranded tokens.

function closePot() external onlyOwner {
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut); // @> 10% sent (to wrong address, see H-01)

      uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
      // @> If claimants.length == 0 this loop never executes
      // @> 90% of remainingRewards stays in the Pot forever — no fallback
      for (uint256 i = 0; i < claimants.length; i++) {
          _transferReward(claimants[i], claimantCut);
      }
      // @> No else branch, no residual transfer, no recovery path
  }

}

Risk

Likelihood:

  • Any contest where all players miss the 90-day claim window triggers the full lock — not an edge case in a protocol explicitly designed around the possibility
    of missed claims

  • Low-visibility contests or those with unclear UX can easily reach close with zero claimants

Impact:

  • 90% of the total reward pool is permanently locked in the Pot contract with no recovery mechanism

  • Combined with H-01, effectively 100% of remainingRewards becomes inaccessible — 10% locked in ContestManager, 90% locked in Pot

Proof of Concept

function testClosePotWithZeroClaimants() public mintAndApproveTokens {
vm.startPrank(user);
contest = ContestManager(conMan).createContest(
players, rewards, IERC20(weth), totalRewards
);
ContestManager(conMan).fundContest(0);
vm.stopPrank();

  vm.warp(block.timestamp + 90 days);

  uint256 managerBalanceBefore = weth.balanceOf(user);

  vm.prank(user);
  ContestManager(conMan).closeContest(contest);

  // Manager did NOT receive all remaining rewards — only 10% (and even that went to ContestManager)
  // 90% of totalRewards is permanently locked in the Pot
  assertEq(weth.balanceOf(contest), (totalRewards * 90) / 100);

}

Recommended Mitigation

uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
  • if (claimants.length == 0) {

  • // No one claimed — return the full remaining balance to the manager
    
  • i_token.transfer(msg.sender, remainingRewards - managerCut);
    
  • } else {
    for (uint256 i = 0; i < claimants.length; i++) {
    _transferReward(claimants[i], claimantCut);
    }

  • }


Updates

Lead Judging Commences

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