"addres") Breaks Compatibility with Standard Signing LibrariesSeverity: Medium (functional flaw – signatures generated by standard tools will be invalid)
Impact: The MESSAGE_TYPEHASH constant contains a misspelled Solidity type name ("addres" instead of "address"). This causes any signature created with a correct, standards‑compliant EIP‑712 implementation to be rejected by the contract. Users relying on common wallets or libraries (e.g., ethers.js, viem) may be unable to claim their airdrop.
Affected Contract: SnowmanAirdrop.sol
EIP‑712 defines a precise method for encoding structured data for signing. The type string used in the MESSAGE_TYPEHASH must match the Solidity parameter types exactly. The contract declares:
The typo "addres" (missing the second s) deviates from the correct "address". Consequently, the hash of the type definition differs from the one used by any standard EIP‑712 implementation. Signatures produced by widely‑used tooling (Metamask, ethers _signTypedData, viem signTypedData) will be built using the correct type string and will fail the _isValidSignature check on‑chain.
The contract verifies a signature using:
MESSAGE_TYPEHASH is the keccak256 hash of the string "SnowmanClaim(addres receiver, uint256 amount)". A correct implementation would use "SnowmanClaim(address receiver,uint256 amount)" (note the space differences also matter, but the critical error is the misspelling of address). Any external signing library that builds the type hash from the struct’s ABI definition will generate a different value, and the final digest will not match the one computed by the contract. Therefore, all signatures created off‑chain using proper EIP‑712 libraries will be rejected.
Deploy the contract as is.
Use a standard EIP‑712 signing library (e.g., ethers v6) to sign a SnowmanClaim(address receiver,uint256 amount) message for a valid receiver.
Call claimSnowman with the resulting signature. The transaction will revert with SA__InvalidSignature().
Modify the constant to keccak256("SnowmanClaim(address receiver,uint256 amount)") and repeat – the signature now passes.
A Foundry test can demonstrate the mismatch:
The test will confirm that the signature is invalid despite being correctly formed for the expected struct.
Failed claims: Users who generate signatures using standard wallets or libraries (the most common approach) will be unable to claim their airdrop, causing frustration and potential loss of opportunity.
Requires custom, non‑standard signing code: The protocol would need to instruct users to manually construct a malformed type string, which is highly error‑prone and non‑trivial for non‑technical participants.
Degraded user experience: Breaks the seamless interaction expected from a properly implemented EIP‑712 scheme.
Correct the MESSAGE_TYPEHASH constant to the proper type string:
Note: The exact spacing inside the parentheses may also need to match the standard (commonly no space after the comma), but the critical fix is changing "addres" to "address". After this change, any compliant EIP‑712 library will produce signatures that pass verification.
# 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.