Alert Source Discuss
⚠️ Review Standards Track: ERC

ERC-5269: ERC 检测与发现

一个接口,用于识别给定的调用者是否支持 ERC 中指定的主要行为或可选行为。

Authors Zainan Victor Zhou (@xinbenlv)
Created 2022-07-15
Requires EIP-5750

摘要

一个通过数字更好地识别和检测 ERC 的接口。 它指定了一个名为 majorERCIdentifier 的字段,该字段通常被称为“ERC 编号”。例如,ERC-721,也就是 ERC-721 有一个 majorERCIdentifier = 721。此 ERC 的 majorERCIdentifier = 5269

将其称为 majorERCIdentifier 而不是 ERCNumber 使其具有面向未来的特性:预测未来的 ERC 可能没有编号,或者我们想要纳入其他类型的标准。

它还提出了一个新的 minorERCIdentifier 概念,该概念留给各个 ERC 的作者来定义。例如,ERC-721 的作者可以将 ERC721Metadata 接口定义为 minorERCIdentifier= keccak256("ERC721Metadata")

它还提出了一个事件,允许智能合约有选择地声明它们支持的 ERC。

动机

创建此 ERC 是为了与 ERC-165 竞争。

以下是此 ERC 与 ERC-165 之间的主要区别。

  1. ERC-165 使用方法签名的哈希来声明一个或多个方法的存在,因此它首先需要存在至少一个方法。在某些情况下,某些 ERC 的接口没有方法,例如某些与数据格式和签名方案相关的 ERC,或者 “Soul-Bound-ness”,也就是 SBT,它可能只是恢复一个 transfer 调用而不需要任何特定方法。
  2. ERC-165 不提供基于调用者的查询能力。 此 ERC 的兼容合约将基于给定的调用者响应它是否支持某些 ERC。

以下是此 ERC 在 ERC-165 已经存在的情况下提出的动机:

  1. 使用 ERC 编号提高了人类可读性,并使其更容易与 ENS 等命名合约一起使用。

  2. 我们已经看到越来越多人有兴趣使用 ERC 编号来识别或指定 ERC,而不是使用 ERC-165 标识符。例如

  • ERC-5267 指定 extensions 为 ERC 编号列表。
  • ERC-600ERC-601m / purpose' / subpurpose' / ERC' / wallet' 路径中指定一个 ERC 编号。
  • ERC-5568 指定 由 ERC 定义的指令的 instruction_id 必须是其 ERC 编号,除非存在特殊情况(合理即可)
  • ERC-6120 指定 struct Token { uint eip; ..., },其中 uint eip 是一个用于识别 ERC 的 ERC 编号。
  • ERC-867(停滞) 提议创建 erpId: 此 ERP 的字符串标识符(可能是关联的 ERC 编号,例如“ERC-1234”)。
  1. 拥有一个 ERC/ERC 编号检测接口减少了智能合约中查找表的需求,以将任何 ERC 中的函数方法或整个接口中以 bytes4 ERC-165 标识符转换为其各自的 ERC 编号,并大大简化了指定用于行为扩展的 ERC 的方式。

  2. 我们还认识到,智能合约对于不同的调用者帐户可能具有不同的行为。最值得注意的用例之一是,当使用透明可升级模式时,代理合约在管理员帐户和非管理员帐户调用时会给予不同的处理。

规范

在以下描述中,我们交替使用 ERC 和 ERC。这是因为虽然大多数时候描述适用于 ERC 标准跟踪的 ERC 类别,但 ERC 编号空间是 ERC 编号空间的子空间,我们有时可能会遇到未被识别为 ERC 但具有值得查询的行为的 ERC。

  1. 任何兼容的智能合约必须实现以下接口
// DRAFTv1
pragma solidity ^0.8.9;

interface IERC5269 {
  event OnSupportERC(
      address indexed caller, // 当使用 `address(0x0)` 发出时,表示所有调用者。
      uint256 indexed majorERCIdentifier,
      bytes32 indexed minorERCIdentifier, // 0 表示整个 ERC
      bytes32 ercStatus,
      bytes extraData
  );

  /// @dev ERC 接口检测的核心方法
  /// @param caller, 要查询的调用者的 `address` 值,以确定是否支持给定的 ERC。
  /// @param majorERCIdentifier, 一个 `uint256` 值,并且应该是被查询的 ERC 编号。除非被未来的 ERC 取代,否则此 ERC 编号应小于或等于 (0, 2^32-1]。对于 `supportERC` 的函数调用,此范围之外的任何值都被视为未指定,并可供实现选择或供未来的 ERC 指定。
  /// @param minorERCIdentifier, 一个 `bytes32` 值,保留给各个 ERC 的作者指定。例如,[ERC-721](/ERCS/eip-721) 的作者可以将 `keccak256("ERC721Metadata")` 或 `keccak256("ERC721Metadata.tokenURI")` 指定为 `minorERCIdentifier` 以查询支持。作者还可以使用此 minorERCIdentifier 指定不同的版本,例如 ERC-712 具有具有不同行为的 V1-V4。
  /// @param extraData, 针对 [ERC-5750](/ERCS/eip-5750) 的 `bytes`,用于未来的扩展。
  /// @return ercStatus, 一个 `bytes32`,指示合约支持的 ERC 的状态。
  ///                    - 对于 FINAL ERC,它必须返回 `keccak256("FINAL")`。
  ///                    - 对于非 FINAL ERC,它应该返回 `keccak256("DRAFT")`。
  ///                      在 ERC 过程中,允许 ERC 作者自行指定
  ///                      `FINAL` 或 `DRAFT` 以外的自己的 ercStatus,例如 `keccak256("DRAFTv1")`
  ///                      或 `keccak256("DRAFT-option1")`,并且此类 ercStatus 值必须记录在 ERC 正文中
  function supportERC(
    address caller,
    uint256 majorERCIdentifier,
    bytes32 minorERCIdentifier,
    bytes calldata extraData)
  external view returns (bytes32 ercStatus);
}

