一、ERC220和ERC777
1.1 ERC20的问题
-
1. 当给某个合约转币,ERC20无法在合约里面记录是谁发过来的币,导致不能进行后续的一系统操作,因为接收者并不知道自己接受了ERC20代币。
上面这一操作可以通过另外的方法:先把用户要转移的金额通过approve方法授权过存币生息合约,再让用户调用存币生息合约的计息函数,计息函数有transforfrom函数把代币从用户手中转移给合约内。
-
2. ERC20合约没有转账通知机制,导致很多ERC20代币误转到合约后,无法提取出来,导致大量代币被锁死,例如:如锁死的QTUM,锁死的EOS.
-
ERC20转账时,无法携带额外的信息
例如:有一些客户希望让用户使用 ERC20 代币购买商品,因为转账没法携带额外的信息, 用户的代币转移过来,不知道用户具体要购买哪件商品,从而增加了线下额外的沟通成本。
1.2 ERC777
ERC777很好的解决了上述的问题,并且兼容ERC20标准,现在大部分DAPP合约正在从ERC20转向ERC777合约,强烈建议开发者们使用。
ERC777可以允许第三方帐户(或合约)使用您的代币。最常见的需求是在使用去中心化交易所智能合约交一键发币时。
ERC777在ERC20的基础上定义了一个send(recipient,amount,data)
函数,data
参数可以用来携带其他信息。send
函数会检查持有者和接收者是否实现了相应的钩子函数,如果有实现(不管是普通用户地址还是合约地址都可以实现钩子函数),则调用相应的钩子函数。
ERC777的优点如下:
- 合约和普通地址都可以通过注册
tokensToSend
hook函数来控制和拒绝发送哪些token(拒绝发送通过在hook函数tokensToSend
里revert
来实现)。 - 合约和普通地址都可以通过注册
tokensReceived
hook函数来控制和拒绝接受哪些token(拒绝接受通过在hook函数tokensReceived
里revert
来实现)。 tokensReceived
可以通过hook函数可以做到在一个交易里完成发送代币和通知合约接受代币,而不像 ERC20必须通过两次调用(approve
/transferFrom
)来完成。- 持有者可以"授权"和"撤销"操作员(operators: 可以代表持有者发送代币)。 这些操作员通常是(去中心化)交易所、支票处理机或自动支付系统。
- 每个代币交易都包含
data
和operatorData
字段, 可以分别传递来自持有者和操作员的数据。 - 可以通过部署实现
tokensReceived
的代理合约来兼容没有实现tokensReceived
函数的地址。
二、ERC777的规范标准
ERC777除了查询函数和ERC20标准一样,其他操作接口都采用独立的命名,ERC777的接口定义如下,要求ERC777的实现合约都必须实现所定义的接口。
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC777/IERC777.sol)
pragma solidity ^0.8.0;
/**
* @dev Interface of the ERC777Token standard as defined in the EIP.
*
* This contract uses the
* https://eips.ethereum.org/EIPS/eip-1820[ERC1820 registry standard] to let
* token holders and recipients react to token movements by using setting implementers
* for the associated interfaces in said registry. See {IERC1820Registry} and
* {ERC1820Implementer}.
*/
interface IERC777 {
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 tokenHolder);
//被取消的授权操作者 => 代币持有者
event RevokedOperator(address indexed operator, address indexed tokenHolder);
function name() external view returns (string memory);
function symbol() external view returns (string memory);
//定义代币最小的划分粒度
function granularity() external view returns (uint256);
function balanceOf(address owner) external view returns (uint256);
//发送代币
function send(
address recipient,
uint256 amount,
bytes calldata data
) external;
//销魂代币
function burn(uint256 amount, bytes calldata data) external;
//判断该操作者是否是被代币持有者授权的
function isOperatorFor(address operator, address tokenHolder) external view returns (bool);
//添加指定操作员
function authorizeOperator(address operator) external;
//撤销指定操作员
function revokeOperator(address operator) external;
//得到默认的操作员
function defaultOperators() external view returns (address[] memory);
//操作者发送代币
function operatorSend(
address sender,
address recipient,
uint256 amount,
bytes calldata data,
bytes calldata operatorData
) external;
//操作者销魂代币
function operatorBurn(
address account,
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
);
}
三、ERC777.sol
前提知识:
ERC777标准通过引入Operator
的概念来解决上述问题。像往常一样,操作者是某些具有执行特定任务权限的帐户。在ERC777标准下,有两类操作者:
- 默认操作者
- 指定操作者
一个指定操作者是允许代表另一个地址发送和销毁代币的地址。 一个默认操作者是允许代表所有代币持有者发送和销毁代币的地址。
任意ERC777代币持有者地址可以通过调用authorizeOperator
方法函数授权给一个指定操作者获得权限。与此类似,任意ERC777代币持有者都可以通过调用revokeOperator
方法函数撤销操作者地址的授权(包括默认运算符)。
在一键发币的ERC777实现中, 默认操作者是用于部署代币的地址。
1.属性
-
using Address for address
给address地址使用Address库,可以调用库里面的方法 -
_totalSupply
,_name
,_symbol
和ERC20的标准一样 -
_defaultOperatorsArray
用于存放默认的操作员,默认操作员只能在代币初始创建时被定义,并且无法被更改。
using Address for address;
IERC1820Registry internal constant _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);
uint256 private _totalSupply;
string private _name;
string private _symbol;
bytes32 private constant _TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender");
bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient");
address[] private _defaultOperatorsArray;
2.映射
-
_balances
表示代币持有人的地址与对应的代币数量 -
_defaultOperators
表示该默认操作者地址是否存在 -
_operators
表示该代币持有者地址对应的指定操作者是否存在 -
_revokedDefaultOperators
表示代币持有者地址对应的撤销操作者是否存在 -
_allowances
和ERC20标准中的一样,授权某个地址可以使用代币持有者的一定数量代币,这个被授权的地址不一定是操作者。
mapping(address => uint256) private _balances;
mapping(address => bool) private _defaultOperators;
mapping(address => mapping(address => bool)) private _operators;
mapping(address => mapping(address => bool)) private _revokedDefaultOperators;
// ERC20-allowances
mapping(address => mapping(address => uint256)) private _allowances;
3.构造器
在合约首次初始化的时,初始化代币名称,符号(代币的图片),默认操作者地址数组,遍历数组,并更新_defalutOperators
mapping
constructor(
string memory name_,
string memory symbol_,
address[] memory defaultOperators_
) {
_name = name_;
_symbol = symbol_;
_defaultOperatorsArray = defaultOperators_;
for (uint256 i = 0; i < defaultOperators_.length; i++) {
_defaultOperators[defaultOperators_[i]] = true;
}
// register interfaces
_ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this));
_ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC20Token"), address(this));
}
4.合约函数
4.1基本函数
-
name
,symbol
,decimals
,totalSupply()
,balanceOf
函数和ERC20标准一样 -
granularity()
用来定义代币最小的划分粒度(>=1), 要求必须在创建时设定,之后不可以更改,不管是在铸币、发送还是销毁操作的代币数量,必须是粒度的整数倍。例如:如果granularity()
返回的是2,则最小转账单位是2
function name() public view virtual override returns (string memory) {
return _name;
}
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
function decimals() public pure virtual returns (uint8) {
return 18;
}
function granularity() public view virtual override returns (uint256) {
return 1;
}
function totalSupply() public view virtual override(IERC20, IERC777) returns (uint256) {
return _totalSupply;
}
function balanceOf(address tokenHolder) public view virtual override(IERC20, IERC777) returns (uint256) {
return _balances[tokenHolder];
}
_mint()
1)先判断接收者地址是否为0地址
2)将调用该合约的地址命为操作者,调用 _beforeTokenTransfer
方法可在转账之前做一些操作
3)将总的代币数量加加,以及接收者地址代币数量加加
4)调用_callTokensReceived()
判断接收者地址是否实现了tokensReceived()
方法,只有实现了该方法,才可以接收代币,否则转账交易终止。
_burn()
1)先判断销毁者地址是否为0地址
2)将调用该合约的地址命为操作者
3)_callTokensToSend()
表明要销毁代币的持有者必须实现tokensToSend()
的方法才可以进行代币发出或交易的操作
4)调用 _beforeTokenTransfer
方法可在转账之前做一些操作
_approve()
1)判断接收者地址和被授权者地址不为0
2)更新_allowance
mapping
function _mint(address account, uint256 amount,bytes memory userData,bytes memory operatorData,bool requireReceptionAck
) internal virtual {
require(account != address(0), "ERC777: mint to the zero address");
address operator = _msgSender();
_beforeTokenTransfer(operator, address(0), account, amount);
_totalSupply += amount;
_balances[account] += amount;
_callTokensReceived(operator, address(0), account, amount, userData, operatorData, requireReceptionAck);
emit Minted(operator, account, amount, userData, operatorData);
emit Transfer(address(0), account, amount);
}
function _burn(address from,uint256 amount,bytes memory data, bytes memory operatorData
) internal virtual {
require(from != address(0), "ERC777: burn from the zero address");
address operator = _msgSender();
_callTokensToSend(operator, from, address(0), amount, data, operatorData);
_beforeTokenTransfer(operator, from, address(0), amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC777: burn amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
}
_totalSupply -= amount;
emit Burned(operator, from, amount, data, operatorData);
emit Transfer(from, address(0), amount);
}
function _approve( address holder,address spender, uint256 value) internal virtual {
require(holder != address(0), "ERC777: approve from the zero address");
require(spender != address(0), "ERC777: approve to the zero address");
_allowances[holder][spender] = value;
emit Approval(holder, spender, value);
}
ERC777的其他重点函数将在下一讲介绍。
参考文章: