剖析DeFi交易產品之UniswapV4:Swap

文章首發于公眾號:Keegan小鋼


Swap 可分為兩種場景:單池交易跨池交易。在 PoolManager 合約里,要完成交易流程,會涉及到 lock()swap()settle()take() 四個函數。單池交易時只需要調一次 swap() 函數,而跨池交易時則需要多次調用 swap() 函數來完成。

我們先來聊聊單池交易如何實現,以下是流程圖:

image.png

第一步,和其他操作一樣,先執行 lock(),鎖定住接下來的系列操作。

第二步,就是在 lockAcquired() 回調函數里執行 swap() 函數。這一步執行完之后,記賬系統中會記錄用戶欠池子的資產數量,即用戶需要支付的代幣;以及池子欠用戶的資產數量,即用戶此次交易可得的代幣。

第三步,執行 settle() 函數,完成代幣的支付。

第四步,執行 take() 函數,取回所得的代幣。

最后,lock() 函數完成,返回結果。

而如果是跨池交易的話,則需要在 Router 層面確定好交易路徑,然后根據路徑執行多次 swap。舉個例子,現在要用 A 兌換成 C,但是 A 和 C 之間沒有直接配對的池子,但是有中間代幣 B,存在 A 和 B 配對的池子,也存在 B 和 C 配對的池子。那交易路徑就可以先用 A 換成 B,再將 B 換成 C,最終實現了 A 換成 C。而不管中間經過了多少次 swap,最后,只需要完成一次 settle 操作,即支付 A,也只需要執行一次 take 操作,即取回最后所得的 C。整個流程大致如下圖所示:

image.png

下面,我們主要剖析講解 swap() 函數的內部實現。

首先,看看其函數聲明,如下:

function swap(PoolKey memory key, IPoolManager.SwapParams memory params, bytes calldata hookData)externaloverridenoDelegateCallonlyByLockerreturns (BalanceDelta delta)

key 指定了要進行交易的池子,params 是具體的交易參數,hookData 即需要回傳給 hooks 合約的數據。

來看看 params 具體有哪些參數:

struct SwapParams {bool zeroForOne;int256 amountSpecified;uint160 sqrtPriceLimitX96;
}

zeroForOne 指名了要用 currency0 兌換 currency1,為 false 的話則反過來用 currency1 兌換 currency0amountSpecified 是指定的確定數額,正數表示輸入,負數表示輸出。sqrtPriceLimitX96 是滑點保護的限定價格。如果之前已經了解過 UniswapV3,那對這幾個字段應該不陌生。

兩個函數修飾器 noDelegateCallonlyByLocker,和之前文章介紹的一樣,就不贅述了。

返回值 delta,其組成里的兩個數,正常情況下就是一個正數,一個負數。

接下來,看看函數體了。先看前面一段代碼:

PoolId id = key.toId();
_checkPoolInitialized(id);if (key.hooks.shouldCallBeforeSwap()) {bytes4 selector = key.hooks.beforeSwap(msg.sender, key, params, hookData);// Sentinel return value used to signify that a NoOp occurred.if (key.hooks.isValidNoOpCall(selector)) return BalanceDeltaLibrary.MAXIMUM_DELTA;else if (selector != IHooks.beforeSwap.selector) revert Hooks.InvalidHookResponse();
}

這部分邏輯很簡單,前兩行代碼,檢查池子是否已經初始化過了,未初始化的則 revert。之后是執行 hooks 合約的 beforeSwap 鉤子函數。

接下來這段代碼是執行 swap 的內部函數:

uint256 feeForProtocol;
uint256 feeForHook;
uint24 swapFee;
Pool.SwapState memory state;
(delta, feeForProtocol, feeForHook, swapFee, state) = pools[id].swap(Pool.SwapParams({tickSpacing: key.tickSpacing,zeroForOne: params.zeroForOne,amountSpecified: params.amountSpecified,sqrtPriceLimitX96: params.sqrtPriceLimitX96})
);

這個內部函數的具體實現比較復雜,我們待會再講,先繼續講完外部函數剩下的代碼。

