[ERC5185] NFTのメタデータを事前に定義されたルールに沿って更新する仕組みを理解しよう！

# はじめに

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

https://amzn.asia/d/gxvJ0Pw

今回は、NFTに紐づいているメタデータを、事前に定義されたルールに沿って更新する仕組みを提案している**ERC5185**についてまとめていきます！

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

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

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

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

# 概要

**ERC5185**は、NFTのメタデータをあらかじめ定義されたルールに従って更新できるようにする標準的な方法を提案しています。
更新は自由に行えるわけではなく、決められた「レシピ（変換ルール）」に従ってのみ許可されます。
これにより、メタデータの変更内容と順序が完全に予測可能かつ検証可能になります。

**ERC5185**は**ERC721**と**ERC1155**を拡張する形で設計されています。
更新の都度イベントが発行されるため、誰でもその変更履歴を追うことができます。

# 動機

NFTのメタデータを全てオンチェーンに保存するのは、ブロックチェーン上に保存するデータ量を超えてしまったり、コスト的にも課題が多いです。

そのため、IPFSのような分散ファイルシステムを使ってオフチェーンに保存する手法が一般的です。
IPFSに保存すれば、NFTのtokenIdとメタデータとの対応関係を検証することはできますが、更新があるとメタデータ全体を差し替える必要があり、「**一部だけを変える**」といった柔軟な更新ができません。

:::note info
AWS S3などに保存することもでき、この場合は一部を変更することは可能です。
:::

また、特定のNFTプロジェクトでは独自にこの問題を解決する方法を取ることも可能ですが、NFTマーケットプレイスや外部ツールがその更新内容を理解して正しく処理するためには共通仕様が必要です。

**ERC5185**は、JSON形式のメタデータに対して事前に定義した手順（レシピ）で段階的に変更を加えられるようにします
どのような属性をどう変えてよいかは、NFTの用途に応じて制限を設けることができます。

例えば、家を表すNFTであれば、所有者の履歴だけを追加する形で更新を許可する、といった制約をかけられます。
一方、ゲームに使うキャラクターNFTでは、「**体力**」や「**経験値**」、「**レベル**」などのステータスは変えてよいが、「外見」などの属性は固定にする、といった設計も可能です。

この仕組みは、EthereumからL2ネットワークへブリッジされたNFTにも対応しており、L2上でのキャッシュ処理も効率的に行えるようになります。

# 仕様

**ERC5185**は、NFTのメタデータを段階的に更新するための仕組みを定めています。
更新は**ERC721**および**EIP-1155トークンに対する**オプションの拡張**として定義されています。
更新は全て事前に決められた「**レシピ**」に従って行われ、その履歴は`MetadataUpdates`イベントを通じてオフチェーンから確認できます。

更新の対象となるメタデータは、「**ERC-5185 Updatable Metadata JSON Schema**」に準拠している必要があります。

## インターフェース

**ERC5185**では、以下のインターフェースが提供されます。

```js:IERC5185UpdatableMetadata.sol
interface IERC5185UpdatableMetadata {
    event MetadataUpdates(string URI);
}
```

このイベントは、メタデータ更新に関するURI（RFC 3986形式）を発行します。
URIは更新内容を記載したJSONファイルを指しており、マーケットプレイスなどはこれらのURIを時系列で適用することで、任意の`tokenId`の最新メタデータを取得できます。

## メタデータ構造

```json
{
    "title": "Asset Updatable Metadata",
    "type": "object",
    "properties": {
        "name": {
            "type": "string",
            "description": "Identifies the asset to which this NFT represents"
        },
        "description": {
            "type": "string",
            "description": "Describes the asset to which this NFT represents"
        },
        "image": {
            "type": "string",
            "description": "A URI pointing to a resource with mime type image/* representing the asset to which this NFT represents. Consider making any images at a width between 320 and 1080 pixels and aspect ratio between 1.91:1 and 4:5 inclusive."
        },
        "updatable": {
            "type": "object",
            "required": ["engine", "recipes"],
            "properties": {
                "engine": {
                    "type": "string",
                    "description": "Non ambiguous transformation method/language (with version) to process updates along recipes defined below"
                },
                "schema": {
                    "type": "object",
                    "description": "if present, a JSON Schema that all sequential post transformation updated metadata need to conform. If a transformed JSON does not conform, the update should be considered voided."
                },
                "recipes": {
                    "type": "object",
                    "description": "A catalog of all possibles recipes identified by their keys",
                    "patternProperties": {
                        ".*": {
                            "type": "object",
                            "description": "The key of this object is used to select which recipe to apply for each update",
                            "required": ["eval"],
                            "properties": {
                                "eval": {
                                    "type": "string",
                                    "description": "The evaluation formula to transform the last JSON metadata using the engine above (can take arguments)"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```

