The PerpetualVault contract assumes a fixed (30) decimal precision for prices when calculating position sizes, while GMX's OrderHandler uses dynamic price decimals. This mismatch can result in incorrect position sizing, potentially leading to reverts, over-leveraged positions or under-utilized capital.
The vulnerability stems from an architectural assumption in the position size calculation logic. When creating increase position orders, the contract calculates sizeDelta
using:
This calculation assumes prices are always in a fixed decimals (1e30) as enforced by the KeeperProxy contract.
However, GMX's OrderHandler accepts prices with variable decimal precision through its price setting functions:
GMX sets the price for its token using the Oracle::setPrices
, Oracle::setPrimaryPrice
or Oracle::setPricesForAtomicAction
in orderHandler (0xe68CAAACdf6439628DFD2fe624847602991A31eB).
See setPrices()
:
See setPrimaryPrice()
:
The Contract validates these set prices using _validatePrices
.
The key thing to notice in _validatePrices
is no specific decimal precision is enforced for the prices.
On the other hand, the KeeperProxy enforces a price decimal of 30. We know the price is always in 30 decimals because of the validation in KeeperProxy::_check
Note that there's no place where the GMX documentation order code explicitly state or enforce that it's prices will be in 30 decimal precisions
When IncreasePositionUtil::increasePosition --> PositionUtils::getExecutionPriceForIncrease
is called during order execution, the returned values ((cache.priceImpactUsd, cache.priceImpactAmount, cache.sizeDeltaInTokens, cache.executionPrice)
) will not match the expected values used for creating our order request, if GMX price precision is not 30.
One of the following will happen :
If price decimal of KeeperProxy is greater than the price decimal of GMX OrderHandler, our sizeDeltaUsd
will be exaggerated, and our order will either revert due to insufficient collateral or open an over leveraged position exposing the vault to higher risk than intended.
If price decimal of KeeperProxy is less than the price decimal of GMX OrderHandler, our sizeDeltaUsd
will be less than intended, and we will end up with a position with a leverage lower than intended, effectively limiting the vaults upside
Consider the following scenario:
KeeperProxy sends price with 30 decimals: 1 ETH = 2000 * 1e30
GMX OrderHandler has price with 8 decimals: 1 ETH = 2000 * 1e8
User deposits 1 ETH with 2x leverage
PerpetualVault calculates sizeDelta:
When executed by GMX:
Expected: 2000)
Actual: Much larger position due to decimal mismatch
The impact depends on the relative decimal precision between KeeperProxy and GMX.
If KeeperProxy has more decimals than GMX, the position size becomes larger than intended, leading to higher leverage and increased liquidation risk. Additionally, orders may revert due to insufficient collateral.
If KeeperProxy has fewer decimals than GMX, the position size is smaller than expected, resulting in lower leverage, reduced capital efficiency, and lower potential returns.
Manual Review
Dynamically sync price decimals with GMX prices.
Also, consider implementing a price normalization layer that handles decimal conversions between systems.
Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point.
Please read the CodeHawks documentation to know which submissions are valid. If you disagree, provide a coded PoC and explain the real likelihood and the detailed impact on the mainnet without any supposition (if, it could, etc) to prove your point. Keepers are added by the admin, there is no "malicious keeper" and if there is a problem in those keepers, that's out of scope. ReadMe and known issues states: " * System relies heavily on keeper for executing trades * Single keeper point of failure if not properly distributed * Malicious keeper could potentially front-run or delay transactions * Assume that Keeper will always have enough gas to execute transactions. There is a pay execution fee function, but the assumption should be that there's more than enough gas to cover transaction failures, retries, etc * There are two spot swap functionalies: (1) using GMX swap and (2) using Paraswap. We can assume that any swap failure will be retried until success. " " * Heavy dependency on GMX protocol functioning correctly * Owner can update GMX-related addresses * Changes in GMX protocol could impact system operations * We can assume that the GMX keeper won't misbehave, delay, or go offline. " "Issues related to GMX Keepers being DOS'd or losing functionality would be considered invalid."
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.
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.