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

Incorrect Price Scaling in KeeperProxy

Summary

The KeeperProxy contract validates prices by comparing on-chain prices (e.g., from GMX) with Chainlink’s oracle prices. However, the scaling logic for tokens with decimals different from 8 (Chainlink’s standard) is flawed, leading to incorrect comparisons.

https://github.com/CodeHawks-Contests/2025-02-gamma/blob/e5b98627a4c965e203dbb616a5f43ec194e7631a/contracts/KeeperProxy.sol#L188

Vulnerability Details

Faulty Code in KeeperProxy

Here’s the problematic code snippet from KeeperProxy.sol:

function _check(address token, uint256 price) internal view {
// ...
uint256 decimals = 30 - IERC20Meta(token).decimals();
price = price / 10 ** (decimals - 8); // ⚠️ Incorrect scaling
// ...
}

Chainlink’s Price Format: Chainlink returns prices with 8 decimals (e.g., $2000 = 2000_00000000).

Token Decimals Mismatch: Tokens like USDC (6 decimals) or WBTC (8 decimals) need proper scaling to match Chainlink’s 8 decimals.

Incorrect Adjustment: The code attempts to normalize price to 8 decimals but uses the wrong formula.

Example Scenario

Assume:

Token: USDC (6 decimals).

On-chain price: $1.00 = 1_000000 (6 decimals).

Chainlink price: $1.00 = 1_00000000 (8 decimals).

Faulty Calculation:

decimals = 30 - 6 = 24;
price = price / 10^(24 - 8) = 1_000000 / 10^16 = 0.0000000000000001; // Wrong!

The scaled price becomes 0.0000000000000001 instead of 1_00000000.

Impact

  1. False Price Validation: The contract incorrectly approves trades at manipulated prices.

  2. Liquidation Risks: Positions may be liquidated unfairly due to incorrect price checks.

Tools Used

Manual review

Recommendation

To normalize the token’s price to 8 decimals:

  • Multiply by 10^(8 - tokenDecimals) for tokens with <8 decimals.

  • Divide by 10^(tokenDecimals - 8) for tokens with >8 decimals.

function _check(address token, uint256 price, MarketPrices memory prices) internal view {
// Get token decimals (e.g., USDC = 6)
uint256 tokenDecimals = IERC20Meta(token).decimals();
// Normalize price to 8 decimals
if (tokenDecimals < 8) {
price = price * (10 ** (8 - tokenDecimals)); // Scale up for <8 decimals
} else if (tokenDecimals > 8) {
price = price / (10 ** (tokenDecimals - 8)); // Scale down for >8 decimals
}
// Fetch Chainlink price (already 8 decimals)
(, int256 chainlinkPrice, , uint256 updatedAt, ) = AggregatorV2V3Interface(dataFeed[token]).latestRoundData();
// Validate freshness and deviation
require(block.timestamp - updatedAt <= maxTimeWindow[token], "stale price");
require(
_absDiff(price, uint256(chainlinkPrice)) * BASIS_POINTS_DIVISOR / uint256(chainlinkPrice) < priceDiffThreshold[token],
"price deviation too high"
);
}

Correct Scaling Logic:

For USDC (6 decimals): Multiply by 10^(8-6) = 100 to convert 1_000000 → 1_00000000.

For WBTC (8 decimals): No scaling needed.

For tokens with 18 decimals (e.g., WETH): Divide by 10^(18-8) = 10^10.

Handled Edge Cases: Stale price checks via maxTimeWindow.

Price deviation thresholds.

Verification

Test Case 1 (USDC):

On-chain price: 1_000000 (6 decimals).

Scaled price: 1_000000 * 100 = 1_00000000 (8 decimals ✅).

Matches Chainlink’s 1_00000000.

Test Case 2 (WETH):

On-chain price: 2000_000000000000000000 (18 decimals).

Scaled price: 2000_000000000000000000 / 10^10 = 2000_00000000 (8 decimals ✅).

Updates

Lead Judging Commences

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