From e3ba978b890fed7f93078df85546434a8a25c57a Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 11:11:53 -0700 Subject: [PATCH 01/25] Add EIP: NFT Dynamic Traits --- EIPS/eip-7nnn.md | 192 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 EIPS/eip-7nnn.md diff --git a/EIPS/eip-7nnn.md b/EIPS/eip-7nnn.md new file mode 100644 index 00000000000000..5cfb5701bafe01 --- /dev/null +++ b/EIPS/eip-7nnn.md @@ -0,0 +1,192 @@ +--- +eip: 7nnn +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) +discussions-to: +status: Draft +type: Standards Track +category: ERC +created: 2023-07-28 +requires: 165, 721 +--- + +## 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 + +Metadata 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 EIP-165 supportsInterface for `0x12345678`, the 4 byte interfaceId for IERC7NNN. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. + +```solidity +interface IERC7NNN { + /* 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 keys + +The `traitKey` is used to identify a single trait. The `traitKey` can be any value, but it is recommended to express nested values in a dot-separated format. For example, `foo.bar.baz` could be used to represent the nested value `baz` in the object `bar` in the object `foo`. For longer or more complex key values, it is recommended to keccak256 hash the value and use the hash as the `traitKey`. The `traitKey` MUST NOT include a `*`. + +If a trait key is queried that has not been set, it MUST revert with the error `UnknownTraitKey()`. + +### Trait labels + +Trait labels are used for user-facing websites to display human readable values for trait keys. The trait labels URI MAY point to an offchain location or an onchain data URI. The specification for the trait labels URI is as follows: + +```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"] + } +} +``` + +The `traitKey` SHOULD be the `bytes32` onchain key. The `fullTraitKey` MUST be defined if the `traitKey` is a keccak256 hashed value that does not directly decode to ASCII characters, so offchain indexers can understand the full `traitKey` value including its nesting. + +The `displayType` is how the trait value MUST be displayed to front-end users. If the `displayType` is not defined, it MUST default to `0`. The following table defines the values for `displayType` and MAY be added to in future EIPs that require this one. + +| Integer | Metadata Display Type | +| ------- | --------------------- | +| 0 | plain value | +| 1 | number / percentage | +| 2 | date | +| 3 | hidden | + +The `editors` field should specify an array of integers below mapping to the entities that can modify the trait. + +| Integer | Editor | +| ------- | --------------------------- | +| 0 | internal (contract address) | +| 1 | contract owner | +| 2 | token owner | +| 3 | custom address list | + +The `acceptableValues` are a set of predefined values that are acceptable to be set for the trait. If any value is accepted, the `*` character SHOULD be used. The `acceptableValues` MAY also define the validation in regex, and if so should start with `regex:`. + +The `fullTraitValues` objects may specify the full trait value display if the desired trait value is larger than the supported bytes32 on the contract itself. The value SHOULD be an integer, that maps to the full trait value. + +### Events + +Updating traits MUST either emit the `TraitUpdated`, `TraitUpdatedBulkConsecutive` or `TraitUpdatedBulkList` event. For the event `TraitUpdatedBulkConsecutive`, 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. Updating the trait labels URI or the contents within the URI MUST emit the event `TraitLabelsURIUpdated` so offchain indexers can be notified to parse the changes. + +The `traitKeyPattern` is used to identify a single trait or range of traits. If the `traitKeyPattern` does not contain a `*`, it is treated as a single trait. If the `traitKeyPattern` contains a `*`, then the pattern MUST be formatted in a dot-separated format and the `*` MUST express all potential values for the level it is nested at. For example, `foo.bar.*` could be used to represent all traits in the object `bar` in the object `foo`. The `traitKeyPattern` MUST NOT contain more than one `*` and the `*` MUST be the last character in the pattern. + +### Conflicting values with metadata URIs + +Traits specified via this specification MUST override any conflicting values specified by the ERC-721 metadata URIs. If the label of the trait has an exact match of the trait that is returned by tokenURI, then the value returned by this EIP MUST match, and if they do not match, the value returned by the onchain dynamic trait lookup MUST be displayed and used in precedence of the value over tokenURI, since that is what onchain contracts will use to guarantee the values. + +If there is a difference in values between the onchain trait and data in the metadata URI, ingestors and websites SHOULD show a warning that there are conflicting values and the onchain trait is to be used for e.g. guaranteeing marketplace transactions. + +### setTrait + +If the methods `setTrait` and `setTraitLabelsURI` are public on the contract they MUST be permissioned and only be callable by authorized users (e.g. token owner or permissioned contract). This is so `setTrait` can be programmatically called, for example by a redeemable contract when a redemption occurs. + +If `setTrait` does not modify the trait's existing value, it MUST revert with the custom error `TraitValueUnchanged()`. + +### Registry functionality + +If this EIP is being used as a "registry" to contain onchain metadata for multiple token addresses, for example to augment existing tokens that cannot have their code upgraded, the first 20 bytes of the `traitKey` MUST be the token address. The remaining `12` bytes can be used for the trait key, as ASCII characters OR as the first 12 bytes of the keccak256 hash of a longer key. When used in this format, the supportsInterface SHOULD NOT return for ERC-721 so external providers can understand that the traits are not for the contract's token address. + +When implemented in a registry format, the trait labels URI JSON MAY specify the `traitKey` as only the last 12 bytes to simplify redundant labels for traitKeys across token addresses. + +### ERC-1155 (Semi-fungibles) + +This standard MAY be applied to ERC-1155 but the traits would apply to all token amounts for specific token identifiers. If the ERC-1155 contract only has tokens with amount of 1, then this specification MAY be used as written. + +## Rationale + +While offchain traits specified by metadata URIs in ERC-721 are useful, they do not provide the full benefits of having traits available onchain. Onchain traits can be used by internal and external contracts to get and mutate traits in a variety of different scenarios. For example, a contract that enables redeemables can check the value of a redemption and update the trait after the redemption is executed. This also allows onchain p2p marketplaces to guarantee certain trait values during order fulfillment, so trait properties cannot be modified before the sale through 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 metadata URIs. + +## Test Cases + +## Test Cases + +Test cases can be found in [https://github.com/ProjectOpenSea/dynamic-traits/tree/main/test](https://github.com/ProjectOpenSea/redeemables/tree/main/test) + +## Reference Implementation + +The reference implementation can be found at [https://github.com/ProjectOpenSea/dynamic-traits/blob/main/src/lib/DynamicTraits.sol](https://github.com/ProjectOpenSea/dynamic-traits/blob/main/src/lib/DynamicTraits.sol) + +## 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) 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). From dfa5c9f22e81f009190d375a9fd787d6024feef6 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 11:19:19 -0700 Subject: [PATCH 02/25] set EIP number to PR number (7500) --- EIPS/{eip-7nnn.md => eip-7500.md} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename EIPS/{eip-7nnn.md => eip-7500.md} (99%) diff --git a/EIPS/eip-7nnn.md b/EIPS/eip-7500.md similarity index 99% rename from EIPS/eip-7nnn.md rename to EIPS/eip-7500.md index 5cfb5701bafe01..2918bf1c731efd 100644 --- a/EIPS/eip-7nnn.md +++ b/EIPS/eip-7500.md @@ -1,5 +1,5 @@ --- -eip: 7nnn +eip: 7500 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) @@ -23,10 +23,10 @@ Metadata for non-fungible tokens are often stored offchain. This makes it diffic 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 EIP-165 supportsInterface for `0x12345678`, the 4 byte interfaceId for IERC7NNN. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. +Contracts implementing this EIP MUST include the events, getters, and setters as defined below, and MUST return `true` for EIP-165 supportsInterface for `0x12345678`, the 4 byte interfaceId for IERC7500. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. ```solidity -interface IERC7NNN { +interface IERC7500 { /* Events */ event TraitUpdated(bytes32 indexed traitKey, uint256 indexed tokenId, bytes32 value); event TraitUpdatedBulkConsecutive(bytes32 indexed traitKeyPattern, uint256 fromTokenId, uint256 toTokenId); From 7a4dc5bd72167040174a0677a70ac577d5ce6285 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 11:32:38 -0700 Subject: [PATCH 03/25] add discussions-to link, add to requires --- EIPS/eip-7500.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-7500.md b/EIPS/eip-7500.md index 2918bf1c731efd..ca8dee5f7d3015 100644 --- a/EIPS/eip-7500.md +++ b/EIPS/eip-7500.md @@ -3,12 +3,12 @@ eip: 7500 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) -discussions-to: +discussions-to: https://ethereum-magicians.org/t/erc-7501-nft-redeemables/15485 status: Draft type: Standards Track category: ERC created: 2023-07-28 -requires: 165, 721 +requires: 165, 721, 1155 --- ## Abstract From cf0c53a93914e268aae86bc83c6955c2a2f81be3 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 11:45:55 -0700 Subject: [PATCH 04/25] add eip links --- EIPS/eip-7500.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-7500.md b/EIPS/eip-7500.md index ca8dee5f7d3015..a874e6141b1471 100644 --- a/EIPS/eip-7500.md +++ b/EIPS/eip-7500.md @@ -13,7 +13,7 @@ requires: 165, 721, 1155 ## 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. +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 @@ -23,7 +23,7 @@ Metadata for non-fungible tokens are often stored offchain. This makes it diffic 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 EIP-165 supportsInterface for `0x12345678`, the 4 byte interfaceId for IERC7500. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. +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`, the 4 byte interfaceId for IERC7500. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. ```solidity interface IERC7500 { @@ -173,8 +173,6 @@ As a new EIP, no backwards compatibility issues are present, except for the poin ## Test Cases -## Test Cases - Test cases can be found in [https://github.com/ProjectOpenSea/dynamic-traits/tree/main/test](https://github.com/ProjectOpenSea/redeemables/tree/main/test) ## Reference Implementation From 04ef7ad70632548f0ef667a7c2275d6a404ff407 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 11:58:57 -0700 Subject: [PATCH 05/25] fixes --- EIPS/eip-7500.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EIPS/eip-7500.md b/EIPS/eip-7500.md index a874e6141b1471..2c6ba935dc2cda 100644 --- a/EIPS/eip-7500.md +++ b/EIPS/eip-7500.md @@ -23,7 +23,7 @@ Metadata for non-fungible tokens are often stored offchain. This makes it diffic 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`, the 4 byte interfaceId for IERC7500. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. +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`, the 4 byte interfaceId for this ERC. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. ```solidity interface IERC7500 { @@ -173,11 +173,11 @@ As a new EIP, no backwards compatibility issues are present, except for the poin ## Test Cases -Test cases can be found in [https://github.com/ProjectOpenSea/dynamic-traits/tree/main/test](https://github.com/ProjectOpenSea/redeemables/tree/main/test) +Coming soon ## Reference Implementation -The reference implementation can be found at [https://github.com/ProjectOpenSea/dynamic-traits/blob/main/src/lib/DynamicTraits.sol](https://github.com/ProjectOpenSea/dynamic-traits/blob/main/src/lib/DynamicTraits.sol) +Coming soon ## Security Considerations From 3e6bcce09137966a0821e50f213512500b6b1784 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 19:04:59 -0700 Subject: [PATCH 06/25] revise --- EIPS/eip-7500.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-7500.md b/EIPS/eip-7500.md index 2c6ba935dc2cda..fe547bf2446a2c 100644 --- a/EIPS/eip-7500.md +++ b/EIPS/eip-7500.md @@ -173,11 +173,11 @@ As a new EIP, no backwards compatibility issues are present, except for the poin ## Test Cases -Coming soon +Authors will include Foundry tests covering functionality of the specification in the assets folder. ## Reference Implementation -Coming soon +Authors will include Solidity contracts in the assets folder. ## Security Considerations From a5e9eeb13c0bd94e84e6c6edfa2b21b21ea392ba Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 22:23:13 -0700 Subject: [PATCH 07/25] fix discussions-to link --- EIPS/eip-7500.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7500.md b/EIPS/eip-7500.md index fe547bf2446a2c..0b749c151572e4 100644 --- a/EIPS/eip-7500.md +++ b/EIPS/eip-7500.md @@ -3,7 +3,7 @@ eip: 7500 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) -discussions-to: https://ethereum-magicians.org/t/erc-7501-nft-redeemables/15485 +discussions-to: https://ethereum-magicians.org/t/erc-7500-nft-dynamic-traits/15484 status: Draft type: Standards Track category: ERC From 2a949dabfd166c663bc077083fbba81807483b6b Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 21 Aug 2023 08:36:59 -0700 Subject: [PATCH 08/25] Update EIPS/eip-7500.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-7500.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7500.md b/EIPS/eip-7500.md index 0b749c151572e4..6e0eb0a75694d8 100644 --- a/EIPS/eip-7500.md +++ b/EIPS/eip-7500.md @@ -1,5 +1,5 @@ --- -eip: 7500 +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) From 86dea79ef1d48c361a645244a9d9ccad42202144 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 21 Aug 2023 08:43:46 -0700 Subject: [PATCH 09/25] update file name --- EIPS/{eip-7500.md => eip-7496.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename EIPS/{eip-7500.md => eip-7496.md} (99%) diff --git a/EIPS/eip-7500.md b/EIPS/eip-7496.md similarity index 99% rename from EIPS/eip-7500.md rename to EIPS/eip-7496.md index 6e0eb0a75694d8..9c8649138869ac 100644 --- a/EIPS/eip-7500.md +++ b/EIPS/eip-7496.md @@ -3,7 +3,7 @@ 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) -discussions-to: https://ethereum-magicians.org/t/erc-7500-nft-dynamic-traits/15484 +discussions-to: https://ethereum-magicians.org/t/erc-7496-nft-dynamic-traits/15484 status: Draft type: Standards Track category: ERC @@ -26,7 +26,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S 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`, the 4 byte interfaceId for this ERC. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. ```solidity -interface IERC7500 { +interface IERC7496 { /* Events */ event TraitUpdated(bytes32 indexed traitKey, uint256 indexed tokenId, bytes32 value); event TraitUpdatedBulkConsecutive(bytes32 indexed traitKeyPattern, uint256 fromTokenId, uint256 toTokenId); From 34282eececbda9d8a53046e59d54f52491d8835a Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 22 Sep 2023 10:38:32 -0700 Subject: [PATCH 10/25] remove getTraitValues - no real use cases in mind to keep --- EIPS/eip-7496.md | 1 - 1 file changed, 1 deletion(-) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index 9c8649138869ac..f262f892164cd3 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -35,7 +35,6 @@ interface IERC7496 { /* 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); From c715e4d70d0eba129c98bb38e75183c5293b6447 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 25 Sep 2023 17:14:19 -0700 Subject: [PATCH 11/25] updates from community feedback: - getTraitValues for querying multiple traitKeys (instead of tokenIds) - Remove metadata nesting to simplify - Make trait labels optional, add translations - Include reference implementation and tests (to be continued to be added to) - Allow event emission of traitKey `*` mean to fetch values for all valid trait keys --- EIPS/eip-7496.md | 114 ++++++++++++------- assets/eip-7496/DynamicTraits.sol | 151 +++++++++++++++++++++++++ assets/eip-7496/DynamicTraits.t.sol | 167 ++++++++++++++++++++++++++++ 3 files changed, 393 insertions(+), 39 deletions(-) create mode 100644 assets/eip-7496/DynamicTraits.sol create mode 100644 assets/eip-7496/DynamicTraits.t.sol diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index f262f892164cd3..4e7bb9f4424d75 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -2,7 +2,7 @@ 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) +author: Adam Montgomery (@montasaurus), Ryan Ghods (@ryanio), 0age (@0age), James Wenzel (emo.eth), Stephan Min (@stephankmin) discussions-to: https://ethereum-magicians.org/t/erc-7496-nft-dynamic-traits/15484 status: Draft type: Standards Track @@ -23,18 +23,19 @@ Metadata for non-fungible tokens are often stored offchain. This makes it diffic 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`, the 4 byte interfaceId for this ERC. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. +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(to be set here when final)`, the 4 byte interfaceId for this ERC. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. ```solidity 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 TraitUpdatedBulkConsecutive(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId); + event TraitUpdatedBulkList(bytes32 indexed traitKey, uint256[] tokenIds); event TraitLabelsURIUpdated(string uri); /* Getters */ function getTraitValue(bytes32 traitKey, uint256 tokenId) external view returns (bytes32); + function getTraitValues(uint256 tokenId, bytes32[] traitKeys) external view returns (bytes32[]); function getTraitKeys() external view returns (bytes32[] memory); function getTotalTraitKeys() external view returns (uint256); function getTraitKeyAt(uint256 index) external view returns (bytes32); @@ -48,13 +49,19 @@ interface IERC7496 { ### Trait keys -The `traitKey` is used to identify a single trait. The `traitKey` can be any value, but it is recommended to express nested values in a dot-separated format. For example, `foo.bar.baz` could be used to represent the nested value `baz` in the object `bar` in the object `foo`. For longer or more complex key values, it is recommended to keccak256 hash the value and use the hash as the `traitKey`. The `traitKey` MUST NOT include a `*`. +The `traitKey` is used to identify a single trait. The `traitKey` MAY be any value, but it is RECOMMENDED to be the ASCII value of the preferred display label of the trait. Longer key values and translated labels MAY be defined in the trait labels URI. -If a trait key is queried that has not been set, it MUST revert with the error `UnknownTraitKey()`. +If a trait key is queried that has not been set, it MUST revert with the error `TraitKeyNotSet(bytes32 traitKey)`. If the tokenId does not exist, it MUST revert, the error MAY be `NonexistentToken()`. + +### Trait values + +Trait values are returned when querying trait keys. Trait values MAY be any value, but it is RECOMMENDED to display as desired when converting the bytes32 value to ASCII. If the desired trait value is longer than 32 characters that can fit in bytes32, the full trait values and translations MAY be defined in the trait labels URI. ### Trait labels -Trait labels are used for user-facing websites to display human readable values for trait keys. The trait labels URI MAY point to an offchain location or an onchain data URI. The specification for the trait labels URI is as follows: +Trait labels are an optional way to define extra information for displaying trait keys, trait values, and who can edit traits along with their acceptable values to allow for websites to provide UIs to change traits. + +The trait labels URI MAY point to an offchain location or an onchain data URI. The specification for the trait labels URI is as follows: ```json { @@ -64,54 +71,79 @@ Trait labels are used for user-facing websites to display human readable values "type": "object", "properties": { "traitKey": { - "type": "string" - }, - "fullTraitKey": { - "type": "string" + "type": "string", + "description": "The bytes32 traitKey stored on the contract." }, "traitLabel": { - "type": ["string"] + "type": ["string"], + "description": "The trait label to be displayed for the traitKey." + }, + "traitLabelTranslations": { + "type": ["object"], + "properties": { + "locale": { + "type": "string" + }, + "traitLabel": { + "type": "string" + } + }, + "description": "The traitLabel translations by locale." }, "displayType": { - "type": ["number"] + "type": ["number"], + "description": "The display type for the label. See 'Metadata Display Type' enum." + }, + "traitValues": { + "type": "object", + "properties": { + "onchainTraitValue": { + "type": "string" + }, + "displayTraitValue": { + "type": "string" + }, + "translations": { + "type": "object", + "properties": { + "locale": { + "type": "string" + }, + "traitValue": { + "type": "string" + } + } + } + }, + "description": "The full trait values to display in replacement of the onchain trait value. Useful when longer than the 32 ASCII characters that bytes32 allows for." }, "editors": { "type": "array", "items": { "type": "number" - } + }, + "description": "The allowed editors who can edit the label. See 'Editor' enum." }, "editorsAddressList": { "type": "array", "items": { "type": "string" - } + }, + "description": "If the allowed editors of a trait if the allowed editors are a list of addresses." }, "acceptableValues": { "type": "array", "items": { "type": "string" - } - }, - "fullTraitValues": { - "type": "object", - "properties": { - "traitValue": { - "type": "string" - }, - "fullTraitValue": { - "type": "string" - } - } + }, + "description": "The acceptable values that the trait can be changed to by the specified editors." } }, - "required": ["traitKey", "traitLabel"] + "required": [] } } ``` -The `traitKey` SHOULD be the `bytes32` onchain key. The `fullTraitKey` MUST be defined if the `traitKey` is a keccak256 hashed value that does not directly decode to ASCII characters, so offchain indexers can understand the full `traitKey` value including its nesting. - The `displayType` is how the trait value MUST be displayed to front-end users. If the `displayType` is not defined, it MUST default to `0`. The following table defines the values for `displayType` and MAY be added to in future EIPs that require this one. | Integer | Metadata Display Type | @@ -130,19 +162,19 @@ The `editors` field should specify an array of integers below mapping to the ent | 2 | token owner | | 3 | custom address list | -The `acceptableValues` are a set of predefined values that are acceptable to be set for the trait. If any value is accepted, the `*` character SHOULD be used. The `acceptableValues` MAY also define the validation in regex, and if so should start with `regex:`. +The `acceptableValues` are a set of predefined values that are acceptable to be set for the trait. If any value is accepted, the `*` character SHOULD be used. The `acceptableValues` MAY also define the validation in regex by starting with `regex:`. -The `fullTraitValues` objects may specify the full trait value display if the desired trait value is larger than the supported bytes32 on the contract itself. The value SHOULD be an integer, that maps to the full trait value. +The `fullTraitValues` may specify the full trait value display if the desired trait value is larger than the supported bytes32 on the contract itself, along with the translations to different locales. ### Events Updating traits MUST either emit the `TraitUpdated`, `TraitUpdatedBulkConsecutive` or `TraitUpdatedBulkList` event. For the event `TraitUpdatedBulkConsecutive`, 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. Updating the trait labels URI or the contents within the URI MUST emit the event `TraitLabelsURIUpdated` so offchain indexers can be notified to parse the changes. -The `traitKeyPattern` is used to identify a single trait or range of traits. If the `traitKeyPattern` does not contain a `*`, it is treated as a single trait. If the `traitKeyPattern` contains a `*`, then the pattern MUST be formatted in a dot-separated format and the `*` MUST express all potential values for the level it is nested at. For example, `foo.bar.*` could be used to represent all traits in the object `bar` in the object `foo`. The `traitKeyPattern` MUST NOT contain more than one `*` and the `*` MUST be the last character in the pattern. +If the `traitKey` is specified as `*`, then offchain indexers MUST call `getTraitKeys()` to query all trait keys and values for the token IDs. ### Conflicting values with metadata URIs -Traits specified via this specification MUST override any conflicting values specified by the ERC-721 metadata URIs. If the label of the trait has an exact match of the trait that is returned by tokenURI, then the value returned by this EIP MUST match, and if they do not match, the value returned by the onchain dynamic trait lookup MUST be displayed and used in precedence of the value over tokenURI, since that is what onchain contracts will use to guarantee the values. +Traits specified via this specification MUST override any conflicting values specified by ERC-721 or ERC-1155 metadata URIs. If the label of the trait has an exact match of the trait that is returned by tokenURI, then the value returned by this EIP MUST match, and if they do not match, the value returned by the onchain dynamic trait lookup MUST be displayed and used in precedence of the value over tokenURI, since that is what onchain contracts will use to guarantee the values. If there is a difference in values between the onchain trait and data in the metadata URI, ingestors and websites SHOULD show a warning that there are conflicting values and the onchain trait is to be used for e.g. guaranteeing marketplace transactions. @@ -152,9 +184,13 @@ If the methods `setTrait` and `setTraitLabelsURI` are public on the contract the If `setTrait` does not modify the trait's existing value, it MUST revert with the custom error `TraitValueUnchanged()`. +### Newly minted tokens + +Newly minted tokens MUST emit the trait updated events for offchain indexers to properly register the traits to be indexed. If the `traitKey` is specified as `*`, then offchain indexers MUST call `getTraitKeys()` to query all trait keys and values for the token IDs. + ### Registry functionality -If this EIP is being used as a "registry" to contain onchain metadata for multiple token addresses, for example to augment existing tokens that cannot have their code upgraded, the first 20 bytes of the `traitKey` MUST be the token address. The remaining `12` bytes can be used for the trait key, as ASCII characters OR as the first 12 bytes of the keccak256 hash of a longer key. When used in this format, the supportsInterface SHOULD NOT return for ERC-721 so external providers can understand that the traits are not for the contract's token address. +If this EIP is being used as a "registry" to contain onchain metadata for multiple token addresses, for example to augment existing tokens that cannot have their code upgraded, the first 20 bytes of the `traitKey` MUST be the token address. The remaining `12` bytes can be used for the trait key, as ASCII characters OR as the first 12 bytes of the keccak256 hash of a longer key. When used in this format, the supportsInterface SHOULD NOT return for ERC-721 or ERC-1155 so external providers can understand that the traits are not for the contract's token address. When implemented in a registry format, the trait labels URI JSON MAY specify the `traitKey` as only the last 12 bytes to simplify redundant labels for traitKeys across token addresses. @@ -164,19 +200,19 @@ This standard MAY be applied to ERC-1155 but the traits would apply to all token ## Rationale -While offchain traits specified by metadata URIs in ERC-721 are useful, they do not provide the full benefits of having traits available onchain. Onchain traits can be used by internal and external contracts to get and mutate traits in a variety of different scenarios. For example, a contract that enables redeemables can check the value of a redemption and update the trait after the redemption is executed. This also allows onchain p2p marketplaces to guarantee certain trait values during order fulfillment, so trait properties cannot be modified before the sale through frontrunning. +While offchain traits specified by metadata URIs are useful, they do not provide the full benefits of having traits available onchain. Onchain traits can be used by internal and external contracts to get and mutate traits in a variety of different scenarios. For example, a contract that enables redeemables can check the value of a redemption and update the trait after the redemption is executed. This also allows onchain p2p marketplaces to guarantee certain trait values during order fulfillment, so trait properties cannot be modified before the sale through 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 metadata URIs. +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 will include Foundry tests covering functionality of the specification in the assets folder. +Authors have included Foundry tests covering functionality of the specification in the assets folder. ## Reference Implementation -Authors will include Solidity contracts in the assets folder. +Authors have included reference implementations of the specification in the assets folder. ## Security Considerations diff --git a/assets/eip-7496/DynamicTraits.sol b/assets/eip-7496/DynamicTraits.sol new file mode 100644 index 00000000000000..373f1ce37f2250 --- /dev/null +++ b/assets/eip-7496/DynamicTraits.sol @@ -0,0 +1,151 @@ +// SPDX-License-Identifier: MIT +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 trying to delete a trait that has not been set + error TraitNotSet(uint256 tokenId, bytes32 traitKey); + ///@notice Thrown when trying to set a trait explicitly to the zero value hash + error TraitCannotBeZeroValueHash(); + ///@notice Thrown when a new trait value is not different from the existing value + error TraitValueUnchanged(); + + bytes32 constant ZERO_VALUE = keccak256("DYNAMIC_TRAITS_ZERO_VALUE"); + ///@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 labels + string internal _traitLabelsURI; + + function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 trait) external virtual; + function deleteTrait(bytes32 traitKey, uint256 tokenId) external virtual; + + /** + * @notice Get the value of a trait for a given token ID. Reverts if the trait is not set. + * @param traitKey The trait key to get the value of + * @param tokenId The token ID to get the trait value for + */ + function getTraitValue(bytes32 traitKey, uint256 tokenId) public view virtual returns (bytes32) { + bytes32 value = _traits[tokenId][traitKey]; + // Revert if the trait is not set + if (value == bytes32(0)) { + revert TraitNotSet(tokenId, traitKey); + } else if (value == ZERO_VALUE) { + // check for zero value hash; return 0 if so + return bytes32(0); + } else { + // otherwise return normal value + return value; + } + } + + /** + * @notice Get the values of a trait for a given list of token IDs. Reverts if the trait is not set on any single token. + * @param traitKey The trait key to get the value of + * @param tokenIds The token IDs to get the trait values for + */ + function getTraitValues(bytes32 traitKey, uint256[] calldata tokenIds) + external + view + virtual + returns (bytes32[] memory traitValues) + { + uint256 length = tokenIds.length; + bytes32[] memory result = new bytes32[](length); + for (uint256 i = 0; i < length; i++) { + uint256 tokenId = tokenIds[i]; + result[i] = getTraitValue(traitKey, tokenId); + } + return result; + } + + /** + * @notice Get the total number of trait keys that have been set + */ + function getTotalTraitKeys() external view virtual returns (uint256) { + return _traitKeys.length(); + } + + /** + * @notice Get the trait key at a given index + * @param index The index of the trait key to get + */ + function getTraitKeyAt(uint256 index) external view virtual returns (bytes32 traitKey) { + return _traitKeys.at(index); + } + + /** + * @notice Get the trait keys that have been set. May revert if there are too many trait keys. + */ + function getTraitKeys() external view virtual returns (bytes32[] memory traitKeys) { + return _traitKeys._inner._values; + } + + /** + * @notice Get the URI for the trait labels + */ + function getTraitLabelsURI() external view virtual returns (string memory labelsURI) { + return _traitLabelsURI; + } + + /** + * @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 traitKey The trait key to set the value of + * @param tokenId The token ID to set the trait value for + * @param newTrait The new trait value to set + */ + function _setTrait(bytes32 traitKey, uint256 tokenId, bytes32 newTrait) internal { + bytes32 existingValue = _traits[tokenId][traitKey]; + + if (newTrait == bytes32(0)) { + newTrait = ZERO_VALUE; + } else if (newTrait == ZERO_VALUE) { + revert InvalidTraitValue(traitKey, newTrait); + } + + if (existingValue == newTrait) { + revert TraitValueUnchanged(); + } + + // no-op if exists + _traitKeys.add(traitKey); + + _traits[tokenId][traitKey] = newTrait; + + emit TraitUpdated(traitKey, tokenId, newTrait); + } + + /** + * @notice Delete the value of a trait for a given token ID. + * @param traitKey The trait key to delete the value of + * @param tokenId The token ID to delete the trait value for + */ + function _deleteTrait(bytes32 traitKey, uint256 tokenId) internal { + bytes32 existingValue = _traits[tokenId][traitKey]; + if (existingValue == bytes32(0)) { + revert TraitValueUnchanged(); + } + + _traits[tokenId][traitKey] = bytes32(0); + emit TraitUpdated(traitKey, tokenId, bytes32(0)); + } + + /** + * @notice Set the URI for the trait labels + * @param uri The new URI to set + */ + function _setTraitLabelsURI(string calldata uri) internal virtual { + _traitLabelsURI = uri; + emit TraitLabelsURIUpdated(uri); + } + + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return interfaceId == type(IERC7496).interfaceId; + } +} \ No newline at end of file diff --git a/assets/eip-7496/DynamicTraits.t.sol b/assets/eip-7496/DynamicTraits.t.sol new file mode 100644 index 00000000000000..c60563cef072ba --- /dev/null +++ b/assets/eip-7496/DynamicTraits.t.sol @@ -0,0 +1,167 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import {IERC7496} from "src/dynamic-traits/interfaces/IERC7496.sol"; +import {ERC721DynamicTraits, DynamicTraits} from "src/dynamic-traits/ERC721DynamicTraits.sol"; +import {Solarray} from "solarray/Solarray.sol"; + +contract ERC721DynamicTraitsTest is Test { + ERC721DynamicTraits token; + + /* 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); + + function setUp() public { + token = new ERC721DynamicTraits(); + } + + function testSupportsInterfaceId() public { + assertTrue(token.supportsInterface(type(IERC7496).interfaceId)); + } + + function testReturnsValueSet() public { + bytes32 key = bytes32("test.key"); + bytes32 value = bytes32("foo"); + uint256 tokenId = 12345; + + vm.expectEmit(true, true, true, true); + emit TraitUpdated(key, tokenId, value); + + token.setTrait(key, tokenId, value); + + assertEq(token.getTraitValue(key, tokenId), value); + } + + function testOnlyOwnerCanSetValues() public { + address alice = makeAddr("alice"); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice)); + token.setTrait(bytes32("test"), 0, bytes32("test")); + } + + function testSetTrait_Unchanged() public { + bytes32 key = bytes32("test.key"); + bytes32 value1 = bytes32("foo"); + uint256 tokenId1 = 1; + + token.setTrait(key, tokenId1, value1); + vm.expectRevert(DynamicTraits.TraitValueUnchanged.selector); + token.setTrait(key, tokenId1, value1); + } + + function testGetTraitValues() public { + bytes32 key = bytes32("test.key"); + bytes32 value1 = bytes32("foo"); + bytes32 value2 = bytes32("bar"); + uint256 tokenId1 = 1; + uint256 tokenId2 = 2; + + token.setTrait(key, tokenId1, value1); + token.setTrait(key, tokenId2, value2); + + bytes32[] memory values = token.getTraitValues(key, Solarray.uint256s(tokenId1, tokenId2)); + assertEq(values[0], value1); + assertEq(values[1], value2); + } + + function testGetTotalTraitKeys() public { + bytes32 key1 = bytes32("test.key"); + bytes32 key2 = bytes32("test.key2"); + bytes32 value1 = bytes32("foo"); + bytes32 value2 = bytes32("bar"); + uint256 tokenId1 = 1; + uint256 tokenId2 = 2; + + assertEq(token.getTotalTraitKeys(), 0); + + token.setTrait(key1, tokenId1, value1); + assertEq(token.getTotalTraitKeys(), 1); + + token.setTrait(key2, tokenId2, value2); + assertEq(token.getTotalTraitKeys(), 2); + } + + function testGetTraitKeyAt() public { + bytes32 key1 = bytes32("test.key"); + bytes32 key2 = bytes32("test.key2"); + bytes32 value1 = bytes32("foo"); + bytes32 value2 = bytes32("bar"); + uint256 tokenId1 = 1; + uint256 tokenId2 = 2; + + token.setTrait(key1, tokenId1, value1); + + token.setTrait(key2, tokenId2, value2); + + assertEq(token.getTraitKeyAt(0), key1); + assertEq(token.getTraitKeyAt(1), key2); + } + + function testGetTraitKeys() public { + bytes32 key1 = bytes32("test.key"); + bytes32 key2 = bytes32("test.key2"); + bytes32 value1 = bytes32("foo"); + bytes32 value2 = bytes32("bar"); + uint256 tokenId1 = 1; + uint256 tokenId2 = 2; + + token.setTrait(key1, tokenId1, value1); + token.setTrait(key2, tokenId2, value2); + + bytes32[] memory traitKeys = token.getTraitKeys(); + assertEq(traitKeys[0], key1); + assertEq(traitKeys[1], key2); + } + + function testGetAndSetTraitLabelsURI() public { + string memory uri = "https://example.com/labels.json"; + token.setTraitLabelsURI(uri); + assertEq(token.getTraitLabelsURI(), uri); + + vm.prank(address(0x1234)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(0x1234))); + token.setTraitLabelsURI(uri); + } + + function testGetTraitValue_TraitNotSet() public { + bytes32 key = bytes32("test.key"); + uint256 tokenId = 1; + + vm.expectRevert(abi.encodeWithSelector(DynamicTraits.TraitNotSet.selector, tokenId, key)); + token.getTraitValue(key, tokenId); + } + + function testGetTraitValue_ZeroValue() public { + bytes32 key = bytes32("test.key"); + uint256 tokenId = 1; + + token.setTrait(key, tokenId, bytes32(0)); + bytes32 result = token.getTraitValue(key, tokenId); + assertEq(result, bytes32(0), "should return bytes32(0)"); + } + + function testGetTraitValues_ZeroValue() public { + bytes32 key = bytes32("test.key"); + uint256 tokenId = 1; + + token.setTrait(key, tokenId, bytes32(0)); + bytes32[] memory result = token.getTraitValues(key, Solarray.uint256s(tokenId)); + assertEq(result[0], bytes32(0), "should return bytes32(0)"); + } + + function testSetTrait_ZeroValueHash() public { + bytes32 key = bytes32("test.key"); + uint256 tokenId = 1; + bytes32 badValue = keccak256("DYNAMIC_TRAITS_ZERO_VALUE"); + + vm.expectRevert(abi.encodeWithSelector(IERC7496.InvalidTraitValue.selector, key, badValue)); + token.setTrait(key, tokenId, badValue); + } + + function testdeleteTrait() public {} +} \ No newline at end of file From 3813658a3724fada7f51ca750726278cf5eb2bd7 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 25 Sep 2023 17:18:44 -0700 Subject: [PATCH 12/25] fix author username --- EIPS/eip-7496.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index 4e7bb9f4424d75..137b9108319380 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -2,7 +2,7 @@ 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 (emo.eth), Stephan Min (@stephankmin) +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 @@ -23,7 +23,7 @@ Metadata for non-fungible tokens are often stored offchain. This makes it diffic 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(to be set here when final)`, the 4 byte interfaceId for this ERC. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. +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. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. ```solidity interface IERC7496 { From dac5ac9531c1dc64bad2a1cd21a8e408bbae57f3 Mon Sep 17 00:00:00 2001 From: Adam Montgomery Date: Wed, 4 Oct 2023 11:25:41 -0400 Subject: [PATCH 13/25] simplify 7496 --- EIPS/eip-7496.md | 189 +++++++---------------- assets/eip-7496/DynamicTraitsSchema.json | 99 ++++++++++++ 2 files changed, 153 insertions(+), 135 deletions(-) create mode 100644 assets/eip-7496/DynamicTraitsSchema.json diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index 137b9108319380..fcff26a6d0b5d2 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -17,190 +17,109 @@ This specification introduces a new interface that extends [ERC-721](./eip-721.m ## Motivation -Metadata 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. +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. The setters are optional to expose if the contract does not wish for others to modify their metadata, however it is RECOMMENDED to still implement them as permissioned methods to enable for external contract use cases like redemptions. If the contract does not implement the setters, the interfaceId including the setters MUST still be used to identify the contract as implementing this EIP. +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. + +If the contract does not wish for others to modify their metadata, the setters MAY revert when called. ```solidity interface IERC7496 { /* Events */ event TraitUpdated(bytes32 indexed traitKey, uint256 indexed tokenId, bytes32 value); - event TraitUpdatedBulkConsecutive(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId); + event TraitUpdatedBulkRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId); event TraitUpdatedBulkList(bytes32 indexed traitKey, uint256[] tokenIds); - event TraitLabelsURIUpdated(string uri); + event TraitMetadataURIUpdated(string uri); /* Getters */ function getTraitValue(bytes32 traitKey, uint256 tokenId) external view returns (bytes32); function getTraitValues(uint256 tokenId, bytes32[] traitKeys) external view returns (bytes32[]); - 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); + function getTraitMetadataURI() external view returns (string memory); /* Setters */ function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) external; - function setTraitLabelsURI(string calldata uri) external; + function setTraitMetadataURI(string calldata uri) external; } ``` -### Trait keys +### Keys & Names -The `traitKey` is used to identify a single trait. The `traitKey` MAY be any value, but it is RECOMMENDED to be the ASCII value of the preferred display label of the trait. Longer key values and translated labels MAY be defined in the trait labels URI. +The `traitKey` is used to identify a single trait. The `traitKey` MUST be a unique `bytes32` value identifying the trait. -If a trait key is queried that has not been set, it MUST revert with the error `TraitKeyNotSet(bytes32 traitKey)`. If the tokenId does not exist, it MUST revert, the error MAY be `NonexistentToken()`. +If the trait metadata defines a trait with a `traitName` that is not more than 32 characters and only contains ASCII characters, the `traitKey` MUST be the ASCII encoded value of the `traitName`. If the `traitName` is longer than 32 characters or contains non-ASCII characters, the `traitKey` MUST be the `keccak256` hash of the `traitName`. -### Trait values +### Metadata -Trait values are returned when querying trait keys. Trait values MAY be any value, but it is RECOMMENDED to display as desired when converting the bytes32 value to ASCII. If the desired trait value is longer than 32 characters that can fit in bytes32, the full trait values and translations MAY be defined in the trait labels URI. +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. -### Trait labels +The trait metadata must be compliant with the [specified schema](../assets/eip-7496/DynamicTraitsSchema.json). -Trait labels are an optional way to define extra information for displaying trait keys, trait values, and who can edit traits along with their acceptable values to allow for websites to provide UIs to change traits. +The trait metadata URI MAY be a data URI or point to an offchain resource. -The trait labels URI MAY point to an offchain location or an onchain data URI. The specification for the trait labels URI is as follows: +Here is an example of the specified schema: ```json { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "array", - "items": { - "type": "object", - "properties": { - "traitKey": { + "traits": { + "color": { + "displayName": "Color", + "dataType": { "type": "string", - "description": "The bytes32 traitKey stored on the contract." - }, - "traitLabel": { - "type": ["string"], - "description": "The trait label to be displayed for the traitKey." - }, - "traitLabelTranslations": { - "type": ["object"], - "properties": { - "locale": { - "type": "string" - }, - "traitLabel": { - "type": "string" - } - }, - "description": "The traitLabel translations by locale." - }, - "displayType": { - "type": ["number"], - "description": "The display type for the label. See 'Metadata Display Type' enum." - }, - "traitValues": { - "type": "object", - "properties": { - "onchainTraitValue": { - "type": "string" - }, - "displayTraitValue": { - "type": "string" - }, - "translations": { - "type": "object", - "properties": { - "locale": { - "type": "string" - }, - "traitValue": { - "type": "string" - } - } - } - }, - "description": "The full trait values to display in replacement of the onchain trait value. Useful when longer than the 32 ASCII characters that bytes32 allows for." - }, - "editors": { - "type": "array", - "items": { - "type": "number" - }, - "description": "The allowed editors who can edit the label. See 'Editor' enum." - }, - "editorsAddressList": { - "type": "array", - "items": { - "type": "string" - }, - "description": "If the allowed editors of a trait if the allowed editors are a list of addresses." - }, - "acceptableValues": { - "type": "array", - "items": { - "type": "string" - }, - "description": "The acceptable values that the trait can be changed to by the specified editors." + "acceptableValues": ["red", "green", "blue"] } }, - "required": [] + "points": { + "displayName": "Total Score", + "dataType": { + "type": "number", + "minValue": 0, + "maxValue": 1000 + } + }, + "name": { + "displayName": "Name", + "dataType": { + "type": "string", + "minLength": 1, + "maxLength": 32, + "valueMappings": { + "0x0": "Unnamed" + } + }, + "tokenOwnerCanUpdateValue": true + }, + "birthday": { + "displayName": "Birthday", + "dataType": { + "type": "date-time" + } + } } } ``` -The `displayType` is how the trait value MUST be displayed to front-end users. If the `displayType` is not defined, it MUST default to `0`. The following table defines the values for `displayType` and MAY be added to in future EIPs that require this one. - -| Integer | Metadata Display Type | -| ------- | --------------------- | -| 0 | plain value | -| 1 | number / percentage | -| 2 | date | -| 3 | hidden | +#### Metadata Data Types -The `editors` field should specify an array of integers below mapping to the entities that can modify the trait. - -| Integer | Editor | -| ------- | --------------------------- | -| 0 | internal (contract address) | -| 1 | contract owner | -| 2 | token owner | -| 3 | custom address list | - -The `acceptableValues` are a set of predefined values that are acceptable to be set for the trait. If any value is accepted, the `*` character SHOULD be used. The `acceptableValues` MAY also define the validation in regex by starting with `regex:`. - -The `fullTraitValues` may specify the full trait value display if the desired trait value is larger than the supported bytes32 on the contract itself, along with the translations to different locales. +TODO: Specify the date-time format +TODO: Support for custom string validators? ### Events -Updating traits MUST either emit the `TraitUpdated`, `TraitUpdatedBulkConsecutive` or `TraitUpdatedBulkList` event. For the event `TraitUpdatedBulkConsecutive`, 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. Updating the trait labels URI or the contents within the URI MUST emit the event `TraitLabelsURIUpdated` so offchain indexers can be notified to parse the changes. - -If the `traitKey` is specified as `*`, then offchain indexers MUST call `getTraitKeys()` to query all trait keys and values for the token IDs. - -### Conflicting values with metadata URIs - -Traits specified via this specification MUST override any conflicting values specified by ERC-721 or ERC-1155 metadata URIs. If the label of the trait has an exact match of the trait that is returned by tokenURI, then the value returned by this EIP MUST match, and if they do not match, the value returned by the onchain dynamic trait lookup MUST be displayed and used in precedence of the value over tokenURI, since that is what onchain contracts will use to guarantee the values. - -If there is a difference in values between the onchain trait and data in the metadata URI, ingestors and websites SHOULD show a warning that there are conflicting values and the onchain trait is to be used for e.g. guaranteeing marketplace transactions. +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. Updating the trait labels URI or the contents within the URI MUST emit the event `TraitMetadataURIUpdated` so offchain indexers can be notified to parse the changes. ### setTrait -If the methods `setTrait` and `setTraitLabelsURI` are public on the contract they MUST be permissioned and only be callable by authorized users (e.g. token owner or permissioned contract). This is so `setTrait` can be programmatically called, for example by a redeemable contract when a redemption occurs. - -If `setTrait` does not modify the trait's existing value, it MUST revert with the custom error `TraitValueUnchanged()`. - -### Newly minted tokens - -Newly minted tokens MUST emit the trait updated events for offchain indexers to properly register the traits to be indexed. If the `traitKey` is specified as `*`, then offchain indexers MUST call `getTraitKeys()` to query all trait keys and values for the token IDs. - -### Registry functionality - -If this EIP is being used as a "registry" to contain onchain metadata for multiple token addresses, for example to augment existing tokens that cannot have their code upgraded, the first 20 bytes of the `traitKey` MUST be the token address. The remaining `12` bytes can be used for the trait key, as ASCII characters OR as the first 12 bytes of the keccak256 hash of a longer key. When used in this format, the supportsInterface SHOULD NOT return for ERC-721 or ERC-1155 so external providers can understand that the traits are not for the contract's token address. - -When implemented in a registry format, the trait labels URI JSON MAY specify the `traitKey` as only the last 12 bytes to simplify redundant labels for traitKeys across token addresses. - -### ERC-1155 (Semi-fungibles) +If a trait defines `tokenOwnerCanUpdateValue` as `true`, then the trait value SHOULD be updatable by the token owner by calling `setTrait`. -This standard MAY be applied to ERC-1155 but the traits would apply to all token amounts for specific token identifiers. If the ERC-1155 contract only has tokens with amount of 1, then this specification MAY be used as written. +TODO: Format for trait values with valueMappings present ## Rationale -While offchain traits specified by metadata URIs are useful, they do not provide the full benefits of having traits available onchain. Onchain traits can be used by internal and external contracts to get and mutate traits in a variety of different scenarios. For example, a contract that enables redeemables can check the value of a redemption and update the trait after the redemption is executed. This also allows onchain p2p marketplaces to guarantee certain trait values during order fulfillment, so trait properties cannot be modified before the sale through frontrunning. +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. ## Backwards Compatibility diff --git a/assets/eip-7496/DynamicTraitsSchema.json b/assets/eip-7496/DynamicTraitsSchema.json new file mode 100644 index 00000000000000..3c3c475bf53456 --- /dev/null +++ b/assets/eip-7496/DynamicTraitsSchema.json @@ -0,0 +1,99 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "required": ["traits"], + "properties": { + "traits": { + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["dataType"], + "properties": { + "displayName": { + "type": ["string"], + "description": "The user-facing display name for the trait." + }, + "dataType": { + "oneOf": [ + { + "type": "object", + "required": ["type"], + "properties": { + "type": { "const": "string" }, + "acceptableValues": { + "type": "array", + "description": "An exclusive list of possible string values that can be set for the trait. If this is not specified, the trait can be set to any reasonable string value. If `valueMappings` is specified, this list must be the `mappedValue`s.", + "items": { + "type": "string" + } + }, + "maxLength": { + "type": "number", + "description": "The maximum length of the string value that can be set for the trait (inclusive). If this is not specified, the trait can be set to any reasonable string length." + }, + "minLength": { + "type": "number", + "description": "The minimum length of the string value that can be set for the trait (inclusive). If this is not specified, it is assumed to be 0." + }, + "valueMappings": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "A dictionary mapping of `traitValue`s returned from the contract to values that an offchain indexer should display. The keys to the dictionary are the onchain values and the dictionary values are the offchain values. Useful when longer than the 32 ASCII characters that bytes32 allows for." + } + } + }, + { + "type": "object", + "required": ["type"], + "properties": { + "type": { "const": "number" }, + "maxValue": { + "type": "number", + "description": "The maximum value of the string value that can be set for the trait (inclusive). If this is not specified, it is assumed to be the int256 max value." + }, + "minValue": { + "type": "number", + "description": "The minimum value of the string value that can be set for the trait (inclusive). If this is not specified, it is assumed to be the int256 min value." + }, + "decimals": { + "type": "number", + "description": "The number of decimal places that the trait value is returned with onchain. If this is not specified, it is assumed to be 0 (i.e. an integer value)." + } + } + }, + { + "type": "object", + "required": ["type"], + "properties": { + "type": { "const": "boolean" }, + "acceptableValues": { + "type": "array", + "description": "An exclusive list of possible boolean values that can be set for the trait.", + "items": { + "type": "boolean" + } + } + } + }, + { + "type": "object", + "required": ["type"], + "properties": { + "type": { "const": "date-time" } + }, + "description": "TODO: Define the format for this." + } + ], + "description": "The data type definition of the trait. See the `Metadata Data Types` section for more information." + }, + "tokenOwnerCanUpdateValue": { + "type": "boolean", + "description": "Whether the token owner is able to set the trait value directly. If this isn't specified, it is assumed to be false." + } + } + } + } + } +} From 75e367d85c7f98e86f4096e89d019090d506b35f Mon Sep 17 00:00:00 2001 From: Adam Montgomery Date: Sat, 7 Oct 2023 15:25:58 -0400 Subject: [PATCH 14/25] round of updates --- EIPS/eip-7496.md | 82 ++++++++++++++--- assets/eip-7496/DynamicTraitsSchema.json | 107 ++++++++++++++++++----- 2 files changed, 153 insertions(+), 36 deletions(-) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index fcff26a6d0b5d2..606887e11f003e 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -42,15 +42,14 @@ interface IERC7496 { /* Setters */ function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) external; - function setTraitMetadataURI(string calldata uri) external; } ``` ### Keys & Names -The `traitKey` is used to identify a single trait. The `traitKey` MUST be a unique `bytes32` value identifying the trait. +The `traitKey` is used to identify a trait. The `traitKey` MUST be a unique `bytes32` value identifying a single trait. -If the trait metadata defines a trait with a `traitName` that is not more than 32 characters and only contains ASCII characters, the `traitKey` MUST be the ASCII encoded value of the `traitName`. If the `traitName` is longer than 32 characters or contains non-ASCII characters, the `traitKey` MUST be the `keccak256` hash of the `traitName`. +The `traitKey` SHOULD be a `keccak256` hash of a human readable trait name. ### Metadata @@ -60,6 +59,17 @@ The trait metadata must be compliant with the [specified schema](../assets/eip-7 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 `consumptionValidationOnSale` 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 SHOULD not be permitted by the marketplace contract. If specified, the value of `consumptionValidationOnSale` MUST be one of the following (or it is assumed to be `none`): + +- `none`: No validation is necessary. +- `requireEq`: The `bytes32` `traitValue` SHOULD be equal to the value at the time the offer to purchase was made. +- `requireUintGte`: The `bytes32` `traitValue` SHOULD 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` SHOULD 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. + Here is an example of the specified schema: ```json @@ -75,10 +85,12 @@ Here is an example of the specified schema: "points": { "displayName": "Total Score", "dataType": { - "type": "number", - "minValue": 0, - "maxValue": 1000 - } + "type": "decimal", + "signed": false, + "bits": 16, + "decimals": 0 + }, + "consumptionValidationOnSale": "requireUintGte" }, "name": { "displayName": "Name", @@ -87,7 +99,8 @@ Here is an example of the specified schema: "minLength": 1, "maxLength": 32, "valueMappings": { - "0x0": "Unnamed" + "0x0": "Unnamed", + "0x92e75d5e42b80de937d204558acf69c8ea586a244fe88bc0181323fe3b9e3ebf": "🙂" } }, "tokenOwnerCanUpdateValue": true @@ -95,27 +108,68 @@ Here is an example of the specified schema: "birthday": { "displayName": "Birthday", "dataType": { - "type": "date-time" + "type": "epochSeconds", + "valueMappings": { + "0x0": null + } + } + }, + "0x77c2fd45bd8bdef5b5bc773f46759bb8d169f3468caab64d7d5f2db16bb867a8": { + "displayName": "🚢 📅", + "dataType": { + "type": "epochSeconds", + "valueMappings": { + "0x0": 1696702201 + } } } } } ``` -#### Metadata Data Types +#### `string` Metadata Type + +The `string` metadata type allows for a string value to be set for a trait. -TODO: Specify the date-time format -TODO: Support for custom string validators? +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 `bytes32` values SHOULD be the `keccak256` hash of the `boolean` value. The `boolean` values MUST be unique. + +#### `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. Updating the trait labels URI or the contents within the URI MUST emit the event `TraitMetadataURIUpdated` so offchain indexers can be notified to parse the changes. -### setTrait +### `setTrait` If a trait defines `tokenOwnerCanUpdateValue` as `true`, then the trait value SHOULD be updatable by the token owner by calling `setTrait`. -TODO: Format for trait values with valueMappings present +If the value the token owner is attempting to set is not valid, the transaction SHOULD revert. If the value is valid, the trait value SHOULD be updated and the `TraitUpdated` event SHOULD 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 diff --git a/assets/eip-7496/DynamicTraitsSchema.json b/assets/eip-7496/DynamicTraitsSchema.json index 3c3c475bf53456..17bea2fbd215f4 100644 --- a/assets/eip-7496/DynamicTraitsSchema.json +++ b/assets/eip-7496/DynamicTraitsSchema.json @@ -13,6 +13,14 @@ "type": ["string"], "description": "The user-facing display name for the trait." }, + "consumptionValidationOnSale": { + "enum": ["none", "requireEq", "requireUintGte", "requireUintLte"], + "description": "Whether the trait value should be validated when the token is sold. If this isn't specified, it is assumed to be `none`." + }, + "tokenOwnerCanUpdateValue": { + "type": "boolean", + "description": "Whether the token owner is able to set the trait value directly by calling `setTrait`. If this isn't specified, it is assumed to be false." + }, "dataType": { "oneOf": [ { @@ -24,7 +32,14 @@ "type": "array", "description": "An exclusive list of possible string values that can be set for the trait. If this is not specified, the trait can be set to any reasonable string value. If `valueMappings` is specified, this list must be the `mappedValue`s.", "items": { - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } }, "maxLength": { @@ -38,7 +53,14 @@ "valueMappings": { "type": "object", "additionalProperties": { - "type": "string" + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] }, "description": "A dictionary mapping of `traitValue`s returned from the contract to values that an offchain indexer should display. The keys to the dictionary are the onchain values and the dictionary values are the offchain values. Useful when longer than the 32 ASCII characters that bytes32 allows for." } @@ -48,18 +70,42 @@ "type": "object", "required": ["type"], "properties": { - "type": { "const": "number" }, - "maxValue": { - "type": "number", - "description": "The maximum value of the string value that can be set for the trait (inclusive). If this is not specified, it is assumed to be the int256 max value." + "type": { "const": "decimal" }, + "signed": { + "type": "boolean", + "description": "Whether the trait value being returned is signed. If this is not specified, it is assumed to be false." + }, + "bits": { + "type": "integer", + "minimum": 1, + "maximum": 256, + "description": "The number of bits that the trait value is returned with onchain. If this is not specified, it is assumed to be 256." + }, + "decimals": { + "type": "integer", + "description": "The number of decimal places that the trait value is returned with onchain. If this is not specified, it is assumed to be 0 (i.e. an integer value)." }, "minValue": { "type": "number", - "description": "The minimum value of the string value that can be set for the trait (inclusive). If this is not specified, it is assumed to be the int256 min value." + "description": "The minimum value that the trait value can be set to (inclusive). If this is not specified, it is assumed to be the minimum value of the `signed`, `bits` and `decimals`." }, - "decimals": { + "maxValue": { "type": "number", - "description": "The number of decimal places that the trait value is returned with onchain. If this is not specified, it is assumed to be 0 (i.e. an integer value)." + "description": "The maximum value that the trait value can be set to (inclusive). If this is not specified, it is assumed to be the maximum value of the `signed`, `bits` and `decimals`." + }, + "valueMappings": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "description": "A dictionary mapping of `traitValue`s returned from the contract to values that an offchain indexer should display. The keys to the dictionary are the onchain values and the dictionary values are the offchain values. Useful for default values of 0x0 and large or magic numbers." } } }, @@ -68,12 +114,19 @@ "required": ["type"], "properties": { "type": { "const": "boolean" }, - "acceptableValues": { - "type": "array", - "description": "An exclusive list of possible boolean values that can be set for the trait.", - "items": { - "type": "boolean" - } + "valueMappings": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ] + }, + "description": "A dictionary mapping of `traitValue`s returned from the contract to values that an offchain indexer should display. The keys to the dictionary are the onchain values and the dictionary values are the offchain values. Useful for default values of 0x0 and magic numbers." } } }, @@ -81,16 +134,26 @@ "type": "object", "required": ["type"], "properties": { - "type": { "const": "date-time" } + "type": { "const": "epochSeconds" }, + "valueMappings": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, + "description": "A dictionary mapping of `traitValue`s returned from the contract to values that an offchain indexer should display. The keys to the dictionary are the onchain values and the dictionary values are the offchain values. Useful for default values of 0x0 and magic numbers." + } }, - "description": "TODO: Define the format for this." + "description": "A datetime type that is the number of seconds since the Unix epoch (January 1, 1970 00:00:00 UTC). Must return an integer value." } ], - "description": "The data type definition of the trait. See the `Metadata Data Types` section for more information." - }, - "tokenOwnerCanUpdateValue": { - "type": "boolean", - "description": "Whether the token owner is able to set the trait value directly. If this isn't specified, it is assumed to be false." + "description": "The data type definition of the trait." } } } From ccfbc99c7e80391d4d45f64fb13cf4349634e957 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 11 Oct 2023 09:55:55 -0700 Subject: [PATCH 15/25] updates: - remove param from TraitMetadataURIUpdated to save gas when emitting on chain uris - `consumptionValidationOnSale` -> `validateOnSale` - some SHOULDs to MUSTs - re-add setTraitMetadataURI and reasoning --- EIPS/eip-7496.md | 31 +++++++++++++++--------- assets/eip-7496/DynamicTraitsSchema.json | 2 +- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index 606887e11f003e..23e16b553f49b0 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -25,7 +25,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S 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. -If the contract does not wish for others to modify their metadata, the setters MAY revert when called. +The contract MUST implement permissions for setters so only privileged users can update traits and metadata. ```solidity interface IERC7496 { @@ -33,7 +33,7 @@ interface IERC7496 { event TraitUpdated(bytes32 indexed traitKey, uint256 indexed tokenId, bytes32 value); event TraitUpdatedBulkRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId); event TraitUpdatedBulkList(bytes32 indexed traitKey, uint256[] tokenIds); - event TraitMetadataURIUpdated(string uri); + event TraitMetadataURIUpdated(); /* Getters */ function getTraitValue(bytes32 traitKey, uint256 tokenId) external view returns (bytes32); @@ -42,6 +42,7 @@ interface IERC7496 { /* Setters */ function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) external; + function setTraitMetadataURI(string calldata newURI, uint256 tokenId, bytes32 value) external; } ``` @@ -63,12 +64,12 @@ The keys in the `traits` object MUST be unique trait names. If the trait name is The `displayName` values MUST be unique and MUST NOT collide with the `displayName` of any other traits defined in the metadata. -The `consumptionValidationOnSale` 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 SHOULD not be permitted by the marketplace contract. If specified, the value of `consumptionValidationOnSale` MUST be one of the following (or it is assumed to be `none`): +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` SHOULD be equal to the value at the time the offer to purchase was made. -- `requireUintGte`: The `bytes32` `traitValue` SHOULD 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` SHOULD 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. +- `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. Here is an example of the specified schema: @@ -90,7 +91,7 @@ Here is an example of the specified schema: "bits": 16, "decimals": 0 }, - "consumptionValidationOnSale": "requireUintGte" + "validateOnSale": "requireUintGte" }, "name": { "displayName": "Name", @@ -161,16 +162,24 @@ The `dataType` object MAY have a `valueMappings` object defined. If the `valueMa ### 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. Updating the trait labels URI or the contents within the URI MUST emit the event `TraitMetadataURIUpdated` so offchain indexers can be notified to parse the changes. +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. + +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()`. A param for `uri` is not included in the event to reduce the gas cost of emitting long onchain URIs. ### `setTrait` -If a trait defines `tokenOwnerCanUpdateValue` as `true`, then the trait value SHOULD be updatable by the token owner by calling `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 SHOULD revert. If the value is valid, the trait value SHOULD be updated and the `TraitUpdated` event SHOULD be emitted. +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`. +### `setTraitMetadataURI` + +To provide seamless trait setup for users who own contracts but are nontechnical, the function signature for the setter `setTraitMetadataURI()` is defined to provide a predictable way to format the transactions. + +This method MUST be permissioned and not be callable by anyone. + ## 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. @@ -191,7 +200,7 @@ Authors have included reference implementations of the specification in the asse 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) or they MAY hash all the trait values to guarantee the same state at the time of order creation. +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 diff --git a/assets/eip-7496/DynamicTraitsSchema.json b/assets/eip-7496/DynamicTraitsSchema.json index 17bea2fbd215f4..8ea83f3de7ccaa 100644 --- a/assets/eip-7496/DynamicTraitsSchema.json +++ b/assets/eip-7496/DynamicTraitsSchema.json @@ -13,7 +13,7 @@ "type": ["string"], "description": "The user-facing display name for the trait." }, - "consumptionValidationOnSale": { + "validateOnSale": { "enum": ["none", "requireEq", "requireUintGte", "requireUintLte"], "description": "Whether the trait value should be validated when the token is sold. If this isn't specified, it is assumed to be `none`." }, From d1c050f38be397b3864f44586658ef1dc088816d Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 11 Oct 2023 10:12:11 -0700 Subject: [PATCH 16/25] add error TraitKeyNotSet --- EIPS/eip-7496.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index 23e16b553f49b0..61936d659b089a 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -52,6 +52,8 @@ The `traitKey` is used to identify a trait. The `traitKey` MUST be a unique `byt The `traitKey` SHOULD be a `keccak256` hash of a human readable trait name. +If a `traitKey` is queried that has not been set, the contract MUST revert with `error TraitKeyNotSet(bytes32 traitKey)` to identify that the trait value should not be used. + ### 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. From cf7a6ccf44f14bdc73d11f22fa0c81e1c334edb9 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 11 Oct 2023 10:40:20 -0700 Subject: [PATCH 17/25] remove setTraitMetadataURI --- EIPS/eip-7496.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index 61936d659b089a..018105613567e5 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -42,7 +42,6 @@ interface IERC7496 { /* Setters */ function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 value) external; - function setTraitMetadataURI(string calldata newURI, uint256 tokenId, bytes32 value) external; } ``` @@ -176,12 +175,6 @@ If the value the token owner is attempting to set is not valid, the transaction If the trait has a `valueMappings` entry defined for the desired value being set, `setTrait` MUST be called with the corresponding `traitValue`. -### `setTraitMetadataURI` - -To provide seamless trait setup for users who own contracts but are nontechnical, the function signature for the setter `setTraitMetadataURI()` is defined to provide a predictable way to format the transactions. - -This method MUST be permissioned and not be callable by anyone. - ## 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. From 9e9f1def4874e50bb51c3d9fd48ebb1fbae159b0 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 11 Oct 2023 12:01:17 -0700 Subject: [PATCH 18/25] updates --- EIPS/eip-7496.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index 018105613567e5..00240f74a6e798 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -72,6 +72,8 @@ The `validateOnSale` value provides a signal to marketplaces on how to validate - `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 @@ -153,7 +155,9 @@ The `dataType` object MAY have a `valueMappings` object defined. If the `valueMa 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 `bytes32` values SHOULD be the `keccak256` hash of the `boolean` value. The `boolean` values MUST be unique. +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 @@ -165,7 +169,7 @@ The `dataType` object MAY have a `valueMappings` object defined. If the `valueMa 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. -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()`. A param for `uri` is not included in the event to reduce the gas cost of emitting long onchain URIs. +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` From b3bd44c5b6dcb6b577281d573043b7ce19b66094 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Thu, 12 Oct 2023 14:29:00 -0700 Subject: [PATCH 19/25] updates re: comments on PR --- EIPS/eip-7496.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index 00240f74a6e798..ff8f863395048d 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -25,8 +25,6 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S 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. -The contract MUST implement permissions for setters so only privileged users can update traits and metadata. - ```solidity interface IERC7496 { /* Events */ @@ -51,8 +49,6 @@ The `traitKey` is used to identify a trait. The `traitKey` MUST be a unique `byt The `traitKey` SHOULD be a `keccak256` hash of a human readable trait name. -If a `traitKey` is queried that has not been set, the contract MUST revert with `error TraitKeyNotSet(bytes32 traitKey)` to identify that the trait value should not be used. - ### 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. From d6da462b53e61041b4550861c566149c950b88a9 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 16 Oct 2023 15:23:40 -0700 Subject: [PATCH 20/25] simplify getters/setters to accept tokenId first --- EIPS/eip-7496.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index ff8f863395048d..c0eeb3f074bfa0 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -34,12 +34,12 @@ interface IERC7496 { event TraitMetadataURIUpdated(); /* Getters */ - function getTraitValue(bytes32 traitKey, uint256 tokenId) external view returns (bytes32); - function getTraitValues(uint256 tokenId, bytes32[] traitKeys) external view returns (bytes32[]); - function getTraitMetadataURI() external view returns (string memory); + 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(bytes32 traitKey, uint256 tokenId, bytes32 value) external; + function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) external; } ``` From ab22c0962c53250da38f1e6ce47d3b9a531bbeed Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Tue, 17 Oct 2023 13:30:39 -0700 Subject: [PATCH 21/25] update reference implementation to latest spec --- assets/eip-7496/DynamicTraits.sol | 131 ++++++------------- assets/eip-7496/DynamicTraits.t.sol | 167 ------------------------ assets/eip-7496/ERC721DynamicTraits.sol | 53 ++++++++ 3 files changed, 90 insertions(+), 261 deletions(-) delete mode 100644 assets/eip-7496/DynamicTraits.t.sol create mode 100644 assets/eip-7496/ERC721DynamicTraits.sol diff --git a/assets/eip-7496/DynamicTraits.sol b/assets/eip-7496/DynamicTraits.sol index 373f1ce37f2250..88cb17667fa21f 100644 --- a/assets/eip-7496/DynamicTraits.sol +++ b/assets/eip-7496/DynamicTraits.sol @@ -7,145 +7,88 @@ import {IERC7496} from "./interfaces/IERC7496.sol"; abstract contract DynamicTraits is IERC7496 { using EnumerableSet for EnumerableSet.Bytes32Set; - ///@notice Thrown when trying to delete a trait that has not been set - error TraitNotSet(uint256 tokenId, bytes32 traitKey); - ///@notice Thrown when trying to set a trait explicitly to the zero value hash - error TraitCannotBeZeroValueHash(); - ///@notice Thrown when a new trait value is not different from the existing value + /// @notice Thrown when a new trait value is not different from the existing value error TraitValueUnchanged(); - bytes32 constant ZERO_VALUE = keccak256("DYNAMIC_TRAITS_ZERO_VALUE"); - ///@notice An enumerable set of all trait keys that have been set + /// @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 + + /// @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 labels - string internal _traitLabelsURI; - function setTrait(bytes32 traitKey, uint256 tokenId, bytes32 trait) external virtual; - function deleteTrait(bytes32 traitKey, uint256 tokenId) external virtual; + /// @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. Reverts if the trait is not set. - * @param traitKey The trait key to get the value of + * @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(bytes32 traitKey, uint256 tokenId) public view virtual returns (bytes32) { - bytes32 value = _traits[tokenId][traitKey]; - // Revert if the trait is not set - if (value == bytes32(0)) { - revert TraitNotSet(tokenId, traitKey); - } else if (value == ZERO_VALUE) { - // check for zero value hash; return 0 if so - return bytes32(0); - } else { - // otherwise return normal value - return value; - } + function getTraitValue(uint256 tokenId, bytes32 traitKey) public view virtual returns (bytes32 traitValue) { + traitValue = _traits[tokenId][traitKey]; } /** - * @notice Get the values of a trait for a given list of token IDs. Reverts if the trait is not set on any single token. - * @param traitKey The trait key to get the value of - * @param tokenIds The token IDs to get the trait values for + * @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(bytes32 traitKey, uint256[] calldata tokenIds) - external + function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys) + public view virtual returns (bytes32[] memory traitValues) { - uint256 length = tokenIds.length; - bytes32[] memory result = new bytes32[](length); - for (uint256 i = 0; i < length; i++) { - uint256 tokenId = tokenIds[i]; - result[i] = getTraitValue(traitKey, tokenId); + 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; + } } - return result; } /** - * @notice Get the total number of trait keys that have been set + * @notice Get the URI for the trait metadata */ - function getTotalTraitKeys() external view virtual returns (uint256) { - return _traitKeys.length(); - } - - /** - * @notice Get the trait key at a given index - * @param index The index of the trait key to get - */ - function getTraitKeyAt(uint256 index) external view virtual returns (bytes32 traitKey) { - return _traitKeys.at(index); - } - - /** - * @notice Get the trait keys that have been set. May revert if there are too many trait keys. - */ - function getTraitKeys() external view virtual returns (bytes32[] memory traitKeys) { - return _traitKeys._inner._values; - } - - /** - * @notice Get the URI for the trait labels - */ - function getTraitLabelsURI() external view virtual returns (string memory labelsURI) { - return _traitLabelsURI; + 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 traitKey The trait key to set the value of * @param tokenId The token ID to set the trait value for - * @param newTrait The new trait value to set + * @param traitKey The trait key to set the value of + * @param newValue The new trait value to set */ - function _setTrait(bytes32 traitKey, uint256 tokenId, bytes32 newTrait) internal { + function _setTrait(uint256 tokenId, bytes32 traitKey, bytes32 newValue) internal { bytes32 existingValue = _traits[tokenId][traitKey]; - if (newTrait == bytes32(0)) { - newTrait = ZERO_VALUE; - } else if (newTrait == ZERO_VALUE) { - revert InvalidTraitValue(traitKey, newTrait); - } - - if (existingValue == newTrait) { + if (existingValue == newValue) { revert TraitValueUnchanged(); } // no-op if exists _traitKeys.add(traitKey); - _traits[tokenId][traitKey] = newTrait; - - emit TraitUpdated(traitKey, tokenId, newTrait); - } - - /** - * @notice Delete the value of a trait for a given token ID. - * @param traitKey The trait key to delete the value of - * @param tokenId The token ID to delete the trait value for - */ - function _deleteTrait(bytes32 traitKey, uint256 tokenId) internal { - bytes32 existingValue = _traits[tokenId][traitKey]; - if (existingValue == bytes32(0)) { - revert TraitValueUnchanged(); - } + _traits[tokenId][traitKey] = newValue; - _traits[tokenId][traitKey] = bytes32(0); - emit TraitUpdated(traitKey, tokenId, bytes32(0)); + emit TraitUpdated(traitKey, tokenId, newValue); } /** - * @notice Set the URI for the trait labels + * @notice Set the URI for the trait metadata * @param uri The new URI to set */ - function _setTraitLabelsURI(string calldata uri) internal virtual { - _traitLabelsURI = uri; - emit TraitLabelsURIUpdated(uri); + 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; } -} \ No newline at end of file +} diff --git a/assets/eip-7496/DynamicTraits.t.sol b/assets/eip-7496/DynamicTraits.t.sol deleted file mode 100644 index c60563cef072ba..00000000000000 --- a/assets/eip-7496/DynamicTraits.t.sol +++ /dev/null @@ -1,167 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; - -import "forge-std/Test.sol"; -import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; -import {IERC7496} from "src/dynamic-traits/interfaces/IERC7496.sol"; -import {ERC721DynamicTraits, DynamicTraits} from "src/dynamic-traits/ERC721DynamicTraits.sol"; -import {Solarray} from "solarray/Solarray.sol"; - -contract ERC721DynamicTraitsTest is Test { - ERC721DynamicTraits token; - - /* 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); - - function setUp() public { - token = new ERC721DynamicTraits(); - } - - function testSupportsInterfaceId() public { - assertTrue(token.supportsInterface(type(IERC7496).interfaceId)); - } - - function testReturnsValueSet() public { - bytes32 key = bytes32("test.key"); - bytes32 value = bytes32("foo"); - uint256 tokenId = 12345; - - vm.expectEmit(true, true, true, true); - emit TraitUpdated(key, tokenId, value); - - token.setTrait(key, tokenId, value); - - assertEq(token.getTraitValue(key, tokenId), value); - } - - function testOnlyOwnerCanSetValues() public { - address alice = makeAddr("alice"); - vm.prank(alice); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice)); - token.setTrait(bytes32("test"), 0, bytes32("test")); - } - - function testSetTrait_Unchanged() public { - bytes32 key = bytes32("test.key"); - bytes32 value1 = bytes32("foo"); - uint256 tokenId1 = 1; - - token.setTrait(key, tokenId1, value1); - vm.expectRevert(DynamicTraits.TraitValueUnchanged.selector); - token.setTrait(key, tokenId1, value1); - } - - function testGetTraitValues() public { - bytes32 key = bytes32("test.key"); - bytes32 value1 = bytes32("foo"); - bytes32 value2 = bytes32("bar"); - uint256 tokenId1 = 1; - uint256 tokenId2 = 2; - - token.setTrait(key, tokenId1, value1); - token.setTrait(key, tokenId2, value2); - - bytes32[] memory values = token.getTraitValues(key, Solarray.uint256s(tokenId1, tokenId2)); - assertEq(values[0], value1); - assertEq(values[1], value2); - } - - function testGetTotalTraitKeys() public { - bytes32 key1 = bytes32("test.key"); - bytes32 key2 = bytes32("test.key2"); - bytes32 value1 = bytes32("foo"); - bytes32 value2 = bytes32("bar"); - uint256 tokenId1 = 1; - uint256 tokenId2 = 2; - - assertEq(token.getTotalTraitKeys(), 0); - - token.setTrait(key1, tokenId1, value1); - assertEq(token.getTotalTraitKeys(), 1); - - token.setTrait(key2, tokenId2, value2); - assertEq(token.getTotalTraitKeys(), 2); - } - - function testGetTraitKeyAt() public { - bytes32 key1 = bytes32("test.key"); - bytes32 key2 = bytes32("test.key2"); - bytes32 value1 = bytes32("foo"); - bytes32 value2 = bytes32("bar"); - uint256 tokenId1 = 1; - uint256 tokenId2 = 2; - - token.setTrait(key1, tokenId1, value1); - - token.setTrait(key2, tokenId2, value2); - - assertEq(token.getTraitKeyAt(0), key1); - assertEq(token.getTraitKeyAt(1), key2); - } - - function testGetTraitKeys() public { - bytes32 key1 = bytes32("test.key"); - bytes32 key2 = bytes32("test.key2"); - bytes32 value1 = bytes32("foo"); - bytes32 value2 = bytes32("bar"); - uint256 tokenId1 = 1; - uint256 tokenId2 = 2; - - token.setTrait(key1, tokenId1, value1); - token.setTrait(key2, tokenId2, value2); - - bytes32[] memory traitKeys = token.getTraitKeys(); - assertEq(traitKeys[0], key1); - assertEq(traitKeys[1], key2); - } - - function testGetAndSetTraitLabelsURI() public { - string memory uri = "https://example.com/labels.json"; - token.setTraitLabelsURI(uri); - assertEq(token.getTraitLabelsURI(), uri); - - vm.prank(address(0x1234)); - vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(0x1234))); - token.setTraitLabelsURI(uri); - } - - function testGetTraitValue_TraitNotSet() public { - bytes32 key = bytes32("test.key"); - uint256 tokenId = 1; - - vm.expectRevert(abi.encodeWithSelector(DynamicTraits.TraitNotSet.selector, tokenId, key)); - token.getTraitValue(key, tokenId); - } - - function testGetTraitValue_ZeroValue() public { - bytes32 key = bytes32("test.key"); - uint256 tokenId = 1; - - token.setTrait(key, tokenId, bytes32(0)); - bytes32 result = token.getTraitValue(key, tokenId); - assertEq(result, bytes32(0), "should return bytes32(0)"); - } - - function testGetTraitValues_ZeroValue() public { - bytes32 key = bytes32("test.key"); - uint256 tokenId = 1; - - token.setTrait(key, tokenId, bytes32(0)); - bytes32[] memory result = token.getTraitValues(key, Solarray.uint256s(tokenId)); - assertEq(result[0], bytes32(0), "should return bytes32(0)"); - } - - function testSetTrait_ZeroValueHash() public { - bytes32 key = bytes32("test.key"); - uint256 tokenId = 1; - bytes32 badValue = keccak256("DYNAMIC_TRAITS_ZERO_VALUE"); - - vm.expectRevert(abi.encodeWithSelector(IERC7496.InvalidTraitValue.selector, key, badValue)); - token.setTrait(key, tokenId, badValue); - } - - function testdeleteTrait() public {} -} \ No newline at end of file diff --git a/assets/eip-7496/ERC721DynamicTraits.sol b/assets/eip-7496/ERC721DynamicTraits.sol new file mode 100644 index 00000000000000..49e3bdda10cbb1 --- /dev/null +++ b/assets/eip-7496/ERC721DynamicTraits.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {Ownable} from "openzeppelin-contracts/access/Ownable.sol"; +import {DynamicTraits} from "./DynamicTraits.sol"; + +contract ERC721DynamicTraits is DynamicTraits, Ownable, ERC721 { + constructor() Ownable(msg.sender) ERC721("ERC721DynamicTraits", "ERC721DT") { + _traitMetadataURI = "https://example.com"; + } + + function setTrait(uint256 tokenId, bytes32 traitKey, bytes32 value) external virtual override onlyOwner { + // Revert if the token doesn't exist. + _requireOwned(tokenId); + + _setTrait(tokenId, traitKey, value); + } + + function getTraitValue(uint256 tokenId, bytes32 traitKey) + public + view + virtual + override + returns (bytes32 traitValue) + { + // Revert if the token doesn't exist. + _requireOwned(tokenId); + + return DynamicTraits.getTraitValue(tokenId, traitKey); + } + + function getTraitValues(uint256 tokenId, bytes32[] calldata traitKeys) + public + view + virtual + override + returns (bytes32[] memory traitValues) + { + // Revert if the token doesn't exist. + _requireOwned(tokenId); + + return DynamicTraits.getTraitValues(tokenId, traitKeys); + } + + function setTraitMetadataURI(string calldata uri) external onlyOwner { + _setTraitMetadataURI(uri); + } + + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, DynamicTraits) returns (bool) { + return ERC721.supportsInterface(interfaceId) || DynamicTraits.supportsInterface(interfaceId); + } +} From 4ceb2260f989aa593bdbb287a6c45cefaa5ce77f Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Wed, 18 Oct 2023 14:45:46 -0700 Subject: [PATCH 22/25] updates --- EIPS/eip-7496.md | 14 ++++++++++---- assets/eip-7496/DynamicTraits.sol | 2 +- assets/eip-7496/ERC721DynamicTraits.sol | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index c0eeb3f074bfa0..68ecf1b050cb39 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -28,9 +28,9 @@ Contracts implementing this EIP MUST include the events, getters, and setters as ```solidity interface IERC7496 { /* Events */ - event TraitUpdated(bytes32 indexed traitKey, uint256 indexed tokenId, bytes32 value); - event TraitUpdatedBulkRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId); - event TraitUpdatedBulkList(bytes32 indexed traitKey, uint256[] tokenIds); + 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(); /* Getters */ @@ -163,7 +163,13 @@ The `dataType` object MAY have a `valueMappings` object defined. If the `valueMa ### 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. +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()`. diff --git a/assets/eip-7496/DynamicTraits.sol b/assets/eip-7496/DynamicTraits.sol index 88cb17667fa21f..37bbea52a4d6d4 100644 --- a/assets/eip-7496/DynamicTraits.sol +++ b/assets/eip-7496/DynamicTraits.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.19; import {EnumerableSet} from "openzeppelin-contracts/contracts/utils/structs/EnumerableSet.sol"; diff --git a/assets/eip-7496/ERC721DynamicTraits.sol b/assets/eip-7496/ERC721DynamicTraits.sol index 49e3bdda10cbb1..b07c40cda15cc5 100644 --- a/assets/eip-7496/ERC721DynamicTraits.sol +++ b/assets/eip-7496/ERC721DynamicTraits.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.19; import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; From 26ea0d7d22e35b1fe495ca245296fa781981650c Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Thu, 19 Oct 2023 12:42:03 -0700 Subject: [PATCH 23/25] add "UniformValue" events --- EIPS/eip-7496.md | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index 68ecf1b050cb39..32611f4a4d5821 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -23,14 +23,16 @@ Trait values for non-fungible tokens are often stored offchain. This makes it di 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. +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 `0xaf332f3e`, the 4 byte `interfaceId` for this ERC. ```solidity -interface IERC7496 { +interface IERC7496 is IERC165 { /* 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 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 */ @@ -163,13 +165,19 @@ The `dataType` object MAY have a `valueMappings` object defined. If the `valueMa ### Events -Updating traits MUST either emit the `TraitUpdated`, `TraitUpdatedBulkRange` or `TraitUpdatedBulkList` event. +Updating traits MUST emit one of: -For the event `TraitUpdatedBulkRange`, the `fromTokenId` and `toTokenId` MUST be a consecutive range of tokens IDs and MUST be treated as an inclusive range. +- `TraitUpdated` +- `TraitUpdatedRange` +- `TraitUpdatedRangeUniformValue` +- `TraitUpdatedList` +- `TraitUpdatedListUniformValue` -For the event `TraitUpdatedBulkList`, the `tokenIds` MAY be in any order. +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 `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. +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 URI MUST emit the event `TraitMetadataURIUpdated` so offchain indexers can be notified to query the contract for the latest changes via `getTraitMetadataURI()`. From 9e2770a3a95191923f259ab052561f7f98e32a58 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 23 Oct 2023 12:23:28 -0700 Subject: [PATCH 24/25] updates from eip editor review: - remove IERC165 inheritance from interface for easier dev - add to motivation section. improve rationale section - add test case file, link to assets folder directly --- EIPS/eip-7496.md | 18 ++- assets/eip-7496/ERC721DynamicTraits.t.sol | 131 ++++++++++++++++++++++ 2 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 assets/eip-7496/ERC721DynamicTraits.t.sol diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index 32611f4a4d5821..60575f43f21712 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -17,7 +17,11 @@ This specification introduces a new interface that extends [ERC-721](./eip-721.m ## 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. +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. + +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. ## Specification @@ -26,7 +30,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S 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 `0xaf332f3e`, the 4 byte `interfaceId` for this ERC. ```solidity -interface IERC7496 is IERC165 { +interface IERC7496 { /* Events */ event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 traitValue); event TraitUpdatedRange(bytes32 indexed traitKey, uint256 fromTokenId, uint256 toTokenId); @@ -179,7 +183,7 @@ 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 URI MUST emit the event `TraitMetadataURIUpdated` so offchain indexers can be notified to query the contract for the latest changes via `getTraitMetadataURI()`. +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` @@ -191,7 +195,9 @@ If the trait has a `valueMappings` entry defined for the desired value being set ## 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. +The design of this specification is primarily a key-value mapping for maximum flexibility. + +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 @@ -199,11 +205,11 @@ As a new EIP, no backwards compatibility issues are present, except for the poin ## Test Cases -Authors have included Foundry tests covering functionality of the specification in the assets folder. +Authors have included Foundry tests covering functionality of the specification in the [assets folder](../assets/eip-7496/DynamicTraits.t.sol). ## Reference Implementation -Authors have included reference implementations of the specification in the assets folder. +Authors have included reference implementations of the specification in the [assets folder](../assets/eip-7496/DynamicTraits.sol). ## Security Considerations diff --git a/assets/eip-7496/ERC721DynamicTraits.t.sol b/assets/eip-7496/ERC721DynamicTraits.t.sol new file mode 100644 index 00000000000000..5804044b528f03 --- /dev/null +++ b/assets/eip-7496/ERC721DynamicTraits.t.sol @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {IERC721Errors} from "openzeppelin-contracts/contracts/interfaces/draft-IERC6093.sol"; +import {ERC721} from "openzeppelin-contracts/contracts/token/ERC721/ERC721.sol"; +import {Ownable} from "openzeppelin-contracts/contracts/access/Ownable.sol"; +import {IERC7496} from "src/dynamic-traits/interfaces/IERC7496.sol"; +import {ERC721DynamicTraits, DynamicTraits} from "src/dynamic-traits/ERC721DynamicTraits.sol"; +import {Solarray} from "solarray/Solarray.sol"; + +contract ERC721DynamicTraitsMintable is ERC721DynamicTraits { + constructor() ERC721DynamicTraits() {} + + function mint(address to, uint256 tokenId) public onlyOwner { + _mint(to, tokenId); + } +} + +contract ERC721DynamicTraitsTest is Test { + ERC721DynamicTraitsMintable token; + + /* Events */ + event TraitUpdated(bytes32 indexed traitKey, uint256 tokenId, bytes32 trait); + 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(); + + function setUp() public { + token = new ERC721DynamicTraitsMintable(); + } + + function testSupportsInterfaceId() public { + assertTrue(token.supportsInterface(type(IERC7496).interfaceId)); + } + + function testReturnsValueSet() public { + bytes32 key = bytes32("test.key"); + bytes32 value = bytes32("foo"); + uint256 tokenId = 12345; + token.mint(address(this), tokenId); + + vm.expectEmit(true, true, true, true); + emit TraitUpdated(key, tokenId, value); + + token.setTrait(tokenId, key, value); + + assertEq(token.getTraitValue(tokenId, key), value); + } + + function testOnlyOwnerCanSetValues() public { + address alice = makeAddr("alice"); + vm.prank(alice); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, alice)); + token.setTrait(0, bytes32("test"), bytes32("test")); + } + + function testSetTrait_Unchanged() public { + bytes32 key = bytes32("test.key"); + bytes32 value = bytes32("foo"); + uint256 tokenId = 1; + token.mint(address(this), tokenId); + + token.setTrait(tokenId, key, value); + vm.expectRevert(DynamicTraits.TraitValueUnchanged.selector); + token.setTrait(tokenId, key, value); + } + + function testGetTraitValues() public { + bytes32 key1 = bytes32("test.key.one"); + bytes32 key2 = bytes32("test.key.two"); + bytes32 value1 = bytes32("foo"); + bytes32 value2 = bytes32("bar"); + uint256 tokenId = 1; + token.mint(address(this), tokenId); + + token.setTrait(tokenId, key1, value1); + token.setTrait(tokenId, key2, value2); + + bytes32[] memory values = token.getTraitValues(tokenId, Solarray.bytes32s(key1, key2)); + assertEq(values[0], value1); + assertEq(values[1], value2); + } + + function testGetAndSetTraitMetadataURI() public { + string memory uri = "https://example.com/labels.json"; + token.setTraitMetadataURI(uri); + assertEq(token.getTraitMetadataURI(), uri); + + vm.prank(address(0x1234)); + vm.expectRevert(abi.encodeWithSelector(Ownable.OwnableUnauthorizedAccount.selector, address(0x1234))); + token.setTraitMetadataURI(uri); + } + + function testGetTraitValue_NonexistantToken() public { + bytes32 key = bytes32("test.key"); + bytes32 value = bytes32(uint256(1)); + uint256 tokenId = 1; + + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId)); + token.setTrait(tokenId, key, value); + + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId)); + token.getTraitValue(tokenId, key); + + vm.expectRevert(abi.encodeWithSelector(IERC721Errors.ERC721NonexistentToken.selector, tokenId)); + token.getTraitValues(tokenId, Solarray.bytes32s(key)); + } + + function testGetTraitValue_ZeroValue() public { + bytes32 key = bytes32("test.key"); + uint256 tokenId = 1; + token.mint(address(this), tokenId); + + bytes32 result = token.getTraitValue(tokenId, key); + assertEq(result, bytes32(0), "should return bytes32(0)"); + } + + function testGetTraitValues_ZeroValue() public { + bytes32 key = bytes32("test.key"); + uint256 tokenId = 1; + token.mint(address(this), tokenId); + + bytes32[] memory result = token.getTraitValues(tokenId, Solarray.bytes32s(key)); + assertEq(result[0], bytes32(0), "should return bytes32(0)"); + } +} From eebe3a25fb21fc321a294722d96cbc03d06b86db Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 23 Oct 2023 12:50:29 -0700 Subject: [PATCH 25/25] fix asset link --- EIPS/eip-7496.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7496.md b/EIPS/eip-7496.md index 60575f43f21712..6f65581e828908 100644 --- a/EIPS/eip-7496.md +++ b/EIPS/eip-7496.md @@ -205,7 +205,7 @@ As a new EIP, no backwards compatibility issues are present, except for the poin ## Test Cases -Authors have included Foundry tests covering functionality of the specification in the [assets folder](../assets/eip-7496/DynamicTraits.t.sol). +Authors have included Foundry tests covering functionality of the specification in the [assets folder](../assets/eip-7496/ERC721DynamicTraits.t.sol). ## Reference Implementation