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

Callback Can Revert on Large `priceImpact` Causing a Vault Freeze

Summary:

FIX -> By allowing raw uint256 subtractions on large priceImpact, the vault’s GMX callback can revert—contradicting the requirement that “it must never revert.” This can freeze the vault in a partial deposit flow, causing a denial of service and locking user funds. A robust fix involves safe checks or try/catch logic so that the callback can gracefully handle edge cases without bricking the protocol.

RootCause & Where It Happens

In afterOrderExecution(), when an increase position on GMX finishes, the code calculates how much a depositor’s deposit should “increase” by factoring in the position fee and price impact:

if (orderResultData.orderType == Order.OrderType.MarketIncrease) {
...
int256 priceImpact = vaultReader.getPriceImpactInCollateral(
curPositionKey,
orderResultData.sizeDeltaUsd,
prevSizeInTokens,
prices
);
// This code tries to do: increased = amount - feeAmount +/- priceImpact - 1
if (priceImpact > 0) {
increased = amount - feeAmount - uint256(priceImpact) - 1;
} else {
increased = amount - feeAmount + uint256(-priceImpact) - 1;
}
_mint(counter, increased, false, prices);
nextAction.selector = NextActionSelector.FINALIZE;
...
}

The problem:

  1. If priceImpact is large (for example, if the deposit is small but the price impact is big),

  2. Then amount - feeAmount - uint256(priceImpact) - 1 can underflow (become negative) in Solidity’s uint256 math.

Solidity will revert on an underflow. Because this is inside afterOrderExecution(), which GMX calls externally, that entire callback reverts. GMX will keep trying to finalize this order or expect the callback to succeed; meanwhile, your protocol gets stuck in a partial deposit flow. No user or keeper can fix it on-chain, because each time GMX triggers the callback, it fails again.

Why This Freezes the Vault

  • The vault is locked in a deposit flow. GMX is “waiting” for a successful callback to finalize the deposit.

  • Each repeated attempt to finalize calls afterOrderExecution() again, which triggers the same arithmetic, which triggers the same underflow → revert.

  • There is no code path (like a try/catch) to absorb or handle big price impacts gracefully. The doc says “This callback function must never revert,” but it can and will if the arithmetic fails.

  • The vault is effectively bricked for that position, because the deposit flow never completes, and the contract’s state machines (flow) do not proceed.

Severity

Potentially high severity. It creates a Denial of Service scenario for the entire deposit flow—and possibly the entire vault if that flow cannot be canceled or completed. Funds remain locked until the vault owner or some external fix (like an upgrade) intervenes.

Attack/Exploit Scenario

  • Even if no one is malicious, a deposit with small amount and high real-time price impact can accidentally cause this revert.

  • A malicious user or a volatile price environment could deliberately trigger large price impacts to freeze the vault’s deposit flow. Once stuck, normal users cannot proceed with deposits/withdrawals.

Proof by Example

  1. User initiates a deposit with a small amount.

  2. The market is extremely volatile, or the GMX price feed calculates an unusually high priceImpact.

  3. priceImpact > (amount - feeAmount - 1) → leads to an underflow in the callback.

  4. Callback reverts → GMX sees a revert → keeps retrying or flags the order as unfulfilled → your vault remains stuck in FLOW.DEPOSIT with _gmxLock = true until forcibly canceled (which may or may not be possible depending on the scenario).

Foundry PoC


// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import "../src/PerpetualVault.sol";
import "../src/libraries/StructData.sol";
/**
* @dev A minimal harness for PerpetualVault that exposes a function to simulate the
* MarketIncrease callback branch where the price impact is used in arithmetic.
*/
contract PriceImpactUnderflowHarness is PerpetualVault {
// Expose a function to simulate the GMX callback for a MarketIncrease order.
// This function sets up deposit state and then computes "increased" as done in afterOrderExecution.
function simulateMarketIncreaseCallback(
uint256 depositAmount,
uint256 feeAmount,
int256 priceImpact,
uint256 prevSizeInTokens
) external {
// Set deposit ID to 1 and simulate a deposit.
counter = 1;
depositInfo[1] = DepositInfo({
amount: depositAmount,
shares: 0,
owner: msg.sender,
executionFee: 0,
timestamp: block.timestamp,
recipient: msg.sender
});
// Set totalShares arbitrarily (won't affect underflow in our arithmetic).
totalShares = depositAmount; // For simplicity, assume totalShares equals depositAmount.
// Set the vault in a deposit flow and simulate an active (open) position.
flow = FLOW.DEPOSIT;
flowData = prevSizeInTokens; // previous size in tokens (from GMX) for calculation.
curPositionKey = keccak256("fakePosition");
// --- This is the vulnerable arithmetic ---
uint256 increased;
if (priceImpact > 0) {
// Vulnerable branch: if priceImpact > (depositAmount - feeAmount - 1) it underflows.
increased = depositAmount - feeAmount - uint256(priceImpact) - 1;
} else {
increased = depositAmount - feeAmount + uint256(-priceImpact) - 1;
}
// Use dummy prices where index, long, and short token prices are all 1.
MarketPrices memory prices = MarketPrices({
indexTokenPrice: PriceRange({min: 1, max: 1}),
longTokenPrice: PriceRange({min: 1, max: 1}),
shortTokenPrice: PriceRange({min: 1, max: 1})
});
// Call _mint() with the computed "increased" value.
// If increased underflows, this call will revert.
_mint(counter, increased, false, prices);
}
}
/**
* @title PriceImpactUnderflowTest
* @dev This Foundry test demonstrates that if the priceImpact is too high, the vault arithmetic underflows.
* For example, if depositAmount = 100, feeAmount = 10, and priceImpact = 200,
* then increased = 100 - 10 - 200 - 1 = -111, which underflows in Solidity's uint256 arithmetic.
*/
contract PriceImpactUnderflowTest is Test {
PriceImpactUnderflowHarness harness;
address tester = address(0xABCD);
function setUp() public {
harness = new PriceImpactUnderflowHarness();
// Set the vault state minimally.
harness.setVaultState(
PerpetualVault.FLOW.NONE,
0,
true, // beenLong = true (implying 1x long)
bytes32("initial"),
false, // positionIsClosed = false (position is open)
false, // _gmxLock = false
PerpetualVault.Action({
selector: PerpetualVault.NextActionSelector.NO_ACTION,
data: hex""
})
);
}
function testPriceImpactUnderflow() public {
vm.prank(tester);
// Setup: depositAmount = 100, feeAmount = 10, priceImpact = 200, prevSizeInTokens = 0.
// Correct calculation: 100 - 10 - 200 - 1 = -111, which will underflow.
vm.expectRevert(); // We expect the underflow to cause a revert.
harness.simulateMarketIncreaseCallback(100, 10, 200, 0);
}
}

Recommended Remediation

  1. Never do raw sub/add with untrusted priceImpact in the callback without safe checks:

    • Use checked arithmetic or require statements like:

      if (priceImpact > 0) {
      require(uint256(priceImpact) <= amount - feeAmount - 1, "Excessive priceImpact");
      increased = amount - feeAmount - uint256(priceImpact) - 1;
      } else {
      ...
      }

      If the condition fails, handle it gracefully rather than letting the entire function revert. For example, treat it as if the deposit is fully consumed by fees/impact (i.e. increased = 0) or abort the deposit flow with a separate state update that doesn’t revert the callback.

  2. Consider a try-catch around any calls or math that might revert. The contract states that _afterOrderExecution must never revert. So either:

    • Implement logic that sets increased = 0 if it’s going negative, or

    • Mark the deposit as “failed” but do not revert.

Updates

Lead Judging Commences

n0kto Lead Judge 3 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.

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.