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