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

By [cyptoJune](https://paragraph.com/@cyptojune) · 2022-02-19

---

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

![](https://storage.googleapis.com/papyrus_images/3979e4df6b5161a01f2a4f7eefdbbc3375259a54798809a27259c6e5da1d4dab.png)

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

**代码解析**

1.  **类型方法增强**
    

![](https://storage.googleapis.com/papyrus_images/3532e6de7a75c1ad34d2d6a35b783f5f30cb78d6a11e72d7d1a129d034047709.png)

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

2\. **常量**

![](https://storage.googleapis.com/papyrus_images/dbb834577e347383af1592709df0da863caa3a0ad0a3a20ba581f5afa81ef035.png)

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

![](https://storage.googleapis.com/papyrus_images/7f8766d2fa722f46df15976cdb0c046c4e5d4a1543e42bc8edfc0d9acf8a304d.png)

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

*   铸造事件
    
*   销毁事件
    
*   交换事件
    
*   同步事件
    

4.**方法**

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

![](https://storage.googleapis.com/papyrus_images/6420507f9af18c14e031529cb98bcd4d68dab95978dfb0f2e6746314b549e31b.png)

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

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

2.外部调用方法

![](https://storage.googleapis.com/papyrus_images/3032ea8f5c18b97cf274d678096588b52a13fe175b83fe154332b0b840102553.png)

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

2.1 **铸造方法**

![铸币方法](https://storage.googleapis.com/papyrus_images/64507cb9d31626a1074f298b32c65d5e4eb84cf35bfb88f40fb249222e89f87f.png)

铸币方法

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

![](https://storage.googleapis.com/papyrus_images/27aa8392b5a0c29c31115f22aacda69ea53c4e1684926a5f12d298dfa74538a3.png)

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

其实就是恒定乘积公式中的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行
    

![](https://storage.googleapis.com/papyrus_images/873d6be677a95cb6b3e0dbd0bc54c9d0f75ff69474acf61c9be7d75db282c30a.png)

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

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

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

2.2 销毁方法

![](https://storage.googleapis.com/papyrus_images/f532083ca71c401d7b20d8e75cb37772e797272eae2b8a7fbcbd13d02a856a1c.png)

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

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

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

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

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

![](https://storage.googleapis.com/papyrus_images/af3eb9ce00c858e69f15163318384a76f80d3071d9f52685b5310d085c98dc27.png)

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

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

接着触发销毁代币事件。

2.3 交换方法

![](https://storage.googleapis.com/papyrus_images/63ade1985ab933cdb41f7e28799b8f1c3e079e790d4a8b5ed6e55728b5cea6a7.png)

![](https://storage.googleapis.com/papyrus_images/687aeb2c5df301cbbccfb5564fa42d22fefef5296096c030cb2309c098cbfa7d.png)

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

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

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

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

![](https://storage.googleapis.com/papyrus_images/06ff7406a53efa7801bb054d38fb04e38be2c4ec3326ffc95ff95b281da5c6c0.png)

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

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

（balance0-amount0In _\* 0.003）_ \*（balance1-amount1In _\* 0.003） = reserve0 \* reserve1_

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

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

2.4 skim方法

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

![](https://storage.googleapis.com/papyrus_images/c3e5c4e44a9d18b777bcc97e88a3bbcad36d35dbd16ac32126fe9dd87076e203.png)

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

2.5 sync方法

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

![](https://storage.googleapis.com/papyrus_images/0800f5aae90e77c93dce317c287b12a4c90a3fa149a6e38c2be11eae9feb4055.png)

---

*Originally published on [cyptoJune](https://paragraph.com/@cyptojune/defi-uniswap-v2-2)*
