Как стать автором
Обновить

Uniswap v3: самые неочевидные моменты логики

Уровень сложностиСредний
Время на прочтение8 мин
Количество просмотров2.8K

В прошлой статье мы уже подробно разбирали принцип работы Uniswap v3.

В этой статье заострим внимание на самых неочевидных моментах логики этого автоматического маркет мейкера

Структура этой статьи

  • Вспоминаем основы математики Uniswap v3

  • Как работают кросс-тик свопы (свопы, при которых изменяются ценовые "тики")

  • Как использовать совокупную ликвидность от разных LP-позиций лучше чем за O(n)? И как это связано с тиками?

  • Допустим, у пуле открыто 1 миллиард разных Uniswap V3 LP-позиций. Как будет выглядеть суммарный график y(x) по всему ценовому пространству?

  • Доказать, что суммарный график y(x) будет непрерывно-дифференцируемой функцией

Основы математики

Первый шаг: whitepaper Uinswap V3 и ключевые вещи. Обменный курс между token0 и token1 определяется как цена. Весь ценовой диапазон разделён на так называемые тики (ticks), где каждый тик представляет собой целую степень числа 1,0001. Цена для i-го тика определяется следующим образом:

Таким образом, каждый тик i (со знаком) соответствует изменению цены на 0,01% (1 базисный пункт) относительно тика с i = 0. Цена может двигаться выше и ниже «1,0» небольшими шагами по 0,01% в пределах целочисленного пространства (-∞, +∞).

Небольшое отступление

В своём телеграм канале делюсь ещё больше полезным контентом по сфере децентрализованных финансов: https://t.me/kirrya_achieves

Ликвидность пула связана с параметром sqrtPrice, который определяется следующим образом:

где x и yвиртуальные (не реальные, это важно) резервы token0 и token1 соответственно.

Пул Uniswap V3 состоит из множества «маленьких» swap-пулов, каждый из которых соответствует отдельному тику. Каждый такой «маленький» пул ведёт себя как Uniswap V2 с постоянным произведением резервов (x \cdot y = const), но с важным отличием — запасы токенов в конкретном тике ограничены. У каждого тика есть максимальная и минимальная цена обмена, а также ограниченное количество token0 и token1.

Когда запасы token0 и token1 в тике полностью исчерпаны, ликвидность этого тика падает до нуля. Таким образом, в Uniswap V3 уравнение постоянного произведения немного отличается и выглядит следующим образом:

