[ERC7303] NFTの所有権に基づいたアクセス制御の仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、トークンの所有権に基づくアクセス制御方式を提案している規格である**ERC7303**についてまとめていきます！

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

https://eips.ethereum.org/EIPS/eip-7303

:::note warn
**7303**は現在（2023年10月12日）では「**Draft**」段階です。
:::

# 概要

ERCはイーサリアム（Ethereum）で使用されるトークンの規格を指します。
この特定のERC（Ethereum Request for Comments）は、アクセス制御の仕組みである「**Token-Controlled Token Circulation（TCTC）**」を導入しています。

TCTCは、特定のロールや権限を、**ERC721**や**ERC1155**トークンとして表現するアイデアです。
これらのトークンは「**制御トークン**」と呼ばれます。
この制御トークンを使うことで、ロールの付与や取り消しをシンプルに行うことができます。
具体的には、制御トークンの発行や焼却を通じて行います。

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

https://chaldene.net/erc721

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

https://chaldene.net/erc1155

:::note info
例えば、あるウェブサイトへのアクセス権を特定のユーザーに付与する場合、そのユーザーに特別な**ERC721**トークンを発行します。
そして、そのユーザーはその**ERC721**トークンを持っている限り、そのウェブサイトにアクセスできます。
逆に、アクセス権を取り消す場合は、その**ERC721**トークンを焼却することで、アクセス権を剥奪できます。
:::

この方法により、アクセス制御をブロックチェーン上で効率的に管理し、トランザクションとして記録できるようになります。
これは、トークンを使用して権限管理を透明かつ効率的に行う方法です。

# 動機

特権を管理する方法の一つとして、よく使われるのが「**ロールベースのアクセス制御**」です。
これは、特定のユーザーやアカウントが特定の「**ロール**」を持つかどうかに基づいて、権限を管理する方法です。
このアクセス制御方法についてのポイントを説明します。

### ロールベースのアクセス制御
ロールベースのアクセス制御は、異なるロール（例：管理者、モデレーター、一般ユーザー）が異なる特権を持つことを前提としています。
つまり、各ロールは特定の操作を実行できる特権を持っており、それに基づいてアクセスが管理されます。

### ERC-5982
このアクセス制御方法は、**ERC5982**という規格で詳細に指定されています。
**ERC5982**は、イーサリアム（Ethereum）のスマートコントラクトでロールベースのアクセス制御を実現するための標準仕様です。

:::note info
**ERC5982**については以下を参考にしてください。
:::

https://qiita.com/cardene/items/6457bb501aa5ac1677af

### オフチェーンの管理ツール
しかし、この方法を実装するには、ブロックチェーン以外の場所で、ユーザーやアカウントにロールを付与または取り消すための管理ツールが必要です。
これは、特別な操作を行うための設定を行うための外部ツールです。

### ウォレットの制約
さらに、多くのウォレットは、ユーザーにロールに基づく特権の状態を表示するための適切なユーザーインターフェースを提供していないことがあります。
そのため、ユーザーは自分の特権の設定を理解するのが難しいことがあります。

このアクセス制御方法は便利ですが、オフチェーンの管理ツールとウォレットの改善が求められています。
特に、ユーザーが自分の特権を簡単に理解し、管理できるようなインターフェースの提供が重要です。

## ユースケース

このERC（Ethereumの規格）は、**ERC5982**で説明されている「**ロールベースのアクセス制御**」が必要なシナリオで非常に有用です。
以下に、このERCの具体的な使用例を詳しく説明します。

### 発行/焼却の権限
アプリケーションがチケット、クーポン、会員カード、ウェブサイトのアクセス権などをトークンとして扱う場合、システム管理者にはこれらのトークンを発行または焼却（無効化）する権限が必要です。
このERCによれば、これらの権限は「**制御トークン**」として表現できます。
つまり、管理者は特別なトークンを持つことで、トークンの発行と焼却を行うことができます。

### 転送の権限
一部の場合、アプリケーション内で特定の機関にトークンの転送権限を制限したいことがあります。
このような場合、対象の機関に「**制御トークン**」が発行されます。
制御トークンを所有することで、トークンの転送を調整および制限することができます。

### アドレスの確認
トークンを発行または転送する時、受信者のアドレスが正確であることを確認する必要があります。
このERCでは、アドレス確認の証拠として「**制御トークン**」が発行されます。
ユーザーはこの制御トークンを持っており、トークンを発行または転送する時に受信者アドレスに必要となります。
これにより、誤った送信先へのトークン送付を防ぐことができます。
場合によっては、政府機関や特定の企業によって、アイデンティティ確認プロセスを経た後にこのアドレス確認用の制御トークンが発行されることもあります。

