UniswapV2-添加流动性

交易(swap)的前提,是交易对象具有流动性。这里,涉及到 swap 和 addLiquidity 两个去中心化交易所最核心也是最基本的功能。
造市商(Market maker) 将两种 token 注入到交易池,为 swap 提供支持,而注入的过程,就是 addLiquidity。本文,将主要探讨流动性添加的过程。

UniswapV2-创建 UniswapV2Pair
UniswapV2-添加流动性
UniswapV2-移除流动性
UniswapV2-兑换(swap)
UniswapV2-公式分析

一、概述

添加流动性的基本步骤包括:

  1. 确定 pair 中,tokenA 和 tokenB 需要输入的比例;
  2. 将一定量的 tokenA 和 tokenB 转入到 pair;
  3. 在 pair 中将收到的 tokenA 和 tokenB 转化为 LP Token,作为其后续取出两者并获取分红的计算依据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol#L61
// 添加流动性
/**
**@tokenA pair 或者说池子中的一种 token
**@tokenB pair 中的另一种 token
**@amountADesired 希望提供的 tokenA 的总量
**@amountBDesired 希望提供的 tokenB 的总量
**@amountAMin 最少提供的 tokenA 的总量
**@amountBMin 最小提供的 tokenB 的总量
**@to 接收 lp token 的地址,用户以后将依赖发给这个地址的 lp token 赎回自己的 token
**@deadline 添加操作应该 deadline 之前完成
***/
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
// 根据报价,获取用户实际需要提供的 tokenA 和 tokenB 的数量。
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
// 获取 token pair 地址
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
// 为 pair 转入 amountA 的 tokenA
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
// 为 pair 转入 amountB 的 tokenB
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
// 两种 token 转入到 pair 完成之后,开始铸币
liquidity = IUniswapV2Pair(pair).mint(to);
}

二、确定 tokenA 和 tokenB 输入量

确定的依据,是通过 tokenA = tokenB.mul(reserveB) / reserveA,获取到实际需要用户提供的 amountA 和 amountB。其中 reverseA 和 reverseB 是当前 pair 中的存量。
对于首次为 pair 提供流动性的情况,此时用户拥有其初始定价权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// https://github.com/Uniswap/v2-periphery/blob/master/contracts/UniswapV2Router02.sol#L33
function _addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin
) internal virtual returns (uint amountA, uint amountB) {
// create the pair if it doesn't exist yet
// 如果 pair 不存在,则创建一个
if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
// 由 Factory 负责 pair 的创建
IUniswapV2Factory(factory).createPair(tokenA, tokenB);
}
// 获取当前 pair 中 tokenA 和 tokenB 的存量
(uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
if (reserveA == 0 && reserveB == 0) {
// 如果是首次创建
(amountA, amountB) = (amountADesired, amountBDesired);
} else {
// 根据 tokenA 的输入量,获取 tokenB 的报价,或者反之。
uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
// amountBOptimal 应不小于 amountBMin
if (amountBOptimal <= amountBDesired) {
require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
(amountA, amountB) = (amountADesired, amountBOptimal);
} else {
uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
assert(amountAOptimal <= amountADesired);
// 或者 amountAOptimal 应不小于 amountAMin
require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
(amountA, amountB) = (amountAOptimal, amountBDesired);
}
}
}

下述代码块将体现其报价公式: dx/dy = x/y

1
2
3
4
5
6
7
8
9
// https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol#L35
// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
// 报价的依据,是恒定乘积模型: x*y=k 。通过该模型,确定 x 和 y 之间相对的价值关系。
function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
// dx/dy = x/y
amountB = amountA.mul(reserveB) / reserveA;
}

三、转入 token 到 pair

在 tokenA 和 tokenB 合约中,分别转入指定量的 amount 给 pair。
先转入,再由 pair 将转化为 LP Token。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// https://github.com/Uniswap/solidity-lib/blob/master/contracts/libraries/TransferHelper.sol#L33
function safeTransferFrom(
address token,
address from,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
require(
success && (data.length == 0 || abi.decode(data, (bool))),
'TransferHelper::transferFrom: transferFrom failed'
);
}

四、铸币,获取 LP Token

