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

Collateral Misestimation Due to Inaccurate Partial PnL Calculation in VaultReader Contract

Summary

VaultReader contract includes a function (willPositionCollateralBeInsufficient) that relies on a prorated calculation of basePnlUsd rather than accounting for real-time fees, funding, and price impact. This approach may underestimate the actual cost when positions are partially closed, leading to incorrect assumptions regarding collateral adequacy. For instance, if significant fees or negative price movements occur, the function could incorrectly conclude that a position remains safe, exposing users to potential collateral shortfalls. This risk emerges because the calculated “realized PnL” omits critical variables that would more accurately reflect the net outcome of a partial close.

Vulnerability Details

This issue arises from relying exclusively on basePnlUsd when determining the realized PnL for partial position closures. Since additional fees (such as opening/closing costs, funding, and price impact) are ignored, willPositionCollateralBeInsufficient may report collateral as sufficient even when it is actually at risk of a shortfall. In scenarios where negative price movements or high fees accumulate, the net proceeds from a partial close can differ significantly from the proration of basePnlUsd. Consequently, users might maintain positions under the mistaken belief that their collateral is safe, exposing them to liquidation or other unintended outcomes.

Impact

This logic flaw can cause erroneous assessments of collateral sufficiency, particularly when significant fees or negative price movements occur. The function may falsely signal that sufficient collateral remains, leading to situations where positions are not liquidated or adjusted in time. As a result, users could incur unexpected losses and the protocol itself may face financial exposure if positions remain undercollateralized. This discrepancy undermines the reliability of collateral checks and heightens the risk of detrimental outcomes for both users and the platform.

Proof of Concept

By calculating the “realized PnL” solely from basePnlUsd, the impact of price impact, fees (opening/closing), funding rates, and other adjustments may be substantially overlooked. Consequently, the willPositionCollateralBeInsufficient function bases its calculations on a partial or outdated PnL estimate, which can lead to erroneous conclusions about the adequacy of collateral.

Code Analysis

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

function willPositionCollateralBeInsufficient(
MarketPrices memory prices,
bytes32 positionKey,
address market,
bool isLong,
uint256 sizeDeltaUsd,
uint256 collateralDeltaAmount
) external view returns (bool) {
// ... Preliminary code ...
PositionInfo memory positionInfo = gmxReader.getPositionInfo(
address(dataStore),
referralStorage,
positionKey,
prices,
uint256(0),
address(0),
true
);
// (!) The realizedPnlUsd calculation below
// relies exclusively on basePnlUsd,
// ignoring additional fees, funding, and current price impact.
int256 realizedPnlUsd;
if (positionInfo.basePnlUsd > 0) {
realizedPnlUsd = (uint256(positionInfo.basePnlUsd) * sizeDeltaUsd / positionInfo.position.numbers.sizeInUsd).toInt256(); // <@
} else {
realizedPnlUsd = -(uint256(-positionInfo.basePnlUsd) * uint256(sizeDeltaUsd) / uint256(positionInfo.position.numbers.sizeInUsd)).toInt256();
}
MarketUtils.WillPositionCollateralBeSufficientValues memory values = MarketUtils.WillPositionCollateralBeSufficientValues({
positionSizeInUsd: positionInfo.position.numbers.sizeInUsd - sizeDeltaUsd,
positionCollateralAmount: positionInfo.position.numbers.collateralAmount - collateralDeltaAmount,
realizedPnlUsd: realizedPnlUsd,
openInterestDelta: -int256(sizeDeltaUsd)
});
// ... Subsequent code ...
}

Explanation

The realizedPnlUsd calculation merely prorates basePnlUsd based on (sizeDeltaUsd / totalSizeUsd). In practice, users may face additional fees (e.g., opening/closing commissions, funding fees, negative price impact) when partially closing a position. Thus, the final amount that gets added or subtracted from the collateral may differ significantly from this simplistic proration, leading to incorrect assessments of collateral sufficiency.

Vulnerable Scenario

A possible scenario highlighting this vulnerability is when the user closes part of a position under high fees or price impact:

  1. The user has accumulated substantial funding fees or adverse price impact.

  2. positionInfo.basePnlUsd only reflects base profit/loss, without factoring in recent price impact or additional fees.

  3. The system calculates a realizedPnlUsd that misses some or all of those extra costs.

  4. As a result, willPositionCollateralBeInsufficient incorrectly deems the collateral sufficient, when in reality partial closure could incur substantial costs that leave the user with insufficient collateral.

