Snowman Merkle Airdrop

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

Unchecked return value in Snow.collectFee() allows silent failure of WETH transfer, permanently locking fees

Root + Impact

Description

  • Describe the normal behavior in one or more sentences

  • The collectFee() function should transfer accumulated WETH fees to the collector address and revert if the transfer fails, ensuring fees are never lost.

  • Explain the specific issue or problem in one or more sentences

  • The function ignores the return value of the i_weth.transfer() call. If the transfer fails (due to insufficient balance, paused token, or other reasons), the function continues execution without reverting, and the fees remain stuck in the contract.

// Root cause in the codebase with @> marks to highlight the relevant section
// @> Root cause in src/Snow.sol lines 101-107
function collectFee() external {
uint256 collection = i_weth.balanceOf(address(this));
// @> Unchecked transfer - return value ignored
i_weth.transfer(s_collector, collection);
(bool collected,) = address(s_collector).call{value: address(this).balance}();
require(collected, "ETH transfer failed");
}

Risk

Likelihood:

Reason 1: WETH transfers can fail for multiple reasons including paused contract state, transfer restrictions, or implementation bugs. Without checking the return value, these failures go unnoticed.

Reason 2: The function is called by external parties to collect fees. If it appears to succeed but silently fails, fees accumulate indefinitely in the contract with no way to recover them unless a fix is deployed.

Impact:

Impact 1: Permanent loss of protocol fees - Failed transfers are not detected, and accumulated WETH fees become permanently locked in the contract with no recovery mechanism.

Impact 2: False success reporting - The function completes without reverting, giving the caller false confidence that fees were collected when they actually remain in the contract. This breaks accounting and monitoring systems.

Proof of Concept

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {Test, console} from "forge-std/Test.sol";
import {Snow} from "../src/Snow.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// Mock WETH that can fail transfers
contract FailingWETH is ERC20 {
bool public shouldFail;
constructor() ERC20("Wrapped Ether", "WETH") {}
function setFailure(bool _fail) external {
shouldFail = _fail;
}
function transfer(address to, uint256 amount) public override returns (bool) {
if (shouldFail) {
return false; // Simulate failed transfer
}
return super.transfer(to, amount);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract UncheckedTransferTest is Test {
Snow snow;
FailingWETH weth;
address collector = makeAddr("collector");
function setUp() public {
weth = new FailingWETH();
// Deploy Snow with failing WETH
// ... setup code ...
}
function testSilentFailure() public {
// Setup: Add WETH fees to the Snow contract
uint256 feeAmount = 10 ether;
weth.mint(address(snow), feeAmount);
uint256 collectorBalanceBefore = weth.balanceOf(collector);
uint256 snowBalanceBefore = weth.balanceOf(address(snow));
console.log("Collector WETH before:", collectorBalanceBefore);
console.log("Snow contract WETH before:", snowBalanceBefore);
// Configure WETH to fail transfers
weth.setFailure(true);
// Call collectFee - should revert but doesn't
snow.collectFee();
uint256 collectorBalanceAfter = weth.balanceOf(collector);
uint256 snowBalanceAfter = weth.balanceOf(address(snow));
console.log("Collector WETH after:", collectorBalanceAfter);
console.log("Snow contract WETH after:", snowBalanceAfter);
// @> Function succeeded but fees were NOT transferred!
assertEq(collectorBalanceAfter, collectorBalanceBefore, "Collector received fees when they shouldn't");
assertEq(snowBalanceAfter, feeAmount, "Fees still stuck in contract");
// @> Fees are permanently locked
console.log("FEES LOCKED: ", snowBalanceAfter);
}
}

Recommended Mitigation

- remove this code
+ add this codefunction collectFee() external {
uint256 collection = i_weth.balanceOf(address(this));
- // Remove this code - unchecked transfer
- i_weth.transfer(s_collector, collection);
+ // Add this code - check return value
+ bool success = i_weth.transfer(s_collector, collection);
+ require(success, "WETH transfer failed");
(bool collected,) = address(s_collector).call{value: address(this).balance}();
require(collected, "ETH transfer failed");
}
Updates

Lead Judging Commences

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