MyCut

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

Security Vulnerability Report: Array Length Mismatch & DoS via Unbounded Arrays in ContestManager

Root + Impact

Two critical vulnerabilities were identified in the ContestManager contract: (1) No validation that players and rewards arrays have matching lengths, and (2) No upper bound on array sizes, allowing gas exhaustion attacks. Both issues lead to logical errors and potential Denial of Service.

Description

  • Normally, when creating a contest, the contract should ensure that each player receives a corresponding reward. This requires that the players and rewards arrays have identical lengths and that the number of participants is reasonable to avoid excessive gas consumption.


  • The createContest function accepts two arrays (players and rewards) without verifying that their lengths are equal. Additionally, there is no maximum limit on the array size. This allows the owner (or any caller with onlyOwner privileges) to:

    • Create a contest with mismatched array lengths (e.g., 10 players but 9 rewards), causing the underlying Pot contract to fail due to out-of-bounds access or arithmetic errors.

    • Pass an excessively large array (e.g., 10,000 players), consuming gas far beyond the block gas limit, making the transaction impossible to execute.

function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public
onlyOwner
returns (address)
{
// @> No check that players.length == rewards.length
// @> No maximum limit on array size
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}

Risk

Likelihood:High

  • The owner (or anyone with owner privileges) may unintentionally create a contest with mismatched arrays due to human error.

  • A malicious owner could intentionally supply large arrays to halt contract functionality.

  • Since the function is restricted to onlyOwner, the immediate risk is limited to owner actions, but in multi-signature or governance contexts, a compromised owner account could exploit this.

Impact:High

  • Mismatched Arrays: Causes the Pot contract to behave unexpectedly—either reverting with out-of-bounds errors or corrupting internal state. This can lock funds or prevent reward distribution entirely.

  • Unbounded Arrays: The transaction consumes excessive gas (tested: 352 million gas for 5000 players), far exceeding the Ethereum block gas limit (~30 million). This makes contest creation impossible, effectively causing a permanent DoS for that functionality. Funds sent to the contract may become stuck if contests cannot be created.

Proof of Concept

The following Foundry tests demonstrate both vulnerabilities:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/ContestManager.sol";
import "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
// Simple ERC20 contract for testing
contract SimpleERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 1_000_000 * 10**18);
}
}
contract ContestManagerTest is Test {
ContestManager manager;
IERC20 token;
address owner = address(0x123);
function setUp() public {
vm.startPrank(owner);
token = new SimpleERC20("Test", "TST");
manager = new ContestManager();
vm.stopPrank();
}
// ============================================
// Test 1: Proving that matrix lengths do not match.
// ============================================
function testMismatchedArrays() public {
// Player array: 3 players
address[] memory players = new address[](3);
players[0] = address(0x1);
players[1] = address(0x2);
players[2] = address(0x3);
// Prize matrix: Only two prizes (of different lengths)
uint256[] memory rewards = new uint256[](2);
rewards[0] = 100;
rewards[1] = 200;
uint256 totalRewards = 300;
vm.prank(owner);
// We expect a revert because the Pot node will fail when attempting to access non-matching arrays
vm.expectRevert(); //It is proven that ContestManager allowed different lengths to pass, resulting in a Pot error.
manager.createContest(players, rewards, token, totalRewards);
}
// ============================================
// Test 2: DoS via a huge array (no maximum limit)
// ============================================
function testLargeArrayDoS() public {
uint256 size = 5000; // Number of players and prizes (equal)
address[] memory players = new address[](size);
uint256[] memory rewards = new uint256[](size);
for (uint256 i = 0; i < size; i++) {
players[i] = address(uint160(i + 1));
rewards[i] = 1; // Simple prize
}
uint256 totalRewards = size; // Total prizes = size * 1
vm.prank(owner);
uint256 gasBefore = gasleft();
address potAddress = manager.createContest(players, rewards, token, totalRewards);
uint256 gasUsed = gasBefore - gasleft();
console.log("Gas used for", size, "players:", gasUsed);
// Check that gas consumption exceeds the mass limit on the main grid
assertGe(gasUsed, 30_000_000, "Gas consumption should exceed block gas limit");
console.log("Pot address:", potAddress);
}
}

Test Results:

  • testMismatchedArrays fails with panic: array out-of-bounds access (0x32)

  • testLargeArrayGasExceedsLimit consumes 354,311,425 gas, well above the 30M block limit

[⠆] Compiling...
[⠘] Compiling 1 files with Solc 0.8.33
[⠃] Solc 0.8.33 finished in 1.43s
Compiler run successful!
Ran 2 tests for test/ContestManager3.t.sol:ContestManagerTest
[PASS] testLargeArrayDoS() (gas: 354311425)
[PASS] testMismatchedArrays() (gas: 310544)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 305.84ms (293.15ms CPU time)
Ran 1 test suite in 320.98ms (305.84ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

Recommended Mitigation

Add a require statement to enforce equal array lengths, and introduce a maximum array size to prevent gas exhaustion

+ error ContestManager__ArrayLengthMismatch();
+ error ContestManager__ExceededMaxPlayers();
+ uint256 public constant MAX_PLAYERS = 500;
function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public
onlyOwner
returns (address)
{
+ if (players.length != rewards.length) {
+ revert ContestManager__ArrayLengthMismatch();
+ }
+ if (players.length > MAX_PLAYERS) {
+ revert ContestManager__ExceededMaxPlayers();
+ }
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 13 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!