はじめに

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

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

今回は、Openseaが新たに提案しているオンチェーンでメタデータを管理する仕組みであるERC7496についてまとめていきます!

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

ERC5008は現在(2023年9月7日)では「Idea」段階です。

概要

この仕様は、NFTを管理するための新しいルールを導入しています。 具体的には、ERC721とERC1155と呼ばれる既存のトークン規格を拡張しています。 これらの規格は、NFTを表現するために使用されています。

新しい仕様では、NFTに「動的な特性」と呼ばれるものを関連付けることができるようになります。 動的な特性は、トークンが持つ情報で、時間が経つにつれて変化することがあります。 これには、トークンの属性や特性、特定の特典、またはその他の属性が含まれます。 これらの動的な特性は、スマートコントラクト上で設定や取得ができます。

たとえば、ゲーム内のNFTキャラクターが新しいスキルを獲得する場合、その情報をトークンに関連付けることができます。 また、デジタルアートNFTが特定の日付に応じて変化する場合、その情報もトークンに結び付けることができます。 そして、他のスマートコントラクトがこれらの特性を読み取ったり変更したりできます。

この新しい仕様は、NFTの柔軟性を高め、さまざまなアプリケーションで利用できるようにするためのものです。 これにより、NFTは単なるコレクションアイテム以上の価値を持つことができます。

動機

通常、NFTの詳細情報(メタデータ)は、ブロックチェーンの外部に保存されています。 これは、NFTの特性や属性をブロックチェーン上のスマートコントラクト内で管理するにはガス代などのコストが膨大にかかるため難しいためです。 しかし、この新しい仕様では、NFTに関連付けられた情報をブロックチェーン上で直接管理できるようになります。

この変更により、NFTの特性をオンチェーンで設定および取得することができます。 これにより、NFTの特性に基づいて以下のような新しいことができるようになります。

  1. トークンの特性に基づいたトランザクション トークンが特定の特性を持っている場合、それに基づいてトークンを取引することができます。 たとえば、特定のアート作品NFTが「限定版」の特性を持っている場合、それを取引する際に特別な条件を設定できます。

  2. オンチェーンで特典の利用 特定の特性を持つNFTを持っている場合、それを使ってオンチェーンで特典や特典を利用できます。 たとえば、特定のNFTが「VIPアクセス」の特性を持っている場合、それを使って特別なサービスにアクセスできるかもしれません。

仕様

javascript
interface IERC7496 {
    /* Events */
    event TraitUpdated(bytes32 indexed traitKey, uint256 indexed tokenId, bytes32 value);
    event TraitUpdatedBulkConsecutive(bytes32 indexed traitKeyPattern, uint256 fromTokenId, uint256 toTokenId);
    event TraitUpdatedBulkList(bytes32 indexed traitKeyPattern, uint256[] tokenIds);
    event TraitLabelsURIUpdated(string uri);

    /* Getters */
    function getTraitValue(bytes32 traitKey, uint256 tokenId) external view returns (bytes32);
    function getTraitValues(bytes32 traitKey, uint256[] calldata tokenIds) external view returns (bytes32[] memory);
    function getTraitKeys() external view returns (bytes32[] memory);
    function getTotalTraitKeys() external view returns (uint256);
    function getTraitKeyAt(uint256 index) external view returns (bytes32);
    function getTraitLabelsURI() external view returns (string memory);

    /* Setters */
    function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) external;
    function setTraitLabelsURI(string calldata uri) external;
}

特性キー(Trait Key)

個々の特性をブロックチェーン上で一意に識別するための名前のようなものです。 これは、特性の名称や場所を示すもので、どんな値でも特性キーとして使用できます。 ただし、特性情報が複雑な階層構造を持つ場合、特性キーは通常、ドットで区切った形式で表現されます。 たとえば、"foo.bar.baz"という特性キーは、"foo"というオブジェクト内にある"bar"というオブジェクト内の"baz"という情報を指定します。

特に、特性キーが非常に長かったり複雑な場合、その特性キーの値をkeccak256ハッシュ関数を使ってハッシュ化し、ハッシュ値を特性キーとして使用することがおすすめされます。 これにより、特性キーが簡潔でかつ一意になります。

