Uniswap 代码构造
Uniswap 智能合约代码由两个 github 我的项目组成。一个是 core,一个是 periphery。
https://github.com/Uniswap/un…
https://github.com/Uniswap/un…
core 偏外围逻辑,LP 流动性挖矿零碎开发(对接 v→hkkf5566),LP 挖矿生态系统定制,单个 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’);