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

从代码上来看,配对合约是继承ERC20合约,那么配对合约实际上就是一个遵守Erc20合约的token.
代码解析
类型方法增强

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

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.事件

上面主要是事件(具体事件的用法包括事件参数修饰符indexed索引等,我会专门讲一期)
铸造事件
销毁事件
交换事件
同步事件
4.方法
初始化方法和构造方法

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

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

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

首次铸币获取的流动性公式如上图所示:
其实就是恒定乘积公式中的k值,这个公式确保了在任意时刻添加流动性时,Lptoken的价值和初始供应的tokenA和tokenB的比例无关。
重点,我们从代码中看到,当初次添加流动性的时候,并没有完全获得对应数量的Lp Token,有minium_liquidity 数量的Lp token被转入0地址(打入黑洞)销毁了。这是为了防止有人可以太高流动性单价从而垄断交易对,导致个人或者散户无力参与,即无法提供流动性。具体攻击流程如下:
首先发送小额token(1wei)到交易对并且Mint()得到1wei的Lp token,此时池子中totalSupply为1wei, reserve0 和 reserve1也为1wei
发送发额的token(2000ether)到交易对,但不调用mint(),而是直接调用sync(),此时池子中totalSupply为1wei,reserve0和reserve1 分别为1wei 2000ether攻击结束,此时流动性为2000 *18** 约等于 2000 ether
换句话说,即使散户提供最小流动性,也要付出2000ether的token.
因此为了解决这个问题,必须降低参与门槛,即降低流动性单价。代码261行

这里 uin使用了safemath,因此sub()操作会有溢出检查,也就是说首次mint总流动性要大于1000,否则会revert,为了避免攻击者通过burn()将流动性销毁,导致总流动性不低于1000的限制被绕过,代码还会从首次铸币本应获得的流动性中扣除1000,将其打入0x0黑洞地址,从而达成限制,这样的话,即使刚才的攻击实现,那么散户可以付出约2ether,就可以提供流动性了。
因此回顾上述初始铸造攻击,为了解决这个流动,牺牲了首次铸币者的利益,实际上,Uniswap中存在很多除法,再无法整除的情况下,不同的round方法,偏向的利益方也是不同的。
如果不是第一次铸币,那么获取流动性公式是266行,流动性的获取会根据存入的两种token分别计算,然后取最小的那个。之后就是将Lp token给到to 地址,更新储备量,之后如果开启平台手续费收取,那么就重新计算k值,最后触发铸造事件。
2.2 销毁方法

销毁方法是流动性提供者想要收回流动性
首先通过getReserves()方法获取现在的储备量。
然后获取token0 和 token1在当前pair合约的余额
紧接着从当前合约的balanceOf获取想要销毁的流动性金额,为什么是从自身获取,因为当前合约的余额是流动性提供者通过路由合约发送到Pair合约要销毁的金额。
接着计算平台手续费,获取totalSupply,然后计算流动性提供者可以取出的token0和token1的数量,数量分别为amount0和amount1。取出逻辑是这样:取出来的数量与持有的流动性占总流动性的比例有关。

接着销毁合约内的流动性数量,发送token0 token1,更新储备量
如果平台手续费收取的话,重新计算k值
接着触发销毁代币事件。
2.3 交换方法


交换方法一般是通过路由合约调用,入参前几个比较容易解释,主要是data参数,这个主要是用于闪电贷回调使用。关于闪电贷,我后期会专门出一期进行讲解。
首先确认amount0Out或者amount1out有一个大于0,然后确保储备量大于要取出的金额
然后确保address(to)不等于对应token地址,然后发送token到对应地址
如果有data,就执行闪电贷的调用

在413行之后,主要是保证交换之后的储备量的乘积是k.
所要注意的是:因为solidity不支持小数运算,可以将上述改成
(balance0-amount0In * 0.003) *(balance1-amount1In * 0.003) = reserve0 * reserve1
其中 reserve0 * reserve1 相当于k值,这个对应白皮书缩写的每次交易手续费的千分之三。
最后更新储备量,触发交换事件。
2.4 skim方法
skim方法是强制让余额等于储备量,一般用于储备量溢出的情况,将多余的余额转出到to地址上,使余额重新等于储备量

其逻辑也很简单,就是将余额减去储备量的token发送到address
2.5 sync方法
skim方法是强制将余额与储备量对等,sync方法是强制让储备量与余额对等,直接调用更新储备量的私有方法。

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

从代码上来看,配对合约是继承ERC20合约,那么配对合约实际上就是一个遵守Erc20合约的token.
代码解析
类型方法增强

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

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.事件

