Alert Source Discuss
Standards Track: ERC

ERC-777: 代币标准

Authors Jacques Dafflon <mail@0xjac.com>, Jordi Baylina <jordi@baylina.cat>, Thomas Shababi <tom@truelevel.io>
Created 2017-11-20
Requires EIP-1820

简单总结

本 EIP 定义了代币合约的标准接口和行为。

摘要

本标准定义了一种与代币合约交互的新方式,同时保持与 ERC-20 的向后兼容性。

它定义了与代币交互的高级功能。 即,operator 代表另一个地址(合约或常规帐户)发送代币,以及 发送/接收 hooks,以便为代币持有者提供对其代币的更多控制。

它利用 ERC-1820 来确定何时以及何地通知合约和常规地址 当他们收到代币时,以及允许与已部署的合约兼容。

动机

本标准试图改进广泛使用的 ERC-20 代币标准。 本标准的主要优点是:

  1. 使用与以太币相同的理念,即代币通过 send(dest, value, data) 发送。

  2. 合约和常规地址都可以通过注册 tokensToSend hook 来控制和拒绝它们发送的代币。 (拒绝通过在 hook 函数中 revert 来完成。)

  3. 合约和常规地址都可以通过注册 tokensReceived hook 来控制和拒绝它们接收的代币。 (拒绝通过在 hook 函数中 revert 来完成。)

  4. tokensReceived hook 允许将代币发送到合约并在单个交易中通知它, 与 ERC-20 不同,ERC-20 需要双重调用 (approve/transferFrom) 才能实现此目的。

  5. 持有人可以“授权”和“撤销”可以代表他们发送代币的 operator。 这些 operator 旨在成为经过验证的合约, 例如交易所、支票处理器或自动收费系统。

  6. 每个代币交易都包含 dataoperatorData 字节字段 可以自由地用于分别传递来自持有人和 operator 的数据。

  7. 通过为钱包部署一个实现 tokensReceived hook 的代理合约,它可以向后兼容不包含 tokensReceived hook 函数的钱包。

规范

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);
}

代币合约必须实现上述接口。 该实现必须遵循下面描述的规范。

代币合约必须通过 ERC-1820 使用其自身地址注册 ERC777Token 接口。

这是通过在 ERC-1820 注册表上调用 setInterfaceImplementer 函数来完成的, 其中代币合约地址既是地址又是实现者, 并且 ERC777Tokenkeccak256 哈希 (0xac7fbab5f54a3ca8194167523c6753bfeb96a445279294b6125b68cce2177054) 作为接口哈希。

如果合约有一个开关来启用或禁用 ERC-777 功能,每次触发开关时, 代币必须相应地通过 ERC1820 为其自身的地址注册或注销 ERC777Token 接口。 注销意味着调用 setInterfaceImplementer 函数,其中代币合约地址作为地址, ERC777Tokenkeccak256 哈希作为接口哈希,0x0 作为实现者。 (有关更多详细信息,请参见 ERC-1820 中的 为地址设置接口。)

当与代币合约交互时,所有金额和余额必须是无符号整数。 即,在内部,所有值都存储为代币的 1E-18 面额。 显示面额——用于向最终用户显示任何金额——必须是内部面额的 1018

换句话说,内部面额类似于 wei,显示面额类似于 ether。 它等效于 ERC-20decimals 函数返回 18。 例如,如果代币合约为用户返回 500,000,000,000,000,000 (0.5×1018) 的余额, 用户界面必须向用户显示 0.5 个代币。 如果用户希望发送 0.3 个代币, 则必须使用 300,000,000,000,000,000 (0.3×1018) 的金额来调用合约。

从代币合约的 ABI 以编程方式生成的用户界面 可以使用并显示内部面额。 但这必须明确说明,例如通过显示 uint256 类型。

View 函数

必须实现下面详细介绍的 view 函数。

name 函数

function name() external view returns (string memory)

获取代币的名称,例如,"MyToken"

identifier: 06fdde03 returns: 代币的名称。

symbol 函数

function symbol() external view returns (string memory)

获取代币的符号,例如,"MYT"

identifier: 95d89b41 returns: 代币的符号。

totalSupply 函数

function totalSupply() external view returns (uint256)

获取已铸造代币的总数。

注意:总供应量必须等于所有地址的余额之和——如 balanceOf 函数返回的那样。

注意:总供应量必须等于所有铸造的代币之和, 如所有 Minted 事件中定义的那样,减去所有销毁的代币之和,如所有 Burned 事件中定义的那样。

identifier: 18160ddd returns: 当前流通中的代幣總供應量。

balanceOf 函数

function balanceOf(address holder) external view returns (uint256)

获取地址为 holder 的帐户的余额。

余额必须为零 (0) 或更高。

identifier: 70a08231 parameters holder: 返回余额的地址。

returns: holder 在代币合约中持有的代币数量。

granularity 函数

function granularity() external view returns (uint256)

获取不可分割的代币的最小部分。

换句话说,粒度是可以随时铸造、发送或销毁的最小代币数量(以内部面额表示)。

