MyCut

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

Block Gas Limit DoS in closePot via Unbounded Iteration

Description

The closePot function iterates through the dynamically growing claimants array using a for loop to distribute the manager's remaining cut to all users who claimed in time. Because there is no upper limit enforced on the number of claimants in a contest, this array can grow significantly large.

During the execution of closePot, each iteration of the loop performs a state read and an external contract call (i_token.transfer). If a contest attracts thousands of legitimate claimants, the total gas required to execute this unbounded loop will inevitably exceed the Ethereum block gas limit, causing an unconditional Out-of-Gas (OOG) revert.

Risk

The closePot function will become entirely bricked (Denial of Service). The manager will be permanently unable to claim their cut, and the remaining rewards intended for the claimants will be permanently locked within the contract.

Proof of Concept

This Foundry test simulates a scenario with a high volume of claimants, demonstrating the massive gas consumption required for the closePot loop, which will exceed mainnet limits.

Solidity

function test_GasLimitDoS() public {
// Assuming the setup initialized 'pot' with 5000 valid players
// 1. All 5000 players successfully claim their initial cut
for(uint160 i = 1; i <= 5000; i++) {
address player = address(i);
vm.prank(player);
pot.claimCut(); // Pushes 5000 addresses to the claimants array
}
// 2. Fast forward past the 90-day deadline
vm.warp(block.timestamp + 91 days);
uint256 gasBefore = gasleft();
// 3. The Manager attempts to close the pot
vm.prank(pot.owner());
pot.closePot();
uint256 gasUsed = gasBefore - gasleft();
// 4. Log the gas used.
// Executing 5000 external ERC20 transfers in a single transaction
// will easily exceed the ~30M block gas limit on Ethereum mainnet.
console.log("Gas used for 5000 claimants: ", gasUsed);
}

Recommended Mitigation

Avoid iterating over unbounded arrays for external token transfers. Implement a "Pull over Push" pattern. Instead of the manager pushing funds to all claimants in a single loop, allow the claimants to pull their final cut individually after the pot is closed.

Solidity

// Mitigation concept:
// 1. Calculate the final claimantCut inside closePot and save it to state.
// 2. Create a new function for claimants to pull their final rewards.
uint256 public finalClaimantCut;
bool public isPotClosed;
function closePot() external onlyOwner {
// ... initial checks and manager cut logic ...
finalClaimantCut = (remainingRewards - managerCut) / claimants.length;
isPotClosed = true;
// Remove the for-loop completely
}
function claimFinalCut() external {
require(isPotClosed, "Pot not closed yet");
require(hasClaimedInitial[msg.sender], "Not a valid claimant");
require(!hasClaimedFinal[msg.sender], "Already claimed final cut");
hasClaimedFinal[msg.sender] = true;
_transferReward(msg.sender, finalClaimantCut);
}
Updates

Lead Judging Commences

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