Snowman Merkle Airdrop

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

Denial of Service (DoS) for Large Token Holders via Unbounded Loop

Denial of Service (DoS) for Large Token Holders via Unbounded Loop

Description

The SnowmanAirdrop contract allows users to claim Snowman NFTs proportional to their Snow ERC20 token balance. The claimSnowman function fetches the user's live token balance as the amount and passes it to Snowman.mintSnowman().

The specific issue is that mintSnowman uses a for loop to mint amount number of NFTs individually. If a user holds a large amount of Snow tokens (e.g., a whale with 500+ tokens), the gas required to execute the loop will exceed the block gas limit. This causes the transaction to revert, permanently preventing large token holders from claiming their airdrop.

// In SnowmanAirdrop.sol
function claimSnowman(...) external nonReentrant {
// ...
uint256 amount = i_snow.balanceOf(receiver);
// ...
@> i_snowman.mintSnowman(receiver, amount); // Passes potentially huge amount
}
// In Snowman.sol
function mintSnowman(address receiver, uint256 amount) external {
@> for (uint256 i = 0; i < amount; i++) { // Unbounded loop
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}

Risk

Likelihood:

  • The attack is deterministic and will trigger automatically for any user whose token balance creates a loop size that exceeds the block gas limit (typically around 150-250 NFTs per transaction depending on gas optimizations).

  • A malicious attacker could also intentionally buy or receive a massive amount of tokens to make their balance large enough to trigger the DoS on their own account, though the primary impact affects legitimate large holders.

Impact:

  • Permanent Denial of Service (DoS) for large token holders. They are completely blocked from participating in the airdrop.

  • If the airdrop is a prerequisite for future protocol interactions, these users are permanently excluded from those features as well.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {SnowmanAirdrop} from "./SnowmanAirdrop.sol";
import {Snow} from "./Snow.sol";
import {Snowman} from "./Snowman.sol";
contract ExploitOP006 {
// Assume contracts are deployed and initialized
SnowmanAirdrop public airdrop;
Snow public snow;
Snowman public snowman;
constructor(address _airdrop, address _snow, address _snowman) {
airdrop = SnowmanAirdrop(_airdrop);
snow = Snow(_snow);
snowman = Snowman(_snowman);
}
// A user with 1000 tokens tries to claim
function attemptClaim(uint256 amount, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s) external {
// 1. User has 1000 Snow tokens
// 2. User calls claimSnowman
// 3. Internally, airdrop calls snowman.mintSnowman(user, 1000)
// 4. The for loop in mintSnowman runs 1000 times
// 5. Gas consumption: ~60,000 gas per mint * 1000 = 60,000,000 gas
// 6. Block gas limit is typically 30,000,000 gas
// Result: Transaction reverts with "Out of Gas"
airdrop.claimSnowman(msg.sender, merkleProof, v, r, s);
}
}

Recommended Mitigation

Avoid using an unbounded loop based on user input or dynamic balance. If the intent is to distribute NFTs proportionally, consider minting a single ERC1155 token representing the balance, or allow users to claim in predefined batches. Alternatively, if only 1 NFT per user is intended, hardcode the mint amount to 1.

function mintSnowman(address receiver, uint256 amount) external {
+ require(amount <= 100, "Batch size too large"); // Limit batch size
+ // OR change logic to mint 1 NFT per claim regardless of amount
for (uint256 i = 0; i < amount; i++) {
_safeMint(receiver, s_TokenCounter);
emit SnowmanMinted(receiver, s_TokenCounter);
s_TokenCounter++;
}
}

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!