uniswap v2 周边源码解析系列

前言:UniswapV2的周边合约主要用做外部账号和核心合约之间的桥梁,也就是用户 => 周边合约 => 核心合约。UniswapV2周边合约主要包含接口定义,工具库、Router和示例实现这四部分

 

该作者系列文章:

https://blog.csdn.net/weixin_39430411/category_10454309.html?spm=1001.2014.3001.5482

 

 

1,接口定义

 

2,工具库

UniswapV2周边合约的工具库包含两个部分,一部分是直接写在项目里的,有三个合约:SafeMath,UniswapV2Library和UniswapV2OracleLibrary。另外一部分是Node.js依赖库,需要使用yarn安装的,也包含几个库。这其中SafeMath就是简单的防溢出库,在前面的系列学习中已经讲过,这里不再学习研究。

3,Router

UniswapV2周边合约的核心实现包含UniswapV2Router01.sol和UniswapV2Router02.sol,这里我们把它简称为Router1和Router2。查看它们实现的接口我们可以看到,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,它的前半部分主要是流动性供给相关的函数(功能),后半部分主要是交易对资产交换相关的函数(功能)。由于篇幅较长,因此该合约学习计划分为上、下两个部分来学习,内容分别为流动性供给函数和资产交换函数。这次先学习流动性供给部分。

UniswapV2Router02.sol(上)

https://blog.csdn.net/weixin_39430411/article/details/109152019

 

UniswapV2Router02.sol(下)

https://blog.csdn.net/weixin_39430411/article/details/109152084

 

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:
重写了 接口route1中的 function factory() external pure returns (address);

virtual,可被子合约重写。正如前面所讲,本合约是无状态的,是可以升级和替代的,因此本合约所有的函数都是virtual的,方便新合约重写它。

 

一、流动性供给接口分类
源码中流动性供给的外部接口可以按照是提供流动性还是移除流动性分为两大类,然后再根据初始资产/最终得到资产是ETH还是普通ERC20代币做了进一步区分。然后移除流动性还增加了支持链下签名消息授权的接口,最后移除流动性增加了支持使用转移代币支付手续费的接口。

注:下文中的TOKEN均为ERC20代币。

1.1、增加流动性
addLiquidity,增加流动性,提供的初始资产为TOKEN/TOKEN。
addLiquidityETH,增加流动性,提供的初始资产为ETH/TOKEN。
1.2、移除流动性
removeLiquidity,移除流动性,得到的最终资产为TOKEN/TOKEN。
removeLiquidityETH,移除流动性,得到的最终资产为ETH/TOKEN。
1.3、移除流动性,支持使用链下签名消息授权
removeLiquidityWithPermit函数,移除流动性,支持使用链下签名消息授权,得到TOKEN/TOKEN。
removeLiquidityETHWithPermit函数,移除流动性,支持使用链下签名消息授权,得到ETH/TOKEN。
1.4、移除流动性,支持使用转移代币支付手续费
removeLiquidityETHSupportingFeeOnTransferTokens函数,移除流动性,支持使用转移代币支付手续费,得到ETH/TOKEN。
1.5、移除流动性,同时支持使用链下签名消息授权和使用转移代币支付手续费
removeLiquidityETHWithPermitSupportingFeeOnTransferTokens函数。功能同标题,得到ETH/TOKEN。
从上面分类也可以得出一些其它结论。

增加流动性没有使用链下签名消息授权,为什么呢?因为增加流动性其流动性代币是直接增发,没有使用第三方转移,所以就没有授权操作,不需要permit。

移除流动性时,支付使用转移代币支付手续费最后得到的一种资产为ETH,说明交易对为ERC20/WETH交易对,也就是不支持两个此类代币构成的交易对。原因未知,还需要进一步研究。

既然移除流动性有使用转移代币支付手续费,那么作为同一个交易对,移除流动性之前必定有增加流动性,因此增加流动性时实际上需要支持此类代币的。但是代码中又没有明确写出支持使用转移代币支付手续费接口。为什么呢?

个人猜想,未必正确:

