聊聊接入Arbitrum的正確姿勢

摘要: 前言我們知道,目前最主流的 Ethereum Layer2 方案中,主要有 Optimistic Rollup 和 ZK Rollup 兩大類。而Optimistic Rollup 的實現方案中,則是Opti …

前言

我們知道,目前最主流的Ethereum Layer2 方案中,主要有Optimistic Rollup 和ZK Rollup 兩大類。而Optimistic Rollup 的實現方案中,則是Optimism 和Arbitrum 最受關注。而我們最近接入了Arbitrum,測試了好一段時間了,期間還踩到了一些很重要的坑,會影響安全性和可用性的,所以我覺得有必要分享下我們的這些經驗,以便後續想接入Arbitrum 的項目團隊避免重複踩坑。

第一步

我原本以為,Arbitrum 和Kovan、Rinkeby 等Layer1 的測試網一樣,是可以將智能合約無縫切換的,即運行在Kovan、Rinkeby 和Ethereum Mainnet 的智能合約無需任何修改,就可以直接部署到Arbitrum。但事實證明,我的這個認知是大錯特錯的。 Arbitrum 跟Layer1 的差異性原來非常關鍵,如果不特殊處理,有些場景甚至都會變得不可用,而且安全性也會大大降低,具體細節後文會再細說。

因此,接入Arbitrum 的第一步工作,我的建議是一定要接入Arbitrum Testnet 進行測試。如果Arbitrum Testnet 上還缺少什麼東西的話,比如沒有UniswapV2 或者SushiSwap,那可以自己部署一套UniswapV2 或SushiSwap 的合約上去。

而要在Arbitrum Testnet 上進行測試,就需要領取Arbitrum Testnet 上的測試幣用來支付Gas,即Arbitrum Testnet 上的ETH。但是,因為Arbitrum Testnet 本身並沒有可領取ETH 的Faucet 水龍頭,所以需要先在Layer1 的測試網領取測試幣,再通過Arbitrum Bridge 將測試幣轉到Arbitrum Testnet 上。

Arbitrum Testnet 所使用的Layer1 測試網絡是Rinkeby,所以就需要先領取Rinkeby 網絡的測試幣。說到這,其實Arbitrum 一開始使用的測試網絡是Kovan 的,但後來不知道為何遷移到了Rinkeby。而事實上,Kovan 網絡比Rinkeby 網絡要穩定很多。就說近一兩個月內,Rinkeby 就已經出現了不止一次長時間不出塊的問題,每次都長達好幾個小時。我們都知道,區塊鏈不出塊,那就什麼都做不了了,無法交易,無法測試,只能乾等網絡恢復。這也可以算是接入Arbitrum 要知道的第一個坑了。

Rinkeby 網絡的水龍頭,我知道的有三個:

  1. https://faucet.rinkeby.io/

  2. https://faucet.paradigm.xyz/

  3. https://faucets.chain.link/rinkeby

第一個水龍頭可以領取到最多幣,一次最多可以領取到18.75 ETH。但我最近幾次嘗試領取都失敗了,說是已經沒幣可領了。

第二個水龍頭每次可以領取到好幾種幣,包括1 ETH, 1 wETH, 500 DAI, and 5 NFTs。不過,對推特賬號有要求,要求至少有1 條推文、15 個followers、註冊1 個月以上。我自己的推特賬號目前也才只有5 個followers,不滿足條件。

第三個水龍頭是Chainlink 提供的,雖然每次只能領取0.1 ETH,但好在沒有推特的要求,也沒有時間限制,所以可以連續多次領取。這也是我最常用的水龍頭。

從Layer1 的水龍頭領取到ETH 之後,就可以通過Arbitrum 橋將ETH 轉到Layer2 的Arbitrum Testnet 了。 Arbitrum 橋的地址為:

不過,使用Arbitrum 橋之前,還要先在MetaMask 錢包中添加Arbitrum Testnet 的信息,包括RPC URL、Chain ID、區塊瀏覽器等。 Arbitrum Testnet 的信息可配置如下:

通過Arbitrum 橋就可以將Token 在Layer1 和Layer2 之間轉移。不過,需要了解,從L1 轉入L2 大概需要10 分鐘的時間才確認到賬,而從L2 轉回L1 卻需要長達一周左右的時間。轉賬確認時間比較久,這也是Optimistic Rollup 的一個弊端。

圖片

