MyCut

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

closePot loops over all claimants with no cap - a large enough pot exceeds the block gas limit and can never be closed

Description

Pot.closePot distributes to every entry in claimants inside a single unbounded loop:

function closePot() external onlyOwner {
...
if (remainingRewards > 0) {
uint256 managerCut = remainingRewards / managerCutPercent;
i_token.transfer(msg.sender, managerCut);
uint256 claimantCut = (remainingRewards - managerCut) / i_players.length;
@> for (uint256 i = 0; i < claimants.length; i++) {
@> _transferReward(claimants[i], claimantCut);
}
}
}

claimants.length is bounded only by players.length, an arbitrary-length array accepted without limit by ContestManager.createContest. Each iteration performs a full ERC20 transfer (storage writes + external call). Measured directly: 700 claimants cost ~3.73M gas (~5,322 gas/claimant average), scaling linearly. At that rate a pot with roughly 5,600+ players already exceeds a realistic 30M mainnet block gas limit — closePot() can never complete in a single transaction, and there is no way to close a pot in batches (no pagination, no partial-close). The pot is then permanently stuck open: the manager never receives their cut and no claimant-cut redistribution can ever happen for that pot.

Risk

Likelihood: Medium

  • Requires a large enough players array at contest creation. createContest places no cap on players.length, so this happens whenever a contest is sized for a sufficiently large community (a realistic scenario for a "contest rewards distribution protocol").

Impact: High

  • Total, permanent denial of service on closePot() for the affected pot: the manager's cut and every claimant's redistribution share become unreachable forever, with no recovery function anywhere in scope.

Proof of Concept

Measured the real per-claimant gas cost, then confirmed the call genuinely fails once scaled past a realistic block gas limit:

function test_H5_unboundedClaimantsLoop_gasDoS() public {
// n = 8000; ~5.3k gas/claimant measured on a smaller run -> ~42M total
// ... (create pot with 8000 players, fund with a surplus, all 8000 claim)
vm.warp(91 days);
(bool ok,) = address(conMan).call{gas: 30_000_000}(
abi.encodeWithSelector(ContestManager.closeContest.selector, contest)
);
assertFalse(ok); // closePot() cannot complete: this pot is permanently unclosable
}

Calibration run (700 claimants, uncapped gas): gas used = 3,725,740~5,322 gas/claimant, confirming the linear-scaling root cause before demonstrating outright failure at 8000.

Recommended Mitigation

Make closing a pot incremental instead of all-or-nothing — e.g. accept a (start, count) range and let closePot be called repeatedly to page through claimants, tracking how much of the loop has completed:

- function closePot() external onlyOwner {
+ function closePot(uint256 start, uint256 count) external onlyOwner {
...
- for (uint256 i = 0; i < claimants.length; i++) {
+ uint256 end = start + count > claimants.length ? claimants.length : start + count;
+ for (uint256 i = start; i < end; i++) {
_transferReward(claimants[i], claimantCut);
}
}
Updates

Lead Judging Commences

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