# EIP 777: ERC777 代币标准
作者 | 状态 | 类型 | 分类 | 创建时间 | 依赖 |
---|---|---|---|---|---|
Jacques Dafflon , Jordi Baylina , Thomas Shababi | Final | Standards Track | ERC | 2017-11-20 | 1820 |
# 简要说明
此EIP定义了ERC777 代币合约标准接口。
# 摘要
ERC777 与ERC20的向后兼容, 同时也定义了一些更高级的方法和代币进行交互。
如:操作员(operators) 可以代表另一个地址(合约或普通账户)发送代币, 以及 send/receive 加入了钩子函数(hooks )让代币持有者可以有更多的控制。
ERC777 同样采用了 ERC1820 标准的优点,判断接受代币的地址是合约还是普通地址,并且判断合约是否兼容ERC777协议。
# 动机
标准尝试改进大家常用的 ERC20 代币标准。 ERC777标准的主要优点有:
使用和发送以太相同的理念发送token,方法为:
send(dest, value, data)
.合约和普通地址都可以通过注册
tokensToSend
hook函数来控制和拒绝发送哪些token(拒绝发送通过在hook函数tokensToSend
里revert
来实现)。合约和普通地址都可以通过注册
tokensReceived
hook函数来控制和拒绝接受哪些token(拒绝接受通过在hook函数tokensReceived
里revert
来实现)。tokensReceived
可以通过hook函数可以做到在一个交易里完成发送代币和通知合约接受代币,而不像 ERC20 必须通过两次调用(approve
/transferFrom
)来完成。持有者可以"授权"和"撤销"操作员(operators: 可以代表持有者发送代币)。 这些操作员通常是(去中心化)交易所、支票处理机或自动支付系统。
每个代币交易都包含
data
和operatorData
字段, 可以分别传递来自持有者和操作员的数据。可以通过部署实现
tokensReceived
的代理合约来兼容没有实现tokensReceived
函数的地址。
# 规范
# ERC777Token (代币合约)
interface ERC777Token {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function totalSupply() external view returns (uint256);
function balanceOf(address holder) external view returns (uint256);
function granularity() external view returns (uint256);
function defaultOperators() external view returns (address[] memory);
function isOperatorFor(
address operator,
address holder
) external view returns (bool);
function authorizeOperator(address operator) external;
function revokeOperator(address operator) external;
function send(address to, uint256 amount, bytes calldata data) external;
function operatorSend(
address from,
address to,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external;
function burn(uint256 amount, bytes calldata data) external;
function operatorBurn(
address from,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external;
event Sent(
address indexed operator,
address indexed from,
address indexed to,
uint256 amount,
bytes data,
bytes operatorData
);
event Minted(
address indexed operator,
address indexed to,
uint256 amount,
bytes data,
bytes operatorData
);
event Burned(
address indexed operator,
address indexed from,
uint256 amount,
bytes data,
bytes operatorData
);
event AuthorizedOperator(
address indexed operator,
address indexed holder
);
event RevokedOperator(address indexed operator, address indexed holder);
}
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
59
60
ERC777 合约必须实现上述接口,也必须遵守以下规范。
合约需要用自己的地址通过 ERC1820 标准注册 ERC777Token
接口。
注册方法是调用ERC1820 注册表合约的
setInterfaceImplementer
方法,参数 _addr 及 _implementer 均是合约的地址,_interfaceHash 是ERC777Token
的keccak256
哈希值, 即0xac7fbab5f54a3ca8194167523c6753bfeb96a445279294b6125b68cce2177054
如果合约有一个开关来启用或禁用ERC777功能,每次触发开关时,代币合约必须相应地通过ERC1820注册或取消注册ERC777Token
接口。
取消注册使用代币合约地址作为参数 _addr
、ERC777Token
的keccak256
哈希作为接口哈希及'0x0作为实现者参数
_implementer调用函数
setInterfaceImplementer`,
有关详细信息,请参阅ERC1820中的为接口设置实现地址
当和代币合约进行交互时,所有的数量和余额都是无符号整型 uint256
类型 。总是以18次方存储( decimals
只能是 18),0.5个代币存储为 500,000,000,000,000,000
(0.5×1018)
ERC20内部处理也是一样(不过decimals可为其他值),最小单位相当于 wei, 用户看见的币相当于 ether。
# 视图函数
以下视图函数必须实现:
# name
函数
function name() external view returns (string memory)
返回代币名称,如: "MyToken"
- 接口ID:
06fdde03
- 返回值:代币名称
# symbol
函数
function symbol() external view returns (string memory)
返回代币的代号,如"MYT"
- 接口ID:
95d89b41
- 返回值:代币的代号
# totalSupply
函数
function totalSupply() external view returns (uint256)
返回代币总流通量。
注意: 总供应量必须是所有账号余额(balanceOf
)之和。
注意: 总供应量必须等于所有挖出的币( Minted
事件定义)减去销毁的币(Burned
事件定义)
- 接口ID:
18160ddd
- 返回值:代币总流通量
# balanceOf
函数
function balanceOf(address holder) external view returns (uint256)
返回帐户(通过参数"holder")的余额,余额 >=0 。
- 接口ID:
70a08231
- 参数holder: 查询的账号
- 返回值: 在代币合约里对应账号余额
# granularity
函数
function granularity() external view returns (uint256)
获得代币最小的划分粒度(基于内部单位的个数),最小的挖矿、发送及销毁粒度。
granularity 需要满足一下规则:
granularity 必须在创建时设置
granularity 任何时候不可以更改。
granularity 必须大于等于1。
所有的余额必须是granularity的整数倍。
挖矿、发送及销毁数量必须是granularity的整数倍。
非granularity的整数倍的操作都需要
revert
。
注意: 大部分的代币应该是完全可切分的,如果没有特别的理由,这个函数应该返回1。
- 接口ID:
556f0dc7
- 返回值: 最小的划分粒度
注意: defaultOperators
和 isOperatorFor
也是视图函数,他们在操作员部分定义。
ERC20 兼容性需要:
代币的 decimals 必须是 18
。
对于一个 纯粹的 ERC777 代币 ERC20 的 decimals
函数是可选的,与合约交互的时候不应该依赖这个值(18是隐含值)。
为了兼容 ERC20 代币, decimals
函数要求必须返回18。 (因为在 ERC20 中decimals
是可选的,也没有明确的默认值,如果没有会被认为是0)
# 操作员
操作员是可以代表持有者发送和销毁代币的账号地址。
当地址成为持有者的操作员时,需要触发 AuthorizedOperator
事件。触发事件时操作员是第
一个参数,持有者是第二个参数。
撤销时需要触发 RevokedOperator
事件。触发事件时操作员是第一个参数,持有者是第二个参数。
注意: 持有者可以有多个操作员。
合约可以定义默认操作员,所有的持有者都隐含授权给默认操作员。
定义默认的操作员不能触发 AuthorizedOperator
事件,默认操作员需遵守以下规则:
代币合约必须在创建时设置默认操作员。
默认操作员是不可变的,任何时候不可以添加和删除。
设置默认操作员不能触发
AuthorizedOperator
事件。持有者必须允许撤销默认操作员,除非默认操作员本身就是持有者。
持有者必须允许重新授权之前的默认操作员。
当默认操作员被显示的授权或撤销,需要相应的触发
AuthorizedOperator
或RevokedOperator
事件
以下是任何操作员需遵守的规则:
每个地址都是其自身的操作员,因此不能被撤销。
如果地址是某个持有者的操作员,
isOperatorFor
返回true
,否则返回false
.触发
AuthorizedOperator
事件,要确保参数正确。参考AuthorizedOperator
事件触发
RevokedOperator
事件,要确保参数正确。参考RevokedOperator
事件
注意: 持有者也许会重复授权一个操作员,每次都需要触发AuthorizedOperator
事件。
注意: A 持有者也许会撤销一个操作员,每次都需要触发RevokedOperator
事件。
event AuthorizedOperator(address indexed operator, address indexed holder)
指示持有者授权一个操作员。
注意: 不能在授权过程之外触发。
- 参数:
operator
: 操作员地址holder
: 持有者地址
event RevokedOperator(address indexed operator, address indexed holder)
指示持有者撤销操作员。
注意: 不能在撤销过程之外触发事件。
- 参数:
operator
: 操作员地址holder
: 持有者地址
下面描述的 defaultOperators
, authorizeOperator
, revokeOperator
及 isOperatorFor
函数用来实现对操作员的管理。
当然代币合约也可以实现其他的函数去管理操作员.
function defaultOperators() external view returns (address[] memory)
获取代币合约默认的操作员列表。
注意: 如果代币合约没有默认操作员, 必须返回空列表。
- 接口ID:
06e48538
- 返回值: 所有的默认操作员列表。
authorizeOperator
函数
function authorizeOperator(address operator) external
设置一个第三方的 operator
地址作为msg.sender
的操作员,此操作员可以代表 msg.sender
发送和销毁代币。
注意: 持有者 (msg.sender
) 总是自身的操作员。
因此,当出现授权自己作为操作员时(operator
等于 msg.sender
)函数需要 revert
。
- 接口ID:
959b8c3f
- 参数:
operator
:msg.sender
的操作员地址
revokeOperator
函数
function revokeOperator(address operator) external
移除 msg.sender
的 操作员权限。
注意: T持有者 (msg.sender
) 总是自身的操作员。因此,当出现移除自己时(即operator
等于 msg.sender
)函数需要 revert
。
- 接口ID:
fad8b32a
- 参数:
operator
: 要取消的msg.sender
的操作员地址
function isOperatorFor(
address operator,
address holder
) external view returns (bool)
2
3
4
是否是某个持有者的操作员。
- 接口ID:
d95b6371
- 参数:
operator
: 操作员holder
: 持有者
- 返回值: 是某个持有者的操作员 返回
true
否则false
。
注意: 要知道持有者有哪些操作员,需要字每个 默认操作员上调用isOperatorFor
,同时解析 AuthorizedOperator
和 RevokedOperator
事件,进行相应的查询。
# 发送代币
当操作员从持有者账号发送 amount
数量的token给接收者时,代币合约必须遵守以下规则:
任何授权的操作员可以发送代币到任何的接收者地址(除了
0x0
)。持有者余额减掉
amount
数量。接收者余额加上
amount
数量。持有者余额必须大于等于
amount
,发送之后结果大于0。代币合约必须触发
Sent
事件,参考Sent
事件。操作员也许会使用
operatorData
参数携带信息。如果持有者有通过 ERC1820 注册
ERC777TokensSender
实现接口, 代币合约必须调用其tokensToSend
钩子函数。如果接收者有通过 ERC1820 注册
ERC777TokensRecipient
实现接口, 代币合约必须调用其tokensReceived
钩子函数。在整个发送过程
data
和operatorData
必须保持不可变 — 因此同样的数据要用于调用调用钩子函数和触发Sent
事件。
发送代币时,在以下的情况下,合约需要 revert
:
未经授权的操作员。
发送之后 持有者或接收者的余额不是 粒度 granularity 的整数倍。
接收者是合约地址,而又没有按 ERC1820实现
ERC777TokensRecipient
接口。持有者或接收者有一个为
0x0
出现结果合约为 负数。
持有者在
tokensToSend
函数中进行了revert
。接收者在
tokensReceived
函数中进行了revert
。
代币合约可以从多个持有者发送到多个接收者,在这种情况下:
- 上面的发送规则对所有的持有者和接收者都要遵守。
- 余额增加的总和要等于发送的
amount
。 - 余额减去的总和要等于发送的
amount
。 - 每一组持有者和接收者(使用相应的金额),都要触发
Sent
。 Sent
事件的金额之和要等于发送的amount
.
注意: 诸如对发送收取费用的机制被视为发送给多个接收者:一个是转移目标接收者, 一个是费用接收者。
注意: 代币的转移也许是链式的,例如:可以在收到代币或转移到其他地址,转移时同样要遵照以上规则。
注意: 发送零个数量也是有效的交易,需要正确处理。
实现要求:
代币合约需要在修改状态之前调用
tokensToSend
钩子函数。代币合约需要在修改状态之后调用
tokensReceived
钩子函数。
如: tokensToSend
要首先调用,然后更新余额状态,再调用 tokensReceived
, 因此如果在tokensToSend
中获取balanceOf
的话是发送之前的结果,同理在 tokensReceived
这是发送之后的值。
注意: data
字段的信息由持有者提供 — 类似于发送以太时的 data 字段。tokensToSend()
和 tokensReceived()
可以使用这个信息来确定是否拒绝交易。
注意: operatorData
字段类似,只不过信息必须由操作员提供。这个信息可以用于日志或特定的场景,如:支付票号,附属签名等。大多数情况下,接收者可以忽略operatorData
(仅做日志记录)。
event Sent(
address indexed operator,
address indexed from,
address indexed to,
uint256 amount,
bytes data,
bytes operatorData
)
2
3
4
5
6
7
8
指示发送代币事件。
注意: 不能在发送函数 send(或 ERC20 transfer 函数 ) 之外触发。
- 参数:
operator
: 触发发送的地址from
: 持有者to
: 接收者amount
: 发送的代币数量data
: 持有者提供的信息operatorData
: 操作员提供的信息
下面的 send
和 operatorSend
必须用于实现发送代币,当然合约也可以实现其他的方法来发送。
send
函数
function send(address to, uint256 amount, bytes calldata data) external
给地址to
发送 amount 数量的代币。
操作员和持有者必须都是msg.sender
.
- 接口ID:
9bd9bbc6
- 参数:
to
: 代币接收者.amount
: 发送的代币数量data
: 持有者提供的信息
operatorSend
函数
function operatorSend(
address from,
address to,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external
2
3
4
5
6
7
操作员(msg.sender)代表 from
地址 给地址to
发送 amount 数量的代币。
记住: 如果操作员没得到 from
地址的授权,必须revert
注意: from
和 msg.sender
可以是相同的地址。
例如: 地址可以自己调用 operatorSend
,相当于调用send
时用了确定的操作员数据(而这不可以通过send
实现)
- 接口ID:
62ad1b83
- 参数:
from
: 代币持有者to
: 代币接收者amount
: 发送的代币数量data
: 持有者提供的信息operatorData
: 操作员提供的信息
# 铸造代币(Minting)
铸造代币是产生新币的过程。
ERC777故意没有定义铸币函数。用意是不希望限制ERC777标准的使用,因为铸币通常特定于特定的代币。
尽管如此,在为接收者铸币必须遵守以下规则:
接收者是不为 0 的任何地址。
发行量需要加上铸币量
0x0
地址的余额不可以减少接收者的余额加上铸币量
代币合约必须触发
Minted
事件,注意参数正确。参考Minted
事件。如果接收者有通过 ERC1820 注册 ERC777TokensRecipient 实现接口, 代币合约必须调用其 tokensReceived 钩子函数。
在整个发送过程 data 和 operatorData 必须保持不可变 — 因此同样的数据要用于调用调用钩子函数
tokensReceived
和触发Minted
事件。
铸币时,如果发生以下情况,代币合约必须 revert
:
- 铸币后持有者或接收者的余额不是 粒度 granularity 的整数倍。
- 接收者是合约地址,而又没有按 ERC1820 实现ERC777TokensRecipient 接口。
- 接收者为 0x0 地址。
- 接收者在 tokensReceived 函数中进行了
revert
。
注意: 代币合约部署时的初始代币供应必须被视为铸币过程,需要符合以上规则:必须发出一个或多个Minted
事件,必须调用接收者的tokensReceived
钩子函数。
ERC20 兼容性需要:
如果合约要需要兼容 ERC20,铸币时 Sent
事件不能触发,应该触发 ERC20 定义的 Transfer
事件 (from
用 0x0
)。
代币合约可以一次为多个接收者铸币,在这种情况下:
- 上面的规则对所有接收者都要遵守。
- 余额增加的总和要等于铸币数量。
Minted
事件必须为每个接收者触发(用相应的数量)。Minted
事件的金额之和要等于铸币数量amount
.
注意: 铸币零个数量也是有效的交易,需要正确处理。
注意: 在发送和销毁时,data 字段由持有者提供,这个不适用铸币,这个数据可以由合约或操作员提供。
注意: operatorData
字段由操作员提供 — 类似于发送以太币交易的 data 字段,tokensReceived() 可以使用这个信息来确定是否拒绝铸币交易。
event Minted(
address indexed operator,
address indexed to,
uint256 amount,
bytes data,
bytes operatorData
)
2
3
4
5
6
7
指示铸币事件。
注意: 铸币事件不能在铸币过程之外触发
- 参数:
operator
: 触发铸币的地址to
: 铸币接收者amount
: 铸币量data
: 提供给接收者的信息.operatorData
: 操作员提供的信息.
# 销毁代币
销毁代币或称燃烧代币
ERC777 明确的定义了两个函数用于销毁代币 (burn
和 operatorBurn
),方便钱包和dapps有统一的接口交互。
但是,代币合约可以出于任何原因阻止部分或全部持有者销毁代币。代币合约也可以定义销毁代币的其他函数。
在销毁持有者的代币时,必须遵守以下规则:
- 代币持有者不可以是
0x0
地址。 - 总供应量必须减少代币销毁量。
- “0x0”的余额不得增加。
- 持有者的余额必须减少代币销毁的数量。
- 代币合约必须触发
Burned
,参考Burned
事件。 - 代币合约必须调用持有者的
tokensToSend
钩子函数, 如果持有者通过ERC1820注册ERC777TokensSender
实现。 - 在销毁过程中
operatorData
必须保持不可变 — 因为同样的operatorData
用于 调用tokensToSend
和触发Burned
事件。
销毁时,如果发生以下情况,代币合约必须 revert:
操作员没有授权。
销毁代币后,持有者余额不是 粒度(granularity) 的整数倍。
持有者的余额不及代币的销毁量(销毁后,为负)。
持有者是
0x0
地址。持有者
tokensToSend
进行了revert
。
ERC20 兼容性需要:
如果合约要需要兼容 ERC20,销毁时 Sent
事件不能触发,应该触发 ERC20 定义的 Transfer
事件 (to
用 0x0
)。
ERC20 没有定义销毁,不过这个是普遍接受的实践。
代币合约可以一次为多个持有者销毁代币。 在这种情况下:
每个持有者都需要满足以上规则。
所有余额减少的总和必须等于总销毁量。
每个持有者必须触发
Burned
事件(用相应的数量)Burned
事件中所有金额的总和必须等于“销毁总量”。
注意: 销毁零个数量也是有效的交易,需要正确处理。
注意: data
字段字段由持有者提供 — 类似于发送以太币交易的 data 字段,两个钩子函数 tokensToSend()
tokensReceived()
可以使用这个信息来确定是否拒绝销毁交易。
注意: operatorData
字段类似,不过它由操作员提供
event Burned(
ddress indexed operator,
address indexed from,
uint256 amount,
bytes data,
bytes operatorData
);
2
3
4
5
6
7
指示销毁事件。
注意: 不能在销毁过程之外触发。
- 参数:
operator
: 触发销毁的地址from
: 从哪个账号销毁amount
: 销毁数量data
: 持有者提供的信息operatorData
: 操作员提供的信息
以下描述的函数 burn
和 operatorBurn
必须用来实现代币销毁,当然也可以实现自己的函数。
burn
函数
function burn(uint256 amount, bytes calldata data) external
从 msg.sender
账号销毁 amount
数量的代币。
操作员和持有者必须都是msg.sender
。
- 接口ID:
fe9d9303
- 参数:
amount
: 销毁数量data
: 持有者提供的信息
operatorBurn
函数
function operatorBurn(
address from,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external
2
3
4
5
6
msg.sender
操作员从 from
账号销毁 amount
数量的代币。
记住: 如果操作员没有授权,需要 revert
。
- 接口ID:
fc673c4f
- 参数:
from
: 销毁代币的账号(持有者)amount
: 销毁数量data
: 持有者提供的信息operatorData
: 操作员提供的信息
注意: 操作员可以用 operatorData
传递任何信息,信息必须是操作员提供的。
注意: from
和 msg.sender
可以是相同的地址。
例如: 持有者可以自己调用 operatorBurn
,相当于用了额外的操作员及信息operatorData
调用 burn
。(而 burn
函数自身无法实现 )
# 调用钩子函数:ERC777TokensSender
和tokensToSend
调用tokensToSend
钩子函数用于通知持有者余额减少(如发送和销毁)。
任何希望收到代币通知的地址(普通地址或合约)都会从需要按ERC1820注册及实现 ERC777TokensSender
接口,描述如下:
通过调用 ERC1820 注册表合约上的 setInterfaceImplementer
函数来完成的,其中持有者地址为地址参数,ERC777TokensSender
的 keccak256哈希值(0x29ddb589b1fb5fc7cf394961c1adf5f8c6454761adf795e67fe149f658abe895
)作为接口哈希参数,以及实现ERC777TokensSender
的合约作为实现者参数。
interface ERC777TokensSender {
function tokensToSend(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external;
}
2
3
4
5
6
7
8
9
10
注意: 普通地址可以用一个实现的合约(代表普通地址来执行)地址注册,合约可以用自己的地址也可以用另一个地址注册,只要其实现了对应的接口。
tokensToSend
函数
function tokensToSend(
address operator,
address from,
address to,
uint256 amount,
bytes calldata userData,
bytes calldata operatorData
) external
2
3
4
5
6
7
8
通知(或请求)从持有人地址发送或销毁 amount 数量的代币。
注意: 请勿在发送(或 ERC20 transfer)或 销毁 之外调用。
- 接口ID:
75ab9782
- 参数:
operator
: 操作员(触发者)from
: 从哪个地址扣除to
: 接收着(销毁时为0x0
)amount
: 数量data
: 持有者信息operatorData
: 操作员信息
调用tokensToSend
钩子函数的规则:
必需是在发送或销毁中调用
tokensToSend
钩子函数调用
tokensToSend
钩子函数必须在状态更新前(即余额还没有减少)operator
必须是触发发送或销毁操作的地址。from
是持有者地址to
是接收者地址销毁时
to
为0x0
amount
必须是发送或销毁的数量data
发送或销毁操作如果有data,信息必须带上。operatorData
发送或销毁操作如果有operatorData
,信息必须带上。持有者也许会通过
revert
阻止交易 (例如拒绝从其账户扣款)。
注意: 也许多个持有者会使用相同的ERC777TokensSender
实现。
注意: 但是对于所有的 ERC777 合约, 一个地址只能注册一个实现。因此ERC777TokensSender
会被多个合约调用,tokensToSend
实现里, msg.sender
是合约地址。
ERC20兼容性需要:
执行钩子函数优先于ERC20,并且触发ERC20的 transfer
和 transferFrom
事件时必须调用(如果注册了)。 从 transfer
函数调用时,operator
和 from
相同。
从 transferFrom
函数调用时,operator
和 触发调用的地址相同。
# 钩子函数 ERC777TokensRecipient
和tokensReceived
调用tokensReceived
钩子函数用于通知接收者余额增加了(如发送和铸币)。
任何希望收到代币通知的地址(普通地址或合约)都会从需要按ERC1820注册及实现 ERC777TokensRecipient
接口,描述如下:
通过调用 ERC1820 注册表合约上的 setInterfaceImplementer
函数来完成的,其中接收者地址为地址参数,ERC777TokensRecipient
的 keccak256哈希值(0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b
)作为接口哈希参数,以及实现ERC777TokensRecipient
的合约作为实现者参数。
interface ERC777TokensRecipient {
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external;
}
2
3
4
5
6
7
8
9
10
如果接收者是合约而又没有 ERC777TokensRecipient
实现,那么代币合约需要:
注意: 普通地址可以用一个实现的合约(代表普通地址来执行)地址注册,合约可以用自己的地址也可以用另一个地址注册,只要其实现了对应的接口。
tokensReceived
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external
2
3
4
5
6
7
8
用于通知接受代币。
注意: 请勿在发送(或 ERC20 transfer)或 铸币 之外调用。
- 接口ID:
0023de29
- 参数:
operator
: 操作员(触发者)from
: 从哪个地址扣除(铸币为0x0
)to
: 接收者amount
: 数量data
: 持有者信息operatorData
: 操作员信息
调用tokensReceived
钩子函数的规则:
必需是在发送或铸币时调用
tokensReceived
钩子函数。调用
tokensReceived
钩子函数必须在状态更新后。operator
必须是触发发送或铸币操作的地址。from
是持有者地址铸币时
from
为0x0
to
是接收者地址amount
必须是发送或铸币的数量data
发送或铸币操作如果有data,信息必须带上。operatorData
发送或铸币操作如果有operatorData
,信息必须带上。接收者也许会通过
revert
阻止交易 (例如拒绝接受代币)。
注意: 也许多个接收者会使用相同的ERC777TokensRecipient
实现。
注意: 但是对于所有的ERC777 合约, 一个地址只能注册一个实现。因此ERC777TokensRecipient
会被多个合约调用,tokensReceived
实现里, msg.sender
是合约地址。
ERC20兼容性需要:
执行钩子函数优先于ERC20,并且触发ERC20的 transfer
和 transferFrom
事件时必须调用(如果注册了)。 从 transfer
函数调用时,operator
和 from
相同。
从 transferFrom
函数调用时,operator
和 触发调用的地址相同。
# 关于Gas消耗
Dapps 和 钱包在发送、铸币、销毁时应该先测量 gas 消耗 — 使用
eth_estimateGas
— 以避免 out-of-gas
# 图标
Image | |||||
---|---|---|---|---|---|
Color | beige | white | light grey | dark grey | black |
Hex | #C99D66 | #FFFFFF | #EBEFF0 | #3C3C3D | #000000 |
钱包和dapps可以使用(修改和调整)上面的图标,以表明符合 ERC777代币实现标准。
ERC777 代币合约作者也可以再这些图标的基础上创建直接的图标,在非ERC777 标准上不能用这个图标。
图标的原始文件在 /assets/eip-777/logo
有png和svg格式
# 原理阐述
该标准的主要目的是解决ERC20的一些缺点,同时保持与ERC20的向后兼容性,并避免EIP223的问题和漏洞。
以下是有关标准主要方面的决定的理由。
注意: Jacques Dafflon(0xjac),该标准的作者之一,联合写了关于标准的master thesis 描述了更详细的信息。
# 生命周期
不仅仅是发送代币,ERC777定义了代币的整个生命周期,从铸币过程开始,然后是发送过程,以及销毁过程结束。
明确定义生命周期对于一致性和准确性非常重要,特别是当价值来自于稀缺性。
相反,当看一些ERC20代币时,可以观察到在totalSupply
和实际流通量返回的值之间差异,
因为ERC20 标准没有明确定义创建和销毁代币的过程。
# 数据
铸币、发送和销毁都是使用了data
和 operatorData
字段(在整个操作过程都会带上)。
对于简单用例,这些字段可能是空的,或者它们可能包含与token移动相关的有价值信息,如发送者或银行本身附加到银行转账的信息。
使用 data
字段同样存在于其他标准提案中,例如EIP223 参与标准的多个社区成员审查了该标准。
# 钩子函数(Hooks)
在大多数情况下,ERC20需要两次调用才能将代币安全地转移到合约而不被合约锁定。
一个调用是发送者的 approve
, 另一个是接收者的 transferFrom
。
此外,还需要各方之间进行额外的沟通,这些沟通尚未明确界定。
最后,持有者可能会在 transfer
和approve
/ transferFrom
之间感到困惑。
使用 transfer
将代币转移到合约很可能导致代币被锁定。
钩子函数让发送过程更简单,并提供一种方式将代币发送给任何接收者。
由于 tokensReceived
钩子函数,合约能够在接收时做出相应响应并防止代币被锁定。
# 持有人更好地控制
调用tokensReceived
钩子函数允许接收人拒绝接收代币,其给了接收者持有人更多的控制(如基于一些参数,如 data
或 operatorData
)谁可以接收(或拒绝)代币。
遵循相同的意图和基于社区的建议,添加了调用tokensToSend
钩子函数以控制和防止代币的移出。
# ERC1820 注册表合约
ERC1820注册表合约允许持有人注册他们的钩子函数,事先链接持有人和函数。
首先是在发送者或接收者的钩子函数定义,方法类似于EIP223,它在接收者合约上提出了一个tokenFallback
函数, 在收到代币时被调用,但它依靠ERC165进行接口检测,这个方法简单直接,但有一些限制。
特别是,发送者和接收者必须是合约才能提供钩子的实现。外部账号地址无法受益于钩子函数。已存在的合约很可能也不兼容,他们不知道也没法定义新的钩子函数实现。因此,现有的智能合约基础设施,如多签钱包其拥有的大量的ether和 代币需要迁移到新的升级合约中。
考虑的第二种方法是使用ERC672,它使用反向ENS为地址提供伪自省。 然而,这种方法严重依赖于ENS,在此基础上需要实施反向查找。带来了更多的复杂性和安全性问题,这将超越方法的好处。
第二种方法是就是标准使用的ERC1820注册表合约, 它允许任何地址注册实现,并从中获益。
为了不让ERC777标准过于复杂, 注册表合约保存在单独的EIP提案中。 更重要的是,注册表合约以灵活的方式设计,这样其他EIP和智能合约基础设施都可以从中受益, 不单单是ERC777或其他的代币。
此注册表的第一个建议是ERC820。不幸的是,从Solidity语言升级到版本0.5及更高版本产生了Bug, 在ERC820最后召集 Last Call 之后被发现,已经不适合修提案。
因此,用于ERC777的注册表的标准成为ERC1820。 ERC1820和ERC820在功能上等效。 ERC1820仅包含针对Solidity较新版本的修复。
# 操作员
标准将操作员的概念作为任何移动代币的地址。 每个地址直观地移动自己的代币,将持有人和操作员的概念分开可以提供更大的灵活性。 主要是因为该标准为持有人定义了一种机制,让其他地址成为他们的操作员。 此外,与ERC20中的 approve 、 transferFrom 不同,其未明确定义批准地址的角色, ERC777详细介绍了与操作员进行交互的意图,包括批准操作员的义务, 以及任何持有人授权和撤销操作员的权利。
# 默认操作员
根据社区对预批准操作员的需求添加了默认操作员。这是默认被所有持有人授权的操作员, 出于明显的安全原因,默认操作员列表只能在代币创建时定义的,并且无法更改。
任何持有人仍然有权撤销默认操作员。 默认操作员的明显优势之一是允许代币的 ether-less 移动。
ether-less: 译者的理解是, 可用于诸如状态通道等二层网络进行离线转移。
默认操作员还具有其他可用性优势, 例如允许代币项目方以模块化方式提供功能,并降低持有人使用通过操作员使用功能的复杂性。
# 向后兼容
ERC777与较早的ERC20代币标准保持向后兼容。
该EIP 不使用transfer
和transferFrom
而是使用 send
和 operatorSend
以避免产生混淆和错误
该标准允许实现ERC20功能transfer
,transferFrom
,approve
和allowance
, 使代币与ERC20完全兼容。
为了与ERC20向后兼容,代币可以实现decimals()
,如果实现,它必须总是返回18。
因此,代币币可以并行实现ERC20和ERC777。
view函数的规范(例如name
, symbol
, balanceOf
, totalSupply
)和内部数据(例如余额映射)可以重叠。
但是请注意,ERC777中必须具有以下功能(必须实现):
name
, symbol
balanceOf
和 totalSupply
(decimals
不属于ERC777标准 )
两种提案标准的状态修改函数是分开的,并且可以彼此独立地操作。 注意,ERC20函数应该仅限于仅从老合约中调用。
如果代币实现ERC20标准,它必须通过ERC1820用自己的地址注册ERC20Token
接口。
通过在ERC1820注册表合约上调用setInterfaceImplementer
函数来完成注册,使用合约地址作为参数: 地址 _addr 和实现者 _implementer, 接口哈希是 ERC20Token
的keccak256
哈希(0xaea199e31a596269b42cdafd93407f14436db6e4cad65417994c2eb37381e05a
)
如果该合约有开关启用或禁用 ERC20 函数,则每次触发该开关时,需要相应地注册或注销“ ERC20Token”接口
注销同样是 调用setInterfaceImplementer
, 使用代币合约地址作为地址_addr 参数, 将“ 0x0”作为实现者 _implementer ,将ERC20Token
的 keccak256
哈希作为接口哈希。可以查阅 erc1820-set 了解更多。
实现 ERC20的 ERC777 合约的区别在于 tokensToSend
和 tokensReceived
钩子函数 优先于ERC20。
即使调用ERC20 transfer
和 transferFrom
,代币也必须通过ERC1820进行检查 from
和to
地址是否分别实现了 tokensToSend
和 tokensReceived
钩子函数, 如果实现了任何钩子函数,则必须调用它。
请注意,当在Token上调用ERC20transfer
时,如果该Token没有实现tokensReceived
,
即使这意味着代币可能会被锁定, transfer
也仍可以调用成功。
下表总结了在不同的操作是, ERC777和ERC20 必须采取的不同措施
ERC1820 | 目标 地址 | ERC777 Sending 和 Minting | ERC20 transfer /transferFrom |
---|---|---|---|
ERC777TokensRecipient 已注册 | 常规用户地址 |
必须调用tokensReceived | |
合约 | |||
ERC777TokensRecipient 未注册 | 常规用户地址 | 正常执行 | |
合约 | 必须 revert | 应该执行 1 |
由于ERC20 没有钩子,因此交易应继续进行,但是,这可能会导致意外锁定代币。
如果要避免意外锁定代币至关重要,则该交易可以 revert </ code>。
如果未实现tokensToSend
,也无需采取任何特殊处理。
转移必须继续进行,并且只有在不遵守其他条件的情况下才能取消,如:余额不足或 tokensReceived
被 revert
例如或“ tokensReceived”中的“还原”(如果存在)。
在发送,铸币和销毁期间,必须触发各自的 Sent
, Minted
和 Burned
事件。
此外,如果代币合约通过ERC1820实现了 ERC20Token
, 代币合约必须触发用于 铸币 和销毁的Transfer
事件(ERC20标准),
在ERC20的 transfer
或transferFrom
函数中,必须发出有效的 Sent
事件。
因此,对于代币的任何转移,可能会发出两个事件:
ERC20 Transfer
和 ERC777 的 Sent
,Minted
或Burned
(取决于转移类型)。
第三方开发人员必须注意不要将两个事件都视为独立的动作。通常,如果应用程序将代币视为ERC20代币,
那么仅应考虑 Transfer
事件。
如果应用程序将代币视为ERC777代币,那么仅应考虑Sent,
Minted和
Burned`事件。
# 测试用例
参考 0xjac/ERC777 。
# 实现
参考 ERC777 ReferenceToken 实现可通过 npm install erc777
安装。
及Openzeppelin ERC777