Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Severity: high
Valid

MESSAGE_TYPEHASH typo "addres" breaks EIP-712 standard compatibility

Title: MESSAGE_TYPEHASH typo "addres" breaks EIP-712 standard compatibility
Impact: High. External EIP-712 tools and wallets cannot generate valid claim signatures.
Likelihood: High. Deterministic — all standard EIP-712 libraries use "address" not "addres".
Reference Files: repos/src/SnowmanAirdrop.sol:49

Description

The MESSAGE_TYPEHASH constant contains a typo: "SnowmanClaim(addres receiver, uint256 amount)" — "addres" instead of the correct "address". The EIP-712 standard specifies the Solidity type name address. While internal signing and verification both use the same typo and work correctly within the protocol, any external tool, wallet, or library (ethers, viem, wagmi) generates EIP-712 signatures using the standard type string, producing a different typeHash that fails on-chain verification.

bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
// keccak256("SnowmanClaim(address receiver, uint256 amount)") → DIFFERENT HASH

This breaks the entire delegation mechanism — the protocol advertises "someone else can claim on behalf of the recipient using signatures," but no standard wallet can produce a valid signature.

Risk

Impact: High. The signature delegation feature is effectively unusable with standard tooling. Any third-party integration, wallet, or dApp attempting to generate EIP-712 signatures for claim delegation will produce incompatible signatures that revert with SA__InvalidSignature. The protocol's stated functionality is broken for external consumers.
Likelihood: High. Every standard EIP-712 implementation uses address as the type name. The typo is baked into the immutable contract bytecode and cannot be fixed without redeployment.
Every integration attempt via ethers.js, viem, wagmi, or any EIP-712 library will fail silently — signatures appear valid off-chain but revert on-chain.

Proof of Concept

function testTypoBreaksStandardEIP712() public {
bytes32 typoHash = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
bytes32 correctHash = keccak256("SnowmanClaim(address receiver, uint256 amount)");
assertFalse(typoHash == correctHash); // Hashes differ!
}

The PoC proves the typo hash is different from the standard EIP-712 hash.

Recommended Mitigation

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

Fix the typo from "addres" to "address". Redeploy with a new Merkle root.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 23 hours ago
Submission Judgement Published
Validated
Assigned finding tags:

[H-02] Unconsistent `MESSAGE_TYPEHASH` with standart EIP-712 declaration on contract `SnowmanAirdrop`

# Root + Impact ## Description * Little typo on `MESSAGE_TYPEHASH` Declaration on `SnowmanAirdrop` contract ```Solidity // src/SnowmanAirdrop.sol 49: bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)"); ``` **Impact**: * `function claimSnowman` never be `TRUE` condition ## Proof of Concept Applying this function at the end of /test/TestSnowmanAirdrop.t.sol to know what the correct and wrong digest output HASH. Ran with command: `forge test --match-test testFrontendSignatureVerification -vvvv` ```Solidity function testFrontendSignatureVerification() public { // Setup Alice for the test vm.startPrank(alice); snow.approve(address(airdrop), 1); vm.stopPrank(); // Simulate frontend using the correct format bytes32 FRONTEND_MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver, uint256 amount)"); // Domain separator used by frontend (per EIP-712) bytes32 DOMAIN_SEPARATOR = keccak256( abi.encode( keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), keccak256("Snowman Airdrop"), keccak256("1"), block.chainid, address(airdrop) ) ); // Get Alice's token amount uint256 amount = snow.balanceOf(alice); // Frontend creates hash using the correct format bytes32 structHash = keccak256( abi.encode( FRONTEND_MESSAGE_TYPEHASH, alice, amount ) ); // Frontend creates the final digest (per EIP-712) bytes32 frontendDigest = keccak256( abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, structHash ) ); // Alice signs the digest created by the frontend (uint8 v, bytes32 r, bytes32 s) = vm.sign(alKey, frontendDigest); // Digest created by the contract (with typo) bytes32 contractDigest = airdrop.getMessageHash(alice); // Display both digests for comparison console2.log("Frontend Digest (correct format):"); console2.logBytes32(frontendDigest); console2.log("Contract Digest (with typo):"); console2.logBytes32(contractDigest); // Compare the digests - they should differ due to the typo assertFalse( frontendDigest == contractDigest, "Digests should differ due to typo in MESSAGE_TYPEHASH" ); // Attempt to claim with the signature - should fail vm.prank(satoshi); vm.expectRevert(SnowmanAirdrop.SA__InvalidSignature.selector); airdrop.claimSnowman(alice, AL_PROOF, v, r, s); assertEq(nft.balanceOf(alice), 0); } ``` ## Recommended Mitigation on contract `SnowmanAirdrop` Line 49 applying this: ```diff - bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)"); + bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver, uint256 amount)"); ```

Support

FAQs

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

Give us feedback!