May 24, 2022

Погружение в UniSwap

Перед прочтением необходимо посмотреть данное видео и полностью понять все, что там говорят - ТЫК

Работать будем с данной репой - ТЫК

А так же необходимо максимально много потыкать кнопки на самом UniSwap

Ну и вот вам бонусом документация UniSwap - ТЫК

Эта мини схемка может понадобиться в течении всей статьи, чтобы понимать чем мы вообще занимаемся

P.s.:

Данная статья была написана исключительно как конспект с разбором кода гитхаба uniswap v3, все писалось параллельно изучению самой технологии, из-за этого могут быть некоторые неточности и грубости

Factory

Тут создаются пулы

Тут нам передаются контракты двух монет, между которыми будет образована пара, а так же комиссия, которая будет установлена в данной паре. Функция возвращает нам адрес нового пула, который мы сформировали

  • Первым делом функция проверяет, что наши токены разные
  • После она вычисляет тот, у которого адрес является меньшим числом и называет меньший адрес token0, а больший token1
  • Теперь она проверяет, что token0 не нулевой (token1 понятно проверять не надо)
  • Мы присваиваем переменной tickSpacing значение, хранящиеся в mapping feeAmountTickSpacing по адресу fee
  • Проверяем чтобы tickSpacing было не нулевым
  • Проверяем, что пула с нашими токенами и нашими комиссиями не существует (трехмерный массив getPool возвращает адрес пула по заданным данным)
  • Делаем вызов функции deploy передавая туда адрес самой фабрики, оба токена, комиссию и tickSpacing и после передаем возвращаемые данные (адрес нового пула) в переменную pool, которая после передается трехмерному массиву getPool с обеими позициями токенов, тем самым сохраняя его, чтобы больше такого пула никто не мог сделать

----------------------------------------------------------------------------------

feeAmountTickSpacing

Хранит в себе расстоянии между тиками, которое соответствует каждому определенному значению комиссии (изначально заданы в конструкторе, но так же могут быть кастомные расстояния, которые задаются овнером контракта отдельно для каждого значения комиссии)

Deploy

Мы передаем в данную функцию адрес фабрики, оба токена, комиссию и tickSpacing, а на выходе получаем адрес нового пула. В самой функции мы создаем структуру Parameters, после создаем новый контракт по конструктору UniswapV3Pool и передаем адрес этого контракта в переменную pool (которую и возвращаем), после мы возвращаем parameters к начальным значениям при помощь delete, тем самым экономим газ

Tick

Для достижения концентрированной ликвидности некогда непрерывный спектр ценового пространства был разделен тиками

Тики — это границы между областями в ценовом пространстве. Тики расположены таким образом, что увеличение или уменьшение на 1 тик представляет собой увеличение или уменьшение цены на 0,01% (1 базисный пункт) в любой точке ценового пространства

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

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

Хотя каждый пул имеет одинаковое количество базовых тиков, на практике только часть из них может служить активными тиками. Из-за характера смарт-контрактов v3 расстояние между тиками напрямую связано с комиссией за своп. Более низкие уровни комиссионных обеспечивают более близкие потенциально активные тики, а более высокие комиссионные позволяют относительно более широкий интервал между потенциальными активными тиками

В то время как неактивные тики не влияют на стоимость транзакции во время свопов, пересечение активного тика увеличивает стоимость транзакции, в которой он пересекается, поскольку пересечение тика активирует ликвидность в любых новых позициях, используя данный тик в качестве границы

В областях, где эффективность капитала имеет первостепенное значение, таких как пары стабильных монет, более узкое расстояние между тиками увеличивает детализацию предоставления ликвидности и, вероятно, снизит влияние на цену при обмене, в результате чего цены на свопы со стабильными монетами значительно улучшатся

----------------------------------------------------------------------------------

Pool

Та штука, которую создает Factory, но из чего она состоит сама?

ВАЖНО! Pool - полноценный смартконтракт, и Factory создает именно полноценный смартконтракт из входных переменных

