The `withdrawTokens` function does not check the return value of the ERC20 `transfer` call, which could fail silently for non-standard ERC20 tokens that don't revert on failure.
**Description**:
* The normal behavior expects ERC20 transfers to either revert on failure (ERC20 standard) or return a boolean indicating success (some implementations).
* The specific issue is that while most ERC20 tokens revert on failure, some implementations return `false` instead. Not checking the return value means failed transfers might go unnoticed, though this is less common in modern implementations.
**Root cause in the codebase**:
```solidity
IERC20(token).transfer(to, amount);
```
The return value is not checked. While OpenZeppelin's ERC20 reverts on failure, the interface `IERC20` doesn't guarantee this behavior.
The following demonstrates how non-standard ERC20 tokens can cause silent failures:
```solidity
contract NonStandardERC20 {
mapping(address => uint256) public balanceOf;
function transfer(address to, uint256 amount) external returns (bool) {
if (balanceOf[msg.sender] < amount) {
return false;
}
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
}
address nonStandardToken = address(new NonStandardERC20());
uint256 withdrawAmount = 100 ether;
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
IERC20(token).transfer(to, amount);
emit TokensWithdrawn(token, to, amount);
}
```
**Step-by-step execution:**
1. Owner calls `withdrawTokens(nonStandardToken, owner, 100 ether)`
2. Contract has only 50 ether balance
3. `transfer()` is called, checks balance, returns `false`
4. Return value is ignored, execution continues
5. Event `TokensWithdrawn` is emitted with amount = 100 ether
6. Function completes without reverting
7. Owner believes withdrawal succeeded
8. Tokens remain in contract (transfer failed silently)
Use OpenZeppelin's `SafeERC20` library which handles both reverting and non-reverting tokens:
```diff
// src/RebateFiHook.sol:14
+import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
// src/RebateFiHook.sol:20
+ using SafeERC20 for IERC20;
// src/RebateFiHook.sol:73-75
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
- IERC20(token).transfer(to, amount);
+ IERC20(token).safeTransfer(to, amount);
- emit TokensWithdrawn(to, token , amount);
+ emit TokensWithdrawn(token, to, amount);
}
```
**Alternative approach (if SafeERC20 is not desired):**
```diff
// src/RebateFiHook.sol:48-50
+ error TransferFailed(address token, address to, uint256 amount);
// src/RebateFiHook.sol:73-75
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
+ bool success = IERC20(token).transfer(to, amount);
+ if (!success) {
+ revert TransferFailed(token, to, amount);
+ }
- IERC20(token).transfer(to, amount);
- emit TokensWithdrawn(to, token , amount);
+ emit TokensWithdrawn(token, to, amount);
}
```
**Explanation:**
- `SafeERC20.safeTransfer()` handles both reverting and non-reverting ERC20 tokens
- For reverting tokens: reverts on failure (standard behavior)
- For non-reverting tokens: checks return value and reverts if false
- Ensures transfers always succeed or revert, never fail silently
- Recommended approach as it's battle-tested and handles edge cases
- Alternative approach manually checks return value but requires more code