# 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标准的主要优点有:

  1. 使用和发送以太相同的理念发送token,方法为:send(dest, value, data).

  2. 合约和普通地址都可以通过注册tokensToSend hook函数来控制和拒绝发送哪些token(拒绝发送通过在hook函数tokensToSendrevert 来实现)。

  3. 合约和普通地址都可以通过注册tokensReceived hook函数来控制和拒绝接受哪些token(拒绝接受通过在hook函数tokensReceivedrevert 来实现)。

  4. tokensReceived 可以通过hook函数可以做到在一个交易里完成发送代币和通知合约接受代币,而不像 ERC20 必须通过两次调用(approve/transferFrom)来完成。

  5. 持有者可以"授权"和"撤销"操作员(operators: 可以代表持有者发送代币)。 这些操作员通常是(去中心化)交易所、支票处理机或自动支付系统。

  6. 每个代币交易都包含 dataoperatorData 字段, 可以分别传递来自持有者和操作员的数据。

  7. 可以通过部署实现 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);
}
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
59
60

ERC777 合约必须实现上述接口,也必须遵守以下规范。 合约需要用自己的地址通过 ERC1820 标准注册 ERC777Token 接口。

注册方法是调用ERC1820 注册表合约的 setInterfaceImplementer 方法,参数 _addr 及 _implementer 均是合约的地址,_interfaceHash 是 ERC777Tokenkeccak256 哈希值, 即0xac7fbab5f54a3ca8194167523c6753bfeb96a445279294b6125b68cce2177054

如果合约有一个开关来启用或禁用ERC777功能,每次触发开关时,代币合约必须相应地通过ERC1820注册或取消注册ERC777Token接口。

取消注册使用代币合约地址作为参数 _addrERC777Tokenkeccak256哈希作为接口哈希及'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)
1

返回代币名称,如: "MyToken"

  • 接口ID: 06fdde03
  • 返回值:代币名称
# symbol 函数
function symbol() external view returns (string memory)
1

返回代币的代号,如"MYT"

  • 接口ID:95d89b41
  • 返回值:代币的代号
# totalSupply 函数
function totalSupply() external view returns (uint256)
1

返回代币总流通量。

注意: 总供应量必须是所有账号余额(balanceOf)之和。 注意: 总供应量必须等于所有挖出的币( Minted 事件定义)减去销毁的币(Burned事件定义)

  • 接口ID:18160ddd
  • 返回值:代币总流通量
# balanceOf 函数
function balanceOf(address holder) external view returns (uint256)
1

返回帐户(通过参数"holder")的余额,余额 >=0 。

  • 接口ID:70a08231
  • 参数holder: 查询的账号
  • 返回值: 在代币合约里对应账号余额
# granularity 函数
function granularity() external view returns (uint256)
1

获得代币最小的划分粒度(基于内部单位的个数),最小的挖矿、发送及销毁粒度。

granularity 需要满足一下规则:

  • granularity 必须在创建时设置

  • granularity 任何时候不可以更改。

  • granularity 必须大于等于1。

  • 所有的余额必须是granularity的整数倍。

  • 挖矿、发送及销毁数量必须是granularity的整数倍。

  • granularity的整数倍的操作都需要 revert

注意: 大部分的代币应该是完全可切分的,如果没有特别的理由,这个函数应该返回1。

  • 接口ID:556f0dc7
  • 返回值: 最小的划分粒度

注意: defaultOperatorsisOperatorFor 也是视图函数,他们在操作员部分定义。

ERC20 兼容性需要:

代币的 decimals 必须是 18。 对于一个 纯粹的 ERC777 代币 ERC20decimals 函数是可选的,与合约交互的时候不应该依赖这个值(18是隐含值)。

为了兼容 ERC20 代币, decimals 函数要求必须返回18。 (因为在 ERC20decimals 是可选的,也没有明确的默认值,如果没有会被认为是0)

# 操作员

操作员是可以代表持有者发送和销毁代币的账号地址。

当地址成为持有者的操作员时,需要触发 AuthorizedOperator 事件。触发事件时操作员是第 一个参数,持有者是第二个参数。

撤销时需要触发 RevokedOperator 事件。触发事件时操作员是第一个参数,持有者是第二个参数。

注意: 持有者可以有多个操作员。

合约可以定义默认操作员,所有的持有者都隐含授权给默认操作员。

