Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Valid

Signature Malleability Enables Double NFT Claims Leading to Protocol Inflation

Description

The SnowmanAirdrop::_isValidSignature function is vulnerable to ECDSA signature malleability attacks due to:

  • Lack of s-value range validation

  • Absence of signature tracking

  • Use of non-standard signature recovery
    This allows attackers to transform a single valid signature into a second valid signature for the same message. When combined with the claim process, this enables double NFT claims from one authorization.

Impact:

Double NFT Minting: Single user authorization mints NFTs twice

Protocol Inflation: Unauthorized 2x increase in NFT supply

Theft of Value: Dilutes NFT value for legitimate holders

Fundamental Trust Breakdown: Undermines cryptographic security guarantees

Risk

Likelihood:

• All ECDSA signatures inherently malleable
• Requires only signature observation (public mempool)
• Exploitable with basic cryptographic knowledge

Impact:

• Permanent 2x supply inflation per claim
• Direct financial loss to protocol
• Cascading devaluation of NFTs

Proof of Concept

Scenario: Balance Change Breaking Legitimate Claims

  • Alice holds 100 SNOW tokens

  • Signs claim message → Digest1 = hash(receiver, 100)

  • Transfers 50 tokens to Bob

  • Attempts claim:

currentBalance = 50
Digest2 = hash(receiver, 50)
// Digest1 ≠ Digest2 → Signature invalid!

Result: Legitimate claim fails despite prior authorization

Scenario: Signature Replay via Balance Reset

  • Alice holds 100 SNOW → Signs message

  • Claims NFTs → Tokens staked

  • Buys 100 more SNOW

  • Reuses same signature → Claim succeeds again
    Result: Double NFT minting from one authorization

Recommended Mitigation

Fixed getMessageHash:

function getMessageHash(
address receiver,
uint256 fixedAmount, // Snapshot-based amount
uint256 nonce, // Unique claim ID
uint256 chainId // Current chain ID
) public view returns (bytes32) {
return _hashTypedDataV4(
keccak256(abi.encode(
MESSAGE_TYPEHASH,
SnowmanClaim({
receiver: receiver,
amount: fixedAmount, // Fixed snapshot amount
nonce: nonce, // Anti-replay nonce
chainId: chainId // Cross-chain protection
})
)
);
}

Fixed _isValidSignature:

function _isValidSignature(bytes32 digest, uint8 v, bytes32 r, bytes32 s)
internal
returns (bool)
{
// 1. Prevent signature malleability
require(uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0,
"Invalid s-value");
// 2. Track used signatures
bytes32 sigHash = keccak256(abi.encodePacked(v, r, s));
require(!usedSignatures[sigHash], "Signature used");
usedSignatures[sigHash] = true;
// 3. Strict ECDSA recovery
(address signer, ECDSA.RecoverError err) = ECDSA.tryRecover(digest, v, r, s);
require(err == ECDSA.RecoverError.NoError, "Recovery failed");
return signer == receiver;
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Lack of claim check

The claim function of the Snowman Airdrop contract doesn't check that a recipient has already claimed a Snowman. This poses no significant risk as is as farming period must have been long concluded before snapshot, creation of merkle script, and finally claiming.

Support

FAQs

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