以太坊主網的Gas 費用一直是老大難問題,尤其是在網路擁塞時更為顯著。在高峰期,用戶往往需要支付極高的交易費用。因此,在智能合約開發階段進行Gas 費用優化尤為重要。優化Gas 消耗不僅能有效降低交易成本,還能提升交易效率,為用戶帶來更經濟、高效的區塊鏈使用體驗。

本文將概述以太坊虛擬機器(EVM)的Gas 費機制、Gas 費優化的相關核心概念,以及開發智慧合約時進行Gas 費優化的最佳實務。希望透過這些內容,能為開發者提供啟發和實用協助,同時也協助一般用戶更能理解EVM 的Gas 費用運作方式,共同因應區塊鏈生態中的挑戰。

EVM 的Gas 費機制簡介

在相容EVM 的網路中,「Gas」是指用於測量執行特定操作所需運算能力的單位。

下圖說明了EVM 的結構佈局。圖中,Gas 消耗分為三個部分:操作執行、外部訊息呼叫以及記憶體和儲存的讀寫。

以太坊智能合約的Gas優化十大最佳實踐

來源:以太坊官網[1]

由於每筆交易的執行都需要計算資源,因此會收取一定費用以防止無限循環和拒絕服務(DoS)攻擊。完成一筆交易所需的費用稱為「Gas 費」。

自EIP-1559(倫敦硬分叉)生效以來,Gas 費透過以下公式計算:

Gas fee = units of gas used * (base fee + priority fee)

基礎費會被銷毀,優先費用則作為激勵,鼓勵驗證者將交易添加到區塊鏈中。在發送交易時設定更高的優先費用,可以提高交易被包含在下一個區塊中的可能性。這類似於用戶向驗證者支付的一種「小費」。

1.理解EVM 中的Gas 優化

當用Solidity 編譯智能合約時,合約會被轉換為一系列“操作碼”,即opcodes。

任何一段操作碼(例如建立合約、進行訊息呼叫、存取帳戶儲存以及在虛擬機器上執行操作)都有一個公認的Gas 消耗成本,這些成本記錄在以太坊黃皮書[2]中。

以太坊智能合約的Gas優化十大最佳實踐

經過多次EIP 的修改,其中一些操作碼的Gas 成本已被調整,可能與黃皮書中有所偏差。有關操作碼最新成本的詳細信息,請參考此處[3]。

2.Gas 優化的基本概念

Gas 優化的核心理念是在EVM 區塊鏈上優先選擇成本效率高的操作,避免Gas 昂貴的操作。

在EVM 中,以下操作成本較低:

  • 讀寫記憶體變數
  • 讀取常數和不可變變量
  • 讀寫本地變數
  • 讀取calldata 變量,例如calldata 數組和結構體
  • 內部函數調用

成本較高的操作包括:

  • 讀寫儲存在合約儲存中的狀態變數
  • 外部函數調用
  • 循環操作

EVM Gas 費用優化最佳實踐

基於上述基本概念,我們為開發者社群整理了一份Gas 費優化最佳實踐清單。透過遵循這些實踐,開發者可以降低智慧合約的Gas 費消耗,降低交易成本,並打造更有效率且用戶友好的應用程式。

1.盡量減少儲存的使用

在Solidity 中,Storage(儲存)是一種有限資源,其Gas 消耗遠高於Memory(記憶體)。每次智能合約從儲存讀取或寫入資料時,都會產生高額的Gas 成本。

根據以太坊黃皮書的定義,儲存操作的成本比記憶體操作高出100 倍以上。例如,OPcodesmload 和mstore 指令只消耗3 個Gas 單位,而儲存操作如sload 和sstore 即使在最理想的情況下,成本也至少需要100 個單位。

以太坊智能合約的Gas優化十大最佳實踐

限制儲存使用的方法包括:

  • 將非永久性資料儲存在記憶體中
  • 減少儲存修改次數:透過將中間結果保存在記憶體中,待所有計算完成後,再將結果分配給儲存變數。

2. 變數打包

智能合約中使用的Storage slot(儲存槽)的數量以及開發者表示資料的方式會極大影響Gas 費的消耗。

Solidity 編譯器會在編譯過程中將連續的儲存變數打包,並以32 個位元組的儲存槽作為變數儲存的基本單位。變數打包是指透過合理安排變數,使多個變數能夠適配到單一儲存槽中。

