Michael.W基于Foundry精读Openzeppelin第23期——ERC165Checker.sol
- 0. 版本
- 0.1 ERC165Checker.sol
- 1. 目标合约
- 2. 代码精读
- 2.1 supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId)
- 2.2 supportsERC165(address account)
- 2.3 supportsInterface(address account, bytes4 interfaceId)
- 2.4 getSupportedInterfaces(address account, bytes4[] memory interfaceIds)
- 2.5 supportsAllInterfaces(address account, bytes4[] memory interfaceIds)
0. 版本
[openzeppelin]:v4.8.3,[forge-std]:v1.5.6
0.1 ERC165Checker.sol
Github: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.8.3/contracts/utils/introspection/ERC165Checker.sol
ERC165Checker库是用来查询已实现IERC165的目标合约自身实现了哪些interface的工具库。在使用时需要注意:利用ERC165Checker提供的查询方法进行查询的过程不会因为目标合约没有实现待查询interface而发生revert。而是通过bool变量来标识目标合约对待查询interface的实现情况,查询方可以通过该bool值进行判断从而进行下一步的逻辑处理。
1. 目标合约
封装ERC165Checker library成为一个可调用合约:
Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/src/utils/introspection/MockERC165Checker.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "openzeppelin-contracts/contracts/utils/introspection/ERC165Checker.sol";
contract MockERC165Checker {
using ERC165Checker for address;
function supportsERC165(address account) external view returns (bool){
return account.supportsERC165();
}
function supportsInterface(address account, bytes4 interfaceId) external view returns (bool){
return account.supportsInterface(interfaceId);
}
function getSupportedInterfaces(address account, bytes4[] memory interfaceIds)
external
view
returns (bool[] memory){
return account.getSupportedInterfaces(interfaceIds);
}
function supportsAllInterfaces(address account, bytes4[] memory interfaceIds) external view returns (bool){
return account.supportsAllInterfaces(interfaceIds);
}
function supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId) external view returns (bool){
return account.supportsERC165InterfaceUnchecked(interfaceId);
}
}
全部foundry测试合约:
Github: https://github.com/RevelationOfTuring/foundry-openzeppelin-contracts/blob/master/test/utils/introspection/ERC165Checker.t.sol
2. 代码精读
2.1 supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId)
查询目标地址account是否继承了interface id对应的interface。即只做对应interface id的显式支持interface检查,而不做对IERC165 interface的支持检查。
注:方法内部使用了内联汇编的staticcall,如果account合约没有实现IERC165 interface 也不会revert。
function supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId) internal view returns (bool) {
// 拼接调用supportsInterface(bytes4 interfaceId)的calldata
bytes memory encodedParams = abi.encodeWithSelector(IERC165.supportsInterface.selector, interfaceId);
// 内联汇编中static call目标合约的supportsInterface方法
bool success;
uint256 returnSize;
uint256 returnValue;
assembly {
// 内联汇编中的staticcall的参数为:staticcall(g, a, in, insize, out, outsize):
// - g: 本次调用时设置的gas上限为30000
// - a: call的合约地址为account
// - in: memory中的staticcall的calldata的起始位置,即encodedParams指针+32个字节
// - insize: memory中的staticcall的calldata的长度,即encodedParams指针指向内存中的值
// - out: memory中存放返回数据的起始位置,即memory中第一个字位置0x00
// - outsize: memory中存放返回数据的长度——0x20个字节(因为supportsInterface方法返回值为bool,内存中用一个字来存储)
// 注: 内联汇编staticcall详解参见博文:https://blog.csdn.net/michael_wgy_/article/details/132196437
success := staticcall(30000, account, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x20)
// static call的returndata的字节长度
returnSize := returndatasize()
// 从memory中取出staticcall的返回值,即bool返回值内容
returnValue := mload(0x00)
}
// 返回true的条件(与关系):
// 1. success为true —— static call成功调用;
// 2. static call的return data字节长度>=32,即staticcall有返回数据;
// 3. static call的返回值为true
return success && returnSize >= 0x20 && returnValue > 0;
}
foundry代码验证
import "openzeppelin-contracts/contracts/utils/introspection/ERC165.sol";
import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
// all kinds of target contracts for test
// no method `supportsInterface(bytes4 interfaceId)`
contract SupportNone {}
contract SupportERC165 is ERC165 {}
contract SupportERC165ButNotInvalidInterfaceId is ERC165 {
function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
return interfaceId == 0xffffffff || super.supportsInterface(interfaceId);
}
}
interface ICustomized {
function helloMichael() external view returns (string memory);
}
contract SupportManyInterfacesButNotERC165 is ERC20, ICustomized {
string _str = "michael.w";
constructor()ERC20("", ""){}
function helloMichael() external view returns (string memory){
return _str;
}
function supportsInterface(bytes4 interfaceId) public pure returns (bool) {
return interfaceId == type(IERC20).interfaceId ||
interfaceId == type(IERC20Metadata).interfaceId ||
interfaceId == type(ICustomized).interfaceId;
}
}
contract SupportManyInterfacesWithERC165 is ERC165, ERC20, ICustomized {
string _str = "michael.w";
constructor()ERC20("", ""){}
function helloMichael() external view returns (string memory){
return _str;
}
function supportsInterface(bytes4 interfaceId) public view override returns (bool) {
return interfaceId == type(IERC20).interfaceId ||
interfaceId == type(IERC20Metadata).interfaceId ||
interfaceId == type(ICustomized).interfaceId ||
super.supportsInterface(interfaceId);
}
}
contract ERC165CheckerTest is Test {
MockERC165Checker testing = new MockERC165Checker();
SupportNone supportNone = new SupportNone();
SupportERC165 supportERC165 = new SupportERC165();
SupportERC165ButNotInvalidInterfaceId supportERC165ButNotInvalidInterfaceId = new SupportERC165ButNotInvalidInterfaceId();
SupportManyInterfacesButNotERC165 supportManyInterfacesButNotERC165 = new SupportManyInterfacesButNotERC165();
SupportManyInterfacesWithERC165 supportManyInterfacesWithERC165 = new SupportManyInterfacesWithERC165();
bytes4 constant INTERFACE_ID_INVALID = 0xffffffff;
function test_SupportsERC165InterfaceUnchecked() external {
// case 1: query to contract SupportNone without revert and return false
assertFalse(testing.supportsERC165InterfaceUnchecked(address(supportNone), type(IERC165).interfaceId));
assertFalse(testing.supportsERC165InterfaceUnchecked(address(supportNone), INTERFACE_ID_INVALID));
// case 2: query to contract SupportERC165
assertTrue(testing.supportsERC165InterfaceUnchecked(address(supportERC165), type(IERC165).interfaceId));
assertFalse(testing.supportsERC165InterfaceUnchecked(address(supportERC165), INTERFACE_ID_INVALID));
// case 3: query to contract SupportERC165ButNotInvalidInterfaceId
assertTrue(testing.supportsERC165InterfaceUnchecked(address(supportERC165ButNotInvalidInterfaceId), type(IERC165).interfaceId));
assertTrue(testing.supportsERC165InterfaceUnchecked(address(supportERC165ButNotInvalidInterfaceId), INTERFACE_ID_INVALID));
// case 4: query to contract SupportManyInterfacesButNotERC165
assertFalse(testing.supportsERC165InterfaceUnchecked(address(supportManyInterfacesButNotERC165), type(IERC165).interfaceId));
assertFalse(testing.supportsERC165InterfaceUnchecked(address(supportManyInterfacesButNotERC165), INTERFACE_ID_INVALID));
assertTrue(testing.supportsERC165InterfaceUnchecked(address(supportManyInterfacesButNotERC165), type(IERC20).interfaceId));
assertTrue(testing.supportsERC165InterfaceUnchecked(address(supportManyInterfacesButNotERC165), type(IERC20Metadata).interfaceId));
assertTrue(testing.supportsERC165InterfaceUnchecked(address(supportManyInterfacesButNotERC165), type(ICustomized).interfaceId));
// case 5: query to contract SupportManyInterfacesWithERC165
assertTrue(testing.supportsERC165InterfaceUnchecked(address(supportManyInterfacesWithERC165), type(IERC165).interfaceId));
assertFalse(testing.supportsERC165InterfaceUnchecked(address(supportManyInterfacesWithERC165), INTERFACE_ID_INVALID));
assertTrue(testing.supportsERC165InterfaceUnchecked(address(supportManyInterfacesWithERC165), type(IERC20).interfaceId));
assertTrue(testing.supportsERC165InterfaceUnchecked(address(supportManyInterfacesWithERC165), type(IERC20Metadata).interfaceId));
assertTrue(testing.supportsERC165InterfaceUnchecked(address(supportManyInterfacesWithERC165), type(ICustomized).interfaceId));
}
}
2.2 supportsERC165(address account)
检查目标地址account是否实现了IERC165 interface。
注:任何实现了IERC165的合约都必须支持对IERC165 interface id的显式支持查询和对invalid interface id (即0xffffffff
)的显式不支持查询。
// 按照EIP-165规范,任何接口的interface id都不应该是0xffffffff
bytes4 private constant _INTERFACE_ID_INVALID = 0xffffffff;
function supportsERC165(address account) internal view returns (bool) {
// 分别用IERC165 interface id和invalid interface id作为参数去static call目标合约的supportsInterface()方法。当返回值依次为true和false表示目标合约account完全支持IERC165 interface
return
supportsERC165InterfaceUnchecked(account, type(IERC165).interfaceId) &&
!supportsERC165InterfaceUnchecked(account, _INTERFACE_ID_INVALID);
}
foundry代码验证
contract ERC165CheckerTest is Test {
MockERC165Checker testing = new MockERC165Checker();
SupportNone supportNone = new SupportNone();
SupportERC165 supportERC165 = new SupportERC165();
SupportERC165ButNotInvalidInterfaceId supportERC165ButNotInvalidInterfaceId = new SupportERC165ButNotInvalidInterfaceId();
SupportManyInterfacesButNotERC165 supportManyInterfacesButNotERC165 = new SupportManyInterfacesButNotERC165();
SupportManyInterfacesWithERC165 supportManyInterfacesWithERC165 = new SupportManyInterfacesWithERC165();
bytes4 constant INTERFACE_ID_INVALID = 0xffffffff;
function test_SupportsERC165() external {
// case 1: query to contract SupportNone without revert and return false
assertFalse(testing.supportsERC165(address(supportNone)));
// case 2: query to contract SupportERC165
assertTrue(testing.supportsERC165(address(supportERC165)));
// case 3: query to contract SupportERC165ButNotInvalidInterfaceId
assertFalse(testing.supportsERC165(address(supportERC165ButNotInvalidInterfaceId)));
// case 4: query to contract SupportManyInterfacesButNotERC165
assertFalse(testing.supportsERC165(address(supportManyInterfacesButNotERC165)));
// case 5: query to contract SupportManyInterfacesWithERC165
assertTrue(testing.supportsERC165(address(supportManyInterfacesWithERC165)));
}
}
2.3 supportsInterface(address account, bytes4 interfaceId)
检查目标地址account是否实现了输入interface id对应的interface。
注:该函数会同时检查目标地址是否支持IERC165 interface。
function supportsInterface(address account, bytes4 interfaceId) internal view returns (bool) {
// 当目标地址同时支持IERC165 interface和对应interface id才被认为是已支持对应interface id对应的interface
return supportsERC165(account) && supportsERC165InterfaceUnchecked(account, interfaceId);
}
foundry代码验证
contract ERC165CheckerTest is Test {
MockERC165Checker testing = new MockERC165Checker();
SupportNone supportNone = new SupportNone();
SupportERC165 supportERC165 = new SupportERC165();
SupportERC165ButNotInvalidInterfaceId supportERC165ButNotInvalidInterfaceId = new SupportERC165ButNotInvalidInterfaceId();
SupportManyInterfacesButNotERC165 supportManyInterfacesButNotERC165 = new SupportManyInterfacesButNotERC165();
SupportManyInterfacesWithERC165 supportManyInterfacesWithERC165 = new SupportManyInterfacesWithERC165();
bytes4 constant INTERFACE_ID_INVALID = 0xffffffff;
function test_SupportsInterface() external {
// case 1: query to contract SupportNone
assertFalse(testing.supportsInterface(address(supportNone), INTERFACE_ID_INVALID));
assertFalse(testing.supportsInterface(address(supportNone), type(IERC165).interfaceId));
// case 2: query to contract SupportERC165
assertFalse(testing.supportsInterface(address(supportERC165), INTERFACE_ID_INVALID));
assertTrue(testing.supportsInterface(address(supportERC165), type(IERC165).interfaceId));
// case 3: query to contract SupportERC165ButNotInvalidInterfaceId
assertFalse(testing.supportsInterface(address(supportERC165ButNotInvalidInterfaceId), INTERFACE_ID_INVALID));
assertFalse(testing.supportsInterface(address(supportERC165ButNotInvalidInterfaceId), type(IERC165).interfaceId));
// case 4: query to contract SupportManyInterfacesButNotERC165
assertFalse(testing.supportsInterface(address(supportManyInterfacesButNotERC165), INTERFACE_ID_INVALID));
assertFalse(testing.supportsInterface(address(supportManyInterfacesButNotERC165), type(IERC165).interfaceId));
assertFalse(testing.supportsInterface(address(supportManyInterfacesButNotERC165), type(IERC20).interfaceId));
assertFalse(testing.supportsInterface(address(supportManyInterfacesButNotERC165), type(IERC20Metadata).interfaceId));
assertFalse(testing.supportsInterface(address(supportManyInterfacesButNotERC165), type(ICustomized).interfaceId));
// case 5: query to contract SupportManyInterfacesWithERC165
assertFalse(testing.supportsInterface(address(supportManyInterfacesWithERC165), INTERFACE_ID_INVALID));
assertTrue(testing.supportsInterface(address(supportManyInterfacesWithERC165), type(IERC165).interfaceId));
assertTrue(testing.supportsInterface(address(supportManyInterfacesWithERC165), type(IERC20).interfaceId));
assertTrue(testing.supportsInterface(address(supportManyInterfacesWithERC165), type(IERC20Metadata).interfaceId));
assertTrue(testing.supportsInterface(address(supportManyInterfacesWithERC165), type(ICustomized).interfaceId));
}
}
2.4 getSupportedInterfaces(address account, bytes4[] memory interfaceIds)
批量检查目标地址account是否实现了输入interface ids对应的interface数组。返回值为一个bool数组,分别对应对相同index的interface ids数组成员的支持与否。
function getSupportedInterfaces(address account, bytes4[] memory interfaceIds)
internal
view
returns (bool[] memory)
{
// 在内存中创建与传入interface ids数组相同长度的bool数组
bool[] memory interfaceIdsSupported = new bool[](interfaceIds.length);
if (supportsERC165(account)) {
// 如果目标地址account已经完全支持IERC165 interface,才会进一步去检查传入的interface ids数组的支持情况。否则,直接认作全不支持
for (uint256 i = 0; i < interfaceIds.length; i++) {
// 遍历查询目标account对各个interfaceId的支持情况,并将检查结果依次存入bool数组
interfaceIdsSupported[i] = supportsERC165InterfaceUnchecked(account, interfaceIds[i]);
}
}
// 返回bool数组
return interfaceIdsSupported;
}
foundry代码验证
contract ERC165CheckerTest is Test {
MockERC165Checker testing = new MockERC165Checker();
SupportNone supportNone = new SupportNone();
SupportERC165 supportERC165 = new SupportERC165();
SupportERC165ButNotInvalidInterfaceId supportERC165ButNotInvalidInterfaceId = new SupportERC165ButNotInvalidInterfaceId();
SupportManyInterfacesButNotERC165 supportManyInterfacesButNotERC165 = new SupportManyInterfacesButNotERC165();
SupportManyInterfacesWithERC165 supportManyInterfacesWithERC165 = new SupportManyInterfacesWithERC165();
bytes4 constant INTERFACE_ID_INVALID = 0xffffffff;
function test_GetSupportedInterfaces() external {
bytes4[] memory interfaceIds = new bytes4[](4);
interfaceIds[0] = type(IERC165).interfaceId;
interfaceIds[1] = type(IERC20).interfaceId;
interfaceIds[2] = type(IERC20Metadata).interfaceId;
interfaceIds[3] = type(ICustomized).interfaceId;
// case 1: query to contract SupportNone
bool[] memory supported = testing.getSupportedInterfaces(address(supportNone), interfaceIds);
assertEq(supported.length, 4);
// all false because of not supporting ERC165 completely
assertFalse(supported[0]);
assertFalse(supported[1]);
assertFalse(supported[2]);
assertFalse(supported[3]);
// case 2: query to contract SupportERC165
supported = testing.getSupportedInterfaces(address(supportERC165), interfaceIds);
assertEq(supported.length, 4);
assertTrue(supported[0]);
assertFalse(supported[1]);
assertFalse(supported[2]);
assertFalse(supported[3]);
// case 3: query to contract SupportERC165ButNotInvalidInterfaceId
supported = testing.getSupportedInterfaces(address(supportERC165ButNotInvalidInterfaceId), interfaceIds);
assertEq(supported.length, 4);
// all false because of not supporting ERC165 completely
assertFalse(supported[0]);
assertFalse(supported[1]);
assertFalse(supported[2]);
assertFalse(supported[3]);
// case 4: query to contract SupportManyInterfacesButNotERC165
supported = testing.getSupportedInterfaces(address(supportManyInterfacesButNotERC165), interfaceIds);
assertEq(supported.length, 4);
// all false because of not supporting ERC165 completely
assertFalse(supported[0]);
assertFalse(supported[1]);
assertFalse(supported[2]);
assertFalse(supported[3]);
// case 5: query to contract SupportManyInterfacesWithERC165
supported = testing.getSupportedInterfaces(address(supportManyInterfacesWithERC165), interfaceIds);
assertEq(supported.length, 4);
// all true
assertTrue(supported[0]);
assertTrue(supported[1]);
assertTrue(supported[2]);
assertTrue(supported[3]);
}
}
2.5 supportsAllInterfaces(address account, bytes4[] memory interfaceIds)
检查目标地址account是否全部实现了输入interface ids对应的interface数组。如果全部实现返回true,否则返回false。
注:每次调用调用supportsInterface(address account, bytes4 interfaceId)
检查目标地址account对某一interface id的支持情况都会进行一次对IERC165的支持检查。而本方法中无论数组interfaceIds的长度是多少都只会进行一次对IERC165的支持检查,非常节约gas。
function supportsAllInterfaces(address account, bytes4[] memory interfaceIds) internal view returns (bool) {
// 如果目标地址account已经完全支持IERC165 interface,才会进一步去检查传入的interface ids数组的支持情况。否则,直接返回false
if (!supportsERC165(account)) {
return false;
}
// 遍历查询目标地址account对各个interface id的支持情况。只要出现任意一个interface id不支持的情况就直接返回false
for (uint256 i = 0; i < interfaceIds.length; i++) {
if (!supportsERC165InterfaceUnchecked(account, interfaceIds[i])) {
return false;
}
}
// 当目标地址account对传入的各个interface id都支持时,返回true
return true;
}
foundry代码验证
contract ERC165CheckerTest is Test {
MockERC165Checker testing = new MockERC165Checker();
SupportNone supportNone = new SupportNone();
SupportERC165 supportERC165 = new SupportERC165();
SupportERC165ButNotInvalidInterfaceId supportERC165ButNotInvalidInterfaceId = new SupportERC165ButNotInvalidInterfaceId();
SupportManyInterfacesButNotERC165 supportManyInterfacesButNotERC165 = new SupportManyInterfacesButNotERC165();
SupportManyInterfacesWithERC165 supportManyInterfacesWithERC165 = new SupportManyInterfacesWithERC165();
bytes4 constant INTERFACE_ID_INVALID = 0xffffffff;
function test_SupportsAllInterfaces() external {
bytes4[] memory interfaceIds = new bytes4[](4);
interfaceIds[0] = type(IERC165).interfaceId;
interfaceIds[1] = type(IERC20).interfaceId;
interfaceIds[2] = type(IERC20Metadata).interfaceId;
interfaceIds[3] = type(ICustomized).interfaceId;
// case 1: query to contract SupportNone
assertFalse(testing.supportsAllInterfaces(address(supportNone), interfaceIds));
// case 2: query to contract SupportERC165
assertFalse(testing.supportsAllInterfaces(address(supportERC165), interfaceIds));
// case 3: query to contract SupportERC165ButNotInvalidInterfaceId
assertFalse(testing.supportsAllInterfaces(address(supportERC165ButNotInvalidInterfaceId), interfaceIds));
// case 4: query to contract SupportManyInterfacesButNotERC165
assertFalse(testing.supportsAllInterfaces(address(supportManyInterfacesButNotERC165), interfaceIds));
// case 5: query to contract SupportManyInterfacesWithERC165
assertTrue(testing.supportsAllInterfaces(address(supportManyInterfacesWithERC165), interfaceIds));
}
}
ps:
本人热爱图灵,热爱中本聪,热爱V神。
以下是我个人的公众号,如果有技术问题可以关注我的公众号来跟我交流。
同时我也会在这个公众号上每周更新我的原创文章,喜欢的小伙伴或者老伙计可以支持一下!
如果需要转发,麻烦注明作者。十分感谢!
公众号名称:后现代泼痞浪漫主义奠基人