---
title: "ERC-4337 gas 預估的運作方式"
description: "了解 Alchemy 工程師如何用 Rust 開發 ERC-4337 bundler，提供精確的 user operation gas 預估，並探討對 entry point contract 提出的修改建議，讓 gas 預估流程更簡單。"
---

# ERC-4337 gas 預估的運作方式

在 Alchemy 建構 ERC-4337 Bundler「Rundler」的過程中，最難處理好的元件就是 user operation 的 gas estimation。本文將說明我們在提供準確 gas 估算時遇到的障礙，以及目前採用的解決方案。

這是一篇技術性的概述文章，適合 ERC-4337 開發者閱讀。若你剛接觸 [ERC-4337 風格的 account abstraction](https://www.alchemy.com/overviews/what-is-account-abstraction)，建議先閱讀我們的 [Account Abstraction 系列入門文章](https://www.alchemy.com/overviews/what-is-account-abstraction)。

## 為什麼 gas estimation 對 ERC-4337 user operation 很重要？

提供準確的 user operation gas 估算，對 ERC-4337 的使用者體驗來說相當重要。如果 gas 估算過低，user operation 可能在模擬階段就 revert，更糟的是可能在鏈上執行階段才 revert，導致使用者為一筆已 revert 的操作支付 gas 費用。如果 gas 估算過高，使用者可能因為成本而卻步，甚至無法送出他們的 operation。

雖然準確度很重要，但 gas estimation 並不需要做到 100% 準確，只要誤差始終是高估（但不能高估太多）即可。在 ERC-4337 中，gas 欄位皆以上限（limit）表示，使用者對於鏈上未消耗的 gas 會獲得退款。因此，gas estimation 並不會影響 operation 的實際成本。

<CalloutBlock>

💡 \*但 `preVerificationGas` 是例外，稍後會詳細說明。

</CalloutBlock>

### ERC-4337 gas 欄位定義

user operation 中的 gas 欄位，依 [ERC-4337 規範](https://eips.ethereum.org/EIPS/eip-4337#definitions) 定義如下：

- `preVerificationGas`：用來補償 bundler 進行 pre-verification 執行與 calldata 所需支付的 gas 數量。
- `verificationGasLimit`：為驗證步驟分配的 gas 數量。
- `callGasLimit`：為主要執行呼叫分配的 gas 數量。
- `maxFeePerGas`：每單位 gas 的最高手續費（類似 [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) 的 `max\_fee\_per\_gas`）。
- `maxPriorityFeePerGas`：每單位 gas 的最高優先費（類似 EIP-1559 的 `max\_priority\_fee\_per\_gas`）。

## ERC-4337 的 gas 計量方式

[ERC-4337 bundler](https://www.alchemy.com/overviews/what-is-a-bundler) 會預先支付將 bundle 交易送至 entry point 合約的成本。entry point 會計量每個 user operation 所使用的 gas，將其乘以計算出的費用，並在 user operation 完成後，以此金額補償 bundler。

實際的計算方式如下：\*

<CalloutBlock>

💡 \*為簡化說明，此處僅適用於支援 EIP-1559 的網路。

</CalloutBlock>

`preVerificationGas` 會直接加上，而 verification gas 與 call gas 則會被「計量」。也就是說，它們的 gas 使用量會由鏈上的 entry point 進行測量，並依其實際使用量收費，上限為各自的 limit。若在 verification 或 call 階段觸及 limit，該 operation 將會 revert。

entry point 無法處理 [gas 退款](https://www.evm.codes/about#gasrefunds)，因為它是在交易過程中計量 gas，而 gas 退款是在交易完成後才發放，因此會直接退還給 Bundler。這對使用者而言是額外的成本來源，對 bundler 而言則是潛在的收入來源。

## User operation 流程

[送出 user operation](http://www.alchemy.com/overviews/user-operations) 的典型流程包含以下步驟：

1. 建構一個部分填寫的 user operation，填入 `sender`、`nonce`、`initCode` 與 `callData`。
   a. 同時以 [「dummy」值](https://www.alchemy.com/blog/dummy-signatures-and-gas-token-transfers) 填入 `signature` 與 `paymasterAndData`。
1. 透過 bundler RPC 的 [`eth\_estimateUserOperationGas`](https://www.alchemy.com/docs/wallets/api-reference/bundler-api/bundler-api-endpoints/eth-estimate-user-operation-gas) 為這個部分填寫的 user operation 估算 gas。
   a. 依回傳值填入 `preVerificationGas`、`verificationGasLimit`、`callGasLimit`。
1. [估算此 operation 所需的 gas 費用](https://www.alchemy.com/blog/user-operation-fee-estimation)，並填入 `maxFeePerGas` 與 `maxPriorityFeePerGas`。
   a. 此步驟不依賴步驟 1 或 2，可以在任何時間執行，或平行執行。
1. （選擇性）將 user operation 送至 sponsoring paymaster 進行簽署
   a. 依回傳值填入 `paymasterAndData`。
1. 對上述 user op 進行簽署，填入 `signature`，並透過 [`eth\_sendUserOperation`](https://www.alchemy.com/blog/erc-4337-gas-estimation#) 送至 bundler。

本文將聚焦於步驟 2，以及 bundler RPC 方法 `eth\_estimateUserOperationGas`。

### User operation gas estimation

`eth\_estimateUserOperationGas` 是一個 RPC 方法，依 [ERC-4337 規範](https://eips.ethereum.org/EIPS/eip-4337#-eth_estimateuseroperationgas) 規定，bundler 必須支援此方法。

**定義如下：**

_估算一個 UserOperation 所需的 gas 值。給定一個選擇性未帶 gas limit 與 gas 價格的 UserOperation，回傳所需的 gas limit。錢包會忽略 signature 欄位，因此該 operation 不需要使用者核准。不過，仍可能需要放入一個「半有效」的簽章（例如長度正確的簽章）。_

**\*參數****：與 *`eth_sendUserOperation`* 相同，gas limit（與價格）參數為選擇性，但若有指定則會被採用。*`maxFeePerGas`* 與 *`maxPriorityFeePerGas`* 預設為零，因此帳戶與 paymaster 皆不需支付費用。\*

**_回傳值：_**

- **\*preVerificationGas**** 此 UserOperation 的 gas 額外開銷\*
- **\*verificationGasLimit**** 此 UserOperation 驗證階段實際使用的 gas\*
- **\*callGasLimit**** 帳戶內部執行所使用的值\*

### PreVerificationGas 的計算方式

`PreVerificationGas` 用來涵蓋 entry point 未計量的 gas 使用量，藉此補償 bundler。

**在簡單情況下，可拆解為 3 項獨立計算：**

1. 該 operation 在 bundle 交易中所佔的 [intrinsic gas](https://ethereum.org/en/developers/docs/gas/#what-is-gas-limit) 份額。
   a. 注意 bundler 必須事先假設一個 bundle 大小，才能決定此份額。
1. 該 operation 所佔的 calldata gas 成本份額。
   a. 這與該 operation 的大小及位元組組成直接相關。
1. 該 operation 所佔、entry point 在計量範圍之外產生的任何執行 gas 額外開銷份額。
   a. 這是透過鏈下對 entry point 合約進行 gas 使用量分析後決定，並依每個 user operation 分攤計算。

請注意 `preVerificationGas` 並非 limit。也就是說，該欄位設定的值一律會全額支付給 bundler。使用此欄位時要小心，因為錯誤的數值可能導致支付給 bundler 的費用超出實際所需！

詳情可參考 StackUp 團隊撰寫的[深入分析](https://www.stackup.sh/blog/an-analysis-of-preverificationgas)，內容針對 `preVerificationGas` 有更詳細的說明。

Bundler 通常會在「pre-check」階段（即完整 trace-based 模擬之前執行的檢查）進行 `preVerificationGas` 的計算。

若某個 operation 的 `preVerificationGas` 低於 bundler 計算出的值，該 bundler 會予以拒絕。這可能成為不同 bundler 實作之間不相容的來源。

若某個 bundler 實作 L 估算出較低的 `preVerificationGas` 值，而另一個實作 H 估算（並要求）較高的值，則 H 會拒絕使用 L 估算值送出的 operation。

目前尚不清楚這種不相容性會如何影響 P2P mempool，但使用者很可能會傾向於稍微提高 `preVerificationGas` 的估算值，以提升自己的 operation 被打包的機率。

## Alchemy 的 ERC-4337 bundler 如何估算 gas limit

準確估算 gas limit 欄位——`verificationGasLimit` 與 `callGasLimit`——十分重要。目標可以這樣描述：提供一個函式，用來估算這兩個階段所使用的 gas，且必須確保只會高估，但高估幅度不能大到讓使用者打消送出 operation 的念頭。

讓我們來討論我們在 [Rundler](https://www.alchemy.com/account-abstraction) 中，為了實現這個函式所做過的各種嘗試。

### 嘗試一：`eth\_estimateGas`

標準 Ethereum JSON-RPC 提供了一個估算交易 gas 的好用方法，那我們直接用 [`eth\_estimateGas`](https://www.alchemy.com/docs/chains/ethereum/ethereum-api-endpoints/eth-estimate-gas) 吧！

**這個方法的做法是：**

- `verificationGasLimit `：呼叫 entry point 上的 `simulateValidation` 函式，並傳給 `eth\_estimateGas`
- `callGasLimit`：呼叫 entry point 上的 `innerHandleOp` 函式，並傳給 `eth\_estimateGas`

這看起來運作得不錯。雖然 `simulateValidation` 與 `innerHandleOp` 都無法精確捕捉各階段被計量的部分，但它們都是嚴格意義上的超集合（superset），因此會稍微高估。

請注意，這兩次呼叫是完全獨立的，但在鏈上是在同一筆交易中執行。在這個方法中，`simulateValidation` 的執行結果不會被保留下來影響 `innerHandleOp`。

#### 如果 `innerHandleOp` 依賴於 `simulateValidation` 期間所做的事情呢？

這種情況會發生在帳戶初次部署時，也可能出現在某些進階的驗證方案中。factory 方法會在驗證階段被呼叫，以部署帳戶合約。執行階段則需要該合約已部署完成，否則 `innerHandleOp` 會 revert，導致任何估算嘗試失敗。

我們需要一種方式，能夠同時估算這兩個 gas limit 欄位，並確保執行階段的估算與驗證階段在同一次呼叫中執行。

接下來看下一個嘗試……

### 嘗試二：模擬並測量

ERC-4337 v0.6 的實作提供了 [`simulateHandleOp`](https://github.com/eth-infinitism/account-abstraction/blob/033b4be2a606defd5cd5226bdde3afcea21db5fa/contracts/core/EntryPoint.sol#L184) 方法，將驗證與執行兩個階段合併在單一函式呼叫中。其回傳值包含 [`preOpGas`](https://www.alchemy.com/blog/erc-4337-gas-estimation#)，也就是驗證階段所用 gas 與 `preVerificationGas` 的總和。它同時也包含了 user op 所支付的總金額 `paid`。

要決定 `callGasLimit`，可以將 `paid` 除以 gas 費用，換算成使用的 gas 量。bundler 可以透過將模擬固定在某個（近期的）區塊高度（該高度的 base fee 已知），並將 priority fee 設為 0，來決定 gas 費用。經過一些簡單的換算，就能計算出所有的 gas 欄位。此方法同樣可能高估所使用的 gas，因為部分未被計量的 entry point 邏輯會被歸入使用的 gas 之中。

另一種合理的方法，是測量整個 `simulateHandleOp` 呼叫過程中使用的 gas，再減去驗證階段所使用的 gas，以此測量 call 階段所使用的 gas。

我們可以部署一個輔助合約，用來計量 `simulateHandleOp` 所使用的 gas，並將其附加到回傳值中。這個方法和前一種方法一樣，都有高估的問題。

對於在意高估程度的人，我們可以進一步精煉這個方法。

#### 我們能否透過追蹤（trace）entry point 的 `simulateHandleOp` 方法來計算 gas？

Bundler 必須具備執行 trace call 的能力，才能進行保護自身免於鏈上失效所需的模擬檢查。我們能否利用這項功能來估算 gas？

**這個方法的做法是：**

1. 追蹤 entry point 的 `simulateHandleOp` 方法，該方法很方便地使用了一種「number marker」機制，讓 tracer 能夠準確得知目前處於哪個執行階段。
1. 在 tracer 中，記錄每個階段開始時剩餘的 gas 量，以及最終結束時的剩餘量。
1. 依這些數值，計算出每個階段所使用的 gas 量。

太好了！我們用了稍微昂貴一些的 trace，精確計算出實際使用的 gas 量。

先別高興得太早，使用上述任一方法，都可能導致某些合約呼叫在執行時 gas 不足。

為什麼？

要回答這個問題，我們可以參考 [Reth 如何估算 gas](https://github.com/paradigmxyz/reth/blob/547911ac19c68b750f7920a6cd613fa691328ce7/crates/rpc/rpc/src/eth/api/call.rs#L72)。注意到裡面的二元搜尋（binary search）了嗎？那是在做什麼？為什麼 Reth 不直接使用上述類似的追蹤方法，畢竟它能取得完全相同的資訊？

其實原因在於，在 EVM 的函式呼叫中：**gas used（已使用的 gas）≠ gas required（所需的 gas）**。

關於為何如此的完整說明，可參閱 Sergio Lerner 撰寫的「[The Dark Side of Ethereum 1/64th CALL Gas Reduction](https://medium.com/iovlabs-innovation-stories/the-dark-side-of-ethereum-1-64th-call-gas-reduction-ba661778568c)」一文。其中最重要的原因稱為 [「63/64th 規則」](https://eips.ethereum.org/EIPS/eip-150)（或依對象不同，也稱為「1/64th 規則」）。

此規則規定，EVM 在每次函式呼叫時，只會轉發剩餘 gas 的 63/64。結果就是，你事先需要準備的 gas 量會略高於最終實際消耗的量。由於每次呼叫都會發生這種情況，因此若驗證/執行階段的呼叫堆疊很深，就可能需要預先保留大量 gas，以應付這些被保留起來的 gas。

我們希望找到一個對「所有」可能的 user operation 都適用的解決方案，因此任何依賴「已使用 gas」測量值來估算 gas limit 的做法都不再適用。在下一個嘗試中，讓我們從 EVM 客戶端汲取靈感。

<CalloutBlock>

💡 或許可以對追蹤函式進行詳細分析，並考慮 63/64th 規則（以及其他 EVM 的細微機制），將已使用的 gas 反推回 gas limit。我們尚未深入探討這個方向。

</CalloutBlock>

### 嘗試三：二元搜尋（binary search）

這裡不需要重新發明輪子。為了處理上述提到的複雜性，節點客戶端在進行 gas estimation 時會使用二元搜尋，找出能讓交易成功的最低 gas 值。我們也可以對 user operation 採用類似的做法！

做法如下：

首先要注意，由於二元搜尋需要對同一個函式進行多次呼叫，我們希望確保該函式的效能夠高。這使得追蹤（tracing）不再是合理的選擇，因此我們需要依賴 `eth\_call`。

**估算** `verificationGasLimit` **的方式：**

1. 先以 `MAX\_VERIFICATION\_GAS`（[bundler 的一項設定](https://eips.ethereum.org/EIPS/eip-4337#client-behavior-upon-receiving-a-useroperation)，定義驗證階段可使用的最大 gas 量）執行一次 `simulateHandleOp`，以確認成功執行是否可行。
   a. 在此次執行中，將 `callGasLimit` 設為 0 以節省運算。
1. 對 `simulateHandleOp` 執行二元搜尋，以逼近不會耗盡 gas 的最低 `verificationGasLimit` 值。
   a. 若驗證階段耗盡 gas，呼叫會 revert。由於我們已經確認在較高 gas limit 下呼叫能成功，因此任何 revert 都可以將二元搜尋的下界往上移動。

這裡有個不錯的最佳化方式，就是意識到我們並不需要 100% 精確的估算結果。bundler 可以自行決定希望接近精確值的程度，一旦落在此範圍內就結束二元搜尋，並始終偏向較高的一側。這樣可以省下許多輪次的呼叫。舉例來說，Rundler 將這個誤差範圍設為 1K gas，省下了 10 次迭代。

第二個可以省下幾次迭代的最佳化方式，是為搜尋找出更好的起始點。將（1）中所使用的 gas 乘上一個係數（例如 Reth 使用 3 倍），並以此作為二元搜尋演算法的初始猜測值。

**估算** `callGasLimit` **的方式：**

1. 將 `verificationGasLimit` 設為 `MAX\_VERIFICATION\_GAS`
1. 嘗試執行 `MAX\_EXECUTION\_GAS`（此並非規範的一部分，但 bundler 需要用它來保護自己免於 DOS 攻擊），並檢查執行是否有可能成功。

到了這一步，這個估算方法就出現問題了。Revert 會[被 entry point 捕捉並以 log 的形式發出](https://github.com/eth-infinitism/account-abstraction/blob/develop/contracts/core/EntryPoint.sol#L250)，而 `eth\_call` 並未提供檢視任何已發出 log 的方式。而且它在每次嘗試中都會完整執行一次驗證，浪費了運算資源。

我們能否找出一種方式，讓 `callGasLimit` 的二元搜尋得以執行？

<CalloutBlock>

💡 若使用 trace，是有可能偵測到此 revert 的，但出於成本考量，我們希望避免這麼做。若某個 bundler 能完全掌控節點客戶端，並能使用原生的 tracer，或許值得重新考慮這個決定。

</CalloutBlock>

#### 解決方案：在 Solidity 中執行二元搜尋

在 `verificationGasLimit` 估算方法的基礎上，這個方法的核心概念是在 [Solidity](https://www.alchemy.com/overviews/solidity) 中執行 `callGasLimit` 的二元搜尋，並將能夠偵測執行部分是否失敗的邏輯直接編碼進去。

**讓我們先定義限制條件：**

1. 二元搜尋必須在驗證執行完成之後才能進行，以確保任何帳戶合約都已部署完成
   a. 一個理想但非必要的限制，是希望驗證只執行一次（或限制執行次數），以減少浪費的運算。
1. 執行呼叫必須從 entry point 發起。許多帳戶實作只允許在 `msg.sender` 等於硬編碼的 entry point 位址時，才能呼叫其執行函式。
1. 二元搜尋必須回傳最終結果。
1. 所有 entry point 函式的行為都必須與正常情況完全一致。

我們想出的解決方案，是使用一個代理合約（proxy contract），透過 `DELEGATECALL` 將呼叫轉發至 entry point 合約，同時加入額外的邏輯。在估算期間，我們使用 `eth\_call` overrides，將原本的 entry point 合約移到一個隨機位址，並以代理合約取代它。這滿足了限制條件（2）與（4）。

如前所述，entry point 會隱藏執行階段的結果。我們改為略過標準的執行呼叫（將 `callGasLimit` 設為 0），並利用傳給 `simulateHandleOp` 的 `target/targetCallData` 參數。這會在驗證之後執行，並能回傳資訊，滿足限制條件（1）與（3）。以下是 entry point 的程式碼（已加上註解）：

對於我們自訂的目標呼叫，我們會呼叫代理合約上的一個方法 `estimateCallGas`，其呼叫資料編碼了以下內容：

1. 該帳戶及其呼叫資料（來自原始的 user operation）
1. 二元搜尋的參數

由於代理合約位於原本 entry point 的位址，`estimateCallGas` 能夠呼叫該帳戶的執行函式，判斷該呼叫是成功還是因耗盡 gas 而失敗，並持續執行二元搜尋演算法，直到成功為止。

大功告成！現在二元搜尋可以在一次 `eth\_call` 中執行，藉此取得 `callGasLimit` 的精確估算值！

實務上，Rundler 會將這單一次 `eth\_call` 拆成多次呼叫，每次都設有一個全域最大 gas limit，以此繞過節點的最大 call gas limit 限制。每次呼叫都會執行二元搜尋迭代，直到下一次迭代會導致該呼叫耗盡 gas 為止。

<CalloutBlock>

💡 此方法會遺漏 entry point 在 `innerHandleOp` 期間所產生的少量額外開銷。這部分開銷為固定值，會加到估算結果中。

</CalloutBlock>

## 潛在的 entry point 修改方向

我們一直在思考對 entry point 進行哪些潛在的修改，能讓上述的 gas estimation 流程更容易實作，也讓使用者獲得更好的效能。這不是一個容易解決的問題，因此如果其他 bundler 或 ERC-4337 社群的成員有任何想法，我們非常樂意聽取！

我們希望確保在 gas estimation 過程中，驗證階段一定會在執行階段之前執行。執行階段可能依賴驗證階段所設定的狀態（例如 USDC paymaster 的轉帳，以及使用 USDC 的執行階段），因此在驗證階段結束後立即進行估算會最為準確。

其中一項潛在的改進，是讓 entry point 在 `simulateHandleOp` 期間回傳一個布林值，用來表示執行部分是否有 revert。bundler 便能在鏈下自行執行二元搜尋，而不需要代理合約與 state overrides。然而，這樣的二元搜尋效率不佳，因為每次迭代都需要重新執行驗證階段。

這讓我們想到，可以將上述嘗試三中由代理合約執行的二元搜尋功能，直接加入 entry point 合約本身（虛擬程式碼）：

我們對上述內容（以代理合約形式）的實作，即將開源釋出。我們認為，若能讓透過二元搜尋進行估算的功能，直接成為 entry point 合約本身的一部分，對 ERC-4337 的使用者而言會相當有幫助。

敬請期待我們即將推出的開源 bundler 實作！🦀

## 繼續閱讀

本系列共四篇文章，後續文章將探討我們在 gas estimation 上遇到的挑戰，以及工程團隊實作的解決方案。

- [Dummy Signatures and Gas Token Transfers](https://www.alchemy.com/blog/dummy-signatures-and-gas-token-transfers)
- [Solving The L2 and Aggregator Problem](https://www.alchemy.com/blog/l2-gas-and-signature-aggregators)
- [User Operation Fee Estimation](https://www.alchemy.com/blog/user-operation-fee-estimation)
