MyCut

AI First Flight #8
Beginner FriendlyFoundry
EXP
View results
Submission Details
Severity: medium
Valid

Zero claimants — manager cannot reclaim unclaimed rewards after closePot

Title: Zero claimants — manager cannot reclaim unclaimed rewards after closePot
Impact: Medium. When no one claims, only 10% is distributed — 90% permanently locked.
Likelihood: Medium. Requires a contest where zero authorized players claim within 90 days.
Reference Files: repos/src/Pot.sol:49-62

Description

When zero players claim during the 90-day window, claimants.length equals zero. In closePot(), the manager's 10% cut is transferred, but the claimantCut loop executes zero iterations — leaving (remainingRewards - managerCut) tokens permanently locked in the Pot. The code has no fallback to return the unclaimed remainder to the manager when no claimants exist.

function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) { revert; }
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut); // 10% distributed
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
for (uint256 i = 0; i < claimants.length; i++) { // claimants.length = 0 → SKIPS
_transferReward(claimants[i], claimantCut);
}
// (remainingRewards - managerCut) remains locked — no transfer back to manager
}
}

The contest description states "the manager takes a cut of the remaining pool and the remainder is distributed equally to those who claimed" — but with zero claimants, the remainder has no destination and stays trapped.

Risk

Impact: Medium. If no players claim during the 90-day window (e.g., contest forgotten, all players lose keys), the manager recovers only 10% of the pool. The remaining 90% is permanently locked in the Pot with no withdrawal mechanism.
Likelihood: Medium. Zero-claimant scenarios are edge cases but realistic — expired contests, abandoned projects, or all claimants losing access within the window. The protocol has no handling for this case.
With a 10,000-token contest and zero claimants, the manager receives 1,000 tokens while 9,000 tokens remain permanently locked.

Proof of Concept

function testZeroClaimantsManagerLosesRemainder() public {
vm.startPrank(owner);
address pot = contestManager.createContest(players, rewards, token, 1000);
contestManager.fundContest(0);
vm.stopPrank();
vm.warp(91 days);
uint256 balanceBefore = token.balanceOf(address(contestManager));
vm.prank(owner); contestManager.closeContest(pot);
uint256 managerReceived = token.balanceOf(address(contestManager)) - balanceBefore;
uint256 locked = token.balanceOf(pot);
assertGt(locked, managerReceived * 8); // locked >> manager's 10%
}

With zero claimants, the manager receives only ~10% of the pool while the remaining ~90% stays locked in the Pot.

Recommended Mitigation

if (claimants.length == 0) {
i_token.transfer(ContestManager(msg.sender).owner(), remainingRewards);
} else {
// existing distribution logic
}

When no claimants exist, transfer the entire remaining pool to the contest owner instead of leaving it locked.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day ago
Submission Judgement Published
Validated
Assigned finding tags:

[M-01] Incorrect Handling of Zero Claimants in `closePot()` Function

## Description In the \`closePot\` function, if the number of claimants is zero, the remaining rewards intended for distribution among claimants may not be properly reclaimed by the Contest Manager. The \`claimantCut\` is calculated using the length of the \`i_players\` array instead of the \`claimants\` array, which could lead to incorrect distribution. Additionally, the function does not have a mechanism to handle the scenario where there are zero claimants, resulting in the potential loss of rewards. ## Vulnerability Details Specifically, when there are no claimants: - The manager's cut is calculated but only a portion or none of the remaining rewards is transferred back to the Contest Manager. - The rewards intended for claimants (\`claimantCut\`) are not distributed because the loop iterating over \`claimants\` does not execute, but there's also no fallback to reclaim these rewards. ## Proof of Concept Add this test in the TestMyCut.t.sol: ```markdown function testClosePotWithZeroClaimants() public mintAndApproveTokens { vm.startPrank(user); // Step 1: Create a new contest contest = ContestManager(conMan).createContest(players, rewards, IERC20(weth), totalRewards); // Step 2: Fund the pot ContestManager(conMan).fundContest(0); // Step 3: Move forward in time by 90 days so the pot can be closed vm.warp(block.timestamp + 90 days); // Step 4: Close the pot with 0 claimants uint256 managerBalanceBefore = weth.balanceOf(user); ContestManager(conMan).closeContest(contest); uint256 managerBalanceAfter = weth.balanceOf(user); vm.stopPrank(); // Step 5: Assert that the Contest Manager received all the remaining rewards // Since there are no claimants, the manager should receive all remaining rewards assertEq(managerBalanceAfter, managerBalanceBefore + totalRewards, "Manager did not reclaim all rewards after closing pot with zero claimants."); ``` In the test `testClosePotWithZeroClaimants`, after closing a pot with zero claimants, the Contest Manager is unable to reclaim all the remaining rewards: ```markdown ├─ [9811] ContestManager::closeContest(Pot: [0x43e82d2718cA9eEF545A591dfbfD2035CD3eF9c0]) │ ├─ [8956] Pot::closePot() │ │ ├─ [5288] 0x5929B14F2984bBE5309c2eC9E7819060C31c970f::transfer(ContestManager: [0x7BD1119CEC127eeCDBa5DCA7d1Bd59986f6d7353], 0) │ │ │ ├─ emit Transfer(from: Pot: [0x43e82d2718cA9eEF545A591dfbfD2035CD3eF9c0], to: ContestManager: [0x7BD1119CEC127eeCDBa5DCA7d1Bd59986f6d7353], value: 0) ``` ## Impact - This bug can lead to incomplete recovery of rewards by the Contest Manager. If no participants claim their rewards, a significant portion of the remaining tokens could remain locked in the contract indefinitely, leading to financial loss and inefficient fund management. - And All the reward is lost except from the little 10 % the manager gets because there was no mechanism to claim the remainingReward ## Recommendations - Adjust Calculation Logic: Modify the \`claimantCut\` calculation to divide by \`claimants.length\` instead of \`i_players.length\`. This ensures that only the claimants are considered when distributing the remaining rewards. - Handle Zero Claimants: Implement a check to determine if there are zero claimants. If true, all remaining rewards should be transferred back to the Contest Manager to ensure no tokens are left stranded in the contract. Example ```markdown if (claimants.length == 0) { i_token.transfer(msg.sender, remainingRewards); } else { for (uint256 i = 0; i < claimants.length; i++) { \_transferReward(claimants[i], claimantCut); } } ``` This approach ensures that in the event of zero claimants, all remaining rewards are securely returned to the Contest Manager.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!