Snowman Merkle Airdrop

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

Malformed EIP-712 type string (misspelled addres + extra spaces) rejects claims signed by standard wallets

Description

SnowmanAirdrop uses EIP-712 typed-data signatures to authorize claims, but the struct type string used to build the type hash is malformed:

bytes32 private constant MESSAGE_TYPEHASH =
keccak256("SnowmanClaim(addres receiver, uint256 amount)"); // @> "addres" + extra space

Two deviations from the canonical EIP-712 encoding of the struct SnowmanClaim(address receiver, uint256 amount):

  1. addres is misspelled — it should be address.

  2. There is an extra space after the comma; the canonical encodeType has no spaces: SnowmanClaim(address receiver,uint256 amount).

The contract is internally self-consistent (its getMessageHash uses this same constant), but the hash it signs over is not the EIP-712 hash that a standards-compliant signer produces.

Risk

Likelihood: Medium — any signer that builds the typed-data hash from the correct struct definition (i.e. any standard wallet / library using eth_signTypedData_v4 with the real field types) will compute a different digest and produce a signature the contract rejects.

Impact: Low — no funds are at risk; the effect is a standards/interoperability defect: standard tooling cannot produce accepted signatures, and claims must instead be signed over the contract's non-standard string.

Impact

Signatures generated by compliant EIP-712 tooling fail _isValidSignature, so claims made through standard wallet flows revert with SA__InvalidSignature. Integrators are forced to reproduce the exact (buggy) type string off-chain, which is error-prone and breaks interoperability with the EIP-712 standard.

Proof of Concept

The contract's type hash is keccak256("SnowmanClaim(addres receiver, uint256 amount)").

A standards-compliant signer for the same struct computes keccak256("SnowmanClaim(address receiver,uint256 amount)") (correct spelling, no spaces). These two pre-images differ, so their keccak256 hashes differ, so the EIP-712 struct hashes differ, so the final digests differ, so ECDSA.recover returns a different address than the one the contract expects — and the claim is rejected.

Concretely, the two type strings that get hashed are:

contract: "SnowmanClaim(addres receiver, uint256 amount)" // used on-chain
standard: "SnowmanClaim(address receiver,uint256 amount)" // what a compliant wallet uses

Any claim whose signature was produced against the standard string will revert at the signature check.

Recommended Mitigation

Use the canonical EIP-712 type string:

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

Lead Judging Commences

ai-first-flight-judge Lead Judge about 2 hours 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!