主頁 > 後端開發 > UniswapV2周邊合約學習(二)-- UniswapV2Router02.sol(上)

UniswapV2周邊合約學習(二)-- UniswapV2Router02.sol(上)

2020-10-20 14:41:13 後端開發

記得朋友圈看到過一句話,如果Defi是以太坊的皇冠,那么Uniswap就是這頂皇冠中的明珠,Uniswap目前已經是V2版本,相對V1,它的功能更加全面優化,然而其合約原始碼卻并不復雜,本文為個人學習UniswapV2原始碼的系列記錄文章,

一、Router合約介紹

UniswapV2的周邊合約主要作用是作為用戶和核心合約之間的橋梁,也就是用戶 => 周邊合約 => 核心合約,UniswapV2周邊合約主要包含介面定義,工具庫和核心實作這三部分,在上一篇文章里已經學習了它的工具庫函式,這次我們主要學習其核心實作,

UniswapV2周邊合約的核心實作包含UniswapV2Router01.solUniswapV2Router02.sol,這里我們把它簡稱為Router1Router2,查看它們實作的介面我們可以看到,Router2僅在Router1上多了幾個介面,那為什么會有兩個路由合約呢,我們到呼叫哪個呢?查看其官方檔案我們可以得到:

Because routers are stateless and do not hold token balances, they can be replaced safely and trustlessly, if necessary. This may happen if more efficient smart contract patterns are discovered, or if additional functionality is desired. For this reason, routers have release numbers, starting at 01. This is currently recommended release, 02.

上面那段話的大致意思就是因為Router合約是無狀態的并且不擁有任何代幣,因此必要的時候它們可以安全升級,當發現更高效的合約模式或者添加更多的功能時就可能升級它,因為這個原因,Router合約具有版本號,從01開始,當前推薦的版本是02

這段話解釋了為什么會有兩個Router,那么它們的區別是什么呢?還是來看官方檔案:

UniswapV2Router01 should not be used any longer, because of the discovery of a low severity bug and the fact that some methods do not work with tokens that take fees on transfer. The current recommendation is to use UniswapV2Router02.

這段話是講因為在Router1中發現了一個低風險的bug,并且有些方法不支持使用轉移的代幣支付手續費,所以不再使用Router1,推薦使用Router2

因此本文也是學習的UniswapV2Router02.sol,它的前半部分主要是流動性供給相關的函式(功能),后半部分主要是交易對資產交換相關的函式(功能),由于篇幅較長,因此該合約學習計劃分為上、下兩個部分來學習,內容分別為流動性供給函式和資產交換函式,這次先學習流動性供給部分,

建議對UniswapV2不熟的讀者在開始學習之前閱讀我的另一篇文章:UniswapV2介紹 來對UniswapV2的整體機制有個大致了解;當然也建議閱讀前面的系列文章,特別是核心合約學習部分,這樣更有助于理解原始碼,

UniswapV2周邊合約在Github上的地址為: uniswap-v2-periphery

二、原始碼中的公共部分

