交易(swap)的前提,是交易对象具有流动性。这里,涉及到 swap 和 addLiquidity 两个去中心化交易所最核心也是最基本的功能。
造市商(Market maker) 将两种 token 注入到交易池,为 swap 提供支持,而注入的过程,就是 addLiquidity。本文,将主要探讨流动性添加的过程。
UniswapV2-创建 UniswapV2Pair
UniswapV2-添加流动性
UniswapV2-移除流动性
UniswapV2-兑换(swap)
UniswapV2-公式分析
一、概述
添加流动性的基本步骤包括:
- 确定 pair 中,tokenA 和 tokenB 需要输入的比例;
- 将一定量的 tokenA 和 tokenB 转入到 pair;
- 在 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
|
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) { (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin); address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); 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
| function _addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin ) internal virtual returns (uint amountA, uint amountB) { if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { IUniswapV2Factory(factory).createPair(tokenA, tokenB); } (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); if (reserveA == 0 && reserveB == 0) { (amountA, amountB) = (amountADesired, amountBDesired); } else { uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); 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); require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT'); (amountA, amountB) = (amountAOptimal, amountBDesired); } } }
|
下述代码块将体现其报价公式: dx/dy = x/y
1 2 3 4 5 6 7 8 9
|
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'); 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
| function safeTransferFrom( address token, address from, address to, uint256 value ) internal { (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
|
function mint(address to) external lock returns (uint liquidity) { (uint112 _reserve0, uint112 _reserve1,) = getReserves(); uint balance0 = IERC20(token0).balanceOf(address(this)); uint balance1 = IERC20(token1).balanceOf(address(this)); uint amount0 = balance0.sub(_reserve0); uint amount1 = balance1.sub(_reserve1);
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; if (_totalSupply == 0) { liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); _mint(address(0), MINIMUM_LIQUIDITY); } else { liquidity = Math.min(amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1); }
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED'); _mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1); if (feeOn) kLast = uint(reserve0).mul(reserve1); emit Mint(msg.sender, amount0, amount1); }
|
上述代码块中,有几点需要注意:
- 协议费,即由 feeTo 收取的费用,其收取方式是 新增 lp token 给 feeTo。当然,前提是 feeTo 为非零地址。关于 feeTo,我们在 创建 UniswapV2Pair 已经做了描述。
- 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
| function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) { address feeTo = IUniswapV2Factory(factory).feeTo(); feeOn = feeTo != address(0); uint _kLast = kLast; if (feeOn) { if (_kLast != 0) { uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1)); uint rootKLast = Math.sqrt(_kLast); if (rootK > rootKLast) { uint numerator = totalSupply.mul(rootK.sub(rootKLast)); uint denominator = rootK.mul(5).add(rootKLast); uint liquidity = numerator / denominator; if (liquidity > 0) _mint(feeTo, liquidity); } } } else if (_kLast != 0) { kLast = 0; } }
|
铸造新的 lp token
,即 totalSupply
和 address
均增加相应的份额。
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