EIP 165: ERC-165 标准接口检测 简要说明
创建一个标准方法来发布和检测智能合约实现的接口。
原文章:https://eips.ethereum.org/EIPS/eip-165
摘要 该提案标准化了以下内容:
接口如何识别
合约如何发布实现的接口
如何检测合约是否实现了 ERC-165
如何检测合约是否实现了某个接口
动机 对于一些“标准的接口”,如: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 代码示例演示如何计算接口标识符 :
1 2 3 4 5 6 7 8 9 10 11 12 13 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
):
1 2 3 4 5 6 7 8 9 10 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 interfaceID
是0x01ffc9a7
(EIP165 标准接口)返回 true
false
:当interfacecID
是0xffffffff
返回 false
true
:任何合约实现了接口的interfaceID
都返回 true
false
:其他的都返回 false
出了按上面的要求返回 bool 值的要求之外,这个函数的实现应该消耗在 30000 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)
来确定它是否实现了对应的接口。
向后兼容 上述机制(使用0xffffffff
)应该适用于此标准之前的大多数合约,以确定它们不兼容ERC-165。
以太坊命名服务ENS 同样实现了这个EIP。
测试用例 以下合约用于检测其他合约实现的哪些接口,来自@fulldecent and @jbaylina:
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 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 } } }
实施 这种方法使用supportsInterface
的view
(视图函数)实现。 任何输入的执行成本都是586 gas。 但合约初始化需要存储每个接口(SSTORE
是20,000 gas)。 ERC165MappingImplementation
合约是通用的,可重用的。
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 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){} }
以下是supportsInterface
的pure
(纯函数)实现。 最坏情况下的执行成本是236 gas,但gas随着支持的接口数量增加而线性增加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 最终确定(首次发布版本)。