【完全保存版】thirdwebのスマートウォレットをしっかり理解しよう
当記事は、こちらの記事を翻訳・編集したものです。
0 はじめに
ERC-4337スマートウォレット用のthirdwebのスマートコントラクトを深く掘り下げようとしています!
「ERC-4337」や「アカウント抽象化」についてよく知らない方は、この説明ブログを読んでキャッチアップしてください。
では、本題に入りましょう。
1 スマートウォレットの展望
スマートウォレットを使いたい理由は様々です。
1 各種機能について
個々人がスマートウォレットを使いたいのは、アカウントリカバリーやマルチシグウォレットなど、MetamaskのようなEOAウォレットでは得られない機能のためでしょう。
(スマートウォレットとEOAウォレットについてはこちら)
2 UXについて
開発者としては、Web3アプリのユーザーにスマートウォレットを発行して、より良いユーザーエクスペリエンス(「見えない財布」エクスペリエンス、トランザクションのバッチ実行、ガスレストランザクションなど)を作りたいと思うかもしれません。
3 現時点におけるスマートウォレット
要するに、スマートウォレットを使用することには多くの利点があるということです。
今日、すでに多くの機能が存在していますが、多くの革新的なスマートウォレット機能はまだ活発に構築されています。
thirdwebでは、今日導入し使用することに意味があり、将来のスマートウォレットの革新に備えるスマートウォレットコントラクトを構築することに着手しました。
2 ERC-4337スマートウォレットコントラクト
1 アカウントコントラクトについて
アカウントコントラクトはスマートウォレットの背後にあるスマートコントラクトです。
2 アカウントファクトリーコントラクトについて
アカウントファクトリーコントラクトは「アカウント」コントラクトを作成します。
私たちは「アカウントファクトリー」+「アカウント」コントラクトの3つのセットを開発しました。
3 スマートウォレットの基本機能
すべてのアカウント・コントラクト(シンプル、ダイナミック、マネージド)は、同じ基本機能を共有しています。
特筆すべきものを列挙します。
1 validateUserOp関数
アカウントはERC-4337と互換性があり、validateUserOp関数を実装しています。
その名の通り、「UserOperation」を検証します。
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;
struct UserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
uint256 callGasLimit;
uint256 verificationGasLimit;
uint256 preVerificationGas;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes paymasterAndData;
bytes signature;
}
interface IAccount {
function validateUserOp(
UserOperation calldata userOp,
bytes32 userOpHash,
uint256 missingAccountFunds
) external returns (uint256 validationData);
}
2 execute関数(executeBatch関数含む)
アカウントは、1つの呼び出しを実行するexecute関数と、1つのトランザクションで複数の呼び出しを実行するexecuteBatch関数を公開します。
/// @notice Executes a transaction (called directly from an admin, or by entryPoint)
function execute(
address _target,
uint256 _value,
bytes calldata _calldata
) external;
/// @notice Executes a sequence transaction (called directly from an admin, or by entryPoint)
function executeBatch(
address[] calldata _target,
uint256[] calldata _value,
bytes[] calldata _calldata
) external;
3 isValidateSignature関数
アカウントは EIP-1271 契約署名をサポートし、isValidSignature関数を実装すします。
function isValidSignature(bytes32 _hash, bytes memory _signature)
external
view
returns (bytes4 magicValue);
4 addDeposit関数とwithdrawDepositTo関数
アカウントにはaddDeposit関数とwithdrawDepositTo関数があります。
それぞれのERC-4337 EntryPointスマートコントラクトでアカウントのネイティブトークン残高を管理します。
(AccountCoreコントラクトにあります。)
/// @notice Deposit funds for this account in Entrypoint.
function addDeposit() public payable {
entryPoint().depositTo{ value: msg.value }(address(this));
}
/// @notice Withdraw funds for this account from Entrypoint.
function withdrawDepositTo(address payable withdrawAddress, uint256 amount) public {
_onlyAdmin();
entryPoint().withdrawTo(withdrawAddress, amount);
}
5 onERC721Received関数について
アカウントはonERC721Received関数とonERC1155Received関数を実装し、NFTの受け入れと所有ができます。
もちろん、アカウントはERC-20やネイティブトークンも所有できます。
/**
* @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
* by `operator` from `from`, this function is called.
*
* It must return its Solidity selector to confirm the token transfer.
* If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
*
* The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`.
*/
function onERC721Received(
address operator,
address from,
uint256 tokenId,
bytes calldata data
) external returns (bytes4);
}
/**
* @dev Handles the receipt of a single ERC1155 token type. This function is
* called at the end of a `safeTransferFrom` after the balance has been updated.
*
* NOTE: To accept the transfer, this must return
* `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
* (i.e. 0xf23a6e61, or its own function selector).
*
* @param operator The address which initiated the transfer (i.e. msg.sender)
* @param from The address which previously owned the token
* @param id The ID of the token being transferred
* @param value The amount of tokens being transferred
* @param data Additional data with no specified format
* @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed
*/
function onERC1155Received(
address operator,
address from,
uint256 id,
uint256 value,
bytes calldata data
) external returns (bytes4);
6 isValidSigner関数
最後に、すべてのアカウントは同じ権限モデルを共有します。
アカウントコントラクトはisValidSignerヘルパー関数を公開します。
与えられた署名者が与えられたUserOpでアカウントを使用する資格があるかどうかをチェックします。
/// @notice Returns whether a signer is authorized to perform transactions using the wallet.
function isValidSigner(address _signer, UserOperation calldata _userOp) public view virtual returns (bool)
4 スマートウォレットのパーミッションモデル。
全てのアカウントコントラクト[Simple、Dynamic、Managed]は同じパーミッションモデルを共有しています。
このセクションでは、この権限モデルについて詳しく説明します。
1 アクターの種類
アカウントは2種類のアクターのみを認識します:
1ー1 管理者
コントラクトの関数を呼び出したり、ERC-4337インフラストラクチャ(バンドル、EntryPointなど)を介さずにコントラクトを使用したり、アカウントのネイティブトークン残高を引き出したりできます。
1ー2 権限を持つ非管理者
「権限を持つ非管理者」を単に「署名者」と呼ぶことします。
署名者がアカウントを使用してトランザクションを実行するには、ERC-4337インフラストラクチャ(バンドル、EntryPointなど)を経由する必要があります。
署名者は一定の制限の下でアカウントを使用できる。
2 署名者の権限(SignerPermissons構造体)
署名者は、以下の制限の下でアカウントを使用することができます
struct SignerPermissions {
address signer;
address[] approvedTargets;
uint256 nativeTokenLimitPerTransaction;
uint128 startTimestamp;
uint128 endTimestamp;
}
各署名者は、アカウントを使用するための独自の権限を持っています。
管理者は署名者の権限を設定します。
基本的に、署名者はアカウントコントラクトを使って特定のウォレットを呼び出したり、最大の特定のネイティブトークンをアカウントから送金したり、特定の時間枠でのみアカウントを使用したりすることができます。
3 セキュリティ上の注意
署名者は、任意のターゲットコントラクトを呼び出したり、任意の量のネイティブトークンを送金できる「キャッチオール」権限を持つことはできません。
秘密鍵の紛失や漏洩は(残念ながら)よくあることなので、これは重要なセキュリティ上の考慮事項です。
このように、アカウントは意図された署名者キーによる利用を促します。
アカウントに「回復」があったとしても、管理者キーが流出するということは、あなたの管理者キーを持つ者がスマートウォレットを無制限にコントロールできることを意味します。
4 署名者への権限の割り当て
管理者は署名者の権限を設定します。
管理者は setPermissionsForSigner 関数を呼び出して、指定された署名者の権限を設定します。
struct SignerPermissionRequest {
address signer;
address[] approvedTargets;
uint256 nativeTokenLimitPerTransaction;
uint128 permissionStartTimestamp;
uint128 permissionEndTimestamp;
uint128 reqValidityStartTimestamp;
uint128 reqValidityEndTimestamp;
bytes32 uid;
}
/// @notice Sets the permissions for a given signer.
function setPermissionsForSigner(SignerPermissionRequest calldata req, bytes calldata signature) external;
この関数はEIP-712型データ署名を使用します。
つまり、管理者はSignerPermissionRequestのペイロードをうめて署名し、ペイロードと生成された署名を関数に渡さなければならない。
署名を使用するこの方法は、UXを考慮したものです。
管理者がERC-4337インフラストラクチャ、つまりバンドラーをバイパスしてスマートウォレットを直接使うことができるとしても、バンドラーを通してスマートウォレットを使っている可能性が高いです。
バンドラーを通してスマートウォレットを使う場合、setPermissionsForSigner関数内のmsg.senderの値はEntryPointコントラクトであり、スマートウォレットを使う管理者ではありません。
そのため、パーミッションチェックを呼び出し元に対して賢明 に実行することができません。
setPermissionsForSigner関数をシグネチャベースにすることで、管理者はバンドラーを使用するUXを壊すことなく、署名者のパーミッションを設定することができます。
5 管理者権限の割り当て
アカウントの既存の管理者は、新しい管理者を追加したり、既存の管理者を削除したり、自分の管理者ステータスを放棄したりすることができます。
これは setAdmin 関数を呼び出すだけで行えます。
/// @notice Adds / removes an account as an admin.
function setAdmin(address account, bool isAdmin) external;
この関数は、意図的に署名に基づかないものとしています。
署名者のパーミッションを割り当てることは重要なユーザーアクションであり、管理者が意識的に直接setAdmin関数を呼び出すことを強制します。
5 アカウントファクトリーコントラクトのパーミッションモデル
すべてのアカウントファクトリー契約[Simply、Dynamic、Managed]はロールベースのアクセス制御を使用します。
3つのアカウントファクトリーコントラクトはすべてDEFAULT_ADMIN_ROLEを持ちます。
このロールの保持者のみがアカウントファクトリーのコントラクトメタデータ(すなわち、名前、説明、画像、およびコントラクトに関連付けるその他のメタデータ)を設定する資格があります。
SimpleおよびDynamicアカウントファクトリーコントラクトには、権限が付与されたアクションはありません。
しかし、Managedアカウントファクトリーコントラクトには、追加のロールEXTENSION_ROLEがあります。
このロールの所有者だけが、管理アカウントファクトリによって作成された子アカウントコントラクトのアップグレードを実行できます。
6 アップグレード可能なアカウントスマートコントラクト
Simpleのアカウント・コントラクトAccountはアップグレードできません。
しかし、DynamicAccountおよびManagedAccountコントラクトはアップグレード可能です。
1 ダイナミック・コントラクト:入門
ダイナミック・コントラクトとマネージド・アカウント・コントラクトは、どちらもダイナミック・コントラクト・パターンを使用します。
ここで簡単な入門知識を説明します。
アップグレーダブルコントラクトとは、実装コントラクト + プロキシ・コントラクトです。
プロキシコントラクトの仕事は、受け取ったコールを、delegateCallを 介して実装コントラクトに転送することです。
省略形として、プロキシ・コントラクトはステートを保存し、実装コントラクトに(呼び出しを受けたときに)ステートをどのように変更するかを常に尋ねます。
このパターンの詳細については、この投稿を読んでください。
ダイナミック・コントラクト・パターンは、ルーター・スマート・コントラクトを導入します。
このルーターコントラクトはプロキシですが、常に同じ実装コントラクトをデリゲートコールするのではありません。
ルーターが受け取る特定の関数コールに対して特定の実装コントラクト(別名「エクステンション」)をデリゲートコールします。
ルーターは、関数セレクタ→与えられた関数が実装される実装コントラクトへのマップを格納します。
「コントラクトをアップグレードする」とは、単に、与えられた関数がどの実装コントラクトにマッピングされるかを更新することを意味します。
2 アップグレードはどのようにダイナミック・アカウント・コントラクトと連動しますか?
DynamicAccountアカウント・スマート・コントラクトは、ダイナミック・コントラクト・パターンで記述され、前述のルーター・コントラクトを継承しています。
したがって、ダイナミック・アカウント・コントラクトは、基本的にルーターについて示した図のように動作します。
個々のダイナミック・アカウント・コントラクト(親アカウント・ファクトリによって作成されるか、スタンドアローンとしてデプロイされる)はアップグレード可能です。
アップグレードは、アカウントの管理者が以下のAPI経由で実行できます:
struct ExtensionMetadata {
string name;
string metadataURI;
address implementation;
}
struct ExtensionFunction {
bytes4 functionSelector;
string functionSignature;
}
struct Extension {
ExtensionMetadata metadata;
ExtensionFunction[] functions;
}
/// @dev Adds a new extension to the router.
function addExtension(Extension memory extension) external;
/// @dev Updates an existing extension in the router, or overrides a default extension.
function updateExtension(Extension memory extension) external;
/// @dev Removes an existing extension from the router.
function removeExtension(Extension memory extension) external;
最終的に、このAPIは、アカウントコントラクトに格納されている拡張契約への関数セレクタ→のマップを更新する責任があります。
3 アップグレードはどのようにマネージド・アカウント・コントラクトと連携するのか?
ダイナミック・アカウント・コントラクトと同様に、ManagedAccountアカウント・コントラクトもダイナミック・コントラクト・パターンで記述されます。
個々のダイナミックアカウントは、関数セレクタ→拡張コントラクトの独自のマップを保存します。
一方、すべてのマネージドアカウントコントラクトは、親であるManagedAccountFactoryファクトリーコントラクトによって保存されたこの同じマップをリッスンすることです。
マネージドアカウントが「マネージド」と呼ばれるのはこのためです。
マネージドアカウントファクトリー契約の管理者は、ファクトリーの子マネージドアカウントの機能を管理する責任があります。
マネージドアカウントファクトリーの管理者がファクトリーコントラクト内の機能セレクタ→拡張マップを更新(スルー)すると、このアップグレードは即座にファクトリーのすべての子アカウントコントラクトに適用されます。
4 テストドライブ:アプリのユーザーにスマートウォレットを発行します。
本稿執筆時点では、スマートウォレットコントラクトは監査済みで、現在ベータアクセス中です。
つまり、スマートウォレットをアプリに統合するエンドツーエンドのテストネットを今すぐ試すことができます。
ご自身のアカウントファクトリーコントラクト(シンプル、ダイナミック、マネージド)をデプロイしてください。
Accountsタブに移動し、自分用のアカウントコントラクトを作成します。
その下で、アカウントファクトリのコントラクトでcreateAccount関数を呼び出しています。
/// @notice Deploys a new Account for admin.
function createAccount(address _admin, bytes calldata _data) external returns (address);
アカウントコントラクトのダッシュボードに移動します。
AccountタブでスマートウォレットのトークンとNFTの残高を確認できます。
このように残高や所有しているNFTが確認できます。
「Account Permissions(アカウント権限)」タブでウォレットのすべての署名者(およびその権限)を確認できます。
いよいよスマートウォレットを使って実際に取引を行ってみましょう!
まず、https://thirdweb.com/dashboard/settings/api-keys でAPIキーを取得してください。
ご不明な場合は、こちらもご確認ください。
コードに戻り、スマートウォレットに接続します。
import { LocalWallet, SmartWallet } from "@thirdweb-dev/wallets";
import { Goerli } from "@thirdweb-dev/chains";
// First, connect the personal wallet, which can be any wallet (metamask, walletconnect, etc.)
// Here we're just generating a new local wallet which can be saved later
const personalWallet = new LocalWallet();
await personalWallet.generate();
// Setup the Smart Wallet configuration
const config: SmartWalletConfig = {
chain: Goerli, // the chain where your smart wallet will be or is deployed
factoryAddress: "{{factory_address}}", // your own deployed account factory address
clientId: "YOUR_CLIENT_ID", // Use client id if using on the client side, get it from dashboard settings
secretKey: "YOUR_SECRET_KEY", // Use secret key if using on a node script or server, get it from dashboard settings
gasless: true, // enable or disable gasless transactions
};
// Then, connect the Smart wallet
const wallet = new SmartWallet(config);
await wallet.connect({
personalWallet,
});
そして、このウォレット・オブジェクトを持って、通常のようにSDKを使ってください。
ここでは、コントラクトをデプロイして、NFTをミントしています。
import { LocalWallet, SmartWallet } from "@thirdweb-dev/wallets";
import { Goerli } from "@thirdweb-dev/chains";
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
// .. previous snippet
const wallet = new SmartWallet(config);
await wallet.connect({
personalWallet,
});
const sdk = ThirdwebSDK.fromWallet(wallet, Goerli, {
clientId: "YOUR_CLIENT_ID", // Use client id if using on the client side, get it from dashboard settings
secretKey: "YOUR_SECRET_KEY", // Use secret key if using on a node script or server, get it from dashboard settings
});
// Under the hood, the SDK will route your call through a bundler
// all transactions will be executed as the Smart Wallet
// deploy a new NFT collection contract
const nftContractAddress = await sdk.deployer.deployNFTCollection({
name: "My first NFT Contract",
primary_sale_recipient: await wallet.getAddress(),
});
// mint an NFT on the deployed NFT collection
const nftContract = await sdk.getContract(nftContractAddress);
const tx = await nftContract.erc721.mint({
name: "My first NFT",
});
thirdwebのスマートウォレットの使用に関する詳細は、ポータルサイトでご覧いただけます。
7 スマート・コントラクト報奨金プログラム
このディープ・ダイブにお付き合いいただきありがとうございます。
いつものように、私たちはガスの最適化とセキュリティ脆弱性の報奨金プログラムをライブで行っています。
私たちのスマートウォレットコントラクトを見てみましょう - それらを改善し、お金を稼いでください。
サポートをしていただけたらすごく嬉しいです😄 いただけたサポートを励みに、これからもコツコツ頑張っていきます😊