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

KeeperProxy.sol Incorrect Price Scaling in the _check Function

KeeperProxy.sol

Summary:
The _check function is meant to compare an internal market price (provided in the MarketPrices struct) against the Chainlink price feed value for a given token. Internal prices are assumed to use 30 decimals (as evidenced by ONE_USD in PerpetualVault), while Chainlink feeds use 8 decimals. To compare these values, the contract scales the provided price as follows:

uint256 decimals = 30 - IERC20Meta(token).decimals();
price = price / 10 ** (decimals - 8); // Chainlink price decimals is always 8.

Issue Path:
For a typical ERC20 token (say with 18 decimals) the calculation proceeds as follows:

  • With token decimals = 18, the computed variable is
    decimals = 30 - 18 = 12.

  • Then the scaling becomes
    price = price / 10 ** (12 - 8) = price / 10 ** 4.

If the internal price is meant to be 30 decimals (e.g. a price of $1 is represented as 1e30), converting it to an 8‑decimal value should require dividing by 10^(30–8) = 10^22. Instead, dividing by 10^4 leaves the scaled price roughly 10^(22–4) = 10^18 times too high.

Impact:
This mis‑scaling means that when _check compares the (incorrectly scaled) internal price against the Chainlink price (which is roughly on the order of 1e8 for a $1 asset), the computed absolute difference will be off by many orders of magnitude. The result is that even valid keeper calls will revert with "price offset too big" because the relative difference (multiplied by BPS and divided by the Chainlink price) far exceeds the configured threshold.

Recommendation:
Revise the scaling calculation. Because internal prices are standardized to 30 decimals regardless of the token’s native decimals, the conversion to 8 decimals should be independent of IERC20Meta(token).decimals(). For example, a correct conversion might be:

// Convert a 30-decimal price to an 8-decimal price:
price = price / 10 ** (30 - 8); // = price / 10**22

This change ensures that a price of 1e30 (representing $1) becomes 1e8, matching the Chainlink feed. If token-specific adjustments are needed, they must be introduced with a clear rationale and tested against the protocol’s invariant that all internal price data uses a uniform 30-decimal format.

Updates

Lead Judging Commences

n0kto Lead Judge 9 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.

Give us feedback!