Snowman Merkle Airdrop

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

High: Snapshot-Validation Mismatch in claimSnowman() - Legitimate User Exclusion & Airdrop Failure

Root + Impact

Description

  • Normal behavior:
    Airdrop claim verification should use a consistent and verifiable data source—typically a snapshot taken before claims begin. Both Merkle tree inclusion and signature validity must be based on the same fixed data.

  • Issue:
    The contract incorrectly uses the current token balance for both Merkle proof generation and signature validation, despite the Merkle tree being built using snapshot balances. This leads to verification failure when users’ balances have changed since the snapshot, preventing valid users from claiming their rewards.

// >>> Root cause: Uses current balance instead of snapshot value, causing Merkle proof failures @>
function claimSnowman(...) external {
uint256 amount = i_snow.balanceOf(receiver); // @> Uses current balance
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
// Merkle tree was built with SNAPSHOT balances!
require(MerkleProof.verify(merkleProof, i_merkleRoot, leaf)); // @> Will fail if balance changed
}

Risk

Likelihood:

  • High — This issue affects all users who have transacted between snapshot and claim.

  • Reproducibility: Consistently fails for active token holders.

  • Ease of exploitation: No exploit needed; claims just fail naturally.

Impact:

  • Airdrop fails for legitimate users who no longer hold their full snapshot balance.

  • Protocol reward mechanism is non-functional.

  • Locked NFTs remain undistributed due to failed claims.

  • Severe user frustration and trust loss.

Proof of Concept

function testBrokenClaim() public {
// Snapshot: Alice holds 100 tokens
createMerkleTree(); // Merkle tree uses Alice: 100
// Alice transfers 50 tokens
vm.prank(alice);
snow.transfer(bob, 50);
// Alice attempts to claim
vm.prank(alice);
vm.expectRevert("Invalid proof"); // Uses balance = 50
airdrop.claimSnowman(alice, proof, v, r, s);
}

Explanation:

  • Merkle proof and signature were generated using Alice’s snapshot balance (100).

  • After transferring tokens, Alice’s current balance is 50.

  • The contract checks Merkle root using this new value — it fails.

  • Result: Alice is denied her rightful airdrop.


Recommended Mitigation

Introduce a snapshotAmount parameter to decouple real-time balances from verification logic, and ensure that both signature and Merkle proof match this static value.

function claimSnowman(
address receiver,
+ uint256 snapshotAmount,
bytes32[] calldata merkleProof,
uint8 v, bytes32 r, bytes32 s
) external nonReentrant {
- uint256 amount = i_snow.balanceOf(receiver);
+ uint256 amount = snapshotAmount;
- if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) {
+ if (!_isValidSignature(receiver, snapshotAmount, v, r, s)) {
revert SA__InvalidSignature();
}
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, snapshotAmount))));
require(MerkleProof.verify(merkleProof, i_merkleRoot, leaf), "Invalid proof");
// continue claim logic...
}

Explanation:

  • Fixes mismatch: Ensures snapshot data is consistent across Merkle proof and signature.

  • Restores functionality: All users who were eligible at snapshot can now claim.

  • Security: Avoids reliance on mutable runtime state (i.e., current balances).

  • Compatibility: Preserves rest of claim logic and structure.

Severity Note:

This is a high-severity logic bug. It causes widespread airdrop failure in any scenario where token holders trade or transfer tokens between snapshot and claim window. This breaks the promise of fair distribution and disrupts core protocol operations.

Verification confirms proper functionality:

function testClaimWithSnapshotAmount() public {
// Alice had 100 tokens at snapshot, now only 50
vm.prank(alice);
airdrop.claimSnowman(alice, 100, proof, v, r, s); // Succeeds using snapshot value
}
Updates

Lead Judging Commences

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