Uniswap V2: swap(). Часть 2
Из прошлого поста мы узнали, что функция swap() не имеет кода для получения пользовательских токенов, что может открывать опцию флеш займов!
Контракт может просто запросить некоторое количество токенов, которое требуется к займу (отметка А на скрине), без какого-либо залога. Сами токены отправляются чуть позже (отметка В).
Вместе с вызовом функции также должны быть отправлены данные, которые позже перенаправляются в контракт, который реализует интерфейс IUniswapV2Callee, где присутствует всего лишь одна функция:
Вообще, нужно сказать, что вызывать функцию swap(), т.е. обменивать токены или брать флеш займы, могут только контракты, так как ЕОА не могут одновременно отправлять токены и вызывать свап в одной транзакции без помощи смарт контрактов.
#uniswap #v2 #swap
Из прошлого поста мы узнали, что функция swap() не имеет кода для получения пользовательских токенов, что может открывать опцию флеш займов!
Контракт может просто запросить некоторое количество токенов, которое требуется к займу (отметка А на скрине), без какого-либо залога. Сами токены отправляются чуть позже (отметка В).
Вместе с вызовом функции также должны быть отправлены данные, которые позже перенаправляются в контракт, который реализует интерфейс IUniswapV2Callee, где присутствует всего лишь одна функция:
function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external;
Вообще, нужно сказать, что вызывать функцию swap(), т.е. обменивать токены или брать флеш займы, могут только контракты, так как ЕОА не могут одновременно отправлять токены и вызывать свап в одной транзакции без помощи смарт контрактов.
#uniswap #v2 #swap
🔥5
Uniswap V2: swap(). Часть 3
Продолжаем разбирать функцию свапа и сегодня поговорим о том, как Юнисвап понимает, сколько токенов было послано в пул.
За это отвечают, по сути, всего две строки (176-177):
Из предыдущих постов мы помним, что переменные _reserve0 и _reserve1 не обновляются внутри функции. Они отражают баланс контракта до того, как новые токены были добавлены при свапе.
С каждым из двух токенов в паре может случится одна из ситуаций:
1. В пуле произошло чистое увеличение количества определенного токена;
2. В пуле произошло чистое уменьшение количества определенного токена или оно осталось неизменным;
То как код контракта определяет ситуацию,, можно переложить на формулу:
currentContractbalanceX > _reserveX - _amountXOut
или
currentContractBalanceX > previousContractBalanceX - _amountXOut
В случае, если зафиксировано уменьшение, то тернарный оператор возвращает ноль, в противном случае учитывается увеличение количества токенов.
amountXIn = balanceX - (_reserveX - amountXOut)
Также можно указать на то, что в любом случае _reserveX > amountXOut, так как идет проверка на строке 162:
Несколько примеров:
1. Предположим, что предыдущий баланс был равен 10, amountOut = 0, и currentBalance = 12. Это значит, что было добавлено 2 токена и amountXIn будет равен 2.
2. Или предыдущий баланс равен 10, amountOut = 7, и currentBalance = 3, поэтому amountXIn = 0.
3. Предыдущий баланс был равен 10, amountOut = 7, и currentBalance = 2. В этом случае amountXIn будет также равен 0, а не -1. Пул получает уменьшение на 8 токенов, а amountXIn не может быть негативным числом.
4. Или же предыдущий баланс равен 10, amountOut = 6. Если currentBalance равен 18, это означает, что пользователь "занял" 6 токенов и вернул 8.
В завершении можно сказать, что amount0In и amount1In отражают чистое увеличение токенов при добавлении в пул, и равняются нулю, если идет уменьшение.
#uniswap #swap
Продолжаем разбирать функцию свапа и сегодня поговорим о том, как Юнисвап понимает, сколько токенов было послано в пул.
За это отвечают, по сути, всего две строки (176-177):
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
Из предыдущих постов мы помним, что переменные _reserve0 и _reserve1 не обновляются внутри функции. Они отражают баланс контракта до того, как новые токены были добавлены при свапе.
С каждым из двух токенов в паре может случится одна из ситуаций:
1. В пуле произошло чистое увеличение количества определенного токена;
2. В пуле произошло чистое уменьшение количества определенного токена или оно осталось неизменным;
То как код контракта определяет ситуацию,, можно переложить на формулу:
currentContractbalanceX > _reserveX - _amountXOut
или
currentContractBalanceX > previousContractBalanceX - _amountXOut
В случае, если зафиксировано уменьшение, то тернарный оператор возвращает ноль, в противном случае учитывается увеличение количества токенов.
amountXIn = balanceX - (_reserveX - amountXOut)
Также можно указать на то, что в любом случае _reserveX > amountXOut, так как идет проверка на строке 162:
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
Несколько примеров:
1. Предположим, что предыдущий баланс был равен 10, amountOut = 0, и currentBalance = 12. Это значит, что было добавлено 2 токена и amountXIn будет равен 2.
2. Или предыдущий баланс равен 10, amountOut = 7, и currentBalance = 3, поэтому amountXIn = 0.
3. Предыдущий баланс был равен 10, amountOut = 7, и currentBalance = 2. В этом случае amountXIn будет также равен 0, а не -1. Пул получает уменьшение на 8 токенов, а amountXIn не может быть негативным числом.
4. Или же предыдущий баланс равен 10, amountOut = 6. Если currentBalance равен 18, это означает, что пользователь "занял" 6 токенов и вернул 8.
В завершении можно сказать, что amount0In и amount1In отражают чистое увеличение токенов при добавлении в пул, и равняются нулю, если идет уменьшение.
#uniswap #swap
❤2
Uniswap V2: swap(). Часть 4
Сегодня мы поговорим про всем известную константу XY = K, а именно, как она работает при свапах и расчете комиссии за свап.
Посмотрите на эти строки в функции свапа:
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
Uniswap V2 взымает установленные в коде 0.3% за каждый свап, поэтому мы видим цифры 1000 и 3.
Для упрощения объяснения работы константы, давайте для начала представим, что Юнисвап вообще не берет комиссию и напишем небольшую проверку, заменяющую предыдущие две строки:
require(balance0 * balance1 >= reserve0 * reserve1, "K");
Если внимательно присмотреться, К - это не совсем константа, не смотря на то, что в документации и примерах ее всегда называют AMM “constant product formula”, или константа формулы продукта.
Объяснение может быть очень простым: смотрите, когда кто-то переводит токены в пул и изменяет константу К, мы же не хотим его ограничивать, так как сами заинтересованы в обогащении наших поставщиков ликвидности и увеличении пула?
Так и Юнисвап не ограничивает вас от перевода очень больших объемов токенов и изменении К. Когда К становится больше, объем пула становится больше, а это именно то, что хотят поставщики ликвидности.
Теперь вернемся к процентам за перевод.
Мы хотим, чтобы не только К становился больше, мы хотим, чтобы он становился больше как минимум на такое количество, чтобы можно было взымать комиссию!
Глядя на код, мы можем сказать, что сама комиссия берется не по размеру пула, а по объему переводов. И применяется только к количеству токенов, которые поступают в пул, а не тех, что отправляются пользователю!
Например:
1. Мы отправили 1000 токенов_0 и забрали 1000 токенов_1. Нам потребуется заплатить комиссию в 3 токена_0;
2. Мы берем займ 1000 токенов_0 и должны будем вернуть 1000 токенов_0 плюс комиссию в 3 токена_0. Токен_1 здесь вообще не участвует, как можно заметить.
Теперь, глядя на изначальные строки расчета комиссии при свапе, мы понимаем, как они работают. Умножение на 1000 необходимо, так как Solidity не работает с "плавающей точкой" и требуется делать дополнительные умножения для проверки получения этих 0.3% комиссии.
#uniswap #constant #swap #fee
Сегодня мы поговорим про всем известную константу XY = K, а именно, как она работает при свапах и расчете комиссии за свап.
Посмотрите на эти строки в функции свапа:
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
Uniswap V2 взымает установленные в коде 0.3% за каждый свап, поэтому мы видим цифры 1000 и 3.
Для упрощения объяснения работы константы, давайте для начала представим, что Юнисвап вообще не берет комиссию и напишем небольшую проверку, заменяющую предыдущие две строки:
require(balance0 * balance1 >= reserve0 * reserve1, "K");
Если внимательно присмотреться, К - это не совсем константа, не смотря на то, что в документации и примерах ее всегда называют AMM “constant product formula”, или константа формулы продукта.
Объяснение может быть очень простым: смотрите, когда кто-то переводит токены в пул и изменяет константу К, мы же не хотим его ограничивать, так как сами заинтересованы в обогащении наших поставщиков ликвидности и увеличении пула?
Так и Юнисвап не ограничивает вас от перевода очень больших объемов токенов и изменении К. Когда К становится больше, объем пула становится больше, а это именно то, что хотят поставщики ликвидности.
Теперь вернемся к процентам за перевод.
Мы хотим, чтобы не только К становился больше, мы хотим, чтобы он становился больше как минимум на такое количество, чтобы можно было взымать комиссию!
Глядя на код, мы можем сказать, что сама комиссия берется не по размеру пула, а по объему переводов. И применяется только к количеству токенов, которые поступают в пул, а не тех, что отправляются пользователю!
Например:
1. Мы отправили 1000 токенов_0 и забрали 1000 токенов_1. Нам потребуется заплатить комиссию в 3 токена_0;
2. Мы берем займ 1000 токенов_0 и должны будем вернуть 1000 токенов_0 плюс комиссию в 3 токена_0. Токен_1 здесь вообще не участвует, как можно заметить.
Теперь, глядя на изначальные строки расчета комиссии при свапе, мы понимаем, как они работают. Умножение на 1000 необходимо, так как Solidity не работает с "плавающей точкой" и требуется делать дополнительные умножения для проверки получения этих 0.3% комиссии.
#uniswap #constant #swap #fee
❤4👍2
Uniswap V2: swap(). Часть 5
Ну, и заключительная часть про свап функцию, в которой поговорим про обновление резервов.
Если посмотреть на функцию, то можно увидеть, что все действие происходит в другой служебной функции:
_update(balance0, balance1, _reserve0, _reserve1);
Именно она и показана на скрине.
Там есть логика для обновления данных оракула, но этого мы коснемся позже. В данном случае нас интересуют строки 82-83, там обновляются переменные состояния контракта:
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
При свапе могут возникнуть некоторые проблемы:
1. Параметр amountIn не обязательно может быть оптимальным для свапа и тогда пользователь переплачивает за сам свап;
2. AmountOut не такой гибкий, как мог бы быт, так как он передается в функцию в качестве аргумента. Если amountIn окажется недостаточным по отношению к AmountOut, то транзакция откатится;
Если условия могут получиться, если один пользователь сделает фронтран транзакции другого пользователя и изменит ратио активов в пуле.
P.S. Напомню, что эти несколько постов были написаны, как перевод статьи от RareSkills.
#uniswap #swap
Ну, и заключительная часть про свап функцию, в которой поговорим про обновление резервов.
Если посмотреть на функцию, то можно увидеть, что все действие происходит в другой служебной функции:
_update(balance0, balance1, _reserve0, _reserve1);
Именно она и показана на скрине.
Там есть логика для обновления данных оракула, но этого мы коснемся позже. В данном случае нас интересуют строки 82-83, там обновляются переменные состояния контракта:
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
При свапе могут возникнуть некоторые проблемы:
1. Параметр amountIn не обязательно может быть оптимальным для свапа и тогда пользователь переплачивает за сам свап;
2. AmountOut не такой гибкий, как мог бы быт, так как он передается в функцию в качестве аргумента. Если amountIn окажется недостаточным по отношению к AmountOut, то транзакция откатится;
Если условия могут получиться, если один пользователь сделает фронтран транзакции другого пользователя и изменит ратио активов в пуле.
P.S. Напомню, что эти несколько постов были написаны, как перевод статьи от RareSkills.
#uniswap #swap
🔥4
Разбор Uniswap V2: mint() & burn()
Переходим потихоньку к другим функциям в Юнисвап, и в этот раз поговорим о минте и сжигании.
На скрине выше представлена функция сжигания токенов, давайте разберем ее:
1. На линии 140 (фиолетовая отметка) ликвидность измеряется в LP токенах, которые в данный момент есть на контракте. Предполагается, что пользователь заранее отправит LP токены перед вызовом burn(). При этом, в рамках безопасности, перевод LP токенов и вызов сжигания следует производить в одной транзакции, чтобы никто не смог сжечь ваши токены и получить другие бесплатно.
2. Красная отметка (линии 142 и 154) отвечает за комиссию, которая может быть введена Юнисвапом для поставщиков ликвидности. На данный момент она отсутствует.
3. Оранжевая отметка (линии 144-145) показывает количество токенов, которые получит пользователь за свои LP токены.
4. Голубая отметка (147-149) - место, где LP токены сжигаются и token0 и token1 пересылаются пользователю.
5. Желтая (линии 150-151) - получают обновленные балансы токенов, для того, чтобы ниже, на линии 153, могла быть вызвана функция для обновления данных _update().
Количество токенов, которые пользователь получит после сжигания, зависит от ратио LP токенов к totalSupply LP. Однако, totalSupply может измениться перед самым моментом сжигания, поэтому для безопасности требуется учитывать проскальзывание (slippage) в транзакции.
#uniswap #v2 #burn
Переходим потихоньку к другим функциям в Юнисвап, и в этот раз поговорим о минте и сжигании.
На скрине выше представлена функция сжигания токенов, давайте разберем ее:
1. На линии 140 (фиолетовая отметка) ликвидность измеряется в LP токенах, которые в данный момент есть на контракте. Предполагается, что пользователь заранее отправит LP токены перед вызовом burn(). При этом, в рамках безопасности, перевод LP токенов и вызов сжигания следует производить в одной транзакции, чтобы никто не смог сжечь ваши токены и получить другие бесплатно.
2. Красная отметка (линии 142 и 154) отвечает за комиссию, которая может быть введена Юнисвапом для поставщиков ликвидности. На данный момент она отсутствует.
3. Оранжевая отметка (линии 144-145) показывает количество токенов, которые получит пользователь за свои LP токены.
4. Голубая отметка (147-149) - место, где LP токены сжигаются и token0 и token1 пересылаются пользователю.
5. Желтая (линии 150-151) - получают обновленные балансы токенов, для того, чтобы ниже, на линии 153, могла быть вызвана функция для обновления данных _update().
Количество токенов, которые пользователь получит после сжигания, зависит от ратио LP токенов к totalSupply LP. Однако, totalSupply может измениться перед самым моментом сжигания, поэтому для безопасности требуется учитывать проскальзывание (slippage) в транзакции.
#uniswap #v2 #burn
👍2❤1
Разбор Uniswap V2: mint() & burn()
А теперь поговорим о функции минта, которая представлена на скрине выше.
Некоторый функционал в ней схож с функцией burn(), поэтому некоторые моменты можно посмотреть в предыдущем посте.
В пуле может быть, как бы, два состояние: без ликвидности и с ликвидность. Оба этих случая учитываются в коде (отмечены желтым маркером). В данном разборе мы коснёмся второго случая, а именно того, как рассчитывается объем минта при депозите токенов в пул.
Посмотрите на эту строку:
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
именно тут происходит вся магия. Здесь выбирается минимальное значение из двух возможных, которые будут сминчены пользователю.
Например, у нас есть 10 токенов_1 и 10 токенов_2. Если пользователь добавит 10 токенов_1 и 0 токенов_2, то он получит 0 LP токенов - (10/10, 0/10)! Или другой пример: если пользователь отправит 5% токена_1 и 10% токена_2, он получит всего 5% LP токенов!
Причина такому подсчёту проста: таким образом протокол мотивирует своих пользователей добавлять оба токена в пул, сохраняя его ратио. Зачем? Давайте разбираться.
Представим, что пул содержит в себе 10 токенов_1 и всего 1 токен_2, а также количество LP равно 1. Также допустим, что цена каждого токена 100$ (за каждый), т.е. общая стоимость пула равна 200$.
Если мы будет минтить по максимальному ратио, хакер может добавить всего 1 токен_2 (стоимостью 100$) и увеличить общую стоимость пула до 300$, т.е. увеличение на 50%. Также он получит 1 LP токен, что фактически равно 50% от общего количества LP токенов. В итоге получается, что он теперь контролирует 50% от пула стоимостью 300$, вложив всего 100$! А это вполне может считаться воровством у других поставщиков ликвидности.
Также как и в случае работы burn() функции, общее количество LP токенов и баланс пула может быть изменен прямо перед нашей транзакцией, поэтому тут также требуется писать дополнительные проверки на проскальзывание (slippage).
#unoswap #v2 #mint
А теперь поговорим о функции минта, которая представлена на скрине выше.
Некоторый функционал в ней схож с функцией burn(), поэтому некоторые моменты можно посмотреть в предыдущем посте.
В пуле может быть, как бы, два состояние: без ликвидности и с ликвидность. Оба этих случая учитываются в коде (отмечены желтым маркером). В данном разборе мы коснёмся второго случая, а именно того, как рассчитывается объем минта при депозите токенов в пул.
Посмотрите на эту строку:
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
именно тут происходит вся магия. Здесь выбирается минимальное значение из двух возможных, которые будут сминчены пользователю.
Например, у нас есть 10 токенов_1 и 10 токенов_2. Если пользователь добавит 10 токенов_1 и 0 токенов_2, то он получит 0 LP токенов - (10/10, 0/10)! Или другой пример: если пользователь отправит 5% токена_1 и 10% токена_2, он получит всего 5% LP токенов!
Причина такому подсчёту проста: таким образом протокол мотивирует своих пользователей добавлять оба токена в пул, сохраняя его ратио. Зачем? Давайте разбираться.
Представим, что пул содержит в себе 10 токенов_1 и всего 1 токен_2, а также количество LP равно 1. Также допустим, что цена каждого токена 100$ (за каждый), т.е. общая стоимость пула равна 200$.
Если мы будет минтить по максимальному ратио, хакер может добавить всего 1 токен_2 (стоимостью 100$) и увеличить общую стоимость пула до 300$, т.е. увеличение на 50%. Также он получит 1 LP токен, что фактически равно 50% от общего количества LP токенов. В итоге получается, что он теперь контролирует 50% от пула стоимостью 300$, вложив всего 100$! А это вполне может считаться воровством у других поставщиков ликвидности.
Также как и в случае работы burn() функции, общее количество LP токенов и баланс пула может быть изменен прямо перед нашей транзакцией, поэтому тут также требуется писать дополнительные проверки на проскальзывание (slippage).
#unoswap #v2 #mint
👍5🔥2
Разбор 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