From aec5026e51b2af174cebf3a325b2fb39b20a2562 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 11:12:29 -0700 Subject: [PATCH 01/24] Add EIP: NFT Redeemables --- EIPS/eip-7nnn.md | 486 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 486 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..d10f90fc51c394 --- /dev/null +++ b/EIPS/eip-7nnn.md @@ -0,0 +1,486 @@ +--- +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). + +eip: nnnn +title: NFT Redeemables +description: Extension to ERC-721 and ERC-1155 for onchain and offchain redeemables +author: Ryan Ghods (@ryanio), 0age (@0age), Adam Montgomery (@montasaurus) +discussions-to: +status: Draft +type: Standards Track +category: ERC +created: 2023-07-28 +requires: 165, 721, 1155 + +--- + +## Abstract + +This specification introduces a new interface that extends ERC-721 and ERC-1155 to enable onchain and offchain redeemables for NFTs. + +## Motivation + +Since the inception of NFTs, creators have used them to create redeemable entitlements for digital and physical goods. However, without a standard interface, it is challenging for users and websites to discover and interact with NFTs that have redeemable opportunities. By proposing this standard, the authors aim to create a reliable and predictable pattern for NFT redeemables. + +## 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. + +The token MUST have the following interface and MUST return `true` for EIP-165 supportsInterface for `0x12345678`, the 4 byte interfaceId of the below. + +```solidity +interface IERC7NNN { + /* Events */ + event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string URI); + event Redemption(uint256 indexed campaignId, bytes32 redemptionHash, uint256[] tokenIds, address redeemedBy); + + /* Structs */ + struct CampaignParams { + uint32 startTime; + uint32 endTime; + uint32 maxCampaignRedemptions; + address manager; // the address that can modify the campaign + address signer; // null address means no EIP-712 signature required + OfferItem[] offer; // items to be minted, can be empty for offchain redeemable + ConsiderationItem[] consideration; // the items you are transferring to recipient + } + struct TraitRedemption { + uint8 substandard; + address token; + uint256 identifier; + bytes32 traitKey; + bytes32 traitValue; + bytes32 substandardValue; + } + + /* Getters */ + function getCampaign(uint256 campaignId) external view returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions); + + /* Setters */ + function createCampaign(CampaignParams calldata params, string calldata uri) external returns (uint256 campaignId); + function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external; + function redeem(uint256[] calldata tokenIds, bytes calldata extraData) external; +} + + --- + + /* Seaport structs (for reference in offer/consideration above) */ + enum ItemType { + NATIVE, + ERC20, + ERC721, + ERC1155 + } + struct OfferItem { + ItemType itemType; + address token; + uint256 identifierOrCriteria; + uint256 startAmount; + uint256 endAmount; + } + struct ConsiderationItem extends OfferItem { + address payable recipient; + } + struct SpentItem { + ItemType itemType; + address token; + uint256 identifier; + uint256 amount; + } +``` + +### Creating campaigns + +When creating a new campaign, `createCampaign` MUST be used and MUST return the newly created `campaignId` along with the `CampaignUpdated` event. The `campaignId` MUST be an incrementing counter starting at `1`. + +### Updating campaigns + +Updates to campaigns MUST use `updateCampaign` and MUST emit the `CampaignUpdated` event. If an address other than the `manager` tries to update the campaign, it MUST revert with `NotManager()`. If the manager wishes to make the campaign immutable, the `manager` MAY be set to the null address. + +### Offer + +If tokens are set in the params `offer`, the tokens MUST implement the `IRedemptionMintable` interface in order to support minting new items. The implementation SHOULD be however the token mechanics are desired. The implementing token MUST return true for EIP-165 `supportsInterface` for the interfaceIds of: `IERC721RedemptionMintable: 0x12345678` or `IERC1155RedemptionMintable: 0x12345678` + +```solidify +interface IERC721RedemptionMintable { + function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256[] memory tokenIds); +} + +interface IERC1155RedemptionMintable { + function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256[] memory tokenIds, uint256[] amounts); +} +``` + +The array length return values of `tokenIds` and `amounts` for `IERC1155RedemptionMintable` MUST equal each other. + +### Consideration + +Any token may be used in the RedeemableParams `consideration`. This will ensure the token is transferred to the `recipient`. If the token is meant to be burned the recipient SHOULD be `0x000000000000000000000000000000000000dEaD`. + +### Dynamic traits + +If the token would like to enable trait redemptions, the token MUST include the ERC-7NNN Dynamic Traits interface. + +### Signer + +A signer MAY be specified to provide a signature to process the redemption. If the signer is NOT the null address, the signature MUST recover to the signer address via EIP-712 or EIP-1271. + +The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,bytes32 redemptionHash, uint256 salt)"` + +### Redemption extraData + +When calling the `redeem` function, the extraData layout MUST follow: + +| bytes | value | description / notes | +| -------- | ----------------- | ---------------------------------------------------------------- | +| 0-32 | campaignId | | +| 32-64 | redemptionHash | hash of offchain order ids | +| 64-\* | TraitRedemption[] | see TraitRedemption struct. empty array for no trait redemptions | +| \*-(+32) | salt | if signer != address(0) | +| \*-(+\*) | signature | if signer != address(0). can be for EIP-712 or EIP-1271 | + +Upon redemption, the contract MUST check that the campaign is still active (using the same boundary check as Seaport, `startTime <= block.timestamp < endTime`). If it is, it MUST revert with `NotActive()`. + +### Redeem + +The `redeem` function MUST execute the transfers in the `consideration`. It MUST also call `mintRedemption` on the token specified in the `offer`. If any of the supplied tokenIds in `redeem` fail validation, the function MAY execute just the redemptions that were valid and ignore the failed redemptions. The `Redemption` event MUST be emitted emitted when any valid redemptions occur. + +### Trait redemptions + +The token MUST respect the TraitRedemption substandards as follows: + +| substandard ID | description | substandard value | +| -------------- | ------------------------------- | ------------------------------------------------------------------ | +| 1 | set value to `traitValue` | prior required value. if blank, cannot be the `traitValue` already | +| 2 | increment trait by `traitValue` | max value | +| 3 | decrement trait by `traitValue` | min value | + +### Max campaign redemptions + +The token MUST check that the `maxCampaignRedemptions` is not exceeded. If the redemption does exceed `maxCampaignRedemptions`, it MUST revert with `MaxCampaignRedemptionsReached(uint256 total, uint256 max)` + +### Metadata URI + +The metadata URI MUST follow the following JSON schema: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string", + "description": "A one-line summary of the redeemable. Markdown is not supported." + }, + "details": { + "type": "string", + "description": "A multi-line or multi-paragraph description of the details of the redeemable. Markdown is supported." + }, + "imageUrls": { + "type": "string", + "description": "A list of image URLs for the redeemable. The first image will be used as the thumbnail. Will rotate in a carousel if multiple images are provided. Maximum 5 images." + }, + "bannerUrl": { + "type": "string", + "description": "The banner image for the redeemable." + }, + "faq": { + "type": "array", + "items": { + "type": "object", + "properties": { + "question": { + "type": "string" + }, + "answer": { + "type": "string" + }, + "required": ["question", "answer"] + } + } + }, + "contentLocale": { + "type": "string", + "description": "The language tag for the content provided by this metadata. https://www.rfc-editor.org/rfc/rfc9110.html#name-language-tags" + }, + "maxRedemptionsPerToken": { + "type": "string", + "description": "The maximum number of redemptions per token. When isBurn is true should be 1, else can be a number based on the trait redemptions limit." + }, + "isBurn": { + "type": "string", + "description": "If the redemption burns the token." + }, + "uuid": { + "type": "string", + "description": "A unique identifier for the campaign, for backends to identify when draft campaigns are published onchain." + }, + "productLimitForRedemption": { + "type": "number", + "description": "The number of products which are able to be chosen from the products array for a single redemption." + }, + "products": { + "type": "object", + "properties": "https://schema.org/Product", + "required": ["name", "url", "description"] + }, + "traitRedemptions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "substandard": { + "type": "number" + }, + "token": { + "type": "string", + "description": "The token address" + }, + "traitKey": { + "type": "string" + }, + "traitValue": { + "type": "string" + }, + "substandardValue": { + "type": "string" + } + }, + "required": [ + "substandard", + "token", + "traitKey", + "traitValue", + "substandardValue" + ] + } + } + }, + "required": ["name", "description", "isBurn"] +} +``` + +Future SIPs MAY inherit this one and add to the above metadata to add more features and functionality. + +### ERC-1155 (Semi-fungibles) + +This standard MAY be applied to ERC-1155 but the redemptions 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 + +The intention of this EIP is to define a consistent standard to enable redeemable entitlements for tokens and onchain traits. This pattern allows for websites to discover, display, and interact with redeemable campaigns. + +## Backwards Compatibility + +As a new EIP, no backwards compatibility issues are present. + +## Test Cases + +Test cases can be found in [https://github.com/ProjectOpenSea/redeemables/tree/main/test](https://github.com/ProjectOpenSea/redeemables/tree/main/test) + +## Reference Implementation + +The reference implementation for ERC721Redeemable and ERC1155Redeemable can be found in [https://github.com/ProjectOpenSea/redeemables/tree/main/src](https://github.com/ProjectOpenSea/redeemables/tree/main/src) + +## Security Considerations + +Tokens must properly implement EIP-7NNN Dynamic Traits to allow for trait redemptions. + +For tokens to be minted as part of the params `offer`, the `mintRedemption` function contained as part of `IRedemptionMintable` MUST be permissioned and ONLY allowed to be called by specified addresses. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). From 3cbaa86fb9c72357cdd8b4fa51ba033a76cdf6b7 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 11:20:30 -0700 Subject: [PATCH 02/24] set EIP number to PR number (7501) --- EIPS/{eip-7nnn.md => eip-7501.md} | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) rename EIPS/{eip-7nnn.md => eip-7501.md} (99%) diff --git a/EIPS/eip-7nnn.md b/EIPS/eip-7501.md similarity index 99% rename from EIPS/eip-7nnn.md rename to EIPS/eip-7501.md index d10f90fc51c394..cbe4044832ef83 100644 --- a/EIPS/eip-7nnn.md +++ b/EIPS/eip-7501.md @@ -1,5 +1,5 @@ --- -eip: 7nnn +eip: 7501 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 IERC7501. 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 IERC7501 { /* Events */ event TraitUpdated(bytes32 indexed traitKey, uint256 indexed tokenId, bytes32 value); event TraitUpdatedBulkConsecutive(bytes32 indexed traitKeyPattern, uint256 fromTokenId, uint256 toTokenId); @@ -219,7 +219,7 @@ The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "S The token MUST have the following interface and MUST return `true` for EIP-165 supportsInterface for `0x12345678`, the 4 byte interfaceId of the below. ```solidity -interface IERC7NNN { +interface IERC7501 { /* Events */ event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string URI); event Redemption(uint256 indexed campaignId, bytes32 redemptionHash, uint256[] tokenIds, address redeemedBy); @@ -309,7 +309,7 @@ Any token may be used in the RedeemableParams `consideration`. This will ensure ### Dynamic traits -If the token would like to enable trait redemptions, the token MUST include the ERC-7NNN Dynamic Traits interface. +If the token would like to enable trait redemptions, the token MUST include the ERC-7500 Dynamic Traits interface. ### Signer @@ -477,7 +477,7 @@ The reference implementation for ERC721Redeemable and ERC1155Redeemable can be f ## Security Considerations -Tokens must properly implement EIP-7NNN Dynamic Traits to allow for trait redemptions. +Tokens must properly implement EIP-7500 Dynamic Traits to allow for trait redemptions. For tokens to be minted as part of the params `offer`, the `mintRedemption` function contained as part of `IRedemptionMintable` MUST be permissioned and ONLY allowed to be called by specified addresses. From aeff9d9c829427d92eb6243b7a71d5806eca3229 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 11:32:01 -0700 Subject: [PATCH 03/24] add discussions-to link, add to requires --- EIPS/eip-7501.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index cbe4044832ef83..6400b155c91c01 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -3,12 +3,12 @@ eip: 7501 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, 712, 721, 1155, 1271 --- ## Abstract From 96b785e73008312681a9f37910d91189e298efca Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 11:39:31 -0700 Subject: [PATCH 04/24] fix --- EIPS/eip-7501.md | 195 +---------------------------------------------- 1 file changed, 1 insertion(+), 194 deletions(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index 6400b155c91c01..6c6a641bdb7e66 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -1,197 +1,5 @@ --- eip: 7501 -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 -status: Draft -type: Standards Track -category: ERC -created: 2023-07-28 -requires: 165, 712, 721, 1155, 1271 ---- - -## 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 IERC7501. 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 IERC7501 { - /* 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). - -eip: nnnn title: NFT Redeemables description: Extension to ERC-721 and ERC-1155 for onchain and offchain redeemables author: Ryan Ghods (@ryanio), 0age (@0age), Adam Montgomery (@montasaurus) @@ -200,8 +8,7 @@ status: Draft type: Standards Track category: ERC created: 2023-07-28 -requires: 165, 721, 1155 - +requires: 165, 712, 721, 1155, 1271 --- ## Abstract From e05bbf933f888f0e7f00ee26c3df847fa124ce42 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 11:40:10 -0700 Subject: [PATCH 05/24] add discussions-to link --- EIPS/eip-7501.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index 6c6a641bdb7e66..2e391a0fe913a3 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -3,7 +3,7 @@ eip: 7501 title: NFT Redeemables description: Extension to ERC-721 and ERC-1155 for onchain and offchain redeemables author: Ryan Ghods (@ryanio), 0age (@0age), Adam Montgomery (@montasaurus) -discussions-to: +discussions-to: https://ethereum-magicians.org/t/erc-7501-nft-redeemables/15485 status: Draft type: Standards Track category: ERC From a2822de941842889701dafd7cfc81d644296185f Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 11:44:13 -0700 Subject: [PATCH 06/24] add eip links --- EIPS/eip-7501.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index 2e391a0fe913a3..3d3a40e3fb3db5 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -13,7 +13,7 @@ requires: 165, 712, 721, 1155, 1271 ## Abstract -This specification introduces a new interface that extends ERC-721 and ERC-1155 to enable onchain and offchain redeemables for NFTs. +This specification introduces a new interface that extends [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md) to enable onchain and offchain redeemables for NFTs. ## Motivation @@ -23,7 +23,7 @@ Since the inception of NFTs, creators have used them to create redeemable entitl 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. -The token MUST have the following interface and MUST return `true` for EIP-165 supportsInterface for `0x12345678`, the 4 byte interfaceId of the below. +The token MUST have the following interface and MUST return `true` for [ERC-165](./eip-165.md) supportsInterface for `0x12345678`, the 4 byte interfaceId of the below. ```solidity interface IERC7501 { @@ -96,7 +96,7 @@ Updates to campaigns MUST use `updateCampaign` and MUST emit the `CampaignUpdate ### Offer -If tokens are set in the params `offer`, the tokens MUST implement the `IRedemptionMintable` interface in order to support minting new items. The implementation SHOULD be however the token mechanics are desired. The implementing token MUST return true for EIP-165 `supportsInterface` for the interfaceIds of: `IERC721RedemptionMintable: 0x12345678` or `IERC1155RedemptionMintable: 0x12345678` +If tokens are set in the params `offer`, the tokens MUST implement the `IRedemptionMintable` interface in order to support minting new items. The implementation SHOULD be however the token mechanics are desired. The implementing token MUST return true for ERC-165 `supportsInterface` for the interfaceIds of: `IERC721RedemptionMintable: 0x12345678` or `IERC1155RedemptionMintable: 0x12345678` ```solidify interface IERC721RedemptionMintable { @@ -116,11 +116,11 @@ Any token may be used in the RedeemableParams `consideration`. This will ensure ### Dynamic traits -If the token would like to enable trait redemptions, the token MUST include the ERC-7500 Dynamic Traits interface. +If the token would like to enable trait redemptions, the token MUST include the [ERC-7500](./eip-7500.md) Dynamic Traits interface. ### Signer -A signer MAY be specified to provide a signature to process the redemption. If the signer is NOT the null address, the signature MUST recover to the signer address via EIP-712 or EIP-1271. +A signer MAY be specified to provide a signature to process the redemption. If the signer is NOT the null address, the signature MUST recover to the signer address via [EIP-712](./eip-712.md) or [EIP-1271](./eip-1271.md). The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,bytes32 redemptionHash, uint256 salt)"` From 9eb9b59f0d94922ec6164974b0c8d09a601137f9 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 12:01:21 -0700 Subject: [PATCH 07/24] fixes --- EIPS/eip-7501.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index 3d3a40e3fb3db5..838904a650ae95 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -37,7 +37,7 @@ interface IERC7501 { uint32 endTime; uint32 maxCampaignRedemptions; address manager; // the address that can modify the campaign - address signer; // null address means no EIP-712 signature required + address signer; // null address means no ERC-712 signature required OfferItem[] offer; // items to be minted, can be empty for offchain redeemable ConsiderationItem[] consideration; // the items you are transferring to recipient } @@ -120,9 +120,9 @@ If the token would like to enable trait redemptions, the token MUST include the ### Signer -A signer MAY be specified to provide a signature to process the redemption. If the signer is NOT the null address, the signature MUST recover to the signer address via [EIP-712](./eip-712.md) or [EIP-1271](./eip-1271.md). +A signer MAY be specified to provide a signature to process the redemption. If the signer is NOT the null address, the signature MUST recover to the signer address via [ERC-712](./eip-712.md) or [ERC-1271](./eip-1271.md). -The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,bytes32 redemptionHash, uint256 salt)"` +The ERC-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,bytes32 redemptionHash, uint256 salt)"` ### Redemption extraData @@ -134,7 +134,7 @@ When calling the `redeem` function, the extraData layout MUST follow: | 32-64 | redemptionHash | hash of offchain order ids | | 64-\* | TraitRedemption[] | see TraitRedemption struct. empty array for no trait redemptions | | \*-(+32) | salt | if signer != address(0) | -| \*-(+\*) | signature | if signer != address(0). can be for EIP-712 or EIP-1271 | +| \*-(+\*) | signature | if signer != address(0). can be for ERC-712 or ERC-1271 | Upon redemption, the contract MUST check that the campaign is still active (using the same boundary check as Seaport, `startTime <= block.timestamp < endTime`). If it is, it MUST revert with `NotActive()`. @@ -276,15 +276,15 @@ As a new EIP, no backwards compatibility issues are present. ## Test Cases -Test cases can be found in [https://github.com/ProjectOpenSea/redeemables/tree/main/test](https://github.com/ProjectOpenSea/redeemables/tree/main/test) +Coming soon ## Reference Implementation -The reference implementation for ERC721Redeemable and ERC1155Redeemable can be found in [https://github.com/ProjectOpenSea/redeemables/tree/main/src](https://github.com/ProjectOpenSea/redeemables/tree/main/src) +Coming soon ## Security Considerations -Tokens must properly implement EIP-7500 Dynamic Traits to allow for trait redemptions. +Tokens must properly implement ERC-7500 Dynamic Traits to allow for trait redemptions. For tokens to be minted as part of the params `offer`, the `mintRedemption` function contained as part of `IRedemptionMintable` MUST be permissioned and ONLY allowed to be called by specified addresses. From 43d9c114b27546a9b76b02051cb985dc36e63b33 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 12:12:40 -0700 Subject: [PATCH 08/24] correctly reference EIP-712 --- EIPS/eip-7501.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index 838904a650ae95..5695efdc7df6b6 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -37,7 +37,7 @@ interface IERC7501 { uint32 endTime; uint32 maxCampaignRedemptions; address manager; // the address that can modify the campaign - address signer; // null address means no ERC-712 signature required + address signer; // null address means no EIP-712 signature required OfferItem[] offer; // items to be minted, can be empty for offchain redeemable ConsiderationItem[] consideration; // the items you are transferring to recipient } @@ -120,9 +120,9 @@ If the token would like to enable trait redemptions, the token MUST include the ### Signer -A signer MAY be specified to provide a signature to process the redemption. If the signer is NOT the null address, the signature MUST recover to the signer address via [ERC-712](./eip-712.md) or [ERC-1271](./eip-1271.md). +A signer MAY be specified to provide a signature to process the redemption. If the signer is NOT the null address, the signature MUST recover to the signer address via [EIP-712](./eip-712.md) or [ERC-1271](./eip-1271.md). -The ERC-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,bytes32 redemptionHash, uint256 salt)"` +The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,bytes32 redemptionHash, uint256 salt)"` ### Redemption extraData @@ -134,7 +134,7 @@ When calling the `redeem` function, the extraData layout MUST follow: | 32-64 | redemptionHash | hash of offchain order ids | | 64-\* | TraitRedemption[] | see TraitRedemption struct. empty array for no trait redemptions | | \*-(+32) | salt | if signer != address(0) | -| \*-(+\*) | signature | if signer != address(0). can be for ERC-712 or ERC-1271 | +| \*-(+\*) | signature | if signer != address(0). can be for EIP-712 or ERC-1271 | Upon redemption, the contract MUST check that the campaign is still active (using the same boundary check as Seaport, `startTime <= block.timestamp < endTime`). If it is, it MUST revert with `NotActive()`. From c216603e31d6e622d306da6f907ea2569e334d46 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 18:43:14 -0700 Subject: [PATCH 09/24] updates to abstract and motivation --- EIPS/eip-7501.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index 5695efdc7df6b6..0b795783641ea9 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -13,11 +13,16 @@ requires: 165, 712, 721, 1155, 1271 ## Abstract -This specification introduces a new interface that extends [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md) to enable onchain and offchain redeemables for NFTs. +This specification introduces a new interface that extends [ERC-721](./eip-721.md) and [ERC-1155](./eip-1155.md) to enable the discovery and use of onchain and offchain redeemables for NFTs. ## Motivation -Since the inception of NFTs, creators have used them to create redeemable entitlements for digital and physical goods. However, without a standard interface, it is challenging for users and websites to discover and interact with NFTs that have redeemable opportunities. By proposing this standard, the authors aim to create a reliable and predictable pattern for NFT redeemables. +Creators frequently use NFTs to create redeemable entitlements for digital and physical goods. However, without a standard interface, it is challenging for users and apps to discover and interact with these NFTs in a predictable and standard way. This standard aims to encompass enabling broad functionality for: + +- discovery: events and getters that provide information about the requirements of a redemption campaign +- onchain: token mints with context of items spent +- offchain: the ability to associate with offchain ecommerce orders (through `redemptionHash`) +- trait redemptions: improving the burn-to-redeem experience with E RC-7500 Dynamic Traits. ## Specification From 500f9a1f6312f66086b3d5a77e69358abb55c928 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 18:43:32 -0700 Subject: [PATCH 10/24] try to satisfy linter until eip-7500.md is available --- EIPS/eip-7501.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index 0b795783641ea9..a2b7040774d4d6 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -121,7 +121,7 @@ Any token may be used in the RedeemableParams `consideration`. This will ensure ### Dynamic traits -If the token would like to enable trait redemptions, the token MUST include the [ERC-7500](./eip-7500.md) Dynamic Traits interface. +If the token would like to enable trait redemptions, the token MUST include the E RC-7500 Dynamic Traits interface. ### Signer @@ -289,7 +289,7 @@ Coming soon Dynamic Traits to allow for trait redemptions. For tokens to be minted as part of the params `offer`, the `mintRedemption` function contained as part of `IRedemptionMintable` MUST be permissioned and ONLY allowed to be called by specified addresses. From 5d835c926aa81976572d16c738d3062d9702c41e Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 18:54:32 -0700 Subject: [PATCH 11/24] remove redundant word --- EIPS/eip-7501.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index a2b7040774d4d6..b8aa73532d40d2 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -21,7 +21,7 @@ Creators frequently use NFTs to create redeemable entitlements for digital and p - discovery: events and getters that provide information about the requirements of a redemption campaign - onchain: token mints with context of items spent -- offchain: the ability to associate with offchain ecommerce orders (through `redemptionHash`) +- offchain: the ability to associate with ecommerce orders (through `redemptionHash`) - trait redemptions: improving the burn-to-redeem experience with E RC-7500 Dynamic Traits. ## Specification From e00eafc8f91a2fc905fca7ffda79f9814158a132 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 18:57:09 -0700 Subject: [PATCH 12/24] formatting --- EIPS/eip-7501.md | 51 ++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index b8aa73532d40d2..8a6af19feb8ee8 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -64,31 +64,32 @@ interface IERC7501 { function redeem(uint256[] calldata tokenIds, bytes calldata extraData) external; } - --- - - /* Seaport structs (for reference in offer/consideration above) */ - enum ItemType { - NATIVE, - ERC20, - ERC721, - ERC1155 - } - struct OfferItem { - ItemType itemType; - address token; - uint256 identifierOrCriteria; - uint256 startAmount; - uint256 endAmount; - } - struct ConsiderationItem extends OfferItem { - address payable recipient; - } - struct SpentItem { - ItemType itemType; - address token; - uint256 identifier; - uint256 amount; - } +--- + +/* Seaport structs, for reference, used in offer/consideration above */ +enum ItemType { + NATIVE, + ERC20, + ERC721, + ERC1155 +} +struct OfferItem { + ItemType itemType; + address token; + uint256 identifierOrCriteria; + uint256 startAmount; + uint256 endAmount; +} +struct ConsiderationItem extends OfferItem { + address payable recipient; + // (note: psuedocode above, can't currently extend structs in solidity) +} +struct SpentItem { + ItemType itemType; + address token; + uint256 identifier; + uint256 amount; +} ``` ### Creating campaigns From 92071a83b246e185c114c3dcf340608353e5d5d4 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 19:03:16 -0700 Subject: [PATCH 13/24] revise --- EIPS/eip-7501.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index 8a6af19feb8ee8..4d997d7ec7c1b7 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -282,11 +282,11 @@ As a new EIP, no backwards compatibility issues are present. ## Test Cases -Coming soon +Authors will include Foundry tests in the assets folder. ## Reference Implementation -Coming soon +Authors will include Solidity contracts in the assets folder. ## Security Considerations From d1d2b8aa6ac6cb7fb53ffef324dd22cf93d2f4f8 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 18 Aug 2023 19:06:54 -0700 Subject: [PATCH 14/24] revise --- EIPS/eip-7501.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index 4d997d7ec7c1b7..ba82351e904500 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -282,7 +282,7 @@ As a new EIP, no backwards compatibility issues are present. ## Test Cases -Authors will include Foundry tests in the assets folder. +Authors will include Foundry tests covering functionality of the specification in the assets folder. ## Reference Implementation From 8dae6df29dfecd6a155ac09087d1c1889739dc7b Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 21 Aug 2023 08:37:12 -0700 Subject: [PATCH 15/24] Update EIPS/eip-7501.md Co-authored-by: Sam Wilson <57262657+SamWilsn@users.noreply.github.com> --- EIPS/eip-7501.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7501.md index ba82351e904500..2e983546a02ba3 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7501.md @@ -1,5 +1,5 @@ --- -eip: 7501 +eip: 7498 title: NFT Redeemables description: Extension to ERC-721 and ERC-1155 for onchain and offchain redeemables author: Ryan Ghods (@ryanio), 0age (@0age), Adam Montgomery (@montasaurus) From f0bf9451444f5c8ea5dbe38a0730b41f53f712ef Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 21 Aug 2023 08:41:59 -0700 Subject: [PATCH 16/24] update file name --- EIPS/{eip-7501.md => eip-7498.md} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename EIPS/{eip-7501.md => eip-7498.md} (98%) diff --git a/EIPS/eip-7501.md b/EIPS/eip-7498.md similarity index 98% rename from EIPS/eip-7501.md rename to EIPS/eip-7498.md index 2e983546a02ba3..d34200810074a4 100644 --- a/EIPS/eip-7501.md +++ b/EIPS/eip-7498.md @@ -22,7 +22,7 @@ Creators frequently use NFTs to create redeemable entitlements for digital and p - discovery: events and getters that provide information about the requirements of a redemption campaign - onchain: token mints with context of items spent - offchain: the ability to associate with ecommerce orders (through `redemptionHash`) -- trait redemptions: improving the burn-to-redeem experience with E RC-7500 Dynamic Traits. +- trait redemptions: improving the burn-to-redeem experience with E RC-7496 Dynamic Traits. ## Specification @@ -122,7 +122,7 @@ Any token may be used in the RedeemableParams `consideration`. This will ensure ### Dynamic traits -If the token would like to enable trait redemptions, the token MUST include the E RC-7500 Dynamic Traits interface. +If the token would like to enable trait redemptions, the token MUST include the E RC-7496 Dynamic Traits interface. ### Signer @@ -290,7 +290,7 @@ Authors will include Solidity contracts in the assets folder. Dynamic Traits to allow for trait redemptions. +Tokens must properly implement E RC-7496 Dynamic Traits to allow for trait redemptions. For tokens to be minted as part of the params `offer`, the `mintRedemption` function contained as part of `IRedemptionMintable` MUST be permissioned and ONLY allowed to be called by specified addresses. From a666be33a758c0df7608d76d4047ce8c66612412 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 21 Aug 2023 08:44:17 -0700 Subject: [PATCH 17/24] update discussions-to permalink --- EIPS/eip-7498.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EIPS/eip-7498.md b/EIPS/eip-7498.md index d34200810074a4..fc3b6e752b8c63 100644 --- a/EIPS/eip-7498.md +++ b/EIPS/eip-7498.md @@ -3,7 +3,7 @@ eip: 7498 title: NFT Redeemables description: Extension to ERC-721 and ERC-1155 for onchain and offchain redeemables author: Ryan Ghods (@ryanio), 0age (@0age), Adam Montgomery (@montasaurus) -discussions-to: https://ethereum-magicians.org/t/erc-7501-nft-redeemables/15485 +discussions-to: https://ethereum-magicians.org/t/erc-7498-nft-redeemables/15485 status: Draft type: Standards Track category: ERC From b3f7c971d0b2e5a330ec563c5f3f70b14166c583 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 25 Sep 2023 17:17:20 -0700 Subject: [PATCH 18/24] updates from community feedaback: - Add new `campaignRequirements` to allow for OR case redemptions (e.g. token from contract A OR contract B) - Include reference implementation and tests (to be continued to be added to) - Destructure extraData in redeem() func into params for easier serialization and understandability of params - Simplify IRedemptionMintable and add campaignId - Add `translations` to redeemable URI --- EIPS/eip-7498.md | 75 ++++---- assets/eip-7498/ERC7498NFTRedeemables.sol | 162 ++++++++++++++++++ assets/eip-7498/RedeemVia7498.t.sol | 104 +++++++++++ .../eip-7498/RedeemablesErrorsAndEvents.sol | 39 +++++ assets/eip-7498/RedeemablesStructs.sol | 27 +++ 5 files changed, 374 insertions(+), 33 deletions(-) create mode 100644 assets/eip-7498/ERC7498NFTRedeemables.sol create mode 100644 assets/eip-7498/RedeemVia7498.t.sol create mode 100644 assets/eip-7498/RedeemablesErrorsAndEvents.sol create mode 100644 assets/eip-7498/RedeemablesStructs.sol diff --git a/EIPS/eip-7498.md b/EIPS/eip-7498.md index fc3b6e752b8c63..152e9ffe937faa 100644 --- a/EIPS/eip-7498.md +++ b/EIPS/eip-7498.md @@ -2,7 +2,7 @@ eip: 7498 title: NFT Redeemables description: Extension to ERC-721 and ERC-1155 for onchain and offchain redeemables -author: Ryan Ghods (@ryanio), 0age (@0age), Adam Montgomery (@montasaurus) +author: Ryan Ghods (@ryanio), 0age (@0age), Adam Montgomery (@montasaurus), Stephan Min (@stephankmin) discussions-to: https://ethereum-magicians.org/t/erc-7498-nft-redeemables/15485 status: Draft type: Standards Track @@ -28,7 +28,7 @@ Creators frequently use NFTs to create redeemable entitlements for digital and p 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. -The token MUST have the following interface and MUST return `true` for [ERC-165](./eip-165.md) supportsInterface for `0x12345678`, the 4 byte interfaceId of the below. +The token MUST have the following interface and MUST return `true` for [ERC-165](./eip-165.md) supportsInterface for `0x12345678(placeholder, to be set here when finalized)`, the 4 byte interfaceId of the below. ```solidity interface IERC7501 { @@ -43,8 +43,12 @@ interface IERC7501 { uint32 maxCampaignRedemptions; address manager; // the address that can modify the campaign address signer; // null address means no EIP-712 signature required + CampaignRequirements[] campaignRequirements; + } + struct CampaignRequirements { OfferItem[] offer; // items to be minted, can be empty for offchain redeemable - ConsiderationItem[] consideration; // the items you are transferring to recipient + ConsiderationItem[] consideration; // items transferring to recipient + TraitRedemption[] traitRedemptions; // the required traitRedemptions } struct TraitRedemption { uint8 substandard; @@ -61,7 +65,7 @@ interface IERC7501 { /* Setters */ function createCampaign(CampaignParams calldata params, string calldata uri) external returns (uint256 campaignId); function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external; - function redeem(uint256[] calldata tokenIds, bytes calldata extraData) external; + function redeem(uint256[] calldata tokenIds, uint256 campaignId, uint256 requirementsIndex, bytes32 redemptionHash, uint256 salt, bytes calldata signature) external payable; } --- @@ -102,52 +106,42 @@ Updates to campaigns MUST use `updateCampaign` and MUST emit the `CampaignUpdate ### Offer -If tokens are set in the params `offer`, the tokens MUST implement the `IRedemptionMintable` interface in order to support minting new items. The implementation SHOULD be however the token mechanics are desired. The implementing token MUST return true for ERC-165 `supportsInterface` for the interfaceIds of: `IERC721RedemptionMintable: 0x12345678` or `IERC1155RedemptionMintable: 0x12345678` +If tokens are set in the params `offer`, the tokens MUST implement the `IRedemptionMintable` interface in order to support minting new items. The implementation SHOULD be however the token mechanics are desired. The implementing token MUST return true for ERC-165 `supportsInterface` for the interfaceId of `IRedemptionMintable`, `0x12345678(placeholder, to be set here when finalized)`. ```solidify -interface IERC721RedemptionMintable { - function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256[] memory tokenIds); -} - -interface IERC1155RedemptionMintable { - function mintRedemption(address to, SpentItem[] calldata spent) external returns (uint256[] memory tokenIds, uint256[] amounts); +interface IRedemptionMintable { + function mintRedemption(uint256 campaignId, address recipient, SpentItem[] calldata spent) external; } ``` -The array length return values of `tokenIds` and `amounts` for `IERC1155RedemptionMintable` MUST equal each other. - ### Consideration -Any token may be used in the RedeemableParams `consideration`. This will ensure the token is transferred to the `recipient`. If the token is meant to be burned the recipient SHOULD be `0x000000000000000000000000000000000000dEaD`. +Any token may be used in the RedeemableParams `consideration`. This will ensure the token is transferred to the `recipient`. If the token is meant to be burned, the recipient SHOULD be `0x0000000000000000000000000000000000000000`, where the token MUST call its internal burn method to delete the token and reduce totalSupply. If an external token is specified, then the recipient for burning SHOULD be `0x000000000000000000000000000000000000dEaD`, and approval must be given to the token contract as an operator on behalf of the user. ### Dynamic traits -If the token would like to enable trait redemptions, the token MUST include the E RC-7496 Dynamic Traits interface. +Including trait redemptions is optional, but if the token would like to enable trait redemptions the token MUST include E RC-7496 Dynamic Traits. ### Signer A signer MAY be specified to provide a signature to process the redemption. If the signer is NOT the null address, the signature MUST recover to the signer address via [EIP-712](./eip-712.md) or [ERC-1271](./eip-1271.md). -The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,bytes32 redemptionHash, uint256 salt)"` +The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,uint256 requirementsIndex, bytes32 redemptionHash, uint256 salt)"` -### Redemption extraData +### Campaign start and end time -When calling the `redeem` function, the extraData layout MUST follow: - -| bytes | value | description / notes | -| -------- | ----------------- | ---------------------------------------------------------------- | -| 0-32 | campaignId | | -| 32-64 | redemptionHash | hash of offchain order ids | -| 64-\* | TraitRedemption[] | see TraitRedemption struct. empty array for no trait redemptions | -| \*-(+32) | salt | if signer != address(0) | -| \*-(+\*) | signature | if signer != address(0). can be for EIP-712 or ERC-1271 | - -Upon redemption, the contract MUST check that the campaign is still active (using the same boundary check as Seaport, `startTime <= block.timestamp < endTime`). If it is, it MUST revert with `NotActive()`. +Upon redemption, the contract MUST check that the campaign is active (using the same boundary check as Seaport, `startTime <= block.timestamp < endTime`). If it is not active, it MUST revert with `NotActive()`. ### Redeem The `redeem` function MUST execute the transfers in the `consideration`. It MUST also call `mintRedemption` on the token specified in the `offer`. If any of the supplied tokenIds in `redeem` fail validation, the function MAY execute just the redemptions that were valid and ignore the failed redemptions. The `Redemption` event MUST be emitted emitted when any valid redemptions occur. +The `requirementsIndex` MUST be the index in the campaignRequirements array that satisfies the redemption. This helps reduce wasted gas searching for the applicable requirement. + +If the campaign `signer` is the null address, the `salt` and `signature` MUST be zero values. + +The `redemptionHash` is designated for offchain redemptions to reference offchain order identifiers to track the redemption to. + ### Trait redemptions The token MUST respect the TraitRedemption substandards as follows: @@ -209,6 +203,21 @@ The metadata URI MUST follow the following JSON schema: "type": "string", "description": "The language tag for the content provided by this metadata. https://www.rfc-editor.org/rfc/rfc9110.html#name-language-tags" }, + "translations": { + "type": "object", + "properties": { + "locale": { + "type": "string" + }, + "originalContent": { + "type": "string" + }, + "translatedContent": { + "type": "string" + } + }, + "description": "Translations for content provided by this metadata." + }, "maxRedemptionsPerToken": { "type": "string", "description": "The maximum number of redemptions per token. When isBurn is true should be 1, else can be a number based on the trait redemptions limit." @@ -219,7 +228,7 @@ The metadata URI MUST follow the following JSON schema: }, "uuid": { "type": "string", - "description": "A unique identifier for the campaign, for backends to identify when draft campaigns are published onchain." + "description": "An optional unique identifier for the campaign, for backends to identify when draft campaigns are published onchain." }, "productLimitForRedemption": { "type": "number", @@ -266,7 +275,7 @@ The metadata URI MUST follow the following JSON schema: } ``` -Future SIPs MAY inherit this one and add to the above metadata to add more features and functionality. +Future EIPs MAY inherit this one and add to the above metadata to add more features and functionality. ### ERC-1155 (Semi-fungibles) @@ -282,15 +291,15 @@ As a new EIP, no backwards compatibility issues are present. ## 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 -Tokens must properly implement E RC-7496 Dynamic Traits to allow for trait redemptions. +If trait redemptions are desired, tokens implementing this EIP must properly implement E RC-7496 Dynamic Traits. For tokens to be minted as part of the params `offer`, the `mintRedemption` function contained as part of `IRedemptionMintable` MUST be permissioned and ONLY allowed to be called by specified addresses. diff --git a/assets/eip-7498/ERC7498NFTRedeemables.sol b/assets/eip-7498/ERC7498NFTRedeemables.sol new file mode 100644 index 00000000000000..ec6bf5acb4ff76 --- /dev/null +++ b/assets/eip-7498/ERC7498NFTRedeemables.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {ERC721ConduitPreapproved_Solady} from "shipyard-core/src/tokens/erc721/ERC721ConduitPreapproved_Solady.sol"; +import {ERC20} from "solady/src/tokens/ERC20.sol"; +import {ERC1155} from "solady/src/tokens/ERC1155.sol"; +import {IERC7498} from "../interfaces/IERC7498.sol"; +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ItemType} from "seaport-types/src/lib/ConsiderationEnums.sol"; +import {IERC721RedemptionMintable} from "../interfaces/IERC721RedemptionMintable.sol"; +import {CampaignParams} from "./RedeemablesStructs.sol"; +import {RedeemableErrorsAndEvents} from "./RedeemablesErrorsAndEvents.sol"; + +contract ERC7498NFTRedeemables is ERC721ConduitPreapproved_Solady, IERC7498, RedeemableErrorsAndEvents { + /// @dev Counter for next campaign id. + uint256 private _nextCampaignId = 1; + + /// @dev The campaign parameters by campaign id. + mapping(uint256 campaignId => CampaignParams params) private _campaignParams; + + /// @dev The campaign URIs by campaign id. + mapping(uint256 campaignId => string campaignURI) private _campaignURIs; + + /// @dev The total current redemptions by campaign id. + mapping(uint256 campaignId => uint256 count) private _totalRedemptions; + + constructor() ERC721ConduitPreapproved_Solady() {} + + function name() public pure override returns (string memory) { + return "ERC7498NFTRedeemables"; + } + + function symbol() public pure override returns (string memory) { + return "RDMBL"; + } + + function tokenURI(uint256 tokenId) public pure override returns (string memory) { + return "https://example.com/"; + } + + function mint(address to, uint256 tokenId) public { + _mint(to, tokenId); + } + + function redeem(uint256[] calldata tokenIds, address recipient, bytes calldata extraData) public payable { + // Get the campaign. + uint256 campaignId = uint256(bytes32(extraData[0:32])); + CampaignParams storage params = _campaignParams[campaignId]; + ConsiderationItem[] memory consideration = params.consideration; + + // Revert if campaign is inactive. + if (_isInactive(params.startTime, params.endTime)) { + revert NotActive(block.timestamp, params.startTime, params.endTime); + } + + // Revert if max total redemptions would be exceeded. + if (_totalRedemptions[campaignId] + tokenIds.length > params.maxCampaignRedemptions) { + revert MaxCampaignRedemptionsReached(_totalRedemptions[campaignId] + 1, params.maxCampaignRedemptions); + } + + // Iterate over the token IDs and check if caller is the owner or approved operator. + // Redeem the token if the caller is valid. + for (uint256 i; i < tokenIds.length;) { + // Get the identifier. + uint256 identifier = tokenIds[i]; + + // Get the token owner. + address owner = ownerOf(identifier); + + // Check the caller is either the owner or approved operator. + if (msg.sender != owner && !isApprovedForAll(owner, msg.sender)) { + revert InvalidCaller(msg.sender); + } + + // Iterate over campaign consideration items. + for (uint256 j; j < consideration.length;) { + // If the consideration item is the internal token and recipient is zero address, burn the token. + if (consideration[j].token == address(this) && consideration[j].recipient == address(0)) { + _burn(identifier); + + // Else if the consideration item is the internal token and recipient is not zero address, transfer the token. + } else if (consideration[j].token == address(this) && consideration[j].recipient != address(0)) { + // Transfer the token to the consideration recipient. + ERC721ConduitPreapproved_Solady(consideration[j].token).safeTransferFrom( + owner, consideration[j].recipient, identifier + ); + } + + unchecked { + ++j; + } + } + + // Mint the redemption token. + IERC721RedemptionMintable(params.offer[0].token).mintRedemption(recipient, identifier); + + unchecked { + ++i; + } + } + } + + function getCampaign(uint256 campaignId) + external + view + override + returns (CampaignParams memory params, string memory uri, uint256 totalRedemptions) + {} + + function createCampaign(CampaignParams calldata params, string calldata uri) + external + override + returns (uint256 campaignId) + { + // Revert if there are no consideration items, since the redemption should require at least something. + if (params.consideration.length == 0) revert NoConsiderationItems(); + + // Revert if startTime is past endTime. + if (params.startTime > params.endTime) revert InvalidTime(); + + // Set the campaign params for the next campaignId. + _campaignParams[_nextCampaignId] = params; + + // Set the campaign URI for the next campaignId. + _campaignURIs[_nextCampaignId] = uri; + + // Set the correct current campaignId to return before incrementing + // the next campaignId. + campaignId = _nextCampaignId; + + // Increment the next campaignId. + _nextCampaignId++; + + emit CampaignUpdated(campaignId, params, _campaignURIs[campaignId]); + } + + function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) + external + override + {} + + /** + * @dev Internal pure function to cast a `bool` value to a `uint256` value. + * + * @param b The `bool` value to cast. + * + * @return u The `uint256` value. + */ + function _cast(bool b) internal pure returns (uint256 u) { + assembly { + u := b + } + } + + function _isInactive(uint256 startTime, uint256 endTime) internal view returns (bool inactive) { + // Using the same check for time boundary from Seaport. + // startTime <= block.timestamp < endTime + assembly { + inactive := or(iszero(gt(endTime, timestamp())), gt(startTime, timestamp())) + } + } +} \ No newline at end of file diff --git a/assets/eip-7498/RedeemVia7498.t.sol b/assets/eip-7498/RedeemVia7498.t.sol new file mode 100644 index 00000000000000..210eb4ee1b8213 --- /dev/null +++ b/assets/eip-7498/RedeemVia7498.t.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {Solarray} from "solarray/Solarray.sol"; +import {TestERC721} from "./utils/mocks/TestERC721.sol"; +import {OfferItem, ConsiderationItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {ItemType, OrderType, Side} from "seaport-sol/src/SeaportEnums.sol"; +import {CampaignParams, TraitRedemption} from "../src/lib/RedeemableStructs.sol"; +import {RedeemableErrorsAndEvents} from "../src/lib/RedeemableErrorsAndEvents.sol"; +import {ERC721RedemptionMintable} from "../src/lib/ERC721RedemptionMintable.sol"; +import {ERC7498NFTRedeemables} from "../src/lib/ERC7498NFTRedeemables.sol"; +import {Test} from "forge-std/Test.sol"; + +contract RedeemVia7498 is RedeemableErrorsAndEvents, Test { + error InvalidContractOrder(bytes32 orderHash); + + ERC7498NFTRedeemables redeemableToken; + ERC721RedemptionMintable redemptionToken; + + address constant _BURN_ADDRESS = 0x000000000000000000000000000000000000dEaD; + + function setUp() public { + redeemableToken = new ERC7498NFTRedeemables(); + redemptionToken = new ERC721RedemptionMintable( + address(redeemableToken), + address(redeemableToken) + ); + vm.label(address(redeemableToken), "redeemableToken"); + vm.label(address(redemptionToken), "redemptionToken"); + } + + function testBurnInternalToken() public { + uint256 tokenId = 2; + redeemableToken.mint(address(this), tokenId); + + redeemableToken.setApprovalForAll(address(redeemableToken), true); + + OfferItem[] memory offer = new OfferItem[](1); + offer[0] = OfferItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redemptionToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1 + }); + + ConsiderationItem[] memory consideration = new ConsiderationItem[](1); + consideration[0] = ConsiderationItem({ + itemType: ItemType.ERC721_WITH_CRITERIA, + token: address(redeemableToken), + identifierOrCriteria: 0, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + { + CampaignParams memory params = CampaignParams({ + offer: offer, + consideration: consideration, + signer: address(0), + startTime: uint32(block.timestamp), + endTime: uint32(block.timestamp + 1000), + maxCampaignRedemptions: 5, + manager: address(this) + }); + + redeemableToken.createCampaign(params, ""); + } + + { + OfferItem[] memory offerFromEvent = new OfferItem[](1); + offerFromEvent[0] = OfferItem({ + itemType: ItemType.ERC721, + token: address(redemptionToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1 + }); + ConsiderationItem[] memory considerationFromEvent = new ConsiderationItem[](1); + considerationFromEvent[0] = ConsiderationItem({ + itemType: ItemType.ERC721, + token: address(redeemableToken), + identifierOrCriteria: tokenId, + startAmount: 1, + endAmount: 1, + recipient: payable(_BURN_ADDRESS) + }); + + assertGt(uint256(consideration[0].itemType), uint256(considerationFromEvent[0].itemType)); + + bytes memory extraData = abi.encode(1, bytes32(0)); // campaignId, redemptionHash + consideration[0].identifierOrCriteria = tokenId; + + uint256[] memory tokenIds = new uint256[](1); + tokenIds[0] = tokenId; + + redeemableToken.redeem(tokenIds, address(this), extraData); + + assertEq(redeemableToken.ownerOf(tokenId), _BURN_ADDRESS); + assertEq(redemptionToken.ownerOf(tokenId), address(this)); + } + } +} \ No newline at end of file diff --git a/assets/eip-7498/RedeemablesErrorsAndEvents.sol b/assets/eip-7498/RedeemablesErrorsAndEvents.sol new file mode 100644 index 00000000000000..696e16a934652e --- /dev/null +++ b/assets/eip-7498/RedeemablesErrorsAndEvents.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; +import {CampaignParams} from "./RedeemablesStructs.sol"; + +interface RedeemableErrorsAndEvents { + /// Configuration errors + error NotManager(); + error InvalidTime(); + error NoConsiderationItems(); + error ConsiderationItemRecipientCannotBeZeroAddress(); + + /// Redemption errors + error InvalidCampaignId(); + error CampaignAlreadyExists(); + error InvalidCaller(address caller); + error NotActive(uint256 currentTimestamp, uint256 startTime, uint256 endTime); + error MaxRedemptionsReached(uint256 total, uint256 max); + error MaxCampaignRedemptionsReached(uint256 total, uint256 max); + error NativeTransferFailed(); + error RedeemMismatchedLengths(); + error TraitValueUnchanged(bytes32 traitKey, bytes32 value); + error InvalidConsiderationLength(uint256 got, uint256 want); + error InvalidConsiderationItem(address got, address want); + error InvalidOfferLength(uint256 got, uint256 want); + error InvalidNativeOfferItem(); + error InvalidOwner(); + error InvalidRequiredValue(bytes32 got, bytes32 want); + error InvalidSubstandard(uint256 substandard); + error InvalidTraitRedemption(); + error InvalidTraitRedemptionToken(address token); + error ConsiderationRecipientNotFound(address token); + error RedemptionValuesAreImmutable(); + + /// Events + event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string uri); + event Redemption(uint256 indexed campaignId, bytes32 redemptionHash); +} \ No newline at end of file diff --git a/assets/eip-7498/RedeemablesStructs.sol b/assets/eip-7498/RedeemablesStructs.sol new file mode 100644 index 00000000000000..9f41112d4c0085 --- /dev/null +++ b/assets/eip-7498/RedeemablesStructs.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {OfferItem, ConsiderationItem, SpentItem} from "seaport-types/src/lib/ConsiderationStructs.sol"; + +struct CampaignParams { + uint32 startTime; + uint32 endTime; + uint32 maxCampaignRedemptions; + address manager; + address signer; + OfferItem[] offer; + ConsiderationItem[] consideration; +} + +struct TraitRedemption { + uint8 substandard; + address token; + uint256 identifier; + bytes32 traitKey; + bytes32 traitValue; + bytes32 substandardValue; +} + +struct RedemptionContext { + SpentItem[] spent; +} \ No newline at end of file From 571b3e0b274183eac71033efda6787a11eca13d5 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 25 Sep 2023 18:21:25 -0700 Subject: [PATCH 19/24] add traitRedemptionTokenIds to redeem() --- EIPS/eip-7498.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-7498.md b/EIPS/eip-7498.md index 152e9ffe937faa..f76794538161c4 100644 --- a/EIPS/eip-7498.md +++ b/EIPS/eip-7498.md @@ -65,7 +65,7 @@ interface IERC7501 { /* Setters */ function createCampaign(CampaignParams calldata params, string calldata uri) external returns (uint256 campaignId); function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external; - function redeem(uint256[] calldata tokenIds, uint256 campaignId, uint256 requirementsIndex, bytes32 redemptionHash, uint256 salt, bytes calldata signature) external payable; + function redeem(uint256[] calldata tokenIds, uint256 campaignId, uint256 requirementsIndex, uint256[] calldata traitRedemptionTokenIds, bytes32 redemptionHash, uint256 salt, bytes calldata signature) external payable; } --- @@ -138,6 +138,8 @@ The `redeem` function MUST execute the transfers in the `consideration`. It MUST The `requirementsIndex` MUST be the index in the campaignRequirements array that satisfies the redemption. This helps reduce wasted gas searching for the applicable requirement. +The `traitRedemptionTokenIds` specifies the token IDs required for the trait redemptions in the requirements array. The order MUST be the same order of the token addresses expected in the array of TraitRedemption structs in the campaign requirement used. + If the campaign `signer` is the null address, the `salt` and `signature` MUST be zero values. The `redemptionHash` is designated for offchain redemptions to reference offchain order identifiers to track the redemption to. From 8e8baf8088432b52d81fc8c72f225b5e3a813c64 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 25 Sep 2023 19:16:09 -0700 Subject: [PATCH 20/24] restore extraData pattern for improved calldata compression --- EIPS/eip-7498.md | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/EIPS/eip-7498.md b/EIPS/eip-7498.md index f76794538161c4..5bb1bb052bfd30 100644 --- a/EIPS/eip-7498.md +++ b/EIPS/eip-7498.md @@ -34,7 +34,7 @@ The token MUST have the following interface and MUST return `true` for [ERC-165] interface IERC7501 { /* Events */ event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string URI); - event Redemption(uint256 indexed campaignId, bytes32 redemptionHash, uint256[] tokenIds, address redeemedBy); + event Redemption(uint256 indexed campaignId, uint256 requirementsIndex, bytes32 redemptionHash, uint256[] tokenIds, address redeemedBy); /* Structs */ struct CampaignParams { @@ -65,7 +65,7 @@ interface IERC7501 { /* Setters */ function createCampaign(CampaignParams calldata params, string calldata uri) external returns (uint256 campaignId); function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external; - function redeem(uint256[] calldata tokenIds, uint256 campaignId, uint256 requirementsIndex, uint256[] calldata traitRedemptionTokenIds, bytes32 redemptionHash, uint256 salt, bytes calldata signature) external payable; + function redeem(uint256[] calldata tokenIds, bytes calldata extraData) external payable; } --- @@ -126,24 +126,41 @@ Including trait redemptions is optional, but if the token would like to enable t A signer MAY be specified to provide a signature to process the redemption. If the signer is NOT the null address, the signature MUST recover to the signer address via [EIP-712](./eip-712.md) or [ERC-1271](./eip-1271.md). -The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,uint256 requirementsIndex, bytes32 redemptionHash, uint256 salt)"` +The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,uint256 campaignId,uint256 requirementsIndex, bytes32 redemptionHash, uint256 salt)"` -### Campaign start and end time +### Redeem function and redemption extraData -Upon redemption, the contract MUST check that the campaign is active (using the same boundary check as Seaport, `startTime <= block.timestamp < endTime`). If it is not active, it MUST revert with `NotActive()`. +The `redeem` function MUST use the `consideration`, `offer`, and `traitRedemptions` specified by the `campaignRequirements` determined by the `campaignId` and `requirementsIndex`: -### Redeem +- Execute the transfers in the `consideration` +- Mutate the traits specified by `traitRedemptions` according to ERC-7496 Dynamic Traits +- Call `mintRedemption()` on every `offer` item -The `redeem` function MUST execute the transfers in the `consideration`. It MUST also call `mintRedemption` on the token specified in the `offer`. If any of the supplied tokenIds in `redeem` fail validation, the function MAY execute just the redemptions that were valid and ignore the failed redemptions. The `Redemption` event MUST be emitted emitted when any valid redemptions occur. +If any of the supplied tokenIds in `redeem` fail validation or execution, the function MAY execute just the redemptions that were valid and ignore the failed redemptions. -The `requirementsIndex` MUST be the index in the campaignRequirements array that satisfies the redemption. This helps reduce wasted gas searching for the applicable requirement. +The `Redemption` event MUST be emitted when any valid redemptions occur. -The `traitRedemptionTokenIds` specifies the token IDs required for the trait redemptions in the requirements array. The order MUST be the same order of the token addresses expected in the array of TraitRedemption structs in the campaign requirement used. +The extraData layout MUST conform to the below: -If the campaign `signer` is the null address, the `salt` and `signature` MUST be zero values. +| bytes | value | description / notes | +| -------- | ----------------- | ---------------------------------------------------------------- | +| 0-32 | campaignId | | +| 32-64 | requirementsIndex | index of the campaignRequirements met | +| 64-96 | redemptionHash | hash of offchain order ids | +| 96-\* | uint256[] traitRedemptionTokenIds | token ids for trait redemptions, MUST be in same order of campaign TraitRedemption[] | +| \*-(+32) | salt | if signer != address(0) | +| \*-(+\*) | signature | if signer != address(0). can be for EIP-712 or EIP-1271 | + +The `requirementsIndex` MUST be the index in the `campaignRequirements` array that satisfies the redemption. This helps reduce gas to find the requirement met. + +The `traitRedemptionTokenIds` specifies the token IDs required for the trait redemptions in the requirements array. The order MUST be the same order of the token addresses expected in the array of `TraitRedemption` structs in the campaign requirement used. + +If the campaign `signer` is the null address the `salt` and `signature` MUST be omitted. The `redemptionHash` is designated for offchain redemptions to reference offchain order identifiers to track the redemption to. +The function MUST check that the campaign is active (using the same boundary check as Seaport, `startTime <= block.timestamp < endTime`). If it is not active, it MUST revert with `NotActive()`. + ### Trait redemptions The token MUST respect the TraitRedemption substandards as follows: @@ -160,7 +177,7 @@ The token MUST check that the `maxCampaignRedemptions` is not exceeded. If the r ### Metadata URI -The metadata URI MUST follow the following JSON schema: +The metadata URI MUST conform to the below JSON schema: ```json { From 595bacc9d8c8960e727929df6a5d317aac8261e6 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Mon, 25 Sep 2023 19:18:03 -0700 Subject: [PATCH 21/24] prettier --- EIPS/eip-7498.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/EIPS/eip-7498.md b/EIPS/eip-7498.md index 5bb1bb052bfd30..ddb2f1ec1c6937 100644 --- a/EIPS/eip-7498.md +++ b/EIPS/eip-7498.md @@ -48,7 +48,7 @@ interface IERC7501 { struct CampaignRequirements { OfferItem[] offer; // items to be minted, can be empty for offchain redeemable ConsiderationItem[] consideration; // items transferring to recipient - TraitRedemption[] traitRedemptions; // the required traitRedemptions + TraitRedemption[] traitRedemptions; // the trait redemptions } struct TraitRedemption { uint8 substandard; @@ -142,14 +142,14 @@ The `Redemption` event MUST be emitted when any valid redemptions occur. The extraData layout MUST conform to the below: -| bytes | value | description / notes | -| -------- | ----------------- | ---------------------------------------------------------------- | -| 0-32 | campaignId | | -| 32-64 | requirementsIndex | index of the campaignRequirements met | -| 64-96 | redemptionHash | hash of offchain order ids | +| bytes | value | description / notes | +| -------- | --------------------------------- | ------------------------------------------------------------------------------------ | +| 0-32 | campaignId | | +| 32-64 | requirementsIndex | index of the campaignRequirements met | +| 64-96 | redemptionHash | hash of offchain order ids | | 96-\* | uint256[] traitRedemptionTokenIds | token ids for trait redemptions, MUST be in same order of campaign TraitRedemption[] | -| \*-(+32) | salt | if signer != address(0) | -| \*-(+\*) | signature | if signer != address(0). can be for EIP-712 or EIP-1271 | +| \*-(+32) | salt | if signer != address(0) | +| \*-(+\*) | signature | if signer != address(0). can be for EIP-712 or EIP-1271 | The `requirementsIndex` MUST be the index in the `campaignRequirements` array that satisfies the redemption. This helps reduce gas to find the requirement met. From b4ced98a8245df0f6fdff2cbeaba7341ca6a4251 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Thu, 28 Sep 2023 10:37:14 -0700 Subject: [PATCH 22/24] simplify `campaignRequirements` -> `requirements` --- EIPS/eip-7498.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/EIPS/eip-7498.md b/EIPS/eip-7498.md index ddb2f1ec1c6937..00803032b28fae 100644 --- a/EIPS/eip-7498.md +++ b/EIPS/eip-7498.md @@ -43,7 +43,7 @@ interface IERC7501 { uint32 maxCampaignRedemptions; address manager; // the address that can modify the campaign address signer; // null address means no EIP-712 signature required - CampaignRequirements[] campaignRequirements; + CampaignRequirements[] requirements; } struct CampaignRequirements { OfferItem[] offer; // items to be minted, can be empty for offchain redeemable @@ -130,7 +130,7 @@ The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,a ### Redeem function and redemption extraData -The `redeem` function MUST use the `consideration`, `offer`, and `traitRedemptions` specified by the `campaignRequirements` determined by the `campaignId` and `requirementsIndex`: +The `redeem` function MUST use the `consideration`, `offer`, and `traitRedemptions` specified by the `requirements` determined by the `campaignId` and `requirementsIndex`: - Execute the transfers in the `consideration` - Mutate the traits specified by `traitRedemptions` according to ERC-7496 Dynamic Traits @@ -145,13 +145,13 @@ The extraData layout MUST conform to the below: | bytes | value | description / notes | | -------- | --------------------------------- | ------------------------------------------------------------------------------------ | | 0-32 | campaignId | | -| 32-64 | requirementsIndex | index of the campaignRequirements met | +| 32-64 | requirementsIndex | index of the campaign requirements met | | 64-96 | redemptionHash | hash of offchain order ids | | 96-\* | uint256[] traitRedemptionTokenIds | token ids for trait redemptions, MUST be in same order of campaign TraitRedemption[] | | \*-(+32) | salt | if signer != address(0) | | \*-(+\*) | signature | if signer != address(0). can be for EIP-712 or EIP-1271 | -The `requirementsIndex` MUST be the index in the `campaignRequirements` array that satisfies the redemption. This helps reduce gas to find the requirement met. +The `requirementsIndex` MUST be the index in the `requirements` array that satisfies the redemption. This helps reduce gas to find the requirement met. The `traitRedemptionTokenIds` specifies the token IDs required for the trait redemptions in the requirements array. The order MUST be the same order of the token addresses expected in the array of `TraitRedemption` structs in the campaign requirement used. From 504c92764397d45058ca56414ccb90a40d08a24c Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 6 Oct 2023 10:16:31 -0700 Subject: [PATCH 23/24] updates --- EIPS/eip-7498.md | 39 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/EIPS/eip-7498.md b/EIPS/eip-7498.md index 00803032b28fae..013ff82c334da3 100644 --- a/EIPS/eip-7498.md +++ b/EIPS/eip-7498.md @@ -34,7 +34,7 @@ The token MUST have the following interface and MUST return `true` for [ERC-165] interface IERC7501 { /* Events */ event CampaignUpdated(uint256 indexed campaignId, CampaignParams params, string URI); - event Redemption(uint256 indexed campaignId, uint256 requirementsIndex, bytes32 redemptionHash, uint256[] tokenIds, address redeemedBy); + event Redemption(uint256 indexed campaignId, uint256 requirementsIndex, bytes32 redemptionHash, uint256[] considerationTokenIds, uint256[] traitRedemptionTokenIds, address redeemedBy); /* Structs */ struct CampaignParams { @@ -43,12 +43,12 @@ interface IERC7501 { uint32 maxCampaignRedemptions; address manager; // the address that can modify the campaign address signer; // null address means no EIP-712 signature required - CampaignRequirements[] requirements; + CampaignRequirements[] requirements; // one requirement must be fully satisfied for a successful redemption } - struct CampaignRequirements { + struct Campaign { OfferItem[] offer; // items to be minted, can be empty for offchain redeemable ConsiderationItem[] consideration; // items transferring to recipient - TraitRedemption[] traitRedemptions; // the trait redemptions + TraitRedemption[] traitRedemptions; // the trait redemptions to process } struct TraitRedemption { uint8 substandard; @@ -65,7 +65,7 @@ interface IERC7501 { /* Setters */ function createCampaign(CampaignParams calldata params, string calldata uri) external returns (uint256 campaignId); function updateCampaign(uint256 campaignId, CampaignParams calldata params, string calldata uri) external; - function redeem(uint256[] calldata tokenIds, bytes calldata extraData) external payable; + function redeem(uint256[] calldata considerationTokenIds, address recipient, bytes calldata extraData) external payable; } --- @@ -110,10 +110,12 @@ If tokens are set in the params `offer`, the tokens MUST implement the `IRedempt ```solidify interface IRedemptionMintable { - function mintRedemption(uint256 campaignId, address recipient, SpentItem[] calldata spent) external; + function mintRedemption(uint256 campaignId, address recipient, ConsiderationItem[] calldata consideration, TraitRedemptions[] calldata traitRedemptions) external; } ``` +When `mintRedemption` is called, it is RECOMMENDED to replace the token identifiers in the consideration items and trait redemptions with the items actually being redeemed. + ### Consideration Any token may be used in the RedeemableParams `consideration`. This will ensure the token is transferred to the `recipient`. If the token is meant to be burned, the recipient SHOULD be `0x0000000000000000000000000000000000000000`, where the token MUST call its internal burn method to delete the token and reduce totalSupply. If an external token is specified, then the recipient for burning SHOULD be `0x000000000000000000000000000000000000dEaD`, and approval must be given to the token contract as an operator on behalf of the user. @@ -126,14 +128,14 @@ Including trait redemptions is optional, but if the token would like to enable t A signer MAY be specified to provide a signature to process the redemption. If the signer is NOT the null address, the signature MUST recover to the signer address via [EIP-712](./eip-712.md) or [ERC-1271](./eip-1271.md). -The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,address redeemedToken, uint256[] tokenIds,uint256 campaignId,uint256 requirementsIndex, bytes32 redemptionHash, uint256 salt)"` +The EIP-712 struct for signing MUST be as follows: `SignedRedeem(address owner,uint256[][] tokenIdsPerRedemption,uint256 campaignId,uint256 requirementsIndex, bytes32 redemptionHash, uint256 salt)"` ### Redeem function and redemption extraData The `redeem` function MUST use the `consideration`, `offer`, and `traitRedemptions` specified by the `requirements` determined by the `campaignId` and `requirementsIndex`: - Execute the transfers in the `consideration` -- Mutate the traits specified by `traitRedemptions` according to ERC-7496 Dynamic Traits +- Mutate the traits specified by `traitRedemptions` according to ERC -7496 Dynamic Traits - Call `mintRedemption()` on every `offer` item If any of the supplied tokenIds in `redeem` fail validation or execution, the function MAY execute just the redemptions that were valid and ignore the failed redemptions. @@ -147,9 +149,9 @@ The extraData layout MUST conform to the below: | 0-32 | campaignId | | | 32-64 | requirementsIndex | index of the campaign requirements met | | 64-96 | redemptionHash | hash of offchain order ids | -| 96-\* | uint256[] traitRedemptionTokenIds | token ids for trait redemptions, MUST be in same order of campaign TraitRedemption[] | -| \*-(+32) | salt | if signer != address(0) | -| \*-(+\*) | signature | if signer != address(0). can be for EIP-712 or EIP-1271 | +| 96- \* | uint256[] traitRedemptionTokenIds | token ids for trait redemptions, MUST be in same order of campaign TraitRedemption[] | +| \*- (+32) | salt | if signer != address(0) | +| \*- (+\*) | signature | if signer != address(0). can be for EIP-712 or EIP-1271 | The `requirementsIndex` MUST be the index in the `requirements` array that satisfies the redemption. This helps reduce gas to find the requirement met. @@ -222,21 +224,6 @@ The metadata URI MUST conform to the below JSON schema: "type": "string", "description": "The language tag for the content provided by this metadata. https://www.rfc-editor.org/rfc/rfc9110.html#name-language-tags" }, - "translations": { - "type": "object", - "properties": { - "locale": { - "type": "string" - }, - "originalContent": { - "type": "string" - }, - "translatedContent": { - "type": "string" - } - }, - "description": "Translations for content provided by this metadata." - }, "maxRedemptionsPerToken": { "type": "string", "description": "The maximum number of redemptions per token. When isBurn is true should be 1, else can be a number based on the trait redemptions limit." From 39e4b98029a40d80accda9722af84d0ef80148ba Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Fri, 6 Oct 2023 10:30:35 -0700 Subject: [PATCH 24/24] make imageUrls an array --- EIPS/eip-7498.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/EIPS/eip-7498.md b/EIPS/eip-7498.md index 013ff82c334da3..bbf0cea396ed96 100644 --- a/EIPS/eip-7498.md +++ b/EIPS/eip-7498.md @@ -198,7 +198,10 @@ The metadata URI MUST conform to the below JSON schema: "description": "A multi-line or multi-paragraph description of the details of the redeemable. Markdown is supported." }, "imageUrls": { - "type": "string", + "type": "array", + "items": { + "type": "string" + }, "description": "A list of image URLs for the redeemable. The first image will be used as the thumbnail. Will rotate in a carousel if multiple images are provided. Maximum 5 images." }, "bannerUrl": {