UniswapV2 peripheral contract learning (9) - ExampleCombinedSwapAddRemoveLiquidity.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 record articles for personal learning UniswapV2 source code.

1. Single asset liquidity supply

We know that Uniswap must simultaneously inject the two assets in the trading pair in proportion to provide liquidity, and then obtain liquidity tokens. Two kinds of assets are needed at the same time, which virtually raises the threshold for users. What if the user has only one (or only one) asset? Can it provide liquidity supply? The answer is yes.

RouterProvides an interface for liquidity management in the contract, as well as an interface for asset transactions. Then we can combine these two functions in the same function, first trade assets and then provide liquidity, or remove liquidity and then trade assets.

If a single asset provides liquidity, first part of the assets provided must be traded to obtain another asset, so that there are two assets. Then call the Routercontract's increase liquidity interface to inject assets to provide liquidity. Then how many assets are divided for trading to ensure that all the assets are injected (the ratio at the time of injection is the same as the ratio in the transaction pair after the transaction) is the core of this operation, and it needs to be calculated using a formula.

If you remove liquidity and get a single asset, this is relatively simple. First remove the liquidity to get two kinds of assets, and then exchange one of the assets for the other. The asset obtained in this way is the amount withdrawn plus the amount obtained by the transaction.

It can be seen that for Uniswap, the single-asset liquidity supply here is essentially dual-asset injection (determined by its underlying realization), but the single-asset is converted into dual-assets in advance.

Note: There are other types of single-asset liquidity supplies, such as BancorV2 (Bancor Second Edition) and some similar DEFIs. They do not need to convert a single asset into dual assets, but generate a liquidity token for each asset in the trading pair, thereby realizing single asset injection.

Second, the ExampleCombinedSwapAddRemoveLiquidity.solsource code

The source code of this contract is not under the branch of the peripheral contract uniswap-v2-peripherymaster , but swap-before-liquidity-eventsunder the branch. The directory is the examplesdirectory.

The contract source code is:

pragma solidity =0.6.6;

import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";
import "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "@uniswap/lib/contracts/libraries/Babylonian.sol";
import "@uniswap/lib/contracts/libraries/TransferHelper.sol";

import "../interfaces/IUniswapV2Router01.sol";
import "../interfaces/IWETH.sol";
import "../interfaces/IERC20.sol";
import "../libraries/SafeMath.sol";
import "../libraries/UniswapV2Library.sol";

