Snowman Merkle Airdrop

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

Replayable delegated claim signatures allow repeated Snowman NFT minting

Root + Impact

Description

The delegated claim functionality in SnowmanAirdrop allows third parties to claim NFTs on behalf of users using ECDSA signatures (v, r, s). However, the signed payload does not appear to include nonce invalidation or replay protection.

Under normal behavior, a signed authorization should only be usable once. After a delegated claim is executed, the signature should become permanently invalid to prevent repeated use of the same authorization.

The issue occurs because the protocol accepts the same valid signature multiple times without consuming or invalidating it after execution.

An attacker or relayer can:

  1. Obtain a valid signed claim authorization from a user

  2. Submit the delegated claim transaction

  3. Re-submit the exact same signature repeatedly

  4. Mint additional Snowman NFTs multiple times for the same authorization

Since signatures remain reusable, the delegated claim process becomes replayable.

This vulnerability is especially dangerous because signatures are intended to represent one-time user consent. Without nonce tracking, expiration validation, or replay protection, a single leaked or reused signature can be abused indefinitely.

// Vulnerable pattern
address signer = ecrecover(messageHash, v, r, s);
require(signer == user, "Invalid signature");
_mintSnowman(user);

The contract verifies that the signature is valid, but it does not verify whether the signature has already been used before.

A secure implementation should consume signatures after successful execution.

// Recommended secure logic
bytes32 digest = keccak256(signatureData);
require(!usedSignatures[digest], "Signature already used");
usedSignatures[digest] = true;
_mintSnowman(user);

Additionally, signatures should include:

  • user address

  • nonce

  • chain ID

  • contract address

  • expiration timestamp

to fully prevent replay attacks.


Risk

Likelihood:

  • Delegated claim signatures can be reused multiple times

  • The attack requires only a single valid signature from a user

  • Relayers or malicious actors can repeatedly submit the same calldata

  • No privileged access is required to exploit the issue

Impact:

  • Unlimited or repeated NFT minting becomes possible

  • Users lose control over how their signatures are used

  • Snowman NFT supply inflation damages collection integrity

  • Protocol reward accounting becomes unreliable

  • Attackers can automate replay attacks at low cost


Proof of Concept

// User signs delegated claim message off-chain
signature = sign(userPrivateKey, claimData);
// Attacker submits delegated claim
claimFor(user, signature);
// NFT is minted successfully
// Attacker reuses same signature again
claimFor(user, signature);
// Additional NFT is minted again
// Signature remains valid indefinitely

Execution Flow

  1. A user signs a delegated claim authorization

  2. The relayer submits the claim transaction

  3. The protocol validates the signature successfully

  4. NFTs are minted

  5. The same signature is submitted again

  6. The contract accepts it again because no replay protection exists

  7. Additional NFTs continue minting with the same authorization


Recommended Mitigation

The protocol should implement replay protection mechanisms for delegated signatures.

Recommended protections include:

  • Signature nonce tracking

  • Used-signature mappings

  • Expiration timestamps

  • EIP-712 typed structured data

  • Chain ID validation

  • Domain separation

- address signer = ecrecover(messageHash, v, r, s);
-
- require(signer == user, "Invalid signature");
-
- _mintSnowman(user);
+ bytes32 digest = keccak256(
+ abi.encode(user, nonce, address(this), block.chainid)
+ );
+ require(!usedDigests[digest], "Signature already used");
+ address signer = ecrecover(digest, v, r, s);
+ require(signer == user, "Invalid signature");
+ usedDigests[digest] = true;
+ _mintSnowman(user);

This ensures each delegated authorization can only be executed once and prevents replay attacks across multiple transactions or chains.

Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 6 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!