左側是一個效率較低的實作方式,將消耗3 個儲存槽;右側是一個更有效率的實作方式。

以太坊智能合約的Gas優化十大最佳實踐

透過這項細節的調整,開發者可以節省20,000 個Gas 單位(儲存一個未使用過的儲存槽需要消耗20,000Gas),但現在只需要兩個儲存槽。

由於每個儲存槽都會消耗Gas,變數打包透過減少所需儲存槽的數量來優化Gas 的使用。

3. 優化資料類型

一個變數可以用多種資料類型表示,但不同的資料類型對應的操作成本也不同。選擇合適的資料類型有助於優化Gas 的使用。

例如,在Solidity 中,整數可以細分為不同的大小:uint8、uint16、uint32 等。由於EVM 是以256 位元為單位執行操作,使用uint8 意味著EVM 必須先將其轉換為uint256,而這種轉換會額外消耗Gas。

以太坊智能合約的Gas優化十大最佳實踐

我們可以透過圖中的程式碼來比較uint8 和uint256 的Gas 成本。 UseUint() 函數消耗120,382 Gas 單位,而UseUInt8() 函數消耗166,111 Gas 單位。

單獨來看,這裡使用uint256 比uint8 便宜。然而,若使用我們先前建議的變數打包優化就不同了。如果開發者能夠將四個uint8 變數打包到一個儲存槽中,那麼迭代它們的總成本將比四個uint256 變數更低。這樣,智能合約就可以讀寫一次儲存槽,並在一次操作中將四個uint8 變數放入記憶體/ 儲存體中。

4. 使用固定大小變數取代動態變數

如果資料可以控制在32 個位元組內,建議使用bytes32 資料類型取代bytes 或strings。一般來說,固定大小的變數比可變大小的變數消耗的Gas 更少。如果位元組長度可以限制,盡量選擇從bytes1 到bytes32 的最小長度。

以太坊智能合約的Gas優化十大最佳實踐

以太坊智能合約的Gas優化十大最佳實踐

5. 映射與數組

Solidity 的資料清單可以用兩種資料類型表示:陣列(Arrays)和映射(Mappings),但它們的語法和結構截然不同。

映射在大多數情況下效率更高而成本更低,但數組具有可迭代性且支援資料類型打包。因此,建議在管理資料列表時優先使用映射,除非需要迭代或可以透過資料類型打包優化Gas 消耗。

6. 使用calldata 取代memory

函數參數中宣告的變數可以儲存在calldata 或memory 中。兩者的主要差異在於,memory 可以被函數修改,而calldata 是不可變的。

記住這個原則:如果函數參數是唯讀的,應優先使用calldata 而非memory。這樣可以避免從函數calldata 到memory 的不必要複製操作。

範例1:使用memory

以太坊智能合約的Gas優化十大最佳實踐

使用memory 關鍵字時,陣列的值會在ABI 解碼過程中從編碼的calldata 複製到memory。這段程式碼區塊的執行成本為3,694 個Gas 單位。

範例2:使用calldata

以太坊智能合約的Gas優化十大最佳實踐

當直接從calldata 讀取值時,跳過中間的memory 操作。這種優化方式使執行成本降至僅2,413 個Gas 單位,Gas 效率提升了35%。

7. 盡可能使用Constant/Immutable 關鍵字

Constant/Immutable 變數不會儲存在合約的儲存中。這些變數會在編譯時計算,並儲存在合約的字節碼中。因此,與儲存相比,它們的存取成本要低得多,建議盡可能使用Constant 或Immutable 關鍵字。

8. 確保不會發生溢位/ 下溢時使用Unchecked

當開發者能夠確定算術操作不會導致溢位或下溢時,可以使用Solidity v0.8.0 引入的unchecked 關鍵字,避免多餘的溢位或下溢檢查,從而節省Gas 成本。

在下圖中,受條件約束i

以太坊智能合約的Gas優化十大最佳實踐

此外,0.8.0 以上版本的編譯器已不再需要使用SafeMath 函式庫,因為編譯器本身已內建了溢位和下溢保護功能。

9. 優化修改器

修改器的程式碼被嵌入到被修改過的函數中,每次使用修改器時,其程式碼都會被複製。這會增加字節碼的大小並提高Gas 消耗。以下是一種優化修改器Gas 成本的方法:

優化前:

以太坊智能合約的Gas優化十大最佳實踐

優化後:

以太坊智能合約的Gas優化十大最佳實踐

