Web3 и блокчейн DeFi-примитивы: AMM, кредитование, стейблкоины, ликвидации, композиция
0%

DeFi-примитивы: AMM, кредитование, стейблкоины, ликвидации, композиция

DeFi-примитивы: AMM, кредитование, стейблкоины, ликвидации, композиция

Предыдущие статьи трека собирали машину: состояние и газ, контракты и их уязвимости, токены, приложение вокруг цепи. Осталась неудобная дыра: почти весь реальный трафик публичных цепей — это финансовые протоколы, и человек, который знает EVM, но не знает, что такое health factor и почему своп на 1 % резерва двигает цену на 2 %, интегрирует их вслепую.

Здесь важно сразу снять два предубеждения. Первое: DeFi — не «финансы с блокчейном», а очень небольшой набор конечных автоматов над балансами, у которых нет права на усмотрение. Банк может отказать в выдаче, отменить перевод, реструктурировать долг по звонку. Контракт не может ничего: он умеет только применить формулу к числам, которые ему передали, и обновить mapping. Из этого вытекает вся конструкция — включая то, почему залог обязан быть избыточным, почему ликвидацию делает случайный посторонний бот, и почему цена приходит извне и оказывается главной точкой отказа.

Второе: сложность DeFi — не в «финансовой математике». Формулы здесь школьного уровня, их четыре штуки. Сложность в том, что каждый примитив — публичный API без аутентификации, который вызывают в одной транзакции вместе с другими такими же, и все инварианты приходится держать не в своей голове, а в коде. Разбираемся по примитивам, а в конце — про их композицию, где и происходят почти все интересные аварии.

1. Пять примитивов, из которых собрано всё остальное

Дальше подробно разбираются первые три — они образуют фундамент, на который опираются остальные. Производные в этой статье затрагиваются вскользь: их риск-модель — это, по сути, кредитование плюс оракул плюс механизм ликвидации, то есть комбинация уже разобранного.

2. Почему в цепи не работает книга заявок

Классическая биржа — это limit order book: отсортированные списки заявок, матчинг лучших встречных. В EVM такая структура ломается об экономику исполнения (газ — не абстракция, а счёт).

  • Заявка — это запись в storage. Выставить и отменить — две транзакции с записью и удалением слотов. Маркетмейкер, который в обычной бирже переставляет котировки десятки раз в секунду, здесь платит за каждое движение.
  • Матчинг — это цикл по заявкам. Стоимость исполнения зависит от глубины стакана, а лимит газа в блоке конечен: заявка может стать неисполнимой просто потому, что стакан вырос.
  • Отмена не приватна. Транзакция отмены висит в мемпуле, её видно, и её можно обогнать. Маркетмейкер обязан рисковать по устаревшей котировке — это классическая проблема stale quote sniping, и в цепи она усиливается.

Ответ, который выжил, — AMM (automated market maker): заменить список заявок формулой. Пул хранит два числа (резервы), а цена — функция от них. Пассивный поставщик ликвидности не переставляет котировки, а один раз вносит активы. Пропускная способность больше не зависит от глубины стакана: своп — это O(1) арифметика и две записи в storage.

Плата за это — качество исполнения. Формула не знает, что происходит на других площадках; она просто движется по кривой. Восстанавливают связь с внешним рынком арбитражёры, и оплачивают эту связь поставщики ликвидности. Это ключ ко всему разделу.

3. AMM с постоянным произведением

3.1. Инвариант и цена

Пул держит резервы двух токенов $x$ и $y$ и обязуется соблюдать инвариант

$$ x \cdot y = k $$

Любой обмен обязан оставить произведение не меньше прежнего. Мгновенная («серединная») цена — это модуль производной кривой:

$$ P = -\frac{dy}{dx} = \frac{y}{x} $$

То есть цена в пуле — это просто отношение резервов. Никакого «источника цены» нет: пул не знает, сколько актив стоит, он знает только, сколько его у него осталось.

Кривая постоянного произведения: своп, мгновенная цена и проскальзывание

3.2. Сколько на самом деле получит трейдер

При обмене $\Delta x$ на $\Delta y$ с комиссией $f$ (доля, остающаяся в пуле) новое произведение не должно упасть:

