SnowmanAirdrop uses EIP-712 typed-data signatures so that a third party can submit a claim on a recipient's behalf using that recipient's (v, r, s). The signed struct is SnowmanClaim(address receiver, uint256 amount), and the contract commits to it through a type hash constant:
The encoded type string is wrong in two ways. It says addres (missing a letter), and it contains spaces. EIP-712 requires the exact, space-free encoding SnowmanClaim(address receiver,uint256 amount). Every compliant signer — MetaMask's eth_signTypedData_v4, ethers, viem — hashes the correct string when producing a signature, so the digest they sign is different from the one getMessageHash / _isValidSignature reconstructs on-chain. The recovered address therefore never matches receiver, and the claim reverts with SA__InvalidSignature.
Impact: Medium. The delegated-claim path — the whole reason the v, r, s arguments exist — cannot be used by anyone signing with standard tooling. A legitimately-signed claim is always rejected, so a core, in-scope feature is broken (a denial of service on that flow).
Likelihood: High. This happens for every real user, unconditionally. Any wallet or library that follows EIP-712 (i.e. all of them) produces a signature the contract will reject.
A correctly-formed EIP-712 signature (built with the standard type string, exactly what a wallet emits) is rejected by the contract:
Because the digest the wallet signs uses the correct type string while the contract rebuilds it from the malformed one, the recovered signer never equals receiver, so the claim always reverts. The only signature that passes is one hand-crafted to reproduce the contract's typo, which no standard wallet will ever produce.
Use the exact EIP-712 encoding:
# 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.