DCIPs/EIPS/eip-5269.md

15 KiB

eip title description author discussions-to status type category created requires
5269 EIP/ERC Detection and Discovery An interface to identify if major behavior or optional behavior specified in an ERC is supported for a given caller. Zainan Victor Zhou (@xinbenlv) https://ethereum-magicians.org/t/erc5269-human-readable-interface-detection/9957 Review Standards Track ERC 2022-07-15 5750

Abstract

An interface for better identification and detection of EIP/ERC by numbers. It designates a field in which it's called majorEIPIdentifier which is normally known or referred to as "EIP number". For example, ERC-721 aka EIP-721 has a majorEIPIdentifier = 721. This EIP has a majorEIPIdentifier = 5269.

Calling it a majorEIPIdentifier instead of EIPNumber makes it future-proof: anticipating there is a possibility where future EIP is not numbered or if we want to incorporate other types of standards.

It also proposes a new concept of minorEIPIdentifier which is left for authors of individual EIP to define. For example, EIP-721's author may define ERC721Metadata interface as minorEIPIdentifier= keccak256("ERC721Metadata").

It also proposes an event to allow smart contracts to optionally declare the EIPs they support.

Motivation

This EIP is created as a competing standard for EIP-165.

Here are the major differences between this EIP and EIP-165.

  1. EIP-165 uses the hash of a method's signature which declares the existence of one method or multiple methods, therefore it requires at least one method to exist in the first place. In some cases, some EIP/ERCs interface does not have a method, such as some EIPs related to data format and signature schemes or the "Soul-Bound-ness" aka SBT which could just revert a transfer call without needing any specific method.
  2. EIP-165 doesn't provide query ability based on the caller. The compliant contract of this EIP will respond to whether it supports certain EIP based on a given caller.

Here is the motivation for this EIP given EIP-165 already exists:

  1. Using EIP/ERC numbers improves human readability as well as make it easier to work with named contract such as ENS.

  2. Instead of using an EIP-165 identifier, we have seen an increasing interest to use EIP/ERC numbers as the way to identify or specify an EIP/ERC. For example

  • EIP-5267 specifies extensions to be a list of EIP numbers.
  • EIP-600, and EIP-601 specify an EIP number in the m / purpose' / subpurpose' / EIP' / wallet' path.
  • EIP-5568 specifies The instruction_id of an instruction defined by an EIP MUST be its EIP number unless there are exceptional circumstances (be reasonable)
  • EIP-6120 specifies struct Token { uint eip; ..., } where uint eip is an EIP number to identify EIPs.
  • EIP-867(Stagnant) proposes to create erpId: A string identifier for this ERP (likely the associated EIP number, e.g. “EIP-1234”).
  1. Having an ERC/EIP number detection interface reduces the need for a lookup table in smart contract to convert a function method or whole interface in any EIP/ERC in the bytes4 EIP-165 identifier into its respective EIP number and massively simplifies the way to specify EIP for behavior expansion.

  2. We also recognize a smart contract might have different behavior given different caller accounts. One of the most notable use cases is that when using Transparent Upgradable Pattern, a proxy contract gives an Admin account and Non-Admin account different treatment when they call.

Specification

In the following description, we use EIP and ERC inter-exchangeably. This was because while most of the time the description applies to an ERC category of the Standards Track of EIP, the ERC number space is a subspace of EIP number space and we might sometimes encounter EIPs that aren't recognized as ERCs but has behavior that's worthy of a query.

  1. Any compliant smart contract MUST implement the following interface
// DRAFTv1
pragma solidity ^0.8.9;

interface IERC5269 {
  event OnSupportEIP(
      address indexed caller, // when emitted with `address(0x0)` means all callers.
      uint256 indexed majorEIPIdentifier,
      bytes32 indexed minorEIPIdentifier, // 0 means the entire EIP
      bytes32 eipStatus,
      bytes extraData
  );