关于 granularity,必须应用以下规则:

  • granularity 值必须在创建时设置。

  • granularity 值永远不能更改。

  • granularity 值必须大于或等于 1

  • 所有余额必须是粒度的倍数。

  • 任何数量的已铸造、发送或销毁的代币(以内部面额表示) 必须是 granularity 值的倍数。

  • 任何会导致余额不是 granularity 值倍数的操作 必须被视为无效,并且交易必须 revert

注意:大多数代币应该是完全可分割的。 也就是说,除非有充分的理由不允许任何代币分数,否则此函数应返回 1

identifier: 556f0dc7 returns: 代币的最小不可分割部分。

注意defaultOperatorsisOperatorFor 也是 view 函数, 为了一致性在 operators 下定义。

ERC-20 兼容性要求: 代币的小数位数必须始终为 18。 对于 ERC-777 代币,ERC-20 decimals 函数是可选的, 并且在与代币合约交互时不得依赖其存在。 (18 的小数位数是隐含的。) 对于 ERC-20 兼容代币,decimals 函数是必需的,并且必须返回 18。 (在 ERC-20 中,decimals 函数是可选的。 如果该函数不存在,则小数位数的值未明确定义,可能会被假定为 0。 因此,出于兼容性原因,必须为 ERC-20 兼容代币实现 decimals 函数。)

Operator

operator 是一个地址,允许代表某些 holder 发送和销毁代币。

当地址成为 holderoperator 时,必须发出一个 AuthorizedOperator 事件。 AuthorizedOperatoroperator(主题 1)和 holder(主题 2) 必须分别是 operatorholder 的地址。

holder 撤销一个 operator 时,必须发出一个 RevokedOperator 事件。 RevokedOperatoroperator(主题 1)和 holder(主题 2) 必须分别是 operatorholder 的地址。

注意:一个 holder 可以同时拥有多个 operator

代币可以定义 默认 operator默认 operator 是所有 holder 隐式授权的 operator。 定义 默认 operator 时,不得发出 AuthorizedOperator 事件。 以下规则适用于 默认 operator

  • 代币合约必须在创建时定义 默认 operator

  • 默认 operator 必须是不变量。也就是说,代币合约不得添加或删除 默认 operator

  • 定义 默认 operator 时,不得发出 AuthorizedOperator 事件。

  • 必须允许 holder 撤销 默认 operator (除非 holder 是有问题的 默认 operator)。

  • 必须允许 holder 重新授权先前撤销的 默认 operator

  • 当为特定 holder 显式授权或撤销 默认 operator 时, 必须分别发出 AuthorizedOperatorRevokedOperator 事件。

以下规则适用于任何 operator

  • 地址必须始终是其自身的 operator。因此,地址不得被撤销为其自身的 operator

  • 如果地址是 holderoperator,则 isOperatorFor 必须返回 true

  • 如果地址不是 holderoperator,则 isOperatorFor 必须返回 false

  • holder 授权地址作为其 operator 时,代币合约必须发出具有正确值的 AuthorizedOperator 事件,如 AuthorizedOperator 事件 中定义的那样。

  • holder 撤销地址作为其 operator 时,代币合约必须发出具有正确值的 RevokedOperator 事件,如 RevokedOperator 事件 中定义的那样。

注意holder 可以授权已经授权的 operator。 每次都必须发出 AuthorizedOperator 事件。

注意holder 可以撤销已经撤销的 operator。 每次都必须发出 RevokedOperator 事件。

AuthorizedOperator 事件

event AuthorizedOperator(address indexed operator, address indexed holder)

表示授权 operator 作为 holderoperator

注意:此事件不得在 operator 授权过程之外发出。

parameters operator: 成为 holderoperator 的地址。 holder: 授权 operator 地址作为 operatorholder 的地址。

RevokedOperator 事件

event RevokedOperator(address indexed operator, address indexed holder)

表示撤销 operator 作为 holderoperator

注意:此事件不得在 operator 撤销过程之外发出。

parameters operator: 作为 holderoperator 被撤销的地址。 holder: 撤销 operator 地址作为 operatorholder 的地址。

必须实现下面描述的 defaultOperatorsauthorizeOperatorrevokeOperatorisOperatorFor 函数 来管理 operator。 代币合约可以实现其他函数来管理 operator

defaultOperators 函数

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

获取代币合约定义的 默认 operator 列表。

注意:如果代币合约没有任何 默认 operator,则此函数必须返回一个空列表。

identifier: 06e48538 returns: 所有 默认 operator 的地址列表。

authorizeOperator 函数

function authorizeOperator(address operator) external

将第三方 operator 地址设置为 msg.senderoperator,以代表其发送和销毁代币。

注意holder (msg.sender) 始终是其自身的 operator。 此权利不得撤销。 因此,如果调用此函数来授权 holder (msg.sender) 作为其自身的 operator(即,如果 operator 等于 msg.sender),则此函数必须 revert

identifier: 959b8c3f parameters operator: 要设置为 msg.senderoperator 的地址。

revokeOperator 函数

function revokeOperator(address operator) external

删除 operator 地址作为 msg.senderoperator 的权利, 并代表其发送和销毁代币。

