Alert Source Discuss
⚠️ Draft Standards Track: ERC

ERC-7498: NFT 可兑换物

用于链上和链下可兑换物的 ERC-721 和 ERC-1155 扩展

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

摘要

本规范引入了一个新的接口,该接口扩展了 ERC-721ERC-1155,以启用 NFT 的链上和链下可兑换物的发现和使用。链上 getter 和事件有助于发现可兑换的活动及其要求。新的链上铸币使用一个接口,该接口向铸币合约提供已兑换内容的上下文。对于兑换实物产品和商品(链下可兑换物),redemptionHashsigner 可以将链上兑换与包含所选产品和运输信息的链下订单标识符联系起来。

动机

创建者经常使用 NFT 来创建数字和实物商品的可兑换权利。但是,如果没有标准接口,用户和应用程序很难以可预测和标准的方式发现和交互这些 NFT。本标准旨在涵盖为以下方面启用广泛的功能:

  • 发现:提供有关兑换活动要求的事件和 getter
  • 链上:包含已花费项目上下文的代币铸造
  • 链下:与电子商务订单关联的能力(通过 redemptionHash
  • 特征兑换:通过 ERC-7496 动态特征改进 burn-to-redeem 体验。

规范

本文档中的关键词“必须”,“禁止”,“必需”,“应该”,“不应该”,“推荐”,“不推荐”,“可以”和“可选”应按照 RFC 2119 和 RFC 8174 中的描述进行解释。

该代币必须具有以下接口,并且对于以下 4 字节 interfaceId 0x1ac61e13ERC-165 的 supportsInterface 必须返回 true

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;
}

创建活动

创建新活动时,必须使用 createCampaign 并且必须返回新创建的 campaignId 以及 CampaignUpdated 事件。campaignId 必须是每次新活动递增的计数器。第一个活动必须具有 id 1

更新活动

对活动的更新可以使用 updateCampaign 并且必须发出 CampaignUpdated 事件。如果 manager 以外的地址尝试更新活动,则必须使用 NotManager() 回退。如果管理员希望使活动不可变,则 manager 可以设置为 null 地址。

Offer

如果在参数 offer 中设置了代币,则这些代币必须实现 IRedemptionMintable 接口,以支持铸造新项目。该实现应该是所需的代币机制。实现代币必须为 IRedemptionMintable 的 interfaceId 0x81fe13c2 的 ERC-165 supportsInterface 返回 true。

interface IRedemptionMintable {
    function mintRedemption(
        uint256 campaignId,
        address recipient,
        OfferItem calldata offer,
        ConsiderationItem[] calldata consideration,
        TraitRedemption[] calldata traitRedemptions
    ) external;
}

当调用 mintRedemption 时,建议将 consideration 项目和特征兑换中的代币标识符替换为实际兑换的项目。

Consideration

可以在活动要求 consideration 中指定任何代币。这将确保代币转移到 recipient。如果要烧毁代币,则 recipient 应该为 0x000000000000000000000000000000000000dEaD。如果代币可以内部处理烧毁自己的代币并减少 totalSupply,则代币可以烧毁代币而不是转移到 recipient 0x000000000000000000000000000000000000dEaD

动态特征

包含特征兑换是可选的,但是如果代币想要启用特征兑换,则代币必须包含 ERC-7496 动态特征。

Signer

可以指定一个签名者来提供签名以处理兑换。如果签名者不是 null 地址,则签名必须通过 EIP-712ERC-1271 恢复为签名者地址。

用于签名的 EIP-712 结构必须如下:SignedRedeem(address owner,uint256[] considerationTokenIds,uint256[] traitRedemptionTokenIds,uint256 campaignId,uint256 requirementsIndex, bytes32 redemptionHash, uint256 salt)"

Redeem 函数

redeem 函数必须使用 campaignIdrequirementsIndex 确定的 requirements 指定的 considerationoffertraitRedemptions

  • 执行 consideration 中的转移
  • 根据 ERC-7496 动态特征改变 traitRedemptions 指定的特征
  • 在每个 offer 项目上调用 mintRedemption()

对于发生的每个有效兑换,必须发出 Redemption 事件。

Redemption extraData

extraData 布局必须符合以下条件:

bytes value description / notes
0-32 campaignId  
32-64 requirementsIndex 满足的活动要求的索引
64-96 redemptionHash 链下订单 id 的哈希值
96-* uint256[] traitRedemptionTokenIds 用于特征兑换的代币 id,必须与活动 TraitRedemption[] 的顺序相同
*-(+32) salt 如果 signer != address(0)
*-(+*) signature 如果 signer != address(0)。可以用于 EIP-712 或 ERC-1271

requirementsIndex 必须是满足兑换的 requirements 数组中的索引。这有助于减少查找满足的要求的 gas 费用。

