The SnowmanAirdrop
contract tracks claim status using the mapping s_hasClaimedSnowman
, but fails to check this mapping before processing a claim. As a result, eligible users can claim their Snowman NFTs multiple times, each time transferring Snow
tokens and receiving new NFTs.
This undermines the integrity of the airdrop, and allows double-claiming or automated draining of NFTs by repeatedly calling claimSnowman()
.
Describe the normal behavior in one or more sentences
Explain the specific issue or problem in one or more sentences
s_hasClaimedSnowman[receiver] = true;
This sets claim status, but…
// This check is missing: // if (s_hasClaimedSnowman[receiver]) { revert AlreadyClaimed(); }
Missing Claim Status Check
Likelihood:
Reason 1 // Describe WHEN this will occur (avoid using "if" statements)
This issue occurs whenever a whitelisted user or any actor with access to a valid Merkle proof and signature calls the claimSnowman()
function repeatedly without restriction.
Reason 2
Since the contract does not validate whether a user has already claimed, any user with tokens and a valid signature can automate the claim process and exploit it at any time, even after a successful initial claim.
Impact:
Impact 1
Users are able to mint an unlimited number of Snowman NFTs, as long as they have Snow tokens to send — effectively breaking the 1:1 mint logic and flooding the NFT supply.
Impact 2
The entire airdrop can be manipulated or drained by a small number of users, leading to financial loss, devaluation of the Snowman NFTs, and loss of community trust.
Assuming receiver
has a valid signature + Merkle proof:
contract Attacker { SnowmanAirdrop public airdrop; address public victim; constructor(address _airdrop, address _victim) { airdrop = SnowmanAirdrop(_airdrop); victim = _victim; } function exploit(bytes32[] calldata proof, uint8 v, bytes32 r, bytes32 s) external { // call repeatedly for (uint i = 0; i < 5; i++) { airdrop.claimSnowman(victim, proof, v, r, s); } } }
This drains 5 NFTs if
victim
has enough tokens and has signed only once.
The SnowmanAirdrop
contract currently does not prevent users from claiming multiple times. Although the mapping s_hasClaimedSnowman
is defined to track who has already claimed, it's only being set, not checked.
This creates a critical flaw:
Anyone with a valid Merkle proof, a valid signature, and a Snow token balance can repeatedly call claimSnowman()
and receive multiple Snowman NFTs.
This can drain the NFT supply and break the intended fairness of the airdrop.
Ensure that each user can claim the Snowman NFT only once — even if they retry with the same inputs or attempt to re-claim later.
To fix this, you need to:
Check whether the user has already claimed before processing the rest of the claim logic.
Revert early if they've already claimed — saving gas and preventing exploitation.
Luckily, the contract already has:
A mapping: mapping(address => bool) private s_hasClaimedSnowman;
A line that sets the claim status to true
after a successful claim:
s_hasClaimedSnowman[receiver] = true;
We just need to add one missing check.
claimSnowman()
function:if (s_hasClaimedSnowman[receiver]) {
revert("Snowman already claimed by this address");
}
error SA__AlreadyClaimed();
And use it like this:
CopyEdit
if (s_hasClaimedSnowman[receiver]) { revert SA__AlreadyClaimed(); }
Here's what the top of your claimSnowman
function should look like after applying the mitigation:
}
The claim function of the Snowman Airdrop contract doesn't check that a recipient has already claimed a Snowman. This poses no significant risk as is as farming period must have been long concluded before snapshot, creation of merkle script, and finally claiming.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.