はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、分散型プロトコルの中で使われる複数のスマートコントラクトを一元管理し、必要に応じて安全かつ柔軟に入れ替え・更新できる仕組みを提案しているERC6224についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIP・BIP・SLIP・CAIP・ENSIP・RFC・ACPについてまとめています。

概要

ERC6224では、分散型プロトコルが自らのスマートコントラクトを効率的に管理するためのオンチェーンレジストリシステムを提案しています。

このシステムは、主に2つのコンポーネントで構成されています。

コンポーネント名 役割
ContractsRegistry プロトコル内で利用される全てのスマートコントラクトの参照情報を保存するコントラクト。必要に応じて自己管理型プロキシを配置し、アップグレード可能な仕組みを提供する。
Dependant ContractsRegistry に登録された依存関係を取得して利用するコントラクト。

ContractsRegistry は、プロトコル内の各コントラクトを一元的に管理し、アップグレードや差し替えを柔軟に行える「ハブ」の役割を果たします。 一方 Dependant は、このレジストリから必要な依存コントラクトを取得する仕組みを備えています。

つまり、ERC6224は「プロトコル全体のコントラクト構成を整理し、アップグレードや依存関係の管理を安全かつ柔軟に行うためのフレームワーク」を提案しています。

動機

Ethereumエコシステムが発展するにつれて、プロジェクトの規模や複雑さは急速に増大しています。 多くの現代的な分散型アプリケーション(DApp)やプロトコルは、以下のような課題に直面しています。

  • 新機能の継続的な提供と改善の必要性 ユーザーのニーズに応えるため、プロトコルは頻繁に新しい機能を追加し、改良を重ねる必要があります。
  • ブロックチェーンの不変性による制約 スマートコントラクトは基本的に一度デプロイすると変更できません。 そのため、後から修正やアップグレードを行うことが難しいという課題があります。
  • 複雑な依存関係の管理問題 コントラクト間の依存関係が増えるにつれて、どのコントラクトがどの機能を利用しているのかが不明瞭になり、システム全体の保守性が低下します。
  • バグリスクの増大 継続的な開発や複雑な構成により、バグの混入率が上昇し、デバッグや修正が困難になります。

こうした背景から、ERC6224は以下を目指しています。

  • 明確なアーキテクチャ構造を持つこと 各コンポーネントの役割や依存関係が明示的で、理解しやすい構造を維持する。
  • 将来の拡張に対応できる設計であること 新しい機能やコントラクトの追加・変更が、既存システムを壊すことなく行えるようにする。
  • 依存関係を透明化し、管理しやすくすること 各コントラクトがどの依存先を利用しているかを可視化し、必要に応じて動的に差し替えられるようにする。

メリット

提案された ContractsRegistry / Dependant システムを導入することで、プロトコル設計者や開発者は以下のようなメリットを得られます。

項目 説明
スマートコントラクトの構造的な管理 専用の管理コントラクト(ContractsRegistry)を通して、全ての依存コントラクトを一元管理できる。
柔軟なアップグレード機能 プロトコル単位でのアドホックなアップグレード(部分的更新)を可能にする。
ランタイムでのコントラクト操作 実行時にスマートコントラクトの追加、削除、置き換えを安全に行える。
依存性注入の仕組み Dependant を通じて、必要な依存関係をレジストリから安全に取得する仕組みを提供。
アクセス制御の柔軟性 プロトコルの維持に必要なカスタムアクセス制御ルールを設定できる。

これらのメリットにより、開発者はシステムの柔軟性と保守性を大きく向上させることができます。 また、依存関係の変更やバグ修正をオンチェーンで安全に行えるため、プロジェクトの継続的な改善を支える基盤として機能します。

仕様

概要

ERC6224は、以下の2つのスマートコントラクトで構成されています。

コントラクト名 説明
ContractsRegistry プロトコル内で使用するスマートコントラクトを登録・管理・アップグレードするための単一(シングルトン)レジストリ。
Dependant Registryの情報を参照するコントラクト。

これら2つのコントラクトの関係は、以下のように整理できます。

  • ContractsRegistryはプロトコル全体の中心的なハブとなり、すべてのコントラクトの参照情報を保持します。
  • DependantはRegistryに依存するコントラクトで、Registryから必要な情報を取得して機能します。

