web3探索之旅---ERC165

100 阅读5分钟

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。