Введение

Мы продолжаем создавать клон Uniswap V1! Наша реализация почти готова: мы реализовали все основные механики смарт-контракта Биржи, включая функции ценообразования, обмена, выпуска LP-токенов и сбора комиссии. Похоже, что наш клон завершен, однако нам не хватает смарт-контракта Фабрики. Сегодня мы реализуем его, и наш клон Uniswap V1 будет завершен.

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

Для чего нужна Фабрика

Смарт-контракт Фабрики нужен для ведения списка созданных Бирж: каждый новый развернутый смарт-контракт Биржи регистрируется в Фабрике. И это важная механика, так как позволяет найти любую Биржу обратившись к реестру Фабрики. А также, наличие подобного реестра позволяет Биржам находить другие Биржи, когда пользователь пытается обменять токен на другой токен (не ether).

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

Оригинальный Uniswap имеет только один смарт-контракт Фабрики, поэтому существует только один реестр пар в Uniswap. Другим разработчикам ничего не мешает развернуть свои собственные Фабрики или даже смарт-контракты Бирж, не зарегистрированные в официальном реестре Uniswap Фабрики. Но такие Биржи не будут распознаваться Uniswap, и у них не будет возможности обмениваться токенами через официальный сайт.

Вот, в принципе, и все. Переходим к коду!

Реализация Фабрики

Фабрика (далее - Factory) - это реестр, как и любому реестру ему нужна структура данных для хранения списка Бирж (далее - Exchange), и для этого мы будем использовать mapping (отображение) адресов на адреса - это позволит находить Exchange по адресам токенам (1 Биржа может обменивать только 1 токен, помните?).

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./Exchange.sol";

contract Factory {
    mapping(address => address) public tokenToExchange;
    
    ...
}

Далее следует функция createExchange, которая позволяет создать Exchange, просто взяв адрес токена:

    function createExchange(address _tokenAddress) public returns (address) {
        require(_tokenAddress != address(0), "invalid token address");
        require(
            tokenToExchange[_tokenAddress] == address(0),
            "Биржа уже существует"
        );
        Exchange exchange = new Exchange(_tokenAddress);
        tokenToExchange[_tokenAddress] = address(exchange);
        return address(exchange);
    }

Здесь присутствует две проверки:

  1. Первый гарантирует, что адрес токена не является нулевым адресом (0x000000000000000000000000000000000000000000000000).

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

Далее мы создаём экземпляр Exchange с предоставленным пользователем адресом токена, вот почему мы должны были импортировать "Exchange.sol" (import "./Exchange.sol") ранее. 

Exchange exchange = new Exchange(_tokenAddress);

В Solidity оператор new фактически размещает смарт-контракт в блокчейне. Возвращаемое значение имеет тип contract, но каждый смарт-контракт может быть преобразован в address. Адрес размещенного Exchange мы получаем с помощью address(exchange), и сохраняем его в реестре tokenToExchange.

Чтобы завершить разработку смарт-контракта, нам нужно создать еще одну функцию - getExchange, которая даст возможность запрашивать информацию реестра Factory из другого смарт-контракта (через механизм известный как интерфейс):

function getExchange(address _tokenAddress) 
   public view returns (address) {
   return tokenToExchange[_tokenAddress];
}

Вот и все что нужно для Factory! Все очень просто.

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

Связывание Exchange с Factory

Каждый Exchange должен знать адрес Factory, но мы не будем вшивать адрес Factory в код Exchange, так как это плохая практика. Чтобы связать Exchage с Factory, нам нужно добавить новую переменную состояния в Exchage, которая будет хранить адрес Factory, после чего ещё и обновим конструктор:

contract Exchange is ERC20 {
    address public tokenAddress;

    address public factoryAddress; // <--- новая строка

    constructor(address _token) ERC20("Zuniswap-V1", "ZUNI-V1") {
        require(_token != address(0), "invalid token address");
        tokenAddress = _token;
        factoryAddress = msg.sender; // <--- новая строка
    }

    ...
}

Обмен токенов на токены

Как обменять токен на токен, когда у нас есть два Exchage, информация по которым сохранена в реестре Factory? Может быть, так:

  1. Начнём стандартный обмен токенов на ether.

  2. Вместо того чтобы отправлять ether пользователю, найдём Exchage для адреса токена, предоставленного пользователем.

  3. Если Exchage существует, отправим ether в этот Exhange, чтобы обменять ether на токены.

  4. Вернём полученные токены пользователю.

Выглядит здорово, правда? Давайте попробуем это построить.

Для этого мы создадим функцию tokenToTokenSwap:

// Exchange.sol
function tokenToTokenSwap(
   uint256 _tokensSold,
   uint256 _minTokensBought,
   address _tokenAddress
    ) public {
        ...
    }

Функция принимает три аргумента: количество продаваемых токенов (_tokenSold), минимальное количество токенов (_minTokensBought), которое необходимо получить в обмен, адрес токена (_tokenAddress), на который необходимо обменять продаваемые токены.

