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

Liquidation will make OpenInterest and Skew 0

Summary

The Protocol allows whitelisted liquidator to liquidate other users' positions in the market. However, after liquidation, it incorrectly updates the skew and openInterest to zero, impacting the market in several ways. Please refer to the impact section of the report for detailed effects.

Vulnerability Details

If a user's position lacks sufficient collateral to maintain it, the position must be liquidated via calling liquidateAccounts. For liquidation, Zeros only allow whitelisted users to perform the process. However, during liquidation, the protocol incorrectly updates the OpenInterest and Skew values to 0.

2024-07-zaros/src/perpetuals/branches/LiquidationBranch.sol:106
106: function liquidateAccounts(uint128[] calldata accountsIds) external {
.........
172: for (uint256 j; j < ctx.activeMarketsIds.length; j++) {
173: // load current active market id into working data
174: ctx.marketId = ctx.activeMarketsIds[j].toUint128();
175:
176: // fetch storage slot for perp market
177: PerpMarket.Data storage perpMarket = PerpMarket.load(ctx.marketId);
178:
179: // load position data for user being liquidated in this market
180: Position.Data storage position = Position.load(ctx.tradingAccountId, ctx.marketId);
181:
182: // save open position size
183: ctx.oldPositionSizeX18 = sd59x18(position.size);
184:
185: // save inverted sign of open position size to prepare for closing the position
186: ctx.liquidationSizeX18 = -ctx.oldPositionSizeX18;
187:
188: // calculate price impact of open position being closed
189: ctx.markPriceX18 = perpMarket.getMarkPrice(ctx.liquidationSizeX18, perpMarket.getIndexPrice());
190:
191: // get current funding rates
192: ctx.fundingRateX18 = perpMarket.getCurrentFundingRate();
193: ctx.fundingFeePerUnitX18 = perpMarket.getNextFundingFeePerUnit(ctx.fundingRateX18, ctx.markPriceX18);
194:
195: // update funding rates for this perpetual market
196: perpMarket.updateFunding(ctx.fundingRateX18, ctx.fundingFeePerUnitX18);
197:
198: // reset the position
199: position.clear();
200:
201: // update account's active markets; this calls EnumerableSet::remove which
202: // is why we are iterating over a memory copy of the trader's active markets
203: tradingAccount.updateActiveMarkets(ctx.marketId, ctx.oldPositionSizeX18, SD59x18_ZERO);
204:
205: // update perp market's open interest and skew; we don't enforce ipen
206: // interest and skew caps during liquidations as:
207: // 1) open interest and skew are both decreased by liquidations
208: // 2) we don't want liquidation to be DoS'd in case somehow those cap
209: // checks would fail
210: perpMarket.updateOpenInterest(ctx.newOpenInterestX18, ctx.newSkewX18); //@audit-issue : values zet to 0
211: }
212:

Line 210: we can see that ctx.newOpenInterestX18 and ctx.newSkewX18 are used to set new values for market. There is not values assign to these values in liquidateAccounts function.

POC:

Add Following function to Base.t.sol:

function marginDeposit(
uint256 amount,
address collateralType,
uint128 tradingAccountId
)
internal
returns (uint128)
{
perpsEngine.depositMargin(tradingAccountId, collateralType, amount);
return tradingAccountId;
}

Add this test code to createMatketOrder.t.sol:

function test_liquidation_will_create_DoS_for_new_order() external {
// give naruto some tokens
uint256 USER_STARTING_BALANCE = 100_000e18;
int128 USER_POS_SIZE_DELTA = 10e18;
// deal({ token: usdz, to: users.naruto, give: USER_STARTING_BALANCE });
deal({ token: address(usdz), to: users.naruto.account, give: USER_STARTING_BALANCE });
// naruto creates a trading account and deposits their tokens as collateral
changePrank({ msgSender: users.naruto.account });
uint128 tradingAccountId = createAccountAndDeposit(USER_STARTING_BALANCE, address(usdz));
// naruto opens first position in BTC market
openManualPosition(
BTC_USD_MARKET_ID, BTC_USD_STREAM_ID, MOCK_BTC_USD_PRICE, tradingAccountId, USER_POS_SIZE_DELTA
);
deal({ token: address(usdz), to: users.sasuke.account, give: USER_STARTING_BALANCE*4 });
// sasuke creates a trading account and deposits their tokens as collateral
changePrank({ msgSender: users.sasuke.account });
uint128 tradingAccountId2 = createAccountAndDeposit(USER_STARTING_BALANCE*4 , address(usdz));
// naruto opens first position in BTC market
openManualPosition(
BTC_USD_MARKET_ID, BTC_USD_STREAM_ID, MOCK_BTC_USD_PRICE, tradingAccountId2, USER_POS_SIZE_DELTA
);
(UD60x18 longsOpenInterest, UD60x18 shortsOpenInterest, UD60x18 totalOpenInterest) = perpsEngine.getOpenInterest(BTC_USD_MARKET_ID);
console.log("Open Interest:" , totalOpenInterest.intoUint256());
// make BTC position liquidatable
updateMockPriceFeed(BTC_USD_MARKET_ID, MOCK_BTC_USD_PRICE / 2);
deal({ token: address(usdz), to: users.sasuke.account, give: USER_STARTING_BALANCE*4 });
// sasuke creates a trading account and deposits their tokens as collateral
changePrank({ msgSender: users.sasuke.account });
tradingAccountId2 = marginDeposit(USER_STARTING_BALANCE*4 , address(usdz) , tradingAccountId2);
// verify naruto can now be liquidated
uint128[] memory liquidatableAccountsIds = perpsEngine.checkLiquidatableAccounts(0, 2);
assertEq(2, liquidatableAccountsIds.length);
console.log("Liq Balance" , liquidatableAccountsIds[0]);
changePrank({ msgSender: liquidationKeeper });
perpsEngine.liquidateAccounts(liquidatableAccountsIds);
( longsOpenInterest, shortsOpenInterest, totalOpenInterest) = perpsEngine.getOpenInterest(BTC_USD_MARKET_ID);
console.log("Open Interest:" , totalOpenInterest.intoUint256());
changePrank({ msgSender: users.sasuke.account });
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: tradingAccountId2,
marketId: BTC_USD_MARKET_ID,
sizeDelta: USER_POS_SIZE_DELTA
})
);
}

Run with cmd : forge test --mt test_liquidation_will_create_DoS_for_new_order -vvv.

Impact

This issue has the following impacts:

  1. No one will be able to create new orders in this market. Liquidating even a single user will render the market useless.

  2. The system will always report that there are no open positions in this market because both OpenInterest and Skew are set to zero.

  3. The incorrect Skew value will negatively impact market hedging strategies.

  4. The market will be mistakenly perceived as inactive, despite ongoing activity.

Tools Used

Manual Review

Recommendations

Recalculate the New OpenInterest and Skew after each Liquidation.

Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months 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.