-
Notifications
You must be signed in to change notification settings - Fork 5.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add EIP: NFT Dynamic Traits #7500
Changes from 24 commits
e3ba978
dfa5c9f
7a4dc5b
cf0c53a
04ef7ad
3e6bcce
a5e9eeb
2a949da
86dea79
34282ee
c715e4d
3813658
dac5ac9
75e367d
9836f5d
ccfbc99
d1c050f
cf7a6cc
9e9f1de
b3bd44c
fa3091e
d6da462
ab22c09
4ceb226
26ea0d7
9e2770a
eebe3a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,208 @@ | ||||||||
--- | ||||||||
eip: 7496 | ||||||||
title: NFT Dynamic Traits | ||||||||
description: Extension to ERC-721 and ERC-1155 for dynamic onchain traits | ||||||||
author: Adam Montgomery (@montasaurus), Ryan Ghods (@ryanio), 0age (@0age), James Wenzel (@jameswenzel), Stephan Min (@stephankmin) | ||||||||
discussions-to: https://ethereum-magicians.org/t/erc-7496-nft-dynamic-traits/15484 | ||||||||
status: Draft | ||||||||
type: Standards Track | ||||||||
category: ERC | ||||||||
created: 2023-07-28 | ||||||||
requires: 165, 721, 1155 | ||||||||
--- | ||||||||
|
||||||||
## Abstract | ||||||||
|
||||||||
This specification introduces a new interface that extends [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md) 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 transacting based on a token's traits or redeeming onchain entitlements. | ||||||||
|
||||||||
## 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](./eip-165.md) `supportsInterface` for `0x12345678(placeholder, to be set when finalized)`, the 4 byte `interfaceId` for this ERC. | ||||||||
|
||||||||
```solidity | ||||||||
interface IERC7496 { | ||||||||
/* Events */ | ||||||||
event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 traitValue); | ||||||||
event TraitUpdatedBulkRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId, bytes32 traitValue); | ||||||||
event TraitUpdatedBulkList(bytes32 indexed traitKey, uint256[] tokenIds, bytes32 traitValue); | ||||||||
event TraitMetadataURIUpdated(); | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the metadata changes without changing the URI, should this event be emitted? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, this is outlined in the
|
||||||||
|
||||||||
/* 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 an optional way to define additional information about which traits are present in a contract, how to parse and display trait values, and permissions for setting trait values. | ||||||||
|
||||||||
The trait metadata must be compliant with the [specified schema](../assets/eip-7496/DynamicTraitsSchema.json). | ||||||||
|
||||||||
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`: The `bytes32` `traitValue` MUST be equal to the value at the time the offer to purchase was made. | ||||||||
- `requireUintGte`: The `bytes32` `traitValue` MUST be greater than or equal to the value at the time the offer to purchase was made. This comparison is made using the `uint256` representation of the `bytes32` value. | ||||||||
- `requireUintLte`: The `bytes32` `traitValue` MUST be less than or equal to the value at the time the offer to purchase was made. This comparison is made using the `uint256` representation of the `bytes32` 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: | ||||||||
|
||||||||
```json | ||||||||
{ | ||||||||
"traits": { | ||||||||
"color": { | ||||||||
"displayName": "Color", | ||||||||
"dataType": { | ||||||||
"type": "string", | ||||||||
"acceptableValues": ["red", "green", "blue"] | ||||||||
} | ||||||||
}, | ||||||||
"points": { | ||||||||
"displayName": "Total Score", | ||||||||
"dataType": { | ||||||||
"type": "decimal", | ||||||||
"signed": false, | ||||||||
"bits": 16, | ||||||||
"decimals": 0 | ||||||||
}, | ||||||||
"validateOnSale": "requireUintGte" | ||||||||
}, | ||||||||
"name": { | ||||||||
"displayName": "Name", | ||||||||
"dataType": { | ||||||||
"type": "string", | ||||||||
"minLength": 1, | ||||||||
"maxLength": 32, | ||||||||
"valueMappings": { | ||||||||
"0x0": "Unnamed", | ||||||||
"0x92e75d5e42b80de937d204558acf69c8ea586a244fe88bc0181323fe3b9e3ebf": "🙂" | ||||||||
} | ||||||||
}, | ||||||||
"tokenOwnerCanUpdateValue": true | ||||||||
}, | ||||||||
"birthday": { | ||||||||
"displayName": "Birthday", | ||||||||
"dataType": { | ||||||||
"type": "epochSeconds", | ||||||||
"valueMappings": { | ||||||||
"0x0": null | ||||||||
} | ||||||||
} | ||||||||
}, | ||||||||
"0x77c2fd45bd8bdef5b5bc773f46759bb8d169f3468caab64d7d5f2db16bb867a8": { | ||||||||
"displayName": "🚢 📅", | ||||||||
"dataType": { | ||||||||
"type": "epochSeconds", | ||||||||
"valueMappings": { | ||||||||
"0x0": 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. | ||||||||
|
||||||||
#### `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 a `bits` value defined. If specified, the `bits` value MUST be a positive integer. The `bits` value determines the maximum number of bits that can be used to represent the `traitValue`. | ||||||||
|
||||||||
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. The `decimals` value MUST be less than or equal to the `bits` value. 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 either emit the `TraitUpdated`, `TraitUpdatedBulkRange` or `TraitUpdatedBulkList` event. | ||||||||
|
||||||||
For the event `TraitUpdatedBulkRange`, the `fromTokenId` and `toTokenId` MUST be a consecutive range of tokens IDs and MUST be treated as an inclusive range. | ||||||||
|
||||||||
For the event `TraitUpdatedBulkList`, the `tokenIds` MAY be in any order. | ||||||||
|
||||||||
For `TraitUpdatedBulkRange` and `TraitUpdatedBulkList`, if the `traitValue` is the same for all updated tokens, it is RECOMMENDED to provide the `traitValue` so offchain indexers don't have to fetch each token ID and can more quickly process bulk updates. If the magic value `keccak256("FETCH_EACH_TRAIT_VALUE")` is provided then the indexers will fetch each token's value individually. | ||||||||
|
||||||||
Updating the trait metadata URI 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 | ||||||||
|
||||||||
Onchain traits can be used by contracts to get and mutate traits 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. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This belongs in the Motivation section. The Rationale section should explain technical choices made within the proposal itself, while the Motivation section should justify the proposal as a whole. The Rationale section could, for example, explain the reasoning behind the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks, updated |
||||||||
|
||||||||
## 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. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think the tests are missing, which is fine. Can always add them later! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oops, added! |
||||||||
|
||||||||
## Reference Implementation | ||||||||
|
||||||||
Authors have included reference implementations of the specification in the assets folder. | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can link to them if you like. Something like: [assets folder](../assets/eip-7496/DynamicTraits.sol) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thanks, linked |
||||||||
|
||||||||
## 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](../LICENSE.md). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
// SPDX-License-Identifier: CC0-1.0 | ||
pragma solidity ^0.8.19; | ||
|
||
import {EnumerableSet} from "openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; | ||
import {IERC7496} from "./interfaces/IERC7496.sol"; | ||
|
||
abstract contract DynamicTraits is IERC7496 { | ||
using EnumerableSet for EnumerableSet.Bytes32Set; | ||
|
||
/// @notice Thrown when a new trait value is not different from the existing value | ||
error TraitValueUnchanged(); | ||
|
||
/// @notice An enumerable set of all trait keys that have been set | ||
EnumerableSet.Bytes32Set internal _traitKeys; | ||
|
||
/// @notice A mapping of token ID to a mapping of trait key to trait value | ||
mapping(uint256 tokenId => mapping(bytes32 traitKey => bytes32 traitValue)) internal _traits; | ||
|
||
/// @notice An offchain string URI that points to a JSON file containing trait metadata | ||
string internal _traitMetadataURI; | ||
|
||
/** | ||
* @notice Get the value of a trait for a given token ID. | ||
* @param tokenId The token ID to get the trait value for | ||
* @param traitKey The trait key to get the value of | ||
*/ | ||
function getTraitValue(uint256 tokenId, bytes32 traitKey) public view virtual returns (bytes32 traitValue) { | ||
traitValue = _traits[tokenId][traitKey]; | ||
} | ||
|
||
/** | ||
* @notice Get the values of traits for a given token ID. | ||
* @param tokenId The token ID to get the trait values for | ||
* @param traitKeys The trait keys to get the values of | ||
*/ | ||
function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys) | ||
public | ||
view | ||
virtual | ||
returns (bytes32[] memory traitValues) | ||
{ | ||
uint256 length = traitKeys.length; | ||
traitValues = new bytes32[](length); | ||
for (uint256 i = 0; i < length;) { | ||
bytes32 traitKey = traitKeys[i]; | ||
traitValues[i] = getTraitValue(tokenId, traitKey); | ||
unchecked { | ||
++i; | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @notice Get the URI for the trait metadata | ||
*/ | ||
function getTraitMetadataURI() external view virtual returns (string memory labelsURI) { | ||
return _traitMetadataURI; | ||
} | ||
|
||
/** | ||
* @notice Set the value of a trait for a given token ID. If newTrait is bytes32(0), sets the zero value hash. | ||
* Reverts if the trait value is the zero value hash. | ||
* @param tokenId The token ID to set the trait value for | ||
* @param traitKey The trait key to set the value of | ||
* @param newValue The new trait value to set | ||
*/ | ||
function _setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) internal { | ||
bytes32 existingValue = _traits[tokenId][traitKey]; | ||
|
||
if (existingValue == newValue) { | ||
revert TraitValueUnchanged(); | ||
} | ||
|
||
// no-op if exists | ||
_traitKeys.add(traitKey); | ||
|
||
_traits[tokenId][traitKey] = newValue; | ||
|
||
emit TraitUpdated(traitKey, tokenId, newValue); | ||
} | ||
|
||
/** | ||
* @notice Set the URI for the trait metadata | ||
* @param uri The new URI to set | ||
*/ | ||
function _setTraitMetadataURI(string calldata uri) internal virtual { | ||
_traitMetadataURI = uri; | ||
emit TraitMetadataURIUpdated(); | ||
} | ||
|
||
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { | ||
return interfaceId == type(IERC7496).interfaceId; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like to see the motivation expanded a bit. Particularly I'd like to know why we need a general interface for traits instead of using regular
getFoo() returns uint
andsetFoo(uint foo)
style functions.What is the motivating use case behind this proposal? What type of dapp would need to interact with different tokens and completely arbitrary traits?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks, added reasoning for the motivating use case and why this over get*/set* style functions