沒想到合約還能這麼寫?這是筆者最近發出的最多的感慨了~
最近在寫一個去中心化交易所開發的教學https://github.com/WTFAcademy/WTF-Dapp ,參考了Uniswap V3 的程式碼實現,學習到了很多知識點。筆者之前曾開發過簡單的NFT 合約,這次是第一次嘗試開發Defi 的合約,相信這些小技巧會對想要學習合約開發的小白會很有幫助。
合約開發的大佬可以直接前往https://github.com/WTFAcademy/WTF-Dapp一起貢獻程式碼,為Web3 添磚加瓦~
接下來就讓我們來看看這些小技巧吧,有的甚至稱得上是奇技淫巧。
合約部署的合約地址有辦法做到是可預測的
我們一般部署合約得到的都是一個看起來隨機的地址,因為和「 nonce 」有關,所以合約地址不好預測。但在Uniswap 中,我們會有這樣的需求:需要透過交易對和相關資訊就能推理出合約的地址。這在很多情況下很管用,例如判斷交易的權限,或是取得池子的位址等。
在Uniswap 中,建立合約是透過「pool = address(new UniswapV3Pool{salt: keccak256(abi.encode(token0, token1, fee))}());」這樣的程式碼來建立的。透過新增了「salt」來使用CREATE2 ( https://github.com/AmazingAng/WTF-Solidity/blob/main/25_Create2/readme.md )的方式來建立合約,這樣的好處是建立出來的合約位址是可預測的,地址產生的邏輯是「新地址= hash("0xFF",創建者地址, salt, initcode)」。
這部分內容你可以查看WTF-DApp 課程的https://github.com/WTFAcademy/WTF-Dapp/blob/main/P103_Factory/readme.md這一章來了解更多。
善用回呼函數
在Solidity 中,合約之間可以互相呼叫。有一個場景是A 在某個方法呼叫B,B 在被呼叫的方法中回呼A,這在某些場景中也很管用。
在Uniswap 中,當你呼叫「UniswapV3Pool」合約的「swap」方法交易時,它會回呼“swapCallback”,回調會傳入計算出來的本次交易實際需要的「Token」,呼叫方需要在回呼中將交易所需的Token轉入“UniswapV3Pool”,而不是把“swap”方法拆開為兩部分讓調用方調用,這樣確保了“swap”方法的安全性,確保整個邏輯都是被完整執行的,而不需要繁瑣的變量記錄來確保安全性。
程式碼片段如下:
你可以學習課程中關於交易的部分內容了解更多https://github.com/WTFAcademy/WTF-Dapp/blob/main/P106_PoolSwap/readme.md 。
用異常來傳遞訊息,用try catch 來實現交易的預估
在參考Uniswap 的程式碼時,我們發現在它的https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/Quoter.sol這個合約中,把「UniswapV3Pool」的「swap」方法用「try catch」包住執行了一下:
這個是為啥呢?因為我們需要模擬「 swap 」方法來預估交易所需的Token,但因為預估的時候並不會實際產生Token 的交換,所以會報錯。在Uniswap 中,它透過在交易的回調函數中拋出一個特殊的錯誤,然後捕獲這個錯誤,從錯誤訊息中解析出所需的資訊。
看起來挺Hack 的,但也很實用。這樣就不需要針對預估交易的需求去改造swap 方法了,邏輯也比較簡單。在我們的課程中,我們也參考這個邏輯實現了https://github.com/WTFAcademy/WTF-Dapp/blob/main/demo-contract/contracts/wtfswap/SwapRouter.sol這個合約。
用大數來解決精度問題
在Uniswap 的程式碼中,有很多的計算邏輯,例如按照當前價格和流動性計算交換的Token,那麼這個過程中我們要避免除法操作的時候丟失精度。在Uniswap 中,計算過程會常用到「<< FixedPoint96.RESOLUTION」這個操作,它代表左移96 位,相當於乘以「2^96」。左移之後再做除法運算,這樣可以在正常交易不溢位(一般用「uint256」來計算,還夠)的情況下保證精度。
程式碼如下(透過價格和流動性計算交易所所需的Token 數):
可以看到,首先在Uniswap 中價格都是用平方根乘以「 2^96 」(對應上面程式碼中的「 sqrtRatioAX96 」和「 sqrtRatioBX96 」),然後流動性「 liquidity 」會左移計算出「 numerator1 」。在下面的計算中,「 2^96 」會在計算過程中被約掉,得到最後的結果。
當然,不管如何,理論上還是會有精度的丟失的,不過這種情況都是最小單位的丟失了,是可以接受的。
更多內容你可以學習https://github.com/WTFAcademy/WTF-Dapp/blob/main/P106_PoolSwap/readme.md這篇課程了解更多。
用Share 的方式來計算收益
在Uniswap 中,我們需要記錄LP(流動性提供者)的手續費收益。顯然,我們不能在每次交易的時候都給每個LP 記錄各自的手續費,這樣會消耗大量的Gas。那怎麼處理呢?
在Uniswap 中,我們可以看到「Position」中定義瞭如下結構體:
其中包含了“ feeGrowthInside0LastX128 和feeGrowthInside1LastX128 ”,他們記錄了每個頭寸(Position)上一次提取手續費時候每個流動性應該收到的手續費。
簡單點說,我只要記錄總的手續費和每個流動性應該分配到多少手續費即可,這樣LP 提取手續費的時候按照手中的流動性就可以計算出他有多少可以提取的手續費。就好像你持有某個公司的股票,你要提取股票收益的時候只要知道公司歷史的每股得收益,以及你上次提取時的收益即可。
之前我們在《巧妙的合約設計,看看stETH 如何按天自動發放收益?讓你的ETH 參與質押獲取穩定利息》這篇文章中也介紹過stETH 的收益計算方法,也是類似的道理。
不是所有資訊都需要從鏈上獲取
鏈上的儲存是相對昂貴的,所以我們並不是所有的資訊都要上鍊,或是從鏈上取得。例如Uniswap 前端網站呼叫的許多介面就是傳統的Web2 的介面。
交易池的清單、交易池的資訊等都可以儲存在普通的資料庫中,有的可能需要定期從鏈上同步,但是我們並不需要去即時呼叫鍊或節點服務提供的PRC 介面來取得相關資料。
當然現在很多區塊鏈PRC 的供應商都提供了一些高級的接口,你可以以更快速更便宜的方式獲取到一些數據,這也是類似的道理。例如ZAN 就提供了類似獲取某個用戶下所有NFT 的接口,這些資訊顯然是可以透過快取來提高性能和效率的,你可以訪問https://zan.top/service/advance-api這個獲取更多。
當然,關鍵的交易肯定是在鏈上進行的。
學會合約的拆分,也要學會利用類似ERC721 這樣已有的標準合約
一個專案可能包含多個實際部署的合約,即便是實際部署只有一個合約,但是我們程式碼可以透過繼承的方式把合約拆分為多個合約來維護。
例如在Uniswap 中, https://github.com/Uniswap/v3-periphery/blob/main/contracts/NonfungiblePositionManager.sol合約繼承了許多合約,程式碼如下:
而且你在看「ERC721Permit」合約的實現時,你會發現它直接使用了「@openzeppelin/contracts/token/ERC721/ERC721.sol」合約,這樣一方面方便透過NFT 的方式來管理頭寸,另外一方面也可以用現有的標準的合約來提高合約的開發效率。
在我們的課程中,你可以學習https://github.com/WTFAcademy/WTF-Dapp/blob/main/P108_PositionManager/readme.md嘗試開發一個簡單的ERC721 的合約來管理頭寸。
總結
看再多的文章也不如自己上手開發來得實在,在嘗試自己實現一個簡易版的去中心化交易所的過程中能讓你更深刻的理解Uniswap 的程式碼實現,也可以學習到更多實際專案中會體會到的知識點。
WTF-DApp 課程是ZAN 的開發者社群和WTF Academy 開發者社群同學共同完成的開源課程。如果你也對Web3,對Defi 專案開發有興趣,可以參考我們的實戰課程https://github.com/WTFAcademy/WTF-Dapp ,一步一步完成一個簡易版的交易所,相信一定會對你有所幫助~
本文由ZAN Team(X 帳號@zan_team ) 的Fisher(X 帳號@yudao1024 )撰寫。