DittoETH

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

Canceling orders leads to the permanent lock of unfilled ETH/ERC

Summary

The OrdersFacet::cancelOrderFarFromOracle() does not return the unfilled ETH/ERC to owners of the canceled orders after canceling them.

As a result, the owners of the canceled orders will be unable to retrieve their unfilled ETH/ERC; they will be locked forever.

Vulnerability Details

When users place the Bid, Ask, or Short orders on the market, their escrowed ETH/ERC will be deposited into the orders through the addBid(), addAsk(), or addShort(), respectively. The addBid() will take the user's escrowed ETH into the created Bid order. The addAsk() will take the escrowed ERC into the created Ask order. Whereas the addShort() will take the escrowed ETH into the generated Short order.

When the order is matched, the deposited ETH/ERC can be partially filled or fully filled. If the order was partially filled or has never been matched, there would be some unfilled ETH/ERC locked in the order.

The OrdersFacet::cancelOrderFarFromOracle() is a public function to handle when the system's orderId has hit the limit (to deter attackers). The function can cancel Bid/Ask/Short orders that are placed far from the oracle price up to 1000 orders if the function is called by the DAO account. If the function is called by a non-DAO account, only the last order will be canceled.

However, I noticed that the cancelOrderFarFromOracle() does not return the unfilled ETH/ERC to the owners of the canceled orders after canceling them.

During the process of canceling an order, the cancelOrderFarFromOracle() will execute the LibOrders::cancelManyOrders() (in L143for Bid orders, L148 for Ask orders, or L153 for Short orders) or the LibOrders::cancelOrder() (in L163 for single Bid order, L168 for single Ask order, or L173 for single Short order).

Eventually, the cancelOrder() will take control of handling the reuse of the canceled order. Specifically, the cancelOrder() will set the canceled order's orderType property to O.Cancelled. Consequently, the canceled order will become inactive and will be re-used (replaced) by the upcoming order.

Since the orderType property will become O.Cancelled, the owner of the canceled order will be unable to retrieve their unfilled ETH/ERC via the cancelBid(), cancelAsk(), cancelShort(), or any other functions. In other words, the owner will lose their ETH/ERC forever.

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 {
//@dev if address is not DAO, you can only cancel last order of a side
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();
}
}
}
  • s.bids.cancelManyOrders(): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/OrdersFacet.sol#L143

  • s.asks.cancelManyOrders(): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/OrdersFacet.sol#L148

  • s.shorts.cancelManyOrders(): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/OrdersFacet.sol#L153

  • s.bids.cancelOrder(): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/OrdersFacet.sol#L163

  • s.asks.cancelOrder(): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/OrdersFacet.sol#L168

  • s.shorts.cancelOrder(): https://github.com/Cyfrin/2023-09-ditto/blob/a93b4276420a092913f43169a353a6198d3c21b9/contracts/facets/OrdersFacet.sol#L173

Impact

The owners of the canceled orders will be unable to retrieve their unfilled ETH/ERC; they will be locked forever.

Tools Used

Manual Review

Recommendations

Return the unfilled ETH/ERC to the canceled orders' owners in the cancelOrderFarFromOracle(). The following presents the code snippets for returning the unfilled ETH/ERC to their owner.

  • Return the unfilled ETH to the canceled Bid order's owner

// --- Return the unfilled ETH to the canceled Bid order's owner ---
STypes.Order storage bid = s.bids[asset][id];
uint256 vault = s.asset[asset].vault;
uint88 eth = bid.ercAmount.mulU88(bid.price);
s.vaultUser[vault][bid.addr].ethEscrowed += eth;
  • Return the unfilled ERC to the canceled Ask order's owner

// --- Return the unfilled ERC to the canceled Ask order's owner ---
STypes.Order storage ask = s.asks[asset][id];
s.assetUser[asset][ask.addr].ercEscrowed += ask.ercAmount;
  • Return the unfilled ETH to the canceled Short order's owner

// --- Return the unfilled ETH to the canceled Short order's owner ---
STypes.Order storage short = s.shorts[asset][id];
STypes.Asset storage Asset = s.asset[asset];
uint88 eth = short.ercAmount.mulU88(short.price).mulU88(
LibOrders.convertCR(short.initialMargin)
);
s.vaultUser[Asset.vault][short.addr].ethEscrowed += eth;
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.