当前位置:网站首页>Uniswap计算过程推演
Uniswap计算过程推演
2022-07-21 05:08:00 【biakia0610】
0、一些假设
假设创建了一个DAI/USDT的UniswapPair交易对,假设目前的DAI和USDT的价格是1:1,DAI的地址排序小于USDT的地址排序,其中有一个用户lpUser负责存入和提取交易对,另一个用户swapUser负责交易。接下来会模拟4次操作,分别是:
a)、lpUser存入2000/2000的DAI/USDT,获取流动性
b)、lpUser再次存入2000/2000的DAI/USDT,获取流动性
c)、swapUser执行一次交易:使用100DAI换取USDT
d)、lpUser提取一半流动性
1、初始状态
UniswapV2Pair的状态:
1、reverse0:这个是DAI的数量,初始为0
2、reverse1:这个是USDT的数量,初始为0
3、KLast:reverse0*reverse1,初始为0
4、totalSupply:UniswapV2Pair本身是ERC20标准token,代币的名称是UNI-V2(其实就是流动性),定义在UniswapV2ERC20中,totalSupply是总的供应量,初始为0
5、balanceOf:不同地址持有的UNI-V2(流动性)数量,是个mapping类型数据结构,key是地址,value是流动性数量。其中feeTo是手续费地址,address(this)是指UniswapV2Pair本身,lpUser是指流动性提供者账户地址。
DAI的状态:
1、balanceOf:不同地址持有DAI的数量,是个mapping类型数据结构,key是地址,value是DAI数量。其中保存了UniswapV2Pair的DAI数量目前为0、lpUser的DAI数量目前为10000、swapUser的DAI数量目前为10000。
USDT的状态:
1、balanceOf:不同地址持有USDT的数量,是个mapping类型数据结构,key是地址,value是USDT数量。其中保存了UniswapV2Pair的USDT数量目前为0、lpUser的USDT数量目前为10000、swapUser的DAI数量目前为0。
2、lpUser存入2000/2000的DAI/USDT,获取流动性
第一次添加流动性,走的是UniswapV2Router02的addLiquidity方法:
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);
}
可以看到代码先调用
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
这里的tokenA是DAI,因此把msg.sender(也就是lpUser)的2000个DAI转给了UniswapV2Pair
然后调用
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
这里的tokenA是USDT,因此把msg.sender(也就是lpUser)的2000个USDT转给了UniswapV2Pair
最后调用
liquidity = IUniswapV2Pair(pair).mint(to);
将计算流动性,然后分配给to地址,这个地址其实是lpUser的地址
接下来我们进入UniswapV2Pair的mint方法:
function mint(address to) external lock returns (uint liquidity) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
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; // 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);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
}
这里先拿到了UniswapV2Pair持有的DAI的数量,也就是前面转入的数量2000个,因此balance0=2000
然后拿到了UniswapV2Pair持有的USDT的数量,也就是前面转入的数量2000个,因此balance1=2000
第一次存入的时候reverse0和reverse1都是0,因此amount0和amount1都是2000。
调用_mintFee计算手续费:
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
address feeTo = IUniswapV2Factory(factory).feeTo();
feeOn = feeTo != address(0);
uint _kLast = kLast; // gas savings
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;
}
}
因为第一次kLast是0,所以不会计算手续费
然后计算流动性:
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);
}
require(liquidity > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_MINTED');
_mint(to, liquidity);
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Mint(msg.sender, amount0, amount1);
由于一开始totalSupply就是0,因此,计算的流动性是2000*2000的开根号然后减去最小流动性(由于USDT和DAI其实是带单位的,2000USDT在合约中其实是2000*10的18次方,而最小流动性定义为1000,和2000*10的18次方相比非常小),这里近似看成2000
然后把2000流动性分配给lpUser,然后调用_update更新reserve0为2000,更新reserve1为2000,然后更新kLast为2000*2000 = 4000000
至此第一次添加流动性完成。
3、lpUser再次存入2000/2000的DAI/USDT,获取流动性
过程和上一步差不多:
先把2000DAI从lpUser转到UniswapV2Pair
再把2000USDT从lpUser转到UniswapV2Pair
然后调用UniswapV2Pair的mint方法增发流动性,第一步是算手续费:
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
address feeTo = IUniswapV2Factory(factory).feeTo();
feeOn = feeTo != address(0);
uint _kLast = kLast; // gas savings
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;
}
}
这里kLast是4000000不是0,所以会先计算rootK,也就是Math.sqrt(2000*2000)= 2000,而rootKLast也是2000,所以rootK>rootKLast的条件通不过,手续费还是不需要计算。
接下来需要计算实际增发的流动性:
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);
}
走的是else分支,amount0是2000,_totalSupply是2000,_reserve0是2000,amount1是2000,_reserve1也是2000,所以实际算出来的流动性是2000
最后把2000流动性增发给用户,然后更新reserve0,reserve1和kLast的值。
4、swapUser执行一次交易,使用100DAI换取USDT
swap的操作使用的是UniswapV2Router02的swapExactTokensForTokens
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint[] memory amounts) {
amounts = UniswapV2Library.getAmountsOut(factory, amountIn, path);
require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
TransferHelper.safeTransferFrom(
path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]
);
_swap(amounts, path, to);
}
amountIn是100,代表swapUser付出的100DAI,amountOutMin是一个预计的值,防止输出过小导致明显的损失,path是一个交易路径,由于Uniswap可以通过中间币对进行链式交易,也就是说可以从DAI-->ETH-->USDT这样换USDT,所以它是个数组,address[i]表示交易链中第i个币的token地址,我们这里是DAI-->USDT,所以path只有两个值,to就是swapUser在USDT里的账户地址,deadline表示超时时间,如果交易执行时间超过了这个值,会进行回滚。
这里一上来先通过UniswapV2Library算出来当前执行交易获得的USDT数量(amounts的值),代码如下:
function getAmountsOut(address factory, uint amountIn, address[] memory path) internal view returns (uint[] memory amounts) {
require(path.length >= 2, 'UniswapV2Library: INVALID_PATH');
amounts = new uint[](path.length);
amounts[0] = amountIn;
for (uint i; i < path.length - 1; i++) {
(uint reserveIn, uint reserveOut) = getReserves(factory, path[i], path[i + 1]);
amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
}
}
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;
}
上面一个函数就是执行链式交易,这里我们关心下面那个函数,其中amountIn是100,所以amountInWithFee是99700,reserveOut和reserveIn都是4000(上面lpUser两次添加交易对),最终的amountOut=(99700*4000)/(4000*1000+99700) = 97.27,也就是当前交易对100DAI可以换取97.27的USDT,而按照当前DAI:USDT=1:1,应该获取100USDT啊,那么剩下的USDT哪去了呢?其实剩下的USDT留在了UniswapV2Pair在USDT的账户里,这就是交易所产生的手续费。随着交易不断进行,UniswapV2Pair在USDT的账户会产生越来越多的USDT,这些USDT就是DAI/USDT交易对的流动性提供者获取的收益。
拿到USDT的数量后,UniswapV2Router02会先将100DAI从swapUser的账户转到UniswapV2Pair的账户,然后调用_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)
);
}
}
这里input其实是token0,也就是DAI,所以amount0Out为0,amount1Out是97.27,to是swapUser在USDT合约的地址
然后执行UniswapV2Pair的swap函数
function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock {
require(amount0Out > 0 || amount1Out > 0, 'UniswapV2: INSUFFICIENT_OUTPUT_AMOUNT');
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
require(amount0Out < _reserve0 && amount1Out < _reserve1, 'UniswapV2: INSUFFICIENT_LIQUIDITY');
uint balance0;
uint balance1;
{ // scope for _token{0,1}, avoids stack too deep errors
address _token0 = token0;
address _token1 = token1;
require(to != _token0 && to != _token1, 'UniswapV2: INVALID_TO');
if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); // optimistically transfer tokens
if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); // optimistically transfer tokens
if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
}
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');
{ // scope for reserve{0,1}Adjusted, avoids stack too deep errors
uint balance0Adjusted = balance0.mul(1000).sub(amount0In.mul(3));
uint balance1Adjusted = balance1.mul(1000).sub(amount1In.mul(3));
require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K');
}
_update(balance0, balance1, _reserve0, _reserve1);
emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
}
这段代码首先把97.27的USDT转到了to地址,然后获取balance0=4100(UniswapV2Pair持有的DAI的数量),获取balance1=3902.73(UniswapV2Pair持有的USDT的数量)。
计算amount0In,由于_reserve0-amount0Out=4000-0,balance0>_reserve0-amount0Out,所以amount0In=4100-4000+0=100
计算amount1In,由于_reserve1-amount1Out=4000-97.27,balance1=_reserve1-amount1Out,所以amount1In = 0
计算balance0Adjusted = 4100*1000 - 100*3 = 4100000-300 = 4099700
计算balance1Adjusted=3902.73*1000-0 = 3902730
balance0Adjusted*balance1Adjusted = 16000022.181 * 1000* 1000 > _reserve0*_reserve1*1000*1000 = 16000000 * 1000 * 1000
最后更新_reserve0和_reserve1
5、lpUser提取一半流动性
调用是从UniswapV2Router02的removeLiquidity开始的:
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) {
address pair = UniswapV2Library.pairFor(factory, tokenA, tokenB);
IUniswapV2Pair(pair).transferFrom(msg.sender, pair, liquidity); // send liquidity to pair
(uint amount0, uint amount1) = IUniswapV2Pair(pair).burn(to);
(address token0,) = UniswapV2Library.sortTokens(tokenA, tokenB);
(amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
require(amountA >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
require(amountB >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
}
这里先获取DAI/USDT交易对UniswapV2Pair,然后把一半流动性(2000)转移到UniswapV2Pair,然后调用UniswapV2Pair的burn函数销毁流动性并取回DAI和USDT:
function burn(address to) external lock returns (uint amount0, uint amount1) {
(uint112 _reserve0, uint112 _reserve1,) = getReserves(); // gas savings
address _token0 = token0; // gas savings
address _token1 = token1; // gas savings
uint balance0 = IERC20(_token0).balanceOf(address(this));
uint balance1 = IERC20(_token1).balanceOf(address(this));
uint liquidity = balanceOf[address(this)];
bool feeOn = _mintFee(_reserve0, _reserve1);
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
}
首先获取UniswapV2Pair持有的DAI数量,balance0=4100
然后获取UniswapV2Pair持有的USDT数量,balance1=3902.73
然后获取lpUser转移过来的流动性,liquidity=2000
然后计算手续费:
function _mintFee(uint112 _reserve0, uint112 _reserve1) private returns (bool feeOn) {
address feeTo = IUniswapV2Factory(factory).feeTo();
feeOn = feeTo != address(0);
uint _kLast = kLast; // gas savings
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;
}
}
这里rootK = 4100*3902.73的开根号,也就是4000.149
由于上一次交易的时候_kLast是没有重算的,所以rootKLast = 4000
rootK其实是大于rootKLast的,所以需要计算手续费:
numerator = 4000*(4000.149-4000)=596
denominator=4000.149*5+4000 = 24000.745
liquidity = 596/24000.745 = 0.0248
然后将手续费交给feeTo地址
计算完手续费后,继续执行burn函数:
uint _totalSupply = totalSupply; // gas savings, must be defined here since totalSupply can update in _mintFee
amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
require(amount0 > 0 && amount1 > 0, 'UniswapV2: INSUFFICIENT_LIQUIDITY_BURNED');
_burn(address(this), liquidity);
_safeTransfer(_token0, to, amount0);
_safeTransfer(_token1, to, amount1);
balance0 = IERC20(_token0).balanceOf(address(this));
balance1 = IERC20(_token1).balanceOf(address(this));
_update(balance0, balance1, _reserve0, _reserve1);
if (feeOn) kLast = uint(reserve0).mul(reserve1); // reserve0 and reserve1 are up-to-date
emit Burn(msg.sender, amount0, amount1, to);
amount0 = 2000*4100/4000.0248 = 2049.987
amount1 = 2000*3902.73/4000.0248 = 1951.352
调用_burn函数销毁2000流动性
将2049.987个DAI和1951.352个USDT转给lpUser(如果DAI和USDT的价格都是1美元,那么用户实际获得的流动性收益为2049.987+1951.352-4000 = 1.339)
更新_reserve0和_reserve1,并重新计算kLast
6、总结
a)、添加和移除流动性,会去算手续费,发给相应地址,然后更新kLast、reserve0、reserve1
b)、交易的时候,会按比例留下一部分币,这些币就是流动性提供者获取的收益,交易只会更新reserve0和reserve1,不会更新kLast
c)、添加流动性-->交易--->提取流动性,整个操作过后,kLast其实是有一点点变化的,并不是严格保持一致。
边栏推荐
- Usage and purpose of as unknown as XXX in typescript
- 【内网渗透】内网不出网机器反弹shell以及CS上线
- 求助大神
- BUUCTF n1book [第二章 web進階]文件上傳
- php三行代码写测试接口
- ECSHOP vulnerability recurrence
- WAP green legend building (pure version)
- Attack and defense World Web Zone difficulty level: 3 (ics-05, MFW, easytornado)
- 【内网渗透】openssl反弹流量加密shell
- 【内网渗透】cobaltstrike流量加密
猜你喜欢
PHP (TP framework) uses Alibaba cloud OSS storage
【内网渗透】openssl反弹流量加密shell
[vulnerability recurrence] redis unauthorized access to windows+linux utilization method
【逆向分析】基础入门-简单控制台登录
BUUCTF-web-随便注
墨者学院-WebShell文件上传分析(第3-5题)
Thinkphp6 uses easywechat5 Development of official account of X (I)
BUUCTF(misc)
Rust简短笔记:Cargo指定依赖版本
Recurrence of the web question of the second online blade cup
随机推荐
TP5 import Excel to database
Realization of interface displacement by linear Schrodinger equation
[permission promotion] MSSQL authorization raising method
[reverse analysis] static analysis of malicious code
Openfoam tips
Introduction to PHP
【内网渗透】openssl反弹流量加密shell
Precision problems and solutions of JS decimal conversion percentage
Nodejs uses the post method to receive JS objects and write JSON files
Thinkphp6 uses easywechat5 Development of official account of X (I)
COMSOL heat conduction method to solve maze problem (path planning)
[intranet penetration] intranet penetration of vulnstack II
BUUCTF [SUCTF 2019]EasySQL
JS converts hexadecimal color to RGBA format
Recurrence of the web question of the second online blade cup
攻防世界web区 难度等级:3(ics-05,MFW,easytornado)
Personal applet: Dream fate
phpmyadmin后台文件包含漏洞分析
Buuctf web casual note
day1 poc与exp学习之pikachu字符注入