[ERC4834] Ethereumアドレスを簡単な名前に変換するENSに似た仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、Ethereumのアドレスを簡単な名前に変換する、ENSと似た目的を持った提案している規格である**ERC4834**についてまとめていきます！

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

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

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

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

# 概要

コントラクトを人間にとって親しみやすい名前で特定できるようにするための規格です。
これは**ERC137**、つまり「ENS（Ethereum Name Service）」と似た目的を持っています。
ENSは、複雑なイーサリアムのアドレスを簡単な名前に変換するシステムです。

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

https://qiita.com/cardene/items/17e6e1a452006f08a197

このEIPによって、ドメインと呼ばれる特定のコントラクトは、簡単な名前で識別できるようになります。
これは、難解なアドレスやコードの代わりに、覚えやすい名前でコントラクトにアクセスできることを意味します。

また、このシステムは複雑なアクセス制御と名前解決の機能を提供します。
これにより、ユーザーのロール（権限や役職）や条件に応じて、コントラクトとのやり取りが可能になります。
たとえば、特定のロールを持つユーザーだけが、`mint`などの特定の関数を実行できるような制御が設定できます。

:::note info
このEIPはイーサリアムのブロックチェーンをより使いやすく、セキュアなものにするための名前解決とアクセス制御システムを目指しています。
:::

# 動機

このテキストでは、イーサリアムのブロックチェーンにおける特定のEIP（Ethereum Improvement Proposal）が、従来の標準と比較してどのような利点を持っているかを説明しています。

このEIPの大きな利点は、名前解決のための最小限のインターフェースを提供し、標準化されたアクセス制御を導入し、シンプルなアーキテクチャを持つことです
ENS（Ethereum Name Service）と比較すると、ENSは便利ですがより複雑なアーキテクチャを持ち、標準化されたアクセス制御がないという点で異なります。

加えて、このEIPでは、すべてのドメイン（サブドメイン、トップレベルドメイン（TLD）、ルート自体を含む）が実際にドメインとして実装されています。
これにより、名前解決は簡単な反復アルゴリズムによって行われるようになり、これはDNS（Domain Name System）に似たものです。

:::note info
このEIPはイーサリアムのブロックチェーンでコントラクトの名前解決をより簡単かつ効率的に行う方法を提供します。
これにより、コントラクトの管理がよりシンプルで使いやすくなります。
また、アクセス制御が標準化されているため、セキュリティと運用管理が強化されます。
:::

# 仕様

## コントラクトインターフェース

```javascript
interface IDomain {
    /// @notice     Query if a domain has a subdomain with a given name
    /// @param      name The subdomain to query, in right to left order
    /// @return     `true` if the domain has a subdomain with the given name, `false` otherwise
    function hasDomain(string[] memory name) external view returns (bool);

    /// @notice     Fetch the subdomain with a given name
    /// @dev        This should revert if `hasDomain(name)` is `false`
    /// @param      name The subdomain to fetch, in right to left order
    /// @return     The subdomain with the given name
    function getDomain(string[] memory name) external view returns (address);
}
```

## 関数

### `hasDomain`

```javascript
function hasDomain(string[] memory name) external view returns (bool);
```

**概要**

ドメインが特定の名前のサブドメインを持っているか確認する関数。

**詳細**

この関数は、指定された名前のサブドメインがドメインに存在するかどうかを確認します。
サブドメインは右から左の順序で指定されます。

**引数**

- `name`
    - クエリするサブドメインの名前（右から左の順序）。

**戻り値**

- `bool`
    - ドメインが指定された名前のサブドメインを持っている場合は`true`、そうでない場合は`false`。

---

### `getDomain`

```javascript
function getDomain(string[] memory name) external view returns (address);
```

**概要**

特定の名前のサブドメインを取得する関数。

**詳細**

この関数は、`hasDomain(name)`が`false`の場合には失敗するべきです。サブドメインは右から左の順序で指定されます。

