ERC-721/ERC-1155 Updatable Metadata Extension


eip: TBD
title: ERC-721/ERC-1155 Updatable Metadata Extension
description: Standard interface extension for ERC-721/ERC-115 controlled metadata updates
author: clb (@christophe)
status: Idea
type: Standards Track
category: ERC
requires: 721 or 1155

Abstract

This specification defines a standard way to allow controlled metadata
updates along predefined formulas. Updates of the original metadata
are restricted and defined by a set of recipes and the sequence and
result of these recipes are deterministic and fully verifiable with
on-chain metadata updates event. The proposal depends on and extends
the existing ERC-721 and ERC-1155.

Motivation

Storing voluminous metadata on-chain is often neither practical neither
cost-efficient.

Storing metadata off-chain on distributed file system like IPFS can
answer some needs of verifiable correlation and permanence between an
NFT tokenId and its metadata but updates come at the cost of being
all or nothing.

This draft ERC allow the original JSON metadata to be modified step by
step along a set of pre-defined JSON transformation formulas.

As an example, a common use of this standard would be to allow in a
game using NFT characters to have some of their attributes changed
from time to time (e.g. health, experience, level, etc) while some
other would be guaranteed to never change (e.g. physicals traits etc).

Specification

The keywords “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

The metadata updates extension is OPTIONAL for ERC-721/ERC-1155 contracts.

/// @title ERC-721/ERC-1155 Updatable Metadata Extension
interface IERCxxxxUpdatableMetadata {
    /// @notice A distinct Uniform Resource Identifier (URI) for a set of updates
    /// @dev This event emits an URI (defined in RFC 3986) of a set of metadata updates.
    /// The URI should point to a JSON file that conforms to the "NFT Metadata Updates JSON Schema"
    /// Third-party platforms such as NFT market can deterministically calculate the lastest
    /// metadata for all tokens using these events by applying them in sequence for each token.
    event MetadataUpdates(string URI);
}

The original metadata SHOULD conform to the “ERCxxx Updatable Metadata JSON Schema” which
is a compatible extension of the “ERC721 Metadata JSON Schema” defined in ERC-721.

“ERCxxx Updatable Metadata JSON Schema” :

{
    "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 sequencial post transformation updated metadata need to conforms. If a transformed JSON does not conform, the update should be consisdered 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)",
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

“NFT Metadata Updates JSON Schema” :

{
    "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"
                        }
                    }
                 }
            }
        }
    }
}

Engines

Only one engine is currently defined in this extension proposal.

If engine in the original metadata is “jsonata@1.8.*”, updated metadata is calculated starting form original metadata and applying
each update sequentially (all updates which are present in all the URIs emitted by the event MetadataUpdates for which tokenId matches).

For each step, the next metadata is obtained by the javascript calculation (or compatible jsonata implementation in other language) :

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

With evalString is found with recipeKey in the original metadata recipes list.

If the key is not present in the original metadata list, previousMetadata is kept as the valid updated metadata.

If the evaluation throws any errors, previousMetadata is kept as the valid updated metadata.

If a validation Schema JSON has been defined and the result JSON
nextMetadata does not conform, that update is not valid and
previousMetadata is kept as the valid updated metadata.

Rationale

There have been numerous interesting uses of ERC721 and ERC1155 smart contracts that associate for each NFT essential and significant metadata. While some project
(e.g. EtherOrcs) have experimented successfully to manage these metadata on-chain, that experimental solution will always be limited by the cost of generating and storing JSON
on-chain. Symmetrically, while storing the JSON metadata at URI endpoint controlled by traditional servers permit limitless updates the the metadata for each NFT,
it is somehow defeating in many uses cases, the whole purpose of using a trust-less blockchain to manage NFT: indeed users may want or demand more permanence
and immutability from the metadata associated with their NFT.

Most uses cases have chosen intermediate solutions like IPFS or arweave to provide some permanence or partial/full immutability of metadata. This is a good solution when
an NFT represents a static asset which characteristics are by nature immutable (like in the art world) but less so with other uses cases like gaming. Distinguishable assets
in a game often should be allowed to evolve and change over time in a controlled way.

The advantages of this standard is precisely to allow these types of controlled transformations over time of each NFT metadata by applying
sequential transformations starting with the original metadata and using formulas themselves defined in the original metadata.

The original metadata for a given NFT is always defined as the JSON pointed by the result of tokenURI for ERC721 and function uri for ERC1155.

The on-chain log trace of updates guarantee than anyone can recalculate and verify interdependently the current updated metadata starting from the original metadata.
The fact that the calculation is deterministic allow easy caching of intermediate transformations and the efficient processing of new updates using these caches.

The number of updates defined by each event is to be determined by the smart contract logic and use case but can easily scale to thousand or millions of updates
per event. The function(s) that should emitting MetadataUpdates and the frequency of these on-chain updates is let at the discretion of this standard implementation.

Transformation engines

We have been experimenting this generic Metadata update proposal using the
JSONata transformation language.

Here is a very simple example of a NFT metadata for an imaginary ‘little monster’ game :

{
    "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} |"          `
        }
      }
    }
}

This updatable metadata can only be updated to increment by one the trait attribute “Level”.

An example JSON updates metadata would be :

{
    "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"}
    ]
}

Caveats

Smart contracts should be careful and conscious of using this
extension and still allow the metadata URI to be updated in some
contexts (by not having the same URI returned by tokenURI or uri
for a given tokenId). They need to take into account if previous
changes could have been already broadcasted for that NFT, if theses
changes are compatible with the new “original metadata” and what
semantic they decide to associate by combining these two kinds of
“updates”.

Backwards Compatibility

The proposal is fully compatible with both ERC721 and ERC1155. Third party application that don’t support Updatable Metadata Extension will still be able to use the original
metadata for each NFT.

gas efficiency

The proposal is extremely gas efficient, since gas costs are only proportional to the frequency of committing changes. Many changes for many tokens
can be batched in one transaction with only one emit cost.

Copyright

Copyright and related rights waived via CC0.

1 Like

Excellent initiative