窥探UniswapV4的核心机制

作者:林玮宸(Albert Lin),发表于台北以太坊聚会 

自从UniswapV4的宣布,这个Swap平台经历了一个巨大的转变。从一个Swap平台发展成了基础设施服务提供者。特别是V4的Hooks功能,引起了广泛的关注。经过一段时间的深入研究后我整理一些内容,希望能让大家更了解这个变革以及实施方式。

UniswapV4的创新重点不在于改进多少AMM技术,更着重于扩展生态系统。具体来说,这次的创新包括以下几个关键功能:

  • Flash Accounting

  • Singleton Contract

  • Hooks Architecture

在接下来的部分,我将会详细解释这些功能的意义以及它们的实现原理。

wY52QFjrKIpW0ubBWJNetoaha5c8TP8LAV518VlP.png

Flash Accounting

Double Entry Bookkeeping

UniswapV4采用了类似于复式簿记(Double Entry Bookkeeping)的记录方式,来跟踪每一个操作对应的Token余额增减变化。这种复式簿记的记录方式要求每一笔交易都必须同时在多个账户中进行记录,并确保这些账户之间的资产价值保持平衡。举个例子,假设用户以100 TokenA向Pool交换50 TokenB,那么在账本中记录会是如下:

USER:TokenA减少100单位(-100),而TokenB增加50单位(+50)。

POOL:TokenA增加100单位(+100),而TokenB减少50单位(-50)。

这种记录方式有助于确保交易的双方在交易过程中的资产变化都得到准确地追踪和记录,从而提高了交易的透明度和可靠性。这也是UniswapV4在Flash Accounting方面的创新之一。

hnBvfdRQ8zM8kTzOOA4ptSZ0TrHmUPYVwLj94ktu.png

Token Delta 相关操作

在 UniswapV4 中,主要操作都会采用这种记账方式,并在程序代码中使用一个名为 lockState.currencyDelta[currency] 的 Storage Variable 来记录 Token 余额的变化量。这个变化量的数值如果为正数,表示 Token 在池中预期增加的数量,反之则表示 Token 在池中预期减少的数量。另一种角度来看,如果数值为正,代表池中缺少的 Token 数量(预计要收到的 Token 数量),而数值为负则代表这个池中多余的 Token 数量(预计使用者要提领的 Token 数量)。以下列出了各种操作对 Token 变化量(TokenDelta)的影响:

L6RwpkurMBBfkadbOJilOmnJRQF6d4GaX499zQMb.png

  • modifyPosition:表示执行添加/移除流动性(Add/Remove liquidity)的操作。对于添加流动性,使用加法更新Token变化量(表示预计添加到池中的TokenA)。对于移除流动性,使用减法更新Token变化量(表示预计从池中提取TokenB)。

  • swap:表示执行Swap操作。以Swap TokenA到TokenB为例,使用加法更新TokenADelta,而使用减法更新TokenBDelta。

  • settle:伴随将Token发送到池中的操作。池子会计算前后Token的增加量,使用减法更新TokenDelta。若池子恰好收到预期中的Token数量,则这里的减法更新将TokenDelta归零。

  • take:伴随将Token从池中提领的操作。池子会使用加法更新TokenDelta,表示Token已经从这个池中移出。

  • mint:更新TokenDelta的行为与"take"相似,只是铸造并不实际从池中提领Token。取而代之,发行对应的ERC1155 Token作为提领的证明,而Token仍然保留在池中。之后,用户可以通过销毁ERC1155 Token来取回池中的Token。猜测其目的有两点:1. 节省ERC20 Token转移的gas成本(contract call + 少一次storage write),未来利用ERC1155 token burn的方式更新TokenDelta来供交易使用。2. 将流动性保留在池中,维持流动性深度让使用者有更好的Swap Token体验。

  • donate:宣告将Token捐赠给池,但实际上仍需要使用"settle"将Token送入池中。因此,在这里使用加法更新Token变化量。

以上操作只有结算和提取会有实际传送Token的行为,其他操作只是单纯去更新TokenDelta数值。

Token Delta示例

以下我们用一个简单的例子来说明实际如何去更新TokenDelta。假设今天我们将100个TokenA兑换为50个TokenB:

inwSCzLQ1TNWwBFJxaUYiG4TkC131Vmz1IUpBmHk.png

交易开始前TokenADelta和TokenBDelta都为0。

swap:计算Pool需要接收多少TokenA,以及用户将收到多少TokenB。此时,TokenADelta = 100,TokenBDelta = -50。

settle:将100个TokenA送入Pool,并更新TokenADelta = 100–100 = 0。

take:将50个TokenB从Pool转移到用户帐户,并更新TokenBDelta = -50 + 50 = 0。

交易结束后TokenADelta和TokenBDelta都为0。

当整个兑换操作完成后,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。

U954ovI1ijl114upkq8m6FV1jQdJbREaQMbdV2eD.png

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()函数的智能合约(这里称为回调合约,CallBack Contract)。回调合约将首先调用PoolManager.lock(),然后PoolManager会调用回调合约的lockAcquired()函数。在lockAcquired()函数中,定义了与Pool操作相关的逻辑,例如swap、settle以及take等操作。最后,在整个lock()即将结束时,PoolManager会检查与这次操作有关的TokenDelta是否已经全部重置为0,以确保Pool中的资产保持平衡。

