窺探UniswapV4的核心機制

作者:林瑋宸(Albert Lin),發表於台北以太坊聚會

自從UniswapV4的宣布,這個Swap平台經歷了一個巨大的轉變。從一個Swap平台發展成了基礎設施服務提供者。特別是V4的Hooks功能,引起了廣泛的關注。經過一段時間的深入研究後我整理一些內容,希望能讓大家更了解這個改變以及實施方式。

UniswapV4的創新重點不在於改進多少AMM技術,更著重於擴展生態系統。具體來說,這次的創新包括以下幾個關鍵功能:

  • Flash Accounting

  • Singleton Contract

  • Hooks Architecture

在接下來的部分,我將會詳細解釋這些功能的意義以及它們的實作原理。

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文章和提供寶貴的意見!

Total
0
Shares
Related Posts