The protocol allows users to open positions with leverage significantly exceeding 100x. This high leverage capability is not properly restricted, potentially leading to extreme systemic risks.
The root cause is the absence of adequate checks on the relationship between the position size and the user's margin balance when creating market orders in OrderBranch::createMarketOrder.
This vulnerability not only contradicts the protocol's documented leverage limit but also exposes users to extreme risk levels that may not be fully understood or anticipated. The lack of proper safeguards could lead to rapid, cascading liquidations.
struct TestHighLeveragePosition {
MarketConfig fuzzMarketConfig;
uint128 tradingAccountId;
MockPriceFeed priceFeed;
uint256 initialPrice;
uint256 newPrice;
}
function test_HighLeveragePosition() public {
TestHighLeveragePosition memory vars;
vars.fuzzMarketConfig = getFuzzMarketConfig(BTC_USD_MARKET_ID);
deal({ token: address(usdc), to: users.naruto.account, give: 1_000_000e6 });
changePrank({ msgSender: users.naruto.account });
vars.tradingAccountId = createAccountAndDeposit(10_000e6, address(usdc));
vars.priceFeed = MockPriceFeed(vars.fuzzMarketConfig.priceAdapter);
vars.initialPrice = 10_000e18;
vars.priceFeed.updateMockPrice(vars.initialPrice);
console.log("\nInitial BTC price:", vars.initialPrice);
int128 positionSize = 300e18;
changePrank({ msgSender: users.naruto.account });
perpsEngine.createMarketOrder(
OrderBranch.CreateMarketOrderParams({
tradingAccountId: vars.tradingAccountId,
marketId: vars.fuzzMarketConfig.marketId,
sizeDelta: positionSize
})
);
changePrank({ msgSender: marketOrderKeepers[vars.fuzzMarketConfig.marketId] });
perpsEngine.fillMarketOrder(
vars.tradingAccountId,
vars.fuzzMarketConfig.marketId,
getMockedSignedReport(vars.fuzzMarketConfig.streamId, vars.initialPrice)
);
Position.Data memory position = perpsEngine.exposed_Position_load(vars.tradingAccountId, vars.fuzzMarketConfig.marketId);
Position.State memory state = perpsEngine.exposed_getState(
vars.tradingAccountId,
vars.fuzzMarketConfig.marketId,
ud60x18(vars.fuzzMarketConfig.imr),
ud60x18(vars.fuzzMarketConfig.mmr),
ud60x18(vars.initialPrice),
sd59x18(position.lastInteractionFundingFeePerUnit)
);
(, , SD59x18 unrealizedPnlUsdX18) = perpsEngine.exposed_getAccountMarginRequirementUsdAndUnrealizedPnlUsd(
vars.tradingAccountId,
0,
SD59x18_ZERO
);
SD59x18 initialMarginBalanceUsdX18 = TradingAccountHarness(address(perpsEngine)).exposed_getMarginBalanceUsd(
vars.tradingAccountId,
unrealizedPnlUsdX18
);
console.log("Position size:", uint256(position.size));
console.log("Notional value:", state.notionalValueX18.intoUint256());
console.log("Margin:", initialMarginBalanceUsdX18.intoInt256());
uint256 notionalValue = (uint256(int256(position.size)) * vars.initialPrice) / 1e18;
uint256 margin = initialMarginBalanceUsdX18.intoUint256();
uint256 leverage = (notionalValue * 1e18) / margin;
console.log("Actual leverage:", leverage / 1e18);
assert(leverage > 100e18);
vars.newPrice = 10_100e18;
vars.priceFeed.updateMockPrice(vars.newPrice);
console.log("\nNew BTC price:", vars.newPrice);
(, , unrealizedPnlUsdX18) = perpsEngine.exposed_getAccountMarginRequirementUsdAndUnrealizedPnlUsd(
vars.tradingAccountId,
0,
SD59x18_ZERO
);
SD59x18 finalMarginBalanceUsdX18 = TradingAccountHarness(address(perpsEngine)).exposed_getMarginBalanceUsd(
vars.tradingAccountId,
unrealizedPnlUsdX18
);
int256 newMarginBalance = finalMarginBalanceUsdX18.intoInt256();
console.log("New margin balance:", uint256(newMarginBalance));
int256 initialMargin = initialMarginBalanceUsdX18.intoInt256();
int256 pnl = unrealizedPnlUsdX18.intoInt256();
int256 percentageIncrease = (pnl * 100e18) / initialMargin;
console.log("PnL from 1% price increase:", unrealizedPnlUsdX18.intoInt256());
console.log("Percentage increase:", uint256(percentageIncrease));
assertAlmostEq(percentageIncrease, 300e18, 1e20);
}
Moreover it goes against the protocol claims to allow position with leverage up to x100.