$$ (x + \Delta x (1 - f)) \cdot (y - \Delta y) \geq x \cdot y \quad\Longrightarrow\quad \Delta y = \frac{y \cdot \Delta x (1 - f)}{x + \Delta x (1 - f)} $$

В целочисленной арифметике EVM это записывают без дробей — ровно так, как в Uniswap V2 с комиссией 0.30 %:

FEE_NUM, FEE_DEN = 997, 1000          # 0.30 % остаётся в пуле

def get_amount_out(dx: int, x: int, y: int) -> int:
    """Выдача пула при внесении dx. Целые числа, округление вниз — в пользу пула.
    Сложность: O(1) по времени, O(1) по памяти."""
    assert dx > 0 and x > 0 and y > 0, "пустой пул или нулевой ввод"
    dx_fee = dx * FEE_NUM
    return (dx_fee * y) // (x * FEE_DEN + dx_fee)

x, y = 1_000 * 10**18, 2_000_000 * 10**18   # пул: 1000 X и 2 000 000 Y, цена 2000
dx = 10 * 10**18                            # сделка на 1 % резерва
out = get_amount_out(dx, x, y)
print(out / dx, y / x, (y - out) / (x + dx))  # курс сделки, цена до, цена после

Вывод для сделки в 1 % резерва: получено 19 743.16 Y, фактический курс 1974.32 против середины 2000.00 — на 1.28 % хуже; цена в пуле после сделки 1960.65, то есть сдвинулась на 1.97 %.

Разложим 1.28 %: 0.30 % — комиссия, остальные ≈0.98 % — проскальзывание, плата за движение по кривой. Для малых сделок работают приближения $\text{проскальзывание} \approx \Delta x / x$ и $\text{сдвиг цены} \approx 2 \Delta x / x$.

Отсюда практическое правило, которое стоит запомнить наизусть: торгуя долю $\alpha$ резерва, вы теряете примерно $\alpha$ и двигаете цену примерно на $2\alpha$. Пул с резервом 1 млн USD не переварит сделку на 100 тыс. USD без 10 % потерь — и никакой «глубины книги» здесь нет, есть только эта гипербола.

Отдельно стоит понять, что контракт пула не считает выдачу. Uniswap V2 устроен наоборот: маршрутизатор оптимистично забирает токены, а пул лишь проверяет, что инвариант не нарушен:

// Ядро Uniswap V2, упрощённо: пул не вычисляет цену, он проверяет неравенство.
function swap(uint256 amount0Out, uint256 amount1Out, address to) external lock {
    require(amount0Out > 0 || amount1Out > 0, "INSUFFICIENT_OUTPUT");
    (uint112 r0, uint112 r1) = (reserve0, reserve1);
    require(amount0Out < r0 && amount1Out < r1, "INSUFFICIENT_LIQUIDITY");

    if (amount0Out > 0) _safeTransfer(token0, to, amount0Out);   // выдали заранее
    if (amount1Out > 0) _safeTransfer(token1, to, amount1Out);

    // сколько реально пришло — считаем по фактическому балансу, а не по аргументам
    uint256 b0 = IERC20(token0).balanceOf(address(this));
    uint256 b1 = IERC20(token1).balanceOf(address(this));
    uint256 in0 = b0 > r0 - amount0Out ? b0 - (r0 - amount0Out) : 0;
    uint256 in1 = b1 > r1 - amount1Out ? b1 - (r1 - amount1Out) : 0;
    require(in0 > 0 || in1 > 0, "INSUFFICIENT_INPUT");

    // инвариант с учётом комиссии 0.30 %: масштаб 1000, вычитаем 3 промилле с входа
    uint256 b0adj = b0 * 1000 - in0 * 3;
    uint256 b1adj = b1 * 1000 - in1 * 3;
    require(b0adj * b1adj >= uint256(r0) * uint256(r1) * 1000**2, "K");
    _update(b0, b1);
}

Две мысли, которые из этого следуют. Во-первых, «выдали, потом проверили» — это и есть механизм flash swap: между выдачей и проверкой можно вызвать чужой контракт (см. раздел 6). Во-вторых, факт прихода средств определяется фактическим балансом, а не аргументом функции: иначе токен с комиссией на перевод сломал бы учёт (разбор странных токенов — в статье о стандартах).

3.3. Поставщик ликвидности и непостоянные потери