接下來一行代碼就是進行記賬了:

_accountPoolBalanceDelta(key, delta);

之后是對協議費和 hook 費用的處理:

unchecked {if (feeForProtocol > 0) {protocolFeesAccrued[params.zeroForOne ? key.currency0 : key.currency1] += feeForProtocol;}if (feeForHook > 0) {hookFeesAccrued[address(key.hooks)][params.zeroForOne ? key.currency0 : key.currency1] += feeForHook;}
}

接著執行 afterSwap 的鉤子函數:

if (key.hooks.shouldCallAfterSwap()) {if (key.hooks.afterSwap(msg.sender, key, params, delta, hookData) != IHooks.afterSwap.selector) {revert Hooks.InvalidHookResponse();}
}

最后,發送事件:

emit Swap(id, msg.sender, delta.amount0(), delta.amount1(), state.sqrtPriceX96, state.liquidity, state.tick, swapFee
);

整個外部函數的邏輯還是比較清晰的。復雜的其實是內部函數的實現。下面就來看看 swap 內部函數的實現邏輯。還是先看函數聲明:

function swap(State storage self, SwapParams memory params)internalreturns (BalanceDelta result,uint256 feeForProtocol,uint256 feeForHook,uint24 swapFee,SwapState memory state)

selfstorage 類型的,其實就是外部函數的 pools[id]。而第二個參數的 SwapParams 不同于外部函數的同名參數,這個內部函數的此參數具體如下:

struct SwapParams {int24 tickSpacing;bool zeroForOne;int256 amountSpecified;uint160 sqrtPriceLimitX96;
}

相比外部函數的此參數,多了 tickSpacing,其他參數則和外部函數的一樣。

返回值比較多。result 就是變動的凈余額,feeForProtocol 是協議費,feeForHook 是 hook 費用,包括 hook 交易費用和提現費用,swapFee 就是池子本身的交易費,最后的 state 是最新的狀態。

接著,開始查看函數體的代碼實現,先看前面一段:

// 指定價格不能為0
if (params.amountSpecified == 0) revert SwapAmountCannotBeZero();
// 讀取出swap前的狀態
Slot0 memory slot0Start = self.slot0;
swapFee = slot0Start.swapFee;
if (params.zeroForOne) { // token0兌換token1// 滑點價格的判斷if (params.sqrtPriceLimitX96 >= slot0Start.sqrtPriceX96) {revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96, params.sqrtPriceLimitX96);}if (params.sqrtPriceLimitX96 <= TickMath.MIN_SQRT_RATIO) {revert PriceLimitOutOfBounds(params.sqrtPriceLimitX96);}
} else { // token1兌換token0// 滑點價格的判斷if (params.sqrtPriceLimitX96 <= slot0Start.sqrtPriceX96) {revert PriceLimitAlreadyExceeded(slot0Start.sqrtPriceX96, params.sqrtPriceLimitX96);}if (params.sqrtPriceLimitX96 >= TickMath.MAX_SQRT_RATIO) {revert PriceLimitOutOfBounds(params.sqrtPriceLimitX96);}
}

接下來是這段代碼:

// 臨時的緩存數據
SwapCache memory cache = SwapCache({liquidityStart: self.liquidity,protocolFee: params.zeroForOne? (getSwapFee(slot0Start.protocolFees) % 64): (getSwapFee(slot0Start.protocolFees) >> 6),hookFee: params.zeroForOne ? (getSwapFee(slot0Start.hookFees) % 64) : (getSwapFee(slot0Start.hookFees) >> 6)
});
// 是否為確定的輸入
bool exactInput = params.amountSpecified > 0;
// 初始化返回值的state
state = SwapState({amountSpecifiedRemaining: params.amountSpecified,amountCalculated: 0,sqrtPriceX96: slot0Start.sqrtPriceX96,tick: slot0Start.tick,feeGrowthGlobalX128: params.zeroForOne ? self.feeGrowthGlobal0X128 : self.feeGrowthGlobal1X128,liquidity: cache.liquidityStart
});

