OrderBook

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

Non-Core Allowed Tokens Can Be Withdrawn by Owner via Emergency Function

Root + Impact

Description

  • In normal behavior, only core order book tokens (WETH, WBTC, WSOL, USDC) are protected from being withdrawn via the emergencyWithdrawERC20 function. Additionally, the contract allows the owner to add any arbitrary ERC20 token to the allowedSellToken list using setAllowedSellToken, enabling users to create orders with those tokens.

  • However, non-core tokens that are added to the allowed list are not protected from emergency withdrawal. This allows the owner to withdraw all user-deposited balances of any non-core allowed token at any time. This violates user expectations, as they may assume that any token marked as "allowed" is also safe from privileged withdrawal.

// Root cause in the codebase with @> marks to highlight the relevant section
function setAllowedSellToken(address _token, bool _isAllowed) external onlyOwner {
if (_token == address(0) || _token == address(iUSDC)) revert InvalidToken();
@> allowedSellToken[_token] = _isAllowed;
emit TokenAllowed(_token, _isAllowed);
}
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");
}
...
}

Risk

Likelihood: Low

  • This can occur when the owner adds a non-core token (e.g., a new ERC20) via setAllowedSellToken and users begin interacting with it.

  • It is likely in protocols with active governance, token listings, or upgradability, where the token list changes dynamically.

Impact: Medium

  • Users may lose funds if they interact with allowed tokens that the owner later withdraws using the emergency function.

  • It undermines trust in the protocol's asset security, especially if the frontend or docs do not distinguish between core and non-core allowed tokens.

Proof of Concept

This test demonstrates that the contract owner can withdraw user-deposited funds of a non-core token that was allowed via setAllowedSellToken, using the emergencyWithdrawERC20 function. Since these tokens are not protected like core tokens (e.g., wETH, wBTC, wSOL, USDC), user funds in such tokens can be drained, violating expected trust and fund safety.

// A mock ERC20 token (testToken) with symbol "mwtestToken" and 18 decimals was deployed for testing.
function test_CanWithdrawAllowedNonCoreToken() public {
vm.startPrank(owner);
// owner sets a new allowed token
book.setAllowedSellToken(address(testToken), true);
vm.stopPrank();
vm.startPrank(alice);
// user creates a sell order with that new allowed token
testToken.approve(address(book), 1000 ether);
book.createSellOrder(address(testToken), 1000 ether, 1000e6, 2 days);
vm.stopPrank();
// Owner is able to withdraw the token — this should NOT be allowed.
vm.startPrank(owner);
book.emergencyWithdrawERC20(address(testToken), 1000 ether, owner);
vm.stopPrank();
}

Recommended Mitigation

the function should also block emergency withdrawals of any token marked as "allowed" in allowedSellToken. This ensures that all tradable tokens, whether core or later added, are equally protected against owner misuse.

- remove this code
+ add this code
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 (
+ _tokenAddress == address(iUSDC) || allowedSellToken[_tokenAddress]
+ ) {
+ revert("Cannot withdraw allowed or core order book tokens");
+ }
if (_to == address(0)) {
revert InvalidAddress();
}
IERC20 token = IERC20(_tokenAddress);
token.safeTransfer(_to, _amount);
emit EmergencyWithdrawal(_tokenAddress, _amount, _to);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 2 months ago
yeahchibyke Lead Judge about 1 month ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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