Внося пару в пропорции текущей цены, поставщик получает LP-токены — долю пула:

$$ \text{доля} = \frac{L_{\text{ваши}}}{L_{\text{всего}}}, \qquad L = \sqrt{x \cdot y} $$

Комиссии не выплачиваются отдельно: они остаются в резервах, поэтому $k$ растёт, и стоимость доли увеличивается сама. Это удобно для газа и неудобно для интуиции — «доход» не виден как транзакция.

Теперь неприятная часть. Арбитраж всегда торгует против пула: когда внешняя цена растёт, у пула забирают подорожавший актив и доливают подешевевший. В результате портфель LP всегда «продавал растущее и покупал падающее». Разница с простым удержанием тех же активов называется непостоянными потерями (impermanent loss). Для пула 50/50 при изменении цены в $r$ раз:

$$ \text{IL}(r) = \frac{2\sqrt{r}}{1 + r} - 1 $$

from math import sqrt

def impermanent_loss(r: float) -> float:
    """Отставание LP-позиции от простого удержания при изменении цены в r раз."""
    return 2 * sqrt(r) / (1 + r) - 1
Изменение цены 1.25x 1.5x 2x 3x 5x 0.5x 0.25x
Отставание от удержания −0.62 % −2.02 % −5.72 % −13.40 % −25.46 % −5.72 % −20.00 %

Функция симметрична по $r$ и $1/r$ и всегда неположительна. «Непостоянными» потери называются потому, что при возврате цены они исчезают — но рассчитывать на возврат нельзя, а комиссии за это время могут их и не покрыть. Честная формулировка: LP продаёт волатильность; он в плюсе, когда торговый оборот велик, а цена ходит вокруг одного уровня, и в минусе при направленном тренде. Более точный современный язык — LVR (loss-versus-rebalancing) из работы Милионис, Мозер, Мозер и Ресник, arxiv.org/abs/2208.06046: арбитражёр систематически снимает с пула ту же величину, что маркетмейкер на обычной бирже потерял бы на неблагоприятном отборе.

3.4. Другие кривые: когда постоянного произведения мало

Гипербола универсальна, но неэффективна для активов, которые должны стоить одинаково: рядом с ценой 1:1 она даёт лишнее проскальзывание. Отсюда семейство кривых:

Кривая Инвариант Где применяется Цена ошибки
Постоянное произведение $x y = k$ любые пары проскальзывание на всём диапазоне
Постоянная сумма $x + y = k$ теоретический предел пул опустошается арбитражём при малейшем отклонении
StableSwap (Curve) $A n^n \sum x_i + D = A D n^n + \frac{D^{n+1}}{n^n \prod x_i}$ стейблкоины, LST при разрыве пега пул целиком набирает слабый актив
Взвешенная (Balancer) $\prod x_i^{w_i} = k$ пулы 80/20, индексы те же непостоянные потери, но асимметричные
Концентрированная (Uniswap V3) $xy=k$ на диапазоне тиков капиталоёмкие пары позиция вне диапазона перестаёт торговать

Инвариант Curve не решается в замкнутой форме — контракт находит $D$ методом Ньютона за несколько итераций прямо в EVM (см. численные методы); отсюда его заметная стоимость в газе. Параметр $A$ («амплификация») задаёт, насколько кривая близка к постоянной сумме внутри пега и как быстро она переходит к гиперболе за его пределами. Концентрированная ликвидность заслуживает отдельной оговорки, потому что меняет профессию LP. В Uniswap V3 поставщик выбирает диапазон цен, а контракт работает с «виртуальными резервами», как если бы в этом диапазоне был обычный x·y=k. Капиталоёмкость растёт в десятки раз, но позиция вне диапазона превращается в 100 % одного актива и перестаёт зарабатывать. Пассивное вложение становится активным управлением — и статистика по кошелькам LP-позиций регулярно показывает, что средний розничный поставщик проигрывает простому удержанию. В Uniswap V4 (2025) к этому добавились хуки — пользовательский код на этапах инициализации, свопа и изменения ликвидности, что превращает пул в платформу и одновременно расширяет поверхность атаки: docs.uniswap.org.

4. Цена в пуле — не оракул

Соблазн понятен: у пула есть getReserves(), отношение резервов — цена, читается одним вызовом. Именно так были потеряны сотни миллионов.

