MyCut

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

One claimant who can't receive tokens (e.g. blacklisted by USDC/USDT) makes `Pot::closePot` revert every time, so no pot leftover can ever be distributed

Root + Impact

Description

  • closePot should always be able to distribute the leftover to the manager and the claimants.

  • closePot sends tokens to every claimant in one loop. If any one transfer reverts (for example, the token issuer blacklisted a claimant's address after they claimed), the whole transaction reverts. There is no way to skip that claimant or pay out in parts, so the pot can never be closed.

for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut); // one revert blocks everyone
}
function _transferReward(address player, uint256 reward) internal {
i_token.transfer(player, reward);
}

Risk

Likelihood:

  • When the pot uses a popular token with a blacklist (USDC, USDT) and any claimant's address is blacklisted during the 90-day period.

  • When a claimant deliberately gets their own address blacklisted (e.g. by interacting with sanctioned contracts) to block the close.

Impact:

  • The manager cut and every other claimant's share of the leftover are locked in the Pot forever.

  • One player can block the payout for every other player in the pot.

Proof of Concept

Paste this contract above contract TestMyCut (it only needs ERC20 imported from the same OpenZeppelin file):

import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract BlacklistToken is ERC20 {
mapping(address => bool) public blacklisted;
constructor() ERC20("USDC", "USDC") {}
function mint(address a, uint256 v) external { _mint(a, v); }
function setBlacklist(address a, bool b) external { blacklisted[a] = b; }
function _update(address from, address to, uint256 v) internal override {
require(!blacklisted[from] && !blacklisted[to], "blacklisted");
super._update(from, to, v);
}
}
function testBlacklistedClaimantBlocksClosePot() public {
BlacklistToken usdc = new BlacklistToken();
usdc.mint(user, 200e18);
address[] memory ps = new address[](3);
ps[0] = player1; ps[1] = player2; ps[2] = makeAddr("player3");
uint256[] memory rs = new uint256[](3);
rs[0] = 50e18; rs[1] = 50e18; rs[2] = 100e18;
vm.startPrank(user);
usdc.approve(conMan, type(uint256).max);
address pot = ContestManager(conMan).createContest(ps, rs, IERC20(address(usdc)), 200e18);
ContestManager(conMan).fundContest(0);
vm.stopPrank();
vm.prank(player1); Pot(pot).claimCut();
vm.prank(player2); Pot(pot).claimCut();
usdc.setBlacklist(player1, true); // player1 gets blacklisted by the token issuer
vm.warp(block.timestamp + 91 days);
vm.prank(user);
vm.expectRevert(bytes("blacklisted"));
ContestManager(conMan).closeContest(pot);
assertEq(usdc.balanceOf(pot), 100e18); // leftover cannot be distributed
}

Recommended Mitigation

Record each claimant's share and let them withdraw it (pull payments), so one failed transfer only affects that claimant. Or skip failed transfers:

for (uint256 i = 0; i < claimants.length; i++) {
- _transferReward(claimants[i], claimantCut);
+ try i_token.transfer(claimants[i], claimantCut) returns (bool ok) {
+ if (!ok) unpaid[claimants[i]] += claimantCut;
+ } catch {
+ unpaid[claimants[i]] += claimantCut;
+ }
}
Updates

Lead Judging Commences

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