このERCは、特定のアクセス制御要件を満たすための柔軟な方法を提供し、トークンを使用してアクセスや特権を効果的に管理したい場合に役立ちます。
アクセス制御や特権管理の要件に応じて、制御トークンを活用できることが魅力です。

# 仕様

**ERC7303**規格を実装するスマートコントラクトは、ロールに必要な特権を**ERC721**トークンや**ERC1155**トークンとして表現する必要があります。
この特権を表すトークンは、このERCでは「**制御トークン**」と呼ばれています。
以下に、**ERC7303**の主要なポイントを説明します。

### 制御トークンの利用
ロールに必要な特権を示すために、**ERC721**トークンまたは**ERC1155**トークンを使用します。
これらのトークンは、特権を保持していることを示すために使われます。

### 制御トークンの種類
制御トークンは、どの種類のトークンでも構いません。
つまり、**ERC20**、**ERC721**、**ERC1155**など、さまざまな種類のトークンを利用できます。

### 再帰的な制御
トークンのトランザクションは、他の制御トークンによって再帰的に制御されることがあります。
つまり、ある制御トークンが別の制御トークンのトランザクションを管理することができます。

### ロールへの関連付け
特定のロールに必要な制御トークンを関連付けるには、その制御トークンのコントラクトが事前にデプロイされたアドレスを使用します。

### アカウントの役割確認
アカウントが特定のロールを持っているかどうかを確認するために、そのアカウントが保有する制御トークンの残高が0より大きいかどうかを確認します。
この確認には**ERC721**や**ERC1155**で定義された`balanceOf`メソッドを使用します。
**ERC1155**を使用する場合は、`typeId`も指定する必要があります。

### ロールの付与
ロールをアカウントに付与するには、特権を表す制御トークンをそのアカウントに対して発行（`mint`）します。
これには**ERC5679**で定義された`safeMint`メソッドを使用します。

### ロールの剥奪
ロールをアカウントから剥奪するには、特権を表す制御トークンを焼却（`burn`）します。
これには**ERC5679**で定義された`burn`メソッドを使用します。

### ロールの表現
スマートコントラクト内で役割は`bytes32`形式で表現されます。
ロール名を`keccak256`ハッシュとして計算し、その値をロールとして使用します。
例えば、`bytes32 role = keccak256("MINTER")`のようになります。

この**ERC7303**規格は、特権とロールの管理を効果的に実現するための強力な枠組みを提供します。
特権のトークン化とトークンを使用したアクセス制御を通じて、スマートコントラクトのセキュリティと機能性を向上させるのに役立ちます。

# 補足

**ERC721**や**ERC1155**トークンを特権の制御トークンとして使用することで、ウォレット内での特権がはっきりと表示され、ユーザーにとって特権の管理が簡単になります。

一般的に、特権をトークンとして表現する場合、Soulbound Token（例：**ERC5192**など）などの仕様が使用されます。
**ERC5192**は**ERC721**を継承しているため、このERCでは特権の制御トークンとして**ERC721**が選択されました。

転送可能な制御トークンを使用することで、ロールの委任が必要な場面に対応できます。
例えば、組織内で権限の移動が必要な場合、特権を別のメンバーに移譲することができます。
制御トークンを転送可能とするかどうかの決定は、アプリケーションの具体的な要件に合わせて行われます。

:::note info
**ERC721**や**ERC1155**トークンを特権の制御トークンとして採用することにより、特権の管理がユーザーフレンドリーになり、ウォレット内で特権が簡単に確認できるようになります。
また、転送可能な特権を持つ制御トークンを使用することで、権限の委任に柔軟に対応できます。
アプリケーションの具体的な要求に応じて、最適なトークンタイプを選択することが大切です。
:::

# 後方互換性

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

:::note info
**ERC5679**については以下を参考にしてください。
:::

https://qiita.com/cardene/items/3be55bed3c7793830513

# 参考実装

**ERC7303**は、アプリケーション内でTCTC（Token-Controlled Token Circulation）アクセス制御を実現するための便利なツールです。
このツールには、2つの主要な機能が含まれています。