Резервы — мгновенное состояние, которым внутри одной транзакции управляет любой, у кого есть капитал (а с флеш-займом — вообще любой). Контракт, считающий залог по спотовой цене пула, оценивается по цене, которую атакующий назначит сам. Механика этих атак, TWAP, кворум источников и требования к проверке ответов Chainlink (updatedAt, answeredInRound, границы агрегатора) подробно разобраны в безопасности контрактов — здесь достаточно инженерного минимума:

  • спотовая цена пула годится для UI и маршрутизации, но не для решений о деньгах; TWAP лучше ровно потому, что стоит атакующему удержания цены на протяжении окна;
  • push-оракулы (Chainlink Data Feeds) обновляются по отклонению и heartbeat: между обновлениями ваша цена устарела, и протокол обязан это учитывать;
  • pull-оракулы (Pyth, Chainlink Data Streams) требуют, чтобы пользователь принёс подписанное обновление в той же транзакции — свежее, но появляется зависимость от доступности сети публикаторов.

5. Кредитование под залог

5.1. Почему залог обязан быть избыточным

У контракта нет ни личности заёмщика, ни суда, ни коллекторов. Единственный доступный ему рычаг — актив, который он уже держит. Значит, заём выдаётся только против залога, который стоит больше долга, и разница должна покрывать: падение цены за время до ликвидации, премию ликвидатору, ошибку оракула и проскальзывание при продаже залога. Отсюда типичные 120–170 % обеспечения и главный вывод: DeFi-кредит — это не кредит в бытовом смысле, это ломбард с автоматическим сроком продажи залога. Займы без обеспечения существуют лишь в форме флеш-займа, где вопрос доверия снят конструкцией транзакции.

5.2. Ставка как функция утилизации

Пулу нужно одновременно платить вкладчикам, брать с заёмщиков и сохранять ликвидность для вывода. Всё это решается одной кусочно-линейной функцией от утилизации:

$$ U = \frac{B}{C + B} $$

где $B$ — занято, $C$ — свободный остаток. Ставка растёт медленно до перелома (kink) и резко после — так рынок сам выталкивает утилизацию обратно, когда вкладчикам становится нечего выводить.

def borrow_rate(u: float, base=0.00, slope1=0.05, kink=0.80, slope2=1.00) -> float:
    """Годовая ставка заёмщика: пологий участок до перелома, крутой после."""
    if u <= kink:
        return base + u * slope1
    return base + kink * slope1 + (u - kink) * slope2

def supply_rate(u: float, reserve_factor=0.10) -> float:
    """Ставка вкладчика: проценты заёмщиков делятся на всех вкладчиков минус резерв."""
    return borrow_rate(u) * u * (1 - reserve_factor)

При утилизации 50 % заёмщик платит 2.5 %, вкладчик получает 1.13 %; при 95 % — 19.0 % и 16.2 %. Разрыв между ставками — не жадность, а арифметика: проценты платит только занятая часть, а делятся они на весь депозит, и часть уходит в резервный фонд протокола (тот самый, из которого потом закрывают плохой долг). Практическое следствие для интеграции: высокая доходность на вкладе почти всегда означает высокую утилизацию, то есть невозможность выйти прямо сейчас — «ликвидный» депозит ликвиден ровно до момента, когда он всем понадобился.

5.3. Индексная бухгалтерия: почему никто не обходит балансы циклом

Проценты капают каждому — но обойти миллион позиций в транзакции нельзя. Решение из мира банковских систем: хранить индекс и пересчитывать баланс лениво.

$$ I_{t+\Delta} = I_t \left(1 + r \cdot \frac{\Delta}{\text{год}}\right), \qquad \text{долг}{\text{сейчас}} = \text{долг}{\text{записанный}} \cdot \frac{I_{\text{сейчас}}}{I_{\text{на момент займа}}} $$

Пользователь хранит «нормализованную» величину, глобально хранится индекс, обновляемый при любом касании рынка. Стоимость начисления процентов всем сразу — O(1). Ровно такая же идея в Compound (borrowIndex, exchangeRate у cToken) и Aave (liquidityIndex, variableBorrowIndex, aToken с растущим балансом).

Обратите внимание на поля supply_cap, liq_threshold и heartbeat: это не украшения, а параметры риска, которыми кто-то управляет. Кто именно — вопрос следующей статьи, и он важнее большинства строк кода.

