OrderBook

First Flight #43
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

[H-2] Missing Balance Check in `OrderBook::emergencyWithdrawERC20()` Enables Misuse and Transfer Failures


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.
​
Updates

Lead Judging Commences

yeahchibyke Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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

Give us feedback!