### 修飾子（Modifier）
**ERC7303**は、アクセス制御を実行するための修飾子を提供します。
修飾子は、特定のアクションや関数が実行される前に、そのアクションや関数を呼び出しようとしているアカウントが、必要なロールを持っているかどうかを確認します。
ロールを持っていない場合、アクションや関数の実行が拒否されます。
これにより、アクセス制御が効果的に実現されます。

### ロール付与関数
**ERC7303**には、特定のロールを指定されたアカウントに付与するための関数も含まれています。
この関数を使用することで、必要な特権を持つアカウントに対して、ロールを付与することができます。
これにより、特別なアクションを実行できるアカウントを設定できます。

:::note info
**ERC7303**はアクセス制御を簡素化し、必要な特権を持つアカウントを確認し、必要に応じて特権を付与するためのツールとして役立ちます。
アプリケーション内でのアクセス管理を効率化し、セキュリティを強化するのに役立つものです。
:::

```javascript
// 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;
    }
}
```

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

```javascript
// 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);
    }
}
```

---

### `_ERC721_Contracts`

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

**概要**

**ERC721**トークンのコントラクト情報を格納する配列。

**詳細**

特定のロール（`bytes32`型のロール）に関連付けられた**ERC721**トークンのコントラクト情報を管理します。
**ERC721**トークンは、異なるコントラクトごとに存在し、各コントラクトにはアドレス（`address`型の契約ID）が関連付けられています。
このマッピングは、ロールごとに複数の**ERC721**トークン情報を保持できるように設計されています。

**パラメータ**

- `role`
    - ロールの識別子。
- `contractId`
    - **ERC721**トークンのコントラクトアドレス。

---

### `_ERC1155_Contracts`

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

**概要**

**ERC1155**トークンのコントラクト情報を格納する配列。

**詳細**

特定のロール（`bytes32`型のロール）に関連付けられた**ERC1155**トークンのコントラクト情報を管理します。
**ERC1155**トークンは、異なるコントラクトごとに存在し、各コントラクトにはアドレス（`address`型のコントラクトID）とトークンのタイプID（`uint256`型のタイプID）が関連付けられています。
このマッピングは、ロールごとに複数の**ERC1155**トークン情報を保持できるように設計されています。

**パラメータ**

- `role`
    - ロールの識別子。
- `contractId`
    - **ERC1155**トークンのコントラクトアドレス。
- `typeId`
    - **ERC1155**トークンのタイプID。

---

### `onlyHasToken`

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

**概要**

特定のアカウントが指定されたロールに必要なトークンを持っているか確認する修飾子。

**詳細**

あるアクションや関数が実行される前に、指定されたアカウントが必要なロールに必要なトークンを持っているかどうかを確認します。
トークンを持っていない場合、アクションや関数の実行は拒否されます。
これにより、アクセス制御が実現されます。

**パラメータ**

- `role`
    - ロールの識別子。
- `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_Contracts`マッピングに追加します。
複数回の呼び出しが許可されており、この場合、ユーザーは少なくとも指定されたトークンの1つを所有している必要があります。

**引数**

- `role`
    - 付与したいロール。
- `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**コントラクトのコントロールトークンを所有するユーザーにロールを付与する関数。

**詳細**

この関数は、指定されたコントラクトが**ERC1155**インターフェースをサポートしていることを確認し、指定されたロールに対応する**ERC1155**トークン情報を`_ERC1155_Contracts`マッピングに追加します。
複数回の呼び出しが許可されており、この場合、ユーザーは少なくとも指定されたトークンの1つを所有している必要があります。

**引数**

- `role`
    - 付与したいロール。
- `contractId`
    - ユーザーが所有する必要のあるトークンの**ERC1155**コントラクトのアドレス。
- `typeId`
    - ユーザーが所有する必要のあるトークンのタイプID。

---

### `_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**トークン情報を取得し、指定されたアカウントがそれらのトークンを所有しているかどうかを確認します。
アカウントが必要なトークンを持っている場合は`true`を返し、持っていない場合は`false`を返します。

**引数**

- `role`
    - 確認するロール。
- `account`
    - トークンの所有者のアカウントアドレス。

**戻り値**

- `bool`
    - 指定されたアカウントが必要なトークンを持っている場合は `true`、持っていない場合は `false`。

---

# セキュリティ考慮事項

議論の必要があります。

# 引用

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.

# 最後に

今回は「トークンの所有権に基づくアクセス制御方式を提案している規格である**ERC7303**」についてまとめてきました！
いかがだったでしょうか？

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

[Twitter @cardene777](https://twitter.com/cardene777)

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

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58