Uniswap V2 添加流动性代码的解释(二)_uniswap v2创建流动性
Uniswap V2 添加流动性的解释(二)
-
- 1.Uniswap V2 添加流动性流程图
-
- 添加流动性流程
- 整体流程代码解释:
- 2. 整体逻辑
-
- 计算最优代币比例
- 获得pair地址
- LP Token 铸造
- 协议费或者是项目方的手续费
- 3.参考资料:
1.Uniswap V2 添加流动性流程图
添加流动性流程
- 选择交易对:用户首先要确定想要提供流动性的交易对,例如ETH/USDT。这一步需要通过UniswapV2Factory合约查询该交易对是否已经存在,如果不存在,需要先创建交易对。
- 准备代币:用户需要准备好要存入流动性池的两种代币(对应交易对中的两种代币),并且确保钱包中有足够的余额。
- 调用mint函数:用户通过调用UniswapV2Pair合约的
mint
函数,将两种代币存入流动性池。合约会根据存入时两种代币的数量比例,按照恒定乘积公式(x * y = k
,x
和y
分别是两种代币的数量,k
是常数)来计算存入操作后流动性池的状态。 - 获取LP Token:存入成功后,UniswapV2Pair合约会调用UniswapV2ERC20合约铸造相应数量的LP(Liquidity Provider) Token,并将其发送给用户。LP Token代表了用户在流动性池中所占的份额,后续可以凭借LP Token参与收益分配以及赎回存入的代币。
整体流程代码解释:
- 用户准备:用户需要事先批准 Router 合约可以转移其持有的两种代币
用户调用addLiquidity
函数发起添加流动性请求 - 计算最优比例:Router 计算两种代币的最优添加比例
- 资金转移:用户的代币被转移到交易对合约
- LP Token 铸造:交易对合约为用户铸造相应数量的 LP Token
- 状态更新:交易对合约更新储备量和价格信息
2. 整体逻辑
用户起始的地方UniswapV2Router02.sol
/** * @dev: 根据两种token的地址向其交易对添加流动性 * @param {address} tokenA:tokenA地址 * @param {address} tokenB:tokenB地址 * @param {uint} amountADesired:期望添加tokenA的数量 * @param {uint} amountBDesired:期望添加tokenB的数量 * @param {uint} amountAMin:愿意接受的最低tokenA数量 * @param {uint} amountBMin:愿意接受的最低tokenB数量 * @param {address} to:接受lptoken的地址 * @param {uint} deadline:交易允许最后执行时间 * @return {uint} amountA:实际添加到资金池中tokenA的数量 * @return {uint} amountB:实际添加到资金池中tokenB的数量 * @return {uint} liquidity:获得lptoken的数量 */ 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); // 获得pair地址 address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB); // 转账两种token的最佳的amount数量到pair合约,这个时候就已经添加了流动性,但是还没有把LP token给LP提供者。 TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA); TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB); // 向LP提供者 to地址铸造lptoken liquidity = IUniswapV2Pair(pair).mint(to); }
计算最优代币比例
调用UniswapV2Router02.sol中_addLiquidity获得最佳的添加流动性的tokenA和tokenB的数量,确保添加的两种代币符合当前交易对的价格比例
/** * @dev: 根据两种token的地址向其交易对添加流动性。准确来说是获得最佳的添加流动性的tokenA和tokenB的数量 * @param {address} tokenA:tokenA地址 * @param {address} tokenB:tokenB地址 * @param {uint} amountADesired:期望添加tokenA的数量 * @param {uint} amountBDesired:期望添加tokenB的数量 * @param {uint} amountAMin:愿意接受的最低tokenA数量,用于控制滑点 * @param {uint} amountBMin:愿意接受的最低tokenB数量,用于控制滑点 * @return {uint} amountA:实际添加到资金池中tokenA的数量 * @return {uint} amountB:实际添加到资金池中tokenB的数量 */ function _addLiquidity( address tokenA, address tokenB, uint amountADesired, uint amountBDesired, uint amountAMin, uint amountBMin ) internal virtual returns (uint amountA, uint amountB) { // 拿到lpToken的地址,若不存在则创建一个交易对 if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) { IUniswapV2Factory(factory).createPair(tokenA, tokenB); } // 获取两种token的储备量 (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB); if (reserveA == 0 && reserveB == 0) { // 首次添加流动性 (amountA, amountB) = (amountADesired, amountBDesired); } else { // 根据两种token的储备量和期望tokenA的数额获取tokenB最佳的数额 uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB); // 如果amountBOptimal不大于amountBDesired并且amountBOptimal不小于amountBMin,则返回amountADesired, amountBOptimal // amountBMin 不能乱设 if (amountBOptimal <= amountBDesired) { require(amountBOptimal >= amountBMin, \'UniswapV2Router: INSUFFICIENT_B_AMOUNT\'); (amountA, amountB) = (amountADesired, amountBOptimal); } else { // 如果amountBOptimal大于amountBDesired,则根据两种token的储备量和期望tokenB的数额获取tokenA最佳的数额 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA); // 断言 assert(amountAOptimal <= amountADesired); // 并且amountAOptimal不小于amountAMin,则返回amountAOptimal, amountBDesired require(amountAOptimal >= amountAMin, \'UniswapV2Router: INSUFFICIENT_A_AMOUNT\'); (amountA, amountB) = (amountAOptimal, amountBDesired); } } }
// 给定数量的某个tokenA和该交易池储备对,返回等量的tokenB数量,等比例兑换,保证y/x=p不变。 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; }
_addLiquidity的好处:
- 价格比例维护:通过
quote
函数确保新添加的代币符合当前储备比例,维持价格稳定 - 滑点控制:
amountAMin
和amountBMin
参数防止因价格波动导致用户接受过低的兑换比例
获得pair地址
调用UniswapV2Library.sol中pairFor获得pair地址,这个是规定的。
// 对两个的token进行排序 function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) { require(tokenA != tokenB, \'UniswapV2Library: IDENTICAL_ADDRESSES\'); (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); require(token0 != address(0), \'UniswapV2Library: ZERO_ADDRESS\'); } // 在不进行任何外部调用的情况下计算一对的CREATE2地址 function pairFor(address factory, address tokenA, address tokenB) internal pure returns (address pair) { // 一定要进行排序,这样才是同一个pair.比如 tokenA:eth tokenB:dai, eth 和dai ,dai和eth 这是同一个。不能创造两个pair (address token0, address token1) = sortTokens(tokenA, tokenB); pair = address(uint(keccak256(abi.encodePacked( hex\'ff\', factory, keccak256(abi.encodePacked(token0, token1)), hex\'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f\' )))); }
LP Token 铸造
在pair合约中,先计算要添加流动性所获得的lptoken数量,再铸造lptoken。状态更新是通过_update(balance0, balance1, _reserve0, _reserve1);来实现的。在第一次添加流动性的时候,会添加虚拟流动性 1000 来防止攻击,永久锁定少量 LP Token,防止流动性池被完全清空 (“流动性归零” 攻击)。
/** * @dev: 铸造lptoken * @param {address} to:接受lptoken的地址 * @return {uint} liquidity: lptoken的数量 */ function mint(address to) external lock returns (uint liquidity) { // 读取代币的存储量 节省gas (uint112 _reserve0, uint112 _reserve1, ) = getReserves(); // 获取这两种代币的余额 uint balance0 = IERC20(token0).balanceOf(address(this)); uint balance1 = IERC20(token1).balanceOf(address(this)); // 计算当前合约中两个代币的净增量,并赋值给amount0和amount1变量 // 净增量等于余额减去储备量 uint amount0 = balance0.sub(_reserve0); uint amount1 = balance1.sub(_reserve1); // 判断是否进行收取手续费 bool feeOn = _mintFee(_reserve0, _reserve1); // 节省gas,必须在这里定义,因为totalSupply可以在_mintFee中更新 uint _totalSupply = totalSupply; // 创建一个新的流动性池 if (_totalSupply == 0) { // 故意添加虚拟流动性 liquidity = Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY); // 调用_mint函数,向零地址铸造最小流动性常量数量的流动性代币,永久锁定MINIMUM_LIQUIDITY _mint(address(0), MINIMUM_LIQUIDITY); } else { // 添加流动性所获得的lptoken数量(进行添加流动性的两种token的数量*目前lptoken的数量/当前token的储备量-->取较小值) liquidity = Math.min( amount0.mul(_totalSupply) / _reserve0, amount1.mul(_totalSupply) / _reserve1 ); } require(liquidity > 0, \"UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED\"); // 铸造lptoken函数 _mint(to, liquidity); // 更新储备函数 ,更新当前合约中两个代币的储备量为最新的余额, 也用于orcle _update(balance0, balance1, _reserve0, _reserve1); // 如果收取手续费,更新交易后的k值 if (feeOn) kLast = uint(reserve0).mul(reserve1); emit Mint(msg.sender, amount0, amount1); }
/** * @dev: 更新储备,并在每个区块的第一次调用时更新价格累加器,用于orcle * @param {uint} balance0:更新后tokenA的储备量 * @param {uint} balance1:更新后tokenA的储备量 * @param {uint112} _reserve0:当前tokenA的储备量 * @param {uint112} _reserve1:当前tokenB的储备量 */ function _update( uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1 ) private { require( balance0 <= uint112(-1) && balance1 <= uint112(-1), \"UniswapV2: OVERFLOW\" ); // 取时间戳的低 32 位 记录当前更新时间 uint32 blockTimestamp = uint32(block.timestamp % 2 ** 32); // 时间间隔 uint32 timeElapsed = blockTimestamp - blockTimestampLast; // 计算出当前的交易价格 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // 永远不会溢出,+ overflow是理想的 // priceCumulativeLast += ((_reserve1 * 2 ** 112 ) / _reserve0 ) * timeElapsed price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * timeElapsed; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } // 将这两种代币的存储量设置为代币的余额 reserve0 = uint112(balance0); reserve1 = uint112(balance1); // 更新当前操作时间 blockTimestampLast = blockTimestamp; emit Sync(reserve0, reserve1); }
协议费或者是项目方的手续费
_mintFee的解释
/** * @dev: 如果打开收费功能,就约等于1/6的增长的根号(k) * @param {uint112} _reserve0:tokenA的储备量 * @param {uint112} _reserve1:tokenB的储备量 * @return {bool} feeOn: 返回是否接受手续费 */ function _mintFee( uint112 _reserve0, uint112 _reserve1 ) private returns (bool feeOn) { // 获取收取手续费的地址 address feeTo = IUniswapV2Factory(factory).feeTo(); feeOn = feeTo != address(0); // 检查该factory是否设置了手续费接收地址 // 节省gas uint _kLast = kLast; if (feeOn) { if (_kLast != 0) { // rootk=sqrt(_reserve0 * _reserve1) uint rootK = Math.sqrt(uint(_reserve0).mul(_reserve1)); // 上一次交易后的sqrt(k)值 uint rootKLast = Math.sqrt(_kLast); if (rootK > rootKLast) { // 分子(lptoken总量*(rootK-rootKLast)) uint numerator = totalSupply.mul(rootK.sub(rootKLast)); // 分母(rooL*5+rooKLast) uint denominator = rootK.mul(5).add(rootKLast); // liquidity = ( totalSupply * ( sqrt(_reserve0 * _reserve1) - sqrt(_kLast) ) ) / sqrt(_reserve0 * _reserve1) * 5 + sqrt(_kLast) uint liquidity = numerator / denominator; if (liquidity > 0) _mint(feeTo, liquidity); // 给feeTo地址铸币liquidity,请注意,并没有把项目费用(协议费)提取出来 } } } else if (_kLast != 0) { kLast = 0; } }
S1是提供者的LP token 或者说是份额。Sm是分给项目方的份额。
这就是这个公式的由来。
liquidity = ( totalSupply * ( sqrt(_reserve0 * _reserve1) - sqrt(_kLast) ) ) / sqrt(_reserve0 * _reserve1) * 5 + sqrt(_kLast)
如果看不明白可以参考:
手续费的计算机制
How Uniswap V2 computes the mintFee
也可以看白皮书的2.4 Protocol fee,但是这个也不是很清楚。
3.参考资料:
深入理解 Uniswap v2 合约代码
uniswap V2 合约调用关系
uniswap v2 代码解读
Uniswap-v2 合约概览
Uniswap V2 core源码解析
5 分钟读懂 Uniswap V2:最全核心合约解析+实战代码
手续费的计算机制
How Uniswap V2 computes the mintFee