Starting from public, Keegan small steel 】 【 welcome attention for more articles, original text links: mp.weixin.qq.com/s/ICE77y_Gx…
preface
In DeFi track, DEX is undoubtedly the most core piece, and Uniswap is the leader in the whole DEX field, such as SushiSwap, PancakeSwap and so on are Fork Uniswap. While there are a lot of articles about Uniswap on the web, most of them are just about the mechanics, not about the implementation, and some questions remain unanswered, such as: How is the commission allocation implemented? How is the optimal path obtained? How to use TWAP? How many LP tokens are returned when liquidity is injected? So, I took a look at Uniswap at the code level to understand these issues, but also to understand Uniswap in general and in detail.
Now, Uniswap is available in both V2 and V3 versions, so let’s talk about V2 first.
The open source project
The entire UniswapV2 product is split into several smaller open source projects, including:
- uniswap-interface
- uniswap-v2-sdk
- uniswap-sdk-core
- uniswap-info
- uniswap-v2-subgraph
- uniswap-v2-core
- uniswap-v2-periphery
- uniswap-lib
The first three are front-end App projects, that is, projects that provide transactions, corresponding to the app.uniswap.org web functionality, the display pages are written in the Uniswap-Interface project, Uniswap-v2-sdk and Uniswap-SDK-core exist as SDKS, uniswap-interface will refer to v2-SDK and SDK-core, Use @uniswap/ v2-SDK and @Uniswap/SDK-core to import the TS files to be used.
However, uniswap-Interface’s latest code is actually synchronized with the online version, i.e. the V3 version is integrated. If you only want to deploy the V2 version of the front end, it can find out the history of the version of the project code for deployment, if it is not with the liquidity mining function, recommend the September 2020 version, if it is with the mining function, it can try the October 2020 version.
Uniswap-info is the Uniswap Analytics project, which corresponds to the official website at info.uniswap.org and shows some statistical analysis data that is read from Subgraph. Uniswap-v2-subgraph is a subgraph project.
The last three are contracts. Uniswap-v2-core is the implementation of the core contract. Uniswap-v2-periphery provides contracts, mainly routing contracts, that interact with UniswapV2. Uniswap-lib encapsulates some of the tool contracts. The implementation of contracts in Core and Periphery is something we’ll focus on later.
In addition, Uniswap actually has a liquidity mining contract project named Liquids-Staker, which few people know about because Uniswap’s liquidity mining contract was only online for a short period last year, but I think it’s worth taking a closer look at this part of the implementation. After all, many imitation disks also have liquidity mining function.
Finally, I strongly recommend you to have a look at master Cui Mian’s video tutorial when you have time. Two sets of tutorials have been released:
- Taught you how to develop a decentralized exchange: www.bilibili.com/video/BV1jk…
- Will be deployed to all UniswapV2 block chain – more decentralized exchange Uniswap chain deployment teaching video: www.bilibili.com/video/BV1ph…
Some of the key things I learned later in this article are also from the above videos. And then we’re going to talk about some of the key contract implementations.
uniswap-v2-core
There are three main contract documents:
- Uniswapv2factory. sol: Factory contract
- Uniswapv2pair. sol: indicates a matching contract
- Uniswapv2erc20.sol: LP Token contract
Matching contracts manage the liquidity pool. Different currency pairs have different matching contract instances. For example, usDT-weth is corresponding to one matching contract instance, while Dai-weth is corresponding to another.
LP tokens, which are essentially similar to Compound’s CTokens, are tokens that users use to inject liquidity into the pool. When a user transfers two coins into the matching contract of a coin pair, that is, adding liquidity, they can get LP tokens returned by the matching contract and enjoy the fee sharing revenue.
Each pairing contract has a corresponding LP Token bound to it. In fact, UniswapV2Pair inherits UniswapV2ERC20, so the pairing contract itself is also an LP Token contract.
A factory contract is used to deploy a pair contract, creating a new pair contract instance through the factory contract createPair() function.
The relationship between the three contracts is shown in the following figure (from master Cui mian’s tutorial video) :
The factory contract
The core factory contract function is createPair(), which is implemented as follows:
It uses Create2 to create the contract, which is an assembler opcode, and that’s what I’m going to focus on.
As many of you know, you can create a new contract by using the new keyword. For example, to create a new match contract, you can write:
UniswapV2Pair newPair = new UniswapV2Pair();
Copy the code
Why not use the new method instead of calling create2 to create a new contract? The biggest benefit of using Create2 is that you can calculate the deployment address of a smart contract before deploying it. The key is the following line of code:
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
Copy the code
The first line retrieves the creation bytecode creationCode for the UniswapV2Pair contract code. The resulting value is typically like this:
0x0cf061edb29fff92bda250b607ac9973edf2282cff7477decd42a678e4f9b868
Copy the code
Similarly, there is a runtime bytecode called runtimeCode, which is not used here.
This creation bytecode would actually be used in UniswapV2Library in the Periphery project and is hardcoded into values. So for convenience, add a line of code to the factory contract to save this creation bytecode:
bytes32 public constant INIT_CODE_PAIR_HASH = keccak256(abi.encodePacked(type(UniswapV2Pair).creationCode));
Copy the code
Going back to the above code, the second line calculates a salt value based on the two token addresses. For any pair of coins, the calculated salt value is also fixed, so the salt value of the pair can also be calculated offline.
We then package an inline assembly code with the Assembly keyword that calls the CREATE2 opcode to create the new contract. Because the creation bytecode of the UniswapV2Pair contract is fixed and the salt value of the two coin pairs is fixed, the final calculated address of the pair is fixed.
Except for this part of the code for Create2 that creates a new contract, it’s pretty easy to understand and I won’t go into it.
UniswapV2ERC20 contracts
The pairing contract inherits the UniswapV2ERC20 contract. Let’s first look at the implementation of the UniswapV2ERC20 contract, which is relatively simple.
UniswapV2ERC20 is a liquid Token contract, also known as LP Token, but the actual name of the Token is UniswapV2, or UNI-V2 for short, which is defined directly in the code:
string public constant name = 'Uniswap V2';
string public constant symbol = 'UNI-V2';
Copy the code
The total amount of tokens, totalSupply, is initially 0 and can be minted by calling _mint() or destroyed by calling _burn(). Add or subtract from totalSupply () and balance (); add or subtract from balance ();
function _mint(address to, uint value) internal {
totalSupply = totalSupply.add(value);
balanceOf[to] = balanceOf[to].add(value);
emit Transfer(address(0), to, value);
}
function _burn(address from, uint value) internal {
balanceOf[from] = balanceOf[from].sub(value);
totalSupply = totalSupply.sub(value);
emit Transfer(from, address(0), value);
}
Copy the code
In addition, UniswapV2ERC20 provides a permit() function that allows users to sign an off-chain approve transaction, generating a signature that anyone can use and submit to the blockchain. There are many articles on the Internet about the specific function and usage of the permit function, but I won’t go into it here.
After that, the rest of the functions are ERC20 compliant.
Matching contract
As mentioned earlier, pairing contracts are created by factory contracts, as we can see from constructors and initializers:
constructor() public {
factory = msg.sender;
}
// called once by the factory at time of deployment
function initialize(address _token0, address _token1) external {
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
token0 = _token0;
token1 = _token1;
}
Copy the code
The constructor sets the MSG. Sender directly to factory, which is the factory contract address. The initialization function requires the caller to be a factory contract, and the factory contract only initializes once.
However, have you ever wondered why we need to define another initializer instead of initializing _token0 and _token1 directly in the constructor? This is because the way the contract was created with CREATE2 restricts the constructor from taking arguments.
In addition, there are three core functions in pairing contracts: mint(), burn(), and swap(). They are the underlying functions of adding liquidity, removing liquidity, and exchanging.
Mint () function
Take a look at the mint() function, which is designed to obtain liquid tokens by simultaneously injecting two token assets:
Since this is an underlying function that adds liquidity, why isn’t there an input of two tokens in the argument? This is probably the first question that comes to most people’s mind. In fact, before this function is called, the routing contract has completed the operation of transferring the user’s token number to the matching contract. So, if you look at the first five lines of code, you compute the amount0 and amount1 inputs for the two tokens by taking the current balances balance0 and balance1 and subtracting _reserve0 and _reserve1, respectively, the original amount of the two tokens in the pool. In addition, we added the lock modifier to this function, which is a reentrant modifier to ensure that every time you add liquidity, no more than one user will transfer money to the matching contract at the same time, otherwise the amount0 and amount1 values of the user cannot be calculated.
Line 6 calculates the protocol fee. There is an address for feeTo in the plant contract that, if set to a non-zero address, represents an agreement fee for adding and removing liquidity, but Uniswap has not set that address until now.
Lines 7 through 15 then calculate how many liquid tokens the user will receive. When totalSupply is 0, it is the initial liquidity, and the calculation formula is:
liquidity = √(amount0*amount1) - MINIMUM_LIQUIDITY
Copy the code
That is, multiply the square root of the two token inputs and subtract the minimum liquidity. The minimum liquidity is 1000, which is permanently locked at address zero. To do so, mainly for security, specific reasons can be viewed in the white paper and official documentation.
If the initial liquidity is not provided, then the liquidity takes the smaller of the following two values:
liquidity1 = amount0 * totalSupply / reserve0
liquidity2 = amount1 * totalSupply / reserve1
Copy the code
After calculating the amount of liquidity that the user will receive, the aforementioned _mint() function will be invoked to create LP tokens of the amount of liquidity and give them to the user.
This is followed by the _update() function, which does two things: update reserve0 and reserve1, and sum price0CumulativeLast and price1CumulativeLast. These two prices are used to calculate TWAP, more on that later.
The second line from the bottom is to update the kLast value, which is the product of reserve0 and Reserve1, if the protocol fee is enabled. This value is actually only used to calculate the protocol fee.
The last line emits a Mint() event.
Burn () function
Next comes the burn() function, which is the underlying function for removing liquidity:
This function is basically to destroy the liquid token and withdraw the corresponding two token assets to the user.
The first one is line 6, which gets the liquid token balance at the current contract address. Normally, there are no liquid tokens in the matching contract, as all liquid tokens are given to the liquidity provider. There is value here because the routing contract transfers the user’s liquid tokens to the matching contract first.
Line 7 calculates the protocol fee the same as the mint() function.
Then it is time to calculate the amount of each token that can be withdrawn. The calculation formula is also very simple:
amount = liquidity / totalSupply * balanceExtract the number=User flow/Total liquidity*Total balance of tokensCopy the code
I’ve changed the order of the calculations so that it makes sense. The user liquidity is divided by the total liquidity to get the user’s share of the total liquidity pool, and then multiplied by the total token balance to get the user’s share of the total tokens. For example, the liquidity of the users is 1000, and the totalSupply is 10000. In other words, the liquidity of the users is 10%. If the total amount of tokens in the pool is 2000, the users can get 10% of the 2,000 tokens, that is, 200 tokens.
The logic behind this is to call _burn() to destroy the liquid token, transfer the calculated amount of the two token assets to the user, and finally update the reserve of both tokens.
The last two lines of code are the same as the mint() function, so I won’t go into detail.
Swap () function
Swap () is the underlying function that does the exchange. Look at the code:
This function takes four parameters, amount0Out and amount1Out representing the number of Token0s and Token1s to be rolled-out as a result of the exchange. These two values are usually one zero and one non-zero, but in a flash transaction they may be neither. The TO parameter is the address of the receiver, and the last data parameter is the data that was passed when the callback was executed, which is 0 if exchanged through the routing contract.
The first three lines of code are easy to understand. The first step is to check whether one of the resulting amounts is greater than zero, then to read out the reserve for both tokens, and then to check whether the amount is less than the reserve.
Starting at line 6 and ending at line 15, a pair of curly braces is used to limit the scope of the two temporary variables _token{0,1} and prevent the stack from being too deep and causing errors.
Then, looking at lines 10 and 11, the tokens are transferred to the recipient’s address. See here, some partners may have questions: this is an external function, any user can call their own, no check on the direct transfer, that is not who can casually withdraw money? In fact, there are checks in the back, as we’ll see if we can scroll down.
In line 12, if the length of the data argument is greater than 0, the to address is converted to IUniswapV2Callee and its uniswapV2Call() function is called, which is essentially a callback function, and the to address needs to implement the interface.
In lines 13 and 14, get the current balance of the two tokens, balance{0,1}, which is the balance after the tokens are transferred out.
Lines 16 and 17 calculate the actual number of tokens transferred. The actual number of inputs is usually one zero and one non-zero. To understand how the formula works, let me give you an example.
So let’s say I roll in token0, I roll out token1, I roll in 100, I roll out 200. Then, the following values would be:
amount0In = 100
amount1In = 0
amount0Out = 0
amount1Out = 200
Copy the code
Reserve 0 and reserve1 are assumed to be 1000 and 2000 respectively. Balance {0,1} and reserve{0,1} are equal before the exchange. And after the completion of the token in and out, in fact, Balance0 becomes 1000 + 100-0 = 1100, balance1 becomes 2000 + 0-200 = 1800. The formula is as follows:
balance0 = reserve0 + amount0In - amout0Out
balance1 = reserve1 + amount1In - amout1Out
Copy the code
Push it back and you get:
amountIn = balance - (reserve - amountOut)
Copy the code
So now you see the logic behind calculating amountIn in your code.
The following code is a constant product check after deducting transaction fees, using the following formula:
Where 0.003 is the transaction transaction rate, X0 and Y0 are reserve0 and reserve1, X1 and Y1 are balance0 and balance1, Xin and Yin are amount0In and amount1In. The fact that this formula holds means that the transaction fee has indeed been charged prior to the underlying exchange.
conclusion
Limited by space, I will stop here and leave the rest for the next part.
Starting from public, Keegan small steel 】 【 welcome attention for more articles, original text links: mp.weixin.qq.com/s/ICE77y_Gx…