RebateFi Hook

First Flight #53
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Severity: medium
Valid

Unsafe ERC20 Transfer in withdrawTokens (Compatibility)

Description:
The withdrawTokens function uses IERC20(token).transfer(to, amount).
The IERC20 interface expects a bool return value.
Some tokens (e.g., USDT) do not return a boolean.
When calling transfer on such tokens using the standard interface, the call will revert because the return data size (0 bytes) does not match the expected size (32 bytes for bool).

Impact:
The owner cannot withdraw tokens that do not follow the standard ERC20 return behavior (like USDT), leading to stuck funds.

Proof of Concept:
withdrawTokens reverts when used with a token that does not return a boolean.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {ReFiSwapRebateHook} from "../src/RebateFiHook.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {PoolManager} from "v4-core/PoolManager.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
// Mock Token that does NOT return a boolean on transfer (like old USDT)
contract MockBadERC20 {
mapping(address => uint256) public balanceOf;
function mint(address to, uint256 amount) external {
balanceOf[to] += amount;
}
// No return value
function transfer(address to, uint256 amount) external {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
}
}
contract M2_UnsafeTransferTest is Test {
ReFiSwapRebateHook public rebateHook;
MockERC20 reFiToken;
PoolManager manager;
MockBadERC20 badToken;
function setUp() public {
manager = new PoolManager(address(0));
reFiToken = new MockERC20("ReFi Token", "ReFi", 18);
badToken = new MockBadERC20();
bytes memory creationCode = type(ReFiSwapRebateHook).creationCode;
bytes memory constructorArgs = abi.encode(manager, address(reFiToken));
uint160 flags = uint160(
Hooks.BEFORE_INITIALIZE_FLAG |
Hooks.AFTER_INITIALIZE_FLAG |
Hooks.BEFORE_SWAP_FLAG
);
(address hookAddress, bytes32 salt) = HookMiner.find(
address(this),
flags,
creationCode,
constructorArgs
);
rebateHook = new ReFiSwapRebateHook{salt: salt}(manager, address(reFiToken));
}
function test_M2_WithdrawFailsForBadToken() public {
// Mint bad tokens to hook
badToken.mint(address(rebateHook), 1000);
// Try to withdraw
// Expect revert because IERC20(token).transfer expects a boolean return value,
// but MockBadERC20.transfer returns nothing.
// Solidity's high-level call will revert when decoding the return data.
vm.expectRevert();
rebateHook.withdrawTokens(address(badToken), address(this), 1000);
}
}

Recommended Mitigation:
Use OpenZeppelin's SafeERC20 library for all token transfers.

using SafeERC20 for IERC20;
IERC20(token).safeTransfer(to, amount);
Updates

Lead Judging Commences

chaossr Lead Judge 12 days ago
Submission Judgement Published
Validated
Assigned finding tags:

Not using safe transfer for ERC20.

Support

FAQs

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

Give us feedback!