[ERC6066] NFTによる署名検証の仕組みを理解しよう！

# はじめに

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

https://cryptogames.co.jp/

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

https://cryptospells.jp/

今回は、**ERC721**や**ERC1155**などのNFTによる署名検証の仕組みを提案している規格である**ERC6066**についてまとめていきます！

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

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

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

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

# 概要

現在のブロックチェーンでは、個人が所有するアカウント（外部所有アカウント、EOA）は`ecrecover()`という関数を使って、署名されたメッセージを検証することができます。
一方で、スマートコントラクトは**ERC1271**の規格に基づいて署名を検証できます。
しかし、NFT（非代替性トークン）が作成または検証する署名については、まだ標準化された方法がありません。

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

https://chaldene.net/erc721

そこで、NFTによる署名の有効性を誰もが検証できる新しい標準方法を提案します。
これは**ERC1271**の署名検証機能をアレンジしたものです。
具体的には、`isValidSignature(tokenId, hash, data)`という関数を使います。
これにより、以下のようにNFTの署名を検証できます。

- `tokenId`
    - 署名をしたNFTの識別ID。
- `hash`
    - 署名されたメッセージのハッシュ値。
- `data`
    - 署名そのもの。

この関数を使うことで、NFTによる署名が本物かどうかを簡単に確かめられるようになります。
これにより、NFTの使用範囲や機能が広がることが期待されます。

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

https://qiita.com/cardene/items/da237b1b8cc185491512

# 動機

最近、非代替性トークン（NFT）が非常に人気になっています。
NFTは、それぞれが独特で交換不可能なデジタルアイテムを表すトークンです。
これまでNFTは主にデジタルアートやプロフィール画像などを代表するために使われてきました。
この使用例は**ERC721**や**ERC1155**といったトークン標準にとって重要ですが、NFTには他にも多くの使い道があります。

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

https://chaldene.net/erc1155

1つの例として、NFTを使って組織内のオフィスや職位を表すことができます。
たとえば、分散型自律組織（DAO）がCEOやCOOなどの管理職を表すバッジとしてNFTを使うことが考えられます。
このシステムでは、四半期ごとに民主的な選挙が行われ、管理職の人が交代する可能性があります。

この場合の問題点は、現職のCOOが行った契約や承認などの署名が、新しいCOOに交代した後も前のCOOのEOA（外部所有アカウント）に残ってしまうことです。
この問題を解決する一つの方法は、DAO全体がマルチシグウォレットを使うことですが、より詳細な責任の分離が必要な場合もあります。
COOとしてスマートコントラクトを任命することも可能ですが、これは複雑になります。
ENS（イーサリアムネームサービス）を使って組織の階層構造を確立しているDAOでは、この提案により、包装されたENSサブドメイン（これもNFTです）が署名を生成できるようになるかもしれません。
これにより、組織内の特定のオフィスやロールに紐付けられた署名を生成し、管理することが可能になります。

# 仕様

```javascript
pragma solidity ^0.8.0;

interface IERC6066 {
    /**
     * @dev MUST return if the signature provided is valid for the provided tokenId and hash
     * @param tokenId   Token ID of the signing NFT
     * @param hash      Hash of the data to be signed
     * @param data      OPTIONAL arbitrary data that may aid verification
     *
     * MUST return the bytes4 magic value 0x12edb34f when function passes.
     * MUST NOT modify state (using STATICCALL for solc < 0.5, view modifier for solc > 0.5)
     * MUST allow external calls
     *
     */
    function isValidSignature(
        uint256 tokenId,
        bytes32 hash,
        bytes calldata data
    ) external view returns (bytes4 magicValue);
}
```

**IERC6066**は、NFT（非代替性トークン）が署名したメッセージの有効性を確認するための特別なインターフェースです。
このインターフェースでは、`isValidSignature`という関数を使ってNFTによる署名が正しいかどうかを判定します。
この関数は、**ERC721**や**ERC1155**に準拠したコントラクトで使用されることを想定しており、NFTの所有者が自分のNFTを使ってメッセージに署名することを可能にします。

`isValidSignature`関数は、以下の情報を必要とします。
- `tokenId`
    - 署名するNFTのID。
- `hash`
    - 署名されるデータのハッシュ値。
- `data`
    - 署名を検証するのに役立つ追加データ（必須ではありません）。

この関数は、特定の`tokenId`と`hash`に対する署名が正しいかどうかを判断し、成功した場合には`0x12edb34f`という特定の値を返します。
重要なのは、この関数は状態を変更せず外部からの呼び出しが可能であることです。
また、`view`修飾子を使っているので、データの読み取り専用であることが保証されています。