定义默认的操作员不能触发 AuthorizedOperator 事件,默认操作员需遵守以下规则:

  • 代币合约必须在创建时设置默认操作员。

  • 默认操作员是不可变的,任何时候不可以添加和删除。

  • 设置默认操作员不能触发 AuthorizedOperator 事件。

  • 持有者必须允许撤销默认操作员,除非默认操作员本身就是持有者。

  • 持有者必须允许重新授权之前的默认操作员。

  • 当默认操作员被显示的授权或撤销,需要相应的触发 AuthorizedOperatorRevokedOperator 事件

以下是任何操作员需遵守的规则:

  • 每个地址都是其自身的操作员,因此不能被撤销。

  • 如果地址是某个持有者的操作员, isOperatorFor 返回 true ,否则返回 false .

  • 触发AuthorizedOperator 事件,要确保参数正确。参考AuthorizedOperator 事件

  • 触发 RevokedOperator 事件,要确保参数正确。参考RevokedOperator 事件

注意: 持有者也许会重复授权一个操作员,每次都需要触发AuthorizedOperator事件。

注意: A 持有者也许会撤销一个操作员,每次都需要触发RevokedOperator事件。

AuthorizedOperator 事件

event AuthorizedOperator(address indexed operator, address indexed holder)
1

指示持有者授权一个操作员。

注意: 不能在授权过程之外触发。

  • 参数:
    • operator: 操作员地址
    • holder: 持有者地址

RevokedOperator 事件

event RevokedOperator(address indexed operator, address indexed holder)
1

指示持有者撤销操作员。

注意: 不能在撤销过程之外触发事件。

  • 参数: operator: 操作员地址 holder: 持有者地址

下面描述的 defaultOperators, authorizeOperator, revokeOperatorisOperatorFor 函数用来实现对操作员的管理。

当然代币合约也可以实现其他的函数去管理操作员.

defaultOperators 函数

function defaultOperators() external view returns (address[] memory)
1

获取代币合约默认的操作员列表。

注意: 如果代币合约没有默认操作员, 必须返回空列表。

  • 接口ID: 06e48538
  • 返回值: 所有的默认操作员列表。

authorizeOperator 函数

function authorizeOperator(address operator) external
1

设置一个第三方的 operator 地址作为msg.sender 的操作员,此操作员可以代表 msg.sender 发送和销毁代币。

注意: 持有者 (msg.sender) 总是自身的操作员。 因此,当出现授权自己作为操作员时(operator 等于 msg.sender)函数需要 revert

  • 接口ID: 959b8c3f
  • 参数:
    • operator: msg.sender 的操作员地址

revokeOperator 函数

function revokeOperator(address operator) external
1

移除 msg.sender 的 操作员权限。

注意: T持有者 (msg.sender) 总是自身的操作员。因此,当出现移除自己时(即operator 等于 msg.sender)函数需要 revert

  • 接口ID: fad8b32a
  • 参数:
    • operator: 要取消的msg.sender 的操作员地址

isOperatorFor 函数

function isOperatorFor(
    address operator,
    address holder
) external view returns (bool)
1
2
3
4

是否是某个持有者的操作员。

  • 接口ID: d95b6371
  • 参数:
    • operator: 操作员
    • holder: 持有者
  • 返回值: 是某个持有者的操作员 返回 true 否则 false

注意: 要知道持有者有哪些操作员,需要字每个 默认操作员上调用isOperatorFor,同时解析 AuthorizedOperatorRevokedOperator 事件,进行相应的查询。

# 发送代币

当操作员从持有者账号发送 amount 数量的token给接收者时,代币合约必须遵守以下规则:

  • 任何授权的操作员可以发送代币到任何的接收者地址(除了 0x0)。

  • 持有者余额减掉 amount 数量。

  • 接收者余额加上 amount 数量。

  • 持有者余额必须大于等于 amount,发送之后结果大于0。

  • 代币合约必须触发Sent事件,参考 Sent事件

  • 操作员也许会使用 operatorData 参数携带信息。

  • 如果持有者有通过 ERC1820 注册 ERC777TokensSender 实现接口, 代币合约必须调用其 tokensToSend 钩子函数。

  • 如果接收者有通过 ERC1820 注册 ERC777TokensRecipient 实现接口, 代币合约必须调用其 tokensReceived 钩子函数。

  • 在整个发送过程 dataoperatorData 必须保持不可变 — 因此同样的数据要用于调用调用钩子函数和触发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 (仅做日志记录)。

Sent 事件

