---
title: "Пошаговый разбор контракта Uniswap v2"
description: "Как работает контракт Uniswap v2? Почему он написан именно так?"
author: "Ори Померанц"
tags:
  - Solidity
  - децентрализованные приложения
skill: intermediate
breadcrumb: "Разбор Uniswap v2"
published: 2021-05-01
lang: ru
---
## Введение {#introduction}

[Uniswap v2](https://app.uniswap.org/whitepaper.pdf) может создать рынок обмена между любыми двумя токенами ERC-20. В этой статье мы рассмотрим исходный код контрактов, реализующих этот протокол, и разберемся, почему они написаны именно так.

### Что делает Uniswap? {#what-does-uniswap-do}

По сути, существует два типа пользователей: поставщики ликвидности и трейдеры.

_Поставщики ликвидности_ предоставляют пулу два токена, которые можно обменивать (назовем их **Token0** и **Token1**). Взамен они получают третий токен, который представляет собой частичное владение пулом и называется _токеном ликвидности_.

_Трейдеры_ отправляют в пул один тип токена и получают другой (например, отправляют **Token0** и получают **Token1**) из пула, предоставленного поставщиками ликвидности. Обменный курс определяется относительным количеством токенов **Token0** и **Token1**, которые есть в пуле. Кроме того, пул взимает небольшой процент в качестве вознаграждения для пула ликвидности.

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

[Нажмите здесь для более подробного описания](https://docs.uniswap.org/contracts/v2/concepts/core-concepts/swaps/).

### Почему v2? Почему не v3? {#why-v2}

[Uniswap v3](https://app.uniswap.org/whitepaper-v3.pdf) — это обновление, которое намного сложнее, чем v2. Проще сначала изучить v2, а затем переходить к v3.

### Основные и периферийные контракты {#contract-types}

Uniswap v2 разделен на два компонента: ядро (core) и периферию (periphery). Такое разделение позволяет основным контрактам, которые хранят активы и поэтому _должны_ быть безопасными, оставаться более простыми и легкими для аудита. Вся дополнительная функциональность, необходимая трейдерам, может быть обеспечена периферийными контрактами.

## Потоки данных и управления {#flows}

Это поток данных и управления, который происходит при выполнении трех основных действий Юнисвоп:

1. Своп между различными токенами
2. Добавление ликвидности на рынок и получение вознаграждения в виде токенов ликвидности ERC-20 парного обменника
3. Сжигание токенов ликвидности ERC-20 и возврат токенов ERC-20, которые парный обменник позволяет трейдерам обменивать

### Своп {#swap-flow}

Это наиболее распространенный поток, используемый трейдерами:

#### Вызывающая сторона {#caller}

1. Предоставить периферийному аккаунту разрешение на сумму, подлежащую свопу.
2. Вызвать одну из множества функций свопа периферийного контракта (какую именно — зависит от того, задействован ли ETH, указывает ли трейдер количество токенов для внесения или количество токенов для получения и т. д.).
   Каждая функция свопа принимает `path` — массив обменников, через которые нужно пройти.

#### В периферийном контракте (UniswapV2Router02.sol) {#in-the-periphery-contract-uniswapv2router02-sol}

3. Определить суммы, которые необходимо обменять на каждом обменнике по пути.
4. Выполнить итерацию по пути. Для каждого обменника на пути отправляется входной токен, а затем вызывается функция `swap` обменника.
   В большинстве случаев адресом назначения для токенов является следующий парный обменник в пути. В последнем обменнике это адрес, предоставленный трейдером.

#### В основном контракте (UniswapV2Pair.sol) {#in-the-core-contract-uniswapv2pairsol-2}

5. Убедиться, что основной контракт не обманут и может поддерживать достаточную ликвидность после свопа.
6. Посмотреть, сколько дополнительных токенов у нас есть помимо известных резервов. Эта сумма и есть количество входных токенов, которые мы получили для обмена.
7. Отправить выходные токены по месту назначения.
8. Вызвать `_update` для обновления сумм резервов

#### Снова в периферийном контракте (UniswapV2Router02.sol) {#back-in-the-periphery-contract-uniswapv2router02-sol}

9. Выполнить необходимую очистку (например, сжечь токены WETH, чтобы получить обратно ETH для отправки трейдеру)

### Добавление ликвидности {#add-liquidity-flow}

#### Вызывающая сторона {#caller-2}

1. Предоставить периферийному аккаунту разрешение на суммы, которые будут добавлены в пул ликвидности.
2. Вызвать одну из функций `addLiquidity` периферийного контракта.

#### В периферийном контракте (UniswapV2Router02.sol) {#in-the-periphery-contract-uniswapv2router02sol-2}

3. Создать новый парный обменник при необходимости
4. Если существует парный обменник, рассчитать количество токенов для добавления. Предполагается, что это одинаковая стоимость для обоих токенов, то есть такое же соотношение новых токенов к существующим.
5. Проверить, приемлемы ли суммы (вызывающие стороны могут указать минимальную сумму, ниже которой они предпочли бы не добавлять ликвидность)
6. Вызвать основной контракт.

#### В основном контракте (UniswapV2Pair.sol) {#in-the-core-contract-uniswapv2pairsol-2-2}

7. Чеканить токены ликвидности и отправить их вызывающей стороне
8. Вызвать `_update` для обновления сумм резервов

### Удаление ликвидности {#remove-liquidity-flow}

#### Вызывающая сторона {#caller-3}

1. Предоставить периферийному аккаунту разрешение на токены ликвидности, которые будут сожжены в обмен на базовые токены.
2. Вызвать одну из функций `removeLiquidity` периферийного контракта.

#### В периферийном контракте (UniswapV2Router02.sol) {#in-the-periphery-contract-uniswapv2router02sol-3}

3. Отправить токены ликвидности на парный обменник

#### В основном контракте (UniswapV2Pair.sol) {#in-the-core-contract-uniswapv2pairsol-3}

4. Отправить на адрес назначения базовые токены пропорционально сожженным токенам. Например, если в пуле 1000 токенов A, 500 токенов B и 90 токенов ликвидности, и мы получаем 9 токенов для сжигания, мы сжигаем 10% токенов ликвидности, поэтому мы отправляем пользователю обратно 100 токенов A и 50 токенов B.
5. Сжечь токены ликвидности
6. Вызвать `_update` для обновления сумм резервов

## Основные контракты {#core-contracts}

Это безопасные контракты, в которых хранится ликвидность.

### UniswapV2Pair.sol {#uniswapv2pair}

[Этот контракт](https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Pair.sol) реализует сам пул, который обменивает токены. Это основная функциональность Юнисвоп.

```solidity
pragma solidity =0.5.16;

import './interfaces/IUniswapV2Pair.sol';
import './UniswapV2ERC20.sol';
import './libraries/Math.sol';
import './libraries/UQ112x112.sol';
import './interfaces/IERC20.sol';
import './interfaces/IUniswapV2Factory.sol';
import './interfaces/IUniswapV2Callee.sol';
```

Это все интерфейсы, о которых должен знать контракт, либо потому, что он их реализует (`IUniswapV2Pair` и `UniswapV2ERC20`), либо потому, что он вызывает контракты, которые их реализуют.

```solidity
contract UniswapV2Pair is IUniswapV2Pair, UniswapV2ERC20 {
```

Этот контракт наследуется от `UniswapV2ERC20`, который предоставляет функции ERC-20 для токенов ликвидности.

```solidity
    using SafeMath  for uint;
```

[Библиотека SafeMath](https://docs.openzeppelin.com/contracts/2.x/api/math) используется для предотвращения переполнения и потери значимости. Это важно, так как в противном случае мы можем столкнуться с ситуацией, когда значение должно быть `-1`, но вместо этого равно `2^256-1`.

```solidity
    using UQ112x112 for uint224;
```

Многие вычисления в контракте пула требуют использования дробей. Однако дроби не поддерживаются EVM.
Решение, которое нашел Юнисвоп, заключается в использовании 224-битных значений, где 112 бит отводится под целую часть, а 112 бит — под дробную. Таким образом, `1.0` представляется как `2^112`, `1.5` представляется как `2^112 + 2^111` и так далее.

Более подробная информация об этой библиотеке доступна [далее в документе](#fixedpoint).

#### Переменные {#pair-vars}

```solidity
    uint public constant MINIMUM_LIQUIDITY = 10**3;
```

Чтобы избежать случаев деления на ноль, существует минимальное количество токенов ликвидности, которые существуют всегда (но принадлежат нулевому аккаунту). Это число — **MINIMUM_LIQUIDITY**, равное тысяче.

```solidity
    bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
```

Это селектор ABI для функции перевода ERC-20. Он используется для перевода токенов ERC-20 на два аккаунта токенов.

```solidity
    address public factory;
```

Это контракт фабрики, который создал этот пул. Каждый пул представляет собой обмен между двумя токенами ERC-20, а фабрика — это центральная точка, которая связывает все эти пулы.

```solidity
    address public token0;
    address public token1;
```

Здесь указаны адреса контрактов для двух типов токенов ERC-20, которые можно обменивать в этом пуле.

```solidity
    uint112 private reserve0;           // использует один слот хранилища, доступный через getReserves
    uint112 private reserve1;           // использует один слот хранилища, доступный через getReserves
```

Резервы, которые есть у пула для каждого типа токенов. Мы предполагаем, что они представляют одинаковую ценность, и поэтому каждый token0 стоит reserve1/reserve0 token1.

```solidity
    uint32  private blockTimestampLast; // использует один слот хранилища, доступный через getReserves
```

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

Одной из самых больших статей расходов газа в контрактах Эфириума является хранилище, состояние которого сохраняется от одного вызова контракта к другому. Каждая ячейка хранилища имеет длину 256 бит. Поэтому три переменные, `reserve0`, `reserve1` и `blockTimestampLast`, распределены таким образом, что одно значение хранилища может включать их все (112+112+32=256).

```solidity
    uint public price0CumulativeLast;
    uint public price1CumulativeLast;
```

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

```solidity
    uint public kLast; // reserve0 * reserve1, сразу после самого последнего события ликвидности
```

Способ, которым парный обмен определяет обменный курс между token0 и token1, заключается в поддержании постоянного произведения двух резервов во время торгов. `kLast` — это и есть данное значение. Оно изменяется, когда поставщик ликвидности вносит или выводит токены, и немного увеличивается из-за рыночной комиссии в 0.3%.

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

| Событие                                       |  reserve0 |  reserve1 | reserve0 \* reserve1 | Средний обменный курс (token1 / token0) |
| ------------------------------------------- | --------: | --------: | -------------------: | --------------------------------------- |
| Начальная настройка                               | 1,000.000 | 1,000.000 |            1,000,000 |                                         |
| Трейдер A совершает своп 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 увеличивается, и наоборот, в зависимости от спроса и предложения.

#### Блокировка {#pair-lock}

```solidity
    uint private unlocked = 1;
```

Существует класс уязвимостей безопасности, основанных на [злоупотреблении повторным входом](https://medium.com/coinmonks/ethernaut-lvl-10-re-entrancy-walkthrough-how-to-abuse-execution-ordering-and-reproduce-the-dao-7ec88b912c14). Юнисвоп должен переводить произвольные токены ERC-20, что означает вызов контрактов ERC-20, которые могут попытаться злоупотребить вызывающим их рынком Юнисвоп.
Имея переменную `unlocked` в составе контракта, мы можем предотвратить вызов функций во время их выполнения (в рамках одной транзакции).

```solidity
    modifier lock() {
```

Эта функция является [модификатором](https://docs.soliditylang.org/en/v0.8.3/contracts.html#function-modifiers) — функцией, которая оборачивает обычную функцию, чтобы каким-то образом изменить ее поведение.

```solidity
        require(unlocked == 1, 'UniswapV2: LOCKED');
        unlocked = 0;
```

Если `unlocked` равно единице, установите его в ноль. Если оно уже равно нулю, выполните откат вызова, чтобы он завершился ошибкой.

```solidity
        _;
```

В модификаторе `_;` представляет собой исходный вызов функции (со всеми параметрами). Здесь это означает, что вызов функции происходит только в том случае, если `unlocked` было равно единице при вызове, и во время ее выполнения значение `unlocked` равно нулю.

```solidity
        unlocked = 1;
    }
```

После возврата из основной функции снимите блокировку.

#### Различные функции {#pair-misc}

```solidity
    function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
        _reserve0 = reserve0;
        _reserve1 = reserve1;
        _blockTimestampLast = blockTimestampLast;
    }
```

Эта функция предоставляет вызывающим сторонам текущее состояние обмена. Обратите внимание, что функции Solidity [могут возвращать несколько значений](https://docs.soliditylang.org/en/v0.8.3/contracts.html#returning-multiple-values).

```solidity
    function _safeTransfer(address token, address to, uint value) private {
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
```

Эта внутренняя функция переводит определенное количество токенов ERC-20 с биржи кому-то другому. `SELECTOR` указывает, что вызываемая нами функция — это `transfer(address,uint)` (см. определение выше).

Чтобы избежать необходимости импортировать интерфейс для функции токена, мы «вручную» создаем вызов, используя одну из [функций ABI](https://docs.soliditylang.org/en/v0.8.3/units-and-global-variables.html#abi-encoding-and-decoding-functions).

```solidity
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
    }
```

Существует два способа, которыми вызов перевода ERC-20 может сообщить об ошибке:

1. Откат. Если вызов внешнего контракта откатывается, то логическое возвращаемое значение равно `false`
2. Нормальное завершение, но с сообщением об ошибке. В этом случае буфер возвращаемого значения имеет ненулевую длину, и при декодировании как логическое значение он равен `false`

Если происходит любое из этих условий, выполните откат.

#### События {#pair-events}

```solidity
    event Mint(address indexed sender, uint amount0, uint amount1);
    event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
```

Эти два события генерируются, когда поставщик ликвидности либо вносит ликвидность (`Mint`), либо выводит ее (`Burn`). В любом случае суммы token0 и token1, которые вносятся или выводятся, являются частью события, так же как и идентификатор аккаунта, который нас вызвал (`sender`). В случае вывода событие также включает цель, получившую токены (`to`), которая может не совпадать с отправителем.

```solidity
    event Swap(
        address indexed sender,
        uint amount0In,
        uint amount1In,
        uint amount0Out,
        uint amount1Out,
        address indexed to
    );
```

Это событие генерируется, когда трейдер совершает своп одного токена на другой. Опять же, отправитель и получатель могут не совпадать.
Каждый токен может быть либо отправлен на биржу, либо получен с нее.

```solidity
    event Sync(uint112 reserve0, uint112 reserve1);
```

Наконец, `Sync` генерируется каждый раз, когда токены добавляются или выводятся, независимо от причины, чтобы предоставить самую свежую информацию о резервах (и, следовательно, об обменном курсе).

#### Функции настройки {#pair-setup}

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

```solidity
    constructor() public {
        factory = msg.sender;
    }
```

Конструктор гарантирует, что мы будем отслеживать адрес фабрики, создавшей пару. Эта информация требуется для `initialize` и для комиссии фабрики (если таковая имеется).

```solidity
    // вызывается один раз фабрикой во время развертывания
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // достаточная проверка
        token0 = _token0;
        token1 = _token1;
    }
```

Эта функция позволяет фабрике (и только фабрике) указать два токена ERC-20, которые будет обменивать эта пара.

#### Внутренние функции обновления {#pair-update-internal}

##### \_update

```solidity
    // обновляет резервы и, при первом вызове в блоке, аккумуляторы цены
    function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
```

Эта функция вызывается каждый раз при внесении или выводе токенов.

```solidity
        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 каждого токена. Пока что это не было проблемой.

```solidity
        uint32 blockTimestamp = uint32(block.timestamp % 2**32);
        uint32 timeElapsed = blockTimestamp - blockTimestampLast; // переполнение желательно
        if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
```

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

```solidity
            // * никогда не переполняется, а переполнение + желательно
            price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
            price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
        }
```

Каждый аккумулятор стоимости обновляется с учетом последней стоимости (резерв другого токена/резерв этого токена), умноженной на прошедшее время в секундах. Чтобы получить среднюю цену, вы считываете совокупную цену в два момента времени и делите на разницу во времени между ними. Например, предположим такую последовательность событий:

| Событие                                                    |  reserve0 |  reserve1 | временная метка | Маржинальный обменный курс (reserve1 / reserve0) |       price0CumulativeLast |
| -------------------------------------------------------- | --------: | --------: | --------- | -------------------------------------------: | -------------------------: |
| Начальная настройка                                            | 1,000.000 | 1,000.000 | 5,000     |                                        1.000 |                          0 |
| Трейдер A вносит 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** между временными метками 5,030 и 5,150. Разница в значении `price0Cumulative` составляет 143.702-29.07=114.632. Это среднее значение за две минуты (120 секунд). Таким образом, средняя цена составляет 114.632/120 = 0.955.

Этот расчет цены является причиной, по которой нам нужно знать старые размеры резервов.

```solidity
        reserve0 = uint112(balance0);
        reserve1 = uint112(balance1);
        blockTimestampLast = blockTimestamp;
        emit Sync(reserve0, reserve1);
    }
```

Наконец, обновите глобальные переменные и сгенерируйте событие `Sync`.

##### \_mintFee

```solidity
    // если комиссия включена, чеканить ликвидность, эквивалентную 1/6 роста sqrt(k)
    function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
```

В Юнисвоп 2.0 трейдеры платят комиссию в размере 0.30% за использование рынка. Большая часть этой комиссии (0.25% от сделки) всегда идет поставщикам ликвидности. Оставшиеся 0.05% могут идти либо поставщикам ликвидности, либо на адрес, указанный фабрикой в качестве комиссии протокола, которая оплачивает Юнисвоп за их усилия по разработке.

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

```solidity
        address feeTo = IUniswapV2Factory(factory).feeTo();
        feeOn = feeTo != address(0);
```

Считайте адрес назначения комиссии фабрики. Если он равен нулю, то комиссия протокола отсутствует, и нет необходимости ее рассчитывать.

```solidity
        uint _kLast = kLast; // экономия газа
```

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

```solidity
        if (feeOn) {
            if (_kLast != 0) {
```

Поставщики ликвидности получают свою долю просто за счет повышения стоимости их токенов ликвидности. Но комиссия протокола требует, чтобы новые токены ликвидности были отчеканены и предоставлены на адрес `feeTo`.

```solidity
                uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
                uint rootKLast = Math.sqrt(_kLast);
                if (rootK > rootKLast) {
```

Если есть новая ликвидность, с которой можно взимать комиссию протокола. Вы можете увидеть функцию квадратного корня [далее в этой статье](#math).

```solidity
                    uint numerator = totalSupply.mul(rootK.sub(rootKLast));
                    uint denominator = rootK.mul(5).add(rootKLast);
                    uint liquidity = numerator / denominator;
```

Этот сложный расчет комиссий объясняется в [белой книге](https://app.uniswap.org/whitepaper.pdf) на странице 5. Мы знаем, что с момента расчета `kLast` до настоящего времени ликвидность не добавлялась и не удалялась (потому что мы запускаем этот расчет каждый раз при добавлении или удалении ликвидности, до того, как она фактически изменится), поэтому любое изменение в `reserve0 * reserve1` должно происходить за счет комиссий за транзакции (без них мы бы сохраняли `reserve0 * reserve1` постоянным).

```solidity
                    if (liquidity > 0) _mint(feeTo, liquidity);
                }
            }
```

Используйте функцию `UniswapV2ERC20._mint` для фактического создания дополнительных токенов ликвидности и назначения их на `feeTo`.

```solidity
        } else if (_kLast != 0) {
            kLast = 0;
        }
    }
```

Если комиссия не установлена, задайте `kLast` равным нулю (если это еще не так). Когда писался этот контракт, существовала [функция возврата газа](https://eips.ethereum.org/EIPS/eip-3298), которая поощряла контракты уменьшать общий размер состояния Эфириума путем обнуления хранилища, которое им не нужно.
Этот код получает этот возврат, когда это возможно.

#### Функции, доступные извне {#pair-external}

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

##### mint

```solidity
    // эта низкоуровневая функция должна вызываться из контракта, который выполняет важные проверки безопасности
    function mint(address to) external lock returns (uint liquidity) {
```

Эта функция вызывается, когда поставщик ликвидности добавляет ликвидность в пул. Она чеканит дополнительные токены ликвидности в качестве вознаграждения. Ее следует вызывать из [периферийного контракта](#uniswapv2router02), который вызывает ее после добавления ликвидности в той же транзакции (чтобы никто другой не смог отправить транзакцию, претендующую на новую ликвидность раньше законного владельца).

```solidity
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // экономия газа
```

Это способ чтения результатов функции Solidity, которая возвращает несколько значений. Мы отбрасываем последнее возвращаемое значение, временную метку блока, потому что она нам не нужна.

```solidity
        uint balance0 = IERC20(token0).balanceOf(address(this));
        uint balance1 = IERC20(token1).balanceOf(address(this));
        uint amount0 = balance0.sub(_reserve0);
        uint amount1 = balance1.sub(_reserve1);
```

Получите текущие балансы и посмотрите, сколько было добавлено каждого типа токенов.

```solidity
        bool feeOn = _mintFee(_reserve0, _reserve1);
```

Рассчитайте комиссии протокола для сбора, если таковые имеются, и отчеканьте токены ликвидности соответствующим образом. Поскольку параметрами для `_mintFee` являются старые значения резервов, комиссия рассчитывается точно на основе только изменений пула из-за комиссий.

```solidity
        uint _totalSupply = totalSupply; // экономия газа, должно быть определено здесь, так как totalSupply может обновиться в _mintFee
        if (_totalSupply == 0) {
            liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
           _mint(address(0), MINIMUM_LIQUIDITY); // навсегда заблокировать первые MINIMUM_LIQUIDITY токенов
```

Если это первый депозит, создайте токены `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 токенов, которые происходят из-за снижения стоимости пула, нанося ущерб вкладчику, которому он принадлежит.

```solidity
        } else {
            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 |

```solidity
        }
        require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
        _mint(to, liquidity);
```

Используйте функцию `UniswapV2ERC20._mint` для фактического создания дополнительных токенов ликвидности и передачи их на правильный аккаунт.

```solidity

        _update(balance0, balance1, _reserve0, _reserve1);
        if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 и reserve1 актуальны
        emit Mint(msg.sender, amount0, amount1);
    }
```

Обновите переменные состояния (`reserve0`, `reserve1` и, если необходимо, `kLast`) и сгенерируйте соответствующее событие.

##### burn

```solidity
    // эта низкоуровневая функция должна вызываться из контракта, который выполняет важные проверки безопасности
    function burn(address to) external lock returns (uint amount0, uint amount1) {
```

Эта функция вызывается, когда ликвидность выводится и соответствующие токены ликвидности необходимо сжечь.
Ее также следует вызывать [с периферийного аккаунта](#uniswapv2router02).

```solidity
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // экономия газа
        address _token0 = token0;                                // экономия газа
        address _token1 = token1;                                // экономия газа
        uint balance0 = IERC20(_token0).balanceOf(address(this));
        uint balance1 = IERC20(_token1).balanceOf(address(this));
        uint liquidity = balanceOf[address(this)];
```

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

```solidity
        bool feeOn = _mintFee(_reserve0, _reserve1);
        uint _totalSupply = totalSupply; // экономия газа, должно быть определено здесь, так как totalSupply может обновиться в _mintFee
        amount0 = liquidity.mul(balance0) / _totalSupply; // использование балансов обеспечивает пропорциональное распределение
        amount1 = liquidity.mul(balance1) / _totalSupply; // использование балансов обеспечивает пропорциональное распределение
        require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
```

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

```solidity
        _burn(address(this), liquidity);
        _safeTransfer(_token0, to, amount0);
        _safeTransfer(_token1, to, amount1);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));

        _update(balance0, balance1, _reserve0, _reserve1);
        if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 и reserve1 актуальны
        emit Burn(msg.sender, amount0, amount1, to);
    }

```

Остальная часть функции `burn` является зеркальным отражением функции `mint` выше.

##### swap

```solidity
    // эта низкоуровневая функция должна вызываться из контракта, который выполняет важные проверки безопасности
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
```

Эта функция также должна вызываться из [периферийного контракта](#uniswapv2router02).

```solidity
        require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
        (uint112 _reserve0, uint112 _reserve1,) = getReserves(); // экономия газа
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');

        uint balance0;
        uint balance1;
        { // область видимости для _token{0,1}, позволяет избежать ошибок слишком глубокого стека
```

Локальные переменные могут храниться либо в памяти, либо, если их не слишком много, непосредственно в стеке.
Если мы сможем ограничить их количество так, чтобы использовать стек, мы потратим меньше газа. Для получения более подробной информации см. [желтую книгу, формальные спецификации Эфириума](https://ethereum.github.io/yellowpaper/paper.pdf), стр. 26, уравнение 298.

```solidity
            address _token0 = token0;
            address _token1 = token1;
            require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
            if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // оптимистичный перевод токенов
            if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // оптимистичный перевод токенов
```

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

```solidity
            if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
```

Сообщите получателю о свопе, если это запрошено.

```solidity
            balance0 = IERC20(_token0).balanceOf(address(this));
            balance1 = IERC20(_token1).balanceOf(address(this));
        }
```

Получите текущие балансы. Периферийный контракт отправляет нам токены перед тем, как вызвать нас для свопа. Это позволяет контракту легко проверить, что его не обманывают — проверка, которая _должна_ происходить в основном контракте (потому что нас могут вызывать другие сущности, а не только наш периферийный контракт).

```solidity
        uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
        uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
        require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
        { // область видимости для reserve{0,1}Adjusted, позволяет избежать ошибок слишком глубокого стека
            uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
            uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
            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 резервов.

```solidity
        }

        _update(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }
```

Обновите `reserve0` и `reserve1`, а при необходимости — аккумуляторы цен и временную метку, и сгенерируйте событие.

##### Sync или Skim

Возможно, что реальные балансы рассинхронизируются с резервами, которые, по мнению парного обмена, у него есть.
Нет способа вывести токены без согласия контракта, но депозиты — это другое дело. Аккаунт может перевести токены на биржу, не вызывая ни `mint`, ни `swap`.

В этом случае есть два решения:

- `sync`, обновить резервы до текущих балансов
- `skim`, вывести лишнюю сумму. Обратите внимание, что любому аккаунту разрешено вызывать `skim`, потому что мы не знаем, кто внес токены. Эта информация генерируется в событии, но события недоступны из блокчейна.

```solidity
    // принудительно привести балансы в соответствие с резервами
    function skim(address to) external lock {
        address _token0 = token0; // экономия газа
        address _token1 = token1; // экономия газа
        _safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
        _safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
    }



    // принудительно привести резервы в соответствие с балансами
    function sync() external lock {
        _update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
    }
}
```

### UniswapV2Factory.sol {#uniswapv2factory}

[Этот контракт](https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Factory.sol) создает парные обмены.

```solidity
pragma solidity =0.5.16;

import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';

contract UniswapV2Factory is IUniswapV2Factory {
    address public feeTo;
    address public feeToSetter;
```

Эти переменные состояния необходимы для реализации комиссии протокола (см. [белую книгу](https://app.uniswap.org/whitepaper.pdf), стр. 5).
Адрес `feeTo` накапливает токены ликвидности для комиссии протокола, а `feeToSetter` — это адрес, которому разрешено изменять `feeTo` на другой адрес.

```solidity
    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;
```

Эти переменные отслеживают пары, обмены между двумя типами токенов.

Первая, `getPair`, представляет собой сопоставление, которое идентифицирует контракт парного обмена на основе двух токенов ERC-20, которые он обменивает. Токены ERC-20 идентифицируются по адресам контрактов, которые их реализуют, поэтому ключи и значение — это все адреса. Чтобы получить адрес парного обмена, который позволяет вам конвертировать из `tokenA` в `tokenB`, вы используете `getPair[<tokenA address>][<tokenB address>]` (или наоборот).

Вторая переменная, `allPairs`, представляет собой массив, который включает все адреса парных обменов, созданных этой фабрикой. В Эфириуме вы не можете перебирать содержимое сопоставления или получить список всех ключей, поэтому эта переменная — единственный способ узнать, какими обменами управляет эта фабрика.

Примечание: Причина, по которой вы не можете перебирать все ключи сопоставления, заключается в том, что хранение данных контракта обходится _дорого_, поэтому чем меньше мы его используем, тем лучше, и чем реже мы его меняем,
тем лучше. Вы можете создавать [сопоставления, поддерживающие итерацию](https://github.com/ethereum/dapp-bin/blob/master/library/iterable_mapping.sol), но они требуют дополнительного хранилища для списка ключей. В большинстве приложений вам это не нужно.

```solidity
    event PairCreated(address indexed token0, address indexed token1, address pair, uint);
```

Это событие генерируется при создании нового парного обмена. Оно включает адреса токенов, адрес парного обмена и общее количество обменов, управляемых фабрикой.

```solidity
    constructor(address _feeToSetter) public {
        feeToSetter = _feeToSetter;
    }
```

Единственное, что делает конструктор, — это указывает `feeToSetter`. Фабрики запускаются без комиссии, и только `feeSetter` может это изменить.

```solidity
    function allPairsLength() external view returns (uint) {
        return allPairs.length;
    }
```

Эта функция возвращает количество обменных пары.

```solidity
    function createPair(address tokenA, address tokenB) external returns (address pair) {
```

Это основная функция фабрики — создание парного обмена между двумя токенами ERC-20. Обратите внимание, что любой может вызвать эту функцию. Вам не нужно разрешение от Юнисвоп для создания нового парного обмена.

```solidity
        require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
```

Мы хотим, чтобы адрес нового обмена был детерминированным, чтобы его можно было рассчитать заранее офчейн (это может быть полезно для [транзакций уровня 2 (l2)](/developers/docs/scaling/)).
Для этого нам нужно иметь согласованный порядок адресов токенов, независимо от того, в каком порядке мы их получили, поэтому мы сортируем их здесь.

```solidity
        require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
        require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // одной проверки достаточно
```

Большие пулы ликвидности лучше маленьких, потому что у них более стабильные цены. Мы не хотим иметь более одного пула ликвидности на пару токенов. Если обмен уже существует, нет необходимости создавать еще один для той же пары.

```solidity
        bytes memory bytecode = type(UniswapV2Pair).creationCode;
```

Для создания нового контракта нам нужен код, который его создает (как функция конструктора, так и код, который записывает в память байт-код EVM самого контракта). Обычно в Solidity мы просто используем `addr = new <name of contract>(<constructor parameters>)`, и компилятор берет все на себя, но чтобы иметь детерминированный адрес контракта, нам нужно использовать [код операции CREATE2](https://eips.ethereum.org/EIPS/eip-1014).
Когда писался этот код, этот код операции еще не поддерживался Solidity, поэтому было необходимо получать код вручную. Это больше не проблема, потому что [Solidity теперь поддерживает CREATE2](https://docs.soliditylang.org/en/v0.8.3/control-structures.html#salted-contract-creations-create2).

```solidity
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
```

Когда код операции еще не поддерживается Solidity, мы можем вызвать его с помощью [встроенного ассемблера](https://docs.soliditylang.org/en/v0.8.3/assembly.html).

```solidity
        IUniswapV2Pair(pair).initialize(token0, token1);
```

Вызовите функцию `initialize`, чтобы сообщить новому обмену, какие два токена он обменивает.

```solidity
        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair; // заполнить отображение в обратном направлении
        allPairs.push(pair);
        emit PairCreated(token0, token1, pair, allPairs.length);
    }
```

Сохраните информацию о новой паре в переменных состояния и сгенерируйте событие, чтобы сообщить миру о новом парном обмене.

```solidity
    function setFeeTo(address _feeTo) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeTo = _feeTo;
    }

    function setFeeToSetter(address _feeToSetter) external {
        require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN');
        feeToSetter = _feeToSetter;
    }
}
```

Эти две функции позволяют `feeSetter` контролировать получателя комиссии (если таковой имеется) и изменять `feeSetter` на новый адрес.

### UniswapV2ERC20.sol {#uniswapv2erc20}

[Этот контракт](https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol) реализует токен ликвидности ERC-20. Он похож на [контракт ERC-20 ОпенЗеппелин](/developers/tutorials/erc20-annotated-code), поэтому я объясню только ту часть, которая отличается — функциональность `permit`.

Транзакции в Эфириуме стоят эфир (ETH), что эквивалентно реальным деньгам. Если у вас есть токены ERC-20, но нет ETH, вы не можете отправлять транзакции, поэтому вы ничего не можете с ними сделать. Одно из решений, позволяющих избежать этой проблемы, — [мета-транзакции](https://docs.uniswap.org/contracts/v2/guides/smart-contract-integration/supporting-meta-transactions).
Владелец токенов подписывает транзакцию, которая позволяет кому-то другому вывести токены офчейн, и отправляет ее через Интернет получателю. Получатель, у которого есть ETH, затем отправляет разрешение от имени владельца.

```solidity
    bytes32 public DOMAIN_SEPARATOR;
    // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
    bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
```

Этот хеш является [идентификатором типа транзакции](https://eips.ethereum.org/EIPS/eip-712#rationale-for-typehash). Единственный, который мы здесь поддерживаем, — это `Permit` с этими параметрами.

```solidity
    mapping(address => uint) public nonces;
```

Получателю нецелесообразно подделывать цифровую подпись. Однако отправить одну и ту же транзакцию дважды тривиально (это форма [атаки повторного воспроизведения](https://wikipedia.org/wiki/Replay_attack)). Чтобы предотвратить это, мы используем [нонс](https://wikipedia.org/wiki/Cryptographic_nonce). Если нонс нового `Permit` не на единицу больше последнего использованного, мы считаем его недействительным.

```solidity
    constructor() public {
        uint chainId;
        assembly {
            chainId := chainid
        }
```

Это код для получения [идентификатора цепи](https://chainid.network/). Он использует диалект ассемблера EVM под названием [Yul](https://docs.soliditylang.org/en/v0.8.4/yul.html). Обратите внимание, что в текущей версии Yul вы должны использовать `chainid()`, а не `chainid`.

```solidity
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
                keccak256(bytes(name)),
                keccak256(bytes('1')),
                chainId,
                address(this)
            )
        );
    }
```

Рассчитайте [разделитель домена](https://eips.ethereum.org/EIPS/eip-712#rationale-for-domainseparator) для EIP-712.

```solidity
    function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
```

Это функция, которая реализует разрешения. Она получает в качестве параметров соответствующие поля и три скалярных значения для [подписи](https://yos.io/2018/11/16/ethereum-signatures/) (v, r и s).

```solidity
        require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
```

Не принимайте транзакции после истечения крайнего срока.

```solidity
        bytes32 digest = keccak256(
            abi.encodePacked(
                '\x19\x01',
                DOMAIN_SEPARATOR,
                keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
            )
        );
```

`abi.encodePacked(...)` — это сообщение, которое мы ожидаем получить. Мы знаем, каким должен быть нонс, поэтому нам нет необходимости получать его в качестве параметра.

Алгоритм подписи Эфириума ожидает получить 256 бит для подписи, поэтому мы используем хеш-функцию `keccak256`.

```solidity
        address recoveredAddress = ecrecover(digest, v, r, s);
```

Из дайджеста и подписи мы можем получить адрес, который ее подписал, используя [ecrecover](https://coders-errand.com/ecrecover-signature-verification-ethereum/).

```solidity
        require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
        _approve(owner, spender, value);
    }

```

Если все в порядке, рассматривайте это как [одобрение ERC-20](https://eips.ethereum.org/EIPS/eip-20#approve).

## Периферийные контракты {#periphery-contracts}

Периферийные контракты — это API (интерфейс прикладного программирования) для Юнисвоп. Они доступны для внешних вызовов, будь то из других контрактов или децентрализованных приложений (dapp). Вы можете вызывать базовые контракты напрямую, но это сложнее, и вы можете потерять средства, если допустите ошибку. Базовые контракты содержат только проверки, чтобы убедиться, что их не обманывают, а не проверки на адекватность для кого-либо еще. Они находятся в периферии, поэтому их можно обновлять по мере необходимости.

### UniswapV2Router01.sol {#uniswapv2router01}

[Этот контракт](https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/UniswapV2Router01.sol) имеет проблемы, и [его больше не следует использовать](https://docs.uniswap.org/contracts/v2/reference/smart-contracts/router-01). К счастью, периферийные контракты не имеют состояния и не хранят никаких активов, поэтому легко объявить его устаревшим и предложить людям использовать вместо него замену — `UniswapV2Router02`.

### UniswapV2Router02.sol {#uniswapv2router02}

В большинстве случаев вы будете использовать Юнисвоп через [этот контракт](https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/UniswapV2Router02.sol).
Вы можете посмотреть, как его использовать, [здесь](https://docs.uniswap.org/contracts/v2/reference/smart-contracts/router-02).

```solidity
pragma solidity =0.6.6;

import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol';
import '@uniswap/lib/contracts/libraries/TransferHelper.sol';

import './interfaces/IUniswapV2Router02.sol';
import './libraries/UniswapV2Library.sol';
import './libraries/SafeMath.sol';
import './interfaces/IERC20.sol';
import './interfaces/IWETH.sol';
```

С большинством из них мы либо сталкивались ранее, либо они довольно очевидны. Единственным исключением является `IWETH.sol`. Юнисвоп v2 позволяет совершать обмены для любой пары токенов ERC-20, но сам эфир (ETH) не является токеном ERC-20. Он появился до этого стандарта и переводится с помощью уникальных механизмов. Чтобы сделать возможным использование ETH в контрактах, которые применяются к токенам ERC-20, люди придумали контракт [обернутого эфира (WETH)](https://weth.tkn.eth.limo/). Вы отправляете этому контракту ETH, и он чеканит вам эквивалентное количество WETH. Или вы можете сжигать WETH и получать ETH обратно.

```solidity
contract UniswapV2Router02 is IUniswapV2Router02 {
    using SafeMath for uint;

    address public immutable override factory;
    address public immutable override WETH;
```

Маршрутизатору нужно знать, какую фабрику использовать, а для транзакций, требующих WETH, — какой контракт WETH использовать. Эти значения являются [неизменяемыми](https://docs.soliditylang.org/en/v0.8.3/contracts.html#constant-and-immutable-state-variables), что означает, что они могут быть установлены только в конструкторе. Это дает пользователям уверенность в том, что никто не сможет изменить их, чтобы они указывали на менее честные контракты.

```solidity
    modifier ensure(uint deadline) {
        require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
        _;
    }
```

Этот модификатор гарантирует, что транзакции с ограничением по времени («сделай X до времени Y, если сможешь») не произойдут после истечения их лимита времени.

```solidity
    constructor(address _factory, address _WETH) public {
        factory = _factory;
        WETH = _WETH;
    }
```

Конструктор просто устанавливает неизменяемые переменные состояния.

```solidity
    receive() external payable {
        assert(msg.sender == WETH); // принимать ETH только через fallback из контракта WETH
    }
```

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

#### Добавление ликвидности {#add-liquidity}

Эти функции добавляют токены в парный обменник, что увеличивает пул ликвидности.

```solidity

    // **** ДОБАВИТЬ ЛИКВИДНОСТЬ ****
    function _addLiquidity(
```

Эта функция используется для расчета количества токенов A и B, которые должны быть внесены в парный обменник.

```solidity
        address tokenA,
        address tokenB,
```

Это адреса контрактов токенов ERC-20.

```solidity
        uint amountADesired,
        uint amountBDesired,
```

Это суммы, которые поставщик ликвидности хочет внести. Они также являются максимальными суммами A и B для внесения.

```solidity
        uint amountAMin,
        uint amountBMin
```

Это минимально приемлемые суммы для внесения. Если транзакция не может быть выполнена с этими суммами или больше, происходит откат. Если вам не нужна эта функция, просто укажите ноль.

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

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

| Параметр       | Значение |
| -------------- | ----: |
| amountADesired |  1000 |
| amountBDesired |  1000 |
| amountAMin     |   900 |
| amountBMin     |   800 |

Пока обменный курс остается в пределах от 0.9 до 1.25, транзакция выполняется. Если обменный курс выходит за пределы этого диапазона, транзакция отменяется.

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

```solidity
    ) internal virtual returns (uint amountA, uint amountB) {
```

Функция возвращает суммы, которые поставщик ликвидности должен внести, чтобы соотношение было равно текущему соотношению между резервами.

```solidity
        // создать пару, если она еще не существует
        if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
            IUniswapV2Factory(factory).createPair(tokenA, tokenB);
        }
```

Если обменника для этой пары токенов еще нет, создайте его.

```solidity
        (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
```

Получите текущие резервы в паре.

```solidity
        if (reserveA == 0 && reserveB == 0) {
            (amountA, amountB) = (amountADesired, amountBDesired);
```

Если текущие резервы пусты, то это новый парный обменник. Суммы для внесения должны быть точно такими же, какие поставщик ликвидности хочет предоставить.

```solidity
        } else {
            uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
```

Если нам нужно узнать, какими будут суммы, мы получаем оптимальную сумму с помощью [этой функции](https://github.com/Uniswap/uniswap-v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol#L35). Мы хотим получить то же соотношение, что и у текущих резервов.

```solidity
            if (amountBOptimal <= amountBDesired) {
                require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
                (amountA, amountB) = (amountADesired, amountBOptimal);
```

Если `amountBOptimal` меньше суммы, которую поставщик ликвидности хочет внести, это означает, что токен B в настоящее время более ценен, чем думает вкладчик ликвидности, поэтому требуется меньшая сумма.

```solidity
            } else {
                uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
                assert(amountAOptimal <= amountADesired);
                require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
                (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.

![Graph](liquidityProviderDeposit.png)

Вы могли бы внести ликвидность непосредственно в базовый контракт (используя [UniswapV2Pair::mint](https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2Pair.sol#L110)), но базовый контракт проверяет только то, что его самого не обманывают, поэтому вы рискуете потерять средства, если обменный курс изменится в период между отправкой вашей транзакции и ее выполнением. Если вы используете периферийный контракт, он вычисляет сумму, которую вы должны внести, и вносит ее немедленно, поэтому обменный курс не меняется, и вы ничего не теряете.

```solidity
    function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
```

Эта функция может быть вызвана транзакцией для внесения ликвидности. Большинство параметров такие же, как в `_addLiquidity` выше, за двумя исключениями:

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

```solidity
    ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
        (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
```

Мы рассчитываем суммы для фактического внесения, а затем находим адрес пула ликвидности. Чтобы сэкономить газ, мы делаем это не путем запроса к фабрике, а с помощью функции библиотеки `pairFor` (см. ниже в разделе библиотек)

```solidity
        TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
        TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
```

Переведите правильные суммы токенов от пользователя в парный обменник.

```solidity
        liquidity = IUniswapV2Pair(pair).mint(to);
    }
```

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

```solidity
    function addLiquidityETH(
        address token,
        uint amountTokenDesired,
```

Когда поставщик ликвидности хочет предоставить ликвидность для парного обменника Токен/ETH, есть несколько отличий. Контракт берет на себя обертывание ETH для поставщика ликвидности. Нет необходимости указывать, сколько ETH пользователь хочет внести, потому что пользователь просто отправляет их вместе с транзакцией (сумма доступна в `msg.value`).

```solidity
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
        (amountToken, amountETH) = _addLiquidity(
            token,
            WETH,
            amountTokenDesired,
            msg.value,
            amountTokenMin,
            amountETHMin
        );
        address pair = UniswapV2Library.pairFor(factory, token, WETH);
        TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
        IWETH(WETH).deposit{value: amountETH}();
        assert(IWETH(WETH).transfer(pair, amountETH));
```

Чтобы внести ETH, контракт сначала обертывает его в WETH, а затем переводит WETH в пару. Обратите внимание, что перевод обернут в `assert`. Это означает, что если перевод не удастся, вызов этого контракта также завершится неудачей, и, следовательно, обертывание на самом деле не произойдет.

```solidity
        liquidity = IUniswapV2Pair(pair).mint(to);
        // возместить пыль эфира, если есть
        if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
    }
```

Пользователь уже отправил нам ETH, поэтому, если останется какой-либо излишек (потому что другой токен менее ценен, чем думал пользователь), нам нужно оформить возврат.

#### Удаление ликвидности {#remove-liquidity}

Эти функции удалят ликвидность и выплатят средства поставщику ликвидности.

```solidity
    // **** УДАЛИТЬ ЛИКВИДНОСТЬ ****
    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
```

Самый простой случай удаления ликвидности. Существует минимальное количество каждого токена, которое поставщик ликвидности согласен принять, и это должно произойти до истечения крайнего срока.

```solidity
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // отправить ликвидность в пару
        (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
```

Функция `burn` базового контракта обрабатывает возврат токенов пользователю.

```solidity
        (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
```

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

```solidity
        (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
```

Преобразуйте суммы из того вида, в котором их возвращает базовый контракт (сначала токен с меньшим адресом), в тот вид, который ожидает пользователь (соответствующий `tokenA` и `tokenB`).

```solidity
        require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
        require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
    }
```

Нормально сначала выполнить перевод, а затем проверить его законность, потому что если это не так, мы откатим все изменения состояния.

```solidity
    function removeLiquidityETH(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
        (amountToken, amountETH) = removeLiquidity(
            token,
            WETH,
            liquidity,
            amountTokenMin,
            amountETHMin,
            address(this),
            deadline
        );
        TransferHelper.safeTransfer(token, to, amountToken);
        IWETH(WETH).withdraw(amountETH);
        TransferHelper.safeTransferETH(to, amountETH);
    }
```

Удаление ликвидности для ETH происходит почти так же, за исключением того, что мы получаем токены WETH, а затем выкупаем их за ETH, чтобы вернуть поставщику ликвидности.

```solidity
    function removeLiquidityWithPermit(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external virtual override returns (uint amountA, uint amountB) {
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        uint value = approveMax ? uint(-1) : liquidity;
        IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
        (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
    }


    function removeLiquidityETHWithPermit(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external virtual override returns (uint amountToken, uint amountETH) {
        address pair = UniswapV2Library.pairFor(factory, token, WETH);
        uint value = approveMax ? uint(-1) : liquidity;
        IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
        (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
    }
```

Эти функции ретранслируют мета-транзакции, чтобы позволить пользователям без эфира выводить средства из пула, используя [механизм разрешений (permit)](#uniswapv2erc20).

```solidity

    // **** УДАЛИТЬ ЛИКВИДНОСТЬ (с поддержкой токенов с комиссией за перевод) ****
    function removeLiquidityETHSupportingFeeOnTransferTokens(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) public virtual override ensure(deadline) returns (uint amountETH) {
        (, amountETH) = removeLiquidity(
            token,
            WETH,
            liquidity,
            amountTokenMin,
            amountETHMin,
            address(this),
            deadline
        );
        TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
        IWETH(WETH).withdraw(amountETH);
        TransferHelper.safeTransferETH(to, amountETH);
    }

```

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

```solidity


    function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external virtual override returns (uint amountETH) {
        address pair = UniswapV2Library.pairFor(factory, token, WETH);
        uint value = approveMax ? uint(-1) : liquidity;
        IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
        amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
            token, liquidity, amountTokenMin, amountETHMin, to, deadline
        );
    }
```

Последняя функция объединяет комиссии за хранение с мета-транзакциями.

#### Торговля {#trade}

```solidity
    // **** СВОП ****
    // требует, чтобы начальная сумма уже была отправлена в первую пару
    function _swap(uint[] memory amounts, address[] memory path, address _to) internal virtual {
```

Эта функция выполняет внутреннюю обработку, которая требуется для функций, доступных трейдерам.

```solidity
        for (uint i; i < path.length - 1; i++) {
```

На момент написания этой статьи существует [388 160 токенов ERC-20](https://eth.blockscout.com/tokens). Если бы для каждой пары токенов существовал парный обменник, их было бы более 150 миллиардов. Во всей цепи на данный момент [всего 0.1% от этого количества аккаунтов](https://eth.blockscout.com/stats/accountsGrowth). Вместо этого функции свопа поддерживают концепцию пути. Трейдер может обменять A на B, B на C и C на D, поэтому нет необходимости в прямом парном обменнике A-D.

Цены на этих рынках, как правило, синхронизированы, потому что их рассинхронизация создает возможность для арбитража. Представьте, например, три токена: A, B и C. Существует три парных обменника, по одному для каждой пары.

1. Исходная ситуация
2. Трейдер продает 24.695 токенов A и получает 25.305 токенов B.
3. Трейдер продает 24.695 токенов B за 25.305 токенов C, оставляя себе примерно 0.61 токена B в качестве прибыли.
4. Затем трейдер продает 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 |

```solidity
            (address input, address output) = (path[i], path[i + 1]);
            (address token0,) = UniswapV2Library.sortTokens(input, output);
            uint amountOut = amounts[i + 1];
```

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

```solidity
            (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOut) : (amountOut, uint(0));
```

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

```solidity
            address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
```

Это последний обмен? Если да, отправьте токены, полученные за сделку, по месту назначения. Если нет, отправьте их в следующий парный обменник.

```solidity

            IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output)).swap(
                amount0Out, amount1Out, to, new bytes(0)
            );
        }
    }
```

Фактически вызовите парный обменник для свопа токенов. Нам не нужен обратный вызов (callback), чтобы узнать об обмене, поэтому мы не отправляем никаких байтов в этом поле.

```solidity
    function swapExactTokensForTokens(
```

Эта функция используется трейдерами напрямую для свопа одного токена на другой.

```solidity
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
```

Этот параметр содержит адреса контрактов ERC-20. Как объяснялось выше, это массив, потому что вам может понадобиться пройти через несколько парных обменников, чтобы получить желаемый актив из того, который у вас есть.

Параметр функции в Solidity может храниться либо в `memory`, либо в `calldata`. Если функция является точкой входа в контракт, вызываемой напрямую пользователем (с помощью транзакции) или из другого контракта, то значение параметра может быть взято непосредственно из данных вызова (call data). Если функция вызывается внутренне, как `_swap` выше, то параметры должны храниться в `memory`. С точки зрения вызываемого контракта `calldata` доступен только для чтения.

Для скалярных типов, таких как `uint` или `address`, компилятор сам выбирает место хранения, но для массивов, которые длиннее и дороже, мы указываем тип используемого хранилища.

```solidity
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
```

Возвращаемые значения всегда возвращаются в памяти.

```solidity
        amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
        require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
```

Рассчитайте сумму для покупки в каждом свопе. Если результат меньше минимума, который трейдер готов принять, откатите транзакцию.

```solidity
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
    }
```

Наконец, переведите начальный токен ERC-20 на аккаунт для первого парного обменника и вызовите `_swap`. Все это происходит в одной и той же транзакции, поэтому парный обменник знает, что любые неожиданные токены являются частью этого перевода.

```solidity
    function swapTokensForExactTokens(
        uint amountOut,
        uint amountInMax,
        address[] calldata path,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint[] memory amounts) {
        amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
        require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
    }
```

Предыдущая функция, `swapTokensForTokens`, позволяет трейдеру указать точное количество входных токенов, которое он готов отдать, и минимальное количество выходных токенов, которое он готов получить взамен. Эта функция выполняет обратный своп: она позволяет трейдеру указать количество выходных токенов, которое он хочет получить, и максимальное количество входных токенов, которое он готов за них заплатить.

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

```solidity
    function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
        external
        virtual
        override
        payable
        ensure(deadline)
        returns (uint[] memory amounts)
    {
        require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
        amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
        require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
        IWETH(WETH).deposit{value: amounts[0]}();
        assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
        _swap(amounts, path, to);
    }


    function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline)
        external
        virtual
        override
        ensure(deadline)
        returns (uint[] memory amounts)
    {
        require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
        amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
        require(amounts[0] <= amountInMax, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, address(this));
        IWETH(WETH).withdraw(amounts[amounts.length - 1]);
        TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
    }



    function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline)
        external
        virtual
        override
        ensure(deadline)
        returns (uint[] memory amounts)
    {
        require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
        amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
        require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, address(this));
        IWETH(WETH).withdraw(amounts[amounts.length - 1]);
        TransferHelper.safeTransferETH(to, amounts[amounts.length - 1]);
    }


    function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline)
        external
        virtual
        override
        payable
        ensure(deadline)
        returns (uint[] memory amounts)
    {
        require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
        amounts = UniswapV2Library.getAmountsIn(factory, amountOut, path);
        require(amounts[0] <= msg.value, 'UniswapV2Router: EXCESSIVE_INPUT_AMOUNT');
        IWETH(WETH).deposit{value: amounts[0]}();
        assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
        _swap(amounts, path, to);
        // возместить пыль эфира, если есть
        if (msg.value > amounts[0]) TransferHelper.safeTransferETH(msg.sender, msg.value - amounts[0]);
    }
```

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

```solidity
    // **** СВОП (с поддержкой токенов с комиссией за перевод) ****
    // требует, чтобы начальная сумма уже была отправлена в первую пару
    function _swapSupportingFeeOnTransferTokens(address[] memory path, address _to) internal virtual {
```

Это внутренняя функция для свопа токенов, у которых есть комиссии за перевод или хранение, чтобы решить ([эту проблему](https://github.com/Uniswap/uniswap-interface/issues/835)).

```solidity
        for (uint i; i < path.length - 1; i++) {
            (address input, address output) = (path[i], path[i + 1]);
            (address token0,) = UniswapV2Library.sortTokens(input, output);
            IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
            uint amountInput;
            uint amountOutput;
            { // область видимости, чтобы избежать ошибок слишком глубокого стека
            (uint reserve0, uint reserve1,) = pair.getReserves();
            (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
            amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
            amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
```

Из-за комиссий за перевод мы не можем полагаться на функцию `getAmountsOut`, чтобы узнать, сколько мы получаем от каждого перевода (как мы это делаем перед вызовом оригинальной `_swap`). Вместо этого мы должны сначала выполнить перевод, а затем посмотреть, сколько токенов мы получили обратно.

Примечание: В теории мы могли бы просто использовать эту функцию вместо `_swap`, но в некоторых случаях (например, если перевод в конечном итоге откатывается, потому что в конце недостаточно средств для достижения требуемого минимума) это привело бы к большим затратам газа. Токены с комиссией за перевод встречаются довольно редко, поэтому, хотя нам и нужно их учитывать, нет необходимости предполагать, что все свопы проходят хотя бы через один из них.

```solidity
            }
            (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
            address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
            pair.swap(amount0Out, amount1Out, to, new bytes(0));
        }
    }


    function swapExactTokensForTokensSupportingFeeOnTransferTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) {
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
        );
        uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
        _swapSupportingFeeOnTransferTokens(path, to);
        require(
            IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
            'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
        );
    }


    function swapExactETHForTokensSupportingFeeOnTransferTokens(
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    )
        external
        virtual
        override
        payable
        ensure(deadline)
    {
        require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
        uint amountIn = msg.value;
        IWETH(WETH).deposit{value: amountIn}();
        assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn));
        uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
        _swapSupportingFeeOnTransferTokens(path, to);
        require(
            IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
            'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
        );
    }


    function swapExactTokensForETHSupportingFeeOnTransferTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    )
        external
        virtual
        override
        ensure(deadline)
    {
        require(path[path.length - 1] == WETH, 'UniswapV2Router: INVALID_PATH');
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
        );
        _swapSupportingFeeOnTransferTokens(path, address(this));
        uint amountOut = IERC20(WETH).balanceOf(address(this));
        require(amountOut >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
        IWETH(WETH).withdraw(amountOut);
        TransferHelper.safeTransferETH(to, amountOut);
    }
```

Это те же варианты, которые используются для обычных токенов, но вместо этого они вызывают `_swapSupportingFeeOnTransferTokens`.

```solidity
    // **** ФУНКЦИИ БИБЛИОТЕКИ ****
    function quote(uint amountA, uint reserveA, uint reserveB) public pure virtual override returns (uint amountB) {
        return UniswapV2Library.quote(amountA, reserveA, reserveB);
    }

    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut)
        public
        pure
        virtual
        override
        returns (uint amountOut)
    {
        return UniswapV2Library.getAmountOut(amountIn, reserveIn, reserveOut);
    }

    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut)
        public
        pure
        virtual
        override
        returns (uint amountIn)
    {
        return UniswapV2Library.getAmountIn(amountOut, reserveIn, reserveOut);
    }

    function getAmountsOut(uint amountIn, address[] memory path)
        public
        view
        virtual
        override
        returns (uint[] memory amounts)
    {
        return UniswapV2Library.getAmountsOut(factory, amountIn, path);
    }

    function getAmountsIn(uint amountOut, address[] memory path)
        public
        view
        virtual
        override
        returns (uint[] memory amounts)
    {
        return UniswapV2Library.getAmountsIn(factory, amountOut, path);
    }
}
```

Эти функции — просто прокси, которые вызывают [функции UniswapV2Library](#uniswapv2library).

### UniswapV2Migrator.sol {#uniswapv2migrator}

Этот контракт использовался для миграции обменников со старой версии v1 на v2. Теперь, когда они были перенесены, он больше не актуален.

## Библиотеки {#libraries}

[Библиотека SafeMath](https://docs.openzeppelin.com/contracts/2.x/api/math) хорошо задокументирована, поэтому нет необходимости описывать ее здесь.

### Math {#math}

Эта библиотека содержит некоторые математические функции, которые обычно не нужны в коде на Solidity, поэтому они не являются частью языка.

```solidity
pragma solidity =0.5.16;

// библиотека для выполнения различных математических операций

library Math {
    function min(uint x, uint y) internal pure returns (uint z) {
        z = x < y ? x : y;
    }

    // вавилонский метод (https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method)
    function sqrt(uint y) internal pure returns (uint z) {
        if (y > 3) {
            z = y;
            uint x = y / 2 + 1;
```

Начните с x в качестве оценки, которая больше квадратного корня (по этой причине нам нужно рассматривать 1-3 как особые случаи).

```solidity
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
```

Получите более точную оценку: среднее значение предыдущей оценки и числа, квадратный корень которого мы пытаемся найти, деленное на предыдущую оценку. Повторяйте до тех пор, пока новая оценка не перестанет быть меньше существующей. Для получения более подробной информации [смотрите здесь](https://wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method).

```solidity
            }
        } else if (y != 0) {
            z = 1;
```

Нам никогда не понадобится квадратный корень из нуля. Квадратные корни из единицы, двойки и тройки примерно равны единице (мы используем целые числа, поэтому игнорируем дробную часть).

```solidity
        }
    }
}
```

### Дроби с фиксированной запятой (UQ112x112) {#fixedpoint}

Эта библиотека работает с дробями, которые обычно не являются частью арифметики Эфириума. Она делает это путем кодирования числа _x_ как _x\*2^112_. Это позволяет нам использовать исходные коды операций сложения и вычитания без изменений.

```solidity
pragma solidity =0.5.16;

// библиотека для работы с двоичными числами с фиксированной запятой (https://wikipedia.org/wiki/Q_(number_format))

// диапазон: [0, 2**112 - 1]
// разрешение: 1 / 2**112

library UQ112x112 {
    uint224 constant Q112 = 2**112;
```

`Q112` — это кодировка для единицы.

```solidity
    // кодировать uint112 как UQ112x112
    function encode(uint112 y) internal pure returns (uint224 z) {
        z = uint224(y) * Q112; // никогда не переполняется
    }
```

Поскольку y равно `uint112`, его максимальное значение может составлять 2^112-1. Это число все еще можно закодировать как `UQ112x112`.

```solidity
    // разделить UQ112x112 на uint112, возвращая UQ112x112
    function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
        z = x / uint224(y);
    }
}
```

Если мы разделим два значения `UQ112x112`, результат больше не будет умножаться на 2^112. Поэтому вместо этого мы берем целое число для знаменателя. Нам пришлось бы использовать аналогичный трюк для умножения, но нам не нужно выполнять умножение значений `UQ112x112`.

### UniswapV2Library {#uniswapv2library}

Эта библиотека используется только периферийными контрактами

```solidity
pragma solidity >=0.5.0;

import '@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol';

import "./SafeMath.sol";

library UniswapV2Library {
    using SafeMath for uint;

    // возвращает отсортированные адреса токенов, используется для обработки возвращаемых значений из пар, отсортированных в этом порядке
    function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
        require(tokenA != tokenB, 'UniswapV2Library: IDENTICAL_ADDRESSES');
        (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), 'UniswapV2Library: ZERO_ADDRESS');
    }
```

Отсортируйте два токена по адресу, чтобы мы могли получить адрес парного обменника для них. Это необходимо, потому что в противном случае у нас было бы две возможности: одна для параметров A,B и другая для параметров B,A, что привело бы к двум обменникам вместо одного.

```solidity
    // вычисляет адрес CREATE2 для пары без выполнения каких-либо внешних вызовов
    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' // хэш кода инициализации
            ))));
    }
```

Эта функция вычисляет адрес парного обменника для двух токенов. Этот контракт создается с использованием [кода операции CREATE2](https://eips.ethereum.org/EIPS/eip-1014), поэтому мы можем вычислить адрес с помощью того же алгоритма, если знаем параметры, которые он использует. Это намного дешевле, чем запрашивать фабрику, и

```solidity
    // получает и сортирует резервы для пары
    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);
    }
```

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

```solidity
    // учитывая некоторое количество актива и резервы пары, возвращает эквивалентное количество другого актива
    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;
    }
```

Эта функция дает вам количество токена B, которое вы получите в обмен на токен A, если комиссия не взимается. Этот расчет учитывает, что перевод изменяет обменный курс.

```solidity
    // учитывая входное количество актива и резервы пары, возвращает максимальное выходное количество другого актива
    function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) internal pure returns (uint amountOut) {
```

Функция `quote` выше отлично работает, если за использование парного обменника не взимается комиссия. Однако, если комиссия за обмен составляет 0,3%, сумма, которую вы фактически получаете, будет меньше. Эта функция рассчитывает сумму после вычета комиссии за обмен.

```solidity

        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;
    }
```

Solidity не поддерживает дроби изначально, поэтому мы не можем просто умножить сумму на 0,997. Вместо этого мы умножаем числитель на 997, а знаменатель на 1000, достигая того же эффекта.

```solidity
    // учитывая выходное количество актива и резервы пары, возвращает необходимое входное количество другого актива
    function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) internal pure returns (uint amountIn) {
        require(amountOut > 0, 'UniswapV2Library: INSUFFICIENT_OUTPUT_AMOUNT');
        require(reserveIn > 0 && reserveOut > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
        uint numerator = reserveIn.mul(amountOut).mul(1000);
        uint denominator = reserveOut.sub(amountOut).mul(997);
        amountIn = (numerator / denominator).add(1);
    }
```

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

```solidity

    // выполняет цепочку вычислений getAmountOut для любого количества пар
    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);
        }
    }

    // выполняет цепочку вычислений getAmountIn для любого количества пар
    function getAmountsIn(address factory, uint amountOut, address[] memory path) internal view returns (uint[] memory amounts) {
        require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
        amounts = new uint[](path.length);
        amounts[amounts.length - 1] = amountOut;
        for (uint i = path.length - 1; i > 0; i--) {
            (uint reserveIn, uint reserveOut) = getReserves(factory, path[i - 1], path[i]);
            amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
        }
    }
}
```

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

### Transfer Helper {#transfer-helper}

[Эта библиотека](https://github.com/Uniswap/uniswap-lib/blob/master/contracts/libraries/TransferHelper.sol) добавляет проверки успешности для переводов ERC-20 и Эфириума, чтобы одинаково обрабатывать откат и возврат значения `false`.

```solidity
// SPDX-License-Identifier: GPL-3.0-or-later

pragma solidity >=0.6.0;

// вспомогательные методы для взаимодействия с токенами ERC-20 и отправки ETH, которые не всегда возвращают true/false
library TransferHelper {
    function safeApprove(
        address token,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('approve(address,uint256)')));
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x095ea7b3, to, value));

