Snowman Merkle Airdrop

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

Missing Claim Check Enables Duplicate NFT Airdrops)

Root + Impact

Description:
Normally, the claimSnowman() function allows eligible users to claim Snowman NFTs using a Merkle proof and ECDSA signature, and is expected to be called only once per address. The contract includes a mapping s_hasClaimedSnowman to track this, but does not check it before minting.

This allows a user to re-acquire the same amount of Snow tokens and repeatedly call claimSnowman() using the same Merkle proof and signature. Since no check is made to prevent reclaims, they will receive NFTs multiple times, inflating their allocation and breaking the intended airdrop distribution.

@function claimSnowman(...) {
...
// Missing check before mint
// require(!s_hasClaimedSnowman[receiver], "Already claimed");
_mintSnowman(receiver, balance);
s_hasClaimedSnowman[receiver] = true;
}

Risk

Likelihood:

  • A user with knowledge of the Merkle structure and claim logic can exploit this intentionally.

  • This may also be triggered accidentally by users with rebought Snow tokens.

  • Because the Merkle leaf is derived from balanceOf() and not a fixed snapshot, attackers can recreate valid leaves post-claim.

Impact:

  • Multiple NFT airdrops for a single address

  • Potential total supply inflation

  • Reduced trust and fairness in distribution

  • Users who claimed once are disadvantaged

Proof of Concept

// First claim succeeds
claimSnowman(user, proof, v, r, s);
// User buys Snow tokens again
// Calls claim again with same proof + sig
claimSnowman(user, proof, v, r, s); // ❌ NFTs minted again

Explanation:
The Merkle root is based on balances. Since balances can be restored after a successful claim, the same Merkle leaf and signature remain valid again, and the function does not restrict reuse. This allows repeated claims.

Recommended Mitigation

+ require(!s_hasClaimedSnowman[receiver], "Already claimed");

Explanation:
This check should be added before minting to prevent duplicate claims. Also, consider setting the flag (s_hasClaimedSnowman[receiver] = true) before calling _mintSnowman() to avoid any risk from unexpected reentrancy, even though nonReentrant is applied.

Updates

Lead Judging Commences

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

Lack of claim check

The claim function of the Snowman Airdrop contract doesn't check that a recipient has already claimed a Snowman. This poses no significant risk as is as farming period must have been long concluded before snapshot, creation of merkle script, and finally claiming.

Support

FAQs

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