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);
}
}
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.