cache 是一個臨時狀態的緩存數據,包括三個字段:

  • liquidityStart:流動性
  • protocolFee:協議費用
  • hookFee:hook 費用

amountSpecified 大于 0 則說明是指定的輸入,即 exactInputtrue

初始化返回值 state 也都是用當前狀態的值進行初始化。這里前兩個字段需要介紹一下,即 amountSpecifiedRemainingamountCalculated。第一個字段表示當前還有多少指定的金額未進行交易計算的,第二個字段表示已經交易計算累加的數額。為了理解這兩個字段,我們舉個例子來說明。假設用戶指定的是輸出的數額,假設為 1000,那 amountSpecifiedRemaining 初始值即為 1000。但是,當前有效的流動性剩余量并不足 1000,假設只剩下 400,所以在當前 tick 下的計算只能用到 400,假設計算所得的輸入數額為 200,那么,次輪計算后,amountSpecifiedRemaining 剩下 1000 - 400 = 600,而 amountCalculated 變為 200。之后,tick 會移動到下一個有流動性的區間內。剩下的 600 繼續計算所得,假設這時的流動性剩余已經超過 600 了,這 600 計算所得的輸入值為 250,那計算完后的 amountSpecifiedRemaining 就變成了 0,而 amountCalculated 則為 200 + 250 = 450,計算結束。這就是這兩個字段的作用。

之后的代碼會做循環判斷,就是上面所說的計算邏輯:

StepComputations memory step;
// continue swapping as long as we haven't used the entire input/output and haven't reached the price limit
while (state.amountSpecifiedRemaining != 0 && state.sqrtPriceX96 != params.sqrtPriceLimitX96) {...
}

while 條件里除了判斷 amountSpecifiedRemaining 不為 0 之外,還判斷了最新價格不能等于滑點價格。如果等于滑點價格了,也會結束循環。

step 用來存儲 while 循環里每一步的計算用到的臨時變量,具體包含以下字段:

struct StepComputations {// the price at the beginning of the stepuint160 sqrtPriceStartX96;// the next tick to swap to from the current tick in the swap directionint24 tickNext;// whether tickNext is initialized or notbool initialized;// sqrt(price) for the next tick (1/0)uint160 sqrtPriceNextX96;// how much is being swapped in in this stepuint256 amountIn;// how much is being swapped outuint256 amountOut;// how much fee is being paid inuint256 feeAmount;
}

接著,來看看 while 循環里面的邏輯,先來看前面一段代碼:

// 初始化當前這一步的價格
step.sqrtPriceStartX96 = state.sqrtPriceX96;
// 獲取出下一個tick
(step.tickNext, step.initialized) =self.tickBitmap.nextInitializedTickWithinOneWord(state.tick, params.tickSpacing, params.zeroForOne);
// 確保下一個tick不會超出邊界
if (step.tickNext < TickMath.MIN_TICK) {step.tickNext = TickMath.MIN_TICK;
} else if (step.tickNext > TickMath.MAX_TICK) {step.tickNext = TickMath.MAX_TICK;
}
// 計算出下一個tick對應的根號價格
step.sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(step.tickNext);

之后,執行當前這步的具體計算:

// compute values to swap to the target tick, price limit, or point where input/output amount is exhausted
(state.sqrtPriceX96, step.amountIn, step.amountOut, step.feeAmount) = SwapMath.computeSwapStep(state.sqrtPriceX96,(params.zeroForOne? step.sqrtPriceNextX96 < params.sqrtPriceLimitX96: step.sqrtPriceNextX96 > params.sqrtPriceLimitX96) ? params.sqrtPriceLimitX96 : step.sqrtPriceNextX96,state.liquidity,state.amountSpecifiedRemaining,swapFee
);

計算返回四個值,sqrtPriceX96 為計算后的最新價格,amountIn 為輸入的數額,amountOut 為輸出的金額,feeAmount 為需要支付的手續費。

繼續看下一段代碼:

