DeFiFoundry
60,000 USDC
View results
Submission Details
Severity: high
Valid

Liquidations reset open interest and skew, which breaks price calculations for an entire Perp Market

Summary

liquidateAccounts() function inside Liquidationbranch.sol updates the open interest and skew values of the perpetual market with uninitialized values (which default to 0), compromising price calculations for that market.

Vulnerability Details

liquidateAccounts() function is responsible for liquidating all market positions, whose margin collateral ratio has dropped below the healthy threshold defined by the protocol.

Reference to code: link

function liquidateAccounts(uint128[] calldata accountsIds) external {
...
// iterate over every account being liquidated; intentionally not caching
// length as reading from calldata is faster
for (uint256 i; i < accountsIds.length; i++) {
...
// iterate over memory copy of active market ids
// intentionally not caching length as expected size < 3 in most cases
for (uint256 j; j < ctx.activeMarketsIds.length; j++) {
...
// update perp market's open interest and skew; we don't enforce ipen
// interest and skew caps during liquidations as:
// 1) open interest and skew are both decreased by liquidations
// 2) we don't want liquidation to be DoS'd in case somehow those cap
// checks would fail
@> perpMarket.updateOpenInterest(ctx.newOpenInterestX18, ctx.newSkewX18);
}
...
}
}

The function iterates through every account and then through every market position of that account and after doing all the necessary checks and actions it calls updateOpenInterest() as the final step of each nested loop.

The function itself updates two important storage variables of a perpMarket:

function updateOpenInterest(Data storage self, UD60x18 newOpenInterest, SD59x18 newSkew) internal {
self.skew = newSkew.intoInt256().toInt128();
self.openInterest = newOpenInterest.intoUint128();
}
  • openInterest - stores the total open positions in the market and is used to track if the market limits have been reached

  • skew - the current skew of a market. This value is very important since it is used for the calculation of the market price through perpMarket.getMarkPrice() . It is a vital function used throughout the whole protocol for creating orders, filling orders, liquidations and etc.

The problem here is that when updateOpenInterest() is called inside liquidateAccounts the provided parameters - ctx.newOpenInterestX18 & ctx.newSkewX18 are both in their default (uninitialized) state, which is 0. If you go through the flow of the function you can see that both fields of the ctx struct are not set anywhere and the updateOpenInterest call is actually the first time where they get used (with their initial 0 values). Compare this with SettlementBranch._fillOrder() and you will see how those should get calculated before being passed to the update function

The final result is the reset of the openInterest & skew for the whole perpMarket when a single market position of an trading account gets liquidated which affects all other existing positions in this marker.

Impact

  • Because openInterest is nullified the PerpMarket.checkOpenInterestLimits() function will not track the required limits properly and all other logic depending on that variable will be distorted market wide.

  • Because skew is nullified, the PerpMarket.getMarkPrice() function - which is a cornerstone for most operations in Zaros - will calculate prices at significantly distorted rates, affecting almost anything in the system - order creation, settlement, liquidations etc.

Tools Used

Manual Review

Recommended Mitigation

Similar to SettlementBranch._fillOrder(), first calculate the new values of openInterest & skew and then provide them as arguments to the update function.

Since this is the liquidation function and it should not revert, the checkOpenInterestLimits() function used in SettlementBranch._fillOrder() should also be modified so that it does not revert during liquidation in case some limits are reached. You can add an additional parameter to it to conditionally check for limits when needed:

function checkOpenInterestLimits(
Data storage self,
SD59x18 sizeDelta,
SD59x18 oldPositionSize,
SD59x18 newPositionSize,
+ bool checkLimits
)
internal
view
returns (UD60x18 newOpenInterest, SD59x18 newSkew)
{
...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

`liquidateAccounts` calls `updateOpenInterest` with uninitialized OI and skew)

Support

FAQs

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

Give us feedback!