Uniswap V2 学习笔记2. 交易算法

  • tonyh
  • 更新于 2022-04-25 10:07
  • 阅读 9607

大家好, 今天继续分享 Uniswap V2 的学习心得, 今天的内容是 Uniswap 的交易算法

大家好, 今天继续分享 Uniswap V2 的学习心得, 今天的内容是 Uniswap的交易算法

* *Uniswap 核心思想 A B = K**

在不考虑手续费的情况下, 交易前后 两个代币数量乘积不变

假设一个池子里有两个币种 tokenA 和 tokenB, 数量分别为 A 和 B 现在用户用 ∆A 的 tokenA 能够换到多少 tokenB 呢? 答案是: ∆B = B ∆A / (A + ∆A) 这样交换后池子中 tokenA 的 余额为 A + ∆A, tokenB的余额为: B - B ∆A / (A + ∆A) 两者乘积后依然等于 A*B

这个设定的好处是, 在交换数量极小的情况下, 用户的成交价格近似等于两个币种的库存比值: ∆A / ∆B = (A+∆A) / B ≈ A / B, 这样可以比较真实的反应供求关系 而在成交量极大时, ∆B 永远不可能超过B, 因此池子永远不能被清空.

这个公式我们还可以理解为: 用户用 ∆A个 tokenA 换成 ∆B个 tokenB, 他们的比值 ∆A/∆B , 应该等于 市场上 tokenA 和 tokenB 的供应量之比

也许大家的第一直觉认为这是错误的, 下面我们举例说明

假设池子中有 10个 tokenA 和 10个 tokenB, 现在用户拿来 10个tokenA ,应该换到多少tokenB呢? 答案是5个 按照 A*B = K 的原则, 池子中 tokenA 翻了1倍, tokenB 应该变为原来的一半, 所以是5个没问题

有人会问, 但是池子中的 tokenA 和 tokenB 不是 1:1 吗? 如果兑换比例等于供应量之比应该是换出 10 个tokenB才对, 为什么只能换到5个呢?

注意, 我们说的 "市场上 tokenA 和 tokenB 的供应量之比" 不是池子中的数量之比, 而是池子中加上交易者手上的代币. 所以 A的总供给应该是 20个, tokenA 和 tokenB 总供给比例是 2:1 因为既然交易者拿了这 10个tokenA来换 ,说明这部分筹码是浮动筹码, 需要进入市场流通的, 理应算入总供给.

也许又有人要问了, 既然要算总供给, 那么还有其他的潜在浮动筹码, 怎么不一起计算进来呢? 因为你说的 "潜在浮动筹码" 那是长期供求关系, 我们现在讨论一笔交易之内的定价, 那就只计算在这一笔交易之内可见的总供给.

综上所述. 一旦把交易者手中的代币也算入供给量, 那么按照 A*B=K, 兑换比例其实就是和总供给比例完全相同.

下面我们学习一下这个交易算法的实现代码

这个算法在两个地方都有实现,

一个是 core/UniswapV2Pair.sol 的 swap函数:

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

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

    ...

    { // 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');
    }
    ...
}

swap 函数代码我节选了一部分, 有兴趣的同学可以参考: https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol 注意 Pair的 swap函数并不会执行 transfer操作, 它假定用户已经将输入币种 tranfer到本合约地址(实际是在Router完成) 同时用户应该自己计算出输入币种的数量应该换取多少输出币种amountOut(实际是在Router完成) ,注意参数中的 amount0Out 和 amount1Out有一个是0

开始的那两条 safeTransfer语句是不是很大方呢? 你要多少 out币种, 我就给你, 看起来是这样, 但是且慢.

最后的require语句就是验证 在扣除手续费后, 两个币种的余额的乘积不能小于交易前的乘积, 这就是 x*y = K 的实现代码. 所以如果你输入的金额如果不够满足条件, 那两条 tranfer 是要被回滚的.

如果用户往 Pair合约 tranfer了多余的代币怎么办? 很抱歉, 这条require只保证 x*y 不小于之前的值, 如果有剩余是不会退还的. 而实际在 Router中会准确计算出 amountOut, 不会存在输入币种过剩的情况.

x*y = K 的第二个实现在 periphery/libraries/UniswapV2Library.sol 的 getAmoutOut函数: (https://github.com/Uniswap/v2-periphery/blob/master/contracts/libraries/UniswapV2Library.sol)

// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
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;
}

