Root + Impact
Description
Normal behavior
The snowmanAirdrop
contract is designed to let eligible users claim a Snowman NFT by verifying a Merkle proof and signing a message. The intent is that each user can only claim once using their i_snow
token balance.
Issue
The contract does not implement any mechanism to prevent duplicate claims by the same user. While the contract tracks whether an address has already claimed using the s_hasClaimedSnowman
mapping, it fails to check this mapping in the claimSnowman
function. As a result, a user can repeatedly call the claimSnowman()
function and claim multiple NFTs, potentially draining the airdrop pool.
1 function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
2 external
3 nonReentrant
4 {
5 if (receiver == address(0)) {
6 revert SA__ZeroAddress();
7 }
8 if (i_snow.balanceOf(receiver) == 0) {
9 revert SA__ZeroAmount();
10 }
11
12 if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) {
13 revert SA__InvalidSignature();
14 }
15
16 uint256 amount = i_snow.balanceOf(receiver);
17 bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
18
19 if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
20 revert SA__InvalidProof();
21 }
22
23 i_snow.safeTransferFrom(receiver, address(this), amount);
24
25
26
27
28
29 emit SnowmanClaimedSuccessfully(receiver, amount);
30
31 i_snowman.mintSnowman(receiver, amount);
32 }
Risk
Likelihood: High
Impact: High
Proof of Concept
Assume the receiver has 1000
SNOW tokens and a valid Merkle proof and signature.
address receiver = 0x123...;
bytes32[] memory proof = [...];
(uint8 v, bytes32 r, bytes32 s) = signClaim(receiver, 1000);
airdrop.claimSnowman(receiver, proof, v, r, s);
airdrop.claimSnowman(receiver, proof, v, r, s);
airdrop.claimSnowman(receiver, proof, v, r, s);
CopyEdit
The claimSnowman() function does not verify whether a user has already claimed, allowing malicious users to repeatedly claim Snowman NFTs by reusing valid Merkle proofs and signatures. This leads to unlimited unauthorized claims, causing direct and severe token inflation.
Recommended Mitigation
Add a check to ensure a user can only claim once, using the s_hasClaimedSnowman
mapping that's already being written to.
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();
}
+ // Add a duplication check to prevent multiple claims
+ if (s_hasClaimedSnowman[receiver]) {
+ revert("Snowman already claimed");
+ }
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);
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}