[ERC4341] 複数のNFTを送付する時の順番を保証する仕組みを理解しよう！

# はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

https://amzn.asia/d/gxvJ0Pw

今回は、複数のNFTの送付の順番を保証する仕組みを提案している**ERC4341**についてまとめていきます！

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

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

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

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

# 概要

**ERC4341**は、複数のNFTを順序付きで扱えるコントラクトインターフェースを提案しています。
トークンIDにユニコード（Unicode）のコードポイントを直接エンコードすることで、NFTそのものを漢字や絵文字などの意味を持つ記号として利用できるようにします。
これにより、ブロックチェーン上で「**文字列**」をそのままNFTの集合として送受信し、意味を損なわずに保存・伝達することが可能になります。

# 動機

従来の**ERC721**や**ERC1155**では、複数NFTをまとめて送る時に順序情報が失われます。
しかし、トークンIDをユニコードに対応させる場合、並び順は語句や文の意味に直結します。
例として、中国語の「**長城**」を表すユニコード `38271` と `22478` を順序付きで送れば「**長城**」と読み取れますが、順序が逆だと「**城長**」となり意味が変わります。
このように順序保持は必須です。

ユニコードを用いたNFTには、以下のような文化的・実用的価値があります。

- 東アジア圏では文字そのものを贈り物にする慣習がありNFTを使って遅れる。
- 漢詩・ことわざ・格言など、少数の文字で深い意味を伝える表現を後世に残す手段となる。
- 絵文字も同様に扱えるため、感情やアイコンを順序付きで送信できる。

ゲーム要素としての中国将棋（象棋）の駒、結婚の誓い、家訓、祈りの言葉など、幅広い応用が期待できます。

# 仕様

## インターフェース

```js:ERC4341.sol
pragma solidity ^0.8.0;

/**
    @title EIP-4341 Multi Ordered NFT Standard
    @dev See https://eips.ethereum.org/EIPS/eip-4341
 */
interface ERC4341 /* is ERC165 */ {
    event Transfer(address indexed from, address indexed to, uint256 id, uint256 amount);

    event TransferBatch(address indexed from, address indexed to, uint256[] ids, uint256[] amounts);

    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;

    function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external;

    function safePhraseTransferFrom(address from, address to, uint256[] calldata phrase, bytes calldata data) external;

    function balanceOf(address owner, uint256 id) external view returns (uint256);

    function balanceOfPhrase(address owner) external view returns (uint256);

    function balanceOfBatch(address[] calldata owners, uint256[] calldata ids) external view returns (uint256[] memory);

    function retrievePhrase(address owner, uint256 phraseId) external view returns (uint256[] memory);

    function setApprovalForAll(address operator, bool approved) external;

    function isApprovedForAll(address owner, address operator) external view returns (bool);
}
```

## イベント

### `Transfer`

```js
event Transfer(address indexed from, address indexed to, uint256 id, uint256 amount);
```

単一トークンが`tranfer`された時に発行されるイベント。
送信者、受信者、トークンID、数量がログに記録されます。

**パラメータ**

* `from`
    * 送信者のウォレットアドレス。
* `to`
    * 受信者のウォレットアドレス。
* `id`
    * `transfer`するトークンのID。
* `amount`
    * `transfer`するトークンの数量。

### `TransferBatch`

```js
event TransferBatch(address indexed from, address indexed to, uint256[] ids, uint256[] amounts);
```

複数のトークンI と数量を順序付きで一括`transfer`した時に発行されるイベント。
`ids` と `amounts` のインデックスが対応しており、対応するインデックスで配列に格納する必要があります。

**パラメータ**

* `from`
    * 送信者のアドレス。
* `to`
    * 受信者のアドレス。
* `ids`
    * `transfer`されたトークン ID の配列。
* `amounts`
    * 各トークン ID に対応する数量の配列。

### `ApprovalForAll`

```js
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
```

`owner` が `operator` に対して自身の全トークンを操作する権限を付与または解除した時に発行されるイベント。
マーケットプレイスやゲームロジックなど外部コントラクトが、所有者の代わりに`transfer`処理を行うことを許可したかどうかを第三者が確認できます。

**パラメータ**

* `owner`
    * トークン所有者のアドレス。
* `operator`
    * 承認を受けるアドレス。
* `approved`
    * `true` の場合は承認、`false` の場合は承認解除。

## 関数

### `safeTransferFrom`

```js
function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
```

単一のトークンIDを送付する関数。
`from` から `to` へ `id` のトークンを `amount` だけ送ります。
受信側が**ERC721**の受取に対応していない場合は呼び出しを`revert`します。

**引数**

* `from`
    * 送信者アドレス。
* `to`
    * 受信者アドレス。
* `id`
    * `transfer`するトークンID。
* `amount`
    * `transfer`するトークン量。
* `data`
    * 追加データを格納する任意のバイト列。

### `safeBatchTransferFrom`

```js
function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external;
```

複数のトークンIDを順序付きで一括送付する関数。
`ids` と `amounts` の順序を保持したまま `from` から `to` へ安全に送付します。
受け取り先がコントラクトで**ERC721**の受取に対応していない場合は`revert`します。

**引数**

* `from`
    * 送信者アドレス。
* `to`
    * 受信者アドレス。
* `ids`
    * `transfer`するトークンID配列。
