Snowman Merkle Airdrop

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

Critical: Receiver/Signer Decoupling in claimSnowman() - Unauthorized NFT Redirection & Allocation Theft

Root + Impact

Description

  • Normal behavior:
    Only the intended recipient should be able to claim their allocation.

  • Issue:
    The signature verification doesn't bind the receiver to the claim, allowing attackers to specify themselves as receivers while using a victim's signature.

// >>> Root cause: Signature does not bind receiver address @>
function claimSnowman(address receiver, ...) ... {
...
// @> Attacker can set themselves as receiver
if (!_isValidSignature(receiver, getMessageHash(receiver), ...))
...
i_snowman.mintSnowman(receiver, amount); // @> Mints to attacker
}

Risk

Likelihood:

  • Medium: Requires signature interception

  • Easily automated with bot monitoring

Impact:

  • Complete theft of victim's allocation

  • Attacker receives victim's NFTs

  • No recourse for legitimate users

Proof of Concept

function testReceiverHijack() public {
// Victim signs their claim
(uint8 v, bytes32 r, bytes32 s) = getSignature(victim);
// Attacker claims with victim's signature but sets receiver = attacker
vm.prank(attacker);
airdrop.claimSnowman(attacker, proof, v, r, s);
// Attacker receives victim's NFTs
assertEq(nft.balanceOf(attacker), victimBalance);
}

Explanation:

Victim signs message containing their address and balance

Attacker calls claimSnowman() with:

  • receiver = attacker

  • Victim's signature

  • Valid Merkle proof

Contract verifies victim signed their own balance

But mints NFTs to attacker instead of victim

Recommended Mitigation

Explanation:
Enforce strict signature binding:

  • Require signer matches the receiver parameter

  • Add explicit non-zero address check

  • Prevent address substitution attacks

  • Maintain EIP-712 compliance for security

function _isValidSignature(
address receiver,
bytes32 digest,
...
) internal pure returns (bool) {
(address actualSigner,,) = ECDSA.tryRecover(digest, v, r, s);
- return actualSigner == receiver;
+ return actualSigner == receiver && receiver != address(0);
}

Severity Note

This issue represents a critical security vulnerability because it enables unauthorized users to redirect NFT claims meant for others. If exploited, it would result in the permanent theft of NFT rewards and the loss of user trust in the protocol. Since attackers can automate this by observing the mempool, the vulnerability is actively exploitable in real-time. Binding the signer to the recipient is fundamental to secure claim validation in signature-based systems.

Verification confirms proper functionality

function testSignatureBinding() public {
// Victim signs a message to claim NFTs
(uint8 v, bytes32 r, bytes32 s) = getSignature(victim);
// Attacker tries to use victim's signature with their own address
vm.prank(attacker);
vm.expectRevert("Unauthorized");
airdrop.claimSnowman(attacker, proof, v, r, s);
// Only victim can successfully claim
vm.prank(victim);
airdrop.claimSnowman(victim, proof, v, r, s);
// Assert victim received NFTs, attacker did not
assertEq(nft.balanceOf(victim), expectedAmount);
assertEq(nft.balanceOf(attacker), 0);
}
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.