ただし、特性キーには「*」(アスタリスク)を含めることができません。 もしも特性キーが設定されていないのに特性キーを問い合わせる場合、スマートコントラクトはエラーとして「UnknownTraitKey()」というエラーメッセージを出し、特性キーが存在しないことを示します。

特性ラベル(Trait Labels)

ユーザー向けのウェブサイトで特性キーに対応する人間に読みやすい値を表示するために使用されます。 特性ラベルのURIは、オフチェーンのURLまたはオンチェーンのデータURIを指すことができます。 特性ラベルURIの仕様は以下の通りです。

json
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "traitKey": {
        "type": "string"
      },
      "fullTraitKey": {
        "type": "string"
      },
      "traitLabel": {
        "type": ["string"]
      },
      "displayType": {
        "type": ["number"]
      },
      "editors": {
        "type": "array",
        "items": {
          "type": "number"
        }
      },
      "editorsAddressList": {
        "type": "array",
        "items": {
          "type": "string"
        }
      },
      "acceptableValues": {
        "type": "array",
        "items": {
          "type": "string"
        }
      },
      "fullTraitValues": {
        "type": "object",
        "properties": {
          "traitValue": {
            "type": "string"
          },
          "fullTraitValue": {
            "type": "string"
          }
        }
      }
    },
    "required": ["traitKey", "traitLabel"]
  }
}
  • traitKey

    • 特性キーはbytes32のオンチェーンキーであることが推奨されます。
    • ただし、traitKeyがASCII文字に直接デコードできないkeccak256ハッシュ値である場合、fullTraitKeyを定義する必要があります。
    • これにより、オフチェーンのインデクサがネストを含むtraitKeyの値を理解できます。
  • displayType

    • 特性の値をフロントエンドユーザーに表示する方法を示します。
    • displayTypeが定義されていない場合、デフォルトで0になります。
    • 以下の表は、displayTypeの値とそれに対応するメタデータの表示タイプを定義しており、将来のEIPで追加できる場合があります。
整数 メタデータ表示タイプ
0 通常の値
1 数値 / パーセンテージ
2 日付
3 非表示
  • editors

    • 特性を変更できるエンティティを指定するための整数の配列です。
    • エディターの種類は以下の通りです。
    整数 エディター
    0 内部(コントラクトアドレス)
    1 コントラクトの所有者
    2 トークンの所有者
    3 カスタムアドレスリスト
  • acceptableValues

    • 特性に設定可能な事前定義の値のセットです。
    • 任意の値が受け入れられる場合、*文字を使用すべきです。
    • acceptableValuesは正規表現で検証することもでき、その場合はregex:で始まる必要があります。
  • fullTraitValues

    • 特性の値がサポートされているbytes32よりも大きい場合、フルな特性値の表示を指定できます。
    • 値はフルな特性値にマッピングされる整数であるべきです。

イベント(Events)

  • 特性情報を更新する際に、TraitUpdated、TraitUpdatedBulkConsecutive、またはTraitUpdatedBulkListイベントを発行します。
  • TraitUpdatedBulkConsecutiveイベントの場合、fromTokenIdとtoTokenIdは連続するトークンIDの範囲を指定し、その範囲内の全ての値と判断されます。
  • TraitUpdatedBulkListイベントの場合、tokenIdsは任意の順序で指定できます。
  • 特性ラベルURIまたはURI内のコンテンツを更新する場合、TraitLabelsURIUpdatedイベントを発行します。
    • これにより、オフチェーンのインデクサーが変更内容を解析できます。

特性キーパターン(traitKeyPattern)

  • 特性または特性の範囲を識別するために使用されます。
  • もしtraitKeyPatternに*が含まれない場合、単一の特性として扱われます。
  • traitKeyPatternに*が含まれる場合、パターンはドットで区切った形式で表現され、*はそれがネストされたレベルでの可能な値を表します。
    • 例えば、foo.bar.*は、fooオブジェクト内のbarオブジェクト内のすべての特性を表すのに使えます。
  • traitKeyPatternには複数の*を含めてはいけず、*はパターン内の最後の文字である必要があります。

