Snowman Merkle Airdrop

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

Arbitrary ERC-20 Transfer from User Wallets

Root + Impact

Description

  • The claimSnowman function allows any caller to transfer SNOW tokens from any receiver address to the contract, provided
    the caller can supply a valid signature and Merkle proof for that receiver. While the function includes signature verification and
    Merkle proof validation, the receiver parameter is user-controlled and not tied to the caller (msg.sender). This enables a malicious
    actor to initiate transfers from any address that has approved the contract (or has a universal allowance) and for which the attacker
    can obtain or forge the required signature and proof.

// Root cause in the codebase with @> marks to highlight the relevant section
@> 69 | function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
@> 70 | external
@> 71 | nonReentrant
@> 72 | {
@> 73 | if (receiver == address(0)) {
@> 74 | revert SA__ZeroAddress();
@> 75 | }
@> 76 | if (i_snow.balanceOf(receiver) == 0) {
@> 77 | revert SA__ZeroAmount();
@> 78 | }
@> 79 |
@> 80 | if (!_isValidSignature(receiver, getMessageHash(receiver), v, r, s)) {
@> 81 | revert SA__InvalidSignature();
@> 82 | }
@> 83 |
@> 84 | uint256 amount = i_snow.balanceOf(receiver);
@> 85 |
@> 86 | bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(receiver, amount))));
@> 87 |
@> 88 | if (!MerkleProof.verify(merkleProof, i_merkleRoot, leaf)) {
@> 89 | revert SA__InvalidProof();
@> 90 | }
@> 91 |
@> 92 | i_snow.safeTransferFrom(receiver, address(this), amount); // send tokens to contract... akin to burning

Risk

Likelihood:

• Medium: The attack requires the attacker to obtain a valid signature and Merkle proof for a target address. If signatures are
generated off-chain and distributed (e.g., via a backend API) or if the signing process is flawed (e.g., using eth_sign which signs
a prefixed hash), they could be intercepted or replayed. Additionally, if the Merkle tree is public, proofs are readily available.

Impact:

High: An attacker can drain SNOW tokens from any user who has granted an allowance to the contract. The stolen tokens are
transferred to the contract (effectively burned in this context, as they are meant to be exchanged for SNOWMAN tokens), causing
permanent loss to the victim.

Proof of Concept

// Conceptual Foundry test
// Assume: Attacker has obtained a valid signature (v, r, s) and Merkle proof for victim's address
// Victim has approved the SnowmanAirdrop contract to spend their SNOW tokens.
function test_arbitraryTransferFrom() public {
address victim = address(0x123...);
address attacker = address(this);
// Attacker obtains signature and proof for victim (e.g., from leaked data)
bytes32[] memory merkleProof = getMerkleProofForVictim(); // From public tree
(uint8 v, bytes32 r, bytes32 s) = getSignatureForVictim(); // From intercepted API response
// Pre-condition: Victim has SNOW balance and has approved the contract
uint256 victimBalance = snow.balanceOf(victim);
assert(victimBalance > 0);
assert(snow.allowance(victim, address(snowmanAirdrop)) >= victimBalance);
// Attacker calls claimSnowman, specifying victim as receiver
snowmanAirdrop.claimSnowman(victim, merkleProof, v, r, s);
// Post-condition: Victim's SNOW tokens are transferred to the contract
assert(snow.balanceOf(victim) == 0);
assert(snow.balanceOf(address(snowmanAirdrop)) == victimBalance);
}

Recommended Mitigation

- function claimSnowman(address receiver, bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
+ function claimSnowman(bytes32[] calldata merkleProof, uint8 v, bytes32 r, bytes32 s)
external
nonReentrant
{
+ address receiver = msg.sender;
if (receiver == address(0)) {
revert SA__ZeroAddress();
}
Updates

Lead Judging Commences

ai-first-flight-judge Lead Judge about 4 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!