MyCut

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

[H-1] External Calls Inside Loop Causing Denial of Service

[H-1] External Calls Inside Loop Causing Denial of Service

Description

  • Normal behavior: The owner should be able to close a pot after the 90-day claim window and distribute remaining rewards to all claimants.

  • Issue: closePot() calls _transferReward() (which calls i_token.transfer) inside a loop. If any claimant is a contract that reverts during the transfer, it blocks the entire pot closure, permanently locking the funds.

for (uint256 i = 0; i < claimants.length; i++) {
@> _transferReward(claimants[i], claimantCut);
}
function _transferReward(address player, uint256 reward) internal {
@> i_token.transfer(player, reward);
}

Risk

Likelihood:

  • Occurs whenever at least one claimant is a contract that reverts during ERC20 transfer.

  • Happens whenever closePot() is called after multiple claimants exist.

Impact:

  • closePot() becomes permanently uncallable.

  • Remaining rewards are permanently locked in the contract.

Severity: High (H)

Proof of Concept

Click to expand PoC
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Pot} from "../src/Pot.sol";
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {Test, console} from "lib/forge-std/src/Test.sol";
contract RevertingERC20 is IERC20 {
string public constant name = "RevertingToken";
string public constant symbol = "REV";
uint8 public constant decimals = 18;
mapping(address => uint256) private balances;
constructor(address initialHolder, uint256 amount) {
balances[initialHolder] = amount;
}
function totalSupply() external pure override returns (uint256) { return 1e18; }
function balanceOf(address account) external view override returns (uint256) { return balances[account]; }
function transfer(address, uint256) external pure override returns (bool) { revert("I revert on transfer!"); }
function allowance(address, address) external pure override returns (uint256) { return 0; }
function approve(address, uint256) external pure override returns (bool) { return true; }
function transferFrom(address, address, uint256) external pure override returns (bool) { revert("I revert on transferFrom!"); }
}
contract TestDoS is Test {
address user = address(0x123);
address[] players = [address(0xAAA), address(0xBBB)];
uint256[] rewards = [1 ether, 1 ether];
function test_closePot_DoS_ByRevertingToken() public {
RevertingERC20 rev = new RevertingERC20(user, 10 ether);
// Deploy a pot using a malicious token
Pot badPot = new Pot(players, rewards, rev, 2 ether);
// Fast-forward beyond claim period
vm.warp(block.timestamp + 91 days);
// closePot() becomes permanently uncallable
vm.expectRevert("I revert on transfer!");
badPot.closePot();
}
}

Recommended Mitigation

// ✅ Pull-based reward withdrawal instead of looping transfers
mapping(address => uint256) public redistributedRewards;
bool public potClosed;
function closePot() external onlyOwner {
require(!potClosed, "Pot already closed");
require(block.timestamp - i_deployedAt >= 90 days, "Still open for claim");
potClosed = true;
uint256 managerCut = remainingRewards / managerCutPercent;
require(i_token.transfer(owner(), managerCut), "Manager cut failed");
uint256 claimantCut = (remainingRewards - managerCut) / claimants.length;
// Accrue rewards for claimants; claimable individually
for (uint256 i = 0; i < claimants.length; i++) {
redistributedRewards[claimants[i]] += claimantCut;
}
remainingRewards = 0;
}
function withdrawRedistributedReward() external {
uint256 amount = redistributedRewards[msg.sender];
require(amount > 0, "Nothing to withdraw");
redistributedRewards[msg.sender] = 0;
require(i_token.transfer(msg.sender, amount), "Transfer failed");
}

Explanation:

  • External calls inside loops are removed.

  • Each claimant withdraws their reward individually.

  • Prevents DoS even with malicious claimants.

  • Maintains accounting correctness.


Updates

Lead Judging Commences

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