メタデータURIとの競合する値

  • この仕様で指定された特性は、ERC721メタデータURIで指定された競合する値を優先します。
    • 特性のラベルがtokenURIで返される特性と完全に一致する場合、このEIPで返される値も一致する必要があります。
    • 一致しない場合、オンチェーンの動的特性の値が優先的に表示および使用されます。
  • オンチェーンの特性とメタデータURI内のデータとの値に差異がある場合、情報提供者とウェブサイトは、競合する値が存在することを警告し、マーケットプレーストランザクションの保証などにおいてオンチェーンの特性を使用するように表示するようにします。

setTraitメソッド

  • スマートコントラクトがsetTraitおよびsetTraitLabelsURIメソッドを公開している場合、これらのメソッドは許可を受けたユーザー(たとえば、トークン所有者または許可されたコントラクト)によってのみ呼び出せます。
    • たとえば、特典の引き換え時に特典コントラクトからプログラム的に呼び出されることがあるためです。
  • setTraitが特性の既存の値を変更しない場合、カスタムエラーTraitValueUnchanged()を発生させます。

レジストリ機能**

  • このEIPが複数のトークンアドレスのオンチェーンメタデータを保持する「レジストリ」として使用される場合、traitKeyの最初の20バイトはトークンアドレスである必要があります。
    • 残りの12バイトは、特性キーとしてASCII文字としても、長いキーのkeccak256ハッシュの最初の12バイトとしても使用できます。
    • このフォーマットで使用される場合、ERC721に対するsupportsInterfaceは返さないべきです。
    • これにより、外部プロバイダーが特性がコントラクトのトークンアドレス用ではないことを理解できます。
  • レジストリ形式で実装される場合、特性ラベルURI JSONはtraitKeyを最後の12バイトのみで指定することができ、特性キーに関するラベルを簡略化できます。

ERC-1155(セミファンジブル)

  • この標準はERC1155に適用できますが、特性は特定のトークン識別子のすべてのトークン数量に適用されます。
  • ERC1155コントラクトにトークンの数量が1のみ存在する場合、この仕様をそのまま使用できます。

補足

ERC721規格において、メタデータURIを通じて提供されるオフチェーンの特性情報は、有用ではありますが、特性をオンチェーンで利用する時は完全に有用ではありません。 オンチェーンの特性情報を使用することで、内部および外部のスマートコントラクトがさまざまなシナリオで特性情報を取得し、変更することができます。 例えば、引き換え可能な特典を提供するスマートコントラクトは、引き換えが実行された後に特典の値を確認し、引き換え後に特性情報を更新することができます。 これにより、オンチェーンのP2Pマーケットプレイスは、注文の履行中に特定の特性情報の値を保証でき、特性情報のプロパティが販売前にフロントランニング攻撃によって変更されることを防げます。

後方互換性

この提案は新しいEIPとして導入されるものであり、既存の規格との後方互換性の問題は存在しません。 ただし、注意すべき点として、この仕様においては、オンチェーンの特性情報がERC721メタデータURIによって指定された特性情報と競合する場合、オンチェーンの特性情報が優先されることが要求されています。 これにより、特性情報の競合を解決し、オンチェーン情報が優先されることが保証されます。

テスト

以下のGithubに格納されています。

参考実装

javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {EnumerableSet} from "openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol";
import {IERCDynamicTraits} from "../interfaces/IERCDynamicTraits.sol";
import {Ownable} from 'openzeppelin/access/Ownable.sol';

