Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7498: NFT Redeemables

Extension to ERC-721 and ERC-1155 for onchain and offchain redeemables

Authors Ryan Ghods (@ryanio), 0age (@0age), Adam Montgomery (@montasaurus), Stephan Min (@stephankmin)
Created 2023-07-28
Discussion Link https://ethereum-magicians.org/t/erc-7498-nft-redeemables/15485
Requires EIP-165, EIP-712, EIP-721, EIP-1155, EIP-1271

Abstract

This specification introduces a new interface that extends ERC-721 and ERC-1155 to enable the discovery and use of onchain and offchain redeemables for NFTs. Onchain getters and events facilitate discovery of redeemable campaigns and their requirements. New onchain mints use an interface that gives context to the minting contract of what was redeemed. For redeeming physical products and goods (offchain redeemables) a redemptionHash and signer can tie onchain redemptions with offchain order identifiers that contain chosen product and shipping information.

Motivation

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 ecommerce orders (through redemptionHash)
  • trait redemptions: improving the burn-to-redeem experience with ERC-7496 Dynamic Traits.

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 ERC-165 supportsInterface for 0x1ac61e13, the 4 byte interfaceId of the below.

interface IERC7498 {
  /* Events */
  event CampaignUpdated(uint256 indexed campaignId, Campaign campaign, string metadataURI);
  event Redemption(uint256 indexed campaignId, uint256 requirementsIndex, bytes32 redemptionHash, uint256[] considerationTokenIds, uint256[] traitRedemptionTokenIds, address redeemedBy);

  /* Structs */
  struct Campaign {
    CampaignParams params;
    CampaignRequirements[] requirements; // one requirement must be fully satisfied for a successful redemption
  }
  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
  }
  struct CampaignRequirements {
    OfferItem[] offer;
    ConsiderationItem[] consideration;
    TraitRedemption[] traitRedemptions;
  }
  struct TraitRedemption {
    uint8 substandard;
    address token;
    bytes32 traitKey;
    bytes32 traitValue;
    bytes32 substandardValue;
  }

  /* Getters */
  function getCampaign(uint256 campaignId) external view returns (Campaign memory campaign, string memory metadataURI, uint256 totalRedemptions);

  /* Setters */
  function createCampaign(Campaign calldata campaign, string calldata metadataURI) external returns (uint256 campaignId);
  function updateCampaign(uint256 campaignId, Campaign calldata campaign, string calldata metadataURI) external;
  function redeem(uint256[] calldata considerationTokenIds, address recipient, bytes calldata extraData) external payable;
}

---

/* 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, as of this writing can't extend structs in solidity)
}
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 a counter incremented with each new campaign. The first campaign MUST have an id of 1.

Updating campaigns

Updates to campaigns MAY 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 ERC-165 supportsInterface for the interfaceId of IRedemptionMintable, 0x81fe13c2.

interface IRedemptionMintable {
    function mintRedemption(
        uint256 campaignId,
        address recipient,
        OfferItem calldata offer,
        ConsiderationItem[] calldata consideration,
        TraitRedemption[] 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 specified in the campaign requirement consideration. This will ensure the token is transferred to the recipient. If the token is meant to be burned, the recipient SHOULD be 0x000000000000000000000000000000000000dEaD. If the token can internally handle burning its own tokens and reducing totalSupply, the token MAY burn the token instead of transferring to the recipient 0x000000000000000000000000000000000000dEaD.

Dynamic traits

Including trait redemptions is optional, but if the token would like to enable trait redemptions the token MUST include ERC-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 or ERC-1271.

The EIP-712 struct for signing MUST be as follows: SignedRedeem(address owner,uint256[] considerationTokenIds,uint256[] traitRedemptionTokenIds,uint256 campaignId,uint256 requirementsIndex, bytes32 redemptionHash, uint256 salt)"

Redeem function

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
  • Call mintRedemption() on every offer item

The Redemption event MUST be emitted for every valid redemption that occurs.

Redemption extraData

The extraData layout MUST conform to the below:

bytes value description / notes
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 ERC-1271

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.

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:

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
4 check value is traitValue n/a

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 conform to the below JSON schema:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "campaigns": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "campaignId": {
            "type": "number"
          },
          "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": "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": {
            "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": "An optional 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"]
          }
        },
        "required": ["campaignId", "name", "description", "imageUrls", "isBurn"]
      }
    }
  }
}

Future EIPs 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 “offer” and “consideration” structs from Seaport were used to create a similar language for redeemable campaigns. The “offer” is what is being offered, e.g. a new onchain token, and the “consideration” is what must be satisfied to complete the redemption. The “consideration” field has a “recipient”, who the token should be transferred to. For trait updates that do not require moving of a token, traitRedemptionTokenIds is specified instead.

The “salt” and “signature” fields are provided primarily for offchain redemptions where a provider would want to sign approval for a redemption before it is conducted onchain, to prevent the need for irregular state changes. For example, if a user lives outside a region supported by the shipping of an offchain redeemable, during the offchain order creation process the signature would not be provided for the onchain redemption when seeing that the user’s shipping country is unsupported. This prevents the user from redeeming the NFT, then later finding out the shipping isn’t supported after their NFT is already burned or trait is mutated.

ERC-7496 Dynamic Traits is used for trait redemptions to support onchain enforcement of trait values for secondary market orders.

Backwards Compatibility

As a new EIP, no backwards compatibility issues are present.

Test Cases

Authors have included Foundry tests covering functionality of the specification in the assets folder.

Reference Implementation

Authors have included reference implementations of the specification in the assets folder.

Security Considerations

If trait redemptions are desired, tokens implementing this EIP must properly implement ERC-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.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Ryan Ghods (@ryanio), 0age (@0age), Adam Montgomery (@montasaurus), Stephan Min (@stephankmin), "ERC-7498: NFT Redeemables [DRAFT]," Ethereum Improvement Proposals, no. 7498, July 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7498.