Core Tokens Can Be Permanently Locked Due to Withdrawal Restrictions
Description
The emergencyWithdrawERC20
function is designed to block the withdrawal of core tokens (WETH, WBTC, WSOL, USDC) to safeguard assets required for protocol operations. However, this restriction means that any core tokens mistakenly transferred directly to the contract address become permanently locked.
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:
A user mistakenly transfers core tokens (WETH, WBTC, WSOL, or USDC) directly to the contract address instead of using one of the contract's functions.
Impact:
The user's mistakenly transferred assets are permanently locked, as there is no function to withdraw them.
Proof of Concept
Below scenario shows that Alice mistakenly transferred WBTC to the OrderBook contract, the withdrawal is reverted and failed to recover.
function test_audit_lockedAsset() public {
uint256 mishandledWBTCAmount = 1e8;
vm.prank(alice);
wbtc.transfer(address(book), mishandledWBTCAmount);
assertEq(wbtc.balanceOf(address(book)), mishandledWBTCAmount);
vm.prank(owner);
vm.expectRevert("Cannot withdraw core order book tokens via emergency function");
book.emergencyWithdrawERC20(address(wbtc), mishandledWBTCAmount, alice);
}
Recommended Mitigation
Implement a mechanism to track locked assets required for active orders (e.g., a lockedAmounts
mapping). Modify emergencyWithdrawERC20
to allow the owner to withdraw any surplus tokens that are not locked in active orders, enabling the recovery of mistakenly sent funds.
+ mapping(address => uint256) public lockedAmounts;
+ createSellOrder: lockedAmounts[_tokenToSell] += _amountToSell;
+ buyOrder / cancelSellOrder: lockedAmounts[order.tokenToSell] -= order.amountToSell;
function emergencyWithdrawERC20(address _tokenAddress, 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);
+ uint256 totalBalance = token.balanceOf(address(this));
+ uint256 locked = lockedAmounts[_tokenAddress];
+ require(totalBalance - locked > _amount);
token.safeTransfer(_to, _amount);
emit EmergencyWithdrawal(_tokenAddress, _amount, _to);
}