朋友们大家好!今天我们主要来读Router01.sol。
开头是常规动作指定编译器版本,以及引入库。

第11行开始进入合约内容,合约名叫UniswapV2Router01,继承自IUniswapV2Router01。

定义了两个地址分别是factory和WETH,会在构造函数中初始化。

15行是一个修饰器,名字叫ensure,传入的参数叫deadline,要求传入的时间晚于区块的时间戳,否则返回'UniswapV2Router: EXPIRED'。由于代币价格变化得非常快,每一次兑换报价都有时效性,把时效判断单独出来作一个修饰器,后续的函数都能复用。

25行是一个receive函数,类似的函数在migrator中见过,是用来接收ETH的函数。函数体里只有一句assert,assert和require类似,都是检查条件抛出异常的语句。
区别是assert只能用来检查内部错误和不变量,require用来检查外部的调用条件和输入。当assert条件不满足报错时,会返回固定的panic错误码,在Solidity 0.8 版本之前会消耗掉全部的gas。就像panic名字表达“恐慌”的一样,这是EVM的红色警报,一旦报警意味着程序内部的状态已经错乱了,需要尽快停止。
回到代码里,这里assert判定的条件是调用方的地址必须是初始化传入的WETH,也就是说这个合约只接收WETH转来的ETH,其他地址的转账一概不收。
添加流动性
_addLiquidity和addLiquidity是一对。
流动性是说有人在市场上想买能买,想卖能卖,提供流动性的意思就是为交易者提供对手盘,收益是交易过程中的手续费。添加流动性就是把两种代币都放进去,让有交易需求的人能有得交易。

_addLiquidity
是private函数,入参有6个字段,分别是tokenA和tokenB的地址、期望投入的金额amountADesired和amountBDesired,两种代币接收的最小量amountAMin和amountBMin,出参是两个代币实际投入的数量。
首先判断tokenA和tokenB的代币对pair是否存在,如果不存在就调用factory的createPair创建一个新的代币对。
42行在获取tokenA和tokenB这个代币对当前的余额,并将代币数量分别存入reserveA和reserveB中。
如果当前池子中两种代币的数量都为0,说明这是首次添加流动性,那么用户想要添加多少就添加多少,把amountDesired赋值给amount。
如果两种代币的数量不全为0,这说明之前池子里是有可供兑换的代币的,它们两者之间的数量比例表征了当前的价格,因此要继续提供流动性的人最优策略就是按照相同的比例往池子里放入两种代币。如果贵的放得多便宜的放得少,流动性提供方就亏了,反过来便宜的放得多池子肯定不同意。这里调用的quote函数就是在计算基于当前价格应该投入的比例是多少

quote计算的基础是保持池子内两种代币的比例一致,reserveA和reserveB是池子里当前两种代币的数量,amountA是要投入的tokenA的数量,在reserveA/reserveB=amountA/amountB的前提下,很容易能算出来应该投入的amountB的数量。
回到_addLiquidity函数中,把调用quote计算出的amountB数量赋值给amountBOptimal,这是理想情况下,基于希望添加amountADesired数量的tokenA前提下,tokenB应该添加的量。
计算出的amountBOptimal和用户希望投入的amountBDesired可能有差异,47至54行是在处理异常情况。

如果应投B(Optimal)比想投B(Desired)小或者相等,并且想投的数量比最低数量高,那么就投入应投数量,准备的钱是够的,只是有剩余。把amountADesired和amountBOptimal的数量赋值给amountA和amountB,作为添加流动性的数量。
反过来,如果应投B(Optimal)比想投B(Desired)大,也就是说当tokenA完全添加的时候,tokenB不够用了,那就反过来算,就乎那个少的。再用quote函数算一下当tokenB完全耗尽时,需要多少tokenA,算出来的值写入amountAOptimal。理论上amountAOptimal是会比amountADesired要小或者相等的,如果算出来大了的话,就会触发assert告警,毕竟不可能两头都就乎不上。
算出来amountAOptimal比要求的最低数量高,就把amountAOptimal, amountBDesired的数量赋值给amountA和amountB,作为添加流动性的数量。
逻辑里写了很长处理边界情况,用语言描述就是计算如何尽量多的把用户的代币投入流动性池。
addLiquidity

58行开始,这个添加流动性的函数是external外部的,很明显它和_addLiquidity是一对。68行调用了_addLiquidity,获得了添加流动性的两种代币的数量。
69行调用了函数pairFor,这个函数是在计算这俩token组成的代币对pair的地址。还记得在factory.sol中分析的create2用法吗,这里就是同样的原理,在已知factory和两个token的前提下,可以计算出它们对应的pair地址。不用调外部函数进行查询,就能省点gas。

