Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Critical: Merkle Leaf Reuse in claimSnowman() - Unlimited NFT Minting & Airdrop Fairness Collapse

Root + Impact

Description

  • Normal behavior:
    Each Merkle leaf in an airdrop claim system should be consumable only once. This ensures that the same proof cannot be reused by the same or different address to claim additional NFTs.

  • Issue:
    The contract only checks whether an address has claimed (s_hasClaimedSnowman[receiver]) but fails to track which Merkle leaves have been used. As a result, an attacker can reuse the same valid Merkle proof multiple times to mint excessive NFTs, exploiting the protocol’s reward allocation.

multiple times and continue minting NFTs, as long as their address matches. This completely breaks the airdrop mechanism.

// >>> Root cause: No Merkle leaf tracking allows duplicate claims using the same proof @>
function claimSnowman(...) external {
...
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount)))); // @> Leaf generation
...
s_hasClaimedSnowman[receiver] = true; // @> Only tracks address, not Merkle leaf
...
i_snowman.mintSnowman(receiver, amount); // @> No leaf tracking → duplicate claims possible
}

Risk

Likelihood:

  • High — Attack is simple and requires only one valid Merkle proof.

  • Reproducibility: Fully reproducible using repeated calls with the same arguments.

  • Ease of exploitation: Very easy with basic scripting; no technical barriers once one valid proof is known.

Impact:

  • Unlimited NFT minting from a single valid proof.

  • Total collapse of airdrop fairness and reward limits.

  • Severe inflation: Rewards vastly exceed intended supply.

  • Protocol integrity failure: Trust in reward distribution is broken.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/SnowmanAirdrop.sol";
contract AirdropExploit is Test {
SnowmanAirdrop airdrop;
address attacker = address(0x666);
bytes32[] validProof;
uint256 expectedAmount = 1;
function testReuseAttack() public {
// First claim - valid proof
airdrop.claimSnowman(attacker, validProof, 27, 0x0, 0x0);
// Second claim - same proof reused
airdrop.claimSnowman(attacker, validProof, 27, 0x0, 0x0);
// Attacker now owns 2x the allowed amount
assertEq(nft.balanceOf(attacker), 2 * expectedAmount);
}
}

Explanation:

  • Attacker uses a valid Merkle proof multiple times to mint NFTs.

  • Since leaf reuse is not tracked, claimSnowman() allows it.

  • Protocol cannot prevent repeated exploitation without leaf status tracking.


Recommended Mitigation

Track used Merkle leaves in a mapping and check for duplication prior to minting. This ensures that each allocation can only be claimed once.

+ mapping(bytes32 => bool) private s_usedLeaves;
function claimSnowman(...) external {
...
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
+ require(!s_usedLeaves[leaf], "Leaf already used");
...
+ s_usedLeaves[leaf] = true;
s_hasClaimedSnowman[receiver] = true;
...
}

Explanation:

  • Solution: Prevent duplicate claims by marking each Merkle leaf as used upon first claim.

  • Security: Guarantees that each allocation is redeemable exactly once.

  • Efficiency: One cold SLOAD + one SSTORE per claim (standard for Merkle airdrops).

  • Compatibility: No breaking changes; complements existing address-based claim checks.

Severity Note:

This is a critical vulnerability due to the ease of exploit and direct inflation of token rewards. Without unique Merkle leaf enforcement, attackers can bypass the entire distribution cap and flood the ecosystem with illegitimate NFTs.

Verification confirms proper functionality:

function testLeafTrackingPreventsReuse() public {
vm.prank(user);
airdrop.claimSnowman(user, validProof, 27, 0x0, 0x0);
vm.expectRevert("Leaf already used");
vm.prank(user);
airdrop.claimSnowman(user, validProof, 27, 0x0, 0x0);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.