ContractsRegistry

ContractsRegistryはERC6224の中核を担うコントラクトであり、プロトコルで使用する全ての独立したコントラクトの参照情報を保持します。 また、登録されたコントラクトの上に任意のプロキシコントラクトをデプロイするよう設定することも可能です。

仕様

  • ContractsRegistry は、ゼロアドレス(address(0))の登録を拒否する必要があります。
  • 登録されたコントラクトは名前で参照され、アップグレードや削除などの操作が行えます。
  • 参照データの提供、プロキシの追加・アップグレードなどの操作が可能です。
javascript
pragma solidity ^0.8.0;

interface IContractsRegistry {
    event ContractAdded(string name, address contractAddress);
    event ProxyContractAdded(string name, address contractAddress, address implementation);
    event ProxyContractUpgraded(string name, address newImplementation);
    event ContractRemoved(string name);

    function getContract(string memory name) external view returns (address);
    function hasContract(string memory name) external view returns (bool);
    function injectDependencies(string memory name) external;
    function injectDependenciesWithData(string memory name, bytes memory data) external;
    function upgradeContract(string memory name, address newImplementation) external;
    function upgradeContractAndCall(string memory name, address newImplementation, bytes memory data) external;
    function addContract(string memory name, address contractAddress) external;
    function addProxyContract(string memory name, address contractAddress) external;
    function addProxyContractAndCall(string memory name, address contractAddress, bytes memory data) external;
    function justAddProxyContract(string memory name, address contractAddress) external;
    function removeContract(string memory name) external;
}
ContractAdded
javascript
event ContractAdded(string name, address contractAddress);

新しいコントラクトがレジストリに追加された時に発行されるイベント。

このイベントは、addContract 関数が呼び出された時に発行されます。 コントラクトが正常に登録されたことを示すために利用されます。

パラメータ

  • name 登録されたコントラクトの名前。
  • contractAddress 登録されたコントラクトのアドレス。
ProxyContractAdded
javascript
event ProxyContractAdded(string name, address contractAddress, address implementation);

新しいプロキシコントラクトがレジストリに追加された時に発行されるイベント。

addProxyContract または addProxyContractAndCall 関数の呼び出し時に発行されます。 プロキシのアドレスと、指している実装コントラクトのアドレスが記録されます。

パラメータ

  • name 登録名。
  • contractAddress デプロイされたプロキシのアドレス。
  • implementation プロキシが指す実装コントラクトのアドレス。
ProxyContractUpgraded
javascript
event ProxyContractUpgraded(string name, address newImplementation);

既存のプロキシコントラクトが新しい実装にアップグレードされた時に発行されるイベント。

upgradeContract または upgradeContractAndCall 関数の呼び出し時に発行されます。 新しい実装コントラクトのアドレスがイベントとして記録されます。

パラメータ

  • name アップグレードされたコントラクトの名前。
  • newImplementation 新しい実装コントラクトのアドレス。
ContractRemoved
javascript
event ContractRemoved(string name);

コントラクトがレジストリから削除された時に発行されるイベント。 removeContract 関数の呼び出しにより発行されます。

パラメータ

  • name 削除されたコントラクトの名前。
getContract
javascript
function getContract(string memory name) external view returns (address);

指定した名前のコントラクトを取得する関数。 レジストリに登録されたコントラクトのアドレスを返します。 ゼロアドレスの場合はリバートします。

引数

  • name コントラクトの登録名。

戻り値

  • address 登録されたコントラクトのアドレス。
hasContract
javascript
function hasContract(string memory name) external view returns (bool);

指定した名前のコントラクトが存在するか確認する関数。 レジストリ内に登録済みであれば true を返します。

引数

  • name コントラクト名。

戻り値

  • bool コントラクトが存在するかどうか。
injectDependencies
javascript
function injectDependencies(string memory name) external;

指定したコントラクトに、必要な他のコントラクトの情報を登録するための関数です。 この関数を実行すると、対象のコントラクトに対して setDependencies(address(this), bytes("")) が呼び出され、 レジストリからそのコントラクトが利用する他のコントラクトのアドレスなどを教える処理を行います。 ここでは追加のデータは渡さず、基本的な設定のみが行われます。

