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

1. ERC777简介

这是是官方文档的说明:链接。我记录的是我读文档和代码的自我理解。

ERC777与ERC20兼容(兼容的意思就是ERC777的功能包括了ERC20的所有功能,实现兼容的方式就是,让ERC777直接继承IERC20接口。),同时引入了operator操作员的概念,操作员可以代表另一个地址(合约或者普通账户)发送代币,这个操作员的身份类似始于ERC20中被 某地址执行 approve操作后的身份,可以托管授权这的资产。同时还引进了sender和receiver的钩子函数(hooks)让代币持有者和代币接收者能有更多的处理。而且ERC777还采用了ERC1820标准的优点,可以判断某合约是否实现ERC777协议的相关接口,更重要的是还可以将sender/receiver的钩子函数放到 地址的 implement去处理,这样一来,使得整个代币体系更丰富,拓展性也大大增强。

2. ERC777代码解读

源码来自 openzeppelin:链接。阅读该代码必须要有ERC1820的前置知识,ERC777的源码可以分为core和hooks部分。

image-20240423211618330

2.1 Core部分

解读 ERC777.sol

1
constructor(string memory name,string memory symbol,address[] memory defaultOperators) 

构造函数:在部署ERC777合约的时候,就需要传入默认的 operator,而且默认的 operator不能进行增加和删除操作。而且还会将自身添加到 ERC1820注册表中_erc1820.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this));这行代码的意思就是,记录address(this)地址实现ERC777Token接口的合约地址是address(this),换句话就是 “我”自己实现了 ERC777接口,并将其记录在注册表中。

2.1.1 View Funciotns

**name() symbol() decimals() totalSupply() balanceOf()*这几个函数和ERC20的用法一样。需要注意的是:granularity()函数,granularity*必须在创建的时设置,且不能修改这个值。同时还要保证这个值必须 >=1,执行 铸币,销币,发送操作的资产数量必须是 granularity的整数倍,否则就会被revert(如何理解呢,举个例子,比如granularity的值是2,执行mint操作的时候,mint(to,3)就会报错,因为 3%2!=0。)但是在 oppenzepelin源码中,granularity的值被设置为了1,所以可以不要太多考虑这个因素。

2.1.2 Operators

操作员是ERC777引入的一个新概念,有普通操作员和默认操作员之分。

  • 普通操作员:即是代币的 holder亲自给 operator授权的,该 operator只能操作 该holder的代币。
  • 默认操作员:即合约初始化设置的操作员,ta的权限是最大的,默认情况下它可以操作所有人的代币,类似于管理员。但是用户可以自行将其移除权限,这样一来ta就不能操作自己的tokens了。

先理解三个mapping:

1
2
3
4
5
// Immutable, but accounts may revoke them (tracked in __revokedDefaultOperators).
mapping(address => bool) private _defaultOperators;
// For each account, a mapping of its operators and revoked default operators.
mapping(address => mapping(address => bool)) private _operators;
mapping(address => mapping(address => bool)) private _revokedDefaultOperators;
  • _defaultOperators:保存某地址是不是默认操作员,在函数初始化的时候赋值。

  • _operators:保存 某地址是不是某代币holder的操作员,传参的方式为:_operators[tokenHolder][operator]

  • _revokedDefaultOperators:保存某地址是否移除了默认操作员,传参的方式为:_revokedDefaultOperators[tokenHolder][operator])

函数理解:

1
isOperatorFor(address operator,address tokenHolder)

查看该 operetor是不是tokenHolder的操作员,如果 operator和tokenHolder相等则返回true(因为每个tokenHolder是自己的operator)。如果不等,则需要判断operator是不是默认操作员,同时该默认操作员不能被该tokenHolder取消授权过,如果是且没有取消授权则返回true。第三个代码段_operators[tokenHolder][operator]则是查看tokenHolder是否对operator授权了,yes true,no false。

1
authorizeOperator(address operator)

msg.sender(tokenHolder)为 指定 operator执行授权操作,需要注意的是 msg.sender不能等于operator,否则revert,因为msg.sendedr本身就是自己的操作员,所以不能再次授权。还需要判断该 operator是不是默认操作员,如果是则执行delete _revokedDefaultOperators[msg.sender][operator]操作(即将该值变成false),如果不是默认操作员,则将 operators映射的值更新为true(授权成功)。

1
revokeOperator(address operator)

msg.sender(tokenHolder)撤销 指定 operator操作员身份,需要注意的是 msg.sender不能等于operator,否则revert,因为msg.sendedr本身就是自己的操作员,所以不能撤销自己。如果 operator是默认操作员,则_revokedDefaultOperators[msg.sender][operator] = true这样一来默认操作员便不能操作msg.sender的tokens了。如果不是默认操作员,则将operator的值更新为false

