UniswapV2 core contract learning (1) — UniswapV2Factory.sol

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 factorythree parts: the contract (UniswapV2Factory.sol), the trading pair template contract (UniswapV2Pair.sol), and the auxiliary tool library and interface definition. Learn the UniswapV2Factorycontract first this time .

2. UniswapV2FactoryList of contract source code

The file name is UniswapV2Factory.soland 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. UniswapV2FactoryLearn the contract source code line by line

3.1, relatively simple code part

  1. 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.16or ^0.5.16.

  2. The two importstatements in the code import the factoryrequired interface contract and trading pair template contract respectively. This is also very simple.

  3. contract UniswapV2Factory is IUniswapV2FactoryIt is defined that the UniswapV2Factorycontract is one IUniswapV2Factory, and it must implement all its interfaces.

  4. feeToThis 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 is feeTonot 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. feeToSet 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 implement IUniswapV2Factory.solthe relevant interface defined in.

  5. feeToSetterThis state variable is used to record who is the feeTosetter. The main purpose of setting the read permission to public is the same as above.

  6. 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 is getPairand is public, 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 code address. 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.

  7. 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.

  8. event PairCreated(address indexed token0, address indexed token1, address pair, uint);An event triggered when a trading pair is created. Note indexedthat the parameter indicates that the parameter can be filtered by the listener (light client).

  9. constructor(address _feeToSetter) public {
          
          
        feeToSetter = _feeToSetter;
    }
    

    The constructor is very simple. The parameter provides an initial feeToSetteraddress as feeTothe address of the setter, but at this time feeToit is still the default address of zero, and the development team fee has not been opened.

  10. 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 fortraverse the array in a form like this outside the contract .

  11. Let's skip the createPairfunction first , and learn the function last. Let's look at the setFeeTofunction 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 feeToswitch 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 use requirethe function to verify the caller must feeToset those feeToSetter, if not it will reset the entire transaction.

  12. setFeeToSetterfunction

    function 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 original feeToSetter, otherwise the entire transaction is reset.

    But there may be such a situation: when feeToSetterthe new setter address _feeToSetteris accidentally entered by feeToSettermistake , 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 one feeToSettermust 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 to Ownedthe ownertransfer 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, createPairfunction

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 externaland 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 ).

  1. 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 token0it only verified , because token1it is larger than it, if it is not a zero address, token1it 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).
  2. Lines 5-10 of this function are used to create and initialize the trading pair contract.

    • Line 5 is used to obtain UniswapV2Pairthe creation bytecode of the transaction pair template contract creationCode. Note that the result it returns is a byte array containing the created bytecode, of type bytes. Similarly, there are runtime bytecodes runtimeCode. creationCodeMainly used to customize the contract creation process in inline assembly, especially when used in create2opcodes, here create2is relative to createopcodes. 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 should saltbe a fixed value and can be calculated offline.

    • The 7th line assemblyrepresents 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 create2function 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 and s 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. CRepresents 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 vthe number of eth send representatives to a new contract (in weiunits), pon behalf of code starting memory address, nthe representative of the code length, son behalf of salt. 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 create2you cannot provide constructor parameters when you create a contract with a function.

  3. 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.
  4. create2Expansion 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 bytesis a dynamically sized byte array, which is not a value type but a reference type. The similar stringis the reference type.

    Note that create2the type information is used in the function call creationCode, combined with the above knowledge expansion, we can get from the function code:

    • bytecodeThe byte array that contains the bytecode in the memory is created. Its type is bytesa 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, creationCodethe length it stores , and the specific method for obtaining it mload(bytecode).
    • creationCodeThe starting address of the actual content in the memory isadd(bytecode, 32) . Why would we bytecodeadd 32 to it? Because just mentioned bytecodethat creationCodethe length from the first 32 bytes is stored, and the actual creationCodecontent is stored from the second 32 bytes .
    • create2The p in the function explanation corresponds to in the code add(bytecode, 32), and the n in the explanation corresponds to mload(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 create2function of the function, and the use of saltcreating contracts is directly supported at the language level . See the contract dcreation 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 newthe Dcontract type salt, which is equivalent to using the create2function in Yul . Note that predictedAddressthe calculation method in this example create2is 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 create2is 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.2above, and we need to pay attention to 0.6.2the specific version and 0.5.16version of Solidity above and modify it.

At this point, UniswapV2Factory.solthe 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.

Guess you like

Origin blog.csdn.net/weixin_39430411/article/details/108842197