Snowman Merkle Airdrop

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

Unsafe Token Transfer in `Snow::collectFee` function causing potential silent failures and loss of funds

Root + Impact

Description

The `collectFee()` function uses the unsafe `i_weth.transfer()` method instead of SafeERC20's `safeTransfer()` when transferring WETH tokens to the collector. This creates a critical vulnerability because many ERC20 tokens do not return boolean values on transfer operations, which can lead to silent failures where the transfer fails but the transaction doesn't revert.
function collectFee() external onlyCollector {
uint256 collection = i_weth.balanceOf(address(this));
//q what happens when the transfer fails?
//@audit tokens like USDT do not return anything if fail
@> i_weth.transfer(s_collector, collection); // Unsafe transfer
(bool collected,) = payable(s_collector).call{value: address(this).balance}("");
require(collected, "Fee collection failed!!!");
}

Risk

Likelihood:

  • Major tokens like USDT don't return booleans, so it will silently fail on transfer


Impact:

- Silent failures: WETH transfers can fail without reverting the transaction, leaving tokens stuck in the contract while appearing to succeed
- Loss of funds: Accumulated WETH fees become permanently inaccessible if transfers consistently fail
- Collector loss: The fee collector may believe they received all fees when only ETH was successfully transferred

Proof of Concept

Run the test in TestSnow.t.sol: showing that:

  • ETH collection succeeds (5 ETH goes to collector)

  • WETH transfer fails silently (1000 WETH stays in contract)

  • Overall transaction appears successful

  • Collector loses the WETH fees


function testSilentFailure() public {
// 1. Contract accumulates 1000 WETH in fees
uint256 initialWethBalance = 1000 ether;
weth.mint(address(snow), initialWethBalance);
// 2. Contract also has 5 ETH
vm.deal(address(snow), 5 ether);
vm.mockCall(
address(weth),
abi.encodeWithSelector(weth.transfer.selector, collector, initialWethBalance),
abi.encode(false) // Make transfer return false
);
// 3. Collector attempts to collect fees
vm.prank(collector);
snow.collectFee(); // Transaction succeeds
// 4. ETH is collected successfully
assertEq(collector.balance, 5 ether);
// 5. But WETH remains stuck in contract (silent failure)
assertEq(weth.balanceOf(address(snow)), initialWethBalance); // Still 1000 WETH
assertEq(weth.balanceOf(collector), 0); // Collector gets 0 WETH
console2.log("Initial Weth balance:", initialWethBalance);
// Transaction appeared successful but WETH was not transferred
console2.log("Transaction appeared successful but WETH was not transferred");
}

Recommended Mitigation

Use SafeERC20's safeTransfer(): Replace the unsafe transfer with the safe version that properly handles non-compliant tokens:
function collectFee() external onlyCollector {
uint256 collection = i_weth.balanceOf(address(this));
//q what happens when the transfer fails?
//@audit tokens like USDT do not return anything if fail on transfer
- 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

yeahchibyke Lead Judge 9 days ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.