MyCut

AI First Flight #8
Beginner FriendlyFoundry
EXP
View results
Submission Details
Severity: high
Valid

closePot unbounded loop over claimants array can exceed block gas limit

Description

Root + Impact

Description

  • closePot() iterates over the entire claimants array, which grows by one entry every time a player calls claimCut(). Each iteration performs an ERC20 transfer (~50k-70k gas). When the claimants array grows large enough, the total gas exceeds the block gas limit (30M), making closePot() permanently uncallable.

// Pot.sol::closePot()
@> for (uint256 i = 0; i < claimants.length; i++) {
_transferReward(claimants[i], claimantCut); // ~65k gas per iteration
}

RisK

Likelihood:

  • Requires a contest with ~460+ players who all claim their rewards. This is unlikely for small contests but realistic for large community contests or airdrops.

  • The claimants array has no size cap; every claim appends to it.

Impact:

  • closePot() becomes permanently uncallable once the array is too large.

  • The manager cut and all claimant bonuses from leftover rewards are locked forever.

  • No pagination or batched processing mechanism exists.

Growth Analysis:

Gas per iteration: ~65,000 (SLOAD + token transfer)
Block gas limit: 30,000,000
Max iterations before DoS: ~460 claimants

Proof of Concept

The following test creates a contest with 500 players who all claim their rewards. When closePot() is called, it must iterate over all 500 claimants and perform a token transfer for each one. The total gas required (~32.5M) exceeds the block gas limit (30M), causing the transaction to always revert.

function testGasLimitDoS() public {
// Create contest with 500 players
address[] memory players = new address[](500);
uint256[] memory rewards = new uint256[](500);
for (uint i = 0; i < 500; i++) {
players[i] = address(uint160(i + 1));
rewards[i] = 1 ether;
}
Pot pot = new Pot(players, rewards, token, 500 ether);
token.transfer(address(pot), 500 ether);
// All 500 players claim
for (uint i = 0; i < 500; i++) {
vm.prank(players[i]);
pot.claimCut();
}
vm.warp(block.timestamp + 91 days);
// closePot iterates 500 times with token transfers
// 500 * 65,000 = 32,500,000 gas > 30M block limit
vm.expectRevert(); // out of gas
pot.closePot();
}

Recommended Mitigation

Replace the push payment loop with a pull payment pattern. Instead of transferring tokens to each claimant in the loop, store the bonus amount in a pendingBonus mapping. Claimants then call a separate claimBonus() function to withdraw their bonus individually. This eliminates the unbounded gas cost and prevents any single user from blocking others.

+ mapping(address => uint256) public pendingBonus;
+ bool public closed;
function closePot() external onlyOwner {
if (block.timestamp - i_deployedAt < 90 days) {
revert Pot__StillOpenForClaim();
}
+ require(!closed, "Already closed");
+ closed = true;
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);
- }
+ uint256 claimantCut = (remainingRewards - managerCut) / claimants.length;
+ for (uint256 i = 0; i < claimants.length; i++) {
+ pendingBonus[claimants[i]] = claimantCut;
+ }
+ remainingRewards = 0;
}
}
+ function claimBonus() external {
+ uint256 bonus = pendingBonus[msg.sender];
+ require(bonus > 0, "No bonus");
+ pendingBonus[msg.sender] = 0;
+ i_token.transfer(msg.sender, bonus);
+ }
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 2 days ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-04] Gas Limit DoS via large amount of claimants

## Description The `Pot.sol` contract contains a vulnerability that can lead to a Denial of Service (DoS) attack. This issue arises from the inefficient handling of claimants in the `closePot` function, where iterating over a large number of claimants can cause the transaction to run out of gas, thereby preventing the contract from executing as intended. ## Vulnerability Details Affected code - <https://github.com/Cyfrin/2024-08-MyCut/blob/946231db0fe717039429a11706717be568d03b54/src/Pot.sol#L58> The vulnerability is located in the `closePot` function of the Pot contract, specifically at the loop iterating over the claimants array: ```javascript function closePot() external onlyOwner { ... if (remainingRewards > 0) { ... @> for (uint256 i = 0; i < claimants.length; i++) { _transferReward(claimants[i], claimantCut); } } } ``` The `closePot` function is designed to distribute remaining rewards to claimants after a contest ends. However, if the number of claimants is extremly large, the loop iterating over the claimants array can consume a significant amount of gas. This can lead to a situation where the transaction exceeds the gas limit and fails, effectively making it impossible to close the pot and distribute the rewards. ## Exploit 1. Attacker initiates a big contest with a lot of players 2. People claim the cut 3. Owner closes the large pot that will be very costly ```javascript function testGasCostForClosingPotWithManyClaimants() public mintAndApproveTokens { // Generate 2000 players address[] memory players2000 = new address[](2000); uint256[] memory rewards2000 = new uint256[](2000); for (uint256 i = 0; i < 2000; i++) { players2000[i] = address(uint160(i + 1)); rewards2000[i] = 1 ether; } // Create a contest with 2000 players vm.startPrank(user); contest = ContestManager(conMan).createContest(players2000, rewards2000, IERC20(ERC20Mock(weth)), 2000 ether); ContestManager(conMan).fundContest(0); vm.stopPrank(); // Allow 1500 players to claim their cut for (uint256 i = 0; i < 1500; i++) { vm.startPrank(players2000[i]); Pot(contest).claimCut(); vm.stopPrank(); } // Fast forward time to allow closing the pot vm.warp(91 days); // Record gas usage for closing the pot vm.startPrank(user); uint256 gasBeforeClose = gasleft(); ContestManager(conMan).closeContest(contest); uint256 gasUsedClose = gasBeforeClose - gasleft(); vm.stopPrank(); console.log("Gas used for closing pot with 1500 claimants:", gasUsedClose); } ``` ```Solidity Gas used for closing pot with 1500 claimants: 6425853 ``` ## Impact The primary impact of this vulnerability is a Denial of Service (DoS) attack vector. An attacker (or even normal usage with a large number of claimants) can cause the `closePot` function to fail due to excessive gas consumption. This prevents the distribution of remaining rewards and the execution of any subsequent logic in the function, potentially locking funds in the contract indefinitely. In the case of smaller pots it would be a gas inefficency to itterate over the state variabel `claimants`. ## Recommendations Gas Optimization: Optimize the loop to reduce gas consumption by using a local variable to itterate over, like in the following example: ```diff - for (uint256 i = 0; i < claimants.length; i++) { - _transferReward(claimants[i], claimantCut); - } + uint256 claimants_length = claimants.length; + ... + for (uint256 i = 0; i < claimants_length; i++) { + _transferReward(claimants[i], claimantCut); + } ``` Batch Processing: Implement batch processing for distributing rewards. This will redesign the protocol functionallity but instead of processing all claimants in a single transaction, allow the function to process a subset of claimants per transaction. This can be achieved by introducing pagination or limiting the number of claimants processed in one call. This could also be fixed if the user would claim their reward after 90 days themselves

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!