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

Token Decimals Assumption in `_check` Might Break for Non-Standard Decimals

Where It Happens

Inside _check(...):

function _check(address token, uint256 price) internal view {
// Get chainlink price
(, int chainLinkPrice, , uint256 updatedAt, ) = AggregatorV2V3Interface(dataFeed[token]).latestRoundData();
require(updatedAt > block.timestamp - maxTimeWindow[token], "stale price feed");
// Retrieve token decimals
uint256 decimals = 30 - IERC20Meta(token).decimals();
// Then shift the `price` accordingly
price = price / 10 ** (decimals - 8); // chainlink price decimals is always 8
require(
_absDiff(price, chainLinkPrice.toUint256()) * BPS / chainLinkPrice.toUint256() < priceDiffThreshold[token],
"price offset too big"
);
}

Potential pitfalls:

  1. Hard-coded Logic for 30 - token.decimals()

    • The contract expects that the GMX system, or the tokens it deals with, always have decimals in the range [0..30]. If a token uses an unexpected decimals value (e.g., 31 or a negative number for extremely exotic reasons), 30 - IERC20Meta(token).decimals() can underflow or produce a nonsensical shift.

  2. Not All Tokens Implement IERC20Meta(token).decimals()

    • ERC20’s original standard does not require an on-chain decimals() method. Some tokens or older tokens might not implement it, causing that call to revert or return 0 incorrectly. In that scenario, _check can revert.

  3. (decimals - 8) Underflow

    • If token.decimals() is < 8, (decimals - 8) is negative, so 10 ** (decimals - 8) reverts or produces the wrong shift. For instance, if token.decimals() == 6, then (decimals - 8) = (30 - 6) - 8 = 16, which works. But if token.decimals() == 10, then (30 - 10) - 8 = 12, which might be correct. However, the code’s comment says “Chainlink price decimals is always 8,” but the formula might be inconsistent if token.decimals() < 8.

Impact

  • Reverts / Inaccurate Price Validation

    • If a token has fewer than 8 decimals (say 6 decimals) or more than 18 decimals, the shift logic can become inaccurate, leading to overzealous reverts (“price offset too big”) or allowing too large a price difference.

  • Possible Underflow

    • If IERC20Meta(token).decimals() is > 30 or less than 8, the expression 10 ** (decimals - 8) can underflow or yield zero.

Why the attention to this issue

  1. Non-Standard Decimals: Many tokens follow 18 decimals, but stablecoins can have 6 decimals, bridging tokens might have 8 decimals, etc. If IERC20Meta(token).decimals() is not in the expected range, _check() can revert or do inaccurate scaling.

  2. Not All Tokens Implement decimals(): The contract forcibly calls IERC20Meta(token).decimals(), which might revert for older tokens or custom tokens that do not implement an interface for decimals.

Recommended Mitigations

  1. Explicit Range Check

    • Validate that IERC20Meta(token).decimals() is in a safe range, for example [6..18], or whatever range you actually support. Revert if it’s outside your design’s scope.

  2. Adjust Logic for Low-Decimals Tokens

    • If a token has fewer than 8 decimals, (decimals - 8) is negative, so you might do something like:

      uint256 tokenDecimals = IERC20Meta(token).decimals();
      require(tokenDecimals <= 30, "Unsupported token decimals");
      uint256 shift = 0;
      // If tokenDecimals < 8, skip or set shift = 0
      // If tokenDecimals > 8, shift = ...
    • Provide a clear branch for stablecoins with 6 decimals, for instance.

  3. Document That All Tokens Must Implement decimals()

    • If you only support tokens that implement IERC20Meta(token).decimals(), ensure the system docs and deployment notes reflect that. Revert if the call fails.

  4. Graceful Handling

    • Alternatively, maintain an explicit mapping of tokens → decimals to avoid relying on the token contract’s decimals() function. Then you can store known decimals for each supported token.

Updates

Lead Judging Commences

n0kto Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Out of scope
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.

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.