EIP4337
https://eips.ethereum.org/EIPS/eip-4337
概要
Account Abstraction の 1 種
2021.09.29 に Vitalik らが提案
EIP2938 から約 1 年後
プロトコルレイヤーを改修することなく実現可能な AA
An account abstraction proposal which completely avoids the need for consensus-layer protocol changes.
ゆえに、EIP のカテゴリは ERC
m0t0k1ch1.icon EIP86 と EIP2938 はプロトコルレイヤーの改修が必要(EIP のカテゴリが Core)な提案で苦しかったが
ユーザーから送信された UserOperation と呼ばれる疑似トランザクションオブジェクトをバンドラーというステークホルダーがまとめて 1 つのトランザクションとし、エントリーポイントコントラクトの handleOps をコールする
Instead of adding new protocol features and changing the bottom-layer transaction type, this proposal instead introduces a higher-layer pseudo-transaction object called a UserOperation. Users send UserOperation objects into a separate mempool. A special class of actor called bundlers (either miners, or users that can send transactions to miners through a bundle marketplace) package up a set of these objects into a transaction making a handleOps call to a special contract, and that transaction then gets included in a block.
m0t0k1ch1.icon 複数の UserOperation を内包したメタトランザクションをイメージするとよい
m0t0k1ch1.icon バンドラーは、メタトランザクションで言うところのリレイヤー
モチベーション
基本的には EIP2938 と同様
以下を達成することを目標としている
AA の主要な目標の達成
ユーザーが自身のプライマリアカウントとして、EOA の代わりに、任意の検証ロジックを有したコントラクトウォレットを利用できるようにする
m0t0k1ch1.icon account "security" abstraction
分散化
どんなバンドラーでもプロセスに参加できるようにする
マイナーがそうであるように
パブリックな mempool 上で発生する全てのアクティビティに対応する
ユーザーは特定のアクターと直接通信するためのアドレス(ex. IP)を知る必要はない
バンドラーに対するトラストを仮定しない
プロトコルレイヤーの改修を必要としない
プロトコルレイヤーの開発は the Merge とその後のスケーラビリティに焦点が当てられているため、長期間、それ以上にプロトコルレイヤーを改修する機会はないと考えられる
迅速な普及の可能性を高めるため、本提案はプロトコルレイヤーの改修を要求しない
その他のユースケースのサポートも試みる
プライバシー保護アプリケーション
アトミックな複数オペレーション実行
EIP3074 と同様
ERC20 トークンによるトランザクション手数料支払い、デベロッパーによるトランザクション手数料代払い、より一般的には、EIP3074 のようなスポンサードトランザクション
詳細
プロトコルレイヤーの改修を避けるため、新しいトランザクション形式は導入しない
UserOperation の導入
ABI エンコードされた構造体
以下のフィールドを有する
sender
type:address
処理を実行するウォレット
nonce
type:uint256
リプレイアタック防止のためのパラメータ
ウォレット生成の際に salt としても利用される
initCode
type:bytes
ウォレットの初期化コード
ウォレット生成の際にのみ必要
callData
type:bytes
sender に渡すデータ
メイン処理を担う
callGas
type:uint256
メイン処理に割り当てられる gas の量
verificationGas
type:uint256
検証処理に割り当てられる gas の量
preVerificationGas
type:uint256
バンドラーに補償する gas の量
検証の前に行う処理や calldata の分
maxFeePerGas
type:uint256
EIP1559 の max_fee_per_gas と同様
maxPriorityFeePerGas
type:uint256
EIP1559 の max_priority_fee_per_gas と同様
paymaster
type:address
トランザクション手数料を代払いするアドレス
代払いの必要がない場合は 0
paymasterData
type:bytes
paymaster に対して送信するデータ
signature
type:bytes
検証処理の中で nonce とともに sender に渡されるデータ
ユーザーは、ウォレットに実行させたい処理を UserOperation としてパッケージングし、UserOperation mempool に送信する
バンドラーは UserOperation mempool で UserOperation を待ち受け、バンドルトランザクションを作成する
バンドルトランザクションは、複数の UserOperation をパッケージングし、エントリーポイントコントラクトの handleOps をコールする
エントリーポイントコントラクトの導入
以下のようなインターフェースを有する
code:IEntryPoint.sol
function handleOps
(UserOperation[] calldata ops, address payable redeemer)
external;
op.paymaster が 0 の場合、handleOps は(UserOperation ごとに)以下の処理を行う
verification loop
ウォレットが存在しない場合は作成する
op.initCode を利用する
ウォレットが存在せず、op.initCode も空の場合、コールは失敗する
ウォレットの validateUserOp を実行する
code:IWallet.sol
function validateUserOp
(UserOperation calldata userOp, uint requiredPrefund)
external;
ウォレットは op.signature と op.nonce を検証し、処理が正当だとみなされる場合は手数料を支払い、自身の nonce をインクリメントする
validateUserOp のコールが失敗した場合、handleOps は少なくともこの UserOperation の execution loop をスキップする
handleOps 全体を revert してもよい
バンドラーがローカル環境でシミュレーションできるようにするため、validateUserOp にはいくつかの制約がある(ほぼ純粋な関数である必要がある)
GASPRICE GASLIMIT DIFFICULTY TIMESTAMP BASEFEE BLOCKHASH NUMBER SELFBALANCE BALANCE ORIGIN GAS といった、実際に実行した際と結果が異なる可能性がある opcode は使えない
基本的に外部コントラクトにアクセスすることはできないが、外部コントラクトのコードが不変であること(SELFDESTRUCT または DELEGATECALL が含まれていないこと)が保証されている場合のみ、外部コントラクトのコードを参照することはできる
m0t0k1ch1.icon コード ≠ ストレージであることに注意
手数料を支払う必要があるので、外部アカウントの残高を更新することは可能
execution loop
op.callData を用いてウォレットをコールする
op.callData をどう解釈するかはウォレット次第
例えば、execute などの関数を介して外部コントラクトをコールするなど
消費されなかった gas 分をウォレットに返金する
https://scrapbox.io/files/61b85d08c2a487001d7526b8.png
op.paymaster が設定されているケースをサポートするには、上記に加えて以下を行う
事前準備
他のユーザーのウォレットを op.paymaster に指定した悪意のある UserOperation による攻撃を防ぐため、paymaster は ETH をエントリーポイントコントラクトにステークしてロックし、paymaster になることに同意する必要がある
code:IEntryPoint.sol
function addStake() external payable
function lockStake() external
function unlockStake(address paymaster) external
function withdrawStake(address payable withdrawAddress) external
m0t0k1ch1.icon このような準備と、以下に対応したコントラクトを用意しておけば、paymaster は受動的になれる(常にオンラインである必要がない)
verification loop
op.paymaster が十分な ETH をエントリーポイントコントラクトにステークしているかどうか確認する
op.paymaster の validatePaymasterUserOp をコールし、この処理に関して代払いが可能かどうか確認する
code:IPaymaster.sol
function validatePaymasterUserOp
(UserOperation calldata userOp, uint maxcost)
external view returns (bytes memory context);
execution loop
メイン処理を終えた後、op.paymaster の postOp をコールする
代払いした手数料を ERC20 トークンで徴収する場合、ここでそれが行われる
code:IPaymaster.sol
function postOp
(PostOpMode mode, bytes calldata context, uint actualGasCost)
external;
enum PostOpMode {
opSucceeded,     // user op succeeded
opReverted,       // user op reverted. still has to pay for gas.
postOpReverted // user op succeeded, but caused postOp to revert
}
https://scrapbox.io/files/61bc8cfb5ab1bb00210f1e24.png
セキュリティ要件
エントリーポイントコントラクトは、全ての EIP4337 ウォレットのトラストポイントとして機能するため、非常に厳しい監査や形式検証を行う必要がある
特に以下の 2 つをカバーする必要がある
任意のハイジャックに対する安全性
エントリーポイントコントラクトは、コールしようとしているウォレットの validateUserOp が成功した場合のみ、そのウォレットをコールする
そのコールは op.callData を用いて行われなければならない
m0t0k1ch1.icon 他人のウォレットを勝手にコールできてしまってはいけないということだと思う
手数料の浪費に対する安全性
validateUserOp が成功した場合、エントリーポイントコントラクトはウォレットに対するコールを行わなければならない
そのコールは op.callData を用いて行われなければならない
m0t0k1ch1.icon validateUserOp だけ実行して終わりにしてしまうと、検証に要した gas が浪費されてしまうのでだめだよということだと思う
サンプルコード
https://github.com/eth-infinitism/account-abstraction
参照
2021.09.29:【記事】ERC4337: account abstraction without Ethereum protocol changes