Perpetual DEX Liquidation System Development
In November 2022, after the FTX collapse, GMX v1 underwent a stress test: prices moved sharply, trading volumes jumped 10x, and the liquidation system held the load. This is because GMX uses keeper-based liquidations with properly aligned incentives: the liquidator gets part of the liquidation fee, but only if executed quickly. A poorly designed liquidation system in such moments either fails to close positions or creates bad debt that falls on LP providers.
Liquidation mechanics in perpetual DEX
What is a liquidatable position
On a perpetual DEX, a trader opens a leveraged position: 10x long ETH for 1000 USDC collateral means a 10,000 USDC notional position. If ETH falls 9%, unrealized loss = 900 USDC (9% × 10,000), collateral shrinks to 100 USDC. Margin ratio = 100/10,000 = 1%. If this is below maintenance margin (typically 0.5-1%), the position is liquidated.
Margin ratio formula: marginRatio = (collateral + unrealized_pnl) / notional_value
The protocol must liquidate the position before collateral + unrealized_pnl < 0 — otherwise bad debt.
Gap risk: the main problem during high volatility
During a gap (sharp price jump, e.g., from news), mark price jumps through several liquidation levels simultaneously. A position can go straight into negative equity without liquidation opportunity along the way.
How GMX v2 and dYdX v4 solve gap risk:
- Insurance fund — reserve formed from part of trading fees
- ADL (Auto-Deleveraging) — if insurance fund doesn't cover, profitable positions on the opposite side are forcibly closed
- Max open interest limits — limiting aggregate OI per asset reduces potential bad debt
Liquidation system architecture
On-chain component
Contract stores positions and continuously updates mark price via oracle. Liquidation happens in two steps:
1. Liquidatability check (view function):
function isLiquidatable(uint256 positionId) public view returns (bool) {
Position memory pos = positions[positionId];
uint256 markPrice = oracle.getMarkPrice(pos.indexToken);
int256 unrealizedPnl = calculatePnl(pos, markPrice);
int256 equity = int256(pos.collateral) + unrealizedPnl;
// Subtract accumulated funding fee
int256 pendingFunding = calculateFundingFee(pos);
equity -= pendingFunding;
uint256 notional = pos.size; // size = notional value
// Below maintenance margin threshold
return equity < int256(notional * MAINTENANCE_MARGIN_BPS / 10000);
}
2. Liquidation execution:
function liquidate(uint256 positionId, address recipient) external nonReentrant {
require(isLiquidatable(positionId), "Not liquidatable");
Position memory pos = positions[positionId];
uint256 markPrice = oracle.getMarkPrice(pos.indexToken);
// Calculate remaining collateral after losses
int256 remainingCollateral = calculateRemainingCollateral(pos, markPrice);
uint256 liquidationFee = pos.collateral * LIQUIDATION_FEE_BPS / 10000;
// Keeper payment
uint256 keeperFee = liquidationFee * KEEPER_SHARE / 100;
token.transfer(recipient, keeperFee);
// Remainder to insurance fund or protocol
if (remainingCollateral > 0) {
uint256 toInsurance = uint256(remainingCollateral) - keeperFee;
insuranceFund.deposit(toInsurance);
} else {
// Bad debt — cover from insurance fund
insuranceFund.cover(uint256(-remainingCollateral));
}
_closePosition(positionId);
emit PositionLiquidated(positionId, msg.sender, keeperFee, block.timestamp);
}
Keeper system
A keeper is an external participant who monitors positions and calls liquidate(). Incentive: keeper fee. This creates a competitive market of liquidators.
To build a keeper network, off-chain infrastructure is needed:
class LiquidationKeeper {
private positionCache: Map<bigint, Position> = new Map();
async monitorPositions(): Promise<void> {
// Subscribe to position update events
contract.on('PositionUpdated', (positionId, position) => {
this.positionCache.set(positionId, position);
});
// Periodic check on each new block
provider.on('block', async (blockNumber) => {
const markPrice = await oracle.getMarkPrice(INDEX_TOKEN);
const liquidatable = [...this.positionCache.entries()]
.filter(([_, pos]) => this.isLiquidatable(pos, markPrice))
.sort((a, b) => this.prioritize(a, b, markPrice)); // Most profitable first
for (const [positionId] of liquidatable) {
await this.attemptLiquidation(positionId);
}
});
}
private prioritize(a: [bigint, Position], b: [bigint, Position], price: bigint): number {
// Priority: larger collateral = higher keeper fee
return Number(b[1].collateral - a[1].collateral);
}
}
Mark price oracle
Key component: mark price must not be manipulated by flash loans. dYdX v4 uses Pyth oracle with aggregated median from multiple sources. GMX v2 uses Chainlink + custom keeper oracle with signature verification.
Oracle requirements:
- Freshness check: price not older than N seconds (typically 30-60)
- Deviation check: new price doesn't differ from previous by more than X% (circuit breaker)
- Multi-source aggregation: median from 3+ sources
function getMarkPrice(address token) external view returns (uint256) {
PriceData memory data = priceData[token];
require(block.timestamp - data.timestamp <= STALENESS_THRESHOLD, "Stale price");
require(data.numSources >= MIN_SOURCES, "Insufficient sources");
return data.medianPrice;
}
ADL mechanism
Auto-Deleveraging — the last line of defense. If the insurance fund is exhausted, the protocol forcibly closes profitable positions at mark price (no slippage). Closing order: positions with highest profit AND highest leverage first (most risky for system).
ADL is painful for traders. Important:
- Clearly disclose ADL risk in documentation
- Show ADL indicator on UI (like on Binance futures)
- Limit OI to minimize need for ADL
Stack
Solidity + Foundry — liquidation contracts, oracle, insurance fund. TypeScript + viem — keeper bot, position monitoring. Chainlink + Pyth — price feeds. Gelato Network — automated keeper function calls (as fallback). Foundry fork tests — stress scenario simulation on mainnet fork.
Working process
Analytics (3-5 days). Risk parameters: maintenance margin, liquidation fee, insurance fund. Stress scenario modeling: what if -50% of main asset in one block.
Development (2-4 weeks). Liquidation contract + keeper bot + oracle integration + insurance fund.
Testing (1 week). Fork tests with historical price shocks (March 2020, LUNA crash, FTX). Invariant: after each liquidation, position margin ratio >= 0.
Audit. For perpetual DEX with real TVL — mandatory.
Timeline estimates
Liquidation system for one asset without ADL — 1-2 weeks. Full system with ADL, insurance fund, multi-oracle aggregation and keeper infrastructure — 4-6 weeks. Cost is calculated individually.







