はじめに

初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、ERC721とERC1155を使用して、ロールベースのアクセスコントロールを実装するコントロールトークンという仕組みを提案しているERC7303についてまとめていきます!

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

他にも様々なEIPについてまとめています。

過去に同じ記事を書いていたので、合わせて読んでいただけるとより理解が深まると思います...。

概要

このERC(イーサリアムの提案)は、「Token-Controlled Token Circulation(TCTC)」というアクセスコントロールスキームを導入しています。 これは、特定のロールに関連する権限をERC721やERC1155トークンで表現することにより(「コントロールトークン」と呼ばれる)、そのロールを与えたり取り消したりするプロセスを、対応するコントロールトークンの発行(mint)や焼却(burn)を通じて容易にするものです。

つまり、ロールと権限をデジタルトークンとして具現化し、それらのトークンの管理を通じて、誰が何の権限を持っているかを制御するということです。 ここで重要なのは2種類のトークン標準、ERC721とERC1155です。

  • ERC721
    • これは一般的に「ユニークな」アセットを表すのに使われ、NFT(ノンファンジブルトークン)としてよく知られています。
    • 各トークンはユニークで、交換可能ではありません。
    • このシステムでは、特定のロールや権限が一意に識別され、それに対応するトークンが発行されます。
  • ERC1155
    • これは「マルチトークン」標準で、1つのスマートコントラクトで複数種類のトークンを管理できます。
    • これにより、ファンジブル(交換可能な)トークンとノンファンジブルトークン(NFT)の両方をサポートします。
    • つまり、同一のロールや権限を持つ複数のエンティティが存在する場合に適しています。

ERC721については以下の記事を参考にしてください。

ERC1155については以下の記事を参考にしてください。

このTCTCスキームによって、権限の付与や剥奪はトークンの発行や焼却という形で行われるため、トランザクションがブロックチェーン上で透明かつ不可逆的に記録されます。 これは、従来のシステムよりも安全で効率的なアクセスコントロールを可能にします。

TCTCはデジタルトークンを使って、誰がどのようなアクセス権を持つかを管理する方法です。 ロールや権限をトークン化することで、これらの権限の管理が簡単かつ透明になります。

動機

この段落では、特権アクションに対するアクセスコントロールを実装するための多くの方法のうち、特に「ロールベース」のアクセスコントロールについて説明しています。 これはERC5982で定義されている方法で、ロールに基づいてユーザーに特定のアクセス権を付与する方式です。

ERC5982については以下の記事を参考にしてください。

しかし、この方式にはいくつかの欠点があります。

オフチェーン管理ツールの必要性

ロールを付与したり取り消したりするためには、外部の管理ツールを使う必要があります。 これはブロックチェーン外で操作を行うことを意味し、これにより追加の手間がかかるだけでなく、ブロックチェーンの持つ透明性やセキュリティの利点を完全には活用できないという問題があります。

ウォレットのUI問題

多くのウォレット(ブロックチェーンのアドレスを管理するアプリケーション)には、ユーザーが自分に付与された権限を理解しやすく表示するインターフェースがありません。 その結果、ユーザーは自分がどのような権限を持っているのか、簡単には確認できないという問題があります。

ロールベースのアクセスコントロールは便利な概念ですが、実際にそれを管理するためには追加のツールが必要であり、またユーザーが自分の権限状態を簡単に確認できないというユーザビリティの問題も抱えています。 これは、アクセスコントロールの実装方法を考える時に、改善の余地がある領域と言えます。

ユースケース

このERCは、特にERC5982で説明されているロールベースのアクセスコントロールを使用するさまざまなシナリオで適用可能です。 具体的な使用例は以下になります。

ミント/バーン権限

  • 応用例
    • チケット、クーポン、メンバーシップカード、サイトアクセス権などをトークンとして流通させるアプリケーション。
  • 必要性
    • これらのトークンを発行(mint)または消去(burn)する権限をシステム管理者に付与する必要があります。
  • 実現方法
    • このスキームでは、これらの権限をコントロールトークンとして実現します。

