Firstly write a test to check the collectFee function properly.
Add this test:
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/Snow.sol";
import "../lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract FailingToken is ERC20 {
constructor() ERC20("FailToken", "FAIL") {
_mint(msg.sender, 1000 ether);
}
function transfer(address, uint256) public pure override returns (bool) {
return false;
}
}
contract MockERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 1_000_000 ether);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
contract SnowTest is Test {
Snow snow;
MockERC20 weth;
address collector = address(1);
address user = address(2);
function setUp() public {
weth = new MockERC20("Wrapped ETH", "WETH");
snow = new Snow(address(weth), 1, collector);
weth.mint(address(snow), 10 ether);
}
function testCollectFeeSucceeds() public {
vm.prank(collector);
snow.collectFee();
assertEq(weth.balanceOf(collector), 10 ether);
}
function testCollectFeeFailsOnBadERC20() public {
FailingToken badToken = new FailingToken();
Snow badSnow = new Snow(address(badToken), 1, collector);
badToken.transfer(address(badSnow), 10 ether);
vm.prank(collector);
vm.expectRevert();
badSnow.collectFee();
}
}
$ forge build
[⠒] Compiling...
[⠒] Compiling 34 files with Solc 0.8.28
[⠢] Solc 0.8.28 finished in 22.20s
Compiler run successful!
HP@GOAT MINGW64 ~/Desktop/2025-06-snowman-merkle-airdrop
$ forge test --match-path test/TestingSnow.t.sol -vvvv
[⠢] Compiling...
No files changed, compilation skipped
Ran 2 tests for test/TestingSnow.t.sol:SnowTest
[FAIL: next call did not revert as expected] testCollectFeeFailsOnBadERC20() (gas: 2365956)
Traces:
[2444123] SnowTest::setUp()
├─ [795108] → new MockERC20@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: SnowTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], value: 1000000000000000000000000 [1e24])
│ └─ ← [Return] 3490 bytes of code
├─ [1519878] → new Snow@0x2e234DAe75C793f67A35089C9d99245E1C58470b
│ ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: SnowTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
│ └─ ← [Return] 6886 bytes of code
├─ [28097] MockERC20::mint(Snow: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 10000000000000000000 [1e19])
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: Snow: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], value: 10000000000000000000 [1e19])
│ └─ ← [Return]
└─ ← [Return]
[2365956] SnowTest::testCollectFeeFailsOnBadERC20()
├─ [754818] → new FailingToken@0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: SnowTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], value: 1000000000000000000000 [1e21])
│ └─ ← [Return] 3292 bytes of code
├─ [1519878] → new Snow@0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9
│ ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: SnowTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
│ └─ ← [Return] 6886 bytes of code
├─ [881] FailingToken::transfer(Snow: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], 10000000000000000000 [1e19]) [staticcall]
│ └─ ← [Return] false
├─ [0] VM::prank(ECRecover: [0x0000000000000000000000000000000000000001])
│ └─ ← [Return]
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [12518] Snow::collectFee()
│ ├─ [3218] FailingToken::balanceOf(Snow: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9]) [staticcall]
│ │ └─ ← [Return] 0
│ ├─ [881] FailingToken::transfer(ECRecover: [0x0000000000000000000000000000000000000001], 0)
│ │ └─ ← [Return] false
│ ├─ [3000] ECRecover::fallback()
│ │ └─ ← [Return]
│ └─ ← [Return]
└─ ← [Revert] next call did not revert as expected
[PASS] testCollectFeeSucceeds() (gas: 63915)
Traces:
[68715] SnowTest::testCollectFeeSucceeds()
├─ [0] VM::prank(ECRecover: [0x0000000000000000000000000000000000000001])
│ └─ ← [Return]
├─ [50138] Snow::collectFee()
│ ├─ [3240] MockERC20::balanceOf(Snow: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]) [staticcall]
│ │ └─ ← [Return] 10000000000000000000 [1e19]
│ ├─ [31979] MockERC20::transfer(ECRecover: [0x0000000000000000000000000000000000000001], 10000000000000000000 [1e19])
│ │ ├─ emit Transfer(from: Snow: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], to: ECRecover: [0x0000000000000000000000000000000000000001], value: 10000000000000000000 [1e19])
│ │ └─ ← [Return] true
│ ├─ [3000] ECRecover::fallback()
│ │ └─ ← [Return]
│ └─ ← [Return]
├─ [1240] MockERC20::balanceOf(ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ └─ ← [Return] 10000000000000000000 [1e19]
├─ [0] VM::assertEq(10000000000000000000 [1e19], 10000000000000000000 [1e19]) [staticcall]
│ └─ ← [Return]
└─ ← [Return]
Suite result: FAILED. 1 passed; 1 failed; 0 skipped; finished in 2.95ms (1.63ms CPU time)
Ran 1 test suite in 1.68s (2.95ms CPU time): 1 tests passed, 1 failed, 0 skipped (2 total tests)
Failing tests:
Encountered 1 failing test in test/TestingSnow.t.sol:SnowTest
[FAIL: next call did not revert as expected] testCollectFeeFailsOnBadERC20() (gas: 2365956)
Encountered a total of 1 failing tests, 1 tests succeeded
$ forge test --match-path test/TestingSnow.t.sol -vvvv
[⠒] Compiling...
[⠒] Compiling 3 files with Solc 0.8.28
[⠑] Solc 0.8.28 finished in 5.49s
Compiler run successful!
Ran 2 tests for test/TestingSnow.t.sol:SnowTest
[PASS] testCollectFeeFailsOnBadERC20() (gas: 2342661)
Traces:
[2342661] SnowTest::testCollectFeeFailsOnBadERC20()
├─ [754818] → new FailingToken@0xF62849F9A0B5Bf2913b396098F7c7019b51A820a
│ ├─ emit Transfer(from: 0x0000000000000000000000000000000000000000, to: SnowTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], value: 1000000000000000000000 [1e21])
│ └─ ← [Return] 3292 bytes of code
├─ [1500257] → new Snow@0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9
│ ├─ emit OwnershipTransferred(previousOwner: 0x0000000000000000000000000000000000000000, newOwner: SnowTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496])
│ └─ ← [Return] 6788 bytes of code
├─ [881] FailingToken::transfer(Snow: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9], 10000000000000000000 [1e19]) [staticcall]
│ └─ ← [Return] false
├─ [0] VM::prank(ECRecover: [0x0000000000000000000000000000000000000001])
│ └─ ← [Return]
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [8629] Snow::collectFee()
│ ├─ [3218] FailingToken::balanceOf(Snow: [0x5991A2dF15A8F6A256D3Ec51E99254Cd3fb576A9]) [staticcall]
│ │ └─ ← [Return] 0
│ ├─ [881] FailingToken::transfer(ECRecover: [0x0000000000000000000000000000000000000001], 0)
│ │ └─ ← [Return] false
│ └─ ← [Revert] SafeERC20FailedOperation(0xF62849F9A0B5Bf2913b396098F7c7019b51A820a)
└─ ← [Return]
[PASS] testCollectFeeSucceeds() (gas: 64087)
Traces:
[68887] SnowTest::testCollectFeeSucceeds()
├─ [0] VM::prank(ECRecover: [0x0000000000000000000000000000000000000001])
│ └─ ← [Return]
├─ [50310] Snow::collectFee()
│ ├─ [3240] MockERC20::balanceOf(Snow: [0x2e234DAe75C793f67A35089C9d99245E1C58470b]) [staticcall]
│ │ └─ ← [Return] 10000000000000000000 [1e19]
│ ├─ [31979] MockERC20::transfer(ECRecover: [0x0000000000000000000000000000000000000001], 10000000000000000000 [1e19])
│ │ ├─ emit Transfer(from: Snow: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], to: ECRecover: [0x0000000000000000000000000000000000000001], value: 10000000000000000000 [1e19])
│ │ └─ ← [Return] true
│ ├─ [3000] ECRecover::fallback()
│ │ └─ ← [Return]
│ └─ ← [Return]
├─ [1240] MockERC20::balanceOf(ECRecover: [0x0000000000000000000000000000000000000001]) [staticcall]
│ └─ ← [Return] 10000000000000000000 [1e19]
├─ [0] VM::assertEq(10000000000000000000 [1e19], 10000000000000000000 [1e19]) [staticcall]
│ └─ ← [Return]
└─ ← [Return]
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 2.28ms (1.11ms CPU time)
Ran 1 test suite in 1.90s (2.28ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
This ensures that the transfer either succeeds or fails with a clear reason, avoiding accidental loss or logic halts due to incompatible contracts.