## 概述 在之前的课程中,我们已经介绍了 [Uniswap v2](https://hackmd.io/@wongssh/uniswap-v2) 和 [Uniswap v3](https://hackmd.io/@wongssh/uniswap-v3-whitepaper) 的代码。在本文中,我们将介绍 Uniswap v4 的内容,与之前的文章类似,本文依旧是基于 [Uniswap v4 Core 白皮书](https://app.uniswap.org/whitepaper-v4.pdf) 作为基础框架编写的。我们假定读者对 Uniswap v3 内的区间流动性下的 Swap 是熟悉的。由于在 Uniswap V4 内,对区间流动性和 Swap 这些核心环节并没有进行大幅度修改,所以本文不会再次详细介绍这些内容。 白皮书在最初介绍时,就给出了 Uniswap v4 内的核心特性: 1. Hooks: 允许 Pool 在部署时指定一个合约用于 swap 的生命周期的不同阶段进行调用,Hook 合约可以实现一些特殊逻辑以影响 swap 的过程 2. Singleton: 在 Uniswap v2 和 v3 内,每次部署新的 Pool 都会调用工厂合约部署一个新的 Pool 合约,但在 Uniswap v4 中,我们部署新的 Pool 不再是部署一个新合约,而只是修改几个状态变量,这降低了部署 Pool 以及进行多跳交易的成本 3. Flash accounting: 用户可以 `unlock` 后任意进行交易,只要在 `unlock` 结束时保证自己和协议之前互相没有欠款即可,该机制也是简化了对 Native ETH 的支持,并且可以配置 ERC-6909 使用 笔者此前编写过 [现代 DeFi: Uniswap V4](https://blog.wssh.dev/posts/uniswap-v4/) 博客,此前这篇博客是在一边阅读源代码一边编写的,而本文是笔者在完全理解 Uniswap v4 后编写的,理论上本文可能更容易阅读,但是有可能会忽视部分细节。 ## Singleton 我们优先介绍单体架构,该架构允许用户不通过创建 Pool 合约的方式创建新的 Pool。单体架构的本质是 `PoolManager` 内的如下状态变量: ```solidity mapping(PoolId id => Pool.State) internal _pools; ``` 此处的 `PoolId` 替代了之前使用 `create2` 利用 token 0 和 token 1 计算 salt 来确定性部署合约地址的实现。`PoolId` 的计算方法如下: ```solidity /// @notice Returns the key for identifying a pool struct PoolKey { /// @notice The lower currency of the pool, sorted numerically Currency currency0; /// @notice The higher currency of the pool, sorted numerically Currency currency1; /// @notice The pool LP fee, capped at 1_000_000. If the highest bit is 1, the pool has a dynamic fee and must be exactly equal to 0x800000 uint24 fee; /// @notice Ticks that involve positions must be a multiple of tick spacing int24 tickSpacing; /// @notice The hooks of the pool IHooks hooks; } library PoolIdLibrary { /// @notice Returns value equal to keccak256(abi.encode(poolKey)) function toId(PoolKey memory poolKey) internal pure returns (PoolId poolId) { assembly ("memory-safe") { // 0xa0 represents the total size of the poolKey struct (5 slots of 32 bytes) poolId := keccak256(poolKey, 0xa0) } } } ``` 计算方法实际上就是 `PoolKey` 哈希后的结果,此处使用了内联汇编进行哈希计算,这是因为 solidity 的 keccak256 实现过于繁琐,目前 foundry lint 工具建议所有的 keccak256 哈希计算都使用内联汇编完成。`PoolKey` 是 Pool 的核心配置,我们在初始化一个 Pool 时也需要传入 `PoolKey` 作为参数。有趣的是,`PoolKey` 内的内容并不会被存储到 storage 内部,Uniswap V4 内所有与 Pool 交互的函数都需要传入 `PoolKey` 结构体作为参数来指定需要与哪一个 Pool 进行交互。这样设计的原因是因为 calldata 是廉价而 storage 的读和写都是昂贵的,计算下来直接传入 `PoolKey` 更加划算。但需要注意,由于链上还是以 PoolKey 作为 key 在 `_pools` 状态变量存储了 `Pool.State`,所以假如用户任意伪造 `PoolKey` 进行调用会触发 `PoolNotInitialized` 的报错。 > 这种只使用 `PoolKey` 的哈希结果 `PoolId` 进行链上存储实际上等效于只存入状态根,然后用户提供状态根对应数据的思路,其实与 Merkle Tree 是类似的。Panoptic 曾更加激进的使用了该策略,在 Panoptic v1 版本内,用户的所有头寸都会被压缩为一个哈希值,当用户希望操作某一个头寸时,用户需要给出头寸的配置和证明头寸存在的证明。但 Panoptic 使用了错误的哈希算法导致可以伪造头寸,最终导致 Panoptic v1 被弃用,具体可以阅读 [Position Spoofing Post Mortem](https://panoptic.xyz/blog/position-spoofing-post-mortem) 与 v3 不一致的是,在 v4 中,初始化 Pool 时不会检查 `fee` 与 `tickSpacing` 的匹配情况: ```solidity function initialize(PoolKey memory key, uint160 sqrtPriceX96) external noDelegateCall returns (int24 tick) { // see TickBitmap.sol for overflow conditions that can arise from tick spacing being too large if (key.tickSpacing > MAX_TICK_SPACING) TickSpacingTooLarge.selector.revertWith(key.tickSpacing); if (key.tickSpacing < MIN_TICK_SPACING) TickSpacingTooSmall.selector.revertWith(key.tickSpacing); if (key.currency0 >= key.currency1) { CurrenciesOutOfOrderOrEqual.selector.revertWith( Currency.unwrap(key.currency0), Currency.unwrap(key.currency1) ); } if (!key.hooks.isValidHookAddress(key.fee)) Hooks.HookAddressNotValid.selector.revertWith(address(key.hooks)); uint24 lpFee = key.fee.getInitialLPFee(); key.hooks.beforeInitialize(key, sqrtPriceX96); PoolId id = key.toId(); tick = _pools[id].initialize(sqrtPriceX96, lpFee); // event is emitted before the afterInitialize call to ensure events are always emitted in order // emit all details of a pool key. poolkeys are not saved in storage and must always be provided by the caller // the key's fee may be a static fee or a sentinel to denote a dynamic fee. emit Initialize(id, key.currency0, key.currency1, key.fee, key.tickSpacing, key.hooks, sqrtPriceX96, tick); key.hooks.afterInitialize(key, sqrtPriceX96, tick); } ``` 我们可以看到 `initialize` 函数只进行了一些简单的检查,此处的 `isValidHookAddress` 是一个较为核心的检查,用于判断某一个 Hook 地址是否满足权限配置要求,我们会在后文介绍 Hook 时详细介绍。此处我们只给出 `isValidHookAddress` 的部分代码: ```solidity // If there is no hook contract set, then fee cannot be dynamic // If a hook contract is set, it must have at least 1 flag set, or have a dynamic fee return address(self) == address(0) ? !fee.isDynamicFee() : (uint160(address(self)) & ALL_HOOK_MASK > 0 || fee.isDynamicFee()); ``` 这段代码实际上与即将介绍的 DynamicFee 机制有关。正如注释,这段代码的含义假如没有设置 Hook,那么就不允许 Pool 使用 DynamicFee 机制。假如设置了 Hook,Hook 满足一些权限配置(我们会在后文介绍,简单来说,Hook 合约地址的最后 14bit 都有特殊含义,代表 Hook 是否启用了某些权限)或者设置了 DynamicFee 机制。而 DynamicFee 机制是如何运作的,我们可以通过 `getInitialLPFee` 了解: ```solidity function isDynamicFee(uint24 self) internal pure returns (bool) { return self == DYNAMIC_FEE_FLAG; } function isValid(uint24 self) internal pure returns (bool) { return self <= MAX_LP_FEE; } function validate(uint24 self) internal pure { if (!self.isValid()) LPFeeTooLarge.selector.revertWith(self); } function getInitialLPFee(uint24 self) internal pure returns (uint24) { // the initial fee for a dynamic fee pool is 0 if (self.isDynamicFee()) return 0; self.validate(); return self; } ``` 正如 `PoolKey` 结构体内的注释,当 `fee` 被设置为 `0x800000` 时,此时 `fee` 会被视为动态的。那么动态费率该如何被更新? 在 `PoolManager` 内存在函数 `updateDynamicLPFee`,代码如下: ```solidity /// @inheritdoc IPoolManager function updateDynamicLPFee(PoolKey memory key, uint24 newDynamicLPFee) external { if (!key.fee.isDynamicFee() || msg.sender != address(key.hooks)) { UnauthorizedDynamicLPFeeUpdate.selector.revertWith(); } newDynamicLPFee.validate(); PoolId id = key.toId(); _pools[id].setLPFee(newDynamicLPFee); } ``` 简单来说,Hook 合约可以通过调用 `updateDynamicLPFee` 来实现更新动态费用的方法,这也是为什么上文介绍的 `isValidHookAddress` 会检查在启用 DynamicLPFee 情况下,是否设置了 Hook 。实际上,除了上述方法外,我们还可以通过 Hook 的 `beforeSwap` 调整费率,我们会在后文介绍 Hook 时对此进行详细介绍。 继续回到 `initialize` 函数,我们可以看到 `tick = _pools[id].initialize(sqrtPriceX96, lpFee);` 语句,该代码实际上调用了 `src/libraries/Pool.sol` 内的 `initialize` 函数,这是一个简单函数: ```solidity function initialize(State storage self, uint160 sqrtPriceX96, uint24 lpFee) internal returns (int24 tick) { if (self.slot0.sqrtPriceX96() != 0) PoolAlreadyInitialized.selector.revertWith(); tick = TickMath.getTickAtSqrtPrice(sqrtPriceX96); // the initial protocolFee is 0 so doesn't need to be set self.slot0 = Slot0.wrap(bytes32(0)).setSqrtPriceX96(sqrtPriceX96).setTick(tick).setLpFee(lpFee); } ``` 正如前文所述,`PoolManager` 不会保存 `PoolKey` 内的数据,但是 `Pool.State` 仍要保存一些数据,比如此处的 `Slot0`。`Slot0` 是一个自定义类型,具体定义位于 `src/types/Slot0.sol` 内部,Slot0 的布局如下: ``` 24 bits empty | 24 bits lpFee | 12 bits protocolFee 1->0 | 12 bits protocolFee 0->1 | 24 bits tick | 160 bits sqrtPriceX96 ``` 在 `Slot0.sol` 文件内部,编写了大量的内联汇编进行数据读取和写入。`Pool` 内部的完整的状态结构如下: ```solidity struct State { Slot0 slot0; uint256 feeGrowthGlobal0X128; uint256 feeGrowthGlobal1X128; uint128 liquidity; mapping(int24 tick => TickInfo) ticks; mapping(int16 wordPos => uint256) tickBitmap; mapping(bytes32 positionKey => Position.State) positions; } ``` 这基本上是对 Uniswap v3 内的 Pool 状态的复刻,其中 `feeGrowthGlobal0X128` / `feeGrowthGlobal1X128` 用于存储手续费有关的数据,而 `ticks` 和 `tickBitmap` 用于存储 tick 相关数据,而 `positions` 则是用于存储区间流动性有关的数据。 从更高的视角看,`src/libraries/Pool.sol` 可以被视为一个完整的 Uniswap v3 Pool 的实现,只是我们现在将其作为 `library` 存在,所有的状态变量都被存储在 `PoolManager` 内部。这就是单体架构的真实样貌,所以假如读者希望构建单体架构的 DeFi 协议,那么第一步就是先将逻辑代码和状态存储分离,状态存储位于单体合约内部,而逻辑代码则放置在 `library` 内部。 我们可以通过 `PoolManager` 内的 `modifyLiquidity` 内部分代码看一下单体架构内如何进行具体的逻辑处理。我们可以看到第一步就是将 `Pool.State` 利用 `PoolId` 从 `_pools` 内部获取出来,然后后续直接在 `pool` 基础上调用 `library` 内的函数。 ```solidity /// @inheritdoc IPoolManager function modifyLiquidity( PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params, bytes calldata hookData ) external onlyWhenUnlocked noDelegateCall returns (BalanceDelta callerDelta, BalanceDelta feesAccrued) { PoolId id = key.toId(); { Pool.State storage pool = _getPool(id); pool.checkPoolInitialized(); ``` ## Flash accounting 接下来,我们介绍 Flash accounting 的内容,Flash accounting 是由于以太坊增加了 transient storage 后才得以实现。Flash accounting 系统是由以下两部分构成的: 1. `CurrencyDelta` 用于记录每一种资产的 delta。delta > 0 意味着用户可以从 PoolManager 处获得资产,反之 delta < 0 意味着用户需要向 PoolManager 支付资产 2. `NonzeroDeltaCount` 用于记录当前 delta != 0 的资产的数量,当 NonzeroDeltaCount > 0 时即意味着用户当前一定完成与 PoolManager 的清算,如果此时用户希望离开 Uniswap v4 的上下文会直接触发 revert 操作 与 Flash accounting 配套的是 `unlock` 函数,如下: ```solidity function unlock(bytes calldata data) external override returns (bytes memory result) { if (Lock.isUnlocked()) AlreadyUnlocked.selector.revertWith(); Lock.unlock(); // the caller does everything in this callback, including paying what they owe via calls to settle result = IUnlockCallback(msg.sender).unlockCallback(data); if (NonzeroDeltaCount.read() != 0) CurrencyNotSettled.selector.revertWith(); Lock.lock(); } ``` 该函数被调用后会首先检查是否出现了重入情况,假如没有,那么就会首先解锁 `Lock.unlock();` ,然后利用 `unlockCallback` 通知调用者进行后续的交易。调用者会在 `unlock` 上下文内进行流动性调整或者 swap 等操作,这些操作结束后,上下文会从调用者处切换到 PoolManager 处,此时执行 `if (NonzeroDeltaCount.read() != 0) CurrencyNotSettled.selector.revertWith();` 的检查避免当前调用者与 PoolManager 之间存在未清算的资产,最后执行 `Lock.lock();` 重新锁定。 围绕着 `CurrencyDelta`,存在 `_accountDelta` 和 `_accountPoolBalanceDelta` 两个内部函数,其中 `_accountDelta` 是最底层的函数,实现如下: ```solidity function _accountDelta(Currency currency, int128 delta, address target) internal { if (delta == 0) return; (int256 previous, int256 next) = currency.applyDelta(target, delta); if (next == 0) { NonzeroDeltaCount.decrement(); } else if (previous == 0) { NonzeroDeltaCount.increment(); } } ``` 该函数主要被 `sync` / `take` / `sync` 等函数使用,这些函数都是用于 `unlock` 调用者直接修改某种资产的 delta。比如 `take` 函数就是调用者直接从 PoolManager 处提取资产并且将 `-amount` 计入自己的 delta 内部。我们会在后文逐一介绍这些函数的作用。 ```solidity function take(Currency currency, address to, uint256 amount) external onlyWhenUnlocked { unchecked { // negation must be safe as amount is not negative _accountDelta(currency, -(amount.toInt128()), msg.sender); currency.transfer(to, amount); } } ``` 而 `_accountPoolBalanceDelta` 实现如下: ```solidity function _accountPoolBalanceDelta(PoolKey memory key, BalanceDelta delta, address target) internal { _accountDelta(key.currency0, delta.amount0(), target); _accountDelta(key.currency1, delta.amount1(), target); } ``` 从函数实现可以看出,该函数主要用于和 Pool 有关的交易环节,更加具体来说就是用于 `swap` / `modifyLiquidity` 环节以及 `donate` 环节。`BalanceDelta` 也是一个自定义类型,该类型底层是 `int256`,布局为高 128bit 为 `amount1` 的值,而低 128bit 为 `amount0` 的值,注意此处的值都是 delta,即为正代表 PoolManager 需要向外支付,而为负代表用户需要向 PoolManager 转入资产。 ```solidity function toBalanceDelta(int128 _amount0, int128 _amount1) pure returns (BalanceDelta balanceDelta) { assembly ("memory-safe") { balanceDelta := or(shl(128, _amount0), and(sub(shl(128, 1), 1), _amount1)) } } ``` 此处以较为简单的 `donate` 为例展示 `_accountPoolBalanceDelta` 的使用方法。在 PoolManager 内部,我们会使用如下方法调用 `pool.donate`,然后将 `pool.donate` 返回的 `delta` 通过 `_accountPoolBalanceDelta` 累计到当前用户的 delta 内部。 ```solidity function donate(PoolKey memory key, uint256 amount0, uint256 amount1, bytes calldata hookData) external onlyWhenUnlocked noDelegateCall returns (BalanceDelta delta) { PoolId poolId = key.toId(); Pool.State storage pool = _getPool(poolId); pool.checkPoolInitialized(); key.hooks.beforeDonate(key, amount0, amount1, hookData); delta = pool.donate(amount0, amount1); _accountPoolBalanceDelta(key, delta, msg.sender); // event is emitted before the afterDonate call to ensure events are always emitted in order emit Donate(poolId, msg.sender, amount0, amount1); key.hooks.afterDonate(key, amount0, amount1, hookData); } ``` 此处的 `donate` 其实是一种将资产捐赠给当前 Pool 内活跃的 LP 的方法。Pool 会在内部的 `donate` 函数内将 `amount0` 和 `amount1` 组装为 `BalanceDelta` 返回。至于如何向活跃 LP 捐赠资金? 实现非常简单只需要增加 `feeGrowthGlobal0X128` 和 `feeGrowthGlobal1X128` 的值即可。 ```solidity /// @notice Donates the given amount of currency0 and currency1 to the pool function donate(State storage state, uint256 amount0, uint256 amount1) internal returns (BalanceDelta delta) { uint128 liquidity = state.liquidity; if (liquidity == 0) NoLiquidityToReceiveFees.selector.revertWith(); unchecked { // negation safe as amount0 and amount1 are always positive delta = toBalanceDelta(-(amount0.toInt128()), -(amount1.toInt128())); // FullMath.mulDiv is unnecessary because the numerator is bounded by type(int128).max * Q128, which is less than type(uint256).max if (amount0 > 0) { state.feeGrowthGlobal0X128 += UnsafeMath.simpleMulDiv(amount0, FixedPoint128.Q128, liquidity); } if (amount1 > 0) { state.feeGrowthGlobal1X128 += UnsafeMath.simpleMulDiv(amount1, FixedPoint128.Q128, liquidity); } } } ``` 继续回到 Flash Accounting 系统,我们首先介绍最简单的 `take` 函数,该函数允许调用者从 PoolManager 处直接获得资金,并为用户增加 -delta 以表示用户对 PoolManager 的债务: ```solidity /// @inheritdoc IPoolManager function take(Currency currency, address to, uint256 amount) external onlyWhenUnlocked { unchecked { // negation must be safe as amount is not negative _accountDelta(currency, -(amount.toInt128()), msg.sender); currency.transfer(to, amount); } } ``` 我们可以使用 `take` 作为闪电贷的资金来源。那么接下来,我们自然想到如何增加自己的 delta? 在 PoolManager 中,我们需要使用 `sync` 和 `settle` 方法。首先,`sync` 的作用是缓存当前 PoolManager 的余额: ```solidity /// @inheritdoc IPoolManager function sync(Currency currency) external { // address(0) is used for the native currency if (currency.isAddressZero()) { // The reserves balance is not used for native settling, so we only need to reset the currency. CurrencyReserves.resetCurrency(); } else { uint256 balance = currency.balanceOfSelf(); CurrencyReserves.syncCurrencyAndReserves(currency, balance); } } ``` 此处的 `CurrencyReserves` 只能存储一种资产的数据,所以实际上用户不能一次性 `sync` 多种资产。而且正如注释所述,`address(0)` 表示的原生 ETH 并不需要记录余额,至于其中的原因,我们会在马上介绍的 `_settle` 函数内介绍。 缓存完成余额后,用户可以直接向 PoolManager 内转入资产,然后使用 `settle` 或 `settleFor(address recipient)` 函数将刚转入的资产计入到 delta 内部。这两个函数底层都是使用了 `_settle` 函数,该函数实现如下: ```solidity // if settling native, integrators should still call `sync` first to avoid DoS attack vectors function _settle(address recipient) internal returns (uint256 paid) { Currency currency = CurrencyReserves.getSyncedCurrency(); // if not previously synced, or the syncedCurrency slot has been reset, expects native currency to be settled if (currency.isAddressZero()) { paid = msg.value; } else { if (msg.value > 0) NonzeroNativeValue.selector.revertWith(); // Reserves are guaranteed to be set because currency and reserves are always set together uint256 reservesBefore = CurrencyReserves.getSyncedReserves(); uint256 reservesNow = currency.balanceOfSelf(); paid = reservesNow - reservesBefore; CurrencyReserves.resetCurrency(); } _accountDelta(currency, paid.toInt128(), recipient); } ``` 我们可以看到对于原生 ETH 而言,我们直接使用 `msg.value` 作为用户支付的代币数量,而对于 ERC20 而言,我们则会再次获取代币余额,使用当前的代币余额 `reservesNow` 与缓存的代币余额 `reservesBefore` 作差的结果作为用户支付的代币数量。所以对于 ETH 而言,我们其实可以直接调用 `settle` 函数而不使用 `sync/settle` 环节。 除了 `take` / `sync` / `settle` / `settleFor` 函数外,还有一个较少被使用的 `clear` 函数,该函数的含义非常简单,就是直接将 delta 内的正数值清空: ```solidity /// @inheritdoc IPoolManager function clear(Currency currency, uint256 amount) external onlyWhenUnlocked { int256 current = currency.getDelta(msg.sender); // Because input is `uint256`, only positive amounts can be cleared. int128 amountDelta = amount.toInt128(); if (amountDelta != current) MustClearExactPositiveDelta.selector.revertWith(); // negation must be safe as amountDelta is positive unchecked { _accountDelta(currency, -(amountDelta), msg.sender); } } ``` `clear` 函数一般被用于某些微量资产残留情况,对这些微量资产,`take` 将其 transfer 出来或者铸造为 ERC6909 的成本都太高,此时直接 `clear` 可能是更加节约 gas 的。上述流程其实都是介绍的从 ERC20 / ETH 与 delta 之间的关系。在 Uniswap v4 内,我们引入了 ERC6909 机制,所以还有一种 delta 与 ERC6909 之间的关系,这些关系依赖于 `mint` 和 `burn` 函数: ```solidity /// @inheritdoc IPoolManager function mint(address to, uint256 id, uint256 amount) external onlyWhenUnlocked { unchecked { Currency currency = CurrencyLibrary.fromId(id); // negation must be safe as amount is not negative _accountDelta(currency, -(amount.toInt128()), msg.sender); _mint(to, currency.toId(), amount); } } /// @inheritdoc IPoolManager function burn(address from, uint256 id, uint256 amount) external onlyWhenUnlocked { Currency currency = CurrencyLibrary.fromId(id); _accountDelta(currency, amount.toInt128(), msg.sender); _burnFrom(from, currency.toId(), amount); } ``` 前者将正 delta 铸造为 ERC6909 代币,而后者是将 ERC6909 代币转化为 delta。这些方法更加具有 gas 效率,所以如无必要,可能将资产直接以 ERC6909 的形式存储在 PoolManager 内对频繁交易者更加友好。ERC6909 是一个非常简单的 ERC,从上述的 `_mint` 和 `_burnFrom` 函数就可以看出相比于 ERC20 而言,ERC6909 与 ERC1155 更加类似。 在实际与 PoolManager 处理 Flash Accounting 时,我们可以参考 `v4-periphery` 内的部分代码。以下是 `v4-periphery` 内实现的所有与 Delta 有关的 Action: ```solidity // closing deltas on the pool manager // settling uint256 internal constant SETTLE = 0x0b; uint256 internal constant SETTLE_ALL = 0x0c; uint256 internal constant SETTLE_PAIR = 0x0d; // taking uint256 internal constant TAKE = 0x0e; uint256 internal constant TAKE_ALL = 0x0f; uint256 internal constant TAKE_PORTION = 0x10; uint256 internal constant TAKE_PAIR = 0x11; uint256 internal constant CLOSE_CURRENCY = 0x12; uint256 internal constant CLEAR_OR_TAKE = 0x13; uint256 internal constant SWEEP = 0x14; // minting/burning 6909s to close deltas // note this is not supported in the position manager or router uint256 internal constant MINT_6909 = 0x17; uint256 internal constant BURN_6909 = 0x18; ``` 一般常用的是 `SETTLE_ALL`,该操作允许我们指定一种代币,然后 Router 会自动帮我们处理剩下的操作(前提是我们已经完成了授权)。以下展示了与 `SETTLE_ALL` 有关的部分代码: ```solidity if (action == Actions.SETTLE_ALL) { (Currency currency, uint256 maxAmount) = params.decodeCurrencyAndUint256(); uint256 amount = _getFullDebt(currency); if (amount > maxAmount) revert V4TooMuchRequested(maxAmount, amount); _settle(currency, msgSender(), amount); return; } function _getFullDebt(Currency currency) internal view returns (uint256 amount) { int256 _amount = poolManager.currencyDelta(address(this), currency); // If the amount is positive, it should be taken not settled. if (_amount > 0) revert DeltaNotNegative(currency); // Casting is safe due to limits on the total supply of a pool amount = uint256(-_amount); } function _settle(Currency currency, address payer, uint256 amount) internal { if (amount == 0) return; poolManager.sync(currency); if (currency.isAddressZero()) { poolManager.settle{value: amount}(); } else { _pay(currency, payer, amount); poolManager.settle(); } } ``` 此处的 `_getFullDebt` 是关键函数,该函数会使用 PoolManager 的 `Exttload` 内定义 `exttload` 函数读取对应代币当前的 delta 数值,然后根据该数值进行清算。除了提供了基础的原语,`v4-core` 内存在 `TransientStateLibrary` 库方便第三方使用,该库内就包含 `currencyDelta` 函数。 上述代码内的 ` _pay` 是一个抽象函数,具体实现需要开发者自行重载,在 `src/PositionManager.sol` 内对 `_pay` 有一个基于 `permit2` 的重载版本。 ## Hook 终于介绍到了 Uniswap v4 内最知名的 Hook 机制。要想了解 Hook 机制,我们需要从 `src/libraries/Hooks.sol` 开始,在 `Hooks` 开始,我们就可以看到一堆定义的常数: ```solidity uint160 internal constant ALL_HOOK_MASK = uint160((1 << 14) - 1); uint160 internal constant BEFORE_INITIALIZE_FLAG = 1 << 13; uint160 internal constant AFTER_INITIALIZE_FLAG = 1 << 12; uint160 internal constant BEFORE_ADD_LIQUIDITY_FLAG = 1 << 11; uint160 internal constant AFTER_ADD_LIQUIDITY_FLAG = 1 << 10; uint160 internal constant BEFORE_REMOVE_LIQUIDITY_FLAG = 1 << 9; uint160 internal constant AFTER_REMOVE_LIQUIDITY_FLAG = 1 << 8; uint160 internal constant BEFORE_SWAP_FLAG = 1 << 7; uint160 internal constant AFTER_SWAP_FLAG = 1 << 6; uint160 internal constant BEFORE_DONATE_FLAG = 1 << 5; uint160 internal constant AFTER_DONATE_FLAG = 1 << 4; uint160 internal constant BEFORE_SWAP_RETURNS_DELTA_FLAG = 1 << 3; uint160 internal constant AFTER_SWAP_RETURNS_DELTA_FLAG = 1 << 2; uint160 internal constant AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG = 1 << 1; uint160 internal constant AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG = 1 << 0; ``` 这些常数与 Hook 的权限是有关的。在上文中,我们已经提到 Hook 作为一个外置合约,Hook 到底可以介入交易的哪些流程是由地址决定的。我们可以看到地址的最后 14 bit 会被用于不同的权限,如果这些位置被设置为 1 代表有权限,被设置为 0 代表无权限。 ```solidity function hasPermission(IHooks self, uint160 flag) internal pure returns (bool) { return uint160(address(self)) & flag != 0; } ``` 这些权限可以被分为 4 类: 1. `beforeInitialize` 和 `afterInitialize` 被用于 Pool 初始化,对于一些单 Hook 合约对多 Pool 的架构,这些 Hook 经常被使用,还有一些情况,我们会编写 Factory 合约部署 Hook 并初始化 Pool,在这种情况下,我们使用 `beforeInitialize` 判断是否是由 Factory 合约初始化 Pool 2. `beforeAddLiquidity` 和 `afterAddLiquidity` 以及 `beforeRemoveLiquidity` 和 `afterRemoveLiquidity` 都是用于流动性管理前后进行 Hook 调用,默认情况下,Hook 不能修改用户的 delta,但是在 `afterAddLiquidityReturnDelta` 和 `afterRemoveLiquidityReturnDelta` 启用情况下,Hook 是可以调整用户管理流动性后获得的 delta,我们会在后文详细介绍 3. `beforeSwap` 和 `afterSwap` 用于 swap 前后调用 Hook,默认情况下,Hook 也是不可以介入 swap 过程中的用户资产,但是在 `beforeSwapReturnDelta` 和 `afterSwapReturnDelta` 情况下,Hook 可以直接修改用户 swap 的资产。在一些特殊的自定义 AMM 曲线的 Hook 中,我们会在 `beforeSwap` 过程中截流所有的用户输入资产,然后跳过 PoolManager 内的 uniswap v4 swap 过程,最后在 `afterSwap` 过程中使用 `hookDelta` 将 delta 正确计入用户 delta 账户内,我们会在后文详细介绍该过程 4. `beforeDonate` 和 `afterDonate` 都是简单的通知函数 在上文介绍初始化时,我们提到了 `isValidHookAddress`,但只介绍了该函数中有关 Dynamic Fee 的部分。但在该部分前,存在一系列检查 Hook 权限的代码: ```solidity // The hook can only have a flag to return a hook delta on an action if it also has the corresponding action flag if (!self.hasPermission(BEFORE_SWAP_FLAG) && self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG)) return false; if (!self.hasPermission(AFTER_SWAP_FLAG) && self.hasPermission(AFTER_SWAP_RETURNS_DELTA_FLAG)) return false; if (!self.hasPermission(AFTER_ADD_LIQUIDITY_FLAG) && self.hasPermission(AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG)) { return false; } if ( !self.hasPermission(AFTER_REMOVE_LIQUIDITY_FLAG) && self.hasPermission(AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG) ) return false; ``` 上述权限检查都是类似检查 `beforeSwap` 和 `beforeSwapReturnDelta` 的兼容情况,因为后者 `beforeSwapReturnDelta` 的作用依赖于 `beforeSwap` 的启用。 当然,在 `src/libraries/Hooks.sol` 内还存在一个 `validateHookPermissions` 函数,该函数没有被 PoolManager 使用,而是一般被用于开发者在 Hook 合约部署时进行自检: ```solidity function validateHookPermissions(IHooks self, Permissions memory permissions) internal pure { if ( permissions.beforeInitialize != self.hasPermission(BEFORE_INITIALIZE_FLAG) || permissions.afterInitialize != self.hasPermission(AFTER_INITIALIZE_FLAG) || permissions.beforeAddLiquidity != self.hasPermission(BEFORE_ADD_LIQUIDITY_FLAG) || permissions.afterAddLiquidity != self.hasPermission(AFTER_ADD_LIQUIDITY_FLAG) || permissions.beforeRemoveLiquidity != self.hasPermission(BEFORE_REMOVE_LIQUIDITY_FLAG) || permissions.afterRemoveLiquidity != self.hasPermission(AFTER_REMOVE_LIQUIDITY_FLAG) || permissions.beforeSwap != self.hasPermission(BEFORE_SWAP_FLAG) || permissions.afterSwap != self.hasPermission(AFTER_SWAP_FLAG) || permissions.beforeDonate != self.hasPermission(BEFORE_DONATE_FLAG) || permissions.afterDonate != self.hasPermission(AFTER_DONATE_FLAG) || permissions.beforeSwapReturnDelta != self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG) || permissions.afterSwapReturnDelta != self.hasPermission(AFTER_SWAP_RETURNS_DELTA_FLAG) || permissions.afterAddLiquidityReturnDelta != self.hasPermission(AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG) || permissions.afterRemoveLiquidityReturnDelta != self.hasPermission(AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG) ) { HookAddressNotValid.selector.revertWith(address(self)); } } ``` 一般来说,我们会在 Hook 合约内实现 `getHookPermissions` 函数,该函数以 `Permissions` 结构体的方式返回当前 Hook 的权限,比如: ```solidity function getHookPermissions() public pure virtual override returns (Hooks.Permissions memory) { return Hooks.Permissions({ beforeInitialize: false, afterInitialize: false, beforeAddLiquidity: true, // -- liquidity must be deposited here directly -- // afterAddLiquidity: false, beforeRemoveLiquidity: false, afterRemoveLiquidity: false, beforeSwap: true, // -- custom curve handler -- // afterSwap: false, beforeDonate: false, afterDonate: false, beforeSwapReturnDelta: true, // -- enable custom curve by skipping poolmanager swap -- // afterSwapReturnDelta: false, afterAddLiquidityReturnDelta: false, afterRemoveLiquidityReturnDelta: false }); } ``` 所以说,在 Hook 部署时,我们可以简单使用 `validateHookPermissions` 确保合约部署的地址与我们设置的权限是匹配的。在 `v4-periphery` 内,`src/utils/BaseHook.sol` 内就存在如下代码: ```solidity constructor(IPoolManager _manager) ImmutableState(_manager) { validateHookAddress(this); } function validateHookAddress(BaseHook _this) internal pure virtual { Hooks.validateHookPermissions(_this, getHookPermissions()); } ``` 当然,上述 Hook 编程的最佳实践也不是一定要遵守的,假如读者编写的 Hook 合约字节码比较紧张,可以不实现 `getHookPermissions` 等函数,不实现这些函数不会影响 PoolManager 的检查。对于如何挖掘一个 Hook 地址,读者可以使用各种 `create2` 地址挖掘工具,比如 `cast create2` \ [create2crunch](https://github.com/0age/create2crunch) \ [createXcrunch](https://github.com/HrikB/createXcrunch) 以及 [网页工具](https://v4hookaddressminer.xyz/)。在网页工具中,开发者提供了一个获得 ` Init Code Hash` 的 solidity 脚本,读者可以参考。 所有的 Hook 在底层都使用了 `callHook` 函数,该函数实现如下: ```solidity function callHook(IHooks self, bytes memory data) internal returns (bytes memory result) { bool success; assembly ("memory-safe") { success := call(gas(), self, 0, add(data, 0x20), mload(data), 0, 0) } // Revert with FailedHookCall, containing any error message to bubble up if (!success) CustomRevert.bubbleUpAndRevertWith(address(self), bytes4(data), HookCallFailed.selector); // The call was successful, fetch the returned data assembly ("memory-safe") { // allocate result byte array from the free memory pointer result := mload(0x40) // store new free memory pointer at the end of the array padded to 32 bytes mstore(0x40, add(result, and(add(returndatasize(), 0x3f), not(0x1f)))) // store length in memory mstore(result, returndatasize()) // copy return data to result returndatacopy(add(result, 0x20), 0, returndatasize()) } // Length must be at least 32 to contain the selector. Check expected selector and returned selector match. if (result.length < 32 || result.parseSelector() != data.parseSelector()) { InvalidHookResponse.selector.revertWith(); } } ``` 此处我们注意到 `CustomRevert.bubbleUpAndRevertWith`。这是一种由 [ERC-7751](https://eips.ethereum.org/EIPS/eip-7751) 规定的更具有语义性的错误,该错误定义如下: ```solidity error WrappedError(address target, bytes4 selector, bytes reason, bytes details); ``` 在 Uniswap v4 内,`target` 指错误发生的合约地址,`selector` 指出现错误的 call 中的选择器参数,`reason` 部分使用是错误 call 返回的数据,而 `details` 是一个额外指明错误位置的选择器,比如上述代码内为 `HookCallFailed`。这种异常抛出方式有助于开发者进行定位上下文。 后续的内联汇编用于将调用 Hook 产生的返回值从 EVM 内的 returndata 区域复制到内存中的 `result` 变量内部。此处额外注意的是 `and(add(returndatasize(), 0x3f), not(0x1f))` 该操作用于将返回值填充为 32 bytes 的整数倍,因为 solidity 内的内存占用总是使用 32 bytes 的倍数。 最后,我们检查返回的长度是否大于 32 bytes 以及返回数据的前 4 bytes 是否与调用数据一致。特别是最后的 4 bytes 检查,这也是为什么我们会在 Hook 内写出类似如下模式的代码: ```solidity function beforeInitialize(address, PoolKey calldata, uint160) internal view override returns (bytes4) { return IHooks.beforeInitialize.selector; } ``` 在 `callHook` 基础上,`v4-core` 内部还存在 `callHookWithReturnDelta` 函数,该函数的额外功能是解析返回值中的 ReturnDelta。该函数用于 `afterModifyLiquidity` 和 `afterSwap`。 在 `src/libraries/Hooks.sol` 内基于 `callHook` 最简单的实现是 `beforeInitialize` 和 `afterInitialize`。`beforeDonate` 和 `afterDonate` 也是完全类似的实现方法。 ```solidity /// @notice calls beforeInitialize hook if permissioned and validates return value function beforeInitialize(IHooks self, PoolKey memory key, uint160 sqrtPriceX96) internal noSelfCall(self) { if (self.hasPermission(BEFORE_INITIALIZE_FLAG)) { self.callHook(abi.encodeCall(IHooks.beforeInitialize, (msg.sender, key, sqrtPriceX96))); } } /// @notice calls afterInitialize hook if permissioned and validates return value function afterInitialize(IHooks self, PoolKey memory key, uint160 sqrtPriceX96, int24 tick) internal noSelfCall(self) { if (self.hasPermission(AFTER_INITIALIZE_FLAG)) { self.callHook(abi.encodeCall(IHooks.afterInitialize, (msg.sender, key, sqrtPriceX96, tick))); } } ``` `beforeAddLiquidity` 和 `beforeRemoveLiquidity` 都通过 `beforeModifyLiquidity` 对 Hook 合约进行调用: ```solidity /// @notice calls beforeModifyLiquidity hook if permissioned and validates return value function beforeModifyLiquidity( IHooks self, PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params, bytes calldata hookData ) internal noSelfCall(self) { if (params.liquidityDelta > 0 && self.hasPermission(BEFORE_ADD_LIQUIDITY_FLAG)) { self.callHook(abi.encodeCall(IHooks.beforeAddLiquidity, (msg.sender, key, params, hookData))); } else if (params.liquidityDelta <= 0 && self.hasPermission(BEFORE_REMOVE_LIQUIDITY_FLAG)) { self.callHook(abi.encodeCall(IHooks.beforeRemoveLiquidity, (msg.sender, key, params, hookData))); } } ``` 理所当然,`afterAddLiquidity` 和 `afterRemoveLiquidity` 使用了 `afterModifyLiquidity` 对 Hook 合约进行调用。但 `afterModifyLiquidity` 配合 `afterAddLiquidityReturnDelta` 或 `afterRemoveLiquidityReturnDelta` 后就可以调整用户的 delta,在 `afterModifyLiquidity` 函数实现中,我们对其进行特殊处理: ```solidity function afterModifyLiquidity( IHooks self, PoolKey memory key, IPoolManager.ModifyLiquidityParams memory params, BalanceDelta delta, BalanceDelta feesAccrued, bytes calldata hookData ) internal returns (BalanceDelta callerDelta, BalanceDelta hookDelta) { if (msg.sender == address(self)) return (delta, BalanceDeltaLibrary.ZERO_DELTA); callerDelta = delta; if (params.liquidityDelta > 0) { if (self.hasPermission(AFTER_ADD_LIQUIDITY_FLAG)) { hookDelta = BalanceDelta.wrap( self.callHookWithReturnDelta( abi.encodeCall( IHooks.afterAddLiquidity, (msg.sender, key, params, delta, feesAccrued, hookData) ), self.hasPermission(AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG) ) ); callerDelta = callerDelta - hookDelta; } } else { if (self.hasPermission(AFTER_REMOVE_LIQUIDITY_FLAG)) { ... } } } ``` 此处我们使用了 `callerDelta = callerDelta - hookDelta;` 对 `callerDelta` 进行调整,我们可以看到上述方法保证了调整后的 callerDelta 与 `hookDelta` 相加仍等于调整前的 callerDelta,以此来避免 Hook 在协议内攫取超过预期的资金。回到 `PoolManager` 合约内部,我们可以看到 `modifyLiquidity` 内存在如下代码: ```solidity BalanceDelta hookDelta; (callerDelta, hookDelta) = key.hooks.afterModifyLiquidity(key, params, callerDelta, feesAccrued, hookData); // if the hook doesn't have the flag to be able to return deltas, hookDelta will always be 0 if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) _accountPoolBalanceDelta(key, hookDelta, address(key.hooks)); _accountPoolBalanceDelta(key, callerDelta, msg.sender); ``` 上述代码在获得 `afterModifyLiquidity` 返回值后将 `hookDelta` 计入到 `hooks` 账户中。 `beforeSwap` 和 `afterSwap` 是最复杂的 hook 调用方法,这是因为 `beforeSwap` 和 `afterSwap` 都存在重新调整用户输入的可能性。对于 `beforeSwap` 而言,我们可以调整用户指定的兑换数量 `amount` 并且返回 `BeforeSwapDelta` 供后续的 `afterSwap` 使用,另外还会返回 `lpFeeOverride` 用于动态费率下调整当前 swap 的 lp 费用。 ```solidity function beforeSwap(IHooks self, PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData) internal returns (int256 amountToSwap, BeforeSwapDelta hookReturn, uint24 lpFeeOverride) { amountToSwap = params.amountSpecified; if (msg.sender == address(self)) return (amountToSwap, BeforeSwapDeltaLibrary.ZERO_DELTA, lpFeeOverride); if (self.hasPermission(BEFORE_SWAP_FLAG)) { bytes memory result = callHook(self, abi.encodeCall(IHooks.beforeSwap, (msg.sender, key, params, hookData))); // A length of 96 bytes is required to return a bytes4, a 32 byte delta, and an LP fee if (result.length != 96) InvalidHookResponse.selector.revertWith(); if (key.fee.isDynamicFee()) lpFeeOverride = result.parseFee(); // skip this logic for the case where the hook return is 0 if (self.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG)) { hookReturn = BeforeSwapDelta.wrap(result.parseReturnDelta()); int128 hookDeltaSpecified = hookReturn.getSpecifiedDelta(); // Update the swap amount according to the hook's return, and check that the swap type doesn't change (exact input/output) if (hookDeltaSpecified != 0) { bool exactInput = amountToSwap < 0; amountToSwap += hookDeltaSpecified; if (exactInput ? amountToSwap > 0 : amountToSwap < 0) { HookDeltaExceedsSwapAmount.selector.revertWith(); } } } } } ``` 这里,我们需要额外关注 `BEFORE_SWAP_RETURNS_DELTA_FLAG` 成立分支内的内容,此处我们看到了 `getSpecifiedDelta`。在 `BeforeSwapDelta` 定义中,高 128bit 用于 specified tokens 的 delta 而低 128 bit 用于 unspecified tokens 的 delta。所谓 specified tokens 指的是用户通过 `SwapParams.amountSpecified` 指定的代币,具体指哪种代币需要结合 `exactIn` / `exactOut` 模式和 `zeroForOne` / `OneForZero` 确定,比如在 `exactIn` 和 `zeroForOne` 的情况下,specified tokens 就是指 token 0。此处我们使用 `amountToSwap += hookDeltaSpecified;` 调整代币兑换数量。 额外的,我们通过 `exactInput ? amountToSwap > 0 : amountToSwap < 0` 避免调整后的 `amountToSwap` 不符合实际情况。基于 `beforeSwap`,我们可以构建出一个自定义 AMM 曲线的 Hook 合约。我们需要利用 `hookReturn` 将用户指定的 `amountToSwap` 调整为 `0`,以此跳过 PoolManager 内部的 swap 过程,同时我们还需要根据 AMM 计算结果将资产利用 `take` 和 `settle` 方法以 Delta 形式结算到 PoolManager 内部,以方便在完成 swap 后,在 `afterSwap` 内完成最终清算。 在 `src/test/CustomCurveHook.sol` 文件内存在一个 1:1 兑换资产的 Hook 合约,该合约在 `beforeSwap` 内完成了上述任务: ```solidity function beforeSwap( address, /* sender **/ PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata /* hookData **/ ) external override onlyPoolManager returns (bytes4, BeforeSwapDelta, uint24) { (Currency inputCurrency, Currency outputCurrency, uint256 amount) = _getInputOutputAndAmount(key, params); // this "custom curve" is a line, 1-1 // take the full input amount, and give the full output amount manager.take(inputCurrency, address(this), amount); outputCurrency.settle(manager, address(this), amount, false); // return -amountSpecified as specified to no-op the concentrated liquidity swap BeforeSwapDelta hookDelta = toBeforeSwapDelta(int128(-params.amountSpecified), int128(params.amountSpecified)); return (IHooks.beforeSwap.selector, hookDelta, 0); } ``` 可能有读者好奇,手续费还会被征收吗?首先,我们可以利用 Hook 返回值将 `swap` 的 `lpFee` 设置为 `0`,但是 `protocolFee` 仍会被征收。在 `src/libraries/Pool.sol` 内,我们可以看到如下代码: ```solidity { uint24 lpFee = params.lpFeeOverride.isOverride() ? params.lpFeeOverride.removeOverrideFlagAndValidate() : slot0Start.lpFee(); swapFee = protocolFee == 0 ? lpFee : uint16(protocolFee).calculateSwapFee(lpFee); } // a swap fee totaling MAX_SWAP_FEE (100%) makes exact output swaps impossible since the input is entirely consumed by the fee if (swapFee >= SwapMath.MAX_SWAP_FEE) { // if exactOutput if (params.amountSpecified > 0) { InvalidFeeForExactOut.selector.revertWith(); } } // swapFee is the pool's fee in pips (LP fee + protocol fee) // when the amount swapped is 0, there is no protocolFee applied and the fee amount paid to the protocol is set to 0 if (params.amountSpecified == 0) return (BalanceDeltaLibrary.ZERO_DELTA, 0, swapFee, result); ``` 在上文中,我们提到了 `afterSwap` 内存在一个清算环节,这是因为 `beforeSwap` 返回给 PoolManager 的 `beforeSwapHookReturn` 没有被计入到 hook 的 delta 账户内部。该步骤我们会在 `afterSwap` 函数内完成。注意此处的 `afterSwap` 是指在 PoolManager 的 `afterSwap` 环节,即使 Hook 合约内没有实现 `afterSwap` 权限,该函数也会正常执行。 ```solidity function afterSwap( IHooks self, PoolKey memory key, IPoolManager.SwapParams memory params, BalanceDelta swapDelta, bytes calldata hookData, BeforeSwapDelta beforeSwapHookReturn ) internal returns (BalanceDelta, BalanceDelta) { if (msg.sender == address(self)) return (swapDelta, BalanceDeltaLibrary.ZERO_DELTA); int128 hookDeltaSpecified = beforeSwapHookReturn.getSpecifiedDelta(); int128 hookDeltaUnspecified = beforeSwapHookReturn.getUnspecifiedDelta(); if (self.hasPermission(AFTER_SWAP_FLAG)) { hookDeltaUnspecified += self.callHookWithReturnDelta( abi.encodeCall(IHooks.afterSwap, (msg.sender, key, params, swapDelta, hookData)), self.hasPermission(AFTER_SWAP_RETURNS_DELTA_FLAG) ).toInt128(); } BalanceDelta hookDelta; if (hookDeltaUnspecified != 0 || hookDeltaSpecified != 0) { hookDelta = (params.amountSpecified < 0 == params.zeroForOne) // params.amountSpecified < 0 is exact in ? toBalanceDelta(hookDeltaSpecified, hookDeltaUnspecified) // exact in, zero for one | exact out, one for zero : toBalanceDelta(hookDeltaUnspecified, hookDeltaSpecified);// exact out, zero for one | exact in, one for zero // the caller has to pay for (or receive) the hook's delta swapDelta = swapDelta - hookDelta; } return (swapDelta, hookDelta); } ``` 我们可以看到在 `afterSwap` 内,假如用户设置了 `afterSwap` 和 `afterSwapReturnDelta`,我们可以对 `hookDeltaUnspecified` 进行调整,但无法对 `beforeSwapHookReturn` 内的 `hookDeltaSpecified` 进行调整,这是因为在 `beforeSwap` 内部,我们将 `hookDeltaSpecified` 已经作用到 `amountToSwap` 内了,所以此处出于职责分离的角度就不再允许 `afterSwap` 再次调整 `hookDeltaSpecified`。 最后,我们将 `hookDeltaSpecified` 和 `hookDeltaUnspecified` 与 token 0 和 token 1 对应,将其打包到 `hookDelta` 内部,此处仍是通过 `swapDelta = swapDelta - hookDelta;` 保证系统内不会凭空产生额外资产。在 PoolManager 的 `swap` 函数内,我们会完成最终的 delta 写入: ```solidity BalanceDelta hookDelta; (swapDelta, hookDelta) = key.hooks.afterSwap(key, params, swapDelta, hookData, beforeSwapDelta); // if the hook doesn't have the flag to be able to return deltas, hookDelta will always be 0 if (hookDelta != BalanceDeltaLibrary.ZERO_DELTA) _accountPoolBalanceDelta(key, hookDelta, address(key.hooks)); _accountPoolBalanceDelta(key, swapDelta, msg.sender); ``` ## 总结 在本文中,我们介绍了 Uniswap v4 内的新特性实现,但是本文没有介绍其他的一些内容,分别列出: 1. `Pool` 内的大部分函数,由于 Pool 基本上是 Uniswap v3 的逻辑,所以此处没有介绍,但是 Pool 仍存在一些有趣的优化,读者可以自行阅读 2. 大量的数学和 gas 优化,Uniswap v4 特别强调 gas 优化,所以代码实现中大量使用了内联汇编并且对于数学计算以及 msb 等算法也特别进行了优化,具体可以参考笔者之前编写的 [现代 DeFi: Uniswap V4 数学库分析](https://blog.wssh.dev/posts/uniswap-math/) 一文 本文主要分析了 Uniswap v4 的新特性,并且带领读者简单阅读了与这些特性相关的核心代码。