if (exactInput) { //指定輸入時unchecked {//remaining減去輸入額和手續費state.amountSpecifiedRemaining -= (step.amountIn + step.feeAmount).toInt256();}//calculated加上輸出額,因為amountOut為負數,所以用減法state.amountCalculated = state.amountCalculated - step.amountOut.toInt256();
} else { //指定輸出時unchecked {//remaining減去輸出額,因為amountOut為負數,所以用加法state.amountSpecifiedRemaining += step.amountOut.toInt256();}//calculated加上輸入額和手續費state.amountCalculated = state.amountCalculated + (step.amountIn + step.feeAmount).toInt256();
}

之后的一段代碼則是計算幾個費用了:

// 協議費用
if (cache.protocolFee > 0) {// A: calculate the amount of the fee that should go to the protocoluint256 delta = step.feeAmount / cache.protocolFee;// A: subtract it from the regular fee and add it to the protocol feeunchecked {step.feeAmount -= delta;feeForProtocol += delta;}
}
// hook費用
if (cache.hookFee > 0) {// step.feeAmount has already been updated to account for the protocol feeuint256 delta = step.feeAmount / cache.hookFee;unchecked {step.feeAmount -= delta;feeForHook += delta;}
}
// 更新全局費用跟蹤器
if (state.liquidity > 0) {unchecked {state.feeGrowthGlobalX128 += FullMath.mulDiv(step.feeAmount, FixedPoint128.Q128, state.liquidity);}
}

while 循環體里的最后一段代碼則如下:

// 如果計算后的新價格到達下一個tick價格就移動tick
if (state.sqrtPriceX96 == step.sqrtPriceNextX96) {// 如果tick已經初始化,則執行移動tickif (step.initialized) {int128 liquidityNet = Pool.crossTick(self,step.tickNext,(params.zeroForOne ? state.feeGrowthGlobalX128 : self.feeGrowthGlobal0X128),(params.zeroForOne ? self.feeGrowthGlobal1X128 : state.feeGrowthGlobalX128));// 如果向左移動,把liquidityNet理解為相反的符號unchecked {if (params.zeroForOne) liquidityNet = -liquidityNet;}// 更新流動性state.liquidity = liquidityNet < 0? state.liquidity - uint128(-liquidityNet): state.liquidity + uint128(liquidityNet);}// 更新tickunchecked {state.tick = params.zeroForOne ? step.tickNext - 1 : step.tickNext;}
} else if (state.sqrtPriceX96 != step.sqrtPriceStartX96) {// 重新計算,除非我們處于較低的刻度邊界(即已經轉換過刻度),并且沒有移動state.tick = TickMath.getTickAtSqrtRatio(state.sqrtPriceX96);
}

整個 while 循環跑完之后,一般來說,可能會存在兩種情況。第一種,指定的金額全部完成兌換,即 amountSpecifiedRemaining 沒有剩余。第二種,兌換到一半,觸發到了滑點保護價格,那 amountSpecifiedRemaining 將會有剩余,只有部分成交。

那么,循環結束之后,整個內部的 swap 函數就只剩下最后的一部分代碼了,如下:

// 將臨時狀態的價格和tick轉為storage狀態
(self.slot0.sqrtPriceX96, self.slot0.tick) = (state.sqrtPriceX96, state.tick);// 更新storage狀態的流動性
if (cache.liquidityStart != state.liquidity) self.liquidity = state.liquidity;// 更新全局的手續費跟蹤器
if (params.zeroForOne) {self.feeGrowthGlobal0X128 = state.feeGrowthGlobalX128;
} else {self.feeGrowthGlobal1X128 = state.feeGrowthGlobalX128;
}
// 凈余額變動值賦值給返回值result
unchecked {if (params.zeroForOne == exactInput) {result = toBalanceDelta((params.amountSpecified - state.amountSpecifiedRemaining).toInt128(),state.amountCalculated.toInt128());} else {result = toBalanceDelta(state.amountCalculated.toInt128(),(params.amountSpecified - state.amountSpecifiedRemaining).toInt128());}
}

