MyCut

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

Division by zero and incorrect array usage causes DoS and fund loss

[H-3] Division by zero and incorrect array usage causes DoS and fund loss

Description: The closePot() function has two critical flaws:

  1. Division by zero: When no users claim rewards (claimants.length = 0), the function attempts to calculate claimantCut = remaining / claimants.length, causing a division by zero panic and making closePot() permanently uncallable.

  2. Wrong array length: The function divides by i_players.length (all registered players) but distributes to claimants.length (only those who claimed), causing significant fund loss when fewer players claim than were registered.

function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
// WARNING: BUG 1: Uses i_players.length instead of claimants.length
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
// WARNING: BUG 2: If claimants.length = 0, division by zero on line above
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
}

Impact:

  • HIGH severity for both issues

  • Permanent DoS when no claimants (division by zero revert)

  • Significant token loss when claimants.length < i_players.length

  • Manager cannot collect their 10% cut

  • All funds permanently locked in contract

Example scenarios:

Scenario 1: No claimants

10 players registered, 0 claimed
Try to close pot after 90 days
claimantCut = 1000 / 0 -> PANIC (division by zero)
Result: All 1000 tokens stuck forever

Scenario 2: Wrong array length

10 players registered, 2 claimed
Remaining: 800 tokens
Manager cut: 80 tokens (10%)
Leftover: 720 tokens
BUG: claimantCut = 720 / 10 = 72 per person
Distributed: 72 * 2 = 144 tokens
Expected: 720 / 2 = 360 per person = 720 total
Lost: 720 - 144 = 576 tokens stuck (80% loss!)

Proof of Concept:

function test_Division_By_Zero_No_Claimants() public {
// Create and fund contest
vm.startPrank(owner);
address pot = manager.createContest(players, rewards, token, 100);
manager.fundContest(0);
vm.stopPrank();
// NO ONE CLAIMS (claimants.length = 0)
vm.warp(block.timestamp + 91 days);
// Try to close - reverts with division by zero
vm.startPrank(owner);
vm.expectRevert(); // Panic: division by zero
manager.closeContest(pot);
vm.stopPrank();
// All 100 tokens stuck forever
}
function test_Wrong_Array_Fund_Loss() public {
// 10 players registered, only 2 claim
address[] memory manyPlayers = new address[](10);
uint256[] memory manyRewards = new uint256[](10);
for (uint256 i = 0; i < 10; i++) {
manyPlayers[i] = makeAddr(string(abi.encodePacked("player", i)));
manyRewards[i] = 10;
}
vm.startPrank(owner);
address pot = manager.createContest(manyPlayers, manyRewards, token, 100);
manager.fundContest(0);
vm.stopPrank();
// Only 2 players claim
vm.prank(manyPlayers);
Pot(pot).claimCut();
vm.prank(manyPlayers); [ppl-ai-file-upload.s3.amazonaws](https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/images/50523984/449e3f4d-9fad-462b-8c42-b08784ab76b6/image.jpg)
Pot(pot).claimCut();
vm.warp(block.timestamp + 91 days);
vm.prank(owner);
manager.closeContest(pot);
// Check how much is stuck
uint256 stuck = token.balanceOf(pot);
console.log("Tokens stuck:", stuck); // ~58 tokens stuck
assertGt(stuck, 50); // More than 50% of remaining funds lost
}

Recommended Mitigation:

function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
+ if (claimants.length == 0) {
+ // No claimants, send all remaining to manager
+ i_token.transfer(msg.sender, remainingRewards - managerCut);
+ } else {
- uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
+ uint256 claimantCut = (remainingRewards - managerCut) / claimants.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
+ }
}
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 10 days 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!