DeFi programming: Uniswap. Part 3
Introduction
We continue to create a clone of Uniswap V1! Our implementation is almost ready: we have implemented all the basic mechanics of the Exchange’s smart contract, including pricing, exchange, LP token issuance and commission collection functions. It looks like our clone is complete, but we’re missing the Factory smart contract. Today we are implementing it and our Uniswap V1 clone will be completed.
To see the full project code click here.
What is the Factory for?
The Smart Contract of the Factory is needed to maintain a list of created Exchanges: each new deployed smart contract of the Exchange is registered in the Factory. And this is an important mechanic, as it allows you to find any Exchange by referring to the register of the Factory. And also, the presence of such a registry allows the Exchanges to find other Exchanges when the user tries to exchange the token for another token (not ether).
The factory provides another useful opportunity — the creation of a new Exchange without the need to work with code, nodes, deployment scripts and any other development tools. The factory should provide a feature that will allow users to create and deploy the Exchange simply by calling that function. Therefore, today we will learn how one smart contract can create and place another smart contract on the blockchain.
The original Uniswap has only one Factory smart contract, so there is only one register of pairs in Uniswap. Other developers are not prevented from deploying their own Factories or even smart contracts of the Exchanges, not registered in the official register of the Uniswap Factory. But such Exchanges will not be recognized by Uniswap, and they will not be able to exchange tokens through the official website.
That’s basically it. Let’s move on to the code!
Factory Implementation
Factory (hereinafter — Factory) is a registry, like any registry it needs a data structure to store the list of Exchanges (hereinafter — Exchange), and for this we will use (display) addresses to addresses — this will allow you to find Exchange by token addresses (1 Exchange can exchange only 1 token, remember?).mapping
// SPDX-License-Identifier: MITpragma solidity ^0.8.0;import "./Exchange.sol";contract Factory {
mapping(address => address) public tokenToExchange;
...
}
What follows is the function , which allows you to create Exchange by simply taking the address of the token:createExchange
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);
}
There are two checks here:
- The first ensures that the token address is not a null address (0x000000000000000000000000000000000000000000000000).
- The following check ensures that the token has not yet been added to the registry. The point is that we want to exclude the creation of different Exchanges for the same token, otherwise liquidity will be scattered across several Exchanges. It is better to concentrate it on one Exchange in order to reduce slippage and provide better exchange rates.
Next, we create an instance of Exchange with a user-provided token address, which is why we had to import “Exchange.sol” () earlier. import "./Exchange.sol"
Exchange exchange = new Exchange(_tokenAddress);
In Solidity, the operator actually places the smart contract on the blockchain. The return value is of type , but each smart contract can be converted to address. We get the hosted Exchange address using , and store it in the registry.newcontractaddress(exchange)tokenToExchange
To complete the development of a smart contract, we need to create another function — , which will allow you to request factory registry information from another smart contract (through a mechanism known as the interface):getExchange
function getExchange(address _tokenAddress)
public view returns (address) {
return tokenToExchange[_tokenAddress];
}
That’s all you need for Factory! It’s as simple as that.
Next, we need to refine the Exchange smart contract so that it can use factory to perform token exchanges for tokens.
Associate Exchange with Factory
Every Exchange should know the Factory address, but we won’t be stitching the Factory address into the Exchange code as this is bad practice. To associate Exchage with Factory, we need to add a new state variable to Exchage, which will store the Factory address, and then update the constructor:
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; // <--- новая строка
} ...
}
Exchange of tokens for tokens
How to exchange a token for a token when we have two Exchages, the information for which is stored in the Factory registry? Maybe so:
- Let’s start the standard exchange of tokens for ether.
- Instead of sending ether to the user, let’s find Exchage for the address of the token provided by the user.
- If Exchage exists, send ether to this Exhange to exchange ether for tokens.
- Let’s return the received tokens to the user.
Looks great, right? Let’s try to build that.
To do this, we will create the function :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),
"Такой Биржи не существует");
Мы используем , который является интерфейсом смарт-контракта Factory. Это хорошая практика — использовать интерфейсы при взаимодействии с другими смарт-контрактами. Однако интерфейсы не позволяют получить доступ к переменным состояния, но так как мы реализовали функцию в смарт-контракте Factory, то мы можем использовать эту функцию через интерфейс.IFactorygetExchange
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);
Aha! It sends the purchased tokens ‘y. In Solidity is dynamic, not static, and it indicates who (or what, in the case of a smart contract) initiated the current call. When a user calls the smart contract function, it will point to the user’s address. But when a smart contract calls another smart contract, it is the address of the calling smart contract!msg.sendermsg.sendermsg.sendermsg.sender
Thus, it will send tokens to the address of the first Exchange! However, this is not a problem as we can call to send tokens to the user. However, there is a more effective solution: let’s save a little and send tokens directly to the user. tokenToTokenSwapERC20(_tokenAddress).transfer(...)gas
To do this, we need to divide the function into two functions: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
is a private function that performs all the same as ethToTokenSwap, with only one difference: it accepts the address of the recipient of the tokens, which gives us flexibility in choosing who we want to send tokens to. ethToTokenSwap, in turn, is now just a wrapper for , which always transmits as a recipient.ethToTokenmsg.sender
Now we need another feature to send tokens to a specific recipient. We could use ethToToken for this, but let’s leave it private and without payable.
function ethToTokenTransfer(uint256 _minTokens, address _recipient)
public
payable
{
ethToToken(_minTokens, _recipient);
}
It’s just a copy that allows you to send tokens to a specific recipient. Now we can use it in the function:ethToTokenSwaptokenToTokenSwap
IExchange(exchangeAddress)
.ethToTokenTransfer{значение: ethBought}(_minTokensBought, msg.sender);
We send tokens to the one who initiated the exchange.
And so, we’re done!
Conclusion
Work on our copy of Uniswap V1 is complete. If you have ideas about what would be useful to add to smart contracts — go for it! For example, in Exchange, you can add a function to calculate the output number of tokens when exchanging tokens for tokens. If you’re having trouble understanding how something works, feel free to check out the tests.
Next time we’ll start exploring Uniswap V2. While it’s basically the same, the same set or basic principles, it provides powerful new features.