  /// @dev The core method of EIP/ERC Interface Detection
  /// @param caller, a `address` value of the address of a caller being queried whether the given EIP is supported.
  /// @param majorEIPIdentifier, a `uint256` value and SHOULD BE the EIP number being queried. Unless superseded by future EIP, such EIP number SHOULD BE less or equal to (0, 2^32-1]. For a function call to `supportEIP`, any value outside of this range is deemed unspecified and open to implementation's choice or for future EIPs to specify.
  /// @param minorEIPIdentifier, a `bytes32` value reserved for authors of individual EIP to specify. For example the author of [EIP-721](/EIPS/eip-721) MAY specify `keccak256("ERC721Metadata")` or `keccak256("ERC721Metadata.tokenURI")` as `minorEIPIdentifier` to be quired for support. Author could also use this minorEIPIdentifier to specify different versions, such as EIP-712 has its V1-V4 with different behavior.
  /// @param extraData, a `bytes` for [EIP-5750](/EIPS/eip-5750) for future extensions.
  /// @return eipStatus, a `bytes32` indicating the status of EIP the contract supports.
  ///                    - For FINAL EIPs, it MUST return `keccak256("FINAL")`.
  ///                    - For non-FINAL EIPs, it SHOULD return `keccak256("DRAFT")`.
  ///                      During EIP procedure, EIP authors are allowed to specify their own
  ///                      eipStatus other than `FINAL` or `DRAFT` at their discretion such as `keccak256("DRAFTv1")`
  ///                      or `keccak256("DRAFT-option1")`and such value of eipStatus MUST be documented in the EIP body
  function supportEIP(
    address caller,
    uint256 majorEIPIdentifier,
    bytes32 minorEIPIdentifier,
    bytes calldata extraData)
  external view returns (bytes32 eipStatus);
}

In the following description, EIP_5269_STATUS is set to be keccak256("DRAFTv1").

In addition to the behavior specified in the comments of IERC5269:

  1. Any minorEIPIdentifier=0 is reserved to be referring to the main behavior of the EIP being queried.
  2. The Author of compliant EIP is RECOMMENDED to declare a list of minorEIPIdentifier for their optional interfaces, behaviors and value range for future extension.
  3. When this EIP is FINAL, any compliant contract MUST return an EIP_5269_STATUS for the call of supportEIP((any caller), 5269, 0, [])

Note: at the current snapshot, the supportEIP((any caller), 5269, 0, []) MUST return EIP_5269_STATUS.

  1. Any complying contract SHOULD emit an OnSupportEIP(address(0), 5269, 0, EIP_5269_STATUS, []) event upon construction or upgrade.
  2. Any complying contract MAY declare for easy discovery any EIP main behavior or sub-behaviors by emitting an event of OnSupportEIP with relevant values and when the compliant contract changes whether the support an EIP or certain behavior for a certain caller or all callers.
  3. For any EIP-XXX that is NOT in Final status, when querying the supportEIP((any caller), xxx, (any minor identifier), []), it MUST NOT return keccak256("FINAL"). It is RECOMMENDED to return 0 in this case but other values of eipStatus is allowed. Caller MUST treat any returned value other than keccak256("FINAL") as non-final, and MUST treat 0 as strictly "not supported".
  4. The function supportEIP MUST be mutability view, i.e. it MUST NOT mutate any global state of EVM.

Rationale

  1. When data type uint256 majorEIPIdentifier, there are other alternative options such as:
  • (1) using a hashed version of the EIP number,
  • (2) use a raw number, or
  • (3) use an EIP-165 identifier.

The pros for (1) are that it automatically supports any evolvement of future EIP numbering/naming conventions. But the cons are it's not backward readable: seeing a hash(EIP-number) one usually can't easily guess what their EIP number is.