contract DynamicTraits is IERCDynamicTraits {
    using EnumerableSet for EnumerableSet.Bytes32Set;

    EnumerableSet.Bytes32Set internal _traitKeys;
    mapping(uint256 tokenId => mapping(bytes32 traitKey => bytes32 traitValue)) internal _traits;
    string internal _traitLabelsURI;

    error TraitValueUnchanged();


  
    function getTraitValue( bytes32 traitKey, uint256 tokenId) external virtual view returns (bytes32 traitValue) {
        return _traits[tokenId][traitKey];
    }

    function getTraitValues(bytes32 traitKey,uint256[] calldata tokenIds) external virtual view returns (bytes32[] memory traitValues) {
        uint256 length = tokenIds.length;
        bytes32[] memory result = new bytes32[](length);
        for (uint256 i = 0; i < length; i++) {
            result[i] = _traits[tokenIds[i]][traitKey];
        }
        return result;
    }

    function getTotalTraitKeys() external virtual view returns (uint256) {
        return _traitKeys.length();
    }

    function getTraitKeyAt(uint256 index) external virtual view returns (bytes32 traitKey) {
        return _traitKeys.at(index);
    }

    function getTraitKeys() external virtual view returns (bytes32[] memory traitKeys) {
        return _traitKeys._inner._values;
    }

    function getTraitLabelsURI() external virtual view returns (string memory labelsURI) {
        return _traitLabelsURI;
    }

    function _setTrait( bytes32 traitKey, uint256 tokenId,bytes32 value) internal {
        bytes32 oldValue = _traits[tokenId][traitKey];
        if (oldValue == value) {
            revert TraitValueUnchanged();
        }

        _traits[tokenId][traitKey] = value;

        if (!_traitKeys.contains(traitKey)) {
            _traitKeys.add(traitKey);
        }

        emit TraitUpdated( traitKey, tokenId, value);
    }


    function _setTraitLabelsURI(string calldata uri) internal virtual {
        _traitLabelsURI = uri;
        emit TraitLabelsURIUpdated(uri);
    }


    function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
        return interfaceId == type(IERCDynamicTraits).interfaceId;
    }
}

_traitKeys

javascript
EnumerableSet.Bytes32Set internal _traitKeys;

概要 特性キー(traitKey)を格納するためのデータ構造。 特性キーは一意の識別子で、これを使用して特性の管理や検索を行います。

パラメータ

  • _traitKeys
    • EnumerableSet.Bytes32Set型の内部変数で、特性キーのセットを保持します。
    • EnumerableSetは一意の値を管理するための便利なデータ構造です。
    • この変数は内部で使用され、特性キーの一覧を追跡します。

_traits

javascript
mapping(uint256 tokenId => mapping(bytes32 traitKey => bytes32 traitValue)) internal _traits;

概要 トークンごとに特性情報を格納するためのマッピング配列。各トークンの特性情報は、特性キー(traitKey)とその値(traitValue)で表現されます。

詳細 各トークンの特性情報は、特性キー(traitKey)とその値(traitValue)で表現されます。

パラメータ

  • tokenId
    • トークンの一意の識別子。
  • traitKey
    • 特性の識別キー。
  • traitValue
    • 特性の値。

_traitLabelsURI

javascript
string internal _traitLabelsURI;

概要 特性ラベルのURI(Uniform Resource Identifier)を格納する変数。

詳細 特性情報を表示する際に特性キーと対応する人間に理解しやすいラベルを提供するために使用されます。


TraitValueUnchanged

javascript
error TraitValueUnchanged();

概要 特性の値を変更しようとした際に特性の値が変更されなかった場合に発生するエラー。

詳細 特性の値を変更しようとしたが、実際には値が変更されなかった場合に発生します。 変更が必要ない場合にエラーを発生させ、無駄なトランザクションを回避するために使用されます。


getTraitValue

javascript
function getTraitValue(bytes32 traitKey, uint256 tokenId) external virtual view returns (bytes32 traitValue) {
    return _traits[tokenId][traitKey];
}

概要 指定したトークンの特性値(traitValue)を取得する関数。

引数

  • traitKey
    • 特性値を一意に識別する特性キー(traitKey)。
  • tokenId
    • 特性値を取得したいトークンの識別子。

戻り値

  • traitValue
    • 指定したトークンの特性値(traitValue)。

getTraitValues

javascript
function getTraitValues(bytes32 traitKey, uint256[] calldata tokenIds) external virtual view returns (bytes32[] memory traitValues) {
    uint256 length = tokenIds.length;
    bytes32[] memory result = new bytes32[](length);
    for (uint256 i = 0; i < length; i++) {
        result[i] = _traits[tokenIds[i]][traitKey];
    }
    return result;
}

概要 複数のトークンに対して指定した特性キー(traitKey)に関連する特性値(traitValues)の一覧を取得する関数。

引数

  • traitKey
    • 特性値を一意に識別する特性キー(traitKey)。
  • tokenIds
    • 特性値を取得したい複数のトークンの識別子が含まれた配列。

戻り値

  • traitValues
    • 指定した特性キーに関連する各トークンの特性値の配列。

getTotalTraitKeys

