The airdrop is meant to be claimable once per recipient. SnowmanAirdrop keeps a mapping for this:
and writes to it inside claimSnowman:
However, claimSnowman never reads s_hasClaimedSnowman. The flag is only read by the getClaimStatus view getter, so it enforces nothing. The only thing that incidentally prevents a second claim is that safeTransferFrom moves the recipient's Snow into the contract, after which i_snow.balanceOf(receiver) == 0 makes the next call revert on the zero-amount check.
That incidental guard is defeatable: the claim amount is the recipient's current Snow balance, and the Merkle leaf is keccak256(keccak256(abi.encode(receiver, amount))). If the recipient re-acquires exactly their leaf amount of Snow (via buySnow), the same Merkle proof and the same EIP-712 signature validate again, and they can claim a second set of Snowman NFTs.
Likelihood: Medium â requires the recipient to re-acquire exactly the entitled amount of Snow; this is cheap and fully under the recipient's control, but it is an extra deliberate step rather than a free repeat.
Impact: Medium â a recipient can mint more reward NFTs than their single entitlement, inflating the reward supply and breaking the one-claim-per-recipient invariant; no funds are stolen from others directly.
The intended claim-once guarantee is unenforced. Any entitled recipient can repeat their claim (once per re-acquisition of the leaf amount), minting duplicate Snowman NFTs and diluting the fairness of the distribution.
Attack sequence (same receiver, Merkle proof, and signature throughout):
receiver holds exactly amount Snow (their Merkle-leaf amount).
receiver calls claimSnowman(receiver, proof, v, r, s) â succeeds. Their Snow is transferred to the contract, s_hasClaimedSnowman[receiver] = true, and amount Snowman NFTs are minted.
receiver calls buySnow(amount) to re-acquire exactly amount Snow.
receiver calls claimSnowman(receiver, proof, v, r, s) again with the identical proof and signature. Because claimSnowman never checks s_hasClaimedSnowman, and balanceOf(receiver) == amount != 0, and the leaf keccak256(keccak256(abi.encode(receiver, amount))) is unchanged, every check passes and a second batch of NFTs is minted.
The guard that was supposed to stop step 4 (s_hasClaimedSnowman[receiver]) is dead-written and does nothing.
Read the flag at the start of claimSnowman and revert on a repeat claim:
The contest is live. Earn rewards by submitting a finding.
Submissions are being reviewed by our AI judge. Results will be available in a few minutes.
View all submissionsThe contest is complete and the rewards are being distributed.