transfer権限

  • 応用例
    • 特定の機関のみにトークンのtransfer能力を制限したい場合。
  • 具体的な方法
    • 機関証明書をコントロールトークンとして発行します。
    • このコントロールトークンの所有権がトークンtransferを規制する手段となります。

アドレス検証

  • 応用例
    • トークンのmintやtransfer時に受取人のアドレスの誤りを防ぐためにアドレス検証が必要なアプリケーション。
  • 実現方法
    • ユーザーに対しアドレス検証の証としてコントロールトークンを発行します。
    • mintやtransferトランザクションを実行する時に受取人にこのコントロールトークンの提示を求めることで、誤配を防ぎます。
  • 特記事項
    • このアドレス検証用コントロールトークンは、政府機関や特定の企業が身元検証プロセスの後に発行する場合があります。

つまり、このERCは、特定の権限をデジタルトークン化することで、より透明性が高く、管理が容易なアクセスコントロールシステムを提供します。 mintやburnの権限、トークンのtransfer制限、アドレス検証など、様々な場面でこのシステムを活用することができます。 これにより、従来のロールベースのアクセスコントロールの問題点を解決し、より効率的で安全な方法を提供します。

仕様

この規格では、ロールに必要な権限をERC721トークンまたはERC1155トークンで表現する必要があります。 これらのトークンを「コントロールトークン」と呼び、ロールと権限の管理に使用されます。

コントロールトークン

  • 定義
    • ロールに必要な権限を表すトークン。
  • 種類
    • ERC721またはERC1155トークン。
  • 特徴
    • 他のコントロールトークンによってトランザクションが再帰的に制御される可能性がある。

ロールとの関連付け

既にデプロイされたコントロールトークンのコントラクトアドレスを使用して、必要なコントロールトークンをロール に関連付けます。

権限の確認

アカウントが必要なロールを持っているかどうかを確認するには、ERC721またはERC1155で定義されたbalanceOfメソッドを使用して、コントロールトークンの残高が0を超えることを確認します。 ERC1155トークンを使用する場合、balanceOfメソッドでtypeIdを指定する必要があります。

ロールの付与と剥奪

  • 付与
    • ERC5679で定義されたsafeMintメソッドを使用して、権限を表すコントロールトークンをアカウントにmintします。
  • 剥奪
    • ERC5679で定義されたburnメソッドを使用して、権限を表すコントロールトークンを焼却します。

ERC5679については以下の記事を参考にしてください。

ロールの表現

ロールはbytes32形式で表され、ロール名の文字列のkeccak256ハッシュ値として計算されることが推奨されます。 例えば、bytes32 role = keccak256("MINTER");のようになります。

ERC7303標準は、スマートコントラクトにおけるロールと権限の管理を、コントロールトークンを通じて行う方法を定めています。 これにより、権限の付与や剥奪がトークンのmintやburnを通じて行われ、権限管理の透明性と効率性が向上します。

補足

特権(権限)を管理するためにERC721またはERC1155トークンをコントロールトークンとして使用するメリットについて説明しています。 コントロールトークンとしてこれらのトークン標準を利用することで、ウォレット内での権限の可視性が高まり、ユーザーが自身の権限を簡単に管理できるようになります。

権限のトークン化

  • Soulbound Tokenの使用
    • 通常、権限をトークンとして実現する時には、Soulbound Token(例えば、ERC5192)のような仕様が使用されます。
    • Soulbound Tokenは、譲渡不可能なトークンを意味し、所有者が変わることはありません。
  • ERC721の選択
    • このERCでは、ERC5192がERC721を継承していることから、コントロールトークンとしてERC721を必須としています。

ERC5192については以下の記事を参考にしてください。

