---
# System prepended metadata

title: Uniswap v4 代码初探

---

## 概述

在之前的课程中，我们已经介绍了 [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 的新特性，并且带领读者简单阅读了与这些特性相关的核心代码。