We choose the (2) in the rationale laid out in motivation.

  1. We have a bytes32 minorEIPIdentifier in our design decision. Alternatively, it could be (1) a number, forcing all EIP authors to define its numbering for sub-behaviors so we go with a bytes32 and ask the EIP authors to use a hash for a string name for their sub-behaviors which they are already doing by coming up with interface name or method name in their specification.

  2. Alternatively, it's possible we add extra data as a return value or an array of all EIP being supported but we are unsure how much value this complexity brings and whether the extra overhead is justified.

  3. Compared to EIP-165, we also add an additional input of address caller, given the increasing popularity of proxy patterns such as those enabled by EIP-1967. One may ask: why not simply use msg.sender? This is because we want to allow query them without transaction or a proxy contract to query whether interface ERC-number will be available to that particular sender.

  4. We reserve the input majorEIPIdentifier greater than or equals 2^32 in case we need to support other collections of standards which is not an ERC/EIP.

Test Cases


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

  describe("Deployment", function () {
    // ...
    it("Should emit proper OnSupportEIP events", async function () {
      let { txDeployErc721 } = await loadFixture(deployFixture);
      let events = txDeployErc721.events?.filter(event => event.event === 'OnSupportEIP');
      expect(events).to.have.lengthOf(4);

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

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

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

      // ...
    });

    it("Should return proper eipStatus value when called supportEIP() for declared supported EIP/features", async function () {
      let { erc721ForTesting, owner } = await loadFixture(deployFixture);
      expect(await erc721ForTesting.supportEIP(owner.address, 5269, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(ethers.utils.id("DRAFTv1"));
      expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(ethers.utils.id("FINAL"));
      expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.id("ERC721Metadata"), [])).to.equal(ethers.utils.id("FINAL"));
      // ...

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

    it("Should return zero as eipStatus value when called supportEIP() for non declared EIP/features", async function () {
      let { erc721ForTesting, owner } = await loadFixture(deployFixture);
      expect(await erc721ForTesting.supportEIP(owner.address, 721, ethers.utils.id("WRONG FEATURE"), [])).to.equal(BigNumber.from(0));
      expect(await erc721ForTesting.supportEIP(owner.address, 9999, ethers.utils.hexZeroPad("0x00", 32), [])).to.equal(BigNumber.from(0));
    });
  });
});

See TestERC5269.ts.

Reference Implementation

Here is a reference implementation for this EIP:

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

    function _supportEIP(
        address /*caller*/,
        uint256 majorEIPIdentifier,
        bytes32 minorEIPIdentifier,
        bytes calldata /*extraData*/)
    internal virtual view returns (bytes32 eipStatus) {
        if (majorEIPIdentifier == 5269) {
            if (minorEIPIdentifier == bytes32(0)) {
                return EIP_STATUS;
            }
        }
        return bytes32(0);
    }

    function supportEIP(
        address caller,
        uint256 majorEIPIdentifier,
        bytes32 minorEIPIdentifier,
        bytes calldata extraData)
    external virtual view returns (bytes32 eipStatus) {
        return _supportEIP(caller, majorEIPIdentifier, minorEIPIdentifier, extraData);
    }
}

See ERC5269.sol.

Here is an example where a contract of EIP-721 also implement this EIP to make it easier to detect and discover:

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

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

  function supportEIP(
    address caller,
    uint256 majorEIPIdentifier,
    bytes32 minorEIPIdentifier,
    bytes calldata extraData)
  external
  override
  view
  returns (bytes32 eipStatus) {
    if (majorEIPIdentifier == 721) {
      if (minorEIPIdentifier == 0) {
        return keccak256("FINAL");
      } else if (minorEIPIdentifier == keccak256("ERC721Metadata")) {
        return keccak256("FINAL");
      } else if (minorEIPIdentifier == keccak256("ERC721Enumerable")) {
        return keccak256("FINAL");
      }
    }
    return super._supportEIP(caller, majorEIPIdentifier, minorEIPIdentifier, extraData);
  }
}

See ERC721ForTesting.sol.

Security Considerations

Similar to EIP-165 callers of the interface MUST assume the smart contract declaring they support such EIP interfaces doesn't necessarily correctly support them.

Copyright and related rights waived via CC0.