# EIP 165: ERC-165 标准接口检测
作者 | 类型 | 分类 | 状态 | 创建时间 | 依赖 |
---|---|---|---|---|---|
Christian Reitwießner 等 | Standards Track | ERC | Final | 2018-01-23 | 214 |
# 简要说明
这个提案创建一个标准方法以发布和检测智能合约实现了哪些接口。
# 摘要
提案标准化了以下内容:
- 接口如何识别
- 合约如何发布实现的接口
- 如何检测合约是否实现了 ERC-165
- 如何检测合约是否实现了某个接口
# 动机
对于一些“标准的接口” ,如: 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;
}
}
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);
}
2
3
4
5
6
7
8
这个接口的接口ID 为 0x01ffc9a7
, 可以使用 bytes4(keccak256('supportsInterface(bytes4)'));
计算得到,或者使用合约函数的selector
方法(如上面Selector)。
因此,合约实现 supportsInterface
函数将返回:
true
:当接口IDinterfaceID
是0x01ffc9a7
(EIP165 标准接口)返回 truefalse
:当interfaceID
是0xffffffff
返回 falsetrue
:任何合约实现了接口的interfaceID
都返回 truefalse
:其他的都返回 false
除了按上面的要求返回bool 值的要求外,这个函数的实现应该消耗在 30,000 gas 以内。
实现说明: 实现这个函数有几种方法。 可以参阅示例实实现和关于gas使用的讨论。
# 如何检测合约是否实现了 ERC-165
- 在合约地址上使用附加数据(input data)
0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000
和 gas 30,000 进行STATICCALL
调用,相当于contract.supportsInterface(0x01ffc9a7)
。 - 如果调用失败或返回false , 说明合约不兼容ERC-165标准
- 如果返回true,则使用输入数据
0x01ffc9a7ffffffff000000000000000000000000000000000000000000000000000000000000进行第二次调用
。 - 如果第二次调用失败或返回true,则目标合约不会实现ERC-165。
- 否则它实现了ERC-165。
# 如何检测合约是否实现了某个接口
- 如果不确定合约是否实现ERC-165,请使用上面的方法进行确认。
- 如果没有实现ERC-165,那么你将不得不看看它采用哪种老式方法。
- 如果实现了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
}
}
}
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
# 实现
这种方法使用supportsInterface
的view
(视图函数)实现。 任何输入的执行成本都是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){}
}
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
以下是supportsInterface
的pure
(纯函数)实现。 最坏情况下的执行成本是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){}
}
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 最终确定(首次发布版本)。