Root + Impact
Description
The `withdrawTokens` function does not verify that the contract has sufficient balance before attempting to transfer, which will cause the transfer to revert but provides a poor user experience and could be exploited for griefing.
**Description**:
* The normal behavior expects the function to check if the contract has enough tokens before attempting a transfer, providing a clear error message if insufficient.
* The specific issue is that `withdrawTokens` directly calls `transfer` without checking the contract's balance first, so if the balance is insufficient, the call will revert with a generic ERC20 error rather than a clear, hook-specific error.
**Root cause in the codebase**:
```solidity
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
IERC20(token).transfer(to, amount);
emit TokensWithdrawn(to, token , amount);
}
```
No balance check is performed before the transfer.
Risk
Likelihood:
Impact:
-
* Transaction reverts with unclear ERC20 error messages, making debugging difficult
* Owner must manually check balances before withdrawing
* Potential for griefing if owner repeatedly attempts invalid withdrawals (wastes gas)
Proof of Concept
The following demonstrates the poor user experience when attempting to withdraw more than available:
```solidity
address token = 0x1234...;
address owner = 0x5678...;
uint256 contractBalance = IERC20(token).balanceOf(address(rebateHook));
uint256 withdrawAmount = 200 tokens;
rebateHook.withdrawTokens(token, owner, withdrawAmount);
```
**Step-by-step execution:**
1. Owner calls `withdrawTokens(token, owner, 200)` when contract has 100 tokens
2. Function proceeds without checking balance
3. ERC20 `transfer()` is called with amount exceeding balance
4. Transfer reverts with generic ERC20 error message
5. Transaction fails, gas consumed, owner confused about failure reason
6. Owner must manually check balance and retry with correct amount
Recommended Mitigation
Add a balance check with a clear error message before attempting the transfer:
```diff
// src/RebateFiHook.sol:48-50
+ error InsufficientBalance(address token, uint256 requested, uint256 available);
// src/RebateFiHook.sol:73-75
function withdrawTokens(address token, address to, uint256 amount) external onlyOwner {
+ uint256 balance = IERC20(token).balanceOf(address(this));
+ if (balance < amount) {
+ revert InsufficientBalance(token, amount, balance);
+ }
IERC20(token).transfer(to, amount);
- emit TokensWithdrawn(to, token , amount);
+ emit TokensWithdrawn(token, to, amount);
}
```
**Explanation:**
- Check contract balance before attempting transfer
- Revert with clear, hook-specific error if insufficient balance
- Error includes token address, requested amount, and available balance for debugging
- Prevents gas waste from failed transfers
- Improves user experience with informative error messages
- Follows best practice of validating inputs before external calls