Разбор Uniswap V2: mint() & burn()
По своей сути, Юнисвап это обычный пул для токенов, а соответственно тут могла бы быть проблема первого депозитора / минтера, как это представлено в ERC4626. Тем не менее, команда протокола постаралась защитить пул и указала обязательное количество для первого минта:
Вообще, первый минт в пуле происходит интересным способом расчета:
Uniswap V2 берет квадратный корень из произведения количества предоставленных токенов, чтобы рассчитать количество акций LP для минтера.
Казалось бы, можно напечатать произвольное количество токенов для первого LP - они владеют 100% акций, так что какая разница, масштабировать их на 0,01 или 100?
Вот что написано в официальной документации:
Uniswap v2 изначально чеканит доли, равные среднему геометрическому от сумм, ликвидность = sqrt(xy). Эта формула гарантирует, что стоимость доли пула ликвидности в любой момент времени по существу не зависит от соотношения, при котором ликвидность была первоначально внесена... Вышеприведенная формула гарантирует, что доля пула ликвидности никогда не будет стоить меньше, чем среднее геометрическое значение резервов в этом пуле.
P.S. Мне потребовалось некоторое время, чтобы это правильно перевести и еще больше времени, чтобы осмыслить (я не силен в математике и их квадратных корнях...).
Попробуем разобрать на примере.
Давайте представим, что мы не будем рассчитывать сумму при помощи квадратного корня и начнем с 10 токенами 1 и 2 в пуле. После некоторых действий там появится 20 токенов обоих видов.
Как вы думаете, удвоилась ли ликвидность или увеличилась в четыре раза? Ведь если не извлекать квадратный корень, то ликвидность начиналась бы со 100 (10 × 10), а закончилась бы 400 (20 × 20), т.е. фактически увеличилась в 4 раза.
Тем не менее, сначала максимальное количество токенов_1, которое вы могли получить, было (асимптотически) 100, но после роста ликвидности "глубина" ликвидности для этого токена удвоилась, а не увеличилась в четыре раза.
Но какое это имеет значение, если будущие поставщики ликвидности не вычисляют ликвидность с помощью квадратного корня при минте или сжигании?
Мы видели, что и первые "вынуждены" поставлять активы по текущему курсу, и вторые могут выкупать их по текущему курсу - никаких квадратных корней здесь нет.
Ответ кроется в том, как Uniswap собирала бы комиссию с LP, если бы решила это делать однажды.
Возвращаясь к нашему предыдущему примеру, когда пул вырос со 100 токенов0 и 100 токенов1 до 200 каждого, прибыль поставщика ликвидности составляет 100 %, поэтому он должен платить комиссию, пропорциональную этой сумме. Если бы мы измерили размер пула со 100 до 400, то им пришлось бы платить комиссию с четырехкратной прибыли.
Uniswap предпочитает взимать комиссию во время сжигания ликвидности, потому что взимание комиссии во время свопа увеличило бы стоимость газа. А это самая популярная опция!
Тем не менее, нужно отметить, что Uniswap V2 никогда на самом деле не включала комиссии за ликвидность, так что это исключительно теоретический пример.
#uniswap #v2 #mint #fee
По своей сути, Юнисвап это обычный пул для токенов, а соответственно тут могла бы быть проблема первого депозитора / минтера, как это представлено в ERC4626. Тем не менее, команда протокола постаралась защитить пул и указала обязательное количество для первого минта:
uint public constant MINIMUM_LIQUIDITY = 10**3;
Вообще, первый минт в пуле происходит интересным способом расчета:
if (_totalSupply == 0) {
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
mint(address(0), MINIMUM_LIQUIDITY);
}Uniswap V2 берет квадратный корень из произведения количества предоставленных токенов, чтобы рассчитать количество акций LP для минтера.
Казалось бы, можно напечатать произвольное количество токенов для первого LP - они владеют 100% акций, так что какая разница, масштабировать их на 0,01 или 100?
Вот что написано в официальной документации:
Uniswap v2 изначально чеканит доли, равные среднему геометрическому от сумм, ликвидность = sqrt(xy). Эта формула гарантирует, что стоимость доли пула ликвидности в любой момент времени по существу не зависит от соотношения, при котором ликвидность была первоначально внесена... Вышеприведенная формула гарантирует, что доля пула ликвидности никогда не будет стоить меньше, чем среднее геометрическое значение резервов в этом пуле.
P.S. Мне потребовалось некоторое время, чтобы это правильно перевести и еще больше времени, чтобы осмыслить (я не силен в математике и их квадратных корнях...).
Попробуем разобрать на примере.
Давайте представим, что мы не будем рассчитывать сумму при помощи квадратного корня и начнем с 10 токенами 1 и 2 в пуле. После некоторых действий там появится 20 токенов обоих видов.
Как вы думаете, удвоилась ли ликвидность или увеличилась в четыре раза? Ведь если не извлекать квадратный корень, то ликвидность начиналась бы со 100 (10 × 10), а закончилась бы 400 (20 × 20), т.е. фактически увеличилась в 4 раза.
Тем не менее, сначала максимальное количество токенов_1, которое вы могли получить, было (асимптотически) 100, но после роста ликвидности "глубина" ликвидности для этого токена удвоилась, а не увеличилась в четыре раза.
Но какое это имеет значение, если будущие поставщики ликвидности не вычисляют ликвидность с помощью квадратного корня при минте или сжигании?
Мы видели, что и первые "вынуждены" поставлять активы по текущему курсу, и вторые могут выкупать их по текущему курсу - никаких квадратных корней здесь нет.
Ответ кроется в том, как Uniswap собирала бы комиссию с LP, если бы решила это делать однажды.
Возвращаясь к нашему предыдущему примеру, когда пул вырос со 100 токенов0 и 100 токенов1 до 200 каждого, прибыль поставщика ликвидности составляет 100 %, поэтому он должен платить комиссию, пропорциональную этой сумме. Если бы мы измерили размер пула со 100 до 400, то им пришлось бы платить комиссию с четырехкратной прибыли.
Uniswap предпочитает взимать комиссию во время сжигания ликвидности, потому что взимание комиссии во время свопа увеличило бы стоимость газа. А это самая популярная опция!
Тем не менее, нужно отметить, что Uniswap V2 никогда на самом деле не включала комиссии за ликвидность, так что это исключительно теоретический пример.
#uniswap #v2 #mint #fee
👍5❤2
Foundry: организация адресов для тестов
Интересный способ для организации и создания пользовательских адресов для тестов были реализованы в контрактах от Sablier. Давайте посмотрим на нее внимательнее.
Для начала у нас есть структура с адресами необходимых нам пользователей, в которую позже будет удобно добавлять новые адреса:
В самих тестах мы определяем эту структуру в переменную:
Users internal users;
И уже в setUp() мы инициализируем эти адреса:
Сама функция createUser() тоже не совсем проста, там сразу начисляются токены:
Таким образом, мы можем обращаться в тестах к нужному пользователю через users., например, users.admin.
Достаточно простая и удобная система, как вы думаете?
Полный код теста можно посмотреть в репо Sablier.
#foundry #users #setup
Интересный способ для организации и создания пользовательских адресов для тестов были реализованы в контрактах от Sablier. Давайте посмотрим на нее внимательнее.
Для начала у нас есть структура с адресами необходимых нам пользователей, в которую позже будет удобно добавлять новые адреса:
struct Users {
// Default admin for all Sablier V2 contracts.
address payable admin;
// Impartial user.
address payable alice;
// Default stream broker.
address payable broker;
// Malicious user.
address payable eve;
// Default NFT operator.
address payable operator;
// Default stream recipient.
address payable recipient;
// Default stream sender.
}
В самих тестах мы определяем эту структуру в переменную:
Users internal users;
И уже в setUp() мы инициализируем эти адреса:
function setUp() public virtual {
users = Users({
admin: createUser("Admin"),
alice: createUser("Alice"),
broker: createUser("Broker"),
eve: createUser("Eve"),
operator: createUser("Operator"),
recipient: createUser("Recipient"),
sender: createUser("Sender")
});
}
Сама функция createUser() тоже не совсем проста, там сразу начисляются токены:
function createUser(string memory name) internal returns (address payable) {
address payable user = payable(makeAddr(name));
vm.deal({ account: user, newBalance: 100 ether });
deal({ token: address(dai), to: user, give: 1_000_000e18 });
deal({ token: address(usdt), to: user, give: 1_000_000e18 });
return user;
}
Таким образом, мы можем обращаться в тестах к нужному пользователю через users., например, users.admin.
Достаточно простая и удобная система, как вы думаете?
Полный код теста можно посмотреть в репо Sablier.
#foundry #users #setup
👍9
Из сохранений по Lending and Borrowing
Сейчас идет небольшой загруз в делах, и не остается времени на написание разборов по Юни или другим Defi. Это все будет, только чуь позже.
Пока что, хочу поделиться парой видео на Ютуб по теме Lending and Borrowing, которые я однажды сохранил себе. В принципе, хоть и на английском, но достаточно просто и доступно все объясняют. Некоторые моменты могут быть в новинку тем, кто погружается в DeFi.
https://www.youtube.com/watch?v=xeST_tbc1O4&list=PLS01nW3RtgopKAIGCW92jePNHwTGk5GmK&index=2&ab_channel=Blockchain-Web3MOOCs
Всего 7 коротких видео по 10-15 минут.
Приятного просмотра!
#lending #borrowing #defi
Сейчас идет небольшой загруз в делах, и не остается времени на написание разборов по Юни или другим Defi. Это все будет, только чуь позже.
Пока что, хочу поделиться парой видео на Ютуб по теме Lending and Borrowing, которые я однажды сохранил себе. В принципе, хоть и на английском, но достаточно просто и доступно все объясняют. Некоторые моменты могут быть в новинку тем, кто погружается в DeFi.
https://www.youtube.com/watch?v=xeST_tbc1O4&list=PLS01nW3RtgopKAIGCW92jePNHwTGk5GmK&index=2&ab_channel=Blockchain-Web3MOOCs
Всего 7 коротких видео по 10-15 минут.
Приятного просмотра!
#lending #borrowing #defi
YouTube
Lecture 6.1 Lending and Borrowing
❤4🔥1
Разработчик смарт контрактов в 2024
На выходных была интересная дискуссия с коллегами по поводу навыков для обычного разработчика смарт контрактов в теперешнее время. Мы все в этой сфере около 2 лет (кто-то больше, кто-то меньше), поэтому могли оценить многие изменения на протяжении всего времени.
Хочу поделиться с вами некоторыми мыслями из нашего разговора.
Вот я сам начал путь в web3 в начале 2022 года. Тогда мой выбор стоял между Solidity для Ethereum и Rust, но последний постепенно сходил на нет для меня вместе с падением интереса сообщества к самому блокчейну Solana.
Я учился по видео на Ютуб, статьям, постам и т.д. На тот момент не было такого большого количества материалов и приходилось вникать в технические статьи для понимания работы EVM.
Проекты были не такими "навороченными" и чаще всего работали как соло протокол, без каких-либо интеграций. Конечно, были те, кто связывался с Юнисвапом или другими Defi, но их было куда меньше, чем сейчас.
Разработчики, хоть и старались писать безопасные контракты, но довольно часто допускали простые ошибки - вспомнить хотя бы все проблемы с transferFrom, которые помечались часто как High.
Был Hardhat и Truffle, а Foundry только создавался.
Этим я хочу сказать, что требования для разработчиков были минимальными: уметь писать на Solidity и писать общие тесты.
Сейчас же, глядя на вакансии и уровень развития web3, требования выросли в разы. Попробую назвать основные:
- хорошее знание Solidity;
- знание assembly / yul как огромный плюс;
- понимание работы обновляемых контрактов;
- умение писать просты тесты на Hardhat / Foundry;
- умение писать фазз тесты;
- понимание принципов инвариантов;
- понимание тестов формальной верификации;
- знание основных DeFi паттернов (lending. borrowing, etc);
- знание основных DeFi протоколов и их контрактов;
- умение интеграции DeFi протоколов в свой проект;
- понимание работы L2 сетей;
- понимание работы других сетей (zk, zerolayer, blast);
- знание основных уязвимостей контрактов;
- начальные знания аудирования;
И это все даже не уровень мидла!
Да, информации сейчас действительно много, есть куча курсов (в том числе бесплатных), но все это требует огромного количества времени!
В ру сегменте все также не много вакансий, поэтому все, кто немного научился писать и работать с СК, выходят на международный уровень и ищут работу там.
На мой взгляд, вместе с новой волной интереса к биткойну будет больше вакансий, поэтому вы вполне можете сейчас потихоньку начинать / продолжать свой обучение.
Этим постом просто держу в курсе тех, кто задумывался о своем старте в web3 и не особо понимает, на что "подписывается". Это сфера постоянного обучения, будьте готовы к этому.
#start
На выходных была интересная дискуссия с коллегами по поводу навыков для обычного разработчика смарт контрактов в теперешнее время. Мы все в этой сфере около 2 лет (кто-то больше, кто-то меньше), поэтому могли оценить многие изменения на протяжении всего времени.
Хочу поделиться с вами некоторыми мыслями из нашего разговора.
Вот я сам начал путь в web3 в начале 2022 года. Тогда мой выбор стоял между Solidity для Ethereum и Rust, но последний постепенно сходил на нет для меня вместе с падением интереса сообщества к самому блокчейну Solana.
Я учился по видео на Ютуб, статьям, постам и т.д. На тот момент не было такого большого количества материалов и приходилось вникать в технические статьи для понимания работы EVM.
Проекты были не такими "навороченными" и чаще всего работали как соло протокол, без каких-либо интеграций. Конечно, были те, кто связывался с Юнисвапом или другими Defi, но их было куда меньше, чем сейчас.
Разработчики, хоть и старались писать безопасные контракты, но довольно часто допускали простые ошибки - вспомнить хотя бы все проблемы с transferFrom, которые помечались часто как High.
Был Hardhat и Truffle, а Foundry только создавался.
Этим я хочу сказать, что требования для разработчиков были минимальными: уметь писать на Solidity и писать общие тесты.
Сейчас же, глядя на вакансии и уровень развития web3, требования выросли в разы. Попробую назвать основные:
- хорошее знание Solidity;
- знание assembly / yul как огромный плюс;
- понимание работы обновляемых контрактов;
- умение писать просты тесты на Hardhat / Foundry;
- умение писать фазз тесты;
- понимание принципов инвариантов;
- понимание тестов формальной верификации;
- знание основных DeFi паттернов (lending. borrowing, etc);
- знание основных DeFi протоколов и их контрактов;
- умение интеграции DeFi протоколов в свой проект;
- понимание работы L2 сетей;
- понимание работы других сетей (zk, zerolayer, blast);
- знание основных уязвимостей контрактов;
- начальные знания аудирования;
И это все даже не уровень мидла!
Да, информации сейчас действительно много, есть куча курсов (в том числе бесплатных), но все это требует огромного количества времени!
В ру сегменте все также не много вакансий, поэтому все, кто немного научился писать и работать с СК, выходят на международный уровень и ищут работу там.
На мой взгляд, вместе с новой волной интереса к биткойну будет больше вакансий, поэтому вы вполне можете сейчас потихоньку начинать / продолжать свой обучение.
Этим постом просто держу в курсе тех, кто задумывался о своем старте в web3 и не особо понимает, на что "подписывается". Это сфера постоянного обучения, будьте готовы к этому.
#start
❤12👍3🔥3🌚1
Закрытая группа для тех, кому интересен аудит
Где-то в января я делал пост на канале, в котором спрашивал у участников, кому интересна тема аудита смарт контрактов и участие в конкурсных аудитах на популярных платформах.
Тогда мы собрали достаточно большую группу, где я рассказал о том, как начать в конкурсных аудитах, как регистрироваться, как отправлять отчеты и т.д. Позже мне пришла идея организовать рабочую группу, в которой мы бы разбирали отчеты по найденным багам в прошедших аудитах. Группа была минимально платной, чтобы зашли в нее только те, кому действительно интересна эта тема.
И сейчас прошел первый месяц работы этой группы. За все это время мы:
1. Написали 39 постов с разборами багов в различных протоколах;
2. Сделали 8 заметок о том, как проводить аудит и на что обращать внимание;
3. Посмотрели 4 задачи для самопроверки;
4. Детально разобрали 1 конкурсный протокол - Spectra от Code4rena;
В следующем месяце мы планируем:
1. Продолжить разбирать баги с прошедших конкурсов;
2. Разобрать еще один небольшой конкурсный аудит (до 1000 строк);
И, будет кое-что новое. А именно разборы нюансов при интеграциях одного протокола в другой. Например, если прокол подключается к Uniswap, Gnosis, Seaport и другим, мы будем разбирать некоторые моменты, на которые следует обращать внимание в таких случаях.
Это такая инфа, которую будет сложно найти где-либо за пределами нашей группы.
Если вам интересно, можете присоединиться на следующий месяц.
Условия:
- Для тех, кто хочет просто читать - 1000 рублей;
- Для авторов - 500 на следующий месяц, если в этом вы написали минимум 5 постов;
Оплатить можно:
1. Написав в чат админу
P.S. В комментариях сделаю небольшой пример самого разбора бага по отчету в конкурсном аудите.
Как вы поняли, вы также можете писать свои заметки по багам и делиться интересными находками.
Группа создана специально, чтобы следить за тенденциями в уязвимостях и понимать, куда и как смотреть в коде, чтобы найти проблему.
Буду рад всем желающим!
#group
Где-то в января я делал пост на канале, в котором спрашивал у участников, кому интересна тема аудита смарт контрактов и участие в конкурсных аудитах на популярных платформах.
Тогда мы собрали достаточно большую группу, где я рассказал о том, как начать в конкурсных аудитах, как регистрироваться, как отправлять отчеты и т.д. Позже мне пришла идея организовать рабочую группу, в которой мы бы разбирали отчеты по найденным багам в прошедших аудитах. Группа была минимально платной, чтобы зашли в нее только те, кому действительно интересна эта тема.
И сейчас прошел первый месяц работы этой группы. За все это время мы:
1. Написали 39 постов с разборами багов в различных протоколах;
2. Сделали 8 заметок о том, как проводить аудит и на что обращать внимание;
3. Посмотрели 4 задачи для самопроверки;
4. Детально разобрали 1 конкурсный протокол - Spectra от Code4rena;
В следующем месяце мы планируем:
1. Продолжить разбирать баги с прошедших конкурсов;
2. Разобрать еще один небольшой конкурсный аудит (до 1000 строк);
И, будет кое-что новое. А именно разборы нюансов при интеграциях одного протокола в другой. Например, если прокол подключается к Uniswap, Gnosis, Seaport и другим, мы будем разбирать некоторые моменты, на которые следует обращать внимание в таких случаях.
Это такая инфа, которую будет сложно найти где-либо за пределами нашей группы.
Если вам интересно, можете присоединиться на следующий месяц.
Условия:
- Для тех, кто хочет просто читать - 1000 рублей;
- Для авторов - 500 на следующий месяц, если в этом вы написали минимум 5 постов;
Оплатить можно:
1. Написав в чат админу
P.S. В комментариях сделаю небольшой пример самого разбора бага по отчету в конкурсном аудите.
Как вы поняли, вы также можете писать свои заметки по багам и делиться интересными находками.
Группа создана специально, чтобы следить за тенденциями в уязвимостях и понимать, куда и как смотреть в коде, чтобы найти проблему.
Буду рад всем желающим!
#group
🔥5👍3
Foundry: организация prank вызовов в функции
В протоколе PoolTogather заметил простой и удобный способ переключения prank вызовов внутри одной функции.
К примеру, у нас есть такая функция:
Да, можно все это сделать с обычными prank, stop/startPrank, hoax, но в итоге может получиться ситуация, когда в тесте будет слишком много этих не нужных читкодов.
Вот как сделала команда PoolTogather.
Они создали модификатор:
а затем поместили его в нужные служебные функции, например, в такую:
и теперь ее можно вызывать внутри других функций, с изменение роли вызывающего:
Т.е. тут идет сначала вызов от одного пользователя, затем в функции _accrueYield() он меняется на другого, выполняются какие-либо действия и роль возвращается к изначальному пользователю.
На мой взгляд достаточно чистый и красивый код теста.
#foundry #prank
В протоколе PoolTogather заметил простой и удобный способ переключения prank вызовов внутри одной функции.
К примеру, у нас есть такая функция:
solidity
function someTestCall() public {
// действия от пользователя_1
// переключение на пользователя_2
// выполнение некоторых действий
// обратное переключение на пользователя_1
// продолжение действий
}
Да, можно все это сделать с обычными prank, stop/startPrank, hoax, но в итоге может получиться ситуация, когда в тесте будет слишком много этих не нужных читкодов.
Вот как сделала команда PoolTogather.
Они создали модификатор:
solidity
modifier prankception(address prankee) {
address prankBefore = currentPrankee;
vm.stopPrank();
vm.startPrank(prankee);
_;
vm.stopPrank();
if (prankBefore != address(0)) {
vm.startPrank(prankBefore);
}
}
а затем поместили его в нужные служебные функции, например, в такую:
solidity
function _accrueYield() internal virtual override prankception(_aTokenWhale) {
IERC20(_aToken).transfer(_yieldVault, 10 ** assetDecimals);
vm.warp(block.timestamp + 1 days);
}
и теперь ее можно вызывать внутри других функций, с изменение роли вызывающего:
solidity
function accrueYield() public returns (uint256) {
uint256 assetsBefore = prizeVault.totalAssets();
@> _accrueYield();
uint256 assetsAfter = prizeVault.totalAssets();
if (yieldVault.balanceOf(address(prizeVault)) > 0) {
require(assetsAfter > assetsBefore, "yield did not accrue");
} else {
if (underlyingAsset.balanceOf(address(prizeVault)) > 0) {
...
} else {
...
}
}
return assetsAfter - assetsBefore;
}
Т.е. тут идет сначала вызов от одного пользователя, затем в функции _accrueYield() он меняется на другого, выполняются какие-либо действия и роль возвращается к изначальному пользователю.
На мой взгляд достаточно чистый и красивый код теста.
#foundry #prank
👍5🔥1
Как работает TWAP в Uniswap V2. Часть 1
С сегодняшнего дня начнем говорить о том, как работает оракул TWAP в версии V2.
Для начала вспомним, а что такое "цена" за актив в пуле вообще.
Например, у нас есть пул, в котором 1 Эфир и 2000 USDT. Цена токена рассчитывается по следующей формуле:
price(token_0) = balance(token_1) / balance(token_0);
Из этого следует, что цена - это отношение балансов, и их нужно хранить в таком типе данных, который имеет десятичные точки (а мы помним, что в Solidity по умолчанию их нет).
Игнорируя decimals, из нашего примера мы можем сказать, что 1 USDT будет равен 0.0005 Эфира.
P.S. Uniswap использует число с фиксированной точкой с точностью 112 бит с каждой стороны десятичной дроби, что занимает в общей сложности 224 бита, и при упаковке с 32-битным числом оно занимает один слот.
Многие протоколы могут обращаться к пулам Юниспава, чтобы узнать актуальную цену на тот или иной токен, и в таком случае пул будет выступать Оракулом. Однако нельзя просто взять соотношение балансов, чтобы получить текущую цену, так как это крайне небезопасно.
Хакеры и другие вредоносные пользователи (мне нравится выражение на английском языке malicious user), могут использовать такие инструменты DeFi протоколов, как флеш займы, чтобы манипулировать текущей ценой в пуле, из-за чего получаемые данные в нашем контракте от Оракула станут недостоверными.
Оракулы Юнисвап V2 защищаются от этого следующими способами:
1. Он предоставляет пользователям механизмы, позволяющие брать среднее значение за предыдущий период времени, который определяется самим пользователем. Это означает, что злоумышленнику придется постоянно манипулировать ценой в течение нескольких блоков, что гораздо дороже, чем использование флэш займа;
2. Также в расчет оракула не включается текущий баланс;
Тем не менее, это все равно не 100% защита. Если актив не обладает большой ликвидностью, или временной интервал для взятия средней цены недостаточно велик, то хакер с большим количеством токенов может поддерживать необходимую цену достаточно долго, успешно манипулируя результатами оракула.
Так как же работает TWAP?
TWAP (Time Weighted Average Price) похож на простую скользящую среднюю, за исключением того, что то время, когда цена оставалась неизменной дольше, получает больший вес. TWAP как бы предает больший вес цене по тому, как долго она остается на определенном уровне. Например,
1. За последние сутки цена актива составляла $10 в течение первых 12 часов и $11 в течение вторых 12 часов. Средняя цена такая же, как и средневзвешенная по времени: $10,5.
2. За последние сутки цена актива составляла $10 за первые 23 часа и $11 за последние. Ожидаемая средняя цена должна быть ближе к $10, чем к $11, но она все равно будет находиться между этими значениями. Точнее, она будет равна ($10 * 23 + $11 * 1) / 24 = $10,0417.
3. За последние сутки цена актива составляла $10 в течение первого часа и $11 в течение последних 23 часов. Мы ожидаем, что TWAP будет ближе к $11, чем к 10. Точнее, он будет равен ($10 * 1 + $11 * 23) / 24 = $10,9583
Дальше узнает, как это все работает в коде контракта.
#uniswap #v2 #twap
С сегодняшнего дня начнем говорить о том, как работает оракул TWAP в версии V2.
Для начала вспомним, а что такое "цена" за актив в пуле вообще.
Например, у нас есть пул, в котором 1 Эфир и 2000 USDT. Цена токена рассчитывается по следующей формуле:
price(token_0) = balance(token_1) / balance(token_0);
Из этого следует, что цена - это отношение балансов, и их нужно хранить в таком типе данных, который имеет десятичные точки (а мы помним, что в Solidity по умолчанию их нет).
Игнорируя decimals, из нашего примера мы можем сказать, что 1 USDT будет равен 0.0005 Эфира.
P.S. Uniswap использует число с фиксированной точкой с точностью 112 бит с каждой стороны десятичной дроби, что занимает в общей сложности 224 бита, и при упаковке с 32-битным числом оно занимает один слот.
Многие протоколы могут обращаться к пулам Юниспава, чтобы узнать актуальную цену на тот или иной токен, и в таком случае пул будет выступать Оракулом. Однако нельзя просто взять соотношение балансов, чтобы получить текущую цену, так как это крайне небезопасно.
Хакеры и другие вредоносные пользователи (мне нравится выражение на английском языке malicious user), могут использовать такие инструменты DeFi протоколов, как флеш займы, чтобы манипулировать текущей ценой в пуле, из-за чего получаемые данные в нашем контракте от Оракула станут недостоверными.
Оракулы Юнисвап V2 защищаются от этого следующими способами:
1. Он предоставляет пользователям механизмы, позволяющие брать среднее значение за предыдущий период времени, который определяется самим пользователем. Это означает, что злоумышленнику придется постоянно манипулировать ценой в течение нескольких блоков, что гораздо дороже, чем использование флэш займа;
2. Также в расчет оракула не включается текущий баланс;
Тем не менее, это все равно не 100% защита. Если актив не обладает большой ликвидностью, или временной интервал для взятия средней цены недостаточно велик, то хакер с большим количеством токенов может поддерживать необходимую цену достаточно долго, успешно манипулируя результатами оракула.
Так как же работает TWAP?
TWAP (Time Weighted Average Price) похож на простую скользящую среднюю, за исключением того, что то время, когда цена оставалась неизменной дольше, получает больший вес. TWAP как бы предает больший вес цене по тому, как долго она остается на определенном уровне. Например,
1. За последние сутки цена актива составляла $10 в течение первых 12 часов и $11 в течение вторых 12 часов. Средняя цена такая же, как и средневзвешенная по времени: $10,5.
2. За последние сутки цена актива составляла $10 за первые 23 часа и $11 за последние. Ожидаемая средняя цена должна быть ближе к $10, чем к $11, но она все равно будет находиться между этими значениями. Точнее, она будет равна ($10 * 23 + $11 * 1) / 24 = $10,0417.
3. За последние сутки цена актива составляла $10 в течение первого часа и $11 в течение последних 23 часов. Мы ожидаем, что TWAP будет ближе к $11, чем к 10. Точнее, он будет равен ($10 * 1 + $11 * 23) / 24 = $10,9583
Дальше узнает, как это все работает в коде контракта.
#uniswap #v2 #twap
👍5🔥2❤1👌1
Как работает TWAP в Uniswap V2. Часть 2
В нашем примере выше мы смотрели цены только за последние 24 часа, но что если вам важны цены на токены за последний час, неделю или другой интервал времени?
Uniswap, понятное, не может хранить все данные о ценах, которые могут однажды быть кому-то интересны. Не удобно также постоянно делать snapshot цен и сохранять их в контракте, так как кому-то все же нужно было бы оплачивать стоимость транзакции за это.
Решение Uniswap заключается в том, что он хранит только числитель значений - каждый раз, когда происходит изменение коэффициента ликвидности (вызывается mint, burn, swap или sync), он записывает новую цену и то, как долго держалась предыдущая цена.
Переменные price0Cumulativelast и price1CumulativeLast на скрине выше являются общедоступными, поэтому заинтересованные пользователи сами должны делать snapshot при необходимости.
Единственное, что нужно отметить это: price0CumulativeLast и price1CumulativeLast обновляются только на строках 79 и 80 в коде выше (оранжевый круг), и могут только увеличиваться, пока не переполнятся (overflow). Нет никакого механизма, заставляющего их "уменьшаться". Они всегда увеличиваются при каждом вызове _update, который и происходит после swap, mint или burn. Это означает, что эти переменные только увеличивают цены с момента запуска пула.
#uniswap #v2 #twap
В нашем примере выше мы смотрели цены только за последние 24 часа, но что если вам важны цены на токены за последний час, неделю или другой интервал времени?
Uniswap, понятное, не может хранить все данные о ценах, которые могут однажды быть кому-то интересны. Не удобно также постоянно делать snapshot цен и сохранять их в контракте, так как кому-то все же нужно было бы оплачивать стоимость транзакции за это.
Решение Uniswap заключается в том, что он хранит только числитель значений - каждый раз, когда происходит изменение коэффициента ликвидности (вызывается mint, burn, swap или sync), он записывает новую цену и то, как долго держалась предыдущая цена.
Переменные price0Cumulativelast и price1CumulativeLast на скрине выше являются общедоступными, поэтому заинтересованные пользователи сами должны делать snapshot при необходимости.
Единственное, что нужно отметить это: price0CumulativeLast и price1CumulativeLast обновляются только на строках 79 и 80 в коде выше (оранжевый круг), и могут только увеличиваться, пока не переполнятся (overflow). Нет никакого механизма, заставляющего их "уменьшаться". Они всегда увеличиваются при каждом вызове _update, который и происходит после swap, mint или burn. Это означает, что эти переменные только увеличивают цены с момента запуска пула.
#uniswap #v2 #twap
❤3🔥1👌1
Как работает TWAP в Uniswap V2. Часть 3
Сегодня заканчиваем говорить о TWAP и посмотрим на чуть более сложные математические примеры. И для начала поговорим о том, почему TWAP должен отслеживать два ратио. Вернемся немного к первому посту.
Цена A по отношению к B - это просто A/B и наоборот. Например, если у нас есть 2000 USDC в пуле (без учета десятичных дробей) и 1 Ether, то цена 1 Ether равна просто 2000 USDC / 1 ETH.
Таким образом, цена USDC, выраженная в ETH, - это просто это число с перевернутыми числителем и знаменателем.
Однако мы не можем просто "перевернуть" одну из цен, чтобы получить другую, когда мы накапливаем цены (не уверен в точном переводе accumulating pricing в данном случае).
Рассмотрим следующее. Если наш накопитель цен начинается с 2 и прибавляет 3, мы не можем просто сделать так:
1 / 2+3 != 1/2 + 1/3
Однако цены все еще "в некоторой степени симметричны", поэтому выбор арифметического представления с фиксированной точкой должен иметь одинаковую емкость для целых и для десятичных чисел.
Если Eth в 1 000 раз "ценнее", чем USDC, то USDC в 1 000 раз "менее ценен", чем USDC. И чтобы хранить эти значения более точно, число с фиксированной точкой должно иметь одинаковый размер по обе стороны decimals, поэтому Uniswap выбрал u112x112 (объяснение в первом посте).
Далее пара слов о значении PriceCumulativeLast, которое всегда увеличивается, пока не переполнится, а затем снова продолжает рост.
Uniswap V2 был создан до версии Solidity 0.8.0, поэтому арифметика по умолчанию могла привести к overflow/underflow. Корректные современные реализации ценового оракула должны использовать блок unchecked, чтобы все работало как положено.
В конце концов, значение priceAccumulators и временная метка блока будет переполняться в контракте. В этом случае предыдущий резерв будет выше нового. Когда оракул вычислит изменение цены, он получит отрицательное значение. Но это не имеет значения в силу правил модульной арифметики.
Чтобы упростить задачу, давайте представим целые числа, которые переполняются при 100.
Мы фиксируем priceAccumulator на 80, а через несколько транзакций/блоков priceAccumulator поднимается до 110, но переполняется до 10. Мы вычитаем 80 из 10, что дает -70. Но значение хранится как беззнаковое целое, поэтому оно дает -70 mod(100), что равно 30. Это тот же результат, который мы ожидали бы, если бы переполнения не было (110-80=30).
Это справедливо для всех границ переполнения, а не только для 100 в нашем примере.
То же самое произойдет, если мы переполним временную метку. Поскольку для ее представления мы используем uint32, отрицательных чисел не будет. Опять же, предположим, что для простоты мы переполнимся на 100. Если мы делаем снимок в момент времени 98 и обращаемся к оракулу цен в момент времени 4, то прошло 6 секунд. 4 - 98 % 100 = 6, как и ожидалось.
Теперь вы знаете о TWAP чуточку больше. Далее мы продолжим говорить о Uniswap V2, V3 и других DeFi протоколах.
#uniswap #v2 #twap
Сегодня заканчиваем говорить о TWAP и посмотрим на чуть более сложные математические примеры. И для начала поговорим о том, почему TWAP должен отслеживать два ратио. Вернемся немного к первому посту.
Цена A по отношению к B - это просто A/B и наоборот. Например, если у нас есть 2000 USDC в пуле (без учета десятичных дробей) и 1 Ether, то цена 1 Ether равна просто 2000 USDC / 1 ETH.
Таким образом, цена USDC, выраженная в ETH, - это просто это число с перевернутыми числителем и знаменателем.
Однако мы не можем просто "перевернуть" одну из цен, чтобы получить другую, когда мы накапливаем цены (не уверен в точном переводе accumulating pricing в данном случае).
Рассмотрим следующее. Если наш накопитель цен начинается с 2 и прибавляет 3, мы не можем просто сделать так:
1 / 2+3 != 1/2 + 1/3
Однако цены все еще "в некоторой степени симметричны", поэтому выбор арифметического представления с фиксированной точкой должен иметь одинаковую емкость для целых и для десятичных чисел.
Если Eth в 1 000 раз "ценнее", чем USDC, то USDC в 1 000 раз "менее ценен", чем USDC. И чтобы хранить эти значения более точно, число с фиксированной точкой должно иметь одинаковый размер по обе стороны decimals, поэтому Uniswap выбрал u112x112 (объяснение в первом посте).
Далее пара слов о значении PriceCumulativeLast, которое всегда увеличивается, пока не переполнится, а затем снова продолжает рост.
Uniswap V2 был создан до версии Solidity 0.8.0, поэтому арифметика по умолчанию могла привести к overflow/underflow. Корректные современные реализации ценового оракула должны использовать блок unchecked, чтобы все работало как положено.
В конце концов, значение priceAccumulators и временная метка блока будет переполняться в контракте. В этом случае предыдущий резерв будет выше нового. Когда оракул вычислит изменение цены, он получит отрицательное значение. Но это не имеет значения в силу правил модульной арифметики.
Чтобы упростить задачу, давайте представим целые числа, которые переполняются при 100.
Мы фиксируем priceAccumulator на 80, а через несколько транзакций/блоков priceAccumulator поднимается до 110, но переполняется до 10. Мы вычитаем 80 из 10, что дает -70. Но значение хранится как беззнаковое целое, поэтому оно дает -70 mod(100), что равно 30. Это тот же результат, который мы ожидали бы, если бы переполнения не было (110-80=30).
Это справедливо для всех границ переполнения, а не только для 100 в нашем примере.
То же самое произойдет, если мы переполним временную метку. Поскольку для ее представления мы используем uint32, отрицательных чисел не будет. Опять же, предположим, что для простоты мы переполнимся на 100. Если мы делаем снимок в момент времени 98 и обращаемся к оракулу цен в момент времени 4, то прошло 6 секунд. 4 - 98 % 100 = 6, как и ожидалось.
Теперь вы знаете о TWAP чуточку больше. Далее мы продолжим говорить о Uniswap V2, V3 и других DeFi протоколах.
#uniswap #v2 #twap
🔥4👌2
Uniswap V2: UniswapV2Library
Мы продолжаем разбирать код одного из самых опулярны протоколов, и следующие пару дней посвятим библиотеке UniswapV2Library:
https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol
Библиотека Uniswap V2 упрощает некоторые взаимодействия с контрактами пар токенов и активно используется контрактами Router. Она содержит всего восемь функций.
getAmountOut() и getAmountIn()
Если мы хотим предсказать количество токенов У, которое мы получим, если добавим фиксированное количество токенов Х, то мы можем вывести количество исходящих токенов, используя следующую последовательность (для простоты игнорируя комиссию).
amount_out = reserve_out * amount_in / reserve_in + amount_in
Учитывая это, функция getAmountOut() в UniswapV2Library.sol должна быть понятна. Обратите внимание, что числа масштабируются на 1000, чтобы учесть комиссию в 0,3%.
Связка логики между getAmountOut() и getAmountsOut() при проведении операций между различными парами токенов
Если трейдер предоставляет последовательность пар (A, B), (B, C), (C, D) и циклически вызывает getAmountOut, начиная с определенной суммы A, то количество полученного токена D может быть предсказано.
Адрес контракта пары UniswapV2 для каждого (A, B), (B, C) и т. д. детерминировано выводится из адресов токенов и адреса фабрики, развернувшей пару с помощью функции create2. Если даны два токена (A, B) и адрес фабрики, функция pairFor() выводит адрес контракта парного пула UniswapV2 для этой пары, используя в качестве вспомогательной функции sortTokens().
Теперь, когда мы знаем адреса всех пар, мы можем получить резервы каждой из них и предсказать, сколько токенов мы получим в конце цепочки обменов. Ниже приведен код функции getAmountsOut (обратите внимание, что написано"Amounts", а не на "Amount").
Обратите внимание на пару моментов:
1. Смарт-контракт не может самостоятельно определить оптимальную последовательность пар, ему нужно сообщить список пар для расчета цепочки свопов. Это лучше всего делать вне цепочки.
2. Он возвращает не просто конечную сумму токеновOut в цепочке, а сумму на каждом шаге.
Далее поговорим об остальных функциях этой библиотеки.
#uniswap #v2 #UniswapV2Library
Мы продолжаем разбирать код одного из самых опулярны протоколов, и следующие пару дней посвятим библиотеке UniswapV2Library:
https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol
Библиотека Uniswap V2 упрощает некоторые взаимодействия с контрактами пар токенов и активно используется контрактами Router. Она содержит всего восемь функций.
getAmountOut() и getAmountIn()
Если мы хотим предсказать количество токенов У, которое мы получим, если добавим фиксированное количество токенов Х, то мы можем вывести количество исходящих токенов, используя следующую последовательность (для простоты игнорируя комиссию).
amount_out = reserve_out * amount_in / reserve_in + amount_in
Учитывая это, функция getAmountOut() в UniswapV2Library.sol должна быть понятна. Обратите внимание, что числа масштабируются на 1000, чтобы учесть комиссию в 0,3%.
function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');
require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
uint amountInWithFee = amountIn.mul(997);
uint numerator = amountInWithFee.mul(reserveOut);
uint denominator = reserveIn.mul(1000).add(amountInWithFee);
amountOut = numerator / denominator;
}
Связка логики между getAmountOut() и getAmountsOut() при проведении операций между различными парами токенов
Если трейдер предоставляет последовательность пар (A, B), (B, C), (C, D) и циклически вызывает getAmountOut, начиная с определенной суммы A, то количество полученного токена D может быть предсказано.
Адрес контракта пары UniswapV2 для каждого (A, B), (B, C) и т. д. детерминировано выводится из адресов токенов и адреса фабрики, развернувшей пару с помощью функции create2. Если даны два токена (A, B) и адрес фабрики, функция pairFor() выводит адрес контракта парного пула UniswapV2 для этой пары, используя в качестве вспомогательной функции sortTokens().
function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {
(address token0, address token1) = sortTokens(tokenA, tokenB);
pair = address(uint(keccak256(abi.encodePacked(
hex'ff',
factory,
keccak256(abi.encodePacked(token0, token1)),
hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'
))));
}Теперь, когда мы знаем адреса всех пар, мы можем получить резервы каждой из них и предсказать, сколько токенов мы получим в конце цепочки обменов. Ниже приведен код функции getAmountsOut (обратите внимание, что написано"Amounts", а не на "Amount").
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[0] = amountIn;
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}Обратите внимание на пару моментов:
1. Смарт-контракт не может самостоятельно определить оптимальную последовательность пар, ему нужно сообщить список пар для расчета цепочки свопов. Это лучше всего делать вне цепочки.
2. Он возвращает не просто конечную сумму токеновOut в цепочке, а сумму на каждом шаге.
Далее поговорим об остальных функциях этой библиотеки.
#uniswap #v2 #UniswapV2Library
🔥3👍1
Uniswap V2: UniswapV2Library. Часть 2
Продолжаем говорить о библиотеке и ее функциях и разберем последние две.
getReserves()
Функция getReserves - это просто обертка функции getReserves из контракта пула токенов Uniswap V2, за исключением того, что она также удаляет временную метку последнего обновления цены.
quote()
Напомним, что цена актива определяется по следующей формуле:
price(foo) = reserves(bar) / reserves(foo)
Эта функция возвращает цену foo, выраженную в bar, по состоянию на последнее обновление. Эту функцию следует использовать с осторожностью, так как она уязвима для атак флэш-кредитования.
Если вы хотите узнать, сколько нужно вложить или ожидать от сделки, а также последовательность сделок по парам, UniswapV2Library - это инструмент, который следует использовать.
К слову сказать, если хотите потренироваться в работе с поиском уязвимостей, то Damn Vulnerable Defi предлагает прекрасную задачу по этой теме:
https://www.damnvulnerabledefi.xyz/challenges/puppet-v2/
Далее мы будем разбирать контракты Router.
#uniswap #v2 #UniswapV2Library
Продолжаем говорить о библиотеке и ее функциях и разберем последние две.
getReserves()
Функция getReserves - это просто обертка функции getReserves из контракта пула токенов Uniswap V2, за исключением того, что она также удаляет временную метку последнего обновления цены.
//Library
function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {
(address token0,) = sortTokens(tokenA, tokenB);
(uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();
(reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
}
//pool
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
quote()
Напомним, что цена актива определяется по следующей формуле:
price(foo) = reserves(bar) / reserves(foo)
Эта функция возвращает цену foo, выраженную в bar, по состоянию на последнее обновление. Эту функцию следует использовать с осторожностью, так как она уязвима для атак флэш-кредитования.
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
amountB = amountA.mul(reserveB) / reserveA;
}
Если вы хотите узнать, сколько нужно вложить или ожидать от сделки, а также последовательность сделок по парам, UniswapV2Library - это инструмент, который следует использовать.
К слову сказать, если хотите потренироваться в работе с поиском уязвимостей, то Damn Vulnerable Defi предлагает прекрасную задачу по этой теме:
https://www.damnvulnerabledefi.xyz/challenges/puppet-v2/
Далее мы будем разбирать контракты Router.
#uniswap #v2 #UniswapV2Library
👍3
Сборник ресурсов по Uniswap V2/V3
Недавно в Твиттере кто-то поделился прекрасным репо, где собраны различные материалы по работе с двумя версиями Uniswap - V2 и V3.
Тут вы сможете найти как примеры контрактов, так и официальную документацию протокола, а также различные статьи, видео, подкасты и многое другое.
Думаю, в течение нашего разбора DeFi протоколов мы иногда будем брать оттуда статьи для разбора на нашем канале.
Ссылка на сборник: https://github.com/Sabnock01/uniswap-resources
Интересно, а когда начнут выходить достойные гайды по V4. Она же должна будет выйти через пару месяцев после обновления самой сети.
#uniswap #github
Недавно в Твиттере кто-то поделился прекрасным репо, где собраны различные материалы по работе с двумя версиями Uniswap - V2 и V3.
Тут вы сможете найти как примеры контрактов, так и официальную документацию протокола, а также различные статьи, видео, подкасты и многое другое.
Думаю, в течение нашего разбора DeFi протоколов мы иногда будем брать оттуда статьи для разбора на нашем канале.
Ссылка на сборник: https://github.com/Sabnock01/uniswap-resources
Интересно, а когда начнут выходить достойные гайды по V4. Она же должна будет выйти через пару месяцев после обновления самой сети.
#uniswap #github
🔥5
Разбор Uniswap V2: Router контракты
Мы продолжаем изучать код протокола Uniswap версии V2, и на этой неделе поговорим про контракт Router и его основные функции.
Итак, этот контракт отвечает за:
1. Безопасный майнинг и сжигание токенов LP (добавление и удаление ликвидности)
2. Безопасный своп токенов;
3. Возможный обмен Эфира при помощи его ERC-20 версии - WETH;
4. Проверки проскальзывания при работе с основным контрактом;
5 Поддержка fee on transfer токенов;
Вообще, если мы зайдем по ссылке репо, то увидим, что там два контакта Router:
UniswapV2Router01.sol
UniswapV2Router02.sol
Вот второй, как раз и добавляет поддержку fee on transfer токенов. Если мы заглянем в него, то увидим, что он наследует от первого контракта и добавляет уникальные функции, например:
function removeLiquidityETHSupportingFeeOnTransferTokens(...) ) public virtual override ensure(deadline) returns (uint amountETH) {}
Теперь о самих функциях.
swapExactTokensForTokens и swapTokensForExactTokens
Обе эти функции используются при свапе токенов, с одной разницей:
1. В swapExactTokensForTokens означает, что количество входного токена, который вы обмениваете, фиксировано.
2. В swapTokensForExactTokens - количество выходного токена, который вы получите, фиксировано.
Если пользователь обменивает только два токена, то он передаст этим функциям массив address[] calldata path [address(tokenIn), address(tokenOut)]. Если пользователь обменивается между пулами, то он укажет [address(tokenIn), address(intermediateToken), ..., address(tokenOut)].
swapExactTokensForTokens
В случае с swapExactTokensForTokens пользователь указывает точное количество первого токена, который он собирается внести, и минимальное количество выходного токена, которое он примет.
Например, предположим, что мы хотим обменять 25 токенов_0 на 50 токенов_1. Если это точная цена в текущем состоянии, это не оставляет никаких шансов на то, что цена изменится до подтверждения нашей транзакции, что приведет к реверту. Поэтому вместо этого мы указываем минимальную цену 49,5 токена_1, неявно оставляя проскальзывание в 1 %.
swapTokensForExactTokens
В этом случае мы указываем, что нам нужно ровно 50 токенов_1, но мы готовы обменять до 25,5 токенов_0, чтобы получить их.
Какую функцию использовать?
Большинство пользователей, использующих EOA, скорее всего, предпочтут использовать функцию swapExactTokensForTokens, поскольку им необходим этап подтверждения, и сделка не состоится, если им придется заплатить больше, чем они хотели.
Однако смарт-контракты, интернирующиеся с Uniswap, могут иметь более сложные требования, поэтому маршрутизатор дает им возможность использовать оба варианта.
Как работает своп токенов
Если входные данные точные (swapExactTokensForTokens), функция предсказывает ожидаемый выход для одного свопа или цепочки свопов. Если полученный результат ниже заданной пользователем суммы, функция откатится.
И наоборот, для точного выхода: она вычисляет требуемый вход и возвращается, если он выше заданного пользователем порога.
Затем обе функции передают токены пользователя в контракт пары (мы же помним, что Uniswap V2 Pair требует, чтобы токены были отправлены на сам контракт до вызова функции swap).
Наконец, они вызывают внутреннюю функцию _swap, о которой пойдет речь далее.
#uniswap #router
Мы продолжаем изучать код протокола Uniswap версии V2, и на этой неделе поговорим про контракт Router и его основные функции.
Итак, этот контракт отвечает за:
1. Безопасный майнинг и сжигание токенов LP (добавление и удаление ликвидности)
2. Безопасный своп токенов;
3. Возможный обмен Эфира при помощи его ERC-20 версии - WETH;
4. Проверки проскальзывания при работе с основным контрактом;
5 Поддержка fee on transfer токенов;
Вообще, если мы зайдем по ссылке репо, то увидим, что там два контакта Router:
UniswapV2Router01.sol
UniswapV2Router02.sol
Вот второй, как раз и добавляет поддержку fee on transfer токенов. Если мы заглянем в него, то увидим, что он наследует от первого контракта и добавляет уникальные функции, например:
function removeLiquidityETHSupportingFeeOnTransferTokens(...) ) public virtual override ensure(deadline) returns (uint amountETH) {}
Теперь о самих функциях.
swapExactTokensForTokens и swapTokensForExactTokens
Обе эти функции используются при свапе токенов, с одной разницей:
1. В swapExactTokensForTokens означает, что количество входного токена, который вы обмениваете, фиксировано.
2. В swapTokensForExactTokens - количество выходного токена, который вы получите, фиксировано.
Если пользователь обменивает только два токена, то он передаст этим функциям массив address[] calldata path [address(tokenIn), address(tokenOut)]. Если пользователь обменивается между пулами, то он укажет [address(tokenIn), address(intermediateToken), ..., address(tokenOut)].
swapExactTokensForTokens
В случае с swapExactTokensForTokens пользователь указывает точное количество первого токена, который он собирается внести, и минимальное количество выходного токена, которое он примет.
Например, предположим, что мы хотим обменять 25 токенов_0 на 50 токенов_1. Если это точная цена в текущем состоянии, это не оставляет никаких шансов на то, что цена изменится до подтверждения нашей транзакции, что приведет к реверту. Поэтому вместо этого мы указываем минимальную цену 49,5 токена_1, неявно оставляя проскальзывание в 1 %.
swapTokensForExactTokens
В этом случае мы указываем, что нам нужно ровно 50 токенов_1, но мы готовы обменять до 25,5 токенов_0, чтобы получить их.
Какую функцию использовать?
Большинство пользователей, использующих EOA, скорее всего, предпочтут использовать функцию swapExactTokensForTokens, поскольку им необходим этап подтверждения, и сделка не состоится, если им придется заплатить больше, чем они хотели.
Однако смарт-контракты, интернирующиеся с Uniswap, могут иметь более сложные требования, поэтому маршрутизатор дает им возможность использовать оба варианта.
Как работает своп токенов
Если входные данные точные (swapExactTokensForTokens), функция предсказывает ожидаемый выход для одного свопа или цепочки свопов. Если полученный результат ниже заданной пользователем суммы, функция откатится.
И наоборот, для точного выхода: она вычисляет требуемый вход и возвращается, если он выше заданного пользователем порога.
Затем обе функции передают токены пользователя в контракт пары (мы же помним, что Uniswap V2 Pair требует, чтобы токены были отправлены на сам контракт до вызова функции swap).
Наконец, они вызывают внутреннюю функцию _swap, о которой пойдет речь далее.
#uniswap #router
❤1
Разбор Uniswap V2: Router контракты. Часть 2
Продолжаем говорить о контрактах Router и сегодня обратим внимание на _swap(), addLiquidity() и addLiquidityEth().
Вообще по свапам у нас уже был отдельный цикл постов с детальным разбором, и тут ничего нового я вам не скажу. Думаю, скрин функции с комментариями поможет вам вспомнить основные моменты работы функции.
_addLiquidity
Помните о проверке безопасности при добавлении ликвидности? В частности, мы хотим убедиться, что вносим два токена в точно таком же соотношении, как и у пары в данный момент, иначе количество токенов LP, которые мы майним, будет худшим из двух соотношений между тем, что мы предоставляем, и тем, что есть на балансе пары.
Однако соотношение может измениться между моментом, когда поставщик ликвидности пытается добавить ликвидность, и моментом подтверждения транзакции.
Чтобы защититься от этого, поставщик ликвидности должен предоставить (в качестве аргумента функции) минимальный баланс, который он хочет внести для токена_0 и токена_1 (в UniswapV2 это amountAMin и amountBMin). Затем они переводят сумму, превышающую эти минимумы (UnsiwapV2 называет их amountADesired и amountBDesired). Если соотношение пар изменилось таким образом, что минимумы больше не соблюдаются, то транзакция откатывается.
_addLiquidity возьмет суммуADesired и вычислит правильное количество токенов_В, которое будет соответствовать соотношению. Если это количество больше, чем amountBDesired (количество B, которое отправил поставщик ликвидности), то он начнет с amountBDesired и вычислит оптимальное количество B. Логика показана на скрине.
P.S. Обратите внимание, что добавление ликвидности может создать новый парный контракт, если он еще не существует.
Например, предположим, что текущий баланс пары составляет 100 token_0 и 300 token_1. Мы хотим добавить 20 и 60 токен_0 и токен_1 соответственно, но соотношение пар может измениться.
Поэтому вместо этого мы одобряем маршрутизатор на 21 токен_0 и 63 токена_1, указывая при этом, что минимальное количество, которое мы хотим внести, составляет 20 и 60. Если соотношение изменится так, что оптимальное количество токенов0 для депозита составит 19,9, то транзакция вернется обратно.
Помните, мы говорили, что котировки цен токенов не следует использовать в качестве оракула? И это по-прежнему верно! Однако для целей добавления ликвидности нас интересует не среднее значение предыдущих цен, а текущая цена (коэффициент пула), поскольку поставщик ликвидности должен ее соблюдать.
addLiquidity() и addLiquidityEth()
Эти функции не требуют пояснений. Сначала они рассчитывают оптимальное соотношение с помощью _addLiquidity, описанного выше, затем переводят активы в контракт пары, после чего вызывают майнинг. Единственное отличие заключается в том, что функция addLiquidityEth сначала обернет Ether в ETH.
В следующем посте поговорим об удалении ликвидности, fee-on-transfer токенах и библиотеке UniswapV2Library.
#uniswap #router
Продолжаем говорить о контрактах Router и сегодня обратим внимание на _swap(), addLiquidity() и addLiquidityEth().
Вообще по свапам у нас уже был отдельный цикл постов с детальным разбором, и тут ничего нового я вам не скажу. Думаю, скрин функции с комментариями поможет вам вспомнить основные моменты работы функции.
_addLiquidity
Помните о проверке безопасности при добавлении ликвидности? В частности, мы хотим убедиться, что вносим два токена в точно таком же соотношении, как и у пары в данный момент, иначе количество токенов LP, которые мы майним, будет худшим из двух соотношений между тем, что мы предоставляем, и тем, что есть на балансе пары.
Однако соотношение может измениться между моментом, когда поставщик ликвидности пытается добавить ликвидность, и моментом подтверждения транзакции.
Чтобы защититься от этого, поставщик ликвидности должен предоставить (в качестве аргумента функции) минимальный баланс, который он хочет внести для токена_0 и токена_1 (в UniswapV2 это amountAMin и amountBMin). Затем они переводят сумму, превышающую эти минимумы (UnsiwapV2 называет их amountADesired и amountBDesired). Если соотношение пар изменилось таким образом, что минимумы больше не соблюдаются, то транзакция откатывается.
_addLiquidity возьмет суммуADesired и вычислит правильное количество токенов_В, которое будет соответствовать соотношению. Если это количество больше, чем amountBDesired (количество B, которое отправил поставщик ликвидности), то он начнет с amountBDesired и вычислит оптимальное количество B. Логика показана на скрине.
P.S. Обратите внимание, что добавление ликвидности может создать новый парный контракт, если он еще не существует.
Например, предположим, что текущий баланс пары составляет 100 token_0 и 300 token_1. Мы хотим добавить 20 и 60 токен_0 и токен_1 соответственно, но соотношение пар может измениться.
Поэтому вместо этого мы одобряем маршрутизатор на 21 токен_0 и 63 токена_1, указывая при этом, что минимальное количество, которое мы хотим внести, составляет 20 и 60. Если соотношение изменится так, что оптимальное количество токенов0 для депозита составит 19,9, то транзакция вернется обратно.
Помните, мы говорили, что котировки цен токенов не следует использовать в качестве оракула? И это по-прежнему верно! Однако для целей добавления ликвидности нас интересует не среднее значение предыдущих цен, а текущая цена (коэффициент пула), поскольку поставщик ликвидности должен ее соблюдать.
addLiquidity() и addLiquidityEth()
Эти функции не требуют пояснений. Сначала они рассчитывают оптимальное соотношение с помощью _addLiquidity, описанного выше, затем переводят активы в контракт пары, после чего вызывают майнинг. Единственное отличие заключается в том, что функция addLiquidityEth сначала обернет Ether в ETH.
В следующем посте поговорим об удалении ликвидности, fee-on-transfer токенах и библиотеке UniswapV2Library.
#uniswap #router
❤1