Snowman Merkle Airdrop

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

Missing Claim Status Check

Root + Impact

Description

  • In a typical airdrop contract, a user should only be able to claim their airdropped tokens or NFTs once. After a successful claim, the contract should record this and prevent further claims by the same address.

  • In the SnowmanAirdrop contract, the claimSnowman function does not verify whether the receiver has already claimed their Snowman NFT. While the contract sets the s_hasClaimedSnowman[receiver] flag to true after a claim, it fails to check this flag before processing a new claim. This oversight allows users to repeatedly call the claim function and mint multiple NFTs illegitimately.

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();
}
@> // MISSING: Check if s_hasClaimedSnowman[receiver] is already true
i_snow.safeTransferFrom(receiver, address(this), amount);
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}

Risk

Likelihood:

  • Any user whose claim is valid (correct Merkle proof and signature) can repeatedly call claimSnowman() with the same parameters.

  • This can occur as long as the user keeps enough tokens in their wallet and keeps reusing the proof and signature.

Impact:

  • A single user can mint multiple Snowman NFTs unfairly.

Proof of Concept

The lack of a s_hasClaimedSnowman[receiver] check allows the same user to reuse the same valid Merkle proof and ECDSA signature to repeatedly claim Snowman NFTs. Since the contract doesn’t track whether an address has already claimed (until after transferring and minting), this logic executes again every time without restriction.

// Assume valid proof and signature for receiver already computed
airdrop.claimSnowman(receiver, merkleProof, v, r, s);
// Repeat the call again with same parameters
airdrop.claimSnowman(receiver, merkleProof, v, r, s);
// Since there's no check for s_hasClaimedSnowman[receiver], this will succeed again.

Recommended Mitigation

The mitigation adds a pre-check to validate whether the receiver has already claimed the NFT.

function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
+ if (s_hasClaimedSnowman[receiver]) {
+ revert("Already claimed");
+ }
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
...
}
Updates

Lead Judging Commences

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