【ERC4337 その2】NFTを転送してみる

Web3.0, ブロックチェーン

お疲れさまです。
次世代ビジネス推進部 Web3開発Gの荻原です。

前回【ERC4337 その1】ERC4337についてでは、EOAの秘密鍵のリスクからの脱却を目指したERC4337について紹介しました。

ERC4337を試してみることはStackupチュートリアルを進めると簡単にできるのですが、今回はこれを応用して自分で発行したNFTをERC4337を使って転送してみようと思います。

目次

  1. 実装する
  2. 実行する
  3. transactionを見てみる
  4. まとめ
  5. 参考

1. 実装する

今回はとりあえず検証のためにNFTが転送できることをゴールとしたため、実装が不十分であったり実用には使えない部分もあったりします。ご了承ください。

StackupのFree Planに登録していることを前提として進めます。PaymasterについてはStackupのFree Planの範囲外のため使用しません。

また、ネットワークはMumbaiネットワークを使用します。

環境は以下の通りです。

erc-4337-examplesは以下の時点のものを利用しています。
erc-4337-examples GitHub

また、Truffleの詳しい使い方はこの記事では割愛しています。

スマートコントラクト

今回はContractFactoryは実装せず、デプロイはTruffleで行います。

コントラクト(NFTTest.solとしました)の実装はETH Infinitismから提供されているaccount-abstractionライブラリのSimpleAccount.solを参考にして実装していきます。

BaseAccount.solの継承

まず、account-abstractionのBaseAccountを継承します。

@account-abstraction/contractsを利用するためにnpmでインストールもしておきます。

最低限必要な実装についてはこの後解説しますが、BaseAccountの実装を見るとNFTTestにどのような実装が必要か何となくわかるかと思います。

EntryPointのGetter

SimpleAccountのコンストラクタを見るとEntryPointのコントラクトアドレスを保存しています。

この _entryPointは以下で使われています。

BaseAccountも見てみるとコメントで説明があり、SimpleAccountはその通りに実装されていることがわかります。

そして entryPoint()は色々な場所で使われることになるので実装しておきます。

関数のアクセスコントロール

BaseAccountを見てみると関数へのアクセスコントロールとしては以下が実装されています。

ERC4337を使う場合はEntryPointのコントラクトが msg.senderとなって実行されるため、onlyOwner等を使っているところでEntryPointを許可する必要があります。
ただし、今回は部分的にTruffleからメソッドを実行するため、SimpleAccountに倣ってownerとEntryPoint両方を許可します。

SimpleAccountに倣ってfunctionで実装し、例えば以下のように各関数のいちばん最初で実行するようにします。

mintの実装

NFTとしての実装は最低限mintを外から叩けるように実装しておきます。
以下はmintに関わる部分だけ抽出しています。

最後の _approve()は、今回はmintした後EntryPointに safeTransferFrom()を実行してもらうため、このtokenの転送を許可しています。

署名の検証

SimpleAccountを見ると _validateSignature()という関数があります。

これはBaseAccountの validateUserOp()で呼び出されており、さらに validateUserOp()はEntryPointからトランザクション実行時に呼び出されます。
そのため、これも実装しておきます。
上記を見るとわかるかと思いますが、署名の方法はここの実装次第で自由に決めることができます。
今回はSimpleAccountをそのままコピペしてしまいます。

また、 validateUserOp()内では _validateNonce()も呼び出されています。ただし、BaseAccountの _validateNonce()定義部分の以下のコメントの通り、nonceがユニークであるかどうかという検証はEntryPoint側(参照)に実装されており、独自にnonceについて制限したければここで実装することになります。今回は特に実装しません。

The actual nonce uniqueness is managed by the EntryPoint, and thus no other action is needed by the account itself.

ここまでで、今回ERC4337を利用するために最低限必要なスマートコントラクトの実装は完了しました。

MATICを受け取るための実装

今回はPaymasterは利用せずContractFactoryも実装していないため、コントラクト自身がガス代を支払う必要があります。そのためコントラクトをデプロイ後、ガス代用のMATICを受け取るための実装もしておきます。

スマートコントラクトの実装全体

ここまででスマートコントラクトの実装は完了です。

コード全体は以下の通りです。

compileしてABIを作成しておきます。

UserOperationをBundlerに送信する

Stackupから提供されているerc-4337-examplesを元にし、NFTTestの safeTranferFrom()を実行できるように実装してみます。

最初にこのリポジトリをcloneしておきます。

ABIの準備

先程作成したNFTTestのABIをerc-4337-examples/src/NFTTestabi.tsに配置しておきます。
以下の形式です。

erc-4337-examples/src/index.tsに以下を追記します。

UserOperationの作成

erc-4337-examples/scripts/simpleAccount/transfer.tsを参考に実装していきます。

erc-4337-examples/scripts/simpleAccount/にsafeTransferFrom.tsを作成します。

