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

Lack of Validation on GMX Position Size Can Lead to Partial Position Loss

Summary

The _createIncreasePosition function in PerpetualVault.sol lacks proper position size validation before interacting with GMX market. When creating increase position orders, the contract calculates sizeDelta without validating against market conditions or GMX's open interest limits. If GMX rejects the order due to insufficient liquidity or exceeded limits, the transaction still costs fees while the position fails to execute properly, leading to partial fund loss.es.

Vulnerability Details

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, // @audit No validation
initialCollateralDeltaAmount: 0,
amountIn: amountIn,
callbackGasLimit: callbackGasLimit,
acceptablePrice: acceptablePrice,
minOutputAmount: 0
});
_gmxLock = true;
gmxProxy.createOrder(orderType, orderData);
}

Key issues:

  1. No validation against GMX's openInterest limits

  2. No check against market liquidity

  3. Position size calculated without considering market depth

Impact

  1. Partial order execution with full fees charged

  2. User funds locked in failed position increases

  3. Degraded position management efficiency

  4. Extra gas costs from failed operations

Proof of Concept

The Proof of Concept demonstrates how the lack of position size validation can lead to fund loss. It executes the following steps:

  1. A user deposits the maximum allowed amount (100k USDC)

  2. The system attempts to open a 3x leveraged position without validation

  3. When market conditions are unfavorable (simulated through GMX mock)

  4. The position fails but fees are still charged

  5. The system gets stuck in an unrecoverable state with user funds locked

The test proves that without proper validation, user funds can be partially lost while also leaving the system in a locked state requiring manual intervention.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "forge-std/Test.sol";
import "../../contracts/PerpetualVault.sol";
import "../../contracts/GmxProxy.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract PositionSizeTest is Test {
PerpetualVault vault;
GmxProxy gmxProxy;
IERC20 usdc;
address user1;
function setUp() public {
user1 = address(0x1);
// Setup with real USDC address on Arbitrum
usdc = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8);
// Deploy contracts
vault = new PerpetualVault();
gmxProxy = new GmxProxy();
vault.initialize(
0x70d95587d40A2caf56bd97485aB3Eec10Bee6336, // ETH/USD market
address(this), // keeper
address(0xTreasury),
address(gmxProxy),
address(vaultReader),
1000e6, // minDeposit 1000 USDC
100000e6, // maxDeposit 100k USDC
30000 // 3x leverage
);
// Fund test account
deal(address(usdc), user1, 100000e6);
}
function testLargePositionFailure() public {
// Step 1: User deposits maximum amount
vm.startPrank(user1);
usdc.approve(address(vault), 100000e6);
vault.deposit(100000e6);
vm.stopPrank();
// Step 2: Mock GMX's market state to have low liquidity
bytes memory mockGmxCall = abi.encodeWithSignature(
"createOrder(Order.OrderType,IGmxProxy.OrderData)",
Order.OrderType.MarketIncrease,
_mockLargeOrder()
);
vm.mockCall(
address(gmxProxy),
mockGmxCall,
abi.encode("GMX: insufficient liquidity")
);
// Step 3: Try to open max leverage position
vault.run(
true, // isOpen
true, // isLong
_mockMarketPrices(),
new bytes[](0)
);
// Step 4: Check state after failed increase
assertTrue(vault.isLock()); // System locked
assertEq(vault.flow(), uint256(FLOW.SIGNAL_CHANGE)); // Stuck in flow
// Step 5: Verify user funds are locked
vm.startPrank(user1);
vm.expectRevert("FlowInProgress()");
vault.withdraw(user1, 1);
vm.stopPrank();
}
function _mockMarketPrices() internal pure returns (MarketPrices memory) {
return MarketPrices({
indexTokenPrice: PriceProps({
min: 2000e30,
max: 2000e30
}),
longTokenPrice: PriceProps({
min: 2000e30,
max: 2000e30
}),
shortTokenPrice: PriceProps({
min: 1e30,
max: 1e30
})
});
}
function _mockLargeOrder() internal pure returns (IGmxProxy.OrderData memory) {
return IGmxProxy.OrderData({
market: address(0x70d95587d40A2caf56bd97485aB3Eec10Bee6336),
indexToken: address(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1),
initialCollateralToken: address(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8),
swapPath: new address[](0),
isLong: true,
sizeDeltaUsd: 300000e30, // 300k position (3x leverage on 100k)
initialCollateralDeltaAmount: 0,
amountIn: 100000e6,
callbackGasLimit: 2000000,
acceptablePrice: type(uint256).max,
minOutputAmount: 0
});
}
}

The PoC demonstrates how a large position increase can lead to system failure and locked funds when market conditions in GMX are unfavorable. The key demonstration is that the vault doesn't validate position sizes against market conditions before attempting to create orders.

Tools Used

  • Manual code review

  • Foundry testing framework

  • Static analysis

Recommended Mitigation

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;
// Calculate position size
uint256 sizeDelta = prices.shortTokenPrice.max * amountIn * leverage / BASIS_POINTS_DIVISOR;
// Validate against GMX limits
require(
vaultReader.validatePositionSize(
market,
_isLong,
sizeDelta
),
"Position size exceeds market limits"
);
collateralToken.safeTransfer(address(gmxProxy), amountIn);
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);
}
// Add to IVaultReader
function validatePositionSize(
address _market,
bool _isLong,
uint256 _sizeDelta
) external view returns (bool);

Key changes:

  1. Added position size validation

  2. Check against GMX market limits

  3. Validate before transferring funds

  4. Added error handling for size limits

Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational or Gas

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Admin is trusted / Malicious keepers

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point. Keepers are added by the admin, there is no "malicious keeper" and if there is a problem in those keepers, that's out of scope. ReadMe and known issues states: " * System relies heavily on keeper for executing trades * Single keeper point of failure if not properly distributed * Malicious keeper could potentially front-run or delay transactions * Assume that Keeper will always have enough gas to execute transactions. There is a pay execution fee function, but the assumption should be that there's more than enough gas to cover transaction failures, retries, etc * There are two spot swap functionalies: (1) using GMX swap and (2) using Paraswap. We can assume that any swap failure will be retried until success. " " * Heavy dependency on GMX protocol functioning correctly * Owner can update GMX-related addresses * Changes in GMX protocol could impact system operations * We can assume that the GMX keeper won't misbehave, delay, or go offline. " "Issues related to GMX Keepers being DOS'd or losing functionality would be considered invalid."

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
Assigned finding tags:

Informational or Gas

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.

Admin is trusted / Malicious keepers

Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point. Keepers are added by the admin, there is no "malicious keeper" and if there is a problem in those keepers, that's out of scope. ReadMe and known issues states: " * System relies heavily on keeper for executing trades * Single keeper point of failure if not properly distributed * Malicious keeper could potentially front-run or delay transactions * Assume that Keeper will always have enough gas to execute transactions. There is a pay execution fee function, but the assumption should be that there's more than enough gas to cover transaction failures, retries, etc * There are two spot swap functionalies: (1) using GMX swap and (2) using Paraswap. We can assume that any swap failure will be retried until success. " " * Heavy dependency on GMX protocol functioning correctly * Owner can update GMX-related addresses * Changes in GMX protocol could impact system operations * We can assume that the GMX keeper won't misbehave, delay, or go offline. " "Issues related to GMX Keepers being DOS'd or losing functionality would be considered invalid."

Support

FAQs

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