在本例中,透過將邏輯重構為內部函數_checkOwner(),允許在修改器中重複使用該內部函數,可減少字節碼大小並降低Gas 成本。

10. 短路優化

對於||和&&運算符,邏輯運算會發生短路評估,也就是如果第一個條件已經能夠確定邏輯表達式的結果,則不會評估第二個條件。

為了優化Gas 消耗,應將計算成本低廉的條件放在前面,這樣可以有可能跳過成本高昂的計算。

附加一般性建議

1. 刪除無用程式碼

如果合約中存在未使用的函數或變量,建議將其刪除。這是減少合約部署成本並保持合約體積小最直接的方法。

以下是一些實用建議:

使用最高效的演算法進行計算。如果合約中直接使用某些計算的結果,那麼就應該去除這些冗餘計算過程。本質上,任何未使用的計算都應該被刪除。

在以太坊中,開發者透過釋放儲存空間可以獲得Gas 獎勵。如果不再需要某個變數時,應使用delete 關鍵字刪除它,或將其設為預設值。

循環優化:避免高成本的循環操作,盡可能合併循環,並將重複計算移出循環體。

2. 使用預編譯合約

預編譯合約提供複雜的函式庫函數,例如加密和雜湊操作。由於程式碼不是在EVM 上運行,而是在客戶端節點本地運行,因此需要的Gas 更少。使用預編譯合約可以透過減少執行智能合約所需的計算工作量來節省Gas。

預編譯合約的範例包括橢圓曲線數位簽章演算法(ECDSA)和SHA2-256 雜湊演算法。透過在智慧合約中使用這些預編譯合約,開發者可以降低Gas 成本,並提高應用程式的運作效率。

關於以太坊網路支援的預編譯合約的完整列表,請參閱此處[4]。

3. 使用內聯彙編程式碼

內聯彙編(in-line assembly)允許開發者編寫可由EVM 直接執行的低階卻高效的程式碼,而無須使用昂貴的Solidity 操作碼。內聯彙編還允許更精確地控制記憶體和儲存的使用,從而進一步減少Gas 費用。此外,內聯彙編可以執行一些僅使用Solidity 難以實現的複雜操作,為優化Gas 消耗提供更多靈活性。

以下是使用內聯彙編節省Gas 的程式碼範例:

以太坊智能合約的Gas優化十大最佳實踐

從上圖可以看到,與標準用例相比,使用了內嵌彙編技術的第二種用例擁有更高的Gas 效率。

然而,使用內聯彙編也可能帶來風險並容易出錯。因此,應謹慎使用,僅限經驗豐富的開發者操作。

4. 使用Layer 2 解決方案

使用Layer 2 解決方案可以減少需要在以太坊主網上儲存和計算的資料量。

像是rollups、側鍊和狀態通道等Layer 2 解決方案能夠將交易處理從主以太坊鏈上卸載,從而實現更快和更便宜的交易。

透過將大量交易捆綁在一起,這些解決方案減少了鏈上交易的數量,從而降低了Gas 費用。使用Layer 2 解決方案還可以提高以太坊的可擴展性,使更多用戶和應用能夠參與網絡,而不會導致網絡超載引起擁塞。

5. 使用優化工具和函式庫

有多個最佳化工具可供使用,例如solc 最佳化器、Truffle 的建置最佳化器和Remix 的Solidity 編譯器。

以太坊智能合約的Gas優化十大最佳實踐

這些工具可以幫助最小化字節碼的大小、刪除無用程式碼,並減少執行智慧合約所需的操作次數。結合其他Gas 最佳化函式庫,如“solmate”,開發者可以有效地降低Gas 成本並提高智慧合約的效率。

結論

優化Gas 消耗是開發者的重要步驟,既可以最小化交易成本又能提高EVM 相容網路上智慧合約的效率。透過優先執行節省成本的操作、減少儲存使用、利用內聯彙編以及遵循本文討論的其他最佳實踐,開發者可以有效地降低合約的Gas 消耗。

不過,必須注意的是,在最佳化過程中,開發者必須謹慎操作,以防引入安全漏洞。在優化程式碼和減少Gas 消耗的過程中,永遠不應犧牲智慧合約固有的安全性。

[1] :https://ethereum.org/en/developers/docs/gas/

[2] :https://ethereum.github.io/yellowpaper/paper.pdf

[3] :https://www.evm.codes/

[4] :https://www.evm.codes/precompiled