MyCut

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

## H-3 — `closePot` is not idempotent: manager cut and payouts are charged again on every call

Root + Impact

closePot reads remainingRewards, pays the manager cut and each claimant, but never sets remainingRewards = 0 and has no closed flag. Every subsequent call re-derives the same cut from the same remainingRewards and pays claimants again until the pot balance is exhausted.

Description

// src/Pot.sol
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);
}
}
// @> no closed flag; remainingRewards never reset -> callable again and again
}

Risk

Likelihood: Trivially reached by calling closeContest twice.

Impact: Repeated manager cuts and repeated claimant payouts drain the pot and over-distribute; stale remainingRewards misreports balances.

Proof of Concept

// test/Exploits.t.sol
function testCloseRepeatableDoubleChargesManagerCut() public {
address[] memory p = new address[](1);
p[0] = alice;
uint256[] memory r = new uint256[](1);
r[0] = 100e18;
vm.prank(owner);
Pot pot = Pot(manager.createContest(p, r, IERC20(address(token)), 100e18));
vm.prank(owner);
manager.fundContest(0);
​
vm.warp(block.timestamp + 91 days);
vm.prank(owner);
manager.closeContest(address(pot));
uint256 afterFirst = token.balanceOf(address(manager)); // 10
​
vm.prank(owner);
manager.closeContest(address(pot));
uint256 afterSecond = token.balanceOf(address(manager)); // 20
​
assertEq(afterFirst, 10e18);
assertEq(afterSecond, 20e18, "manager cut charged twice from same remainingRewards");
assertEq(pot.getRemainingRewards(), 100e18, "remainingRewards never reconciled");
}

Recommended Mitigation

+ bool private i_closed;
​
function closePot() external onlyOwner {
+ if (i_closed) revert Pot__AlreadyClosed();
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
+ i_closed = true;
if (remainingRewards > 0) {
...
}
+ remainingRewards = 0;
}

Updates

Lead Judging Commences

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