Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Valid

Unbounded Loop in mintSnowman (Potential DoS via Gas Limit)

Unbounded Loop in mintSnowman (Potential DoS via Gas Limit)

Description

  • the Snowman contract is all about minting awesome NFT snowmen for an airdrop. It’s built on the trusty ERC721 standard, but there’s a sneaky issue in the mintSnowman function. It’s got a loop that lets anyone try to mint a ton of tokens in one go, and that could grind things to a halt faster than a sled on dry grass. Why? Because it could eat up more gas than an Ethereum block can handle (~30M gas).

  • The mintSnowman function has a for loop that mints tokens one by one, based on a user-provided amount. Sounds fine, right? But there’s no cap on how big amount can be. Someone could crank it up to, say, 10,000 tokens, and each mint chews through ~33,577 gas (per our PoC). Do the math, and that’s a whopping 335M gas—way over Ethereum’s 30M gas block limit. The transaction would crash like a snowball hitting a brick wall, leaving legit users stuck and unable to mint.

// Root cause in the codebase with @> marks to highlight the relevant section
```solidity
function mintSnowman(address receiver, uint256 amount) external {
@> for (uint256 i = 0; i < amount; i++) { //No cap on the maximum amount of token a user is allowed to mint per time
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}
## Risk
**Likelihood: **: Medium
* Reason 1 // Describe WHEN this will occur (avoid using "if" statements)
Someone’s gotta be a grinch and input a huge `amount` on purpose. It’s not hard, especially since the functions wide open to anyone
**Severity: ** Medium
*Reason 1
This issue won’t steal your tokens or burn down the contract, but it can jam up the minting process, leaving everyone high and dry. Its like locking the doors to the snowball fightnobody gets to play!
**Impact **:
- Stops `mintSnowman` from working for big mints, screwing up the airdrop vibe.
- Frustrates users who just want their snowmen, hurting the project’s rep.
- Made worse by the missing access control (Issue #1), since any random Joe can try this stunt.
## Proof of Concept
Copy the following code base and paste inside: => TestSnowman.t.sol file
Run it with: forge test --match-test testUnboundedLoopGasLimit -vvv
```solidity
//Declaring the attacker
address attacker = makeAddr("attacker");
function testUnboundedLoopGasLimit() public {
uint256 testAmount = 100; // Smaller amount to measure gas usage
uint256 largeAmount = 10000; // Large amount to simulate DoS
uint256 initialTokenCounter = nft.getTokenCounter();
// Measure gas usage for a smaller mint to estimate gas per mint
vm.prank(attacker);
uint256 gasStart = gasleft();
nft.mintSnowman(attacker, testAmount);
uint256 gasUsed = gasStart - gasleft();
// Calculate gas per mint and max mints in a 30M gas block
uint256 gasPerMint = gasUsed / testAmount;
uint256 maxMintsInBlock = 30_000_000 / gasPerMint;
// Log for debugging
console2.log("Gas used for minting", testAmount, "NFTs:", gasUsed);
console2.log("Estimated gas per mint:", gasPerMint);
console2.log("Max mints in 30M gas block:", maxMintsInBlock);
// Assert that largeAmount exceeds feasible mints in a block
assertTrue(largeAmount > maxMintsInBlock, "Large amount should exceed block gas limit");
// Verify state after test mint
assertEq(nft.balanceOf(attacker), testAmount, "Attacker balance should reflect test mint");
assertEq(nft.getTokenCounter(), initialTokenCounter + testAmount, "Token counter should reflect test mint");
// Log final state
console2.log("Token counter after test:", nft.getTokenCounter());
console2.log("Attacker balance after test:", nft.balanceOf(attacker));
}

What Happened When I Ran It:

Ran 1 test for test/TestSnowman.t.sol:TestSnowman
[PASS] testUnboundedLoopGasLimit() (gas: 3401177)
Logs:
Gas used for minting 100 NFTs: 3357766
Estimated gas per mint: 33577
Max mints in 30M gas block: 893
Token counter after test: 100
Attacker balance after test: 100
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 15.63ms

What’s This Telling Us?

  • Minting 100 tokens burned 3,357,766 gas, so each mint costs ~33,577 gas.

  • In a 30M gas block, you could only mint about 893 tokens (30,000,000 / 33,577).

  • Trying to mint 10,000 tokens would need ~335,770,000 gas, which is way over the limit. On Ethereum mainnet, that’d crash and burn, proving the DoS risk is real.

Recommended Mitigation

Set a cap on the amount parameter in mintSnowman to keep gas usage in check. Here’s a quick code snippet to make it happen:

uint256 public constant MAX_MINT_PER_TX = 100;
function mintSnowman(address receiver, uint256 amount) external {
@> require(amount <= MAX_MINT_PER_TX, "Whoa, too many snowmen at once!");
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}
+ uint256 public constant MAX_MINT_PER_TX = 100;
...
+ require(amount <= MAX_MINT_PER_TX, "Whoa, too many snowmen at once!");
...
Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Unrestricted NFT mint function

The mint function of the Snowman contract is unprotected. Hence, anyone can call it and mint NFTs without necessarily partaking in the airdrop.

Support

FAQs

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