Обзор контракта Uniswap-v2
Введение
Uniswap v2 (opens in a new tab) может создавать рынок обмена между любыми двумя токенами ERC-20. В этой статье мы рассмотрим исходный код контрактов, реализующих этот протокол, и разберемся, почему они написаны именно так.
Что делает Uniswap?
По сути, существует два типа пользователей: поставщики ликвидности и трейдеры.
Поставщики ликвидности предоставляют пулу два токена, которые можно обменять (мы будем называть их Token0 и Token1). Взамен они получают третий токен, который представляет собой частичное владение пулом и называется токен ликвидности.
Трейдеры отправляют в пул один тип токенов и получают из него другой (например, отправляют Token0 и получают Token1), который был предоставлен поставщиками ликвидности. Обменный курс определяется относительным количеством токенов Token0 и Token1 в пуле. Кроме того, пул берет небольшой процент в качестве вознаграждения для пула ликвидности.
Когда поставщики ликвидности хотят вернуть свои активы, они могут сжечь токены пула и получить обратно свои токены, включая свою долю вознаграждений.
Более подробное описание см. здесь (opens in a new tab).
Почему v2? Почему не v3?
Uniswap v3 (opens in a new tab) — это обновление, которое гораздо сложнее, чем v2. Проще сначала изучить v2, а затем перейти к v3.
Основные контракты и периферийные контракты
Uniswap v2 разделен на два компонента: основной и периферийный. Такое разделение позволяет сделать основные контракты, которые хранят активы и поэтому должны быть безопасными, более простыми и легкими для аудита. Вся дополнительная функциональность, необходимая трейдерам, может быть предоставлена периферийными контрактами.
Потоки данных и управления
Это поток данных и управления, который возникает при выполнении трех основных действий Uniswap:
- Обмен между различными токенами
- Добавление ликвидности на рынок и получение вознаграждения в виде токенов ликвидности ERC-20 для пары обмена
- Сжигание токенов ликвидности ERC-20 и получение обратно токенов ERC-20, которые пара обмена позволяет обменивать трейдерам
Обмен
Это самый распространенный поток, используемый трейдерами:
Вызывающая сторона
- Предоставляет периферийному аккаунту разрешение (allowance) на сумму, подлежащую обмену.
- Вызывает одну из многочисленных функций обмена периферийного контракта (выбор зависит от того, участвует ли ETH, указывает ли трейдер сумму токенов для внесения или сумму токенов для получения и т. д.).
Каждая функция обмена принимает
path— массив бирж, через которые нужно пройти.
В периферийном контракте (UniswapV2Router02.sol)
- Определяет объемы, которые необходимо обменять на каждой бирже по пути.
- Проходит по пути. Для каждой биржи по пути он отправляет входной токен, а затем вызывает функцию
swapэтой биржи. В большинстве случаев адресом назначения для токенов является следующая пара обмена на пути. На последней бирже это адрес, предоставленный трейдером.
В основном контракте (UniswapV2Pair.sol) {#in-the-core-contract-uniswapv2pairsol-2}5. Проверяет, что основной контракт не обманывают и что он может поддерживать достаточную ликвидность после обмена.
- Проверяет, сколько дополнительных токенов имеется сверх известных резервов. Эта сумма и есть количество входных токенов, полученных для обмена.
- Отправляет выходные токены по адресу назначения.
- Вызывает
_updateдля обновления размеров резервов
Возвращаемся к периферийному контракту (UniswapV2Router02.sol)
- Выполняет необходимую очистку (например, сжигает токены WETH, чтобы получить ETH для отправки трейдеру)
Добавление ликвидности
Вызывающая сторона
- Предоставляет периферийному аккаунту разрешение (allowance) на суммы, которые будут добавлены в пул ликвидности.
- Вызывает одну из функций
addLiquidityпериферийного контракта.
В периферийном контракте (UniswapV2Router02.sol)
- При необходимости создает новую пару обмена
- Если пара обмена уже существует, вычисляет количество добавляемых токенов. Предполагается, что стоимость обоих токенов одинакова, поэтому соотношение новых токенов к существующим должно быть одинаковым.
- Проверяет, являются ли суммы приемлемыми (вызывающие стороны могут указать минимальную сумму, ниже которой они не хотят добавлять ликвидность)
- Вызывает основной контракт.
В основном контракте (UniswapV2Pair.sol)
- Создает (минтит) токены ликвидности и отправляет их вызывающей стороне
- Вызывает
_updateдля обновления размеров резервов
Удаление ликвидности
Вызывающая сторона
- Предоставляет периферийному аккаунту разрешение (allowance) на токены ликвидности, которые будут сожжены в обмен на базовые токены.
- Вызывает одну из функций
removeLiquidityпериферийного контракта.
В периферийном контракте (UniswapV2Router02.sol)
- Отправляет токены ликвидности в пару обмена
В основном контракте (UniswapV2Pair.sol)
- Отправляет базовые токены на адрес назначения пропорционально сожженным токенам. Например, если в пуле есть 1000 токенов A, 500 токенов B и 90 токенов ликвидности, и мы получаем 9 токенов для сжигания, мы сжигаем 10% токенов ликвидности, поэтому мы возвращаем пользователю 100 токенов A и 50 токенов B.
- Сжигает токены ликвидности
- Вызывает
_updateдля обновления размеров резервов
Основные контракты
Это безопасные контракты, которые хранят ликвидность.
UniswapV2Pair.sol
Этот контракт (opens in a new tab) реализует фактический пул, который обменивает токены. Это основная функциональность Uniswap.
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Pair.sol';4import './UniswapV2ERC20.sol';5import './libraries/Math.sol';6import './libraries/UQ112x112.sol';7import './interfaces/IERC20.sol';8import './interfaces/IUniswapV2Factory.sol';9import './interfaces/IUniswapV2Callee.sol';Показать всеЭто все интерфейсы, о которых должен знать контракт, либо потому что контракт их реализует (IUniswapV2Pair и UniswapV2ERC20), либо потому что он вызывает контракты, которые их реализуют.
1contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {Этот контракт наследуется от UniswapV2ERC20, который предоставляет функции ERC-20 для токенов ликвидности.
1 using SafeMath for uint;Библиотека SafeMath (opens in a new tab) используется для предотвращения переполнений и опустошений. Это важно, потому что в противном случае мы можем столкнуться с ситуацией, когда значение должно быть -1, а вместо этого оно равно 2^256-1.
1 using UQ112x112 for uint224;Многие вычисления в контракте пула требуют дробей. Однако дроби не поддерживаются EVM.
Решение, которое нашел Uniswap, заключается в использовании 224-битных значений, где 112 бит отводится под целую часть и 112 бит — под дробную. Так, 1,0 представляется как 2^112, 1,5 — как 2^112 + 2^111 и т. д.
Более подробная информация об этой библиотеке доступна далее в документе.
Переменные
1 uint public constant MINIMUM_LIQUIDITY = 10**3;Чтобы избежать случаев деления на ноль, существует минимальное количество токенов ликвидности, которые всегда существуют (но принадлежат нулевому аккаунту). Это число равно MINIMUM_LIQUIDITY, то есть тысяче.
1 bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));Это селектор ABI для функции передачи ERC-20. Он используется для перевода токенов ERC-20 на два аккаунта токенов.
1 address public factory;Это контракт-фабрика, создавший этот пул. Каждый пул — это биржа между двумя токенами ERC-20, а фабрика — это центральная точка, которая соединяет все эти пулы.
1 address public token0;2 address public token1;Это адреса контрактов для двух типов токенов ERC-20, которые могут быть обменены в этом пуле.
1 uint112 private reserve0; // uses single storage slot, accessible via getReserves2 uint112 private reserve1; // uses single storage slot, accessible via getReservesРезервы пула для каждого типа токенов. Мы предполагаем, что оба они представляют одинаковую стоимость, и поэтому каждый token0 стоит reserve1/reserve0 токенов token1.
1 uint32 private blockTimestampLast; // uses single storage slot, accessible via getReservesВременная метка для последнего блока, в котором произошел обмен, используется для отслеживания обменных курсов с течением времени.
Одной из самых больших статей расхода газа в контрактах Ethereum является хранилище, которое сохраняется между вызовами контракта. Каждая ячейка хранилища имеет длину 256 бит. Поэтому три переменные, reserve0, reserve1 и blockTimestampLast, распределены таким образом, чтобы одно значение в хранилище могло включать все три (112+112+32=256).
1 uint public price0CumulativeLast;2 uint public price1CumulativeLast;Эти переменные хранят совокупные цены для каждого токена (каждая в единицах другого). Их можно использовать для расчета среднего обменного курса за определенный период времени.
1 uint public kLast; // reserve0 * reserve1, as of immediately after the most recent liquidity eventСпособ, которым пара обмена определяет обменный курс между token0 и token1, заключается в том, чтобы поддерживать постоянным произведение двух резервов во время торгов. Это значение — kLast. Оно изменяется, когда поставщик ликвидности вносит или выводит токены, и немного увеличивается из-за рыночной комиссии в 0,3 %.
Вот простой пример. Обратите внимание, что для простоты в таблице указано только три знака после запятой, и мы игнорируем торговую комиссию в 0,3 %, поэтому числа не являются точными.
| Событие | reserve0 | reserve1 | reserve0 * reserve1 | Средний обменный курс (token1 / token0) |
|---|---|---|---|---|
| Начальная установка | 1 000,000 | 1 000,000 | 1 000 000 | |
| Трейдер А обменивает 50 token0 на 47,619 token1 | 1 050,000 | 952,381 | 1 000 000 | 0,952 |
| Трейдер B обменивает 10 token0 на 8,984 token1 | 1 060,000 | 943,396 | 1 000 000 | 0,898 |
| Трейдер C обменивает 40 token0 на 34,305 token1 | 1 100,000 | 909,090 | 1 000 000 | 0,858 |
| Трейдер D обменивает 100 token1 на 109,01 token0 | 990,990 | 1 009,090 | 1 000 000 | 0,917 |
| Трейдер E обменивает 10 token0 на 10,079 token1 | 1 000,990 | 999,010 | 1 000 000 | 1,008 |
По мере того как трейдеры предоставляют больше token0, относительная стоимость token1 увеличивается, и наоборот, в зависимости от спроса и предложения.
Блокировка
1 uint private unlocked = 1;Существует класс уязвимостей безопасности, основанный на злоупотреблении повторным входом (opens in a new tab). Uniswap должен передавать произвольные токены ERC-20, что означает вызов контрактов ERC-20, которые могут попытаться злоупотребить рынком Uniswap, который их вызывает.
Имея переменную unlocked в составе контракта, мы можем предотвратить вызов функций во время их выполнения (в рамках одной транзакции).
1 modifier lock() {Эта функция является модификатором (opens in a new tab) — функцией, которая оборачивает обычную функцию, чтобы каким-либо образом изменить ее поведение.
1 require(unlocked == 1, 'UniswapV2: LOCKED');2 unlocked = 0;Если unlocked равно единице, установить его в ноль. Если оно уже равно нулю, отменить вызов, то есть сделать его неудачным.
1 _;В модификаторе _; — это исходный вызов функции (со всеми параметрами). Здесь это означает, что вызов функции происходит только в том случае, если unlocked было равно единице при вызове, и во время выполнения значение unlocked равно нулю.
1 unlocked = 1;2 }После возврата основной функции снимите блокировку.
Разное функции
1 function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {2 _reserve0 = reserve0;3 _reserve1 = reserve1;4 _blockTimestampLast = blockTimestampLast;5 }Эта функция предоставляет вызывающим сторонам текущее состояние биржи. Обратите внимание, что функции Solidity могут возвращать несколько значений (opens in a new tab).
1 function _safeTransfer(address token, address to, uint value) private {2 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));Эта внутренняя функция переводит определенное количество токенов ERC20 с биржи кому-то другому. SELECTOR указывает, что вызываемая нами функция — transfer(address,uint) (см. определение выше).
Чтобы не импортировать интерфейс для функции токена, мы «вручную» создаем вызов, используя одну из функций ABI (opens in a new tab).
1 require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');2 }Существует два способа, которыми вызов перевода ERC-20 может сообщить о сбое:
- Отмена. Если вызов внешнего контракта отменяется, то возвращаемое логическое значение равно
false - Завершиться нормально, но сообщить о сбое. В этом случае буфер возвращаемого значения имеет ненулевую длину, и при декодировании в качестве логического значения оно равно
false
Если происходит любое из этих условий, отмените операцию.
События
1 event Mint(address indexed sender, uint amount0, uint amount1);2 event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);Эти два события генерируются, когда поставщик ликвидности либо вносит ликвидность (Mint), либо выводит ее (Burn). В любом случае, количество внесенных или выведенных токенов token0 и token1 является частью события, как и идентификатор аккаунта, который нас вызвал (sender). В случае вывода событие также включает цель, которая получила токены (to), которая может не совпадать с отправителем.
1 event Swap(2 address indexed sender,3 uint amount0In,4 uint amount1In,5 uint amount0Out,6 uint amount1Out,7 address indexed to8 );Это событие генерируется, когда трейдер обменивает один токен на другой. Опять же, отправитель и получатель могут не совпадать. Каждый токен может быть либо отправлен на биржу, либо получен с нее.
1 event Sync(uint112 reserve0, uint112 reserve1);Наконец, Sync генерируется каждый раз, когда токены добавляются или выводятся, независимо от причины, для предоставления последней информации о резервах (и, следовательно, обменного курса).
Функции настройки
Эти функции должны быть вызваны один раз при настройке новой пары обмена.
1 constructor() public {2 factory = msg.sender;3 }Конструктор гарантирует, что мы будем отслеживать адрес фабрики, создавшей пару. Эта информация требуется для initialize и для комиссии фабрики (если таковая имеется)
1 // called once by the factory at time of deployment2 function initialize(address _token0, address _token1) external {3 require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check4 token0 = _token0;5 token1 = _token1;6 }Эта функция позволяет фабрике (и только фабрике) указать два токена ERC-20, которые эта пара будет обменивать.
Внутренние функции обновления
_update
1 // update reserves and, on the first call per block, price accumulators2 function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {Эта функция вызывается каждый раз, когда токены вносятся или выводятся.
1 require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');Если balance0 или balance1 (uint256) больше, чем uint112(-1) (=2^112-1) (то есть оно переполняется и возвращается к 0 при преобразовании в uint112), то _update не выполняется, чтобы предотвратить переполнения. Для обычного токена, который может быть разделен на 10^18 единиц, это означает, что каждая биржа ограничена примерно 5,1*10^15 каждого токена. До сих пор это не было проблемой.
1 uint32 blockTimestamp = uint32(block.timestamp % 2**32);2 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired3 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {Если прошедшее время не равно нулю, это означает, что мы являемся первой транзакцией обмена в этом блоке. В этом случае нам нужно обновить аккумуляторы цен.
1 // * never overflows, and + overflow is desired2 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;3 price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;4 }Каждый аккумулятор цен обновляется последней ценой (резерв другого токена/резерв этого токена), умноженной на прошедшее время в секундах. Чтобы получить среднюю цену, вы считываете совокупную цену в двух моментах времени и делите на разницу во времени между ними. Например, предположим следующую последовательность событий:
| Событие | reserve0 | reserve1 | временная метка | Предельный обменный курс (reserve1 / reserve0) | price0CumulativeLast |
|---|---|---|---|---|---|
| Начальная установка | 1 000,000 | 1 000,000 | 5 000 | 1,000 | 0 |
| Трейдер А вносит 50 token0 и получает обратно 47,619 token1 | 1 050,000 | 952,381 | 5 020 | 0,907 | 20 |
| Трейдер B вносит 10 token0 и получает обратно 8,984 token1 | 1 060,000 | 943,396 | 5 030 | 0,890 | 20+10*0.907 = 29.07 |
| Трейдер C вносит 40 token0 и получает обратно 34,305 token1 | 1 100,000 | 909,090 | 5 100 | 0,826 | 29.07+70*0.890 = 91.37 |
| Трейдер D вносит 100 token1 и получает обратно 109,01 token0 | 990,990 | 1 009,090 | 5 110 | 1,018 | 91.37+10*0.826 = 99.63 |
| Трейдер E вносит 10 token0 и получает обратно 10,079 token1 | 1 000,990 | 999,010 | 5 150 | 0,998 | 99.63+40*1.1018 = 143.702 |
Допустим, мы хотим рассчитать среднюю цену Token0 между временными метками 5030 и 5150. Разница в значении price0Cumulative составляет 143,702-29,07=114,632. Это среднее значение за две минуты (120 секунд). Таким образом, средняя цена составляет 114,632/120 = 0,955.
Этот расчет цены является причиной, по которой нам нужно знать старые размеры резервов.
1 reserve0 = uint112(balance0);2 reserve1 = uint112(balance1);3 blockTimestampLast = blockTimestamp;4 emit Sync(reserve0, reserve1);5 }Наконец, обновите глобальные переменные и сгенерируйте событие Sync.
_mintFee
1 // if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)2 function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {В Uniswap 2.0 трейдеры платят комиссию в размере 0,30 % за использование рынка. Большая часть этой комиссии (0,25 % от сделки) всегда идет поставщикам ликвидности. Оставшиеся 0,05 % могут пойти либо поставщикам ликвидности, либо на адрес, указанный фабрикой, в качестве комиссии протокола, которая оплачивает усилия Uniswap по разработке.
Для уменьшения вычислений (и, следовательно, затрат на газ) эта комиссия рассчитывается только при добавлении или удалении ликвидности из пула, а не при каждой транзакции.
1 address feeTo = IUniswapV2Factory(factory).feeTo();2 feeOn = feeTo != address(0);Прочитайте адрес назначения комиссии фабрики. Если он равен нулю, то комиссии протокола нет и нет необходимости ее рассчитывать.
1 uint _kLast = kLast; // gas savingsПеременная состояния kLast находится в хранилище, поэтому ее значение сохраняется между разными вызовами контракта.
Доступ к хранилищу намного дороже, чем доступ к временной памяти, которая освобождается по завершении вызова функции контракта, поэтому мы используем внутреннюю переменную для экономии газа.
1 if (feeOn) {2 if (_kLast != 0) {Поставщики ликвидности получают свою долю просто за счет роста стоимости их токенов ликвидности. Но комиссия протокола требует создания (минтинга) новых токенов ликвидности и их предоставления на адрес feeTo.
1 uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));2 uint rootKLast = Math.sqrt(_kLast);3 if (rootK > rootKLast) {Если есть новая ликвидность, с которой можно взимать комиссию протокола. Вы можете увидеть функцию извлечения квадратного корня далее в этой статье
1 uint numerator = totalSupply.mul(rootK.sub(rootKLast));2 uint denominator = rootK.mul(5).add(rootKLast);3 uint liquidity = numerator / denominator;Этот сложный расчет комиссий объясняется в техническом документе (whitepaper) (opens in a new tab) на странице 5. Мы знаем, что между моментом расчета kLast и настоящим моментом ликвидность не добавлялась и не удалялась (потому что мы выполняем этот расчет каждый раз при добавлении или удалении ликвидности, до того, как она фактически изменится), поэтому любое изменение в reserve0 * reserve1 должно происходить из-за комиссий за транзакции (без них мы бы сохраняли reserve0 * reserve1 постоянным).
1 if (liquidity > 0) _mint(feeTo, liquidity);2 }3 }Используйте функцию UniswapV2ERC20._mint для фактического создания дополнительных токенов ликвидности и их назначения feeTo.
1 } else if (_kLast != 0) {2 kLast = 0;3 }4 }Если комиссия не установлена, установите kLast в ноль (если это еще не так). Когда этот контракт был написан, существовала функция возврата газа (opens in a new tab), которая поощряла контракты уменьшать общий размер состояния Ethereum путем обнуления хранилища, которое им не нужно.
Этот код получает этот возврат, когда это возможно.
Внешне доступные функции
Обратите внимание, что хотя любая транзакция или контракт могут вызывать эти функции, они предназначены для вызова из периферийного контракта. Если вы вызовете их напрямую, вы не сможете обмануть пару обмена, но можете потерять стоимость из-за ошибки.
минтинг (создание)
1 // this low-level function should be called from a contract which performs important safety checks2 function mint(address to) external lock returns (uint liquidity) {Эта функция вызывается, когда поставщик ликвидности добавляет ликвидность в пул. Она создает (минтит) дополнительные токены ликвидности в качестве вознаграждения. Ее следует вызывать из периферийного контракта, который вызывает ее после добавления ликвидности в той же транзакции (чтобы никто другой не смог отправить транзакцию, претендующую на новую ликвидность до законного владельца).
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savingsЭто способ считывания результатов функции Solidity, которая возвращает несколько значений. Мы отбрасываем последнее возвращаемое значение, временную метку блока, потому что оно нам не нужно.
1 uint balance0 = IERC20(token0).balanceOf(address(this));2 uint balance1 = IERC20(token1).balanceOf(address(this));3 uint amount0 = balance0.sub(_reserve0);4 uint amount1 = balance1.sub(_reserve1);Получите текущие балансы и посмотрите, сколько было добавлено каждого типа токенов.
1 bool feeOn = _mintFee(_reserve0, _reserve1);Рассчитайте взимаемые комиссии протокола, если таковые имеются, и соответственно создайте (минтите) токены ликвидности. Поскольку параметры _mintFee — это старые значения резервов, комиссия рассчитывается точно, основываясь только на изменениях в пуле из-за комиссий.
1 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee2 if (_totalSupply == 0) {3 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);4 _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokensЕсли это первый депозит, создайте MINIMUM_LIQUIDITY токенов и отправьте их на нулевой адрес, чтобы заблокировать их. Их никогда нельзя будет выкупить, что означает, что пул никогда не будет полностью опустошен (это спасает нас от деления на ноль в некоторых местах). Значение MINIMUM_LIQUIDITY равно тысяче, что, учитывая, что большинство ERC-20 делятся на единицы, равные 10^-18 токена, как ETH делится на wei, составляет 10^-15 от стоимости одного токена. Невысокая цена.
Во время первого депозита мы не знаем относительной стоимости двух токенов, поэтому мы просто умножаем суммы и извлекаем квадратный корень, предполагая, что депозит предоставляет нам равную стоимость в обоих токенах.
Мы можем доверять этому, потому что в интересах вкладчика предоставить равную стоимость, чтобы избежать потери стоимости из-за арбитража. Допустим, стоимость двух токенов одинакова, но наш вкладчик внес в четыре раза больше Token1, чем Token0. Трейдер может использовать тот факт, что пара обмена считает, что Token0 более ценный, чтобы извлечь из него стоимость.
| Событие | reserve0 | reserve1 | reserve0 * reserve1 | Стоимость пула (reserve0 + reserve1) |
|---|---|---|---|---|
| Начальная установка | 8 | 32 | 256 | 40 |
| Трейдер вносит 8 токенов Token0, получает обратно 16 Token1 | 16 | 16 | 256 | 32 |
Как видите, трейдер заработал дополнительные 8 токенов, которые получены за счет уменьшения стоимости пула, что наносит ущерб вкладчику, которому он принадлежит.
1 } else {2 liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);При каждом последующем депозите мы уже знаем обменный курс между двумя активами и ожидаем, что поставщики ликвидности предоставят равную стоимость в обоих. Если они этого не делают, мы даем им токены ликвидности на основе меньшей предоставленной ими стоимости в качестве наказания.
Независимо от того, является ли это первоначальным депозитом или последующим, количество предоставляемых нами токенов ликвидности равно квадратному корню из изменения reserve0*reserve1, и стоимость токена ликвидности не меняется (если только мы не получаем депозит, в котором нет равных стоимостей обоих типов, в этом случае «штраф» распределяется). Вот еще один пример с двумя токенами, имеющими одинаковую стоимость, с тремя хорошими депозитами и одним плохим (депозит только одного типа токена, поэтому он не производит никаких токенов ликвидности).
| Событие | reserve0 | reserve1 | reserve0 * reserve1 | Стоимость пула (reserve0 + reserve1) | Токены ликвидности, созданные (сминченные) для этого депозита | Всего токенов ликвидности | стоимость каждого токена ликвидности |
|---|---|---|---|---|---|---|---|
| Начальная установка | 8,000 | 8,000 | 64 | 16,000 | 8 | 8 | 2,000 |
| Депозит по четыре каждого типа | 12,000 | 12,000 | 144 | 24,000 | 4 | 12 | 2,000 |
| Депозит по два каждого типа | 14,000 | 14,000 | 196 | 28,000 | 2 | 14 | 2,000 |
| Депозит с неравной стоимостью | 18,000 | 14,000 | 252 | 32,000 | 0 | 14 | ~2,286 |
| После арбитража | ~15,874 | ~15,874 | 252 | ~31,748 | 0 | 14 | ~2,267 |
1 }2 require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');3 _mint(to, liquidity);Используйте функцию UniswapV2ERC20._mint, чтобы фактически создать дополнительные токены ликвидности и передать их на правильный аккаунт.
12 _update(balance0, balance1, _reserve0, _reserve1);3 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date4 emit Mint(msg.sender, amount0, amount1);5 }Обновите переменные состояния (reserve0, reserve1 и, при необходимости, kLast) и сгенерируйте соответствующее событие.
сжигание
1 // this low-level function should be called from a contract which performs important safety checks2 function burn(address to) external lock returns (uint amount0, uint amount1) {Эта функция вызывается при выводе ликвидности, и соответствующие токены ликвидности должны быть сожжены. Ее также следует вызывать из периферийного аккаунта.
1 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings2 address _token0 = token0; // gas savings3 address _token1 = token1; // gas savings4 uint balance0 = IERC20(_token0).balanceOf(address(this));5 uint balance1 = IERC20(_token1).balanceOf(address(this));6 uint liquidity = balanceOf[address(this)];Периферийный контракт перевел ликвидность, подлежащую сжиганию, в этот контракт перед вызовом. Таким образом, мы знаем, сколько ликвидности сжечь, и можем убедиться, что она будет сожжена.
1 bool feeOn = _mintFee(_reserve0, _reserve1);2 uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee3 amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution4 amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution5 require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');Поставщик ликвидности получает равную стоимость обоих токенов. Таким образом, мы не меняем обменный курс.
1 _burn(address(this), liquidity);2 _safeTransfer(_token0, to, amount0);3 _safeTransfer(_token1, to, amount1);4 balance0 = IERC20(_token0).balanceOf(address(this));5 balance1 = IERC20(_token1).balanceOf(address(this));67 _update(balance0, balance1, _reserve0, _reserve1);8 if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date9 emit Burn(msg.sender, amount0, amount1, to);10 }11Показать всеОстальная часть функции burn является зеркальным отражением функции mint выше.
обмен
1 // this low-level function should be called from a contract which performs important safety checks2 function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {Эта функция также должна вызываться из периферийного контракта.
1 require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');2 (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings3 require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');45 uint balance0;6 uint balance1;7 { // scope for _token{0,1}, avoids stack too deep errorsЛокальные переменные могут храниться либо в памяти, либо, если их не слишком много, непосредственно в стеке. Если мы можем ограничить их количество, чтобы использовать стек, мы потребляем меньше газа. Для получения более подробной информации см. «желтую книгу», официальные спецификации Ethereum (opens in a new tab), стр. 26, уравнение 298.
1 address _token0 = token0;2 address _token1 = token1;3 require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');4 if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens5 if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokensЭтот перевод является оптимистичным, потому что мы переводим средства до того, как убедимся, что все условия выполнены. В Ethereum это допустимо, потому что если условия не будут выполнены позже в вызове, мы отменим его и все созданные им изменения.
1 if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);При необходимости сообщите получателю об обмене.
1 balance0 = IERC20(_token0).balanceOf(address(this));2 balance1 = IERC20(_token1).balanceOf(address(this));3 }Получение текущих балансов. Периферийный контракт отправляет нам токены перед тем, как вызвать нас для обмена. Это позволяет контракту легко проверить, что его не обманывают, — проверка, которая должна происходить в основном контракте (потому что нас могут вызывать другие сущности, кроме нашего периферийного контракта).
1 uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;2 uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;3 require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');4 { // scope for reserve{0,1}Adjusted, avoids stack too deep errors5 uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));6 uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));7 require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');Это проверка на вменяемость, чтобы убедиться, что мы не теряем на обмене. Ни при каких обстоятельствах обмен не должен уменьшать reserve0*reserve1. Здесь также мы гарантируем, что при обмене взимается комиссия в размере 0,3 %; перед проверкой значения K мы умножаем оба баланса на 1000 и вычитаем суммы, умноженные на 3, что означает, что 0,3 % (3/1000 = 0,003 = 0,3 %) вычитается из баланса перед сравнением его значения K с текущим значением K резервов.
1 }23 _update(balance0, balance1, _reserve0, _reserve1);4 emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);5 }Обновите reserve0 и reserve1, а при необходимости — аккумуляторы цен и временную метку, и сгенерируйте событие.
Синхронизация или снятие излишков
Возможно, что реальные балансы рассинхронизируются с резервами, которые, по мнению пары обмена, у нее есть.
Вывести токены без согласия контракта невозможно, но с депозитами дело обстоит иначе. Аккаунт может перевести токены на биржу, не вызывая ни mint, ни swap.
В этом случае есть два решения:
sync(синхронизация), обновить резервы до текущих балансовskim(снятие излишков), вывести лишнюю сумму. Обратите внимание, что любой аккаунт может вызыватьskim, потому что мы не знаем, кто внес токены. Эта информация генерируется в событии, но события недоступны из блокчейна.
1 // force balances to match reserves2 function skim(address to) external lock {3 address _token0 = token0; // gas savings4 address _token1 = token1; // gas savings5 _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));6 _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));7 }891011 // force reserves to match balances12 function sync() external lock {13 _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);14 }15}Показать всеUniswapV2Factory.sol
Этот контракт (opens in a new tab) создает пары обмена.
1pragma solidity =0.5.16;23import './interfaces/IUniswapV2Factory.sol';4import './UniswapV2Pair.sol';56contract UniswapV2Factory is IUniswapV2Factory {7 address public feeTo;8 address public feeToSetter;Эти переменные состояния необходимы для реализации комиссии протокола (см. технический документ (whitepaper) (opens in a new tab), стр. 5).
Адрес feeTo накапливает токены ликвидности для комиссии протокола, а feeToSetter — это адрес, которому разрешено изменять feeTo на другой адрес.
1 mapping(address => mapping(address => address)) public getPair;2 address[] public allPairs;Эти переменные отслеживают пары, то есть биржи между двумя типами токенов.
Первая, getPair, — это сопоставление (mapping), которое идентифицирует контракт пары обмена на основе двух токенов ERC-20, которые он обменивает. Токены ERC-20 идентифицируются по адресам контрактов, которые их реализуют, поэтому ключи и значение — все это адреса. Чтобы получить адрес пары обмена, которая позволяет вам конвертировать из tokenA в tokenB, вы используете getPair[<адрес tokenA>][<адрес tokenB>] (или наоборот).
Вторая переменная, allPairs, — это массив, который включает все адреса пар обмена, созданных этой фабрикой. В Ethereum вы не можете итерировать по содержимому сопоставления (mapping) или получить список всех ключей, поэтому эта переменная — единственный способ узнать, какими биржами управляет эта фабрика.
Примечание: причина, по которой вы не можете итерировать по всем ключам сопоставления, заключается в том, что хранение данных контракта дорого, поэтому чем меньше мы его используем, тем лучше, и чем реже мы его меняем, тем лучше. Вы можете создавать сопоставления, поддерживающие итерацию (opens in a new tab), но они требуют дополнительного хранилища для списка ключей. В большинстве приложений это не требуется.
1 event PairCreated(address indexed token0, address indexed token1, address pair, uint);Это событие генерируется при создании новой пары обмена. Оно включает адреса токенов, адрес пары обмена и общее количество бирж, управляемых фабрикой.
1 constructor(address _feeToSetter) public {2 feeToSetter = _feeToSetter;3 }Единственное, что делает конструктор, — это указывает feeToSetter. Фабрики начинают без комиссии, и только feeSetter может это изменить.
1 function allPairsLength() external view returns (uint) {2 return allPairs.length;3 }Эта функция возвращает количество пар обмена.
1 function createPair(address tokenA, address tokenB) external returns (address pair) {Это основная функция фабрики — создание пары обмена между двумя токенами ERC-20. Обратите внимание, что любой может вызвать эту функцию. Вам не нужно разрешение от Uniswap для создания новой пары обмена.
1 require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');2 (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);Мы хотим, чтобы адрес новой биржи был детерминированным, чтобы его можно было рассчитать заранее вне сети (это может быть полезно для транзакций второго уровня). Для этого нам нужен последовательный порядок адресов токенов, независимо от порядка, в котором мы их получили, поэтому мы сортируем их здесь.
1 require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');2 require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficientБольшие пулы ликвидности лучше маленьких, потому что у них более стабильные цены. Мы не хотим иметь более одного пула ликвидности на пару токенов. Если биржа уже существует, нет необходимости создавать еще одну для той же пары.
1 bytes memory bytecode = type(UniswapV2Pair).creationCode;Для создания нового контракта нам нужен код, который его создает (и функция конструктора, и код, который записывает в память байт-код EVM фактического контракта). Обычно в Solidity мы просто используем addr = new <имя контракта>(<параметры конструктора>), и компилятор делает все за нас, но для получения детерминированного адреса контракта нам нужно использовать опкод CREATE2 (opens in a new tab).
Когда этот код был написан, этот опкод еще не поддерживался Solidity, поэтому было необходимо вручную получать код. Это больше не проблема, потому что Solidity теперь поддерживает CREATE2 (opens in a new tab).
1 bytes32 salt = keccak256(abi.encodePacked(token0, token1));2 assembly {3 pair := create2(0, add(bytecode, 32), mload(bytecode), salt)4 }Когда опкод еще не поддерживается Solidity, мы можем вызвать его с помощью встроенного ассемблера (opens in a new tab).
1 IUniswapV2Pair(pair).initialize(token0, token1);Вызовите функцию initialize, чтобы сообщить новой бирже, какие два токена она обменивает.
1 getPair[token0][token1] = pair;2 getPair[token1][token0] = pair; // populate mapping in the reverse direction3 allPairs.push(pair);4 emit PairCreated(token0, token1, pair, allPairs.length);5 }Сохраните информацию о новой паре в переменных состояния и сгенерируйте событие, чтобы сообщить миру о новой паре обмена.
1 function setFeeTo(address _feeTo) external {2 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');3 feeTo = _feeTo;4 }56 function setFeeToSetter(address _feeToSetter) external {7 require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');8 feeToSetter = _feeToSetter;9 }10}Показать всеЭти две функции позволяют feeSetter контролировать получателя комиссии (если таковой имеется) и изменять feeSetter на новый адрес.
UniswapV2ERC20.sol
Этот контракт (opens in a new tab) реализует токен ликвидности ERC-20. Он похож на контракт ERC-20 от OpenZeppelin, поэтому я объясню только ту часть, которая отличается, — функциональность permit.
Транзакции в Ethereum стоят эфира (ETH), что эквивалентно реальным деньгам. Если у вас есть токены ERC-20, но нет ETH, вы не можете отправлять транзакции, поэтому вы ничего не можете с ними сделать. Одним из решений этой проблемы являются метатранзакции (opens in a new tab). Владелец токенов подписывает транзакцию, которая позволяет кому-то другому вывести токены вне сети, и отправляет ее через Интернет получателю. Получатель, у которого есть ETH, затем отправляет разрешение от имени владельца.
1 bytes32 public DOMAIN_SEPARATOR;2 // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");3 bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;Этот хэш является идентификатором типа транзакции (opens in a new tab). Единственный, который мы здесь поддерживаем, — это Permit с этими параметрами.
1 mapping(address => uint) public nonces;Получателю невозможно подделать цифровую подпись. Однако тривиально отправить одну и ту же транзакцию дважды (это форма атаки повторения (opens in a new tab)). Чтобы предотвратить это, мы используем nonce (opens in a new tab). Если nonce нового Permit не на единицу больше, чем последний использованный, мы считаем его недействительным.
1 constructor() public {2 uint chainId;3 assembly {4 chainId := chainid5 }Это код для получения идентификатора сети (opens in a new tab). Он использует диалект ассемблера EVM под названием Yul (opens in a new tab). Обратите внимание, что в текущей версии Yul вы должны использовать chainid(), а не chainid.
1 DOMAIN_SEPARATOR = keccak256(2 abi.encode(3 keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),4 keccak256(bytes(name)),5 keccak256(bytes('1')),6 chainId,7 address(this)8 )9 );10 }Показать всеРассчитайте разделитель домена (opens in a new tab) для EIP-712.
1 function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {Это функция, которая реализует разрешения. Она получает в качестве параметров соответствующие поля и три скалярных значения для подписи (opens in a new tab) (v, r и s).
1 require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');Не принимать транзакции после крайнего срока.
1 bytes32 digest = keccak256(2 abi.encodePacked(3 '\x19\x01',4 DOMAIN_SEPARATOR,5 keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))6 )7 );abi.encodePacked(...) — это сообщение, которое мы ожидаем получить. Мы знаем, каким должен быть nonce, поэтому нам не нужно получать его в качестве параметра.
Алгоритм подписи Ethereum ожидает получить 256 бит для подписи, поэтому мы используем хэш-функцию keccak256.
1 address recoveredAddress = ecrecover(digest, v, r, s);Из дайджеста и подписи мы можем получить адрес, который его подписал, с помощью ecrecover (opens in a new tab).
1 require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');2 _approve(owner, spender, value);3 }4Если все в порядке, рассматривайте это как одобрение ERC-20 (opens in a new tab).
Периферийные контракты
Периферийные контракты — это API (интерфейс прикладного программирования) для Uniswap. Они доступны для внешних вызовов, либо из других контрактов, либо из децентрализованных приложений. Вы могли бы вызывать основные контракты напрямую, но это сложнее, и вы можете потерять стоимость, если совершите ошибку. Основные контракты содержат только тесты, чтобы убедиться, что их не обманывают, а не проверки на вменяемость для кого-либо еще. Они находятся на периферии, чтобы их можно было обновлять по мере необходимости.
UniswapV2Router01.sol
Этот контракт (opens in a new tab) имеет проблемы и больше не должен использоваться (opens in a new tab). К счастью, периферийные контракты не имеют состояния и не хранят никаких активов, поэтому его легко объявить устаревшим и предложить людям использовать вместо него замену — UniswapV2Router02.
UniswapV2Router02.sol
В большинстве случаев вы будете использовать Uniswap через этот контракт (opens in a new tab). Вы можете увидеть, как его использовать, здесь (opens in a new tab).
1pragma solidity =0.6.6;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';4import '@uniswap/lib/contracts/libraries/TransferHelper.sol';56import './interfaces/IUniswapV2Router02.sol';7import './libraries/UniswapV2Library.sol';8import './libraries/SafeMath.sol';9import './interfaces/IERC20.sol';10import './interfaces/IWETH.sol';Показать всеБольшинство из этого мы либо встречали раньше, либо они довольно очевидны. Единственное исключение — IWETH.sol. Uniswap v2 позволяет обменивать любую пару токенов ERC-20, но сам эфир (ETH) не является токеном ERC-20. Он появился раньше стандарта и передается с помощью уникальных механизмов. Чтобы обеспечить использование ETH в контрактах, применяемых к токенам ERC-20, люди придумали контракт обернутого эфира (WETH) (opens in a new tab). Вы отправляете этому контракту ETH, и он создает (минтит) вам эквивалентное количество WETH. Или вы можете сжечь WETH и получить обратно ETH.
1contract UniswapV2Router02 is IUniswapV2Router02 {2 using SafeMath for uint;34 address public immutable override factory;5 address public immutable override WETH;Маршрутизатору нужно знать, какую фабрику использовать, а для транзакций, требующих WETH, — какой контракт WETH использовать. Эти значения неизменяемы (opens in a new tab), что означает, что их можно установить только в конструкторе. Это дает пользователям уверенность в том, что никто не сможет изменить их, чтобы они указывали на менее честные контракты.
1 modifier ensure(uint deadline) {2 require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');3 _;4 }Этот модификатор гарантирует, что транзакции с ограниченным временем («сделать X до времени Y, если возможно») не будут выполнены после истечения их срока.
1 constructor(address _factory, address _WETH) public {2 factory = _factory;3 WETH = _WETH;4 }Конструктор просто устанавливает неизменяемые переменные состояния.
1 receive() external payable {2 assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract3 }Эта функция вызывается, когда мы обмениваем токены из контракта WETH обратно на ETH. Только используемый нами контракт WETH уполномочен делать это.
Добавление ликвидности
Эти функции добавляют токены в пару обмена, что увеличивает пул ликвидности.
12 // **** ADD LIQUIDITY ****3 function _addLiquidity(Эта функция используется для расчета количества токенов A и B, которые должны быть внесены в пару обмена.
1 address tokenA,2 address tokenB,Это адреса контрактов токенов ERC-20.
1 uint amountADesired,2 uint amountBDesired,Это суммы, которые поставщик ликвидности хочет внести. Они также являются максимальными суммами A и B, которые будут внесены.
1 uint amountAMin,2 uint amountBMinЭто минимально приемлемые суммы для внесения. Если транзакция не может быть выполнена с этими суммами или большими, отмените ее. Если вы не хотите использовать эту функцию, просто укажите ноль.
Поставщики ликвидности обычно указывают минимум, потому что они хотят ограничить транзакцию обменным курсом, близким к текущему. Если обменный курс колеблется слишком сильно, это может означать новости, которые изменяют базовые значения, и они хотят решить вручную, что делать.
Например, представьте случай, когда обменный курс равен один к одному, и поставщик ликвидности указывает эти значения:
| Параметр | Значение |
|---|---|
| amountADesired | 1000 |
| amountBDesired | 1000 |
| amountAMin | 900 |
| amountBMin | 800 |
Пока обменный курс остается в диапазоне от 0,9 до 1,25, транзакция выполняется. Если обменный курс выходит за пределы этого диапазона, транзакция отменяется.
Причина этой предосторожности в том, что транзакции не являются мгновенными, вы отправляете их, и в конечном итоге валидатор включит их в блок (если только ваша цена на газ не очень низкая, в этом случае вам нужно будет отправить другую транзакцию с тем же nonce и более высокой ценой на газ, чтобы перезаписать ее). Вы не можете контролировать, что происходит в промежутке между отправкой и включением.
1 ) internal virtual returns (uint amountA, uint amountB) {Функция возвращает суммы, которые поставщик ликвидности должен внести, чтобы соотношение было равно текущему соотношению между резервами.
1 // create the pair if it doesn't exist yet2 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {3 IUniswapV2Factory(factory).createPair(tokenA, tokenB);4 }Если биржи для этой пары токенов еще нет, создайте ее.
1 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);Получите текущие резервы в паре.
1 if (reserveA == 0 && reserveB == 0) {2 (amountA, amountB) = (amountADesired, amountBDesired);Если текущие резервы пусты, то это новая пара обмена. Суммы, подлежащие внесению, должны быть точно такими же, как те, которые хочет предоставить поставщик ликвидности.
1 } else {2 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);Если нам нужно посмотреть, какими будут суммы, мы получаем оптимальную сумму с помощью этой функции (opens in a new tab). Мы хотим такое же соотношение, как и у текущих резервов.
1 if (amountBOptimal <= amountBDesired) {2 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 (amountA, amountB) = (amountADesired, amountBOptimal);Если amountBOptimal меньше суммы, которую поставщик ликвидности хочет внести, это означает, что токен B в настоящее время более ценен, чем думает поставщик ликвидности, поэтому требуется меньшая сумма.
1 } else {2 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);3 assert(amountAOptimal <= amountADesired);4 require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');5 (amountA, amountB) = (amountAOptimal, amountBDesired);Если оптимальная сумма B больше желаемой суммы B, это означает, что токены B в настоящее время менее ценны, чем думает поставщик ликвидности, поэтому требуется большая сумма. Однако желаемая сумма является максимальной, поэтому мы не можем этого сделать. Вместо этого мы рассчитываем оптимальное количество токенов A для желаемого количества токенов B.
Собрав все вместе, мы получаем этот график. Предположим, вы пытаетесь внести тысячу токенов A (синяя линия) и тысячу токенов B (красная линия). Ось X — это обменный курс, A/B. Если x=1, они равны по стоимости, и вы вносите по тысяче каждого. Если x=2, A в два раза дороже B (вы получаете два токена B за каждый токен A), поэтому вы вносите тысячу токенов B, но только 500 токенов A. Если x=0,5, ситуация обратная: тысяча токенов A и пятьсот токенов B.
Вы могли бы внести ликвидность непосредственно в основной контракт (используя UniswapV2Pair::mint (opens in a new tab)), но основной контракт проверяет только то, что его не обманывают, поэтому вы рискуете потерять стоимость, если обменный курс изменится между моментом отправки вашей транзакции и моментом ее выполнения. Если вы используете периферийный контракт, он вычисляет сумму, которую вы должны внести, и немедленно вносит ее, поэтому обменный курс не меняется, и вы ничего не теряете.
1 function addLiquidity(2 address tokenA,3 address tokenB,4 uint amountADesired,5 uint amountBDesired,6 uint amountAMin,7 uint amountBMin,8 address to,9 uint deadlineПоказать всеЭта функция может быть вызвана транзакцией для внесения ликвидности. Большинство параметров такие же, как в _addLiquidity выше, за двумя исключениями:
. to — это адрес, который получает новые токены ликвидности, созданные (сминченные), чтобы показать долю поставщика ликвидности в пуле
. deadline — это ограничение по времени на транзакцию
1 ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {2 (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);3 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);Мы рассчитываем суммы для фактического внесения, а затем находим адрес пула ликвидности. Для экономии газа мы делаем это не путем запроса к фабрике, а с помощью библиотечной функции pairFor (см. ниже в библиотеках)
1 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);2 TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);Переведите правильные суммы токенов от пользователя в пару обмена.
1 liquidity = IUniswapV2Pair(pair).mint(to);Взамен дайте адресу to токены ликвидности для частичного владения пулом. Функция mint основного контракта видит, сколько у него лишних токенов (по сравнению с тем, что было в последний раз, когда менялась ликвидность), и соответственно создает (минтит) ликвидность.
1 function addLiquidityETH(2 address token,3 uint amountTokenDesired,Когда поставщик ликвидности хочет предоставить ликвидность в паре обмена Токен/ETH, есть несколько отличий. Контракт обрабатывает обертывание ETH для поставщика ликвидности. Нет необходимости указывать, сколько ETH хочет внести пользователь, потому что пользователь просто отправляет их с транзакцией (сумма доступна в msg.value).
1 uint amountTokenMin,2 uint amountETHMin,3 address to,4 uint deadline5 ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {6 (amountToken, amountETH) = _addLiquidity(7 token,8 WETH,9 amountTokenDesired,10 msg.value,11 amountTokenMin,12 amountETHMin13 );14 address pair = UniswapV2Library.pairFor(factory, token, WETH);15 TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);16 IWETH(WETH).deposit{value: amountETH}();17 assert(IWETH(WETH).transfer(pair, amountETH));Показать всеДля внесения ETH контракт сначала оборачивает его в WETH, а затем переводит WETH в пару. Обратите внимание, что перевод обернут в assert. Это означает, что если перевод не удастся, этот вызов контракта также не удастся, и, следовательно, обертывание на самом деле не произойдет.
1 liquidity = IUniswapV2Pair(pair).mint(to);2 // refund dust eth, if any3 if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);Пользователь уже отправил нам ETH, поэтому, если осталась какая-либо лишняя сумма (потому что другой токен менее ценен, чем думал пользователь), нам нужно произвести возврат.
Удаление ликвидности
Эти функции удалят ликвидность и вернут средства поставщику ликвидности.
1 // **** REMOVE LIQUIDITY ****2 function removeLiquidity(3 address tokenA,4 address tokenB,5 uint liquidity,6 uint amountAMin,7 uint amountBMin,8 address to,9 uint deadline10 ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {Показать всеПростейший случай удаления ликвидности. Существует минимальная сумма каждого токена, которую поставщик ликвидности соглашается принять, и это должно произойти до крайнего срока.
1 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);2 IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair3 (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);Функция burn основного контракта обрабатывает возврат токенов пользователю.
1 (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);Когда функция возвращает несколько значений, но нас интересуют только некоторые из них, вот как мы получаем только эти значения. Это несколько дешевле с точки зрения газа, чем считывание значения и его неиспользование.
1 (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);Переведите суммы из способа, которым их возвращает основной контракт (сначала токен с меньшим адресом), в способ, который ожидает пользователь (соответствующий tokenA и tokenB).
1 require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');2 require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');3 }Допустимо сначала выполнить перевод, а затем проверить его законность, потому что если это не так, мы отменим все изменения состояния.
1 function removeLiquidityETH(2 address token,3 uint liquidity,4 uint amountTokenMin,5 uint amountETHMin,6 address to,7 uint deadline8 ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {9 (amountToken, amountETH) = removeLiquidity(10 token,11 WETH,12 liquidity,13 amountTokenMin,14 amountETHMin,15 address(this),16 deadline17 );18 TransferHelper.safeTransfer(token, to, amountToken);19 IWETH(WETH).withdraw(amountETH);20 TransferHelper.safeTransferETH(to, amountETH);21 }Показать всеУдаление ликвидности для ETH почти такое же, за исключением того, что мы получаем токены WETH, а затем обмениваем их на ETH, чтобы вернуть поставщику ликвидности.
1 function removeLiquidityWithPermit(2 address tokenA,3 address tokenB,4 uint liquidity,5 uint amountAMin,6 uint amountBMin,7 address to,8 uint deadline,9 bool approveMax, uint8 v, bytes32 r, bytes32 s10 ) external virtual override returns (uint amountA, uint amountB) {11 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);12 uint value = approveMax ? uint(-1) : liquidity;13 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);14 (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);15 }161718 function removeLiquidityETHWithPermit(19 address token,20 uint liquidity,21 uint amountTokenMin,22 uint amountETHMin,23 address to,24 uint deadline,25 bool approveMax, uint8 v, bytes32 r, bytes32 s26 ) external virtual override returns (uint amountToken, uint amountETH) {27 address pair = UniswapV2Library.pairFor(factory, token, WETH);28 uint value = approveMax ? uint(-1) : liquidity;29 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);30 (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);31 }Показать всеЭти функции передают метатранзакции, чтобы позволить пользователям без эфира выводить средства из пула, используя механизм разрешений.
12 // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****3 function removeLiquidityETHSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint deadline10 ) public virtual override ensure(deadline) returns (uint amountETH) {11 (, amountETH) = removeLiquidity(12 token,13 WETH,14 liquidity,15 amountTokenMin,16 amountETHMin,17 address(this),18 deadline19 );20 TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));21 IWETH(WETH).withdraw(amountETH);22 TransferHelper.safeTransferETH(to, amountETH);23 }24Показать всеЭта функция может использоваться для токенов, у которых есть комиссии за перевод или хранение. Когда у токена есть такие комиссии, мы не можем полагаться на функцию removeLiquidity, чтобы узнать, сколько токена мы получим обратно, поэтому нам нужно сначала вывести средства, а затем получить баланс.
123 function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(4 address token,5 uint liquidity,6 uint amountTokenMin,7 uint amountETHMin,8 address to,9 uint deadline,10 bool approveMax, uint8 v, bytes32 r, bytes32 s11 ) external virtual override returns (uint amountETH) {12 address pair = UniswapV2Library.pairFor(factory, token, WETH);13 uint value = approveMax ? uint(-1) : liquidity;14 IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);15 amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(16 token, liquidity, amountTokenMin, amountETHMin, to, deadline17 );18 }Показать всеПоследняя функция объединяет плату за место хранения с метатранзакциями.
Торговля
1 // **** ОБМЕН ****2 // требует, чтобы начальная сумма уже была отправлена первой паре3 function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {Эта функция выполняет внутреннюю обработку, которая требуется для функций, доступных для трейдеров.
1 for (uint i; i < path.length - 1; i++) {На момент написания этой статьи существует 388 160 токенов ERC-20 (opens in a new tab). Если бы для каждой пары токенов существовала парная биржа, то их было бы более 150 миллиардов. Во всей сети на данный момент всего 0,1 % от этого количества аккаунтов (opens in a new tab). Вместо этого функции обмена поддерживают концепцию пути. Трейдер может обменять A на B, B на C и C на D, поэтому нет необходимости в прямой парной бирже A-D.
Цены на этих рынках имеют тенденцию к синхронизации, потому что, когда они рассинхронизированы, это создает возможность для арбитража. Представьте, например, три токена: A, B и C. Есть три парные биржи, по одной для каждой пары.
- Исходная ситуация
- Трейдер продает 24,695 токенов A и получает 25,305 токенов B.
- Трейдер продает 24,695 токенов B за 25,305 токенов C, оставляя примерно 0,61 токена B в качестве прибыли.
- Затем трейдер продает 24,695 токенов C за 25,305 токенов A, оставляя примерно 0,61 токена C в качестве прибыли. У трейдера также есть 0,61 лишних токенов A (25,305, которые трейдер получает в итоге, минус первоначальные инвестиции в размере 24,695).
| Шаг | Биржа A-B | Биржа B-C | Биржа A-C |
|---|---|---|---|
| 1 | A:1000 B:1050 A/B=1.05 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
| 2 | A:1024.695 B:1024.695 A/B=1 | B:1000 C:1050 B/C=1.05 | A:1050 C:1000 C/A=1.05 |
| 3 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1050 C:1000 C/A=1.05 |
| 4 | A:1024.695 B:1024.695 A/B=1 | B:1024.695 C:1024.695 B/C=1 | A:1024.695 C:1024.695 C/A=1 |
1 (address input, address output) = (path[i], path[i + 1]);2 (address token0,) = UniswapV2Library.sortTokens(input, output);3 uint amountOut = amounts[i + 1];Получаем пару, которую мы сейчас обрабатываем, сортируем ее (для использования с парой) и получаем ожидаемую выходную сумму.
1 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));Получаем ожидаемые выходные суммы, отсортированные так, как их ожидает парная биржа.
1 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;Это последняя биржа? Если да, отправляем токены, полученные в результате сделки, по месту назначения. Если нет, отправляем их на следующую парную биржу.
12 IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(3 amount0Out, amount1Out, to, new bytes(0)4 );5 }6 }Фактически вызываем парную биржу, чтобы обменять токены. Нам не нужен обратный вызов для уведомления о бирже, поэтому мы не отправляем никаких байтов в это поле.
1 function swapExactTokensForTokens(Эта функция используется трейдерами напрямую для обмена одного токена на другой.
1 uint amountIn,2 uint amountOutMin,3 address[] calldata path,Этот параметр содержит адреса контрактов ERC-20. Как объяснялось выше, это массив, потому что вам может понадобиться пройти через несколько парных бирж, чтобы перейти от актива, который у вас есть, к активу, который вы хотите.
Параметр функции в Solidity может храниться либо в memory, либо в calldata. Если функция является точкой входа в контракт, вызываемой непосредственно пользователем (с помощью транзакции) или из другого контракта, то значение параметра можно взять непосредственно из данных вызова. Если функция вызывается внутри, как _swap выше, то параметры должны храниться в memory. С точки зрения вызываемого контракта calldata предназначены только для чтения.
Для скалярных типов, таких как uint или address, компилятор сам выбирает место хранения, но для массивов, которые длиннее и дороже, мы указываем тип используемого места хранения.
1 address to,2 uint deadline3 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {Возвращаемые значения всегда возвращаются в памяти.
1 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);2 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');Рассчитываем сумму для покупки при каждом обмене. Если результат меньше минимума, который готов принять трейдер, откатываем транзакцию.
1 TransferHelper.safeTransferFrom(2 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]3 );4 _swap(amounts, path, to);5 }Наконец, переводим начальный токен ERC-20 на аккаунт первой парной биржи и вызываем _swap. Все это происходит в рамках одной транзакции, поэтому парная биржа знает, что любые неожиданные токены являются частью этого перевода.
1 function swapTokensForExactTokens(2 uint amountOut,3 uint amountInMax,4 address[] calldata path,5 address to,6 uint deadline7 ) external virtual override ensure(deadline) returns (uint[] memory amounts) {8 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);9 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');10 TransferHelper.safeTransferFrom(11 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]12 );13 _swap(amounts, path, to);14 }Показать всеПредыдущая функция, swapTokensForTokens, позволяет трейдеру указать точное количество входных токенов, которое он готов отдать, и минимальное количество выходных токенов, которое он готов получить взамен. Эта функция выполняет обратный обмен: она позволяет трейдеру указать количество выходных токенов, которое он хочет получить, и максимальное количество входных токенов, которое он готов за них заплатить.
В обоих случаях трейдер должен сначала предоставить этому периферийному контракту разрешение на перевод токенов.
1 function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)2 external3 virtual4 override5 payable6 ensure(deadline)7 returns (uint[] memory amounts)8 {9 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');10 amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);11 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');12 IWETH(WETH).deposit{value: amounts[0]}();13 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));14 _swap(amounts, path, to);15 }161718 function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)19 external20 virtual21 override22 ensure(deadline)23 returns (uint[] memory amounts)24 {25 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');26 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);27 require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');28 TransferHelper.safeTransferFrom(29 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]30 );31 _swap(amounts, path, address(this));32 IWETH(WETH).withdraw(amounts[amounts.length - 1]);33 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);34 }35363738 function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)39 external40 virtual41 override42 ensure(deadline)43 returns (uint[] memory amounts)44 {45 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');46 amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);47 require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');48 TransferHelper.safeTransferFrom(49 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]50 );51 _swap(amounts, path, address(this));52 IWETH(WETH).withdraw(amounts[amounts.length - 1]);53 TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);54 }555657 function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)58 external59 virtual60 override61 payable62 ensure(deadline)63 returns (uint[] memory amounts)64 {65 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');66 amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);67 require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');68 IWETH(WETH).deposit{value: amounts[0]}();69 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));70 _swap(amounts, path, to);71 // возврат остатка eth, если он есть72 if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);73 }Показать всеВсе эти четыре варианта связаны с торговлей между ETH и токенами. Единственное различие заключается в том, что мы либо получаем ETH от трейдера и используем его для минта WETH, либо мы получаем WETH с последней биржи на пути и сжигаем его, отправляя трейдеру обратно полученный ETH.
1 // **** ОБМЕН (с поддержкой токенов с комиссией за перевод) ****2 // требует, чтобы начальная сумма уже была отправлена первой паре3 function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {Это внутренняя функция для обмена токенов, имеющих комиссию за перевод или хранение, для решения (этой проблемы (opens in a new tab)).
1 for (uint i; i < path.length - 1; i++) {2 (address input, address output) = (path[i], path[i + 1]);3 (address token0,) = UniswapV2Library.sortTokens(input, output);4 IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));5 uint amountInput;6 uint amountOutput;7 { // область видимости для предотвращения ошибок переполнения стека8 (uint reserve0, uint reserve1,) = pair.getReserves();9 (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);10 amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);11 amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);Показать всеИз-за комиссий за перевод мы не можем полагаться на функцию getAmountsOut, чтобы узнать, сколько мы получим от каждого перевода (как мы это делаем перед вызовом исходной функции _swap). Вместо этого мы должны сначала выполнить перевод, а затем посмотреть, сколько токенов мы получили обратно.
Примечание. Теоретически мы могли бы просто использовать эту функцию вместо _swap, но в некоторых случаях (например, если перевод будет отменен из-за того, что в конце не хватит средств для достижения требуемого минимума) это приведет к большим затратам газа. Токены с комиссией за перевод встречаются довольно редко, поэтому, хотя нам и нужно их учитывать, нет необходимости, чтобы все обмены предполагали прохождение хотя бы через один из них.
1 }2 (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));3 address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;4 pair.swap(amount0Out, amount1Out, to, new bytes(0));5 }6 }789 function swapExactTokensForTokensSupportingFeeOnTransferTokens(10 uint amountIn,11 uint amountOutMin,12 address[] calldata path,13 address to,14 uint deadline15 ) external virtual override ensure(deadline) {16 TransferHelper.safeTransferFrom(17 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn18 );19 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);20 _swapSupportingFeeOnTransferTokens(path, to);21 require(22 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,23 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'24 );25 }262728 function swapExactETHForTokensSupportingFeeOnTransferTokens(29 uint amountOutMin,30 address[] calldata path,31 address to,32 uint deadline33 )34 external35 virtual36 override37 payable38 ensure(deadline)39 {40 require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');41 uint amountIn = msg.value;42 IWETH(WETH).deposit{value: amountIn}();43 assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));44 uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);45 _swapSupportingFeeOnTransferTokens(path, to);46 require(47 IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,48 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'49 );50 }515253 function swapExactTokensForETHSupportingFeeOnTransferTokens(54 uint amountIn,55 uint amountOutMin,56 address[] calldata path,57 address to,58 uint deadline59 )60 external61 virtual62 override63 ensure(deadline)64 {65 require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');66 TransferHelper.safeTransferFrom(67 path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn68 );69 _swapSupportingFeeOnTransferTokens(path, address(this));70 uint amountOut = IERC20(WETH).balanceOf(address(this));71 require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');72 IWETH(WETH).withdraw(amountOut);73 TransferHelper.safeTransferETH(to, amountOut);74 }Показать всеЭто те же варианты, которые используются для обычных токенов, но вместо этого они вызывают _swapSupportingFeeOnTransferTokens.
1 // **** БИБЛИОТЕЧНЫЕ ФУНКЦИИ ****2 function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {3 return UniswapV2Library.quote(amountA, reserveA, reserveB);4 }56 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)7 public8 pure9 virtual10 override11 returns (uint amountOut)12 {13 return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);14 }1516 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)17 public18 pure19 virtual20 override21 returns (uint amountIn)22 {23 return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);24 }2526 function getAmountsOut(uint amountIn, address[] memory path)27 public28 view29 virtual30 override31 returns (uint[] memory amounts)32 {33 return UniswapV2Library.getAmountsOut(factory, amountIn, path);34 }3536 function getAmountsIn(uint amountOut, address[] memory path)37 public38 view39 virtual40 override41 returns (uint[] memory amounts)42 {43 return UniswapV2Library.getAmountsIn(factory, amountOut, path);44 }45}Показать всеЭти функции — просто прокси, которые вызывают функции UniswapV2Library.
UniswapV2Migrator.sol
Этот контракт использовался для миграции бирж со старой версии v1 на v2. Теперь, когда они были перенесены, он больше не актуален.
Библиотеки
Библиотека SafeMath (opens in a new tab) хорошо задокументирована, поэтому нет необходимости документировать ее здесь.
Математика
Эта библиотека содержит некоторые математические функции, которые обычно не нужны в коде Solidity, поэтому они не являются частью языка.
1pragma solidity =0.5.16;23// библиотека для выполнения различных математических операций45library Math {6 function min(uint x, uint y) internal pure returns (uint z) {7 z = x < y ? x : y;8 }910 // вавилонский метод (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)11 function sqrt(uint y) internal pure returns (uint z) {12 if (y > 3) {13 z = y;14 uint x = y / 2 + 1;Показать всеНачинаем с x в качестве оценки, которая выше квадратного корня (именно поэтому нам нужно рассматривать 1-3 как особые случаи).
1 while (x < z) {2 z = x;3 x = (y / x + x) / 2;Получаем более точную оценку — среднее значение предыдущей оценки и числа, квадратный корень которого мы пытаемся найти, деленного на предыдущую оценку. Повторяем до тех пор, пока новая оценка не станет ниже существующей. Подробнее смотрите здесь (opens in a new tab).
1 }2 } else if (y != 0) {3 z = 1;Нам никогда не понадобится квадратный корень из нуля. Квадратные корни из одного, двух и трех примерно равны единице (мы используем целые числа, поэтому игнорируем дробную часть).
1 }2 }3}Дроби с фиксированной точкой (UQ112x112)
Эта библиотека обрабатывает дроби, которые обычно не являются частью арифметики Ethereum. Это достигается путем кодирования числа x как x*2^112. Это позволяет нам использовать исходные опкоды сложения и вычитания без изменений.
1pragma solidity =0.5.16;23// библиотека для работы с двоичными числами с фиксированной точкой (https://wikipedia.org/wiki/Q_(number_format))45// диапазон: [0, 2**112 - 1]6// разрешение: 1 / 2**11278library UQ112x112 {9 uint224 constant Q112 = 2**112;Показать всеQ112 — это кодировка единицы.
1 // кодирует uint112 как UQ112x1122 function encode(uint112 y) internal pure returns (uint224 z) {3 z = uint224(y) * Q112; // никогда не вызывает переполнения4 }Поскольку y — это uint112, максимальное значение, которое он может принять, равно 2^112-1. Это число все еще может быть закодировано как UQ112x112.
1 // делит UQ112x112 на uint112, возвращая UQ112x1122 function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {3 z = x / uint224(y);4 }5}Если мы разделим два значения UQ112x112, результат больше не будет умножаться на 2^112. Поэтому вместо этого мы берем целое число в качестве знаменателя. Нам пришлось бы использовать похожий трюк для умножения, но нам не нужно умножать значения UQ112x112.
UniswapV2Library
Эта библиотека используется только периферийными контрактами.
1pragma solidity >=0.5.0;23import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';45import "./SafeMath.sol";67library UniswapV2Library {8 using SafeMath for uint;910 // возвращает отсортированные адреса токенов, используется для обработки возвращаемых значений из пар, отсортированных в этом порядке11 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {12 require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');13 (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);14 require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');15 }Показать всеСортируем два токена по адресу, чтобы мы могли получить адрес их парной биржи. Это необходимо, потому что в противном случае у нас было бы две возможности (одна для параметров A, B, а другая для параметров B, A), что привело бы к двум биржам вместо одной.
1 // вычисляет адрес CREATE2 для пары без выполнения каких-либо внешних вызовов2 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) {3 (address token0, address token1) = sortTokens(tokenA, tokenB);4 pair = address(uint(keccak256(abi.encodePacked(5 hex'ff',6 factory,7 keccak256(abi.encodePacked(token0, token1)),8 hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' // хэш кода инициализации9 ))));10 }Показать всеЭта функция вычисляет адрес парной биржи для двух токенов. Этот контракт создается с помощью опкода CREATE2 (opens in a new tab), поэтому мы можем вычислить адрес, используя тот же алгоритм, если мы знаем параметры, которые он использует. Это намного дешевле, чем запрашивать у фабрики, и
1 // получает и сортирует резервы для пары2 function getReserves(address factory, address tokenA, address tokenB) internal view returns (uint reserveA, uint reserveB) {3 (address token0,) = sortTokens(tokenA, tokenB);4 (uint reserve0, uint reserve1,) = IUniswapV2Pair(pairFor(factory, tokenA, tokenB)).getReserves();5 (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);6 }Эта функция возвращает резервы двух токенов, которые есть у парной биржи. Обратите внимание, что она может получать токены в любом порядке и сортирует их для внутреннего использования.
1 // по заданному количеству актива и резервам пары возвращает эквивалентное количество другого актива2 function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {3 require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');4 require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 amountB = amountA.mul(reserveB) / reserveA;6 }Эта функция показывает количество токена B, которое вы получите в обмен на токен A, если нет комиссии. Этот расчет учитывает, что перевод изменяет обменный курс.
1 // по заданному входному количеству актива и резервам пары возвращает максимальное выходное количество другого актива2 function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {Вышеупомянутая функция quote отлично работает, если нет комиссии за использование парной биржи. Однако, если есть комиссия за обмен в размере 0,3 %, сумма, которую вы фактически получаете, будет ниже. Эта функция рассчитывает сумму после вычета комиссии за обмен.
12 require(amountIn > 0, 'UniswapV2Library: INSUFFICIENT_INPUT_AMOUNT');3 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');4 uint amountInWithFee = amountIn.mul(997);5 uint numerator = amountInWithFee.mul(reserveOut);6 uint denominator = reserveIn.mul(1000).add(amountInWithFee);7 amountOut = numerator / denominator;8 }Solidity не имеет встроенной поддержки дробей, поэтому мы не можем просто умножить выводимую сумму на 0,997. Вместо этого мы умножаем числитель на 997, а знаменатель на 1000, достигая того же эффекта.
1 // по заданному выходному количеству актива и резервам пары возвращает требуемое входное количество другого актива2 function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {3 require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');4 require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');5 uint numerator = reserveIn.mul(amountOut).mul(1000);6 uint denominator = reserveOut.sub(amountOut).mul(997);7 amountIn = (numerator / denominator).add(1);8 }Эта функция делает примерно то же самое, но она получает выходную сумму и предоставляет входную.
12 // выполняет цепочку вычислений getAmountOut для любого количества пар3 function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {4 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');5 amounts = new uint[](path.length);6 amounts[0] = amountIn;7 for (uint i; i < path.length - 1; i++) {8 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);9 amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);10 }11 }1213 // выполняет цепочку вычислений getAmountIn для любого количества пар14 function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {15 require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');16 amounts = new uint[](path.length);17 amounts[amounts.length - 1] = amountOut;18 for (uint i = path.length - 1; i > 0; i--) {19 (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);20 amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);21 }22 }23}Показать всеЭти две функции обрабатывают определение значений, когда необходимо пройти через несколько парных бирж.
Помощник по переводам
Эта библиотека (opens in a new tab) добавляет проверки успешности вокруг переводов ERC-20 и Ethereum, чтобы одинаково обрабатывать отмену и возврат значения false.
1// SPDX-License-Identifier: GPL-3.0-or-later23pragma solidity >=0.6.0;45// вспомогательные методы для взаимодействия с токенами ERC20 и отправки ETH, которые не всегда возвращают true/false6library TransferHelper {7 function safeApprove(8 address token,9 address to,10 uint256 value11 ) internal {12 // bytes4(keccak256(bytes('approve(address,uint256)')));13 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));14Показать всеМы можем вызвать другой контракт одним из двух способов:
- Использовать определение интерфейса для создания вызова функции
- Использовать двоичный интерфейс приложений (ABI) (opens in a new tab) «вручную» для создания вызова. Именно так решил поступить автор кода.
1 require(2 success && (data.length == 0 || abi.decode(data, (bool))),3 'TransferHelper::safeApprove: approve failed'4 );5 }Для обратной совместимости с токенами, созданными до стандарта ERC-20, вызов ERC-20 может завершиться неудачно либо путем отмены (в этом случае success равно false), либо путем успешного выполнения и возврата значения false (в этом случае есть выходные данные, и если вы декодируете их как логическое значение, вы получите false).
123 function safeTransfer(4 address token,5 address to,6 uint256 value7 ) internal {8 // bytes4(keccak256(bytes('transfer(address,uint256)')));9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));10 require(11 success && (data.length == 0 || abi.decode(data, (bool))),12 'TransferHelper::safeTransfer: transfer failed'13 );14 }Показать всеЭта функция реализует функциональность transfer стандарта ERC-20 (opens in a new tab), которая позволяет одному аккаунту тратить средства, предоставленные ему другим аккаунтом.
12 function safeTransferFrom(3 address token,4 address from,5 address to,6 uint256 value7 ) internal {8 // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));9 (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));10 require(11 success && (data.length == 0 || abi.decode(data, (bool))),12 'TransferHelper::transferFrom: transferFrom failed'13 );14 }Показать всеЭта функция реализует функциональность transferFrom стандарта ERC-20 (opens in a new tab), которая позволяет одному аккаунту тратить средства, предоставленные ему другим аккаунтом.
12 function safeTransferETH(address to, uint256 value) internal {3 (bool success, ) = to.call{value: value}(new bytes(0));4 require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');5 }6}Эта функция переводит эфир на аккаунт. Любой вызов другого контракта может попытаться отправить эфир. Поскольку нам не нужно вызывать какую-либо функцию, мы не отправляем никаких данных с вызовом.
Заключение
Это длинная статья примерно на 50 страниц. Если вы дошли до этого места, поздравляем! Надеюсь, к этому моменту вы поняли, что следует учитывать при написании реального приложения (в отличие от коротких примеров программ), и сможете лучше писать контракты для собственных сценариев использования.
А теперь идите, напишите что-нибудь полезное и удивите нас.
Больше моих работ смотрите здесь (opens in a new tab).
Последнее обновление страницы: 25 февраля 2026 г.