注意holder (msg.sender) 始终是其自身的 operator。 此权利不得撤销。 因此,如果调用此函数来撤销 holder (msg.sender) 作为其自身的 operator(即,如果 operator 等于 msg.sender),则此函数必须 revert

identifier: fad8b32a parameters operator: 要撤销作为 msg.senderoperator 的地址。

isOperatorFor 函数

function isOperatorFor(
    address operator,
    address holder
) external view returns (bool)

指示 operator 地址是否为 holder 地址的 operator

identifier: d95b6371 parameters operator: 可能是 holderoperator 的地址。 holder: 可能将 operator 地址作为 operatorholder 的地址。

returns: 如果 operatorholderoperator,则为 true,否则为 false

注意:要了解哪些地址是给定 holderoperator, 必须为每个 默认 operator 使用 holder 调用 isOperatorFor, 并解析有问题的 holderAuthorizedOperatorRevokedOperator 事件。

发送代币

operatoramount 的代币从 holder 发送到 recipient 并附带 dataoperatorData 时,代币合约必须应用以下规则:

  • 任何授权的 operator 都可以将代币发送给任何 recipient(除了 0x0)。

  • holder 的余额必须减少 amount

  • recipient 的余额必须增加 amount

  • holder 的余额必须大于或等于amount——这样 其结果余额在发送后大于或等于零 (0)。

  • 代币合约必须发出具有正确值的 Sent 事件,如 Sent 事件 中定义的那样。

  • operator 可以在 operatorData 中包含信息。

  • 如果 holder 通过 ERC-1820 注册了一个 ERC777TokensSender 实现,则代币合约必须调用 holdertokensToSend hook。

  • 如果 recipient 通过 ERC-1820 注册了一个 ERC777TokensRecipient 实现,则代币合约必须调用 recipienttokensReceived hook。

  • dataoperatorData 在整个发送过程中必须是不可变的——因此 必须使用相同的 dataoperatorData 来调用这两个 hook 并发出 Sent 事件。

在以下任何情况下发送时,代币合约必须 revert

  • operator 地址不是 holder 的授权 operator。

  • 发送之后,产生的 holder 余额或 recipient 余额 不是由代币合约定义的 granularity 的倍数。

  • recipient 是一个合约,并且它没有通过 ERC-1820 实现 ERC777TokensRecipient 接口。

  • holderrecipient 的地址是 0x0

  • 任何产生的余额都变成负数,即变得小于零 (0)。

  • holdertokensToSend hook revert

  • recipienttokensReceived hook revert

代币合约可以从多个 holder 向多个 recipient 发送代币,或者两者都有。在这种情况下:

  • 之前的发送规则必须适用于所有 holder 和所有 recipient
  • 所有增加的余额之和必须等于总发送的 amount
  • 所有减少的余额之和必须等于总发送的 amount
  • 必须为每个 holderrecipient 对发出一个带有对应金额的 Sent 事件。
  • 来自 Sent 事件的所有金额之和必须等于总发送的 amount

注意:诸如在发送时应用费用之类的机制被认为是一种发送给多个 recipient 的行为: 预期的 recipient 和费用 recipient

注意:代币的移动可以被链接。 例如,如果一个合约在收到代币后将其进一步发送到另一个地址。 在这种情况下,之前的发送规则按顺序适用于每个发送。

注意:发送零 (0) 个代币的金额是有效的,必须被视为正常发送。

实现要求

  • 代币合约必须在更新状态之前调用 tokensToSend hook。
  • 代币合约必须在更新状态之后调用 tokensReceived hook。 也就是说,必须首先调用 tokensToSend, 然后必须更新余额以反映发送, 最后必须在之后调用 tokensReceived。 因此,在 tokensToSend 中调用 balanceOf 返回地址在发送之前的余额, 而在 tokensReceived 中调用 balanceOf 返回地址在发送之后的余额。

注意data 字段包含由 holder 提供的信息——类似于 常规以太币发送交易中的 data 字段。 tokensToSend() hook、tokensReceived() 或两者 可以使用该信息来决定是否要拒绝该交易。

注意operatorData 字段类似于 data 字段,只是它应由 operator 提供。

operatorData 必须仅由 operator 提供。 它更适合用于日志记录目的和特殊情况。 (示例包括付款参考、支票号码、反签名等。) 在大多数情况下,recipient 将忽略 operatorData,或者充其量,它会记录 operatorData

Sent 事件

event Sent(
    address indexed operator,
    address indexed from,
    address indexed to,
    uint256 amount,
    bytes data,
    bytes operatorData
)

指示 operator 地址将 amount 的代币从 from 地址发送到 to 地址。

注意:此事件不得在发送或 ERC-20 转账过程之外发出。

parameters operator: 触发发送的地址。 from: 其代币被发送的 Holder to: 代币的 recipient。 amount: 发送的代币数量。 data: 由 holder 提供的信息。 operatorData: 由 operator 提供的信息。

必须实现下面描述的 sendoperatorSend 函数来发送代币。 代币合约可以实现其他函数来发送代币。

send 函数

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

amount 的代币从地址 msg.sender 发送到地址 to

operatorholder 都必须是 msg.sender