**IERC6066**に準拠するコントラクトは、この`isValidSignature`関数を実装して、NFT所有者が自分のNFTを使ってメッセージに署名する機能を提供できます。
そして、署名をサポートしたいコントラクトは、署名者が**ERC721**や**ERC1155**のNFTを持っている場合に、このメソッドを呼び出す必要があります。
これにより、NFTを使った署名の正当性を確認することが可能になります。

# 補足

この提案では、署名を生成する時の決まった方法（標準）を設けていません。
その理由は、署名の仕組みに柔軟性を持たせたいためです。
これは、スマートコントラクトの署名方法を定めない**ERC1271**の考え方に似ています。
さらに、この提案はシンプルかつ効果的であると証明されているGnosis Safeのコントラクト署名方法を参考にしています。

もし署名の検証に追加データが必要な場合、`bytes calldata data`というパラメータを使うことができますが、これは必須ではなく任意です。
この点も、将来の互換性を考慮し**ERC5750**に適合しているためです。

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

https://qiita.com/cardene/items/216265154b75c1612904

:::note info
この提案は、署名生成に厳しい規則を設けず、Gnosis Safeのような単純で効率的な方法を採用しています。
そして、署名の検証に追加情報が必要な場合だけ、その情報を含めることができるようになっています。
これにより、将来の技術の進歩にも柔軟に対応することが可能になります。
:::

# 後方互換性

このEIP（イーサリアム改善提案）は、従来の署名検証方法とは違うアプローチを取っています。
この提案では、複雑な暗号技術を用いて生成された署名を検証するのではなく、署名をもっとシンプルな形、つまり「**同意**」を示すブール値（真または偽を表す値）のフラグとして使用します。
この方式は、Gnosis Safeというコントラクトで使われている方法と一致しています。

:::note info
このEIPでは、署名が「**はい**」または「**いいえ**」のような単純な同意を示すために使われます。
これにより、署名が「**同意あり**」か「**同意なし**」かを示すための簡単な方法として機能します。
従来のように複雑な暗号技術に依存するのではなく、このようなシンプルな形式を取ることで、署名のプロセスをより手軽で分かりやすいものにしています。
これは、Gnosis Safeのコントラクトで採用されている実績のあるアプローチです。
:::

# 参考実装

**ERC721**に準拠し、**ERC6066**に準拠したカスタム署名関数の実装例は以下になります。

```javascript
pragma solidity ^0.8.0;

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

contract ERC6066Reference is ERC721, IERC6066 {
    // type(IERC6066).interfaceId
    bytes4 public constant MAGICVALUE = 0x12edb34f;
    bytes4 public constant BADVALUE = 0xffffffff;

    mapping(uint256 => mapping(bytes32 => bool)) internal _signatures;

    error ENotTokenOwner();

    /**
     * @dev Checks if the sender owns NFT with ID tokenId
     * @param tokenId   Token ID of the signing NFT
     */
    modifier onlyTokenOwner(uint256 tokenId) {
        if (ownerOf(tokenId) != _msgSender()) revert ENotTokenOwner();
        _;
    }

    constructor(string memory name_, string memory symbol_)
        ERC721(name_, symbol_)
    {}

    /**
     * @dev SHOULD sign the provided hash with NFT of tokenId given sender owns said NFT
     * @param tokenId   Token ID of the signing NFT
     * @param hash      Hash of the data to be signed
     */
    function sign(uint256 tokenId, bytes32 hash)
        external
        onlyTokenOwner(tokenId)
    {
        _signatures[tokenId][hash] = true;
    }

    /**
     * @dev MUST return if the signature provided is valid for the provided tokenId, hash, and optionally data
     */
    function isValidSignature(uint256 tokenId, bytes32 hash, bytes calldata data)
        external
        view
        override
        returns (bytes4 magicValue)
    {
        // The data parameter is unused in this example
        return _signatures[tokenId][hash] ? MAGICVALUE : BADVALUE;
    }

    /**
     * @dev ERC-165 support
     */
    function supportsInterface(
        bytes4 interfaceId
    ) public view virtual override returns (bool) {
        return
            interfaceId == type(IERC6066).interfaceId ||
            super.supportsInterface(interfaceId);
    }
}
```

---

### MAGICVALUE

```javascript
bytes4 public constant MAGICVALUE = 0x12edb34f;
```

**概要**
署名が有効であることを示すために使用される固定されたバイト値。

**詳細**
この値は`isValidSignature`関数が成功した際に返されるべき値です。
固定されたこのバイト値が返されることで、署名が正しいと認識されます。

