MyCut

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

Missing rewards[] / totalRewards Invariant in Constructor — Players Unable to Claim

Missing rewards[] / totalRewards Invariant in Constructor — Players Unable to Claim

Description

The Pot contract expects the deployer to provide a totalRewards parameter that matches the sum of individual rewards[] entries. The totalRewards value determines how many ERC20 tokens are transferred into the Pot during ContestManager.fundContest(), while rewards[] determines what each player can claim via claimCut(). These two values must be equal for the protocol to function correctly.

The Pot constructor stores both values without validating that sum(rewards[]) == totalRewards. Additionally, there is no duplicate address check in players[], which silently overwrites earlier reward entries. This allows contests to be deployed in broken states where claimants are either denied service (under-funded) or excess tokens are locked (over-funded).

// Pot.sol L22-34
constructor(
address[] memory players,
uint256[] memory rewards,
IERC20 token,
uint256 totalRewards
) {
i_players = players;
i_rewards = rewards;
i_token = token;
@> i_totalRewards = totalRewards; // No validation against sum(rewards)
@> remainingRewards = totalRewards; // Set from parameter, not computed sum
i_deployedAt = block.timestamp;
for (uint256 i = 0; i < i_players.length; i++) {
@> playersToRewards[i_players[i]] = i_rewards[i]; // Overwrites on duplicate
}
}

Risk

Likelihood: High

  • The constructor is called on every contest creation. There is no on-chain or off-chain validation layer between the admin's input and the contract deployment.

  • A simple arithmetic mistake in the admin's reward allocation array creates an irrecoverable mismatch.

Impact: High

  • Under-funding: later claimants' claimCut() calls revert due to insufficient ERC20 balance, causing denial of service.

  • Over-funding: excess tokens beyond sum(rewards[]) are permanently locked with no sweep function.

  • Duplicate players: the mapping overwrites the first reward value, effectively stealing that allocation.

Severity: High

Proof of Concept

An admin creates a contest with totalRewards = 500 but provides rewards = [200, 200, 200] (sum = 600). Only 500 tokens are transferred during fundContest(). The first two players claim 200 each (400 used). The third player calls claimCut() but the Pot only has 100 tokens remaining — the ERC20 transfer reverts, permanently denying the third player their reward.

function test_H02_rewards_mismatch() public {
address[] memory players = new address[](3);
uint256[] memory rewards = new uint256[](3);
players[0] = address(0x1); rewards[0] = 200;
players[1] = address(0x2); rewards[1] = 200;
players[2] = address(0x3); rewards[2] = 200;
// totalRewards = 500 but sum(rewards) = 600
Pot pot = new Pot(players, rewards, token, 500);
token.transfer(address(pot), 500);
// First two claim successfully
vm.prank(address(0x1)); pot.claimCut();
vm.prank(address(0x2)); pot.claimCut();
// Third player reverts — insufficient balance
vm.prank(address(0x3));
vm.expectRevert();
pot.claimCut();
}

Recommended Mitigation

Adding constructor validation ensures the invariant sum(rewards[]) == totalRewards holds at deployment. The duplicate check prevents silent overwrites that break accounting.

constructor(...) {
require(players.length == rewards.length, "Length mismatch");
uint256 computedTotal;
for (uint256 i = 0; i < players.length; i++) {
+ require(playersToRewards[players[i]] == 0, "Duplicate player");
playersToRewards[players[i]] = rewards[i];
computedTotal += rewards[i];
}
+ require(computedTotal == totalRewards, "Rewards mismatch");
// ... rest of constructor
}
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!