(где Pa и Pb - нижняя и верхняя границы ценового диапазона для данного тика.

График этой функции представляет собой гиперболу, однако, в отличие от Uniswap V2, ликвидность пула может закончиться (когда доходим до оси абсцисс или ординат)

Если резервы токенов в текущем тике закончились, а обмен еще не завершен (не достигнуто желаемое количество токенов или цена), Uniswap V3 переходит к следующему активному тику и выполняет следующий «прыжок» (hop), продолжая своп. Этот процесс повторяется до тех пор, пока не будет получено нужное количество токенов или не будет достигнута целевая цена обмена.

Кросс-тик свопы

Рассмотрим кросс-тик своп на упрощенной схеме:

https://mixbytes.io/blog/uniswap-v3-ticks-dive-into-concentrated-liquidity

Начальное состояние пула: текущая цена (тик) определяет, где начинается своп (зелёный цвет). Пользователь передаёт token0 (синий) и получает token1 (жёлтый).

Если объем свопа невелик и цена не выходит за пределы P_a или P_b (нижней и верхней границы текущего тика), то обмен происходит по формуле постоянного произведения (виртуальных балансов)

Если же объем свопа превышает текущий тик:

  1. При обмене token0 → token1 (движение вправо на схеме) резервы token1 (желтый) в текущем тике полностью исчерпываются, оставляя только token0 (синий).

  2. Затем текущий тик переходит на следующий тик, где есть резервы token1 (тик "i+n" на схеме).

  3. Своп продолжается до тех пор, пока не будет достигнут нужный объем токенов или цена.

  4. В результате все тики слева от текущего тика содержат только token0, а тики справатолько token1.

Свопы в обратном направлении (token1 → token0) работают аналогично, но в зеркальном порядке: токены перемещаются влево, и текущий тик обновляется при исчерпании token0.

Параметр n на схеме (шага между тиками) связан с параметром tickSpacing пула и будет рассмотрен позже.

Прямое внедрение этой логики с индивидуальными резервами для каждого тика, обновлениями при каждом свопе и изменении ликвидности, а также расчётами комиссий в стиле Uniswap V2 привело бы к значительному количеству переменных и высоким затратам на газ.

Поэтому необходим математический подход, позволяющий удешевить кросс-тик-свопы, расчёты ликвидности и комиссий. Наша цель — добиться практически постоянных затрат на газ и минимального использования хранилища. Эти требования стали основной причиной использования ликвидности (L и ΔL) и sqrtPrice (√P и Δ√P) вместо прямых объёмов токенов, как это было в Uniswap V2.

Первый важный аспект использования единого значения ликвидности (L) и квадратного корня цены (√P) заключается в том, что при свопах и добавлении/удалении ликвидности изменяется только одно из этих значений. √P изменяется при свопе внутри тика, а L изменяется при пересечении тика или изменении ликвидности (mint/burn).

Кроме того, использование √P вместо P позволяет избежать необходимости вычисления квадратного корня sqrt(), который не имеет постоянной стоимости газа. В Uniswap V2 sqrt() вычисляется методом Вавилона, что делает его затраты на газ переменными. В Uniswap V3 вычисления sqrt() вообще отсутствуют — вместо этого используются предварительно рассчитанные значения в TickMath.sol (здесь).

Использование √P и L позволяет легко вычислить количество получаемых токенов при свопе и изменение √P по следующим простым формулам:

 Для token1
Для token1 (Δy)

и

 Для token0
Для token0 (Δx)

Эти вычисления реализованы в функциях getAmount0Delta() и getAmount1Delta().

Следующий важный аспект реализации Uniswap V3 — это tickSpacing. Этот параметр определяет «шаг» между тиками, в которых допускается наличие ликвидности. Например, если tickSpacing установлен на 60, то ликвидность может существовать только в тиках 0, 60, -60, 120, -120 и так далее.

tickSpacing дает создателям пулов возможность выбора: они могут сделать шаг между тиками меньше, получив более «плотный» пул (более детализированные ценовые шаги, но более дорогие кросс-тик свопы, так как своп проходит через большее количество тиков), или сделать пул более «разреженным», где ценовые шаги будут крупнее (меньше пересечений тиков — меньше затрат на газ, но и меньшая точность для поставщиков ликвидности).

Как использовать совокупную ликвидность от разных LP-позиций? И как это связано с тиками?

В Uniswap V3 ликвидность предоставляется путём создания «позиций», принадлежащих пользователям. Этот механизм отличается от Uniswap V2, где владение ликвидностью реализовано через токены LP стандарта ERC-20. В Uniswap V3 «позиция» представляет собой объект, принадлежащий пользователю, содержащий определённое количество ликвидности и диапазон тиков (от tickLower до tickUpper). Как видно, операции mint и burn очень похожи: при добавлении ликвидности (mint) она прибавляется к позиции, а при удалении (burn) — вычитается.

Можно задаться вопросом – а всё же зачем нужны тики? Почему мы без них не можем?

С одной стороны, мы хотим чтобы пользователи могли предоставлять ликвидность в произвольном диапазоне цен. С другой стороны, мы хотим чтобы каждый своп стоит константное количество газа (другими словами, сложность свопа чтобы была O(1)).

С другой стороны, если допустить произвольные интервалы цен для предоставления ликвидности, значит количество точек где изменяется значения суммарной ликвидности, будет расти как O(n), n - количество ЛП позиций.

Поэтому, если мы ограничим количество тиков (в uniswap v3 их примерно 1.4 млн), сложность свопа не будет зависеть от кол-ва ЛП позиций, а ограничена сверху количеством тиков, которых константное число.

Теперь рассмотрим процесс создания и удаления позиции в функции _updatePosition(). Она принимает tickLower, tickUpper, количество ликвидности, которое нужно добавить или убрать (liquidityDelta), а также текущий тик пула.

Эффективность математической модели Uniswap V3 хорошо заметна в этой части _updatePosition(). Можно предположить, что поставщики ликвидности (LP) должны распределять ликвидность по ВСЕМ тикам в диапазоне от tickLower до tickUpper (с учётом tickSpacing), но в коде мы видим только два обновления:

flippedLower = ticks.update(
     tickLower,
     ...
);
flippedUpper = ticks.update(
     tickUpper,
     ...
     );

То есть обновляются только нижний и верхний тики. Это эффективно, но как же тогда собираются комиссии со ВСЕХ тиков внутри этого диапазона без хранения значений ликвидности в каждом тике? Сначала разберемся с комиссиями.

Как начислять комиссии за каждую сделку одновременно для всех LP-позиций?

Ключевая концепция системы комиссий Uniswap V3 заключается в отслеживании «внешнего» (outside) объема комиссий «выше» и «ниже» тика и сохранении этих значений внутри каждого тика. Эти комиссии рассчитываются по следующим формулам из вайтпеппера:

(где f_o(i) - значение feeGrowthOutside, хранящееся по i-му тику, f_g - общая сумма накопленных комиссий в пуле)

Важно: считаем комиссии в размере на единицу ликвидности

При этом в реализации используется только ОДНО значение для отслеживания этих объемов комиссий — feeGrowthOutside, хранящееся внутри тика (отдельно для token0 и token1).

Применяя вышеуказанные формулы, значение feeGrowthOutside для заданного тика (i) равно:

  • Кумулятивной сумме комиссий НИЖЕ тика i, если он находится ЛЕВЕЕ i_c (текущего тика цены)

  • Кумулятивной сумме комиссий ВЫШЕ тика i, если он находится СПРАВА ic

Где ic – текущий тик цены, i – рассматриваемый тик.

Примеры:

Если i ≤ ic мы движемся вправо, то feeGrowthOutside хранит «левую» часть комиссий:

https://mixbytes.io/blog/uniswap-v3-ticks-dive-into-concentrated-liquidity

Если i > i_c мы пересекли текущий ценовой тик, и теперь feeGrowthOutside хранит «правую» часть комиссий:

https://mixbytes.io/blog/uniswap-v3-ticks-dive-into-concentrated-liquidity

Поскольку тики пересекаются последовательно, при каждом пересечении feeGrowthOutside обновляется, отражая либо «левую», либо «правую» часть глобальных комиссий.

Благодаря такому подходу расчёт комиссий, относящихся к заданному диапазону (i_l — нижний тик, i_u — верхний тик), становится простым:

Мы просто вычитаем «комиссии ниже нижнего тика» и «комиссии выше верхнего тика» из общего пула комиссий. Кроме того, важно помнить, что feesGrowth измеряется в «токенах на единицу ликвидности», поэтому расчёт конечного объёма комиссий также упрощается.

Чтобы определить комиссии, относящиеся к позиции пользователя, сначала удаляется ликвидность (remove liquidity), а затем выполняются вычисления в ticks.getFeeGrowthInside(), используя описанные выше формулы:

if (tickCurrent >= tickLower) {
        feeGrowthBelow0X128 = lower.feeGrowthOutside0X128;
        feeGrowthBelow1X128 = lower.feeGrowthOutside1X128;
    } else {
        feeGrowthBelow0X128 = feeGrowthGlobal0X128 - lower.feeGrowthOutside0X128;
        feeGrowthBelow1X128 = feeGrowthGlobal1X128 - lower.feeGrowthOutside1X128;
    }

Таким образом, для расчёта собранных комиссий внутри диапазона тиков достаточно считать состояние только двух тиков и глобальное состояние пула, что делает процесс чрезвычайно эффективным.

Возможно, вы заметили, что нигде в коде не накладываются ограничения на tickSpacing, и кажется, что поставщики ликвидности могут устанавливать tickLower и tickUpper на любых значениях. Однако эта проверка выполняется внутри функции flipTick(), которая включает и отключает тики.

Допустим, у пуле открыт 1 миллиард разных Uniswap V3 LP-позиций. Как будет выглядеть суммарный график y(x) по всему ценовому пространству?
Доказать, что суммарный график будет непрерывно-дифференцируемой функцией

Вспомним, что на каждом ценовом тике у нас просто гипербола, смещенная влево и вниз. Причем коэффициенты смещения зависят от значения самой ликвидности L.

Вспомним также, что цена в Uniswap v3 гиперболе – это просто производная (касательная).

Как определяется склеивание двух гипербол на соседних тиках? Оно определяется так, что нижняя цена верхней гиперболы совпадает с верхней ценой нижней гиперболы. А это значит что цены справа и слева совпадают. Цена - это производная к нашему суммарному графику, и пределы справа и слева совпадают

Таким образом, несмотря на всю сложность конструкции Uniswap v3 LP позиций, суммарный график будет гладкой (непрерывно-дифференцируемой) кривой.

Надеюсь, что статья для вас была интересной и полезной. Буду рад прочитать ваши комментарии

В своём телеграм канале делюсь ещё больше полезным контентом по сфере децентрализованных финансов:

https://t.me/kirrya_achieves

References:

https://mixbytes.io/blog/uniswap-v3-ticks-dive-into-concentrated-liquidity

Теги:
Хабы:
Всего голосов 7: ↑7 и ↓0+12
Комментарии0

Публикации

Работа

Ближайшие события