Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: medium
Likelihood: high
Invalid

Merkle Leaf Uses Mutable Snow Balance Causing Permanent Claim Inconsistency

TITLE

Merkle Leaf Uses Mutable Snow Balance Causing Permanent Claim Inconsistency

IMPACT

Medium

LIKELIHOOD

High

SCOPE

src/SnowmanAirdrop.sol
src/Snow.sol
script/SnowMerkle.s.sol
script/GenerateInput.s.sol

DESCRIPTION

Normal Behavior

The protocol uses a Merkle tree to define which users are eligible to claim Snowman NFTs.
Each Merkle leaf is expected to represent a fixed snapshot of a user’s entitlement, so that
a valid Merkle proof remains valid at claim time.

Users stake Snow tokens and receive Snowman NFTs proportional to their Snow token holdings.
The Merkle root is deployed once, and users later submit Merkle proofs to claim their NFTs.

Specific Issue

The Merkle leaf construction depends on the user’s Snow token balance, which is dynamically
queried at claim time using balanceOf(receiver). Since Snow balances are mutable after the
Merkle tree is generated, the Merkle leaf computed during verification can differ from the
leaf used to build the Merkle tree.

As a result, valid users can become permanently unable to claim their Snowman NFTs due to
normal balance changes that occur after Merkle root generation.

ROOT CAUSE + IMPACT

Root Cause

The Merkle leaf is constructed using a mutable runtime value instead of an immutable snapshot.

In SnowmanAirdrop.sol:

@> uint256 amount = i_snow.balanceOf(receiver);

@> bytes32 leaf = keccak256(
@> abi.encodePacked(receiver, amount)
@> );

@> require(
@> MerkleProof.verify(proof, s_merkleRoot, leaf),
@> "Invalid proof"
@> );

The amount value is not stored or snapshotted when the Merkle tree is generated. Instead,
it is recomputed at claim time using the current Snow token balance.

Risk

Likelihood:

  1. Snow token balances are expected to change regularly due to weekly earning, purchasing,
    transfers, or staking.

  2. Merkle root generation and user claims do not occur atomically, making balance drift a
    normal and expected scenario.

Impact:

  1. Legitimate users can be permanently blocked from claiming Snowman NFTs despite being
    included in the Merkle tree.

  2. Claim eligibility becomes time-dependent and unpredictable, violating standard Merkle
    airdrop assumptions and breaking user trust.

PROOF OF CONCEPT

  1. Generate the Merkle tree at time T0 when Alice has a Snow balance of 100.

    • Merkle leaf used: hash(Alice, 100)

  2. Deploy the Merkle root to the SnowmanAirdrop contract.

  3. Before Alice submits her claim, her Snow balance changes (e.g., she earns weekly Snow),
    resulting in a new balance of 120.

  4. Alice submits her Merkle proof.

  5. The contract computes the leaf at claim time as:

    • hash(Alice, 120)

  6. Merkle proof verification fails because:

    • hash(Alice, 120) ≠ hash(Alice, 100)

This failure occurs through normal protocol usage and permanently prevents Alice from
claiming her Snowman NFTs.

RECOMMENDED MITIGATION

Use an immutable snapshot value for Merkle leaf construction instead of a runtime balance.

For example:

  • remove this code

  • uint256 amount = i_snow.balanceOf(receiver);

  • add this code

  • uint256 amount = snapshottedAmount[receiver];

Alternatively, include the entitlement amount directly in the Merkle leaf and verify it
without recomputing it from mutable on-chain state.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 11 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!