Snowman Merkle Airdrop

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

Double Claim Vulnerability

Author Revealed upon completion

Root + Impact

Description

  • in normal situations users need to claim or call just one time the function


  • at this case there is no check to prevent a users from calling claimSnowman more than once. As long as the user’s Snow token balance remains nonzero (or is replenished), they can submit the same Merkle proof and signature repeatedly to mint multiple NFTs.

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();
// … signature & Merkle checks …
i_snow.safeTransferFrom(receiver, address(this), amount);
s_hasClaimedSnowman[receiver] = true;
i_snowman.mintSnowman(receiver, amount);
}
// … No guard prevents a second call, so the same user can mint repeatedly.

Risk

Likelihood:

  • Reason 1 // Attackers only need to resubmit the same proof/signature; no extra effort or tool required.

Impact:

  • Attackers can mint unlimited Snowman NFTs, diluting supply and breaking the intended one‑per‑user distribution.

Proof of Concept

// 1st claim (legitimate)
snowmanAirdrop.claimSnowman(receiver, merkleProof, v, r, s);
// 2nd claim (replay attack)
snowmanAirdrop.claimSnowman(receiver, merkleProof, v, r, s); // Succeeds again

Recommended Mitigation

- remove this code
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();
// … signature & Merkle checks …
i_snow.safeTransferFrom(receiver, address(this), amount);
s_hasClaimedSnowman[receiver] = true;
i_snowman.mintSnowman(receiver, amount);
}
+ add this code
+ 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();
if (i_snow.balanceOf(receiver) == 0) revert SA__ZeroAmount();
// … signature & Merkle checks …
i_snow.safeTransferFrom(receiver, address(this), amount);
s_hasClaimedSnowman[receiver] = true;
i_snowman.mintSnowman(receiver, amount);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge about 19 hours 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.