Snowman Merkle Airdrop

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

Incorrect EIP-712 Type Hash Can Break Typed-Data Signing Compatibility Impact: Low

Root + Impact

Description

  • The intended behavior is that users sign an EIP-712 typed-data message authorizing a SnowmanClaim. The airdrop contract then verifies that the recovered signer is the same as the receiver, allowing a third-party gas payer to submit the claim on the receiver’s behalf.

  • The issue is that the EIP-712 struct type string contains a typo: addres instead of address. This means the contract is not using the canonical typed-data encoding for SnowmanClaim(address receiver,uint256 amount). Although signatures can still be produced when using the contract’s own digest, standard wallets, frontends, SDKs, and off-chain typed-data builders may reject the type or generate a different digest than the contract expects.

// Root cause in the codebase with @> marks to highlight the relevant section
struct SnowmanClaim {
address receiver;
uint256 amount;
}
@> bytes32 private constant MESSAGE_TYPEHASH =
@> keccak256("SnowmanClaim(addres receiver, uint256 amount)");
function getMessageHash(address receiver) public view returns (bytes32) {
if (i_snow.balanceOf(receiver) == 0) {
revert SA__ZeroAmount();
}
uint256 amount = i_snow.balanceOf(receiver);
return _hashTypedDataV4(
@> keccak256(abi.encode(MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: amount})))
);
}

Risk

Likelihood: Medium

  • Claim signatures are expected to be generated off-chain by wallets or frontend tooling.

  • EIP-712 tooling expects valid Solidity ABI types, and addres is not a valid type.

  • Off-chain code using the correct address type will compute a different digest from the contract.

Impact: Low

  • Users may be unable to produce signatures that the contract accepts through normal wallet flows.

  • Frontend and backend integrations can fail even though the on-chain claim logic appears correct.

  • The project may need custom signing logic that hashes the typo exactly, increasing integration risk and user confusion.


Proof of Concept

The following test-style example demonstrates the mismatch. The contract hashes the misspelled type string, while standard off-chain typed-data code will hash the correct EIP-712 type string. These two type hashes are different, so the final digest and signature will also differ.

function testTypeHashMismatch() public pure {
bytes32 contractTypeHash =
keccak256("SnowmanClaim(addres receiver, uint256 amount)");
bytes32 canonicalTypeHash =
keccak256("SnowmanClaim(address receiver,uint256 amount)");
assertTrue(contractTypeHash != canonicalTypeHash);
}

This means a user signing canonical typed data for SnowmanClaim(address receiver,uint256 amount) will not produce a signature that matches the digest generated by getMessageHash.

Recommended Mitigation

Correct the EIP-712 type string to use the valid address type and remove the extra spaces to match the canonical EIP-712 representation commonly generated by tooling.

- bytes32 private constant MESSAGE_TYPEHASH =
- keccak256("SnowmanClaim(addres receiver, uint256 amount)");
+ bytes32 private constant MESSAGE_TYPEHASH =
+ keccak256("SnowmanClaim(address receiver,uint256 amount)");
//After changing this, update any frontend, script, or test signing code to use the same canonical struct:
`SnowmanClaim(address receiver,uint256 amount)`
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge 4 days 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!