**引数**

- `name`
    - 取得するサブドメインの名前（右から左の順序）。

**戻り値**

- `address`
    - 指定された名前のサブドメイン。

---

## 名前解決

このテキストでは、イーサリアムのブロックチェーン上で動作するスマートコントラクトの中で、特定の名前（ドメイン名）を解決する方法について説明しています。
具体的には、**a.b.c**のような名前をどのようにして特定のアドレスに対応付けるかについての説明です。

まず、解決したい名前をピリオド（.）で区切って分割します。
例えば **a.b.c**は **["a", "b", "c"]** という配列になります。
次に、最初のドメインをルートドメイン（最上位のドメイン）とし、パス（途中経過を記録するリスト）を空に設定します。

その後、配列の最後の要素（この例では **c**）を取り出し、パスに追加します。
そして、`domain.hasDomain(path)`関数を使用して、現在のドメインがそのサブドメイン（ここでは **c**）を持っているかを確認します。
もし持っていなければ、名前解決は失敗となります。
持っていれば、`domain.getDomain(path)`関数を使って新しいドメイン（サブドメイン）を取得し、これを現在のドメインとして設定します。

このプロセスは、配列の要素がなくなるまで繰り返します。
つまり、一つずつ要素を取り出し、それが有効なサブドメインであるかを確認して次のサブドメインに進んでいきます。
このようにして、最終的に完全なドメイン名に対応するアドレスを得ることができます。

例として挙げられた `resolve`関数は、この名前解決プロセスを実装したSolidityのコードです。
この関数は、分割された名前の配列とルートドメインを受け取り、上記の手順に従って名前を解決し、最終的に対応するアドレスを返します。
このような仕組みにより、イーサリアムのブロックチェーン上で効率的に名前解決を行うことができます。

```javascript
function resolve(string[] calldata splitName, IDomain root) public view returns (address) {
    IDomain current = root;
    string[] memory path = [];
    for (uint i = splitName.length - 1; i >= 0; i--) {
        // Append to back of list
        path.push(splitName[i]);
        // Require that the current domain has a domain
        require(current.hasDomain(path), "Name resolution failed");
        // Resolve subdomain
        current = current.getDomain(path);
    }
    return current;
}
```

## 登録機能（オプション拡張）