Сначала мы проверяем, существует ли Exchage для адреса токена, предоставленного пользователем. Если такового нет, будет выдана ошибка.

address exchangeAddress = 
		IFactory(factoryAddress).getExchange(_tokenAddress);

require(exchangeAddress != address(this) && exchangeAddress != address(0),
        "Такой Биржи не существует");

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

interface IFactory {
    function getExchange(address _tokenAddress) external returns (address);
}

Далее мы используем текущий Exchange для обмена токенов на ether и переводим токены пользователя на Exchage. Это стандартная процедура обмена ether на токены:

uint256 tokenReserve = getReserve();
uint256 ethBought = getAmount(
										_tokensSold,
                    tokenReserve,
                    address(this).balance);

IERC20(tokenAddress).transferFrom(
            msg.sender,
            address(this),
            _tokensSold);

Последний этап работы - использование другого Exchange для обмена ether на токены в функции ethToTokenSwap:

IExchange(exchangeAddress)
		.ethToTokenSwap{value: ethBought}(_minTokensBought);

И мы закончили!

Вообще-то, нет. Вы видите проблему? Давайте посмотрим на последнюю строку ethToTokenSwap:

IERC20(tokenAddress).transfer(msg.sender, tokensBought);

Ага! Он отправляет купленные токены msg.sender’у. В Solidity msg.sender динамический, а не статический, и он указывает на того, кто (или что, в случае смарт-контракта) инициировал текущий вызов. Когда пользователь вызывает функцию смарт-контракта, msg.sender будет указывать на адрес пользователя. Но когда смарт-контракт вызывает другой смарт-контракт, то msg.sender - это адрес вызывающего смарт-контракта!

Таким образом, tokenToTokenSwap отправит токены на адрес первой Биржи! Однако это не проблема, поскольку мы можем вызвать ERC20(_tokenAddress).transfer(...), чтобы отправить токены пользователю. Однако есть и более эффективное решение: давайте сэкономим немного gas и отправим токены непосредственно пользователю. 

Для этого нам понадобится разделить функцию ethToTokenSwap на две функции:

function ethToToken(uint256 _minTokens, address recipient) private {
        uint256 tokenReserve = getReserve();
        uint256 tokensBought = getAmount(
            msg.value,
            address(this).balance - msg.value,
            tokenReserve
        );
        require(tokensBought >= _minTokens, "недостаточное количество вывода");
        IERC20(tokenAddress).transfer(recipient, tokensBought);
    }

function ethToTokenSwap(uint256 _minTokens) public payable {
        ethToToken(_minTokens, msg.sender);
}

ethToToken - это private функция, которая выполняет все то же самое, что и ethToTokenSwap, только с одним отличием: она принимает адрес получателя токенов, что дает нам гибкость в выборе того, кому мы хотим отправить токены. ethToTokenSwap, в свою очередь, теперь просто обертка для ethToToken, которая всегда передает msg.sender в качестве получателя.

Теперь нам нужна еще одна функция для отправки токенов определенному получателю. Мы могли бы использовать для этого ethToToken, но давайте оставим ее private и без payable.

function ethToTokenTransfer(uint256 _minTokens, address _recipient)
   public
   payable
    {
        ethToToken(_minTokens, _recipient);
    }

Это просто копия ethToTokenSwap, которая позволяет отправлять токены определенному получателю. Теперь мы можем использовать его в функции tokenToTokenSwap:

IExchange(exchangeAddress)
	.ethToTokenTransfer{значение: ethBought}(_minTokensBought, msg.sender);

Мы отправляем токены тому, кто инициировал обмен.

И вот, мы закончили!

Заключение

Работа над нашей копией Uniswap V1 завершена. Если у вас есть идеи по поводу того, что было бы полезным добавить в смарт-контракты - дерзайте! Например, в Exchange можно добавить функцию для вычисления выходного количества токенов при обмене токенов на токены. Если у вас возникли проблемы с пониманием того, как что-то работает, не стесняйтесь проверить тесты.

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

Серия статей

  1. Программирование DeFi: Uniswap. Часть 1

  2. Программирование DeFi: Uniswap. Часть 2

  3. Программирование DeFi: Uniswap. Часть 3

Полезные ссылки

  1. Полный код этой части

  2. Техническое описание Uniswap V1

  3. Оригинальный исходный код Uniswap V1

Комментарии (3)


  1. derikn_mike
    12.08.2021 21:15

    Ждем от вас написание каких нибудь реальных приложений где наглядно будет показано как решаются основные проблемы разработки высоконагруженных распределенных систем, такие как например:

    (для кого трудно читайте все примеры с мыслью что эти пункты будут выполнять 100 потоков одновременно)

    1) атомарные операции в базе биллинга

    2) или инкрементация номера сообщения в чате

    3) или транзакции(атомарность) выполнения последовательно две функции из кода которые записывают в две разных базы

    4) конечно же как обстоять дела с идемпотентностью!


    1. aleks_raiden
      13.08.2021 12:16
      +1

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


      1. derikn_mike
        14.08.2021 20:10

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

        по текущей статье вопросов нету