获取pair的地址,是为了把代币转入,只有进入池子才能真正提供流动性为其他用户提供兑换。70至71行分别将amountA个tokenA和amountB个tokenB转给pair。
在pair.sol中我们分析过mint函数,可以基于pair池子中两种代币的增量计算出应该铸造多少LP代币作为liquidity流动性凭证。
_addLiquidity和addLiquidity组合在一起,_addLiquidity作为私有函数计算出两种代币的最优存入量,addLiquidity作为公共函数完成转入和LP铸造的功能。
addLiquidityETH
这个函数看起来和上边的addLiquidity长得很像,既然已经有通用的添加流动性的函数,为什么还要单独再写一个来处理ETH的问题呢?
这是因为ETH和其他的erc20代币不同,ETH是以太坊原生的代币,先有ETH,后续代币多了才有了ERC20的接口规范,ETH是没有transferFrom函数的。ETH和其他erc20代币最大的区别是ETH只能主动给,通过msg.value把钱“附带”过去,合约无法从他人账户中扣。
为了能让ETH融入整个erc20的defi生态,解决办法是把ETH包装一层成为Wrap-ETH,WETH是严格遵守erc20标准的合约:给一个ETH,1:1铸造一个WETH,如果想要换回ETH,就销毁对应数量的WETH。

从74行开始,我们逐行来看这个函数。
首先输入是6个参数,token、希望存入的token数量amountTokenDesired、合约接受的最小token数量amountTokenMin、合约接受的最小ETH数量amountETHMin、地址to、最晚交易时刻deadline。
输出是三个变量:存入的token的数量amountToken、存入的ETH的数量amountETH、对应的LP代币数量liquidity。
和_addLiquidity的入参相比,少了tokenB的名称和希望投入tokenB的数量amountTokenBDesired。对于这个addLiquidityETH函数来说,tokenB就是WETH,传入的WETH数量是通过msg.value传入的ETH数量。因此第82行调用_addLiquidity函数把缺省值也传入,计算获得了两种代币的最佳存入方案。
90行基于已有信息计算出pair的地址。
91行的操作和函数addLiquidity中tokenA的转账动作一样,将符合erc20标准的代币余额转出指定数量到pair地址中。
但ETH无法也这么操作,需要先把ETH转换为WETH。因此需要先将需要存入的ETH转换为WETH,然后再将WETH转到pair池子里,最后铸造相应数量的LP代币。第95行也是针对ETH的特殊处理,将通过msg.value转入多出来的ETH找零还给用户,其他token没有这个问题,是因为ETH是主动转入操作,而价格是快速变化的,很难提前预估出准确的数量,并且大部分情况会都转一些,来覆盖可能出现的滑点之类的意外支出。第95行就是针对多转金额的找零动作,为什么addLiquidity不需要找零呢?因为符合erc20规范的token是可以做transferFrom的,需要多少去扣款多少就行,不用找零。
移除流动性
和添加流动性类型,removeLiquidity是在做相反的动作:将两种代币从pair池中取回,然后销毁对应的LP凭证。
removeLiquidity

先看99行的removeLiquidity函数,输入是7个参数,分别是:
两种代币tokenA和tokenB、持有的LP代币数量liquidity、两种token存入/取出的最小金额amountAMin和amountBMin、转出地址to、交易最晚时间deadline。输出参数是取回的两种token的数量amountA和amountB。
函数体从108行开始,首先是获取代币对的地址,然后将LPtoken转给pair地址,pair地址调用burn函数根据LP代币的数量计算出应该取回的两种token的数量,并且销毁对应的LP代币。
111行的sortTokens函数是在给两种token排序,地址小的赋值给token0,如果token0和tokenA一致,那么amoutA=amount0,否则amoutB=amount0。uniswapv2合约里大量函数都需要用到排序逻辑,把排序动作解耦封装成可以复用的安全函数,既能减少代码重复,而且减少了每次重写出错的可能。
removeLiquidityETH
同样这里对ETH也有额外的处理removeLiquidityETH。

124行开始的函数主体,先是调用了removeLiquidity,把WETH作为tokenB参与计算,把转出的token和WETH先转到router01地址上,然后做一步二传,把WETH兑换回ETH,最后再将token和ETH转给用户。
removeLiquidityWithPermit

从函数名字上看,这也是移除流动性的函数,和removeLiquidity的区别在于增加了通过链下签名完成授权,从而让removeLiquidity能够完成transferFrom转走LP代币的动作。
输入的参数和removeLiquidity函数相比,多了四个:是否为最大额授权approveMax,和三个签名参数v、r、s。148行授权金额判定这里,如果是最大额度授权了,就将授权值赋为最大值,否则赋值为本次需要销毁的LP代币数量。然后将签名参数和授权金额传入,调用permit函数使pair完成授权账户LP代币的扣除。获取授权之后就直接调用removeLiquidity函数完成流动性移除即可。
removeLiquidityETHWithPermit

152行的removeLiquidityETHWithPermit函数,是在Permit的基础上对ETH做了额外的处理,道理和removeLiquidityWithPermit一样,调用permit函数使pair完成授权账户LP代币的扣除之后,调用了removeLiquidityETH函数,对ETH做了特殊处理完成流动性移除。
SWAP
SWAP是交换兑换的意思,在pair.sol中我们分析过swap函数,它是基于要转出的金额计算存入的金额是否足够,够数的话将用户需要的金额兑出。
在router中的swap函数和pair中的swap类似,但是更核心的是在完成多跳兑换的功能。_swap利用循环,按照预定的兑换路线依次完成兑换,逐个将换出的代币交给路径中下一个交易对,直到将最终输出代币发送给用户。
_swap

