Snowman Merkle Airdrop

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

No Check for Double Claim

Root + Impact

Description

Normal Behavior:

When a user is eligible for a Snowman NFT via an airdrop, they should be able to claim it exactly once by providing a valid Merkle proof and EIP-712 signature. The contract uses s_hasClaimedSnowman to track if a user has already claimed.


Issue:

The claimSnowman() function does not check whether a user has already claimed their NFT. This allows a user to call the function and receive unlimited NFTs repeatedly, provided they still hold the token amount and can replay the same proof and signature.

function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
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);
}

Risk

Likelihood:

  • Any user with a valid Merkle proof and EIP-712 signature can execute the claimSnowman() function multiple times.

  • No verification exists to block repeat claims; the s_hasClaimedSnowman[receiver] flag is written to after the transfer and mint, but never checked.

Impact:

  • Unlimited NFT mints to the same user, potentially devaluing the collection.

  • Infinite draining of Snow tokens from the user to the contract with no rate limiting, increasing risk if tokens can be recovered later or used maliciously.

Proof of Concept

// Assume this is called once successfully
airdrop.claimSnowman(receiver, validProof, v, r, s);
// Can be repeated with the same parameters
airdrop.claimSnowman(receiver, validProof, v, r, s); // Second NFT received
airdrop.claimSnowman(receiver, validProof, v, r, s); // Third NFT received
// ... until balance runs out

Recommended Mitigation

Add a check at the start of the claimSnowman() function and define SA__AlreadyClaimed():
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
+ if (s_hasClaimedSnowman[receiver]) {
+ revert SA__AlreadyClaimed();
+ }
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
Updates

Lead Judging Commences

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