event Sent(
    address indexed operator,
    address indexed from,
    address indexed to,
    uint256 amount,
    bytes data,
    bytes operatorData
)
1
2
3
4
5
6
7
8

指示发送代币事件。

注意: 不能在发送函数 send(或 ERC20 transfer 函数 ) 之外触发。

  • 参数:
    • operator: 触发发送的地址
    • from: 持有者
    • to: 接收者
    • amount: 发送的代币数量
    • data: 持有者提供的信息
    • operatorData: 操作员提供的信息

下面的 sendoperatorSend 必须用于实现发送代币,当然合约也可以实现其他的方法来发送。

send 函数

function send(address to, uint256 amount, bytes calldata data) external
1

给地址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
1
2
3
4
5
6
7

操作员(msg.sender)代表 from地址 给地址to发送 amount 数量的代币。

记住: 如果操作员没得到 from地址的授权,必须revert

注意: frommsg.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事件 (from0x0)。

代币合约可以一次为多个接收者铸币,在这种情况下:

  • 上面的规则对所有接收者都要遵守。
  • 余额增加的总和要等于铸币数量。
  • Minted事件必须为每个接收者触发(用相应的数量)。
  • Minted事件的金额之和要等于铸币数量amount.

注意: 铸币零个数量也是有效的交易,需要正确处理。

注意: 在发送和销毁时,data 字段由持有者提供,这个不适用铸币,这个数据可以由合约或操作员提供。

注意: operatorData 字段由操作员提供 — 类似于发送以太币交易的 data 字段,tokensReceived() 可以使用这个信息来确定是否拒绝铸币交易。

Minted 事件

event Minted(
    address indexed operator,
    address indexed to,
    uint256 amount,
    bytes data,
    bytes operatorData
)
1
2
3
4
5
6
7

指示铸币事件。

注意: 铸币事件不能在铸币过程之外触发

  • 参数:
    • operator: 触发铸币的地址
    • to: 铸币接收者
    • amount: 铸币量
    • data: 提供给接收者的信息.
    • operatorData: 操作员提供的信息.

# 销毁代币

销毁代币或称燃烧代币

ERC777 明确的定义了两个函数用于销毁代币 (burnoperatorBurn),方便钱包和dapps有统一的接口交互。

但是,代币合约可以出于任何原因阻止部分或全部持有者销毁代币。代币合约也可以定义销毁代币的其他函数。

在销毁持有者的代币时,必须遵守以下规则:

  • 代币持有者不可以是 0x0 地址。
  • 总供应量必须减少代币销毁量。
  • “0x0”的余额不得增加。
  • 持有者的余额必须减少代币销毁的数量。
  • 代币合约必须触发Burned,参考 Burned 事件
  • 代币合约必须调用持有者的tokensToSend钩子函数, 如果持有者通过ERC1820注册ERC777TokensSender 实现。
  • 在销毁过程中 operatorData 必须保持不可变 — 因为同样的 operatorData 用于 调用 tokensToSend 和触发 Burned 事件。

销毁时,如果发生以下情况,代币合约必须 revert:

  • 操作员没有授权。

  • 销毁代币后,持有者余额不是 粒度(granularity) 的整数倍。

  • 持有者的余额不及代币的销毁量(销毁后,为负)。

  • 持有者是 0x0 地址。

  • 持有者 tokensToSend 进行了 revert

ERC20 兼容性需要:

如果合约要需要兼容 ERC20,销毁时 Sent 事件不能触发,应该触发 ERC20 定义的 Transfer事件 (to0x0)。 ERC20 没有定义销毁,不过这个是普遍接受的实践。

代币合约可以一次为多个持有者销毁代币。 在这种情况下:

  • 每个持有者都需要满足以上规则。

  • 所有余额减少的总和必须等于总销毁量。

  • 每个持有者必须触发Burned事件(用相应的数量)

  • Burned事件中所有金额的总和必须等于“销毁总量”。

注意: 销毁零个数量也是有效的交易,需要正确处理。

注意: data 字段字段由持有者提供 — 类似于发送以太币交易的 data 字段,两个钩子函数 tokensToSend() tokensReceived() 可以使用这个信息来确定是否拒绝销毁交易。

注意: operatorData 字段类似,不过它由操作员提供

Burned 事件

event Burned(
    ddress indexed operator,
    address indexed from,
    uint256 amount,
    bytes data,
    bytes operatorData
);
1
2
3
4
5
6
7

