Snowman Merkle Airdrop

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

An attacker can transfer tokens from any randomly selected user.

Description

The attcker can steal tokens from a random address that have approved the tokens, can also steal tokens from a contract, in short, this bug allows an attacker to pull approved tokens from any user into your contract, effectively stealing them.

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();
}
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

  1. High

    Since either the contract or the victim(user) have already approved the contract, attacker can obtain a valid signature or proof for token transfer.

Impact

  1. High

    Stealing of tokens by an attacker leads to direct financial loss of funds for the user or the contract itself

Proof of Concept

  1. Alice (victim) has previously called approve( ) on the ERC-20 token contract, granting your SnowManAirdrop contract permission to spend X amount of her tokens.

  2. Bob (attacker) wants to steal Alice's tokens.

Attack Execution

Bob calls the ClaimSnowMan function with the following parameters:

  1. receiver = Alice's address

  2. All other parameters (proof, v, r, s) can be arbitrary values that pass the function's validation (e.g., dummy values if the function doesn't properly verify them)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/SnowmanAirdrop.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ArbitrarySendPoC is Test {
SnowmanAirdrop public airdrop;
IERC20 public token;
address public alice = address(0xAlice);
address public bob = address(0xBob);
uint256 public constant AMOUNT = 1000 * 10**18;
function setUp() public {
// Deploy token and airdrop contract
token = new MockERC20();
airdrop = new SnowmanAirdrop(address(token), /* other params */);
// Mint tokens to Alice
MockERC20(address(token)).mint(alice, AMOUNT);
// Alice approves the airdrop contract to spend her tokens
vm.prank(alice);
token.approve(address(airdrop), AMOUNT);
}
function testArbitraryTransfer() public {
// Bob calls claimSnowman with Alice as the receiver
bytes32[] memory dummyProof = new bytes32[](0);
bytes32 dummyR = bytes32(0);
bytes32 dummyS = bytes32(0);
vm.prank(bob);
airdrop.claimSnowman(
alice, // receiver = Alice's address (arbitrary from)
dummyProof,
0,
dummyR,
dummyS
);
// Alice's tokens are now in the airdrop contract
assertEq(token.balanceOf(address(airdrop)), AMOUNT);
assertEq(token.balanceOf(alice), 0);
}
}

Recommended Mitigation

The safest and most professional fix is to change receiver to msg.sender inside the safeTransferFrom call. This aligns with the standard ERC-20 authorization model and prevents any external party from moving other users' tokens through your contract

i_snow.safeTransferFrom(msg.sender, address(this), amount);
Updates

Lead Judging Commences

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