Root + Impact
Description
In Snow.sol at line 103, the collectFee function calls i_weth.transfer(s_collector, collection) instead of using the safeTransfer wrapper from the SafeERC20 library. While the contract imports and declares using SafeERC20 for IERC20, the transfer call on line 103 uses the raw ERC20 transfer function. If the WETH token were to return false instead of reverting on failure (as some non-standard ERC20 tokens do), the transfer would silently fail and the function would continue executing.
function collectFee() external onlyCollector {
uint256 collection = i_weth.balanceOf(address(this));
i_weth.transfer(s_collector, collection);
(bool collected,) = payable(s_collector).call{value: address(this).balance}("");
require(collected, "Fee collection failed!!!");
}
Risk
Likelihood:
Impact:
If the WETH transfer fails silently, the collector would believe fees were collected when they were not. The ETH portion would still be sent successfully, masking the WETH transfer failure.
Proof of Concept
This test demonstrates the vulnerability using a mock ERC20 that returns false instead of reverting on transfer failure (simulating a non-standard token). The collector
calls collectFee, the ETH is sent successfully, but the WETH silently fails — and no revert occurs.
// Mock token that returns false instead of reverting
contract MockReturnFalseToken is ERC20 {
constructor() ERC20("BadToken", "BAD") {}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
function transfer(address, uint256) public pure override returns (bool) {
return false;
}
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
return super.transferFrom(from, to, amount);
}
}
function test_collectFeesilentlyFailsWethTransfer() public {
MockReturnFalseToken badToken = new MockReturnFalseToken();
address testCollector = makeAddr("testCollector");
Snow snowWithBadToken = new Snow(address(badToken), 5, testCollector);
address user = makeAddr("user");
badToken.mint(user, snowWithBadToken.s_buyFee());
vm.startPrank(user);
badToken.approve(address(snowWithBadToken), snowWithBadToken.s_buyFee());
snowWithBadToken.buySnow(1);
vm.stopPrank();
address ethUser = makeAddr("ethUser");
deal(ethUser, snowWithBadToken.s_buyFee());
vm.prank(ethUser);
snowWithBadToken.buySnow{value: snowWithBadToken.s_buyFee()}(1);
uint256 contractWethBefore = badToken.balanceOf(address(snowWithBadToken));
assert(contractWethBefore > 0);
assert(address(snowWithBadToken).balance > 0);
vm.prank(testCollector);
snowWithBadToken.collectFee();
assert(address(snowWithBadToken).balance == 0);
assert(testCollector.balance > 0);
assert(badToken.balanceOf(address(snowWithBadToken)) == contractWethBefore);
assert(badToken.balanceOf(testCollector) == 0);
}
Recommended Mitigation
Use safeTransfer from the SafeERC20 library:
- i_weth.transfer(s_collector, collection);
+ i_weth.safeTransfer(s_collector, collection);