The KeeperProxy contract incorrectly scales protocol prices when comparing against Chainlink feeds. This mismatch in decimal representation can lead to invalid price checks that either block legitimate transactions or allow manipulated prices to pass through.
The KeeperProxy is designed to validate prices between the protocol’s internal 30-decimal format and Chainlink’s 8-decimal format. However, it incorrectly scales protocol prices by 10^(30 - tokenDecimals - 8)
instead of scaling Chainlink’s price to 30 decimals. This creates a mismatch in price magnitudes, for USDC token, where a $1.00 price is represented as 1e30
in the protocol but scales to 1e14
during validation.
Meanwhile, Chainlink reports the same price as 1e8
(8 decimals). The resulting comparison falsely flags a 99,999,900% deviation, allowing invalid trades or blocking legitimate ones.
Token: USDC (6 decimals).
Protocol Price: $1.00 (1e30 in 30-decimal terms).
Chainlink Price: $1.00 (1e8 in 8-decimal terms).
Threshold: 1% (100 basis points).
In the _check() function below, the internal price is scaled using the following logic:-
Meaning, according to the scaling logic used in the code, for a token with 6 decimals (e.g., USDC):
decimals = 30 - 6 = 24
price = price / 10^(24 - 8) = price / 10^16
If price = 1e30
(protocol’s $1.00):
--> price = 1e30 / 1e16 = 1e14
(scaled to 14 decimals).
Comparison with Chainlink:
Chainlink’s price = 1e8
(8 decimals), which means, the code compares 1e14
(scaled protocol price) to 1e8
(Chainlink price).
Deviation calculation: |1e14 - 1e8| / 1e8 * 10,000 = 99,990,000%
This exceeds any reasonable threshold (e.g., 1% = 100 BPS), causing false failures. Therefore, a keeper opens a position using a stale or manipulated price, bypassing safety checks. Positions are liquidated unfairly or trades execute at incorrect prices, draining user funds.
In hindsight, the protocol scales down its 30-decimal
price to a lower decimal format, while Chainlink’s price remains at 8 decimals
. This creates an apples-to-oranges comparison. However, both prices should have been normalized to the same decimal precision (e.g., 30 decimals).
Intended Behavior --> Both prices should be normalized to the same decimal precision (e.g., 30 decimals).
Actual Behavior -->
Protocol price: 1e30 → 1e14
(incorrectly scaled down).
Chainlink price: 1e8
(unchanged).
A valid price (both $1.00) appears as a 99,990,000% deviation, causing the check to fail.
To compare prices accurately, scale Chainlink’s price to 30 decimals instead of scaling the protocol’s price down:
After this changes :-
Chainlink price = 1e8 (8 decimals).
Scaling factor = 10^(30 - 8 - 6) = 10^16.
chainlinkScaled = 1e8 * 1e16 = 1e24 (30 decimals).
Protocol price = 1e30 (30 decimals).
Deviation: |1e30 - 1e24| / 1e24 * 10,000 = 0.99% (99 BPS) → Valid for a 1% threshold.
Price validation is critical for keeper-triggered operations (like opening or closing positions). If the scaling is off, orders might be executed at highly inaccurate prices or legitimate orders could be blocked. This flaw could allow an attacker to force a keeper to execute orders at manipulated prices or, conversely, cause the protocol to revert valid transactions under normal market conditions—both scenarios posing systemic risk.
Manual Review
Properly scale Chainlink’s 8‑decimal price up to the 30‑decimal standard.
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.