5.4. Здоровье позиции и ликвидация

Единственная метрика, которой живёт заёмщик:

$$ HF = \frac{\sum_i C_i \cdot P_i \cdot LT_i}{\sum_j D_j \cdot P_j} $$

где $C_i$ — залоги, $LT_i$ — их пороги ликвидации, $D_j$ — долги, $P$ — цены из оракула. Позиция ликвидируема при $HF < 1$.

Ликвидацию делает не протокол, а любой посторонний бот: он гасит часть долга своими деньгами и забирает залог со скидкой. Это и есть весь механизм принуждения — экономический стимул вместо администратора. Ключевые параметры: close factor (какую долю долга можно погасить за раз, типично 50 %) и liquidation bonus (премия, типично 5–10 %).

def health_factor(collateral_qty, collateral_price, lt, debt_usd) -> float:
    return collateral_qty * collateral_price * lt / debt_usd

def liquidate(collateral_qty, price, lt, debt_usd, close_factor=0.5, bonus=0.05):
    """Одна ликвидация: гасим долю долга, забираем залог с премией. O(1)."""
    repaid = debt_usd * close_factor
    seized_usd = repaid * (1 + bonus)
    seized_qty = seized_usd / price
    return collateral_qty - seized_qty, debt_usd - repaid, seized_usd - repaid

qty, lt, debt = 10.0, 0.825, 15_000.0   # 10 ETH залога, порог 82.5 %, долг 15 000 USD
print([round(health_factor(qty, p, lt, debt), 3) for p in (2000, 1900, 1800)])
print(liquidate(qty, 1800, lt, debt))   # остаток залога, остаток долга, прибыль

Числа: при цене 2000 USD здоровье 1.100, при 1900 — 1.045, при 1800 — 0.990, то есть позиция открыта для ликвидации. Ликвидатор гасит 7500 USD, забирает 4.375 ETH (7875 USD), кладёт в карман 375 USD, а у заёмщика остаётся 5.625 ETH против долга 7500 USD — здоровье 1.114. Система вернулась в норму, «наказание» заёмщика составило те самые 375 USD.

Что здесь важно инженерно:

  1. Ликвидация — гонка. Прибыльные позиции разбирают боты за один блок, и это классический MEV: та же приоритетная комиссия, тот же приватный поток транзакций (см. раздел о порядке транзакций).
  2. Премия — не подарок, а цена страховки. Слишком маленькая — ликвидаторы не придут при волатильности и высоком газе, слишком большая — заёмщики платят лишнего.
  3. Оракул задаёт момент. Push-оракул с порогом отклонения 0.5 % означает, что ликвидация происходит не в момент падения цены, а в момент её публикации. Все окна риска считаются от этого.

5.5. Плохой долг

Если цена проваливается быстрее, чем боты успевают отработать (или ликвидация становится убыточной из-за проскальзывания при продаже залога), у протокола образуется долг без обеспечения. Дальше его закрывают из резервного фонда, из страхового модуля со стейкингом токена протокола, либо он молча размазывается по вкладчикам через невозможность вывести всё.

Хрестоматийный пример — «чёрный четверг» MakerDAO 12 марта 2020: перегруженная сеть, взлетевший газ, аукционы залога, выигранные нулевыми ставками, — и дыра в несколько миллионов USD, закрытая аукционом MKR. Разбор и последующие изменения параметров: blog.makerdao.com и постмортемы в forum.makerdao.com. Урок ровно один: риск-модель протокола проверяется не средним днём, а худшим часом, когда одновременно падает цена, растёт газ и стоит очередь в мемпуле.

6. Флеш-займы: заём длиной в одну транзакцию

Флеш-займ — самое непонятое место в DeFi. Никакого «взлома» в нём нет: это прямое следствие атомарности транзакции. Если весь заём и его возврат происходят внутри одной транзакции, то отказ возврата откатывает и выдачу. Кредитор не рискует ничем, поэтому и залог не нужен.

Стандартный интерфейс — ERC-3156; у Aave V3 своя пара flashLoanSimple/executeOperation с премией порядка 0.05 %; у Uniswap V2 то же самое достигается flash swap (выдача до проверки инварианта, которую мы видели в коде выше).

