Snowman Merkle Airdrop

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

No Nonce in Signature Allows Permanent Signature Validity

Root + Impact

Missing nonce in the EIP-712 signature scheme means signatures remain valid indefinitely and cannot be invalidated by the signer after creation.

Description

EIP-712 signatures for delegated claims should include a nonce to ensure each signature can only be used once and becomes invalid after use. This prevents replay attacks and gives signers control over signature validity.

The MESSAGE_TYPEHASH and signature generation do not include a nonce, meaning signatures remain valid indefinitely and cannot be invalidated by the signer.

// SnowmanAirdrop.sol
// @> No nonce field in the type hash
bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
function getMessageHash(address receiver) public view returns (bytes32) {
// @> No nonce included in the hash
uint256 amount = i_snow.balanceOf(receiver);
return _hashTypedDataV4(
keccak256(abi.encode(MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: amount})))
);
}

Risk

Likelihood:MEDIUM

  • Reason 1 :Exposed signatures from failed transactions or logs can be reused

  • Reason 2 : Signers cannot revoke signatures once created

Impact:MEDIUM

  • Impact 1:Reduced security compared to standard EIP-712 implementations

  • Impact 2: Signatures remain valid even if signer wants to cancel

Proof of Concept

This test demonstrates that once Alice signs a claim message, she has no way to invalidate it. Even if she changes her mind or the signature is leaked, anyone holding the signature can use it to claim on her behalf at any future time.

function testSignatureCannotBeInvalidated() public {
address alice = makeAddr("alice");
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alicePk, messageHash);
// Alice wants to cancel but cannot
// Signature remains permanently valid
vm.warp(block.timestamp + 365 days);
// Signature still works a year later
airdrop.claimSnowman(alice, merkleProof, v, r, s);
}

Recommended Mitigation

Add a nonce mapping and include it in the message hash. Increment the nonce after each claim to ensure old signatures become invalid. This follows standard EIP-712 best practices for signature security.

+ mapping(address => uint256) public nonces;
- bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(addres receiver, uint256 amount)");
+ bytes32 private constant MESSAGE_TYPEHASH = keccak256("SnowmanClaim(address receiver, uint256 amount, uint256 nonce)");
- function getMessageHash(address receiver) public view returns (bytes32) {
+ function getMessageHash(address receiver, uint256 amount) public view returns (bytes32) {
return _hashTypedDataV4(
- keccak256(abi.encode(MESSAGE_TYPEHASH, SnowmanClaim({receiver: receiver, amount: amount})))
+ keccak256(abi.encode(MESSAGE_TYPEHASH, receiver, amount, nonces[receiver]))
);
}
function claimSnowman(...) external nonReentrant {
// ... verification ...
+ nonces[receiver]++;
// ... rest of function ...
}
Updates

Lead Judging Commences

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