Snowman Merkle Airdrop

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

M01- s_hasClaimedSnowman is not checked in claimSnowman

Root + Impact

Description

The principle of an airdrop is to authorize the claim for a same user only once
The function claimSnowmanallows multiple claims as long as the user has tokens.


While, this feature can certainly be desired by the project author, at the end of the function however, a mapping is updated to indicate that the receiver has claimed. So we can assume that the function should only be called once.

// Root cause in the codebase with @> marks to highlight the relevant section
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
// @audit here
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}

Risk

Likelihood:

  • A receiver or someone with the signature calls the function twice or more

Impact:

  • Behavior not anticipated by the project authors

    • If the receiver has received some new Snow tokens since the first claim, he can claim twice

    • Otherwise, the function will revert (SA__ZeroAmount())


Proof of Concept

Here Alice claims again after receing tokens from another token holder, Bob

function testClaimSnowmanTwice() 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);
// satoshi calls claims on behalf of alice using her signed message
vm.prank(satoshi);
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
assert(nft.balanceOf(alice) == 1);
assert(nft.ownerOf(0) == alice);
// Transfer of Snow tokens Bob -> Alice
vm.prank(bob);
snow.transfer(alice, 1);
// Alice claims Again
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
assert(nft.balanceOf(alice) == 2);
assert(nft.ownerOf(1) == alice);
}

Recommended Mitigation

- remove this code
+ add this code
In the contract:
+ error AlreadyClaimed();
In the function claimSnowman, at the beginning
+ require(!s_hasClaimedSnowman[receiver], AlreadyClaimed());
Updates

Lead Judging Commences

yeahchibyke Lead Judge 12 days 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.