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

Uncapped leverage allows positions exceeding 300x, violating Protocol's 100x limit

Summary

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.

Vulnerability Details

Proof of Code

Add the following code to the test suite:

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 }); // 1 million USDC
changePrank({ msgSender: users.naruto.account });
vars.tradingAccountId = createAccountAndDeposit(10_000e6, address(usdc)); // Deposit 10,000 USDC
vars.priceFeed = MockPriceFeed(vars.fuzzMarketConfig.priceAdapter);
vars.initialPrice = 10_000e18; // $10,000 per BTC
vars.priceFeed.updateMockPrice(vars.initialPrice);
console.log("\nInitial BTC price:", vars.initialPrice);
// Calculate position size for 300x leverage
// 10,000 USDC * 300 = $3,000,000 position value
// $3,000,000 / $10,000 per BTC = 300 BTC
int128 positionSize = 300e18; // 300 BTC
// Open a long position with 200x leverage
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)
);
// Check the position details
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());
// Calculate and verify leverage
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 that leverage is greater than 100x
assert(leverage > 100e18);
// Test the impact of a small price movement
vars.newPrice = 10_100e18; // 1% increase
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
);
// Recalculate margin balance after price movement
int256 newMarginBalance = finalMarginBalanceUsdX18.intoInt256();
console.log("New margin balance:", uint256(newMarginBalance));
// Calculate the percentage increase
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));
// Assert that the percentage increase is approximately 300% (300x leverage * 1% price change)
assertAlmostEq(percentageIncrease, 300e18, 1e20);
}

Impact

The ability to use excessive leverage can lead to:

  • Increased risk of sudden, cascading liquidations during minor market movements.

  • Heightened risk of significant losses for traders, potentially exceeding their initial deposits.

  • Increased strain on the protocol's insurance fund and potential for socialized losses.

Moreover it goes against the protocol claims to allow position with leverage up to x100.

Tools Used

Manual testing

Recommendations

Add a configurable MAX_ALLOWED_LEVERAGE parameter to the protocol's global configuration in src/utils/Constants.sol and implement a maximum leverage limit in the createMarketOrder function:

function createMarketOrder(OrderBranch.CreateMarketOrderParams memory params) public {
// ... existing checks ...
+ uint256 accountMargin = getAccountMarginBalance(params.tradingAccountId).intoUint256();
+ uint256 positionValue = uint256(abs(params.sizeDelta)) * getMarkPrice(params.marketId);
+ uint256 leverage = positionValue * 1e18 / accountMargin;
+ if(leverage > MAX_ALLOWED_LEVERAGE) {
+ raise Errors.ExceedMaxLeverage();
+ }
// ... rest of the function ...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge
over 1 year ago
inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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

Give us feedback!