```

Мы можем вызвать другой контракт одним из двух способов:

- Использовать определение интерфейса для создания вызова функции
- Использовать [двоичный интерфейс приложения (ABI)](https://docs.soliditylang.org/en/v0.8.3/abi-spec.html) «вручную» для создания вызова. Именно так решил поступить автор кода.

```solidity
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            'TransferHelper::safeApprove: approve failed'
        );
    }
```

Ради обратной совместимости с токенами, которые были созданы до стандарта ERC-20, вызов ERC-20 может завершиться неудачей либо путем отката (в этом случае `success` равно `false`), либо будучи успешным и возвращая значение `false` (в этом случае есть выходные данные, и если вы декодируете их как логическое значение, вы получите `false`).

```solidity


    function safeTransfer(
        address token,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('transfer(address,uint256)')));
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            'TransferHelper::safeTransfer: transfer failed'
        );
    }
```

Эта функция реализует [функциональность перевода ERC-20](https://eips.ethereum.org/EIPS/eip-20#transfer), которая позволяет аккаунту расходовать разрешение, предоставленное другим аккаунтом.

```solidity

    function safeTransferFrom(
        address token,
        address from,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            'TransferHelper::transferFrom: transferFrom failed'
        );
    }
```

Эта функция реализует [функциональность transferFrom ERC-20](https://eips.ethereum.org/EIPS/eip-20#transferfrom), которая позволяет аккаунту расходовать разрешение, предоставленное другим аккаунтом.

```solidity

    function safeTransferETH(address to, uint256 value) internal {
        (bool success, ) = to.call{value: value}(new bytes(0));
        require(success, 'TransferHelper::safeTransferETH: ETH transfer failed');
    }
}
```

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

## Заключение {#conclusion}

Это длинная статья объемом около 50 страниц. Если вы дочитали до этого места, поздравляем! Надеемся, теперь вы понимаете, что нужно учитывать при написании реального приложения (в отличие от коротких примеров программ), и лучше подготовлены к написанию контрактов для собственных сценариев использования.

А теперь идите, напишите что-нибудь полезное и удивите нас.

[Здесь вы можете найти больше моих работ](https://cryptodocguy.pro/).