Snowman Merkle Airdrop

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

False Balance-Based Merkle Proof Verification

Description

The airdrop contract contains a critical race condition vulnerability where the user's token balance is read at claim time to generate the merkle leaf, rather than using a predetermined allocation amount. This creates a vulnerability where users can manipulate their balance between the merkle proof generation and the actual claim execution.

Risk

Likelihood: HIgh

  • The balance check occurs at runtime during the claim transaction

  • No time locks or balance snapshots prevent manipulation

  • Attackers can front-run their own transactions to optimize timing

  • The vulnerability can be exploited repeatedly by different users

Impact:

  • Protocol Failure: The entire airdrop mechanism becomes unreliable and exploitable

  • Fund Drainage: Contract may fail to collect the intended tokens from users



Root cause

The contract dynamically calculates the merkle leaf using current balance instead of the predetermined allocation amount that was used to build the original merkle tree.

- uint256 amount = i_snow.balanceOf(receiver);
bytes32 leaf = keccak256(
bytes.concat(keccak256(abi.encode(receiver, amount)))
);
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert SA__InvalidProof();
}
i_snow.safeTransferFrom(receiver, address(this), amount);

Proof of Concept

Setup Phase:

  • Alice is allocated 100 tokens in the merkle tree

  • Alice approves the airdrop contract for 100 tokens

  • Merkle tree contains leaf: keccak256(abi.encode(alice, 100))

Manipulation Phase:

  • Alice transfers 50 tokens to Bob

  • Alice's balance drops to 50 tokens

  • Approval remains at 100 tokens (sufficient for transfer)

Claim Attempt:

  • Contract reads i_snow.balanceOf(alice) = 50

  • Contract generates leaf: keccak256(abi.encode(alice, 50))

  • Merkle proof verification fails (proof was for amount=100, not amount=50)

  • Transaction reverts with SA__InvalidProof()

Security Implication: This test reveals that users cannot claim if they reduce their balance, but more importantly, it shows the contract's merkle verification is unreliable and dependent on mutable state rather than immutable allocations.

function testClaimFailsIfBalanceChanges() public {
uint256 allocation = 100;
deal(address(snow), alice, allocation);
vm.prank(alice);
snow.approve(address(airdrop), allocation);
// Generate digest and signature for Alice with allocation = 100
bytes32 digest = airdrop.getMessageHash(alice);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alKey, digest);
// Alice transfers out 50 tokens before claiming
vm.prank(alice);
snow.transfer(bob, 50);
// Alice tries to claim herself
vm.prank(alice);
vm.expectRevert();
airdrop.claimSnowman(alice, AL_PROOF, v, r, s);
}

Recommended Mitigation

Implement Fixed Allocation Parameter with Balance Validation :

function claimSnowman(
address receiver,
+ uint256 allocation, // Fixed allocation from merkle tree
bytes32[] calldata merkleProof,
uint8 v,
bytes32 r,
bytes32 s
) external nonReentrant {
Updates

Lead Judging Commences

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