Введение
Мы продолжаем создавать клон 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);
}
Здесь присутствует две проверки:
Первый гарантирует, что адрес токена не является нулевым адресом (0x000000000000000000000000000000000000000000000000).
Следующая проверка гарантирует, что токен еще не был добавлен в реестр. Смысл в том, что мы хотим исключить создание разных Биржи для одного и того же токена, иначе ликвидность будет разбросана по нескольким Биржам. Лучше сконцентрировать её на одной Бирже, чтобы уменьшить проскальзывание и обеспечить лучшие обменные курсы.
Далее мы создаём экземпляр 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? Может быть, так:
Начнём стандартный обмен токенов на ether.
Вместо того чтобы отправлять ether пользователю, найдём Exchage для адреса токена, предоставленного пользователем.
Если Exchage существует, отправим ether в этот Exhange, чтобы обменять ether на токены.
Вернём полученные токены пользователю.
Выглядит здорово, правда? Давайте попробуем это построить.
Для этого мы создадим функцию 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. Хотя это в основном то же самое, тот же набор или основные принципы, он он предоставляет новые мощные возможности.
Серия статей
Программирование DeFi: Uniswap. Часть 3