# [ERC7303] NFTを使用したアクセスコントロールの仕組みを理解しよう！

> このERC（イーサリアムの提案）は、「Token-Controlled Token Circulation（TCTC）」というアクセスコントロールスキームを導入しています。

- URL: https://cardene.dev/articles/1135a58ef498232b05ab
- 公開日: 2024-03-18
- 更新日: 2024-03-18
- トピック: Blockchain
- タグ: ethereum, solidity, smartcontract, ブロックチェーン

## はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

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

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

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

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

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4

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

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

## 概要

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

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

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

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

https://chaldene.net/erc721

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

https://chaldene.net/erc1155

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

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

## 動機

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

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

[\[ERC5982\] ロールベースのアクセス制御の仕組みを理解しよう！](/articles/6457bb501aa5ac1677af)

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

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

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

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

### ユースケース

この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`メソッドを使用して、権限を表すコントロールトークンを焼却します。

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

[\[EIP5679\] ERC20, ERC721, ERC1155のMintとBurnの実装を統一しよう！](/articles/3be55bed3c7793830513)

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

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

## 補足

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

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

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

[\[ERC5192\] NFTを送付できなくする『SBT』について理解しよう！](/articles/01b2463cb87f391498a8)

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

:::message
**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などからお気軽に質問してください！

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

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

https://chaldene.net/

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://cardene.notion.site/ERC-EIP-2a03fa3ea33d43baa9ed82288f98d4a9?pvs=4
