The EIP-712 MESSAGE_TYPEHASH should be computed from the exact canonical type string of the SnowmanClaim struct, enabling any standard EIP-712 compliant wallet or library (MetaMask, ethers.js, viem, Ledger) to generate valid signatures.
The type string contains a typo — "addres" instead of "address" — producing a different bytes32 hash than every EIP-712 compliant tool will compute. Any signature built using the correct type string will always fail _isValidSignature and revert with SA__InvalidSignature, making the entire claim flow inaccessible through standard tooling.
Likelihood:
Guaranteed to affect every user who generates a signature with any standard EIP-712 signing tool — the typehash mismatch is deterministic and permanent.
Occurs on the very first claim attempt by any user.
Impact:
The entire airdrop is permanently broken. No legitimate user can ever successfully call claimSnowman because their valid signatures will always be rejected.
Users who discover the typo and intentionally sign the malformed string bypass EIP-712 protections, undermining the security model.
The test first asserts that the contract's typo'd typehash and the correctly spelled typehash produce different bytes32 values, establishing that the mismatch is deterministic. It then simulates the exact failure every real user will encounter: Alice signs a claim message using the canonical EIP-712 type string "SnowmanClaim(address receiver, uint256 amount)" — the string every wallet, hardware device, and signing library uses. The contract internally hashes the message using its typo'd string, producing a different digest. When ECDSA recovery is applied to Alice's valid signature against the wrong digest, the recovered address does not match Alice's address and SA__InvalidSignature is thrown. The vm.expectRevert assertion confirms the revert, proving no legitimately produced off-chain signature can ever pass verification.
To run: forge test --match-test test_TypoMakesStandardSignatureAlwaysFail -vvvv
Fix the typo in the type string by replacing the "addres" with "address"
# 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)"); ```
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.