EIP-165:标准接口检测

时间:2022-12-12 11:01:56

EIP-165:标准接口检测

简单总结

创建一个标准方法来发布和检测智能合约实现的接口。

抽象的

在此,我们标准化以下内容:

  1. 如何识别接口
  2. 合约将如何发布它实现的接口
  3. 如何检测合约是否实现了 ERC-165
  4. 如何检测合约是否实现了任何给定的接口

动机

对于一些“标准接口”,如​​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​

  • (EIP165接口)

​false​​​​interfaceID​

  • 什么时候

​0xffffffff​​​​true​​​​interfaceID​

  • 对于本合同实施的任何其他

​false​

  • 对于任何其他

​interfaceID​

此函数必须返回一个布尔值并最多使用 30,000 gas。

实现注意,有几种逻辑方法可以实现这个功能。请参阅示例实现和关于气体使用的讨论。

如何检测合约是否实现了 ERC-165

  1. 源合约

​STATICCALL​

  1. 向目标地址发送输入数据:

​0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000​

  1. 和 gas 30,000。这对应于

​contract.supportsInterface(0x01ffc9a7)​

  1. 如果调用失败或返回 false,则目标合约不执行 ERC-165。
  2. 如果调用返回 true,则使用输入数据进行第二次调用

​0x01ffc9a7ffffffff00000000000000000000000000000000000000000000000000000000​

  1. 如果第二次调用失败或返回 true,则目标合约不实现 ERC-165。
  2. 否则它执行 ERC-165。

如何检测合约是否实现了任何给定接口

  1. 如果您不确定合约是否执行 ERC-165,请使用上述程序进行确认。
  2. 如果它没有实现 ERC-165,那么你将不得不看看它使用了哪些老式的方法。
  3. 如果它实现了 ERC-165,那么只需调用它

​supportsInterface(interfaceID)​

  1. 来确定它是否实现了您可以使用的接口。

基本原理

我们试图使该规范尽可能简单。此实现也与当前的 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​​放弃版权和相关权利。