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

2020-10-20 12:00:30

記得朋友圈看到過一句話,如果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(下)–資產交易函數的學習。

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