Snowman Merkle Airdrop

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

Replay attack

Root + Impact

Description

  • Each user should be able to claim the Snowman NFT only once.\

  • However, the contract does not check if a user has already claimed, allowing users to repeatedly claim multiple NFTs as long as they have the tokens and valid addresses

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 here:
// if (s_hasClaimedSnowman[receiver]) {
// revert SA__AlreadyClaimed();
// }
i_snow.safeTransferFrom(receiver, address(this), amount);
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}’’’

Risk

Likelihood:

  • Users with a valid Merkle proof and signature can repeatedly claim the snowmen as long as they hold tokens.\

  • The contract allows multiple claims without stopping receivers that have already claimed.

Impact:

  • Allows receivers to claim multiple Snowman NFTs.

  • Could claim all the snowmen so there isn’t any for anyone else

  • Could lead to loss of user trust and unfair distribution.

Proof of Concept


Step 1: Meet Requirements

  • Receiver fulfills all necessary conditions to claim a snowman.

  • Step 2: Claim Execution

    • The following code runs:

      i_snow.safeTransferFrom(receiver, address(this), amount); s_hasClaimedSnowman[receiver] = true; emit SnowmanClaimedSuccessfully(receiver, amount); i_snowman.mintSnowman(receiver, amount);

    • The snowman is successfully claimed.

  • Step 3: Replay Attack

    • Since there's no check preventing multiple claims:

      • The receiver can call the same function again.

      • They keep receiving snowmen with no limit.

  • Impact

    • One user can drain the entire snowman supply.

    • Others are blocked from claiming.

// A user calls claimSnowman() multiple times, each time sending their tokens to the contract
// and receiving new NFTs repeatedly.
for (uint i = 0; i < 10; i++) {
snowmanAirdrop.claimSnowman(userAddress, merkleProof, v, r, s);
}

Recommended Mitigation

📌 Key Mitigation Points
✅ Prevents multiple claims by the same user.

🔐 Stops replay attacks by tracking and checking claim history.

⛔ Blocks abuse where users try to drain snowman supply.

💡 Simple, effective fix by leveraging the existing s_hasClaimedSnowman mapping.

🧱 Can be extended with further enhancements (e.g., cooldown timers, claim limits).

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();
}
// ✅ Mitigation: prevent multiple claims
if (s_hasClaimedSnowman[receiver]) {
revert SA__AlreadyClaimed();
}
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 5 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.