EIP 165: ERC-165 标准接口检测

简要说明

创建一个标准方法来发布和检测智能合约实现的接口。

原文章:https://eips.ethereum.org/EIPS/eip-165

摘要

该提案标准化了以下内容:

  1. 接口如何识别
  2. 合约如何发布实现的接口
  3. 如何检测合约是否实现了 ERC-165
  4. 如何检测合约是否实现了某个接口

动机

对于一些“标准的接口”,如:ERC 20标准代币接口,有时查询合约是否支持接口以及是否支持接口的版本很有用,以便调整与合约的交互方式。 特别是对于ERC-20,已经提出了版本标识符。

本提议标准化了接口的概念,并标准化了接口标识(命名)。

规范

接口如何识别:

函数选择器

在此标准中,接口是由以太坊 ABI 定义的一组函数选择器。这是 Solidity 的接口(ABI)概念子集,ABI 接口还定义了返回类型,可变性(mutability)和事件。

函数选择器:函数签名,如:”myMethod(uint256, string)”的 Keccak(SHA-3)哈希的前 4 字节

ABI :Application Binary Interface 应用二进制接口

接口ID(interface identifier)定义为接口所有函数选择器的异或(XOR)

interface identifier ,也称为接口标识符

下面的 Solidity 代码示例演示如何计算接口标识符

pragma solidity ^0.4.20;

interface Solidity101 {
	function hello() external pure;
	function world(int) external pure;
}

contract Selector {
	function calculateSelector() public pure returns (bytes4) {
		Solidity101 i;
		return i.hello.selector ^ i.world.selector;
	}
}

注意: 接口不允许可选函数,因此接口标识符不包含它们。

合约如何发布实现的接口

兼容 ERC-165 的合约应该实现以下接口(ERC165.sol):

pragma solidity  ^0.4.20;

interface ERC165 {
	/// @notice 查询一个合约是否实现了一个接口
	/// @param interfaceID 参数:接口ID,参考上面的定义
	/// @dev 接口标识在ERC-165中指定。 这个功能
     /// 使用少于 30,000 Gas。
	/// @return 如果函数实现了 interfaceID(interfaceID 不为 0xffffffff)返回 true,否则为 false
	function supportsInterface(bytes4 interfaceID) external view returns(bool);
}

这个接口的接口 ID 为0x01ffc9a7,可以使用bytes4(keccak256('supportsInterface(bytes4)'));计算得到,或者使用合约函数的selector方法(如上面的Selector)。

因此,合约实现supportsInterface函数将返回:

  • true:当接口ID interfaceID0x01ffc9a7(EIP165 标准接口)返回 true
  • false:当interfacecID0xffffffff返回 false
  • true:任何合约实现了接口的interfaceID都返回 true
  • false:其他的都返回 false

出了按上面的要求返回 bool 值的要求之外,这个函数的实现应该消耗在 30000 gas 以内。

实现说明:实现这个函数有几种方法。可以参阅示例实现和关于 gas 使用的讨论。

如何检测合约是否实现了 ERC-165

  1. 在合约地址上使用附加数据(input data)0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000 和 gas 30,000 进行STATICCALL调用,相当于contract.supportsInterface(0x01ffc9a7)
  2. 如果调用失败或返回false,说明合约不兼容 ERC-165 标准。
  3. 如果返回 true,则使用输入数据0x01ffc9a7ffffffff000000000000000000000000000000000000000000000000000000000000进行二次调用
  4. 如果第二次调用失败或返回 true,则目标合约不会实现 ERC-165
  5. 否则它实现了 ERC-165。

如何检测合约是否实现了某个接口

  1. 如果不确定合约是否实现ERC-165,请使用上面的方法进行确认。
  2. 如果没有实现ERC-165,那么你将不得不看看它采用哪种老式方法。
  3. 如果实现了ERC-165,那么只需调用 supportsInterface(interfaceID) 来确定它是否实现了对应的接口。

向后兼容

上述机制(使用0xffffffff)应该适用于此标准之前的大多数合约,以确定它们不兼容ERC-165。

