作者:林玮宸(AlbertLin),发表于台北以太坊聚会自从UniswapV4的宣布,这个Swap平台经历了一个巨大的转变。从一个Swap平台发展成了基础设施服务提供者。特别是V4的Hooks功能,引起了广泛的关注。经过一段时间的深入研究后我整理一些内容,希望能让大家更了解这个变革以及实施方式。UniswapV4的创新重点不在于改进多少AMM技术,更着重于扩展生态系统。具体来说,这次的创新包括以下几个关键功能:FlashAccountingSingletonContractHooksArchitecture在接下来的部分,我将会详细解释这些功能的意义以及它们的实现原理。FlashAccountingDoubleEntryBookkeepingUniswapV4采用了类似于复式簿记(DoubleEntryBookkeeping)的记录方式,来跟踪每一个操作对应的Token余额增减变化。这种复式簿记的记录方式要求每一笔交易都必须同时在多个账户中进行记录,并确保这些账户之间的资产价值保持平衡。举个例子,假设用户以100TokenA向Pool交换50TokenB,那么在账本中记录会是如下:USER:TokenA减少100单位(-100),而TokenB增加50单位(+50)。POOL:TokenA增加100单位(+100),而TokenB减少50单位(-50)。这种记录方式有助于确保交易的双方在交易过程中的资产变化都得到准确地追踪和记录,从而提高了交易的透明度和可靠性。这也是UniswapV4在FlashAccounting方面的创新之一。TokenDelta相关操作在UniswapV4中,主要操作都会采用这种记账方式,并在程序代码中使用一个名为lockState.currencyDelta[currency]的StorageVariable来记录Token余额的变化量。这个变化量的数值如果为正数,表示Token在池中预期增加的数量,反之则表示Token在池中预期减少的数量。另一种角度来看,如果数值为正,代表池中缺少的Token数量(预计要收到的Token数量),而数值为负则代表这个池中多余的Token数量(预计使用者要提领的Token数量)。以下列出了各种操作对Token变化量(TokenDelta)的影响:modifyPosition:表示执行添加/移除流动性(Add/Removeliquidity)的操作。对于添加流动性,使用加法更新Token变化量(表示预计添加到池中的TokenA)。对于移除流动性,使用减法更新Token变化量(表示预计从池中提取TokenB)。swap:表示执行Swap操作。以SwapTokenA到TokenB为例,使用加法更新TokenADelta,而使用减法更新TokenBDelta。settle:伴随将Token发送到池中的操作。池子会计算前后Token的增加量,使用减法更新TokenDelta。
若池子恰好收到预期中的Token数量,则这里的减法更新将TokenDelta归零。take:伴随将Token从池中提领的操作。池子会使用加法更新TokenDelta,表示Token已经从这个池中移出。mint:更新TokenDelta的行为与"take"相似,只是铸造并不实际从池中提领Token。取而代之,发行对应的ERC1155Token作为提领的证明,而Token仍然保留在池中。之后,用户可以通过销毁ERC1155Token来取回池中的Token。猜测其目的有两点:1.节省ERC20Token转移的gas成本(contractcall+少一次storagewrite),未来利用ERC1155tokenburn的方式更新TokenDelta来供交易使用。2.将流动性保留在池中,维持流动性深度让使用者有更好的SwapToken体验。donate:宣告将Token捐赠给池,但实际上仍需要使用"settle"将Token送入池中。因此,在这里使用加法更新Token变化量。以上操作只有结算和提取会有实际传送Token的行为,其他操作只是单纯去更新TokenDelta数值。TokenDelta示例以下我们用一个简单的例子来说明实际如何去更新TokenDelta。假设今天我们将100个TokenA兑换为50个TokenB:交易开始前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:Transientstorageopcodes之前提到UniswapV4利用StorageVariable来记录TokenDelta,但在合约内部,StorageVariable的读写是相当高成本的。这时候就要提到另一个Uniswap所推出来的EIP:EIP1153—TransientStorageOpcodes。
UniswapV4计划使用EIP1153所提供的TSTORE和TLOAD这两个OPCode来更新TokenDelta。采用TransientStorageOpcodes的StorageVariable会在Transaction结束后被丢弃(类似MemoryVariable),从而不必写入硬盘,进而降低Gas费用。EIP1153已被确定会被包含在下次的坎昆升级,同时UniswapV4也指出将会在坎昆升级之后上线UniswapV4。FlashAccounting—LockUniswapV4引入了lock机制,这意味着在进行Pool操作之前,必须首先调用PoolManager.lock()以获取一个锁(Lock)。在lock()的执行结束前,会检查TokenDelta的数值是否为0,否则将引发revert。当调用PoolManager.lock()并成功获取锁之后,将会调用msg.sender的lockAcquired()函数。在lockAcquired()函数中,才执行与Pool相关的操作(例如swap、modifyPosition等操作)。以下以图示为例来说明这个过程。当用户需要进行TokenSwap操作时,必须调用一个具有lockAcquired()函数的智能合约(这里称为回调合约,CallBackContract)。回调合约将首先调用PoolManager.lock(),然后PoolManager会调用回调合约的lockAcquired()函数。在lockAcquired()函数中,定义了与Pool操作相关的逻辑,例如swap、settle以及take等操作。最后,在整个lock()即将结束时,PoolManager会检查与这次操作有关的TokenDelta是否已经全部重置为0,以确保Pool中的资产保持平衡。SingletonContractSingletonContract意味着UniswapV4已经废弃了以往的Factory-Pool模式。每个Pool不再是一个独立的智能合约,而是所有Pool共用同一个单例(singleton)合约。这种设计与FlashAccounting机制结合,只需要更新必要的StorageVariable,进一步降低了操作的复杂性和成本。以下以图示为例,以UniswapV3为例,将ETH兑换为DAI至少需要执行四次Token转移(Storage写入操作)。这包括对USDC、USDT和DAIToken的多次变化记录。然而,通过UniswapV4的改进,搭配FlashAccounting机制,只需要一次Token转移(将DAI由Pool转移到用户),这大幅降低了操作的次数和成本。HooksArchitectureUniswapV4这次的更新中,最引人注目的要属HooksArchitecture。这项更新将围绕在Pool可利用性上提供了极大的灵活性。Hooks是指在对Pool执行特定操作时,会额外调用HooksContract来执行额外的动作。而这些动作可以分为不同类别,包括initialize(createpool)、modifyPosition(add/removeliquidity)、swap和donate,每个类别都有执行前和执行后的动作:beforeInitialize/afterInitializebeforeModifyPosition/afterModifyPositionbeforeSwap/afterSwapbeforeDonate/afterDonate这种设计让使用者能够更灵活地在特定操作前后执行自定义的逻辑,从而扩展了UniswapV4的功能。
HookExample—LimitOrderHook接下来会用限价订单(LimitOrder)的例子来说明Hooks的实际操作流程。在开始之前先简单解释在UniswapV4中实现限价订单的原理。UniswapV4LimitOrder机制UniswapV4中实现限价订单的原理是通过将流动性添加(AddLiquidity)到特定价格区间,然后如果该区间的流动性被交换,则执行移除流动性(RemoveLiquidity)操作来达成。举个例子,假设我们在ETH的价格范围为1900–2000之间添加了流动性,然后当ETH价格从1800上涨到2100时。此时,我们之前在1900–2000价格区间内添加的ETH流动性已经全部被交换成USDC(假设在ETH-USDCPool)。此刻移除了流动性就可以获得类似以当前价格1900–2000执行ETH市价订单的效果。LimitOrderHookContract这个示例来自UniswapV4的GitHub提供。在这个示例中,LimitOrderHook合约提供了两个Hooks,分别是afterInitialize和afterSwap。其中afterInitialize用于记录建立Pool时的价格区间(tick),以便在有人做swap之后确定哪些限价订单已经被匹配。PlaceOrder当用户需要下单时,Hook合约会根据用户指定的价格区间和数量执行添加流动性的操作。在限价订单的Hook合约中,你可以看到有place()函数。主要的逻辑是在获得锁定(Lock)后调用lockAcquiredPlace()函数来执行添加流动性的操作,这部分等同于下单一个限价订单。afterSwapHook用户完成在这个Pool内的SwapToken后,Pool会调用Hook合约的afterSwap()函数。afterSwap的主要逻辑是将之前价格区间到目前价格区间之间已经执行过的下单操作进行移除流动性的动作。这样的行为等同于订单已经被执行(orderfilled)。LimitOrderFlow以下是限价订单成交的流程示意图:1.订单下单者将订单发送给Hook合约。2.Hook合约根据订单信息执行添加流动性操作。3.一般用户在Pool中进行SwapToken操作。4.SwapToken操作完成后,Pool会调用Hook合约的afterSwap()函数。5.Hook合约根据SwapToken的价格区间变化,执行已成交限价订单的移除流动性操作。以上就是使用Hook机制来实现Limit-Order的整个流程。Hooks:OtherfeaturesHooks还有几个笔者在研究时觉得有趣的点,觉得值得提出来跟大家分享。
HooksContractAddressBit判断是否需要执行before/after特定操作是由Hook合约地址的最左边的1个byte来决定的。1个byte等于8个位元(bits),正好对应到8个额外的动作。Pool会检查该动作的位元是否为1,以确定是否应该调用Hook合约的相应hook函数。这同时也意味着Hook合约的地址需要按照特定的方式设计,并且不能随意选择合约地址作为Hook合约。这种设计主要目的是为了降低Gas的消耗,将成本转移到合约部署上,以实现更高效的操作。(PS:实际上可以使用不同CREATE2salt来暴力计算出符合条件的contractaddress)DynamicFee除了能够在每个动作的前后执行额外的操作外,Hooks还支持动态手续费(dynamicfee)的实现。在建立Pool时,可以指定是否启用动态手续费。如果启用了动态手续费,在SwapToken时会调用Hook合约的getFee()函数。Hook合约可以根据当时的Pool状态来决定应该收取多少手续费。这种设计使得手续费的计算可以根据实际情况进行调整,提高了系统的灵活性。PoolCreation每个Pool在建立时需要决定Hook合约,之后不能更改(不过不同的Pool可以共用相同的Hook合约)。这主要是因为Hooks被视为组成PoolKey的一部分,PoolManager使用PoolKey来识别对哪个Pool执行操作。即使资产相同,但如果Hook合约不同,则这将被视为不同的Pool。这种设计确保了不同Pool的状态和操作可以被独立管理,并确保了Pool的一致性。但同时也因为Pool数量增多而增加路由(routing)的复杂性(也许UniswapX就是设计来解决这个问题的方式之一)。TL;DRFlashAccounting用于跟踪每个Token的数量变化,确保在完成交易后所有变化都被归零。为了节省Gas费用,FlashAccounting使用了EIP1153提供的特殊存储方式。SingletonContract的设计有助于减少Gas消耗,因为它避免了对多个存储变量的更新。Hooks架构提供了额外的操作,分为“预执行”和“后执行”阶段。这使得每个Pool操作可以更为灵活,但也使得Pool的路由变得更加复杂。UniswapV4显然更加强调扩展整个Uniswap生态系统,将其打造成基础设施,以便更多服务能够建立在UniswapPool的基础上。这有助于增强Uniswap的竞争力,减少其他服务替代的风险,但是否能如预期那样取得成功,还需要进一步观察。一些亮点包括FlashAccounting和EIP1153的结合,未来预计会有更多服务采用这些功能,并出现多种不同的应用场景。UniswapV4的核心概念是为了让大家更深入地了解其运作方式。如果文章中有任何错误,欢迎指正,也欢迎一同讨论和交流意见。最后感谢AntonCheng和PingChen帮忙Review文章和提供宝贵的意见!