Snowman Merkle Airdrop

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

s_hasClaimedSnowman not checked

Root + Impact

Description

  • hasClaimedSnowman is set in claimSnowman but is never being used

  • Users can call claimSnowman multiple times, as long as they have a SNOW balance. Consider if it is intended to have multiple claims or not. Having the balance as part of the Merkle Tree suggests that a user should only claim once.

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();
}
@> // Should verify if the receiver already claimedsnowman
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:

  • When a user claims snowmen for a second time, while still having snow tokens, result in a succesful mint (as long as the proof succeeds which is another issue)

  • Reason 2

Impact:

  • Users can claim multiple times

  • The claims can be considered fairas the user still needs to have SNOW that is burned

Proof of Concept

See below the steps on how to claim snowmen multiple time. If this is intended behaviour (as opposed to how most airdrops work), then see suggestion at the bottom of the submission.

1. User has 10 SNOW
2. User claims snowmen -> success
3. User receives 10 SNOW (transfer or time mismatch in the lock of Snow.sol)
4. User claims snowmen -> Succeeds again

Recommended Mitigation

A mitigation is as simple as adding an extra check if you want to user to only claim once. Otherwise consider the suggestion at the bottom to track the balance of the claimed tokens.

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();
}
+ if(s_hasClaimedSnowman[receiver]) {
+ revert SA__AlreadyClaimed();
+ }
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);
}

Alternative suggestion

Alternatively, consider keeping a map of claimed tokens per address:

mapping(address => uint256) private s_claimedSnowMan; // mapping to track the amount of Snowman claimed by each address
function claimSnowman(..., uint256 amount){
// note amount should come from a param in claimSnowman (see other submission)
if (s_claimedSnowMan[receiver] >= amount) {
revert SA__AlreadyClaimed();
}
//...
s_claimedSnowMan[receiver] += amount;
}
Updates

Lead Judging Commences

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