指示销毁事件。

注意: 不能在销毁过程之外触发。

  • 参数:
    • operator: 触发销毁的地址
    • from: 从哪个账号销毁
    • amount: 销毁数量
    • data: 持有者提供的信息
    • operatorData: 操作员提供的信息

以下描述的函数 burnoperatorBurn 必须用来实现代币销毁,当然也可以实现自己的函数。

burn 函数

function burn(uint256 amount, bytes calldata data) external
1

msg.sender 账号销毁 amount 数量的代币。

操作员和持有者必须都是msg.sender

  • 接口ID: fe9d9303
  • 参数:
    • amount: 销毁数量
    • data: 持有者提供的信息

operatorBurn 函数

function operatorBurn(
    address from,
    uint256 amount,
    bytes calldata data,
    bytes calldata operatorData
) external
1
2
3
4
5
6

msg.sender 操作员从 from 账号销毁 amount 数量的代币。

记住: 如果操作员没有授权,需要 revert

  • 接口ID: fc673c4f
  • 参数:
    • from: 销毁代币的账号(持有者)
    • amount: 销毁数量
    • data: 持有者提供的信息
    • operatorData: 操作员提供的信息

注意: 操作员可以用 operatorData 传递任何信息,信息必须是操作员提供的。

注意: frommsg.sender 可以是相同的地址。

例如: 持有者可以自己调用 operatorBurn ,相当于用了额外的操作员及信息operatorData 调用 burn。(而 burn 函数自身无法实现 )

# 调用钩子函数:ERC777TokensSendertokensToSend

调用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;
}
1
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
1
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 是接收者地址

  • 销毁时 to0x0

  • amount 必须是发送或销毁的数量

  • data 发送或销毁操作如果有data,信息必须带上。

  • operatorData 发送或销毁操作如果有operatorData,信息必须带上。

  • 持有者也许会通过 revert 阻止交易 (例如拒绝从其账户扣款)。

注意: 也许多个持有者会使用相同的ERC777TokensSender 实现。

注意: 但是对于所有的 ERC777 合约, 一个地址只能注册一个实现。因此ERC777TokensSender 会被多个合约调用,tokensToSend 实现里, msg.sender 是合约地址。

ERC20兼容性需要:

执行钩子函数优先于ERC20,并且触发ERC20transfertransferFrom事件时必须调用(如果注册了)。 从 transfer 函数调用时,operatorfrom 相同。 从 transferFrom 函数调用时,operator 和 触发调用的地址相同。

# 钩子函数 ERC777TokensRecipienttokensReceived

调用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;
}
1
2
3
4
5
6
7
8
9
10

