The claimSnowman()
function in the SnowmanAirdrop
contract allows eligible users to claim Snowman NFTs by submitting a valid EIP-712 signature and Merkle proof. This signature is verified using ECDSA.tryRecover()
to ensure it was signed by the claimant (receiver
). The contract assumes the recovered address is unique and correct, and relies on it to authorize the claim.
Explain the specific issue or problem in one or more sentences
ECDSA.tryRecover()
The contract uses ECDSA.tryRecover()
to verify claim signatures, but it does not enforce a canonical signature format. This opens the door to signature malleability, where multiple (r, s, v)
combinations can still produce a valid signature for the same message hash.
Although on-chain claims are tracked by address (s_hasClaimedSnowman[receiver]
), this can lead to:
Front-running risks, where a malicious actor preempts a user’s claim with a different valid signature.
Replay issues in off-chain systems that assume signatures are unique.
Likelihood:
Reason 1 // Describe WHEN this will occur (avoid using "if" statements)
This occurs whenever a user signs a valid claim message, and a malicious actor observes the message and front-runs the transaction by submitting a malleated version of the signature (e.g., with a different s
or v
value) before the original sender's transaction is mined.
Reason 2
It is also triggered in any off-chain or relayer-dependent system that assumes ECDSA signatures are unique and unforgeable. The attacker can exploit this by replaying a differently structured signature with the same hash.
Impact:
Impact 1
A malicious actor may front-run a legitimate claim, making the real user ineligible afterward due to s_hasClaimedSnowman[receiver] = true
, thereby stealing the NFT or reward tied to the claim.
Impact 2
In systems integrated off-chain (e.g., with relayers or analytics), duplicate or unexpected signature formats could lead to inconsistent state, misattribution of rewards, or failed verification due to signature format mismatch.
/ Simulate a front-run attack using a malleable ECDSA signature
// Original user signs a valid claim message
(address receiver, uint256 amount) = (0xAlice, 100 ether);
bytes32 hash = snowmanAirdrop.getMessageHash(receiver);
// Signature is malleable: both (v, r, s) and (v', r, s') are valid for the same hash
// Attacker observes the signature and crafts another valid one
(v1, r, s1) = originalSignature;
(v2, r, s2) = malleatedSignature; // s2 = -s1 mod N (ECDSA curve order)
// Attacker submits the claim before the original user
snowmanAirdrop.claimSnowman(receiver, proof, v2, r, s2);
// Original user’s later claim fails because s_hasClaimedSnowman[receiver] == true
This works because ECDSA.tryRecover()
accepts both the original and the malleated signature as valid, recovering the same signer address.
The vulnerability stems from the use of ECDSA.tryRecover()
which does not enforce signature non-malleability. This means a valid ECDSA signature can have multiple representations due to the flexible s
value — a known risk in Ethereum cryptography.
This allows attackers to submit alternative, yet still valid, versions of a signature, which can interfere with replay protection and signature uniqueness assumptions in systems like your airdrop.
Use OpenZeppelin’s ECDSA.recover()
instead of tryRecover()
. Unlike tryRecover()
, the recover()
method strictly validates the signature by rejecting malleable forms (i.e., signatures where s
is not in the lower half of the secp256k1 curve order).
Replace the following code in _isValidSignature()
:
(address actualSigner,,) = ECDSA.tryRecover(digest, v, r, s);
return actualSigner == receiver;
With this safer version:
address actualSigner = ECDSA.recover(digest, v, r, s);
return actualSigner == receiver;
Ensures that only canonical signatures are accepted.
Prevents signature replay or double-processing using malleated signatures.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.