Snowman Merkle Airdrop

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

Signature Replay Vulnerability in `claimFor` Allows Multiple NFT Claims with a Single Signature

Root + Impact

The claimFor function lacks a nonce or any other replay protection. A captured signature can be reused indefinitely, allowing an attacker to steal a user's airdrop by replaying a legitimate claim transaction they observed in the mempool.

Description

The claimFor function validates a signature but never invalidates it after use. An attacker can front-run a legitimate transaction, copy the signature, and submit it themselves. Because the signature remains valid, the attacker's transaction will succeed, stealing the claim.

// src/SnowmanAirdrop.sol
function claimFor(
address who,
uint256 amount,
bytes32[] calldata proof,
bytes calldata signature
) public {
@> bytes32 digest = _hash(who, amount, proof);
@> address signer = ECDSA.recover(digest, signature);
if (signer != who) {
revert InvalidSignature();
}
// State is not updated to prevent replay of the same signature.
// ...
}

Risk

Likelihood:

  • Any claimFor transaction submitted to a public mempool is vulnerable to being front-run.

  • The lack of a nonce is a fundamental flaw in the signature verification logic, making every claimFor call vulnerable.

Impact:

  • A user's entire airdrop can be stolen by an attacker who replays their signature.

  • Attackers can steal airdrops, rendering the claimFor feature completely insecure and breaking user trust.

Proof of Concept

A legitimate user, Alice, provides a signature to Bob to claim on her behalf. An attacker, Eve, sees Bob's transaction in the mempool, copies the signature, and submits an identical transaction with a higher gas fee. Eve's transaction gets mined first, and the claim is processed. Bob's subsequent transaction will fail.

// Conceptual PoC
function testExploitSignatureReplay() public {
// 1. Alice creates a valid signature for her claim.
// 2. Attacker Eve copies the signature from a pending transaction.
// 3. Eve front-runs the transaction with the copied signature.
vm.prank(eve);
snowmanAirdrop.claimFor(alice, amount, proof, signature);
// 4. Eve's transaction succeeds. The legitimate transaction will now fail.
vm.prank(bob);
vm.expectRevert(); // e.g., Already claimed
snowmanAirdrop.claimFor(alice, amount, proof, signature);
}

Recommended Mitigation

Implement EIP-712 and include a nonce in the signed hash. The nonce must be stored and incremented on-chain for each user upon a successful claim, making each signature single-use.

// src/SnowmanAirdrop.sol
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
contract SnowmanAirdrop is EIP712 {
mapping(address => uint256) public nonces;
// ...
function _hash(...) internal view returns (bytes32) {
// Hash must include the user's current nonce
return _hashTypedDataV4(keccak256(abi.encode(
// ...
nonces[who]
)));
}
function claimFor(...) public {
// ...
// After successful verification:
@> nonces[who]++;
// ...
}
}
Updates

Lead Judging Commences

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

Support

FAQs

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