traitRedemptionTokenIds 指定活动要求中特征兑换所需的代币 ID。该顺序必须与活动要求中使用的 TraitRedemption 结构数组中预期的代币地址的顺序相同。

如果活动 signer 为 null 地址,则必须省略 saltsignature

redemptionHash 指定用于链下兑换以引用链下订单标识符以跟踪兑换。

该函数必须检查活动是否处于活动状态(使用与 Seaport 相同的边界检查,startTime <= block.timestamp < endTime)。如果未激活,则必须使用 NotActive() 回退。

特征兑换

代币必须遵守 TraitRedemption 子标准,如下所示:

substandard ID description substandard value
1 将值设置为 traitValue 先前要求的 value。如果为空,则不能是已经存在的 traitValue
2 将特征递增 traitValue 最大值
3 将特征递减 traitValue 最小值
4 检查值是否为 traitValue 不适用

最大活动兑换

代币必须检查是否超过了 maxCampaignRedemptions。如果兑换确实超过了 maxCampaignRedemptions,则必须使用 MaxCampaignRedemptionsReached(uint256 total, uint256 max) 回退

Metadata URI

元数据 URI 必须符合以下 JSON 模式:

{
  "$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": "可兑换物的一行摘要。不支持 Markdown。"
          },
          "details": {
            "type": "string",
            "description": "可兑换物的详细信息的多行或多段描述。支持 Markdown。"
          },
          "imageUrls": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "可兑换物的图像 URL 列表。第一张图像将用作缩略图。如果提供多个图像,将在轮播中旋转。最多 5 张图像。"
          },
          "bannerUrl": {
            "type": "string",
            "description": "可兑换物的横幅图像。"
          },
          "faq": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "question": {
                  "type": "string"
                },
                "answer": {
                  "type": "string"
                },
                "required": ["question", "answer"]
              }
            }
          },
          "contentLocale": {
            "type": "string",
            "description": "此元数据提供的内容的语言标签。https://www.rfc-editor.org/rfc/rfc9110.html#name-language-tags"
          },
          "maxRedemptionsPerToken": {
            "type": "string",
            "description": "每个代币的最大兑换数量。如果 isBurn 为 true,则应为 1,否则可以基于特征兑换限制的数字。"
          },
          "isBurn": {
            "type": "string",
            "description": "如果兑换烧毁代币。"
          },
          "uuid": {
            "type": "string",
            "description": "活动的 Optional 唯一标识符,供后端识别链上发布的草稿活动。"
          },
          "productLimitForRedemption": {
            "type": "number",
            "description": "能够从 products 数组中选择用于单个兑换的产品数量。"
          },
          "products": {
            "type": "object",
            "properties": "https://schema.org/Product",
            "required": ["name", "url", "description"]
          }
        },
        "required": ["campaignId", "name", "description", "imageUrls", "isBurn"]
      }
    }
  }
}

未来的 EIP 可能会继承此 EIP 并在上面的元数据中添加更多功能。

ERC-1155(半同质化代币)

此标准可以应用于 ERC-1155,但兑换将适用于特定代币标识符的所有代币数量。如果 ERC-1155 合约仅具有数量为 1 的代币,则可以按原样使用此规范。

理由

使用了 Seaport 中的 “offer” 和 “consideration” 结构来为可兑换活动创建类似的语言。”offer” 是正在提供的,例如,一个新的链上代币,而 “consideration” 是必须满足的才能完成兑换。”consideration” 字段具有一个 “recipient”,代币应转移到该 recipient。对于不需要移动代币的特征更新,而是指定 traitRedemptionTokenIds

提供 “salt” 和 “signature” 字段主要是用于线下兑换,其中提供者希望在链上进行兑换之前签署批准,以防止需要不规则的状态更改。例如,如果用户居住在链下可兑换物运输不支持的区域之外,则在链下订单创建过程中,当看到用户的运输国家/地区不受支持时,将不会为链上兑换提供签名。这可以防止用户兑换 NFT,然后在 NFT 已烧毁或特征已改变后发现运输不受支持。

ERC-7496 动态特征用于特征兑换,以支持二级市场订单的特征值的链上强制执行。

向后兼容性

作为新的 EIP,不存在向后兼容性问题。

测试用例

作者已包括 Foundry 测试,涵盖了 assets folder 中规范的功能。

参考实现

作者已包括 assets folder 中规范的参考实现。

安全考虑

如果需要特征兑换,则实现此 EIP 的代币必须正确实现 ERC-7496 动态特征。

对于作为参数 offer 的一部分铸造的代币,IRedemptionMintable 中包含的 mintRedemption 函数必须获得许可,并且仅允许由指定的地址调用。

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

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