引数

  • name 設定対象となるコントラクトの名前。
injectDependenciesWithData
javascript
function injectDependenciesWithData(string memory name, bytes memory data) external;

指定したコントラクトに、追加情報を渡しながら他のコントラクトの情報を登録するための関数です。

この関数を実行すると、対象のコントラクトに対して setDependencies(address(this), data) が呼び出されます。 data には特定の状況に応じた設定や初期化に使う追加情報を含めることができ、レジストリが対象コントラクトに必要な他のコントラクトのアドレスなどを伝えると同時に、そのデータも一緒に渡します。

引数

  • name 設定対象となるコントラクトの名前。
  • data 追加で渡す情報(任意)。
upgradeContract
javascript
function upgradeContract(string memory name, address newImplementation) external;

既存のプロキシコントラクトを新しい実装にアップグレードする関数。 互換性の確認はオーナーで行います。 アップグレード後に ProxyContractUpgraded イベントが発行されます。

引数

  • name プロキシコントラクト名。
  • newImplementation 新しい実装コントラクトのアドレス。
upgradeContractAndCall
javascript
function upgradeContractAndCall(string memory name, address newImplementation, bytes memory data) external;

新しい実装にアップグレードし、指定のデータを使って初期化などの呼び出しを行う関数。 upgradeContract に加えて、アップグレード後にデータ付きで呼び出しが可能です。

引数

  • name コントラクト名。
  • newImplementation 新しい実装のアドレス。
  • data 呼び出すデータ(ABIエンコードされた関数呼び出しなど)。
addContract
javascript
function addContract(string memory name, address contractAddress) external;

非プロキシのコントラクトをレジストリに追加する関数。 アップグレード不要のコントラクトや外部依存コントラクトの登録に利用します。

引数

  • name コントラクト名。
  • contractAddress 登録するコントラクトのアドレス。
addProxyContract
javascript
function addProxyContract(string memory name, address contractAddress) external;

実装アドレスを指定してプロキシコントラクトをレジストリに追加する関数。 アップグレード可能なコントラクトを追加する時に使用します。

引数

  • name コントラクト名。
  • contractAddress 実装コントラクトのアドレス。
addProxyContractAndCall
javascript
function addProxyContractAndCall(string memory name, address contractAddress, bytes memory data) external;

データ付きでプロキシコントラクトを追加する関数。 追加時に初期化関数を呼び出すなど、柔軟な登録が可能です。

引数

  • name コントラクト名。
  • contractAddress 実装コントラクトのアドレス。
  • data ABIエンコードされた初期化呼び出しなど。
justAddProxyContract
javascript
function justAddProxyContract(string memory name, address contractAddress) external;

既にデプロイ済みのプロキシコントラクトをレジストリに追加する関数。 システム移行時などに使用されます。 新しいレジストリがアップグレード権限を持つ必要があります。

引数

  • name コントラクト名。
  • contractAddress 既存のプロキシのアドレス。
removeContract
javascript
function removeContract(string memory name) external;

登録済みのコントラクトを削除する関数。 対象コントラクトをレジストリから削除します。 削除時に ContractRemoved イベントが発行されます。

引数

  • name 削除対象のコントラクト名。

Dependant

Dependantは、ContractsRegistryと連携して動作するコントラクトです。 プロトコル内のすべてのスタンドアロンコントラクトは、この Dependant を継承する必要があります。

依存関係は、コンストラクタや初期化関数ではなく、setDependencies 関数で設定されます。

また、他のコントラクトの情報の設定時はアクセス制御が必要です。

インターフェース

javascript
pragma solidity ^0.8.0;

interface IDependant {
    function setDependencies(address contractsRegistry, bytes memory data) external;
    function setInjector(address injector) external;
    function getInjector() external view returns (address);
}
setDependencies
javascript
function setDependencies(address contractsRegistry, bytes memory data) external;