是因为此类代币转移过程中有损耗,而损耗多少未知,所以无法精确知道到底要提前转移多少代币到交易对中,在进行按比例计算时会得到预期外的值。所以写此类接口无法向用户返回相关数量值。
如果用户不考虑返回值的话,直接使用addLiquidity或者addLiquidityETH函数是可以对此类代币进行增加流动性操作的。因为交易对计算注入代币的数量时是以交易对合约地址当前代币余额减去交易对合约资产池中的代币余额,和损耗没有任何关系,因此,增发的流动性是准确的。

 

二、资产交易函数分类
上面这么多swap函数,大家肯定看得眼花缭乱了👻👻👻。下面我们根据交易资产的种类和指定的是卖出资产数量/买进资产数量,对它们做一个简单的分类:

2.1、 TOKEN => TOKEN
就是两种ERC20代币交易,可分为:

指定卖出代币数量,得到另一种代币,函数为swapExactTokensForTokens。
指定买进代币数量,卖出另一种代币,函数为swapTokensForExactTokens。
2.2、ETH => TOKEN
ETH兑换成ERC20代币,也分为两种:

指定卖出ETH数量,得到另一种ERC20代币,函数为swapExactETHForTokens。
指定买进ERC20代币数量,卖出ETH,函数为swapETHForExactTokens。
2.3、TOKEN => ETH
ERC20代币兑换成ETH。等等,有人会说这不是和 ETH => TOKEN 一样的么,既然能通过交易链实现 ETH => TOKEN,那么必能反向通过该交易链实现 TOKEN => ETH。

是这样的没错,但是因为不能直接交易ETH,所以会涉及到一个ETH和WETH的相兑换(转换发生在不同方向的交易链的不同阶段),因此实现逻辑还是不同的,所以这里提供了另外两个接口。

指定卖出ERC20代币数量,得到ETH,函数为swapExactTokensForETH。
指定买进ETH数量,卖出另一种ERC20代币,函数为swapTokensForExactETH。
2.4、支持FeeOnTransferTokens函数
此外还有三个支持FeeOnTransferTokens函数,分别为函数9、函数10,函数11。注意它们的函数名称,均表示指定卖出资产数量。也就是说它们只能用于交易链中指定卖出资产数量这种场景,不支持指定买进资产的场景中进行的反向交易链数值计算,因此只有3个该类函数。

为什么会这样呢?

个人认为是因为此类资产在转移过程中可能会有损耗,但损耗到底多少是无法知晓的。因此指定买进资产数量反推卖出资产数量的话,是无法得到的。因为该值为计算得到的值加上损耗值。如果指定卖出资产数量的话,每个交易对的实际卖出资产数量和最终接收的买进资产数量均可以通过比较相应地址交易前后的资产余额来计算出,因此此种交易场景是可行的。

因此2.1-2.3三种交易类型每种类型只有一个支持FeeOnTransferTokens函数,分别为:

TOKEN => TOKEN 为 swapExactTokensForTokensSupportingFeeOnTransferTokens函数。
ETH => TOKEN 为swapExactETHForTokensSupportingFeeOnTransferTokens函数。
TOKEN => ETH 为swapExactTokensForETHSupportingFeeOnTransferTokens函数。
综合得到Router2合约用于资产交易的对外接口共分四类9个接口。

 

从前面的学习中可以看出,虽然资产交易对外提供了四类共9个接口,但来回就是对两个核心_swap函数的调用。其中支持使用转移的代币支付手续费的接口中,转移资产的实际数量不再等于根据恒定乘积计算出来的结果值,而需要根据相应地址的两次资产余额相减计算出来。交易链中如果有涉及到ETH交易的,需要在交易链的对应阶段(开始或者结束阶段)进行ETH/WETH的兑换。因为UniswapV2交易对全部为ERC20/ERC20交易对,因此交易链中间流程不可能有ETH出现。

 

4,示例实现

 

posted @ 2021-05-29 16:12  走走停停走走  Views(2730)  Comments(0Edit  收藏  举报