Snowman Merkle Airdrop

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

Missing Claim Status Check Allows Multiple Claims

Root + Impact

Description

The SnowmanAirdrop contract tracks claim status using the mapping s_hasClaimedSnowman, but fails to check this mapping before processing a claim. As a result, eligible users can claim their Snowman NFTs multiple times, each time transferring Snow tokens and receiving new NFTs.

This undermines the integrity of the airdrop, and allows double-claiming or automated draining of NFTs by repeatedly calling claimSnowman().

  • Describe the normal behavior in one or more sentences

  • Explain the specific issue or problem in one or more sentences

Affected Code:

solidity

s_hasClaimedSnowman[receiver] = true;

This sets claim status, but…

solidity

// This check is missing: // if (s_hasClaimedSnowman[receiver]) { revert AlreadyClaimed(); }

// Root cause in the codebase with @> marks to highlight the relevant section

Risk

Missing Claim Status Check

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

This issue occurs whenever a whitelisted user or any actor with access to a valid Merkle proof and signature calls the claimSnowman() function repeatedly without restriction.

  • Reason 2

  • Since the contract does not validate whether a user has already claimed, any user with tokens and a valid signature can automate the claim process and exploit it at any time, even after a successful initial claim.

Impact:

  • Impact 1

  • Users are able to mint an unlimited number of Snowman NFTs, as long as they have Snow tokens to send — effectively breaking the 1:1 mint logic and flooding the NFT supply.

  • Impact 2

  • The entire airdrop can be manipulated or drained by a small number of users, leading to financial loss, devaluation of the Snowman NFTs, and loss of community trust.

Proof of Concept

Assuming receiver has a valid signature + Merkle proof:

solidity

contract Attacker { SnowmanAirdrop public airdrop; address public victim; constructor(address _airdrop, address _victim) { airdrop = SnowmanAirdrop(_airdrop); victim = _victim; } function exploit(bytes32[] calldata proof, uint8 v, bytes32 r, bytes32 s) external { // call repeatedly for (uint i = 0; i < 5; i++) { airdrop.claimSnowman(victim, proof, v, r, s); } } }

This drains 5 NFTs if victim has enough tokens and has signed only once.

Recommended Mitigation

Problem Recap

The SnowmanAirdrop contract currently does not prevent users from claiming multiple times. Although the mapping s_hasClaimedSnowman is defined to track who has already claimed, it's only being set, not checked.

This creates a critical flaw:

  • Anyone with a valid Merkle proof, a valid signature, and a Snow token balance can repeatedly call claimSnowman() and receive multiple Snowman NFTs.

  • This can drain the NFT supply and break the intended fairness of the airdrop.


Goal of the Mitigation

Ensure that each user can claim the Snowman NFT only once — even if they retry with the same inputs or attempt to re-claim later.


Thought Process

To fix this, you need to:

  1. Check whether the user has already claimed before processing the rest of the claim logic.

  2. Revert early if they've already claimed — saving gas and preventing exploitation.

Luckily, the contract already has:

  • A mapping: mapping(address => bool) private s_hasClaimedSnowman;

  • A line that sets the claim status to true after a successful claim:
    s_hasClaimedSnowman[receiver] = true;

We just need to add one missing check.


Code Fix

Add this at the beginning of the claimSnowman() function:

solidity

if (s_hasClaimedSnowman[receiver]) {

revert("Snowman already claimed by this address");

}

solidity

error SA__AlreadyClaimed();

And use it like this:

solidity

CopyEdit

if (s_hasClaimedSnowman[receiver]) { revert SA__AlreadyClaimed(); }


Final Updated Function Snippet

Here's what the top of your claimSnowman function should look like after applying the mitigation:

solidity
// New mitigation check
if (s_hasClaimedSnowman[receiver]) {
revert SA__AlreadyClaimed(); // prevents re-claiming
}
if (i_snow.balanceOf(receiver) == 0) {
revert SA__ZeroAmount();
}
if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) {
revert SA__InvalidSignature();
}
uint256 amount = i_snow.balanceOf(receiver);
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert SA__InvalidProof();
}
i_snow.safeTransferFrom(receiver, address(this), amount);
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);

}

Updates

Lead Judging Commences

yeahchibyke Lead Judge 25 days 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.