Примитивы DeFi: залог, займы, ликвидации и стейблкоины
В прошлом уроке мы разобрали рынок в его классическом виде: стакан, спред, плечо, опцион. Теперь то же самое переезжает в контракты, и одна деталь меняет всё устройство. В контракте нет ни паспорта, ни кредитной истории, ни суда. Из этого одного ограничения выводится вся конструкция DeFi: избыточный залог, здоровье позиции, открытая ликвидация и стейблкоин, который держится не обещанием, а математикой.
Кредит без личности
Банк даёт в долг под обещание вернуть, а если не вернули, обращается к суду. Контракт не знает, кто ты, и позвать никого не может. Значит, обещание не работает, и остаётся один инструмент: деньги, которые заёмщик оставил и потеряет, если не вернёт долг.
Отсюда странность, которая первым делом смущает всех: чтобы занять 16 тысяч, нужно положить активов на 20. Зачем занимать меньше, чем у тебя есть? Причин ровно три.
Не продавать актив. Ты веришь в эфир и не хочешь его продавать, но нужны деньги сейчас. Займ даёт деньги, оставляя актив за тобой (и налоговое событие тоже не наступает).
Плечо. Занял стейблкоины под эфир, купил ещё эфира, положил в залог, занял снова. Такой круг даёт позицию больше капитала, ровно как плечо из прошлого урока, только собранное вручную.
Короткая позиция. Занял актив, продал, ждёшь падения, выкупаешь дешевле, возвращаешь.
Все три встречаются постоянно, и все три упираются в одно: как контракт понимает, что позиция ещё жива.
Три числа на каждый актив
Протокол хранит на каждый рынок три параметра риска, и путать их нельзя.
pub struct Reserve {
/// Какую долю стоимости залога разрешено занять. Всегда меньше 100%:
/// разница это подушка на волатильность.
pub collateral_factor_bps: u32,
/// Порог, ниже которого позиция считается больной. Всегда выше
/// collateral_factor_bps, иначе позиция была бы под ликвидацией уже в
/// момент открытия.
pub liquidation_threshold_bps: u32,
/// Премия ликвидатору: за что он берётся за работу.
pub liquidation_bonus_bps: u32,
pub feed: PriceFeed,
}
Коэффициент обеспечения и порог ликвидации это два разных числа, и зазор между ними принципиален. Заняв ровно по лимиту, ты сразу оказываешься в положении, где до ликвидации ещё есть расстояние. Если бы числа совпадали, любая позиция вспыхивала бы в момент открытия от первого движения цены.
Здоровье позиции: одна дробь, от которой всё зависит
pub fn health(&self, account: &Account, now: u64) -> Result<Health, LendError> {
let mut collateral_value = 0.0;
let mut adjusted_value = 0.0;
let mut borrow_limit = 0.0;
for (asset, amount) in &account.deposits {
let reserve = self.reserve(asset)?;
let value = reserve.feed.price_at(now)? * amount;
collateral_value += value;
adjusted_value += value * f64::from(reserve.liquidation_threshold_bps) / 10_000.0;
borrow_limit += value * f64::from(reserve.collateral_factor_bps) / 10_000.0;
}
let mut debt_value = 0.0;
for (asset, amount) in &account.borrows {
let reserve = self.reserve(asset)?;
debt_value += reserve.feed.price_at(now)? * amount;
}
let health_factor = if debt_value == 0.0 {
f64::INFINITY
} else {
adjusted_value / debt_value
};
Ok(Health { collateral_value, adjusted_value, borrow_limit, debt_value, health_factor })
}
Коэффициент здоровья и есть тот единственный показатель, за которым следит и заёмщик, и охотник за ликвидациями.
Проследим за конкретной позицией. Эфир стоит 2000, коэффициент обеспечения 80%, порог 85%.
| Состояние | Залог | Скорректированный | Долг | Здоровье |
|---|---|---|---|---|
| Внёс 10 ETH | 20 000 | 17 000 | 0 | бесконечность |
| Занял 16 000 USDC | 20 000 | 17 000 | 16 000 | 1.0625 |
| Эфир упал до 1800 | 18 000 | 15 300 | 16 000 | 0.956 |
| Эфир упал до 1700 | 17 000 | 14 450 | 16 000 | 0.903 |
Заняв ровно по лимиту, заёмщик получил запас всего в шесть процентов: здоровье падает до единицы уже на цене 1882, то есть при снижении эфира на 5.9%. Дальше позиция становится добычей, и на 1700 её здоровье 0.903.
Ликвидация: чужая проблема как рабочее место
В банке просрочкой занимается отдел взыскания. В контракте нет отделов, поэтому работа выставляется наружу: кто угодно может погасить часть чужого долга и забрать за это залог со скидкой.
pub fn liquidate(
&self,
account: &mut Account,
repay_asset: &str,
repay_amount: f64,
seize_asset: &str,
now: u64,
) -> Result<f64, LendError> {
let health = self.health(account, now)?;
if health.health_factor >= 1.0 {
return Err(LendError::PositionHealthy);
}
let debt = *account.borrows.get(repay_asset).ok_or(LendError::UnknownAsset)?;
let max_repay = debt * f64::from(self.close_factor_bps) / 10_000.0;
if repay_amount > max_repay + 1e-9 {
return Err(LendError::RepayTooLarge);
}
let repay_price = self.reserve(repay_asset)?.feed.price_at(now)?;
let seize_reserve = *self.reserve(seize_asset)?;
let seize_price = seize_reserve.feed.price_at(now)?;
let bonus = 1.0 + f64::from(seize_reserve.liquidation_bonus_bps) / 10_000.0;
let seize_amount = repay_amount * repay_price * bonus / seize_price;
// Дальше проверки и списание залога.
Ok(seize_amount)
}
Продолжим сценарий: эфир на 1700, здоровье 0.903, долг 16 000 USDC.
Ликвидатор гасит 8000 USDC (это close factor 50%) и забирает залога на 8400 долларов, то есть 4.9412 ETH. Его прибыль это премия 5%, четыреста долларов минус газ.
Что стало с позицией: залога осталось на 8600, долга 8000, здоровье 0.914. Стало лучше, но всё ещё меньше единицы. Одна ликвидация лечит не до конца и это не баг: близкая к единице позиция вылечится полностью, а глубоко просевшая потребует нескольких заходов. Так протокол не режет всё разом при мелком провале.
Два следствия, которые нужно держать в голове проектировщику.
Премия это не подарок ликвидатору, а цена скорости. Она должна покрывать газ, риск отката транзакции и проигранную конкуренцию. Слишком маленькая премия означает, что позиции никто не лечит, и протокол набирает безнадёжный долг.
Ликвидация это MEV в чистом виде. Все охотники видят одну и ту же больную позицию в одном и том же блоке. Механику конкуренции за такую сделку мы разбирали в уроке про MEV, а бота, который её находит, напишем в уроке 50.
премиум-урок
Продолжение в премиуме: 1 200 ₽ в месяц или 10 000 ₽ в год. Вместе с ним серверные проверки задач и школьные токены AI-коуча.
Оплата пока не подключена: премиум и курсы открываются промокодом.