MyCut

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

No closed-state flag in closePot — pot can be re-closed after receiving any post-close token balance

Root + Impact

Description

  • closePot() enforces a 90-day window by checking block.timestamp - i_deployedAt < 90 days. Once 90 days have elapsed, this check is permanently true. The function executes the distribution and returns — but it sets no state flag indicating the pot has been closed.

  • After the first closePot call, remainingRewards is not zeroed and claimants is not cleared. If any tokens arrive at the contract address after closure (fee-on-transfer rebates, direct transfers, or intentional re-funding), a second closePot call will compute a non-zero claimantCut from the stale remainingRewards and re-distribute to all entries in claimants — including any addresses added post-close via claimCut.

function closePot() external onlyOwner {
// @> time check passes forever after 90 days — no isClosed guard
if (block.timestamp - i_deployedAt < 90 days) { revert Pot__StillOpenForClaim(); }
// @> remainingRewards and claimants not reset after distribution
uint256 managerCut = remainingRewards / managerCutPercent;
// ...
}

Risk

Likelihood:

  • A second closePot call with a zero-balance pot is a no-op (transfers of 0 succeed or claimantCut is 0), so routine operation does not trigger harm without an explicit re-funding event.

  • The scenario requires either a direct token deposit to the pot address post-close, or a fee-on-transfer token that sends rebates back — both are uncommon but not impossible.

Impact:

  • Double-distribution pays all entries in claimants (including post-close claimants) from the new balance. The pot has no on-chain finality — its closure is not reflected in any state variable, making it impossible to reason about whether a pot is open or closed without off-chain tracking.

  • Addresses that called claimCut after the pot was supposed to be closed would incorrectly receive a distribution bonus.

Proof of Concept

Place this test in test/ and run forge test --match-test testPotCanBeClosedTwice. The test demonstrates that closePot() can be called multiple times because there is no closed-state guard, potentially re-executing the manager cut transfer and emitting duplicate events.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {Pot} from "src/Pot.sol";
import {MockERC20} from "test/mocks/MockERC20.sol";
contract L10Test is Test {
MockERC20 token;
Pot pot;
address manager = address(0x1);
address player1 = address(0x2);
function setUp() public {
token = new MockERC20();
address[] memory players = new address[](1);
players[0] = player1;
uint256[] memory rewards = new uint256[](1);
rewards[0] = 100e18;
token.mint(manager, 200e18);
vm.prank(manager);
pot = new Pot(players, rewards, token, 100e18);
vm.prank(manager);
token.transfer(address(pot), 100e18);
}
function testPotCanBeClosedTwice() public {
vm.prank(player1); pot.claimCut();
vm.warp(block.timestamp + 91 days);
vm.prank(manager); pot.closePot(); // first close
// Re-fund the pot after close
vm.prank(manager);
token.transfer(address(pot), 100e18);
// Second close succeeds — no isClosed guard
vm.prank(manager);
pot.closePot(); // second close distributes again
}
}

Recommended Mitigation

Add a bool private isClosed flag and require !isClosed at the top of closePot(), setting it to true on the first successful execution to prevent re-entry. Setting remainingRewards = 0 alongside the isClosed flag ensures that any re-invocation of closePot() cannot distribute a non-zero balance even if the flag check is somehow bypassed.

+ bool private isClosed;
function closePot() external onlyOwner {
+ require(!isClosed, "Already closed");
if (block.timestamp - i_deployedAt < 90 days) { revert Pot__StillOpenForClaim(); }
+ isClosed = true;
+ remainingRewards = 0;
// ... distribution logic
}
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!