[ERC7644] 有効期限付きの名前をNFTに紐づける仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、名前レジストリを使用して、NFTに紐づく有効期限付きの一意の名前の登録と管理を行う仕組みを提案している**ERC7644**についてまとめていきます！

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

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

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

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

# 概要

この規格では、**ERC721**トークンに命名メカニズムを追加するインターフェースを提案しています。
これにより、各トークンに有効期限が設定された一意の名前をつけることができ、NFTコントラクトないので一意性が確保されます。

:::note info
NFTコントラクト内では、`tokenId`という値で一意性が担保されていますが、それと同じように人間が読みやすい名前をつけるという提案です。
:::

インターフェースには、名前の設定と割り当て、更新、照会を行う関数が含まれていて、設定された有効期限まで一意の名前が紐付きます。
名前の設定を担当するアドレスなどは、ユースケースやシナリオによって異なります。

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

https://chaldene.net/erc721

# 動機

EOAアドレスなどに一意の名前を付与する仕組みは**ENS**として存在します。
この規格では、同じような仕組みでNFTにユーザー名を関連づけることで、NFTをより識別しやすくなります。

### 直感性
現状のNFTでは`tokenId`を用いて識別していますが、これは直感的ではありません。
ここにユーザー名を組み込むことでよりNFTが管理しやすく使いやすくなります。

### ユーザー名エコノミー

ユーザー名の登録メカニズムにより、ID検証やソーシャルインタラクションなどのメリットが提供されます。

### NFTとの相乗効果

ユーザー名とNFTの融合により、ソーシャルインタラクションやパーソナライズされたデジタル資産など、新しい仕組みを実装しやすくなります。

# 仕組み

この規格に準拠するコントラクトは以下の機能を実装する必要があります。

```js
pragma solidity ^0.8.0;

/**
 * @title INameRegistry
 * @dev Interface for the NameRegistry smart contract.
 * This interface allows interaction with a NameRegistry, 
 * enabling the registration, management, and lookup of names 
 * with associated expiry dates tied to specific tokens.
 */
interface IERC7644 /* is IERC721 */ {

    /**
     * @dev Emitted when the name of a token is changed.
     * @param tokenId The token ID whose name is changed.
     * @param oldName The previous name of the token.
     * @param newName The new name assigned to the token.
     * @param expiryDate The expiry date of the new name registration.
     */
    event NameChanged(uint256 indexed tokenId, bytes32 oldName, bytes32 newName, uint256 expiryDate);

    /**
     * @dev Returns the name of the specified token, if the name has not expired.
     * @param tokenId The token ID to query for its name.
     * @return The name of the token, or an empty bytes32 if no name is set or it has expired.
     */
    function nameOf(uint256 tokenId) external view returns (bytes32);

    /**
     * @dev Returns the token ID associated with a given name, if the name registration has not expired.
     * @param _name The name to query for its associated token ID.
     * @return The token ID associated with the name, or zero if no token is found or the name has expired.
     */
    function tokenIdOf(bytes32 _name) external view returns (uint256);

    /**
     * @dev Allows a token owner to set or update the name of their token, subject to a duration for the name's validity.
     * @param tokenId The token ID whose name is to be set or updated.
     * @param _name The new name to assign to the token.
     * @param duration The duration in seconds for which the name is valid, starting from the time of calling this function.
     * Note: The name must be unique and not currently in use by an active (non-expired) registration.
     */
    function setName(uint256 tokenId, bytes32 _name, uint256 duration) external;

    /**
     * @dev Returns the tokenId and expiryDate for a given name, if the name registration has not expired.
     * @param _name The name to query for its associated token ID and expiry date.
     * @return tokenId The token ID associated with the name.
     * @return expiryDate The expiry date of the name registration.
     */
    function nameInfo(bytes32 _name) external view returns (uint256 tokenId, uint256 expiryDate);
	
}
```

### `NameChanged`

特定のトークンIDの名前が変更されたときに発行されるイベント。
変更前の名前、変更後の名前、新しい名前の有効期限が含まれます。

- **パラメータ**
    - `tokenId`
        - 名前が変更されたトークンID。
    - `oldName`
        - 変更前の名前。
    - `newName`
        - 新しい名前。
    - `expiryDate`
        - 新しい名前の登録の有効期限。

### `nameOf

指定されたトークンIDの名前を返す関数。

- **引数**
    - `tokenId`
        - 名前を取得したいトークンID。
- **戻り値**
    - トークンの名前（`bytes32`型）。
    - 名前が設定されていないか期限切れの場合は空の`bytes32`。

### `tokenIdOf`

指定された名前に関連付けられたトークンIDを返す関数。

- **引数**
    - `_name`
        - トークンIDを取得したい名前。
- **戻り値**
    - 名前に関連付けられたトークンID（`uint256`型）。名前が見つからないか期限切れの場合は`0`。

### `setName`

トークンの所有者がトークンの名前を設定または更新する関数。
この名前の有効期間を秒単位で指定します。
名前は一意で、現在有効な登録に使用されていない必要があります。

- **引数**
    - `tokenId`
        - 名前を設定したいトークンID。
    - `_name`
        - 新しい名前。
    - `duration`
        - 名前の有効期間（秒単位）。

### `nameInfo`

指定された名前に関連付けられたトークンIDと有効期限を返す関数。
名前が見つからないか期限切れの場合は`0`を返します。

- **引数**
    - `_name`
        - 情報を取得したい名前。