contract Arbitrageur is IERC3156FlashBorrower {
    bytes32 private constant CALLBACK_OK = keccak256("ERC3156FlashBorrower.onFlashLoan");
    address private immutable lender;
    address private immutable owner;

    constructor(address _lender) { lender = _lender; owner = msg.sender; }

    function onFlashLoan(
        address initiator, address token, uint256 amount, uint256 fee, bytes calldata data
    ) external returns (bytes32) {
        // Две проверки, без которых контракт превращается в бесплатный кошелёк:
        require(msg.sender == lender, "callback not from lender");   // зовёт именно кредитор
        require(initiator == address(this), "loan not initiated by us"); // заём начали мы
        _executeStrategy(token, amount, data);                        // полезная работа
        IERC20(token).approve(lender, amount + fee);                  // возврат с премией
        return CALLBACK_OK;
    }
}

Легитимные применения существуют и полезны: арбитраж между площадками (он же выравнивает цены), ликвидация без собственного капитала, рефинансирование позиции без её закрытия, миграция залога между протоколами. Но у флеш-займа есть и второе лицо: он обнуляет требование к капиталу для любой атаки, где нужна временная власть над числами внутри одной транзакции. Отсюда две канонические катастрофы:

  • манипуляция оракулом: занять, продавить цену в мелком пуле, взять заём под раздутый залог в чужом протоколе, вернуть (Mango Markets, октябрь 2022, ≈114 млн USD);
  • захват управления: занять, купить голоса, провести и мгновенно исполнить своё предложение (Beanstalk, апрель 2022, ≈182 млн USD) — механику этой атаки и защиту от неё разбираем в следующей статье.

Правильный вывод: не «флеш-займы опасны», а любая величина, которую можно изменить внутри одной транзакции, не является метрикой доверия. Спотовая цена, текущий баланс, totalSupply, вес голоса в моменте — всё это мгновенные значения, а не факты о мире.

7. Стейблкоины: три способа держать пег

«Стейблкоин» — не свойство токена, а обещание, которое кто-то поддерживает. Способов ровно три, и они отличаются тем, кому вы доверяете.

Обеспеченные фиатом вне цепи (USDC, USDT). Токен — расписка эмитента; пег держит обещание выкупа 1:1 и арбитраж вокруг него. Технический риск невелик, зато в полный рост встают риски эмитента, банка и цензуры: у контракта есть чёрный список, а активы лежат в обычной банковской системе. В марте 2023 года USDC отклонился до ≈0.88 USD за сутки — не из-за кода, а из-за 3.3 млрд USD резервов в рухнувшем банке. Практический вывод для интегратора: чёрный список означает, что ваш контракт может внезапно потерять способность переводить средства, и это должно быть в модели угроз.

Обеспеченные криптоактивом с избытком (CDP: MakerDAO/Sky, Liquity). Пользователь запирает волатильный залог и печатает стейбл под него — та же схема, что в разделе 5, включая ликвидации. Пег держится процентной ставкой (stability fee), ликвидациями и модулем прямого обмена на USDC (PSM). Здесь тоже стоит быть честным: чем больше PSM, тем ближе такой стейбл к обычной расписке — независимость обменивается на устойчивость.

Алгоритмические — пег без обеспечения, за счёт эмиссии парного токена. Механизм работает ровно до момента, когда рынок перестаёт верить в парный токен: спрос на выкуп разгоняет эмиссию, эмиссия обваливает цену, обвал усиливает спрос на выход. Это петля положительной обратной связи (см. обратные связи в системах). Май 2022 года, Terra/UST: примерно 40 млрд USD стоимости исчезли за несколько дней. Модель считается несостоятельной не потому, что «плохо реализовали», а потому, что её устойчивость опирается на ожидания участников — то есть на самую хрупкую из возможных опор.

