Snowman Merkle Airdrop

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

[H-3] claimSnowman() has no checks to ensure that an address has already claimed and a person can claim twice

[H-3] SnowmanAirdrop::claimSnowman() has no checks to ensure that an address has already claimed and a person can claim twice

Description

  • Airdrop contract uses markle root to check for eligibility

  • However, address remains eligible even if person has claimed once, since there are no other checks

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:

  • very high

Impact:

  • Users claiming more than once breaks invariants

Proof of Concept

Add the following test case to the test suite of SnowmanAirdrop. The contract has a merkle hash where alice's wallet with 1 Snow token is eligible for minting the airdrop. Therefore, we are minting snowman once. Then, Alice's wallet address becomes 0. However, we are transferring Alice another 1 Snow token and thus she again becomes eligible for the airdrop and can claim an NFT, as proved by the test

function test_claimAgain() public {
assert(nft.balanceOf(alice) == 0);
vm.prank(alice);
snow.approve(address(airdrop), 2);
bytes32 alDigest = airdrop.getMessageHash(alice);
(uint8 alV, bytes32 alR, bytes32 alS) = vm.sign(alKey, alDigest);
vm.prank(satoshi);
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
assert(nft.balanceOf(alice) == 1);
assert(nft.ownerOf(0) == alice);
vm.prank(bob);
snow.transfer(alice, 1);
vm.prank(satoshi);
airdrop.claimSnowman(alice, AL_PROOF, alV, alR, alS);
assert(nft.balanceOf(alice) == 2);
assert(nft.ownerOf(1) == alice);
}

Recommended Mitigation

Add a mapping from address to bool which keeps track of the people who have claimed the token, and add a check using the mapping to prevent people from claiming again

+ mapping(address=>bool) public hasClaimed;
.
.
.
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
+ assert(hasClaimed[msg.sender] != 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();
}
+ hasClaimed[msg.sender] = true;
Updates

Lead Judging Commences

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