Summary
The calculation in PerpetualVault._createIncreasePosition
uses the maximum short token price, inflating the computed USD size of a position and risking undercollateralization.
Vulnerability Details
In the _createIncreasePosition function,
* @notice Creates an increase position request.
* @dev We register position information with GMX peripheral contracts to open perp positions.
* This function doesn't open the position actually; it just registers the request of creating position.
* The actual opening/closing is done by a keeper of the GMX vault.
* @param _isLong If true, the position is long; if false, the position is short.
*/
function _createIncreasePosition(
bool _isLong,
uint256 acceptablePrice,
MarketPrices memory prices
) internal {
uint256 amountIn;
if (flow == FLOW.DEPOSIT) {
amountIn = depositInfo[counter].amount;
flowData = vaultReader.getPositionSizeInTokens(curPositionKey);
} else {
amountIn = collateralToken.balanceOf(address(this));
}
Order.OrderType orderType = Order.OrderType.MarketIncrease;
collateralToken.safeTransfer(address(gmxProxy), amountIn);
uint256 sizeDelta = prices.shortTokenPrice.max * amountIn * leverage / BASIS_POINTS_DIVISOR;
IGmxProxy.OrderData memory orderData = IGmxProxy.OrderData({
market: market,
indexToken: indexToken,
initialCollateralToken: address(collateralToken),
swapPath: new address[](0),
isLong: _isLong,
sizeDeltaUsd: sizeDelta,
initialCollateralDeltaAmount: 0,
amountIn: amountIn,
callbackGasLimit: callbackGasLimit,
acceptablePrice: acceptablePrice,
minOutputAmount: 0
});
_gmxLock = true;
gmxProxy.createOrder(orderType, orderData);
}
The position size is determined by:
uint256 sizeDelta = prices.shortTokenPrice.max * amountIn * leverage / BASIS_POINTS_DIVISOR;
By using the maximum price value rather than a more representative price such as the current market price or an average, the calculation artificially increases the position’s USD size. In practice, if the execution price turns out lower than the max price used for calculation, the resulting position will have a higher effective leverage than intended, thereby reducing the margin of safety.
This means, say:
A user deposits an amount equivalent to 1000 USDC (with proper scaling to 30 decimals).
The token’s short price feed indicates a maximum value that is 10% higher than the actual trading price.
The function calculates the position size using the inflated max price, e.g., sizeDelta = inflatedPrice * amountIn * leverage / divisor
, resulting in a position that appears 10% larger than it should be.
An attacker or a manipulative off-chain script leverages this calculation error by triggering the increase position function when market conditions show a significant discrepancy between the max price and the actual execution price.
Once the position is opened, the actual price being lower means the position is undercollateralized. In adverse market moves, the position is quickly liquidated, causing losses that are disproportionate to the actual deposit.
Impact
An overestimated position size directly increases the liquidation risk for traders, as the actual collateral backing the position is insufficient relative to the overstated exposure. In volatile market conditions, this miscalculation can lead to unexpected margin calls or liquidations.
Tools Used
Manual Review
Recommendations
Change the calculation to use a more representative price metric—such as the minimum price, an average of the min and max, or an execution price provided by the oracle—so that the position size reflects realistic market conditions.
/**
* @notice Creates an increase position request.
* @dev We register position information with GMX peripheral contracts to open perp positions.
* This function doesn't open the position actually; it just registers the request of creating position.
* The actual opening/closing is done by a keeper of the GMX vault.
* @param _isLong If true, the position is long; if false, the position is short.
*/
function _createIncreasePosition(
bool _isLong,
uint256 acceptablePrice,
MarketPrices memory prices
) internal {
// Check available amounts to open positions
uint256 amountIn;
if (flow == FLOW.DEPOSIT) {
amountIn = depositInfo[counter].amount;
flowData = vaultReader.getPositionSizeInTokens(curPositionKey);
} else {
amountIn = collateralToken.balanceOf(address(this));
}
Order.OrderType orderType = Order.OrderType.MarketIncrease;
collateralToken.safeTransfer(address(gmxProxy), amountIn);
- uint256 sizeDelta = prices.shortTokenPrice.max * amountIn * leverage / BASIS_POINTS_DIVISOR;
+ uint256 executionPrice = (prices.shortTokenPrice.min + prices.shortTokenPrice.max) / 2;
IGmxProxy.OrderData memory orderData = IGmxProxy.OrderData({
market: market,
indexToken: indexToken,
initialCollateralToken: address(collateralToken),
swapPath: new address[](0),
isLong: _isLong,
sizeDeltaUsd: sizeDelta,
initialCollateralDeltaAmount: 0,
amountIn: amountIn,
callbackGasLimit: callbackGasLimit,
acceptablePrice: acceptablePrice,
minOutputAmount: 0
});
_gmxLock = true;
gmxProxy.createOrder(orderType, orderData);
}