DeFi交易龙头之Uniswap:V2(核心合约-配对合约)

前面介绍了工厂合约,我们知道配对合约其实是需要工厂合约来进行部署的。

post image

从代码上来看,配对合约是继承ERC20合约,那么配对合约实际上就是一个遵守Erc20合约的token.

代码解析

  1. 类型方法增强

post image

从上图看,首先是讲SafeMath和UQ112*112 的库方法给到了对应类型上。为什么要赋予uint224呢,这是因为在solidity中没有非整形的类型,但是实际上token的数量会出现小数位,使用库UQ112**112去模拟浮点数。

2. 常量

post image

16行中定义了一个最小流动性,在白皮书的3.4中初始化流动性代币供应这节会讲到,结论就是通过保证最小数量的流动性份额,会大大增加上述攻击的成本。具体的原理我会单独在开一篇章节进行讲解。

18行的selector的常量值是transfer(address, uint256)字符串哈希值的前4个字节,这个用于直接使用call方法调用token的转账方法。

22行工厂地址:因为pair合约是通过工厂合约进行部署的,所有会有一个变量专门去存放工厂合约地址。

23到27行主要是token地址和储备量地址相关。主要是存放两个token的地址,便于调用。储备量是当前pair合约所持有的token数量

28行blockTimestampLast主要是用于判断是不是区块的第一笔交易。

30和32行主要是价格累计,主要用于Uniswap V2所提供的价格预言机上,该数值会在每个区块的第一笔交易进行更新。

36行kLast值这个变量是在没有开启收费的时候,是等于0的,只有当平台开始收费的时候,这个值才等于k值,因为一般开启平台收费,那么k值就不会一直等于两个储备量相乘的结果。

3.事件

post image

上面主要是事件(具体事件的用法包括事件参数修饰符indexed索引等,我会专门讲一期)

  • 铸造事件

  • 销毁事件

  • 交换事件

  • 同步事件

4.方法

  1. 初始化方法和构造方法

post image

构造方法中是直接把msg.sender值赋予给了factory,因为pair合约是通过工厂合约来部署的,因此msg.sender的值等于工厂合约的地址

初始化方法:initialize 它是仅仅只有在合约创建之后调用一次,为什么使用初始化方法来初始化pair合约而不是在构造函数中进行初始化,这是因为pair合约是通过create2部署的,而create2部署合约的特点就在于部署合约的地址是可预测的,并且后一次部署的合约可以把前一次部署的合约给覆盖掉,从而实现了合约的升级部署,那么就会有一个问题,如果想实现升级,那么就需要构造函数不能有任何参数,这样才能保证每次部署的地址都保持一致。

2.外部调用方法

post image

获取储备量方法:返回token0,token1储备量以及上一个区块的时间戳。

2.1 铸造方法

铸币方法
铸币方法

在铸币方法中,mint函数输入为to 地址,输出为该地址提供的流动性,在Uniswap中,流动性也被体现成token即Lp token,铸币流程发生在router合约向pair合约发送代币之后,因此通过此次的储备量方法查询和当前合约查询的token合约的数量是不一致的,其差值就是本次添加流动性的两种token的数量,然后在255行会有收取平台手续费,这个放到后来来讲,之后获取总的流动性的供应量totalSupply,如果totalSupply等于0的话,就表示首次铸币。

post image

首次铸币获取的流动性公式如上图所示:

其实就是恒定乘积公式中的k值,这个公式确保了在任意时刻添加流动性时,Lptoken的价值和初始供应的tokenA和tokenB的比例无关。

重点,我们从代码中看到,当初次添加流动性的时候,并没有完全获得对应数量的Lp Token,有minium_liquidity 数量的Lp token被转入0地址(打入黑洞)销毁了。这是为了防止有人可以太高流动性单价从而垄断交易对,导致个人或者散户无力参与,即无法提供流动性。具体攻击流程如下:

  1. 首先发送小额token(1wei)到交易对并且Mint()得到1wei的Lp token,此时池子中totalSupply为1wei, reserve0 和 reserve1也为1wei

  2. 发送发额的token(2000ether)到交易对,但不调用mint(),而是直接调用sync(),此时池子中totalSupply为1wei,reserve0和reserve1 分别为1wei 2000ether攻击结束,此时流动性为2000 *18** 约等于 2000 ether

    换句话说,即使散户提供最小流动性,也要付出2000ether的token.

  3. 因此为了解决这个问题,必须降低参与门槛,即降低流动性单价。代码261行

post image

这里 uin使用了safemath,因此sub()操作会有溢出检查,也就是说首次mint总流动性要大于1000,否则会revert,为了避免攻击者通过burn()将流动性销毁,导致总流动性不低于1000的限制被绕过,代码还会从首次铸币本应获得的流动性中扣除1000,将其打入0x0黑洞地址,从而达成限制,这样的话,即使刚才的攻击实现,那么散户可以付出约2ether,就可以提供流动性了。

因此回顾上述初始铸造攻击,为了解决这个流动,牺牲了首次铸币者的利益,实际上,Uniswap中存在很多除法,再无法整除的情况下,不同的round方法,偏向的利益方也是不同的。

如果不是第一次铸币,那么获取流动性公式是266行,流动性的获取会根据存入的两种token分别计算,然后取最小的那个。之后就是将Lp token给到to 地址,更新储备量,之后如果开启平台手续费收取,那么就重新计算k值,最后触发铸造事件。

2.2 销毁方法

post image

销毁方法是流动性提供者想要收回流动性

首先通过getReserves()方法获取现在的储备量。

然后获取token0 和 token1在当前pair合约的余额

紧接着从当前合约的balanceOf获取想要销毁的流动性金额,为什么是从自身获取,因为当前合约的余额是流动性提供者通过路由合约发送到Pair合约要销毁的金额

接着计算平台手续费,获取totalSupply,然后计算流动性提供者可以取出的token0和token1的数量,数量分别为amount0和amount1。取出逻辑是这样:取出来的数量与持有的流动性占总流动性的比例有关

post image

接着销毁合约内的流动性数量,发送token0 token1,更新储备量

如果平台手续费收取的话,重新计算k值

接着触发销毁代币事件。

2.3 交换方法

post image
post image

交换方法一般是通过路由合约调用,入参前几个比较容易解释,主要是data参数,这个主要是用于闪电贷回调使用。关于闪电贷,我后期会专门出一期进行讲解。

首先确认amount0Out或者amount1out有一个大于0,然后确保储备量大于要取出的金额

然后确保address(to)不等于对应token地址,然后发送token到对应地址

如果有data,就执行闪电贷的调用

post image

在413行之后,主要是保证交换之后的储备量的乘积是k.

所要注意的是:因为solidity不支持小数运算,可以将上述改成

(balance0-amount0In * 0.003) *(balance1-amount1In * 0.003) = reserve0 * reserve1

其中 reserve0 * reserve1 相当于k值,这个对应白皮书缩写的每次交易手续费的千分之三。

最后更新储备量,触发交换事件。

2.4 skim方法

skim方法是强制让余额等于储备量,一般用于储备量溢出的情况,将多余的余额转出到to地址上,使余额重新等于储备量

post image

其逻辑也很简单,就是将余额减去储备量的token发送到address

2.5 sync方法

skim方法是强制将余额与储备量对等,sync方法是强制让储备量与余额对等,直接调用更新储备量的私有方法。

post image