When a liquidation for a position is executed, all market open positions are intended to be removed and all the margin transfered to some protocol receivers. This is the coded implementation to liquidate each position:
struct LiquidationContext {
UD60x18 liquidationFeeUsdX18;
uint128 tradingAccountId;
SD59x18 marginBalanceUsdX18;
UD60x18 liquidatedCollateralUsdX18;
uint256[] activeMarketsIds;
uint128 marketId;
SD59x18 oldPositionSizeX18;
SD59x18 liquidationSizeX18;
UD60x18 markPriceX18;
SD59x18 fundingRateX18;
SD59x18 fundingFeePerUnitX18;
UD60x18 newOpenInterestX18;
SD59x18 newSkewX18;
}
function liquidateAccounts(uint128[] calldata accountsIds) external {
...
LiquidationContext memory ctx;
for (uint256 i; i < accountsIds.length; i++) {
ctx.tradingAccountId = accountsIds[i];
if (ctx.tradingAccountId == 0) continue;
TradingAccount.Data storage tradingAccount = TradingAccount.loadExisting(ctx.tradingAccountId);
(, UD60x18 requiredMaintenanceMarginUsdX18, SD59x18 accountTotalUnrealizedPnlUsdX18) =
tradingAccount.getAccountMarginRequirementUsdAndUnrealizedPnlUsd(0, SD59x18_ZERO);
ctx.marginBalanceUsdX18 = tradingAccount.getMarginBalanceUsd(accountTotalUnrealizedPnlUsdX18);
if (!TradingAccount.isLiquidatable(requiredMaintenanceMarginUsdX18, ctx.marginBalanceUsdX18)) {
continue;
}
ctx.liquidatedCollateralUsdX18 = tradingAccount.deductAccountMargin({
feeRecipients: FeeRecipients.Data({
marginCollateralRecipient: globalConfiguration.marginCollateralRecipient,
orderFeeRecipient: address(0),
settlementFeeRecipient: globalConfiguration.liquidationFeeRecipient
}),
pnlUsdX18: requiredMaintenanceMarginUsdX18,
orderFeeUsdX18: UD60x18_ZERO,
settlementFeeUsdX18: ctx.liquidationFeeUsdX18
});
MarketOrder.load(ctx.tradingAccountId).clear();
ctx.activeMarketsIds = tradingAccount.activeMarketsIds.values();
for (uint256 j; j < ctx.activeMarketsIds.length; j++) {
ctx.marketId = ctx.activeMarketsIds[j].toUint128();
PerpMarket.Data storage perpMarket = PerpMarket.load(ctx.marketId);
Position.Data storage position = Position.load(ctx.tradingAccountId, ctx.marketId);
ctx.oldPositionSizeX18 = sd59x18(position.size);
ctx.liquidationSizeX18 = -ctx.oldPositionSizeX18;
ctx.markPriceX18 = perpMarket.getMarkPrice(ctx.liquidationSizeX18, perpMarket.getIndexPrice());
ctx.fundingRateX18 = perpMarket.getCurrentFundingRate();
ctx.fundingFeePerUnitX18 = perpMarket.getNextFundingFeePerUnit(ctx.fundingRateX18, ctx.markPriceX18);
perpMarket.updateFunding(ctx.fundingRateX18, ctx.fundingFeePerUnitX18);
position.clear();
tradingAccount.updateActiveMarkets(ctx.marketId, ctx.oldPositionSizeX18, SD59x18_ZERO);
perpMarket.updateOpenInterest(ctx.newOpenInterestX18, ctx.newSkewX18);
}
emit LogLiquidateAccount(
msg.sender,
ctx.tradingAccountId,
ctx.activeMarketsIds.length,
requiredMaintenanceMarginUsdX18.intoUint256(),
ctx.marginBalanceUsdX18.intoInt256(),
ctx.liquidatedCollateralUsdX18.intoUint256(),
ctx.liquidationFeeUsdX18.intoUint128()
);
}
}
Looking closely to the sequence of actions that this function does we can sum up like this:
This process works fine apart from the update of open interest and skew. If you look closely to the implementation it just calls the function updateOpenInterest
with the variables ctx.newOpenInterestX18
and ctx.newSkewX18
. If you try to find where these variables are set within the liquidate function you will notice that they are not set during the whole execution, that means that they will be 0. And when it calls updateOpenInterest
it will reset and override the open interest and skew for each market of the position being liquidate.
This issue has a high impact for the protocol because the skew is used to balance the amount of short and long positions in the protocol and to incentive its balancing via maker/taker fees and constraining a maximum allowed skew. The open interest is also a security measure to set a maximum amount of open size, so if it gets reset, this protection will be completely useless and will not take effect.
It is true that this issue only happens for the active markets of a position being liquidated, but a user can open a size in all markets in the protocol on purpose and when he will get automatically liquidated, it will reset all markets open interest and skew.
High impact, these 2 variables are really important for the protocol integrity and are reset automatically when positions get liquidated, so it will happen often.
function liquidateAccounts(uint128[] calldata accountsIds) external {
...
// save open position size
ctx.oldPositionSizeX18 = sd59x18(position.size);
// save inverted sign of open position size to prepare for closing the position
ctx.liquidationSizeX18 = -ctx.oldPositionSizeX18;
+ UD60x18 currentOpenInterest = ud60x18(perpMarket.openInterest);
+ ctx.newOpenInterestX18 = currentOpenInterest.sub(ctx.liquidationSizeX18.abs().intoUD60x18()).intoUD60x18();
+ SD59x18 currentSkew = sd59x18(perpMarket.skew);
+ ctx.newSkewX18 = currentSkew.add(ctx.liquidationSizeX18);
perpMarket.updateOpenInterest(ctx.newOpenInterestX18, ctx.newSkewX18);
...
}