MyCut

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

createContest accepts an empty players array, and closePot then divides by i_players.length == 0, reverting with Panic(0x12) on every close so the fully-funded pot is permanently frozen

Root + Impact

Description

  • Normally, every pot has at least one registered winner; createContest should refuse degenerate configurations outright.

  • With players = [] the constructor loop maps nothing, every claimCut reverts Pot__RewardNotFound, and after 90 days every closeContest computes claimantCut = (remainingRewards - managerCut) / 0 — Panic(0x12). The panic reverts the whole transaction including the manager-cut transfer, so nothing ever leaves the pot. There is no sweep, so the funds are unrecoverable, and operators see a raw panic instead of a domain error.

// src/ContestManager.sol
function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public onlyOwner returns (address)
{
...
@> Pot newPot = new Pot(players, rewards, token, totalRewards); // players.length == 0 accepted
// src/Pot.sol
function closePot() external onlyOwner {
...
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
@> uint256 claimantCut = (remainingRewards - managerCut) / i_players.length; // /0 -> Panic(0x12)

Risk

Likelihood:

  • Deploying a contest before the winners list is assembled (or a mis-pasted empty array from tooling) sails through creation and funding without any warning; the failure only appears 90 days later at close time.

Impact:

  • Verified: 1000 WETH funded into an empty-players pot can never be retrieved — the claim path is structurally impossible and the close path panics forever, a 100% loss of the funding.

  • The failure mode is a bare Panic(0x12) rather than a descriptive revert, which turns an operational mistake into a hard-to-diagnose frozen contract.

Proof of Concept

PoC explanation: The test walks a fully-funded, empty-players pot through its only two exits and shows both are bricked. Setup: the admin creates a contest with players = [] and rewards = [] — createContest accepts it without any emptiness check — and funds it with 1000 WETH. Step 1 (claim path): any caller's claimCut reverts with Pot__RewardNotFound, asserted via the custom-error selector — no address was ever mapped, so the money is structurally unclaimable. Step 2 (close path): after vm.warp(91 days), closeContest computes claimantCut = (remainingRewards - managerCut) / i_players.length = (1000 - 100) / 0 and aborts with Panic(0x12), asserted via stdError.divisionError — because the panic reverts the whole transaction, even the managerCut transfer inside the same call never lands. Final assertion: the pot still holds all 1000 WETH, a 100% loss surfaced only as a raw panic with no domain error to guide operators. Run with: forge test --match-test test_POC9_EmptyPlayers_DivByZeroAllLocked -vv (PASS; asserts both reverts and the frozen 1000).

// forge test --match-test test_POC9_EmptyPlayers_DivByZeroAllLocked -> PASS
function test_POC9_EmptyPlayers_DivByZeroAllLocked() public {
address[] memory players = new address[](0);
uint256[] memory rewards = new uint256[](0);
address pot = _createAndFund(players, rewards, 1000);
​
vm.prank(p1);
vm.expectRevert(Pot.Pot__RewardNotFound.selector);
Pot(pot).claimCut(); // nobody can ever claim
​
vm.warp(91 days);
vm.prank(admin);
vm.expectRevert(stdError.divisionError); // Panic(0x12)
conMan.closeContest(pot); // close is permanently bricked
​
assertEq(weth.balanceOf(pot), 1000, "100% funds locked forever");
}

Recommended Mitigation

// src/ContestManager.sol — createContest
+ if (players.length == 0) revert ContestManager__EmptyPlayers();
+ if (players.length != rewards.length) revert ContestManager__LengthMismatch();
Pot newPot = new Pot(players, rewards, token, totalRewards);

⚠ Combination note: the sum(rewards) == totalRewards validation of V7 subsumes this check for any positive budget (sum of an empty array is 0 != totalRewards) — implement the full entrance validation once rather than patching the panic site.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 34 minutes 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!