A malicious actor could block a market if minLimitOrderAmountInBase
is set too low
Description
The minLimitOrderAmountInBase
setting for a market specifies the minimum base asset amount acceptable for placing a limit order on the order book.
The lower bound for minLimitOrderAmountInBase
is 10, as defined by the constant MIN_MIN_LIMIT_ORDER_AMOUNT_BASE
:
uint256 constant MIN_MIN_LIMIT_ORDER_AMOUNT_BASE = 10;
If a market sets minLimitOrderAmountInBase
to this low value, users can place orders with this minimum amount at the minimum-allowed price. When such an order is filled, matchData.quoteDelta
can become zero due to rounding down during division within the _matchIncomingOrder()
function.
function _matchIncomingOrder(
PerpBook storage ds,
Order storage matchedOrder,
Order memory incomingOrder,
bool amountIsBase // true if incomingOrder is in base, false if in quote
) internal returns (MatchData memory matchData) {
// [...]
if (amountIsBase) {
// denominated in base
matchData.baseDelta = matchData.matchedAmount = matchedOrder.amount.min(incomingOrder.amount);
matchData.quoteDelta = matchData.baseDelta.fullMulDiv(matchedOrder.price, 1e18);
} else {
// denominated in quote
matchData.baseDelta = matchedOrder.amount.min(incomingOrder.amount.fullMulDiv(1e18, matchedOrder.price));
matchData.quoteDelta = matchData.baseDelta.fullMulDiv(matchedOrder.price, 1e18);
matchData.matchedAmount =
matchData.baseDelta != matchedOrder.amount ? incomingOrder.amount : matchData.quoteDelta;
}
// [...]
}
Subsequently, the outer function of _matchIncomingOrder()
, such as _matchIncomingBid()
, could fail. This failure occurs because a sanity check prevents result.totalQuoteTokenSent
from being zero when result.totalBaseTokenReceived
is nonzero.
function _matchIncomingBid(PerpBook storage ds, Order memory incomingOrder, bool amountIsBase)
internal
returns (MatchResult memory result)
{
uint256 bestAsk = ds.getBestAsk();
while (bestAsk <= incomingOrder.price && incomingOrder.amount > 0) {
// [...]
MatchData memory data = _matchIncomingOrder(ds, bestAskOrder, incomingOrder, amountIsBase);
incomingOrder.amount -= data.matchedAmount;
result.totalBaseTokenReceived += data.baseDelta;
result.totalQuoteTokenSent += data.quoteDelta;
result.totalQuoteTokenFees += data.takerFee;
bestAsk = ds.getBestAsk();
}
if (result.totalBaseTokenReceived == 0 || result.totalQuoteTokenSent == 0) {
if (result.totalBaseTokenReceived != result.totalQuoteTokenSent) revert ZeroCostTrade();
}
}
Consequently, aggregations of orders with such a low amount cannot be settled when matched.
Impact
If minLimitOrderAmountInBase
is set to its minimum value of 10, a malicious actor could block the market by
filling all sell orders on the order book, which is easier if the market is newly created.
placing numerous sell limit orders with the lowest allowed amount at the lowest allowed price. Since these orders set the best ask price, bid orders will attempt to match with them. However, these orders cause reverts during settlement, preventing users from filling bid orders and effectively blocking the market.
Recommendations
We recommend to increase the MIN_MIN_LIMIT_ORDER_AMOUNT_BASE
constant. The new minimum value must be set sufficiently high to ensure that matchData.quoteDelta
, calculated during order matching, does not round down to zero.
Remediation
This issue has been acknowledged by Liquid Labs, Inc., and a fix was implemented in commit ceba555c↗.