本文详细解释UniswapV4功能的意义以及它们的实作原理。
原文作者:林瑋宸 Albert Lin
原文来源:medium
自从 UniswapV4 的宣佈,这个 Swap 平台经历了一个巨大的转变。从一个 Swap 平台发展成了基础设施服务提供者。特别是 V4 的 Hooks 功能,引起了广泛的关注。经过一段时间的深入研究后我整理一些内容,希望能让大家更了解这个变革以及实施方式。
UniswapV4 的创新重点不在于改进多少 AMM 技术,更著重于扩展生态系统。具体来说,这次的创新包括以下几个关键功能:
在接下来的部分,我将会详细解释这些功能的意义以及它们的实作原理。
Flash Accounting
Double Entry Bookkeeping
UniswapV4 采用了类似于複式簿记(Double Entry Bookkeeping)的记录方式,来跟踪每一个操作对应的 Token 馀额增减变化。这种複式簿记的记录方式要求每一笔交易都必须同时在多个帐户中进行记录,并确保这些帐户之间的资产价值保持平衡。举个例子,假设使用者以 100 TokenA 向 Pool 交换 50 TokenB ,那麽在帐本中记录会是如下:
Token Delta 相关操作
在 UniswapV4 中,主要操作都会採用这种记帐方式,并在程式码中使用一个名为 lockState.currencyDelta[currency] 的 Storage Variable 来记录 Token 馀额的变化量。这个变化量的数值如果为正数,表示 Token 在池中预期增加的数量,反之则表示 Token 在池中预期减少的数量。另一个角度来看,如果数值为正,代表池中缺少的 Token 数量(预计要收到的 Token 数量),而数值为负则代表这个池中多馀的 Token 数量(预计使用者要提领的 Token 数量)。以下列出了各种操作对 Token 变化量(TokenDelta)的影响:
以上操作只有 settle 和 take 会有实际传送 Token 的行为,其他操作只是单纯去更新 TokenDelta 数值。
Token Delta Example
以下我们用一个简单的例子来说明实际是如何去更新 TokenDelta 。假设今天我们将 100 个 TokenA 兑换为 50 个 TokenB:
当整个兑换操作完成后, TokenADelta 和 TokenBDelta 都被重置为 0。这样代表操作已经完全平衡,藉此来保证帐户馀额的一致性。
EIP-1153: Transient storage opcodes
之前提到 UniswapV4 利用 Storage Variable 来记录 TokenDelta,但在合约内部,Storage Variable 的读写是相当高成本的。这时候就要提到另一个 Uniswap 所推出来的 EIP:EIP1153 — Transient Storage Opcodes。
UniswapV4 计划使用 EIP1153 所提供的 TSTORE 和 TLOAD 这两个 OP Code 来更新 TokenDelta。採用 Transient Storage Opcodes 的 Storage Variable 会在 Transaction 结束后被丢弃(类似 Memory Variable),从而不必写入硬碟,进而降低 Gas 费用。
EIP1153 已被确定会被包含在下次的坎昆升级,同时 UniswapV4 也指出将会在坎昆升级之后上线 UniswapV4。
Flash Accounting — Lock
UniswapV4 引入了 lock 机制,这意味著在进行 Pool 操作之前,必须首先调用 PoolManager.lock() 以获取一个锁(Lock)。在 lock() 的执行结束前,会检查 TokenDelta 的数值是否为 0,否则将引发 revert。当调用 PoolManager.lock() 并成功获得锁之后,将会呼叫 msg.sender的 lockAcquired() 函数。在 lockAcquired() 函数中,才执行与 Pool 相关的操作(例如 swap、modifyPosition 等操作)。
以下以图示为例来说明这个过程。当使用者需要进行 Token Swap 操作时,必须呼叫一个具有 lockAcquired() 函数的 Smart Contract(这裡称为回调合约,CallBack Contract)。回调合约将首先呼叫 PoolManager.lock(),然后 PoolManager 会呼叫回调合约的 lockAcquired() 函数。在 lockAcquired() 函数中,定义了与 Pool 操作相关的逻辑,例如 swap、settle 以及 take 等操作。最后,在整个 lock() 即将结束时,PoolManager 会检查与这次操作有关的 TokenDelta 是否已经全部重置为 0,以确保 Pool 中的资产保持平衡。
Singleton Contract
Singleton Contract 意味著 UniswapV4 已经弃用了以往的 Factory-Pool 模式。每个 Pool 不再是一个独立的 Smart Contract,而是所有 Pool 共用同一个单例(singleton)合约。这种设计与 Flash Accounting 机制结合,只需要更新必要的 Storage Variable,进一步降低了操作的複杂性和成本。
以下以图示为例,以 UniswapV3 为例,将 ETH 兑换为 DAI 至少需要执行四次 Token 转移( Storage 写入操作)。这包括对 USDC、USDT 和 DAI Token 的多次变化记录。然而,透过 UniswapV4 的改进,搭配 Flash Accounting 机制,只需要一次 Token 转移(将 DAI 由 Pool 转移到使用者),这大幅降低了操作的次数和成本。
Hooks Architecture
UniswapV4 这次的更新中,最引人注目的要属 Hooks Architecture。这项更新将围绕在 Pool 可利用性上提供了极大的灵活性。Hooks 是指在对 Pool 执行特定操作时,会额外调用 Hooks Contract 来执行额外的动作。而这些动作可以分为不同类别,包括initialize(create pool)、modifyPosition(add/remove liquidity)、swap和 donate,每个类别都有执行前和执行后的动作:
这种设计让使用者能够更灵活地在特定操作前后执行自定义的逻辑,从而扩展了 UniswapV4 的功能。
Hook Example — Limit Order Hook
接下来会用限价订单(Limit Order)的例子来说明 Hooks 的实际操作流程。在开始之前先简单解释在 UniswapV4 中 实现限价订单的原理。
UniswapV4 Limit Order 机制
UniswapV4 中实现限价订单的原理是通过将流动性添加(Add Liquidity)到特定价格区间,然后如果该区间的流动性被交换,则执行移除流动性(Remove Liquidity)操作来达成。
举个例子,假设我们在 ETH 的价格范围为 1900–2000 之间添加了流动性,然后当 ETH 价格从 1800 上涨到 2100 时。此时,我们之前在 1900–2000 价格区间内添加的 ETH 流动性已经全部被交换成 USDC(假设在 ETH-USDC Pool )。此刻移除了流动性就可以获得类似以当前价格 1900–2000 执行 ETH 市价订单的效果。
Limit Order Hook Contract
这个范例是来自UniswapV4 的 GitHub 提供。在这个范例中,Limit Order Hook 合约提供了两个 Hooks,分别是 afterInitialize 和 afterSwap。其中 afterInitialize 用于记录建立 Pool 时的价格区间(tick),以便在有人做 swap 之后确定哪些限价订单已经被匹配。
Place Order
当使用者需要下单时,Hook 合约会根据使用者指定的价格区间和数量执行添加流动性的操作。在限价订单的 Hook 合约中,你可以看到有 place() 函数。主要的逻辑是在获得锁定(Lock)后调用 lockAcquiredPlace() 函数来执行添加流动性的操作,这部分等同于下单一个限价订单。
afterSwap Hook
使用者完成在这个 Pool 内的 Swap Token 后,Pool 会调用Hook 合约的 afterSwap() 函数。afterSwap 的主要逻辑是将之前价格区间到目前价格区间之间已经执行过的下单操作进行移除流动性的动作。这样的行为等同于订单已经被执行(order filled)。
Limit Order Flow
以下是限价订单成交的流程示意图:
以上就是使用 Hook 机制来实现 Limit -Order 的整个流程。
Hook: Other features
Hooks 还有几个笔者在研究时觉得有趣的点,觉得值得提出来跟大家分享。
Hooks Contract Address Bit
判断是否需要执行 before/after 特定操作是由 Hook 合约地址的最左边的 1 个 byte 来决定的。1 个 byte 等于 8 个位元(bits),正好对应到 8 个额外的动作。Pool 会检查该动作的位元是否为 1,以确定是否应该调用 Hook 合约的相应 hook 函数。这同时也意味著 Hook 合约的地址需要按照特定的方式设计,并且不能随意选择合约地址作为 Hook 合约。这种设计主要目的是为了降低 Gas 的消耗,将成本转移到合约部署上,以实现更高效的操作。(PS: 实务上可以使用不同 CREATE2 salt 来暴力计算出符合条件的 contract address)
Dynamic Fee
除了能够在每个动作的前后执行额外的操作外,Hooks还支持动态手续费(dynamic fee)的实现。在建立 Pool 时,可以指定是否启用动态手续费。如果启用了动态手续费,在 Swap Token 时会调用 Hook 合约的 getFee() 函数。Hook合约可以根据当时的 Pool 状态来决定应该收取多少手续费。这种设计使得手续费的计算可以根据实际情况进行调整,提高了系统的灵活性。
Pool Creation
每个 Pool 在建立时需要决定 Hook 合约,之后不能更改(不过不同的 Pool 可以共用相同的 Hook 合约)。这主要是因为 Hooks 被视为组成 PoolKey 的一部分,PoolManager 使用 PoolKey 来识别对哪个 Pool 执行操作。即使资产相同,但如果 Hook 合约不同,则这将被视为不同的 Pool。这种设计确保了不同 Pool 的状态和操作可以被独立管理,并确保了 Pool 的一致性。但同时也因为 Pool 数量增多而增加路由(routing)的複杂性(也许 UniswapX 就是设计来解决这个问题的方式之一)。
TL;DR
UniswapV4 显然更加强调扩展整个 Uniswap 生态系统,将其打造成基础设施,以便更多服务能够建立在 Uniswap Pool 的基础上。这有助于增强 Uniswap 的竞争力,减少其他服务替代的风险,但是否能如预期那样取得成功,还需要进一步观察。其中一些亮点包括 Flash Accounting 和 EIP1153 的结合,我们相信未来将会有更多服务採用这些功能,并出现多种不同的应用场景。这就是 UniswapV4 的核心概念,我们希望这能让大家对 UniswapV4 的运作方式有更深入的了解。如果文章中有任何错误,欢迎指正,也欢迎一同讨论和交流意见。
最后感谢 Anton Cheng 以及 Ping Chen 帮忙 Review 文章和给出宝贵的意见!