至此,就完成了 swap 的全部代碼邏輯講解了。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/39732.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/39732.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/39732.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【面向就業的Linux基礎】從入門到熟練,探索Linux的秘密(七)-shell語法(5)

shell語法的一些知識和練習&#xff0c;可以當作筆記收藏一下&#xff01;&#xff01; 文章目錄 前言 一、shell 二、shell語法 1.文件重定向 2.引入外部腳本 3.作業 總結 前言 shell語法的一些知識和練習&#xff0c;可以當作筆記收藏一下&#xff01;&#xff01; 提示&…

七種大模型微調方法:讓你的Offer拿到爽

在當今的人工智能和機器學習領域&#xff0c;大型預訓練模型&#xff08;如GPT、BERT等&#xff09;已成為解決自然語言處理&#xff08;NLP&#xff09;任務的強大工具。然而&#xff0c;要讓這些模型更好地適應特定任務或領域&#xff0c;往往需要進行微調。本文將詳細介紹七…

手把手教你:如何在51建模網免費下載3D模型?

作為國內領先的3D互動展示平臺&#xff0c;51建模網不僅匯聚了龐大的3D模型資源庫&#xff0c;供用戶免費下載&#xff0c;更集成了在線編輯、格式轉換、內嵌展示及互動體驗等一站式功能&#xff0c;為3D創作者及愛好者搭建起夢想與現實的橋梁。 如何在51建模網免費下載3D模型…

鴻蒙認證值得考嗎?

鴻蒙認證值得考嗎&#xff1f; 鴻蒙認證&#xff08;HarmonyOS Certification&#xff09;是華為為了培養和認證開發者在鴻蒙操作系統&#xff08;HarmonyOS&#xff09;領域的專業技能而設立的一系列認證項目。這些認證旨在幫助開發者和企業工程師提升在鴻蒙生態中的專業技能…

linux——IPC 進程間通信

IPC 進程間通信 interprocess communicate IPC&#xff08;Inter-Process Communication&#xff09;&#xff0c;即進程間通信&#xff0c;其產生的原因主要可以歸納為以下幾點&#xff1a; 進程空間的獨立性 資源隔離&#xff1a;在現代操作系統中&#xff0c;每個進程都…

圖解 Kafka 架構

寫在前面 Kafka 是一個可橫向擴展&#xff0c;高可靠的實時消息中間件&#xff0c;常用于服務解耦、流量削峰。 好像是 LinkedIn 團隊開發的&#xff0c;后面捐贈給apache基金會了。 kafka 總體架構圖 Producer&#xff1a;生產者&#xff0c;消息的產生者&#xff0c;是消息的…

【高考志愿】測繪科學與技術

目錄 一、專業介紹 1.1 專業概述 1.2 專業方向 1.3 課程內容 二、就業前景 三、報考注意事項 四、測繪科學與技術專業排名 五、職業規劃與未來發展 高考志愿選擇測繪科學與技術專業&#xff0c;對于許多有志于空間信息技術領域發展的學生來說&#xff0c;無疑是一個極具…

怎么把錄音轉文字?推薦幾個簡單易操作的方法

在小暑這個節氣里&#xff0c;炎熱的天氣讓人分外渴望效率up&#xff01;Up&#xff01;Up&#xff01; 對于那些在會議或課堂中急需記錄信息的朋友們&#xff0c;手寫筆記的速度往往難以跟上講話的節奏。此時&#xff0c;電腦錄音轉文字軟件就像一陣及時雨&#xff0c;讓記錄…

PHP pwn 學習 (1)

文章目錄 A. PHP extensions for C1. 運行環境與工作目錄初始化2. 構建與加載3. 關鍵結構定義PHP_FUNCTIONINTERNAL_FUNCTION_PARAMETERSzend_execute_data等ZEND_PARSE_PARAMETERS_START等zend_parse_arg_stringzend_module_entryzend_function_entry等PHP類相關 原文鏈接&…

Python 作業題1 (猜數字)