在以下描述中,ERC_5269_STATUS 设置为 keccak256("DRAFTv1")

除了 IERC5269 的注释中指定的行为之外:

  1. 任何 minorERCIdentifier=0 都保留用于引用被查询的 ERC 的主要行为。
  2. 建议兼容 ERC 的作者声明 minorERCIdentifier 列表,用于他们的可选接口、行为和未来扩展的值范围。
  3. 当此 ERC 为 FINAL 时,任何兼容的合约都必须为 supportERC((any caller), 5269, 0, []) 的调用返回一个 ERC_5269_STATUS

注意:在当前快照中,supportERC((any caller), 5269, 0, []) 必须返回 ERC_5269_STATUS

  1. 任何符合要求的合约都应该在构造或升级时发出一个 OnSupportERC(address(0), 5269, 0, ERC_5269_STATUS, []) 事件。
  2. 任何符合要求的合约都可以通过发出具有相关值的 OnSupportERC 事件来声明任何 ERC 的主要行为或子行为,以便于发现,并且当符合要求的合约更改是否支持某个 ERC 或针对某个调用者或所有调用者的某些行为时。
  3. 对于任何非 Final 状态的 ERC-XXX,当查询 supportERC((any caller), xxx, (any minor identifier), []) 时,它不得返回 keccak256("FINAL")。建议在这种情况下返回 0,但允许使用 ercStatus 的其他值。调用者必须将 keccak256("FINAL") 以外的任何返回值视为非最终状态,并且必须将 0 严格视为“不受支持”。
  4. 函数 supportERC 必须是 mutability view,即它不得改变 EVM 的任何全局状态。

理由

  1. 当数据类型为 uint256 majorERCIdentifier 时,还有其他替代方案,例如:
  • (1) 使用 ERC 编号的哈希版本,
  • (2) 使用原始数字,或
  • (3) 使用 ERC-165 标识符。

(1) 的优点是它可以自动支持未来 ERC 编号/命名规范的任何发展。 但缺点是它不向后可读:看到 hash(ERC-number) 通常不容易猜出它们的 ERC 编号是什么。

我们选择 (2) 作为动机中列出的理由。

  1. 在我们的设计决策中,我们有一个 bytes32 minorERCIdentifier。或者,它可以是 (1) 一个数字,强制所有 ERC 作者为其子行为定义编号,因此我们使用 bytes32 并要求 ERC 作者使用字符串名称的哈希来表示他们的子行为,他们已经通过在他们的规范中提出接口名称或方法名称来做到这一点。

  2. 或者,我们可以添加额外的数据作为返回值或所有受支持的 ERC 的数组,但我们不确定这种复杂性带来了多少价值以及额外的开销是否合理。

  3. ERC-165 相比,我们还添加了一个额外的输入 address caller,因为代理模式(例如 ERC-1967 启用的那些模式)越来越受欢迎。有人可能会问:为什么不直接使用 msg.sender?这是因为我们希望允许在没有事务或代理合约的情况下查询接口 ERC-number 是否可用于该特定发送者。

  4. 我们保留大于或等于 2^32 的输入 majorERCIdentifier,以防我们需要支持其他标准集合,这些标准不是 ERC/ERC。

测试用例


