MyCut

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

M-02 Double Spend Vulnerability in closePot()

Root + Impact:

Description:

The closePot() function does not update the remainingRewards state variable after distributing rewards to claimants. This allows the owner to call closePot() multiple times, each time taking an additional manager cut and redistributing (or attempting to redistribute) the remaining rewards.

Since remainingRewards is never set to zero after distribution, and the check if (remainingRewards > 0) will pass on subsequent calls (if any tokens were sent back or if the initial distribution didn't zero it out), the function can be exploited for 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);
}
}
@> }

Additionally, if the contract receives any tokens after the first closePot() call (e.g., via direct transfer), subsequent calls will drain those as well.

Risk:

Likelihood: High - The owner can call this function multiple times; no state prevents re-execution.

Impact:

  • Owner can repeatedly take manager cuts from the same pot

  • Protocol economic invariant broken: manager gets more than the intended percentage

  • Claimants may receive duplicate payments (if tokens are available)

Proof of Concept:

This POC demonstrates that closePot() does not update remainingRewards to zero after distribution, allowing the owner to call it multiple times and extract additional manager cuts.

function testValidateImpact() public {
uint256 ownerBalanceBefore = token.balanceOf(owner);
// Call closePot once
vm.prank(owner);
pot.closePot();
// The bug: remainingRewards is not set to 0
// If we can call closePot again (e.g., contract received more tokens),
// owner takes another manager cut
// For this test, let's send more tokens to the pot and call closePot again
uint256 extraTokens = 500 ether;
token.mint(address(pot), extraTokens);
// Now closePot can be called again because remainingRewards > 0 check may pass
// (depending on whether the first call updated remainingRewards)
// Since remainingRewards is NOT updated in closePot(),
// the second call will also distribute (wrongly) again
vm.prank(owner);
pot.closePot();
uint256 ownerBalanceAfter = token.balanceOf(owner);
uint256 totalManagerCut = ownerBalanceAfter - ownerBalanceBefore;
console.log("Total manager cut extracted:", totalManagerCut);
console.log("Intended manager cut (10% of 1000):", uint256(100 ether));
// Owner got more than the intended 10%
assertGt(totalManagerCut, 100 ether);
console.log("BUG: Owner extracted more than 10% manager cut!");
}

Recommended Mitigation:

Setting remainingRewards = 0 after distribution ensures the owner cannot call closePot() multiple times to extract additional 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) / claimants.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 7 days 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!