Snowman Merkle Airdrop

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

Typo in MESSAGE_TYPEHASH Completely Breaks Signature Verification

Root + Impact

Typo in MESSAGE_TYPEHASH ("addres" instead of "address") causes all signature verifications to fail, making the delegated claim functionality completely non-functional.

Description

The EIP-712 signature verification system requires the MESSAGE_TYPEHASH to exactly match the type signature used by signers when creating signatures. The contract should use the correctly spelled type hash "SnowmanClaim(address receiver, uint256 amount)" to validate signatures from users claiming their Snowman NFTs.

The MESSAGE_TYPEHASH contains a typo where "address" is misspelled as "addres", causing all signature verifications to fail since the hash generated by the contract will never match the hash used by signers with the correct spelling.

// SnowmanAirdrop.sol
bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
// @> "addres" instead of "address"

Risk

Likelihood:HIGH

  • Reason 1: This occurs on every single signature verification attempt since the typo is in a constant used for all validations

  • Reason 2: Any user attempting to claim via signature delegation will have their transaction revert

Impact:HIGH

  • Impact 1:The entire signature-based claiming mechanism is non-functional

  • Impact 2:Core protocol functionality is completely broken from deployment

Proof of Concept

function testSignatureVerificationFails() public {
// User creates signature with correct spelling
bytes32 correctTypeHash = keccak256("SnowmanClaim(address receiver, uint256 amount)");
// Contract uses incorrect spelling
bytes32 contractTypeHash = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
// These will never match
assertNotEq(correctTypeHash, contractTypeHash);
// Any claim attempt with valid signature will revert with SA__InvalidSignature
}

This test demonstrates that the contract's typo produces a different hash than the correct spelling, causing all signature verifications to fail. When a user signs a message using the correct "address" spelling, the contract computes a different hash using "addres", resulting in signature mismatch and transaction revert.

Recommended Mitigation

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

Fix the typo by changing "addres" to "address" in the MESSAGE_TYPEHASH constant. This ensures the contract's type hash matches what signers generate, enabling successful signature verification for delegated claims.

Updates

Lead Judging Commences

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