callDataはethers.jsの encodeFunctionData()で取得可能です。
providerはBundlerのJSON-RPC URI(config.jsonの rpcUrl)を指定して取得します。
実行したいContractAddress( sender)、実行したいメソッドの引数( fromtotokenId)はコマンドラインから取得する想定です。

nonceについては、EntryPointの getNonce()を叩くメソッドがBaseAccountに実装されているため、これを用います。

今回NFTTestに実装した _validateSignature()に対応するsignatureは以下のように取得します。 userOpHashはUserOperationやEntryPointのアドレス、Chain IDを合わせてハッシュ化した値です。(具体的にはHow to sign and validate the signature of UserOperation in EIP-4337 (Account Abstraction)参照)

そして以下のようにしてuserop.jsの UserOperationBuilder()を作成します。

Presets.Middleware.getGasPrice()はmaxFeePerGasとmaxPriorityFeePerGasの最新の値を取得します。

Presets.Middleware.estimateUserOperationGas()はBundlerのJSON-RPC APIである eth_estimateUserOperationGasを実行し、preVerificationGas、verificationGasLimit、callGasLimitの適切な値を計算します。

Presets.Middleware.EOASignature()は先程紹介したsignatureを求める部分をやってくれます。 useDefaults()では estimateUserOperationGas()実行時に必要になる仮の署名をsignatureに指定していますが、 Presets.Middleware.EOASignature()実行時に置き換えられます。

ここまででUserOperationの作成は完了です。次にこれをBundlerに送信します。
こちらもuserop.jsのClientを使用します。

従来のtransactionであればJSON-RPC APIの eth_sendTransactionを呼び出します。ERC4337ではBundlerのJSON-RPC APIの eth_sendUserOperationを呼び出します。
第1引数には先程作成したUserOperationを設定したbuilderを指定します。
第2引数はoptionです。dryRunを指定すると eth_sendUserOperationを呼び出さず、buildのみをします。onBuildはbuild時に実行する内容を指定しています。

コード全体は以下の通りです。

後は必要な引数をコマンドから取得するようにします。
以下のようにerc-4337-examples/scripts/simpleAccount/index.tsに追記します。(Paymasterは使わないですが、型定義の関係で入れています)

2. 実行する

まずはTruffleを使ってMumbaiネットワークにNFTTestをデプロイします。

0xB4080F494aB7aE2d44C205958cCcd2BAe3E25A95でデプロイできました。

このコントラクトにMATICを送金します。

mintをします。mintももちろん先程実装したUserOperationのcallDataをmintに変更するだけでERC4337を利用できますが、今回はTruffleを使ってしまいます。

この後、ERC4337を使ってNFTを転送していくのですが、そのトランザクションはEntryPointのnonceが0になります。
userop.jsのPresetは初回実行時はinitCodeを用いてコントラクトをデプロイする前提となってしまっているため、今回の場合はその制限のある箇所をコメントアウトする必要があります。

最後にerc-4337-examples/config.jsonで環境変数を設定します。
rpcUrlにはStackupのAPI Keyを登録します。これでStackupのBundlerを利用できます。
signingKeyは署名に使うEOAのprivate keyです。今回はスマートコントラクトの _validateSignature()でownerのみ署名可能としたのでownerのアドレスを指定しました。
entryPointはデフォルトのままで、eth-infinitismのEntryPointを利用できます。
その他は今回は使いません。

ここまでで準備は完了です。
コマンドを実行します。

以上でERC4337を使ってNFTを転送してみることができました。

3. transactionを見てみる

実行されたtransactionを見てみましょう。

ポイントとなるのは次の部分かと思います。

ガス代の支払いの流れです。
スクリーンショット 2023-06-20 19.25.32.png
まず、コントラクト→EntryPointに支払われています。
次にEntryPoint→Bundlerに余った分が支払われています。

このトランザクションで実行されている内容です。
スクリーンショット 2023-06-20 19.40.34.png
EntryPointの handleOps()というメソッドで、引数はUserOperationとBundlerのアドレスです。

イベントのログを見てみると、ガス代の支払い周り以外ではDeposited、BeforeExecution、Transfer、UserOperationEventがemitされていました。

  • DepositedはEntryPointでコントラクト→EntryPointに支払われたガス代のログです。
  • BeforeExecutionは handleOps()の中でUserOperationの内容(今回はsafeTransferFrom)の実行前にemitされます。
  • TransferはsafeTransferFromが実行された際にemitされます。
  • UserOperationEventは全体の実行完了後にemitされています。

これらを元にするとEntryPointの処理の流れをさらに詳しく理解することもできると思います。

4. まとめ

今回はERC4337を利用してNFTを転送してみました。
userop.jpやaccount-abstractionを利用することで実装の難易度としては簡単だったのではないかと思います。
ERC4337について勉強する際の手助けになれば幸いです。
最後まで読んでいただき、ありがとうございました。

5. 参考