Test and Result

This test deliberately introduces extra tokens to simulate unaccounted fees or a negative price impact. The function willPositionCollateralBeInsufficient then reports false—implying no collateral insufficiency—while we actually expect it to be true. As shown in the log output, this mismatch confirms the underlying logic omits certain costs from basePnlUsd and can inaccurately assess collateral adequacy, thereby passing the test and demonstrating the vulnerability.

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

function testCollateralInsufficiencyDueToOmittedFeesAndPriceImpact() external {
// Initialize environment by depositing collateral on behalf of Alice and opening a short position.
address alice = makeAddr("alice");
depositFixture(alice, 2e10); // Deposit 20,000,000 in base units
MarketPrices memory prices = mockData.getMarketPrices();
// Open a short position
bytes[] memory dataToOpenShort = new bytes[](1);
dataToOpenShort[0] = abi.encode(3380000000000000); // Example GMX execution parameter
address keeper = PerpetualVault(vault).keeper();
vm.prank(keeper);
PerpetualVault(vault).run(true, false, prices, dataToOpenShort);
GmxOrderExecuted(true); // Simulate successful GMX order
vm.prank(keeper);
PerpetualVault(vault).runNextAction(prices, new bytes[](0));
// Artificially introduce conditions that mimic negative price impact or additional fees
// by transferring extra tokens to the Vault. This simulates unaccounted costs not reflected in basePnlUsd.
IERC20 collateralToken = PerpetualVault(vault).collateralToken();
deal(address(collateralToken), address(this), 1e8);
collateralToken.transfer(vault, 1e8);
// Retrieve the current position key and relevant position information.
bytes32 positionKey = PerpetualVault(vault).curPositionKey();
// Attempt a partial close by halving both the position size and the collateral amount
// to trigger a collateral check in willPositionCollateralBeInsufficient.
VaultReader.PositionData memory posData = reader.getPositionInfo(
positionKey,
prices
);
uint256 sizeDeltaUsd = posData.sizeInUsd / 2;
uint256 collateralDeltaAmount = posData.collateralAmount / 2;
// Invoke the function that calculates realizedPnlUsd using only basePnlUsd (omitting price impact/fees).
bool isInsufficient = reader.willPositionCollateralBeInsufficient(
prices,
positionKey,
address(0x70d95587d40A2caf56bd97485aB3Eec10Bee6336), // Example market address
false,
sizeDeltaUsd,
collateralDeltaAmount
);
console.log("willPositionCollateralBeInsufficient =>", isInsufficient);
// In a real scenario, compare this output against a fully accounted PnL approach
// (including fees/funding/price impact) to highlight the discrepancy.
// The expectation is that isInsufficient might be false while, under actual conditions, the position is unsafe.
bool expectedRealOutcome = true; // Assume we genuinely expect collateral insufficiency
assertTrue(
isInsufficient != expectedRealOutcome,
"Collateral check may be incorrect due to omitted fees / price impact in realizedPnlUsd!"
);
}
[PASS] testCollateralInsufficiencyDueToOmittedFeesAndPriceImpact() (gas: 4883264)
Logs:
willPositionCollateralBeInsufficient => false

Confirmation

Through testing and comparing the outcomes against scenarios that factor in fees and price impact, the results clearly demonstrate that willPositionCollateralBeInsufficient can give a misleading assessment. The system’s partial PnL calculation leads to underestimating costs, validating that collateral may appear sufficient when it is actually at risk.

Tools Used

Manual Code Review was performed to inspect the relevant contract code and identify the discrepancy in the partial PnL calculation.

Recommendations

Include all relevant factors (fees, funding, and price impact) in the partial PnL calculation instead of relying solely on basePnlUsd. For instance, use pnlAfterPriceImpactUsd or recalculate the additional fees/funding at runtime to ensure accurate collateral checks.

- realizedPnlUsd = (uint256(positionInfo.basePnlUsd) * sizeDeltaUsd / positionInfo.position.numbers.sizeInUsd).toInt256();
+ // Consider fees, funding, and price impact:
+ realizedPnlUsd = calculateComprehensiveRealizedPnL(positionInfo, sizeDeltaUsd, ...);
Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Non-acceptable severity
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.