譲渡可能なコントロールトークン

  • ロールの委譲
    • 譲渡可能なコントロールトークンを使用することで、組織内での権限の委譲が必要な場合(例えば、組織内の権威者が交代したり休暇を取る場合)に、その権限を他のメンバーに移すことが可能になります。
  • アプリケーションのニーズに応じた選択
    • コントロールトークンを譲渡可能にするかどうかは、アプリケーションの具体的なニーズに応じて決定されます。

ERC721やERC1155をコントロールトークンとして使用することで、ユーザーが自身の特権をウォレットで簡単に確認し管理できるようになると同時に、権限の委譲が必要な場合には、譲渡可能なコントロールトークンを選択することで、そのようなシナリオにも対応できる柔軟性が提供されます。

互換性

このERCは、ERC721、ERC1155、ERC5679にそれぞれ対応するように設計されています。

実装

ERC7303は、アプリケーションにおけるTCTCアクセス制御の実装を容易にするための修飾子を提供しています。 この修飾子は、アカウントが必要なロールを所有しているかどうかをチェックします。 また、ERC7303は、指定されたアカウントに特定のロールを付与する関数も含んでいます。

ts
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";

abstract contract ERC7303 {
    struct ERC721Token {
        address contractId;
    }

    struct ERC1155Token {
        address contractId;
        uint256 typeId;
    }

    mapping (bytes32 => ERC721Token[]) private _ERC721_Contracts;
    mapping (bytes32 => ERC1155Token[]) private _ERC1155_Contracts;

    modifier onlyHasToken(bytes32 role, address account) {
        require(_checkHasToken(role, account), "ERC7303: not has a required token");
        _;
    }

    /**
     * @notice Grant a role to user who owns a control token specified by the ERC-721 contractId. 
     * Multiple calls are allowed, in this case the user must own at least one of the specified token.
     * @param role byte32 The role which you want to grant.
     * @param contractId address The address of contractId of which token the user required to own.
     */
    function _grantRoleByERC721(bytes32 role, address contractId) internal {
        require(
            IERC165(contractId).supportsInterface(type(IERC721).interfaceId),
            "ERC7303: provided contract does not support ERC721 interface"
        );
        _ERC721_Contracts[role].push(ERC721Token(contractId));
    }

    /**
     * @notice Grant a role to user who owns a control token specified by the ERC-1155 contractId. 
     * Multiple calls are allowed, in this case the user must own at least one of the specified token.
     * @param role byte32 The role which you want to grant.
     * @param contractId address The address of contractId of which token the user required to own.
     * @param typeId uint256 The token type id that the user required to own.
     */
    function _grantRoleByERC1155(bytes32 role, address contractId, uint256 typeId) internal {
        require(
            IERC165(contractId).supportsInterface(type(IERC1155).interfaceId),
            "ERC7303: provided contract does not support ERC1155 interface"
        );
        _ERC1155_Contracts[role].push(ERC1155Token(contractId, typeId));
    }

    function _checkHasToken(bytes32 role, address account) internal view returns (bool) {
        ERC721Token[] memory ERC721Tokens = _ERC721_Contracts[role];
        for (uint i = 0; i < ERC721Tokens.length; i++) {
            if (IERC721(ERC721Tokens[i].contractId).balanceOf(account) > 0) return true;
        }

        ERC1155Token[] memory ERC1155Tokens = _ERC1155_Contracts[role];
        for (uint i = 0; i < ERC1155Tokens.length; i++) {
            if (IERC1155(ERC1155Tokens[i].contractId).balanceOf(account, ERC1155Tokens[i].typeId) > 0) return true;
        }

        return false;
    }
}

ERC721Token

javascript
struct ERC721Token {
    address contractId;
}

概要

ERC721トークンの情報を格納する構造体。

詳細

この構造体は、ERC721トークンに関連するコントラクトのアドレスを保持します。 主に、特定のロールに対して付与されたERC721トークンの情報を管理するのに使用されます。

パラメータ

  • address contractId
    • ERC721トークンのコントラクトアドレス。

