UniswapV2 core contract learning (2)-UniswapV2ERC20.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 the second in a series of articles for individuals to learn UniswapV2 core contract source code.

In the previous article, I have learned the first source code in the UniswapV2 core contract-the source code of the contract UniswapV2Factory.sol. This time we will learn UniswapV2ERC20.solthe source code of the second core contract . It is the parent contract of the transaction pair contract, which mainly implements the ERC20 token function and adds support for the authorization of offline signature messages. It has its own interface in addition to the standard ERC20 interface, so it is named UniswapV2ERC20.

Readers are advised to read another article of mine before starting to learn: Introduction to UniswapV2 to have a general understanding of the overall mechanism of UniswapV2, which is more helpful to understand the source code.

1. Contract source code

As usual, post the source code of the contract first, the contract is not long, the code is only 94 lines (including blank lines):

pragma solidity =0.5.16;

import './interfaces/IUniswapV2ERC20.sol';
import './libraries/SafeMath.sol';

contract UniswapV2ERC20 is IUniswapV2ERC20 {
    
    
    using SafeMath for uint;

    string public constant name = 'Uniswap V2';
    string public constant symbol = 'UNI-V2';
    uint8 public constant decimals = 18;
    uint  public totalSupply;
    mapping(address => uint) public balanceOf;
    mapping(address => mapping(address => uint)) public allowance;

    bytes32 public DOMAIN_SEPARATOR;
    // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
    bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
    mapping(address => uint) public nonces;

    event Approval(address indexed owner, address indexed spender, uint value);
    event Transfer(address indexed from, address indexed to, uint value);

    constructor() public {
    
    
        uint chainId;
        assembly {
    
    
            chainId := chainid
        }
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
                keccak256(bytes(name)),
                keccak256(bytes('1')),
                chainId,
                address(this)
            )
        );
    }

    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);
    }

    function _approve(address owner, address spender, uint value) private {
    
    
        allowance[owner][spender] = value;
        emit Approval(owner, spender, value);
    }

    function _transfer(address from, address to, uint value) private {
    
    
        balanceOf[from] = balanceOf[from].sub(value);
        balanceOf[to] = balanceOf[to].add(value);
        emit Transfer(from, to, value);
    }

    function approve(address spender, uint value) external returns (bool) {
    
    
        _approve(msg.sender, spender, value);
        return true;
    }

    function transfer(address to, uint value) external returns (bool) {
    
    
        _transfer(msg.sender, to, value);
        return true;
    }

    function transferFrom(address from, address to, uint value) external returns (bool) {
    
    
        if (allowance[from][msg.sender] != uint(-1)) {
    
    
            allowance[from][msg.sender] = allowance[from][msg.sender].sub(value);
        }
        _transfer(from, to, value);
        return true;
    }

    function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
    
    
        require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
        bytes32 digest = keccak256(
            abi.encodePacked(
                '\x19\x01',
                DOMAIN_SEPARATOR,
                keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
            )
        );
        address recoveredAddress = ecrecover(digest, v, r, s);
        require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
        _approve(owner, spender, value);
    }
}

2. Source code learning line by line