函数的入参是三个字段:记录每一个交易对后输出的量amounts、代币兑换路径path和转出地址_to。这是一个private的私有函数,外部无法直接调用,因为在调用之前需要先完成资产转移。
170行进入函数体,首先是一个循环,目的是遍历兑换路径中每一对相邻代币。
举个例子,用户最终需要将A兑换为C,但是当前没有人提供A+C这个交易对,但是分别有A+B和B+C,因此兑换路径path中存储的就是A-B-C,兑换金额中存储是amountA-amountB-amountC。
循环开始,首先将path[0]A和path[1]B赋值给input和output,给input和output排序后,将地址小的赋值给token0,如果input=token0,说明B是token1,那么要兑换的金额amount[1]也就是amountB会赋值给amount1Out,同时amount0Out=0,此时swap函数的入参的含义输出0个A和amount1Out个B,实现了A到B的兑换。
175在判断接下当前是不是最后一跳,如果是的话就将最后的兑换结果发送给用户,否则继续执行后续兑换。
176行是在调用pair中分析过的swap函数,完成兑换。
swapExactTokensForTokens

这个函数名字叫swapExactTokensForTokens,意思是说“有确定数量的代币要兑换,能换多少是多少”,最开始输入的代币数量是一定的,按照指定路线去兑换,有多少算多少,兜底条件是最后输出的最终代币高于能接受的最低值。固定输入计算输出。
179行,输入5个参数,分别是存入数量amountIn、最小兑出数量amountOutMin、兑换路径path、最终转出地址to、最晚交易时间deadline。输出变量是一个数组记录每一次兑换出的金额amount。
186行进入函数体,调用的函数是getAmountsOut,getAmountsOut最终调用的是getAmountOut,我们逐层来看。
getAmountsOut这个函数是在计算,一定量的A代币在当前价格下支付完手续费之后可以兑换出多少B代币。
首先支付0.3%的手续费,扣完手续费之后还剩余的A数量是真正可以参与兑换的A代币。恒定乘积做市要求两种代币的数量乘积永远是一个常数,因此兑换前后的乘积K值不变。
兑换前A的数量*兑换前B的数量=(兑换前A的数量+(存入的A的数量-0.3%的手续费))*(兑换前B的数量-兑换出B的数量)
一个等式里四个变量,其中三个已知,兑换出B的数量是可以算出来的。

这个getAmountsOut是router01里实际调用的,和上边函数的区别在于输入变量的不同,上边函数的输入是单独,下边是一个数组,但数组输入的函数最终是在调用单个变量输入的函数,组合完成计算的。

回到swapExactTokensForTokens函数中,在逐个计算出每次兑换的金额之后,187行在检查最后输出的金额是否大于等于amountOutMin,amountOutMin是用户接受的最小兑换金额,当价格剧烈波动或者滑点过大的时候,可能兑换出的数量过少,这里能做兜底直接回滚,保护用户在兑换中不吃大亏。
188行,计算出第一步兑换的地址,把代币转过去,接着调用_swap函数,就能启动多跳兑换了。
swapTokensForExactTokens

这个函数和刚刚分析过的swapExactTokensForTokens函数逻辑相对应,swapTokensForExactTokens是说要兑换出指定数量的最终代币,算出来需要存入多少代币就花多少代币,固定输出计算输入。
从输入的参数可以看到,amountOut是最终要兑换出的代币数量,amountInMax是最多能接受存入代币的数量,path是兑换路径,to是最终用户地址,deadline是最晚交易时间,函数的返回值是每一跳兑换的金额。
整个的大逻辑和swapExactTokensForTokens类似,差异是这里基于输出计算输入,所以调用的是getAmountsIn函数来计算每次应该存入多少金额,计算逻辑是一样的,通过一个等式已知三个变量,求第四个值。
倒推算出来最开始需要存入的代币数量之后,如果比用户设置的最大输入要小,就可以执行后续的连跳兑换。
swapExact+ETH

从203行到259行,都是针对ETH逻辑的特殊处理,exact在前或者在后有两种情况,再乘上ETH在前或者在后两种情况,一共有4种情况需要处理,主要多的就是ETH和WETH的兑换以及找零,这两种处理我们在上边添加或者移除流动性的函数里都逐行分析过了。这里就不再赘述。
quote、getAmountOut、getAmountIn

261行到280行,是完成上述计算中用到的函数比如quote、getAmountOut、getAmountIn,具体的逻辑在用到的时候已经详细分析过了。
总结
Router01的完成了添加流动性、移除流动性,以及在需要多步兑换的时候基于指定兑换路径完成兑换动作。下次我们将继续分析Router02的代码。
夜雨聆风