ORbR8KIOQvNlX1ynIcPJ0GlgGRQarT2yhVFUGltI.png

Singleton Contract

Singleton Contract意味着UniswapV4已经废弃了以往的Factory-Pool模式。每个Pool不再是一个独立的智能合约,而是所有Pool共用同一个单例(singleton)合约。这种设计与Flash Accounting机制结合,只需要更新必要的Storage Variable,进一步降低了操作的复杂性和成本。

以下以图示为例,以UniswapV3为例,将ETH兑换为DAI至少需要执行四次Token转移(Storage写入操作)。这包括对USDC、USDT和DAI Token的多次变化记录。然而,通过UniswapV4的改进,搭配Flash Accounting机制,只需要一次Token转移(将DAI由Pool转移到用户),这大幅降低了操作的次数和成本。

Unl1TmglZkC4mOKGGDn9PGunuOmpEwD3ZDuvJAQE.png

Hooks Architecture

UniswapV4这次的更新中,最引人注目的要属Hooks Architecture。这项更新将围绕在Pool可利用性上提供了极大的灵活性。Hooks是指在对Pool执行特定操作时,会额外调用Hooks Contract来执行额外的动作。而这些动作可以分为不同类别,包括initialize(create pool)、modifyPosition(add/remove liquidity)、swap和donate,每个类别都有执行前和执行后的动作:

beforeInitialize / afterInitialize

beforeModifyPosition / afterModifyPosition

beforeSwap / afterSwap

beforeDonate / afterDonate

这种设计让使用者能够更灵活地在特定操作前后执行自定义的逻辑,从而扩展了UniswapV4的功能。

610LLQpevNyukt0Z4RjsCArR4WGsfvX8MJjGDZT9.png

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市价订单的效果。

tGpq7NKqQDkKrKa7sLZdWDHIhVOokvCBUPL35Nx7.png

Limit Order Hook Contract

这个示例来自UniswapV4的GitHub提供。在这个示例中,Limit Order Hook合约提供了两个Hooks,分别是afterInitialize和afterSwap。其中afterInitialize用于记录建立Pool时的价格区间(tick),以便在有人做swap之后确定哪些限价订单已经被匹配。

Place Order

当用户需要下单时,Hook合约会根据用户指定的价格区间和数量执行添加流动性的操作。在限价订单的Hook合约中,你可以看到有place()函数。主要的逻辑是在获得锁定(Lock)后调用lockAcquiredPlace()函数来执行添加流动性的操作,这部分等同于下单一个限价订单。

HX2t6IAjpPjXIDztPN5TnDe2jn8A6lc9k1OEyWAY.png

afterSwap Hook

用户完成在这个Pool内的Swap Token后,Pool会调用Hook合约的afterSwap()函数。afterSwap的主要逻辑是将之前价格区间到目前价格区间之间已经执行过的下单操作进行移除流动性的动作。这样的行为等同于订单已经被执行(order filled)。

PLhQAEEvaJ0x7tYHUWZdB2Nycivbgf24RSmV0xjI.png

Limit Order Flow

以下是限价订单成交的流程示意图:

MaQuXsWP4k0VeF3DbEUlYB3UPo74UvdQKw9UD8cQ.png

1.订单下单者将订单发送给Hook合约。

2.Hook合约根据订单信息执行添加流动性操作。

3.一般用户在Pool中进行Swap Token操作。

4.Swap Token操作完成后,Pool会调用Hook合约的afterSwap()函数。

5.Hook合约根据Swap Token的价格区间变化,执行已成交限价订单的移除流动性操作。

以上就是使用Hook机制来实现Limit -Order的整个流程。

Hooks: 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)

4fXGDcBz5qFCxZFf37uLQP3ujnFTSVNprwuVqLWc.png

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就是设计来解决这个问题的方式之一)。

hlB99b9Gjk7TdOevtYoiYbZS43HM91j5Z1Iq5Dxx.png

TL;DR

  • Flash Accounting用于跟踪每个Token的数量变化,确保在完成交易后所有变化都被归零。为了节省Gas费用,Flash Accounting使用了EIP1153提供的特殊存储方式。

  • Singleton Contract的设计有助于减少Gas消耗,因为它避免了对多个存储变量的更新。

  • Hooks架构提供了额外的操作,分为“预执行”和“后执行”阶段。这使得每个Pool操作可以更为灵活,但也使得Pool的路由变得更加复杂。

UniswapV4显然更加强调扩展整个Uniswap生态系统,将其打造成基础设施,以便更多服务能够建立在Uniswap Pool的基础上。这有助于增强Uniswap的竞争力,减少其他服务替代的风险,但是否能如预期那样取得成功,还需要进一步观察。一些亮点包括Flash Accounting和EIP1153的结合,未来预计会有更多服务采用这些功能,并出现多种不同的应用场景。UniswapV4的核心概念是为了让大家更深入地了解其运作方式。如果文章中有任何错误,欢迎指正,也欢迎一同讨论和交流意见。

最后感谢Anton Cheng和Ping Chen帮忙Review文章和提供宝贵的意见!

如有疑问联系邮箱:
*本文转载自网络转载,版权归原作者所有。本站只是转载分享,不代表赞同其中观点。请自行判断风险,本文不构成投资建议。*