Snowman Merkle Airdrop

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

[M] MESSAGE_TYPEHASH does not strictly follow the EIP-712 standard types

Root + Impact

Description

In SnowmanAirdrop.sol:50, MESSAGE_TYPEHASH uses the string SnowmanClaim(addres receiver, uint256 amount), where "addres" is not the valid type "address" as specified by EIP-712. This implementation is inconsistent with the comment "EIP-712 compliant message signing", creating a compatibility risk with the standard EIP-712 signing flow: standard wallets/SDKs typically perform typed data encoding and validation according to the legally defined types, which may lead to signature rejection or a mismatch between the off-chain hash and the on-chain hash, causing the claim process to fail. Although the frontend can non‑standardly force alignment with the erroneous string to "adapt to the current contract", this practice breaks standard interoperability and increases risks for third-party integration, maintenance, upgrades, and multi-client compatibility.

// Root cause in the codebase with @> marks to highlight the relevant section
@> bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");

Risk

Likelihood: Medium

  • The issue is highly likely to be triggered in real integrations because standard EIP-712 tooling expects valid canonical type definitions (e.g., address), while the contract hardcodes a non-standard type string (addres) in MESSAGE_TYPEHASH. In typical wallet/SDK flows (eth_signTypedData_v4), this mismatch commonly causes signing rejection or digest divergence between off-chain and on-chain computations. Exploitation does not require privileged access; normal user claim attempts through standard clients are sufficient to surface the failure.


Impact: Medium

  • A malformed EIP-712 type definition in MESSAGE_TYPEHASH ("addres" instead of "address") breaks standards compliance and can cause signature-generation/verification incompatibility across common wallets and SDKs. As a result, legitimate users may be unable to produce signatures that match the on-chain digest, leading to failed claims and degraded protocol usability. While this issue does not directly enable unauthorized fund theft, it can effectively deny intended functionality and create significant integration/operational friction for third-party relayers and client applications.

Recommended Mitigation

In MESSAGE_TYPEHASH, we have changed the original incorrect type "addres" to the standard type "address."

- 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 1 hour 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!