Merkle Leaf Uses Mutable Snow Balance Causing Permanent Claim Inconsistency
Medium
High
src/SnowmanAirdrop.sol
src/Snow.sol
script/SnowMerkle.s.sol
script/GenerateInput.s.sol
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.
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.
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.
Snow token balances are expected to change regularly due to weekly earning, purchasing,
transfers, or staking.
Merkle root generation and user claims do not occur atomically, making balance drift a
normal and expected scenario.
Legitimate users can be permanently blocked from claiming Snowman NFTs despite being
included in the Merkle tree.
Claim eligibility becomes time-dependent and unpredictable, violating standard Merkle
airdrop assumptions and breaking user trust.
Generate the Merkle tree at time T0 when Alice has a Snow balance of 100.
Merkle leaf used: hash(Alice, 100)
Deploy the Merkle root to the SnowmanAirdrop contract.
Before Alice submits her claim, her Snow balance changes (e.g., she earns weekly Snow),
resulting in a new balance of 120.
Alice submits her Merkle proof.
The contract computes the leaf at claim time as:
hash(Alice, 120)
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.
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.
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.