{"content":{"title":"剖析DeFi交易产品之UniswapV4：Swap","body":"文章首发于公众号：[Keegan小钢](https://mp.weixin.qq.com/s?__biz=MzA5OTI1NDE0Mw==&mid=2652494703&idx=1&sn=54b00ab8c2ad98d89a098e288f77bac9&chksm=8b68517fbc1fd86969d0e305e0064e3093f158b73b796bbd23795f874f3f1368cfa637e0cbe4&token=846491392&lang=zh_CN#rd)\r\n***\r\nSwap 可分为两种场景：**单池交易**和**跨池交易**。在 **PoolManager** 合约里，要完成交易流程，会涉及到 `lock()`、`swap()`、`settle()`、`take()` 四个函数。单池交易时只需要调一次 `swap()` 函数，而跨池交易时则需要多次调用 `swap()` 函数来完成。\r\n\r\n我们先来聊聊单池交易如何实现，以下是流程图：\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/12/FAbGZChU656e935b504ac.png)\r\n\r\n第一步，和其他操作一样，先执行 `lock()`，锁定住接下来的系列操作。\r\n\r\n第二步，就是在 `lockAcquired()` 回调函数里执行 `swap()` 函数。这一步执行完之后，记账系统中会记录用户欠池子的资产数量，即用户需要支付的代币；以及池子欠用户的资产数量，即用户此次交易可得的代币。\r\n\r\n第三步，执行 `settle()` 函数，完成代币的支付。\r\n\r\n第四步，执行 `take()` 函数，取回所得的代币。\r\n\r\n最后，`lock()` 函数完成，返回结果。\r\n\r\n而如果是跨池交易的话，则需要在 Router 层面确定好交易路径，然后根据路径执行多次 `swap`。举个例子，现在要用 A 兑换成 C，但是 A 和 C 之间没有直接配对的池子，但是有中间代币 B，存在 A 和 B 配对的池子，也存在 B 和 C 配对的池子。那交易路径就可以先用 A 换成 B，再将 B 换成 C，最终实现了 A 换成 C。而不管中间经过了多少次 `swap`，最后，只需要完成一次 `settle` 操作，即支付 A，也只需要执行一次 `take` 操作，即取回最后所得的 C。整个流程大致如下图所示：\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/12/pvLiq84P656e93704825e.png)\r\n\r\n下面，我们主要剖析讲解 `swap()` 函数的内部实现。\r\n\r\n首先，看看其函数声明，如下：\r\n\r\n```C++\r\nfunction swap(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData)\r\n  external\r\n  override\r\n  noDelegateCall\r\n  onlyByLocker\r\n  returns (BalanceDelta delta)\r\n```\r\n\r\n`key` 指定了要进行交易的池子，`params` 是具体的交易参数，`hookData` 即需要回传给 hooks 合约的数据。\r\n\r\n来看看 `params` 具体有哪些参数：\r\n\r\n```C++\r\nstruct SwapParams {\r\n    bool zeroForOne;\r\n    int256 amountSpecified;\r\n    uint160 sqrtPriceLimitX96;\r\n}\r\n```\r\n\r\n`zeroForOne` 指名了要用 `currency0` 兑换 `currency1`，为 `false` 的话则反过来用 `currency1` 兑换 `currency0`。`amountSpecified` 是指定的确定数额，正数表示输入，负数表示输出。`sqrtPriceLimitX96` 是滑点保护的限定价格。如果之前已经了解过 UniswapV3，那对这几个字段应该不陌生。\r\n\r\n两个函数修饰器 `noDelegateCall` 和 `onlyByLocker`，和之前文章介绍的一样，就不赘述了。\r\n\r\n返回值 `delta`，其组成里的两个数，正常情况下就是一个正数，一个负数。\r\n\r\n接下来，看看函数体了。先看前面一段代码：\r\n\r\n```C++\r\nPoolId id = key.toId();\r\n_checkPoolInitialized(id);\r\n\r\nif (key.hooks.shouldCallBeforeSwap()) {\r\n    bytes4 selector = key.hooks.beforeSwap(msg.sender, key, params, hookData);\r\n    // Sentinel return value used to signify that a NoOp occurred.\r\n    if (key.hooks.isValidNoOpCall(selector)) return BalanceDeltaLibrary.MAXIMUM_DELTA;\r\n    else if (selector != IHooks.beforeSwap.selector) revert Hooks.InvalidHookResponse();\r\n}\r\n```\r\n\r\n这部分逻辑很简单，前两行代码，检查池子是否已经初始化过了，未初始化的则 `revert`。之后是执行 hooks 合约的 `beforeSwap` 钩子函数。\r\n\r\n接下来这段代码是执行 `swap` 的内部函数：\r\n\r\n```C++\r\nuint256 feeForProtocol;\r\nuint256 feeForHook;\r\nuint24 swapFee;\r\nPool.SwapState memory state;\r\n(delta, feeForProtocol, feeForHook, swapFee, state) = pools[id].swap(\r\n    Pool.SwapParams({\r\n        tickSpacing: key.tickSpacing,\r\n        zeroForOne: params.zeroForOne,\r\n        amountSpecified: params.amountSpecified,\r\n        sqrtPriceLimitX96: params.sqrtPriceLimitX96\r\n    })\r\n);\r\n```\r\n\r\n这个内部函数的具体实现比较复杂，我们待会再讲，先继续讲完外部函数剩下的代码。\r\n\r\n接下来一行代码就是进行记账了：\r\n\r\n```C++\r\n_accountPoolBalanceDelta(key, delta);\r\n```\r\n\r\n之后是对协议费和 hook 费用的处理：\r\n\r\n```C++\r\nunchecked {\r\n    if (feeForProtocol > 0) {\r\n        protocolFeesAccrued[params.zeroForOne ? key.currency0 : key.currency1] += feeForProtocol;\r\n    }\r\n    if (feeForHook > 0) {\r\n        hookFeesAccrued[address(key.hooks)][params.zeroForOne ? key.currency0 : key.currency1] += feeForHook;\r\n    }\r\n}\r\n```\r\n\r\n接着执行 `afterSwap` 的钩子函数：\r\n\r\n```C++\r\nif (key.hooks.shouldCallAfterSwap()) {\r\n    if (key.hooks.afterSwap(msg.sender, key, params, delta, hookData) != IHooks.afterSwap.selector) {\r\n        revert Hooks.InvalidHookResponse();\r\n    }\r\n}\r\n```\r\n\r\n最后，发送事件：\r\n\r\n```C++\r\nemit Swap(\r\n    id, msg.sender, delta.amount0(), delta.amount1(), state.sqrtPriceX96, state.liquidity, state.tick, swapFee\r\n);\r\n```\r\n\r\n整个外部函数的逻辑还是比较清晰的。复杂的其实是内部函数的实现。下面就来看看 `swap` 内部函数的实现逻辑。还是先看函数声明：\r\n\r\n```C++\r\nfunction swap(State storage self, SwapParams memory params)\r\n  internal\r\n  returns (\r\n      BalanceDelta result,\r\n      uint256 feeForProtocol,\r\n      uint256 feeForHook,\r\n      uint24 swapFee,\r\n      SwapState memory state\r\n  )\r\n```\r\n\r\n`self` 是 `storage` 类型的，其实就是外部函数的 `pools[id]`。而第二个参数的 `SwapParams` 不同于外部函数的同名参数，这个内部函数的此参数具体如下：\r\n\r\n```C++\r\nstruct SwapParams {\r\n    int24 tickSpacing;\r\n    bool zeroForOne;\r\n    int256 amountSpecified;\r\n    uint160 sqrtPriceLimitX96;\r\n}\r\n```\r\n\r\n相比外部函数的此参数，多了 `tickSpacing`，其他参数则和外部函数的一样。\r\n\r\n返回值比较多。`result` 就是变动的净余额，`feeForProtocol` 是协议费，`feeForHook` 是 hook 费用，包括 hook 交易费用和提现费用，`swapFee` 就是池子本身的交易费，最后的 `state` 是最新的状态。\r\n\r\n接着，开始查看函数体的代码实现，先看前面一段：\r\n\r\n```C++\r\n// 指定价格不能为0\r\nif (params.amountSpecified == 0) revert SwapAmountCannotBeZero();\r\n// 读取出swap前的状态\r\nSlot0 memory slot0Start = self.slot0;\r\nswapFee = slot0Start.swapFee;\r\nif (params.zeroForOne) { // token0兑换token1\r\n  \t// 滑点价格的判断\r\n    if (params.sqrtPriceLimitX96 >= slot0Start.sqrtPriceX96) {\r\n        revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96, params.sqrtPriceLimitX96);\r\n    }\r\n    if (params.sqrtPriceLimitX96 <= TickMath.MIN_SQRT_RATIO) {\r\n        revert PriceLimitOutOfBounds(params.sqrtPriceLimitX96);\r\n    }\r\n} else { // token1兑换token0\r\n  \t// 滑点价格的判断\r\n    if (params.sqrtPriceLimitX96 <= slot0Start.sqrtPriceX96) {\r\n        revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96, params.sqrtPriceLimitX96);\r\n    }\r\n    if (params.sqrtPriceLimitX96 >= TickMath.MAX_SQRT_RATIO) {\r\n        revert PriceLimitOutOfBounds(params.sqrtPriceLimitX96);\r\n    }\r\n}\r\n```\r\n\r\n接下来是这段代码：\r\n\r\n```C++\r\n// 临时的缓存数据\r\nSwapCache memory cache = SwapCache({\r\n    liquidityStart: self.liquidity,\r\n    protocolFee: params.zeroForOne\r\n        ? (getSwapFee(slot0Start.protocolFees) % 64)\r\n        : (getSwapFee(slot0Start.protocolFees) >> 6),\r\n    hookFee: params.zeroForOne ? (getSwapFee(slot0Start.hookFees) % 64) : (getSwapFee(slot0Start.hookFees) >> 6)\r\n});\r\n// 是否为确定的输入\r\nbool exactInput = params.amountSpecified > 0;\r\n// 初始化返回值的state\r\nstate = SwapState({\r\n    amountSpecifiedRemaining: params.amountSpecified,\r\n    amountCalculated: 0,\r\n    sqrtPriceX96: slot0Start.sqrtPriceX96,\r\n    tick: slot0Start.tick,\r\n    feeGrowthGlobalX128: params.zeroForOne ? self.feeGrowthGlobal0X128 : self.feeGrowthGlobal1X128,\r\n    liquidity: cache.liquidityStart\r\n});\r\n```\r\n\r\n`cache` 是一个临时状态的缓存数据，包括三个字段：\r\n\r\n* `liquidityStart`：流动性\r\n* `protocolFee`：协议费用\r\n* `hookFee`：hook 费用\r\n\r\n`amountSpecified` 大于 0 则说明是指定的输入，即 `exactInput` 为 `true`。\r\n\r\n初始化返回值 `state` 也都是用当前状态的值进行初始化。这里前两个字段需要介绍一下，即 `amountSpecifiedRemaining` 和 `amountCalculated`。第一个字段表示当前还有多少指定的金额未进行交易计算的，第二个字段表示已经交易计算累加的数额。为了理解这两个字段，我们举个例子来说明。假设用户指定的是输出的数额，假设为 1000，那 `amountSpecifiedRemaining` 初始值即为 1000。但是，当前有效的流动性剩余量并不足 1000，假设只剩下 400，所以在当前 tick 下的计算只能用到 400，假设计算所得的输入数额为 200，那么，次轮计算后，`amountSpecifiedRemaining` 剩下 1000 - 400 = 600，而 `amountCalculated` 变为 200。之后，tick 会移动到下一个有流动性的区间内。剩下的 600 继续计算所得，假设这时的流动性剩余已经超过 600 了，这 600 计算所得的输入值为 250，那计算完后的 `amountSpecifiedRemaining` 就变成了 0，而 `amountCalculated` 则为 200 + 250 = 450，计算结束。这就是这两个字段的作用。\r\n\r\n之后的代码会做循环判断，就是上面所说的计算逻辑：\r\n\r\n```C++\r\nStepComputations memory step;\r\n// continue swapping as long as we haven't used the entire input/output and haven't reached the price limit\r\nwhile (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != params.sqrtPriceLimitX96) {\r\n\t...\r\n}\r\n```\r\n\r\n`while` 条件里除了判断 `amountSpecifiedRemaining` 不为 0 之外，还判断了最新价格不能等于滑点价格。如果等于滑点价格了，也会结束循环。\r\n\r\n`step` 用来存储 `while` 循环里每一步的计算用到的临时变量，具体包含以下字段：\r\n\r\n```C++\r\nstruct StepComputations {\r\n    // the price at the beginning of the step\r\n    uint160 sqrtPriceStartX96;\r\n    // the next tick to swap to from the current tick in the swap direction\r\n    int24 tickNext;\r\n    // whether tickNext is initialized or not\r\n    bool initialized;\r\n    // sqrt(price) for the next tick (1/0)\r\n    uint160 sqrtPriceNextX96;\r\n    // how much is being swapped in in this step\r\n    uint256 amountIn;\r\n    // how much is being swapped out\r\n    uint256 amountOut;\r\n    // how much fee is being paid in\r\n    uint256 feeAmount;\r\n}\r\n```\r\n\r\n接着，来看看 `while` 循环里面的逻辑，先来看前面一段代码：\r\n\r\n```C++\r\n// 初始化当前这一步的价格\r\nstep.sqrtPriceStartX96 = state.sqrtPriceX96;\r\n// 获取出下一个tick\r\n(step.tickNext, step.initialized) =\r\n    self.tickBitmap.nextInitializedTickWithinOneWord(state.tick, params.tickSpacing, params.zeroForOne);\r\n// 确保下一个tick不会超出边界\r\nif (step.tickNext < TickMath.MIN_TICK) {\r\n    step.tickNext = TickMath.MIN_TICK;\r\n} else if (step.tickNext > TickMath.MAX_TICK) {\r\n    step.tickNext = TickMath.MAX_TICK;\r\n}\r\n// 计算出下一个tick对应的根号价格\r\nstep.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);\r\n```\r\n\r\n之后，执行当前这步的具体计算：\r\n\r\n```C++\r\n// compute values to swap to the target tick, price limit, or point where input/output amount is exhausted\r\n(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(\r\n    state.sqrtPriceX96,\r\n    (\r\n        params.zeroForOne\r\n            ? step.sqrtPriceNextX96 < params.sqrtPriceLimitX96\r\n            : step.sqrtPriceNextX96 > params.sqrtPriceLimitX96\r\n    ) ? params.sqrtPriceLimitX96 : step.sqrtPriceNextX96,\r\n    state.liquidity,\r\n    state.amountSpecifiedRemaining,\r\n    swapFee\r\n);\r\n```\r\n\r\n计算返回四个值，`sqrtPriceX96` 为计算后的最新价格，`amountIn` 为输入的数额，`amountOut` 为输出的金额，`feeAmount` 为需要支付的手续费。\r\n\r\n继续看下一段代码：\r\n\r\n```C++\r\nif (exactInput) { //指定输入时\r\n    unchecked {\r\n      \t//remaining减去输入额和手续费\r\n        state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();\r\n    }\r\n  \t//calculated加上输出额，因为amountOut为负数，所以用减法\r\n    state.amountCalculated = state.amountCalculated - step.amountOut.toInt256();\r\n} else { //指定输出时\r\n    unchecked {\r\n      \t//remaining减去输出额，因为amountOut为负数，所以用加法\r\n        state.amountSpecifiedRemaining += step.amountOut.toInt256();\r\n    }\r\n  \t//calculated加上输入额和手续费\r\n    state.amountCalculated = state.amountCalculated + (step.amountIn + step.feeAmount).toInt256();\r\n}\r\n```\r\n\r\n之后的一段代码则是计算几个费用了：\r\n\r\n```C++\r\n// 协议费用\r\nif (cache.protocolFee > 0) {\r\n    // A: calculate the amount of the fee that should go to the protocol\r\n    uint256 delta = step.feeAmount / cache.protocolFee;\r\n    // A: subtract it from the regular fee and add it to the protocol fee\r\n    unchecked {\r\n        step.feeAmount -= delta;\r\n        feeForProtocol += delta;\r\n    }\r\n}\r\n// hook费用\r\nif (cache.hookFee > 0) {\r\n    // step.feeAmount has already been updated to account for the protocol fee\r\n    uint256 delta = step.feeAmount / cache.hookFee;\r\n    unchecked {\r\n        step.feeAmount -= delta;\r\n        feeForHook += delta;\r\n    }\r\n}\r\n// 更新全局费用跟踪器\r\nif (state.liquidity > 0) {\r\n    unchecked {\r\n        state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);\r\n    }\r\n}\r\n```\r\n\r\n`while` 循环体里的最后一段代码则如下：\r\n\r\n```C++\r\n// 如果计算后的新价格到达下一个tick价格就移动tick\r\nif (state.sqrtPriceX96 == step.sqrtPriceNextX96) {\r\n    // 如果tick已经初始化，则执行移动tick\r\n    if (step.initialized) {\r\n        int128 liquidityNet = Pool.crossTick(\r\n            self,\r\n            step.tickNext,\r\n            (params.zeroForOne ? state.feeGrowthGlobalX128 : self.feeGrowthGlobal0X128),\r\n            (params.zeroForOne ? self.feeGrowthGlobal1X128 : state.feeGrowthGlobalX128)\r\n        );\r\n        // 如果向左移动，把liquidityNet理解为相反的符号\r\n        unchecked {\r\n            if (params.zeroForOne) liquidityNet = -liquidityNet;\r\n        }\r\n\t\t\t\t// 更新流动性\r\n        state.liquidity = liquidityNet < 0\r\n            ? state.liquidity - uint128(-liquidityNet)\r\n            : state.liquidity + uint128(liquidityNet);\r\n    }\r\n\t\t// 更新tick\r\n    unchecked {\r\n        state.tick = params.zeroForOne ? step.tickNext - 1 : step.tickNext;\r\n    }\r\n} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {\r\n    // 重新计算，除非我们处于较低的刻度边界(即已经转换过刻度)，并且没有移动\r\n    state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);\r\n}\r\n```\r\n\r\n整个 `while` 循环跑完之后，一般来说，可能会存在两种情况。第一种，指定的金额全部完成兑换，即 `amountSpecifiedRemaining` 没有剩余。第二种，兑换到一半，触发到了滑点保护价格，那 `amountSpecifiedRemaining` 将会有剩余，只有部分成交。\r\n\r\n那么，循环结束之后，整个内部的 `swap` 函数就只剩下最后的一部分代码了，如下：\r\n\r\n```C++\r\n// 将临时状态的价格和tick转为storage状态\r\n(self.slot0.sqrtPriceX96, self.slot0.tick) = (state.sqrtPriceX96, state.tick);\r\n\r\n// 更新storage状态的流动性\r\nif (cache.liquidityStart != state.liquidity) self.liquidity = state.liquidity;\r\n\r\n// 更新全局的手续费跟踪器\r\nif (params.zeroForOne) {\r\n    self.feeGrowthGlobal0X128 = state.feeGrowthGlobalX128;\r\n} else {\r\n    self.feeGrowthGlobal1X128 = state.feeGrowthGlobalX128;\r\n}\r\n// 净余额变动值赋值给返回值result\r\nunchecked {\r\n    if (params.zeroForOne == exactInput) {\r\n        result = toBalanceDelta(\r\n            (params.amountSpecified - state.amountSpecifiedRemaining).toInt128(),\r\n            state.amountCalculated.toInt128()\r\n        );\r\n    } else {\r\n        result = toBalanceDelta(\r\n            state.amountCalculated.toInt128(),\r\n            (params.amountSpecified - state.amountSpecifiedRemaining).toInt128()\r\n        );\r\n    }\r\n}\r\n```\r\n\r\n至此，就完成了 `swap` 的全部代码逻辑讲解了。"},"author":{"user":"https://learnblockchain.cn/people/96","address":null},"history":null,"timestamp":1701745583,"version":1}