Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: low
Valid

Missing Claimed Check Allows Double Claim in SnowmanAirdrop

Root + Impact

Description

  • Normally, each user should only be able to claim the Snowman NFT airdrop once; the contract should prevent multiple claims.

  • However, the current claimSnowman function only sets s_hasClaimedSnowman[receiver] = true; when claiming, but does not check this mapping at the beginning of the function. This allows the same address to successfully claim the NFT multiple times.

function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
if (i_snow.balanceOf(receiver) == 0) {
revert SA__ZeroAmount();
}
if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) {
revert SA__InvalidSignature();
}
uint256 amount = i_snow.balanceOf(receiver);
bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
revert SA__InvalidProof();
}
i_snow.safeTransferFrom(receiver, address(this), amount); // send tokens to contract... akin to burning
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}

Risk

Likelihood: High

  • Any user will succeed every time they call claimSnowman, because the mapping is not checked

  • As long as the contract has resources, an attacker can claim repeatedly

Impact: High

  • Airdrop resources are abused, NFTs or tokens can be claimed multiple times

  • Causes economic loss and damages project reputation

Proof of Concept

Just calls the claimSnowman twice, observe mint snowman 2 times

// Test double claim
function testDoubleClaim() public {
// Alice claim test
assert(nft.balanceOf(alice) == 0);
vm.prank(alice);
snow.approve(address(airdrop), 2);
// Get alice's digest
bytes32 alDigest = airdrop.getMessageHash(alice);
// alice signs a message
(uint8 alV, bytes32 alR, bytes32 alS) = vm.sign(alKey, alDigest);
// Alice 1st claim
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
// After 1 more week, Alice try to claim another snow again
vm.warp(block.timestamp + 1 weeks);
vm.prank(alice);
snow.earnSnow();
// Alice 2nd claim
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
assert(nft.balanceOf(alice) == 2);
assert(nft.ownerOf(0) == alice);
assert(nft.ownerOf(1) == alice);
}

Recommended Mitigation

Use s_hasClaimedSnowman check if the receiver already claim their NFT

- s_hasClaimedSnowman[receiver] = true;
+ require(!s_hasClaimedSnowman[receiver], "Already claimed");
+ s_hasClaimedSnowman[receiver] = true;
Updates

Lead Judging Commences

yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Lack of claim check

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.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.