如果接收者是合约而又没有 ERC777TokensRecipient 实现,那么代币合约需要:

  • 如果是从ERC777的 send 和 mint调用时,必须回退状态(revert

  • 如果是兼容ERC20transfertransferFrom 应该继续完成交易。

注意: 普通地址可以用一个实现的合约(代表普通地址来执行)地址注册,合约可以用自己的地址也可以用另一个地址注册,只要其实现了对应的接口。

tokensReceived

function tokensReceived(
    address operator,
    address from,
    address to,
    uint256 amount,
    bytes calldata data,
    bytes calldata operatorData
) external
1
2
3
4
5
6
7
8

用于通知接受代币。

注意: 请勿在发送(或 ERC20 transfer)或 铸币 之外调用。

  • 接口ID: 0023de29
  • 参数:
    • operator: 操作员(触发者)
    • from: 从哪个地址扣除(铸币为0x0)
    • to: 接收者
    • amount: 数量
    • data: 持有者信息
    • operatorData: 操作员信息

调用tokensReceived钩子函数的规则:

  • 必需是在发送或铸币时调用tokensReceived钩子函数。

  • 调用tokensReceived钩子函数必须在状态更新后。

  • operator 必须是触发发送或铸币操作的地址。

  • from 是持有者地址

  • 铸币时 from0x0

  • to 是接收者地址

  • amount 必须是发送或铸币的数量

  • data 发送或铸币操作如果有data,信息必须带上。

  • operatorData 发送或铸币操作如果有operatorData,信息必须带上。

  • 接收者也许会通过 revert 阻止交易 (例如拒绝接受代币)。

注意: 也许多个接收者会使用相同的ERC777TokensRecipient 实现。

注意: 但是对于所有的ERC777 合约, 一个地址只能注册一个实现。因此ERC777TokensRecipient 会被多个合约调用,tokensReceived 实现里, msg.sender 是合约地址。

ERC20兼容性需要:

执行钩子函数优先于ERC20,并且触发ERC20transfertransferFrom事件时必须调用(如果注册了)。 从 transfer 函数调用时,operatorfrom 相同。 从 transferFrom 函数调用时,operator 和 触发调用的地址相同。

# 关于Gas消耗

Dapps 和 钱包在发送、铸币、销毁时应该先测量 gas 消耗 — 使用 eth_estimateGas — 以避免 out-of-gas

# 图标

Image beige logo white logo light grey logo dark grey logo black logo
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 标准没有明确定义创建和销毁代币的过程。

# 数据

铸币、发送和销毁都是使用了dataoperatorData 字段(在整个操作过程都会带上)。

对于简单用例,这些字段可能是空的,或者它们可能包含与token移动相关的有价值信息,如发送者或银行本身附加到银行转账的信息。

使用 data 字段同样存在于其他标准提案中,例如EIP223 参与标准的多个社区成员审查了该标准。

# 钩子函数(Hooks)

在大多数情况下,ERC20需要两次调用才能将代币安全地转移到合约而不被合约锁定。 一个调用是发送者的 approve, 另一个是接收者的 transferFrom

此外,还需要各方之间进行额外的沟通,这些沟通尚未明确界定。

最后,持有者可能会在 transferapprove / transferFrom 之间感到困惑。 使用 transfer 将代币转移到合约很可能导致代币被锁定。

钩子函数让发送过程更简单,并提供一种方式将代币发送给任何接收者。 由于 tokensReceived 钩子函数,合约能够在接收时做出相应响应并防止代币被锁定。

# 持有人更好地控制

调用tokensReceived钩子函数允许接收人拒绝接收代币,其给了接收者持有人更多的控制(如基于一些参数,如 dataoperatorData)谁可以接收(或拒绝)代币。

遵循相同的意图和基于社区的建议,添加了调用tokensToSend钩子函数以控制和防止代币的移出。

# ERC1820 注册表合约

ERC1820注册表合约允许持有人注册他们的钩子函数,事先链接持有人和函数。

首先是在发送者或接收者的钩子函数定义,方法类似于EIP223,它在接收者合约上提出了一个tokenFallback 函数, 在收到代币时被调用,但它依靠ERC165进行接口检测,这个方法简单直接,但有一些限制。

特别是,发送者和接收者必须是合约才能提供钩子的实现。外部账号地址无法受益于钩子函数。已存在的合约很可能也不兼容,他们不知道也没法定义新的钩子函数实现。因此,现有的智能合约基础设施,如多签钱包其拥有的大量的ether和 代币需要迁移到新的升级合约中。

考虑的第二种方法是使用ERC672,它使用反向ENS为地址提供伪自省。 然而,这种方法严重依赖于ENS,在此基础上需要实施反向查找。带来了更多的复杂性和安全性问题,这将超越方法的好处。

第二种方法是就是标准使用的ERC1820注册表合约, 它允许任何地址注册实现,并从中获益。

为了不让ERC777标准过于复杂, 注册表合约保存在单独的EIP提案中。 更重要的是,注册表合约以灵活的方式设计,这样其他EIP和智能合约基础设施都可以从中受益, 不单单是ERC777或其他的代币。

此注册表的第一个建议是ERC820。不幸的是,从Solidity语言升级到版本0.5及更高版本产生了Bug, 在ERC820最后召集 Last Call 之后被发现,已经不适合修提案。

因此,用于ERC777的注册表的标准成为ERC1820ERC1820ERC820在功能上等效。 ERC1820仅包含针对Solidity较新版本的修复。

# 操作员

标准将操作员的概念作为任何移动代币的地址。 每个地址直观地移动自己的代币,将持有人和操作员的概念分开可以提供更大的灵活性。 主要是因为该标准为持有人定义了一种机制,让其他地址成为他们的操作员。 此外,与ERC20中的 approve 、 transferFrom 不同,其未明确定义批准地址的角色, ERC777详细介绍了与操作员进行交互的意图,包括批准操作员的义务, 以及任何持有人授权和撤销操作员的权利。

# 默认操作员

根据社区对预批准操作员的需求添加了默认操作员。这是默认被所有持有人授权的操作员, 出于明显的安全原因,默认操作员列表只能在代币创建时定义的,并且无法更改。

任何持有人仍然有权撤销默认操作员。 默认操作员的明显优势之一是允许代币的 ether-less 移动。

ether-less: 译者的理解是, 可用于诸如状态通道等二层网络进行离线转移。

默认操作员还具有其他可用性优势, 例如允许代币项目方以模块化方式提供功能,并降低持有人使用通过操作员使用功能的复杂性。

# 向后兼容

ERC777与较早的ERC20代币标准保持向后兼容。

该EIP 不使用transfertransferFrom 而是使用 sendoperatorSend 以避免产生混淆和错误

该标准允许实现ERC20功能transfertransferFromapproveallowance, 使代币与ERC20完全兼容。

为了与ERC20向后兼容,代币可以实现decimals(),如果实现,它必须总是返回18。

因此,代币币可以并行实现ERC20ERC777。 view函数的规范(例如name, symbol, balanceOf, totalSupply)和内部数据(例如余额映射)可以重叠。 但是请注意,ERC777中必须具有以下功能(必须实现): name, symbol balanceOftotalSupplydecimals 不属于ERC777标准 )