```javascript
interface IDomainRegisterable is IDomain {
    //// Events
    
    /// @notice     Must be emitted when a new subdomain is created (e.g. through `createDomain`)
    /// @param      sender msg.sender for createDomain
    /// @param      name name for createDomain
    /// @param      subdomain subdomain in createDomain
    event SubdomainCreate(address indexed sender, string name, address subdomain);

    /// @notice     Must be emitted when the resolved address for a domain is changed (e.g. with `setDomain`)
    /// @param      sender msg.sender for setDomain
    /// @param      name name for setDomain
    /// @param      subdomain subdomain in setDomain
    /// @param      oldSubdomain the old subdomain
    event SubdomainUpdate(address indexed sender, string name, address subdomain, address oldSubdomain);

    /// @notice     Must be emitted when a domain is unmapped (e.g. with `deleteDomain`)
    /// @param      sender msg.sender for deleteDomain
    /// @param      name name for deleteDomain
    /// @param      subdomain the old subdomain
    event SubdomainDelete(address indexed sender, string name, address subdomain);

    //// CRUD
    
    /// @notice     Create a subdomain with a given name
    /// @dev        This should revert if `canCreateDomain(msg.sender, name, pointer)` is `false` or if the domain exists
    /// @param      name The subdomain name to be created
    /// @param      subdomain The subdomain to create
    function createDomain(string memory name, address subdomain) external payable;

    /// @notice     Update a subdomain with a given name
    /// @dev        This should revert if `canSetDomain(msg.sender, name, pointer)` is `false` of if the domain doesn't exist
    /// @param      name The subdomain name to be updated
    /// @param      subdomain The subdomain to set
    function setDomain(string memory name, address subdomain) external;

    /// @notice     Delete the subdomain with a given name
    /// @dev        This should revert if the domain doesn't exist or if `canDeleteDomain(msg.sender, name)` is `false`
    /// @param      name The subdomain to delete
    function deleteDomain(string memory name) external;


    //// Parent Domain Access Control

    /// @notice     Get if an account can create a subdomain with a given name
    /// @dev        This must return `false` if `hasDomain(name)` is `true`.
    /// @param      updater The account that may or may not be able to create/update a subdomain
    /// @param      name The subdomain name that would be created/updated
    /// @param      subdomain The subdomain that would be set
    /// @return     Whether an account can update or create the subdomain
    function canCreateDomain(address updater, string memory name, address subdomain) external view returns (bool);

    /// @notice     Get if an account can update or create a subdomain with a given name
    /// @dev        This must return `false` if `hasDomain(name)` is `false`.
    ///             If `getDomain(name)` is also a domain implementing the subdomain access control extension, this should return `false` if `getDomain(name).canMoveSubdomain(msg.sender, this, subdomain)` is `false`.
    /// @param      updater The account that may or may not be able to create/update a subdomain
    /// @param      name The subdomain name that would be created/updated
    /// @param      subdomain The subdomain that would be set
    /// @return     Whether an account can update or create the subdomain
    function canSetDomain(address updater, string memory name, address subdomain) external view returns (bool);

    /// @notice     Get if an account can delete the subdomain with a given name
    /// @dev        This must return `false` if `hasDomain(name)` is `false`.
    ///             If `getDomain(name)` is a domain implementing the subdomain access control extension, this should return `false` if `getDomain(name).canDeleteSubdomain(msg.sender, this, subdomain)` is `false`.
    /// @param      updater The account that may or may not be able to delete a subdomain
    /// @param      name The subdomain to delete
    /// @return     Whether an account can delete the subdomain
    function canDeleteDomain(address updater, string memory name) external view returns (bool);
}
```

## イベント

### `SubdomainCreate`

```javascript
event SubdomainCreate(address indexed sender, string name, address subdomain);
```

**概要**

新しいサブドメインが作成されたときに発行されるイベント。

**詳細**

このイベントは、例えば`createDomain`関数を通じて新しいサブドメインが作成された場合に発行されます。

**パラメータ**

- `sender`
    - `createDomain`を呼び出した際の送信者のアドレス。
- `name`
    - `createDomain`で使用される名前。
- `subdomain`
    - `createDomain`で作成されたサブドメインのアドレス。

---

### `SubdomainUpdate`

```javascript
event SubdomainUpdate(address indexed sender, string name, address subdomain, address oldSubdomain);
```

**概要**

ドメインの解決されたアドレスが変更されたときに発行されるイベント。

**詳細**

このイベントは、例えば`setDomain`を使用してドメインの解決されたアドレスが変更された場合に発行されます。

**パラメータ**

- `sender`
    - `setDomain`を呼び出した際の送信者のアドレス。
- `name`
    - `setDomain`で使用される名前。
- `subdomain`
    - `setDomain`で指定された新しいサブドメインのアドレス。
- `oldSubdomain`
    - 以前のサブドメインのアドレス。

---

### `SubdomainDelete`

```javascript
event SubdomainDelete(address indexed sender, string name, address subdomain);
```

**概要**

ドメインがアンマップ（削除）されたときに発行されるイベント。

**詳細**

このイベントは、例えば`deleteDomain`を使用してドメインがアンマップされた場合に発行されます。

**パラメータ**

- `sender`
    - `deleteDomain`を呼び出した際の送信者のアドレス。
- `name`
    - `deleteDomain`で使用される名前。
- `subdomain`
    - 削除されたサブドメインのアドレス。

---