这个函数的功能是, 给定 input币种的数量 amountIn, 计算出 output币种数量 amountOut

首先amountIn就会被扣除 0.3% 的手续费, 实际的有效输入就是 amountInWithFee = 0.997 amountIn 最后的输出金额 = reserveOut amountInWithFee / (reserveIn + amountInWithFee),

这和我们之前的 ∆B = B * ∆A / (A + ∆A) 保持一致. 注意在这里首先把分子分母都乘以1000, 因为 evm不支持浮点数.

流动性添加和移除

流动性添加和移除的算法比较简单

  • 添加流动性计算方法是, 用户发送 token0 , token1 到 pair, pair根据 (balance-reserve)/reserve 决定应该 mint 多少比例的 LP 给用户 (这里用户发送的代币数量就是通过 balance-reserve 计算得出), 用户得到的 LP = LP_supply * (balance-reserve)/reserve

    如果两个代币的充值的比值不同, 取较小的一个计算 mint 数量,确保平台不吃亏.

    如果是第一次添加流动性, 那么获得的 LP = sqrt(balance0 balance1) - 1000 但是系统的 LP supply = sqrt(balance0 balance1), 其中1000 wei 的 LP 被永久锁定 (确保资金池始终有库存).

    下面看看 UniswapV2Router02.sol 的添加流动性函数 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);
      }

    上面的 addLiquidity 是暴露给应用层的方法, 他会调用内部函数 _addLiquidity() 计算出 tokenA 和 tokenB的充值数量, 发送到交易对合约, 然后让交易对 mint lp 代币给用户指定的接收者. _addLiquidity() 实际并不执行任何操作, 它只执行计算功能: 计算 tokenA 和 tokenB 的数量:

    // **** ADD LIQUIDITY ****
     function _addLiquidity(
         address tokenA,
         address tokenB,
         uint amountADesired,
         uint amountBDesired,
         uint amountAMin,
         uint amountBMin
     ) internal virtual returns (uint amountA, uint amountB) {
         // create the pair if it doesn't exist yet
         if (IUniswapV2Factory(factory).getPair(tokenA, tokenB) == address(0)) {
             IUniswapV2Factory(factory).createPair(tokenA, tokenB);
         }
    
         (uint reserveA, uint reserveB) = UniswapV2Library.getReserves(factory, tokenA, tokenB);
         if (reserveA == 0 && reserveB == 0) {
             (amountA, amountB) = (amountADesired, amountBDesired);
         } else {
             uint amountBOptimal = UniswapV2Library.quote(amountADesired, reserveA, reserveB);
             if (amountBOptimal <= amountBDesired) {
                 require(amountBOptimal >= amountBMin, 'UniswapV2Router: INSUFFICIENT_B_AMOUNT');
                 (amountA, amountB) = (amountADesired, amountBOptimal);
             } else {
                 uint amountAOptimal = UniswapV2Library.quote(amountBDesired, reserveB, reserveA);
                 assert(amountAOptimal <= amountADesired);
                 require(amountAOptimal >= amountAMin, 'UniswapV2Router: INSUFFICIENT_A_AMOUNT');
                 (amountA, amountB) = (amountAOptimal, amountBDesired);
             }
         }
     }```

上面就是_addLiquidity() 计算两个币种需要发送到交易池的数量的具体逻辑, 实际上我们看到它调用的是 UniswapV2Library.quote(amountBDesired, reserveB, reserveA):

// given some amount of an asset and pair reserves, returns an equivalent amount of the other asset
        function quote(uint amountA, uint reserveA, uint reserveB) internal pure returns (uint amountB) {
            require(amountA > 0, 'UniswapV2Library: INSUFFICIENT_AMOUNT');
            require(reserveA > 0 && reserveB > 0, 'UniswapV2Library: INSUFFICIENT_LIQUIDITY');
            amountB = amountA.mul(reserveB) / reserveA;
        }

上面的代码中, 给定 amountA, 需要的 amountB = amountA * reserveB / reserveA, 所以添加流动性时, 两个币种添加的数量是等比例的数量. 而获得的 lp数量是多少呢? mint lp 的操作是在 addLiquidity() 中调用了 mint()完成:

liquidity = IUniswapV2Pair(pair).mint(to);

下面是 UniswapV2Pair.sol 中的 mint实现:

// this low-level function should be called from a contract which performs important safety checks
    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);
    }

上面这段代码逻辑是, 正常情况下 mint 的代币数 = sqrt( reserve0 * reserve1 ), 但是如果是第一次添加流动性(totalSupply=0的情况), 那么得到的代币有 1000 wei 的 lp 将被锁定, 所以对于第一个流动性添加者, 他会损失一部分代币. 如果如果他在首次添加流动性后立即撤销流动性, 他拿回的代币会有所损失. 系统这样设置是为了确保流动池里始终有流动性存在, 即使所有做市商都撤出, 交易池依然可以继续运作.

移除流动性的计算法, 按照 LP/LP_supply 分别发送 等比例的 token0 和 token1 给用户. 具体可以查看 UniswapV2Router02.sol 的 removeLiquidity() 和 UniswapV2Pair.sol的 burn() 函数. 下面是 burn 函数的方法:

// this low-level function should be called from a contract which performs important safety checks
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);
}

这里由于不涉及计算 tokenA 和 tokenA的比例, 所以不再调用 libraty 的 quote方法, 而是直接按照lp在supply中的比例分配 tokenA 和 tokenB:

amount0 = liquidity.mul(balance0) / _totalSupply; // using balances ensures pro-rata distribution
amount1 = liquidity.mul(balance1) / _totalSupply; // using balances ensures pro-rata distribution
  • 涉及ETH的流动性添加和移除:

添加流动性时 Router会将 ETH 换成 WETH, 再发送到 (WETH, token)交易对进行 mint 移除流动性时 Router会将(WETH, token) 的 LP token发送到 Pair, burn掉 LP 后得到的token直接发送给用户, 得到的 WETH 换成 ETH发送给用户. 下面我们看看 UniswapV2Router02.sol 的 addLiquidityETH()函数.

function addLiquidityETH(
    address token,
    uint amountTokenDesired,
    uint amountTokenMin,
    uint amountETHMin,
    address to,
    uint deadline
) external virtual override payable ensure(deadline) returns (uint amountToken, uint amountETH, uint liquidity) {

    (amountToken, amountETH) = _addLiquidity(
        token,
        WETH,
        amountTokenDesired,
        msg.value,
        amountTokenMin,
        amountETHMin
    );
    address pair = UniswapV2Library.pairFor(factory, token, WETH);
    TransferHelper.safeTransferFrom(token, msg.sender, pair, amountToken);
    IWETH(WETH).deposit{value: amountETH}();
    assert(IWETH(WETH).transfer(pair, amountETH));
    liquidity = IUniswapV2Pair(pair).mint(to);
    // refund dust eth, if any
    if (msg.value > amountETH) TransferHelper.safeTransferETH(msg.sender, msg.value - amountETH);
}

我们看到, 这个函数和 addLiquidity不同的是, 他不是调用两条 safeTransferFrom 发送 tokenA 和 tokenB 到交易池, 而是首先发送非 ETH代币, 然后使用用户发送来的 ETH 换成 WETH, 在调用 IWETH(WETH).transfer(pair, amountETH) 将 WETH 发送到交易池

另外还有涉及 permit的 removeLiquidityWithPermit, 我们在下一篇单独介绍.

Pair 合约中的 reserve 和 balance

pair合约中有两个重要变量:

uint112 private reserve0; uint112 private reserve1;

这两个变量记录了当前资金池中两个代币的交易后余额, 或者叫结清余额/库存.

在Pair中并不存在 balance0, balance1这两个变量, 但是我们可以在任何时候, 包括交易内部和外部, 使用 IERC20(token0或1).balanceOf(pair_address) 获取真实余额 Pair的真实balance 和 reserve 并不完全等同, 例如在添加流动性时, Router会将用户的代币转到 pair合约, 在交易结束之前, balance0 > reserve0, balance1 > reserve1, 而 Pair合约正是通过其差值, 得以计算出用户发送代币的数量, 从而mint对应的LP代币给用户. 在每一笔可能涉及余额变化的交易之后, 都会执行更新 reserve的操作, 使得 reserve = balance

因此,在交易之外, balance总是等于 reserve, 交易内部则有可能不同.

更新 reserve的操作写在 Pair 的 _update函数中

function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private {
    ...
    reserve0 = uint112(balance0);
    reserve1 = uint112(balance1);
    ...
}

(这个函数还更新了一些统计信息, 如价格积分, 最后更新的时间戳等)

swap 交易流程:

Router 的 swap 函数有很多个: swapExactTokensForTokens # 输入确定数量tokenIn, 输出待定tokenOut swapTokensForExactTokens # 输入待定tokenIn, 输出确定数量tokenOut swapExactETHForTokens # 输入固定数量ETH, 输出待定的目标币种 swapETHForExactTokens # 输入待定ETH, 输出固定数量目标币种 swapExactTokensForETH # 输入固定数量币种, 输出待定的ETH swapTokensForExactETH # 输入待定数量币种, 输出固定的ETH swapExactTokensForTokensSupportingFeeOnTransferTokens # 输入固定数量token0, 输出未知数量token1 swapExactETHForTokensSupportingFeeOnTransferTokens # 输入固定数量ETH, 输出未知数量token1 swapExactTokensForETHSupportingFeeOnTransferTokens # 输入固定数量token0, 输出未知数量ETH

以上所有函数都是支持路径的兑换函数. 调用者可以指定兑换路径 path[address], 从第一个币种开始逐个兑到最后一个币种.

所有这些函数的实现, 其实都是使用以下内部函数完成的: getAmountOut, getAmountIn, getAmountsOut, getAmountsIn, _swap, _swapSupportingFeeOnTransferTokens

上面的 getAmountOut 是对单个交易对, 给定输入币种数量, 求出应该换到多少的输出币种, 这个代码我们已经在前面贴了, 但是我们这里再贴出来看一次, 它实际上就是根据 A * B = K 的原则来计算兑换量:

// given an input amount of an asset and pair reserves, returns the maximum output amount of the other asset
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 扣掉手续费后, 按照乘积不变原则, 换成目标币种, 最后的 amountOut就等于: 有效输入 = amountIn 0.997 amountOut = 有效输入 reserveOut / (reserveIn + 有效输入), 这就是我们前面提到的兑换公式.

另外, getAmountsOut 计算的是多个交易对连续兑换后, 得到的最终输出币种数量:

// performs chained getAmountOut calculations on any number of pairs
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);
    }
}

这里的path是兑换路径, path[0]是输入币种, path[len-1] 是输出币种, 假设从第一个币种兑换到最后一个币种, 需要计算最后输出币种的数量, 计算逻辑就是 一次对每个 path[i], path[i+1] 交易对, 获取它的 reserve0, reserve1, 使用前面的 getAmountOut 计算出当前的输出, 然后将输出量交给下一个交易对计算, 直到最后一个交易对得出最终amountOut.

另外两个函数 getAmountIn 和 getAmountsIn 是给定输出币种数量, 求需要输入多少 input币种, 原理与 getAmountIn/getAmountsIn 类似, 这里就不展开了.

下面简单介绍 Router的 _swap 函数, 这个函数实际上会调用 Pair的 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)
        );
    }
}

这个函数调用时, 已经得到了路径上每个币种的输出量(实际是在公共函数,即前面的 9个 swapXXX()中调用 getAmountsOut或者 getAmountsIn 计算出来的), _swap 的逻辑很简单, 既然给定了每个币种的输出量, 那么就遍历路径上每个交易对, 依次执行交易对的 swap():

// this low-level function should be called from a contract which performs important safety checks
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);
}

交易对的 _swap 逻辑有点颠倒, 它一上来就把用户要求的 amountOut发送给用户, 直到最后他才检查用户是否充值足够的输入币种, 使用的逻辑依然是 A B = K , 注意 A 和 B 中的输入币种是首先被扣除了手续费, 所以执行swap后, 真实的 A B 应该大于之前的值.

如果 path 路径中存在 fee-on-transfer 的代币 (前面 9个 swapXXX 的后3个,) 则是通过调用 _swapSupportingFeeOnTransferTokens 实现兑换.

fee-on-transfer 是一些特殊的 ERC20, 在转账时会被收取手续费, 而手续费又是未知的, 因此无法使用 getAmountsOut(In) 预先计算路径上每个代币的换取数量, 所以 swap 的实现方法有所不同:

function _swapSupportingFeeOnTransferTokens(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);
        IUniswapV2Pair pair = IUniswapV2Pair(UniswapV2Library.pairFor(factory, input, output));
        uint amountInput;
        uint amountOutput;
        { // scope to avoid stack too deep errors
        (uint reserve0, uint reserve1,) = pair.getReserves();
        (uint reserveInput, uint reserveOutput) = input == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
        amountInput = IERC20(input).balanceOf(address(pair)).sub(reserveInput);
        amountOutput = UniswapV2Library.getAmountOut(amountInput, reserveInput, reserveOutput);
        }
        (uint amount0Out, uint amount1Out) = input == token0 ? (uint(0), amountOutput) : (amountOutput, uint(0));
        address to = i < path.length - 2 ? UniswapV2Library.pairFor(factory, output, path[i + 2]) : _to;
        pair.swap(amount0Out, amount1Out, to, new bytes(0));
    }
}

这个函数和前面的 _swap相比, 少了一个参数 amounts[], 因为无法确定.

正因为如此, 我们可以注意到一条规律, 所有带有 SupportingFeeOnTransferTokens 的 swap 函数, 都是 from Exact To Unkown, 没有 from Unkown To Exact 的情况

_swapSupportingFeeOnTransferTokens 具体逻辑是:

遍历路径上每一个pair: 根据 balance - reserve 计算输入, 然后 使用 getAmountOut 得出这个 pair 的输出数量 amountOut 然后调用 pair 的 swap 换出目标代币, 如果当前 pair是最后一个pair, 那么接收者就是函数指定的接收者, 否则接收者是下一个 pair

很多同学可能有疑问: 不是说 fee-on-transfer 代币的 amountOut无法计算吗, 为什么每个pair都能计算出amountOut呢?

注意这里的 getAmountOut是获取单个pair的 输出数量, 在执行 getAmountOut时, 当前pair已经收到代币, 转账手续费已经被扣掉了, 此时的 balance - reserve 就是 pair 已经确定收到的数量. 至于换成目标代币发送到下一个pair时, 是否会扣手续费, 那不重要, 因为下一个 pair 依然是按照自己收到的数量计算输出数量.

OK, 基本的 swap 原理搞清楚了, 那么前面9个 swap 的逻辑就很简单:

从 Exact 到 待定数量的 swap: 使用 getAmountsOut计算路径上每一个币种的out, 再调用 _swap 从 待定数量 到 Exact 的 swap: 使用 getAmountsIn计算路径上每一个币种的out, 再调用 _swap 这里我们就看看最简单的 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);
    }

对于涉及 ETH的 swap, 如果 ETH是输入币种, 那么首先调用 WETH.deposit 换成 WETH再调用 _swap 如果 ETH是输出币种, 那么调用 _swap 得到 WETH 后用 WETH.withdraw 换成 ETH 发送给用户:

function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline)
        external
        virtual
        override
        payable
        ensure(deadline)
        returns (uint[] memory amounts)
    {
        require(path[0] == WETH, 'UniswapV2Router: INVALID_PATH');
        amounts = UniswapV2Library.getAmountsOut(factory, msg.value, path);
        require(amounts[amounts.length - 1] >= amountOutMin, 'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT');
        IWETH(WETH).deposit{value: amounts[0]}();
        assert(IWETH(WETH).transfer(UniswapV2Library.pairFor(factory, path[0], path[1]), amounts[0]));
        _swap(amounts, path, to);
    }

如果 path 路径中存在 fee-on-transfer 的代币 (前面 9个 swapXXX 的后3个,) 则是通过调用 _swapSupportingFeeOnTransferTokens 实现兑换:

function swapExactTokensForTokensSupportingFeeOnTransferTokens(
        uint amountIn,
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external virtual override ensure(deadline) {
        TransferHelper.safeTransferFrom(
            path[0], msg.sender, UniswapV2Library.pairFor(factory, path[0], path[1]), amountIn
        );
        uint balanceBefore = IERC20(path[path.length - 1]).balanceOf(to);
        _swapSupportingFeeOnTransferTokens(path, to);
        require(
            IERC20(path[path.length - 1]).balanceOf(to).sub(balanceBefore) >= amountOutMin,
            'UniswapV2Router: INSUFFICIENT_OUTPUT_AMOUNT'
        );
    }

OK, 今天的内容基本介绍了 pair 和 router的交易算法, 我们下期再见.

点赞 5
收藏 8
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
tonyh
tonyh
https://github.com/star4evar