MyCut

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

Pot.closePot() underpays claimants and locks funds by dividing by total players instead of total claimants

Root + Impact

Description

  • Normal behavior: After 90 days, the manager takes a cut of the remaining pool and the remainder is distributed equally to those who claimed in time (README.md (line 19)).

  • Issue: closePot() computes claimantCut using i_players.length but only pays it to claimants, so when not all players claim, claimants are underpaid and the leftover remains stuck in the Pot with no recovery path.

function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) revert Pot__StillOpenForClaim();
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
// ⊕ Wrong denominator: uses ALL players, not those who claimed
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
// ⊕ But payout loop only pays claimants
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
}
}

Risk

Likelihood:

  • Occurs when at least one eligible player claims and at least one eligible player does not claim before close.

  • Common contest behavior: not all players claim within the claim window.

Impact:

  • Claimants receive less than the “remainder distributed equally to those who claimed in time” promise.

  • ERC20 funds remain permanently stuck in the Pot (no sweep/withdraw), and can also make late claims revert due to insufficient pot balance.

Proof of Concept

Repro command
forge test --match-contract 'F001' --use ./audit/tooling/solc/solc-0.8.20 --offline -vvv

  • Test passes showing claimant gets 225 extra instead of 450, and 225 tokens remain locked in the pot.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Test} from "lib/forge-std/src/Test.sol";
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {Pot} from "../../src/Pot.sol";
import {ERC20Mock} from "../../test/ERC20Mock.sol";
contract F001WrongDenominatorDistribution is Test {
address private player1 = makeAddr("player1");
address private player2 = makeAddr("player2");
function test_F001_underpaysClaimants_and_locksFunds() public {
address[] memory players = new address[](2);
players[0] = player1;
players[1] = player2;
uint256[] memory rewards = new uint256[](2);
rewards[0] = 500;
rewards[1] = 500;
uint256 totalRewards = 1000;
ERC20Mock token = new ERC20Mock("TKN", "TKN", address(this), 0);
Pot pot = new Pot(players, rewards, IERC20(address(token)), totalRewards);
token.mint(address(this), totalRewards);
token.transfer(address(pot), totalRewards);
vm.prank(player1);
pot.claimCut();
assertEq(token.balanceOf(player1), rewards[0]);
vm.warp(block.timestamp + 91 days);
pot.closePot();
uint256 remainingRewards = totalRewards - rewards[0];
uint256 managerCut = remainingRewards / 10;
uint256 expectedExtraIfDistributedToClaimants = remainingRewards - managerCut;
uint256 actualExtraPaid = token.balanceOf(player1) - rewards[0];
assertEq(actualExtraPaid, 225);
assertEq(expectedExtraIfDistributedToClaimants, 450);
assertEq(token.balanceOf(address(pot)), expectedExtraIfDistributedToClaimants - actualExtraPaid);
vm.prank(player2);
vm.expectRevert();
pot.claimCut();
}
}

Recommended Mitigation

- uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
- for (uint256 i = 0; i < claimants.length; i++) {
- _transferReward(claimants[i], claimantCut);
- }
+ uint256 distributable = remainingRewards - managerCut;
+ if (claimants.length == 0) {
+ i_token.transfer(msg.sender, distributable);
+ } else {
+ uint256 claimantCut = distributable / claimants.length;
+ for (uint256 i = 0; i < claimants.length; i++) {
+ _transferReward(claimants[i], claimantCut);
+ }
+ uint256 remainder = distributable - (claimantCut * claimants.length);
+ if (remainder > 0) i_token.transfer(msg.sender, remainder);
+ }
+ remainingRewards = 0;
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 1 day 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!