EIP165
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 代码示例演示如何计算接口标识符:
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
:当接口IDinterfaceID
是0x01ffc9a7
(EIP165 标准接口)返回 truefalse
:当interfacecID
是0xffffffff
返回 falsetrue
:任何合约实现了接口的interfaceID
都返回 truefalse
:其他的都返回 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:
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
合约是通用的,可重用的。
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随着支持的接口数量增加而线性增加。
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 最终确定(首次发布版本)。