Snowman Merkle Airdrop

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

Dynamic EIP-712 Hash Breaks Signature Validity with Changing Token Balance

Root + Impact

Description

Normally, EIP-712 signature verification should involve static, signed data — a fixed message that the user signs once off-chain.

  • In this contract, the signed message includes the user's balanceOf amount, which can change between signing and claiming. This makes the digest unstable, invalidating the signature if the balance changes.

function getMessageHash(address receiver) public view returns (bytes32) {
uint256 amount = i_snow.balanceOf(receiver); // <--- balance is dynamic
return _hashTypedDataV4(
keccak256(abi.encode(MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: amount})))
);
}

Risk

Likelihood:

Occurs if a user signs a message while holding 1000 tokens, but the balance changes (e.g., due to transfer) before claiming.

  • Even if the Merkle proof is correct and the signature is valid at the time of signing, the hash used in _isValidSignature() will not match, causing rejection.

Impact:

Users are unable to claim NFTs if their balance changes between signing and claiming.

  • Airdrop UX is severely degraded and prone to unexplained signature failures.

Proof of Concept

The contract generates the EIP-712 message hash dynamically using the user's current token balance This means the signature is only valid as long as the user's balance stays the same between signing the message off-chain and calling claimSnowman on-chain. For instance, a user might generate a valid signature off-chain while holding 1,000 Snow tokens. If the user transfers tokens, receives more tokens, or interacts with any other contract that alters their Snow balance before claiming, the amount used in the EIP-712 message will differ from the originally signed value. This will change the computed digest As a result, the recovered address from ECDSA signature verification will not match the original signer, causing the transaction to revert with SA__InvalidSignature(). This flaw turns a valid signature into an invalid one simply due to state changes outside the airdrop system, which defeats the purpose of using off-chain signing for deterministic authorization.

uint256 amount = i_sow.balanceOf(receiver);
_hashTypedDataV4(keccak256(abi.encode(...)))

Recommended Mitigation


By decoupling the EIP-712 hash input from the live token balance, users can pre-sign messages confidently without risk that their claim will be invalidated by any unrelated token transfer. This restores the deterministic behavior expected of off-chain signatures.

- uint256 amount = i_snow.balanceOf(receiver);
- bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
+ uint256 amount = _signedAmount; // passed as input
+ bytes32 leaf = keccak256(abi.encodePacked(receiver, amount));
Updates

Lead Judging Commences

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