- **戻り値**
    - 名前に関連付けられたトークンID（`uint256`型）。
    - 名前の有効期限（`uint256`型）。

# 補足

## 名前の有効期限

ユーザー名に有効期限を設けることでいくつか利点が生まれます。
例えば、古くなったユーザー名や使用されていないユーザー名を開放することができます。
通常であれば、一度紐づいてしまうとそのNFTの保有者などが設定を変更しない限りその`tokenId`に紐づいてしまいます。
有効期限を設けることで、一定のタイミングで紐付きがなくなり、他のユーザーその名前を使用できるようになります。

## 名前の一意性

NFTに紐づける名前は一意である必要があります。
同じ名前が存在すると混乱を招いたり、不正が生まれる可能性があります。

:::note info
例えば、「**この名前のNFTが資金を引き出せる**」などの機能があった場合、他のNFTにも同じ名前を紐付けできてしまうため。
:::

## 名前登録システム

ユーザー名の登録システムを導入すると不正行為が防止されて命名リソースへの公平なアクセスが確保されます。
名前の解放を待つ予約と更新のメカニズムを実装することで、ユーザー名の独占を防ぎながら欲しているユーザー名を使用できます。

# 互換性

**ERC721**と完全に互換性があります。

# 参考実装

```js
pragma solidity ^0.8.0;

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

contract ERC7644 is ERC721 {
    event NameChanged(uint256 indexed tokenId, bytes32 oldName, bytes32 newName, uint256 expiryDate);

    struct NameRegistration {
        uint256 tokenId;
        uint256 expiryDate;
    }

    mapping(uint256 => bytes32) private _tokenNames;
    mapping(bytes32 => NameRegistration) private _nameRegistrations;
    mapping(uint256 => uint256) private _lastSetNameTime;

    uint256 public constant MAX_DURATION = 10 * 365 days;
    uint256 public constant MIN_SET_NAME_INTERVAL = 1 days;

    constructor() ERC721("Asd Token", "ASDT") {}

    function nameOf(uint256 tokenId) public view returns (bytes32) {
        if(_tokenNames[tokenId] != bytes32(0) && _nameRegistrations[_tokenNames[tokenId]].expiryDate > block.timestamp)
        {
            return _tokenNames[tokenId];
        }else{
            return bytes32(0);
        }
    }

    function tokenIdOf(bytes32 _name) public view returns (uint256) {
        require(_nameRegistrations[_name].expiryDate > block.timestamp, "NameRegistry: Name expired");
        if(_nameRegistrations[_name].tokenId > 0)
        {
            return _nameRegistrations[_name].tokenId;
        }else{
            return uint256(0);
        }
    }

    function setName(uint256 tokenId, bytes32 _name, uint256 duration) public {
        require(ownerOf(tokenId) == msg.sender, "NameRegistry: Caller is not the token owner");
        require(duration <= MAX_DURATION, "NameRegistry: Duration exceeds maximum limit");
        require(block.timestamp - _lastSetNameTime[tokenId] >= MIN_SET_NAME_INTERVAL, "NameRegistry: Minimum interval not met");
        require(tokenIdOf(_name) == uint256(0) || tokenIdOf(_name) == tokenId, "NameRegistry: Name already in use and not expired");

        bytes32 oldName = _tokenNames[tokenId];
        uint256 expiryDate = block.timestamp + duration;
        _setTokenName(tokenId, _name, expiryDate);

        emit NameChanged(tokenId, oldName, _name, expiryDate);

        _lastSetNameTime[tokenId] = block.timestamp;
    }

    function nameInfo(bytes32 _name) public view returns (uint256, uint256) {
        require(_nameRegistrations[_name].tokenId > 0 && _nameRegistrations[_name].expiryDate > block.timestamp, "NameRegistry: Name expired or does not exist");
        NameRegistration memory registration = _nameRegistrations[_name];
        return (registration.tokenId, registration.expiryDate);
    }

    function _setTokenName(uint256 tokenId, bytes32 _name, uint256 expiryDate) internal {
        _tokenNames[tokenId] = _name;
        _nameRegistrations[_name] = NameRegistration(tokenId, expiryDate);
    }
}
```

# セキュリティ

## 不正行為の防止とリソースの蓄積を制限

名前を設定できる最小の間隔（例えば、一度設定した名前の有効期限が切れてから、次の名前を設定できるまでの期間）と有効期限の最大期間の設定、連続した名前の登録を制限によりスパムや悪意のある攻撃を防ぎます。
これにより、名前の無期限独占を防ぎユーザーが目的の名前を取得しやすくします。

## ユーザー名の制限

インデックス作成とガス効率の向上のために、ユーザー名は3~32文字の長さに制限する必要があります・
この範囲に制限することで、ガスコストの向上と長すぎる名前管理の難しさから解放されます。
また、使用できる文字を[a-zA-Z0-9] の範囲に制限することで読みやすさが向上し、特殊文字の使用を制限して命名システムの悪用を防ぎます。

# 引用

Chen Liaoyuan (@chenly), "ERC-7644: ERC-721 Name Registry Extension [DRAFT]," Ethereum Improvement Proposals, no. 7644, March 2024. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7644.

# 最後に

今回は「名前レジストリを使用して、NFTに紐づく有効期限付きの一意の名前の登録と管理を行う仕組みを提案している**ERC7644**」についてまとめてきました！
いかがだったでしょうか？

質問などがある方は以下の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