Snowman Merkle Airdrop

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

[L-01] `collectFee()` calls `i_weth.transfer()` directly instead of `safeTransfer()`, ignoring return value

Description

Snow.sol imports SafeERC20 and declares using SafeERC20 for IERC20, but collectFee() calls i_weth.transfer() directly on line 103 instead of using i_weth.safeTransfer(). The transfer() return value is silently discarded. If the WETH transfer fails (returns false), the function continues to send ETH to the collector, and the WETH fees are lost.

Vulnerability Details

// src/Snow.sol, lines 19, 101-107
using SafeERC20 for IERC20; // @> declared but not used on line 103
function collectFee() external onlyCollector {
uint256 collection = i_weth.balanceOf(address(this));
i_weth.transfer(s_collector, collection); // @> direct call, NOT safeTransfer
(bool collected,) = payable(s_collector).call{value: address(this).balance}("");
require(collected, "Fee collection failed!!!");
}

Standard WETH returns true on success, so the return value is technically always truthy. But the contract explicitly imported SafeERC20 for this purpose and then didn't use it. With any non-standard ERC20 (some tokens return no value), the transfer() call would succeed at the EVM level but not actually transfer tokens.

Risk

Likelihood:

  • With standard WETH on Ethereum mainnet, the transfer will succeed. The risk materializes if the protocol is deployed with a non-standard WETH wrapper or on a chain with a different WETH implementation.

Impact:

  • WETH fees silently lost. The collector receives ETH but not WETH. The discrepancy goes undetected because the function doesn't revert.

Proof of Concept

function testExploit_UnsafeTransfer() public {
// Verify that Snow.sol uses .transfer() not .safeTransfer()
// The issue is visible in the source code:
// Line 19: using SafeERC20 for IERC20; (imported)
// Line 103: i_weth.transfer(...) (not used)
// With a mock token that returns false on transfer:
// collectFee() would succeed (no revert) but WETH stays in the contract
// The collector only receives the ETH portion of fees
Snow snow = new Snow(address(weth), 5, collector);
// ... buy Snow with WETH to generate fees ...
// collectFee() calls transfer() directly — return value ignored
}

Recommendations

Use safeTransfer() as intended by the import:

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!