Uniswap v3 設計原理詳解(一):掌握 Uniswap 的核心技術!

剛看完 Uniswap v2 的代碼,本來打算寫一個 Uniswap v2 設計與實現,結果 Uniswap v3 就發布了。趁著這個機會就先寫一個 Uniswap v3 設計與實現吧。

因為 v3 版本的實現復雜度和 v2 已經不在一個量級了,難免會有理解上的偏差,本文權當是拋磚引玉,也希望有更多的人參與討論。因為實現比較復雜,本系列會拆分成多篇文章,持續更新。

本文假定讀者都能理解 AMM 的基本概念,并且閱讀過 v3 的實現細節(最好讀過白皮書)來撰寫的,因此不會具體的解釋每一個概念的實現邏輯。

設計原理

官方的白皮書已經比較詳盡的描述了 v3 的設計原理,這里僅對白皮書中的內容做一些補充,包含本人對其中一些機制的理解和思考。

Uniswap v2 版本使用 x⋅y=kx⋅y=k 這樣一個簡潔的公式實現了 AMM Dex,正是由于其簡潔易用性,使其在短短的一年的時間內迅速成長為 DeFi 領域的龍頭項目。但是隨著 DeFi 生態走過了「從無到有」的階段,因為 v2 無法滿足某些特定需求,從而誕生了 Curve, Balancer 這些針對某些功能進行改進的 AMM。

簡單來說,官方認為 v2 版本最大的痛點是資金利用率(Capital Efficiency)太低,v3 版本在解決這個問題的同時,還帶了了新的改進,總體總結如下:

可靈活選擇價格區間提供流動性

更好用的預言機

order book 功能

靈活的費率

提升資金利用率

解決資金利用問題之前,我們可以觀察到大部分的交易對的價格,在大部分時間內都只是在一個固定范圍內波動。例如 ETH/DAI 交易對,在近一個月時間內都是在 1300 ~ 2200 DAI/ETH 這個范圍內波動。更極端的例子是 DAI/USDC 這樣的穩定幣交易對,在大部分時間內都只是在 1.001 ~ 1.002 DAI/USDC 范圍內波動。

v2 的問題

我們先來看一看 v2 版本的資金利用率是怎樣的,假設 ETH/DAI 交易對的實時價格為 1500 DAI/ETH,交易對的流動性池中共有資金:4500 DAI 和 3 ETH,根據 x⋅y=kx⋅y=k,可以算出池內的 k 值:

k=4500×3=13500k=4500×3=13500

假設 xx 表示 DAI,yy 表示 ETH,即初始階段 x1=4500,y1=3×1=4500,y1=3,當價格下降到 1300 DAI/ETH 時:

