MyCut

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

createContest never validates players.length > 0 - a zero-player pot causes closePot to divide by zero, permanently locking the entire funded amount

Description

ContestManager.createContest never validates that players.length > 0 before deploying a Pot:

function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public onlyOwner returns (address)
{
Pot pot = new Pot(players, rewards, token, totalRewards); // no length check
...
}

If a pot is created with an empty players array but a nonzero totalRewards (and funded via fundContest), i_players.length is permanently 0 (it's set once in the constructor with no setter). Nobody can ever call claimCut() successfully (there is no player whose address maps to a nonzero reward), so remainingRewards stays at the full totalRewards forever. When closePot() is eventually called:

uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
@> uint256 claimantCut = (remainingRewards - managerCut) / i_players.length; // divide by 0

i_players.length is 0, so this line panics with a division-by-zero error. The panic unwinds the entire call, including the managerCut transfer executed on the line immediately above — Solidity reverts undo all state changes and value transfers made earlier in the same call. closePot() can never succeed for this pot (i_players.length is immutable), so this isn't a one-time revert to route around — it's permanent.

Risk

Likelihood: Low-Medium

  • Requires an admin to create a pot with an empty players array while still funding it with a nonzero amount — a straightforward input-validation gap, not a deliberate multi-step setup.

Impact: Critical

  • Total, unconditional, and permanent loss of the entire totalRewards funded into the pot. Unlike every other finding in this report, here nobody receives anything — not the claimants (there are none), and not even the manager, since the panic reverts their cut too. There is no recovery function anywhere in scope.

Proof of Concept

A pot funded with 1000 tokens and zero players can never be closed; the funds are provably stuck:

function test_H6_zeroPlayers_divisionByZero_locksEntirePot() public {
address contest = ContestManager(conMan).createContest(new address[](0), new uint256[](0), IERC20(weth), 1000);
ContestManager(conMan).fundContest(0);
assertEq(weth.balanceOf(contest), 1000);
vm.warp(91 days);
vm.expectRevert(stdError.divisionError); // panic: division by zero
ContestManager(conMan).closeContest(contest);
assertEq(weth.balanceOf(contest), 1000); // still stuck -- closePot() can never succeed
}

Recommended Mitigation

Validate at creation, where the invalid state actually originates:

function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public onlyOwner returns (address)
{
+ if (players.length == 0) revert ContestManager__NoPlayers();
Pot pot = new Pot(players, rewards, token, totalRewards);
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!