Разработка системы ликвидаций для perpetual DEX
В ноябре 2022 года, после краха FTX, GMX v1 испытал стресс-тест: цены резко двигались, объёмы торгов взлетели в 10x, и система ликвидаций держала нагрузку. Это потому что GMX использует keeper-based ликвидации с правильно выстроенными incentives: ликвидатор получает часть ликвидационного fee, но только если исполняет быстро. Плохо спроектированная система ликвидаций в такие моменты либо не успевает закрывать позиции, либо создаёт bad debt, который ложится на LP провайдеров.
Механика ликвидаций в perpetual DEX
Что такое ликвидируемая позиция
На perpetual DEX трейдер открывает позицию с leverage: 10x long ETH за 1000 USDC коллатераль означает позицию на 10,000 USDC notional. Если ETH падает на 9%, unrealized loss = 900 USDC (9% × 10,000), коллатераль уменьшается до 100 USDC. Margin ratio = 100/10,000 = 1%. Если это ниже maintenance margin (обычно 0.5-1%), позиция ликвидируется.
Формула margin ratio: marginRatio = (collateral + unrealized_pnl) / notional_value
Протокол должен ликвидировать позицию до того, как collateral + unrealized_pnl < 0 — иначе bad debt.
Gap risk: главная проблема при высокой волатильности
При gap (резкий прыжок цены, например при новости) mark price прыгает через несколько уровней ликвидации одновременно. Позиция может уйти сразу в negative equity без возможности ликвидации по пути.
Как GMX v2 и dYdX v4 решают gap risk:
- Insurance fund — резерв, формируемый из части trading fees
- ADL (Auto-Deleveraging) — если insurance fund не покрывает, принудительно закрываются прибыльные позиции противоположной стороны
- Max open interest limits — ограничение совокупного OI по активу снижает потенциальный bad debt
Архитектура системы ликвидаций
On-chain компонент
Контракт хранит позиции и постоянно обновляет mark price через оракул. Ликвидация происходит в два шага:
1. Проверка ликвидируемости (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;
// Вычитаем накопленный funding fee
int256 pendingFunding = calculateFundingFee(pos);
equity -= pendingFunding;
uint256 notional = pos.size; // size = notional value
// Ниже maintenance margin threshold
return equity < int256(notional * MAINTENANCE_MARGIN_BPS / 10000);
}
2. Исполнение ликвидации:
function liquidate(uint256 positionId, address recipient) external nonReentrant {
require(isLiquidatable(positionId), "Not liquidatable");
Position memory pos = positions[positionId];
uint256 markPrice = oracle.getMarkPrice(pos.indexToken);
// Рассчитываем остаток коллатераля после убытков
int256 remainingCollateral = calculateRemainingCollateral(pos, markPrice);
uint256 liquidationFee = pos.collateral * LIQUIDATION_FEE_BPS / 10000;
// Выплата keeper'у
uint256 keeperFee = liquidationFee * KEEPER_SHARE / 100;
token.transfer(recipient, keeperFee);
// Остаток в insurance fund или протокол
if (remainingCollateral > 0) {
uint256 toInsurance = uint256(remainingCollateral) - keeperFee;
insuranceFund.deposit(toInsurance);
} else {
// Bad debt — списываем из insurance fund
insuranceFund.cover(uint256(-remainingCollateral));
}
_closePosition(positionId);
emit PositionLiquidated(positionId, msg.sender, keeperFee, block.timestamp);
}
Keeper система
Keeper — внешний участник, который мониторит позиции и вызывает liquidate(). Incentive: keeper fee. Это создаёт конкурентный рынок ликвидаторов.
Для построения keeper-сети нужна off-chain инфраструктура:
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)); // Самые выгодные первыми
for (const [positionId] of liquidatable) {
await this.attemptLiquidation(positionId);
}
});
}
private prioritize(a: [bigint, Position], b: [bigint, Position], price: bigint): number {
// Приоритет: чем больше коллатераль — тем выше keeper fee
return Number(b[1].collateral - a[1].collateral);
}
}
Оракул для mark price
Ключевой компонент: mark price не должен манипулироваться flash loan'ами. dYdX v4 использует Pyth oracle с aggregated median из нескольких источников. GMX v2 — Chainlink + custom keeper oracle с верификацией подписи.
Требования к оракулу:
- Freshness check: цена не старше N секунд (обычно 30-60)
- Deviation check: новая цена не более чем на X% отличается от предыдущей (circuit breaker)
- Multi-source aggregation: медиана из 3+ источников
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 механизм
Auto-Deleveraging — последняя линия защиты. Если insurance fund исчерпан, протокол принудительно закрывает прибыльные позиции по mark price (без slippage). Порядок закрытия: позиции с наибольшим profit AND наибольшим leverage первыми (наиболее рискованные для системы).
ADL — болезненный механизм для трейдеров. Важно:
- Четко раскрывать риск ADL в документации
- Показывать ADL indicator на UI (как на Binance futures)
- Ограничивать OI чтобы минимизировать необходимость ADL
Стек
Solidity + Foundry — ликвидационные контракты, оракул, insurance fund. TypeScript + viem — keeper бот, мониторинг позиций. Chainlink + Pyth — price feeds. Gelato Network — автоматизированный вызов keeper функций (как fallback). Foundry fork tests — симуляция стресс-сценариев на mainnet fork.
Процесс работы
Аналитика (3-5 дней). Параметры рисков: maintenance margin, ликвидационный fee, страховой фонд. Моделирование stress scenarios: что при -50% основного актива за один блок.
Разработка (2-4 недели). Ликвидационный контракт + keeper bot + oracle интеграция + insurance fund.
Тестирование (1 неделя). Fork-тесты с историческими price shocks (March 2020, LUNA crash, FTX). Инвариант: после каждой ликвидации margin ratio позиции не меньше 0.
Аудит. Для perpetual DEX с реальным TVL — обязательно.
Ориентиры по срокам
Ликвидационная система для одного актива без ADL — 1-2 недели. Полная система с ADL, insurance fund, мульти-оракульным агрегатором и keeper инфраструктурой — 4-6 недель. Стоимость рассчитывается индивидуально.