{x2⋅y2=13500x2y2=1300{x2⋅y2=13500x2y2=1300

得出 x2=4192.54,y2=3.22×2=4192.54,y2=3.22,資金利用率為:Δxx1=6.84%Δxx1=6.84%。同樣的計算方式,當價格變為 2200 DAI/ETH 時,資金利用率約為 21.45%.

也就是說,在大部分的時間內池子中的資金利用與低于 25%. 這個問題對于穩定幣池來說更加嚴重。

解決方案

v3 版本的解決方案是允許用戶只在一段價格區間內提供流動性。如下圖:

Uniswap v3 詳解(一):設計原理

此圖展示了一個 x⋅y=kx⋅y=k 的函數曲線圖。為了滿足讓用戶可以選擇只在 [a,b][a,b] 價格區間內提供流動性。對于圖中 [a,b][a,b] 區間的任意點,都有:

x=xvirtual+xrealy=yvirtual+yrealx=xvirtual+xrealy=yvirtual+yreal

其中 xreal,yrealxreal,yreal 分別表示用戶提供的 x token, y token 數量,xvirtual,yvirtualxvirtual,yvirtual 分別表示流動池虛擬出的 x token y token 數量。當流動池的價格來到用戶設置的零界點時(例如圖中的 a 點或者 b 點),用戶實際提供的 x token 或者 y token 將為 0,x 或 y 將完全由虛擬 token 組成。當價格進一步變動,移動到用戶設定的價格區間之外時,流動池將移除這部分流動性,以保證虛擬的 x token 或 y token 數量不會減少,因此這部分虛擬的 token 只會在價格處于設定的區間內時參與價格的計算,而不會真的參與流動性提供。

例如,當價格到達 a 點時,用戶的所有資金轉換為 x,此時 yreal=0,y=yvirtualyreal=0,y=yvirtual,當價格繼續降低時,流動池將移除這部分流動性。用戶的資金狀態將停留在 a 點,直至價格再次回到 a 點并進入 [a,b][a,b] 價格區間。

通過這樣的設計,用戶的資金只會在 [a,b][a,b]價格區間內提供流動性,并且因為虛擬 token xvirtual,yvirtualxvirtual,yvirtual 的參與,這部分流動性也滿足 x⋅y=kx⋅y=k 公式,計算價格的方式并沒有產生變化。

Uniswap v3 詳解(一):設計原理

上圖展示了用戶選擇在價格 [a,b][a,b] 之間提供流動性時,通過虛擬 token 的參與,將曲線 f(real)f(real) (橘紅色)向右上方移動至 f(virtual)f(virtual)(綠色),實現了價格計算的一致性(即滿足x⋅y=kx⋅y=k)。

交易過程

v2 版本

在 v2 版本中,用戶與一個交易對發生交易時,假設用戶提供 x token,資金量為 ΔxΔx,AMM 需要計算出用戶可以得到的 y token,即 ΔyΔy. 如下圖所示,池中資金從 a 點隨著曲線移動到 b 點:

Uniswap v3 詳解(一):設計原理

可以用過下面步驟計算 ΔyΔy:

x⋅y=(x+Δx)(y−Δy)=kx⋅y=(x+Δx)(y−Δy)=k

計算出:

Δy=y−x⋅yx+Δx=Δxyx+ΔxΔy=y−x⋅yx+Δx=Δxyx+Δx

具體的實現,可以參考 v2 代碼實現。

v3 版本

在 v3 版本中,因為一個交易池中會有多個不同深度的流動池(每一個可以單獨設置交易價格區間),因此一次交易的過程可能跨越多個不同的深度:

Uniswap v3 詳解(一):設計原理

如上圖最右邊所示,當價格變化時,流動池中的總流動性也會隨之變化。因此 v3 版本流動池中資金的關系不能像 v2 版本一樣用一個平滑的 bonding curve 曲線來表示。那麼如何計算交易結果呢?

因為 v3 版本交易可能并不在一個平滑的曲線中進行,需要根據池中資金的價格,選用不用的流動性來進行計算。流動性可以用 kk 表示,即 k=x⋅yk=x⋅y,用 PP 表示 xx 的價格,即 P=yxP=yx

因為每一次的價格變動都可能會引起流動性的變化,v3 需要圍繞價格來進行交易相關的計算,例如當使用 x token 交換 y token 時:

交易至指定價格(通常是某一個流動性的價格邊界)PP,需要的 x token 數 ΔxΔx,可以獲得的 y token 數 ΔyΔy

給定 x token 數 ΔxΔx(假設此交易不會引起流動性發生變化),可以獲得的 y token 數 ΔyΔy,以及最終的價格 PP

當 k 值不變時,根據定義:

{x⋅y=kP=yx{x⋅y=kP=yx

可以推導出:

⎧⎩⎨⎪⎪x=kP−−√y=kP−−−√⇒⎧⎩⎨Δx=Δ1P−−√⋅k−−√Δy=ΔP−−√⋅k−−√{x=kPy=kP⇒{Δx=Δ1P⋅kΔy=ΔP⋅k

這樣一來計算過程并不需要關注池中的 x token 和 y token 余額,通過 kk 值和價格 PP 就可以完成交易過程的計算。

為了減少計算過程中的開根號運算,v3 合約直接存儲 P−−√P 的值,同時合約中沒有存儲 kk 的值而是存儲 L=k−−√L=k,通過 LL 來表示池中當前的流動性大小(存儲 LL 還有一個好處是減少溢出的可能性)。

在實際交易過程中,一個交易可能跨越不同的流動性階段,因此合約需要維護每個用戶提供流動性的價格邊界,當價格到達邊界時,需要增加或移除對應流動性。通過分段計算的方式完成交易結果的計算,具體的實現過程會在后面的代碼分析中講解。

價格精度問題

因為用戶可以在任意 [P0,P1][P0,P1] 價格區間內提供流動性,Uniswap v3 需要保存每一個用戶提供流動性的邊界價格,即 P0P0 和 P1P1。這樣就引入了一個新的問題,假設兩個用戶提供的流動性價格下限分別是 5.00000001 和 5.00000002,那麼 Uniswap 需要標記價格為 5.00000001 和 5.00000002 的對應的流動性大小。同時當交易發生時,需要將 [5.00000001,5.00000002][5.00000001,5.00000002] 作為一個單獨的價格區間進行計算。這樣會導致:

幾乎很難有兩個流動性設置相同的價格邊界,這樣會導致消耗大量合約存儲空間保存這些狀態

當進行交易計算時,價格變化被切分成很多個小的范圍區間,需要逐一分段進行計算,這會消耗大量的 gas,并且如果范圍的價差太小,可能會引發計算精度的問題

Uniswap v3 解決這個問題的方式是,將 [Pmin,Pmax][Pmin,Pmax] 這一段連續的價格范圍為,分割成有限個離散的價格點。每兩個相鄰的價格區間稱為一個 tick,用戶在設置流動性的價格區間時,只能選擇這些離散的價格點中的某一個作為流動性的邊界價格。

Uniswap v3 采用了等比數列的形式確定價格數列,公比為 1.0001。即下一個價格點為當前價格點的 100.01%,前面我們說過 Uniswap v3 實際存儲的是 P−−√P,那麼下一個價格與當前價格的關系為

Pnext−−−−√=1.0001−−−−−√⋅Pcurrent−−−−−−√Pnext=1.0001⋅Pcurrent

如此一來 Uniswap v3 可以提供比較細粒度的價格選擇范圍(每個可選價格之間的差值為 0.01%),同時又可以將計算的復雜度控制在一定范圍內。

tick 管理

簡單說,一個 tick 就代表 Uniswap 價格的等比數列中的某一個價格,因此每一個用戶提供的流動性的價格邊界可以用 ticklowerticklower 和 tickuppertickupper 來表示。為了計算的方便,對于每一個交易對,uni 都定義有一個價格為 1 的 tick。將所有 tick 通過索引來表示,定義整數 ii 表示 tick 的索引:

i=log1.0001√p–√i=log1.0001⁡p

這樣一來,只需要通過整數索引 ii 就能找到對應的 tick,并且 ii 為 0 時價格為 1.

Uniswap 不需要記錄每個 tick 所有的信息,只需要記錄所有作為 upper/lower tick 所包含的流動性元數據即可。看下面這個例子:

Uniswap v3 詳解(一):設計原理

兩個用戶分別在 [a,c][a,c] 和 [b,d][b,d] 兩個區間提供了流動性 L1L1 和 L2L2,對于 Uniswap 來說它會在 a, b, c, d 四個 tick 上記錄對應的流動性增減情況。例如當價格從圖中從左向右移動時,代幣池的流動性需要做對應的增減(即從左側 tick 進入一個流動性時增加流動性,移出流動性的右側 tick 時減去相應的流動性)。

靈活的手續費選擇

v3 版本內置了三種梯度的手續費率(0.05%, 0.30%, and 1.00%),同時可以在未來增加更多的費率值。關于手續費的計算過程,這部分放在后文來詳解(鏈接:交易手續費)。需要注意的是,由于需要支持多種費率,同一個代幣對 v3 版本會有多個不同的流動池。例如 ETH/DAI 代幣對,會分成三個池,分別對應 0.05%, 0.30%, 1.00% 的手續費。

更多的費率選擇性,這樣做會更加靈活,但是同時也會帶來一定的流動性分裂,uni 官方表示后續可以通過治理添加更多的費率可選值,這也勢必會讓流動性更加分裂。那麼可能會出現一種情況是,即使是只使用 uniswap v3 這單個 AMM 來完成一筆交易,但是因為代幣對的流通性分散在多個池子中。那麼最優的交易策略是使用交易聚合器(例如 1inch)來進行交易,即將單筆交易拆散,同時使用多個流動性池來完成交易。就目前 uniswap v3 前端代碼情況來看,官方的界面是不支持這種聚合交易的,其 sdk 代碼中的注釋也說明了這個問題:SDK 代碼。

手續費與 tick 的關系

前文說過,為了減少開根號的計算,Uniswap 記錄的是 P−−√P,v3 使用 Q64.96 精度的定點數來存儲 P−−√P 的內容,那麼可以支持的 Pmax−−−−√≈264Pmax≈264,為了對應,讓 Pmin−−−−√=2−64Pmin=2−64

那麼可以計算出對于 tick 來說, imin=−887272,imax=887272imin=−887272,imax=887272

我們知道 tick 越多,價格可選的值越精細,但是合約在計算時候的價格區間就可能越多,那麼 gas 消耗也會更加的多,因此我們需要讓 tick 的數量保持在一個合理的范圍內。Uniswap 針對不同類型的代幣對推薦使用不同類型的費率。

例如穩定幣交易對 USDC/USDT,它的范圍波動比較小,我們需要給它更精細的價格可選值,并且設置一個比較低的手續費(0.05%)。Uniswap 引入了 tickSpacing 的概念,即每個 tick 之間跳過 N 個 tick,這樣讓合約在計算的時候,gas 更可控。

對于價格波動較小的交易池,我們希望 tickSpacing 更小,這樣價格可選值更多,同時也希望費率更低。反之波動大的交易對,可以讓 tickSpacing 更大,這樣更節約 gas,但是我們希望它的費率更高。

Uniswap 默認設置了費率和 tickSpacing 的關系:

費率 tickSpacing
0.05% 10
0.30% 60
1.00% 200

代碼架構

Uniswap v3 在代碼層面的架構和 v2 基本保持一致,將合約分成了兩個倉庫:

uniswap-v3-core

uniswap-v3-periphery

core 倉庫的功能主要包含在以下 2 個合約中:

UniswapV3Factory: 提供創建 pool 的接口,并且追蹤所有的 pool

UniswapV3Pool: 實現代幣交易,流動性管理,交易手續費的收取,oracle 數據管理。接口的實現粒度比較低,不適合普通用戶使用,錯誤的調用其中的接口可能會造成經濟上的損失。

peirphery 倉庫的功能主要包含在以下 2 個合約:

SwapRouter: 提供代幣交易的接口,它是對 UniswapV3Pool 合約中交易相關接口的進一步封裝,前端界面主要與這個合約來進行對接。

NonfungiblePositionManager: 用來增加/移除/修改 Pool 的流動性,并且通過 NFT token 將流動性代幣化。使用 ERC721 token(v2 使用的是 ERC20)的原因是同一個池的多個流動性并不能等價替換(v3 的集中流性動功能)。

這些合約間的關系大致如下圖:

Uniswap v3 詳解(一):設計原理

本系列后續會從常用的 Uniswap v3 操作入手,講解代碼調用流程。一般來說,用戶的操作都是與 uniswap-v3-periphery 開始。

Update 05-23

本系列文章主要參考的是 Uniswap v3 3月底的代碼,已經和其最新代碼又一定差異,但是這部分差異不大,并不會影響主體業務邏輯的理解。

發文者:鏈站長,轉載請註明出處:https://www.jmb-bio.com/4166.html

讚! (0)
Donate 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
Previous 2023 年 2 月 28 日 下午 7:12
Next 2023 年 2 月 28 日 下午 7:20

相關文章

  • 函數式程式設計技術解析:使用 Rust 和 Elixir 讀寫乙太坊智慧合約!

    本系列將重點介紹兩種函數式編程語言:Rust&Elixir。本篇分享函數式編程的思想和實踐。 在這篇文章中將展示Elixir&Rust讀取以太坊智能合約的功能。重要的是,該程序不僅在以太坊上工作,而且還在任何支持EVM的區塊鏈上工作,例如,Polkadot上的Moonbeam ! Ethereumex & ExABI 我更喜歡 Eli…

    2023 年 2 月 28 日
  • EVM 存儲機制詳解:深入理解乙太坊技術與安全問題!

    前言 EVM 是一個輕量級的虛擬機,其設計初衷就是提供一種可以忽略硬件、操作系統等兼容性的虛擬的執行環境供以太坊網絡運行智能合約。 簡單來說 EVM 是一個完全獨立的沙盒,在 EVM 中運行的代碼是無法訪問網絡、文件系統和其他進程的,以此來避免錯誤的代碼能讓智能合約毀滅或者影響外部環境。 在此基礎上,知道創宇區塊鏈安全實驗室 帶大家一起深入理解 E…

    2023 年 2 月 28 日
  • PolyYeld Finance 被攻擊事件全解析:代幣 YELD 價格跳水歸零!

    北京時間7月28日,安全公司Rugdoc在推特表示,收益耕作協議PolyYeld Finance遭到攻擊,所有者已宣布合約已被利用并鑄造了大量YELD代幣。CoinGeckko行情顯示,YELD代幣價格直線跳水歸零,狂跌100%。 事件概覽 攻擊如何發生 Event overview PolyYeld Finance 是 Polygon 網絡上的下一代產量農…

    2023 年 2 月 28 日
  • DeFi 安全攻略:保障您的數字資產安全!

    無論是開發DeFi協議還是其他的智能合約應用,在上線到區塊鏈主網前都需要考慮到許多安全因素。很多團隊在審核代碼時只關注Solidity相關的陷阱,但要確保dApp的安全性足夠支撐上線主網,通常還有很多工作要做。了解大多數流行的DeFi安全漏洞可能會為你和你的用戶節省數十億美元并且免除后續的各種煩惱,如預言機攻擊、暴力攻擊和許多其他威脅等。 考慮到這一點,我們…

    2023 年 2 月 28 日
  • Taproot 技術詳解:如何使用 Signet 測試網嘗鮮!

    Taproot是Bitcoin網絡最重要的升級之一,而從區塊709,632開始(預計在今年11月份),Bitcoin用戶將能夠安全地發送和接收Taproot交易。 那如何搶先體驗Taproot呢?你可以通過testnet或signet測試網使用Taproot。與使用 Bitcoin Core 的 regtest 模式創建本地測試網絡相比,使用testnet …

    2023 年 2 月 28 日
每日鏈頭條給你最新幣圈相關資訊!