Snowman Merkle Airdrop

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

Dynamic balance usage in merkle tree breaks standard airdrop functionality

Description:

The SnowmanAirdrop contract implements a fundamentally flawed airdrop mechanism that violates established industry standards for Merkle tree-based token distributions. The contract uses dynamic balance checking (i_snow.balanceOf(receiver)) to generate Merkle tree leaves during claim verification, while the original Merkle tree was constructed with fixed snapshot values. This approach contradicts standard airdrop practices where Merkle trees contain immutable snapshot data, and balance verification is performed separately from proof validation.

Standard airdrop implementations follow these principles:

  1. Fixed snapshot data: Merkle trees contain immutable (address, amount) pairs from a specific block/timestamp

  2. Separate balance verification: Current token balance is checked independently from Merkle proof validation

  3. Deterministic leaf generation: Leaves are generated using fixed values, not dynamic state

The current implementation breaks these standards by making leaf generation dependent on mutable state (balanceOf), creating a fundamental mismatch between the static Merkle tree structure (built with amount = 1 for all users) and dynamic runtime verification.

Attack path:

  1. User transfers 0.5 SNOW tokens to another address, reducing balance to 0.5

  2. User attempts claimSnowman()

  3. Function reverts immediately with SA__ZeroAmount() due to balance check

  4. User loses airdrop eligibility through normal token usage

Impact:

Complete breakdown of airdrop functionality

Users lose legitimate airdrop rewards

The vulnerability makes any token activity (transferring, receiving) incompatible with claiming, which contradicts the fundamental expectation that snapshot-based airdrops should be independent of post-snapshot token movements.

Recommended Mitigation:

****

Implement standard airdrop pattern with fixed claim amounts:

uint256 constant CLAIM_AMOUNT = 1; // Fixed amount per user from snapshot
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s) external {
require(!s_hasClaimedSnowman[receiver], "Already claimed");
// Use FIXED amount for leaf generation (matches Merkle tree)
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, CLAIM_AMOUNT))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert SA__InvalidProof();
}
// Separate balance verification (standard practice)
if (i_snow.balanceOf(receiver) < CLAIM_AMOUNT) {
revert SA__InsufficientBalance();
}
// Signature verification with fixed amount
if (!_isValidSignature(receiver, _getFixedMessageHash(receiver), v, r, s)) {
revert SA__InvalidSignature();
}
s_hasClaimedSnowman[receiver] = true;
i_snow.safeTransferFrom(receiver, address(this), CLAIM_AMOUNT);
i_snowman.mintSnowman(receiver, 1);
}
function _getFixedMessageHash(address receiver) internal view returns (bytes32) {
return _hashTypedDataV4(
keccak256(abi.encode(MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: CLAIM_AMOUNT})))
);
}

Required Merkle tree reconstruction:

Update input.json generation to match new approach and regenerate Merkle tree with consistent leaf structure.

Updates

Lead Judging Commences

yeahchibyke Lead Judge 19 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Invalid merkle-proof as a result of snow balance change before claim action

Claims use snow balance of receiver to compute the merkle leaf, making proofs invalid if the user’s balance changes (e.g., via transfers). Attackers can manipulate balances or frontrun claims to match eligible amounts, disrupting the airdrop.

Support

FAQs

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