Snowman Merkle Airdrop

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

Incorrect EIP-712 MESSAGE_TYPEHASH prevents valid Snowman claims

Root + Impact

Description

  • SnowmanAirdrop uses EIP-712 signatures to authorize SnowmanClaim operations

  • However, the contract defines the EIP-712 type hash incorrectly:

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

Risk

Likelihood:

  • The reciever field is declard as 'addres' instead of correct EIP-712 type address.

  • Consequently, the type hash used by the contract does not match the standard EIP-712 representation of the SnowmanClaim structure.

  • When a user signs the intended EIP-712 message using a standard wallet or EIP-712 implementation, the wallet hashes:

    SnowmanClaim(address reciever, uint256 amount)

    while the contract reconstruct the digest using :

    SnowmanClaim(addres reciever, uint256 amount)

    This produces different hashes, causing the signature recovery to fail

Impact:

The incorrect type hash breaks the signature-based Snowman claiming functionality.

Legitimate users can possess:

  • a valid Merkle proof,

  • the required Snow balance, and

  • a valid EIP-712 signature for the intended SnowmanClaim(address, uint256) structure,

yet still unable to claim because the contract reconstruct a different message digest.

This results in a denial of service for the signature-based claim path.

Proof of Concept

The included Foundry test generates a valid EIP-712 signature for the intended SnowmanClaim(address reciever, uint256 amount) structure using the user's private key. The test verifies that the signature correctly recovers the user's address. it the compares the correctly generated EIP-712 digest with SnowmanAIrdrop.getMessageHas(), demonstrating that the contract produces a different digest due to incorrect addres type in MESSAGE_TYPEHASH. Finally, the test submits the valid signatures to claimSnowman() and demonstrates that the transaction reverts with SA__InvalidSignature().


The PoC passes succesfully and demonstrates that the contract's EIP-712 digest is incompatible with the intended standard SnowClaim(address, uint256) message.

test/SnowmanAirdropPoC.t.sol

Recommended Mitigation

Replacing the incorrect type hash:


All of-chain signing code should use the exact same EIP-712 structure:

SnowmanClaim(address reciever, uint256 amount)

- 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 16 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!