ERC1155Token

javascript
struct ERC1155Token {
    address contractId;
    uint256 typeId;
}

概要

ERC1155トークンの情報を格納する構造体。

詳細

この構造体は、ERC1155**トークンに関連するコントラクトのアドレスと、トークンの種類を示すtypeIdを保持します。これは、特定のロールに対して付与されたERC1155**トークンの詳細情報を管理するのに使用されます。

パラメータ

  • address contractId
    • ERC1155トークンのコントラクトアドレス。
  • uint256 typeId
    • トークンの種類を識別するID。

_ERC721_Contracts

javascript
mapping (bytes32 => ERC721Token[]) private _ERC721_Contracts;

概要

特定のロールに対して付与されたすべてのERC721トークンのコントラクトアドレスを管理するマッピング配列。

詳細

このマッピングは、ロール(bytes32型のキー)ごとに、そのロールに関連付けられたERC721トークンのリスト(ERC721Token[])を保持します。 アクセスコントロールの検証に使用され、特定のロールに対して付与されたERC721トークンの情報を管理します。

パラメータ

  • bytes32
    • ロールを示すユニークなキー。
  • ERC721Token[]
    • 特定のロールに対して付与されたERC721トークンのリスト。

_ERC1155_Contracts

javascript
mapping (bytes32 => ERC1155Token[]) private _ERC1155_Contracts;

概要

特定のロールに対して付与されたすべての*ERC1155**トークンのコントラクトアドレスとtypeIdを管理するマッピング配列。

詳細

このマッピングは、ロール(bytes32型のキー)ごとに、そのロールに関連付けられたERC1155トークンの詳細情報のリスト(ERC1155Token[])を保持します。 特定のロールに対して付与されたERC1155トークンの情報を管理し、アクセスコントロールの検証に使用されます。

パラメータ

  • bytes32
    • ロールを示すユニークなキー。
  • ERC1155Token[]
    • 特定のロールに対して付与されたERC1155トークンの詳細情報のリスト。

onlyHasToken

javascript
modifier onlyHasToken(bytes32 role, address account) {
    require(_checkHasToken(role, account), "ERC7303: not has a required token");
    _;
}

概要

onlyHasTokenは、特定のロールに対する権限を持っているかを検証する修飾子。

詳細

この修飾子は、関数が実行される前に特定のアカウントが指定されたロールに対する権限(コントロールトークンを持っているか)を持っているかどうかを検証します。 _checkHasToken関数を呼び出して検証を行い、必要なトークンを持っていない場合はエラーを発生させます。

パラメータ

  • bytes32 role
    • 検証対象のロール。
  • address account
    • 検証対象のアカウントアドレス。

_grantRoleByERC721

javascript
function _grantRoleByERC721(bytes32 role, address contractId) internal {
    require(
        IERC165(contractId).supportsInterface(type(IERC721).interfaceId),
        "ERC7303: provided contract does not support ERC721 interface"
    );
    _ERC721_Contracts[role].push(ERC721Token(contractId));
}

概要

ユーザーが指定したERC721コントラクトのコントロールトークンを所有している場合に、そのユーザーにロールを付与する関数。

詳細

この関数は、指定されたコントラクトがERC721インターフェースをサポートしていることを確認した上で、ロールに対応するERC721コントラクトのアドレスを内部のマッピングに追加します。 これにより、そのロールを持つためには、ユーザーが指定されたERC721トークンを少なくとも1つ所有している必要があることを示します。

引数

  • bytes32 role
    • 付与したいロール。
  • address contractId
    • ユーザーが所有する必要があるERC721トークンのコントラクトアドレス。

_grantRoleByERC1155

javascript
function _grantRoleByERC1155(bytes32 role, address contractId, uint256 typeId) internal {
    require(
        IERC165(contractId).supportsInterface(type(IERC1155).interfaceId),
        "ERC7303: provided contract does not support ERC1155 interface"
    );
    _ERC1155_Contracts[role].push(ERC1155Token(contractId, typeId));
}

