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

Arithmetic Underflow in `getPositionInfo` for Highly Negative PnL

RootCause & Where It Happens

In the getPositionInfo function:

uint256 netValue =
positionInfo.position.numbers.collateralAmount * prices.shortTokenPrice.min +
positionInfo.fees.funding.claimableLongTokenAmount * prices.longTokenPrice.min +
positionInfo.fees.funding.claimableShortTokenAmount * prices.shortTokenPrice.min -
positionInfo.fees.borrowing.borrowingFeeUsd -
positionInfo.fees.funding.fundingFeeAmount * prices.shortTokenPrice.min -
positionInfo.fees.positionFeeAmount * prices.shortTokenPrice.min;
if (positionInfo.basePnlUsd >= 0) {
netValue = netValue + uint256(positionInfo.basePnlUsd);
} else {
// Potential underflow if 'uint256(-positionInfo.basePnlUsd) > netValue'
netValue = netValue - uint256(-positionInfo.basePnlUsd);
}

Here, positionInfo.basePnlUsd can be a large negative integer, meaning the final line performs:

netValue = netValue - uint256(-basePnlUsd);

If -basePnlUsd exceeds netValue, this subtraction underflows in Solidity 0.8+, causing a revert. Consequently, the entire read operation fails—no result is returned. It’s impossible to “view” that position via getPositionInfo() in situations where the PnL is deeply negative.

Impact

  1. Denial of Service: Any on-chain calls (or off-chain scripts that rely on a transaction simulation) to getPositionInfo() will fail, preventing critical vantage into severely underwater positions.

  2. Breaking Monitoring Tools: DApps or integrators that read positions for display or risk checks can unexpectedly revert, losing insight just when the position is at its most negative.

  3. No Graceful Handling of Negative netValue: The function never returns a negative netValue. Instead, it reverts, providing no opportunity for the vault or other smart contracts to handle deeply negative positions in a structured manner.

Root Cause

The function tries to store everything in a uint256 despite the possibility of a net negative outcome. In typical DeFi position accounting, it is entirely possible for PnL to be more negative than all positive offsets (collateral, claimable funding, etc.), so the final net should be negative. However, the code attempts to do:

netValue = netValue - <large number>;

…which reverts if <large number> is bigger than netValue.

Foundry PoC

PoC test that demonstrates the vulnerability in which a highly negative PnL (expressed via a negative basePnlUsd) causes an arithmetic underflow in the _totalAmount() computation (via getPositionInfo()). In our test, we simulate a GMX position with 100 units of collateral and a basePnlUsd of –200. When the vault code attempts to subtract 200 from 100 (after scaling), it underflows and reverts. This PoC clearly shows that getPositionInfo() will revert on deeply negative PnL.


// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
import "forge-std/Test.sol";
import "../src/VaultReader.sol";
import "../src/interfaces/IVaultReader.sol";
import "../src/libraries/StructData.sol";
// Minimal definitions to match StructData (simplified)
struct PriceRange {
uint256 min;
uint256 max;
}
struct MarketPrices {
PriceRange indexTokenPrice;
PriceRange longTokenPrice;
PriceRange shortTokenPrice;
}
struct MarketProps {
address indexToken;
address longToken;
address shortToken;
}
struct PositionNumbers {
uint256 collateralAmount;
uint256 sizeInUsd;
uint256 sizeInTokens;
}
struct PositionFlags {
bool isLong;
}
struct PositionInfoStruct {
PositionNumbers numbers;
PositionFlags flags;
}
struct FeeInfo {
uint256 claimableLongTokenAmount;
uint256 claimableShortTokenAmount;
uint256 borrowingFeeUsd;
uint256 fundingFeeAmount;
uint256 positionFeeAmount;
}
struct PositionInfo {
PositionInfoStruct position;
FeeInfo fees;
int256 basePnlUsd;
}
struct PositionData {
uint256 sizeInUsd;
uint256 sizeInTokens;
uint256 collateralAmount;
uint256 netValue;
int256 pnl;
bool isLong;
}
// Minimal interface for IGmxReader used by VaultReader.
interface IGmxReader {
function getPositionInfo(
address dataStore,
address referralStorage,
bytes32 key,
MarketPrices memory prices,
uint256,
address,
bool
) external pure returns (PositionInfo memory);
function getMarket(address dataStore, address market) external pure returns (MarketProps memory);
}
/**
* @dev A fake GmxReader that simulates a deeply negative PnL.
*
* It returns a PositionInfo with:
* - collateralAmount = 100
* - All fee fields = 0
* - basePnlUsd = -200
*
* When VaultReader calculates netValue:
*
* netValue = 100 * prices.shortTokenPrice.min (which is 100, given min = 1)
* Then, since basePnlUsd < 0, it does:
* netValue = 100 - uint256(200) = 100 - 200,
* which underflows in Solidity 0.8+ and reverts.
*/
contract FakeGmxReader is IGmxReader {
function getPositionInfo(
address,
address,
bytes32,
MarketPrices memory prices,
uint256,
address,
bool
) external pure override returns (PositionInfo memory) {
PositionNumbers memory numbers = PositionNumbers({
collateralAmount: 100,
sizeInUsd: 1000,
sizeInTokens: 500
});
PositionFlags memory flags = PositionFlags({ isLong: true });
PositionInfoStruct memory posStruct = PositionInfoStruct({
numbers: numbers,
flags: flags
});
FeeInfo memory fees = FeeInfo({
claimableLongTokenAmount: 0,
claimableShortTokenAmount: 0,
borrowingFeeUsd: 0,
fundingFeeAmount: 0,
positionFeeAmount: 0
});
// Set basePnlUsd to -200.
return PositionInfo({
position: posStruct,
fees: fees,
basePnlUsd: -200
});
}
function getMarket(address, address) external pure override returns (MarketProps memory) {
MarketProps memory mp;
mp.indexToken = address(0);
mp.longToken = address(0);
mp.shortToken = address(0);
return mp;
}
}
/**
* @dev A minimal fake DataStore. For our PoC, its getUint is not used by our vulnerability.
*/
contract FakeDataStore {
function getUint(bytes32) public pure returns (uint256) {
return 0;
}
}
/**
* @dev A test harness that deploys VaultReader using our FakeGmxReader.
*/
contract VaultReaderHarness is VaultReader {
constructor(
address _orderHandler,
address _dataStore,
address _orderVault,
address _gmxReader,
address _referralStorage
) VaultReader(_orderHandler, _dataStore, _orderVault, _gmxReader, _referralStorage) {}
}
/**
* @title NegativePnLUnderflowTest
* @dev This Foundry test demonstrates that if the GMX position has a highly negative PnL,
* the arithmetic in getPositionInfo() underflows and reverts.
*/
contract NegativePnLUnderflowTest is Test {
VaultReaderHarness reader;
FakeGmxReader fakeGmx;
FakeDataStore fakeStore;
// Dummy addresses for orderVault, referralStorage, orderHandler.
address constant dummyOrderHandler = address(0x1000);
address constant dummyOrderVault = address(0x2000);
address constant dummyReferral = address(0x3000);
function setUp() public {
fakeGmx = new FakeGmxReader();
fakeStore = new FakeDataStore();
// Deploy our harness using the fake GmxReader.
reader = new VaultReaderHarness(
dummyOrderHandler,
address(fakeStore),
dummyOrderVault,
address(fakeGmx),
dummyReferral
);
}
function testUnderflowOnNegativePnL() public {
// Use an arbitrary key.
bytes32 key = keccak256("testKey");
// Use dummy market prices with all min values = 1.
MarketPrices memory prices;
prices.indexTokenPrice = PriceRange({ min: 1, max: 1 });
prices.longTokenPrice = PriceRange({ min: 1, max: 1 });
prices.shortTokenPrice = PriceRange({ min: 1, max: 1 });
// Expect a revert due to underflow.
vm.expectRevert(); // Underflow should cause a revert in Solidity 0.8+
reader.getPositionInfo(key, prices);
}
}

Recommended Fixes

  1. Use Signed Integers for netValue

    • If the contract design expects positions that can go below zero, keep track of netValue in an int256 and return negative values gracefully.

    • The calling context can decide how to handle negative net values (e.g. display them or consider the position liquidated).

  2. Graceful Underflow Handling

    • If you must keep uint256, clamp the result at 0 or revert with a clearer reason:

      if (uint256(-basePnlUsd) > netValue) {
      // Option 1: revert("Position is too negative");
      // Option 2: netValue = 0; // if you prefer not to revert, but that can mask real negativity
      }
      netValue -= uint256(-basePnlUsd);
    • This at least prevents silent underflow reverts and clarifies the negative scenario.

  3. Return Negative Values at the High Level

    • Instead of a single uint256 netValue, the contract can return both a “base collateral” and a pnl in separate fields. If the net is negative, the caller can see that distinctly.

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.