ERC-165: 标准接口检测
Authors | Christian Reitwießner <chris@ethereum.org>, Nick Johnson <nick@ethereum.org>, Fabian Vogelsteller <fabian@lukso.network>, Jordi Baylina <jordi@baylina.cat>, Konrad Feldmeier <konrad.feldmeier@brainbot.com>, William Entriken <github.com@phor.net> |
---|---|
Created | 2018-01-23 |
Requires | EIP-214 |
Table of Contents
简单总结
创建一个标准方法来发布和检测智能合约实现的接口。
摘要
在此,我们标准化以下内容:
- 如何识别接口
- 合约如何发布其实现的接口
- 如何检测合约是否实现了 ERC-165
- 如何检测合约是否实现了任何给定的接口
动机
对于某些“标准接口”,例如 ERC-20 代币接口,有时需要查询合约是否支持该接口,如果是,则查询接口的哪个版本,以便调整与合约交互的方式。特别是对于 ERC-20,已经提出了一个版本标识符。本提案标准化了接口的概念并标准化了接口的标识(命名)。
规范
如何识别接口
对于此标准,接口是以太坊 ABI 定义的函数选择器集合。这是 Solidity 的接口概念 和 interface
关键字定义的一个子集,该定义还定义了返回类型、可变性和事件。
我们将接口标识符定义为接口中所有函数选择器的 XOR。此代码示例展示了如何计算接口标识符:
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 接口标识符,如 ERC-165 中所指定
/// @dev 接口标识在 ERC-165 中指定。此函数使用少于 30,000 gas。
/// @return 如果合约实现了 `interfaceID` 且 `interfaceID` 不是 0xffffffff,则返回 `true`,否则返回 `false`
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
此接口的接口标识符为 0x01ffc9a7
。您可以通过运行 bytes4(keccak256('supportsInterface(bytes4)'));
或使用上面的 Selector
合约来计算此值。
因此,实现合约将具有一个 supportsInterface
函数,该函数返回:
- 当
interfaceID
为0x01ffc9a7
时,返回true
(EIP165 接口) - 当
interfaceID
为0xffffffff
时,返回false
- 对于此合约实现的任何其他
interfaceID
,返回true
- 对于任何其他
interfaceID
,返回false
此函数必须返回一个布尔值,并且最多使用 30,000 gas。
实现说明,有几种逻辑方法可以实现此函数。请参阅示例实现和有关 gas 使用的讨论。
如何检测合约是否实现了 ERC-165
- 源合约使用输入数据:
0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000
和 gas 30,000 对目标地址进行STATICCALL
。这对应于contract.supportsInterface(0x01ffc9a7)
。 - 如果调用失败或返回 false,则目标合约未实现 ERC-165。
- 如果调用返回 true,则使用输入数据
0x01ffc9a7ffffffff00000000000000000000000000000000000000000000000000000000
进行第二次调用。 - 如果第二次调用失败或返回 true,则目标合约未实现 ERC-165。
- 否则,它实现了 ERC-165。
如何检测合约是否实现了任何给定的接口
- 如果您不确定合约是否实现了 ERC-165,请使用上述过程进行确认。
- 如果它没有实现 ERC-165,那么您将不得不使用传统方法来查看它使用了哪些方法。
- 如果它实现了 ERC-165,那么只需调用
supportsInterface(interfaceID)
来确定它是否实现了您可以使用的接口。
理由
我们试图使本规范尽可能简单。此实现还与当前的 Solidity 版本兼容。
向后兼容性
上述机制(使用 0xffffffff
)应该适用于此标准之前的的大多数合约,以确定它们未实现 ERC-165。
此外,ENS 已经实现了此 EIP。
测试用例
以下是一个检测其他合约实现的接口的合约。来自 @fulldecent 和 @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 地址
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 您不得将元素 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){}
}
以下是 supportsInterface
的 pure
函数实现。最坏情况下的执行成本为 236 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 本身作为必需的支持接口),映射方法(在每种情况下)比纯方法(在最坏情况下)花费更少的 gas。
版本历史
-
PR 1640,于 2019-01-23 定稿 – 这修正了 noThrowCall 测试用例以使用 36 字节,而不是之前的 32 字节。之前的代码是一个错误,仍然在 Solidity 0.4.x 中静默工作,但被 Solidity 0.5.0 中引入的新行为破坏。此更改在 #1640 中进行了讨论。
-
EIP 165,于 2018-04-20 定稿 – 原始发布版本。
版权
通过 CC0 放弃版权和相关权利。
Citation
Please cite this document as:
Christian Reitwießner <chris@ethereum.org>, Nick Johnson <nick@ethereum.org>, Fabian Vogelsteller <fabian@lukso.network>, Jordi Baylina <jordi@baylina.cat>, Konrad Feldmeier <konrad.feldmeier@brainbot.com>, William Entriken <github.com@phor.net>, "ERC-165: 标准接口检测," Ethereum Improvement Proposals, no. 165, January 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-165.