Snowman Merkle Airdrop

AI First Flight #10
Beginner FriendlyFoundrySolidityNFT
EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Arbitrary from parameter in transferFrom allows theft of approved tokens from any user

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • The SnowmanAirdrop.claimSnowman() function should allow eligible users to claim their airdrop by transferring tokens from the airdrop contract to themselves.

  • Explain the specific issue or problem in one or more sentences

  • The function uses the user-controlled receiver parameter as the from address in safeTransferFrom, allowing anyone to transfer tokens from any address that has approved the airdrop contract.

// Root cause in the codebase with @> marks to highlight the relevant section
// @> Root cause in the codebase - Line 92 in src/SnowmanAirdrop.sol
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s) external {
// ... validation code ...
// @> Vulnerable line - receiver is user-controlled
i_snow.safeTransferFrom(receiver, address(this), amount);
}

Risk

Likelihood:

  • Reason 1 // Describe WHEN this will occur (avoid using "if" statements)

  • Any user can call claimSnowman() with an arbitrary receiver address. If any legitimate user has approved the SnowmanAirdrop contract to spend their tokens, an attacker can drain those tokens.

  • Reason 2

  • Users who have interacted with the protocol and granted approvals are at immediate risk. The attack requires no special conditions and can be executed by anyone.

Impact:

  • Impact 1

  • Complete loss of funds for any user who has approved the SnowmanAirdrop contract. All approved tokens can be stolen.

  • Impact 2

  • Breaks core protocol functionality and trust. Users will lose confidence in the entire Snowman ecosystem if their tokens can be arbitrarily transferred.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test} from "forge-std/Test.sol";
import {SnowmanAirdrop} from "../src/SnowmanAirdrop.sol";
import {Snow} from "../src/Snow.sol";
contract ExploitTest is Test {
SnowmanAirdrop airdrop;
Snow snow;
address victim = makeAddr("victim");
address attacker = makeAddr("attacker");
function testStealApprovedTokens() public {
// Setup: Victim has tokens and has approved the airdrop contract
vm.startPrank(victim);
snow.approve(address(airdrop), 1000e18);
vm.stopPrank();
// Attack: Attacker calls claimSnowman with victim as receiver
vm.startPrank(attacker);
// Attacker creates valid merkle proof and signature for victim's address
bytes32[] memory proof = new bytes32[](0);
(uint8 v, bytes32 r, bytes32 s) = createValidSignature(victim);
// @> This transfers tokens FROM victim TO airdrop contract
airdrop.claimSnowman(victim, proof, v, r, s);
// Victim's tokens are now stolen
assertEq(snow.balanceOf(victim), 0);
vm.stopPrank();
}
}

Recommended Mitigation

- remove this code
function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s) external {
- // Remove this code - don't use receiver as from address
- i_snow.safeTransferFrom(receiver, address(this), amount);
+ // Add this code - ensure receiver is msg.sender
+ require(receiver == msg.sender, "Can only claim for yourself");
+ i_snow.safeTransferFrom(address(this), receiver, amount);
}
+ add this code
function claimSnowman(bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s) external {
address receiver = msg.sender;
// ... rest of function ...
i_snow.safeTransferFrom(address(this), receiver, amount);
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 7 hours ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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

Give us feedback!