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 代币标准。 本标准的主要优点是:
-
使用与以太币相同的理念,即代币通过
send(dest, value, data)
发送。 -
合约和常规地址都可以通过注册
tokensToSend
hook 来控制和拒绝它们发送的代币。 (拒绝通过在 hook 函数中revert
来完成。) -
合约和常规地址都可以通过注册
tokensReceived
hook 来控制和拒绝它们接收的代币。 (拒绝通过在 hook 函数中revert
来完成。) -
tokensReceived
hook 允许将代币发送到合约并在单个交易中通知它, 与 ERC-20 不同,ERC-20 需要双重调用 (approve
/transferFrom
) 才能实现此目的。 -
持有人可以“授权”和“撤销”可以代表他们发送代币的 operator。 这些 operator 旨在成为经过验证的合约, 例如交易所、支票处理器或自动收费系统。
-
每个代币交易都包含
data
和operatorData
字节字段 可以自由地用于分别传递来自持有人和 operator 的数据。 -
通过为钱包部署一个实现
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
函数来完成的, 其中代币合约地址既是地址又是实现者, 并且ERC777Token
的keccak256
哈希 (0xac7fbab5f54a3ca8194167523c6753bfeb96a445279294b6125b68cce2177054
) 作为接口哈希。
如果合约有一个开关来启用或禁用 ERC-777 功能,每次触发开关时,
代币必须相应地通过 ERC1820 为其自身的地址注册或注销 ERC777Token
接口。
注销意味着调用 setInterfaceImplementer
函数,其中代币合约地址作为地址,
ERC777Token
的 keccak256
哈希作为接口哈希,0x0
作为实现者。
(有关更多详细信息,请参见 ERC-1820 中的 为地址设置接口。)
当与代币合约交互时,所有金额和余额必须是无符号整数。 即,在内部,所有值都存储为代币的 1E-18 面额。 显示面额——用于向最终用户显示任何金额——必须是内部面额的 1018。
换句话说,内部面额类似于 wei,显示面额类似于 ether。
它等效于 ERC-20 的 decimals
函数返回 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
parametersholder
: 返回余额的地址。returns:
holder
在代币合约中持有的代币数量。
granularity
函数
function granularity() external view returns (uint256)
获取不可分割的代币的最小部分。
换句话说,粒度是可以随时铸造、发送或销毁的最小代币数量(以内部面额表示)。
关于 granularity,必须应用以下规则:
-
granularity 值必须在创建时设置。
-
granularity 值永远不能更改。
-
granularity 值必须大于或等于
1
。 -
所有余额必须是粒度的倍数。
-
任何数量的已铸造、发送或销毁的代币(以内部面额表示) 必须是 granularity 值的倍数。
-
任何会导致余额不是 granularity 值倍数的操作 必须被视为无效,并且交易必须
revert
。
注意:大多数代币应该是完全可分割的。
也就是说,除非有充分的理由不允许任何代币分数,否则此函数应返回 1
。
identifier:
556f0dc7
returns: 代币的最小不可分割部分。
注意:defaultOperators
和 isOperatorFor
也是 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 发送和销毁代币。
当地址成为 holder 的 operator 时,必须发出一个 AuthorizedOperator
事件。
AuthorizedOperator
的 operator
(主题 1)和 holder
(主题 2)
必须分别是 operator 和 holder 的地址。
当 holder 撤销一个 operator 时,必须发出一个 RevokedOperator
事件。
RevokedOperator
的 operator
(主题 1)和 holder
(主题 2)
必须分别是 operator 和 holder 的地址。
注意:一个 holder 可以同时拥有多个 operator。
代币可以定义 默认 operator。
默认 operator 是所有 holder 隐式授权的 operator。
定义 默认 operator 时,不得发出 AuthorizedOperator
事件。
以下规则适用于 默认 operator:
-
代币合约必须在创建时定义 默认 operator。
-
默认 operator 必须是不变量。也就是说,代币合约不得添加或删除 默认 operator。
-
定义 默认 operator 时,不得发出
AuthorizedOperator
事件。 -
必须允许 holder 撤销 默认 operator (除非 holder 是有问题的 默认 operator)。
-
必须允许 holder 重新授权先前撤销的 默认 operator。
-
当为特定 holder 显式授权或撤销 默认 operator 时, 必须分别发出
AuthorizedOperator
或RevokedOperator
事件。
以下规则适用于任何 operator:
-
地址必须始终是其自身的 operator。因此,地址不得被撤销为其自身的 operator。
-
如果地址是 holder 的 operator,则
isOperatorFor
必须返回true
。 -
如果地址不是 holder 的 operator,则
isOperatorFor
必须返回false
。 -
当 holder 授权地址作为其 operator 时,代币合约必须发出具有正确值的
AuthorizedOperator
事件,如AuthorizedOperator
事件 中定义的那样。 -
当 holder 撤销地址作为其 operator 时,代币合约必须发出具有正确值的
RevokedOperator
事件,如RevokedOperator
事件 中定义的那样。
注意:holder 可以授权已经授权的 operator。
每次都必须发出 AuthorizedOperator
事件。
注意:holder 可以撤销已经撤销的 operator。
每次都必须发出 RevokedOperator
事件。
event AuthorizedOperator(address indexed operator, address indexed holder)
表示授权 operator
作为 holder
的 operator。
注意:此事件不得在 operator 授权过程之外发出。
parameters
operator
: 成为holder
的 operator 的地址。holder
: 授权operator
地址作为 operator 的 holder 的地址。
event RevokedOperator(address indexed operator, address indexed holder)
表示撤销 operator
作为 holder
的 operator。
注意:此事件不得在 operator 撤销过程之外发出。
parameters
operator
: 作为holder
的 operator 被撤销的地址。holder
: 撤销operator
地址作为 operator 的 holder 的地址。
必须实现下面描述的 defaultOperators
、authorizeOperator
、revokeOperator
和 isOperatorFor
函数
来管理 operator。
代币合约可以实现其他函数来管理 operator。
function defaultOperators() external view returns (address[] memory)
获取代币合约定义的 默认 operator 列表。
注意:如果代币合约没有任何 默认 operator,则此函数必须返回一个空列表。
identifier:
06e48538
returns: 所有 默认 operator 的地址列表。
authorizeOperator
函数
function authorizeOperator(address operator) external
将第三方 operator
地址设置为 msg.sender
的 operator,以代表其发送和销毁代币。
注意:holder (msg.sender
) 始终是其自身的 operator。
此权利不得撤销。
因此,如果调用此函数来授权 holder (msg.sender
)
作为其自身的 operator(即,如果 operator
等于 msg.sender
),则此函数必须 revert
。
identifier:
959b8c3f
parametersoperator
: 要设置为msg.sender
的 operator 的地址。
revokeOperator
函数
function revokeOperator(address operator) external
删除 operator
地址作为 msg.sender
的 operator 的权利,
并代表其发送和销毁代币。
注意:holder (msg.sender
) 始终是其自身的 operator。
此权利不得撤销。
因此,如果调用此函数来撤销 holder (msg.sender
)
作为其自身的 operator(即,如果 operator
等于 msg.sender
),则此函数必须 revert
。
identifier:
fad8b32a
parametersoperator
: 要撤销作为msg.sender
的 operator 的地址。
function isOperatorFor(
address operator,
address holder
) external view returns (bool)
指示 operator
地址是否为 holder
地址的 operator。
identifier:
d95b6371
parametersoperator
: 可能是holder
的 operator 的地址。holder
: 可能将operator
地址作为 operator 的 holder 的地址。returns: 如果
operator
是holder
的 operator,则为true
,否则为false
。
注意:要了解哪些地址是给定 holder 的 operator,
必须为每个 默认 operator 使用 holder 调用 isOperatorFor
,
并解析有问题的 holder 的 AuthorizedOperator
和 RevokedOperator
事件。
发送代币
当 operator 将 amount
的代币从 holder 发送到 recipient
并附带 data
和 operatorData
时,代币合约必须应用以下规则:
-
任何授权的 operator 都可以将代币发送给任何 recipient(除了
0x0
)。 -
holder 的余额必须减少
amount
。 -
recipient 的余额必须增加
amount
。 -
holder 的余额必须大于或等于
amount
——这样 其结果余额在发送后大于或等于零 (0
)。 -
代币合约必须发出具有正确值的
Sent
事件,如Sent
事件 中定义的那样。 -
operator 可以在
operatorData
中包含信息。 -
如果 holder 通过 ERC-1820 注册了一个
ERC777TokensSender
实现,则代币合约必须调用 holder 的tokensToSend
hook。 -
如果 recipient 通过 ERC-1820 注册了一个
ERC777TokensRecipient
实现,则代币合约必须调用 recipient 的tokensReceived
hook。 -
data
和operatorData
在整个发送过程中必须是不可变的——因此 必须使用相同的data
和operatorData
来调用这两个 hook 并发出Sent
事件。
在以下任何情况下发送时,代币合约必须 revert
:
-
operator 地址不是 holder 的授权 operator。
-
发送之后,产生的 holder 余额或 recipient 余额 不是由代币合约定义的 granularity 的倍数。
-
recipient 是一个合约,并且它没有通过 ERC-1820 实现
ERC777TokensRecipient
接口。 -
holder 或 recipient 的地址是
0x0
。 -
任何产生的余额都变成负数,即变得小于零 (
0
)。 -
holder 的
tokensToSend
hookrevert
。 -
recipient 的
tokensReceived
hookrevert
。
代币合约可以从多个 holder 向多个 recipient 发送代币,或者两者都有。在这种情况下:
- 之前的发送规则必须适用于所有 holder 和所有 recipient。
- 所有增加的余额之和必须等于总发送的
amount
。 - 所有减少的余额之和必须等于总发送的
amount
。 - 必须为每个 holder 和 recipient 对发出一个带有对应金额的
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
。
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 提供的信息。
必须实现下面描述的 send
和 operatorSend
函数来发送代币。
代币合约可以实现其他函数来发送代币。
send
函数
function send(address to, uint256 amount, bytes calldata data) external
将 amount
的代币从地址 msg.sender
发送到地址 to
。
operator 和 holder 都必须是 msg.sender
。
identifier:
9bd9bbc6
parametersto
: 代币的 recipient。amount
: 要发送的代币数量。data
: 由 holder 提供的信息。
operatorSend
函数
function operatorSend(
address from,
address to,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external
代表地址 from
将 amount
的代币发送到地址 to
。
提醒:如果 operator 地址不是 from
地址的授权 operator,
则发送过程必须 revert
。
注意:from
和 msg.sender
可以是相同的地址。
也就是说,一个地址可以为其自身调用 operatorSend
。
此调用必须等效于 send
,此外,
operator 可以为 operatorData
指定一个显式值
(这不能通过 send
函数完成)。
identifier:
62ad1b83
parametersfrom
: 正在发送其代币的 Holder。to
: 代币的 recipient。amount
: 要发送的代币数量。data
: 由 holder 提供的信息。operatorData
: 由 operator 提供的信息。
铸造代币
铸造代币是指生产新的代币。 ERC-777 有意不定义用于铸造代币的特定函数。 这种意图来自于不限制使用 ERC-777 标准的愿望, 因为铸造过程通常对于每个代币都是特定的。
但是,在为 recipient 铸造时,必须遵守以下规则:
-
可以为任何 recipient 地址铸造代币(除了
0x0
)。 -
总供应量必须增加铸造的代币数量。
-
0x0
的余额不得减少。 -
recipient 的余额必须增加铸造的代币数量。
-
代币合约必须发出具有正确值的
Minted
事件,如Minted
事件 中定义的那样。 -
如果 recipient 通过 ERC-1820 注册了一个
ERC777TokensRecipient
实现,则代币合约必须调用 recipient 的tokensReceived
hook。 -
data
和operatorData
在整个铸造过程中必须是不可变的——因此 必须使用相同的data
和operatorData
来调用tokensReceived
hook 并发出Minted
事件。
在以下任何情况下铸造时,代币合约必须 revert
:
- 铸造后,生成的 recipient 余额不是由代币合约定义的 granularity 的倍数。
- recipient 是一个合约,并且它没有通过 ERC-1820 实现
ERC777TokensRecipient
接口。 - recipient 的地址是
0x0
。 - recipient 的
tokensReceived
hookrevert
。
注意:在创建代币合约时的初始代币供应必须被视为铸造
对于接收初始供应的一个或多个地址的初始供应数量。
这意味着必须发出一个或多个 Minted
事件
并且必须调用 recipient 的 tokensReceived
hook。
ERC-20 兼容性要求:
虽然在铸造时不得发出 Sent
事件,
但如果代币合约是 ERC-20 向后兼容的,
则应按照 ERC-20 标准中的定义,发出 from
参数设置为 0x0
的 Transfer
事件。
代币合约可以一次为多个 recipient 铸造代币。在这种情况下:
- 之前的铸造规则必须适用于所有 recipient。
- 所有增加的余额之和必须等于总铸造量。
- 必须为每个 recipient 发出一个带有对应金额的
Minted
事件。 - 来自
Minted
事件的所有金额之和必须等于总铸造的amount
。
注意:铸造零 (0
) 个代币的金额是有效的,必须被视为正常铸造。
注意:虽然在发送或销毁期间,数据由 holder 提供,但它不适用于铸造。 在这种情况下,数据可以由代币合约或 operator 提供, 例如,以确保成功铸造到期望特定数据的 holder。
注意:operatorData
字段包含由 operator 提供的信息——类似于
常规以太币发送交易中的 data 字段。
tokensReceived()
hooks 可以使用该信息来决定是否要拒绝该交易。
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 显式定义了两个销毁代币的函数(burn
和 operatorBurn
)。
这些函数有助于将销毁过程集成到钱包和 dapps 中。
但是,代币合约可以出于任何原因阻止某些或所有 holder 销毁代币。
代币合约还可以定义其他销毁代币的函数。
在销毁 holder 的代币时,必须遵守以下规则:
-
可以从任何 holder 地址销毁代币(除了
0x0
)。 -
总供应量必须减少销毁的代币数量。
-
0x0
的余额不得增加。 -
holder 的余额必须减少销毁的代币数量。
-
代币合约必须发出具有正确值的
Burned
事件,如Burned
事件 中定义的那样。 -
如果 holder 通过 ERC-1820 注册了一个
ERC777TokensSender
实现,则代币合约必须调用 holder 的tokensToSend
hook。 -
operatorData
在整个销毁过程中必须是不可变的——因此 必须使用相同的operatorData
来调用tokensToSend
hook 并发出Burned
事件。
在以下任何情况下销毁时,代币合约必须 revert
:
-
operator 地址不是 holder 的授权 operator。
-
销毁后,生成的 holder 余额不是 granularity 的倍数 由代币合约定义。
-
holder 的余额低于要销毁的代币数量 (即,导致 holder 的余额为负数)。
-
holder 的地址是
0x0
。 -
holder 的
tokensToSend
hookrevert
。
ERC-20 兼容性要求:
虽然在销毁时不得发出 Sent
事件;
如果代币合约已启用 ERC-20,则应发出 to
参数设置为 0x0
的 Transfer
事件。
ERC-20 标准未定义销毁代币的概念,但这是普遍接受的做法。
代币合约可以一次为多个 holder 销毁代币。在这种情况下:
- 之前的销毁规则必须适用于每个 holder。
- 所有减少的余额之和必须等于总销毁量。
- 必须为每个 holder 发出一个带有对应金额的
Burned
事件。 - 来自
Burned
事件的所有金额之和必须等于总销毁的amount
。
注意:销毁零 (0
) 个代币的金额是有效的,必须被视为正常销毁。
注意:data
字段包含由 holder 提供的信息——类似于
常规以太币发送交易中的 data 字段。
tokensToSend()
hook、tokensReceived()
或两者
可以使用该信息来决定是否要拒绝该交易。
注意:operatorData
字段类似于 data
字段,只是它应由 operator 提供。
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 提供的信息。
必须实现下面描述的 burn
和 operatorBurn
函数来销毁代币。
代币合约可以实现其他函数来销毁代币。
burn
函数
function burn(uint256 amount, bytes calldata data) external
从地址 msg.sender
销毁 amount
的代币。
operator 和 holder 都必须是 msg.sender
。
identifier:
fe9d9303
parametersamount
: 要销毁的代币数量。data
: 由 holder 提供的信息。
operatorBurn
函数
function operatorBurn(
address from,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external
代表地址 from
销毁 amount
的代币。
提醒:如果 operator 地址不是 from
地址的授权 operator,
则销毁过程必须 revert
。
identifier:
fc673c4f
parametersfrom
: 其代币将被销毁的 Holder。amount
: 要销毁的代币数量。data
: 由 holder 提供的信息。operatorData
: 由 operator 提供的信息。
注意:operator 可以通过 operatorData
传递任何信息。
operatorData
必须仅由 operator 提供。
注意:from
和 msg.sender
可以是相同的地址。
也就是说,一个地址可以为其自身调用 operatorBurn
。
此调用必须等效于 burn
此外,operator 可以为 operatorData
指定一个显式值
(这不能通过 burn
函数完成)。
ERC777TokensSender
和 tokensToSend
Hook
tokensToSend
hook(钩子)通知指定的 holder 的余额减少(发送和销毁)的任何请求。
任何希望收到从其地址扣除 token 通知的地址(常规地址或合约地址)
可以通过 ERC-1820 注册一个实现了下面描述的 ERC777TokensSender
接口的合约地址。
这是通过在 ERC-1820 注册表上调用
setInterfaceImplementer
函数完成的, 其中 holder 地址作为地址,ERC777TokensSender
的keccak256
哈希值 (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
地址发送或销毁(如果 to
是 0x0
)amount
个 token 的请求。
注意: 此函数不得在销毁、发送或 ERC-20 转账过程之外被调用。
identifier:
75ab9782
parametersoperator
: 触发余额减少的地址(通过发送或销毁)。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-20 的 transfer
和 transferFrom
事件时调用(如果已注册)。
从 transfer
调用时,operator
必须与 from
的值相同。
从 transferFrom
调用时,operator
必须是发出 transferFrom
调用的地址。
ERC777TokensRecipient
和 tokensReceived
钩子
tokensReceived
钩子通知指定的 recipient 的余额增加(发送和增发)的任何请求。
任何希望收到发送到其地址的 token 通知的地址(常规地址或合约地址)
可以通过 ERC-1820 注册一个实现了下面描述的 ERC777TokensRecipient
接口的合约地址。
这是通过在 ERC-1820 注册表上调用
setInterfaceImplementer
函数完成的, 其中 recipient 地址作为地址,ERC777TokensRecipient
的keccak256
哈希值 (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
transfer
或transferFrom
调用中调用tokensReceived
钩子,则应该继续处理交易。
注意: 常规地址可以注册一个不同的地址——一个合约的地址——代表它来实现接口。 合约必须注册其地址或另一个合约的地址, 但所述地址必须代表它来实现接口。
tokensReceived
function tokensReceived(
address operator,
address from,
address to,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external
通知一个由 operator
地址发起的,从 from
地址向 to
地址发送或增发(如果 from
是 0x0
)amount
个 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-20 的 transfer
和 transferFrom
事件时调用(如果已注册)。
从 transfer
调用时,operator
必须与 from
的值相同。
从 transferFrom
调用时,operator
必须是发出 transferFrom
调用的地址。
关于 Gas 消耗的说明
Dapps 和钱包应该首先估计在发送、增发或销毁 token 时所需的 gas——使用 eth_estimateGas
——以避免在交易期间耗尽 gas。
Logo
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
文件夹中找到 SVG
和 PNG
格式。
该 logo 的 PNG
版本提供了一些像素大小。
如果需要,可以通过从 SVG
转换为 PNG
来创建其他大小。
理由
此标准的主要意图是 解决 ERC-20 的一些缺点,同时保持与 ERC-20 的向后兼容性, 并避免 EIP-223 的问题和漏洞。
以下是关于该标准主要方面决策的理由。
注意: 该标准作者之一 Jacques Dafflon (0xjac) 共同撰写了他的关于该标准的 [硕士论文], 其中包含的细节比直接包含在该标准中的更多, 并且可以提供关于某些方面或决策的进一步说明。
生命周期
ERC-777 不仅仅是发送 token,还定义了 token 的整个生命周期, 从增发过程开始,然后是发送过程,最后是销毁过程。
拥有清晰定义的生命周期对于一致性和准确性非常重要,
尤其是在价值来自稀缺性时。
相比之下,在查看某些 ERC-20 token 时,可以观察到 totalSupply
返回的值与实际流通供应量之间存在差异,
因为该标准没有明确定义创建和销毁 token 的过程。
数据
增发、发送和销毁过程都可以使用 data
和 operatorData
字段,
这些字段被传递给任何移动(增发、发送或销毁)。
对于简单的用例,这些字段可能为空,
或者它们可能包含与 token 移动相关的有价值的信息,
类似于发送者或银行附加到银行转账的信息。
data
字段的使用同样存在于其他标准提案中,例如 EIP-223,
并且是审查此标准的社区多个成员的要求。
钩子
在大多数情况下,ERC-20 需要两次调用才能安全地将 token 转移到合约而不会锁定它们。
一次是发送者的调用,使用 approve
函数;
另一次是接收者的调用,使用 transferFrom
。
此外,这需要各方之间额外的通信,而这些通信没有明确定义。
最后,持有者可能会对 transfer
和 approve
/transferFrom
感到困惑。
使用前者将 token 转移到合约很可能会导致 token 被锁定。
钩子允许简化发送过程,并提供一种将 token 发送到任何接收者的单一方式。
由于 tokensReceived
钩子,合约能够对接收做出反应并防止锁定 token。
持有者更大的控制权
tokensReceived
钩子还允许持有者拒绝接收某些 token。
这为持有者提供了更大的控制权,他们可以根据某些参数(例如位于 data
或 operatorData
字段中)接受或拒绝传入 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-1820。 ERC-1820 和 ERC-820 在功能上是等效的。ERC-1820 只是包含了对较新版本 Solidity 的修复。
操作员
该标准将操作员的概念定义为任何移动 token 的地址。 虽然直观上每个地址都会移动自己的 token, 但分离持有者和操作员的概念可以实现更大的灵活性。 首先,这源于这样一个事实,即该标准定义了一种机制,供持有者 让其他地址成为他们的操作员。 此外,与 ERC-20 中的 approve 调用不同,其中已批准地址的角色没有明确定义, ERC-777 详细说明了与操作员的意图和交互, 包括操作员有义务获得批准, 以及任何持有者都有权撤销操作员。
默认操作员
添加默认操作员是基于社区对预先批准的操作员的需求。 也就是说,默认情况下,为所有持有者批准的操作员。 出于显而易见的安全原因,默认操作员列表是在 token 合约创建时定义的, 并且无法更改。 任何持有者仍然有权撤销默认操作员。 默认操作员的明显优势之一是允许无以太币的 token 移动。 默认操作员还提供其他可用性优势, 例如允许 token 提供商以模块化的方式提供功能, 并降低持有者使用通过操作员提供的功能的复杂性。
向后兼容性
此 EIP 不引入向后不兼容性,并且与旧的 ERC-20 token 标准向后兼容。
此 EIP 不使用 transfer
和 transferFrom
,而是使用 send
和 operatorSend
以避免在破译正在使用的 token 标准时出现混淆和错误。
此标准允许实现 ERC-20 函数 transfer
、transferFrom
、approve
和 allowance
以及使 token 与 ERC-20 完全兼容。
token 可以实现 decimals()
以与 ERC-20 向后兼容。
如果已实现,则必须始终返回 18
。
因此,token 合约可以并行实现 ERC-20 和 ERC-777。
view
函数(如 name
、symbol
、balanceOf
、totalSupply
)和内部数据
(如余额的映射)的规范重叠而没有问题。
但是请注意,以下函数在 ERC-777 中是强制性的,并且必须实现:
name
、symbol
、balanceOf
和 totalSupply
(decimals
不是 ERC-777 标准的一部分)。
来自两个标准的状态修改函数已分离,可以彼此独立地运行。 请注意,ERC-20 函数应仅限于旧合约的调用。
如果 token 实现了 ERC-20,
则必须通过 ERC-1820 使用其自己的地址注册 ERC20Token
接口。
这是通过在 ERC-1820 注册表上调用 setInterfaceImplementer
函数完成的,
其中 token 合约地址既是地址又是 implementer,
以及 ERC20Token
的 keccak256
哈希值 (0xaea199e31a596269b42cdafd93407f14436db6e4cad65417994c2eb37381e05a
)
作为接口哈希值。
如果合约具有启用或禁用 ERC-20 功能的开关,则每次触发该开关时,
token 必须通过 ERC1820 相应地为其自己的地址注册或注销 ERC20Token
接口。
注销意味着使用 token 合约地址作为地址,
ERC20Token
的 keccak256
哈希值作为接口哈希值,以及 0x0
作为 implementer 来调用 setInterfaceImplementer
。
(有关更多详细信息,请参阅 为地址设置接口 中的 ERC-1820。)
实现 ERC-20 的新合约的区别在于
tokensToSend
和 tokensReceived
钩子优先于 ERC-20。
即使使用 ERC-20 transfer
和 transferFrom
调用,token 合约也必须通过 ERC-1820 检查
from
和 to
地址是否分别实现了 tokensToSend
和 tokensReceived
钩子。
如果实现了任何钩子,则必须调用它。
请注意,如果在合约上调用 ERC-20 transfer
,如果合约未实现 tokensReceived
,
即使这意味着 token 可能会被锁定,也应该仍然接受 transfer
调用。
下表总结了 token 合约在通过 ERC-777 和 ERC-20 发送、增发和转移 token 时必须采取的不同操作:
ERC1820 | to address |
ERC777 发送和增发 | ERC20 transfer /transferFrom |
---|---|---|---|
ERC777TokensRecipient 已注册 |
常规地址 |
必须调用 tokensReceived
|
|
合约 | |||
ERC777TokensRecipient 未注册 |
常规地址 | 继续 | |
合约 | 必须 revert |
应该继续1 |
1. 为了清楚起见,交易应该继续,因为 ERC20 不知道钩子。 但是,这可能会导致意外锁定的 token。 如果要避免意外锁定的 token,则交易可以
revert
。
如果未实现 tokensToSend
,则无需采取任何特定操作。
移动必须继续,并且只有在不尊重另一个条件时才取消,
例如资金不足或 tokensReceived
中的 revert
(如果存在)。
在发送、增发和销毁期间,必须发出相应的 Sent
、Minted
和 Burned
事件。
此外,如果 token 合约声明它通过 ERC-1820 实现了 ERC20Token
,
则 token 合约应该为增发和销毁发出 Transfer
事件,
并且必须为发送发出 Transfer
事件(如 ERC-20 标准中所指定)。
在 ERC-20 的 transfer
或 transferFrom
函数期间,必须发出有效的 Sent
事件。
因此,对于 token 的任何移动,可以发出两个事件:
ERC-20 Transfer
和 ERC-777 Sent
、Minted
或 Burned
(取决于移动的类型)。
第三方开发人员必须小心不要将这两个事件都视为单独的移动。
作为一般规则,如果应用程序将 token 视为 ERC20 token,
则必须只考虑 Transfer
事件。
如果应用程序将 token 视为 ERC777 token,
则必须只考虑 Sent
、Minted
和 Burned
事件。
测试用例
具有参考实现的存储库 包含所有 测试。
实现
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.