1
defaultOperators()

返回默认操作员列表。

2.1.3 Send Tokens
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function _callTokensToSend(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData
)
private
{
address implementer = _erc1820.getInterfaceImplementer(from, TOKENS_SENDER_INTERFACE_HASH);
if (implementer != address(0)) {
IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);
}
}

_callTokensToSend函数,先通过注册表getInterfaceImplementer(from, TOKENS_SENDER_INTERFACE_HASH)查看 tokenHolder 用来实现IERC777TokensSender接口的合约地址ADDRESS,如果有则调用ADDRESS中的tokensToSend函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function _callTokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData,
bool requireReceptionAck
)
private
{
address implementer = _erc1820.getInterfaceImplementer(to, TOKENS_RECIPIENT_INTERFACE_HASH);
if (implementer != address(0)) {
IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);
} else if (requireReceptionAck) {
require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient");
}
}

_callTokensReceived函数,先通过注册表getInterfaceImplementer(from, TOKENS_RECIPIENT_INTERFACE_HASH)查看 tokenHolder 用来实现IERC777Recipient接口的合约地址ADDRESS,如果有则调用ADDRESS中的tokensReceived函数,如果没有还需要判断传入的参数requireReceptionAck,如果参数为 true,那么则需要检测接收者to是否为合约地址,如果是合约地址则revert(其目的即使为了保证contract receiver必须实现 ERC777TokensRecipient)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function _move(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData
)
private
{
_balances[from] = _balances[from].sub(amount);
_balances[to] = _balances[to].add(amount);

emit Sent(operator, from, to, amount, userData, operatorData);
emit Transfer(from, to, amount);
}

_move函数则是负责更新余额,修改Holder和receiver的代币余额。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function _send(
address operator,
address from,
address to,
uint256 amount,
bytes memory userData,
bytes memory operatorData,
bool requireReceptionAck
)
private
{
require(from != address(0), "ERC777: send from the zero address");
require(to != address(0), "ERC777: send to the zero address");

_callTokensToSend(operator, from, to, amount, userData, operatorData);

_move(operator, from, to, amount, userData, operatorData);

_callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck);
}

ERC77 Token转移代币的逻辑,基本上都离不开这个函数。该函数要求 tokenHolder和receiver不能为零地址。先调用 tokenHolder的钩子函数,再更新账户的余额,最后调用receiver的钩子函数。

1
2
3
4
5
6
7
operatorSend(
address sender,
address recipient,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
)

调用该函数需要判断 msg.sender是不是 sender的operator。随后调用 _send函数。

1
send(address recipient, uint256 amount, bytes calldata data)

这是提供给tokenHolder的函数,因为调用_send函数的方式为:_send(msg.sender, msg.sender, recipient, amount, data, "", true);自己是自己的操作员,同时还要求接收者必须实现ERC777TokensRecipient接口(如果接收者是合约地址)。

1
transfer(address recipient, uint256 amount)

功能类似于send函数,也是提供给 tokenHolder 的函数。不过不同于send函数是,transfer不能传userdata,而且,如果recipient是合约地址的话,不需要满足合约地址一定要实现ERC777TokensRecipient接口的要求(前提是接收者不不能为零地址)。

1
transferFrom(address holder, address recipient, uint256 amount)

功能类似ERC20中的transferFrom函数,需要 tokenHoldermsg.sender授权,并且检查授权额度的操作在_approve(holder, spender, _allowances[holder][spender].sub(amount)),函数体没有显示的判断授权额度,而是通过了直接减的方法来验证,如果amount大于授权额度这个减法操作肯定会报错,也算一种另类的隐式检验了。

2.1.4 Mint & Burn Tokens
1
2
3
4
5
6
7
_mint(
address operator,
address account,
uint256 amount,
bytes memory userData,
bytes memory operatorData
)

铸币功能,铸多少币 _totalSupply 就要加多少。 铸币会调用_callTokensReceived函数,tokenHolder为零地址,而且还需要检测 recipient,如果recipient是合约地址的话,不需要满足合约地址一定要实现ERC777TokensRecipient接口的要求(前提是接收者不不能为零地址)。

注:在调用_mint()函数的函数中,要严格添加访问控制,因为 _mint()函数本身没有访问控制,避免出现人人可铸币的现象。

1
2
3
4
5
6
7
_burn(
address operator,
address from,
uint256 amount,
bytes memory data,
bytes memory operatorData
)

