Description
The `emergencyWithdrawERC20()` function allows the owner to withdraw non-core tokens accidentally or maliciously sent to the contract.
While it's a good mechanism for protocol hygiene, its current implementation lacks a critical safety check:
it does not verify whether the contract holds sufficient balance of the token being withdrawn.
This results in two potential issues:
1. Transfer Failure:
If _amount exceeds the contract’s actual balance of _tokenAddress, the transaction will revert — wasting gas unnecessarily.
2. Intentional Misuse or Theft Attempt (malicious):
A compromised or malicious owner could try to “withdraw” tokens they expect to be sent in later, front-running a future transfer.
Since the function does not validate token balance or enforce a time constraint, it enables value extraction strategies.
Risk
Impact:
This is an owner-only function, but it carries high impact:
Reverts due to insufficient balance make the emergency function unreliable.
In the event of a compromise, the attacker could call this method repeatedly with unrealistic `_amounts` to grief or trick token holders.
This creates ambiguity around the contract’s solvency and can damage user trust.
Proof of Concept
Context:
This function is designed to allow the contract owner to recover tokens (other than core tokens) that may have been accidentally sent to the contract.
However, due to the absence of a balance validation check, this function can:
1. Fail unexpectedly during real emergency withdrawal attempts.
2. Be abused by a compromised/malicious owner to exploit timing of token deposits.
PoC 1: Emergency Withdrawal Fails Due to Insufficient Balance
Scenario:
A user mistakenly sends 500 FAKE tokens to the contract.
Owner tries to call:
```javascript
emergencyWithdrawERC20(FAKE_TOKEN_ADDRESS, 1000e18, owner);
```
Expected: Transfer of 1000 tokens.
Reality: Contract only holds 500 tokens → transfer fails and reverts.
This defeats the "emergency" purpose of the function, wasting gas and requiring a second call with a corrected amount.
PoC 2: Malicious Owner Front-Runs Token Deposits
Scenario:
An attacker becomes the contract owner (either by bug, compromised keys, or rugpull).
They predict that a user or token integration will send tokens to the contract.
They call:
```javascript
emergencyWithdrawERC20(TARGET_TOKEN, 100000000e18, attacker);
```
Even if the contract holds 0 of TARGET_TOKEN now, there's no check — transfer simply fails silently.
Now, the attacker watches the mempool. As soon as someone sends tokens to the contract,
the attacker re-submits the same function and front-runs the actual depositor.
This is not theoretical — it's a known attack pattern, especially in DeFi protocols with emergency withdrawal functions.
PoC 3: Dust Draining and Spamming
A malicious owner could spam the function with:
```javascript
\\ for each token in tokenList:
emergencyWithdrawERC20(token, type(uint256).max, attacker);
\\ This will try to drain all tokens that have a non-zero balance, wasting gas, confusing logs,
and potentially griefing users who later interact with the contract.
```
Recommended Mitigation
We need to ensure:
Emergency withdrawals cannot fail silently or due to bad inputs.
Attackers cannot withdraw more than the contract holds.
Transparent and intentional withdrawals (not guesses or spam).
Implementation:
```diff
function emergencyWithdrawERC20(address _tokenAddress, uint256 _amount, address _to) external onlyOwner {
if (
_tokenAddress == address(iWETH) || _tokenAddress == address(iWBTC) ||
_tokenAddress == address(iWSOL) || _tokenAddress == address(iUSDC)
) {
revert("Cannot withdraw core order book tokens via emergency function");
}
if (_to == address(0)) {
revert InvalidAddress();
}
+ if (_amount == 0) {
+ revert InvalidAmount(); // Prevent 0-value spam withdrawals
+ }
IERC20 token = IERC20(_tokenAddress);
+ uint256 balance = token.balanceOf(address(this));
+ if (_amount > balance) {
+ revert InvalidAmount(); // New check: prevent overdraw
+ }
token.safeTransfer(_to, _amount);
emit EmergencyWithdrawal(_tokenAddress, _amount, _to);
}
```
=> Optional Safeguards
You can further harden the function by logging the current balance in the event:
```javascript
emit EmergencyWithdrawal(_tokenAddress, _amount, _to, balance);
```
Adding a rate limit (e.g., block.timestamp throttle) to avoid spamming.
Restricting access to multisig or timelock contracts to reduce abuse risk.
Benefits of Fix
=> Check for `amount > balance`, Prevents failed transfers and overdraws.
=> Revert on `amount == 0`, Prevents spam calls and gas griefing.
=> Protects against front-running withdrawals, Stronger operational security.
=> Clearer event logs, Easier monitoring for off-chain observers.