Liquidation System Development for Perpetual DEX
Flash crash by 50% in a couple of minutes—and if the liquidation module doesn't process positions in time, the protocol incurs bad debt. One popular perpetual exchange faced this last year when volumes grew tenfold and the keeper network couldn't keep up. In such scenarios, a properly designed liquidation system with incentives for liquidators holds up: the liquidator gets a fee share, but only upon fast execution. Poor architecture either misses positions or generates unserviceable debt for LPs. We design solutions that withstand extreme market movements. Contact us for an audit of your protocol.
Our engineers have 5+ years of experience in DeFi development and have implemented over 15 liquidation solutions on Ethereum, Arbitrum, and Polygon. We guarantee correct operation even during sharp price jumps.
How Is the Liquidation Threshold Calculated?
On a perpetual DEX (Wikipedia), a trader opens a position with leverage: 10x long ETH, depositing 1000 USDC collateral, position = 10,000 USDC notional. If ETH drops 9%, the unrealized loss is 900 USDC, collateral decreases to 100 USDC. Margin ratio = 100/10,000 = 1%. If this is below maintenance margin (usually 0.5-1%), the position is subject to forced closure.
Margin ratio formula: marginRatio = (collateral + unrealized_pnl) / notional_value. The protocol must liquidate the position before collateral + unrealized_pnl < 0—otherwise bad debt occurs.
Why Is Gap Risk the Main Threat?
A gap (sharp price jump, e.g., on news) skips several liquidation levels in one tick. A position can immediately go into negative equity without intermediate liquidation. The dYdX v4 documentation states that gap risk is the main cause of bad debt.
GMX v2 and dYdX v4 use several mechanisms to mitigate gap risk:
- Insurance fund—a reserve from part of trading fees
- ADL (Auto-Deleveraging)—if the insurance fund is insufficient, profitable positions of the opposite side are forcibly closed
- Max open interest limits—restricting total OI per asset reduces potential bad debt
Comparison of Approaches: Keeper vs On-Chain Liquidation
| Parameter | Keeper-based | On-chain automatic |
|---|---|---|
| Reaction speed | Depends on gas and competition | Instant per block |
| Complexity | Medium (off-chain infrastructure) | High (gas, complexity) |
| Protocol control | Indirect (via incentives) | Direct |
| MEV risk | High | Low |
| Example | GMX, dYdX | Synthetix (past versions) |
Keeper-based approach reduces gas costs for liquidation by 2-3 times compared to on-chain automatic. This is confirmed in practice on our projects.
Liquidation System Architecture
On-Chain Component
The contract stores positions and constantly updates the mark price via an oracle. Liquidation occurs 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;
// Payment to keeper
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
Keeper—an external participant monitoring positions and calling liquidate(). Incentive—keeper fee, creating a competitive market of liquidators.
Example keeper bot config in TypeScript with viem
class LiquidationKeeper {
private positionCache: Map<bigint, Position> = new Map();
async monitorPositions(): Promise<void> {
contract.on('PositionUpdated', (positionId, position) => {
this.positionCache.set(positionId, position);
});
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 {
return Number(b[1].collateral - a[1].collateral);
}
}
Mark Price Oracle
Key element: the mark price must not be manipulable via flash loans. dYdX v4 uses Pyth oracle with aggregated median from multiple sources. GMX v2 uses Chainlink (official documentation) plus custom keeper oracle with signature verification.
Oracle requirements:
- Freshness check: price not older than N seconds (usually 30-60)
- Deviation check: new price no more than X% different from previous (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;
}
What Happens When the Insurance Fund Runs Out? (ADL)
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 the highest profit and leverage first—as they are most risky for the system.
ADL is a painful mechanism for traders. Important:
- Clearly disclose ADL risk in documentation
- Show an ADL indicator on the UI (as on Binance futures)
- Limit OI to minimize the need for ADL
What's Included in Turnkey System Development
| Stage | Duration | Result |
|---|---|---|
| Risk analysis and modeling | 3-5 days | Parameters for maintenance margin, fee, insurance fund size |
| Smart contract development | 2-4 weeks | Liquidation contracts, insurance fund, oracle adapter |
| Keeper bot integration | 1-2 weeks | Off-chain infrastructure in TypeScript |
| Fork testing | 1 week | Simulation of stress scenarios (flash crash and stablecoin collapse) |
| Audit | 2-3 weeks | Independent auditor's report |
| Deployment and monitoring | 1 week | Documentation, scripts, dashboard |
The full cycle takes 8-12 weeks. Cost is calculated individually.
Tech Stack
Solidity + Foundry—liquidation contracts, oracle, insurance fund. TypeScript + viem—keeper bot, monitoring. Chainlink + Pyth—price feeds. Gelato Network—fallback for calling keeper functions. Foundry fork tests—simulation on mainnet fork.
How We Ensure Reliability
We rely on 5 years of Web3 experience and dozens of successful audits. We implement formal verification for critical contracts. We provide a warranty on the code for 6 months after deployment.
Get a consultation: describe your protocol, and we will assess the risks and propose an architecture.