題目 你要根據線索猜出一個三位數。游戲會根據你的猜測給出以下提示之一&#xff1a;如果你猜對一位數字但數字位置不對&#xff0c;則會提示“Pico”&#xff1b;如果你同時猜對了一位數字及其位置&#xff0c;則會提示“Fermi”&#xff1b;如果你猜測的數字及其位置都不對&…

Flower花所:穩定運營的數字貨幣交易所

Flower花所是一家穩定運營的數字貨幣交易所&#xff0c;致力于為全球用戶提供安全、高效的數字資產交易服務。作為一家長期穩定運營的數字貨幣交易平臺&#xff0c;Flower花所以其可靠的技術基礎和優質的客戶服務而聞名。 平臺穩定性與可靠性&#xff1a; 持續運營&#xff1a;…

Vue前端練習

此練習項目只涉及前端&#xff0c;主要是vue和ElementUI框架的使用。&#xff08;ElementUI官網&#xff1a;Element - The worlds most popular Vue UI framework&#xff09; 一、環境準備 安裝idea 安裝Node.js 一鍵式安裝(不需要做任何配置) npm -v&#xff08;也可用nod…

mysql-sql-第十五周

學習目標&#xff1a; sql 學習內容&#xff1a; 41.查詢沒有學全所有課程的同學的信息 select *from students where students.stunm not in (select score.stunm from score group by score.stunm having count(score.counm) (select count(counm) from course)) 42.查詢…

數據結構_線性表

線性表的定義和特點 線性表是具有相同特性的數據元素的一個有限序列 :線性起點/起始節點 :的直接前驅 :的直接后繼 :線性終點/終端節點 n:元素總個數,表長 下標:是元素的序號,表示元素在表中的位置 n0時稱為空表 線性表 由n(n>0)個數據元素(結點),組成的有限序列 將…

安卓模擬器如何修改ip地址

最近很多老鐵玩游戲的&#xff0c;想多開模擬器一個窗口一個IP&#xff0c;若模擬器窗口開多了&#xff0c;IP一樣會受到限制&#xff0c;那么怎么更換自己電腦手機模擬器IP地址呢&#xff0c;今天就教大家一個修改模擬器IP地址的方法&#xff01;廢話不多說&#xff0c;直接上…

Matlab 中 fftshift 與 ifftshift

文章目錄 【 1. fftshift、ifftshift 的區別】【 2. fftshift(fft(A)) 作圖 】【 3. fftshift(fft(A)) 還原到 A 】Matlab 直接對信號進行 FFT 的結果中,前半部分是正頻,后半部分是負頻,為了更直觀的表示,需要將 負頻 部分移到 前面。【 1. fftshift、ifftshift 的區別】 M…

alibaba EasyExcel 簡單導出數據到Excel

導入依賴 <dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>4.0.1</version> </dependency> 1、alibaba.excel.EasyExcel導出工具類 import com.alibaba.excel.EasyExcel; import …

探索哈希函數:數據完整性的守護者

引言 銀行在處理數以百萬計的交易時&#xff0c;如何確保每一筆交易都沒有出錯&#xff1f;快遞公司如何跟蹤成千上萬的包裹&#xff0c;確保每個包裹在運輸過程中沒有丟失或被替換&#xff1f;醫院和診所為龐大的患者提供有效的醫療保健服務&#xff0c;如何確保每個患者的醫療…

假陽性和假陰性、真陽性和真陰性

在深度學習的分類問題中&#xff0c;真陽性、真陰性、假陽性和假陰性是評估模型性能的重要指標。它們的定義和計算如下&#xff1a; 真陽性&#xff08;True Positive, TP&#xff09;&#xff1a; 定義&#xff1a;模型預測為正類&#xff08;陽性&#xff09;&#xff0c;且實…

電梯修理升級,安裝【電梯節能】能量回饋設備

電梯修理升級&#xff0c;安裝【電梯節能】能量回饋設備 1、節能率評估 15%—45% 2、降低機房環境溫度&#xff0c;改善電梯控制系統的運行環境&#xff1b; 3、延長電梯使用壽命&#xff1b; 4、機房可以不需要使用空調等散熱設備的耗電&#xff0c;間接節省電能。 歡迎私詢哦…