ContractsRegistry から呼び出され、指定されたコントラクトが利用する他のコントラクトの情報を設定するための関数です。 この関数は、msg.sender が ContractsRegistry であることを確認したうえで呼び出されます。 ここで、対象コントラクトが利用する他のコントラクトのアドレスなどをレジストリから取得し、内部で設定します。 これにより、外部のレジストリがそのコントラクトに必要な情報を教える形になります。

引数

  • contractsRegistry 情報を取得するレジストリのアドレス。
  • data 状況に応じた追加情報(初期化などに利用)。
setInjector
javascript
function setInjector(address injector) external;

新しい情報提供者(injector)を設定する関数です。 この関数は、誰がそのコントラクトに対して他のコントラクトの情報を渡せるかを指定します。 呼び出し権限は制御され、許可されたアドレスのみがこの設定を変更できます。

引数

  • injector 新しい情報提供者のアドレス。
getInjector
javascript
function getInjector() external view returns (address);

現在設定されている情報提供者(injector)のアドレスを取得する関数です。 この関数を呼び出すと、今そのコントラクトに他のコントラクトの情報を伝える権限を持っているアドレスが返されます。

戻り値

  • address 現在の情報提供者のアドレス。

ストレージスロット仕様

Dependant コントラクトは、情報提供者(通常は ContractsRegistry)のアドレスを 特定のストレージスロットに保存しておくことができます。

このスロットは以下の式で定義されます。

text
0x3d1f25f1ac447e55e7fec744471c4dab1c6a2b6ffb897825f9ea3d2e8c9be583

この値は、以下のようにして導き出されます。

text
bytes32(uint256(keccak256("eip6224.dependant.slot")) - 1)

このように特定のスロットを指定しておくことで、他のストレージ変数と重ならず、 安全かつ標準化された形で情報提供者のアドレスを保持できます。

補足

ContractsRegistry に関する設計

コントラクト識別子

ContractsRegistry では、コントラクトを識別するために string 型の識別子を採用しています。 他の選択肢として uint256 や bytes32 も検討されましたが、以下の理由から文字列型が選ばれました。

項目 説明
可読性の重視 コントラクト名を文字列で扱うことで、人間が理解しやすくなります。特に、ブロックエクスプローラや DAO などを通じて直接操作する場合、名前で指定できる方が直感的です。
操作ミスの防止 数値やハッシュ値で扱うよりも、文字列名を使うことで人為的なミス(識別子の誤りなど)を減らせます。
ユーザー操作を意識した設計 ContractsRegistry はプロトコル全体の中心的なコントラクトであり、エンドユーザーや開発者が直接触る機会が多いため、使いやすさを最優先にしています。

一方で、この設計はガスコストの面では効率的ではありません。 しかし、設計方針として「ガス効率よりも明快さを優先する」ことが選ばれています。

また、string 型の採用に伴って、イベント(ContractAdded など)のパラメータは indexed にされていません。 もし string 型を indexed にすると、32バイトを超える長さの場合、自動的に keccak256 ハッシュ化されてしまい、イベントログから直接名前を読み取ることが難しくなります。 そのため、「検索性よりも人間が読めること」を優先して、あえて非indexedのままとされています。

リバート

getContract 関数は、指定されたコントラクトのアドレスが address(0)(未登録)だった場合にリバート(実行失敗)します。 これは、プロトコルの誤った初期化や未登録状態での利用を防ぐために必須の設計です。

レジストリの整合性を保つために、コントラクトが登録される順序にも意図があります。 すなわち、依存関係を設定する前に、必要な全コントラクトが ContractsRegistry に正しく登録されていなければなりません。

同様に、以下の関数も address(0) が入力された場合はリバートします。

関数名 理由
addContract 無効なアドレスを登録しようとした場合にエラーを出す。
addProxyContract 実装のアドレスが存在しない場合を防止する。
addProxyContractAndCall 初期化付き登録でも同様に安全性を確保する。
justAddProxyContract 既存のプロキシ登録時にも空アドレスを禁止する。

このように、すべての登録系関数でゼロアドレスを明示的に拒否することで、誤った登録やプロトコルの不整合を未然に防ぐ仕組みが作られています。

Dependant に関する設計

データパラメータ

