はじめに
『DApps開発入門』という本や色々記事を書いているかるでねです。
今回は、コントラクトアカウントをモジュール構成にすることができる仕組みを提案しているERC5005についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIPについてまとめています。
概要
ERC5005は、Ethereum上でのプログラマブルアカウントに関するツールをより柔軟かつ相互運用可能にするためのインターフェース標準を提案しています。
従来、DAOなどのプログラマブルアカウントは、認証処理と実行処理が密接に結合されたモノリシックな設計で構成されることが一般的でした。
しかしERC5005ではその設計を改め、アカウント本体(avatar
)と認証および実行ロジック(guard
やmodule
)を分離することで、再利用性と拡張性を向上させることを目的としています。
主要コンポーネントの分離
ERC5005では、アカウントを構成する以下のコンポーネントにインターフェースを定義します。
- Avatar(アバター)
実際のアカウント本体であり、IAvatarインターフェースを実装します。
アカウントの実行はこのコントラクトを通して行われます。
- Guard(ガード)
アカウントが特定の処理を実行してよいかどうかをチェックするための認証ロジックを持つコンポーネントで、IGuardインターフェースを実装します。
- Module(モジュール)
任意の形を取れる拡張コンポーネントで、追加の機能や処理を柔軟に実装できます。
これにより、アカウントの操作ロジックを自由に差し替えたり、複数のガバナンス手法を組み合わせることが可能になります。
動機
現在の多くのDAOツールやアカウントフレームワークは、認証と実行ロジックを一体化して設計されており以下のような課題を抱えています。
- 拡張性の欠如
認証と実行が一体化しているため、後から別のロジックを追加するのが難しい。
- ツールのロックイン
一度導入したシステムを別のフレームワークに切り替えるに手間がかかる。
- ガバナンスの柔軟性不足
プロジェクトの成長に合わせて、より複雑な意思決定プロセスへ移行しづらい。
ERC5005は、こうした課題を解決するために、アカウント制御の責任範囲を明確に分離する設計を採用しています。
これにより、以下のメリットが得られます。
- 柔軟なモジュール型アカウント制御が可能
- ツールやフレームワーク間の切り替えが容易
- 複数の制御手段を同時に使用可能(例:マルチシグとロールベース承認を併用)
- クロスチェーン/クロスレイヤーでのガバナンスに対応
- 段階的な分散化を実現(初期は単純な制御、後に複雑な分散ガバナンスに移行)
仕様
構成要素
ERC5005では以下の4つの構成要素を定義しています。
Avatar(アバター)
Ethereum上の実際のアカウント(アドレス)で、資産の保持、トランザクションの実行、他のシステムとの接続などを行います。
IAvatar
インターフェースを実装する必要があります。
モジュールを有効化/無効化し、モジュールからのトランザクション実行を受け入れます。
Module(モジュール)
アバターにより有効化される実行ロジックの単位です。
任意の形式を取ることができ、特定のアクションやルールを定義するコントラクトです。
Modifier(モディファイア)
モジュールとアバターの中間に配置されるコントラクトで、モジュールの挙動に制限や変化を加えます。
例としては、すべての関数呼び出しに遅延を加えたり、実行できるトランザクションを制限することが挙げられます。
IAvatar
インターフェースを実装する必要があります。
Guard(ガード)
モジュールやモディファイアに任意で追加できるセキュリティ機構です。
トランザクションの**実行前と実行後にチェックを行います。
IGuard
インターフェースを実装し、ガード可能なコントラクトは Guardable
を継承し、checkTransaction()
と checkAfterExecution()
を適切なタイミングで呼び出す必要があります。
Avatarインターフェース
/// @title Avatar - A contract that manages modules that can execute transactions via this contract.
pragma solidity >=0.7.0 <0.9.0;
import "./Enum.sol";
interface IAvatar {
event EnabledModule(address module);
event DisabledModule(address module);
event ExecutionFromModuleSuccess(address indexed module);
event ExecutionFromModuleFailure(address indexed module);
/// @dev Enables a module on the avatar.
/// @notice Can only be called by the avatar.
/// @notice Modules should be stored as a linked list.
/// @notice Must emit EnabledModule(address module) if successful.
/// @param module Module to be enabled.
function enableModule(address module) external;
/// @dev Disables a module on the avatar.
/// @notice Can only be called by the avatar.
/// @notice Must emit DisabledModule(address module) if successful.
/// @param prevModule Address that pointed to the module to be removed in the linked list
/// @param module Module to be removed.
function disableModule(address prevModule, address module) external;
/// @dev Allows a Module to execute a transaction.
/// @notice Can only be called by an enabled module.
/// @notice Must emit ExecutionFromModuleSuccess(address module) if successful.
/// @notice Must emit ExecutionFromModuleFailure(address module) if unsuccessful.
/// @param to Destination address of module transaction.
/// @param value Ether value of module transaction.
/// @param data Data payload of module transaction.
/// @param operation Operation type of module transaction: 0 == call, 1 == delegate call.
function execTransactionFromModule(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation
) external returns (bool success);
/// @dev Allows a Module to execute a transaction and return data
/// @notice Can only be called by an enabled module.
/// @notice Must emit ExecutionFromModuleSuccess(address module) if successful.
/// @notice Must emit ExecutionFromModuleFailure(address module) if unsuccessful.
/// @param to Destination address of module transaction.
/// @param value Ether value of module transaction.
/// @param data Data payload of module transaction.
/// @param operation Operation type of module transaction: 0 == call, 1 == delegate call.
function execTransactionFromModuleReturnData(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation
) external returns (bool success, bytes memory returnData);
/// @dev Returns if an module is enabled
/// @return True if the module is enabled
function isModuleEnabled(address module) external view returns (bool);
/// @dev Returns array of modules.
/// @param start Start of the page.
/// @param pageSize Maximum number of modules that should be returned.
/// @return array Array of modules.
/// @return next Start of the next page.
function getModulesPaginated(address start, uint256 pageSize)
external
view
returns (address[] memory array, address next);
}
イベント
EnabledModule
モジュールが有効化された時に発行されるイベント。
DisabledModule
モジュールが無効化された時に発行されるイベント。
ExecutionFromModuleSuccess
モジュールによるトランザクションが成功した時に発行されるイベント。
ExecutionFromModuleFailure
モジュールによるトランザクションが失敗した時に発行されるイベント。
関数
enableModule
指定されたモジュールをアバターに登録して有効化する関数。
モジュールはリンクリスト構造で保持されます。
アバターからのみ呼び出すことができます。
実行成功時にEnabledModule
イベントを発行します。
引数
-
module
- 有効化したいモジュールのアドレス。
disableModule
指定されたモジュールを無効化する関数。
リンクリスト構造で管理されているため、直前のモジュールアドレスが必要です。
アバターからのみ呼び出すことができます。
実行成功時にDisabledModule
イベントを発行します。
引数
-
prevModule
- 無効化したいモジュールの直前にあるモジュールのアドレス。
-
module
- 無効化するモジュールのアドレス。
execTransactionFromModule
モジュールがアバター経由でトランザクションを実行する関数。
実行成功時にExecutionFromModuleSuccess
イベント、失敗時は ExecutionFromModuleFailure
イベントを発行します。
引数
-
to
- 送信先アドレス。
-
value
- 送金するETHの量。
-
data
- 呼び出す関数や引数を含むバイト列。
-
operation
- 実行の種類(
Call
かDelegateCall
)。
- 実行の種類(
戻り値
実行の成否を示すbool値
。
execTransactionFromModuleReturnData
execTransactionFromModule
関数の処理に加えて、呼び出し先からの戻り値を取得できる関数。
戻り値
実行の成否を示すbool値
と呼び出し先からの戻り値。
isModuleEnabled
モジュールが現在有効か確認する関数。
引数
-
module
- 対象モジュール。
戻り値
true
(有効)または false
(無効)。
getModulesPaginated
モジュール一覧をページネーション形式で取得する関数。
引数
-
start
- 取得を開始するモジュールのアドレス。
-
pageSize
- 最大取得件数。
戻り値
-
array
- モジュールアドレスの配列。
-
next
- 次のページの開始アドレス。
Guardインターフェース・Guardableコントラクト
IGuardインターフェース
pragma solidity >=0.7.0 <0.9.0;
import "./Enum.sol";
interface IGuard {
function checkTransaction(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address payable refundReceiver,
bytes memory signatures,
address msgSender
) external;
function checkAfterExecution(bytes32 txHash, bool success) external;
}
checkTransaction
トランザクションが実行される前に呼び出される関数。
送信先アドレスや実行データ、送金額などを含む、処理実行のための事前確認を行います。
モジュール取引では使われないパラメータもありますが、互換性のために引数に含まれます。
checkAfterExecution
トランザクションが実行された後に呼ばれる関数。
事後処理やロギングを行います。
引数
-
txHash
- 実行したトランザクションのハッシュ。
-
success
- 実行結果の成功・失敗フラグ。
Guardableコントラクト
pragma solidity >=0.7.0 <0.9.0;
import "./Enum.sol";
import "./BaseGuard.sol";
/// @title Guardable - A contract that manages fallback calls made to this contract
contract Guardable {
address public guard;
event ChangedGuard(address guard);
/// `guard_` does not implement IERC165.
error NotIERC165Compliant(address guard_);
/// @dev Set a guard that checks transactions before execution.
/// @param _guard The address of the guard to be used or the 0 address to disable the guard.
function setGuard(address _guard) external {
if (_guard != address(0)) {
if (!BaseGuard(_guard).supportsInterface(type(IGuard).interfaceId))
revert NotIERC165Compliant(_guard);
}
guard = _guard;
emit ChangedGuard(guard);
}
function getGuard() external view returns (address _guard) {
return guard;
}
}
guard
現在設定されているガードアドレス。
setGuard
指定されたガードを登録する関数。
0アドレスを指定するとガードは無効化されます。
また、設定するガードはIERC165を実装している必要があります。
条件を満たさない場合、NotIERC165Compliant
エラーが返されます。
ERC165については以下の記事を参考にしてください。
getGuard
現在設定されているガードアドレスを取得する関数。
ChangedGuard
ガードの設定変更時に発行されるイベント。
BaseGuard
pragma solidity >=0.7.0 <0.9.0;
import "./Enum.sol";
import "./IERC165.sol";
import "./IGuard.sol";
abstract contract BaseGuard is IERC165 {
function supportsInterface(bytes4 interfaceId)
external
pure
override
returns (bool)
{
return
interfaceId == type(IGuard).interfaceId || // 0xe6d7a83a
interfaceId == type(IERC165).interfaceId; // 0x01ffc9a7
}
/// @dev Module transactions only use the first four parameters: to, value, data, and operation.
/// Module.sol hardcodes the remaining parameters as 0 since they are not used for module transactions.
function checkTransaction(
address to,
uint256 value,
bytes memory data,
Enum.Operation operation,
uint256 safeTxGas,
uint256 baseGas,
uint256 gasPrice,
address gasToken,
address payable refundReceiver,
bytes memory signatures,
address msgSender
) external virtual;
function checkAfterExecution(bytes32 txHash, bool success) external virtual;
}
BaseGuard
はIGuardとIERC165の実装を前提とした抽象コントラクトであ、ガードの共通的なロジックを定義します。
関数
supportsInterface
コントラクトが対応しているインターフェースIDを返す関数。
IGuardとIERC165の両方に対応している必要があります。
checkTransaction
実装を持たない関数で、継承先で定義される関数です。
checkAfterExecution
実装を持たない関数で、継承先で定義される関数です。
Enumコントラクト
pragma solidity >=0.7.0 <0.9.0;
/// @title Enum - Collection of enums
contract Enum {
enum Operation {Call, DelegateCall}
}
Enum
コントラクトは操作の種類を列挙する Operation
型を定義しています。
-
Call
- 通常のコントラクト呼び出し(状態変更は呼び出し先で発生)。
-
DelegateCall
- 呼び出し元のストレージを使って、呼び出し先のコードを実行する。
このEnumは、どのような形でトランザクションが実行されるかを示す重要な情報です。
補足
ERC5005は、現在広く使われているプログラマブルアカウント(例:Gnosis Safeなど)との高い互換性を意識して設計されています。
この目的は、既存のツールやインフラに最小限の変更で統合できるようにすることです。
これにより、今までのアーキテクチャに基づいて構築されたツールやスマートコントラクトを使い続けながら、新しい標準に段階的に移行できます。
開発者が一から作り直す必要がないというメリットもあります。
互換性
ERC5005は互換性の問題はありません。
セキュリティ
モジュールに完全な権限がある
有効化されたモジュールは、そのアバターに対して完全な操作権限を持ちます。
つまり、そのモジュールを通してアバターに保有された全ての資産が操作可能になることを意味します。
そのため、モジュールの導入は慎重に行うべきです。
- 信頼できないモジュールは絶対に有効化しない
- 全ての資産を託しても問題ないと判断できるモジュールのみ許可する
レースコンディションのリスク
アバターは複数のモジュールを同時に有効にすることができます。
このとき、複数のモジュールが同時にトランザクションを発行・制御することによるレースコンディション(競合状態)が発生する可能性があります。
- モジュール間の競合によって想定外の状態遷移や資産移動が起こるリスク
- ガード(IGuard)による事前・事後チェックの導入を強く推奨
モジュールを全て削除すると操作不能になる
全てのモジュールを削除してしまった場合、そのアバターには操作を行う手段が一切なくなり、永久にロックされてしまう可能性があります。
これを「brick」と呼びます。
- 最後のモジュールを削除する前に、必ず操作手段を確保していることを確認
- 冗長なモジュール構成やバックアップ手段を用意するのが望ましい
引用
Auryn Macmillan (@auryn-macmillan), Kei Kreutler (@keikreutler), "ERC-5005: Zodiac Modular Accounts [DRAFT]," Ethereum Improvement Proposals, no. 5005, April 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5005.
最後に
今回は「コントラクトアカウントをモジュール構成にすることができる仕組みを提案しているERC5005」についてまとめてきました!
いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!