接口检测 ERC165 合约

本章讲解什么是 ERC-165 标准,以及它的作用、用法和实现代码。

视频Bilibili  |  Youtube
官网binschool.app
推特@BinSchool    DiscordBinDAO   微信:bkra50 

ERC-165 是一个以太坊智能合约的标准,它提供了一种机制来检测某个合约是否支持特定的接口。

ERC-165 标准使用比较广泛。比如:MetaMask 钱包如何判断某个地址是 ERC-721 合约,还是 ERC-20  合约呢?

通常,钱包就会按照 ERC-165 标准的要求,对合约地址进行检测,来确定它到底符合哪种标准。

ERC-165 标准非常简单,它只要求实现一个 supportsInterface 函数。该函数接收一个接口标识符 interfaceId 作为参数,并返回一个布尔值,指示合约是否支持该 interfaceId 所代表的接口。

以下 ERC-165 标准接口定义:

interface IERC165 {
  function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

任何一个合约只有实现了 IERC165 接口,才具有向外部告知支持哪些接口。

那么,钱包是如何检测某个合约地址是否符合 ERC-721 标准呢?

首先,这个智能合约必须实现 ERC-165 标准要求的 supportsInterface 函数,在这个函数中告知自己是一个 ERC-721 NFT 合约,因为它实现了所有的 ERC-721 标准所要求的函数。

然后,钱包调用 supportsInterface 函数,传一个特定的接口标识符参数,如果返回 true,就说明这个合约是 ERC-721 NFT 合约;如果返回 false,或者因为没有这个函数,调用出错,那就说明它不是一个 ERC-721 NFT 合约。

以下是 openzepplin 中 ERC-721 合约范例的部分代码:

contract ERC721 is IERC721, IERC165 {

   function supportsInterface(bytes4 interfaceId)
     public view virtual override(ERC165, IERC165)
     returns (bool) {

     // 判断 interfaceId 是否等于 ERC721 接口的 interfaceId
     return interfaceId == type(IERC721).interfaceId;
   }
}

其中,type(IERC721).interfaceId 的值为 0x80ac58cd,这是 ERC-721 标准接口的 interfaceId

对于任何的 ERC-721 NFT 智能合约,必须实现 supportsInterface 方法,这样才能被钱包应用识别为  NFT

钱包应用针对某个合约,调用它的 supportsInterface 函数,并传入 0x80ac58cd 作为参数,如果返回 true,就说明它是一个 ERC-721 NFT 合约,否则就不是。

另外,ERC-20 合约的  interfaceId0x01ffc9a7

如何计算 interfaceId 

接口标识符 interfaceId 是通过计算接口中所有函数的选择器的 Keccak-256 哈希值,并进行异或运算得到的结果。

下面是一个测试合约,它定义了一个接口 MyInterface

函数 getId() 通过内部函数来获取 MyInterfaceinterfaceId

函数 calcId() 通过手动计算来获取 MyInterfaceinterfaceId

这两个函数的计算结果是相同的。

下面的示例合约实现了 ERC-165 标准。所以,我们可以通过 supportsInterface 函数查询它是否支持 MyInterface 接口。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/interfaces/IERC165.sol";

interface MyInterface {
   function foo() external pure returns(uint256);
   function bar() external pure returns(bool);
}

contract CalcInterfaceId is MyInterface, IERC165 {
   // 直接获取接口的 interfaceId
   function getId() external pure returns(bytes4) {
     return type(MyInterface).interfaceId;
   }

   // 手动计算接口的 interfaceId
   // 接口中所有的函数选择器,进行异或操作
   function calcId() external pure returns(bytes4) {
     return bytes4(keccak256("foo()"))
       ^ bytes4(keccak256("bar()"));
   }

   // 实现 IERC165 的接口,用来判断合约是否支持 MyInterface
   function supportsInterface(bytes4 interfaceId) external pure returns(bool) {
     return interfaceId == type(MyInterface).interfaceId;
   }

   // 实现接口 MyInterface 的方法 foo
   function foo() external pure returns(uint256) {
     return 0;
   }

   // 实现接口 MyInterface 的方法 bar
   function bar() external pure returns(bool) {
     return true;
   }
}