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:
174: ctx.marketId = ctx.activeMarketsIds[j].toUint128();
175:
176:
177: PerpMarket.Data storage perpMarket = PerpMarket.load(ctx.marketId);
178:
179:
180: Position.Data storage position = Position.load(ctx.tradingAccountId, ctx.marketId);
181:
182:
183: ctx.oldPositionSizeX18 = sd59x18(position.size);
184:
185:
186: ctx.liquidationSizeX18 = -ctx.oldPositionSizeX18;
187:
188:
189: ctx.markPriceX18 = perpMarket.getMarkPrice(ctx.liquidationSizeX18, perpMarket.getIndexPrice());
190:
191:
192: ctx.fundingRateX18 = perpMarket.getCurrentFundingRate();
193: ctx.fundingFeePerUnitX18 = perpMarket.getNextFundingFeePerUnit(ctx.fundingRateX18, ctx.markPriceX18);
194:
195:
196: perpMarket.updateFunding(ctx.fundingRateX18, ctx.fundingFeePerUnitX18);
197:
198:
199: position.clear();
200:
201:
202:
203: tradingAccount.updateActiveMarkets(ctx.marketId, ctx.oldPositionSizeX18, SD59x18_ZERO);
204:
205:
206:
207:
208:
209:
210: perpMarket.updateOpenInterest(ctx.newOpenInterestX18, ctx.newSkewX18);
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 {
uint256 USER_STARTING_BALANCE = 100_000e18;
int128 USER_POS_SIZE_DELTA = 10e18;
deal({ token: address(usdz), to: users.naruto.account, give: USER_STARTING_BALANCE });
changePrank({ msgSender: users.naruto.account });
uint128 tradingAccountId = createAccountAndDeposit(USER_STARTING_BALANCE, address(usdz));
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 });
changePrank({ msgSender: users.sasuke.account });
uint128 tradingAccountId2 = createAccountAndDeposit(USER_STARTING_BALANCE*4 , address(usdz));
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());
updateMockPriceFeed(BTC_USD_MARKET_ID, MOCK_BTC_USD_PRICE / 2);
deal({ token: address(usdz), to: users.sasuke.account, give: USER_STARTING_BALANCE*4 });
changePrank({ msgSender: users.sasuke.account });
tradingAccountId2 = marginDeposit(USER_STARTING_BALANCE*4 , address(usdz) , tradingAccountId2);
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:
No one will be able to create new orders in this market. Liquidating even a single user will render the market useless.
The system will always report that there are no open positions in this market because both OpenInterest
and Skew
are set to zero.
The incorrect Skew
value will negatively impact market hedging strategies.
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.