两种提案标准的状态修改函数是分开的,并且可以彼此独立地操作。 注意,ERC20函数应该仅限于仅从老合约中调用。

如果代币实现ERC20标准,它必须通过ERC1820用自己的地址注册ERC20Token接口。 通过在ERC1820注册表合约上调用setInterfaceImplementer函数来完成注册,使用合约地址作为参数: 地址 _addr 和实现者 _implementer, 接口哈希是 ERC20Tokenkeccak256哈希(0xaea199e31a596269b42cdafd93407f14436db6e4cad65417994c2eb37381e05a

如果该合约有开关启用或禁用 ERC20 函数,则每次触发该开关时,需要相应地注册或注销“ ERC20Token”接口 注销同样是 调用setInterfaceImplementer , 使用代币合约地址作为地址_addr 参数, 将“ 0x0”作为实现者 _implementer ,将ERC20Tokenkeccak256 哈希作为接口哈希。可以查阅 erc1820-set 了解更多。

实现 ERC20ERC777 合约的区别在于 tokensToSendtokensReceived 钩子函数 优先于ERC20

即使调用ERC20 transfertransferFrom,代币也必须通过ERC1820进行检查 fromto 地址是否分别实现了 tokensToSendtokensReceived 钩子函数, 如果实现了任何钩子函数,则必须调用它。

请注意,当在Token上调用ERC20transfer时,如果该Token没有实现tokensReceived, 即使这意味着代币可能会被锁定, transfer 也仍可以调用成功。

下表总结了在不同的操作是, ERC777ERC20 必须采取的不同措施

ERC1820 目标 地址 ERC777 Sending 和 Minting ERC20 transfer/transferFrom
ERC777TokensRecipient
已注册
常规用户地址 必须调用tokensReceived
合约
ERC777TokensRecipient
未注册
常规用户地址 正常执行
合约 必须 revert 应该执行 1

1.

由于ERC20 没有钩子,因此交易应继续进行,但是,这可能会导致意外锁定代币。 如果要避免意外锁定代币至关重要,则该交易可以 revert </ code>。

如果未实现tokensToSend,也无需采取任何特殊处理。

转移必须继续进行,并且只有在不遵守其他条件的情况下才能取消,如:余额不足或 tokensReceivedrevert 例如或“ tokensReceived”中的“还原”(如果存在)。

在发送,铸币和销毁期间,必须触发各自的 Sent, MintedBurned事件。 此外,如果代币合约通过ERC1820实现了 ERC20Token , 代币合约必须触发用于 铸币 和销毁的Transfer事件(ERC20标准),

ERC20transfertransferFrom 函数中,必须发出有效的 Sent 事件。

因此,对于代币的任何转移,可能会发出两个事件: ERC20 TransferERC777SentMintedBurned(取决于转移类型)。

第三方开发人员必须注意不要将两个事件都视为独立的动作。通常,如果应用程序将代币视为ERC20代币, 那么仅应考虑 Transfer 事件。 如果应用程序将代币视为ERC777代币,那么仅应考虑Sent,MintedBurned`事件。

# 测试用例

参考 0xjac/ERC777

# 实现

参考 ERC777 ReferenceToken 实现可通过 npm install erc777 安装。 及Openzeppelin ERC777

# 版权

原文采用CC0, 本翻译采用BY-NC-ND许可协议,译者:深入浅出区块链 Tiny熊。