identifier: 9bd9bbc6 parameters to: 代币的 recipient。 amount: 要发送的代币数量。 data: 由 holder 提供的信息。

operatorSend 函数

function operatorSend(
    address from,
    address to,
    uint256 amount,
    bytes calldata data,
    bytes calldata operatorData
) external

代表地址 fromamount 的代币发送到地址 to

提醒:如果 operator 地址不是 from 地址的授权 operator, 则发送过程必须 revert

注意frommsg.sender 可以是相同的地址。 也就是说,一个地址可以为其自身调用 operatorSend。 此调用必须等效于 send,此外, operator 可以为 operatorData 指定一个显式值 (这不能通过 send 函数完成)。

identifier: 62ad1b83 parameters from: 正在发送其代币的 Holder to: 代币的 recipient。 amount: 要发送的代币数量。 data: 由 holder 提供的信息。 operatorData: 由 operator 提供的信息。

铸造代币

铸造代币是指生产新的代币。 ERC-777 有意不定义用于铸造代币的特定函数。 这种意图来自于不限制使用 ERC-777 标准的愿望, 因为铸造过程通常对于每个代币都是特定的。

但是,在为 recipient 铸造时,必须遵守以下规则:

  • 可以为任何 recipient 地址铸造代币(除了 0x0)。

  • 总供应量必须增加铸造的代币数量。

  • 0x0 的余额不得减少。

  • recipient 的余额必须增加铸造的代币数量。

  • 代币合约必须发出具有正确值的 Minted 事件,如 Minted 事件 中定义的那样。

  • 如果 recipient 通过 ERC-1820 注册了一个 ERC777TokensRecipient 实现,则代币合约必须调用 recipienttokensReceived hook。

  • dataoperatorData 在整个铸造过程中必须是不可变的——因此 必须使用相同的 dataoperatorData 来调用 tokensReceived hook 并发出 Minted 事件。

在以下任何情况下铸造时,代币合约必须 revert

  • 铸造后,生成的 recipient 余额不是由代币合约定义的 granularity 的倍数。
  • recipient 是一个合约,并且它没有通过 ERC-1820 实现 ERC777TokensRecipient 接口。
  • recipient 的地址是 0x0
  • recipienttokensReceived hook revert

注意:在创建代币合约时的初始代币供应必须被视为铸造 对于接收初始供应的一个或多个地址的初始供应数量。 这意味着必须发出一个或多个 Minted 事件 并且必须调用 recipient 的 tokensReceived hook。

ERC-20 兼容性要求: 虽然在铸造时不得发出 Sent 事件, 但如果代币合约是 ERC-20 向后兼容的, 则应按照 ERC-20 标准中的定义,发出 from 参数设置为 0x0Transfer 事件。

代币合约可以一次为多个 recipient 铸造代币。在这种情况下:

  • 之前的铸造规则必须适用于所有 recipient
  • 所有增加的余额之和必须等于总铸造量。
  • 必须为每个 recipient 发出一个带有对应金额的 Minted 事件。
  • 来自 Minted 事件的所有金额之和必须等于总铸造的 amount

注意:铸造零 (0) 个代币的金额是有效的,必须被视为正常铸造。

注意:虽然在发送或销毁期间,数据由 holder 提供,但它不适用于铸造。 在这种情况下,数据可以由代币合约或 operator 提供, 例如,以确保成功铸造到期望特定数据的 holder

注意operatorData 字段包含由 operator 提供的信息——类似于 常规以太币发送交易中的 data 字段。 tokensReceived() hooks 可以使用该信息来决定是否要拒绝该交易。

Minted 事件

event Minted(
    address indexed operator,
    address indexed to,
    uint256 amount,
    bytes data,
    bytes operatorData
)

指示 operator 地址向 to 地址铸造了 amount 的代币。

注意:此事件不得在铸造过程之外发出。

parameters operator: 触发铸造的地址。 to: 代币的 recipient。 amount: 铸造的代币数量。 data: 为 recipient 提供的信息。 operatorData: 由 operator 提供的信息。

销毁代币

销毁代币是指销毁现有代币的行为。 ERC-777 显式定义了两个销毁代币的函数(burnoperatorBurn)。 这些函数有助于将销毁过程集成到钱包和 dapps 中。 但是,代币合约可以出于任何原因阻止某些或所有 holder 销毁代币。 代币合约还可以定义其他销毁代币的函数。

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

  • 可以从任何 holder 地址销毁代币(除了 0x0)。

  • 总供应量必须减少销毁的代币数量。

  • 0x0 的余额不得增加。

  • holder 的余额必须减少销毁的代币数量。

  • 代币合约必须发出具有正确值的 Burned 事件,如 Burned 事件 中定义的那样。

  • 如果 holder 通过 ERC-1820 注册了一个 ERC777TokensSender 实现,则代币合约必须调用 holdertokensToSend hook。

  • operatorData 在整个销毁过程中必须是不可变的——因此 必须使用相同的 operatorData 来调用 tokensToSend hook 并发出 Burned 事件。