销币功能,私有函数,销毁多少币,_totalSupply 就要减多少。销币回调用_callTokensToSend函数,recipient为零地址。

1
operatorBurn(address account, uint256 amount, bytes calldata data, bytes calldata operatorData)

外部销币函数,必须要满足isOperatorFor(msg.sender, account)才能成功调用。

2.2 Hooks部分

2.2.1 ERC777TokensSender :: tokensToSend Hook

这是一个执行转账或销币的前置钩子函数,即在执行转账之前需要从 ERC1820注册表中获取 实现接口的合约implementer,并执行 implementer合约中的tokensToSend ()函数,执行该钩子函数的逻辑。至于是什么逻辑,具体取决于合约的编写者,如果是执行恶意操作则可能会造成资金损失(重入攻击)。

涉及到的函数有:transferFrom() transfer() send() operatorSend() operatorBurn()。

2.2.2 ERC777TokensRecipient :: tokensReceived Hook

这是一个执行转账或铸币的后置钩子函数,即在执行转账之后(更新完账户余额,即执行完 _move()函数)需要从 ERC1820注册表中获取 实现接口的合约implementer,并执行 implementer合约中的tokensReceived()函数,执行该钩子函数的逻辑。逻辑取决于合约的编写者,如果是执行恶意操作则可能会造成资金损失(重入攻击)。

涉及到的函数有:transferFrom() transfer() send() operatorSend() _mint()。

2.2.3 Hook的工作原理复现

复现原理为:address(this)通过 operatorSend(address(this), address(this), 0, "", "")自己给自己转账,再执行转账之前,将address(this)的实现 IERC777Sender接口的合约为 sender,并写入注册表中;同理将address(this)的实现 IERC777Recipient接口的合约为 recipient,并写入注册表中。复现旨在说明,执行转账操作时,余额更新前后可以做一系列操作,需要注意防范。在 sender::tokensToSend输出事件emit BeforeMove(operator, from, to, amount, "====== BeforeMove() ======"),在recipient::tokensReceived也输出事件emit AfterMove(operator, from, to, amount, "====== AfterMove() ======")

TestHooks.sol

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "forge-std/Test.sol";
import {IERC777Sender} from "./IERC777Sender.sol";
import {IERC777Recipient} from "./IERC777Recipient.sol";
import {IERC1820Registry} from "../ERC1820/IERC1820Registry.sol";
import {ERC777} from "./ERC777.sol";

contract TestHooks is Test {

string constant ERC1820_PATH = "out/ERC1820.sol/ERC1820Registry.json";
string constant ERC777Sender_PATH = "out/IERC777Sender.sol/ERC777Sender.json";
string constant ERCRecipient_PATH = "out/IERC777Recipient.sol/ERC777Recipient.json";
// string constant ERC777_PATH = "../../../out/ERC777.sol/ERC777.json";

IERC777Sender sender;
IERC777Recipient recipient;
IERC1820Registry registry;
ERC777 erc777;
address[] defaultOperators;

// to deploy these contracts
function setUp() public {
sender = IERC777Sender(deployer(ERC777Sender_PATH));
recipient = IERC777Recipient(deployer(ERCRecipient_PATH));
registry = IERC1820Registry(deployer(ERC1820_PATH));
// erc777 = ERC777(deployer(ERC777_PATH));
defaultOperators.push(address(this));
erc777 = new ERC777("TOKEN", "token", address(registry), defaultOperators);
}

function pre_test() internal {

// address(this)作为 defaultOperator
// 1. address(this)用来实现 tokensToSend 函数的合约是 sender,将其写入注册表
registry.setInterfaceImplementer(address(this), keccak256(abi.encodePacked("ERC777TokensSender")), address(sender));
// 2. address(this)用来实现 tokensReceived 函数的合约是 recipient,将其写入注册表
registry.setInterfaceImplementer(address(this), keccak256(abi.encodePacked("ERC777TokensRecipient")), address(recipient));

}

function test_Hooks() public {
pre_test();
// 通过自己给自己转账,触发 sender和recipient中的hook函数
erc777.operatorSend(address(this), address(this), 0, "", "");
}

// 部署合约
function deployer(string memory path) internal returns (address addr_) {
bytes memory creationCode = abi.encodePacked(vm.getCode(path));

assembly {
addr_ := create(0, add(creationCode, 0x20), mload(creationCode))
}
}

}

image-20240424205637722

3. ERC777的安全隐患

ERC777协议存在的安全隐患便是:重入漏洞(基本上就是在tokensToSend()tokensReceived())。该漏洞存在于_callTokensToSend()_callTokensReceived()。具体的攻击事件和攻击步骤,后面再补充。

评论



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