block.number 的坑

熟悉Solidity 的同學們都知道,在智能合約中可以通過調用block.number 獲取當前的區塊高度。

智能合約部署在Ethereum 主網,就獲取到主網的區塊高度;部署在Kovan 測試網,就獲取到Kovan 網絡的區塊高度;部署在Rinkeby 測試網,就獲取到Rinkeby 網絡的區塊高度。因此,直覺上會認為block.number 獲取到的就是當前網絡的區塊高度。

但在Arbitrum 中發現,原來並非如此。在Arbitrum 中運行的智能合約,block.number 讀取的並非當前Arbitrum 網絡的區塊高度,而是Layer1 的區塊高度。而且,讀取Layer1 的區塊高度還不是連續的,會隔幾個區塊才讀取一次。

比如,在Arbitrum Testnet 中,block.number 實際讀取到的是Rinkeby 網絡的區塊高度;在Arbitrum Mainnet 中,則讀取到的是Ethereum Mainnet 的區塊高度。而且,假設block.number 當前讀取到的區塊高度為9992886,那下一次讀取到有變化的區塊高度不是9992887,而是9992890。經過測試,在Arbitrum Testnet 中會隔4 個Layer1 的區塊才更新一次,這個間隔可能會跨越Layer2 的10 幾到30 幾個區塊。

這是一個大坑啊,還是反直覺的,我至今也不明白為什麼不直接讀取當前Layer2 網絡的區塊高度?因為Layer2 的合約,是無法直接讀取Layer1 的合約的,那麼廣泛使用的block.number 返回Layer1 的非連續的區塊高度有什麼用呢?我也想不到在什麼樣的場景下,Layer2 的智能合約需要去讀取Layer1 的區塊高度?

這種情況下,很多使用block.number 作為條件判斷或計算的Dapp,都會大大降低可用性和安全性。

以Compound 為例子,CToken 合約中有下面這段代碼,用來累加計算最新產生的利息的:

function accrueInterest()public returns(uint) { /* 記住初始塊號 */ uint currentBlockNumber = getBlockNumber();

uint accrualBlockNumberPrior = accrualBlockNumber; /* 短路累積 0 利息 */ if (accrualBlockNumberPrior == currentBlockNumber) {

返回 uint(Error.NO_ERROR);

}

……

}

因為Compound 的利息是按區塊計算的,所以只要發生了存取借還,每個區塊都會計算一次利息並累加更新。以上代碼就是獲取當前區塊和上一次更新的區塊,如果是同個區塊則不再計算了。這在Layer1 上是沒有任何問題的,但在Arbitrum 上,就會導致連續幾十個區塊都不會計算利息,這期間就給黑客提供很多想像空間了,可用性和安全性都大大降低。

再說說我目前負責的DEX 的一個場景,為了防範閃電貸攻擊,我們限制了同個賬戶不能在同個區塊內同時開平倉,所以,開倉和平倉函數,都會有這樣一個判斷:

要求(交易者最新操作[trader] != block.number, “ONE_BLOCK_TWICE_OPERATION”);

交易員最新操作[trader] 會保存trader 上一次開倉或平倉的時間。原本的這段邏輯只會限制在同個區塊內不能多次操作,但如今卻變成了用戶將在幾十個區塊內都無法操作,這大大降低了可用性,自然不是我們想要的結果。

那如何解決這個問題呢?諮詢了Arbitrum 的團隊之後,終於有了解決方案。原來Arbitrum 中有自己封裝了一個合約叫ArbSys,合約地址為0×0000000000000000000000000000000000000064,其中有個arbBlockNumber() 函數可以讀取到Arbitrum 網絡本身的當前區塊高度。

ArbSys(100).arbBlockNumber() // 返回仲裁塊編號

因此,只要將使用block.number 的地方,替換成調用ArbSys(100).arbBlockNumber() 就可以解決問題了。

雖然問題解決了,但這樣的話,對於需要部署到多鏈的Dapp 來說,就需要根據不同的鏈進行兼容適配了,無法做到一套代碼完全通用。

不過,block.number 的坑其實還不是最大的,我們遇到最大的坑其實在於block.timestamp。

block.timestamp 的坑

