MyCut

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

Owner can claim pot manager cut unlimited times

closePot() fails to update rewards to 0 at the end , which allows the owner to claim pot manager commission or cut unlimited times.

Description

  • closePot() is designed to distribute remaining rewards and close the pot after 90 days. The function should set remainingRewards to 0 after distribution to mark the pot as closed.

  • The function fails to update remainingRewards to 0, allowing the owner to call closePot() repeatedly and never truly close the pot.

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);
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
// @> remainingRewards is never set to 0
}
}

Risk

Likelihood: High

Impact: Medium (Owner privilege needed)

Proof of Concept


File: mytest.t.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Pot.sol";
import "../src/ContestManager.sol";
import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract MockToken is ERC20 {
constructor() ERC20("Mock", "MOCK") {
_mint(msg.sender, 10000 * 10**18);
}
}
contract PotCloseTest is Test {
ContestManager public contestManager;
MockToken public token;
address public owner = address(0x1);
address[] public players;
uint256[] public rewards;
uint256 public totalRewards = 1000 * 10**18;
Pot public pot;
function setUp() public {
vm.startPrank(owner);
token = new MockToken();
contestManager = new ContestManager();
players = [address(0x2), address(0x3), address(0x4)];
rewards = [300 * 10**18, 300 * 10**18, 400 * 10**18];
address potAddress = contestManager.createContest(players, rewards, IERC20(address(token)), totalRewards);
token.approve(address(contestManager), totalRewards);
contestManager.fundContest(0);
pot = Pot(potAddress);
vm.stopPrank();
}
function test_closePotNeverCloses() public {
vm.warp(block.timestamp + 91 days);
// The owner of Pot is ContestManager, so we need to call through ContestManager
vm.startPrank(owner);
uint256 remainingBefore = pot.getRemainingRewards();
// Call closeContest on ContestManager, which will call closePot on Pot
contestManager.closeContest(address(pot));
uint256 remainingAfter = pot.getRemainingRewards();
assertEq(remainingBefore, remainingAfter, "remainingRewards should be 0 but wasn't changed");
contestManager.closeContest(address(pot));
uint256 remainingAfterSecond = pot.getRemainingRewards();
assertEq(remainingBefore, remainingAfterSecond, "remainingRewards still not 0 after second call");
vm.stopPrank();
}
}

forge test --match-path test/mytest.t.sol

Ran 1 test for test/mytest.t.sol:PotCloseTest
[PASS] test_closePotNeverCloses() (gas: 77130)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.15ms (134.71µs CPU time)
Ran 1 test suite in 7.26ms (1.15ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

remainingRewards = 0; ensures the pot closes permanently after distribution. Without it, remainingRewards retains its value, allowing the owner to call closePot() repeatedly and drain the pot through repeated manager cuts.

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);
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut);
}
+ remainingRewards = 0;
}
}
Updates

Lead Judging Commences

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