Dependant コントラクトの setDependencies 関数には、data パラメータが存在します。 このパラメータは、アプリケーション固有の追加情報(コンテキスト)を渡すために用意されています。 これにより、コントラクトごとに異なる初期化や設定処理を行うことが可能です。

例えば以下のような挙動を実現できます。

  • あるコントラクトでは特定のロール設定やトークン情報を初期化時に渡す
  • あるモジュールでは外部サービスの設定データを一緒に受け取る

つまり、data パラメータは「標準的な情報伝達の仕組みを拡張するための柔軟なフック」として設計されています。

Injector

setInjector 関数は、他のコントラクトに情報を渡す権限を持つ「情報提供者(injector)」を設定するためのものです。 この関数は external として定義されており、外部から呼び出すことができます。 これは、ファクトリーコントラクト(Factory)によって生成されるコントラクトにも対応するための設計です。

ファクトリー経由で作成されたコントラクトは、デプロイ時に ContractsRegistry のアドレスを自動で受け取らない場合があります。 そのため、後から setInjector を使ってレジストリを設定できるようにする必要があります。

ただし、この関数は外部呼び出しが可能なため、アクセス制御を厳格に行うことが推奨されます。 誤って第三者に呼ばれると、レジストリをすり替えられるリスクがあるため、実装時には onlyOwner や onlyInjector のような修飾子を適用することが望まれます。

参考実装

ContractsRegistry

js
pragma solidity ^0.8.0;

import {Address} from "@openzeppelin/contracts/utils/Address.sol";
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

import {Dependant} from "./Dependant.sol";

interface IContractsRegistry {
    event ContractAdded(string name, address contractAddress);
    event ProxyContractAdded(
        string name,
        address contractAddress,
        address implementation
    );
    event ProxyContractUpgraded(string name, address newImplementation);
    event ContractRemoved(string name);

    function getContract(string memory name) external view returns (address);

    function hasContract(string memory name) external view returns (bool);

    function injectDependencies(string memory name) external;

    function injectDependenciesWithData(string memory name, bytes memory data)
        external;

    function upgradeContract(string memory name, address newImplementation)
        external;

    function upgradeContractAndCall(
        string memory name,
        address newImplementation,
        bytes memory data
    ) external;

    function addContract(string memory name, address contractAddress) external;

    function addProxyContract(string memory name, address contractAddress)
        external;

    function addProxyContractAndCall(
        string memory name,
        address contractAddress,
        bytes memory data
    ) external;

    function justAddProxyContract(string memory name, address contractAddress)
        external;

    function removeContract(string memory name) external;
}

contract ProxyUpgrader {
    using Address for address;

    address private immutable _OWNER;

    modifier onlyOwner() {
        _onlyOwner();
        _;
    }

    constructor() {
        _OWNER = msg.sender;
    }

    function upgrade(address what_, address to_, bytes calldata data_) external onlyOwner {
        if (data_.length > 0) {
            TransparentUpgradeableProxy(payable(what_)).upgradeToAndCall(to_, data_);
        } else {
            TransparentUpgradeableProxy(payable(what_)).upgradeTo(to_);
        }
    }

    function getImplementation(address what_) external view onlyOwner returns (address) {
        // bytes4(keccak256("implementation()")) == 0x5c60da1b
        (bool success_, bytes memory returndata_) = address(what_).staticcall(hex"5c60da1b");

        require(success_, "ProxyUpgrader: not a proxy");

        return abi.decode(returndata_, (address));
    }

    function _onlyOwner() internal view {
        require(_OWNER == msg.sender, "ProxyUpgrader: not an owner");
    }
}

