ERC165是接口检测标准,标准化了接口如何识别,合约如何发布已实现的接口,以及如何检测一个合约是否实现了某个接口。
如何识别接口
接口通过接口ID进行标识
函数选择器
函数签名是指一个函数只包含函数名和参数部分(参数只包含类型,没有参数名,参数间使用逗号隔开)的字符串,如函数
function transfer(address to, uint256 value) external returns (bool)
的签名可表示为"transfer(address,uint256)"
函数选择器是函数签名的keccak256哈希值的前四个字节,上述函数的选择器在solidity直接计算如下:
bytes4(keccak256(bytes("transfer(address,uint256)")))
也可以直接通过api([Contract].[function].selector)获取:
接口ID(interfaceId)
接口ID定义为接口中所有函数选择器的亦或值,可以通过api(type([interface]).interfaceId)获取
以下是在foundry的测试
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;
import {Test, console} from "forge-std/Test.sol";
interface IContract {
function totalSupply() external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
}
contract ERC165Test is Test {
function test_functionSelector() public {
bytes4 v1 = bytes4(keccak256(bytes("transfer(address,uint256)")));
bytes4 v2 = IContract.transfer.selector;
assertEq(v1, v2);
console.logBytes4(v1);
}
function test_interfaceId() public {
bytes4 v1 = type(IContract).interfaceId;
bytes4 v2 = IContract.totalSupply.selector ^ IContract.transfer.selector;
assertEq(v1, v2);
console.logBytes4(v1);
}
}
输出为:
如何发布已实现的接口
ERC165只包含一个函数
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
这一个只读函数将返回合约是否实现了接口id为interfaceId的接口
基本上该接口有两种实现思路,一种是维护一个bytes4⇒bool的mapping,在构造函数中在mapping中写入支持哪些接口。该实现会增加部署合约的gas消耗,但在查询时消耗的gas时恒定的。
contract ERC165MappingImplementation is IERC165 {
mapping(bytes4 => bool) internal supportedInterfaces;
constructor() {
supportedInterfaces[this.supportsInterface.selector] = true;
}
function supportsInterface(bytes4 interfaceID) external view returns (bool) {
return supportedInterfaces[interfaceID];
}
}
另一种实现将支持的接口直接编码在函数中,该实现在部署时消耗更低,在查询时,如果支持的接口较少,消耗的gas也比较低,但如果支持的接口超过三个,最差情况下消耗的gas就将超过上一种实现方案
abstract contract ERC165 is IERC165 {
function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) {
return interfaceId == type(IERC165).interfaceId;
}
}
以下是在foundry中的测试用例以及测试结果
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.10;
import {Test, console} from "forge-std/Test.sol";
interface IERC165 {
/**
* @dev Returns true if this contract implements the interface defined by
* `interfaceId`. See the corresponding
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
* to learn more about how these ids are created.
*
* This function call must use less than 30 000 gas.
*/
function supportsInterface(bytes4 interfaceId) external view returns (bool);
}
interface IContract {
function totalSupply() external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
}
interface IA {
function a() external view returns (uint256);
}
interface IB {
function b() external view returns (uint256);
}
interface IC {
function c() external view returns (uint256);
}
contract ERC165MappingImplementation is IERC165 {
mapping(bytes4 => bool) internal supportedInterfaces;
constructor() {
supportedInterfaces[this.supportsInterface.selector] = true;
}
function supportsInterface(bytes4 interfaceID) external view returns (bool) {
return supportedInterfaces[interfaceID];
}
}
contract MyERC165A is ERC165MappingImplementation, IContract, IA, IB, IC {
constructor(){
supportedInterfaces[type(IContract).interfaceId] = true;
supportedInterfaces[type(IA).interfaceId] = true;
supportedInterfaces[type(IB).interfaceId] = true;
supportedInterfaces[type(IC).interfaceId] = true;
}
function totalSupply() external view returns (uint256){
return 0;
}
function transfer(address to, uint256 value) external returns (bool){
return false;
}
function a() external view returns (uint256){
return 0;
}
function b() external view returns (uint256){
return 0;
}
function c() external view returns (uint256){
return 0;
}
}
contract MyERC165B is IERC165, IContract, IA, IB, IC {
function supportsInterface(bytes4 interfaceId) external view returns (bool){
return interfaceId == this.supportsInterface.selector
|| interfaceId == type(IContract).interfaceId
|| interfaceId == type(IA).interfaceId
|| interfaceId == type(IB).interfaceId
|| interfaceId == type(IC).interfaceId;
}
function totalSupply() external view returns (uint256){
return 0;
}
function transfer(address to, uint256 value) external returns (bool){
return false;
}
function a() external view returns (uint256){
return 0;
}
function b() external view returns (uint256){
return 0;
}
function c() external view returns (uint256){
return 0;
}
}
contract ERC165Test is Test {
function test_supportsInterface_gas() public {
MyERC165A a = new MyERC165A();
MyERC165B b = new MyERC165B();
bytes4 id = type(IERC165).interfaceId;
assert(a.supportsInterface(id));
assert(b.supportsInterface(id));
id = type(IContract).interfaceId;
assert(a.supportsInterface(id));
assert(b.supportsInterface(id));
id = type(IA).interfaceId;
assert(a.supportsInterface(id));
assert(b.supportsInterface(id));
id = type(IB).interfaceId;
assert(a.supportsInterface(id));
assert(b.supportsInterface(id));
id = type(IC).interfaceId;
assert(a.supportsInterface(id));
assert(b.supportsInterface(id));
}
}
可以看出第一种实现方法,部署时消耗了183779gas,比第二种实现部署消耗高了100000左右,调用supportsInterface函数消耗的gas总是稳定的490,而第二种实现方式调用supportsInterface函数在需要判断三次的情况下gas消耗gas超过了第一种实现。
总结
ERC160相对来说比较简单,因此我对其技术细节进行了一些探索。但由于我在合约层的经验有限,难免有谬误的地方,欢迎大家指正。
ERC160规范了如何在声明和判断合约实现了哪些接口,有很多ERC标准都实现了ERC160,比如ENS,又比如稍后我将更新的,曾经在国内火爆,如今声名狼藉的NFT—ERC721。