MyCut

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

closePot push payment loop can be DoS'd by a single reverting or blacklisted claimant

Root + Impact

Description

  • closePot() uses a push payment pattern, iterating over all claimants and sending tokens to each one in a single transaction. If any single claimant address is blacklisted by the token contract (USDC, USDT) or is a smart contract that reverts on token receipt, the entire transaction reverts.

  • This blocks the manager cut and all other claimants from receiving their bonus.// Root cause in the codebase with @> marks to highlight the relevant section

// Pot.sol::closePot()
@> for (uint256 i = 0; i < claimants.length; i++) {
@> _transferReward(claimants[i], claimantCut); // one revert blocks ALL
}

Risk

Likelihood:

  • USDC and USDT both have blacklist functionality. A claimant who gets blacklisted after claiming (added to claimants[]) will cause closePot() to permanently revert.

  • A malicious player can deploy a contract that claims via claimCut() then intentionally reverts on any incoming token transfer, permanently DoS'ing closePot().

Impact:

  • closePot() becomes permanently uncallable — the manager cut and all claimant bonuses from leftover rewards are locked forever.

  • There is no alternative withdrawal or emergency function to recover funds.

  • The owner has no way to remove the problematic claimant from the array.

Proof of Concept

The following test deploys a malicious contract as one of the contest players. The malicious contract claims its reward (getting added to claimants[]), then sets a flag to revert on any future incoming token transfer. When the owner tries to call closePot(), the loop hits the malicious address, reverts, and permanently bricks the entire close operation.

contract MaliciousClaimant {
Pot pot;
IERC20 token;
bool blockTransfers = false;
constructor(Pot _pot) { pot = _pot; token = pot.getToken(); }
function claim() external {
pot.claimCut(); // adds this contract to claimants[]
blockTransfers = true;
}
// After claiming, revert on any incoming transfer
fallback() external {
if (blockTransfers) revert("DoS");
}
}
function testDoSClosePot() public {
// ... setup contest with MaliciousClaimant as one player ...
maliciousClaimant.claim();
vm.warp(block.timestamp + 91 days);
vm.expectRevert(); // MaliciousClaimant reverts on transfer
pot.closePot();
// closePot permanently bricked
}

Recommended Mitigation

Wrap each individual token transfer in a try/catch block so that a single failed transfer (due to blacklisting, reverting contract, etc.) does not block the entire closePot() execution. This ensures the manager cut and other claimants' bonuses are still distributed even if one address cannot receive tokens.

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);
+ try IERC20(i_token).transfer(claimants[i], claimantCut) {
+ // success
+ } catch {
+ // skip failed transfer, optionally track for later claim
+ }
}
+ remainingRewards = 0;
}
}
Updates

Lead Judging Commences

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