contract ContractsRegistry is IContractsRegistry, OwnableUpgradeable {
    ProxyUpgrader private _proxyUpgrader;

    mapping(string => address) private _contracts;
    mapping(address => bool) private _isProxy;

    function __ContractsRegistry_init() public initializer {
        _proxyUpgrader = new ProxyUpgrader();

        __Ownable_init();
    }

    function getContract(string memory name_) public view returns (address) {
        address contractAddress_ = _contracts[name_];

        require(
            contractAddress_ != address(0),
            "ContractsRegistry: this mapping doesn't exist"
        );

        return contractAddress_;
    }

    function hasContract(string memory name_) public view returns (bool) {
        return _contracts[name_] != address(0);
    }

    function getProxyUpgrader() external view returns (address) {
        return address(_proxyUpgrader);
    }

    function injectDependencies(string memory name_) public virtual onlyOwner {
        injectDependenciesWithData(name_, bytes(""));
    }

    function injectDependenciesWithData(string memory name_, bytes memory data_)
        public
        virtual
        onlyOwner
    {
        address contractAddress_ = _contracts[name_];

        require(
            contractAddress_ != address(0),
            "ContractsRegistry: this mapping doesn't exist"
        );

        Dependant dependant_ = Dependant(contractAddress_);
        dependant_.setDependencies(address(this), data_);
    }

    function upgradeContract(string memory name_, address newImplementation_)
        public
        virtual
        onlyOwner
    {
        upgradeContractAndCall(name_, newImplementation_, bytes(""));
    }

    function upgradeContractAndCall(
        string memory name_,
        address newImplementation_,
        bytes memory data_
    ) public virtual onlyOwner {
        address contractToUpgrade_ = _contracts[name_];

        require(
            contractToUpgrade_ != address(0),
            "ContractsRegistry: this mapping doesn't exist"
        );
        require(
            _isProxy[contractToUpgrade_],
            "ContractsRegistry: not a proxy contract"
        );

        _proxyUpgrader.upgrade(contractToUpgrade_, newImplementation_, data_);

        emit ProxyContractUpgraded(name_, newImplementation_);
    }

    function addContract(string memory name_, address contractAddress_)
        public
        virtual
        onlyOwner
    {
        require(
            contractAddress_ != address(0),
            "ContractsRegistry: zero address is forbidden"
        );

        _contracts[name_] = contractAddress_;

        emit ContractAdded(name_, contractAddress_);
    }

    function addProxyContract(string memory name_, address contractAddress_)
        public
        virtual
        onlyOwner
    {
        addProxyContractAndCall(name_, contractAddress_, bytes(""));
    }

    function addProxyContractAndCall(
        string memory name_,
        address contractAddress_,
        bytes memory data_
    ) public virtual onlyOwner {
        require(
            contractAddress_ != address(0),
            "ContractsRegistry: zero address is forbidden"
        );

        address proxyAddr_ = _deployProxy(
            contractAddress_,
            address(_proxyUpgrader),
            data_
        );

        _contracts[name_] = proxyAddr_;
        _isProxy[proxyAddr_] = true;

        emit ProxyContractAdded(name_, proxyAddr_, contractAddress_);
    }

    function justAddProxyContract(string memory name_, address contractAddress_)
        public
        virtual
        onlyOwner
    {
        require(
            contractAddress_ != address(0),
            "ContractsRegistry: zero address is forbidden"
        );

        _contracts[name_] = contractAddress_;
        _isProxy[contractAddress_] = true;

        emit ProxyContractAdded(
            name_,
            contractAddress_,
            _proxyUpgrader.getImplementation(contractAddress_)
        );
    }

    function removeContract(string memory name_) public virtual onlyOwner {
        address contractAddress_ = _contracts[name_];

        require(
            contractAddress_ != address(0),
            "ContractsRegistry: this mapping doesn't exist"
        );

        delete _isProxy[contractAddress_];
        delete _contracts[name_];

        emit ContractRemoved(name_);
    }

    function _deployProxy(
        address contractAddress_,
        address admin_,
        bytes memory data_
    ) internal virtual returns (address) {
        return
            address(
                new TransparentUpgradeableProxy(contractAddress_, admin_, data_)
            );
    }
}

Dependant

js
pragma solidity ^0.8.0;

interface IDependant {
    function setDependencies(address contractsRegistry, bytes memory data) external;
 
    function setInjector(address injector) external;
 
    function getInjector() external view returns (address);
}

