Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
Submission Details
Impact: high
Likelihood: high

Incorrect Signature Validation in SnowmanAirdrop Contract

Author Revealed upon completion

Root + Impact

Description

  • Expected: The EIP-712 getMessageHash function in SnowmanAirdrop should generate a valid type hash using the correct type signature string, which matches the one used to generate off-chain signatures (e.g., via MetaMask, Ethers.js, Ledger, etc.). This type hash is critical to ensuring deterministic domain separation and preventing signature forgery.

  • Bug: The current implementation contains a typo in the type string used to generate the EIP-712 MESSAGE_TYPEHASH:

// ❌ Incorrect typehash
bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");

Note that "addres" is misspelled — it should be "address". As a result:

  • The on-chain hashTypedDataV4 will never match any valid off-chain signature, since every EIP-712 signer will hash a different string.

  • Signature recovery will fail, and no user will be able to claim their NFT via signature-based verification.

  • This completely breaks the EIP-712 part of the airdrop flow.

Risk

Likelihood:

  • This issue will always occur when someone tries to submit a valid EIP-712 signature.

  • Off-chain wallets (e.g., MetaMask) use correct EIP-712 formats, but the contract is using a mismatched hash due to the typo.

  • Thus, signature-based claims will consistently fail, even for legitimate users.

Impact:

  • This leads to a complete denial-of-service for all users trying to claim NFTs via signature verification.

  • Breaks hybrid Merkle + EIP-712 claim mechanisms, which may have been used to prevent Sybil attacks.

  • Destroys user trust and utility of the signature system — even users with valid airdrop entries cannot claim.

  • Airdrop becomes non-functional for users relying on signatures.

Proof of Concept

// Simulated EIP-712 hash generation (off-chain, correct format)
bytes32 correctTypeHash = keccak256("SnowmanClaim(address receiver, uint256 amount)");
// Actual contract value
bytes32 buggyTypeHash = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
assert(correctTypeHash != buggyTypeHash); // ✅ Passes — the hashes differ due to typo

This mismatch causes ECDSA signature recovery (ECDSA.recover) to fail, as the message digest signed off-chain will never match the one computed on-chain.

Recommended Mitigation

- bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
+ bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver, uint256 amount)");

Explanation:

Correct the typo in the EIP-712 type string. Once fixed:

The on-chain digest will match the one signed off-chain.

The recovered signer will be valid, and signature-based claims will succeed.

Signature validation becomes secure and functional as intended.

If additional EIP-712 fields are added in the future (e.g., deadline, nonce), ensure the type string is updated accurately and tested using eth_signTypedData_v4.

Support

FAQs

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