ERC-4337 UserOperationパッキングの脆弱性
著者: Adam Egyed
追加コントリビューター: fangting@alchemy.com, @drortirosh, @Gooong, @taylorjdawson, @leekt, @livingrockrises
概要
2023年3月7日、Alchemyおよび@Gooong、@taylorjdawson、@leekt、@livingrockrisesを含むオープンソース開発者コミュニティのメンバーが、ERC-4337 EntryPointコントラクトおよびサンプルのVerifyingPaymasterコントラクトにおけるcalldataデコードの問題を発見しました。
これらのコントラクトは現在複数のチェーンにデプロイされており、user operationに対してハッシュを生成します。この実装は署名方式によって一貫性のないハッシュを生成する結果となり、同一のUserOperationに対して異なるハッシュが生成されたり、異なるUserOperationのハッシュが衝突したりするなど、複数の二次的な影響を引き起こす可能性があります。
議論は@drortirosh が中心となって進められ、このGithub issueに記録されています。
詳細な内訳
以下では、影響を受けるコードの内訳、EntryPointのパッキング脆弱性とVerifyingPaymasterのパッキング脆弱性の説明、そしてそれぞれの影響について説明します。
影響を受けるコード
問題となっているコードセグメントは以下の通りです:
UserOperation.sol:61-75
VerifyingPaymaster.sol:35-49
参考として、UserOperation構造体は以下のように定義されています:
これら両方のコードセグメントは、アセンブリを使用してcalldataの大部分をメモリにコピーし、ハッシュ化するためにuser operationの一部を取得しようとしています。
UserOperationLib内のpackメソッドは、senderからmaxPriorityFeePerGasまでのuser operationのすべてのフィールド(ABIエンコーディングでdynamic fieldと呼ばれる可変長フィールドであるinitCode、callData、paymasterAndDataを含む)を取得することを意図しています。
VerifyingPaymaster内のpackメソッドは、paymasterAndDataフィールドがまだ定義されていないため、そのフィールドを除くすべてのフィールドを含みます。
これを実装するために、両方のメソッドはcalldata内のdynamic typeに提供されるYulの便利なフィールド、.offsetを使用しています。これは、構造体のABIエンコーディングで提供される値を指しており、Solidity仕様のここで定義されています。(実際にはオフセットの後のメモリワードを指していますが、これは読み込み時の便宜上のものです)。
標準的なABIエンコーダーは、dynamic fieldの値(ABIコーダーではtailと呼ばれます)を出現順にエンコードします。
以下のような、calldata内で生成され得るuser operationのエンコーディングを考えてみましょう:
💡 注: この例では、簡潔にするためにすべてのdynamic fieldが1ワード未満の長さになっているuser operationを示しています。
💡 **注: ここでのメモリアドレス空間はuser operation構造体自体の中にあります。実際のcalldataでは、メソッドセレクターと引数タプルが占めるスペースのため、別の場所に配置されます。
この例では、pnd.offsetに従ってuser operationのパッキングを生成すると、calldataの以下のような「スライス」が得られます:
これはまさに私たちが求めているものです!
しかし、ABIエンコードされた引数を使用するコントラクトは、フィールドがどの順序で定義されているか、あるいはオフセットが有効かどうかさえ検証しません。
signature.offsetまたはpnd.offsetを使用すると、対応する「オフセット」値をcalldataから直接読み込みます。
これを境界として使用することで、通常とは異なるハッシュの性質を持つuser operationのcalldata上の有効な表現を構築することが可能になります。
これがEntryPointとVerifyingPaymasterにそれぞれどのような影響を与えるかを見ていきましょう。
EntryPointパッキング脆弱性
この脆弱性を示すには、提供されているSimpleAccount.solとは異なるウォレットコントラクトを考える必要があります。なぜなら、そのサンプルはEntryPointの脆弱なコードを再利用しているためです。
ハッシュの不一致は、EntryPointとウォレットコントラクトの間で異なるハッシュスキームが使用された場合、またはウォレットが非標準のuser operationエンコーディングに署名した場合に重要になります。
EntryPointに導入されるリスクは、1つのuser operationが複数の「user opハッシュ」によって表現され得ること、また同じ「user opハッシュ」が複数のuser operationを表現し得ることです。
ExampleAccountと呼ばれる、独自のExampleAccountFactoryを持つこのアカウントを考えてみましょう。このサンプルアカウントは、user operationを検証するために単一の署名者を使用します。
user operationを実行する許可を与えるには、署名自体を除くuser operationのすべてのフィールドに対するハッシュが生成され、署名されます。
validateUserOpメソッドは以下のように定義されています:
これは比較的シンプルな署名検証の実装で、entrypointアドレスとchain idとともに、userOpのすべてのフィールドをチェックします。
アカウント抽象化の目標の一つとして、validateUserOpメソッドは(ストレージアクセスの制限による制約はありますが)任意のロジックを含むことができます。なぜなら、このメソッドはuser operationがどのような条件下で発生し得るかを表しているためです。
このサンプルアカウントでは、user operationはオーナーによる署名から始まります。しかし、より一般的には、user operationはオンチェーンの状態、複数の署名、アプリ固有の署名など、任意の条件から発生し得ます――これがアカウント抽象化の特徴です。
この脆弱性を示すために、EntryPointによって発行されるUserOperationEventが予期しない値を持つように、EntryPoint.handleOpsへの悪意のあるcalldataを構築してみましょう。
サンプルのUserOperation memory uo構造体を定義した後、以下のようにcalldataを構築できます:
rightPadBytesは、bytes型を最も近いフルワード長に整列させるために書かれたヘルパー関数です。
以下のように定義されています:
さて、handleOpsを呼び出すと、発行されるイベントとEntryPoint.getUserOpHash\(\)の結果は異なるものになります。
影響
悪意のあるバンドラー、またはEntryPoint.handleOpsを呼び出す非バンドラーのEOAは、calldata内のUserOpの表現を変更することで、発行されるイベント内のUOハッシュを変更できます。これはオフチェーンシステムがイベントと統合する際に問題を引き起こす可能性があります。なぜなら、これらのイベントは特定のUOに対して非決定的であることが明らかになるためです。
さらに、バンドラーはEntryPointコントラクトから発行されたuserOpHashを読み取る際に、非決定性に対処する必要があります。EntryPointから発行されたUserOperationEventがバンドラーのローカルmempool内のuser operationに対応するかどうかを確認するには、ハッシュ値の比較だけでは不十分です。なぜなら、handleOpsへのcalldataを変更することでハッシュ値を変えることができるからです。
代わりに、バンドラーはトランザクションレシートを調べ、handleOpsに送信されたcalldataを取得し、そのcalldataをデコードした上で、標準のABIコーダーで再エンコードしEntryPoint.getUserOpHash\(...\)を呼び出すことで「正規の」ハッシュを取得する必要があります。これは、ローカルmempool内のuser operationがマイニングされたかどうかを判断するために必要です。さらに、EntryPoint.handleOpsへの呼び出しは他のコントラクト呼び出しの内部から発生し得るため、デコードはコールスタックの深いところで行われる可能性があります。
この不一致は、user opハッシュがeth\_getUserOperationByHashとeth\_getUserOperationReceiptでの識別に使用されるため、バンドラーRPCメソッドの実装にも影響します。
バンドラーは、イベントから発行されたハッシュをEntryPoint.getUserOpHash\(...\)からの「正規の」ハッシュに変換するために、EntryPoint.handleOps\(...\)へのcalldataの高コストな検索、パース、デコードを行う必要があります。
💡 注: この脆弱性は、悪意のあるSCWがuser operationを再利用できるという事実とは別のものです。再利用されたuser operation、そしてより一般的にはすべてのuser operationは、決定論的なハッシュを持つべきです。ERC-4337を基盤として構築される他のアプリケーションやサービスは、これが解決されない限り、独自の緩和策を実装する必要があります。
ERC-4337は全体として採用の初期段階にあるため、この脆弱性が広範なエコシステムに与える潜在的な影響を説明するのは困難です。影響の範囲は、バンドラー、user operationエクスプローラー、インデクサー、その他のオフチェーンサービスの実装に依存します。
最低限、混乱するユーザー体験を引き起こす可能性があります。user operationハッシュは(トランザクションハッシュと同様に)送信時から取り込み時の間で変化し得るため、一部のウォレットがこの違いを考慮せず、ユーザーへの更新表示に失敗するかもしれません。
中程度のリスクケースでは、ウォレットが意図的にインデックスを回避するように設計され、すべてのuser opハッシュを同一にするように設定される可能性があります(@leektが提供したこの例を参照)。
高リスクケースでは、user operationの取り込みを監視するオフチェーンサービスが特定のuser operationの取り込みを見逃し、データやキーの再送や誤った処理を試みる可能性があります。
概念実証
完全な概念実証はこのリポジトリを参照してください。
VerifyingPaymasterパッキング脆弱性
VerifyingPaymasterに導入されるリスクは、署名時とオンチェーンへの取り込み時の間でuser operationの内容が異なる可能性があることです。これは、2つの異なるuser operationがVerifyingPaymaster.getHash\(UserOperation userOp, uint48 validUntil, uint48 validAfter\)から同じハッシュを返す場合に発生し得ます。
この関数へのcalldataを構築して、これがどのように起こり得るかを示してみましょう:
Calldata 1
このエンコーディングはdynamic fieldの順序を変更していますが、それ以外は依然として有効であることに注目してください――オフセットは正しい位置を指しており、長さもすべて有効です。しかし、paymasterAndDataのオフセットが予期しない値であるため、pack\(\)から得られるスライスは以下のようになります:
initCodeとcallDataがスライスから除外されているのがわかります!
2番目の入力calldataを構築してみましょう。今回は両方のフィールドを悪意を持って変更します:
initCodeは1517c0deから1517c0de02に変わりますcallDataはca11dada1からca11data02に変わります
これにより以下のようになります:
Calldata 2
この異なるcalldataに対して同じpack操作を実行すると、以前と同じスライスが得られます!そして、以下によってハッシュが同じであることを確認できます:
これをfoundryのテスト環境で実行すると、両方のuser opが同じハッシュを持つことがわかります: 0x736f86d224bab46a95ae119947e172efa694379d9ac682d4ca780b7640a89b06
完全なPOCについてはこのテストファイルを参照してください。
影響
この脆弱性では、ハッシュが期待される要素よりも少ない要素をカバーするように変更される可能性があり、initCode、callData、その他の静的フィールドがハッシュから除外され、署名時と使用時の間で変化する可能性があります。これにより、paymasterのスポンサーシップ署名が意図とは異なる目的で使用される可能性があります。
例えば、ウォレットコントラクトのデプロイヤーファクトリーと、ウォレットのexecute関数への呼び出しを置き換えることができます。paymasterが以前、自社のウォレットのユーザーのみをスポンサーし、かつ特定のNFTをミントする場合にのみスポンサーしたいと考えていた場合、そのルールを回避される可能性があります。
ルールを回避するために、senderは署名を取得した後にuserOp.initCodeとuserOp.callDataを変更するでしょう。すると、paymasterのネイティブトークン(ETHなど)が、本来意図していたガスレスNFTミントとは別の目的に使用されることになります。
署名のためにABIエンコードされた形式でuser operationを受け取るオフチェーンの署名者、または署名用のデータを準備するコントラクト統合を持つ署名者は、この脆弱性の影響を受けます。これは本質的に「自分自身を攻撃する」ことになるため範囲は限定的ですが、paymasterサービスの運用にはリスクをもたらします。
これに対する防御策には、更新版のVerifyingPaymasterをデプロイすること、またはユーザー入力からのABIエンコーディングのプロセスを自ら処理することが含まれます。
結論
エコシステムのメンバーとの数々の有意義な議論の後、@drortiroshがこの脆弱性に対処するための最適化された読みやすいEntrypointコントラクトへのパッチをマージしました。再デプロイ後、Entrypointコントラクトは上記で説明した挙動を示さなくなります。
さらに、このシステムをさらに強化する、Entrypointにおけるnonceサポートを抽象化する提案もあります。paymasterへのリスクは限定的であるため、公式のアップストリームパッチは作成されておらず、paymasterの運用者は自身の実装に応じて必要な対応を決定できます。
@drortirosh、@Gooong、@taylorjdawson、@leekt、@livingrockrisesを含む、この脆弱性に共に取り組んでくれた4337コミュニティに感謝します。
ご質問や議論したいトピックはありますか?
account-abstraction@alchemy.comまでご連絡ください。
Alchemy Newsletter
リリース情報をいち早く受け取る
ニュースレターに登録する
Alchemyの最新のプロダクト情報とリソースをお届けします
メールアドレスを入力すると、当社のマーケティング情報およびプロダクト最新情報の受信に同意したことになります。Alchemyが受け取った情報をプライバシー通知に従って取り扱うことに同意するものとします。購読はいつでも解除できます。
関連記事

Glamsterdam: Ethereum開発者のための完全ガイド
GlamsterdamはSepoliaで稼働中です。開発者にとって何を意味するか、Alchemyがネットワークアップグレードにどう備えたかをまとめた完全ガイドです。

トレーディングチームが低レイテンシーRPCにAlchemyを選ぶ理由
トレーディングチームがAlchemyを選ぶのは、市場が動くときの低レイテンシーな読み取りと書き込み、そして出来高のピーク時でも資金が決済される際の信頼できる記録のためだ。

東京で構築するなら、どのRPCプロバイダーを選ぶべきか
東京を提供するRPCプロバイダー5社を、5つのチェーンでベンチマークした。比較結果と、レイテンシに敏感なチームにとって差が出るポイントを示す。