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

Inaccurate Price Impact Calculation in VaultReader: Collateral Token Ignored for Long Positions

Summary

The VaultReader contract calculates the price impact in collateral by unconditionally using shortTokenPrice.min, disregarding whether the position is Long or Short and which token is actually collateralized. As a result, positions that rely on a different collateral asset (for example, a stablecoin in a Long position) may experience inaccurate price impact calculations. This discrepancy can lead to misreported costs, imprecise estimates of unrealized profit and loss, and potential misjudgment of liquidation thresholds.

Vulnerability Details

The logic in getPriceImpactInCollateral inherently assumes that collateral must be priced using shortTokenPrice.min, even when the position might be Long and collateralized by a different asset, such as a stablecoin or the indexToken itself. This oversight leads to misalignment between real collateral value and the value utilized during price-impact calculations, creating inaccurate cost estimates and potentially leading to erroneous decisions about fees, profit/loss, or liquidation thresholds. By relying solely on shortTokenPrice.min, the function disregards vital context—namely, whether the position is actually Long or Short—and what the true collateral token is.

Impact

This logic error can produce incorrect price impact calculations whenever the collateral differs from the short token, leading to possible under- or over-charging of fees, miscalculation of slippage, and inaccuracies in profit/loss or liquidation estimates. In extreme cases, users could be prematurely liquidated or retain positions for longer than intended based on misrepresented collateral values.

Proof of Concept

The getPriceImpactInCollateral function always converts “price impact” into collateral using prices.shortTokenPrice.min, completely ignoring whether the position is Long or Short and which token is actually used as collateral.

As a result, if the collateral is not the short token (for example, in Long positions collateralized by a stablecoin or the same indexToken), this calculation may return inaccurate values, potentially leading to decisions or price impact data being misrepresented.

Code Analysis

Below is the relevant portion of the getPriceImpactInCollateral function, highlighting the exact location where the issue occurs:

function getPriceImpactInCollateral(
bytes32 positionKey,
uint256 sizeDeltaInUsd,
uint256 prevSizeInTokens,
MarketPrices memory prices
) external view returns (int256) {
uint256 expectedSizeInTokensDelta = sizeDeltaInUsd / prices.indexTokenPrice.min;
uint256 curSizeInTokens = getPositionSizeInTokens(positionKey);
uint256 realSizeInTokensDelta = curSizeInTokens - prevSizeInTokens;
int256 priceImpactInTokens = expectedSizeInTokensDelta.toInt256() - realSizeInTokensDelta.toInt256();
// (!) The function unconditionally uses `shortTokenPrice.min`
int256 priceImpactInCollateralTokens =
priceImpactInTokens * prices.indexTokenPrice.min.toInt256() / prices.shortTokenPrice.min.toInt256();
return priceImpactInCollateralTokens;
}

Where the Issue Lies:
Within the calculation of priceImpactInCollateralTokens, the function assumes that the “collateral” always corresponds to shortTokenPrice.min, without checking isLong or verifying which token is truly being used as collateral.

Explanation

  1. The function computes priceImpactInTokens based on the indexTokenPrice.

  2. It then converts that “price impact” into “collateral” by dividing by prices.shortTokenPrice.min.

  3. The core problem:

    • It does not take into account whether the position is Long or Short.

    • It does not verify which token is used as collateral.

    If a position is Long and the collateral is a stable token (or any asset other than the short token), this formula becomes inaccurate and may produce an erroneous output.

Vulnerable Scenario

A user opens a Long position using collateral that is not the short token (e.g., a stablecoin like USDC). The protocol then calls getPriceImpactInCollateral to calculate the price impact in collateral terms. Because the calculation leverages prices.shortTokenPrice.min, it returns an incorrect value by ignoring that the user’s collateral might be a stablecoin.

As a result:

  • The protocol may apply incorrect discounts or charges for slippage.

  • Gains/losses on collateral could be under- or over-estimated, affecting the precision of liquidations and fees.

Test and Result

