LoginSignup
0
0

【ERC4337】Thirdwebを使用したアカウント抽象化(Account Abstraction)を理解する

Last updated at Posted at 2024-02-20

ERC4337で定義されるアカウント抽象化(Account Abstraction) は、ウォレット(の秘密鍵)の管理やGas代の支払いからエンドユーザーを開放できる点で、人々がweb3世界へアクセスする際のUXを向上する技術として期待されています。
スマートコントラクトの作成、ウォレットの作成等、web3に関する開発を支援するツールを提供するThirdwebが、Account Abstractionの実装も提供しているようだったので、本記事ではその詳細を見ていきます。

背景

以前より、Account Abstractionの開発ツールを提供していることは知っていました。
ツールの使用方法の理解と、実装ベースでAccount Abstractionの仕組みを整理することを目的に本記事を記載しています。

Account Abstractionとは

MetaMaskのようなウォレットアプリで管理するEOAではなく、ウォレットの機能を備えたコントラクト(Contract Wallet)を利用する考え方です。
これにより、EOAを利用することによる様々な制約(自身でGas代を支払う、秘密鍵を管理するなど)を回避でき、一般ユーザーがウォレットを扱う際のUXを向上させることができます。

筆者がAccount Abstractionを理解するうえで非常に参考になった記事をいくつか記載します。

本編

以下のリンクに、Thirdwebを利用してContract Wallet(「SmartWallet」とも呼ばれる)を発行する方法について公式の解説記事があります。

また、nodeで動くバックエンドのデモアプリもGithubに用意されています。

これらの記事とソースを参考にしていきます。

ソースコード

上述したデモアプリのソースについて、主要な部分は以下の通りです。

index.ts
import { config } from "dotenv";
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import { Goerli } from "@thirdweb-dev/chains";
import { LocalWalletNode } from "@thirdweb-dev/wallets/evm/wallets/local-wallet-node";
import { SmartWallet, SmartWalletConfig } from "@thirdweb-dev/wallets";

config();

const chain = Goerli;
const factoryAddress = "0x5425683F8D635Ad0c80A4a166f8597C7DFA9b30F"; // AccountFactory
const tokenContract = "0xc54414e0E2DBE7E9565B75EFdC495c7eD12D3823"; // TokenDrop
const secretKey = process.env.THIRDWEB_SECRET_KEY as string;