Below we will study the contract source code line by line from top to bottom. Note that in the rest of this article, the first few lines described do not contain blank lines.

  • pragma solidity =0.5.16; As usual, specify the solidity version to be used.

  • import './interfaces/IUniswapV2ERC20.sol'; import './libraries/SafeMath.sol';These two lines import the interface that the contract must implement IUniswapV2ERC20.soland an overflow-proof mathematical tool library SafeMath. The interface implemented by a contract represents its basic functions; anti-overflow math tool library applications are very common, mainly because the value can be infinitely large, but the number of storage bits is limited. For example, the maximum is 256 bits, so the largest unsigned integer is 2**256-1. No matter how big it is, it will overflow, and you will get unexpected results. In addition, because in Solidity, unsigned integers are the most used, if the subtraction results in a negative number, according to the binary representation, the result will be considered as another unsigned integer. In the early smart contracts, there were overflow loopholes or negative values ​​and losses. The smart contracts currently written generally prevent this problem from occurring, and the use of SafeMathtool libraries is the most common prevention method. Note that there are only three calculations in this library: addition, subtraction and multiplication, and no division. Because the division does not overflow; if it is divided by zero, the Solidity language itself will report an error to reset the entire transaction without additional processing.

  • contract UniswapV2ERC20 is IUniswapV2ERC20 { This line defines the IUniswapV2ERC20interface that the contract must implement the import . This interface is composed of a standard ERC20 interface plus a custom offline signature message support interface, so it UniswapV2ERC20is also an ERC20 token contract. The last curly brace is the beginning of the scope.

  • using SafeMath for uint;It means to use the SafeMathlibrary on uint256 (uint is its same name) type . The library function in Solidity is similar to the structure in the language when specifying the calling instance (such as in this example .sub), and the Rustinstance is automatically used as the first parameter in the library function.

  • string public constant name = 'Uniswap V2';
    string public constant symbol = 'UNI-V2';
    uint8 public constant decimals = 18;
    

    These three lines of code define three external state variables (token metadata) of the ERC20 token: name, symbol, and precision. The precision here is the number of decimal places. Note that since this contract is the parent contract of the transaction pair contract, and an infinite number of transaction pair contracts can be created, the names, symbols, and precisions of the ERC20 tokens in these numerous transaction pair contracts are all the same. What we usually see in exchanges is only the symbol of ERC20 tokens. From here, we can see that the symbol can be repeated and is not uniquely determined. The fundamental difference between tokens is the contract address, which is unique, and different addresses are different tokens, even if the contract code is exactly the same.

  • uint public totalSupplyState variables that record the total amount of tokens issued. Why is the access right public? This has already been discussed in the learning series (1). It mainly uses the compiler's function of automatically constructing functions with the same name to realize the corresponding interface.

  • mapping(address => uint) public balanceOf;Use a map to record the token balance of each address.

  • mapping(address => mapping(address => uint)) public allowance;It is used to record the authorization distribution of each address and is used for indirect transfer of tokens (for example, calling a third-party contract to transfer). This concept is difficult for beginners to understand, why do you need to authorize to transfer tokens? Here, as an analogy, the token contract is equivalent to the bank, and you don’t need authorization to go directly to the bank to transfer (token). But if you use WeChat to recharge and recharge the money in your bank card to your WeChat wallet, WeChat must get your authorization (including the limit) so that WeChat can transfer the money in your bank card within your authorized limit. If there is no authorization mechanism and you can transfer money directly, WeChat may empty your bank card silently. Similarly, if you access a third-party contract (non-token contract), the third-party contract cannot transfer your tokens without your authorization. Otherwise, if you encounter a malicious contract, all your tokens will be stolen at once.

  • bytes32 public DOMAIN_SEPARATOR;It is used to distinguish signature messages with the same structure and content between different Dapps. This value also helps users identify which Dapps are trusted. For details, see the eip-712 proposal.

  • bytes32 public constant PERMIT_TYPEHASHThis line of code permitcalculates the hash value according to the partial definition of the function agreed in advance , and is used when reconstructing the message signature.

  • mapping(address => uint) public nonces;Record the number of off-chain signature message transactions used by each address in the contract to prevent replay attacks.

  • The next two eventare two event definitions in the ERC20 standard, which are convenient for clients to track.

  • constructorConstructor. The constructor only does one thing, the calculated DOMAIN_SEPARATORvalue. According to the introduction of EIP-712, this value is domainSeparator = hashStruct(eip712Domain)calculated. Among them eip712Domainis a EIP712Domainstructure named , which can have one or more of the following fields:

    • string name The name of the readable signature field, such as the name of the Dapp, in this case the token name.
    • string versionThe version of the current signature field, which is "1" in this example.
    • uint256 chainId. The ID of the current chain. Note that because Solidity does not support obtaining this value directly, it uses inline assembly to obtain it.
    • address verifyingContractThe address of the verification contract is the address of the contract in this example.
    • bytes32 saltUsed to eliminate ambiguity salt, it can be used as DOMAIN_SEPARATORthe last measure. In this embodiment of the 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'performed keccak256hash value obtained by the calculation.

    Note: The structure itself cannot directly perform the hash operation, so the conversion hashStructis performed in the constructor first, which means the process of converting the structure and calculating the final hash.

  • _mintFunction, to issue additional tokens, note that it is a internalfunction, so it cannot be called externally.

  • _burnFunction, to burn tokens, it is also a internalfunction.

  • _approveFunction, perform authorization operations, note that it is a privatefunction, which means that it can only be called directly within this contract. However, it can be indirectly called through an internal or public function in the sub-contract.

  • _transferFunction, transfer token operation, note that it is also a privatefunction.

  • approveFunction, note that it is an external(external) function, an external call interface through which users usually perform authorization operations.

  • transferFunction, same as above, the external call interface for the user to transfer tokens.

  • transferFromThe token authorization transfer function, which is an external function, is mainly called by a third-party contract. Note that in its implementation (the implementation of UniswapV2), an assumption is made. If your authorization limit is the maximum (almost inexhaustible, equivalent to permanent authorization), in order to reduce the number of operation steps and gas, the authorization balance is not Deduct the corresponding amount of transferred tokens. If there is no authorization here (the authorization limit is 0), what will happen? A check that can .sub(value)not be passed when the library function is called will cause the entire transaction to be reset. So without authorization, the third-party contract cannot transfer your tokens, and you don't have to worry about your assets being stolen by other contracts.SafeMathrequire

  • permitUse offline signature messages for authorization operations. Why is there a way of using offline signatures and then online verification operations? First, offline signing does not cost any gas, and then any other account or smart contract can verify the signed message, and then perform the corresponding operations (this step may cost gas, and the signature itself does not cost gas). Another advantage of offline signature is to reduce the number of transactions on Ethereum. The use of offline signature messages in UniswapV2 is mainly to eliminate the need for authorized transactions when tokens are authorized for transfer.

3. Knowledge expansion

3.1, signed messages off-chain

Under the signed message chain knowledge can refer to the official documentation Solidity Solidity by Exampleat Micropayment Channelan example. According to different application scenarios, the signed message contains different content, but generally contains an element to prevent replay attacks. Usually the same technique as the Ethereum transaction itself is used, that is, a nonce is used to record the number of transactions in the account, and the smart contract checks the nonce to ensure that the signed message is not used multiple times. The present embodiment signed message comprises: [PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline]. As nonces[owner]++you can see from the code , every time you call permit, the nonce of the corresponding address will increase by 1, so that the original signed message can no longer be verified (the reconstructed signed message is incorrect), which prevents replay attacks. .

In Ethereum, one is added to the original rsum sof the ECDSA signature, and vthey can be used to verify the account of the signed message. There is a built-in function in Solidity ecrecoverto get the signature address of the message, which uses the signed message and r,s,vas a parameter.

The common process of using off-chain signature messages is to first reconstruct the entire signature message on the chain according to the input parameters, and then process and compare the reconstructed signature message with the input signature message to make relevant judgments and verify that the input information has not been tampered with.

The signature calculation chain is essentially analog in Solidity keccak256and abi.encodePackedfunction, thus calculating the present embodiment contracts signed message to bytes32 digest = keccak256(this line and the following code. After the calculation, a hash value is obtained digest. Using this value and the function parameter, the address of the message signer can be obtained by r,s,vusing the ecrecoverfunction. By comparing this address with the ownercomparison, you can verify whether the message is ownersigned (obviously each account can only authorize this address). Note: The signature content contains a spendersum value. If any value of the signature content is changed, the original r,s,vone cannot be verified.

UniswapV2 checked the front end, which uses web3-reactthe eth_signTypedData_v4method of calculating the signature of the message r,s,v, and eventually passed to the permitfunction as an argument. Here, the front end of V1 version directly uses Javascript + React, and the front end of V2 version uses TypeScript + React.

3.2, EIP-712

The proposal is to enhance the availability of off-chain signed messages on the chain. For details, please refer to the EIP address on github: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md , it also provides a test example Example.sol, DOMAIN_SEPARATORthe calculation method in this contract and the example Is consistent. Because the original signed message is not user-friendly, users cannot get more information from it. Using EIP-712 first allows users to understand the general description of the message signature, and second, it allows users to identify which are trusted Dapps and which are high. Risky Dapp, so you don’t want to sign the message casually and you suffer losses (for example, a malicious Dapp disguised, etc.).

3.3, why there is a permit function

Now we come to understand why the permitfunction is saved . Although the core contract of UniswapV2 has complete functions, it is not user-friendly. Users need to use its peripheral contracts to interact with the core contract. But when it comes to liquidity supply, for example, users reduce liquidity, at this time users need to burn their liquidity tokens (an ERC20 token). Since the user is calling the peripheral contract, the peripheral contract cannot be burned without authorization (mentioned above). At this point, if you follow the normal operation, the user needs to first call the transaction to authorize the surrounding contract, and then call the surrounding contract to burn. This process is essentially calling two transactions of two different contracts (cannot be combined into one transaction) , It is divided into two steps, users need to make two transactions to complete.

After using offline message signatures, one of the transactions can be reduced, and all operations can be executed in one transaction, ensuring the atomicity of the transaction. In a peripheral contract, when reducing liquidity to withdraw assets, the peripheral contract first calls the permitfunction of the trading pair within a function to authorize, and then performs operations such as transferring liquidity tokens to the trading pair contract and withdrawing tokens. All operations are carried out in the same function of the surrounding contract, which achieves the atomicity of the transaction and the user-friendliness.

The reason why the permitfunction exists and the authorization operation is performed:

When a third-party contract transfers ERC20 tokens (token transaction), the user first needs to call the token contract for authorization (authorized transaction), and then can call the third-party contract for transfer. In this way, the whole process will constitute two transactions in stages, and the user must transact twice, losing the atomicity of the transaction. Using offline message signature online verification can eliminate the need for authorized transactions, which permitis to perform online verification and perform authorization functions at the same time.

Of course, if the user knows how to operate it, he can also manually authorize the permittransaction without using the peripheral contract interface related to the function.

3.4. Token Metadata

What is token metadata? It refers to the token name, symbol (shorthand) and precision. Although these three kinds of metadata exist in the standard ERC20 protocol and must be implemented, they have no effect or meaning for the token transfer itself (the token transfer function transferand transferFromthey are not used). They belong to the external display attribute, so in the ERC1155 protocol, whether it is a homogenous token or a non-homogeneous token (such as the ERC721 collection), these three kinds of metadata have been cancelled and managed to put them off-chain (but put them in Off-chain means that an additional storage medium is required). However, the current wallet support for ERC1155 is not very friendly, and ERC1155 tokens uniformly handle various assets, which cannot meet the needs of multiple scenarios at the same time. Although the ERC1155 proposal has been in the Finalstate for two years, it has not been applied on a large scale.

This is the end of this study. Due to limited personal ability, it is inevitable that there will be mistakes or incorrect understandings. Please leave a message for correction.

Guess you like

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