MyCut

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

No validation that sum(rewards[]) == totalRewards in constructor

Root + Impact

Description

  • The constructor accepts rewards[] and totalRewards as independent parameters without verifying that their sum matches.

  • A misconfigured Pot can result in players unable to claim (their mapping entry is 0) or the Pot running out of tokens mid-distribution.

// No check: rewards[0] + rewards[1] + ... == totalRewards
for (uint256 i = 0; i < i_players.length; i++) {
playersToRewards[i_players[i]] = i_rewards[i];
}

Risk

Likelihood:

  • createContest is onlyOwner, so only the trusted admin can create a misconfigured Pot.

  • A mismatch is most likely to arise from an operator scripting error: a typo, an off-by-one, or a unit mismatch (e.g., passing reward amounts in USDC units while totalRewards is in wei).

  • Low likelihood in a well-operated deployment, but an on-chain guard is cheap and eliminates a class of silent misconfiguration bugs.

Proof of Concept

The following test shows a Pot with sum(rewards) < totalRewards is created and funded with no revert. Two hundred ether remain in the Pot, unallocated and unclaimable by any registered player — silently distorting the distribution at closePot. Run with forge test --match-test testL02 -vvv:

function testL2_MismatchedRewardsAndTotalRewards() public {
// Under-allocation: sum(rewards) = 800 ether, but totalRewards = 1000 ether.
// 200 ether are funded into the Pot but assigned to no player.
address[] memory players = new address[](2);
players[0] = player1;
players[1] = player2;
uint256[] memory rewards = new uint256[](2);
rewards[0] = 400 ether; // sum = 800 ether
rewards[1] = 400 ether;
uint256 customTotal = 1000 ether; // 200 ether unallocated — mismatch!
vm.startPrank(owner);
weth.mint(owner, customTotal);
weth.approve(address(conMan), customTotal);
// No revert — the mismatch is not caught on-chain
address contestAddr = conMan.createContest(players, rewards, IERC20(address(weth)), customTotal);
conMan.fundContest(0);
vm.stopPrank();
// Both players claim their (under-allocated) 400 ether each
vm.prank(player1); Pot(contestAddr).claimCut();
vm.prank(player2); Pot(contestAddr).claimCut();
// 200 ether remain that no player can directly claim —
// the protocol treats this as "unclaimed rewards" to redistribute at close
assertEq(Pot(contestAddr).getRemainingRewards(), 200 ether, "200 ether orphaned");
assertEq(weth.balanceOf(contestAddr), 200 ether, "Pot holds unallocated tokens");
console.log("--- L-02 Results ---");
console.log("Orphaned tokens (ether):", weth.balanceOf(contestAddr) / 1 ether);
console.log("No on-chain error raised despite rewards mismatch.");
}

Recommended Mitigation

Add a sum check in the constructor and declare a dedicated custom error:

+ error Pot__RewardsMismatch();
constructor(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards) {
+ uint256 rewardSum;
+ for (uint256 i = 0; i < players.length; i++) {
+ rewardSum += rewards[i];
+ }
+ if (rewardSum != totalRewards) revert Pot__RewardsMismatch();
+
i_players = players;
i_rewards = rewards;
// ... rest of constructor
}
Updates

Lead Judging Commences

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