### 基本構成

メタデータは以下のような構造を持ちます。

- `name`
    - NFTの名前。
- `description`
    - 説明文。
- `image`
    - 画像のURL（推奨サイズ・アスペクト比あり）。

### updatable セクション

更新可能なメタデータに必要なセクションです。

```json
"updatable": {
  "engine": "...",
  "schema": {...},
  "recipes": {
    "レシピ名": {
      "eval": "更新処理式"
    }
  }
}
```

- `engine`
    - どの言語・ライブラリで更新処理を行うか（例：`jsonata@1.8.*`）。
- `schema`（省略可能）
    - 更新後のメタデータが準拠すべきJSON Schema。
- `recipes`
    - 適用可能な変換レシピの一覧。
    - 各レシピは`eval`という式を持つ。

### 更新ファイルの構造

```json
{
    "title": "Metadata Updates JSON Schema",
    "type": "object",
    "properties": {
        "updates": {
            "type": "array",
            "description": "A list of updates to apply sequentially to calculate updated metadata",
            "items": { "$ref": "#/$defs/update" },
            "$defs": {
                "update": {
                    "type": "object",
                    "required": ["tokenId", "recipeKey"],
                    "properties": {
                        "tokenId": {
                            "type": "string",
                            "description": "The tokenId for which the update recipe should apply"
                         },
                        "recipeKey": {
                            "type": "string",
                            "description": "recipeKey to use to get the JSON transformation expression in current metadata"
                        },
                        "args": {
                            "type": "string",
                            "description": "arguments to pass to the JSON transformation"
                        }
                    }
                 }
            }
        }
    }
}
```

実際の更新内容は、外部に配置されたJSONファイル（URI）に記載されます。
構造は以下のようになっています。

```json
{
  "updates": [
    {
      "tokenId": "1",
      "recipeKey": "levelUp",
      "args": "{ \"amount\": 1 }"
    }
  ]
}
```

- `tokenId`
    - 更新対象のNFT。
- `recipeKey`
    - 適用するレシピのキー。
- `args`
    - レシピに渡すパラメータ（JSON文字列）。

## 更新エンジンの動作

**ERC5185**では、**jsonata** というJSON処理エンジン（バージョン1.8.*）のみが定義されています。

更新の処理は以下のように行います。

```js
const nextMetadata = jsonata(evalString).evaluate(previousMetadata, args)
```

- `evalString`
    - レシピの式（`recipes`セクションから取得）。
- `previousMetadata`
    - 直前のメタデータ。
- `args`
    - 引数（オプション）。

更新処理の結果、以下のいずれかに該当する場合は更新が無効となり、前のメタデータが維持されます。

- `recipeKey`が存在しない。
- 処理中にエラーが発生。
- `schema`が定義されていて、更新後のJSONがそのスキーマに合致しない。

# 補足

## オンチェーン保存の限界

一部のプロジェクト（例：EtherOrcs）は、NFTのメタデータを完全にオンチェーンで管理する試みをしています。
しかし、JSON形式のメタデータをチェーン上に保存・更新するにはコストが高く、処理速度も遅いため、スケーラビリティに課題があります。

## オフチェーン保存の問題点

一方で、従来型のWebサーバーで管理されるURIを使えば、メタデータを自由に更新できますが、ブロックチェーンを利用する意味が薄れてしまいます。
ユーザーが本来期待している「**改ざんできないメタデータ**」や「**信頼できる履歴**」といった特性が損なわれます。

## IPFSやArweaveなどの中間的手段

多くのプロジェクトは、IPFSやArweaveといった分散ストレージを使うことで、ある程度の永続性と改ざん耐性を確保しています。
これは静的なアート作品のように、性質が変わらないNFTには有効です。