上面主要是事件(具体事件的用法包括事件参数修饰符indexed索引等,我会专门讲一期)
铸造事件
销毁事件
交换事件
同步事件
4.方法
初始化方法和构造方法

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

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

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

首次铸币获取的流动性公式如上图所示:
其实就是恒定乘积公式中的k值,这个公式确保了在任意时刻添加流动性时,Lptoken的价值和初始供应的tokenA和tokenB的比例无关。
重点,我们从代码中看到,当初次添加流动性的时候,并没有完全获得对应数量的Lp Token,有minium_liquidity 数量的Lp token被转入0地址(打入黑洞)销毁了。这是为了防止有人可以太高流动性单价从而垄断交易对,导致个人或者散户无力参与,即无法提供流动性。具体攻击流程如下:
首先发送小额token(1wei)到交易对并且Mint()得到1wei的Lp token,此时池子中totalSupply为1wei, reserve0 和 reserve1也为1wei
发送发额的token(2000ether)到交易对,但不调用mint(),而是直接调用sync(),此时池子中totalSupply为1wei,reserve0和reserve1 分别为1wei 2000ether攻击结束,此时流动性为2000 *18** 约等于 2000 ether
换句话说,即使散户提供最小流动性,也要付出2000ether的token.
因此为了解决这个问题,必须降低参与门槛,即降低流动性单价。代码261行

这里 uin使用了safemath,因此sub()操作会有溢出检查,也就是说首次mint总流动性要大于1000,否则会revert,为了避免攻击者通过burn()将流动性销毁,导致总流动性不低于1000的限制被绕过,代码还会从首次铸币本应获得的流动性中扣除1000,将其打入0x0黑洞地址,从而达成限制,这样的话,即使刚才的攻击实现,那么散户可以付出约2ether,就可以提供流动性了。
因此回顾上述初始铸造攻击,为了解决这个流动,牺牲了首次铸币者的利益,实际上,Uniswap中存在很多除法,再无法整除的情况下,不同的round方法,偏向的利益方也是不同的。
如果不是第一次铸币,那么获取流动性公式是266行,流动性的获取会根据存入的两种token分别计算,然后取最小的那个。之后就是将Lp token给到to 地址,更新储备量,之后如果开启平台手续费收取,那么就重新计算k值,最后触发铸造事件。
2.2 销毁方法

销毁方法是流动性提供者想要收回流动性
首先通过getReserves()方法获取现在的储备量。
然后获取token0 和 token1在当前pair合约的余额
紧接着从当前合约的balanceOf获取想要销毁的流动性金额,为什么是从自身获取,因为当前合约的余额是流动性提供者通过路由合约发送到Pair合约要销毁的金额。
接着计算平台手续费,获取totalSupply,然后计算流动性提供者可以取出的token0和token1的数量,数量分别为amount0和amount1。取出逻辑是这样:取出来的数量与持有的流动性占总流动性的比例有关。

接着销毁合约内的流动性数量,发送token0 token1,更新储备量
如果平台手续费收取的话,重新计算k值
接着触发销毁代币事件。
2.3 交换方法


交换方法一般是通过路由合约调用,入参前几个比较容易解释,主要是data参数,这个主要是用于闪电贷回调使用。关于闪电贷,我后期会专门出一期进行讲解。
首先确认amount0Out或者amount1out有一个大于0,然后确保储备量大于要取出的金额
然后确保address(to)不等于对应token地址,然后发送token到对应地址
如果有data,就执行闪电贷的调用

在413行之后,主要是保证交换之后的储备量的乘积是k.
所要注意的是:因为solidity不支持小数运算,可以将上述改成
(balance0-amount0In * 0.003) *(balance1-amount1In * 0.003) = reserve0 * reserve1
其中 reserve0 * reserve1 相当于k值,这个对应白皮书缩写的每次交易手续费的千分之三。
最后更新储备量,触发交换事件。
2.4 skim方法
skim方法是强制让余额等于储备量,一般用于储备量溢出的情况,将多余的余额转出到to地址上,使余额重新等于储备量

其逻辑也很简单,就是将余额减去储备量的token发送到address
2.5 sync方法
skim方法是强制将余额与储备量对等,sync方法是强制让储备量与余额对等,直接调用更新储备量的私有方法。

cypto is the future!
cypto is the future!
Share Dialog
Share Dialog

Subscribe to cyptoJune

Subscribe to cyptoJune
<100 subscribers
<100 subscribers
No activity yet