abstract contract Dependant is IDependant {
    /**
     * @dev bytes32(uint256(keccak256("eip6224.dependant.slot")) - 1)
     */
    bytes32 private constant _INJECTOR_SLOT =
        0x3d1f25f1ac447e55e7fec744471c4dab1c6a2b6ffb897825f9ea3d2e8c9be583;

    modifier dependant() {
        _checkInjector();
        _;
        _setInjector(msg.sender);
    }

    function setDependencies(address contractsRegistry_, bytes memory data_) public virtual;

    function setInjector(address injector_) external {
        _checkInjector();
        _setInjector(injector_);
    }

    function getInjector() public view returns (address injector_) {
        bytes32 slot_ = _INJECTOR_SLOT;

        assembly {
            injector_ := sload(slot_)
        }
    }

    function _setInjector(address injector_) internal {
        bytes32 slot_ = _INJECTOR_SLOT;

        assembly {
            sstore(slot_, injector_)
        }
    }

    function _checkInjector() internal view {
        address injector_ = getInjector();

        require(injector_ == address(0) || injector_ == msg.sender, "Dependant: not an injector");
    }
}

セキュリティ

全体的な注意点

ContractsRegistry のオーナー(管理者)は、自分の秘密鍵を安全に保管することが最も重要です。 なぜなら、オーナーの鍵が漏えいした場合や失われた場合、そのプロトコルの運用は事実上不可能になり、「ポイント・オブ・ノーリターン(取り返しのつかない状態)」に陥るからです。

ContractsRegistry はプロトコルのハブのような存在であり、すべてのコントラクトを統括する仕組みを持っています。 したがって、アクセス権限は完全に信頼できる当事者のみに限定し、マルチシグウォレットやハードウェアウォレットを利用するなど、堅牢な管理体制を整えることが強く推奨されます。

ContractsRegistry のセキュリティ

ContractsRegistry にはアップグレード機能がありますが、新しい実装と既存の実装の互換性を自動で検証する仕組みは存在しません。

つまり、upgradeContract や upgradeContractAndCall を使ってプロキシをアップグレードする時には、開発者またはオーナー自身が互換性を確認しなければならないということです。 ここで誤った実装を指定すると、プロトコル全体が破損したり、機能しなくなる可能性があります。

リスク 説明 対応策
不正な実装のアップグレード 新しい実装が旧バージョンと互換性を持たない場合、プロトコルが壊れる可能性がある。 コントラクトのストレージ構造や関数シグネチャを事前に検証する。
不正アクセスによるレジストリ操作 管理者権限を奪取されると、全てのコントラクトを差し替えられる可能性がある。 アクセス制御を厳格に設定し、署名管理を強化する。

このように、ContractsRegistry の安全性は単なるコード上の問題ではなく、オフチェーンでの権限管理(秘密鍵管理・マルチシグ設定など)と密接に関係しています。

Dependantのセキュリティ

Dependant コントラクトにおいても、依存関係を設定する際に特有のリスクがあります。 特に重要なのは、setDependencies 関数が最初に呼ばれるタイミングです。

仕様上、Dependant は最初の setDependencies 呼び出しが行われる前に、 「情報提供者(injector)」を設定する必要があります。 しかし、もしこの設定が遅れた場合、第三者が先回りして最初の呼び出しを行う可能性があります。 これは「フロントラン攻撃(front-running)」と呼ばれるリスクです。

項目 説明
フロントラン攻撃のリスク setDependencies の初回呼び出しが誰でも実行できる状態のまま放置されると、悪意のあるアドレスが injector を不正に設定できる。
影響範囲 一度不正な injector が設定されると、正しいレジストリと接続できなくなり、依存情報が偽装される恐れがある。
対策 コントラクトのデプロイ直後に、すぐに正しい injector を設定する。また、初回呼び出し前のアクセス制御を厳格に行う。

したがって、Dependant を安全に運用するには、デプロイ後すぐに injector を正しく設定して第三者が割り込めない状態を作ることが不可欠です。

引用

Artem Chystiakov (@arvolear), "ERC-6224: Contracts Dependencies Registry [DRAFT]," Ethereum Improvement Proposals, no. 6224, December 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6224.

最後に

今回は「分散型プロトコルの中で使われる複数のスマートコントラクトを一元管理し、必要に応じて安全かつ柔軟に入れ替え・更新できる仕組みを提案しているERC6224」についてまとめてきました! いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

他の媒体でも情報発信しているのでぜひ他も見ていってください!