This test sets up a 2x Long position with collateral that is not the short token, then calls getPriceImpactInCollateral. It compares the function’s actual result—which always uses shortTokenPrice.min—against a hypothetical calculation using the correct collateral price. The logs show a massive discrepancy (-723383 vs 1.000029671490240875e33), confirming that the function is ignoring the actual collateral token. The test passes because it detects this mismatch, thereby validating the logical error.

  • Add the following test to test/PerpetualVault.t.sol

function testPriceImpactCalculationWhenCollateralIsNotShortToken() external {
// Prepare the environment by depositing funds into 'vault2x'
address alice = makeAddr("alice");
depositFixtureInto2x(alice, 1e9);
// Open a 2x Long position (isLong = true)
MarketPrices memory prices = mockData.getMarketPrices();
bytes[] memory data = new bytes[](1);
data[0] = abi.encode(3390000000000000); // Parameter for GMX
address keeper = PerpetualVault(vault2x).keeper();
vm.prank(keeper);
PerpetualVault(vault2x).run(true, true, prices, data);
// Simulate a successful GMX order execution
GmxOrderExecuted2x(true);
// Move to the next state
vm.prank(keeper);
PerpetualVault(vault2x).runNextAction(prices, new bytes[](0));
// Confirm that the vault2x is indeed in a Long position
bytes32 positionKey = PerpetualVault(vault2x).curPositionKey();
require(positionKey != bytes32(0), "No position key set");
bool isLong = PerpetualVault(vault2x).beenLong();
require(isLong == true, "Position is not recognized as Long");
// Retrieve position information for further calculations
VaultReader.PositionData memory posData = reader.getPositionInfo(
positionKey,
prices
);
// Simulate partial closure by reducing half of the current size
uint256 halfSizeDeltaUsd = posData.sizeInUsd / 2;
uint256 halfPrevSizeInTokens = posData.sizeInTokens / 2;
// Invoke the function that uses an incorrect logic for collateral price
int256 priceImpact = reader.getPriceImpactInCollateral(
positionKey,
halfSizeDeltaUsd,
halfPrevSizeInTokens,
prices
);
console.logInt(priceImpact);
emit log_named_int(
"Calculated price impact (using shortTokenPrice.min)",
priceImpact
);
// Compare against a hypothetical correct approach where the collateral price would be used appropriately
int256 hypotheticalPriceImpact = (int256(halfSizeDeltaUsd) *
int256(prices.indexTokenPrice.min)) /
int256(prices.longTokenPrice.min);
console.logInt(hypotheticalPriceImpact);
emit log_named_int(
"Hypothetical price impact (if using correct collateral price)",
hypotheticalPriceImpact
);
// The significant mismatch confirms the logical error of always using shortTokenPrice.min
bool mismatch = (priceImpact != hypotheticalPriceImpact);
assertTrue(
mismatch,
"Price impact matches unexpectedly! The function should differ if collateral != shortToken."
);
}
[PASS] testPriceImpactCalculationWhenCollateralIsNotShortToken() (gas: 4594479)
Logs:
-723383
Calculated price impact (using shortTokenPrice.min): -723383
1000029671490240875000000000000000
Hypothetical price impact (if using correct collateral price): 1000029671490240875000000000000000

Confirmation

The absence of any validation related to isLong or checks on the actual collateral token—combined with the fixed use of shortTokenPrice.min—confirms this finding. It applies to any scenario where the position is not a Short with short-token collateral.

Tools Used

Manual Code Review
The code was thoroughly inspected on a line-by-line basis to pinpoint logical inconsistencies and confirm the error through targeted testing.

Recommendations

Replace the unconditional use of shortTokenPrice.min with logic that checks whether the position is Long or Short, and then applies the correct collateral price accordingly.

- int256 priceImpactInCollateralTokens = priceImpactInTokens * prices.indexTokenPrice.min.toInt256() / prices.shortTokenPrice.min.toInt256();
+ if (isLong) {
+ priceImpactInCollateralTokens = priceImpactInTokens * prices.indexTokenPrice.min.toInt256() / prices.longTokenPrice.min.toInt256();
+ } else {
+ priceImpactInCollateralTokens = priceImpactInTokens * prices.indexTokenPrice.min.toInt256() / prices.shortTokenPrice.min.toInt256();
+ }
Updates

Lead Judging Commences

n0kto Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity

Support

FAQs

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