以太坊命名服务ENS 同样实现了这个EIP。

测试用例

以下合约用于检测其他合约实现的哪些接口,来自@fulldecent and @jbaylina:

pragma solidity ^0.4.20;

contract ERC165Query {
    bytes4 constant InvalidID = 0xffffffff;
    bytes4 constant ERC165ID = 0x01ffc9a7;

    function doesContractImplementInterface(address _contract, bytes4 _interfaceId) external view returns (bool) {
        uint256 success;
        uint256 result;

        (success, result) = noThrowCall(_contract, ERC165ID);
        if ((success==0)||(result==0)) {
            return false;
        }

        (success, result) = noThrowCall(_contract, InvalidID);
        if ((success==0)||(result!=0)) {
            return false;
        }

        (success, result) = noThrowCall(_contract, _interfaceId);
        if ((success==1)&&(result==1)) {
            return true;
        }
        return false;
    }

    function noThrowCall(address _contract, bytes4 _interfaceId) constant internal returns (uint256 success, uint256 result) {
        bytes4 erc165ID = ERC165ID;

        assembly {
                let x := mload(0x40)               // Find empty storage location using "free memory pointer"
                mstore(x, erc165ID)                // Place signature at beginning of empty storage
                mstore(add(x, 0x04), _interfaceId) // Place first argument directly next to signature

                success := staticcall(
                                    30000,         // 30k gas
                                    _contract,     // To addr
                                    x,             // Inputs are stored at location x
                                    0x24,          // Inputs are 36 bytes long
                                    x,             // Store output over input (saves space)
                                    0x20)          // Outputs are 32 bytes long

                result := mload(x)                 // Load the result
        }
    }
}

实施

这种方法使用supportsInterfaceview (视图函数)实现。 任何输入的执行成本都是586 gas。 但合约初始化需要存储每个接口(SSTORE是20,000 gas)。 ERC165MappingImplementation合约是通用的,可重用的。

pragma solidity ^0.4.20;

import "./ERC165.sol";

contract ERC165MappingImplementation is ERC165 {
    /// @dev You must not set element 0xffffffff to true
    mapping(bytes4 => bool) internal supportedInterfaces;

    function ERC165MappingImplementation() internal {
        supportedInterfaces[this.supportsInterface.selector] = true;
    }

    function supportsInterface(bytes4 interfaceID) external view returns (bool) {
        return supportedInterfaces[interfaceID];
    }
}

interface Simpson {
    function is2D() external returns (bool);
    function skinColor() external returns (string);
}

contract Lisa is ERC165MappingImplementation, Simpson {
    function Lisa() public {
        supportedInterfaces[this.is2D.selector ^ this.skinColor.selector] = true;
    }

    function is2D() external returns (bool){}
    function skinColor() external returns (string){}
}

以下是supportsInterfacepure(纯函数)实现。 最坏情况下的执行成本是236 gas,但gas随着支持的接口数量增加而线性增加。

pragma solidity ^0.4.20;

import "./ERC165.sol";

interface Simpson {
    function is2D() external returns (bool);
    function skinColor() external returns (string);
}

contract Homer is ERC165, Simpson {
    function supportsInterface(bytes4 interfaceID) external view returns (bool) {
        return
          interfaceID == this.supportsInterface.selector || // ERC165
          interfaceID == this.is2D.selector
                         ^ this.skinColor.selector; // Simpson
    }

    function is2D() external returns (bool){}
    function skinColor() external returns (string){}
}

有三个或更多支持的接口(包括ERC165本身作为所需的支持接口),映射方法(在任何情况下)比纯函数方法(在最坏的情况下)花费更少的气体。

版本历史

  • PR 1640,在2019-01-23最终确定, 它将noThrowCall测试用例更正为使用36个字节而不是之前的32个字节。 之前的代码有一个错误,它仍然在Solidity 0.4.x中默默的工作,但是被Solidity 0.5.0中引入的新行为打破了。 这一变化在#1640进行了讨论。
  • EIP 165 在2018-04-20 最终确定(首次发布版本)。