MyCut

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

H-04: Missing Input Validation in createContest() - Array Out-of-Bounds and Division by Zero

Root + Impact

Root Cause
In ContestManager.sol lines 26-36, the createContest() function lacks critical input validation, allowing creation of broken pots:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract ContestManager is Ownable {
// ... state variables ...
function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public
onlyOwner
returns (address)
{
// @> ROOT CAUSE: NO VALIDATION
// - players.length == rewards.length
// - players.length > 0
// - players[i] != address(0)
// - totalRewards == sum(rewards)
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}
}

Impact

  1. Mismatched Arrays → Array Out-of-Bounds Panic: If players.length != rewards.length, Pot constructor panics

  2. Zero Players → Division by Zero: If players.length == 0, closePot() divides by zero

  3. Zero Addresses → Locked Funds: address(0) gets rewards but can never claim

  4. Reward Sum Mismatch → Accounting Errors: totalRewards != sum(rewards) breaks invariants


Description

The createContest() function lacks critical input validation, allowing creation of broken pots that either:

  1. Revert on deployment (array out-of-bounds panic)

  2. Create pots with zero players (division by zero in closePot)

  3. Mismatched reward allocations (players without rewards)

// Lines 26-36 in ContestManager.sol - NO VALIDATION
function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public
onlyOwner
returns (address)
{
// NO CHECKS:
// - players.length == rewards.length
// - players.length > 0
// - players[i] != address(0)
// - totalRewards == sum(rewards)
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}

Risk

Likelihood:

  1. Mismatched Arrays → Array Out-of-Bounds Panic: If players.length != rewards.length, Pot constructor panics

  2. Zero Players → Division by Zero: If players.length == 0, closePot() divides by zero

  3. Zero Addresses → Locked Funds: address(0) gets rewards but can never claim

  4. Reward Sum Mismatch → Accounting Errors: totalRewards != sum(rewards) breaks invariants
    Impact:
    Impact Assessment
    | Dimension | Assessment |
    | ---------------------- | ----------------------------------------------------- |
    | Deployment Failure | Mismatched arrays cause panic on creation |
    | Fund Lock | Zero-player pots cannot be closed (division by zero) |
    | Silent Bugs | Zero addresses in players array = unclaimable rewards |
    | Accounting Errors | totalRewards != sum(rewards) breaks invariants |


Proof of Concept

Test 1: Mismatched Arrays → Array Out-of-Bounds

function test_POC_MissingValidation_MismatchedArrays() public {
address[] memory players = new address[](3);
players[0] = player1;
players[1] = player2;
players[2] = player3;
uint256[] memory rewards = new uint256[](2); // Only 2 rewards!
rewards[0] = 100;
rewards[1] = 100;
vm.startPrank(user);
// This should revert but doesn't - creates broken pot
potAddr = conMan.createContest(players, rewards, IERC20(weth), 200);
// REVERTS HERE: panic: array out-of-bounds access (0x32)
vm.stopPrank();
}

Result: ✅ REVERTS with panic: array out-of-bounds access (0x32)

[FAIL: panic: array out-of-bounds access (0x32)] test_POC_MissingValidation_MismatchedArrays() (gas: 315230)

Test 2: Zero Players → Division by Zero in closePot

function test_POC_MissingValidation_ZeroPlayers() public {
address[] memory players = new address[](0);
uint256[] memory rewards = new uint256[](0);
vm.startPrank(user);
// Creates pot with no players
potAddr = conMan.createContest(players, rewards, IERC20(weth), 0);
pot = Pot(potAddr);
conMan.fundContest(0);
vm.stopPrank();
vm.warp(91 days);
vm.startPrank(user);
// closePot with 0 players = division by zero
vm.expectRevert(); // Division by zero
conMan.closeContest(potAddr);
vm.stopPrank();
}

Result: closePot doesn't revert because remainingRewards = 0 so loop skipped. But if funded > 0, division by zero occurs.


Recommended Mitigation

Factory Level Validation (ContestManager.sol):

  • Verifies that the players and rewards arrays match in length and are not empty.

  • Iterates through arrays to reject zero addresses and zero-value rewards.

  • Ensures that the cumulative sum of individual rewards precisely matches the specified totalRewards.

// ContestManager.sol createContest()
function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public
onlyOwner
returns (address)
{
+ // Validate array lengths
+ require(players.length == rewards.length, "Length mismatch");
+ require(players.length > 0, "No players");
+
+ // Validate no zero addresses and sum rewards
+ uint256 sumRewards = 0;
+ for (uint256 i = 0; i < players.length; i++) {
+ require(players[i] != address(0), "Zero address");
+ require(rewards[i] > 0, "Zero reward");
+ sumRewards += rewards[i];
+ }
+ require(totalRewards == sumRewards, "Reward sum mismatch");
+
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}

Also add defense in depth to Pot.sol constructor:
Defense in Depth (Pot.sol Constructor):

  • Implements redundant safety checks directly inside the Pot contract constructor to independently enforce array length equality and non-empty constraints, securing the contract even if deployed or initialized outside the factory.

// Pot.sol constructor
constructor(...) {
+ require(players.length == rewards.length, "Length mismatch");
+ require(players.length > 0, "No players");
// ... rest
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours 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!