MyCut

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

createContest does not validate players and rewards array length match

Root + Impact

Description

  • createContest() passes players[] and rewards[] arrays directly to the Pot constructor without verifying they have equal lengths.

  • If rewards.length < players.length, the constructor loop will revert with an out-of-bounds index at i_rewards[i], wasting gas. If rewards.length > players.length, extra rewards are silently ignored but still counted in totalRewards, locking excess tokens permanently.

// ContestManager.sol::createContest()
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 validation
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}

--- Risk ---

Likelihood:

  • The owner constructs arrays externally (frontend, script). A mismatch is a common mistake, especially when arrays are built dynamically.

  • No on-chain validation exists to catch this before deployment.

Impact:

  • If rewards is shorter: Pot deployment reverts, wasting gas but no state corruption.

  • If rewards is longer: Pot deploys successfully but extra reward values are not assigned to any player. The corresponding token amount is locked permanently in the Pot since remainingRewards includes the unassigned portion.

--- Proof of Concept ---

The following test creates a Pot with 2 players but 3 reward entries. The Pot deploys successfully, but the third reward (40 ether) is never assigned to any player. Those 40 ether are funded into the Pot but can never be claimed, permanently locking them.

function testArrayLengthMismatch() public {
address[] memory players = new address[](2);
uint256[] memory rewards = new uint256[](3);
players[0] = address(0x1); rewards[0] = 30 ether;
players[1] = address(0x2); rewards[1] = 30 ether;
rewards[2] = 40 ether; // extra — no player for this reward
// Pot deploys successfully, but 40 ether worth of rewards is unassigned
Pot pot = new Pot(players, rewards, token, 100 ether);
// Only 60 ether is claimable, 40 ether locked
}

--- Recommended Mitigation ---

Add an array length equality check in createContest() before passing the arrays to the Pot constructor. This prevents both the revert case (rewards shorter) and the silent fund lock case (rewards longer) at the earliest point possible.

function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public
onlyOwner
returns (address)
{
+ require(players.length == rewards.length, "Array length mismatch");
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}

Description

  • createContest() passes players[] and rewards[] arrays directly to the Pot constructor without verifying they have equal lengths.

  • If rewards.length < players.length, the constructor loop will revert with an out-of-bounds index at i_rewards[i], wasting gas. If rewards.length > players.length, extra rewards are silently ignored but still counted in totalRewards, locking excess tokens permanently.

// ContestManager.sol::createContest()
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 validation
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}

Risk

Likelihood:

  • The owner constructs arrays externally (frontend, script). A mismatch is a common mistake, especially when arrays are built dynamically.

  • No on-chain validation exists to catch this before deployment.

Impact:

  • If rewards is shorter: Pot deployment reverts, wasting gas but no state corruption.

  • If rewards is longer: Pot deploys successfully but extra reward values are not assigned to any player. The corresponding token amount is locked permanently in the Pot since remainingRewards includes the unassigned portion.

Proof of Concept

The following test creates a Pot with 2 players but 3 reward entries. The Pot deploys successfully, but the third reward (40 ether) is never assigned to any player. Those 40 ether are funded into the Pot but can never be claimed, permanently locking them.

function testArrayLengthMismatch() public {
address[] memory players = new address[](2);
uint256[] memory rewards = new uint256[](3);
players[0] = address(0x1); rewards[0] = 30 ether;
players[1] = address(0x2); rewards[1] = 30 ether;
rewards[2] = 40 ether; // extra — no player for this reward
// Pot deploys successfully, but 40 ether worth of rewards is unassigned
Pot pot = new Pot(players, rewards, token, 100 ether);
// Only 60 ether is claimable, 40 ether locked
}

Recommended Mitigation

Add an array length equality check in createContest() before passing the arrays to the Pot constructor. This prevents both the revert case (rewards shorter) and the silent fund lock case (rewards longer) at the earliest point possible.

function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public
onlyOwner
returns (address)
{
+ require(players.length == rewards.length, "Array length mismatch");
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 2 days 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!