Snowman Merkle Airdrop

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

SnowmanAirdrop::claimSnowman mints one NFT per wei of Snow - unbounded loop reverts every real claim (DoS)

Root + Impact

Description

  • A holder of Snow tokens is supposed to claim Snowman NFTs proportional to their stake, with the NFT count derived from how much Snow they hold.

  • claimSnowman sets amount to i_snow.balanceOf(receiver) — a raw 18-decimal token amount in wei — and passes it straight to mintSnowman, which loops amount times calling _safeMint. For any realistic balance (1 Snow == 1e18), the loop must run ~1e18 iterations, which always exceeds the block gas limit, so the claim reverts out-of-gas.

```solidity
// src/SnowmanAirdrop.sol
@> uint256 amount = i_snow.balanceOf(receiver); // 1 Snow == 1e18 (wei-scale)
i_snow.safeTransferFrom(receiver, address(this), amount);
@> i_snowman.mintSnowman(receiver, amount); // loops ~1e18 times -> out of gas
```

The NFT count is wrongly equated to the token's wei balance instead of a whole-token count, so the mint loop size is multiplied by 1e18.

Risk

Likelihood:

  • Triggers on every claim by any holder with a normal (non-dust) Snow balance, because 1e18 loop iterations always exceed the block gas limit.

  • Only sub-block-gas-limit dust balances (such as the 1-wei earnSnow mint) could ever complete a claim, so the feature is unusable for real holders.

Impact:

  • The core airdrop functionality is permanently broken for all legitimate holders — every real claim reverts.

  • The intended NFT distribution can never be performed, freezing the protocol's central feature.

Proof of Concept

A receiver is given 1 full Snow token (1e18 wei) and a valid Merkle proof + signature. The claim reverts because mintSnowman attempts ~1e18 _safeMint iterations:

```solidity
function test_real_claim_reverts_out_of_gas() public {
// receiver holds exactly 1 Snow and approves the airdrop
deal(address(snow), receiver, 1e18);
vm.prank(receiver);
snow.approve(address(airdrop), 1e18);

// proof, v, r, s built for (receiver, 1e18) via the project's Merkle + EIP-712 helpers
vm.expectRevert(); // mintSnowman loops 1e18 times -> out of gas
airdrop.claimSnowman(receiver, proof, v, r, s);

}
```

Because the gas cost grows linearly with the wei balance, no honest holder of 1 or more whole Snow tokens can ever claim.

Recommended Mitigation

Decouple the NFT count from token wei — mint a bounded count (for example, one NFT per whole Snow token) instead of one NFT per wei:

```diff

  • uint256 amount = i_snow.balanceOf(receiver);

  • i_snow.safeTransferFrom(receiver, address(this), amount);

  • i_snowman.mintSnowman(receiver, amount);

  • uint256 snowBalance = i_snow.balanceOf(receiver);

  • i_snow.safeTransferFrom(receiver, address(this), snowBalance);

  • // mint a bounded NFT count, decoupled from token wei (e.g. 1 NFT per whole Snow)

  • uint256 nftCount = snowBalance / 1e18;

  • i_snowman.mintSnowman(receiver, nftCount);
    ```

Updates

Lead Judging Commences

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