简单总结
创建一个标准方法来发布和检测智能合约实现的接口。
抽象的
在此,我们标准化以下内容:
- 如何识别接口
- 合约将如何发布它实现的接口
- 如何检测合约是否实现了 ERC-165
- 如何检测合约是否实现了任何给定的接口
动机
对于一些“标准接口”,如ERC-20 接口,有时查询合约是否支持该接口以及如果是,接口的版本是有用的,以适应合约交互的方式和。专门针对 ERC-20,已经提出了版本标识符。本提案规范了接口的概念,规范了接口的标识(命名)。
规格
如何识别接口
对于此标准,接口是由Ethereum ABI 定义的一组函数选择器。这是Solidity 的接口概念和interface
关键字定义的子集,关键字定义还定义了返回类型、可变性和事件。
我们将接口标识符定义为接口中所有函数选择器的异或。此代码示例显示如何计算接口标识符:
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.8.0;
interface ERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
该接口的接口标识符是0x01ffc9a7
。bytes4(keccak256('supportsInterface(bytes4)'));
您可以通过运行或使用上面的合约来计算Selector
。因此,执行合约将有一个supportsInterface
返回的函数:true
interfaceID
0x01ffc9a7
false
interfaceID
0xffffffff
true
interfaceID
false
interfaceID
此函数必须返回一个布尔值并最多使用 30,000 gas。
实现注意,有几种逻辑方法可以实现这个功能。请参阅示例实现和关于气体使用的讨论。
如何检测合约是否实现了 ERC-165
- 源合约
STATICCALL
- 向目标地址发送输入数据:
0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000
- 和 gas 30,000。这对应于
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。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
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)
internal
view
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
}
}
}
执行
这种方法使用view
的函数实现supportsInterface
。任何输入的执行成本都是 586 gas。但是合约初始化需要存储每个接口(SSTORE
是 20,000 gas)。合同ERC165MappingImplementation
是通用的和可重用的。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ERC165.sol";
contract ERC165MappingImplementation is ERC165 {
/// @dev You must not set element 0xffffffff to true
mapping(bytes4 => bool) internal supportedInterfaces;
constructor() {
supportedInterfaces[this.supportsInterface.selector] = true;
}
function supportsInterface(bytes4 interfaceID)
external
view
override
returns (bool)
{
return supportedInterfaces[interfaceID];
}
}
interface Simpson {
function is2D() external returns (bool);
function skinColor() external returns (string memory);
}
contract Lisa is ERC165MappingImplementation, Simpson {
constructor() {
supportedInterfaces[
this.is2D.selector ^ this.skinColor.selector
] = true;
}
function is2D() external returns (bool) {}
function skinColor() external returns (string memory) {}
}
以下是pure
的函数实现supportsInterface
。最坏情况下的执行成本是 236 gas,但随着支持接口数量的增加而线性增加。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./ERC165.sol";
interface Simpson {
function is2D() external returns (bool);
function skinColor() external returns (string memory);
}
contract Homer is IERC165, Simpson {
mapping(bytes4 => bool) _supportsInterfaces;
constructor() {
_supportsInterfaces[
Simpson.is2D.selector ^ Simpson.skinColor.selector
] = true;
}
function supportsInterface(bytes4 interfaceID)
external
view
override
returns (bool)
{
return _supportsInterfaces[interfaceID];
}
function getInterfaceId() public pure returns (bytes4, bytes4) {
return (
this.supportsInterface.selector,
this.is2D.selector ^ this.skinColor.selector
);
}
function is2D() external returns (bool) {}
function skinColor() external returns (string memory) {}
}
使用三个或更多支持的接口(包括 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放弃版权和相关权利。