javascript
function getTotalTraitKeys() external virtual view returns (uint256) {
    return _traitKeys.length();
}

概要 現在登録されている特性キー(traitKey)の総数を取得する関数。

戻り値

  • 特性キーの総数。

getTraitKeyAt

javascript
function getTraitKeyAt(uint256 index) external virtual view returns (bytes32 traitKey) {
    return _traitKeys.at(index);
}

概要 指定したインデックスに位置する特性キー(traitKey)を取得する関数。

引数

  • index
    • 取得したい特性キーの位置。

戻り値

  • traitKey
    • 指定したインデックスに位置する特性キー。

getTraitKeys

javascript
function getTraitKeys() external virtual view returns (bytes32[] memory traitKeys) {
    return _traitKeys._inner._values;
}

概要 すべての登録されている特性キー(traitKey)の一覧を取得する関数。

戻り値

  • traitKeys
    • 登録されている特性キーの一覧の配列。

getTraitLabelsURI

javascript
function getTraitLabelsURI() external virtual view returns (string memory labelsURI) {
    return _traitLabelsURI;
}

概要 特性ラベルのURI(Uniform Resource Identifier)を取得する関数。

戻り値

  • labelsURI
    • 特性ラベルのURI。

_setTrait

javascript
function _setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) internal {
    bytes32 oldValue = _traits[tokenId][traitKey];
    if (oldValue == value) {
        revert TraitValueUnchanged();
    }

    _traits[tokenId][traitKey] = value;

    if (!_traitKeys.contains(traitKey)) {
        _traitKeys.add(traitKey);
    }

    emit TraitUpdated(traitKey, tokenId, value);
}

概要 内部で使用される特性値(traitValue)を設定または更新する関数。

引数

  • traitKey
    • 特性値を一意に識別する特性キー(traitKey)。
  • tokenId
    • 特性値を設定または更新したいトークンの識別子。
  • value
    • 設定または更新する特性値。

_setTraitLabelsURI

javascript
function _setTraitLabelsURI(string calldata uri) internal virtual {
    _traitLabels

URI = uri;
    emit TraitLabelsURIUpdated(uri);
}

概要 特性ラベルのURIを設定する関数。

引数

  • uri
    • 設定する特性ラベルのURI(Uniform Resource Identifier)。

supportsInterface

javascript
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
    return interfaceId == type(IERCDynamicTraits).interfaceId;
}

概要 指定されたインターフェースをサポートしているか確認する関数。

引数

  • interfaceId
    • 確認したいインターフェースの識別子。

戻り値

  • 指定されたインターフェースをサポートしている場合には true、それ以外の場合には false 。

セキュリティ考慮事項

アクセス権限の制御

外部から呼び出される可能性のあるset*メソッドは、誰でも自由に呼び出せる状態にしてはいけません。 代わりに、特定の役割やアドレスによって制限されたアクセス権限を持つユーザーやコントラクトのみがこれらのメソッドを実行できるようにする必要があります。 この制限を設けることで、不正な変更やトランザクションが行われるのを防ぎ、コントラクトのセキュリティを向上させます。

オフチェーン特性情報への依存の危険性

マーケットプレイスなどのアプリケーションは、オフチェーンの特性情報に過度に依存すべきではありません。 なぜなら、オフチェーンの特性情報はフロントランニング攻撃の対象となる可能性があるからです。 フロントランニング攻撃とは、トランザクションがマイナーによって操作され、望ましくない結果を引き起こす攻撃のことです。 したがって、アプリケーションは特性情報がオフチェーンに保存されている場合でも、トランザクションのタイミングにおけるオンチェーンの特性情報の現在の状態を確認することが重要です。

オンチェーン特性情報の利用

特に、特性情報の一部がトークンの価値や状態に影響を与える場合(たとえば、引き換え可能な特典の状態など)、アプリケーションはトランザクション実行時のオンチェーンの特性情報を確認すべきです。 これにより、望ましくない変更や攻撃からトークンの価値を保護することができます。 特性のすべての値をハッシュ化して、注文作成時に同じ状態であることを確認することも考慮すべきセキュリティ対策の一つです。

最後に

今回は「Openseaが新たに提案しているオンチェーンでメタデータを管理する仕組みであるERC7496」についてまとめてきました!

いかがだったでしょうか?

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