## 関数

### `createDomain`

```javascript
function createDomain(string memory name, address subdomain) external payable;
```

**概要**

指定された名前でサブドメインを作成する関数。

**詳細**

この関数は、`canCreateDomain(msg.sender, name, pointer)`が`false`である場合、またはドメインが既に存在する場合には失敗します。

**引数**

- `name`
    - 作成するサブドメインの名前。
- `subdomain`
    - 作成するサブドメイン。

---

### `setDomain`

```javascript
function setDomain(string memory name, address subdomain) external;
```

**概要**

指定された名前のサブドメインを更新する関数。

**詳細**

この関数は、`canSetDomain(msg.sender, name, pointer)`が`false`である場合、またはドメインが存在しない場合には失敗します。

**引数**

- `name`
    - 更新するサブドメインの名前。
- `subdomain`
    - 設定するサブドメイン。

---

### `deleteDomain`

```javascript
function deleteDomain(string memory name) external;
```

**概要**

指定された名前のサブドメインを削除する関数。

**詳細**

この関数は、ドメインが存在しない場合や`canDeleteDomain(msg.sender, name)`が`false`である場合には失敗します。

**引数**

- `name`
    - 削除するサブドメインの名前。

---

### `canCreateDomain`

```javascript
function canCreateDomain(address updater, string memory name, address subdomain) external view returns (bool);
```

**概要**

アカウントが指定された名前でサブドメインを作成できるか取得する関数。

**詳細**

この関数は、`hasDomain(name)`が`true`である場合に`false`を返さなければなりません。

**引数**

- `updater`
    - サブドメインを作成/更新する可能性があるアカウント。
- `name`
    - 作成/更新されるサブドメインの名前。
- `subdomain`
    - 設定されるサブドメイン。

**戻り値**

- `bool`
    - アカウントがサブドメインを更新または作成できるかどうか。

---

### `canSetDomain`

```javascript
function canSetDomain(address updater, string memory name, address subdomain) external view returns (bool);
```

**概要**

アカウントが指定された名前でサブドメインを更新または作成できるか取得する関数。

**詳細**

この関数は、`hasDomain(name)`が`false`である場合に`false`を返さなければなりません。
また、`getDomain(name)`がサブドメインアクセス制御拡張を実装しているドメインである場合、`getDomain(name).canMoveSubdomain(msg.sender, this, subdomain)`が`false`であれば`false`を返します。

**引数**

- `updater`
    - サブドメインを作成/更新する可能性があるアカウント。
- `name`
    - 作成/更新されるサブドメインの名前。
- `subdomain`
    - 設定されるサブドメイン。

**戻り値**

- `bool`
    - アカウントがサブドメインを更新または作成できるかどうか。

---

### `canDeleteDomain`

```javascript
function canDeleteDomain(address updater, string memory name) external view returns (bool);
```

**概要**

アカウントが指定された名前のサブドメインを削除できるか取得する関数。

**詳細**

この関数は、`hasDomain(name)`が`false`である場合に`false`を返さなければなりません。
また、`getDomain(name)`がサブドメインアクセス制御拡張を実装しているドメインである場合、`getDomain(name).canDeleteSubdomain(msg.sender, this, subdomain)`が`false`であれば`false`を返します。

**引数**

- `updater`
    - サブドメインを削除する可能性があるアカウント。
- `name`
    - 削除されるサブドメインの名前。

**戻り値**

- `bool`
    - アカウントがサブドメインを削除できるかどうか。

---

## 列挙機能（オプション拡張）

```javascript
interface IDomainEnumerable is IDomain {
    /// @notice     Query all subdomains. Must revert if the number of domains is unknown or infinite.
    /// @return     The subdomain with the given index.
    function subdomainByIndex(uint256 index) external view returns (string memory);
    
    /// @notice     Get the total number of subdomains. Must revert if the number of domains is unknown or infinite.
    /// @return     The total number of subdomains.
    function totalSubdomains() external view returns (uint256);
}
```

