Snowman Merkle Airdrop

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

Nft token drain in `SnowmanAirdrop::claimSnowman()` due to missing claim check.

Description

The SnowmanAirdrop::claimSnowman() function includes a mapping s_hasClaimedSnowman[receiver] = true to track which addresses have already claimed the airdrop. However, the function fails to check this mapping is true/false before allowing a claim. As a result, eligible users can repeatedly call the function and mint multiple NFTs.

function claimSnowman(address user, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s) external {
...
@> s_hasClaimedSnowman[receiver] = true;
}

Impact

Any qualified user can call the SnowmanAirdrop::claimSnowman() function multiple times minting an unlimited amount of nfts till the contract is drained or deflates the price of the Nft.

Proof of Concept

  1. Alice calls the SnowmanAirdrop::claimSnowman() function.

  2. Alice calls the SnowmanAirdrop::claimSnowman() function again and succeeds.

  3. Alice now has multiple NFTs claimed from a single intent.

Proof of Code

function testClaimMultipleTimes() public {
assert(nft.balanceOf(alice) == 0);
vm.startPrank(alice);
snow.approve(address(airdrop), type(uint256).max);
bytes32 alDigest = airdrop.getMessageHash(alice);
(uint8 alV, bytes32 alR, bytes32 alS) = vm.sign(alKey, alDigest);
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
assert(nft.balanceOf(alice) == 1);
assert(nft.ownerOf(0) == alice);
uint256 FEE = snow.s_buyFee();
snow.buySnow{value: FEE}(1); // alice buy snow token to match signature proof
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
assert(nft.balanceOf(alice) == 2);
}
Ran 1 test for test/TestSnowmanAirdrop.t.sol:TestSnowmanAirdrop
[PASS] testClaimMultipleTimes() (gas: 265735)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 12.35ms (1.22ms CPU time)

Recommended Mitigation

To fix this, add a check at the top of the SnowmanAirdrop::claimSnowman() function to check if a user has claimed or not.

+ error SA__UserAlreadyClaimed();
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
+ if (s_hasClaimedSnowman[receiver]) {
revert SA__UserAlreadyClaimed();
} else {
s_hasClaimedSnowman[receiver] = true;
}
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);
}
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.