UniswapV2Router02.sol原始碼的公共部分從第一行開始,到回呼函式receive結束,主要是匯入檔案和公共變數定義、函式修飾符及構造器等,

  • 第一行,指定Solidity版本

  • 第2-3行,匯入Node.js依賴庫,注意匯入的檔案也是.sol結尾,第一個為核心合約factory的介面,第二個為TransferHelper庫,這個庫在我的上一篇文章周邊合約工具庫學習時有簡單提及,

  • 4-8行,匯入專案內其它介面或者庫,分別為本合約要實作的介面,自定義的工具庫(在周邊合約學習一中有介紹),SafeMath標準ERC20介面和WETH介面,

  • contract *UniswapV2Router02* is IUniswapV2Router02 { 合約定義,本合約實作了IUniswapV2Router02介面,

  • using SafeMath for uint;很常見,在uint上使用SafeMath,防止上下溢位,

  • address public immutable override factory;
    address public immutable override WETH;
    

    這兩行代碼使用兩個狀態變數分別記錄了factory合約的地址WETH合約的地址,這里有兩個關鍵詞immutableoverride需要深入學習一下,

    • immutable,不可變的,類似別的語言的final變數,也就是它初始化后值就無法再改變了,它和constant(常量)類似,但又有些不同,主要區別在于:常量在編譯時就是確定值,而immutable狀態變數除了在定義的時候初始化外,還可以在構造器中初始化(合約創建的時候),并且在構造器中只能初始化,是讀取不了它們的值的,并不是所有資料型別都可以為immutable變數或者常量的型別,當前只支持值型別和字串型別(string),

    • override這個很常見,通常用于函式定義中,代表它重寫了一個父函式,例如也可以用于函式修飾符來代表它被重寫,不過應用于狀態變數卻稍有不同,

      Public state variables can override external functions if the parameter and return types of the function matches the getter function of the variable:

      這句話的意思是:如果external函式的引數和回傳值同公共狀態變數的getter函式相符的話,這個公共狀態變數可以重寫該函式,但是狀態變數本身卻不能被重寫,我們來找一下它到底重寫了哪個函式,在它實作的介面IUniswapV2Router02中,有這么一個函式定義:

      function factory() external pure returns (address);,可見factory公共狀態變數重寫了其介面的external同名函式,

      這里有人可能會問,Router2介面定義中不是沒有這個函式嗎?因為Router2介面繼承了Router1介面,Router1介面定義了該函式,Router2介面就自動擁有該函式,

  • 接下來是個ensure構造器修飾符,比較簡單,就是判定當前區塊(創建)時間不能超過最晚交易時間,代碼為:

    modifier ensure(uint deadline) {
        require(deadline >= block.timestamp, 'UniswapV2Router: EXPIRED');
        _;
    }
    
  • 接下來是構造器,也很簡單,將上面兩個immutable狀態變數初始化,

    constructor(address _factory, address _WETH) public {
        factory = _factory;
        WETH = _WETH;
    }
    
  • 接下來是一個接收ETH的函式receive,從Solidity 0.6.0起,沒有匿名回呼函式了,它拆分成兩個,一個專門用于接收ETH,就是這個receive函式,另外一個在找不到匹配的函式時呼叫,叫fallback函式,該receive函式限定只能從WETH合約直接接收ETH,也就是在WETH提取為ETH時,注意仍然有可以有別的方式來向此合約直接發送以太幣,例如設定為礦工地址等,這里不展開闡述,

    receive() external payable {
        assert(msg.sender == WETH); // only accept ETH via fallback from the WETH contract
    }
    

三、原始碼中的流動性供給部分

  • _addLiquidity函式,看名字為增加流動性,為一個internal函式,提供給多個外部介面呼叫,它主要功能是計算擬向交易對合約注入的代幣數量,函式代碼如下:

    // **** ADD LIQUIDITY ****
    function _addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin
    ) internal virtual returns (uint amountA, uint amountB) {
        // create the pair if it doesn't exist yet
        if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
            IUniswapV2Factory(factory).createPair(tokenA, tokenB);
        }
        (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
        if (reserveA == 0 && reserveB == 0) {
            (amountA, amountB) = (amountADesired, amountBDesired);
        } else {
            uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
            if (amountBOptimal <= amountBDesired) {
                require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
                (amountA, amountB) = (amountADesired, amountBOptimal);
            } else {
                uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
                assert(amountAOptimal <= amountADesired);
                require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
                (amountA, amountB) = (amountAOptimal, amountBDesired);
            }
        }
    }
    

    該函式以下劃線開頭,根據約定一般它為一個內部函式,六個輸入引數分別為交易對中兩種代幣的地址,計劃注入的兩種代幣數量和注入代幣的最小值(否則重置),回傳值為優化過的實際注入的代幣數量,

    • 函式的前三行,注釋說的很清楚,如果交易對不存在(獲取的地址為零值),則創建之,

    • 函式的第四行獲取交易對資產池中兩種代幣reserve數量,當然如果是剛創建的,就都是0,

    • 第五行到結束是一個if - else陳述句,如果是剛創建的交易對,則擬注入的代幣全部轉化為流動性,初始流動性計算公式及初始流動性燃燒見我的核心合約學習三那篇文章,如果交易對已經存在,由于注入的兩種代幣的比例和交易對中資產池中的代幣比例可能不同,再用一個if - else陳述句來選擇以哪種代幣作為標準計算實際注入數量,(如果比例不同,總會存在一種代幣多一種代幣少,肯定以代幣少的計算實際注入數量),

      這里可以這樣理解,假定A/B交易對,然后注入了一定數量的A和B,根據交易對當前的比例,如果以A計算B,B不夠,此時肯定不行;只能反過來,以B計算A,這樣A就會有多余的,此時才能進行實際注入(這樣注入的A和B數量都不會超過擬注入數量),

    • 那為什么要按交易對的比例來注入兩種代幣呢?在核心合約學習三那篇文章里有提及,流動性的增加數量是分別根據注入的兩種代幣的數量進行計算,然后取最小值,如果不按比例交易對比例來充,就會有一個較大值和一個較小值,取最小值流行性提供者就會有損失,如果按比例充,則兩種代幣計算的結果一樣的,也就是理想值,不會有損失,

    • 該函式也涉及到了部分UniswapV2Library庫函式的呼叫,可以看上一篇文章周邊合約工具庫學習,

  • addLiquidity函式,學習了前面的_addLiquidity函式,這個就比較好理解了,它是一個external函式,也就是用戶呼叫的介面,函式引數和_addLiquidity函式類似,只是多了一個接收流動性代幣的地址和最遲交易時間,代碼片斷為:

    function addLiquidity(
        address tokenA,
        address tokenB,
        uint amountADesired,
        uint amountBDesired,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
        (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
        TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
        liquidity = IUniswapV2Pair(pair).mint(to);
    }
    

    這里deadline從UniswapV1就開始存在了,主要是保護用戶,不讓交易過了很久才執行,超過用戶預期,函式回傳值是實際注入的兩種代幣數量和得到的流動性代幣數量,

    • 函式的第一行是呼叫_addLiquidity函式計算需要向交易對合約轉移(注入)的實際代幣數量,

    • 函式的第二行是獲取交易對地址(注意,如果交易對不存在,在對_addLiquidity呼叫時會創建),注意,它和_addLiquidity函式獲取交易對地址略有不同,一個是呼叫factory合約的介面得到(這里不能使用根據salt創建合約的方式計算得到,因為不管合約是否存在,總能得到該地址);另一個是根據salt創建合約的方式計算得到,雖然兩者用起來都沒有問題,個人猜想本函式使用salt方式計算是因為呼叫的庫函式是pure的,不讀取狀態變數,并且為內部呼叫,能節省gas;而呼叫factory合約介面是個外部EVM呼叫,有額外的開銷,個人猜想,未必正確,

    • 第三行和第四行是將實際注入的代幣轉移至交易對,

    • 第五行是呼叫交易對合約的mint函式來給接收者增發流動性,

    對于這個合約介面(外部函式),Uniswap檔案也提到了三點注意事項:

    1. 為了覆寫所有場景,呼叫者需要給該Router合約一定額度的兩種代幣授權,因為注入的資產為ERC20代幣,第三方合約如果不得到授權(或者授權額度不夠),就無法轉移你的代幣到交易對合約中去,
    2. 總是按理想的比例注入代幣(因為計算比例和注入在一個交易內進行),具體取決于交易執行時的價格,這一點在介紹_addLiquidity函式時已經講了,
    3. 如果交易對不存在,則會自動創建,擬注入的代幣數量就是真正注入的代幣數量,
  • addLiquidityETH函式,和addLiquidity函式類似,不過這里有一種初始注入資產為ETH,因為UniswapV2交易對都是ERC20交易對,所以注入ETH會先自動轉換為等額WETH(一種ERC20代幣,通過智能合約自由兌換,比例1:1),這樣就滿足了ERC20交易對的要求,因此真實交易對為WETH/ERC20交易對,

    本函式的引數和addLiquidity函式的引數相比,只是將其中一種代幣換成了ETH,注意這里沒有擬注入的amountETHDesired,因為隨本函式發送的ETH數量就是擬注入的數量,所以該函式必須是payable的,這樣才可以接收以太幣,函式代碼為:

    function addLiquidityETH(
        address token,
        uint amountTokenDesired,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {
        (amountToken, amountETH) = _addLiquidity(
            token,
            WETH,
            amountTokenDesired,
            msg.value,
            amountTokenMin,
            amountETHMin
        );
        address pair = UniswapV2Library.pairFor(factory, token, WETH);
        TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
        IWETH(WETH).deposit{value: amountETH}();
        assert(IWETH(WETH).transfer(pair, amountETH));
        liquidity = IUniswapV2Pair(pair).mint(to);
        // refund dust eth, if any
        if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
    }
    
    • 函式的第一行仍舊是呼叫_addLiquidity函式來計算優化后的注入代幣值,正如前面分析的那樣,它使用WETH地址代替另一種代幣地址,使用msg.value來代替擬注入的另一種代幣(因為WETH與ETH是等額兌換)數量,當然,如果WETH/TOKEN交易對不存在,則先創建之,

    • 函式的第二行是獲取交易對地址,注意它獲取的方式仍然是計算得來,

    • 第三行是將其中一種代幣token轉移到交易對中(轉移的數量為由第一行計算得到)

    • 第四行是將ETH兌換成WETH,它呼叫了WETH合約的兌換介面,這些介面在IWETH.sol中定義,兌換的數量也在第一行中計算得到,當然,如果ETH數量不夠,則會重置整個交易,

    • 第五行將剛剛兌換的WETH轉移至交易對合約,注意它直接呼叫的WETH合約,因此不是授權交易,不需要授權,另外由于WETH合約開源,可以看到該合約代碼中轉移資產成功后會回傳一個true,所以使用了assert函式進行驗證,

    • 第六行是呼叫交易對合約的mint方法來給接收者增發流動性,

    • 最后一行是如果呼叫進隨本函式發送的ETH數量msg.value有多余的(大于amountETH,也就是兌換成WETH的數量),那么多余的ETH將退還給呼叫者,

  • removeLiquidity函式,移除(燃燒)流動性(代幣),從而提取交易對中注入的兩種代幣,該函式的7個引數分別為兩種代幣地址,燃燒的流動性數量,提取的最小代幣數量(保護用戶),接收者地址和最遲交易時間,它的回傳引數是提取的兩種代幣數量,該函式是virtual的,可被子合約重寫,正如前面所講,本合約是無狀態的,是可以升級和替代的,因此本合約所有的函式都是virtual的,方便新合約重寫它,下面是該函式的代碼片斷:

    // **** REMOVE LIQUIDITY ****
    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline
    ) public virtual override ensure(deadline) returns (uint amountA, uint amountB) {
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
        (uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
        (address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
        (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
        require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
        require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
    }
    
    • 函式的第一行計算兩種代幣的交易對地址,注意它是計算得來,而不是從factory合約查詢得來,所以就算該交易對不存在,得到的地址也不是零地址,

    • 函式的第二行呼叫交易對合約的授權交易函式,將要燃燒的流動性轉回交易對合約,如果該交易對不存在,則第一行代碼計算出來的合約地址的代碼長度就為0,呼叫其transferFrom函式就會報錯重置整個交易,所以這里不用擔心交易對不存在的情況,

    • 函式的第三行呼叫交易對的burn函式,燃燒掉剛轉過去的流動性代幣,提取相應的兩種代幣給接收者,

    • 第四行和第五行是將結果排下序(因為交易對回傳的提取代幣數量的前后順序是按代幣地址從小到大排序的),使輸出引數匹配輸入引數的順序,

    • 第六行和第七行是確保提取的數量不能小于用戶指定的下限,否則重置交易,為什么會有這個保護呢,因為提取前可以存在多個交易,使交易對的兩種代幣比值(價格)和數量發生改變,從而達不到用戶的預期值,

    • 用戶呼叫該函式之前同樣需要給Router合約交易對流動性代幣的一定授權額度,因為中間使用到了授權交易transferFrom

  • removeLiquidityETH函式,同removeLiquidity函式類似,函式名多了ETH,它代表著用戶希望最后接收到ETH,也就意味著該交易對必須為一個TOKEN/WETH交易對,只有交易對中包含了WETH代幣,才能提取交易對資產池中的WETH,然后再將WETH兌換成ETH給接收者,函式代碼為:

    function removeLiquidityETH(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) public virtual override ensure(deadline) returns (uint amountToken, uint amountETH) {
        (amountToken, amountETH) = removeLiquidity(
            token,
            WETH,
            liquidity,
            amountTokenMin,
            amountETHMin,
            address(this),
            deadline
        );
        TransferHelper.safeTransfer(token, to, amountToken);
        IWETH(WETH).withdraw(amountETH);
        TransferHelper.safeTransferETH(to, amountETH);
    }
    
    • 因為WETH的地址公開且已知,所以函式的輸入引數就只有一個ERC20代幣地址,相應的,其中的一個Token文字值也換成了ETH

    • 函式的第一行直接呼叫上一個函式removeLiquidity來進行流動性移除操作,只不過將提取資產的接收地址改成本合約,為什么呢?因為提取的是WETH,用戶希望得到ETH,所以不能直接提取給接收者,還要多一步WETH/ETH兌換操作,

      注意,在呼叫本合約的removeLiquidity函式程序中,msg.sender保持不變(在另一種智能合約編程語言Vyper語言中,這種場景下msg.sender會發生變化),

    • 函式的第二行將燃燒流動性提取的另一種ERC20代幣(非WETH)轉移給接收者,

    • 第三行將燃燒流動性提取的WETH換成ETH,

    • 第四行將兌換的ETH發送給接收乾,

    • 因為呼叫了removeLiquidity函式,同樣需要用戶事先進行授權,見removeLiquidity函式分析,

  • removeLiquidityWithPermit函式,同樣也是移除流動性,同時提取交易對資產池中的兩種ERC20代幣,它和removeLiquidity函式的區別在于本函式支持使用線下簽名訊息來進行授權驗證,從而不需要提前進行授權(這樣會有一個額外交易),授權和交易均發生在同一個交易里,參考系列文章中的核心合約學習二中的permit函式學習,函式代碼為:

    function removeLiquidityWithPermit(
        address tokenA,
        address tokenB,
        uint liquidity,
        uint amountAMin,
        uint amountBMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external virtual override returns (uint amountA, uint amountB) {
        address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
        uint value = approveMax ? uint(-1) : liquidity;
        IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
        (amountA, amountB) = removeLiquidity(tokenA, tokenB, liquidity, amountAMin, amountBMin, to, deadline);
    }
    
    • removeLiquidity函式相比,它輸入引數多了bool approveMaxuint8 v, bytes32 r, bytes32 sapproveMax的含義為是否授權為uint256最大值(2 ** 256 -1),如果授權為最大值,在授權交易時有特殊處理,不再每次交易減少授權額度,相當于節省gas,這個核心合約學習二中也有提及,v,r,s用來和重建后的簽名訊息一起驗證簽名者地址,具體見核心合約學習二中的permit函式學習,

    • 函式的第一行照例是計算交易對地址,注意不會為零地址,

    • 函式的第二行用來根據是否為最大值設定授權額度,

    • 函式的第三行呼叫交易對合約的permit函式進行授權,

    • 函式的第四行呼叫removeLiquidity函式進行燃燒流動性從而提取代幣的操作,因為在第三行代碼里已經授權了,所以這里和前兩個函式有區別,不需要用戶提前進行授權了,

  • removeLiquidityETHWithPermit函式,功能同removeLiquidityWithPermit類似,只不過將最后提取的資產由TOKEN變為ETH,代碼可以比對removeLiquidityETH函式,因此這里大家可以自己學習一下,只是貼出函式代碼:

    function removeLiquidityETHWithPermit(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline,
        bool approveMax, uint8 v, bytes32 r, bytes32 s
    ) external virtual override returns (uint amountToken, uint amountETH) {
        address pair = UniswapV2Library.pairFor(factory, token, WETH);
        uint value = approveMax ? uint(-1) : liquidity;
        IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
        (amountToken, amountETH) = removeLiquidityETH(token, liquidity, amountTokenMin, amountETHMin, to, deadline);
    }
    
  • removeLiquidityETHSupportingFeeOnTransferTokens函式,名字很長,從函式名字中可以看到,它支持使用轉移的代幣支付手續費(支持包含此類代幣交易對),

    為什么會有使用轉移的代幣支付手續費這種提法呢?假定用戶有某種代幣,他想轉給別人,但他還必須同時有ETH來支付手續費,也就是它需要有兩種幣,轉的幣和支付手續費的幣,這就大大的提高了人們使用代幣的門檻,于是有人想到,可不可以使用轉移的代幣來支付手續費呢?有人也做了一些探索,由此衍生了一種新型別的代幣,ERC865代幣,它也是ERC20代幣的一個變種,ERC865代幣的詳細描述見ERC865: Pay transfer fees with tokens instead of ETH,

    然而本合約中的可支付轉移手續費的代幣卻并未指明是ERC865代幣,但是不管它是什么代幣,我們可以簡化為一點:此類代幣在轉移程序中可能發生損耗(損耗部分發送給第三方以支付整個交易的手續費),因此用戶發送的代幣數量未必就是接收者收到的代幣數量,

    本函式的功能和removeLiquidityETH函式相同,但是支持使用token支付費用,函式的代碼為:

    // **** REMOVE LIQUIDITY (supporting fee-on-transfer tokens) ****
    function removeLiquidityETHSupportingFeeOnTransferTokens(
        address token,
        uint liquidity,
        uint amountTokenMin,
        uint amountETHMin,
        address to,
        uint deadline
    ) public virtual override ensure(deadline) returns (uint amountETH) {
        (, amountETH) = removeLiquidity(
            token,
            WETH,
            liquidity,
            amountTokenMin,
            amountETHMin,
            address(this),
            deadline
        );
        TransferHelper.safeTransfer(token, to, IERC20(token).balanceOf(address(this)));
        IWETH(WETH).withdraw(amountETH);
        TransferHelper.safeTransferETH(to, amountETH);
    }
    

    我們將它的代碼和removeLiquidityETH函式的代碼相比較,只有稍微不同:

    1. 函式回傳引數及removeLiquidity函式回傳值中沒有了amountToken,因為它的一部分可能要支付手續費,所以removeLiquidity函式的回傳值不再為當前接收到的代幣數量,
    2. 不管損耗多少,它把本合約接收到的所有此類TOKEN直接發送給接收者,
    3. WETH不是可支付轉移手續費的代幣,因此它不會有損耗,
  • removeLiquidityETHWithPermitSupportingFeeOnTransferTokens函式,功能同removeLiquidityETHSupportingFeeOnTransferTokens函式相同,但是支持使用鏈下簽名訊息進行授權,本函式的代碼片斷為:

    function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens(
          address token,
          uint liquidity,
          uint amountTokenMin,
          uint amountETHMin,
          address to,
          uint deadline,
          bool approveMax, uint8 v, bytes32 r, bytes32 s
      ) external virtual override returns (uint amountETH) {
          address pair = UniswapV2Library.pairFor(factory, token, WETH);
          uint value = approveMax ? uint(-1) : liquidity;
          IUniswapV2Pair(pair).permit(msg.sender, address(this), value, deadline, v, r, s);
          amountETH = removeLiquidityETHSupportingFeeOnTransferTokens(
              token, liquidity, amountTokenMin, amountETHMin, to, deadline
          );
      }
    

    參照前面的函式學習可以很容易的看出本函式的代碼邏輯,這里大家自己嘗試一下,

四、流動性供給介面分類

原始碼中流動性供給的外部介面可以按照是提供流動性還是移除流動性分為兩大類,然后再根據初始資產/最終得到資產是ETH還是普通ERC20代幣做了進一步區分,然后移除流動性還增加了支持鏈下簽名訊息授權的介面,最后移除流動性增加了支持使用轉移代幣支付手續費的介面,

注:下文中的TOKEN均為ERC20代幣,

4.1、增加流動性

  1. addLiquidity,增加流動性,提供的初始資產為TOKEN/TOKEN,
  2. addLiquidityETH,增加流動性,提供的初始資產為ETH/TOKEN,

4.2、移除流動性

  1. removeLiquidity,移除流動性,得到的最終資產為TOKEN/TOKEN,
  2. removeLiquidityETH,移除流動性,得到的最終資產為ETH/TOKEN,

4.3、移除流動性,支持使用鏈下簽名訊息授權

  1. removeLiquidityWithPermit函式,移除流動性,支持使用鏈下簽名訊息授權,得到TOKEN/TOKEN,
  2. removeLiquidityETHWithPermit函式,移除流動性,支持使用鏈下簽名訊息授權,得到ETH/TOKEN,

4.4、移除流動性,支持使用轉移代幣支付手續費

  1. removeLiquidityETHSupportingFeeOnTransferTokens函式,移除流動性,支持使用轉移代幣支付手續費,得到ETH/TOKEN,

4.5、移除流動性,同時支持使用鏈下簽名訊息授權和使用轉移代幣支付手續費

  1. removeLiquidityETHWithPermitSupportingFeeOnTransferTokens函式,功能同標題,得到ETH/TOKEN,

從上面分類也可以得出一些其它結論,

  1. 增加流動性沒有使用鏈下簽名訊息授權,為什么呢?因為增加流動性其流動性代幣是直接增發,沒有使用第三方轉移,所以就沒有授權操作,不需要permit

  2. 移除流動性時,支付使用轉移代幣支付手續費最后得到的一種資產為ETH,說明交易對為ERC20/WETH交易對,也就是不支持兩個此類代幣構成的交易對,原因未知,還需要進一步研究,

  3. 既然移除流動性有使用轉移代幣支付手續費,那么作為同一個交易對,移除流動性之前必定有增加流動性,因此增加流動性時實際上需要支持此類代幣的,但是代碼中又沒有明確寫出支持使用轉移代幣支付手續費介面,為什么呢?

    個人猜想,未必正確:

    • 是因為此類代幣轉移程序中有損耗,而損耗多少未知,所以無法精確知道到底要提前轉移多少代幣到交易對中,在進行按比例計算時會得到預期外的值,所以寫此類介面無法向用戶回傳相關數量值,
    • 如果用戶不考慮回傳值的話,直接使用addLiquidity或者addLiquidityETH函式是可以對此類代幣進行增加流動性操作的,因為交易對計算注入代幣的數量時是以交易對合約地址當前代幣余額減去交易對合約資產池中的代幣余額,和損耗沒有任何關系,因此,增發的流動性是準確的,

至此,UniswapV2Router02.sol學習(上)–流動性借給函式的學習就到此結束了,下一次計劃學習UniswapV2Router02.sol(下)–資產交易函式的學習,

由于個人能力有限,難免有理解錯誤或者不正確的地方,還請大家多多留言指正,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/182227.html

標籤:java

上一篇:動態鏈接 & 靜態鏈接(區別及優缺點)

下一篇:邊城科技“區塊鏈+溯源”平臺助力坪朗豆腐品牌升級

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more