// enables adding and removing liquidity with a single token to/from a pair
// adds liquidity via a single token of the pair, by first swapping against the pair and then adding liquidity
// removes liquidity in a single token, by removing liquidity and then immediately swapping
contract ExampleCombinedSwapAddRemoveLiquidity {
    
    
    using SafeMath for uint;

    IUniswapV2Factory public immutable factory;
    IUniswapV2Router01 public immutable router;
    IWETH public immutable weth;

    constructor(IUniswapV2Factory factory_, IUniswapV2Router01 router_, IWETH weth_) public {
    
    
        factory = factory_;
        router = router_;
        weth = weth_;
    }

    // grants unlimited approval for a token to the router unless the existing allowance is high enough
    function approveRouter(address _token, uint256 _amount) internal {
    
    
        uint256 allowance = IERC20(_token).allowance(address(this), address(router));
        if (allowance < _amount) {
    
    
            if (allowance > 0) {
    
    
                // clear the existing allowance
                TransferHelper.safeApprove(_token, address(router), 0);
            }
            TransferHelper.safeApprove(_token, address(router), uint256(-1));
        }
    }

    // returns the amount of token that should be swapped in such that ratio of reserves in the pair is equivalent
    // to the swapper's ratio of tokens
    // note this depends only on the number of tokens the caller wishes to swap and the current reserves of that token,
    // and not the current reserves of the other token
    function calculateSwapInAmount(uint reserveIn, uint userIn) public pure returns (uint) {
    
    
        return Babylonian.sqrt(reserveIn.mul(userIn.mul(3988000) + reserveIn.mul(3988009))).sub(reserveIn.mul(1997)) / 1994;
    }

    // internal function shared by the ETH/non-ETH versions
    function _swapExactTokensAndAddLiquidity(
        address from,
        address tokenIn,
        address otherToken,
        uint amountIn,
        uint minOtherTokenIn,
        address to,
        uint deadline
    ) internal returns (uint amountTokenIn, uint amountTokenOther, uint liquidity) {
    
    
        // compute how much we should swap in to match the reserve ratio of tokenIn / otherToken of the pair
        uint swapInAmount;
        {
    
    
            (uint reserveIn,) = UniswapV2Library.getReserves(address(factory), tokenIn, otherToken);
            swapInAmount = calculateSwapInAmount(reserveIn, amountIn);
        }

        // first take possession of the full amount from the caller, unless caller is this contract
        if (from != address(this)) {
    
    
            TransferHelper.safeTransferFrom(tokenIn, from, address(this), amountIn);
        }
        // approve for the swap, and then later the add liquidity. total is amountIn
        approveRouter(tokenIn, amountIn);

        {
    
    
            address[] memory path = new address[](2);
            path[0] = tokenIn;
            path[1] = otherToken;

            amountTokenOther = router.swapExactTokensForTokens(
                swapInAmount,
                minOtherTokenIn,
                path,
                address(this),
                deadline
            )[1];
        }

        // approve the other token for the add liquidity call
        approveRouter(otherToken, amountTokenOther);
        amountTokenIn = amountIn.sub(swapInAmount);

        // no need to check that we transferred everything because minimums == total balance of this contract
        (,,liquidity) = router.addLiquidity(
            tokenIn,
            otherToken,
        // desired amountA, amountB
            amountTokenIn,
            amountTokenOther,
        // amountTokenIn and amountTokenOther should match the ratio of reserves of tokenIn to otherToken
        // thus we do not need to constrain the minimums here
            0,
            0,
            to,
            deadline
        );
    }

    // computes the exact amount of tokens that should be swapped before adding liquidity for a given token
    // does the swap and then adds liquidity
    // minOtherToken should be set to the minimum intermediate amount of token1 that should be received to prevent
    // excessive slippage or front running
    // liquidity provider shares are minted to the 'to' address
    function swapExactTokensAndAddLiquidity(
        address tokenIn,
        address otherToken,
        uint amountIn,
        uint minOtherTokenIn,
        address to,
        uint deadline
    ) external returns (uint amountTokenIn, uint amountTokenOther, uint liquidity) {
    
    
        return _swapExactTokensAndAddLiquidity(
            msg.sender, tokenIn, otherToken, amountIn, minOtherTokenIn, to, deadline
        );
    }

    // similar to the above method but handles converting ETH to WETH
    function swapExactETHAndAddLiquidity(
        address token,
        uint minTokenIn,
        address to,
        uint deadline
    ) external payable returns (uint amountETHIn, uint amountTokenIn, uint liquidity) {
    
    
        weth.deposit{
    
    value: msg.value}();
        return _swapExactTokensAndAddLiquidity(
            address(this), address(weth), token, msg.value, minTokenIn, to, deadline
        );
    }

    // internal function shared by the ETH/non-ETH versions
    function _removeLiquidityAndSwap(
        address from,
        address undesiredToken,
        address desiredToken,
        uint liquidity,
        uint minDesiredTokenOut,
        address to,
        uint deadline
    ) internal returns (uint amountDesiredTokenOut) {
    
    
        address pair = UniswapV2Library.pairFor(address(factory), undesiredToken, desiredToken);
        // take possession of liquidity and give access to the router
        TransferHelper.safeTransferFrom(pair, from, address(this), liquidity);
        approveRouter(pair, liquidity);

        (uint amountInToSwap, uint amountOutToTransfer) = router.removeLiquidity(
            undesiredToken,
            desiredToken,
            liquidity,
        // amount minimums are applied in the swap
            0,
            0,
        // contract must receive both tokens because we want to swap the undesired token
            address(this),
            deadline
        );

        // send the amount in that we received in the burn
        approveRouter(undesiredToken, amountInToSwap);

        address[] memory path = new address[](2);
        path[0] = undesiredToken;
        path[1] = desiredToken;

        uint amountOutSwap = router.swapExactTokensForTokens(
            amountInToSwap,
        // we must get at least this much from the swap to meet the minDesiredTokenOut parameter
            minDesiredTokenOut > amountOutToTransfer ? minDesiredTokenOut - amountOutToTransfer : 0,
            path,
            to,
            deadline
        )[1];

        // we do this after the swap to save gas in the case where we do not meet the minimum output
        if (to != address(this)) {
    
    
            TransferHelper.safeTransfer(desiredToken, to, amountOutToTransfer);
        }
        amountDesiredTokenOut = amountOutToTransfer + amountOutSwap;
    }

    // burn the liquidity and then swap one of the two tokens to the other
    // enforces that at least minDesiredTokenOut tokens are received from the combination of burn and swap
    function removeLiquidityAndSwapToToken(
        address undesiredToken,
        address desiredToken,
        uint liquidity,
        uint minDesiredTokenOut,
        address to,
        uint deadline
    ) external returns (uint amountDesiredTokenOut) {
    
    
        return _removeLiquidityAndSwap(
            msg.sender, undesiredToken, desiredToken, liquidity, minDesiredTokenOut, to, deadline
        );
    }

    // only WETH can send to this contract without a function call.
    receive() payable external {
    
    
        require(msg.sender == address(weth), 'CombinedSwapAddRemoveLiquidity: RECEIVE_NOT_FROM_WETH');
    }

    // similar to the above method but for when the desired token is WETH, handles unwrapping
    function removeLiquidityAndSwapToETH(
        address token,
        uint liquidity,
        uint minDesiredETH,
        address to,
        uint deadline
    ) external returns (uint amountETHOut) {
    
    
        // do the swap remove and swap to this address
        amountETHOut = _removeLiquidityAndSwap(
            msg.sender, token, address(weth), liquidity, minDesiredETH, address(this), deadline
        );

        // now withdraw to ETH and forward to the recipient
        weth.withdraw(amountETHOut);
        TransferHelper.safeTransferETH(to, amountETHOut);
    }
}