Механизм Что держит пег Главный риск Что проверять интегратору
Фиатное обеспечение обещание выкупа эмитентом эмитент, банк, чёрный список адрес эмитента, право заморозки, отчётность по резервам
Избыточный залог ликвидации и ставка обвал залога, срыв ликвидаций коэффициенты, состав залога, доля PSM
Алгоритмический ожидания рынка спираль эмиссии — (интеграция такого актива как залога — ошибка)
Обёртка с моста подписанты моста компрометация моста кто мостовые подписанты (см. https://courses.digitable.life/post/web3/10-scaling/)

8. Откуда на самом деле берётся доходность

Полезная привычка: увидев цифру APY, назвать источник денег. Источников конечное число.

  1. Комиссии и проценты — платят трейдеры и заёмщики. Реальный доход, ограниченный оборотом и утилизацией.
  2. Награды за консенсус — платит протокол цепи валидаторам, а через ликвидный стейкинг — держателям LST (см. консенсус).
  3. MEV — платят другие пользователи ухудшением исполнения. Доход реальный, происхождение стоит понимать.
  4. Эмиссия токена протокола — не доход, а разводнение: вам платят долей в будущем, печатая её сейчас. Проверка простая: останется ли ставка привлекательной без эмиссии.
  5. Премия за риск — вы продали кому-то страховку (страховой модуль, транш с первым убытком) и получаете деньги, пока событие не наступило.

Отдельно про арифметику: APR — простая годовая ставка, APY — с учётом реинвестирования, $APY = (1 + r/n)^n - 1$. Витрины любят показывать APY при ежеблочном начислении, что при 100 % APR даёт эффектные 171 % — чтобы их получить, нужно реально реинвестировать, платя газ. Стандартная упаковка для «доходной» стратегии — хранилище ERC-4626 с его известной атакой на первого вкладчика; там же разобрано, почему округление в таких контрактах всегда делают в пользу протокола.

9. Композиция: главная сила и главный системный риск

Уникальность DeFi не в отдельных примитивах — банки умеют то же самое лучше. Уникальность в том, что чужой протокол вызывается как библиотека, без договора и разрешения, а вся цепочка исполняется атомарно.

Цена этой силы — четыре класса системных рисков, о которых лучше думать заранее.

  • Заимствованные инварианты. Ваш протокол корректен, пока корректен чужой. Обновление чужого контракта (или его параметров через голосование) может сломать ваш инвариант без единой строчки изменений у вас.
  • Каскадные ликвидации. Падение цены вызывает ликвидации, продажи залога давят цену дальше, что вызывает следующие ликвидации. Та же положительная обратная связь, что в алгоритмическом стейблкоине, только быстрее.
  • Рефлексивные оракулы. Если актив A принимается как залог для выпуска актива B, а цена A берётся из пула, ликвидность которого создана на B, — контур замкнулся, и «стоимость» держится сама на себе.
  • Общий одиночный отказ. Один популярный оракул, один мост, одна обёртка стейблкоина, один компилятор. Взлом пулов Curve в июле 2023 года был спровоцирован ошибкой в конкретных версиях компилятора Vyper (нерабочая защита от повторного входа), и накрыл сразу несколько независимых протоколов — postmortem.

Отдельная и очень практичная разновидность — read-only reentrancy: чужой контракт читает вашу функцию просмотра в момент, когда ваше состояние временно несогласованно. Механика разобрана в безопасности контрактов; в контексте композиции важно другое — ваши view-функции такой же публичный API, как и обычные, и их согласованность — ваша обязанность.

10. Как интегрировать DeFi-протокол и не потерять деньги

Практический слой, который отделяет «работает на форке» от «работает в мейннете».

/// @notice Обмен через маршрутизатор с честной защитой от проскальзывания.
function swapExactIn(
    address tokenIn, address tokenOut, uint256 amountIn,
    uint256 minAmountOut,      // считается офчейн из котировки и допуска пользователя
    uint256 deadline           // подпись/намерение протухает
) external returns (uint256 received) {
    require(block.timestamp <= deadline, "expired");
    require(minAmountOut > 0, "no slippage protection");   // ноль здесь — дверь для сэндвича
    IERC20(tokenIn).safeTransferFrom(msg.sender, address(this), amountIn);
    IERC20(tokenIn).forceApprove(router, amountIn);        // ноль-затем-значение для странных токенов

    uint256 before = IERC20(tokenOut).balanceOf(address(this));
    IRouter(router).swap(tokenIn, tokenOut, amountIn, minAmountOut, address(this));
    received = IERC20(tokenOut).balanceOf(address(this)) - before;  // факт, а не возвращённое значение
    require(received >= minAmountOut, "slippage");         // повторная проверка у себя
    IERC20(tokenOut).safeTransfer(msg.sender, received);
}

Чек-лист интеграции:

  1. minAmountOut и deadline обязательны. Своп без ограничения снизу — это приглашение к сэндвич-атаке; без дедлайна транзакция может исполниться через сутки по чужой цене.
  2. Проверяйте фактическое изменение баланса, а не возвращаемое значение. Токены с комиссией на перевод, ребейзящие токены и нестандартные реализации — норма жизни.
  3. Никогда не берите цену из спота пула для решений о залоге и ликвидации. Оракул с проверкой возраста ответа или TWAP.
  4. Считайте decimals явно. USDC — 6 знаков, WBTC — 8, ETH — 18. Половина ошибок в расчёте позиций живёт здесь.
  5. Округляйте в пользу протокола. Правило: выдаём — вниз, начисляем долг — вверх. Иначе получается бесплатная эмиссия по копейке за вызов.
  6. Не давайте бесконечный approve собственным контрактом. Разрешение живёт дольше вашей уверенности в чужом коде.
  7. Фиксируйте инварианты в тестах и фаззьте их («после любой последовательности операций протокол не должен потерять активы») и прогоняйте сценарии на форке мейннета с реальной ликвидностью, включая «цена ушла на 30 % за блок» — механика invariant-тестов Foundry разобрана в статье о контрактах.
  8. Читайте параметры риска чужого протокола как код: потолки, пороги, кто их меняет и с какой задержкой.
  9. Заложите деградацию: что делает ваше приложение, когда оракул устарел, ликвидность иссякла, а стейблкоин отклонился на 5 %. Ответ «ничего не делает и не даёт открывать новые позиции» — совершенно нормальный.

11. Частые заблуждения

  • «Пул — это биржа со стаканом». Нет: это формула. Цена в нём вторична и всегда отстаёт от внешнего рынка ровно настолько, насколько арбитражёру невыгодно её выравнивать.
  • «Непостоянные потери временные». Они исчезают только при возврате цены; позиция, закрытая после тренда, фиксирует их окончательно.
  • «Флеш-займ — это уязвимость». Это конструкция. Уязвимость — использование мгновенных величин как источника истины. Кредита без залога в цепи нет и не может быть без внешней идентичности заёмщика.
  • «Стейблкоин стабилен». Стабильно только обещание, и у каждого обещания есть автор с рисками.
  • «APY 400 % — это доход». Чаще всего это эмиссия, то есть ваша будущая доля, выданная вам же сегодня.
  • «Аудит был — значит безопасно». Композиция создаёт риски, которых не было ни в одном из аудированных контрактов по отдельности.

Мини-итог

  1. AMM заменяет книгу заявок формулой, потому что в EVM хранение и отмена заявок стоят газа, а мемпул публичен. Цена в пуле — отношение резервов, и ничего больше.
  2. Торгуя долю $\alpha$ резерва, вы теряете примерно $\alpha$ на проскальзывании и двигаете цену на $2\alpha$. Это единственная формула, которую нужно помнить при оценке исполнения.
  3. Поставщик ликвидности продаёт волатильность: его отставание от простого удержания равно $2\sqrt{r}/(1+r) - 1$ и покрывается только оборотом.
  4. Кредитование в цепи — ломбард: избыточный залог, ставка от утилизации, ленивая индексная бухгалтерия, ликвидация посторонним ботом за премию. Health factor ниже единицы — это не предупреждение, а открытый аукцион на вашу позицию.
  5. Флеш-займ возможен из-за атомарности транзакции и стоит ровно столько, сколько газ. Значит, мгновенные величины (спот, баланс, вес голоса) нельзя использовать как факты.
  6. Стейблкоин — это чьё-то обещание: эмитента, залоговой механики или рынка. Третий вариант исторически не выдержал ни одной паники.
  7. Композиция даёт бесплатную интеграцию и бесплатное же распространение чужих аварий. Инварианты, которые вы не проверяете у себя, вам никто не гарантирует.

Источники

Что дальше

Ончейн-управление: DAO, голоса, таймлоки и захват протокола — разбираем последний слой, который в этой статье всплывал постоянно: кто меняет пороги ликвидации, кто может обновить контракт, почему вес голоса берут из прошлого блока и как одна транзакция с флеш-займом уносит казну протокола, не взломав ни одной строчки кода.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов