Розробка системи ліквідацій для Perpetual DEX
У листопаді 2022 року, після краху FTX, GMX v1 пройшов стрес-тест: ціни різко рухалися, об'єми торгів злетіли в 10x, і система ліквідацій витримала навантаження. Це тому що GMX використовує keeper-based ліквідації з правильно вибудованими incentive-ами: ліквідатор отримує частину ліквідаційного fee, але тільки якщо виконує швидко. Погано спроектована система ліквідацій в такі моменти або не встигає закривати позиції, або створює bad debt, який ляжає на LP провайдерів.
Механіка ліквідацій у perpetual DEX
Що таке ліквідована позиція
На perpetual DEX трейдер відкриває позицію з левередж: 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 — резерв, сформований з частини торгових комісій
- ADL (Auto-Deleveraging) — якщо insurance fund не покриває, прибуткові позиції протилежної сторони примусово закриваються
- Max open interest limits — обмеження сукупного OI по активу знижує потенціальний bad debt
Архітектура системи ліквідацій
On-chain компонент
Контракт зберігає позиції та постійно оновлює mark price через oracle. Ліквідація відбувається в два кроки:
1. Перевірка ліквідовності (view функція):
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 порогу
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> {
// Підписка на eventi оновлення позицій
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 oracle
Ключовий компонент: mark price не повинен маніпулюватися flash loan-ами. dYdX v4 використовує Pyth oracle з агрегованою медіаною з кількох джерел. GMX v2 використовує Chainlink + custom keeper oracle з верифікацією підпису.
Вимоги до 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 найбільшим leverge першими (найбільш ризиковані для системи).
ADL — болісний механізм для трейдерів. Важливо:
- Чітко розкрити ризик ADL в документації
- Показувати ADL indicator на UI (як на Binance futures)
- Обмежити OI щоб мінімізувати потребу в ADL
Стек
Solidity + Foundry — ліквідаційні контракти, oracle, insurance fund. TypeScript + viem — keeper бот, моніторинг позицій. Chainlink + Pyth — price feeds. Gelato Network — автоматизований виклик keeper функцій (як fallback). Foundry fork tests — симуляція стрес-сценаріїв на mainnet fork.
Процес роботи
Аналітика (3-5 днів). Параметри ризиків: maintenance margin, ліквідаційний fee, страховий фонд. Моделювання стрес-сценаріїв: що при -50% основного активу за один блок.
Розробка (2-4 тижні). Ліквідаційний контракт + keeper бот + oracle інтеграція + insurance fund.
Тестування (1 тиждень). Fork-тести з історичними price шоками (March 2020, LUNA краш, FTX). Інваріант: після кожної ліквідації margin ratio позиції не менше 0.
Аудит. Для perpetual DEX з реальним TVL — обов'язково.
Орієнтири по термінам
Ліквідаційна система для одного активу без ADL — 1-2 тижні. Повна система з ADL, insurance fund, мульти-oracle агрегатором і keeper інфраструктурою — 4-6 тижнів. Вартість рахується індивідуально.







