抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

1. ERC1820简介

ERC1820标准定义了一个通用的注册表合约,任何地址(不管是合约地址还是E0A账户地址)都可以注册它支持的接口以及哪个智能合约负责接口实现。

2. ERC1820代码解读

source code:链接。如果想看每个函数的各个参数代表什么意思,可以到这里:链接

mapping(address => mapping(bytes32 => address)) internal interfaces:作用是保存某地址实现某接口的地址(说实话我感觉怪怪的,我感觉有点说不过去,举个例子:接口为I,A实现I的地址是B,换句话说就是,A用B地址来实现I接口,类似代理合约的逻辑,proxy基本上都是通过logic合约来执行逻辑)。

mapping(address => address) internal managers:作用是保存某地址的管理员

mapping(address => mapping(bytes4 => bool)) internal erc165Cached:作用是用作缓存表,用来记录某地址是否实现了 IERC165接口。

***noThrowCall(address _contract, bytes4 _interfaceId)***函数,的运作原理是_contract.staticcall(abi.encodeWithSelector(ERC165.supportsInterface.selector,_interfaceId)),即就是为了检测_contract合约是否实现了 IERC165且是否实现了指定接口_interfaceId。两个返回值的意思分别是,调用函数是否成功,返回值是否为true。

对于noThrowCall()函数,可以查缺补漏。

我好奇的是,对于 bytes4类型的 _interfaceId,执行mstore(add(x, 0x04), _interfaceId)操作之后,再预存储的32bytes里,ta是会被放在左端还是右端,同理对mstore(x, erc165ID)也是一样好奇,但是按照编码规则calldata应该为:bytes4(functon.selector)+paramters,所以应该是放在左端,写过测试用例验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
pragma solidity ^0.8.0;

contract TestAssembly {

event msgdata(bytes);

function test() public {
emit msgdata(msg.data);
}


function noThrowCall(address _contract, bytes4 _interfaceId)
public returns (uint256 success, uint256 result)
{
bytes4 erc165ID = 0xf8a8fd6d;

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 := call(
gas(), // 30k gas
_contract, // To addr
0, // msg.value
x, // Inputs are stored at location x
0x24, // Inputs are 36 (4 + 32) bytes long
x, // Store output over input (saves space)
0x20 // Outputs are 32 bytes long
)

result := mload(x) // Load the result
}
}
}

image-20240422205348910

_interfaceIduint32类型时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
pragma solidity ^0.8.0;

contract TestAssembly {

event msgdata(bytes);

function test() public {
emit msgdata(msg.data);
}

function noThrowCall(address _contract, uint32 _interfaceId)
public returns (uint256 success, uint256 result)
{
bytes4 erc165ID = 0xf8a8fd6d;

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 := call(
gas(), // 30k gas
_contract, // To addr
0, // msg.value
x, // Inputs are stored at location x
0x24, // Inputs are 36 (4 + 32) bytes long
x, // Store output over input (saves space)
0x20 // Outputs are 32 bytes long
)

result := mload(x) // Load the result
}
}
}

image-20240422205318130

所以对于 bytes(n)类型的操作,写入方式为从高位写入(即左端写入),而对于uint(n)类型则是从低位写入(即右端写入)。

***implementsERC165InterfaceNoCache(address _contract, bytes4 _interfaceId)***函数,作用是在不使用或更新缓存的情况下检查合约是否实现IERC165接口,且是否实现 _interfaceId接口。检查的方式很简单,即调用noThrowCall()函数,只有当函数调用成功,且实现了 _interfaceId 接口(即 result==true )时,该函数才返回true。

***isERC165Interface(bytes32 _interfaceHash)***函数用来检测,传入的接口hash值是不是IERC165接口,判断方法就是:对传入的参数进行与运算,如果后28为0,那么则判断该接口为IERC165接口。

***implementsERC165Interface(address _contract, bytes4 _interfaceId)***函数,作用是检查 _contract合约是否实现了 _interfaceId(多指IERC165),如果不在缓存表中存储过,则通过implementsERC165InterfaceNoCache() 函数检测,如果在缓存表中,则判断用于实现 _interfaceId的合约是不是参数 _contract本身,yes return true,no return false。

***updateERC165Cache(address _contract, bytes4 _interfaceId)***函数,通过implementsERC165InterfaceNoCache()函数来判断 _contract 是否实现了 _interfaceId,如果实现了则更新 interfaces映射,同时更新缓存(这个换成始终都是被设置为true,有什么用呢?我的理解是,在implementsERC165Interface()函数中就用的了这个映射,如果这个映射的值为 fasle则会调用implementsERC165InterfaceNoCache()函数,那么如果值为true那么则不需要调用。那么这样一来就可以节约gas的花费了)。

***getManager(address _addr)***函数,查询某地址的管理员是谁,如果没有管理员,则返回自己(相对于自己的管理员是自己)。

***setManager(address _addr, address _newManager)***函数,设置管理员,确保只能是 _addr的管理员亲自调用,如果管理员给自己又设置一边管理员,那么managers[_addr] =address(0),相对于重置管理员了,管理员变成了_addr自己。

***getInterfaceImplementer(address _addr, bytes32 _interfaceHash)***函数,查询地址是否实现了接口以及通过哪个合约实现的,如果 _addr是零地址则将 _addr看作是msg.sender。先判断是 IERC165接口hash吗,如果是,则通过implementsERC165Interface()来获取实现的地址,如果不是则通过映射interfaces来查看。

***setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer)***:设置某个地址的接口由哪个合约实现,需要由管理员来设置,待设置的关联接口的地址(如果’_addr’是零地址,则假定为’msg.sender’),而且 _interfaceHash 不能为 IERC165,然后通过实现者_implementercanImplementInterfaceForAddress()函数来检测,如果实现了则通过。这个函数可以在这里:链接看到实现逻辑(这里需要注意重入风险,具体情况具体分析)。

注:ERC1820ImplementerInterface(_implementer).canImplementInterfaceForAddress(_interfaceHash, addr)这行代码很重要,有重入风险,同时要求实现者_implementer必须按要求返回指定的值ERC1820_ACCEPT_MAGIC

3. 总结

ERC1820协议主要用于以太坊智能合约的接口查询和管理。它为智能合约之间的交互提供了标准化的方式,使得合约可以公开声明并查询它们所实现的接口。这对于智能合约的互操作性和扩展性非常重要。

以下是一些具体的应用场景:

  1. 合约功能发现:通过ERC1820协议,合约可以公开声明它们实现的接口,其他合约就可以查询这些接口,了解如何与该合约交互。这使得合约之间的交互更为灵活和高效。
  2. 合约升级:智能合约一旦部署,其代码就不能更改。但是,通过ERC1820协议,可以将接口实现的逻辑放在另一个可以升级的合约中。这样,即使主合约的代码不变,也可以通过更改实现接口的合约来升级功能。
  3. 合约互操作性:ERC1820协议支持任意接口的注册,这使得不同的合约可以实现和支持各种各样的接口,大大增强了合约之间的互操作性。
  4. 合约安全性:ERC1820协议的查询机制可以避免对未实现特定接口的合约进行错误的调用,从而增加了智能合约的安全性。

总的来说,ERC1820协议的作用就是提供了一种标准化的方式,让智能合约可以公开声明和查询接口,从而简化了合约之间的互动。

有了这些前置知识,就可以继续学习ERC777了。

评论



政策 · 统计 | 本站使用 Volantis 主题设计