# Why GMX was upgraded (v2.1 <-> v2.2)
## TL;DR
- GMX validation must bind to economic truth and apply caps only in settlement (not in-between calculations).
- Funding/fees must follow balance improvement and paying side, not price-impact sign or receiving side.
- State passed across steps stays raw; any capped/rounded values are strictly post-validation.
- Governance parameters must not alter user intent checks; they only shape settlement.
> Key Principle: GMX separates economic truth from accounting constraints. Bugs happen when this boundary is crossed.
## Purpose
This handbook documents GMX-specific security invariants and code smells observed in the v2.1 release, derived from:
- historical fixes in v2.2,
- public audit findings,
- and concrete exploit pathways in perp protocols.
It is designed to help future auditors and developers:
- identify where to look,
- harden their systems to any risks exposed in v2.1,
- understand why something is dangerous,
- and test how it can fail.
## Scope
- GMX v2.1 codebase
- Core perp flows: increase, decrease, liquidation, funding, pricing
- Governance & oracle interaction
*excludes v2.2-only systems (GLV, multichain extensions)
## GMX Economic Model
- Trades execute against virtual liquidity, not a real orderbook
- Price impact is a function of skew, not slippage
- Funding redistributes value between sides
- Caps exist for settlement safety, not pricing truth
- Keepers & oracles are part of the trust model
<!-- ## What can an attacker do?
### Bypass acceptablePrice
- Inject capped impact into validation
- Exploit multi-step execution reuse
### Extract funding or rebates
- Flip payer logic
- Abuse sign-based fee selection
- Oscillate skew around zero
### Governance-assisted griefing
- Reduce caps or fee bounds
- Dilute user intent without changing code -->
## Key Invariants
### Invariant 1 — Validation uses raw economics
All user intent validation (acceptablePrice, triggerPrice, liquidation checks) must use raw, uncapped economic values for price impact or execution price. If validation uses capped or post-settlement prices, tight acceptablePrice/triggers can be bypassed even though the user would have rejected the true (raw) fill.
#### Violations:
- capped price impact used in validation
- post-settlement price reused for checks
```solidity
// contracts/position/PositionUtils.sol
cache.executionPrice = BaseOrderUtils.getExecutionPriceForDecrease(
indexTokenPrice,
params.position.sizeInUsd(),
params.position.sizeInTokens(),
sizeDeltaUsd,
cache.priceImpactUsd, // already capped
params.order.acceptablePrice(), // validated on capped path
params.position.isLong()
);
// contracts/order/BaseOrderUtils.sol
function getExecutionPriceForDecrease(
Price.Props memory indexTokenPrice,
uint256 positionSizeInUsd,
uint256 positionSizeInTokens,
uint256 sizeDeltaUsd,
int256 priceImpactUsd, // only capped is available
uint256 acceptablePrice,
bool isLong
) internal pure returns (uint256) {
// acceptablePrice check uses this capped impact → bypass risk
}
```
### Invariant 2 — Funding is paid only by the paying side
Funding must be computed from the size of the paying side, not total or larger open interest.
Funding was derived from the larger/receiving side’s size, so the payer could flip when the imbalance or rounding changed, meaning the wrong side could end up paying (or paying more) simply because the other side was larger, not because the rate sign said they’re the payer.
#### Violations:
- funding derived from larger side
- payer flips based on imbalance rounding
```solidity
// contracts/pricing/MarketUtils.sol
function getNextFundingAmountPerSize(...) {
cache.sizeOfLargerSide = cache.longOpenInterest > cache.shortOpenInterest ? cache.longOpenInterest : cache.shortOpenInterest
// uses size of larger side, which may not be the correct paying side
...
cache.fundingUsd = Precision.applyFactor(cache.sizeOfLargerSide, cache.durationInSeconds * result.fundingFactorPerSecond);
cache.fundingUsd = cache.fundingUsd / divisor;
}
// contracts/pricing/PositionPricingUtils.sol
function getPositionFees() {
fees.funding.latestFundingFeeAmountPerSize = MarketUtils.getFundingFeeAmountPerSize(...);
...
fees.funding = getFundingFees(fees.funding, params.position);
// applies payer-side funding from the larger side only
fees.totalCostAmount = fees.totalCostAmountExcludingFunding + fees.funding.fundingFeeAmount;
}
```
### Invariant 3 — Fee Factors Follow Balance Improvement, Not Price Impact Sign
Fee tier selection must depend on whether a trade improves market balance, not the sign of priceImpactUsd.
#### Violations:
- sign-based fee factor selection
- virtual inventory masking balance improvement
```solidity
::contracts/position/DecreasePositionCollateralUtils.sol::
PositionPricingUtils.GetPositionFeesParams memory getPositionFeesParams = PositionPricingUtils.GetPositionFeesParams(
params.contracts.dataStore,
params.contracts.referralStorage,
params.position,
cache.collateralTokenPrice,
values.priceImpactUsd > 0, // balance-improvement signal drives fee factors
params.market.longToken,
params.market.shortToken,
params.order.sizeDeltaUsd(),
params.order.uiFeeReceiver()
);
PositionPricingUtils.PositionFees memory fees = PositionPricingUtils.getPositionFees(getPositionFeesParams);
```
### Invariant 4 — Governance Cannot Dilute User Guarantees
Governance parameters must not weaken user-specified guarantees retroactively.
#### Violations:
- changing caps affects acceptablePrice
- funding / fee parameters alter validation outcome