ido丨defi丨dapp丨lp预售代币合约流动性质押挖矿分红系统开发说明及详细
Uniswap代码结构:
Uniswap智能合约代码由两个github项目组成。一个是core,一个是periphery。
core偏核心逻辑,单个swap的逻辑。periphery偏外围服务,一个个swap的基础上构建服务。单个swap,两种代币形成的交易对,俗称“池子”。每个交易对有一些基本属性:reserve0/reserve1以及total supply。reserve0/reserve1是交易对的两种代币的储存量。total supply是当前流动性代币的总量。每个交易对都对应一个流动性代币(LPT-liquidity provider token)。简单的说,LPT记录了所有流动性提供者的贡献。所有流动性代币的总和就是total supply。Uniswap协议的思想是reserve0*reserve1的乘积不变。
Periphery逻辑:本文由系统开发对接V:ch3nguang编辑整理发布。
核心逻辑实现在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<i>,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/reserve0*totalsupply),(y0/reserve1*totalsupply))
也就说,按照注入的流动性和当前的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);
发表回复