describe("ERC5269", function () {
  async function deployFixture() {
    // ...
  }

  describe("Deployment", function () {
    // ...
    it("Should emit proper OnSupportERC events", async function () {
      // 应该发出正确的 OnSupportERC 事件
      let { txDeployErc721 } = await loadFixture(deployFixture);
      let events = txDeployErc721.events?.filter(event => event.event === 'OnSupportERC');
      expect(events).to.have.lengthOf(4);

      let ev5269 = events!.filter(
        (event) => event.args!.majorERCIdentifier.eq(5269));
      expect(ev5269).to.have.lengthOf(1);
      expect(ev5269[0].args!.caller).to.equal(BigNumber.from(0));
      expect(ev5269[0].args!.minorERCIdentifier).to.equal(BigNumber.from(0));
      expect(ev5269[0].args!.ercStatus).to.equal(ethers.utils.id("DRAFTv1"));

      let ev721 = events!.filter(
        (event) => event.args!.majorERCIdentifier.eq(721));
      expect(ev721).to.have.lengthOf(3);
      expect(ev721[0].args!.caller).to.equal(BigNumber.from(0));
      expect(ev721[0].args!.minorERCIdentifier).to.equal(BigNumber.from(0));
      expect(ev721[0].args!.ercStatus).to.equal(ethers.utils.id("FINAL"));

      expect(ev721[1].args!.caller).to.equal(BigNumber.from(0));
      expect(ev721[1].args!.minorERCIdentifier).to.equal(ethers.utils.id("ERC721Metadata"));
      expect(ev721[1].args!.ercStatus).to.equal(ethers.utils.id("FINAL"));

      // ...
    });

    it("Should return proper ercStatus value when called supportERC() for declared supported ERC/features", async function () {
      // 当为声明支持的 ERC/功能调用 supportERC() 时,应返回正确的 ercStatus 值
      let { erc721ForTesting, owner } = await loadFixture(deployFixture);
      expect(await erc721ForTesting.supportERC(owner.address, 5269, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(ethers.utils.id("DRAFTv1"));
      expect(await erc721ForTesting.supportERC(owner.address, 721, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(ethers.utils.id("FINAL"));
      expect(await erc721ForTesting.supportERC(owner.address, 721, ethers.utils.id("ERC721Metadata"), [])).to.equal(ethers.utils.id("FINAL"));
      // ...

      expect(await erc721ForTesting.supportERC(owner.address, 721, ethers.utils.id("WRONG FEATURE"), [])).to.equal(BigNumber.from(0));
      expect(await erc721ForTesting.supportERC(owner.address, 9999, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(BigNumber.from(0));
    });

    it("Should return zero as ercStatus value when called supportERC() for non declared ERC/features", async function () {
      // 当为未声明的 ERC/功能调用 supportERC() 时,应返回零作为 ercStatus 值
      let { erc721ForTesting, owner } = await loadFixture(deployFixture);
      expect(await erc721ForTesting.supportERC(owner.address, 721, ethers.utils.id("WRONG FEATURE"), [])).to.equal(BigNumber.from(0));
      expect(await erc721ForTesting.supportERC(owner.address, 9999, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(BigNumber.from(0));
    });
  });
});

请参阅 TestERC5269.ts

参考实现

以下是此 ERC 的参考实现:

contract ERC5269 is IERC5269 {
    bytes32 constant public ERC_STATUS = keccak256("DRAFTv1");
    constructor () {
        emit OnSupportERC(address(0x0), 5269, bytes32(0), ERC_STATUS, "");
    }

    function _supportERC(
        address /*caller*/,
        uint256 majorERCIdentifier,
        bytes32 minorERCIdentifier,
        bytes calldata /*extraData*/)
    internal virtual view returns (bytes32 ercStatus) {
        if (majorERCIdentifier == 5269) {
            if (minorERCIdentifier == bytes32(0)) {
                return ERC_STATUS;
            }
        }
        return bytes32(0);
    }

    function supportERC(
        address caller,
        uint256 majorERCIdentifier,
        bytes32 minorERCIdentifier,
        bytes calldata extraData)
    external virtual view returns (bytes32 ercStatus) {
        return _supportERC(caller, majorERCIdentifier, minorERCIdentifier, extraData);
    }
}

请参阅 ERC5269.sol

这是一个 ERC-721 的合约也实现此 ERC 以使其更容易检测和发现的示例:

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "../ERC5269.sol";
contract ERC721ForTesting is ERC721, ERC5269 {

    bytes32 constant public ERC_FINAL = keccak256("FINAL");
    constructor() ERC721("ERC721ForTesting", "E721FT") ERC5269() {
        _mint(msg.sender, 0);
        emit OnSupportERC(address(0x0), 721, bytes32(0), ERC_FINAL, "");
        emit OnSupportERC(address(0x0), 721, keccak256("ERC721Metadata"), ERC_FINAL, "");
        emit OnSupportERC(address(0x0), 721, keccak256("ERC721Enumerable"), ERC_FINAL, "");
    }

  function supportERC(
    address caller,
    uint256 majorERCIdentifier,
    bytes32 minorERCIdentifier,
    bytes calldata extraData)
  external
  override
  view
  returns (bytes32 ercStatus) {
    if (majorERCIdentifier == 721) {
      if (minorERCIdentifier == 0) {
        return keccak256("FINAL");
      } else if (minorERCIdentifier == keccak256("ERC721Metadata")) {
        return keccak256("FINAL");
      } else if (minorERCIdentifier == keccak256("ERC721Enumerable")) {
        return keccak256("FINAL");
      }
    }
    return super._supportERC(caller, majorERCIdentifier, minorERCIdentifier, extraData);
  }
}

请参阅 ERC721ForTesting.sol

安全考虑

ERC-165 类似,接口的调用者必须假定声明他们支持此类 ERC 接口的智能合约不一定正确地支持它们。

版权

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

Citation

Please cite this document as:

Zainan Victor Zhou (@xinbenlv), "ERC-5269: ERC 检测与发现 [DRAFT]," Ethereum Improvement Proposals, no. 5269, July 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5269.