MyCut

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

Array Length Mismatch in `createContest()` Causes DoS or Silent Fund Misallocation

Description

The createContest() function in ContestManager accepts players and rewards arrays and passes them directly to the Pot constructor without validating that players.length == rewards.length.

Inside the Pot constructor, a loop iterates over i_players.length and accesses i_rewards[i] at each index. When the arrays have mismatched lengths, two failure modes exist.

// src/Pot.sol:32-34 — @> no length check before this loop
@> for (uint256 i = 0; i < i_players.length; i++) {
@> playersToRewards[i_players[i]] = i_rewards[i]; // @> panics if i >= rewards.length
}

When rewards.length < players.length, the constructor panics with an array out-of-bounds access, bricking the entire pot creation. When rewards.length > players.length, excess reward entries are silently ignored, but totalRewards can be set to include them, causing the pot to be overfunded with funds that no player can ever claim via claimCut().

// src/ContestManager.sol:22 — @> passes arrays without validation
Pot pot = new Pot(players, rewards, token, totalRewards);

Risk

Likelihood:

  • The owner calls createContest() with accidentally mismatched array lengths (e.g., building arrays programmatically where one list is filtered differently than the other)

  • A front-running or MEV scenario where an attacker observes a pending createContest() transaction and uses a governance or oracle manipulation to influence array inputs

Impact:

  • DoS (rewards.length < players.length): Pot creation reverts permanently, blocking the owner from launching the contest. Funds minted for the contest are stranded until the owner can re-submit with corrected arrays.

  • Fund misallocation (rewards.length > players.length): Excess rewards silently ignored. Owner funds the pot with totalRewards including the phantom entries, but only a subset of players can claim. The excess is locked until closePot() — if it executes.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {ContestManager} from "../../src/ContestManager.sol";
import {Pot} from "../../src/Pot.sol";
import {Test} from "lib/forge-std/src/Test.sol";
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
import {ERC20Mock} from "../ERC20Mock.sol";
/// @title PoC: Array Length Mismatch in createContest()
contract PocArrayMismatch is Test {
ContestManager public cm;
ERC20Mock public token;
address public owner = makeAddr("owner");
event Result(string message, uint256 value);
function setUp() public {
vm.startPrank(owner);
cm = new ContestManager();
token = new ERC20Mock("WETH", "WETH", owner, 100_000_000 ether);
token.approve(address(cm), type(uint256).max);
vm.stopPrank();
}
/// @notice Scenario 1: fewer rewards than players → Pot constructor panics with array OOB
function test_PoC_FewerRewardsReverts() public {
address[] memory players = new address[](4);
uint256[] memory rewards = new uint256[](2);
for (uint256 i = 0; i < 4; i++) {
players[i] = makeAddr(string(abi.encodePacked("p", vm.toString(i))));
}
rewards[0] = 1 ether;
rewards[1] = 1 ether;
vm.startPrank(owner);
token.mint(owner, 2 ether);
try cm.createContest(players, rewards, IERC20(address(token)), 2 ether) {
revert("Should have reverted");
} catch {
emit Result("DoS: Pot creation reverts with mismatched arrays", 1);
}
vm.stopPrank();
}
/// @notice Scenario 2: more rewards than players → 3 ether excess silently locked
function test_PoC_MoreRewardsExcessLocked() public {
address[] memory players = new address[](2);
players[0] = makeAddr("alice");
players[1] = makeAddr("bob");
uint256[] memory rewards = new uint256[](5);
for (uint256 i = 0; i < 5; i++) {
rewards[i] = 1 ether;
}
uint256 totalRewards = 5 ether;
vm.startPrank(owner);
token.mint(owner, totalRewards);
address potAddr = cm.createContest(players, rewards, IERC20(address(token)), totalRewards);
cm.fundContest(0);
vm.stopPrank();
vm.prank(makeAddr("alice"));
Pot(potAddr).claimCut();
vm.prank(makeAddr("bob"));
Pot(potAddr).claimCut();
assertEq(token.balanceOf(makeAddr("alice")), 1 ether, "Alice claimed 1 ether");
assertEq(token.balanceOf(makeAddr("bob")), 1 ether, "Bob claimed 1 ether");
assertEq(Pot(potAddr).getRemainingRewards(), 3 ether, "3 ether excess from mismatched array");
}
}

Recommended Mitigation

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