窺探 UniswapV4 的核心機制

林瑋宸 Albert Lin
Taipei Ethereum Meetup
16 min readOct 4, 2023

--

自從 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)。

Token Delta 相關操作

在 UniswapV4 中,主要操作都會採用這種記帳方式,並在程式碼中使用一個名為 lockState.currencyDelta[currency] 的 Storage Variable 來記錄 Token 餘額的變化量。這個變化量的數值如果為正數,表示 Token 在池中預期增加的數量,反之則表示 Token 在池中預期減少的數量。另一個角度來看,如果數值為正,代表池中缺少的 Token 數量(預計要收到的 Token 數量),而數值為負則代表這個池中多餘的 Token 數量(預計使用者要提領的 Token 數量)。以下列出了各種操作對 Token 變化量(TokenDelta)的影響:

  • modifyPosition:表示執行 Add/Remove liquidity 的操作。對於 Add liquidity,使用加法更新 Token 變化量(表示預計添加到池中的 TokenA)。對於 Remove liquidity,則使用減法更新 Token 變化量(表示預計從池中提取 TokenB)。
  • swap:表示執行 Swap 操作。以 Swap TokenA 到 TokenB 為例,使用加法更新 TokenADelta,而使用減法更新 TokenBDelta。
  • settle:伴隨將 Token 傳送到 Pool 的操作。Pool 會計算前後 Token 的增加量,使用減法更新 TokenDelta。若 Pool 剛好收到預期中的 Token 數量,則這裡的減法更新剛好將 TokenDelta 歸零。
  • take:伴隨將 Token 從 Pool 中提領的操作。Pool 會使用加法更新 TokenDelta,表示 Token 已經從這個 Pool 中移出。
  • mint:更新 TokenDelta 的行為與 "take" 相似,只是 mint 並不實際從池中提領 Token。取而代之發行對應的 ERC1155 Token 作為提領的證明,而 token 仍然保留在池中。之後,用戶可以通過銷毀 ERC1155 Token 來取回 Pool 中的 Token。猜測其目的有兩點:1.節省 ERC20 Token 轉移的 gas 成本(contract call +少一次 storage write),未來利用 ERC1155 token burn 的方式更新 TokenDelta 來供交易使用。2. 將流動性保留在 Pool 中,維持流動性深度讓使用者有更好的 Swap Token 體驗。
  • donate:宣告將 Token 捐贈給 Pool,但實際上仍需要使用 "settle" 將 Token 送入 Pool 中。因此,在這裡使用加法更新 Token 變化量。

以上操作只有 settle 和 take 會有實際傳送 Token 的行為,其他操作只是單純去更新 TokenDelta 數值。

Token Delta Example

以下我們用一個簡單的例子來說明實際是如何去更新 TokenDelta 。假設今天我們將 100 個 TokenA 兌換為 50 個 TokenB:

  1. 交易開始前 TokenADelta 和 TokenBDelta 都為 0。
  2. swap:計算 Pool 需要接收多少TokenA,以及使用者將收到多少TokenB。此時,TokenADelta = 100,TokenBDelta = -50。
  3. settle:將 100 個 TokenA 送入Pool ,並更新 TokenADelta = 100–100 = 0。
  4. take:將 50 個 TokenB 從 Pool 轉移到使用者帳戶,並更新 TokenBDelta = -50 + 50 = 0。
  5. 交易結束後 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 所提供的 TSTORETLOAD 這兩個 OP Code 來更新 TokenDelta。採用 Transient Storage Opcodes 的 Storage Variable 會在 Transaction 結束後被丟棄(類似 Memory Variable),從而不必寫入硬碟,進而降低 Gas 費用。

EIP1153 已被確定會被包含在下次的坎昆升級,同時 UniswapV4 也指出將會在坎昆升級之後上線 UniswapV4。

source: https://etherworld.co/2022/12/13/transient-storage-for-beginners/

Flash Accounting — Lock

UniswapV4 引入了 lock 機制,這意味著在進行 Pool 操作之前,必須首先調用 PoolManager.lock() 以獲取一個鎖(Lock)。在 lock() 的執行結束前,會檢查 TokenDelta 的數值是否為 0,否則將引發 revert。當調用 PoolManager.lock() 並成功獲得鎖之後,將會呼叫 msg.senderlockAcquired() 函數。在 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 轉移到使用者),這大幅降低了操作的次數和成本。

source: https://twitter.com/Uniswap/status/1671208668304486404

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 的功能。

source: https://github.com/Uniswap/v4-core/blob/main/whitepaper-v4-draft.pdf

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() 函數來執行添加流動性的操作,這部分等同於下單一個限價訂單。

source: https://github.com/Uniswap/v4-periphery/blob/main/contracts/hooks/examples/LimitOrder.sol#L246

afterSwap Hook

使用者完成在這個 Pool 內的 Swap Token 後,Pool 會調用Hook 合約的 afterSwap() 函數。afterSwap 的主要邏輯是將之前價格區間到目前價格區間之間已經執行過的下單操作進行移除流動性的動作。這樣的行為等同於訂單已經被執行(order filled)。

source: https://github.com/Uniswap/v4-periphery/blob/main/contracts/hooks/examples/LimitOrder.sol#L192

Limit Order Flow

以下是限價訂單成交的流程示意圖:

  1. 訂單下單者將訂單發送給 Hook 合約。
  2. Hook 合約根據訂單信息執行添加流動性操作。
  3. 一般用戶在 Pool 中進行 Swap Token 操作。
  4. Swap Token 操作完成後,Pool 會調用 Hook 合約的 afterSwap() 函數。
  5. Hook 合約根據 Swap Token 的價格區間變化,執行已成交限價訂單的移除流動性操作。

以上就是使用 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

  • Flash Accounting 用來追蹤每個 Token 的數量變化,確保在完成交易後所有變化都被歸零。為了節省 Gas 費用,Flash Accounting 使用了 EIP1153 提供的特殊儲存方式。
  • Singleton Contract 的設計有助於減少 Gas 消耗,因為它避免了對多個儲存變數的更新。
  • Hooks 架構則提供了額外的操作,分為 “預執行” 和 “後執行” 階段。這讓每個 Pool 操作可以更為彈性,但也使得 Pool 的 routing 變得更加複雜。

UniswapV4 顯然更加強調擴展整個 Uniswap 生態系統,將其打造成基礎設施,以便更多服務能夠建立在 Uniswap Pool 的基礎上。這有助於增強 Uniswap 的競爭力,減少其他服務替代的風險,但是否能如預期那樣取得成功,還需要進一步觀察。其中一些亮點包括 Flash Accounting 和 EIP1153 的結合,我們相信未來將會有更多服務採用這些功能,並出現多種不同的應用場景。這就是 UniswapV4 的核心概念,我們希望這能讓大家對 UniswapV4 的運作方式有更深入的了解。如果文章中有任何錯誤,歡迎指正,也歡迎一同討論和交流意見。

最後感謝 Anton Cheng 以及 Ping Chen 幫忙 Review 文章和給出寶貴的意見!

--

--