Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: low
Invalid

`Snowman::mintSnowman` mints in an unbounded loop — gas DoS for large balances

Root + Impact

Description

  • claimSnowman mints one Snowman per Snow token by calling mintSnowman(receiver, amount), which loops amount times.

  • The loop is unbounded and proportional to the staked balance, so a sufficiently large balance makes the transaction exceed the block gas limit and always revert.

```solidity
function mintSnowman(address receiver, uint256 amount) external {
@> for (uint256 i = 0; i < amount; i++) { // unbounded: one mint per token, no cap
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}
```

Risk

Likelihood:

  • Occurs for any recipient whose Snow balance is large enough that amount iterations exceed the block gas limit (each _safeMint writes storage + may call onERC721Received).

  • Gas cost grows linearly with the staked amount, so large legitimate holders are affected during normal claims.

Impact:

  • The claim transaction reverts, so the affected user can never receive their Snowman NFTs (denial of service).

  • The more eligible a user is (the more Snow they hold), the more likely their claim is bricked.

Proof of Concept

Self-contained Foundry test measuring the linear gas growth. Run: `forge test --match-test test_M4_MintGasScalesWithAmount -vvv`
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {Snowman} from "../src/Snowman.sol";
contract M4 is Test {
Snowman nft;
function setUp() public { nft = new Snowman("ipfs://svg"); }
function test_M4_MintGasScalesWithAmount() public {
uint256 g1 = gasleft(); nft.mintSnowman(address(0xA11CE), 1); uint256 used1 = g1 - gasleft();
uint256 g50 = gasleft(); nft.mintSnowman(address(0xB0B), 50); uint256 used50 = g50 - gasleft();
uint256 perMint = used50 / 50;
uint256 tokensToBrickAt30M = 30_000_000 / perMint;
console2.log("gas / mint (~)", perMint); // ~33_648
console2.log("balance that bricks claim", tokensToBrickAt30M); // ~891
assertGt(used50, used1 * 15); // linear growth
assertLt(tokensToBrickAt30M, 1000); // a few hundred Snow already exceeds 30M gas
}
}
```

Recommended Mitigation

```diff
- function mintSnowman(address receiver, uint256 amount) external {
- for (uint256 i = 0; i < amount; i++) {
- _safeMint(receiver, s_TokenCounter);
- emit SnowmanMinted(receiver, s_TokenCounter);
- s_TokenCounter++;
- }
- }
+ // Option A: mint a single NFT that represents the staked position, storing `amount` as metadata.
+ // Option B: expose a paginated/bounded mint so a claim can never exceed the block gas limit.
```
Updates

Lead Judging Commences

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