В библиотеке Position хранится вся инфа по каждой конкретной позиции, то есть по каждому случаю внесению ликвидности в пул

Mint

Данная функция отвечает за добавление ликвидности в пул

На вход мы даем следующий набор переменных:

  • recipient - адрес пула, в который му будем добавлять ликву
  • tickLower - нижний тик
  • tickUpper - верхний тик
  • amount - количество ликвидности, которую мы добавляем
  • data - любые данные, которые должны быть переданы callback

На выходе функция выдает нам:

  • amount0 - сумма token0, которая была заплачена за добавления данного количества ликвидности. Соответствует значению в callback
  • amount1 - количество token1, которое было заплачено за добавления данного количества ликвидности. Соответствует значению в callback

Что делает сама функция?

  • Проверяет что мы добавляем хоть какую-то ликвидность, а не ноль
  • После мы определяем в каких пропорциях будут внесены токены (это происходит в функции _modifyPosition, а ModifyPositionParams нужно для приведения к нужному типу данных)
  • После мы присваиваем полученные из _modifyPosition значения тем, что нам должна вернуть основная функция mint (amount0 и amount1)
  • В конце мы обновляем значения о запасах монет в пулах

----------------------------------------------------------------------------------

_modifyPosition

Эта функция считает, в каких пропорциях мы должны внести монеты для нашего пула

На вход этой функции мы даем структуру ModifyPositionParams, а выход она нам выкидывает ?позицию?, amount0 и amount1

Что делает функция:

  • Проверяет что тики находятся в дозволенных границах
  • Создаем локальную переменную _slot0 типа Slot0 для экономии газа
  • Присваиваем переменной position выход функции _updatePosition
  • params.liquidityDelta проверяет, чтобы в ликвидности были изменения и в этом случае:

Сравнивает текущий тик и границы установленных тиков, и если

Текущий тик ниже наших границ наших тиков, то мы предоставляем больше token0 ведь он становится более ценным

Текущий тик находится в границах наших тиков, то мы предоставляем равное количество монет

Текущий тик находится выше границ наших тиков, то мы предоставляем больше token1 ведь он становится более ценным

-----------------------------------------

_updatePosition

Опираясь на входные данные возвращает такую структуру:

Slot0

-----------------------------------------

ModifyPositionParams

- -----------------------------------------------------------------------------------

Collect

Данная функция позволяет нам выводить комиссии из пула

На вход мы даем следующий набор переменных:

  • recipient - адрес пула, из которого мы будем забирать ликву
  • tickLower - нижний тик
  • tickUpper - верхний тик
  • amount0Requested - сколько token0 следует вывести из комиссий
  • amount1Requested - сколько token1 следует вывести из комиссий

На выходе функция выдает нам:

  • amount0 - сумма комиссий, собранных в token0
  • amount1 - сумма комиссий, собранных в token1

Что делает сама функция?

  • Первым делом мы получаем изначальную позицию исходя из msg.sender, а так же верхнего и нижнего тика
  • После мы получаем количество ликвы, которую можно вывести из каждой монеты пары (опираясь на комиссионные)
  • В конце мы вычитаем монеты которые вывели из нашей общей позиции

Swap

Данная функция позволяет нам обменивать монеты

На вход мы даем следующий набор переменных:

  • recipient - адрес для получения средств со свапа
  • zeroForOne - направление обмена, true для token0 -> token1, false для token1-> token0
  • amountSpecified - Сумма свапа, которая неявно настраивает свап как точный ввод (положительный) или точный вывод (отрицательный).
  • sqrtPriceLimitX96 - Ценовой предел Q64.96 (число с точно определенной запятой, где 64 бита отвечают за целую часть, а 96 за дробную) sqrt. Если ноль за единицу, цена не может быть меньше этого значения после свапа. Если единица за ноль, цена не может быть больше этого значения после свапа
  • data - любые данные, которые должны быть переданы callback

