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

PerpetualVault’s Collateral Check Misaligns Decimals Allowing Undercollateralized Positions

Brief

The PerpetualVault’s collateral sufficiency check in MarketUtils.sol improperly multiplies raw token amounts (with standard ERC20 decimals) by token prices expressed in 1e30 precision. Because there is no step to align the collateral token’s decimal scale with the 30-decimal price scale, positions can appear better collateralized than they actually are, potentially allowing undercollateralized positions to remain open.

Details

The issue comes from the willPositionCollateralBeSufficient function in MarketUtils.sol:

int256 remainingCollateralUsd =
values.positionCollateralAmount.toInt256() * collateralTokenPrice.min.toInt256();

Here, positionCollateralAmount uses the token’s native decimals (e.g., 6 for USDC, 18 for ETH) but is multiplied by collateralTokenPrice.min (a 1e30-based price). Without normalizing decimals, the result is inflated or inaccurately scaled. Such an error directly feeds the vault’s collateral checks, letting positions report higher collateral values than what truly exists.

Specific Impact

Because the system misjudges a position’s real collateral, users can open and maintain undercollateralized positions. This creates a considerable risk of unexpected liquidations, cascading insolvency scenarios, and systemic losses if badly collateralized positions are forcibly unwound.

Example Calculation:

Imagine you have 1 USDC as collateral, which is recorded as 1,000,000 units because USDC uses 6 decimals. The price of 1 USDC is 1 USD, and in the code, this price is represented as 1 followed by 30 zeros, or 1e30. The correct way to find the USD value would be to take the 1,000,000 units, divide it by 1,000,000 to adjust for the 6 decimals, and then multiply by 1, which gives you 1 USD. In the system’s 1e30 precision, this should be written as 1 followed by 30 zeros, or 1e30. However, the code doesn’t adjust for decimals, it just multiplies 1,000,000 by 1e30 directly.

This results in 1e6 times 1e30, which equals 1e36, a number with 36 zeros. The code’s result, 1e36, is 1,000,000 times larger than the correct value of 1e30. So, for 1 USDC, the collateral appears to be worth 1 million times more than it really is.

Updates

Lead Judging Commences

n0kto Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Appeal created

itsgreg Submitter
8 months ago
n0kto Lead Judge
8 months ago
n0kto Lead Judge 8 months ago
Submission Judgement Published
Invalidated
Reason: Design choice
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.