{"content":{"title":"剖析DeFi交易产品之UniswapV3：Pool合约","body":"UniswapV3Pool 合约则复杂很多了，其引用的库合约就达到了 13 个，通过 using 方式使用的也达到了 9 个，如下所示：\r\n\r\n```C++\r\nusing LowGasSafeMath for uint256;\r\nusing LowGasSafeMath for int256;\r\nusing SafeCast for uint256;\r\nusing SafeCast for int256;\r\nusing Tick for mapping(int24 => Tick.Info);\r\nusing TickBitmap for mapping(int16 => uint256);\r\nusing Position for mapping(bytes32 => Position.Info);\r\nusing Position for Position.Info;\r\nusing Oracle for Oracle.Observation[65535];\r\n```\r\n\r\n`LowGasSafeMath` 是用于加减乘除算法计算的，`SafeCast` 用于类型转换，`Tick` 和 `TickBitmap` 用于管理 tick 处理相关的操作和计算，`Position` 则主要用于更新流动性的头寸，`Oracle` 则是用于预言机计算的。\r\n\r\n接着，来看看定义了哪些状态变量：\r\n\r\n```C++\r\naddress public immutable override factory;\r\naddress public immutable override token0;\r\naddress public immutable override token1;\r\nuint24 public immutable override fee;\r\nint24 public immutable override tickSpacing;\r\nuint128 public immutable override maxLiquidityPerTick;\r\n\r\nstruct Slot0 {\r\n    // the current price\r\n    uint160 sqrtPriceX96;\r\n    // the current tick\r\n    int24 tick;\r\n    // the most-recently updated index of the observations array\r\n    uint16 observationIndex;\r\n    // the current maximum number of observations that are being stored\r\n    uint16 observationCardinality;\r\n    // the next maximum number of observations to store, triggered in observations.write\r\n    uint16 observationCardinalityNext;\r\n    // the current protocol fee as a percentage of the swap fee taken on withdrawal\r\n    // represented as an integer denominator (1/x)%\r\n    uint8 feeProtocol;\r\n    // whether the pool is locked\r\n    bool unlocked;\r\n}\r\nSlot0 public override slot0;\r\n\r\nuint256 public override feeGrowthGlobal0X128;\r\nuint256 public override feeGrowthGlobal1X128;\r\n\r\n// accumulated protocol fees in token0/token1 units\r\nstruct ProtocolFees {\r\n    uint128 token0;\r\n    uint128 token1;\r\n}\r\nProtocolFees public override protocolFees;\r\n\r\nuint128 public override liquidity;\r\n\r\nmapping(int24 => Tick.Info) public override ticks;\r\nmapping(int16 => uint256) public override tickBitmap;\r\nmapping(bytes32 => Position.Info) public override positions;\r\nOracle.Observation[65535] public override observations;\r\n```\r\n\r\n前 5 个变量我们都已经了解过了，第 6 个变量 `maxLiquidityPerTick` 表示每个 tick 能接受的最大流动性，是在构造函数中根据 tickSpacing 计算出来的。\r\n\r\n`slot0` 记录了当前的一些状态值，都封装在了结构体 `Slot0` 中，其共有 7 个字段。`sqrtPriceX96` 是当前价格，记录的是根号价格，且做了扩展，准确来说：`sqrtPriceX96 = (token1数量 / token0数量) ^ 0.5 * 2^96`。换句话说，这个值代表的是 token0 和 token1 数量比例的平方根，经过放大以获得更高的精度。这样设计的目的是为了方便和优化合约中的一些计算。如果想从 `sqrtPriceX96` 得出具体的价格，还需要做一些额外的计算。`tick` 记录了当前价格对应的价格点。`observationIndex`、`observationCardinality` 和 `observationCardinalityNext` 是跟 `observations` 数组有关的，也是计算预言机价格时需要的，这在之前的文章《[价格预言机的使用总结（三）：UniswapV3篇](https://mp.weixin.qq.com/s?__biz=MzA5OTI1NDE0Mw==&mid=2652494455&idx=1&sn=79b855a19261a9647fc83a1b9504b2ff&chksm=8b685067bc1fd971f356b0b10f534200733413f37fdec8876618501b1009711b8d262914d6b9&token=2071110718&lang=zh_CN#rd)》讲解 UniswapV3 预言机时已经介绍过，这里不再赘述。`feeProtocol` 则用来存储协议费率，初始化时为 0，可通过 `setFeeProtocol` 函数来重置该值。`unlocked` 记录池子的锁定状态，初始化时为 true，主要作为一个防止重入锁来使用。\r\n\r\n`feeGrowthGlobal0X128` 和 `feeGrowthGlobal1X128` 记录两个 token 的每单位流动性所获取的手续费。\r\n\r\n`protocolFees` 则记录了两个 token 的累计未被领取的协议手续费。\r\n\r\n`liquidity` 记录了池子当前可用的流动性。注意，这里不是指注入池子里的所有流动性总量，而是包含了当前价格的那些有效头寸的流动性总量。\r\n\r\n`ticks` 记录池子里每个 tick 的详细信息，key 为 tick 的序号，value 就是详细信息。`tickBitmap` 记录已初始化的 tick 的位图。如果一个 tick 没有被用作流动性区间的边界点，即该 tick 没有被初始化，那在交易过程中可以跳过这个 tick。而为了更高效地寻找下一个已初始化的 tick，就使用了 tickBitmap 来记录已初始化的 tick。如果 tick 已被初始化，位图中对应于该 tick 序号的位置设置为 1，否则为 0。\r\n\r\n`positions` 记录每个流动性头寸的详细信息，具体信息如下：\r\n\r\n```C++\r\nlibrary Position {\r\n    // 用于存储每个用户的头寸信息\r\n    struct Info {\r\n        // 当前头寸的总流动性\r\n        uint128 liquidity;\r\n        // 截止最后一次更新流动性或所欠费用时，每单位流动性的费用增长\r\n        uint256 feeGrowthInside0LastX128;\r\n        uint256 feeGrowthInside1LastX128;\r\n        // 欠头寸所有者的费用\r\n        uint128 tokensOwed0;\r\n        uint128 tokensOwed1;\r\n    }\r\n    ...\r\n}\r\n```\r\n\r\n`observations` 则是存储了计算预言机价格相关的累加值，包括 tick 累加值和流动性累加值。具体用法在《[价格预言机的使用总结（三）：UniswapV3篇](https://mp.weixin.qq.com/s?__biz=MzA5OTI1NDE0Mw==&mid=2652494455&idx=1&sn=79b855a19261a9647fc83a1b9504b2ff&chksm=8b685067bc1fd971f356b0b10f534200733413f37fdec8876618501b1009711b8d262914d6b9&token=2071110718&lang=zh_CN#rd)》一文中已经介绍过，这里也不再赘述。\r\n\r\n接下来就到合约函数了，**UniswapV3Pool** 核心的函数在 **IUniswapV3PoolActions** 接口里有定义，该接口共定义了 7 个函数：\r\n\r\n* `initialize`：初始化 slot0 状态\r\n* `mint`：添加流动性\r\n* `collect`：提取收益\r\n* `burn`：移除流动性\r\n* `swap`：兑换\r\n* `flash`：闪电贷\r\n* `increaseObservationCardinalityNext`：扩展 `observations` 数组可存储的容量\r\n\r\n`initialize` 通常会在第一次添加流动性时被调用，主要会初始化 slot0 状态变量，其中 sqrtPriceX96 是直接作为入参传入的，因为第一次添加流动性时，价格其实是由 LP 自己定的。初始的 tick 则是根据 sqrtPriceX96 计算出来的。而最后一个函数`increaseObservationCardinalityNext` 是用于预言机的，因为默认的 `observations` 数组实际存储的容量只是 1，需要扩展这个容量才可计算预言机价格。\r\n\r\n### mint 函数\r\n\r\n`mint` 是添加流动性的底层函数，以下是其代码实现：\r\n\r\n```C++\r\nfunction mint(\r\n    address recipient,\r\n    int24 tickLower,\r\n    int24 tickUpper,\r\n    uint128 amount,\r\n    bytes calldata data\r\n) external override lock returns (uint256 amount0, uint256 amount1) {\r\n    require(amount > 0);\r\n    (, int256 amount0Int, int256 amount1Int) =\r\n        _modifyPosition(\r\n            ModifyPositionParams({\r\n                owner: recipient,\r\n                tickLower: tickLower,\r\n                tickUpper: tickUpper,\r\n                liquidityDelta: int256(amount).toInt128()\r\n            })\r\n        );\r\n\r\n    amount0 = uint256(amount0Int);\r\n    amount1 = uint256(amount1Int);\r\n\r\n    uint256 balance0Before;\r\n    uint256 balance1Before;\r\n    if (amount0 > 0) balance0Before = balance0();\r\n    if (amount1 > 0) balance1Before = balance1();\r\n    IUniswapV3MintCallback(msg.sender).uniswapV3MintCallback(amount0, amount1, data);\r\n    if (amount0 > 0) require(balance0Before.add(amount0) <= balance0(), 'M0');\r\n    if (amount1 > 0) require(balance1Before.add(amount1) <= balance1(), 'M1');\r\n\r\n    emit Mint(msg.sender, recipient, tickLower, tickUpper, amount, amount0, amount1);\r\n}\r\n```\r\n\r\n其有 5 个入参：\r\n\r\n* `recipient`：流动性的接收者地址\r\n* `tickLower`：区间价格下限的 tick 序号\r\n* `tickUpper`：区间价格上限的 tick 序号\r\n* `amount`：待添加的流动性数量\r\n* `data`：传给回调函数的数据\r\n\r\n其中，tick 的上下限和 amount 其实都是通过前端 SDK 根据用户的输入计算好对应的值，通常是通过流动性管理的入口合约 **NonfungiblePositionManager** 合约下传进来的。关于 NonfungiblePositionManager 合约的实现后面文章再详解。\r\n\r\n添加流动性的主要操作其实是在 `_modifyPosition` 私有函数里，执行完该函数后，返回值包括了需要添加到池子里的两种 token 的具体数额 `amount0` 和 `amount1`。之后，查询并临时记录下两种 token 在池子里的当前余额。然后，调用 `msg.sender` 的回调函数 `uniswapV3MintCallback`，在回调函数中需要完成两种 token 的支付。`msg.sender` 一般是 **NonfungiblePositionManager** 合约，所以 NonfungiblePositionManager 合约会实现该回调函数来完成支付。执行完回调函数之后，那池子里两种 token 的余额就会发生变化，判断其前后余额即可。\r\n\r\n`_modifyPosition` 封装了主要的处理逻辑，其代码如下：\r\n\r\n```C++\r\nfunction _modifyPosition(ModifyPositionParams memory params)\r\n    private\r\n    noDelegateCall\r\n    returns (\r\n        Position.Info storage position,\r\n        int256 amount0,\r\n        int256 amount1\r\n    )\r\n{\r\n    // 检查Tick的上下限是否符合边界条件\r\n    checkTicks(params.tickLower, params.tickUpper);\r\n    // 从storage位置转存到内存中，后续访问可节省gas\r\n    Slot0 memory _slot0 = slot0;\r\n    // 第一步核心操作\r\n    position = _updatePosition(\r\n        params.owner,\r\n        params.tickLower,\r\n        params.tickUpper,\r\n        params.liquidityDelta,\r\n        _slot0.tick\r\n    );\r\n\r\n    if (params.liquidityDelta != 0) {\r\n        if (_slot0.tick < params.tickLower) {\r\n            // 当前报价低于传递的范围；流动性只能通过从左到右交叉而进入范围内，需要提供更多token0\r\n            amount0 = SqrtPriceMath.getAmount0Delta(\r\n                TickMath.getSqrtRatioAtTick(params.tickLower),\r\n                TickMath.getSqrtRatioAtTick(params.tickUpper),\r\n                params.liquidityDelta\r\n            );\r\n        } else if (_slot0.tick < params.tickUpper) {\r\n            // 当前报价在传递的范围内\r\n            uint128 liquidityBefore = liquidity;\r\n\r\n            // 更新预言机相关状态数据\r\n            (slot0.observationIndex, slot0.observationCardinality) = observations.write(\r\n                _slot0.observationIndex,\r\n                _blockTimestamp(),\r\n                _slot0.tick,\r\n                liquidityBefore,\r\n                _slot0.observationCardinality,\r\n                _slot0.observationCardinalityNext\r\n            );\r\n            // 计算当前价格到价格区间上限之间需支付的amount0\r\n            amount0 = SqrtPriceMath.getAmount0Delta(\r\n                _slot0.sqrtPriceX96,\r\n                TickMath.getSqrtRatioAtTick(params.tickUpper),\r\n                params.liquidityDelta\r\n            );\r\n            // 计算从价格区间下限到当前价格之间需支付的amount1\r\n            amount1 = SqrtPriceMath.getAmount1Delta(\r\n                TickMath.getSqrtRatioAtTick(params.tickLower),\r\n                _slot0.sqrtPriceX96,\r\n                params.liquidityDelta\r\n            );\r\n            // 当前有效头寸的总流动性增加\r\n            liquidity = LiquidityMath.addDelta(liquidityBefore, params.liquidityDelta);\r\n        } else {\r\n            // 当前报价高于传递的范围；流动性只能通过从右到左交叉而进入范围内，需要提供更多token1\r\n            amount1 = SqrtPriceMath.getAmount1Delta(\r\n                TickMath.getSqrtRatioAtTick(params.tickLower),\r\n                TickMath.getSqrtRatioAtTick(params.tickUpper),\r\n                params.liquidityDelta\r\n            );\r\n        }\r\n    }\r\n}\r\n```\r\n\r\n其中，第一步的核心操作是调用 `_updatePosition` 函数，先更新头寸。之后的核心操作是计算此次调整头寸流动性时对应的 amount0 和 amount1，这需要根据三种不同情况分别计算：\r\n\r\n* 当前 tick 小于头寸的 tick 区间下限时，则只需要更多 token0，所以也只需要计算 amount0\r\n* 当前 tick 大于头寸的 tick 区间上限时，则只需要更多 token1，所以也只需要计算 amount1\r\n* 当前 tick 处于头寸的 tick 区间内时，分别计算 amount0 和 amount1，且池子里处于激活状态的总流动性也跟着调整\r\n\r\n前两种状态，添加的流动性都是没有激活的，所以不需要把添加的流动性追加到当前的 liquidity 里。\r\n\r\n下面，再来看看私有函数 `_updatePosition` 的代码实现逻辑，如下所示：\r\n\r\n```C++\r\nfunction _updatePosition(\r\n    address owner,\r\n    int24 tickLower,\r\n    int24 tickUpper,\r\n    int128 liquidityDelta,\r\n    int24 tick\r\n) private returns (Position.Info storage position) {\r\n    // 获取用户的流动性头寸\r\n    position = positions.get(owner, tickLower, tickUpper);\r\n\r\n    uint256 _feeGrowthGlobal0X128 = feeGrowthGlobal0X128; // SLOAD for gas optimization\r\n    uint256 _feeGrowthGlobal1X128 = feeGrowthGlobal1X128; // SLOAD for gas optimization\r\n\r\n    // 是否需要将tick从初始化翻转为未初始化，或者反之亦然\r\n    bool flippedLower;\r\n    bool flippedUpper;\r\n    if (liquidityDelta != 0) {\r\n        uint32 time = _blockTimestamp();\r\n        // 预言机相关数据\r\n        (int56 tickCumulative, uint160 secondsPerLiquidityCumulativeX128) =\r\n            observations.observeSingle(\r\n                time,\r\n                0,\r\n                slot0.tick,\r\n                slot0.observationIndex,\r\n                liquidity,\r\n                slot0.observationCardinality\r\n            );\r\n        // 更新tickLower的数据\r\n        flippedLower = ticks.update(\r\n            tickLower,\r\n            tick,\r\n            liquidityDelta,\r\n            _feeGrowthGlobal0X128,\r\n            _feeGrowthGlobal1X128,\r\n            secondsPerLiquidityCumulativeX128,\r\n            tickCumulative,\r\n            time,\r\n            false,\r\n            maxLiquidityPerTick\r\n        );\r\n        // 更新tickUpper的数据\r\n        flippedUpper = ticks.update(\r\n            tickUpper,\r\n            tick,\r\n            liquidityDelta,\r\n            _feeGrowthGlobal0X128,\r\n            _feeGrowthGlobal1X128,\r\n            secondsPerLiquidityCumulativeX128,\r\n            tickCumulative,\r\n            time,\r\n            true,\r\n            maxLiquidityPerTick\r\n        );\r\n        if (flippedLower) {\r\n            // 在tick位图中翻转lower tick的状态\r\n            tickBitmap.flipTick(tickLower, tickSpacing);\r\n        }\r\n        if (flippedUpper) {\r\n            // 在tick位图中翻转upper tick的状态\r\n            tickBitmap.flipTick(tickUpper, tickSpacing);\r\n        }\r\n    }\r\n    // 计算增长的手续费\r\n    (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) =\r\n        ticks.getFeeGrowthInside(tickLower, tickUpper, tick, _feeGrowthGlobal0X128, _feeGrowthGlobal1X128);\r\n    // 更新头寸元数据\r\n    position.update(liquidityDelta, feeGrowthInside0X128, feeGrowthInside1X128);\r\n    // 清理不再需要用到的tick数据\r\n    if (liquidityDelta < 0) {\r\n        if (flippedLower) {\r\n            ticks.clear(tickLower);\r\n        }\r\n        if (flippedUpper) {\r\n            ticks.clear(tickUpper);\r\n        }\r\n    }\r\n}\r\n```\r\n\r\n我们看到有五个入参，其中，`owner`、`tickLower`、`tickUpper` 这三个组合起来的哈希值其实就是状态变量 `positions` 的 key。实际上，key 的计算是通过 keccak256 算法所得的：\r\n\r\n````C++\r\nkeccak256(abi.encodePacked(owner, tickLower, tickUpper))\r\n````\r\n\r\n实现代码的第一行，就是通过这三个参数得到 **Position.Info** 类型的 `position` 变量，从而得到待更新的头寸数据。另外，owner 其实是 **NonfungiblePositionManager** 合约。其实，对于底层 Pool 合约来说，所有的头寸 owner 都是 **NonfungiblePositionManager** 合约，而每个用户的头寸则是在 **NonfungiblePositionManager** 合约里进行区分管理的。\r\n\r\n入参中的 **liquidityDelta** 是需要增加或减少的流动性，该值为正数则表示要增加流动性，负数则是要减少流动性。\r\n\r\n入参的 `tick` 是当前激活的 `tick`，即 `slot0` 中保存的 `tick`。\r\n\r\n该内部函数的核心操作逻辑是：先分别更新 tick 的下限和上限的元数据；如果 tick 的流动性从 0 增长为非 0 状态，或从非 0 状态减少成了为 0 的状态，则需要在 tick 位图中执行翻转操作；接着更新头寸元数据，包括流动性的加减和手续费的计算；最后将已经不再需要用到的 tick 数据给清理掉。\r\n\r\n至此，池子底层添加流动性的 `mint` 函数全流程就讲解完了。\r\n\r\n### burn 函数\r\n\r\n接下来看看做移除流动性操作的 `burn` 函数，其实现逻辑相对简单很多，以下是其代码实现：\r\n\r\n```C++\r\nfunction burn(\r\n    int24 tickLower,\r\n    int24 tickUpper,\r\n    uint128 amount\r\n) external override lock returns (uint256 amount0, uint256 amount1) {\r\n    (Position.Info storage position, int256 amount0Int, int256 amount1Int) =\r\n        _modifyPosition(\r\n            ModifyPositionParams({\r\n                owner: msg.sender,\r\n                tickLower: tickLower,\r\n                tickUpper: tickUpper,\r\n                liquidityDelta: -int256(amount).toInt128() // 移除流动性需转为负数\r\n            })\r\n        );\r\n    // 将负数转为正数\r\n    amount0 = uint256(-amount0Int);\r\n    amount1 = uint256(-amount1Int);\r\n\r\n    if (amount0 > 0 || amount1 > 0) {\r\n        (position.tokensOwed0, position.tokensOwed1) = (\r\n            position.tokensOwed0 + uint128(amount0),\r\n            position.tokensOwed1 + uint128(amount1)\r\n        );\r\n    }\r\n\r\n    emit Burn(msg.sender, tickLower, tickUpper, amount, amount0, amount1);\r\n}\r\n```\r\n\r\n该函数移除的是 `msg.sender` 的流动性头寸。其有三个入参，`tickLower` 和 `tickUpper` 用来指定要移动哪个头寸，`amount` 指定要移除的流动性数额。\r\n\r\n和 `mint` 的时候一样，第一步核心操作也是先 `_modifyPosition`。不过，因为是减少流动性，所以传入的 `liquidityDelta` 参数转为负数。而返回的 `amount0Int` 和 `amount1Int` 也会是负数，所以转为 uint256 类型的 `amount0` 和 `amount1` 时，又需要加上负号将负数再转为正数。之后，将 `amount0` 和 `amount1` 分别累加到了头寸的 `tokensOwed0` 和 `tokensOwed1`。\r\n\r\n这时候可能有人会产生疑问，既然是移除流动性，为什么没有转账逻辑？不是应该把 `amount0` 和 `amount1` 转回给用户吗？其实，这也是和 UniswapV2 移除流动性时不同的地方了。UniswapV3 的处理方式并不是移除流动性时直接把两种 token 资产转给用户，而是先累加到 `tokensOwed0` 和 `tokensOwed1`，代表这是欠用户的资产，其中也包括该头寸已赚取到的手续费。之后，用户其实是要通过 `collect` 函数来提取 `tokensOwed0` 和 `tokensOwed1` 里的资产。\r\n\r\n### collect 函数\r\n\r\n`collect` 函数其实很简单，以下是其代码实现：\r\n\r\n```C++\r\nfunction collect(\r\n    address recipient,\r\n    int24 tickLower,\r\n    int24 tickUpper,\r\n    uint128 amount0Requested,\r\n    uint128 amount1Requested\r\n) external override lock returns (uint128 amount0, uint128 amount1) {\r\n    // we don't need to checkTicks here, because invalid positions will never have non-zero tokensOwed{0,1}\r\n    Position.Info storage position = positions.get(msg.sender, tickLower, tickUpper);\r\n\r\n    amount0 = amount0Requested > position.tokensOwed0 ? position.tokensOwed0 : amount0Requested;\r\n    amount1 = amount1Requested > position.tokensOwed1 ? position.tokensOwed1 : amount1Requested;\r\n\r\n    if (amount0 > 0) {\r\n        position.tokensOwed0 -= amount0;\r\n        TransferHelper.safeTransfer(token0, recipient, amount0);\r\n    }\r\n    if (amount1 > 0) {\r\n        position.tokensOwed1 -= amount1;\r\n        TransferHelper.safeTransfer(token1, recipient, amount1);\r\n    }\r\n\r\n    emit Collect(msg.sender, recipient, tickLower, tickUpper, amount0, amount1);\r\n}\r\n```\r\n\r\n5 个入参很好理解，`recipient` 就是接收 token 的地址，`tickLower` 和 `tickUpper `指定了头寸区间，`amount0Requested` 和 `amount1Requested` 是用户希望提取的数额。返回值 `amount0` 和 `amount1` 就是实际提取的数额。\r\n\r\n实现逻辑的第一行，通过 `msg.sender`、`tickLower`、`tickUpper` 来读取出用户的头寸。接着判断用户希望提取的数额 `amount0Requested` 和头寸里的 `tokensOwed0` 哪个值小就实际提取哪个，`amount1` 的也同样。之后就是从头寸的 `tokensOwed` 里减掉提取的数额并转账给接收地址。最后发送 `Collect` 事件。\r\n\r\n### swap 函数\r\n\r\nswap 函数是实现交易的底层函数，其代码逻辑复杂很多，我们对其进行逐步拆解来看。\r\n\r\n首先，其入参有 5 个：\r\n\r\n```C++\r\nfunction swap(\r\n    // 收款地址\r\n    address recipient,\r\n    // 交易方向，true表示用token0交换token1，false则相反\r\n    bool zeroForOne,\r\n    // 指定的交易数额，如果是正数则为指定的输入，负数则为指定的输出\r\n    int256 amountSpecified,\r\n    // 限定的价格\r\n    uint160 sqrtPriceLimitX96,\r\n    // 传给回调函数的参数\r\n    bytes calldata data\r\n) external override noDelegateCall returns (int256 amount0, int256 amount1)\r\n```\r\n\r\n其中，如果 `zeroForOne` 为 `true` 的话，那交易后的价格不能小于 `sqrtPriceLimitX96`；如果 `zeroForOne` 为 `false`，则交易后的价格不能大于 `sqrtPriceLimitX96`。返回值 `amount0` 和 `amount1` 是交易后两个 token 的实际成交数额。\r\n\r\n下面我们只摘取一些重要代码添加注解进行说明，以下是执行实际交易前的一些准备工作：\r\n\r\n```C++\r\n// 将状态变量保存在内存中，后续访问通过 MLOAD 完成，可以节省 gas\r\nSlot0 memory slot0Start = slot0;\r\n// 防止重入\r\nslot0.unlocked = false;\r\n// 缓存交易前的数据，以节省 gas\r\nSwapCache memory cache =\r\n    SwapCache({\r\n        liquidityStart: liquidity,\r\n        blockTimestamp: _blockTimestamp(),\r\n        feeProtocol: zeroForOne ? (slot0Start.feeProtocol % 16) : (slot0Start.feeProtocol >> 4),\r\n        secondsPerLiquidityCumulativeX128: 0,\r\n        tickCumulative: 0,\r\n        computedLatestObservation: false\r\n    });\r\n// 如果 amountSpecified 为正数，则指定的是确定的输入数额\r\nbool exactInput = amountSpecified > 0;\r\n// 缓存交易过程中需要用到的临时变量\r\nSwapState memory state =\r\n    SwapState({\r\n        // 剩余可交易金额\r\n        amountSpecifiedRemaining: amountSpecified,\r\n        // 已交易互换的金额，指与 amountSpecifiedRemaining 互换的 token\r\n        amountCalculated: 0,\r\n        sqrtPriceX96: slot0Start.sqrtPriceX96,\r\n        tick: slot0Start.tick,\r\n        feeGrowthGlobalX128: zeroForOne ? feeGrowthGlobal0X128 : feeGrowthGlobal1X128,\r\n        protocolFee: 0,\r\n        liquidity: cache.liquidityStart\r\n    });\r\n```\r\n\r\n之后在一个 while 循环中处理实际的交易逻辑：\r\n\r\n```C++\r\n// 当剩余可交易金额为零，或交易后价格达到了限定的价格之后才退出循环\r\nwhile (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != sqrtPriceLimitX96) {\r\n    // 缓存每一次循环的状态变量\r\n    StepComputations memory step;\r\n    // 交易的起始价格\r\n    step.sqrtPriceStartX96 = state.sqrtPriceX96;\r\n    // 通过 tick 位图找到下一个已初始化的 tick，即下一个流动性边界点\r\n    (step.tickNext, step.initialized) = tickBitmap.nextInitializedTickWithinOneWord(\r\n        state.tick,\r\n        tickSpacing,\r\n        zeroForOne\r\n    );\r\n    ...\r\n    // 将上一步找到的下一个 tick 转为根号价格\r\n    step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);\r\n    // 在当前价格和下一口价格之间计算交易结果，返回最新价格、消耗的 amountIn、输出的 amountOut 和手续费 feeAmount\r\n    (state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(\r\n        state.sqrtPriceX96,\r\n        (zeroForOne ? step.sqrtPriceNextX96 < sqrtPriceLimitX96 : step.sqrtPriceNextX96 > sqrtPriceLimitX96)\r\n            ? sqrtPriceLimitX96\r\n            : step.sqrtPriceNextX96,\r\n        state.liquidity,\r\n        state.amountSpecifiedRemaining,\r\n        fee\r\n    );\r\n    \r\n    if (exactInput) {\r\n        // 此时的剩余可交易金额为正数，需减去消耗的输入 amountIn 和手续费 feeAmount\r\n        state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();\r\n        // 此时该值表示 tokenOut 的累加值，结果为负数\r\n        state.amountCalculated = state.amountCalculated.sub(step.amountOut.toInt256());\r\n    } else {\r\n        // 此时的剩余可交易金额为负数，需加上输出的 amountOut\r\n        state.amountSpecifiedRemaining += step.amountOut.toInt256();\r\n        // 此时该值表示 tokenIn 的累加值，结果为正数\r\n        state.amountCalculated = state.amountCalculated.add((step.amountIn + step.feeAmount).toInt256());\r\n    }\r\n    ...\r\n    // 如果达到了下一个价格，则需要移动 tick\r\n    if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {\r\n        // 如果 tick 已经初始化，则需要执行 tick 的转换\r\n        if (step.initialized) {\r\n            ...\r\n            // 转换到下一个 tick\r\n            int128 liquidityNet =\r\n                ticks.cross(\r\n                    step.tickNext,\r\n                    (zeroForOne ? state.feeGrowthGlobalX128 : feeGrowthGlobal0X128),\r\n                    (zeroForOne ? feeGrowthGlobal1X128 : state.feeGrowthGlobalX128),\r\n                    cache.secondsPerLiquidityCumulativeX128,\r\n                    cache.tickCumulative,\r\n                    cache.blockTimestamp\r\n                );\r\n            // 根据交易方向增加/减少相应的流动性\r\n            if (zeroForOne) liquidityNet = -liquidityNet;\r\n            // 更新流动性\r\n            state.liquidity = LiquidityMath.addDelta(state.liquidity, liquidityNet);\r\n        }\r\n        // 更新 tick\r\n        state.tick = zeroForOne ? step.tickNext - 1 : step.tickNext;\r\n    } else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {\r\n        // 如果不需要移动 tick，则根据最新价格换算成最新的 tick\r\n        state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);\r\n    }\r\n}\r\n```\r\n\r\n一笔交易有时候会跨越多个流动性区间，所以需要使用循环处理在每一个区间内的交易。当剩余可交易金额已经消耗完，或价格已经达到了指定的限定价格后，循环也就结束了，即交易主流程结束了。\r\n\r\n之后就是一些交易收尾的工作了，包括更新 tick、价格、流动性、手续费增长系数等。最后很关键的一步就是做转账和支付，以下是最后的代码：\r\n\r\n```C++\r\n// do the transfers and collect payment\r\nif (zeroForOne) {\r\n    if (amount1 < 0) TransferHelper.safeTransfer(token1, recipient, uint256(-amount1));\r\n\r\n    uint256 balance0Before = balance0();\r\n    IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);\r\n    require(balance0Before.add(uint256(amount0)) <= balance0(), 'IIA');\r\n} else {\r\n    if (amount0 < 0) TransferHelper.safeTransfer(token0, recipient, uint256(-amount0));\r\n\r\n    uint256 balance1Before = balance1();\r\n    IUniswapV3SwapCallback(msg.sender).uniswapV3SwapCallback(amount0, amount1, data);\r\n    require(balance1Before.add(uint256(amount1)) <= balance1(), 'IIA');\r\n}\r\n\r\n// 发送 Swap 事件\r\nemit Swap(msg.sender, recipient, amount0, amount1, state.sqrtPriceX96, state.tick);\r\n// 解除防止重入的锁\r\nslot0.unlocked = true;\r\n```\r\n\r\n先将 tokenOut 转给了用户，然后执行了回调函数 `uniswapV3SwapCallback`，在回调函数里会完成 tokenIn 的支付，执行完回调函数后的余额校验是为了确保回调函数确实完成了 tokenIn 的支付。因为先将 tokenOut 转给了用户，之后才完成支付，因此在回调函数中其实还可以做和 UniswapV2 一样的 flash swap。\r\n\r\n### flash 函数\r\n\r\nflash 函数实现了闪电贷功能，与 flash swap 不同，闪电贷借什么就需要还什么。另外，UniswapV3 的闪电贷可以两种 token 都借。\r\n\r\nflash 函数的代码实现相对比较简单，以下是其代码实现：\r\n\r\n```C++\r\nfunction flash(\r\n    address recipient,\r\n    uint256 amount0,\r\n    uint256 amount1,\r\n    bytes calldata data\r\n) external override lock noDelegateCall {\r\n    uint128 _liquidity = liquidity;\r\n    require(_liquidity > 0, 'L');\r\n    // 计算借贷的手续费\r\n    uint256 fee0 = FullMath.mulDivRoundingUp(amount0, fee, 1e6);\r\n    uint256 fee1 = FullMath.mulDivRoundingUp(amount1, fee, 1e6);\r\n    // 记录还款前的余额\r\n    uint256 balance0Before = balance0();\r\n    uint256 balance1Before = balance1();\r\n    // 将所借 token 转给用户\r\n    if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0);\r\n    if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1);\r\n    // 调用回调函数，在该函数里需要完成还款，包括还所借 token 和支付手续费\r\n    IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data);\r\n    // 读取还款后的余额\r\n    uint256 balance0After = balance0();\r\n    uint256 balance1After = balance1();\r\n    // 还款后的余额不能小于还款前的余额加上手续费\r\n    require(balance0Before.add(fee0) <= balance0After, 'F0');\r\n    require(balance1Before.add(fee1) <= balance1After, 'F1');\r\n    // 计算出实际收到的手续费\r\n    uint256 paid0 = balance0After - balance0Before;\r\n    uint256 paid1 = balance1After - balance1Before;\r\n    // 手续费分配\r\n    if (paid0 > 0) {\r\n        uint8 feeProtocol0 = slot0.feeProtocol % 16;\r\n        uint256 fees0 = feeProtocol0 == 0 ? 0 : paid0 / feeProtocol0;\r\n        if (uint128(fees0) > 0) protocolFees.token0 += uint128(fees0);\r\n        feeGrowthGlobal0X128 += FullMath.mulDiv(paid0 - fees0, FixedPoint128.Q128, _liquidity);\r\n    }\r\n    if (paid1 > 0) {\r\n        uint8 feeProtocol1 = slot0.feeProtocol >> 4;\r\n        uint256 fees1 = feeProtocol1 == 0 ? 0 : paid1 / feeProtocol1;\r\n        if (uint128(fees1) > 0) protocolFees.token1 += uint128(fees1);\r\n        feeGrowthGlobal1X128 += FullMath.mulDiv(paid1 - fees1, FixedPoint128.Q128, _liquidity);\r\n    }\r\n\r\n    emit Flash(msg.sender, recipient, amount0, amount1, paid0, paid1);\r\n}\r\n```\r\n\r\n入参有 4 个，`recipient` 是接收所贷 token 的地址，`amount0` 和 `amount1` 是所要借贷的两个 token 数量，`data` 是给回调函数的参数。\r\n\r\n还款则需在 `uniswapV3FlashCallback` 回调函数中完成。\r\n\r\n最终，闪电贷赚取的手续费也是分配给 LP 和协议费。"},"author":{"user":"https://learnblockchain.cn/people/96","address":null},"history":null,"timestamp":1699338490,"version":1}