Pectra Upgradeが行われてからだいぶたっており、いまさら感はありますが個人的にずっと気になっていたのでEIP-7702について調べたことをまとめる。
いつも通り、go-ethereumの実装を参照しながら、EIP-7702に動作について詳しく見ていく。まずは入門としてEIP-7702の基本的な動きを確認する。
- EIP-7702入門:委譲の基本動作
EIP-7702入門:委譲の基本動作
概要
EIP-7702は、EOA(Externally Owned Account)がスマートコントラクトのコードを一時的に借用できるようにする仕組みである。本記事では最も基本的な委譲の設定と実行について、仕様書とソースコードを交えて解説する。
仕様についてはEIP-7702仕様書を、実装コードは go-ethereumを参考とした。
EIP-7702の目的
EIP-7702仕様書Abstractの以下の記載の通り、
Add a new transaction type that adds a list of `[chain_id, address, nonce, y_parity, r, s]` authorization tuples. For each tuple, write a delegation designator `0xef0100 || address` to the signing account's code.
EIP-7702の主な目的は、EOAにスマートコントラクトの機能を付与することで、アカウント抽象化のUXを改善することである。
基本的なシナリオ
最もオーソドックスな使用例を説明する:
Step 1: SetCodeTxによる委譲設定
トランザクション構造
EIP-7702仕様書Set Code Transaction
We introduce a new EIP-2718 transaction type `0x04` with the payload:
rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, value, data, access_list, authorization_list, y_parity, r, s])
実装コード
SetCodeTxは、通常のトランザクションフィールドに加えて、`AuthList`という委譲認可リストを持つ新しいトランザクションタイプである。
core/types/tx_setcode.go:50-66
type SetCodeTx struct { ChainID *uint256.Int Nonce uint64 GasTipCap *uint256.Int GasFeeCap *uint256.Int Gas uint64 To common.Address Value *uint256.Int Data []byte AccessList AccessList AuthList []SetCodeAuthorization // ← 委譲認可リスト(EIP-7702特有) // Signature values V, R, S *uint256.Int }
このトランザクションの特徴は、`AuthList`に複数の委譲設定を含められることである。つまり、1つのトランザクションで複数のEOAの委譲を一括設定できる。
SetCodeAuthorization(=AuthList)の構造
各委譲認可は、EOAの所有者が「私のアカウントをこのコントラクトに委譲することを許可する」という意思表示を署名付きで行うものである。
core/types/tx_setcode.go:71-78
type SetCodeAuthorization struct { ChainID uint256.Int Address common.Address // SmartWalletのアドレス(委譲先) Nonce uint64 // Aliceの現在のnonce(リプレイ攻撃防止) V uint8 // Aliceの署名(yパリティ) R, S uint256.Int // Aliceの署名値 }
この構造体は、Aliceが自分の秘密鍵で署名することで作成される。`ChainID`と`Nonce`により、この認可が特定のチェーンの特定の時点でのみ有効となることを保証する。
委譲設定の処理
SetCodeTxが実行されると、各認可に対して以下の処理が行われる:
core/state_transition.go:604-628
func (st *stateTransition) applyAuthorization(auth *types.SetCodeAuthorization) error { // 1. 署名からAliceのアドレスを復元 authority, err := st.validateAuthorization(auth) if err != nil { return err } // 2. Aliceのコード領域に委譲コードを設定 // "0xef0100" + SmartWalletのアドレス(合計23バイト) st.state.SetCode(authority, types.AddressToDelegation(auth.Address), tracing.CodeChangeAuthorization) return nil }
重要なのは、ここで実際のSmartWalletのコードをコピーするのではなく、「SmartWalletを参照せよ」という23バイトの委譲指示を設定することである。これによりストレージ使用量を最小限に抑える。
委譲プレフィックスの構造
EIP-7702仕様書 Delegation Indicator
The delegation indicator is followed by a 20-byte address. The code for the account is thus exactly `0xef0100 || address`, for a total of 23 bytes.
core/types/tx_setcode.go:33-35
var DelegationPrefix = []byte{0xef, 0x01, 0x00} func AddressToDelegation(addr common.Address) []byte { return append(DelegationPrefix, addr.Bytes()...) // 結果: [0xef, 0x01, 0x00] + [20バイトアドレス] = 23バイト }
`0xef`は通常のEVMでは無効なオペコードである。これを意図的に使用することで、このコードが直接実行されることを防ぎ、必ず委譲解決処理を経由するようにしている。
設定後の状態:
Aliceのコード = 0xef0100 + SmartWalletのアドレス
この時点で、AliceのEOAは見た目上「コードを持つアカウント」になるが、そのコードは実際の実行可能コードではなく、SmartWalletへの参照情報である。
Step 2: 委譲されたEOAの呼び出し
トランザクション送信
任意のユーザー(Bob)がAliceのアドレスにトランザクションを送信する:
from: Bob to: Alice value: 1 ETH data: 0x... (SmartWalletの関数呼び出しデータ)
委譲コードの解決
TxがAliceに送信されると、EVMは通常通り`Call`関数を実行する。ここで重要なのは、Aliceが委譲設定済みかどうかを自動的に判定することである。
// トランザクション実行(通常のトランザクション処理と同じ) ret, st.gasRemaining, vmerr = st.evm.Call(msg.From, st.to(), msg.Data, st.gasRemaining, value)
実は、以前からEVMの中ではEOAとContractは区別されておらず、Txを実行するときには必ずToのアドレスからcodeを読み出す処理が行われる。EIP-7702以前ではこの時にcodeがnilの場合はEOAとして扱われていた。つまりこの辺りのコードは以前から変更はない。EIP-7702で変更されたのは以降で説明する部分となる。
`evm.Call`の内部では、呼び出し先(Alice)のコードを取得する際に、委譲の解決が行われる:
func (evm *EVM) Call(caller, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, err error) { // Aliceのコードを解決(委譲チェックを含む) code := evm.resolveCode(addr) // addr = Alice // codeにはSmartWalletの実際のコードが入る // ... }
resolveCode関数の動作
`resolveCode`関数は、EIP-7702の核心となる委譲解決ロジックである。この関数が、通常のコードと委譲コードを透過的に処理する。
func (evm *EVM) resolveCode(addr common.Address) []byte { // 1. Aliceのコードを取得 code := evm.StateDB.GetCode(addr) // → "0xef0100 + SmartWallet" (23バイト) if !evm.chainRules.IsPrague { return code // Prague以前は委譲機能なし } // 2. 委譲プレフィックスをチェック if target, ok := types.ParseDelegation(code); ok { // 3. SmartWalletの実際のコードを取得して返す return evm.StateDB.GetCode(target) // target = SmartWallet } return code // 委譲でない場合はそのまま返す }
この処理により、呼び出し側は委譲の有無を意識せずに、通常通りトランザクションを送信できる。
EOAのcode部に移譲prefixを持つ23byteコードが設定されている場合は、EVMが委譲を検出し後述するParseDelegationで委譲コードを持つアドレスを抽出して、適切なコードを取得・実行する。
ParseDelegationの処理
`ParseDelegation`は、コードが委譲指示かどうかを判定し、委譲先アドレスを抽出する関数である。
core/types/tx_setcode.go:36-41
func ParseDelegation(b []byte) (common.Address, bool) { // 23バイトかつ0xef0100で始まるかチェック if len(b) != 23 || !bytes.HasPrefix(b, DelegationPrefix) { return common.Address{}, false } // 後ろ20バイトをアドレスとして抽出 return common.BytesToAddress(b[len(DelegationPrefix):]), true }
この関数により、以下の判定が行われる:
- **長さが23バイトちょうど**:それ以外は通常のコード
- **0xef0100で始まる**:委譲プレフィックスの確認
- **後ろ20バイト**:委譲先のアドレス(SmartWallet)
これらの条件を満たさない場合は、通常のコントラクトコードとして扱われる。
Step 3: SmartWalletコードの実行
コンテキストの設定
委譲の最も重要な特徴は、SmartWalletのコードが**Aliceのコンテキスト**で実行されることである。
// SmartWalletのコードをAliceのコンテキストで実行 contract := NewContract(caller, addr, value, gas, evm.jumpDests) contract.SetCallCode(evm.resolveCodeHash(addr), code) // addr = Alice (実行コンテキスト) // code = SmartWalletのバイトコード
`SetCallCode`メソッドにより、実行するコードとコンテキストが分離される。これは`CALLCODE`オペコードと似た動作であるが、EOAレベルで自動的に行われる点が異なる。
実行の特徴
委譲実行時のコンテキストは以下のようになる:
Solidityから見た環境変数**:
- **`msg.sender`**: Bobのアドレス(トランザクション送信者)
- **`address(this)`**: Aliceのアドレス(実行コンテキスト)
- **ストレージ**: Aliceのストレージ領域を読み書き
- **残高**: Aliceの残高を参照・変更
つまり、SmartWalletのコードは「自分がAliceである」という前提で動作する。つまりEOAが実行結果としてstateを保持することも可能となる。これがほかのAbstract Account提案とは大きく異なり、大きな可能性を秘めているEIP-7702独自の部分である。
実行結果
SmartWalletのコード実行は、通常のコントラクト実行と同じ方法で処理される。
// SmartWalletのコードを実行 ret, err = evm.Run(contract, input, false) if err != nil { // エラー時は全ての変更を巻き戻し evm.StateDB.RevertToSnapshot(snapshot) }
実行が成功した場合:
- Aliceのストレージが更新される
- ETH残高の変更が確定する
- イベントログが記録される
実行が失敗(revertなど)した場合:
- スナップショットから全ての変更が巻き戻される
- ETH送金も取り消される
- ガス代のみが消費される
実行フローの整理
[Phase 1: 委譲設定] 1. AliceがSetCodeTxを作成 - SmartWalletのアドレスを指定 - 自身の秘密鍵で署名 2. SetCodeTx実行 - Aliceのcode = "0xef0100" + SmartWallet [Phase 2: 委譲実行] 3. BobがAliceにトランザクション送信 - To: Alice - Data: SmartWalletの関数データ 4. EVM処理 - resolveCode(Alice) - → GetCode(Alice) = "0xef0100..." - → ParseDelegation() = SmartWallet - → GetCode(SmartWallet) = 実際のコード 5. 実行 - SmartWalletのコードを - Aliceのコンテキストで実行 - Aliceのストレージ・残高を使用
コード移譲の1レベル制限
EIP-7702仕様書: Delegation Indicator - Loops
In case a delegation indicator points to another delegation, creating a potential chain or loop of delegations, clients must retrieve only the first code and then stop following the delegation chain.
にある通り、EOAの移譲コード呼出しは1レベルのみしか処理されないようになっている。
この制限は、セキュリティ上重要な役割を果たす:
なぜ1レベル制限が必要か**:
- **無限ループ防止**: A→B→C→Aのような循環参照を防ぐ
- **ガス消費の予測可能性**: 委譲解決のコストを一定に保つ
- **複雑性の制限**: デバッグやセキュリティ監査を容易にする
resolveCodeが行われる処理ではgas代が消費されないため、EOAの移譲コードが再帰的にほかのEOAの移譲コードを呼びだす場合、ストッパーがかからずに容易に無限ループが発生してしまう。これを防ぐための制限である。
1レベル制限が発生する仕組み:
Alice → Carol(別のEOA、委譲設定済み) → SmartWallet この場合: - resolveCode(Alice) → Carolのコード(0xef0100 + SmartWallet)を取得 - このコードは委譲プレフィックスを含むため、実行時に0xefオペコードエラーで失敗 - resolveCodeはcallが呼ばれたときに必ず1度しか処理されないため、上記のようなチェーンを再帰的には解決しない
つまり、委譲先は必ずContractでなければならない。
まとめ
EIP-7702による委譲は以下の特徴を持つ:
技術的特徴
- **永続的な委譲**:
- 一度設定された委譲は、明示的にクリアするまで有効
- チェーンの状態として保存される
- **透明な実行**:
- 通常のトランザクションで委譲コードが自動実行
- 送信者は委譲の有無を意識する必要がない
- **コンテキスト保持**:
- EOAのアドレス、ストレージ、残高を維持したまま実行
- SmartWalletはAliceの資産を直接操作できる
- AliceのEOAはstateを保持することができる
- **安全性**:
- 1レベル制限により無限委譲ループを防止
- 署名による明示的な認可が必要
実用的な利点
この仕組みにより、EOAは以下のような高度な機能を利用できるようになる:
- **アカウント抽象化**: EOAでもスマートコントラクトウォレット機能を利用
- **バッチ処理**: 複数の操作を1トランザクションで実行
- **ガス代理支払い**: 第三者がガス代を負担する仕組み
- **アクセス制御**: マルチシグや日次送金制限などのセキュリティ機能
この辺りはほかのブログ記事で詳細にまとめられているので本記事での説明は割愛する。
以上がEIP-7702の基本的な概要である。次回の記事ではより詳細な動作や、特殊なケースなどでどのようにEIP-7702が動作するのかを調査し解説していく予定絵ある。