Snowman Merkle Airdrop

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

Signature Replay in ```claimSnowman()``` Allows Multiple Claims per Signature

The function claimSnowman() does not check whether a user has already claimed their reward before** **processing the claim leading to over-minting of tokens.

Description

  • The claimSnowman() function is designed to allow eligible users to claim Snowman tokens once, by verifying their eligibility through a valid off-chain signature and a Merkle proof. After a successful claim, the contract should mark the user as having claimed to prevent multiple redemptions.

  • The function fails to check whether a user has already claimed their tokens before processing the claim. As a result, a user can replay the same valid signature and Merkle proof multiple times to repeatedly claim rewards.

function claimSnowman(
address receiver,
bytes32[] calldata merkleProof,
uint8 v,
bytes32 r,
bytes32 s
) external nonReentrant {
@> //No check for user that already claim
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:

  • This occurs whenever a user holds the required i_snow balance and has access to a valid signature and Merkle proof, they can repeatedly call claimSnowman() using the same data.

Impact:

  • Users can bypass the one-time claim restriction, resulting in unauthorized multiple claims and over-minting of Snowman tokens.

Proof of Concept

The test bellow prove that someone can claim twice .

function test_ReplayClaimWithSameSignatureAndProof() public {
// Give Alice 1 Snow token and approve the airdrop contract
deal(address(snow), alice, 1);
vm.prank(alice);
snow.approve(address(airdrop), 1);
// Alice signs the claim message
bytes32 digest = airdrop.getMessageHash(alice);
(uint8 v, bytes32 r, bytes32 s) = vm.sign(alKey, digest);
// First claim (expected to succeed)
vm.prank(satoshi);
airdrop.claimSnowman(alice, AL_PROOF, v, r, s);
assertEq(nft.balanceOf(alice), 1);
// Replay: give Alice another 1 Snow token to repeat the claim
deal(address(snow), alice, 1);
vm.prank(alice);
snow.approve(address(airdrop), 1);
// Reuse the same proof and signature
vm.prank(satoshi);
airdrop.claimSnowman(alice, AL_PROOF, v, r, s);
// Alice now has 2 NFTs (should only be 1) ---> This is BAD
assertEq(nft.balanceOf(alice), 2); // Fails if protection is added
}
r

Recommended Mitigation

Before allowing a user to claim, the contract should check if they’ve already claimed. This is typically done using a mapping(address => bool) to store whether each user has claimed.

+ if (s_hasClaimedSnowman[receiver]) {
revert SA__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.