在以下任何情况下销毁时,代币合约必须 revert

  • operator 地址不是 holder 的授权 operator。

  • 销毁后,生成的 holder 余额不是 granularity 的倍数 由代币合约定义。

  • holder 的余额低于要销毁的代币数量 (即,导致 holder 的余额为负数)。

  • holder 的地址是 0x0

  • holdertokensToSend hook revert

ERC-20 兼容性要求: 虽然在销毁时不得发出 Sent 事件; 如果代币合约已启用 ERC-20,则应发出 to 参数设置为 0x0Transfer 事件。 ERC-20 标准未定义销毁代币的概念,但这是普遍接受的做法。

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

  • 之前的销毁规则必须适用于每个 holder
  • 所有减少的余额之和必须等于总销毁量。
  • 必须为每个 holder 发出一个带有对应金额的 Burned 事件。
  • 来自 Burned 事件的所有金额之和必须等于总销毁的 amount

注意:销毁零 (0) 个代币的金额是有效的,必须被视为正常销毁。

注意data 字段包含由 holder 提供的信息——类似于 常规以太币发送交易中的 data 字段。 tokensToSend() hook、tokensReceived() 或两者 可以使用该信息来决定是否要拒绝该交易。

注意operatorData 字段类似于 data 字段,只是它应由 operator 提供。

Burned 事件

event Burned(
    address indexed operator,
    address indexed from,
    uint256 amount,
    bytes data,
    bytes operatorData
);

指示 operator 地址从 from 地址销毁了 amount 的代币。

注意:此事件不得在销毁过程之外发出。

parameters operator: 触发销毁的地址。 from: 其代币被销毁的 Holder amount: 销毁的代币数量。 data: 由 holder 提供的信息。 operatorData: 由 operator 提供的信息。

必须实现下面描述的 burnoperatorBurn 函数来销毁代币。 代币合约可以实现其他函数来销毁代币。

burn 函数

function burn(uint256 amount, bytes calldata data) external

从地址 msg.sender 销毁 amount 的代币。

operatorholder 都必须是 msg.sender

identifier: fe9d9303 parameters amount: 要销毁的代币数量。 data: 由 holder 提供的信息。

operatorBurn 函数

function operatorBurn(
    address from,
    uint256 amount,
    bytes calldata data,
    bytes calldata operatorData
) external

代表地址 from 销毁 amount 的代币。

提醒:如果 operator 地址不是 from 地址的授权 operator, 则销毁过程必须 revert

identifier: fc673c4f parameters from: 其代币将被销毁的 Holder amount: 要销毁的代币数量。 data: 由 holder 提供的信息。 operatorData: 由 operator 提供的信息。

注意operator 可以通过 operatorData 传递任何信息。 operatorData 必须仅由 operator 提供。

注意frommsg.sender 可以是相同的地址。 也就是说,一个地址可以为其自身调用 operatorBurn。 此调用必须等效于 burn 此外,operator 可以为 operatorData 指定一个显式值 (这不能通过 burn 函数完成)。

ERC777TokensSendertokensToSend Hook

tokensToSend hook(钩子)通知指定的 holder 的余额减少(发送和销毁)的任何请求。 任何希望收到从其地址扣除 token 通知的地址(常规地址或合约地址) 可以通过 ERC-1820 注册一个实现了下面描述的 ERC777TokensSender 接口的合约地址。

这是通过在 ERC-1820 注册表上调用 setInterfaceImplementer 函数完成的, 其中 holder 地址作为地址, ERC777TokensSenderkeccak256 哈希值 (0x29ddb589b1fb5fc7cf394961c1adf5f8c6454761adf795e67fe149f658abe895) 作为接口哈希值, 以及实现 ERC777TokensSender 的合约地址作为 implementer。

interface ERC777TokensSender {
    function tokensToSend(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    ) external;
}

注意: 常规地址可以注册一个不同的地址——一个合约的地址——代表它来实现接口。 合约可以注册其地址或另一个合约的地址, 但所述地址必须代表它来实现接口。

tokensToSend

function tokensToSend(
    address operator,
    address from,
    address to,
    uint256 amount,
    bytes calldata userData,
    bytes calldata operatorData
) external

