Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
Submission Details
Impact: high
Likelihood: high

roken EIP-712 Signature Recovery

Author Revealed upon completion

Root + Impact

Description

  • The contract uses EIP-712 typed signatures to ensure that a valid Snowman claim is authorized before processing a Merkle proof and minting an NFT.

  • The signature check only verifies that the receiver signed their own data, which means anyone can self-sign and pass the check. This defeats the entire purpose of using ECDSA-based authorization

function _isValidSignature(address receiver, bytes32 digest, uint8 v, bytes32 r, bytes32 s)
internal
pure
returns (bool)
{
@> (address actualSigner,,) = ECDSA.tryRecover(digest, v, r, s);
@> return actualSigner == receiver;
}

Risk

Likelihood:

  • Any externally owned account can sign their own claim and satisfy the signature check.

  • No off-chain authorization or role-based signing is enforced, allowing all claims to self-validate.

Impact:

  • All signature-based protections are bypassed, reducing the Merkle proof to the only real gate — which itself depends on live balance (also broken).

  • Any address can mint Snowman NFTs without trusted approval.

Proof of Concept

// Any user can do this from a web3 wallet or script
address receiver = msg.sender;
uint256 amount = snow.balanceOf(receiver);
// Get Merkle proof from a snapshot tool
bytes32[] memory proof = ...;
// Sign own claim locally (in browser, script, or dApp)
bytes32 digest = airdrop.getMessageHash(receiver);
(bytes32 r, bytes32 s, uint8 v) = sign(receiverPrivateKey, digest);
// Claim Snowman NFT
airdrop.claimSnowman(receiver, proof, v, r, s); // succeeds even though no off-chain validation occurred

Recommended Mitigation

- return actualSigner == receiver;
+ // Require that a trusted signer (not the user) signed the claim
+ return actualSigner == TRUSTED_SIGNER;

Support

FAQs

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