概要

ユーザーが指定した*ERC1155**コントラクトのコントロールトークンを特定のtypeIdで所有している場合に、そのユーザーにロールを付与する関数。

詳細

この関数は、指定されたコントラクトがERC1155インターフェースをサポートしていることを確認した上で、ロールに対応するERC1155コントラクトのアドレスとtypeIdを内部のマッピングに追加します。 これにより、そのロールを持つためには、ユーザーが指定されたERC1155トークンの特定のtypeIdを少なくとも1つ所有している必要があることを示します。

引数

  • bytes32 role
    • 付与したいロール。
  • address contractId
    • ユーザーが所有する必要があるERC1155トークンのコントラクトアドレス。
  • uint256 typeId
    • ユーザーが所有する必要があるトークンのtypeId。

_checkHasToken

javascript
function _checkHasToken(bytes32 role, address account) internal view returns (bool) {
    ERC721Token[] memory ERC721Tokens = _ERC721_Contracts[role];
    for (uint i = 0; i < ERC721Tokens.length; i++) {
        if (IERC721(ERC721Tokens[i].contractId).balanceOf(account) > 0) return true;
    }

    ERC1155Token[] memory ERC1155Tokens = _ERC1155_Contracts[role];
    for (uint i = 0; i < ERC1155Tokens.length; i++) {
        if (IERC1155(ERC1155Tokens[i].contractId).balanceOf(account, ERC1155Tokens[i].typeId) > 0) return true;
    }

    return false;
}

概要

指定されたアカウントが特定のロールに必要なコントロールトークンを所有しているか確認する関数。

詳細

この関数は、指定されたロールに対応するERC721およびERC1155トークンのリストをチェックし、指定された

アカウントがこれらのトークンのいずれかを所有している(balanceOfが0より大きい)かどうかを確認します。 少なくとも1つのトークンを所有している場合はtrueを返し、そうでない場合はfalseを返します。

引数

  • bytes32 role
    • 確認したいロール。
  • address account
    • 確認対象のアカウントアドレス。

戻り値

  • bool
    • 指定されたアカウントがロールに必要なトークンを所有しているかどうか。

以下は、ERC721トークン内でERC7303を利用して「minter」と「burner」のロールを定義する簡単な例です。 これらのロールを持つアカウントは、ERC721またはERC1155コントロール・トークンを指定することで、新しいトークンの作成と既存のトークンの破棄が可能になります。

ts
// SPDX-License-Identifier: Apache-2.0

pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "./ERC7303.sol";

contract MyToken is ERC721, ERC7303 {
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE");

    constructor() ERC721("MyToken", "MTK") {
        // Specifies the deployed contractId of ERC721 control token.
        _grantRoleByERC721(MINTER_ROLE, 0x...);
        _grantRoleByERC721(BURNER_ROLE, 0x...);

        // Specifies the deployed contractId and typeId of ERC1155 control token.
        _grantRoleByERC1155(MINTER_ROLE, 0x..., ...);
        _grantRoleByERC1155(BURNER_ROLE, 0x..., ...);
    }

    function safeMint(address to, uint256 tokenId, string memory uri)
        public onlyHasToken(MINTER_ROLE, msg.sender)
    {
        _safeMint(to, tokenId);
    }

    function burn(uint256 tokenId) 
        public onlyHasToken(BURNER_ROLE, msg.sender) 
    {
        _burn(tokenId);
    }
}

セキュリティ

引用

Ko Fujimura (@kofujimura), "ERC-7303: Token-Controlled Token Circulation [DRAFT]," Ethereum Improvement Proposals, no. 7303, July 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7303.

最後に

今回は「ERC721とERC1155を使用して、ロールベースのアクセスコントロールを実装するコントロールトークンという仕組みを提案しているERC7303」についてまとめてきました! いかがだったでしょうか?

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

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