### `subdomainByIndex`

```javascript
function subdomainByIndex(uint256 index) external view returns (string memory);
```

**概要**

指定されたインデックスにあるサブドメインを照会する関数。

**詳細**

この関数は、指定されたインデックスに対応するサブドメインを返します。
ドメインの数が未知または無限である場合、この関数は失敗する必要があります。

**引数**

- `index`
    - 照会するサブドメインのインデックス。

**戻り値**

- `string memory`
    - 指定されたインデックスのサブドメイン。

---

### `totalSubdomains`

```javascript
function totalSubdomains() external view returns (uint256);
```

**概要**

全てのサブドメインの総数を取得する関数。

**詳細**

この関数は、サブドメインの総数を返します。
ドメインの数が未知または無限である場合、この関数は失敗する必要があります。

**戻り値**

- `uint256`
    - サブドメインの総数。

---

## アクセスコントロール（オプション拡張）

```javascript
interface IDomainAccessControl is IDomain {
    /// @notice     Get if an account can move the subdomain away from the current domain
    /// @dev        May be called by `canSetDomain` of the parent domain - implement access control here!!!
    /// @param      updater The account that may be moving the subdomain
    /// @param      name The subdomain name
    /// @param      parent The parent domain
    /// @param      newSubdomain The domain that will be set next
    /// @return     Whether an account can update the subdomain
    function canMoveSubdomain(address updater, string memory name, IDomain parent, address newSubdomain) external view returns (bool);

    /// @notice     Get if an account can unset this domain as a subdomain
    /// @dev        May be called by `canDeleteDomain` of the parent domain - implement access control here!!!
    /// @param      updater The account that may or may not be able to delete a subdomain
    /// @param      name The subdomain to delete
    /// @param      parent The parent domain
    /// @return     Whether an account can delete the subdomain
    function canDeleteSubdomain(address updater, string memory name, IDomain parent) external view returns (bool);
}
```

---

### `canMoveSubdomain`

```javascript
function canMoveSubdomain(address updater, string memory name, IDomain parent, address newSubdomain) external view returns (bool);
```

**概要**

アカウントが現在のドメインからサブドメインを移動できるか取得する関数。

**詳細**

この関数は、あるアカウントが特定のサブドメインを新しいドメインに移動できるかどうかを判断します。
これは、親ドメインの`canSetDomain`から呼び出される可能性があり、アクセス制御をここで実装する必要があります。

**引数**

- `updater`
    - サブドメインを移動する可能性があるアカウント。
- `name`
    - サブドメインの名前。
- `parent`
    - 親ドメイン。
- `newSubdomain`
    - 次に設定されるドメイン。

**戻り値**

- `bool`
    - アカウントがサブドメインを更新できるかどうか。

---

### `canDeleteSubdomain`

```javascript
function canDeleteSubdomain(address updater, string memory name, IDomain parent) external view returns (bool);
```

**概要**

アカウントがこのドメインをサブドメインとして解除できるか取得する関数。

**詳細**

この関数は、あるアカウントが特定のサブドメインを削除（解除）できるかどうかを判断します。
これは、親ドメインの`canDeleteDomain`から呼び出される可能性があり、アクセス制御をここで実装する必要があります。

**引数**

- `updater`
    - サブドメインを削除する可能性があるアカウント。
- `name`
    - 削除するサブドメインの名前。
- `parent`
    - 親ドメイン。

**戻り値**

- `bool`
    - アカウントがサブドメインを削除できるかどうか。

---

# 補足

このEIPの主な目的は、名前解決のための簡単で使いやすいインターフェースを提供することです。

## 名前解決アルゴリズムの特徴
このEIPにおける名前解決は、ENS（Ethereum Name Service）とは異なり、解決過程にあるコントラクトによって完全にコントロールされます。
この方法はユーザーにとって理解しやすいです。
このアプローチにより、例えば時間帯に応じて解決先を変更するなど、より柔軟な設計が可能になります。

