Incorrect position update order relative to isLiquidatable
check in processMakerFill
permits fund theft
Description
The processMakerFill
function performs an isLiquidatable
check to ensure that settling a maker's limit order does not immediately make the maker's account liquidatable.
function processMakerFill(ClearingHouse storage self, bytes32 asset, MakerFillParams memory params)
internal
returns (bool unfillable)
{
// [...]
if (self.isLiquidatable(params.maker, params.subaccount, margin)) {
market.setPosition(params.maker, params.subaccount, position); // revert to old position
return true;
}
// [...]
if (result.fullClose) self.positions[params.maker][params.subaccount].remove(asset);
else self.positions[params.maker][params.subaccount].add(asset);
// [...]
}
However, the asset
data is added to the account's positions
EnumerableSet only after this isLiquidatable
check. Consequently, if the maker does not have an existing position in the specified asset
market, the isLiquidatable
check does not account for the newly settled position in that market.
If the isLiquidatable
check does not consider this new position, the maker's account can incur significant bad debt if the trade execution price deviates substantially from the index price. An attacker filling the limit order can then capture this bad debt as profit.
Impact
An attacker can profit from this vulnerability by performing the following steps:
Clear the order book by filling existing orders to allow subsequent orders to be settled at arbitrary prices.
Use a sacrificial account (account A) to place a limit order at a price designed to create immediate debt for account A upon settlement.
Use another account (account B) to fill this order, generating unrealized profit and loss (PNL) for account B from account A's debt.
Use a third sacrificial account (account C) to place a limit order that, when matched, closes account B's position.
Use account B to fill account C's order, thereby converting account B's unrealized PNL into realized profit.
Abandon accounts A and C, retaining the extracted funds in account B.
Because orders can be settled at arbitrary prices once the order book is cleared and there is no cap on trading volume, an attacker's potential profit can significantly outweigh the cost of performing this attack. Such an attack could potentially result in the draining of all funds from the contract.
Recommendations
We recommend performing the isLiquidatable
check only after the account's positions
EnumerableSet is updated to include the new asset
market.
Remediation
This issue has been acknowledged by Liquid Labs, Inc., and fixes were implemented in the following commits: