ERC4337からAccountAbstractionを理解する
こんにちは。次世代システム研究室のT.M です。
注意
本稿は2023/3/31 時点の情報です。情報が更新されていることがありますので、ご注意ください。特にERC4337 は現在もまだドラフトであり、仕様が変更になる可能性が大いにあります。はじめに
Ethereum では、セキュリティやユーザビリティの向上のため、Account Abstraction という概念が以前から提唱されていました。しかし、これを実現するためには、Ethereum のプロトコルの変更が必要なため、今日まで実現に至っていません。そこで、プロトコルの変更をせずに実現をするために、ERC4337 が提案され、先日メインネットにデプロイされました。本稿では、その実装を解説していきます。Account Abstraction
ユーザーがトランザクションを作成するためには、EOA と呼ばれるアカウントが必要です。Account Abstraction では、そのEOA を抽象化することで、マルチシグや紛失時の復旧、複数端末での共有、ガスの代払いなど、セキュリティやユーザビリティを向上させることができます。ERC4337
Account Abstraction を実現するためには、Ethereum のプロトコルを変更する必要があります。Ethereum のプロトコルの変更は容易に行えるものではなく、今日に至るまで実現していません。そのため、プロトコルの変更なしに実現する方法として、ERC4337 が提唱されました。ERC4337 は現在もドラフト状態であり、まだ仕様が確定していません。先日、OpenZeppelin が監査したERC4337 のコントラクトがメインネットにデプロイされた、ということが話題になりましたが、このコントラクトはあくまでその時点でのERC4337 の仕様で実装されたものです。また、Stackup がそれ以前にデプロイをしており、利用されています。
全体の流れは以下のようになっており、EntryPoint がERC4337 の機能の大部分を占めています。
- Bundler はユーザーのリクエストをまとめて、EntryPoint に送る
- EntryPoint はリクエストが正しいかをWallet contract(アカウント)でチェックする
- 他のリクエストも同様にチェックする
- EntryPoint はアカウントに対してリクエストを実行する
- 他のリクエストも同様に実行する
参考: https://eips.ethereum.org/EIPS/eip-4337
トランザクションを作成するのは、Bundler であるので、EOA が必要なのはBundler のみであり、ユーザーはEOA が不要です。また、アカウントはWallet contract としてデプロイされており、Wallet contract にロジックを用意することで、マルチシグや紛失時の復旧などを実現することができます。
UserOperation.sol
ユーザーはアカウントを操作するために、UserOperation をRPC eth_sendUserOperation でBundler に投げます。そのUserOperation を作成するためのコードです。命令内容やnonce、ガスの設定などをencode しています。function pack(UserOperation calldata userOp) internal pure returns (bytes memory ret) { address sender = getSender(userOp); uint256 nonce = userOp.nonce; bytes32 hashInitCode = calldataKeccak(userOp.initCode); bytes32 hashCallData = calldataKeccak(userOp.callData); uint256 callGasLimit = userOp.callGasLimit; uint256 verificationGasLimit = userOp.verificationGasLimit; uint256 preVerificationGas = userOp.preVerificationGas; uint256 maxFeePerGas = userOp.maxFeePerGas; uint256 maxPriorityFeePerGas = userOp.maxPriorityFeePerGas; bytes32 hashPaymasterAndData = calldataKeccak(userOp.paymasterAndData); return abi.encode( sender, nonce, hashInitCode, hashCallData, callGasLimit, verificationGasLimit, preVerificationGas, maxFeePerGas, maxPriorityFeePerGas, hashPaymasterAndData ); }
EntryPoint.sol
Bundler が受け取ったUserOperation をまとめてEntryPoint のhandleOp() に投げて、トランザクションを発行します。_validatePrepayment() や_validateAccountAndPaymasterValidationData() でUserOperation のチェックを行い、_executeUserOp() でUserOperation を実行します。function handleOps(UserOperation[] calldata ops, address payable beneficiary) public nonReentrant { uint256 opslen = ops.length; UserOpInfo[] memory opInfos = new UserOpInfo[](opslen); unchecked { for (uint256 i = 0; i < opslen; i++) { UserOpInfo memory opInfo = opInfos[i]; (uint256 validationData, uint256 pmValidationData) = _validatePrepayment(i, ops[i], opInfo); _validateAccountAndPaymasterValidationData(i, validationData, pmValidationData, address(0)); } uint256 collected = 0; emit BeforeExecution(); for (uint256 i = 0; i < opslen; i++) { collected += _executeUserOp(i, ops[i], opInfos[i]); } _compensate(beneficiary, collected); } //unchecked }validation の一つとして、以下が呼ばれています。
IAccount(sender).validateUserOp{gas : mUserOp.verificationGasLimit}(op, opInfo.userOpHash, missingAccountFunds)アカウントはIAccount を実装する際に、UserOperation のvalidation を実装します。BaseAccount では以下のような実装になっており、EntryPoint のチェックやサインのチェック、nonce のチェックなどをしています。
function validateUserOp(UserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) external override virtual returns (uint256 validationData) { _requireFromEntryPoint(); validationData = _validateSignature(userOp, userOpHash); _validateNonce(userOp.nonce); _payPrefund(missingAccountFunds); }
まとめ
Account Abstraction を実現するためのERC4337 の一つの実装について解説しました。ERC4337 はまだドラフトであるため、仕様が確定していませんが、メインネットにコントラクトがデプロイされるなど、進んでいることを実感します。Account Abstraction はユーザビリティを大きく向上させるので、とても注目しています。これは幅広く利用される技術なので、今後も注目していきたいと思います。次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。インフラ設計、構築経験者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。
皆さんのご応募をお待ちしています。