Snowman Merkle Airdrop

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

Claim signature is not bound to msg.sender and has no nonce, making it freely relayable and front-runnable

Signature omits msg.sender, nonce, and expiry, letting any observer relay or front-run a claim

Description

_isValidSignature only checks that the recovered signer equals receiver; msg.sender is not part of the signed SnowmanClaim struct and is never validated, and the struct carries no nonce or expiry.

struct SnowmanClaim {
address receiver;
uint256 amount;
}
// ...
function _isValidSignature(address receiver, bytes32 digest, uint8 v, bytes32 r, bytes32 s)
internal pure returns (bool) {
(address actualSigner,,) = ECDSA.tryRecover(digest, v, r, s); // @> caller not bound, no nonce/expiry
return actualSigner == receiver;
}

Any observer of a valid (v, r, s) can submit the claim on the receiver's behalf.

Risk

Likelihood:

A valid signature is visible in the mempool or wherever it is shared. Replaying it requires no privilege.

Impact:

The NFT still mints to receiver, so direct theft is limited. But combined with the live-balance leaf bug, a front-runner can grief the receiver's balance to invalidate the receiver's own transaction, and the absence of a nonce/expiry leaves no replay or time bound. Low severity in isolation.

Proof of Concept

A third party submits the receiver's signature and the claim executes.

function test_anyoneCanRelay() public {
(uint8 v, bytes32 r, bytes32 s) = vm.sign(receiverPk, airdrop.getMessageHash(receiver));
vm.prank(randomRelayer);
airdrop.claimSnowman(receiver, proof, v, r, s); // succeeds without receiver as caller
}

Recommended Mitigation

Bind the caller (if relaying is unwanted) or add a per-address nonce and expiry to the signed struct.

+ if (msg.sender != receiver) revert SA__NotReceiver();
// or extend SnowmanClaim with: uint256 nonce; uint256 deadline; and validate both.
Updates

Lead Judging Commences

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