Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: medium
Invalid

High: Malleable Signature Vulnerability in _isValidSignature() - Unauthorized Claim Replays & NFT Theft

Root + Impact

Description

  • Normal behavior:
    Signature verification should ensure cryptographic uniqueness and resistance to tampering, using standardized formats such as EIP-712.

  • Issue:
    The contract performs signature verification using raw ecrecover without enforcing EIP-712 typed data standards. This allows for malleable signatures, where the same message can be verified using multiple valid signature formats. An attacker can manipulate a valid signature into an alternate form (changing s to n - s) and reuse it to claim unauthorized rewards.

// >>> Root cause: Signature recovered using malleable ecrecover without EIP-712 enforcement @>
function _isValidSignature(...) internal pure returns (bool) {
(address actualSigner,,) = ECDSA.tryRecover(digest, v, r, s); // @> Malleable
return actualSigner == receiver;
}

Risk

Likelihood:

  • Medium — Signature malleability is a well-documented cryptographic property of ECDSA.

  • Reproducibility: Exploit is reliable once a valid signature is known.

  • Ease of exploitation: Requires understanding of s malleability and access to one valid signature.

Impact:

  • Replay attacks with altered signature formats.

  • Unauthorized NFT minting or token claiming.

  • User allocation theft if an attacker can reuse or intercept claim data.

  • Loss of protocol trust and value dilution.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/SnowmanAirdrop.sol";
contract MalleabilityPoC is Test {
SnowmanAirdrop airdrop;
bytes32[] validProof;
bytes memory originalSig;
bytes memory malleableSig;
function testSignatureMalleability() public {
// Get original valid signature
originalSig = getSignature(attacker);
// Modify s -> n - s, flip v bit
malleableSig = forgeMalleableSig(originalSig);
// Both are accepted as valid
assertTrue(airdrop._isValidSignature(..., originalSig));
assertTrue(airdrop._isValidSignature(..., malleableSig)); // Should fail
}
}

Explanation:

  • A valid ECDSA signature can be modified (s ↔ n-s) and still pass verification.

  • Because the contract uses raw signature recovery, both original and modified forms are accepted.

  • A malicious actor can forge duplicates and bypass unique claim protections.


Recommended Mitigation

Implement EIP-712 signature standardization using typed structured data and domain separation. This prevents replay and signature malleability vulnerabilities.

function _isValidSignature(...) internal view returns (bool) {
- (address actualSigner,,) = ECDSA.tryRecover(digest, v, r, s);
+ bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
+ MESSAGE_TYPEHASH,
+ SnowmanClaim({receiver: receiver, amount: amount})
+ )));
+ address actualSigner = ECDSA.recover(digest, v, r, s);
return actualSigner == receiver;
}

Explanation:

  • Solution: Applies EIP-712 hashing to generate unique digest using domain separator.

  • Security: Prevents malleable signature formats and replay attacks.

  • Scalability: Standard approach across the Ethereum ecosystem.

  • Gas impact: Minimal additional cost for _hashTypedDataV4.

Severity Note:

This is a medium to high-severity vulnerability depending on the attack context. While the bug requires some technical skill, its implications on trust, claim validation, and allocation control are serious.

Verification confirms proper functionality:

function testValidSignatureWithEIP712() public {
bytes memory validSignature = signTypedData(receiver, amount);
assertTrue(airdrop._isValidSignature(..., validSignature));
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.