Snowman Merkle Airdrop

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

Typo in `SnowmanAirdrop::MESSAGE_TYPEHASH` Breaks EIP-712 Signature Verification.

[H-2] Typo in SnowmanAirdrop::MESSAGE_TYPEHASH Breaks EIP-712 Signature Verification.

Description: The contract defines an EIP-712 typeHash for the struct SnowmanClaim, but contains a typo in the struct's type string:

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

The word "addres" is not a valid Solidity type and should be "address". Since EIP-712 requires exact string encoding of struct types for hashing and signature verification, this typo causes the hash to mismatch the intended structure.
As a result, the getMessageHash() function will produce an incorrect digest, leading to signature verification failure when using _hashTypedDataV4().

Impact:

  • Any off-chain EIP-712 signatures will not match the on-chain MESSAGE_TYPEHASH, causing signature validation logic to fail silently.

  • The airdrop mechanism becomes non-functional.

  • Users will be unable to claim Snowman NFTs using valid signatures.

  • The entire airdrop or delegation mechanism becomes non-functional, defeating the core purpose of signature-based access control.

  • This may result in loss of trust, inaccessible rewards, or a complete failure of the signature-based claim flow.

Proof of Concept: This test shows how the incorrect MESSAGE_TYPEHASH breaks EIP-712 signature validation, even if everything else is correct.

Alice’s off-chain signature is based on: keccak256("SnowmanClaim(address receiver, uint256 amount)")
The contract computes the digest using:
keccak256("SnowmanClaim(addres receiver, uint256 amount)")
→ This changes the keccak256 output entirely.

As a result: ecrecover(digest, v, r, s) will return the wrong address.

Add this into the TestSnowmanAirdrop.t.sol:

function testInvalidTypeHashBreaksSignature() public {
vm.prank(alice);
snow.approve(address(airdrop), 1);
uint256 amount = snow.balanceOf(alice);
// ✅ Construct the CORRECT typeHash manually (should be in frontend or external signer)
bytes32 correctTypeHash = keccak256("SnowmanClaim(address receiver, uint256 amount)");
// ❌ The contract used keccak256("SnowmanClaim(addres receiver, uint256 amount)")
// So this digest should differ
// Reconstruct structHash manually using correct type string
bytes32 structHash = keccak256(abi.encode(correctTypeHash, alice, amount));
// Get domain separator from contract (after adding getDomainSeparator())
bytes32 domainSeparator = airdrop.getDomainSeparator();
// Final EIP-712 digest (as frontend/off-chain would compute it)
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01", domainSeparator, structHash
));
// Alice signs correct digest (what the frontend would do)
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alKey, digest);
// Revert expected: contract will hash something different (wrong MESSAGE_TYPEHASH)
vm.expectRevert();
vm.prank(satoshi);
airdrop.claimSnowman(alice, AL_PROOF, v, r, s);
}

Also add this into SnowAirdrop.sol:

function getDomainSeparator() external view returns (bytes32) {
return _domainSeparatorV4();
}

Recommended Mitigation: Fix the typo in the 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

yeahchibyke Lead Judge 5 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Inconsistent MESSAGE_TYPEHASH with standard EIP-712 declaration

A typo in the `MESSAGE_TYPEHASH` variable of the `SnowmanAirdrop` contract will prevent signature verification claims. Used `addres` instead of `address`

Support

FAQs

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