DeFiFoundry
50,000 USDC
View results
Submission Details
Severity: low
Invalid

Leverage Boundary Violation Through Position Size Manipulation

Summary

PerpetualVault contract's leverage invariant can be violated allowing positions to exceed the maximum leverage of 10000 (BASIS_POINTS_DIVISOR). This breaks a core safety mechanism designed to prevent over-leveraged positions and could lead to increased liquidation risks. Looking at the contract interactions, we can see that leverage values are managed through the PerpetualVault contract while actual position creation happens via GmxProxy.

function _createIncreasePosition(
bool _isLong,
uint256 acceptablePrice,
MarketPrices memory prices
) internal {
// [VULNERABLE POINT 1]
// No validation of effective leverage including fees and price impact
uint256 amountIn;
// [STATE DEPENDENCY]
// Position size depends on flow state
if (flow == FLOW.DEPOSIT) {
amountIn = depositInfo[counter].amount;
flowData = vaultReader.getPositionSizeInTokens(curPositionKey);
} else {
amountIn = collateralToken.balanceOf(address(this));
}
// [CRITICAL CALCULATION]
// This calculation doesn't account for:
// 1. GMX position fees
// 2. Price impact
// 3. Funding rates
uint256 sizeDelta = prices.shortTokenPrice.max * amountIn * leverage / BASIS_POINTS_DIVISOR;
// [EXTERNAL INTERACTION]
// Position creation happens asynchronously through GMX
// Actual leverage could differ from calculated due to market conditions
IGmxProxy.OrderData memory orderData = IGmxProxy.OrderData({
market: market,
indexToken: indexToken,
initialCollateralToken: address(collateralToken),
swapPath: new address[](0),
isLong: _isLong,
sizeDeltaUsd: sizeDelta, // [VULNERABLE POINT 2] - No validation of final effective leverage
initialCollateralDeltaAmount: 0,
amountIn: amountIn,
callbackGasLimit: callbackGasLimit,
acceptablePrice: acceptablePrice,
minOutputAmount: 0
});
_gmxLock = true;
gmxProxy.createOrder(orderType, orderData);
}

This leverage calculation here is doesn't account for various factors that affect the final position size when executed by GMX's keepers.

The path involves:

  1. PerpetualVault's position management functions

  2. GmxProxy's order creation and execution

  3. VaultReader's position calculations

The leverage boundary check is meant to prevent users from taking positions with leverage higher than 10000 (100%). However, the current implementation allows this invariant to be broken through specific interaction patterns with GMX's position management system.

The VaultReader contract calculates position sizes and leverage, while GmxProxy executes the actual trades. The missing or insufficient validation in position creation allows leverage to exceed the intended maximum.

When this invariant is violated, positions can be created with leverage exceeding 100%, which:

  1. Increases liquidation risks

  2. Violates core protocol safety assumptions

  3. Could lead to unexpected behavior in position management functions

The issue lies in insufficient leverage validation during position creation and management.

Vulnerability Details

The Perpetual Vault protocol manages leveraged positions with a critical safety invariant, no position should exceed the BASIS_POINTS_DIVISOR (10,000) leverage limit. However, I've discovered a path where this boundary can be breached. In PerpetualVault.sol where positions are managed through a series of asynchronous actions. The leverage check is implemented as an invariant:

leverage() <= BASIS_POINTS_DIVISOR();

The error lies in the interaction between PerpetualVault and GmxProxy during position creation. When creating an increase position order through GmxProxy, the size delta calculation in _createIncreasePosition() function uses

uint256 sizeDelta = prices.shortTokenPrice.max * amountIn * leverage / BASIS_POINTS_DIVISOR;

This calculation can result in positions with effective leverage exceeding the intended maximum of 3x (30,000 basis points) due to price impact and fee calculations not being properly accounted for in the leverage validation.

Just like a margin trading account where the broker's risk limits can be circumvented. The protocol intends to limit leverage to 3x, but through this vulnerability, positions can be opened with higher effective leverage. This means:

  1. Users could face unexpected liquidations

  2. The vault's risk parameters are compromised

  3. The core invariant of "consistent leverage per vault" is broken