---

### BADVALUE

```javascript
bytes4 public constant BADVALUE = 0xffffffff;
```

**概要**
署名が無効であることを示すために使用される固定されたバイト値。

**詳細**
この値は、署名が無効であることを示すために使用されます。
`isValidSignature`関数が失敗した際にこの値が返されることが想定されています。

---

### _signatures

```javascript
mapping(uint256 => mapping(bytes32 => bool)) internal _signatures;
```

**概要**
特定のNFTに関連する署名の有効性を追跡するための内部マッピング配列。

**詳細**
このマッピングは、トークンIDとデータのハッシュをキーとして使用し、その署名が有効かどうかをブール値で保存します。

---

### ENotTokenOwner

```javascript
error ENotTokenOwner();
```

**概要**
トークンの所有者でない場合に発生するエラー。

**詳細**
このエラーは、特定のアクションを実行しようとしたユーザーがトークンの所有者でない場合に使用されます。

---

### onlyTokenOwner

```javascript
modifier onlyTokenOwner(uint256 tokenId) {
    if (ownerOf(tokenId) != _msgSender()) revert ENotTokenOwner();
    _;
}
```

**概要**
特定の関数が特定のトークンの所有者によってのみ呼び出されることを保証する修飾子。

**詳細**
この修飾子は、関数の実行前にトークンの所有者をチェックします。
もし呼び出し元がトークンの所有者でなければ、`ENotTokenOwner`エラーが発生し、関数の実行が中止されます。

**パラメータ**
- `tokenId`: 確認するトークンのIDです。

---

### `sign`

```javascript
function sign(uint256 tokenId, bytes32 hash)
    external
    onlyTokenOwner(tokenId)
{
    _signatures[tokenId][hash] = true;
}
```

**概要**
指定されたトークンIDのNFTによってデータのハッシュを署名する関数。

**詳細**
この関数は、トークンの所有者のみが呼び出すことができるように`onlyTokenOwner`修飾子を使用しています。
署名プロセスは、トークンIDとハッシュをキーとして使い、内部の`_signatures`マッピングにその組み合わせの署名の有効性を`true`として記録します。

**引数**
- `tokenId`
    - 署名するNFTのトークンID。
- `hash`
    - 署名されるデータのハッシュ。

---

### `isValidSignature`

```javascript
function isValidSignature(uint256 tokenId, bytes32 hash, bytes calldata data)
    external
    view
    override
    returns (bytes4 magicValue)
{
    // The data parameter is unused in this example
    return _signatures[tokenId][hash] ? MAGICVALUE : BADVALUE;
}
```

**概要**
提供されたトークンIDとハッシュに対する署名が有効か判断する関数。

**詳細**
関数は、`_signatures`マッピングを参照して、指定されたトークンIDとハッシュの組み合わせに対する署名が有効かどうかをチェックします。
有効な署名の場合は`MAGICVALUE`を、そうでない場合は`BADVALUE`を返します。
`data`パラメータはこの例では使用されていません。

**引数**
- `tokenId`
    - 署名を検証するNFTのトークンID。
- `hash`
    - 検証する署名のデータのハッシュ。
- `data`
    - 追加データ（この例では未使用）。

**戻り値**
- `magicValue`
    - 署名が有効か無効かを示すバイト値。

---

### `supportsInterface`

```javascript
function supportsInterface(
    bytes4 interfaceId
) public view virtual override returns (bool) {
    return
        interfaceId == type(IERC6066).interfaceId ||
        super.supportsInterface(interfaceId);
}
```

**概要**
コントラクトが特定のインターフェースをサポートしているか判断する関数。

**詳細**
この関数は、指定された`interfaceId`が**IERC6066**のインターフェースIDと一致するか、または親クラスの`supportsInterface`関数によってサポートされているかどうかをチェックします。

**引数**
- `interfaceId`
    - チェックするインターフェースのID。

**戻り値**
- 戻り値
    - 指定されたインターフェースIDがサポートされているかどうかのブール値。

---

# セキュリティ考慮事項

こんとらくとベースの署名の取り消し可能な性質は、このEIPにも引き継がれています。
開発者もユーザーも同様に、このことを考慮に入れるべきです。

# 引用

Jack Boyuan Xu (@boyuanx), "ERC-6066: Signature Validation Method for NFTs," Ethereum Improvement Proposals, no. 6066, November 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6066.

# 最後に

今回は「**ERC721**や**ERC1155**などのNFTによる署名検証の仕組みを提案している規格である**ERC6066**」についてまとめてきました！
いかがだったでしょうか？

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