Разработка системы ликвидаций для perpetual DEX
Flash crash на 50% за пару минут — и если ликвидационный модуль не успевает обработать позиции, протокол получает bad debt. Одна popular perpetual биржа в прошлом году столкнулась с этим, когда объёмы выросли на порядок, а keeper-сеть не справилась. В таких сценариях правильно спроектированная система ликвидаций с инсентивами для ликвидаторов держит удар: ликвидатор получает долю комиссии, но только при быстром исполнении. Плохая архитектура либо пропускает позиции, либо генерирует необслуживаемый долг для LP. Мы проектируем решения, выдерживающие экстремальные движения рынка. Свяжитесь с нами для аудита вашего протокола.
Наши инженеры имеют 5+ лет опыта в DeFi разработке, реализовали более 15 решений для ликвидаций на Ethereum, Arbitrum и Polygon. Гарантируем корректную работу даже при резких скачках цены.
Как вычисляется ликвидационный порог?
На perpetual DEX (бессрочные фьючерсы) (Wikipedia) трейдер открывает позицию с leverage: 10x long ETH, внеся 1000 USDC коллатераль, позиция = 10,000 USDC notional. При падении ETH на 9% unrealized loss составляет 900 USDC, коллатераль уменьшается до 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 (резкий прыжок цены, например при выходе новостей) перескакивает через несколько уровней ликвидации за один тик. Позиция может сразу уйти в negative equity без возможности промежуточной ликвидации. Разработчики dYdX указывают в документации к v4, что gap risk — основная причина bad debt.
GMX v2 и dYdX v4 применяют несколько механизмов для смягчения gap risk:
- Insurance fund — резерв из части trading fees
- ADL (Auto-Deleveraging) — если insurance fund не покрывает, принудительно закрываются прибыльные позиции противоположной стороны
- Max open interest limits — ограничение совокупного OI по активу снижает потенциальный bad debt
Сравнение подходов: keeper vs on-chain ликвидация
| Параметр | Keeper-based | On-chain автоматическая |
|---|---|---|
| Скорость реакции | Зависит от gas и конкуренции | Мгновенно при каждом блоке |
| Сложность | Средняя (инфраструктура оффчейн) | Высокая (газ, сложность) |
| Контроль протокола | Косвенный (через incentives) | Прямой |
| Риск MEV | Высокий | Низкий |
| Пример | GMX, dYdX | Synthetix (прошлые версии) |
Keeper-based подход снижает газовые затраты на ликвидацию в 2-3 раза по сравнению с on-chain автоматическим. Это подтверждено на практике в наших проектах.
Архитектура системы ликвидаций
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(). Инсентайв — keeper fee, создающий конкурентный рынок ликвидаторов.
Пример конфига keeper-бота на TypeScript с 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)); // Самые выгодные первыми
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
Ключевой элемент: 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 и leverage — как наиболее рискованные для системы.
ADL — болезненный механизм для трейдеров. Важно:
- Четко раскрывать риск ADL в документации
- Показывать ADL indicator на UI (как на Binance futures)
- Ограничивать OI, чтобы минимизировать необходимость ADL
Что входит в разработку системы под ключ
| Этап | Длительность | Результат |
|---|---|---|
| Анализ рисков и моделирование | 3-5 дней | Параметры maintenance margin, fee, размер insurance fund |
| Разработка смарт-контрактов | 2-4 недели | Контракты ликвидации, insurance fund, oracle adapter |
| Интеграция keeper-бота | 1-2 недели | Off-chain инфраструктура на TypeScript |
| Fork-тестирование | 1 неделя | Симуляция стресс-сценариев (flash crash и крах стейблкоина) |
| Аудит | 2-3 недели | Отчет независимого аудитора |
| Деплой и мониторинг | 1 неделя | Документация, скрипты, дашборд |
Полный цикл занимает 8-12 недель. Стоимость рассчитывается индивидуально.
Стек
Solidity + Foundry — ликвидационные контракты, оракул, insurance fund. TypeScript + viem — keeper-бот, мониторинг. Chainlink + Pyth — price feeds. Gelato Network — fallback для вызова keeper-функций. Foundry fork tests — симуляция на mainnet fork.
Как мы гарантируем надёжность
Опираемся на 5-летний опыт в Web3 и десятки успешных аудитов. Внедряем формальную верификацию для критических контрактов. Предоставляем гарантию на код в течение 6 месяцев после деплоя.
Получите консультацию: опишите свой протокол, и мы оценим риски и предложим архитектуру.