* `amounts`
    * 各トークンIDに対応するトークン量配列。
* `data`
    * 追加データ用のバイト列。

### `safePhraseTransferFrom`

```js
function safePhraseTransferFrom(address from, address to, uint256[] calldata phrase, bytes calldata data) external;
```

ユニコードの並び（フレーズ）を表すトークン列をまとめて送付する関数。
`phrase` 配列の順序そのものが意味を構成するため、その順序を変えずに `from` から `to` へ安全に送付します。

**引数**

* `from`
    * 送信者アドレス。
* `to`
    * 受信者アドレス。
* `phrase`
    * フレーズを構成するトークンIDの配列。
* `data`
    * 追加情報を含むバイト列。

### `balanceOf`

```js
function balanceOf(address owner, uint256 id) external view returns (uint256);
```

指定アドレスが保有する特定トークンIDの数量を返す関数。
ユニコードに対応した 1 文字分のトークン残高を確認できます。

**引数**

* `owner`
    * 残高を照会するアドレス。
* `id`
    * 確認したいトークンID。

**戻り値**

* `uint256`
    * 所有トークン量。

### `balanceOfPhrase`

```js
function balanceOfPhrase(address owner) external view returns (uint256);
```

指定アドレスが形成できるフレーズ単位の残高を返す関数。
文字列を構成する十分なトークンを揃えている回数など、実装依存のロジックで算出できます。

**引数**

* `owner`
    * 残高を照会するアドレス。

**戻り値**

* `uint256`
    * フレーズ単位の残高トークン量。

### `balanceOfBatch`

```js
function balanceOfBatch(address[] calldata owners, uint256[] calldata ids) external view returns (uint256[] memory);
```

複数のアドレスと複数のトークンIDに対する残高を一括で取得する関数。
`owners[i]` と `ids[i]` の組み合わせごとに残高を返すため、配列の長さは一致させる必要があります。

**引数**

* `owners`
    * 照会するアドレス配列。
* `ids`
    * 確認したいトークンID配列。

**戻り値**

* `uint256[]`
    * 各組み合わせに対応する残高配列。

### `retrievePhrase`

```js
function retrievePhrase(address owner, uint256 phraseId) external view returns (uint256[] memory);
```

指定アドレスが保有するフレーズIDに紐づくトークンIDの並びを取得する関数。
`phraseId` は実装側が管理するフレーズ識別子で、対応する文字列を構成するトークン列を返します。

**引数**

* `owner`
    * 照会対象のアドレス。
* `phraseId`
    * 取得したいフレーズ識別子。

**戻り値**

* `uint256[]`
    * フレーズを構成する順序付きトークンID配列。

### `setApprovalForAll`

```js
function setApprovalForAll(address operator, bool approved) external;
```

呼び出し元が所有する全てのトークンを、`operator`に送付許可・許可の取り消しを行う関数。
マーケットプレイスなどが大量のNFTの送付を代行する時などに使用し、`ApprovalForAll` イベントが発行されます。

**引数**

* `operator`
    * 権限を設定するアドレス。
* `approved`
    * `true` で承認、`false` で解除します。

### `isApprovedForAll`

```js
function isApprovedForAll(address owner, address operator) external view returns (bool);
```

`operator` が `owner` のトークンを操作できるかを確認する関数。
`setApprovalForAll` により付与されたロール状態を読み取り、現在の承認有無を返します。

**引数**

* `owner`
    * トークン所有者アドレス。
* `operator`
    * 承認状態を確認するアドレス。

**戻り値**

* `bool`
    * `true` なら承認済み、`false` なら未承認です。

# 補足

## 順序情報を保持する理由

ユニコード値をトークンIDとして用いると、IDの並び順そのものが文字列や語句を形成して意味を決定づけます。
従来のバッチ`transfer`（**ERC1155** の `safeBatchTransferFrom` など）は順序を保証しないため、「**長城**」と「**城長**」のような意味の入れ替わりが発生します。
**ERC4341**はフレーズ構造を導入してこの問題を解決します。

## フレーズの定義

フレーズとは、少数の基本文字またはユニコード列を順序付きで束ねた概念です。
実装上は `uint256[]` 配列として保持されてNFTの総量には一切影響しません。
フレーズ自体を`transfer`することはできず、必要に応じて取り出してデコードして元のユニコード列を復元します。

## フレーズの技術仕様

* **保存形式**
    * `uint256[]` にトークンIDを順番どおり格納する。
* **操作制限**
    * フレーズは生成・削除のみで、他アドレスへ直接転`transfer`できない。
* **読み出し**
    * `retrievePhrase` 関数で取得してデコードして文字列を表示する。
* **安全性**
    * フレーズ生成はNFT数量を増減させないため、資産残高の整合性を保つ。

## ストレージへの影響

フレーズ配列を保持するため、**ERC1155**だけの場合よりストレージスロットが追加で必要になります。
順序付きデータを永続的に扱うコストとトレードオフであり、文字列表現という新たな用途を実現するための最小限の追加です。

# 引用

Simon Tian (@simontianx), "ERC-4341: Ordered NFT Batch Standard [DRAFT]," Ethereum Improvement Proposals, no. 4341, October 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4341.

# 最後に

今回は「複数のNFTの送付の順番を保証する仕組みを提案している**ERC4341**」についてまとめてきました！
いかがだったでしょうか？

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