Hooks 合约是 Uniswap V4 主要的特性之一。池子的执行可以划分为多个阶段,例如:池子创建的前后、池子交换的前后,通过 Hooks 合约,可以在池子执行的各个阶段,执行 Hooks 合约预定义的生命周期函数,因此,开发者可以编写多种 Hooks 合约,利用它的生命周期函数实现多种新特性,这非常有利于开发者基于 Hooks 扩展多种业务,比如:限价单、动态费率、TWAMM、Yield 生息等等。本篇是 Uniswap V4 系列的 Hooks 篇,从 Hooks 合约实现原理、交互流程的角度对其进行分析。
Hooks 合约的核心代码在仓库 v4-core(https://github.com/Uniswap/v4-core) 中,PoolManager 合约管理着池子并存储着所有池子的状态。先从 PoolManager 合约创建池子的 initialize 函数开始分析。
initialize 函数
initialize 对指定的池子进行初始化:
在初始化函数 initialize 的参数列表中,第一个参数是 PoolKey,它决定了 Pool 的唯一性,PoolKey 定义如下:
相比于 Uniswap V3,池子的唯一索引里添加一个新字段 hooks,这表明即使币対 (currency0,currency1)、费率 (fee)、刻度间距 (tickSpacing) 相同,一旦 hooks 不同,仍然是一个不同的池子。
onlyByLocker 修饰符,仅允许当前的 Locker 或最近调用的预许可挂钩之外的任何地址调用。
initialize 函数的主要功能包括:
一系列参数校验
静态费用是否越界;
刻度间距 tickSpacing 是否越界,刻度间距越大,越节省 gas,同时滑点越大,适合价格波动大的币対;
currency0 必须小于 currency1,避免创建重复币対;
对 hooks 合约地址的校验
如果允许 NoOp,则至少应允许 beforeModifyPosition、beforeSwap 和 beforeDonate 之一;
如果没有设置 Hooks 合约,则费用无法设置为动态的;
如果设置了 Hooks 合约,则必须至少设置 1 个标志,或者设置了动态费用。
先后调用 hooks 的 beforeInitialize() 和 afterInitialize() 函数,至此,我们知道,Hooks 合约之所以能在池子初始化前后被回调,是因为:
PoolKey 指定了用于回调的 Hooks 合约;
PoolMananger 合约在使用 initialize 函数初始化池子时,回调了 hooks 合约的 beforeInitialize() 和 afterInitialize() 函数。
初始化一个新池子 pools[id]。在 Uniswap V3 里面,创建池子时创建了一个新的 UniswapV3Pool 合约的实例。而在 Uniswap V4 里,创建池子时,并没有创建一个新合约,所有池子的状态存储在 mapping 类型的 pools 中,这就是 Uniswap V4 的单例模式,取代 Uniswap V3 工厂模式,这方式降低了创建池子的 gas 成本,所有池子都由 PoolManager 管理。
lock 函数
上面提到,只有 Locker 角色才能调用 intialize 函数,成为 Locker 需要调用 lock 函数,如下图所示:
把 lockTarger 添加为 Locker;
回调 Hooks 合约(lockTarger)的 lockAcquired 函数;
校验和设置 Lockers 数组,如果 Lockers 的长度为 1,Lockers 的 nonzeroDeltaCount() 必须为 0。
因此,我们可以得到 Hooks 合约与 PoolManager 的主要交互流程如下:
modifyPosition 函数
modifyPosition 函数用于流动性的变更。
Note: 在新的 commit(https://github.com/Uniswap/v4-core/commit/36160ced8440fe211805f38860334359c73ef490) 中, modifyPosition 被重命名为 modifyLiquidity 函数,以及 beforeModifyPosition 和 afterModifyPosition 重命名为 beforeModifyLiquidity 和 afterModifyLiquidity,但主体逻辑不变,本文基于 commit 835571 分析。
和 initialize 函数不同的是,多了一个 noDelegateCall modifier,它禁止了 delegate call;
校验池子是否初始化;
先后回调 Hooks 的 beforeModifyPosition 函数和 afterModifyPosition 函数;
修改流动性
owner 代表仓位的 owner;
tickLower 是仓位区间的下界,tickUpper 是仓位区间的上界(当现价低于仓位区间的下界或者高于上界时,仓位就会仅剩一种代币);
liquidityDelta 可正可负,代表流动池的新增或者移除;
tickSpacing 是 tick 的刻度间距,在 initialize 函数已介绍。(modifyPosition 类似于 Uniswap V3 中的流动性管理,本文不进行展开讨论)
更新代币余额,并更新 nonzeroDeltaCount(),这个值在上文提到的 lock 函数中被校验,hooks 退出时,要求 nonzeroDeltaCount() 为 0,即用户和池子之间没有代币未结清。
swap 函数
swap 函数为用户处理代币交换业务。
swap 函数的访问权限和 modifyPosition 函数一样,由 noDelegateCall 和 onlyByLocker modifier 控制。
校验池子是否初始化;
先后调用 Hooks 合约的 beforeSwap 和 afterSwap 回调函数;
交换代币:
tickSpacing 是刻度间距(在 initialize 函数中已介绍);
zeroForOne 是代币交换的方向,它决定收取费用的币种;
amountSpecified 是交换的代币数量。
sqrtPriceLimitX96 是限制价格,用于预防滑点。(swap 同样类似于 Uniswap V3 的 swap 业务,本文不展开讨论)
代币记账,注意这里并没有用户和池子间的代币转移操作,最终的代币结算由 settle 函数,take 函数完成;
协议费用记账。
take 函数
从池子内转移代币给 to 地址;
可以用于闪电贷。
settle 函数
用于计算用户用户支付给池子的代币
调用 settle 函数前,用户需要提前把代币转给 PoolManager 合约,如果该代币是 ERC20 代币。
donate 函数
捐赠代币给池子的函数
主体流程和之前的 initialize 函数一致:参数校验,先后调用 beforeDonate 和 afterDonate 回调函数,代币捐赠,代币结算;
BaseHook.sol 和官方示例 Hooks 在仓库 v4-periphery(https://github.com/Uniswap/v4-periphery) 中。仓库 v4-periphery 中共有 5 个示例 Hooks:
FullRange.sol,在整个价格范围内添加和移除流动性,类似 Uniswap V2;
GeomeanOracle.sol, 支持 Uniswap 池充当预言机的 Hook;
LimitOrder.sol,支持用户下限价单的 Hook;
TWAMM.sol,TWAMM(Time Weighted Average Market Maker) 是一种使时间加权平均数来计算资产价格的做市商,该 Hook 支持 TWAMM 方式交易代币。详见官网博文 Uniswap v4 TWAMM Hook(https://blog.uniswap.org/v4-twamm-hook);
VolatilityOracle.sol,支持动态 fee。
注意: 截稿时,Uniswap V4 还未上线主网,代码仍在更新中。官方提到 (https://github.com/Uniswap/v4-periphery?tab=readme-ov-file#repository-structure),尽管部分 hooks 经过审计并处于生产可用状态,但并不保证它们对所有用户来说都是安全的,建议在 Hooks 上线前进行安全审计。
BaseHook 合约
BaseHook 合约是一个抽象合约,作为 Hooks 合约的父合约,包含 Hooks 合约的基本接口,需要 Hooks 合约去实现。
FullRange 合约
以 FullRange 合约为例进行分析。FullRange 的流动性范围是整个价格区间,而不是一段区间的集中流动性,主要分析一下函数。
getHooksCalls 函数,它暗示了该合约会实现 3 个回调函数 beforeInitialize,beforeModifyPosition,和 beforeSwap;
beforeInitialize 函数,记录新池子的信息在 poolInfo 中。
addLiquidity 函数,它为用户添加流动性的入口
获取池子流动性并计算新增的流动性;
modifyPosition 进行实际的流动性管理,该函数会调用 poolManager.lock 函数,在 lock 函数中再回调 FullRange 合约的 LockAcquired 函数;
为用户铸 LP 代币,并校验滑点。(第一个主要 LP 代币的人,会被扣减数量为 MINIMUM_LIQUIDITY 份额的 LP 代币,这其实就是 Uniswap V2(https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L120-L121) 里的做法)
lockAcquired 函数,它是一个回调函数,例如,addLiquidity 函数会触发该函数的调用,并完成代币的转移结算。
removeLiquidity 函数,为用户提供移除流动性功能,相当于 addLiquidity 的逆操作。
除了官网外,社区也已经出现许多 Hooks,例如:
Multi-Sig(https://github.com/atj3097/mfa-multisig-hook-v4/tree/main),要求某些矿池操作需要多个签名,例如添加或删除流动性。这可用于为池添加额外的安全层;
Whitelist(https://github.com/atj3097/whitelist-hook),限制已批准地址的白名单参与资金池。这可用于阻止某些人参与资金池,例如被平台禁止的人或被认为是高风险交易者的人。
Stop Loss Order(https://github.com/saucepoint/v4-stoploss),允许用户对其头寸下止损单。这意味着如果价格达到一定水平,该仓位将自动平仓;
Ref Fee Hook(https://github.com/mergd/ref-fee-hook): 收取掉期和流动性增加推荐费的 Hooks。这可以用来激励用户推荐其他人加入池;
Dynamic Fee Hook(https://github.com/umbrellaresearch/uni-v4-hooks-tutorial),使用波动率费用预言机根据货币对的实际波动率调整池费用的挂钩。
注意: 社区合约并没有对其安全性做出保证,建议在 Hooks 上线前进行安全审计。
Uniswap V4 将在坎昆升级后上线,在这期间它的代码还可能进一步更新。本篇文章基于以下 commits 分析:
V4-core: 83557113a0425eb3d81570c30e7a5ce550037149, Dec 11, 2023
V4-periphery: 63d64fcd82bff9ec0bad89730ce28d7ffa8e4225, Dec 20, 2023
新的 commit(https://github.com/Uniswap/v4-core/commit/36160ced8440fe211805f38860334359c73ef490) 对接口 modifyposition 和回调函数进行了重命名,但基本结构不变。
Uniswap.org
https://github.com/Uniswap/v4-core
https://github.com/Uniswap/v4-periphery
https://github.com/hyperoracle/awesome-uniswap-hooks
【免责声明】市场有风险,投资需谨慎。本文不构成投资建议,用户应考虑本文中的任何意见、观点或结论是否符合其特定状况。据此投资,责任自负。