NFT Dynamic Traits
Abstract
This specification introduces a new interface that extends ERC-721 and ERC-1155 that defines methods for setting and getting dynamic onchain traits associated with non-fungible tokens. These dynamic traits can be used to represent properties, characteristics, redeemable entitlements, or other attributes that can change over time. By defining these traits onchain, they can be used and modified by other onchain contracts.
Motivation
Trait values for non-fungible tokens are often stored offchain. This makes it difficult to query and mutate these values in contract code. Specifying the ability to set and get traits onchain allows for new use cases like redeeming onchain entitlements and transacting based on a token’s traits.
Onchain traits can be used by contracts in a variety of different scenarios. For example, a contract that wants to entitle a token to a consumable benefit (e.g. a redeemable) can robustly reflect that onchain. Marketplaces can allow bidding on these tokens based on the trait value without having to rely on offchain state and exposing users to frontrunning attacks. The motivating use case behind this proposal is to protect users from frontrunning attacks on marketplaces where users can list NFTs with certain traits where they are expected to be upheld during fulfillment.
Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.
Contracts implementing this EIP MUST include the events, getters, and setters as defined below, and MUST return true
for ERC-165 supportsInterface
for 0xaf332f3e
, the 4 byte interfaceId
for this ERC.
interface IERC7496 {
/* Events */
event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 traitValue);
event TraitUpdatedRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId);
event TraitUpdatedRangeUniformValue(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId, bytes32 traitValue);
event TraitUpdatedList(bytes32 indexed traitKey, uint256[] tokenIds);
event TraitUpdatedListUniformValue(bytes32 indexed traitKey, uint256[] tokenIds, bytes32 traitValue);
event TraitMetadataURIUpdated();
/* Getters */
function getTraitValue(uint256 tokenId, bytes32 traitKey) external view returns (bytes32 traitValue);
function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys) external view returns (bytes32[] traitValues);
function getTraitMetadataURI() external view returns (string memory uri);
/* Setters */
function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) external;
}
Keys & Names
The traitKey
is used to identify a trait. The traitKey
MUST be a unique bytes32
value identifying a single trait.
The traitKey
SHOULD be a keccak256
hash of a human readable trait name.
Metadata
Trait metadata is necessary to provide information about which traits are present in a contract, how to display trait names and values, and other optional features.
The trait metadata must be compliant with the specified schema.
The trait metadata URI MAY be a data URI or point to an offchain resource.
The keys in the traits
object MUST be unique trait names. If the trait name is 32 byte hex string starting with 0x
then it is interpreted as a literal traitKey
. Otherwise, the traitKey
is defined as the keccak256
hash of the trait name. A literal traitKey
MUST NOT collide with the keccak256
hash of any other traits defined in the metadata.
The displayName
values MUST be unique and MUST NOT collide with the displayName
of any other traits defined in the metadata.
The validateOnSale
value provides a signal to marketplaces on how to validate the trait value when a token is being sold. If the validation criteria is not met, the sale MUST not be permitted by the marketplace contract. If specified, the value of validateOnSale
MUST be one of the following (or it is assumed to be none
):
none
: No validation is necessary.requireEq
: Thebytes32
traitValue
MUST be equal to the value at the time the offer to purchase was made.requireUintGte
: Thebytes32
traitValue
MUST be greater than or equal to the value at the time the offer to purchase was made. This comparison is made using theuint256
representation of thebytes32
value.requireUintLte
: Thebytes32
traitValue
MUST be less than or equal to the value at the time the offer to purchase was made. This comparison is made using theuint256
representation of thebytes32
value.
Note that even though this specification requires marketplaces to validate the required trait values, buyers and sellers cannot fully rely on marketplaces to do this and must also take their own precautions to research the current trait values prior to initiating the transaction.
Here is an example of the specified schema:
{
"traits": {
"color": {
"displayName": "Color",
"dataType": {
"type": "string",
}
},
"points": {
"displayName": "Total Score",
"dataType": {
"type": "decimal",
"signed": false,
"decimals": 0
},
"validateOnSale": "requireUintGte"
},
"name": {
"displayName": "Name",
"dataType": {
"type": "string",
"minLength": 1,
"maxLength": 32,
"valueMappings": {
"0x0000000000000000000000000000000000000000000000000000000000000000": "Unnamed",
"0x92e75d5e42b80de937d204558acf69c8ea586a244fe88bc0181323fe3b9e3ebf": "🙂"
}
},
"tokenOwnerCanUpdateValue": true
},
"birthday": {
"displayName": "Birthday",
"dataType": {
"type": "epochSeconds",
"valueMappings": {
"0x0000000000000000000000000000000000000000000000000000000000000000": null
}
}
},
"0x77c2fd45bd8bdef5b5bc773f46759bb8d169f3468caab64d7d5f2db16bb867a8": {
"displayName": "🚢 📅",
"dataType": {
"type": "epochSeconds",
"valueMappings": {
"0x0000000000000000000000000000000000000000000000000000000000000000": 1696702201
}
}
}
}
}
string
Metadata Type
The string
metadata type allows for a string value to be set for a trait.
The dataType
object MAY have a minLength
and maxLength
value defined. If minLength
is not specified, it is assumed to be 0. If maxLength
is not specified, it is assumed to be a reasonable length.
The dataType
object MAY have a valueMappings
object defined. If the valueMappings
object is defined, the valueMappings
object MUST be a mapping of bytes32
values to string
or unset null
values. The bytes32
values SHOULD be the keccak256
hash of the string
value. The string
values MUST be unique. If the trait for a token is updated to null
, it is expected offchain indexers to delete the trait for the token.
decimal
Metadata Type
The decimal
metadata type allows for a numeric value to be set for a trait in decimal form.
The dataType
object MAY have a signed
value defined. If signed
is not specified, it is assumed to be false
. This determines whether the traitValue
returned is interpreted as a signed or unsigned integer.
The dataType
object MAY have minValue
and maxValue
values defined. These should be formatted with the decimals specified. If minValue
is not specified, it is assumed to be the minimum value of signed
and decimals
. If maxValue
is not specified, it is assumed to be the maximum value of the signed
and decimals
.
The dataType
object MAY have a decimals
value defined. The decimals
value MUST be a non-negative integer. The decimals
value determines the number of decimal places included in the traitValue
returned onchain. If decimals
is not specified, it is assumed to be 0.
The dataType
object MAY have a valueMappings
object defined. If the valueMappings
object is defined, the valueMappings
object MUST be a mapping of bytes32
values to numeric or unset null
values.
boolean
Metadata Type
The boolean
metadata type allows for a boolean value to be set for a trait.
The dataType
object MAY have a valueMappings
object defined. If the valueMappings
object is defined, the valueMappings
object MUST be a mapping of bytes32
values to boolean
or unset null
values. The boolean
values MUST be unique.
If valueMappings
is not used, the default trait values for boolean
should be bytes32(0)
for false
and bytes32(uint256(1))
(0x0000000000000000000000000000000000000000000000000000000000000001
) for true
.
epochSeconds
Metadata Type
The epochSeconds
metadata type allows for a numeric value to be set for a trait in seconds since the Unix epoch.
The dataType
object MAY have a valueMappings
object defined. If the valueMappings
object is defined, the valueMappings
object MUST be a mapping of bytes32
values to integer or unset null
values.
Events
Updating traits MUST emit one of:
TraitUpdated
TraitUpdatedRange
TraitUpdatedRangeUniformValue
TraitUpdatedList
TraitUpdatedListUniformValue
For the Range
events, the fromTokenId
and toTokenId
MUST be a consecutive range of tokens IDs and MUST be treated as an inclusive range.
For the List
events, the tokenIds
MAY be in any order.
It is RECOMMENDED to use the UniformValue
events when the trait value is uniform across all token ids, so offchain indexers can more quickly process bulk updates rather than fetching each trait value individually.
Updating the trait metadata MUST emit the event TraitMetadataURIUpdated
so offchain indexers can be notified to query the contract for the latest changes via getTraitMetadataURI()
.
setTrait
If a trait defines tokenOwnerCanUpdateValue
as true
, then the trait value MUST be updatable by the token owner by calling setTrait
.
If the value the token owner is attempting to set is not valid, the transaction MUST revert. If the value is valid, the trait value MUST be updated and one of the TraitUpdated
events MUST be emitted.
If the trait has a valueMappings
entry defined for the desired value being set, setTrait
MUST be called with the corresponding traitValue
.
Rationale
The design of this specification is primarily a key-value mapping for maximum flexibility. This interface for traits was chosen instead of relying on using regular getFoo()
and setFoo()
style functions to allow for brevity in defining, setting, and getting traits. Otherwise, contracts would need to know both the getter and setter function selectors including the parameters that go along with it. In defining general but explicit get and set functions, the function signatures are known and only the trait key and values are needed to query and set the values. Contracts can also add new traits in the future without needing to modify contract code.
The traits metadata allows for customizability of both display and behavior. The valueMappings
property can define human-readable values to enhance the traits, for example, the default label of the 0
value (e.g. if the key was “redeemed”, “0” could be mapped to “No”, and “1” to “Yes”). The validateOnSale
property lets the token creator define which traits should be protected on order creation and fulfillment, to protect end users against frontrunning.
Backwards Compatibility
As a new EIP, no backwards compatibility issues are present, except for the point in the specification above that it is explicitly required that the onchain traits MUST override any conflicting values specified by the ERC-721 or ERC-1155 metadata URIs.
Test Cases
Authors have included Foundry tests covering functionality of the specification in the assets folder.
Reference Implementation
Authors have included reference implementations of the specification in the assets folder.
Security Considerations
The set* methods exposed externally MUST be permissioned so they are not callable by everyone but only by select roles or addresses.
Marketplaces SHOULD NOT trust offchain state of traits as they can be frontrunned. Marketplaces SHOULD check the current state of onchain traits at the time of transfer. Marketplaces MAY check certain traits that change the value of the NFT (e.g. redemption status, defined by metadata values with validateOnSale
property) or they MAY hash all the trait values to guarantee the same state at the time of order creation.
Copyright
Copyright and related rights waived via CC0.