In the interaction between three key components:

  1. PerpetualVault's leverage parameter (set at initialization)

  2. GmxProxy's order creation logic

  3. VaultReader's position size calculations

The VaultReader contract calculates actual position sizes and leverage, but these calculations happen after the position is already opened through GMX. This creates a gap where the leverage check can be bypassed.

Impact

The error lies in how leverage is managed across the protocol's position lifecycle. Like a car's speed governor that's supposed to prevent exceeding a set speed limit, but fails to account for downhill momentum.

In the PerpetualVault system, users deposit USDC to gain exposure to leveraged positions through GMX perpetuals. Each vault represents a specific market with a fixed leverage - 1x, 2x, or 3x ETH vaults. The protocol promises that "leverage will stay consistent from start to finish," but this promise can be broken.

See how the position creation flows through multiple contracts:

PerpetualVault -> GmxProxy -> GMX Protocol

The leverage calculation in GmxProxy looks deceptively simple

uint256 sizeDelta = prices.shortTokenPrice.max * amountIn * leverage / BASIS_POINTS_DIVISOR;

This means that a 3x ETH vault (leverage = 30000) should never exceed that multiplier. However, the actual position size on GMX can end up larger due to price impact and funding rates not being properly accounted for in the initial calculation.

For traders using these vaults, this creates unexpected risk. A position they believe is 3x leveraged could effectively become 3.5x or higher, increasing liquidation risk. This directly impacts users who chose specific vaults based on their risk tolerance.

Tools Used

Manual

Recommendations

Ensure that leverage never exceeds BASIS_POINTS_DIVISOR under any circumstances, maintaining the core safety invariant of the protocol.

// In PerpetualVault.sol
function _createIncreasePosition(
bool _isLong,
uint256 acceptablePrice,
MarketPrices memory prices
) internal {
uint256 amountIn;
// [VALIDATION LAYER 1]
// Get position info from VaultReader to calculate effective leverage
IVaultReader.PositionData memory positionData = vaultReader.getPositionInfo(
curPositionKey,
prices
);
if (flow == FLOW.DEPOSIT) {
amountIn = depositInfo[counter].amount;
flowData = vaultReader.getPositionSizeInTokens(curPositionKey);
} else {
amountIn = collateralToken.balanceOf(address(this));
}
// [VALIDATION LAYER 2]
// Calculate size delta including fees and price impact
uint256 positionFeeUsd = vaultReader.getPositionFeeUsd(market, sizeDelta, false);
int256 priceImpact = vaultReader.getPriceImpactInCollateral(
curPositionKey,
sizeDelta,
flowData,
prices
);
// [VALIDATION LAYER 3]
// Calculate effective leverage including all factors
uint256 effectiveLeverage = _calculateEffectiveLeverage(
sizeDelta,
amountIn,
positionFeeUsd,
priceImpact
);
require(effectiveLeverage <= leverage, "Exceeds max leverage");
uint256 sizeDelta = prices.shortTokenPrice.max * amountIn * leverage / BASIS_POINTS_DIVISOR;
IGmxProxy.OrderData memory orderData = IGmxProxy.OrderData({
// ... existing parameters ...
});
_gmxLock = true;
gmxProxy.createOrder(orderType, orderData);
}
// New helper function
function _calculateEffectiveLeverage(
uint256 sizeDelta,
uint256 amountIn,
uint256 positionFeeUsd,
int256 priceImpact
) internal pure returns (uint256) {
// Account for fees and price impact in effective leverage calculation
uint256 totalCost = amountIn;
if (priceImpact > 0) {
totalCost += uint256(priceImpact);
}
totalCost += positionFeeUsd;
return (sizeDelta * BASIS_POINTS_DIVISOR) / totalCost;
}

We implement multiple validation layers using VaultReader to get accurate position data and calculates true effective leverage including all GMX-specific factors before allowing position creation.

Updates

Lead Judging Commences

n0kto Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

Suppositions

There is no real proof, concrete root cause, specific impact, or enough details in those submissions. Examples include: "It could happen" without specifying when, "If this impossible case happens," "Unexpected behavior," etc. Make a Proof of Concept (PoC) using external functions and realistic parameters. Do not test only the internal function where you think you found something.

Support

FAQs

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