Failure to update order state after withdrawal in OrderBook::emergencyWithdrawERC20
Description
When a user calls OrderBook::emergencyWithdrawERC20
, the function is not updating the state/status of the order. Thus, it will create confusion for users on which order is active or not. This will affect all orders that are withdrawn through the OrderBook::emergencyWithdrawERC20
function.
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: High
Impact: Medium
Proof of Concept
Input this unit test in TestOrderBook.t.sol. I also had to create a new "MockERC20" in the "test/mocks" folder to showcase this vulnerability using a non-core token.
function test_orderStatusNotChangedAfterEmergencyWithdrawal() public {
MockERC20 mockToken = new MockERC20(18);
vm.prank(owner);
book.setAllowedSellToken(address(mockToken), true);
vm.stopPrank();
vm.startPrank(alice);
mockToken.mint(alice, 1000);
mockToken.approve(address(book), 1000e18);
uint256 aliceId = book.createSellOrder(address(mockToken), 1000e18, 500e6, 2 days);
vm.stopPrank();
vm.warp(block.timestamp + 1 days);
assert(mockToken.balanceOf(alice) == 0);
console2.log("Alice's Mock Token balance after creating order:", mockToken.balanceOf(alice));
vm.prank(owner);
book.emergencyWithdrawERC20(address(mockToken), 1000e18, address(alice));
assert(mockToken.balanceOf(alice) == 1000e18);
assert(mockToken.balanceOf(address(book)) == 0);
console2.log("Alice's Mock Token balance after emergency withdrawal:", mockToken.balanceOf(alice));
string memory orderDetails = book.getOrderDetailsString(aliceId);
console2.log("Order details after emergency withdrawal:", orderDetails);
}
Here are the output logs showing that order status is still active after withdrawal:
Ran 1 test for test/TestOrderBook.t.sol:TestOrderBook
[PASS] test_orderStatusNotChangedAfterEmergencyWithdrawal() (gas: 1273338)
Logs:
Alice's Mock Token balance after creating order: 0
Alice's Mock Token balance after emergency withdrawal: 1000000000000000000000
Order details after emergency withdrawal: Order ID: 1
Seller: 0xaf6db259343d020e372f4ab69cad536aaf79d0ac
Selling: 1000000000000000000000
Asking Price: 500000000 USDC
Deadline Timestamp: 172801
Status: Active
Recommended Mitigation
Add two lines below. The order line to keep track of which order is cancelled. The "order.isActive" line to update state to false/inactive after the emergency withdrawal.
function emergencyWithdrawERC20(address _tokenAddress, uint256 _amount, address _to) external onlyOwner {
+ Order storage order = orders[_orderId];
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();
}
+ order.isActive = false;
IERC20 token = IERC20(_tokenAddress);
token.safeTransfer(_to, _amount);
emit EmergencyWithdrawal(_tokenAddress, _amount, _to);
}