Snowman Merkle Airdrop

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

`Snow::collectFee` uses unchecked `transfer()` instead of `safeTransfer()` for WETH, risking silent fee loss

Description

  • The Snow contract imports SafeERC20 and declares using SafeERC20 for IERC20. The buySnow() function correctly uses safeTransferFrom for WETH payments. The collectFee() function is intended to collect all accumulated WETH and ETH fees to the collector address.

  • collectFee() calls i_weth.transfer() directly, bypassing the SafeERC20 wrapper. For ERC20 tokens that return false on failure instead of reverting (non-compliant but common in the wild), this transfer silently fails and the collector loses all accumulated WETH fees.

// src/Snow.sol:15
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
// src/Snow.sol:19
using SafeERC20 for IERC20;
// src/Snow.sol:101-107
function collectFee() external onlyCollector {
uint256 collection = i_weth.balanceOf(address(this));
i_weth.transfer(s_collector, collection); // @> Direct transfer(), not safeTransfer()
(bool collected,) = payable(s_collector).call{value: address(this).balance}("");
require(collected, "Fee collection failed!!!");
}

Risk

Likelihood:

  • The protocol uses WETH which is OZ-compliant, making silent failure unlikely with canonical WETH

  • Non-standard ERC20 tokens that return false instead of reverting are common across DeFi — deploying with such a token triggers the bug

Impact:

  • All accumulated WETH fees are permanently lost — transfer() returns false but execution continues

  • The ETH portion is sent successfully, masking the WETH failure from the collector

  • No event or revert indicates the WETH transfer failed

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {Snow} from "../src/Snow.sol";
import {MockWETH} from "../src/mock/MockWETH.sol";
contract PoC_M01 is Test {
Snow snow;
MockWETH weth;
address buyer;
address collector;
function setUp() public {
weth = new MockWETH();
collector = makeAddr("collector");
snow = new Snow(address(weth), 5, collector);
buyer = makeAddr("buyer");
}
// M-01: collectFee uses i_weth.transfer() instead of SafeERC20.safeTransfer().
// With non-compliant ERC20 tokens that return false instead of reverting,
// the WETH transfer silently fails and fees are permanently lost.
function test_M01_UncheckedTransfer() public {
uint256 fee = snow.s_buyFee();
weth.mint(buyer, fee);
vm.startPrank(buyer);
weth.approve(address(snow), fee);
snow.buySnow(1);
vm.stopPrank();
console2.log("WETH in Snow contract:", weth.balanceOf(address(snow)));
vm.prank(collector);
snow.collectFee();
console2.log("Collector WETH balance:", weth.balanceOf(collector));
assertEq(weth.balanceOf(collector), fee);
assertEq(weth.balanceOf(address(snow)), 0);
console2.log("Works with OZ ERC20, but would silently fail with non-compliant tokens");
}
}

Output:

[PASS] test_M01_UncheckedTransfer() (gas: 214015)
Logs:
WETH in Snow contract: 5000000000000000000
Collector WETH balance: 5000000000000000000
Works with OZ ERC20, but would silently fail with non-compliant tokens

Recommended Mitigation

Replace the direct i_weth.transfer() call with i_weth.safeTransfer() from the SafeERC20 library that is already imported and declared in the contract. safeTransfer wraps the low-level call and reverts on failure or a false return value, ensuring fee collection either succeeds completely or reverts the entire transaction.

function collectFee() external onlyCollector {
uint256 collection = i_weth.balanceOf(address(this));
- i_weth.transfer(s_collector, collection);
+ i_weth.safeTransfer(s_collector, collection);
(bool collected,) = payable(s_collector).call{value: address(this).balance}("");
require(collected, "Fee collection failed!!!");
}
Updates

Lead Judging Commences

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