和block.number 一樣,在Arbitrum 讀取的block.timestamp 也不是當前網絡的區塊時間。那是否和block.number 一樣,是取自Layer1 的區塊時間呢?其實也不是,諮詢過Arbitrum 的技術人員,說是比Layer1 的區塊時間要稍微早一些。而且,也因為Arbitrum 並不會從Layer1 連續讀取每個區塊,所以,timestamp 的更新也是同樣有著高時延。經過測試,Arbitrum Testnet 的block.timestamp 更新時延為1 分鐘。

那麼,是否和block.number 一樣,Arbitrum 自身提供了合約函數可以讀取當前網絡的當前區塊時間呢?結果是沒有,Arbitrum 提供的ArbSys 合約只提供了方法查詢Layer2 的區塊高度和chainid,但卻沒有提供方法查詢Layer2 的當前區塊時間。連解決方案都沒有提供,所以才說這是最大的坑。我也是沒想明白,既然都提供了查詢Layer2 的區塊高度,為何就不提供查詢區塊時間呢?是技術上有難度嗎?

因為沒有方法可獲取到Arbitrum 當前網絡的區塊時間,就會導致很多依賴於block.timestamp 的Dapp 面臨可用性和安全性降低的可能。其中包括Uniswap TWAP 價格預言機,包括UniswapV2 的,也包括UniswapV3 的。

我們知道,TWAP 價格的計算,數據來源於UniswapV2Pair 合約或UniswapV3Pool 合約所保存的累計價格或累計Tick 值。而在合約實現中,累計值只會在block.timestamp 不一樣時才會更新, UniswapV2Pair 就是在以下函數中更新累計值price0CumulativeLast 和price1CumulativeLast:

// 更新準備金,並在每個區塊的第一次調用時,價格累加器function _update(uint balance0, uint balance1, uint112 _reserve0, uint112 _reserve1) private { require(balance0 <= uint112(-1) && balance1 <= uint112(-1) , 'UniswapV2: 溢出'); uint32 blockTimestamp = uint32(block.timestamp % 2**32); uint32 timeElapsed = blockTimestamp – blockTimestampLast; // 需要溢出 if (timeElapsed > 0 && _reserve0 != 0 && _reserve1 != 0) { // * 永遠不會溢出,並且需要 + 溢出 price0CumulativeLast += uint(UQ112x112.encode(_reserve1).uqdiv(_reserve0)) * 時間流逝; price1CumulativeLast += uint(UQ112x112.encode(_reserve0).uqdiv(_reserve1)) * timeElapsed; } Reserve0 = uint112(balance0); 儲備1 = uint112(餘額1); blockTimestampLast = blockTimestamp; 發出同步(reserve0,reserve1);}

因此,在Arbitrum Testnet 中,累計值至少1 分鐘才會更新一次,Arbitrum 主網中沒精確測試過,但應該是差不多的。因為Arbitrum 的出塊時間大概為2~6 秒,所以累計值可能長達30 個Arbitrum 區塊才會更新一次。如此嚴重的高時延,那計算出來的TWAP 的準確性自然也大幅降低了。

同為Optimistic Rollup 的Optimism 其實也存在同樣的問題,所以在Uniswap 的官方文檔中還有下面這段說明:

圖片

不過,Optimism 的時延只有20 多秒,沒有Arbitrum 的這麼高時延。另外,也不知道Optimism 有沒有提供方法查詢Layer2 的區塊時間,我目前沒找到。

總而言之,這種情況下,對於想要接入Arbitrum 的項目來說,當需要使用到block.timestamp 作為判斷條件時,沒有太優雅的解決方案,我只能提供一些思路。

首先,思考下是否可以不用區塊時間而改用區塊高度,那就可以用ArbSys(100).arbBlockNumber() 方案解決問題。

其次,如果業務上的時間週期比較長,比如30 分鐘、幾小時甚至幾天,那延後1 分鐘還是可以接受的。比如,假設讀取的是1 小時內的TWAP 價格,那1 分鐘的時延倒是影響沒那麼大。

最後,若實在必須要求低時延,那也許只能等未來Arbitrum 在這方面有所優化了。

總結

目前,在Arbitrum 上主要遇到的問題就是這些了,block.number 和block.timestamp 是最大的兩個坑,其他問題都是小問題。其他項目在接入Arbitrum 之前,可以先考慮好對應問題的解決方案。也希望Arbitrum 能盡快優化自身,以能達到所有Dapp 的智能合約真的能夠無需修改地從Layer1 無縫遷移到Layer2。

Total
0
Shares
Related Posts