const main = async () => {
  if (!secretKey) {
    throw new Error(
      "No API Key found, get one from https://thirdweb.com/dashboard"
    );
  }
  console.log("Running on", chain.slug, "with factory", factoryAddress);

  // ---- Connecting to a Smart Wallet ----

  // Load or create personal wallet
  // here we generate LocalWallet that will be stored in wallet.json
  const adminWallet = new LocalWalletNode();
  await adminWallet.loadOrCreate({
    strategy: "encryptedJson",
    password: "password",
  });
  const adminWalletAddress = await adminWallet.getAddress();
  console.log("Admin wallet address:", adminWalletAddress);

  // Configure the smart wallet
  const config: SmartWalletConfig = {
    chain,
    factoryAddress,
    secretKey,
    gasless: true,
  };

  // Connect the smart wallet
  const smartWallet = new SmartWallet(config);
  await smartWallet.connect({
    personalWallet: adminWallet,
  });

  // ---- Using the Smart Wallet ----

  // now use the SDK normally to perform transactions with the smart wallet
  let sdk = await ThirdwebSDK.fromWallet(smartWallet, chain, {
    secretKey: secretKey,
  });

  console.log("Smart Account address:", await sdk.wallet.getAddress());
  console.log("Balance:", (await sdk.wallet.balance()).displayValue);

  console.log("Claiming using Admin key");
  // Claim a ERC20 token
  await claimERC20Tokens(sdk);


const claimERC20Tokens = async (sdk: ThirdwebSDK) => {
  const contract = await sdk.getContract(tokenContract);
  const tx = await contract.erc20.claim(1);
  console.log("Claimed 1 ERC20 token, tx hash:", tx.receipt.transactionHash);
  const tokenBalance = await contract.erc20.balance();
  console.log("ERC20 token balance:", tokenBalance.displayValue);
};

main();

上から順にポイントを見ていきます。

ローカルウォレットとContract Walletの作成

index.ts
  // Load or create personal wallet
  // here we generate LocalWallet that will be stored in wallet.json
  const adminWallet = new LocalWalletNode();
  await adminWallet.loadOrCreate({
    strategy: "encryptedJson",
    password: "password",
  });
  const adminWalletAddress = await adminWallet.getAddress();
  console.log("Admin wallet address:", adminWalletAddress);

    // Configure the smart wallet
  const config: SmartWalletConfig = {
    chain,
    factoryAddress,
    secretKey,
    gasless: true,
  };

  // Connect the smart wallet
  const smartWallet = new SmartWallet(config);
  await smartWallet.connect({
    personalWallet: adminWallet,
  });

ここでは、LocalWalletNode()で、ローカルウォレットインスタンス"adminWallet"を作成しています。
その後、smartWalletインスタンスを作成しsmartWallet.connect()で"adminWallet"を"smartWallet"を結び付けています。

adminWalletの役割はContract Walletを作成するための「鍵」として使用することだと解説記事には記載があります。

原文 For each smart wallet, there needs to be a personal wallet that acts as a "key" for the smart wallet. This personal wallet is able to perform transactions on the smart wallet, such as adding additional signers to the account. To create a seamless experience, you can use local wallet (a wallet created and stored on a device) or a Paper wallet which allows your users to log in with email (or Google) and create non-custodial wallets that you can use as a key for your user's smart wallets.

整理すると以下のようになります。

  • 変数"smartWallet"はContract Walletである
  • 変数"adminWallet"はローカルに保存されたEOAであり、Contract Walletである"smartWallet"のトランザクションへの署名に使用する

また、この時点においてContract Walletはソース上にインスタンスとして存在するだけで、ブロックチェーン上にはデプロイされていません
Contract Walletが何かしらトランザクションを実行しようとしたときに、はじめてデプロイされます。こうすることで、デプロイにかかる不要なGas代を節約できるようです(コントラクトのデプロイにはそれなりのGas代がかかるので、「デプロイしたが結局一度も使わないContract Wallet」を減らしたい)

原文 Each wallet can unlock one smart account per account factory. The user's smart wallet contract is deployed once a transaction is initiated from the smart wallet. The smart wallet address is deterministic, meaning that the address of the smart wallet is known prior to deployment.

Due to this feature, we are able to provide users with their smart wallet address without having to deploy their smart wallet contract until they perform their first transaction. This is beneficial because it removes potentially unnecessary costs of deploying a smart contract on-chain until you're sure the user needs to perform an on-chain action.

ERCトークンの請求

index.ts
  // now use the SDK normally to perform transactions with the smart wallet
  let sdk = await ThirdwebSDK.fromWallet(smartWallet, chain, {
    secretKey: secretKey,
  });

  console.log("Smart Account address:", await sdk.wallet.getAddress());
  console.log("Balance:", (await sdk.wallet.balance()).displayValue);

  console.log("Claiming using Admin key");
  // Claim a ERC20 token
  await claimERC20Tokens(sdk);
index.ts
const claimERC20Tokens = async (sdk: ThirdwebSDK) => {
  const contract = await sdk.getContract(tokenContract);
  const tx = await contract.erc20.claim(1);
  console.log("Claimed 1 ERC20 token, tx hash:", tx.receipt.transactionHash);
  const tokenBalance = await contract.erc20.balance();
  console.log("ERC20 token balance:", tokenBalance.displayValue);
};

ここではまず、Contract WalletをもとにThirdwebSDKインスタンスを作成しています。
ThirdWebSDKインスタンスについては、「様々な関数を備えてるインスタンスで、Thirdwebを使用した開発で使うものだ」くらいの認識でよいと思っています(筆者自身そのレベルの認識です・・)。
そして、claimERC20Tokens()の中で、contract.erc20.claim(1)によってERCトークンを1だけ請求しています。

ちなみにここでやり取りしているトークンのアドレスは以下です(もとからハードコードされていました)。

0xc54414e0E2DBE7E9565B75EFdC495c7eD12D3823

EtherScanで見てみると、あらかじめ用意されたテスト用トークンのようです。
image.png

contract.erc20.claim(1)の中身を追ってみたのですが、筆者の力不足で、具体的な処理にはたどり着けませんでした・・・

erc-20.d.ts
    claim: {
        (amount: string | number, options?: ClaimOptions | undefined): Promise<Omit<{
            receipt: import("@ethersproject/abstract-provider").TransactionReceipt;
            data: () => Promise<unknown>;
        }, "data">>;
        prepare: (amount: string | number, options?: ClaimOptions | undefined) => Promise<Transaction<Omit<{
            receipt: import("@ethersproject/abstract-provider").TransactionReceipt;
            data: () => Promise<unknown>;
        }, "data">>>;
    };

image.pnghttps://www.erc4337.io/docs/understanding-ERC-4337/architecture

ここからは推測です。
ERC4337の構成図と照らし合わせると、contract.erc20.claim(1)は以下の処理を秘匿化しているのだと思っています。

  • 「受取先はContract Walletで、ERC20トークンを1請求する」というUserOperationを作成する
  • 作成したUserOperationをMempoolに放り込む

その後、実際にContract WalletにERC20トークンが入金されるまでの流れは以下の通りだと思っています(ソースから読み取れることの範囲外ですが)。

  • ThirdwebのBundlerがMempoolからUser Operationを収集する
  • Bundlerが複数のUser Operationを束ねてトランザクションを作成する
  • Bundlerが作成したトランザクションを、ThirdwebのEntryPointに向けて送る
  • EntryPoint・Contract Wallet間で検証等のやり取り、コントラクトの実行を行う
index.ts
  // Configure the smart wallet
  const config: SmartWalletConfig = {
    chain,
    factoryAddress,
    secretKey,
    gasless: true,
  };

  // Connect the smart wallet
  const smartWallet = new SmartWallet(config);
  await smartWallet.connect({
    personalWallet: adminWallet,
  });

また、少し前に戻りますが、Contract Walletを作成する際にオプションとしてgasless: trueを指定していました。
contract.erc20.claim(1)により、「Contract Walletの作成」と「ERCトークンの請求」のトランザクション実行にあたり、Contract WalletはGas代を支払わなくなります。その代わりに、Paymasterと呼ばれるコントラクトがGas代を肩代わりしているのだろうと考えられます。
このソースはGoerliテストネット上で動くので、おそらくGoerliETHを潤沢に保有するPaymasterをThirdwebが用意していて、そことやり取りしているのだと思います。

所感

Account Abstractionについて実際のソースを追うのは初めてでした。
Contract Walletの作成にEOAであるローカルウォレットが必要な点(Thirdwebの実装固有なのかわかっていませんが)はややこしく感じましたが、ContractWalletの作成についてそもそも意識すらできていなかったので、学びになりました。
JSON-RPC APIの呼び出しからコントラクトの実行までがThirdweb SDKの関数によって秘匿化されているのは、中で何が起こっているのか把握できない点で少しもやっとする結果でした。とはいえ、中身の理解ではなくAccount Abstractionの実装が目的であれば、Thirdwebはとても便利なツールです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0