通知一个由 operator 地址发起的,从 from 地址向 to 地址发送或销毁(如果 to0x0amount 个 token 的请求。

注意: 此函数不得在销毁、发送或 ERC-20 转账过程之外被调用。

identifier: 75ab9782
parameters operator: 触发余额减少的地址(通过发送或销毁)。 from: 被发送 token 的 Holder to: 发送 token 的接收者(对于销毁,则为 0x0)。 amount: holder 余额减少的 token 数量。 data: holder 提供的信息。 operatorData: operator 提供的信息。

调用 tokensToSend 钩子时,适用以下规则:

  • 必须为每个发送和销毁过程调用 tokensToSend 钩子。

  • tokensToSend 钩子必须在状态更新之前调用——即在余额减少之前

  • operator 必须是触发发送或销毁过程的地址。

  • from 必须是被发送或销毁 token 的 holder 的地址。

  • to 必须是接收发送 token 的 recipient 的地址。

  • 对于销毁,to 必须是 0x0

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

  • data 必须包含提供给发送或销毁过程的额外信息(如果有)。

  • operatorData 必须包含触发余额减少的地址提供的额外信息(如果有)。

  • holder 可以通过 revert 来阻止发送或销毁过程。 (即,拒绝从其帐户中提款 token。)

注意: 多个 holders 可以使用相同的 ERC777TokensSender 实现。

注意: 对于所有 ERC-777 token,一个地址在任何给定时间最多可以注册一个实现。 因此,ERC777TokensSender 必须预期被不同的 token 合约调用。 tokensToSend 调用的 msg.sender 预计是 token 合约的地址。

ERC-20 兼容性要求: 此钩子优先于 ERC-20,并且必须在调用 ERC-20transfertransferFrom 事件时调用(如果已注册)。 从 transfer 调用时,operator 必须与 from 的值相同。 从 transferFrom 调用时,operator 必须是发出 transferFrom 调用的地址。

ERC777TokensRecipienttokensReceived 钩子

tokensReceived 钩子通知指定的 recipient 的余额增加(发送和增发)的任何请求。 任何希望收到发送到其地址的 token 通知的地址(常规地址或合约地址) 可以通过 ERC-1820 注册一个实现了下面描述的 ERC777TokensRecipient 接口的合约地址。

这是通过在 ERC-1820 注册表上调用 setInterfaceImplementer 函数完成的, 其中 recipient 地址作为地址, ERC777TokensRecipientkeccak256 哈希值 (0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b) 作为接口哈希值, 以及实现 ERC777TokensRecipient 的合约地址作为 implementer。

interface ERC777TokensRecipient {
    function tokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external;
}

如果 recipient 是一个尚未注册 ERC777TokensRecipient 实现的合约; 那么 token 合约:

  • 如果从增发或发送调用中调用 tokensReceived 钩子,则必须 revert

  • 如果从 ERC-20 transfertransferFrom 调用中调用 tokensReceived 钩子,则应该继续处理交易。

注意: 常规地址可以注册一个不同的地址——一个合约的地址——代表它来实现接口。 合约必须注册其地址或另一个合约的地址, 但所述地址必须代表它来实现接口。

tokensReceived

function tokensReceived(
    address operator,
    address from,
    address to,
    uint256 amount,
    bytes calldata data,
    bytes calldata operatorData
) external

通知一个由 operator 地址发起的,从 from 地址向 to 地址发送或增发(如果 from0x0amount 个 token。

注意: 此函数不得在增发、发送或 ERC-20 转账过程之外被调用。

identifier: 0023de29
parameters
operator: 触发余额增加的地址(通过发送或增发)。
from: 被发送 token 的 Holder(对于增发,则为 0x0)。
to: token 的接收者。
amount: recipient 余额增加的 token 数量。
data: holder 提供的信息。
operatorData: operator 提供的信息。

调用 tokensReceived 钩子时,适用以下规则:

  • 必须为每个发送和增发过程调用 tokensReceived 钩子。

  • tokensReceived 钩子必须在状态更新之后调用——即在余额增加之后

  • operator 必须是触发发送或增发过程的地址。

  • from 必须是被发送 token 的 holder 的地址。

  • 对于增发,from 必须是 0x0

  • to 必须是接收 token 的 recipient 的地址。

  • amount 必须是 recipient 发送或增发的 token 数量。

  • data 必须包含提供给发送或增发过程的额外信息(如果有)。

  • operatorData 必须包含触发余额增加的地址提供的额外信息(如果有)。

  • holder 可以通过 revert 来阻止发送或增发过程。 (即,拒绝接收 token。)

注意: 多个 holders 可以使用相同的 ERC777TokensRecipient 实现。

注意: 对于所有 ERC-777 token,一个地址在任何给定时间最多可以注册一个实现。 因此,ERC777TokensRecipient 必须预期被不同的 token 合约调用。 tokensReceived 调用的 msg.sender 预计是 token 合约的地址。

ERC-20 兼容性要求: 此钩子优先于 ERC-20,并且必须在调用 ERC-20transfertransferFrom 事件时调用(如果已注册)。 从 transfer 调用时,operator 必须与 from 的值相同。 从 transferFrom 调用时,operator 必须是发出 transferFrom 调用的地址。

关于 Gas 消耗的说明

Dapps 和钱包应该首先估计在发送、增发或销毁 token 时所需的 gas——使用 eth_estimateGas——以避免在交易期间耗尽 gas。

Image ![米黄色 logo] ![白色 logo] ![浅灰色 logo] ![深灰色 logo] ![黑色 logo]
Color 米黄色 白色 浅灰色 深灰色 黑色
Hex #C99D66 #FFFFFF #EBEFF0 #3C3C3D #000000

该 logo 可以用于推广有效的 ERC-777 token 实现和 ERC-777 兼容技术(如钱包和 dapps),并可以进行修改和适配。

ERC-777 token 合约作者可以基于此 logo 为其 token 创建特定的 logo。

该 logo 不得用于宣传、推广或以任何方式关联不符合 ERC-777 的技术——如 token。

该标准的 logo 可以在 /assets/eip-777/logo 文件夹中找到 SVGPNG 格式。 该 logo 的 PNG 版本提供了一些像素大小。 如果需要,可以通过从 SVG 转换为 PNG 来创建其他大小。

理由

此标准的主要意图是 解决 ERC-20 的一些缺点,同时保持与 ERC-20 的向后兼容性, 并避免 EIP-223 的问题和漏洞。

以下是关于该标准主要方面决策的理由。

注意: 该标准作者之一 Jacques Dafflon (0xjac) 共同撰写了他的关于该标准的 [硕士论文], 其中包含的细节比直接包含在该标准中的更多, 并且可以提供关于某些方面或决策的进一步说明。

生命周期

ERC-777 不仅仅是发送 token,还定义了 token 的整个生命周期, 从增发过程开始,然后是发送过程,最后是销毁过程。

拥有清晰定义的生命周期对于一致性和准确性非常重要, 尤其是在价值来自稀缺性时。 相比之下,在查看某些 ERC-20 token 时,可以观察到 totalSupply 返回的值与实际流通供应量之间存在差异, 因为该标准没有明确定义创建和销毁 token 的过程。

数据

增发、发送和销毁过程都可以使用 dataoperatorData 字段, 这些字段被传递给任何移动(增发、发送或销毁)。 对于简单的用例,这些字段可能为空, 或者它们可能包含与 token 移动相关的有价值的信息, 类似于发送者或银行附加到银行转账的信息。

data 字段的使用同样存在于其他标准提案中,例如 EIP-223, 并且是审查此标准的社区多个成员的要求。

钩子

在大多数情况下,ERC-20 需要两次调用才能安全地将 token 转移到合约而不会锁定它们。 一次是发送者的调用,使用 approve 函数; 另一次是接收者的调用,使用 transferFrom。 此外,这需要各方之间额外的通信,而这些通信没有明确定义。 最后,持有者可能会对 transferapprove/transferFrom 感到困惑。 使用前者将 token 转移到合约很可能会导致 token 被锁定。

钩子允许简化发送过程,并提供一种将 token 发送到任何接收者的单一方式。 由于 tokensReceived 钩子,合约能够对接收做出反应并防止锁定 token。

持有者更大的控制权

tokensReceived 钩子还允许持有者拒绝接收某些 token。 这为持有者提供了更大的控制权,他们可以根据某些参数(例如位于 dataoperatorData 字段中)接受或拒绝传入 token。

遵循相同的意图并基于社区的建议, 添加了 tokensToSend 钩子,以控制和防止传出 token 的移动。

ERC-1820 注册表

ERC-1820 注册表允许持有者注册他们的钩子。 事先已经检查了其他替代方案来链接钩子和持有者。

第一种方法是在发送者或接收者的地址上定义钩子。 这种方法类似于 EIP-223,它在接收者合约上提出了一个 tokenFallback 函数, 以便在接收 token 时调用, 但通过依赖 ERC-165 进行接口检测来改进它。 虽然实现起来很简单,但这种方法施加了一些限制。 特别是,发送者和接收者必须是合约才能提供其钩子的实现。 防止外部拥有的地址从钩子中受益。 现有合约很可能不兼容, 因为它们无疑不知道并且没有定义新的钩子。 因此,现有的智能合约基础设施(如多重签名钱包) 可能持有大量以太币和 token,需要迁移到新的更新合约。

考虑的第二种方法是使用 ERC-672,它使用反向 ENS 为地址提供伪内省。 但是,这种方法严重依赖于 ENS,并且需要在其上实现反向查找。 对此方法的分析很快揭示了一定程度的复杂性和安全问题, 这将超越该方法的益处。

第三种解决方案——在此标准中使用——是依赖于一个唯一的注册表, 任何地址都可以在其中注册代表其实现钩子的合约地址。 这种方法的优点是外部拥有的帐户和合约都可以从钩子中受益, 包括可以依赖于部署在代理合约上的钩子的现有合约。

决定将此注册表保存在一个单独的 EIP 中, 以避免使此标准过于复杂。 更重要的是,注册表的设计方式很灵活, 以便其他 EIP 和智能合约基础设施可以从中受益 用于他们自己的用例,超出 ERC-777 和 token 的范围。 此注册表的第一个提案是 ERC-820。 不幸的是,Solidity 语言升级到 0.5 及更高版本所产生的问题 导致注册表中一个分离部分出现了一个错误,需要进行更改。 这是在最后一次征求意见期之后发现的。 试图避免创建单独的 EIP(如 ERC-820a)的尝试被拒绝了。 因此,用于 ERC-777 的注册表标准变成了 ERC-1820ERC-1820ERC-820 在功能上是等效的。ERC-1820 只是包含了对较新版本 Solidity 的修复。

操作员

该标准将操作员的概念定义为任何移动 token 的地址。 虽然直观上每个地址都会移动自己的 token, 但分离持有者和操作员的概念可以实现更大的灵活性。 首先,这源于这样一个事实,即该标准定义了一种机制,供持有者 让其他地址成为他们的操作员。 此外,与 ERC-20 中的 approve 调用不同,其中已批准地址的角色没有明确定义, ERC-777 详细说明了与操作员的意图和交互, 包括操作员有义务获得批准, 以及任何持有者都有权撤销操作员。

默认操作员

添加默认操作员是基于社区对预先批准的操作员的需求。 也就是说,默认情况下,为所有持有者批准的操作员。 出于显而易见的安全原因,默认操作员列表是在 token 合约创建时定义的, 并且无法更改。 任何持有者仍然有权撤销默认操作员。 默认操作员的明显优势之一是允许无以太币的 token 移动。 默认操作员还提供其他可用性优势, 例如允许 token 提供商以模块化的方式提供功能, 并降低持有者使用通过操作员提供的功能的复杂性。

向后兼容性

此 EIP 不引入向后不兼容性,并且与旧的 ERC-20 token 标准向后兼容。

此 EIP 不使用 transfertransferFrom,而是使用 sendoperatorSend 以避免在破译正在使用的 token 标准时出现混淆和错误。

此标准允许实现 ERC-20 函数 transfertransferFromapproveallowance 以及使 token 与 ERC-20 完全兼容。

token 可以实现 decimals() 以与 ERC-20 向后兼容。 如果已实现,则必须始终返回 18

因此,token 合约可以并行实现 ERC-20ERC-777view 函数(如 namesymbolbalanceOftotalSupply)和内部数据 (如余额的映射)的规范重叠而没有问题。 但是请注意,以下函数在 ERC-777 中是强制性的,并且必须实现: namesymbolbalanceOftotalSupplydecimals 不是 ERC-777 标准的一部分)。

来自两个标准的状态修改函数已分离,可以彼此独立地运行。 请注意,ERC-20 函数应仅限于旧合约的调用。

如果 token 实现了 ERC-20, 则必须通过 ERC-1820 使用其自己的地址注册 ERC20Token 接口。 这是通过在 ERC-1820 注册表上调用 setInterfaceImplementer 函数完成的, 其中 token 合约地址既是地址又是 implementer, 以及 ERC20Tokenkeccak256 哈希值 (0xaea199e31a596269b42cdafd93407f14436db6e4cad65417994c2eb37381e05a) 作为接口哈希值。

如果合约具有启用或禁用 ERC-20 功能的开关,则每次触发该开关时, token 必须通过 ERC1820 相应地为其自己的地址注册或注销 ERC20Token 接口。 注销意味着使用 token 合约地址作为地址, ERC20Tokenkeccak256 哈希值作为接口哈希值,以及 0x0 作为 implementer 来调用 setInterfaceImplementer。 (有关更多详细信息,请参阅 为地址设置接口 中的 ERC-1820。)

实现 ERC-20 的新合约的区别在于 tokensToSendtokensReceived 钩子优先于 ERC-20。 即使使用 ERC-20 transfertransferFrom 调用,token 合约也必须通过 ERC-1820 检查 fromto 地址是否分别实现了 tokensToSendtokensReceived 钩子。 如果实现了任何钩子,则必须调用它。 请注意,如果在合约上调用 ERC-20 transfer,如果合约未实现 tokensReceived, 即使这意味着 token 可能会被锁定,也应该仍然接受 transfer 调用。

下表总结了 token 合约在通过 ERC-777ERC-20 发送、增发和转移 token 时必须采取的不同操作:

ERC1820 to address ERC777 发送和增发 ERC20 transfer/transferFrom
ERC777TokensRecipient
已注册
常规地址 必须调用 tokensReceived
合约
ERC777TokensRecipient
未注册
常规地址 继续
合约 必须 revert 应该继续1

1. 为了清楚起见,交易应该继续,因为 ERC20 不知道钩子。 但是,这可能会导致意外锁定的 token。 如果要避免意外锁定的 token,则交易可以 revert

如果未实现 tokensToSend,则无需采取任何特定操作。 移动必须继续,并且只有在不尊重另一个条件时才取消, 例如资金不足或 tokensReceived 中的 revert(如果存在)。

在发送、增发和销毁期间,必须发出相应的 SentMintedBurned 事件。 此外,如果 token 合约声明它通过 ERC-1820 实现了 ERC20Token, 则 token 合约应该为增发和销毁发出 Transfer 事件, 并且必须为发送发出 Transfer 事件(如 ERC-20 标准中所指定)。 在 ERC-20transfertransferFrom 函数期间,必须发出有效的 Sent 事件。

因此,对于 token 的任何移动,可以发出两个事件: ERC-20 TransferERC-777 SentMintedBurned(取决于移动的类型)。 第三方开发人员必须小心不要将这两个事件都视为单独的移动。 作为一般规则,如果应用程序将 token 视为 ERC20 token, 则必须只考虑 Transfer 事件。 如果应用程序将 token 视为 ERC777 token, 则必须只考虑 SentMintedBurned 事件。

测试用例

具有参考实现的存储库 包含所有 测试

实现

GitHub 存储库 0xjac/ERC777 包含 [参考实现]。 参考实现也可以通过 npm 获得,可以使用 npm install erc777 安装。

版权

版权和相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Jacques Dafflon <mail@0xjac.com>, Jordi Baylina <jordi@baylina.cat>, Thomas Shababi <tom@truelevel.io>, "ERC-777: 代币标准," Ethereum Improvement Proposals, no. 777, November 2017. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-777.