1. 概述
Uniswap
作为一个去中心化的交易所,它舍弃了传统订单薄的撮合方式,采用流动池加恒定乘积公式算法( )为不同加密资产提供即时报价和兑换服务。Uniswap
会为每一对token
生成一个交易对,每一个交易对就是UniswapV2Pair
部署上去产生的。UniswapV2Pair
合约在Uniswap V2的核心合约代码库中,本文将会参照v2的白皮书来解读UniswapV2Pair
合约。
2. UniswapV2Pair合约继承关系
UniswapV2Pair
合约有一个接口合约IUniswapV2Pair
定义了UniswapV2Pair
所要实现的方法,除此之外还继承了之前文章所分析的UniswapV2ERC20
合约,这就说明这个Pair合约实际上就是一个token
。
3. UniswapV2Pair合约代码解析
3.1 类型方法增强
首先是将SafeMath
和UQ112x112
的库方法给到对应的类型上。
using SafeMath for uint;
using UQ112x112 for uint224;
为什么要赋予unit224
类型新的方法?这是因为在Solidity里面没有非整型的类型,但是token
的数量肯定会出现小数位,使用UQ112x112
的库去模拟浮点数类型。将unit224
中的112位当作浮点数的整数部分,另外112位当作浮点数的小数部分,这样的话其精度可以达到 , 剩余的32位主要是存放blockTimestampLast
变量,该变量的作用以及更新会在后面讲到。
library UQ112x112 {
uint224 constant Q112 = 2**112;
// encode a uint112 as a UQ112x112
function encode(uint112 y) internal pure returns (uint224 z) {
z = uint224(y) * Q112; // never overflows
}
// divide a UQ112x112 by a uint112, returning a UQ112x112
function uqdiv(uint224 x, uint112 y) internal pure returns (uint224 z) {
z = x / uint224(y);
}
}
3.2 常量
- 最小流动性定义 最小流动性的定义是1000,具体细节我会在后面铸币方法的解析中提到,为什么是1000在白皮书中也提到了,具体细节可以查看v2的白皮书中的3.4节。
uint public constant MINIMUM_LIQUIDITY = 10**3;
- SELECTOR
SELECTOR常量值为transfer(address,unit256)
字符串哈希值的前4个字节,这个用于直接使用call
方法调用token
的转账方法。
bytes4 private constant SELECTOR = bytes4(keccak256(bytes('transfer(address,uint256)')));
- 工厂地址 因为
pair
合约是通过工厂合约进行部署的,所有会有一个变量专门存放工厂合约的地址。
address public factory;
- token地址
pair
合约的含义,就是一对token
,所有在合约中会存放两个token
的地址,便于调用。
address public token0;
address public token1;
- 储备量相关
储备量是当前pair
合约所持有的token
数量,blockTimestampLast
主要用于判断是不是区块的第一笔交易。reserve0
、reserve1
和blockTimestampLast
三者的位数加起来正好是unit
的位数。
uint112 private reserve0;
uint112 private reserve1;
uint32 private blockTimestampLast;
- 价格最后累计
这里的价格最后累计,是用于Uniswap v2
所提供的价格预言机上,该数值会在每个区块的第一笔交易进行更新。
uint public price0CumulativeLast;
uint public price1CumulativeLast;
- k值
kLast
这个变量在没有开启收费的时候,是等于0的,只有当开启平台收费的时候,这个值才等于k值,因为一般开启平台收费,那么k值就不会一直等于两个储备量向乘的结果来。
uint public kLast;
3.3 事件
- 铸造事件
event Mint(address indexed sender, uint amount0, uint amount1);
- 销毁事件
event Burn(address indexed sender, uint amount0, uint amount1, address indexed to);
- 交换事件
event Swap(
address indexed sender,
uint amount0In,
uint amount1In,
uint amount0Out,
uint amount1Out,
address indexed to
);
- 同步事件
event Sync(uint112 reserve0, uint112 reserve1);
3.4 修饰方法
在pair
合约里面定义了一个修饰方法,主要是防止重入攻击的。
// 锁定变量,防止重入
uint private unlocked = 1;
/*
* @dev 修饰方法,锁定运行防止重入
*/
modifier lock() {
require(unlocked == 1, 'UniswapV2: LOCKED');
unlocked = 0;
_;
unlocked = 1;
}
3.5 方法
3.5.1 构造方法
构造方法比较简单,直接给factory
变量赋予msg.sender
的值,为什么要这样赋值,因为pair
合约是通过factory
合约进行部署的,所以msg.sender
的值就等于工厂合约的地址。
constructor() public {
factory = msg.sender;
}
3.5.2 初始化方法
initialze
方法是Solidity中一个比较特殊的方法,它仅仅只有在合约创建之后调用一次,为什么使用initialze
方法初始化pair
合约而不是在构造函数中初始化,这是因为pair
合约是通过create2
部署的,create2
部署合约的特点就在于部署合约的地址是可预测的,并且后一次部署的合约可以把前一次部署的合约给覆盖,这样可以实现合约的升级。如果想要实现升级,就需要构造函数不能有任何参数,这样才能让每次部署的地址都保持一致,具体细节可以查看create2
的文档。 在这个initialize
方法中,主要是将两个token
的地址分别赋予。
function initialize(address _token0, address _token1) external {
// 确认调用者为工厂地址
require(msg.sender == factory, 'UniswapV2: FORBIDDEN'); // sufficient check
token0 = _token0;
token1 = _token1;
}
3.5.3 外部调用方法
- 获取储备量方法
返回三个信息,token0
的储备量,token1
的储备量,blockTimestampLast
:上一个区块的时间戳。
function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
_reserve0 = reserve0;
_reserve1 = reserve1;
_blockTimestampLast = blockTimestampLast;
}
- 铸币方法
mint
函数的输入为一个地址to
,输出为该地址所提供的流动性,在Uniswap中,流动性也被体现成token
即LP token
。 铸币流程发生在router
合约向pair
合约发送代币之后,因此此次的储备量和合约的token
余额是不相等的,中间的差值就是需要铸币的token
金额,即amount0
和amount1
。然后是收取平台手续费,这部分代码会在后面进行解释。然后获取总的流动性的供应量totoalSupply
,如果totalSupply
等于0的话,就代表是首次铸币。 在Uniswap V2中,首次铸币获取的流动性由以下公式得到
这里的k
就是恒定乘积公式中的k
。这个公式确保了在任意时刻添加流动性时,LP token
的价值与初始供应的tokenA
和tokenB
的比例无关,比如在最开始的时候,一个ABC
的价格等于100
个XYZ
的价格,有人存入2个ABC
和200个XYZ
,这个时候他会获得20个LP token
,这20个LP token
的价值为2个ABC
加上200个XYZ
,如果有人在最开始的时候存入2个ABC
和800个XYZ
,这个时候他会获得40个LP token
。
但是我们可以看到代码里面,当第一个人存入token
的时候,并没有完全获得对应数量的LP token
,有MININUM_LIQUIDITY
数量的LP token
被转入0地址销毁了。这里是为了防止有人刻意抬高流动性单价从而垄断交易对,使得散户无力参与,即无法停供流动性。 具体的攻击流程如下: 1. 首先发送小额toekn
(比如1 wei)到交易对并且mint()
,获得价值1 wei的LP token
,此时的totalSupply
等于1 wei, reserve0
和reserve
也为1 wei。 2. 然后发送大额token(比如2000 ether)到交易对,但不调用mint()
,而是直接调用sync()
,此时totalSupply
为1 wei, reserve0
和reserve1
分别为1 wei + 2000 ether。 3. 这个时候,1 wei的LP token
的价值就约等于2000 ether了,后来的人再想获得1 wei的流动性,就需要付出价值2000 ether
的token了。
因此为了防止这种攻击的发生,只有去限制流动性的下限,在首次铸币的时候,会要求铸币者提供大于MININUM_LIQUIDITY
数量的流动性,否则就无法注入流动性。
并且为了防止攻击者通过burn()
将流动性销毁,导致总流动性不低于MININUM_LIQUIDIY
的限制被绕过,代码直接在第一次铸币的时候就把首次铸币者应该获得的流动性扣除了MININUM_LIQUIDITY
数量的LP token
发送到address(0)
锁住,依次达到限制。
然后我们接着看如果不是第一次铸币的时候,流动性的获取公式如下:
根据以上公式,流动性的获取会根据存入的两种token
分别计算,然后取最小的那个。
之后就判断流动性是否大于0,如果不大于0,就revert整个流程。
之后就是将LP token
给到address(to)
, 更新储备量, 更新储备量这个方法我们会在后面进行解析。
之后如果开启了平台手续费收取,那么就会在这里重新计算k
值。
最后触发铸造事件。
/*
* @dev 铸造方法
* @param to to地址
* @return liquidity 流动性
* @notice 应该从执行重要安全检查的合约中调用此底层方法
* this low-level function should be called from a contract which performs important safety checks
*/
function mint(address to) external lock returns (uint liquidity) {
// 获取储备量0和储备量1
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
// 获取当前合约在token0合约内的余额
uint balance0 = IERC20(token0).balanceOf(address(this));
// 获取当前合约在token1合约内的余额
uint balance1 = IERC20(token1).balanceOf(address(this));
// amount0 = 余额0 - 储备0
uint amount0 = balance0.sub(_reserve0);
// amount1 = 余额1 - 储备1
uint amount1 = balance1.sub(_reserve1);
// 返回铸造费开关
bool feeOn = _mintFee(_reserve0, _reserve1);
// 获取totalSupply
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
// 如果_totalSupply等于0
if (_totalSupply == 0) {
// 流动性 = (数量0 * 数量1)的平方根 - 最小流动性1000
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
// 在总量为0的初始状态,永久锁定最低流动性
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} else {
// 流动性 = 最小值(amount0 * _totalSupply / _reserve0 和 (amount1 * _totalSupply) / reserve1)
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}
// 确认流动性 > 0
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
// 铸造流动性给to地址
_mint(to, liquidity);
// 更新储备量
_update(balance0, balance1, _reserve0, _reserve1);
// 如果铸造费开关为true,k值 = 储备0 * 储备1
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
// 触发铸造事件
emit Mint(msg.sender, amount0, amount1);
}
- 销毁流动性方法
如果流动性的提供者想要收回流动性,那么就需要调用该方法。
首先通过getReserves()
方法获取现在的储备量。
然后获取token0
和token1
在当前pair
合约的余额
然后从当前合约的balanceOf
获取要销毁的流动性金额,这里为什么是从自身获取,是因为当前合约的余额是流动性提供者通过路由合约发送到pair
合约要销毁的金额。
计算平台手续费,这部分代码在后面解析。
获取totalSupply
,然后计算流动性提供者可以取出的token0
和token1
的数量,数量分别为amount0
和amount1
。直接通过如下公式计算得到
实际上,上述公式可以转换成如下公式,取出来token
的数量与持有的流动性占总流动性的比例有关,这样可以将流动性提供者在存入流动性期间所获取的流动性挖矿的收益也取出。
然后确保取出的amount
大于0
销毁合约内的流动性数量,发送token
给address(to)
, 更新储备量。
如果有平台手续费收取的话,重新计算k
值。
发送销毁代币事件。
/*
* @dev 销毁方法
* @param to to地址
* @return amount0
* @return amount1
* @notice 应该从执行重要安全检查的合同中调用此低级功能
* this low-level function should be called from a contract which performs important safety checks
*/
function burn(address to) external lock returns (uint amount0, uint amount1) {
// 获取储备量0,储备量1
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
// 带入变量
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
// 获取当前合约在token0合约内的余额
uint balance0 = IERC20(_token0).balanceOf(address(this));
// 获取当前合约在token1合约内的余额
uint balance1 = IERC20(_token1).balanceOf(address(this));
// 从当前合约的balanceOf映射中获取当前合约自身流动性数量
// 当前合约的余额是用户通过路由合约发送到pair合约要销毁的金额
uint liquidity = balanceOf[address(this)];
// 返回铸造费开关
bool feeOn = _mintFee(_reserve0, _reserve1);
// 获取totalSupply
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
// amount0和amount1是用户能取出来多少的数额
// amount0 = 流动性数量 * 余额0 / totalSupply 使用余额确保按比例分配
// 取出来的时候包含了很多个千分之三的手续费
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
// amount1 = 流动性数量 * 余额1 / totalSupply 使用余额确保按比例分配
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
// 确认amount0和amount1都大于0
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
// 销毁当前合约内的流动性数量
_burn(address(this), liquidity);
// 将amount0数量的_token0发送给to地址
_safeTransfer(_token0, to, amount0);
// 将amount1数量的_toekn1发给to地址
_safeTransfer(_token1, to, amount1);
// 更新balance0和balance1
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
}
- 交换token方法
交换token
方法一般通过路由合约调用,功能是交换token
,需要的参数包括:amount0Out
:token0
要交换出的数额;amount1Out
:token1
要交换出的数额,to
:交换token
要发到的地址,一般是其它pair
合约地址;data
用于闪电贷回调使用。
首先确认amount0Out
或者amount1Out
有一个大于0,然后确保储备量大于要取出的金额。
然后确保address(to)
不等于对应的token
地址。然后发送token
到对应的地址上。
然后data
有数据,就执行闪电贷的调用。
之后获取两个token
的余额,判断是否在交换之间,有token
的输入,如果没有输入就revert。
如果有输入,还需要保证交换之后的储备量的乘积等于k
,具体代码中计算公式如下:
代码中的公式这样的原因还是因为Solidity不支持小数运算,上述公式可以改写成如下形态:
其中 相当于k
值。这个公式就对应白皮书所写的每次交易收取千分之三的手续费。
最后更新储备量,触发交换事件。
/*
* @dev 交换方法
* @param amount0Out 输出数额0
* @param amount1Out 输出数额1
* @param to to地址
* @param data 用于回调的数据
* @notice 应该从执行重要安全检查的合同中调用此低级功能
* this low-level function should be called from a contract which performs important safety checks
*/
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
// 确认amount0Out和amount1Out都大于0
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
// 获取储备量0和储备量1
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
// 确认取出的量不能大于它的 储备量
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
// 初始化变量
uint balance0;
uint balance1;
{ // scope for _token{0,1}, avoids stack too deep errors
// 标记_toekn{0,1}的作用域,避免堆栈太深
address _token0 = token0;
address _token1 = token1;
// 确保to地址不等于token0和token1的地址
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
// 发送token0代币
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
// 发送token1代币
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
// 如果data的长度大于0,调用to地址的接口
// 闪电贷
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
// 余额0,1 = 当前合约在token0,1合约内的余额
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
// 如果余额0 > 大于储备0 - amount0Out 则 amount0In = 余额0 - (储备0 - amount0Out) 否则amount0In = 0
uint amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
uint amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
// 确保输入数量0||1大于0
require(amount0In > 0 || amount1In > 0, 'UniswapV2: INSUFFICIENT_INPUT_AMOUNT');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
// 调整后的余额0 = 余额0 * 1000 - (amount0In * 3)
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
// 调整后的余额1 = 余额1 * 1000 - (amount1In * 3)
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
// 确保balance0Adjusted * balance1Adjusted >= 储备0 * 储备1 * 1000000
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}
// 更新储备量
_update(balance0, balance1, _reserve0, _reserve1);
// 触发交换事件
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
- skim方法
skim
方法的功能是强制让余额等于储备量,一般用于储备量溢出的情况下,将多余的余额转出到address(to)
上,使余额重新等于储备量。
具体的逻辑也非常简单,就是将余额减去储备量的token
发送到address(t0)
上
/*
* @dev 强制平衡以匹配储备,按照储备量匹配余额
* @param to to地址
force balances to match reserves
*/
function skim(address to) external lock {
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
// 将当前合约在token1,2的余额-储备量0,1安全发送到to地址上
_safeTransfer(_token0, to, IERC20(_token0).balanceOf(address(this)).sub(reserve0));
_safeTransfer(_token1, to, IERC20(_token1).balanceOf(address(this)).sub(reserve1));
}
- sync方法
刚才skim
方法是强制让余额与储备量对等,sync
方法则是强制让储备量与余额对等,直接调用就是更新储备量的私有方法。
/*
* @dev 强制准备金与余额匹配,按照余额匹配储备量
* force reserves to match balances
*/
function sync() external lock {
_update(IERC20(token0).balanceOf(address(this)), IERC20(token1).balanceOf(address(this)), reserve0, reserve1);
}
3.5.4 私有方法
- 私有转账方法 该方法实现了只知道
token
合约地址就可以直接调用transfer
方法的功能,具体实现如下,这个方法传入了三个参数,分别是token
:合约的地址,to
:要转账的地址,value
:要转账的金额。然后直接使用call
方法直接调用对应token
合约的transfer
方法,获取返回值,需要判断返回值为true
并且返回的data
长度为0或者解码后为true
。使用call
方法的优势在于可以在不知道token
合约具体代码的前提下调用其方法。
/*
* @dev 私有安全发送
* @param token token地址
* @param to to地址
* @param value 数额
*/
function _safeTransfer(address token, address to, uint value) private {
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(SELECTOR, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'UniswapV2: TRANSFER_FAILED');
}
- 私有更新储备量方法
私有更新储备量方法主要用于每次添加流动性或者减少流动性之后调用,用于将余额同步给储备量。并且会判断时间流逝,在每一个区块的第一次调用时候,更新价格累加器,用于Uniswap v2
的价格预言机。
在方法最开始的时候,会判断余额会不会导致储备量溢出,如果溢出的话,就revert,这个时候就需要有人从外部调用skim
方法,修正溢出,将多出的token
转出。
在这个方法里面除了更新reserve0
和reserve1
之外,还更新了blockTimestampLast
,它的取值是当前区块时间对 取余得到的结果。
/*
* @dev 更新储备量,并在每一个区块的第一次调用时,更新价格累加器
* @param balance0 余额0
* @param balance1 余额1
* @param _reserve0 储备量0
* @param _reserve1 储备量1
* update reserves and, on the first call per block, price accumulators
*/
function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
// 确认余额0和余额1小于等于最大的uint112
require(balance0 <= uint112(-1) && balance1 <= uint112(-1), 'UniswapV2: OVERFLOW');
// 区块时间戳,将时间戳转换成uint32
uint32 blockTimestamp = uint32(block.timestamp % 2**32);
// 计算时间流逝
uint32 timeElapsed = blockTimestamp - blockTimestampLast; // overflow is desired
// 如果时间流逝>0,并且储备量0、1不等于0,也就是第一个调用
if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) {
// * never overflows, and + overflow is desired
// 价格0最后累计 += 储备量1 * 2**112 / 储备量0 * 时间流逝
price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed;
// 价格1最后累计 += 储备量0 * 2**112 / 储备量1 * 时间流逝
price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed;
}
// 余额0,1放入储备量0,1
reserve0 = uint112(balance0);
reserve1 = uint112(balance1);
// 更新最后时间戳
blockTimestampLast = blockTimestamp;
// 触发同步事件
emit Sync(reserve0, reserve1);
}
- 平台手续费收取方法 平台手续费的是否开启的开关是在
factory
合约中定义的,后续会解析factory
合约。
如果收取平台收费的话,那么收取的金额为交易手续费的1/6。具体计算方式可以去参考白皮书。
_mintFee
函数首先获取factory
合约里面的address(feeTo)
, 这是平台收取手续费的地址。然后判断address(feeTo)
是否等于address(0)
决定是否收取平台手续费。
如果开启平台手续费收取,首先会判断kLast
是否为0,如果不为0就进行平台手续费的计算。
平台手续费的计算,首先要明确一点,因为每一笔交易都会有千分之三的手续费,那么k
值也会随着缓慢增加,所有连续两个时刻之间的k
值差值就是这段时间的手续费。以上过程可以用如下公式表达:
其中 表示当前时刻的平台手续费占当前时刻流动性的比重, 和 分别表示前一时刻流动性的值和当前时刻流动性的值。
此时假设当前时刻的LP token
的总供应量是 ,需要发放给address(feeTo)
的LP token
的量为 , 由于LP token
不能减发,且发送到address(feeTo)
的LP token
需要在发送给流动性提供者LP token
之前发放,故而需要满足如下等式:
其中 是平台手续费占交易手续费的比例。由上述两式可推算出 的结果如下:
上述式子,就等价于如下代码中计算平台手续费的过程。
/*
* @dev 如果收费,铸造流动性相当于1/6的增长sqrt(k)
* @param _reserve0 储备量0
* @param _reserve1 储备量1
* @return feeOn
* 这一部分可参考白皮书协议费用那部分·
* if fee is on, mint liquidity equivalent to 1/6th of the growth in sqrt(k)
*/
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
// 查询工厂合约的feeTo变量值
address feeTo = IUniswapV2Factory(factory).feeTo();
// 如果feeTo不等于0地址,feeOn等于true否则false
feeOn = feeTo != address(0);
// 定义k值
uint _kLast = kLast; // gas savings
// 如果feeOn等于true
if (feeOn) {
// 如果k值不等于0
if (_kLast != 0) {
// 计算(_reserve0*_reserve1)的平方根
uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
// 计算k值的平方根
uint rootKLast = Math.sqrt(_kLast);
// 如果rootK>rootKLast
if (rootK > rootKLast) {
// 分子 = erc20总量 * (rootK - rootKLast)
uint numerator = totalSupply.mul(rootK.sub(rootKLast));
// 分母 = rootK * 5 + rootKLast
uint denominator = rootK.mul(5).add(rootKLast);
// 流动性 = 分子 / 分母
uint liquidity = numerator / denominator;
// 如果流动性 > 0 将流动性铸造给feeTo地址
if (liquidity > 0) _mint(feeTo, liquidity);
}
}
// 否则如果_kLast不等于0
} else if (_kLast != 0) {
kLast = 0;
}}
4. 总结
本篇文章解析了Uniswap V2
核心合约代码中的pair
合约的代码,在实际部署当作pair
合约不会直接进行部署,而是通过factory
合约进行部署的,接下来会继续解析factory
合约的代码。