Snowman Merkle Airdrop

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

No `deadline` or `nonce` in the EIP-712 payload — signatures are valid forever and across redeployments with same domain

Root + Impact

Description

  • A safe EIP-712 claim signature includes a deadline, a per-user nonce, or both. The signed struct here contains only (receiver, amount).

  • The signature is therefore valid for all time on this chain and on any future redeployment whose domain separator matches (same name="Snowman Airdrop", version="1", same chainId, same address — the last one limits it but is not future-proof).

// src/SnowmanAirdrop.sol
struct SnowmanClaim {
@> address receiver;
@> uint256 amount;
// no deadline, no nonce
}

Risk

Likelihood:

  • Reason 1: Users cannot revoke a signature once leaked, and rotating Snow approval to zero does not invalidate the EIP-712 message.

  • Reason 2: A v2 redeployment that reuses name and version produces an identical type/domain hash.

Impact:

  • Impact 1: Stretches C-03 to be permanent — no time-bound expiration.

  • Impact 2: Cross-deployment signature reuse if the team ever spins up a "Snowman Airdrop v1.0.1" without bumping version.

Proof of Concept

The PoC sets up Alice's claim signature in the present and then warps forward five years using Foundry's vm.warp. No matter how much time elapses, the signed payload's digest does not change because (receiver, amount) are time-independent. When the airdrop is finally called, the signature still verifies and the claim executes — proving the signed authorization has no temporal scope. The PoC's choice of five years is arbitrary; the same result holds for any time delta, including across multiple protocol redeployments if the domain separator collides.

function test_signatureWorksYearsLater() public {
(uint8 v, bytes32 r, bytes32 s) = _signClaim(alicePk);
vm.warp(block.timestamp + 365 days * 5);
airdrop.claimSnowman(alice, proof, v, r, s); // still valid
}

Recommended Mitigation

The fix extends the EIP-712 struct with nonce and deadline fields, and tracks a per-user nonce in storage. The nonce makes every signature single-use even at the EIP-712 layer (independent of the C-03 fix), and the deadline gives users a way to bound their signed authorization in time. Both protections must be added together: a nonce alone allows indefinite replay until the user manually invalidates it; a deadline alone allows replay until the deadline. Together they reduce the signature's "blast radius" to a single claim within a bounded window.

struct SnowmanClaim {
address receiver;
uint256 amount;
+ uint256 nonce;
+ uint256 deadline;
}
+ mapping(address => uint256) public nonces;
+
- bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver,uint256 amount)");
+ bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver,uint256 amount,uint256 nonce,uint256 deadline)");
function claimSnowman(..., uint256 deadline, ...) external nonReentrant {
+ if (block.timestamp > deadline) revert SA__SignatureExpired();
+ uint256 nonce = nonces[receiver]++;
... // include nonce + deadline in the digest build
}
Updates

Lead Judging Commences

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