## 親ドメインのアクセス制御
一般的な「`ownable`」インターフェースは使用されず、より汎用的な設計が採用されています。
もし特定の「`ownable`」実装が必要な場合、それは別途実装可能です。
これにより親ドメインは、サブドメインのアクセス制御メソッドを呼び出すことができ、サブドメインも自由にアクセス制御メカニズムを選択できます。

## サブドメインのアクセス制御
サブドメインは、必ずしも親ドメインのアクセス制御に従う必要はなく、これに対するメソッドが提供されています。
例えば、ルートドメインは譲渡不可能なトークンを持つDAOによって管理されることがあり、TLD（トップレベルドメイン）は持ち株を表すトークンを持つDAOによって管理されることがあります。
TLDの下のドメインは単一のオーナーによって、またそのサブドメインはNFTにリンクされた単一のオーナーによって管理されることもあります。
サブドメインのアクセス制御機能は、例えば鍵が失われた場合にサブドメインを回復するためのオーナーオーバーライドを含む提案です。

:::note info
このEIPは、ブロックチェーン上でのドメインとサブドメインの管理において、より柔軟で直感的なアクセス制御と名前解決の方法を提供することを目指しています。
:::

# 後方互換性

このEIPは、非常に汎用的な設計がされているため、ENSのような特定のシステムもサポートできるほど広範囲に対応しています。
一方で、ENSはこのEIPが提供するほどの幅広い機能や柔軟性を持っていないです。
つまり、このEIPはENSよりも多様な用途や機能に対応できるように作られています。

:::note info
このEIPはENSを含む広い範囲のシステムやニーズに適応できる一方で、ENS自体はこのEIPのような広範な用途には対応できない、ということを意味しています。
:::

# セキュリティ考慮事項

## 悪意のある`canMoveSubdomain`（ブラックホール）

### 説明
`canMoveSubdomain`は、サブドメインを`setDomain`関数を使って移動する際の潜在的な危険性に関するものです。
親ドメインの実装次第で、新しいサブドメインが`canMoveSubdomain`で意図せず`false`を返した場合、そのサブドメインはドメインの所有権を事実上固定することができます。
また、期待に反して`true`を返す（バックドアのように）、コントラクトのオーナーがドメインを掌握する可能性もあります。

### 対策
クライアントは、新しいサブドメインの`canMoveSubdomain`や`canDeleteSubdomain`が`false`に変わる場合に警告を出すことが望ましいです。
これらの関数は、状況によって結果が変わる可能性があるため、`setDomain`を実行する前に新しいサブドメインのソースコードの監査が推奨されます。

## 親ドメインの名前解決
### 説明
親ドメインはサブドメインの名前解決に完全なコントロールを持ちます。
例えば、特定のドメインが**a.b.c**にリンクされている場合、**b.c**は、そのコードによって**a.b.c**を任意のドメインに設定することができ、**c**は**b.c**自体を任意のドメインに設定できます。

### 対策
既にリンクされているドメインを取得する前に、関連するコントラクトとその親ドメイン（ルートに至るまで）の監査を行うことが推奨されます。

これらの問題は、ブロックチェーン上でのドメイン操作を行う際の重要な安全上の考慮事項です。
ユーザーは、これらのリスクを理解し、適切な監査を通じて対策を講じる必要があります。

# 引用

Gavin John (@Pandapip1), "ERC-4834: Hierarchical Domains," Ethereum Improvement Proposals, no. 4834, February 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4834.

# 最後に

今回は「Ethereumのアドレスを簡単な名前に変換する、ENSと似た目的を持った提案している規格である**ERC4834**」についてまとめてきました！
いかがだったでしょうか？

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

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

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

https://chaldene.net/

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58


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