しかし、ゲームのキャラクターや不動産証書のように、時間と共に状態が変わるNFTには適していません。
こういったNFTは、状態変化の記録を反映できる必要があります。

## ERC5185の利点

**ERC5185**では、オリジナルのメタデータに対して「**決められた手順**」で更新を加える仕組みを用いることで、ステート変化を安全かつ追跡可能な形で記録できます。
更新に使う「**レシピ**」や処理方法は最初のメタデータに含まれており、更新履歴はすべてオンチェーンイベント（`MetadataUpdates`）で記録されます。

更新は常に`tokenURI`（**ERC721**）または`uri`（**ERC1155**）で取得される最初のメタデータを基点として行われ、誰でも履歴を追って現在のステートを再現できます。
この処理は決定的（deterministic）であるため、キャッシュを使って中間データを保存すれば更新の再計算も効率的に行えます。

## スケーラビリティとガスコスト

更新イベントには複数のNFTをまとめて記録することができ、数千件、数百万件に及ぶ更新を一度のトランザクションで記録可能です。
イベントの発行頻度やタイミングは実装者に委ねられており、柔軟な設計が可能です。

この仕組みは非常にガス効率がよく、変更の頻度に比例してガスが発生するのみで、1回のイベントの`emit`で多数のトークンの変更を処理できます。
結果として、運用コストを抑えつつ、信頼性と透明性の高いメタデータ更新が実現できます。

# 参考実装

JSON変換言語の **JSONata**（バージョン1.8.*）が使用されています。
以下は、ゲーム用NFT「**リトルモンスター**」の簡単な例です。

```json
{
  "name": "Monster 1",
  "description": "Little monsters you can play with.",
  "attributes": [
    { "trait_type": "Level", "value": 0 },
    { "trait_type": "Stamina", "value": 100 }
  ],
  "updatable": {
    "engine": "jsonata@1.8.*",
    "recipes": {
      "levelUp": {
        "eval": "$ ~> | attributes[trait_type='Level'] | {'value': value + 1} |"
      },
      "updateDescription": {
        "eval": "$ ~> | $ | {'description': $newDescription} |"
      }
    }
  }
}
```

この例では、以下のような操作が可能です。

- `levelUp`
    - 「Level」属性の値を+1する。
- `updateDescription`
    - 説明文を更新する（引数が必要）。

更新イベントのJSONは以下のようになります。

```json
{
  "updates": [
    { "tokenId": "1", "action": "levelUp" },
    { "tokenId": "2", "action": "levelUp" },
    { "tokenId": "1", "action": "updateDescription", "args": { "newDescription": "Now I'm a big monster" }},
    { "tokenId": "1", "action": "levelUp" },
    { "tokenId": "3", "action": "levelUp" }
  ]
}
```

このように、定義されたレシピに従ってメタデータの更新が段階的に行われます。

# セキュリティ

悪意のあるレシピを用いたメタデータが作られる可能性があります。
特に、NFTマーケットプレイスや外部ツールが、これらのレシピを実行して最新メタデータを計算する場合、過剰な計算を強制されることでDDoS攻撃の原因となるリスクがあります。

そのため、これらの更新処理を実行する側は、処理対象のエンジンや更新数に適切な制限や隔離された環境を設けることが推奨されます。

また、コントラクト側が`tokenURI`や`uri`の返す内容を後から変える可能性がある場合は、すでにブロードキャストされた変更との整合性や意味合いに注意が必要です。

# 互換性

**ERC5185**は、**ERC721**および**ERC1155**の仕様と完全に互換性があります。

拡張に対応しているアプリケーションだけが、メタデータの更新履歴を追って、最新の状態を再構築できます。
これにより、既存のエコシステムとの摩擦を最小限に抑えつつ、より柔軟な表現力を実現しています。

# 引用

Christophe Le Bars (@clbrge), "ERC-5185: NFT Updatable Metadata Extension [DRAFT]," Ethereum Improvement Proposals, no. 5185, June 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5185.

# 最後に

今回は「NFTに紐づいているメタデータを、事前に定義されたルールに沿って更新する仕組みを提案している**ERC5185**」についてまとめてきました！
いかがだったでしょうか？

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