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

Decimal Precision Mismatch in `getPositionInfo()` Leads to Incorrect Share Calculation and Asset Value Representation

Summary

The getPositionInfo() function in VaultReader.sol incorrectly adds token amounts with different decimal precisions without proper normalization, specifically for claimableLongTokenAmount. This leads to incorrect netValue calculations, which affects share calculations in PerpetualVault.sol and could result in users receiving incorrect share amounts during deposits and withdrawals.

Vulnerability Details

In getPositionInfo(), several token amounts with different decimal precisions are added together to calculate netValue:

uint256 netValue =
positionInfo.position.numbers.collateralAmount * prices.shortTokenPrice.min + // 1e6 * 1e30 = 1e36
positionInfo.fees.funding.claimableLongTokenAmount * prices.longTokenPrice.min + // 1e18 * 1e30 = 1e48
positionInfo.fees.funding.claimableShortTokenAmount * prices.shortTokenPrice.min - // 1e6 * 1e30 = 1e36
positionInfo.fees.borrowing.borrowingFeeUsd - // 1e36
positionInfo.fees.funding.fundingFeeAmount * prices.shortTokenPrice.min - // 1e6 * 1e30 = 1e36
positionInfo.fees.positionFeeAmount * prices.shortTokenPrice.min; // 1e6 * 1e30 = 1e36

Most terms in this calculation result in 1e36 precision, except for the claimableLongTokenAmount term which results in 1e48 precision (1e18 * 1e30) when the long token has 18 decimals (e.g. WETH) . This inconsistency means we're adding numbers with different decimal places without proper normalization.

Impact Details

At its core, it affects the share calculation in PerpetualVault's _mint() function through the _totalAmount() dependency on getPositionInfo(). This causes the vault to appear to have more value than it actually does due to the inflated precision. Most critically, this leads to incorrect share distributions during deposits, as the share calculation relies on an accurate representation of the total vault value.

Tools Used

Manual Review

Recommendations

Normalize the claimableLongTokenAmount term based on the decimal difference between the long token and short token:

uint256 netValue =
positionInfo.position.numbers.collateralAmount * prices.shortTokenPrice.min + // 1e6 * 1e30 = 1e36
(positionInfo.fees.funding.claimableLongTokenAmount * prices.longTokenPrice.min) /
(10 ** (IERC20Metadata(longToken).decimals() - IERC20Metadata(shortToken).decimals())) + // normalized to match short token decimals
positionInfo.fees.funding.claimableShortTokenAmount * prices.shortTokenPrice.min -
positionInfo.fees.borrowing.borrowingFeeUsd -
positionInfo.fees.funding.fundingFeeAmount * prices.shortTokenPrice.min -
positionInfo.fees.positionFeeAmount * prices.shortTokenPrice.min;
Updates

Lead Judging Commences

n0kto Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement
Assigned finding tags:

invalid_prices_decimals

GMX github documentation: “Prices stored within the Oracle contract represent the price of one unit of the token using a value with 30 decimals of precision. Representing the prices in this way allows for conversions between token amounts and fiat values to be simplified, e.g. to calculate the fiat value of a given number of tokens the calculation would just be: token amount * oracle price, to calculate the token amount for a fiat value it would be: fiat value / oracle price.” Sponsor confirmed the keeper does the same, so price decimals change in function of the token, to be sure the above rule is true. Example for USDC (6 decimals): Prices will have 24 decimals → 1e6 * 1e24 = 1e30. Just a reminder for some submissions: shortToken == collateralTokens, so the decimals is 1e24 for shortToken prices.

Support

FAQs

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