I remember a sentence in the circle of friends, if Defi is the crown of Ethereum, then Uniswap is the jewel in this crown. Uniswap is currently the V2 version. Compared to V1, its functions are more fully optimized, but its contract source code is not complicated. This article is a series of records of personal learning UniswapV2 source code.
1. Brief introduction of UniswapV2 contract
UniswapV2 contracts are divided into core contracts and peripheral contracts, all written in Solidity language. Its core contract implements the complete functions of UniswapV2 (creating trading pairs, liquidity supply, trading tokens, price oracles, etc.), but it is not friendly to user operations; and peripheral contracts are used to make it more convenient for users to interact with the core contract .
The UniswapV2 core contract is mainly composed of factory
three parts: the contract (UniswapV2Factory.sol), the trading pair template contract (UniswapV2Pair.sol), and the auxiliary tool library and interface definition. Learn the UniswapV2Factory
contract first this time .
2. UniswapV2Factory
List of contract source code
The file name is UniswapV2Factory.sol
and the source code is:
pragma solidity =0.5.16;
import './interfaces/IUniswapV2Factory.sol';
import './UniswapV2Pair.sol';
contract UniswapV2Factory is IUniswapV2Factory {
address public feeTo;
address public feeToSetter;
mapping(address => mapping(address => address)) public getPair;
address[] public allPairs;
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
constructor(address _feeToSetter) public {
feeToSetter = _feeToSetter;
}
function allPairsLength() external view returns (uint) {
return allPairs.length;
}
function createPair(address tokenA, address tokenB) external returns (address pair) {
require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
(address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
bytes memory bytecode = type(UniswapV2Pair).creationCode;
bytes32 salt = keccak256(abi.encodePacked(token0, token1));
assembly {
pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
}
IUniswapV2Pair(pair).initialize(token0, token1);
getPair[token0][token1] = pair;
getPair[token1][token0] = pair; // populate mapping in the reverse direction
allPairs.push(pair);
emit PairCreated(token0, token1, pair, allPairs.length);
}
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;
}
}
The file code is very short, only 49 lines, let's learn the code line by line.
3. UniswapV2Factory
Learn the contract source code line by line
3.1, relatively simple code part
-
The first line of the code sets the version of the Solidity compiler used. It is estimated here to be more rigorous, using a precise version of the compiler
0.5.16
, rather than our usual>= 0.5.16
or^0.5.16
. -
The two
import
statements in the code import thefactory
required interface contract and trading pair template contract respectively. This is also very simple. -
contract UniswapV2Factory is IUniswapV2Factory
It is defined that theUniswapV2Factory
contract is oneIUniswapV2Factory
, and it must implement all its interfaces. -
feeTo
This state variable is mainly used to switch the development team fee switch. In UniswapV2, when users trade tokens, three-thousandths of the transaction amount will be charged and distributed to all liquidity providers. If it isfeeTo
not a zero address, it means that the switch is turned on. At this time, 1/6 of the handling fee will be divided among the development team.feeTo
Set to a zero address (default value), the switch is closed, and the 1/6 handling fee is not divided from the liquidity provider. After its access permission is set to public, the compiler will build a public function with the same name by default, which is just used to implementIUniswapV2Factory.sol
the relevant interface defined in. -
feeToSetter
This state variable is used to record who is thefeeTo
setter. The main purpose of setting the read permission to public is the same as above. -
mapping(address => mapping(address => address)) public getPair;
This state variable is a map (its key is an address type, and its value is also a map), which is used to record all transaction pair addresses. Note that its name isgetPair
and ispublic
, so the purpose is to let the default built function of the same name implement the corresponding interface. Note that there are three in this line of codeaddress
. The first two are the addresses of the two ERC20 token contracts in the transaction pair, and the last is the address of the transaction pair contract itself. -
allPairs
, An array that records the addresses of all transaction pairs. Although the transaction address has been recorded using map before, the map cannot be traversed. If you want to traverse and index, you must use an array. Pay attention to its name and permissions, also to implement the interface. -
event PairCreated(address indexed token0, address indexed token1, address pair, uint);
An event triggered when a trading pair is created. Noteindexed
that the parameter indicates that the parameter can be filtered by the listener (light client). -
constructor(address _feeToSetter) public { feeToSetter = _feeToSetter; }
The constructor is very simple. The parameter provides an initial
feeToSetter
address asfeeTo
the address of the setter, but at this timefeeTo
it is still the default address of zero, and the development team fee has not been opened. -
function allPairsLength() external view returns (uint) { return allPairs.length; }
This function is very simple. It returns the length of the address array of all transaction pairs, so that it is convenient to
for
traverse the array in a form like this outside the contract . -
Let's skip the
createPair
function first , and learn the function last. Let's look at thesetFeeTo
function first :function setFeeTo(address _feeTo) external { require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); feeTo = _feeTo; }
This function is also very simple. It is used to set a new
feeTo
switch for the development team's handling fee (it can be the address for the development team to receive the handling fee, or it can be a zero address). Note that this function first to userequire
the function to verify the caller mustfeeTo
set thosefeeToSetter
, if not it will reset the entire transaction. -
setFeeToSetter
functionfunction setFeeToSetter(address _feeToSetter) external { require(msg.sender == feeToSetter, 'UniswapV2: FORBIDDEN'); feeToSetter = _feeToSetter; }
This function is used to transfer
feeToSetter
. It first determines that the caller must be the originalfeeToSetter
, otherwise the entire transaction is reset.But there may be such a situation: when
feeToSetter
the new setter address_feeToSetter
is accidentally entered byfeeToSetter
mistake , the setting will take effect immediately. At this time, it is an incorrect or unfamiliar address without control rights, which can no longer be set through this function. come back. Although the UniswapV2 team will not have such negligence, it may happen when we use it ourselves. One way to solve this problem is to use an intermediate address value to transition, and the new onefeeToSetter
must call an accept method to truly become the setter. If a setting error is found before accepting, the original setter can reset it. The specific code implementation can refer toOwned
theowner
transfer implementation of the following contract :pragma solidity ^0.4.24; contract Owned { address public owner; address public newOwner; event OwnershipTransferred(address indexed _from, address indexed _to); constructor() public { owner = msg.sender; } modifier onlyOwner { require(msg.sender == owner,"invalid operation"); _; } function transferOwnership(address _newOwner) public onlyOwner { newOwner = _newOwner; } function acceptOwnership() public { require(msg.sender == newOwner,"invalid operation"); emit OwnershipTransferred(owner, newOwner); owner = newOwner; newOwner = address(0); } }
3.2, createPair
function
As the name suggests, this function is used to create trading pairs. The reason for putting this function at the end is that it is relatively complicated and there are some knowledge points to expand. Now let's analyze the function in detail. The function code has been listed in the source code section. Note: The first few lines mentioned below do not contain blank lines (blank lines are skipped).
This function accepts any two token addresses as parameters to create a new trading pair contract and return the address of the new contract. Note that its visibility is external
and is not limited, which means that any account (or contract) outside the contract can call this function to create a new ERC20/ERC20 trading pair (provided that the ERC20/ERC20 trading pair has not been created ).
-
The first four lines of this function are mainly used for parameter verification and at the same time sort the token addresses from small to large.
- The first line is used to verify that the contract addresses of the two tokens cannot be the same, that is, the transaction pair must be two different ERC20 tokens.
- The second line is used to sort the contract addresses of the two tokens from small to large, because the bottom layer of the address type is actually uint160, so there are sizes that can be sorted.
- Line 3 is used to verify that the two addresses cannot be zero addresses. Why is
token0
it only verified , becausetoken1
it is larger than it, if it is not a zero address,token1
it is definitely not a zero address. - Line 4 is used to verify that the transaction pair has not been created (the same transaction pair cannot be created repeatedly).
-
Lines 5-10 of this function are used to create and initialize the trading pair contract.
-
Line 5 is used to obtain
UniswapV2Pair
the creation bytecode of the transaction pair template contractcreationCode
. Note that the result it returns is a byte array containing the created bytecode, of typebytes
. Similarly, there are runtime bytecodesruntimeCode
.creationCode
Mainly used to customize the contract creation process in inline assembly, especially when used increate2
opcodes, herecreate2
is relative tocreate
opcodes. Note that this value cannot be obtained in the contract itself or in the inherited contract, because this will cause self-circular references. -
Line 6 is used to calculate one
salt
. Note that it uses two token addresses as the calculation source, which means that for any transaction pair, it shouldsalt
be a fixed value and can be calculated offline. -
The 7th line
assembly
represents this is a piece of embedded assembly code, and the embedded assembly language in Solidity is Yul language. In Yul, use the built-in function of the same name instead of directly using the opcode, which is easier to read. The following left parenthesis represents the beginning of the inline assembly scope. -
Line 8 uses a
create2
function in the Yul code (the function name indicates that the create2 opcode is used) to create a new contract. Let's look at the definition of this function:create2(v, p, n, s) C create new contract with code mem[p…(p+n)) at address keccak256(0xff . this . s . keccak256(mem[p…(p+n))) and send v wei and return the new address, where 0xff
is a 1 byte value,this
is the current contract’s address as a 20 byte value ands
is a big-endian 256-bit value-
The first column is the function definition, you can see that it has four parameters.
-
The second column represents the version of Ethereum that is starting to apply.
C
Represents Constantinople, which is available from the Constantinople version. Correspondingly there are F-Frontier version, H-Homeland version, B-Byzantine version, I-Istanbul version. On the timeline, the different versions from old to new are:F => H => B => C => I, which is the frontier => Homeland => Byzantium => Constantinople => Istanbul.
Pay attention to the corresponding Ethereum version when using this function.
-
The third column is the explanation, which you can see
v
the number of eth send representatives to a new contract (inwei
units),p
on behalf of code starting memory address,n
the representative of the code length,s
on behalf ofsalt
. In addition, it also gives the calculation formula for the new contract address.
-
-
Line 9 is the end of the inline assembly scope.
-
Line 10 is to call an initialization method of the newly created transaction pair contract and pass the sorted token addresses. Why do you want to do this, because
create2
you cannot provide constructor parameters when you create a contract with a function.
-
-
Lines 11-14 of this function are used to record the newly created transaction pair address and trigger the transaction pair creation event.
- Lines 11 and 12 are used to record the transaction pair address in the map. Because: 1. A/B transaction pair is also a B/A transaction pair; 2. When querying the transaction pair, the two token addresses provided by the user are not sorted, so it needs to be recorded twice.
- Line 13 records the address of the transaction pair in the array to facilitate external indexing and traversal of the contract.
- Line 14 triggers the trading pair creation event.
-
create2
Expansion of knowledge points in functions.-
Here we first mention the memory management of the account in the Ethereum virtual machine. Each account (including the contract) has a memory area, which is linear and is addressed at the byte level, but is limited to 256 bits (32 bytes) for reading, and 8 bits for writing ( 1 byte) or 256 bits (32 bytes) in size.
-
When accessing local variables in embedded assembly in Solidity, if the local variable is a value type, the value is used directly; if the local variable is a reference type (reference to memory or calldata), then its address in memory or calldata will be used, and Not the value itself. In Solidity, it
bytes
is a dynamically sized byte array, which is not a value type but a reference type. The similarstring
is the reference type.
Note that
create2
the type information is used in the function callcreationCode
, combined with the above knowledge expansion, we can get from the function code:bytecode
The byte array that contains the bytecode in the memory is created. Its type isbytes
a reference type . According to the above-mentioned memory read restrictions and the rules of inline assembly to access local reference variables, the actual value in the inline assembly is the memory address of the byte array. The function first reads the starting 256 bits (32 bytes) of the memory address,creationCode
the length it stores , and the specific method for obtaining itmload(bytecode)
.creationCode
The starting address of the actual content in the memory isadd(bytecode, 32)
. Why would webytecode
add 32 to it? Because just mentionedbytecode
thatcreationCode
the length from the first 32 bytes is stored, and the actualcreationCode
content is stored from the second 32 bytes .create2
The p in the function explanation corresponds to in the codeadd(bytecode, 32)
, and the n in the explanation corresponds tomload(bytecode)
.
-
In fact, this method is very common in Ethereum. For example, when the parameter of a function call is an array (calldata type), after the parameter part is encoded, first the first unit (32 bytes) records the length of the array, and then the array Elements, each element (value type) has a unit (32 bytes).
Because the use of inline assembly will increase the difficulty of reading, after Solidity 0.6.2, a new syntax is provided to realize the create2
function of the function, and the use of salt
creating contracts is directly supported at the language level . See the contract d
creation process in the sample code below :
pragma solidity >0.6.1 <0.7.0;
contract D {
uint public x;
constructor(uint a) public {
x = a;
}
}
contract C {
function createDSalted(bytes32 salt, uint arg) public {
/// This complicated expression just tells you how the address
/// can be pre-computed. It is just there for illustration.
/// You actually only need ``new D{salt: salt}(arg)``.
address predictedAddress = address(bytes20(keccak256(abi.encodePacked(
byte(0xff),
address(this),
salt,
keccak256(abi.encodePacked(
type(D).creationCode,
arg
))
))));
D d = new D{
salt: salt}(arg);
require(address(d) == predictedAddress);
}
}
In this code, a custom contract is created by adding options directly after new
the D
contract type salt
, which is equivalent to using the create2
function in Yul . Note that predictedAddress
the calculation method in this example create2
is the same as the address calculation method in the function explanation.
Note that using the syntax in the example to create a new contract can also provide constructor parameters. There create2
is no problem that the constructor parameters cannot be used in the function, so it also removes some of the requirements of the new contract initialization function (initialization is performed in the builder ). But UniswapV2 specifies the compiler version of Solidity 0.5.16
, so this syntax cannot be used. If we want to use it ourselves, we need to specify the compiler version as 0.6.2
above, and we need to pay attention to 0.6.2
the specific version and 0.5.16
version of Solidity above and modify it.
At this point, UniswapV2Factory.sol
the learning of the first contract in the UniswapV2 core contract is over. Due to limited personal ability, it is inevitable that there will be misunderstandings or incorrect places. Please leave a message and correct me.