DittoETH

Ditto
DeFiFoundryOracle
55,000 USDC
View results
Submission Details
Severity: high
Valid

All orders can be cancelled once `Asset.orderId` reaches 65,000

Summary

The only requirement for cancelling the last order of any type (bid, ask or short) is that the orderId of the asset has reached 65,000. As orderId is an ever-increasing variable, orders can be cancelled even if the number of active orders is below 65,000.

Vulnerability Details

The last order of any type (bid, ask, or short) can be cancelled by anyone by calling cancelOrderFarFromOracle with the only requirement of the orderId of the asset having reached 65,000.

Given that the orderId does never decrease, once the orderId of an asset reaches 65,000, the last orders of any type can always be cancelled by anyone, no matter the current number of active orders. This means that all the orders of an asset can be cancelled by anyone, breaking the protocol completely.

Note that the number of 65,000 orders can be reached organically, but it can also be reached by a malicious actor. This can be achieved by creating a large number of orders and cancelling them via cancelBid, cancelAsk, or cancelShort to recover the collateral locked in the orders.

Another important remark is that even if the number of active orders, instead of the orderId, was used as a condition for cancelling orders, there is another problem with this approach, as using the total number of active orders does not take into account that most of the orders can be of one type (e.g. bids) while another type (e.g. shorts) has only a few orders, and it would still be possible to cancel the last orders of the type with few orders.

Proof of Concept

Add the following code snippet into test/CancelOrder.t.sol and run forge test --mt testCancelAllOrders.

function testCancelAllOrders() public {
uint16 HEAD = 1;
uint16 TAIL = 1;
uint16 lastOrderId = 64999;
setOrderIdAndMakeOrders({orderType: O.LimitShort});
// Cancel all short orders
while (lastOrderId != HEAD) {
uint16 prevId = diamond.getShortOrder(asset, lastOrderId).prevId;
diamond.cancelOrderFarFromOracle({
asset: asset,
orderType: O.LimitShort,
lastOrderId: lastOrderId,
numOrdersToCancel: 1
});
lastOrderId = prevId;
}
// There are no short orders left
assertEq(diamond.getShortOrder(asset, HEAD).nextId, TAIL);
// Create new short order
fundLimitShortOpt(DEFAULT_PRICE, DEFAULT_AMOUNT, receiver);
// Cancel it
diamond.cancelOrderFarFromOracle({
asset: asset,
orderType: O.LimitShort,
lastOrderId: diamond.getShortOrder(asset, HEAD).nextId,
numOrdersToCancel: 1
});
}

Impact

All orders can be cancelled by anyone once the orderId of an asset reaches 65,000, which will in practice make the protocol unusable.

Tools Used

Manual inspection.

Recommendations

Add to the Asset struct three different counters for the number of active bids, asks, and shorts, and use them as conditions for cancelling orders.

Updates

Lead Judging Commences

0xnevi Lead Judge almost 2 years ago
Submission Judgement Published
Validated
Assigned finding tags:

finding-436

Support

FAQs

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