Snowman Merkle Airdrop

First Flight #42
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Impact: high
Likelihood: medium
Invalid

Unauthorized Token Transfers in claimSnowman Function

Description

  • The claimSnowman function in the SnowmanAirdrop contract allows users to exchange their Snow tokens for Snowman NFTs through a verified claiming process.

  • The function accepts a receiver parameter that can be any address, but it doesn't verify that the caller (msg.sender) is the same as the receiver. This allows anyone to initiate a claim on behalf of another user if that user has previously approved the contract to spend their tokens.

function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
// ...
@> i_snow.safeTransferFrom(receiver, address(this), amount); // send tokens to contract
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}

Risk

Likelihood:

  • This vulnerability can be exploited whenever a user has approved the SnowmanAirdrop contract to spend their Snow tokens, which is a common practice for interacting with token-based contracts.

Impact:

  • An attacker can force a user to claim Snowman NFTs and burn their Snow tokens without their consent.

  • This could lead to financial loss if the user was holding Snow tokens for other purposes or if the market value of Snow tokens is higher than the Snowman NFTs.

  • Users may lose trust in the protocol if their tokens can be spent without their explicit authorization for each transaction.

Proof of Concept

function testUnauthorizedClaim() public {
// Setup: Victim has Snow tokens and has approved the airdrop contract
address victim = makeAddr("victim");
uint256 initialAmount = 100 ether;
snow.transfer(victim, initialAmount);
vm.startPrank(victim);
snow.approve(address(snowmanAirdrop), type(uint256).max); // Approve airdrop contract to spend tokens
vm.stopPrank();
// Attacker prepares valid merkleProof, v, r, s for the victim
// (This would require the victim to be in the merkle tree)
bytes32[] memory merkleProof = /* valid merkle proof */;
uint8 v = /* valid v */;
bytes32 r = /* valid r */;
bytes32 s = /* valid s */;
// Attacker forces victim to claim
address attacker = makeAddr("attacker");
vm.prank(attacker);
snowmanAirdrop.claimSnowman(victim, merkleProof, v, r, s);
// Victim's tokens are now burned and they received Snowman NFTs they didn't request
assertEq(snow.balanceOf(victim), 0);
assertEq(snowman.balanceOf(victim), initialAmount);
}

Recommended Mitigation

function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
+ if (receiver != msg.sender) {
+ revert SA__Unauthorized();
+ }
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);
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
3 months ago
yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.