网络日志

LP流动性挖矿系统开发构思,详细逻辑

Uniswap 代码结构

  Uniswap 智能合约代码由两个 github 项目组成。一个是 core,一个是 periphery。

  https://github.com/Uniswap/un...

  https://github.com/Uniswap/un...

  core 偏核心逻辑,(LP 流动性挖矿系统开发,详细对接v+hkkf5566)单个 swap 的逻辑。periphery 偏外围服务,一个个 swap 的基础上构建服务。单个 swap,两种代币形成的交易对,俗称“池子”。每个交易对有一些基本属性:reserve0/reserve1 以及 total supply。reserve0/reserve1 是交易对的两种代币的储存量。total supply 是当前流动性代币的总量。每个交易对都对应一个流动性代币(LPT-liquidity provider token)。简单的说,LPT 记录了所有流动性提供者的贡献。所有流动性代币的总和就是 total supply。Uniswap 协议的思想是 reserve0*reserve1 的乘积不变。

  Periphery 逻辑

  核心逻辑实现在 UniswapV2Router02.sol 中。称为 Router,因为 Periphery 实现了“路由”,支持各个 swap 之间的连接。基本上实现了三个功能:1/add liquidity(增加流动性)2/remove liqudity(抽取流动性)3/swap(交换)。

  1.add liqudity

  增加流动性,就是同时提供两种代币。因为代币有可能是 ETH,针对不同情况有不同的接口。逻辑类似。

  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)

  add liqudity 查看之前有没有创建相应的交易对。如果有相应的交易对,确定目前的兑换比例在希望的范围内(期望 amountDesired 和不低于 amountMin)。如果兑换比例 OK,将相应的代币转入对应的交易对池子,并调用其的 mint 函数。

  2.remove liqudity

  提供流动性的相反的操作就是抽取流动性。也就是说,流动性提供者不再提供相应的流动性:

  function removeLiquidity(

  address tokenA,

  address tokenB,

  uint liquidity,

  uint amountAMin,

  uint amountBMin,

  address to,

  uint deadline

  )public virtual override ensure(deadline)returns(uint amountA,uint amountB){

  liquidity 是抽取的流动性的量。amountMin 是抽取代币的最小的个数。to 是抽取代币的目标地址。deadline 是个有意思的设计:抽取的操作有时效性。超过了一定的 deadline(区块高度),这次抽取操作看成无效。

  先收回需要抽取的 Token,并且销毁:

  IUniswapV2Pair(pair).transferFrom(msg.sender,pair,liquidity);//send liquidity to pair

  (uint amount0,uint amount1)=IUniswapV2Pair(pair).burn(to);

  3.swap

  swap 是普通用户进行代币交易的操作。普通用户通过 swap 操作实现两种 token 之间的交易。

  function swapExactTokensForTokens(

  uint amountIn,

  uint amountOutMin,

  address[]calldata path,

  address to,

  uint deadline

  )external virtual override ensure(deadline)returns(uint[]memory amounts){

  Uniswap 支持多种代币的交换。具体的含义是,Uniswap 提供了多级交易池的路由功能。举个例子,已有两个交易对 TokenA-TokenB,以及 TokenB-TokenC,通过 swap 接口,可以实现 TokenA-TokenC 的交换,其中经过的 TokenA-TokenB,TokenB-TokenC,称为路径(path)。amountIn 是路径中的第一个代币的数量,amountOutMin 是期望的交换后的最少的数量。

  amounts=UniswapV2Library.getAmountsOut(factory,amountIn,path);

  require(amounts[amounts.length-1]>=amountOutMin,'UniswapV2Router:INSUFFICIENT_OUTPUT_AMOUNT');

  amounts 是每个路径上的交换后的数量。amounts[amounts.length-1]也就是最后一条路径的输出数量。注意,UniswapV2Library.getAmountsOut 的实现(在获取每个交易对的 reserve 信息后,调用 getAmountOut 函数):

  function getAmountOut(uint amountIn,uint reserveIn,uint reserveOut)internal pure returns(uint amountOut){

  require(amountIn>0,'UniswapV2Library:INSUFFICIENT_INPUT_AMOUNT');

  require(reserveIn>0&&reserveOut>0,'UniswapV2Library:INSUFFICIENT_LIQUIDITY');

  uint amountInWithFee=amountIn.mul(997);

  uint numerator=amountInWithFee.mul(reserveOut);

  uint denominator=reserveIn.mul(1000).add(amountInWithFee);

  amountOut=numerator/denominator;

  }

  注意,其中的 997/1000 的系数。在进入每个交易池之前,进入的金额先扣除了 0.3%的本金。这个就是交易费。注意的是,路径上的交易池,每个池子都收。有点像高速收费站,一段段的收。

  TransferHelper.safeTransferFrom(

  path[0],msg.sender,UniswapV2Library.pairFor(factory,path[0],path[1]),amounts[0]

  );

  将代币 path[0],转入到交易对,数量为 amounts[0]。转入代币后,进行真正的 swap 操作:

  function _swap(uint[]memory amounts,address[]memory path,address _to)internal virtual{

  for(uint i;i<path.length-1;i++){

  (address input,address output)=(path,path[i+1]);

  (address token0,)=UniswapV2Library.sortTokens(input,output);

  uint amountOut=amounts[i+1];

  (uint amount0Out,uint amount1Out)=input==token0?(uint(0),amountOut):(amountOut,uint(0));

  address to=i<path.length-2?UniswapV2Library.pairFor(factory,output,path[i+2]):_to;

  IUniswapV2Pair(UniswapV2Library.pairFor(factory,input,output)).swap(

  amount0Out,amount1Out,to,new bytes(0)

  );

  }

  }

  原理比较简单,针对每一条路径,调用交易对的 swap 操作。

  Core 逻辑

  Core 逻辑实现了单个交易对的逻辑。通过 UniswapV2Factory 可以创建一个个 Pair(交易池)。每个具体实现逻辑在 UniswapV2Pair 中。

  1.mint

  每个交易对创建流动性。

  function mint(address to)external lock returns(uint liquidity){

  因为在调用 mint 函数之前,在 addLiquidity 函数已经完成了转账,所以,从这个函数的角度,两种代币数量的计算方式如下:

  uint balance0=IERC20(token0).balanceOf(address(this));

  uint balance1=IERC20(token1).balanceOf(address(this));

  uint amount0=balance0.sub(_reserve0);

  uint amount1=balance1.sub(_reserve1);

  当前的 balance 是当前的 reserve 加上注入的流动性的代币数量。

  uint _totalSupply=totalSupply;//gas savings,must be defined here since totalSupply can update in _mintFee

  if(_totalSupply==0){

  liquidity=Math.sqrt(amount0.mul(amount1)).sub(MINIMUM_LIQUIDITY);

  _mint(address(0),MINIMUM_LIQUIDITY);//permanently lock the first MINIMUM_LIQUIDITY tokens

  }else{

  liquidity=Math.min(amount0.mul(_totalSupply)/_reserve0,amount1.mul(_totalSupply)/_reserve1);

  }

  _mint(to,liquidity);

  流动性 liquidity 的计算方式在第一次提供流动性时和其他时候稍稍不同。第一次提供流动性的计算公式如下:

  liquidity=sqrt(x0*y0)-min

  其中 min 是 10^3。也就是说,第一次提供流动性是有最小流动性要求的。其他提供流动性的计算公式如下:

  liquidity=min((x0/reserve0totalsupply),(y0/reserve1totalsupply))

  也就说,按照注入的流动性和当前的 reserve 的占比一致。

  2.burn

  burn 函数用在抽取流动性。burn 逻辑和 mint 逻辑类似。

  function burn(address to)external lock returns(uint amount0,uint amount1){

  3.swap

  swap 函数实现两种代币的兑换。

  function swap(uint amount0Out,uint amount1Out,address to,bytes calldata data)external lock{

  一个交易池的 swap 操作支持两个方向的兑换,可以从 TokenA 换到 TokenB,或者 TokenB 换到 TokenA。

  if(amount0Out>0)_safeTransfer(_token0,to,amount0Out);//optimistically transfer tokens

  if(amount1Out>0)_safeTransfer(_token1,to,amount1Out);//optimistically transfer tokens

  因为在 swapExactTokensForTokens 的 getAmountOut 函数已经确定兑换处的金额。所以,先直接转账。

  在不做 swap 之前,balance 应该和 reserve 相等的。通过 balance 和 reserve 的差值,可以反推出输入的代币数量:

  uint amount0In=balance0>_reserve0-amount0Out?balance0-(_reserve0-amount0Out):0;

  uint amount1In=balance1>_reserve1-amount1Out?balance1-(_reserve1-amount1Out):0;

  确保反推的输入代币数量不小于零。

  require(amount0In>0||amount1In>0,'UniswapV2:INSUFFICIENT_INPUT_AMOUNT');