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
}
}
}
当
_interfaceId
为uint32
类型时:
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
}
}
}
所以对于 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,然后通过实现者_implementer
的canImplementInterfaceForAddress()
函数来检测,如果实现了则通过。这个函数可以在这里:链接看到实现逻辑(这里需要注意重入风险,具体情况具体分析)。
注:ERC1820ImplementerInterface(_implementer).canImplementInterfaceForAddress(_interfaceHash, addr)
这行代码很重要,有重入风险,同时要求实现者_implementer
必须按要求返回指定的值ERC1820_ACCEPT_MAGIC
。
3. 总结
ERC1820协议主要用于以太坊智能合约的接口查询和管理。它为智能合约之间的交互提供了标准化的方式,使得合约可以公开声明并查询它们所实现的接口。这对于智能合约的互操作性和扩展性非常重要。
以下是一些具体的应用场景:
- 合约功能发现:通过ERC1820协议,合约可以公开声明它们实现的接口,其他合约就可以查询这些接口,了解如何与该合约交互。这使得合约之间的交互更为灵活和高效。
- 合约升级:智能合约一旦部署,其代码就不能更改。但是,通过ERC1820协议,可以将接口实现的逻辑放在另一个可以升级的合约中。这样,即使主合约的代码不变,也可以通过更改实现接口的合约来升级功能。
- 合约互操作性:ERC1820协议支持任意接口的注册,这使得不同的合约可以实现和支持各种各样的接口,大大增强了合约之间的互操作性。
- 合约安全性:ERC1820协议的查询机制可以避免对未实现特定接口的合约进行错误的调用,从而增加了智能合约的安全性。
总的来说,ERC1820协议的作用就是提供了一种标准化的方式,让智能合约可以公开声明和查询接口,从而简化了合约之间的互动。
有了这些前置知识,就可以继续学习ERC777了。