---
title: "ERC-4337 UserOperation 打包漏洞"
description: "2023 年 3 月 7 日，Alchemy 與開源社群成員發現 ERC-4337 EntryPoint 合約及範例 VerifyingPaymaster 合約存在 calldata 解碼問題。"
---

# ERC-4337 UserOperation 打包漏洞

**Additional Contributors**:
[fangting@alchemy.com](mailto:fangting@alchemy.com), @drortirosh, @Gooong, @taylorjdawson, @leekt, @livingrockrises

## 概述

2023 年 3 月 7 日，Alchemy 與開源開發者社群的其他成員，包括 [@Gooong](https://github.com/Gooong)、[@taylorjdawson](https://github.com/taylorjdawson)、[@leekt](https://github.com/leekt) 和 [@livingrockrises](https://github.com/livingrockrises)，發現了 ERC-4337 EntryPoint 合約與範例 VerifyingPaymaster 合約中的 calldata 解碼問題。

這些合約目前已部署到多條鏈上，並對 user operation 產生雜湊值。此實作方式會依簽署方法的不同而產生不一致的雜湊值，進而可能導致一些二階效應，例如同一個 UserOperation 產生不同雜湊值、或不同的 UserOperation 產生相同雜湊值（碰撞）。

討論由 [@drortirosh ](https://github.com/drortirosh)主持，並記錄於[這個 Github issue](https://github.com/eth-infinitism/account-abstraction/issues/237#issuecomment-1466686252)中。

## 詳細分析

以下是受影響程式碼的分析，包含 EntryPoint Packing Vulnerability、VerifyingPaymaster Packing Vulnerability 的說明，以及各自的影響。

### 受影響的程式碼

問題所在的程式碼片段如下：

#### ‍[UserOperation.sol:61-75](https://github.com/eth-infinitism/account-abstraction/blob/556f03fadcaba0d7d04cd901e6eb9601db50a998/contracts/interfaces/UserOperation.sol#L61-l75)

#### [VerifyingPaymaster.sol:35-49](https://github.com/eth-infinitism/account-abstraction/blob/556f03fadcaba0d7d04cd901e6eb9601db50a998/contracts/samples/VerifyingPaymaster.sol#L35-l49)

作為參考，`UserOperation` 結構定義如下：

這兩段程式碼都使用組合語言（assembly）將一大段 calldata 複製到記憶體中，目的是擷取 user operation 中要進行雜湊運算的部分。

`UserOperationLib` 中的 `pack` 方法，其目的是擷取 user operation 從 `sender` 到 `maxPriorityFeePerGas` 的所有欄位，包括可變長度欄位（在 ABI 編碼中稱為動態欄位）`initCode`、`callData` 和 `paymasterAndData`。

`VerifyingPaymaster` 中的 `pack` 方法包含除 `paymasterAndData` 欄位以外的所有這些欄位，因為該欄位當時尚未定義。

為實作此功能，這兩個方法都使用了 [Yul](https://docs.soliditylang.org/en/v0.8.17/yul.html) 為 calldata 中動態型別提供的便利欄位，名為 `.offset`。這指的是結構體 ABI 編碼中提供的值，其定義可參見[此處的 Solidity 規範](https://docs.soliditylang.org/en/v0.8.18/abi-spec.html#formal-specification-of-the-encoding)。**（實際上它指的是 offset 之後的那個記憶體字，但這只是為了方便讀取而已）。**

標準的 ABI-encoder 會依照欄位出現的順序，對動態欄位（在 ABI coder 中稱為其 tail）的值進行編碼。

考慮以下可能產生的 user operation calldata 編碼方式：

<CalloutBlock>

💡 **注意：** 為簡潔起見，此範例顯示的是所有動態欄位長度皆不超過一個 word 的 user operation。

</CalloutBlock>

<CalloutBlock>

💡‍ **\*‍**\***注意：** 此處的記憶體位址空間僅限於 user operation 結構體本身。在實際的 calldata 中，由於方法選擇器（method selector）和參數 tuple 會佔用空間，其位置會有所不同。

</CalloutBlock>

在此範例中，依照 `pnd.offset` 對 user operation 進行打包，會得到以下這段 calldata 的「片段」：

這正好是我們想要的內容！

然而，使用 ABI 編碼引數的合約並不會驗證欄位定義的順序，甚至不會驗證 offset 是否有效。

使用 `signature.offset` 或 `pnd.offset` 會直接從 calldata 中讀取對應的「offset」值。

利用這一點作為邊界，就有可能建構出在 calldata 中有效、但具有異常雜湊特性的 user operation 表示方式。

接下來讓我們分別探討這如何影響 EntryPoint 與 VerifyingPaymaster。

### EntryPoint packing vulnerability

要示範此漏洞，我們必須考慮一個與所提供的 `SimpleAccount.sol` 不同的錢包合約，因為該範例重複使用了 `EntryPoint` 中存在漏洞的程式碼。

當 EntryPoint 與錢包合約使用不同的雜湊方案，或錢包對非標準的 user operation 編碼進行簽署時，雜湊值不一致的問題就會產生實質影響。

這對 `EntryPoint` 帶來的風險是：單一 user operation 可以對應到多個「user op hash」，而同一個「user op hash」也可以對應到多個 user operation。

考慮這個帳戶，稱為 `ExampleAccount`，它有自己的 `ExampleAccountFactory`。此範例帳戶使用單一簽署者來驗證 user operation。

要授予執行 user operation 的權限，會針對 user operation 中除簽章本身以外的所有欄位產生一個雜湊值並加以簽署。

`validateUserOp` 方法的定義如下：

這是一個相對簡單的簽章驗證實作，因為它會檢查 `userOp` 的所有欄位，連同 entrypoint 位址與 chain id 一併檢查。

作為帳戶抽象化（account abstraction）目標之一，`validateUserOp` 方法可以包含任意邏輯（雖然會受到儲存存取的限制），因為此方法代表 user operation 得以發起的條件。

對於此範例帳戶，user operation 是由擁有者的簽章發起的。但更廣泛而言，user operation 可以來自任意條件：鏈上狀態、多重簽章，或應用程式特定的簽章——這正是帳戶抽象化的一項特性。

為示範此漏洞，讓我們建構惡意 calldata 傳入 `EntryPoint.handleOps`，使得 `EntryPoint` 所發出的 `UserOperationEvent` 帶有非預期的值。

在定義一個範例 `UserOperation memory uo` 結構之後，以下是我們如何建構 calldata：

`rightPadBytes` 是一個輔助函式，用於將 `bytes` 型別對齊到最接近的完整 word 長度。

其定義如下：

現在，當呼叫 `handleOps` 時，所發出的事件與 `EntryPoint.getUserOpHash\(\)` 的結果將會不同。

#### 影響

惡意的 bundler，或呼叫 `EntryPoint.handleOps` 的非 bundler EOA，可以修改 calldata 中 `UserOp` 的表示方式，藉此改變在發出事件中的 UO hash。這可能破壞與所發出事件整合的鏈下系統，因為這些事件現在被揭露對於同一個 UO 並非確定性的。

此外，bundler 在讀取 `EntryPoint` 合約所發出的 userOpHashes 時，也必須處理非確定性的問題。要判斷 `EntryPoint` 所發出的 `UserOperationEvent` 是否對應到 bundler 本地 mempool 中的某個 user operation，僅比對雜湊值已不再足夠，因為傳入 `handleOps` 的 calldata 可被修改以改變雜湊值。

取而代之，bundler 必須查詢交易收據、取得傳送至 `handleOps` 的 calldata、解碼該 calldata，然後透過標準 ABI coder 重新編碼並呼叫 `EntryPoint.getUserOpHash\(...\)` 來取得「標準」雜湊值。這是判斷本地 mempool 中的 user operation 是否已被打包所必需的步驟。此外，由於對 `EntryPoint.handleOps` 的呼叫可能發生在其他合約呼叫內部，解碼工作可能會深入呼叫堆疊之中。

此雜湊不一致的問題也會影響 bundler RPC 方法的實作，因為 user op hash 在 `eth\_getUserOperationByHash` 和 `eth\_getUserOperationReceipt` 中被用於識別。

Bundler 將需要進行昂貴的搜尋、剖析與解碼傳入 `EntryPoint.handleOps\(...\)` 的 calldata，才能將事件中發出的雜湊值轉換為來自 `EntryPoint.getUserOpHash\(...\)` 的「標準」雜湊值。

<CalloutBlock>

💡 **注意：** 此漏洞與惡意 SCW 可重複使用 user operation 這件事是不同的問題。重複使用的 user operation，以及更廣泛而言所有的 user operation，都應該具有確定性的雜湊值。其他建構在 ERC-4337 之上的應用程式與服務，若此問題尚未解決，就必須自行實作對應的緩解措施。

</CalloutBlock>

由於 ERC-4337 整體上仍處於採用初期，很難描述此漏洞對更廣泛生態系統的潛在影響。影響範圍取決於 bundler、user operation 瀏覽器、索引器及其他鏈下服務的實作方式。

在**最低程度**上，這會造成令人困惑的使用者體驗，因為 user operation hash（類似於 transaction hash）可能在提交與納入區塊的時間點之間發生變化，因此某些錢包可能未考量到此差異，導致無法向使用者顯示正確的更新。

在**中度風險**的情況下，錢包可以被設計成刻意規避索引，方法是讓其所有的 user op hash 都相同（參見 [@leekt 提供的範例](https://github.com/leekt/foundry-4337/compare/PoC/sameUserOpHash)）。

在**高風險**的情況下，監控 user operation 是否被納入區塊的鏈下服務，可能會錯過某個 user operation 已被納入的事實，進而嘗試重新傳送或以其他方式錯誤處理資料與金鑰。

#### 概念驗證

完整的概念驗證請見[此 repo](https://github.com/alchemyplatform/entrypoint-hash-poc)。

### VerifyingPaymaster packing vulnerability

`VerifyingPaymaster` 所帶來的風險是，user operation 在簽署時與鏈上納入時，內容可能有所不同。當兩個不同的 user operation 從 `VerifyingPaymaster.getHash\(UserOperation userOp, uint48 validUntil, uint48 validAfter\)` 得到相同的雜湊值時，就可能發生這種情況。

讓我們針對此函式建構 calldata，以示範這種情況如何發生：

#### Calldata 1
請注意，此編碼方式改變了動態欄位的順序，但其餘部分仍然有效——offset 指向正確的位置，長度也都有效。但由於 `paymasterAndData` 的 offset 是一個非預期的值，我們從 `pack\(\)` 得到的片段將會是以下內容：

可以看到 `initCode` 和 `callData` 都被排除在該片段之外！

現在讓我們建構第二筆輸入 calldata，這次惡意地同時修改這兩個欄位：

- `initCode` 會從 `1517c0de` 變成 `1517c0de02`
- `callData` 會從 `ca11dada1` 變成 `ca11data02`

這會得到以下結果：

#### Calldata 2
如果我們對這筆不同的 calldata 執行相同的 `pack` 操作，會得到與先前相同的片段！我們可以透過以下方式驗證兩者的雜湊值是相同的：

在 foundry 的測試環境中執行此程式碼，會發現這兩個 user op 的雜湊值相同：`0x736f86d224bab46a95ae119947e172efa694379d9ac682d4ca780b7640a89b06`

完整的 POC 請見[此測試檔案](https://github.com/alchemyplatform/entrypoint-hash-poc/blob/main/test/VerifyingPaymasterHashPOC.t.sol)。

#### 影響

在此漏洞中，雜湊值可被修改為只涵蓋比預期更少的元素，使得 initCode、callData 以及可能的其他靜態欄位被排除在雜湊之外，因而在簽署與實際使用之間產生差異。這可能導致 paymaster 的贊助簽章被用於原本不打算允許的用途。

舉例來說，錢包合約的部署工廠（deployer factory）以及對錢包 `execute` 函式的呼叫都可能被替換。如果某個 paymaster 原本只想贊助其錢包的使用者，且僅在使用者鑄造特定 NFT 時才提供贊助，這些規則都有可能被繞過。

要繞過這些規則，發送者可以在取得簽章之後修改 `userOp.initCode` 和 `userOp.callData`。如此一來，paymaster 的原生代幣（ETH 或其他）就會被用於其原本無 gas NFT 鑄造意圖以外的其他用途。

以 ABI 編碼格式接收 user operation 進行簽署的鏈下簽署者，或是擁有合約整合以準備待簽署資料的簽署者，都容易受到此問題影響。這個影響範圍有限，因為本質上他們是在「自我攻擊」，但這對於營運 paymaster 服務仍構成風險。

針對此問題的防禦措施包括部署更新版本的 `VerifyingPaymaster`，或自行處理從使用者輸入進行 ABI 編碼的流程。

## 結論

在與生態系成員進行多次精彩的討論後，**@drortirosh** 合併了一個經過最佳化且易讀的[修補程式，套用於 Entrypoint 合約](https://github.com/eth-infinitism/account-abstraction/commit/6a7cbe7c6c786d6302d52b94405b88c6596f95cb)，以解決此漏洞。重新部署後，Entrypoint 合約將不再出現上述行為。

此外，也有一項[提案](https://docs.google.com/document/d/1MywdH_TCkyEjD3QusLZ_kUZg4ZEI00qp97mBze9JI4k/edit#heading=h.gyhqxhuyd59n)要在 Entrypoint 中對 nonce 支援進行抽象化，藉此進一步強化此系統。由於此問題對 paymaster 造成的風險有限，目前並未針對上游做出正式修補，paymaster 營運者可依其實作需求自行決定如何處理此問題。

我們要感謝 4337 社群，包括 **@drortirosh**、**@Gooong**、**@taylorjdawson**、**@leekt** 和 **@livingrockrises**，與我們一同解決此漏洞。

有任何問題或想討論的主題嗎？

歡迎透過 account-abstraction@alchemy.com 與我們聯繫。
