Uniswap计算过程推演

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其实是有一点点变化的,并不是严格保持一致。