OrderBook

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

`OrderBook::emergencyWithdrawERC20` Allows Admin to freely withdraw User Funds by Withdrawing Non-Core Tokens

Root + Impact

Description

The `emergencyWithdrawERC20` function allows the contract owner to arbitrarily withdraw any ERC20 token from the contract as long as it's not one of the core tokens `(iWETH, iWBTC, iWSOL, iUSDC)`.
While the function is gated with `onlyOwner`, there are no additional constraints verifying that the token being withdrawn wasn’t deposited by a user through mistake, abuse, or even through interaction with a previously supported token that was later de-listed via `setAllowedSellToken`.
Since the contract does not prevent users from sending arbitrary tokens to its address (there’s no fallback to block native ETH either), and there’s no mechanism for users to reclaim such tokens, this function effectively gives the owner unilateral control over any tokens that land in the contract and aren’t hardcoded as “core.”
@> 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();
}
IERC20 token = IERC20(_tokenAddress);
token.safeTransfer(_to, _amount);
emit EmergencyWithdrawal(_tokenAddress, _amount, _to);
}

Risk

Likelihood:

  • This is medium issue because if users follow the right procedures they wont accidentally deposit tokens directly to the contract address

  • But once any user sends funds directly via contract address instead

    createSellOrder, non-core ERC20 tokens, its only the owner that can withdraw it.


Impact:

- The contract owner can withdraw any non-core ERC20 tokens accidentally or intentionally sent to the contract.
- Users have no way to recover mistakenly sent or disabled token funds.
- Creates a severe trust assumption around the contract owner.
- No distinction between actual emergencies and routine operations

Proof of Concept

Run `forge test --mt test_adminEmergencyWithdrawToDrainTokensMistakenIntoContract -vvvvv` with the below code in `TestOrderBook.t.so`
function test_adminEmergencyWithdrawToDrainTokensMistakenIntoContract()
public
{
// Deploy a new non-core token (not part of the order book)
MockUSDC NonCoreERC20Token = new MockUSDC(18);
// Simulate a victim user sending tokens directly to the contract (e.g., by mistake)
address ox_fff = makeAddr("naive");
NonCoreERC20Token.mint(ox_fff, 1_000e18);
vm.prank(ox_fff);
NonCoreERC20Token.transfer(address(book), 1_000e18);
assertEq(
NonCoreERC20Token.balanceOf(address(book)),
1_000e18,
"Contract should now hold the user's token"
);
// Admin uses emergencyWithdrawERC20 to take the tokens to a personal wallet
address adminPersonalWallet = makeAddr("admin_wallet");
vm.prank(owner);
book.emergencyWithdrawERC20(
address(NonCoreERC20Token),
1_000e18,
adminPersonalWallet
);
// Confirm that the contract no longer holds the tokens and owner has them
assertEq(
NonCoreERC20Token.balanceOf(address(book)),
0,
"Contract balance should be drained"
);
assertEq(
NonCoreERC20Token.balanceOf(adminPersonalWallet),
1_000e18,
"Admin wallet should now hold the drained tokens"
);
}

Recommended Mitigation

Add proper emergency conditions verification
Implement a waiting period for non-emergency token recoveries
Add community governance for valuable token recoveries (above a certain threshold)
Create transparent criteria for what constitutes an emergency
Consider implementing a community treasury for recovered tokens
Updates

Lead Judging Commences

yeahchibyke Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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