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

作者 类型 分类 状态 创建时间 依赖
Christian Reitwießner 等 Standards Track ERC Final 2018-01-23 214

# 简要说明

这个提案创建一个标准方法以发布和检测智能合约实现了哪些接口。

# 摘要

提案标准化了以下内容:

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

# 动机

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

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

# 规范

# 接口如何识别

函数选择器

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

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

ABI: Application Binary Interface 参考:应用程序二进制接口, 接口规范 ABI-SPEC

接口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;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

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

# 合约如何发布实现的接口

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

pragma solidity ^0.4.20;

interface ERC165 {
    /// @notice 查询一个合约时候实现了一个接口
    /// @param interfaceID  参数:接口ID, 参考上面的定义
    /// @return true 如果函数实现了 interfaceID (interfaceID 不为 0xffffffff )返回true, 否则为 false
    function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
1
2
3
4
5
6
7
8

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

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

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

除了按上面的要求返回bool 值的要求外,这个函数的实现应该消耗在 30,000 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) 来确定它是否实现了对应的接口。

# 原理阐述

我们试图使这个规范尽可能简单。 此实现还与当前的Solidity版本兼容。

# 向后兼容

上述机制(使用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
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 实现

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

pragma solidity ^0.4.20;

import "./ERC165.sol";

contract ERC165MappingImplementation is ERC165 {
    /// @dev 不能设置 0xffffffff 为 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){}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

以下是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){}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

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

# 版本历史

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

  • EIP 165 在2018-04-20 最终确定(首次发布版本)。

# 版权

原文采用CC0, 本翻译采用BY-NC-ND许可协议,译者:深入浅出区块链 Tiny熊。