Alert Source Discuss
Standards Track: ERC

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

简单总结

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

摘要

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

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

动机

对于某些“标准接口”,例如 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 函数,该函数返回:

  • interfaceID0x01ffc9a7 时,返回 true(EIP165 接口)
  • interfaceID0xffffffff 时,返回 false
  • 对于此合约实现的任何其他 interfaceID,返回 true
  • 对于任何其他 interfaceID,返回 false

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

实现说明,有几种逻辑方法可以实现此函数。请参阅示例实现和有关 gas 使用的讨论。

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

  1. 源合约使用输入数据:0x01ffc9a701ffc9a700000000000000000000000000000000000000000000000000000000 和 gas 30,000 对目标地址进行 STATICCALL。这对应于 contract.supportsInterface(0x01ffc9a7)
  2. 如果调用失败或返回 false,则目标合约未实现 ERC-165。
  3. 如果调用返回 true,则使用输入数据 0x01ffc9a7ffffffff00000000000000000000000000000000000000000000000000000000 进行第二次调用。
  4. 如果第二次调用失败或返回 true,则目标合约未实现 ERC-165。
  5. 否则,它实现了 ERC-165。

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

  1. 如果您不确定合约是否实现了 ERC-165,请使用上述过程进行确认。
  2. 如果它没有实现 ERC-165,那么您将不得不使用传统方法来查看它使用了哪些方法。
  3. 如果它实现了 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
        }
    }
}

实现

此方法使用 supportsInterfaceview 函数实现。对于任何输入,执行成本为 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){}
}

以下是 supportsInterfacepure 函数实现。最坏情况下的执行成本为 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.