Snowman Merkle Airdrop

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

Signature Replay Attack via Missing Nonce and Deadline in SnowmanAirdrop Contract

Description

Normal Behavior

The SnowmanAirdrop contract should implement EIP-712 signature verification with replay protection to ensure signatures can only be used once and within a specific time window.

Issue Description

The MESSAGE_TYPEHASH and SnowmanClaim struct lack nonce and deadline fields, making signatures reusable across different contract deployments and without expiration, creating a critical replay attack vulnerability.

Root + Impact

Description

The contract implements EIP-712 signature verification but omits critical replay protection mechanisms, allowing malicious actors to reuse valid signatures indefinitely.

Root Cause

// @> Critical: Missing nonce and deadline in type hash and struct
struct SnowmanClaim {
address receiver;
uint256 amount;
// @> Missing: uint256 nonce;
// @> Missing: uint256 deadline;
}
bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
// @> Typo: "addres" should be "address"
// @> Missing nonce and deadline parameters

The signature verification in _isValidSignature() 2 and message hashing in getMessageHash() 3 use this incomplete structure.

Risk

Likelihood:

  • Reason 1: Signatures are permanently valid without deadline enforcement

  • Reason 2: No nonce tracking allows unlimited reuse of the same signature

Impact:

  • Impact 1: Signatures can be replayed across different contract deployments, bypassing verification

  • Impact 2: Malicious actors can reuse old signatures to claim NFTs indefinitely, compromising the entire airdrop mechanism

Proof of Concept

function testSignatureReplayAttack() public {
// Attacker gets a valid signature for victim
(uint8 v, bytes32 r, bytes32 s) = getValidSignature(victim, 100);
// First claim succeeds
vm.prank(attacker);
airdrop.claimSnowman(victim, validProof, v, r, s);
// Attacker reuses same signature - VULNERABILITY!
vm.prank(attacker);
airdrop.claimSnowman(victim, validProof, v, r, s); // Succeeds again!
// Signature can be reused even after contract redeployment
SnowmanAirdrop newAirdrop = new SnowmanAirdrop(merkleRoot, address(snow), address(snowman));
vm.prank(attacker);
newAirdrop.claimSnowman(victim, validProof, v, r, s); // Still works!
}

Recommended Mitigation

struct SnowmanClaim {
address receiver;
uint256 amount;
+ uint256 nonce;
+ uint256 deadline;
}
- bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
+ bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver, uint256 amount, uint256 nonce, uint256 deadline)");
mapping(address => uint256) private s_nonces;
function claimSnowman(
address receiver,
uint256 amount,
uint256 nonce,
uint256 deadline,
bytes32[] calldata merkleProof,
uint8 v, bytes32 r, bytes32 s
) external nonReentrant {
require(block.timestamp <= deadline, "Signature expired");
require(s_nonces[receiver] < nonce, "Invalid nonce");
// ... existing verification logic ...
s_nonces[receiver] = nonce;
// ... rest of function
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 3 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!