На выходе функция выдает нам:

  • amount0 - дельта баланса token0 пула, точная при отрицательном, минимальная при положительном
  • amount1 - дельта баланса token1 пула, точная при отрицательном, минимальная при положительном

Что делает сама функция?

  • Проверяет что свап не нулевой
  • Создает локальную переменную slot0Start типо Slot0, в которую передаем slot0, и проверяет чтобы пул был активен
  • Проверяем в каком направлении будет выполнен свап (token0 -> token1 или token1-> token0)
  • В структуру slot0 записываем false в поле "unlocked", чтобы на время свапа заблокировать пул
  • Создаем переменную cache типа SwapCache и передаем в нее ликвидность в паре, timestamp блока, комсу для input монеты, так же обнуляется значение tickCumulatives (совокупность значений тиков за каждую секунду от timestamp блока) и secondsPerLiquidityCumulativeX128s (суммарное количество секунд на единицу ликвидности в диапазоне за каждую секунду от timestamp блока), а в конце присваиваем false в computedLatestObservation (true, если мы смогли посчитать два вышеупомянутых значения, false в противном случае)
  • Теперь мы окончательно присваиваем в exactInput ту монету, которая будет нами отдана в свап
  • Создаем переменную state типа SwapState и передаем в нее обьем нашего свапа, сколько мы уже свапнули (поэтому 0), текущая цена в паре, тик цены, комса по input токену, количество input токена, которое мы уплатили как комсу (в начале как 0 устанавливается), ликвидность в тике на данный момент
  • Теперь мы начинаем производить условный свап до тех пор, пока не свапнем всю сумму полностью
  • Инициализируем пустую структуру Step типа StepComputations
  • Передаем в эту переменную Step в поле sqrtPriceStartX96 значение из state из поля sqrtPriceX96
  • После вызываем функцию nextInitializedTickWithinOneWord библиотеки tickBitmap и передаем туда значения state.tick (нынешний тик), tickSpacing (расстояние между тиками), zeroForOne, а возвращает два значения, а именно следующий тик, и инициализирован ли он
  • Убеждаемся, что мы не превышаем минимальный/максимальный tick, так как bitmap tick не знает об этих границах и передаем результат (следующий тик) в структуру step в поле tickNext
  • После в структуру step в поле sqrtPriceNextX96 присваиваем цену следующего тика
  • Вычисляет значение для перехода к целевому тику, ценовому пределу или точке, где сумма ввода/вывода исчерпана, делаем это все через функцию данной библиотеки. На выход получаем 4 значения и записываем их в state.sqrtPriceX96 (текущая цена в паре), step.amountIn (обьем свапа который надо произвести на данном этапе), step.amountOut (обьем свапа, который удалось произвести на данном этапе), step.feeAmount (сколько комсы было заплачено). В конце мы считаем в какой из какой менты в пуле надо вычесть обьем свапа, а к какой прибавить
  • Если комса на пуле включена, подсчитывает размер комсы, уменьшает step.feeAmount и увеличивает state.protocolFee
  • После мы обновляем комиссию в пуле путем вызова функции FullMath.mulDiv
  • Далее идет большой блок, который можно описать как "сдвиг тика, если мы достигли следующей цены"
  • Далее мы просто обновляем тик и записываем результат от оракула (про него думаю отдельная статья будет), если тик изменился, в ином случае просто обновляем цену, приравнивая ее к state.sqrtPriceX96
  • После обновляем ликвидность, если она изменилась
  • Теперь меняем комиссию в пуле по монетам, в зависимости от того, в какую сторону у нас был свап
  • И наконец мы делаем сам трансфер с вызовом всем нам знакомомй функции safeTransfer и изменением балансов токенов в пуле
  • И в конце снова разблокируем пул!

- -----------------------------------------------------------------------------------

SwapCache

SwapState

StepComputations

tickBitmap.nextInitializedTickWithinOneWord

- -----------------------------------------------------------------------------------

Надеюсь статья была интересной и понятной!

Мой телеграмм канал - https://t.me/ortomich_crypto