Three, a brief study of the source code

  • pragmaWith import. You can see that there are more imported content, and Routerthere is one more IUniswapV2Pair.soland one more than the import of the contract Babylonian.sol. The extras are the trading pair interface and a tool library for calculating square roots.

  • The definition of the contract itself has nothing to say. What is useful is its annotation, which introduces the contract function and implementation process.

  • using SafeMath for uint;, I have learned it before.

  • Next are three state variable definitions, all of which are contract types (each contract is a different contract type). They are the factorycontract instance, Routercontract instance and WETHcontract instance of UniswapV2 .

  • constructor, Constructor. Here the constructor parameters directly use contract type variables instead of address type variables. This is not consistent with our usual usage, but the external ABI type of the contract type is still the address type. After testing, it is feasible to directly use the address type as the constructor parameter when deploying externally. But if the contract is created in the contract (use newcreate), the constructor parameter must be the contract type, not the address type.

  • approveRouterFunction, the token of a single asset is authorized, the authorized object is the Routercontract, and the authorized amount is the user input. Note that this is the contract for authorization, not the user for authorization. Because the Routerlast transfer of the contract is the token of this contract. Its comments explain its logic, but the original authorization is cleared before reauthorizing to a new value. Why do you do this? It is estimated to be more secure, because you don't know how the third-party ERC20 contract is implemented.

  • calculateSwapInAmountfunction. The core function is to calculate how many assets the user needs to separate and exchange first when a single asset is injected. Its two input parameters are the quantity of a certain asset in the current trading pair and the quantity of a single asset that the user wants to inject. The note mentioned that this calculation has nothing to do with another asset in the trading pair, but the author is still unclear on how to calculate it.

  • _swapExactTokensAndAddLiquidityfunction. An internal function is known by the name. When a single asset is injected, the assets are first exchanged and then liquidity is increased. Multiple external interfaces use the same logic, so the same is extracted into an independent internal function. Function input parameters:

    • fromWhether to transfer assets from this contract or from where.
    • tokenInWith otherToken. The token address of the asset injected into the transaction pair and another token address.
    • amountIn, The total amount the user intends to inject, part of which will be traded first and converted into another token.
    • minOtherTokenIn, The minimum amount of exchange into another token in the middle process`.
    • to, Receiving liquidity address.
    • deadline, The latest trading time.

    Function output parameters:

    • amountTokenIn, The amount of the transaction (exchange) part of the single asset to be injected.
    • amountTokenOther, The amount of exchange into another token in the intermediate process.
    • liquidity, The final liquidity.

    Function code:

    • The first four lines, first of all, get the amount of the asset to be injected in the trading pair (calculated using the tool library function). Next, call the calculateSwapInAmountfunction to calculate the number of transactions (exchange) in the assets to be injected.
    • 5-7 lines. If the source of the assets to be injected is not in this contract, first transfer all the assets to be injected to this contract (so that the contract has corresponding assets). This process will require authorization to transfer.
    • Line 8. This contract authorizes the injected assets, the authorization object is the routing contract, and the authorization limit is the number of assets to be injected.
    • The content in the next pair of {} is to call the Routercontract swapExactTokensForTokensmethod to swapInAmountexchange the amount of injected assets into another asset. Note that the receiving address is this address, because another asset will be used to provide liquidity. Why is there another one [1]later, because the swapExactTokensForTokensfunction returns the participation amount of two tokens, which is an array. The order of the elements in the array is consistent with the transaction path, so it [1]represents the amount of another asset obtained.
    • approveRouter(otherToken, amountTokenOther);Also authorize another asset obtained (otherwise the Routercontract cannot be transferred when liquidity is provided ). The authorized object is the Routercontract, and the quota is the amount of another asset obtained (also the injected amount).
    • amountTokenIn = amountIn.sub(swapInAmount);Calculate the actual amount of initial assets participating in the provision of liquidity, and subtract the amount of participating transactions from the amount to be injected.
    • Finally, Routerthe addLiquidityfunction of the contract is called to provide liquidity operations. You can look at some explanations in the comments.
  • swapExactTokensAndAddLiquidityfunction. The external interface actually called by the user, and the one-way asset to be injected is ordinary ERC20 tokens. In this case, just call the _swapExactTokensAndAddLiquidityfunction directly .

  • swapExactETHAndAddLiquidityfunction. The external interface actually called by the user, the one-way asset to be injected is ETH. Because the asset the user wants to inject is ETH, the UniswapV2 trading pair must be the WETH/ERC20 trading pair. Compared with the above function, this function only has one more step to convert ETH to WETH, and the others are almost the same. Note that because the exchanged WETH is in this contract (not on the user), the _swapExactTokensAndAddLiquidityfirst parameter when calling the function is address(this).

  • _removeLiquidityAndSwapfunction. And _swapExactTokensAndAddLiquidityfunction is similar, but the removal of liquidity and trading, and it can be seen that _swapExactTokensAndAddLiquiditythe process functions just the opposite. Please refer to the above _swapExactTokensAndAddLiquidityfunction to take a look at the input parameters and output parameters of this function. undesiredTokenOn behalf of another asset, desiredTokenon behalf of the last asset you want. Note that first remove the liquidity and then the transaction does not calculate the optimized value, which is no different from the ordinary separate operation. Let's look directly at the function code:

    • The first line gets the transaction pair address
    • The second line transfers the liquidity to be removed to this contract.
    • The third line authorizes the liquidity, the authorized object is the Routercontract, and the amount is the value of the liquidity to be removed.
    • Next is Routerthe removeLiquiditycall of a contract to remove liquidity and withdraw two assets. Note that the receiving address is the contract address, because there will be transactions later. It returns two values, which are the values ​​of the two assets extracted. Their order is input parameters undesiredTokenand desiredTokensequence corresponding thereto.
    • approveRouter(undesiredToken, amountInToSwap);, Authorize another kind of asset that you don't want, the object is the Routercontract, and the amount is the full amount.
    • The next three lines construct a trading path to prepare for trading assets.
    • Next, call Routerthe swapExactTokensForTokensmethod of the contract to exchange the unwanted asset into the desired asset. The [1]meaning here is similar to that mentioned above, which is the number of assets obtained by the transaction. At the same time, you also need to pay attention to the logic of the minimum amount it sets: if the amount obtained during the previous extraction has exceeded the user-defined minimum value, it is not needed here, set to 0; what if it is less? Then the transaction must at least get the difference between the two (that is, the minimum setting), otherwise it will not be able to meet user expectations.
    • if (to != address(this))Code break, if the receiving address of the transaction is not the address of this contract, if it is an external address. Because the assets withdrawn when the liquidity is removed are still in this contract ( tonot consistent), the assets withdrawn need to be sent to the toaddress as well. If it is the address of the contract, it means that all the assets obtained are in this contract, and they are the tosame and do not need to be processed.
    • The total quantity of a single asset calculated in the last line. The value is the amount withdrawn plus the amount exchanged.
  • removeLiquidityAndSwapToTokenfunction. After learning the _removeLiquidityAndSwapfunction, this is very simple. It is an external interface called by the user to get a single ERC20 token. This function directly calls the _removeLiquidityAndSwapfunction.

  • receivefunction. Only accept ETH sent from WETH contract, because this contract only sends ETH to WETH contract (used for ETH/WETH exchange).

  • removeLiquidityAndSwapToETHThe function is also very simple, except that the single asset obtained is turned into an ERC20 token. First call directly to _removeLiquidityAndSwapget a single asset such as WETH (note that the toaddress is this contract at this time , because the user specifies to get ETH, which needs to be exchanged again). Then convert WETH to ETH, and finally send the obtained ETH to the recipient address to.

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/109273647