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
Faulty Code in KeeperProxy
Here’s the problematic code snippet from KeeperProxy.sol:
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:
The scaled price becomes 0.0000000000000001 instead of 1_00000000.
False Price Validation: The contract incorrectly approves trades at manipulated prices.
Liquidation Risks: Positions may be liquidated unfairly due to incorrect price checks.
Manual review
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.
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 ✅).
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.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.