铸币环节,在添加流动性时调用,发生 UniswapV2Pair 中。
合约中,将生成 lp token 作为用户以后赎回其原注入 token 的依据。
以下代码块,是具体的铸币过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L110
// this low-level function should be called from a contract which performs important safety checks
function mint(address to) external lock returns (uint liquidity) {
// 获取存量
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
// pair 在 token0 合约中的余额
// 由于添加了流动性,balance0 已经是转账之后的额度
uint balance0 = IERC20(token0).balanceOf(address(this));
// pair 在 token1 合约中的余额
uint balance1 = IERC20(token1).balanceOf(address(this));
// 本次实际转账数量 = 转账后账户额度 - 转账前账户额度
uint amount0 = balance0.sub(_reserve0);
uint amount1 = balance1.sub(_reserve1);

bool feeOn = _mintFee(_reserve0, _reserve1);

// 注意,此时如果 feeOn 为打开状态,
// 则 _totalSupply 已经包括了本次转给 feeTo 的 lp
uint _totalSupply = totalSupply;

if (_totalSupply == 0) {
// 第一次铸币,lp token = sqrt(x*y) - MINIMUM_LIQUIDITY。 其中,这 MINIMUM_LIQUIDITY 被发送给了黑洞地址。MINIMUM_LIQUIDITY = 1000
liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);
_mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
} else {
// 非第一次铸币
// lp token = (本次实际转账额度 dx 或者 dy) / (已经存在的 x 或者 y 的份额) * (_totalSupply) .
// 公式:dx / d(lp) = x / totalSupply
// 如果 feeOn 关闭,则
liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1);
}

// 因此,第一次铸币, lp 不得小于 MINIMUM_LIQUIDITY
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);

_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
}

上述代码块中,有几点需要注意:

  1. 协议费,即由 feeTo 收取的费用,其收取方式是 新增 lp token 给 feeTo。当然,前提是 feeTo 为非零地址。关于 feeTo,我们在 创建 UniswapV2Pair 已经做了描述。
  2. feeTo 地址目前还是零地址,即还没有开始收取

五、协议费(Protocol fees)

如果协议费开关打开,且 K 值实现增长,其收取的费用,是 造市商 收益的 1/6。其计算公式如下图所示,将收益以 sqrt(k) 的增长表示,那么,公式(1)中,代表了某段时间内 收益 占 当前流动性的比例。公式(2)中,s1 表示当前 lp token 的 totalSupply,sm表示在这段时间内平台收取的协议费。

下述是一个示例。
假设创建 pair 后,造市商初始提供了 100DAI 和 1ETH,他收到的份额是 10;一段时间内,一直没有其它造市商再提供流动性;当前,经过这段时间的 swap ,pair 中剩余 96个 DAI 和 1.5 个 ETH,当造市商想要移除流动性的时候,如果平台协议费开始收费,则其可以收取的协议费为:

这时,totalSupply 变成了 100+0.0286,即,造市商在进行资金赎回的时候,是以 100.0286 的为基础的分成比例上进行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L89
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
address feeTo = IUniswapV2Factory(factory).feeTo();
// 如果 feeto 是 零 地址,则说明 feeon 开关处于关闭状态
feeOn = feeTo != address(0);
uint _kLast = kLast; // gas savings
if (feeOn) {
if (_kLast != 0) { // 第一次添加流动性时, _kLast 为 0.
uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1));
uint rootKLast = Math.sqrt(_kLast);
if (rootK > rootKLast) { // 如果 K 值增长
uint numerator = totalSupply.mul(rootK.sub(rootKLast));
uint denominator = rootK.mul(5).add(rootKLast);
// Sm = [ (sqrt(k2) - sqrt(k1)) / 5*sqrt(k2) + sqrt(k1) ] * S1
uint liquidity = numerator / denominator;
if (liquidity > 0) _mint(feeTo, liquidity);
}
}
} else if (_kLast != 0) {
kLast = 0;
}
}

铸造新的 lp token,即 totalSupplyaddress 均增加相应的份额。

1
2
3
4
5
function _mint(address to, uint value) internal {
totalSupply = totalSupply.add(value);
balanceOf[to] = balanceOf[to].add(value);
emit Transfer(address(0), to, value);
}

六、小结

添加流动性,就是分别向两种 token 池中注入一定的量,这样兑换者才能通过向一个 token 池中输入一定量的方式,从另外一个 token 池中取出另外一种 token。

添加流动性,还会进行协议费的计算。协议费是平台方向 LP 薅羊毛,与兑换过程无关。只不过,协议费用的收取,目前还没有开始。

最后,添加流动性,成为造市商,就一定能够实现盈利吗?未必,套利者无处不在,这就是所谓的 无常损失,将在后面的内容中介绍。

参考链接

Uniswap v2 Core
UniswapV2公式推导
How do LIQUIDITY POOLS work?
How Do Liquidity Pools Work? DeFi Explained
UniswapV3Factory
UniswapV2Factory

© 2024 YueGS