加载中 ...

ERC20 的问题和ERC777 功能型代币(通证)最佳实践

2019-09-30 21:16 编辑:币牛牛 来源:币源

想必很多同学都已经使用过ERC20 创建过代币[1],或许已经被老板要求在ERC20代币上实现一些附加功能搞的焦头烂额,如果还有选择,一定要选择 ERC777 。


ERC20 的问题



以下是一个遇到很多次的场景:有一天老板过来找你(开发者),最近存币生息很火,我们也做一个合约吧, 用户打币过来给他计算利息, 看起来是一个很简单的需求,你满口答应说好,结果自己一研究发现,使用 ERC20 标准没办法在合约里记录是谁发过来多少币,从而没法计算利息(因为接收者合约并不知道自己接收到ERC20代币)。



ERC20 标准下,可以通过一个变通的办法,采用两个交易组合完成,方法是:第1步:先让用户把要转移的金额用 ERC20 的approve 授权的存币生息合约(这步通常称为解锁),第2步:再次让用户调用存币生息合约的计息函数,计息函数中通过 transferFrom 把代币从用户手里转移的合约内,并开始计息。



同样由于ERC20 标准没有一个转账通知机制,很多ERC20代币误转到合约之后,再也没有办法把币转移出来,已经有大量的ERC20 因为这个原因被锁死,如锁死的QTUM[2],锁死的EOS[3] 。



另外一个问题是ERC20 转账时,无法携带额外的信息,例如:我们有一些客户希望让用户使用 ERC20 代币购买商品,因为转账没法携带额外的信息, 用户的代币转移过来,不知道用户具体要购买哪件商品,从而展加了线下额外的沟通成本。



ERC777很好的解决了这些问题,同时ERC777 也兼容 ERC20 标准。因此强烈建议新开发的代币使用ERC777标准。



ERC777 在 ERC20的基础上定义了 send(dest, value, data) 来转移代币, send函数额外的参数用来携带其他的信息,send函数会检查持有者和接收者是否实现了相应的钩子函数,如果有实现(不管是普通用户地址还是合约地址都可以实现钩子函数),则调用相应的钩子函数。



ERC1820 接口注册表合约



即便是一个普通用户地址,同样可以实现对 ERC777 转账的监听, 听起来有点神奇,其实这是通过 ERC1820 接口注册表合约来是实现的。



ERC1820 如此的重要,以至于ERC777单独把它拆出来作为一个EIP。



ERC1820 是一个全局的合约,有一个唯一在以太坊链上都相同的合约地址,它总是 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24 ,这个合约是通过非常巧妙的方式进行部署的,有兴趣的同学可以阅读EIP1820文档[4]。



ERC 1820 合约的官方实现代码在ERC1820文档[5]可以查阅,这里说明合约实现的主要内容。



ERC1820合约提过了两个主要接口:



?setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer) 用来设置地址(_addr)的接口(_interfaceHash 接口名称的 keccak256 )由哪个合约实现(_implementer)。



?getInterfaceImplementer(address _addr, bytes32 _interfaceHash) external view returns (address) 这个函数用来查询地址(_addr)的接口由哪个合约实现。



setInterfaceImplementer函数会参数信息记录到下面这个interfaces映射里:



// 记录 地址(第一个键) 的接口(第二个键)的实现地址(第二个值)
mapping(address => mapping(bytes32 => address)) interfaces;
相对应的 getInterfaceImplementer() 通过 interfaces 这个mapping 来获得接口的实现。



ERC777 使用 send转账时会分别在持有者和接收者地址上使用ERC1820 的getInterfaceImplementer函数进行查询,查看是否有对应的实现合约,ERC777 标准规范里预定了接口及函数名称,如果有实现则进行相应的调用。



ERC777 标准规范

ERC777 接口

ERC777 为了在实现上可以兼容ERC20,除了查询函数和ERC20一致外,操作接口均采用的独立的命名(避免相同的命令无法分辨是哪个标准),ERC777的接口定义如下,要求所有的ERC777代币合约都必须实现这些接口:



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);
}
接口定义在 openzeppelin代码库[6] 里找到,路径为:contracts/token/ERC777/IERC777.sol 。



接口说明与实现约定



所有的ERC777 合约除了必须实现上述接口,还有一些其他的必须遵守的约定(直接导致了ERC777官方文档又长又臭...哭~)。



ERC777 合约必须要通过 ERC1820 注册 ERC777Token 接口,这样任何人都可以查询合约是否是ERC777标准的合约,注册方法是: 调用ERC1820 注册合约的 setInterfaceImplementer 方法,参数 _addr 及 _implementer 均是合约的地址,_interfaceHash 是 ERC777Token 的 keccak256 哈希值(0xac7fbab5...177054)



如果 ERC777 要实现ERC20标准,还必须通过ERC1820 注册ERC20Token接口。



ERC777 信息说明函数



name(),symbol(),totalSupply(),balanceOf(address) 和含义和在ERC20 中完全一样。



granularity() 用来定义代币最小的划分粒度(>=1), 要求必须在创建时设定,之后不可以更改,不管是在铸币、发送还是销毁操作的代币数量,必需是粒度的整数倍。



365怎么买外围granularity 和 ERC20 的 decimals 不一样,decimals用来定义小数位数,decimals 是ERC20 可选函数,为了兼容 ERC20 代币, decimals 函数要求必须返回18。而 granularity 表示的是基于最小位数(内部存储)的划分粒度。例如:0.5个代币存储为 500,000,000,000,000,000 (0.5 X 10^18),如果粒度为2,则最小转账单位是2(相对于500,000,000,000,000,000)。



操作员



ERC777 定义了一个新的操作员角色,操作员被作为移动代币的地址。每个地址直观地移动自己的代币,将持有人和操作员的概念分开可以提供更大的灵活性。



与ERC20中的 approve 、 transferFrom 不同,其未明确定义批准地址的角色。



此外,ERC777还可以定义默认操作员(默认操作员列表只能在代币创建时定义的,并且不能更改),默认操作员是被所有持有人授权的操作员,这可以为项目方管理代币带来方便,当然认何持有人仍然有权撤销默认操作员。



操作员相关的函数:



?defaultOperators(): 获取代币合约默认的操作员列表.
?authorizeOperator(address operator): 设置一个地址作为msg.sender 的操作员,需要触发AuthorizedOperator事件。
?revokeOperator(address operator): 移除 msg.sender 上 operator 操作员的权限, 需要触发RevokedOperator事件。
?isOperatorFor(address operator, address holder):是否是某个持有者的操作员。



发送代币



ERC777 发送代币 使用以下两个方法:



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



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



operatorSend 可以通过参数operatorData携带操作者的信息,发送代币除了执行对应账户的余额加减和触发事件之外,还有额外的规定:



1.如果持有者有通过 ERC1820 注册 ERC777TokensSender 实现接口, 代币合约必须调用其 tokensToSend 钩子函数。
2.如果接收者有通过 ERC1820 注册 ERC777TokensRecipient 实现接口, 代币合约必须调用其 tokensReceived 钩子函数。
3.如果有 tokensToSend 钩子函数,必须在修改余额状态之前调用。
4.如果有 tokensReceived 钩子函数,必须在修改余额状态之后调用。
5.调用钩子函数及触发事件时, data 和 operatorData必须原样传递,因为 tokensToSend 和 tokensReceived 函数可能根据这个数据取消转账(触发 revert)。



ERC777TokensSender 接口定义如下:



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



如果持有者希望在转账时收到代币转移通知,就需要在ERC1820合约上注册及实现 ERC777TokensSender 接口(稍后有案例介绍)。



有一个地方需要注意: 对于所有的 ERC777 合约, 一个持有者地址只能注册一个ERC777TokensSender接口实现。因此 ERC777TokensSender 实现会被多个ERC777合约调用,在ERC777TokensSender接口的实现合约里, msg.sender 是ERC777合约地址,而不是操作者。



ERC777TokensRecipient 接口定义如下:



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



如果接收者希望在转账时收到代币转移通知,就需要在ERC1820合约上注册及实现 ERC777TokensRecipient 接口。



如果接收者是一个合约地址, 则必须要注册及实现 ERC777TokensRecipient 接口(这样可以防止代币被锁死),如果没有实现,ERC777代币合约必须revert 回退交易状态。



铸币与销毁



铸币(挖矿)是产生新币的过程,销毁代币则相反,在ERC20 中,没有明确定义这两个行为,通常会transfer方法和Transfer事件来表达。ERC777 则定义了代币从铸币、转移到销毁的整个生命周期。



ERC777 没有定义铸币的方法名,只定义了 Minted事件,因为很多代币,是在创建的时候就确定好代币的数量。如果有需要合约可以自己定义铸币函数,铸币函数在实现时要求:



1.必须触发Minted事件
2.发行量需要加上铸币量, 接收者是不为 0 ,且接收者余额加上铸币量。
3.如果接收者有通过 ERC1820 注册 ERC777TokensRecipient 实现接口, 代币合约必须调用其 tokensReceived 钩子函数。



ERC777 定义了两个函数用于销毁代币 (burn 和 operatorBurn),可以方便钱包和dapps有统一的接口交互。burn 和 operatorBurn 的实现要求:



1.必须触发Burned事件。
2.总供应量必须减少代币销毁量, 持有者的余额必须减少代币销毁的数量。
3.如果持有者通过ERC1820注册ERC777TokensSender 实现,必须调用持有者的tokensToSend钩子函数。



注意,零个代币数量的交易(不管是转移、铸币与销毁)也是合法的,同样满足粒度(granularity) 的整数倍,因此需要正确处理。



ERC777 代币实现
OpenZeppelin 实现了一个 ERC777 基础合约,要实现自己的ERC777代币只需要继承 OpenZeppelin ERC777。想了解 OpenZeppelin 的 ERC777 的实现可阅读ERC777 源码解析[7]。



如果大家是Truffle开发(或者是Node工程),可以使用以下方式安装 OpenZeppelin 合约库:



npm install @openzeppelin/contracts
发行一个 2100 个的 LBC7 代币的代码就很简单了:



pragma solidity ^0.5.0;



import "@openzeppelin/contracts/token/ERC777/ERC777.sol";



contract MyERC777 is ERC777 {
constructor(
address[] memory defaultOperators
)
ERC777("MyERC777", "LBC7", defaultOperators)
public
{
uint initialSupply = 2100 * 10 ** 18;
_mint(msg.sender, msg.sender, initialSupply, "", "");
}
}



实现主要是两步:通过基类ERC777的构造函数确认代币名称、代号以及默认操作员(可为空),然后调用 _mint 初始化发行量,注意发行量的小数位是固定的18位(和ether保持一致),在合约内部是按小数位保存的,因此发行的币数需要乘上1018。



监听代币收款



我们假设有这样一个需求:寺庙要实现了一个功德箱合约接收捐赠,功德箱合约需要记录每位施主的善款金额。这时候就可以通过实现 ERC777TokensRecipient接口来完成。代码也很简单:



pragma solidity ^0.5.0;



import "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol";
import "@openzeppelin/contracts/token/ERC777/IERC777.sol";
import "@openzeppelin/contracts/introspection/IERC1820Registry.sol";



contract Merit is IERC777Recipient {



mapping(address => uint) public givers;
address _owner;
IERC777 _token;



IERC1820Registry private _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);



// keccak256("ERC777TokensRecipient")
bytes32 constant private TOKENS_RECIPIENT_INTERFACE_HASH =
0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b;



constructor(IERC777 token) public {
_erc1820.setInterfaceImplementer(address(this), TOKENS_RECIPIENT_INTERFACE_HASH, address(this));
_owner = msg.sender;
_token = token;
}



// 收款时被回调
function tokensReceived(
address operator,
address from,
address to,
uint amount,
bytes calldata userData,
bytes calldata operatorData
) external {
givers[from] += amount;
}



// 方丈取回功德箱token
function withdraw () external {
require(msg.sender == _owner, "no permision");
uint balance = _token.balanceOf(address(this));
_token.send(_owner, balance, "");
}



}



功德箱合约在构造时,调用 ERC1820 注册表合约的 setInterfaceImplementer函数 注册ERC777TokensRecipient接口实现(接口的实现是自身),这样在收到代币时,会回调 tokensReceived函数,tokensReceived函数通过givers映射来保存每个施主的善款金额。



注意:如果是在本地的开发者网络环境,可能会没有ERC1820 注册表合约,如果没有需要先部署ERC1820注册表合约,参考eip-1820 中文文档[8]。



功德箱这个实例仅仅是抛砖引玉,告诉大家如何实现收款时的回调,之后有时间,我写一个完整的存币生息应用。



普通账户地址监听代币转出
功德箱合约的例子,收款地址和收款监听是同一个合约, 现在来看看一个普通的用户地址,如何委托一个合约来监听代币的转出。监听代币的转出可以让持有者对发出去的代币有更多的控制,例如持有者可以设置一些黑名单,禁止操作员对黑名单内账号转账。



References
[1] ERC20 创建过代币: https://learnblockchain.cn/2018/01/12/create_token...
[2] 锁死的QTUM: https://etherscan.io/address/0x9a642d6b3368ddc662C...
[3] 锁死的EOS: https://etherscan.io/address/0x86fa049857e0209aa7d...
[4] EIP1820文档: https://learnblockchain.cn/docs/eips/eip-1820.html
[5] ERC1820文档: https://learnblockchain.cn/docs/eips/eip-1820.html
[6] openzeppelin代码库: https://github.com/OpenZeppelin/openzeppelin-contr...
[7] ERC777 源码解析: https://learnblockchain.cn/2019/09/26/erc777-code/
[8] eip-1820 中文文档: https://learnblockchain.cn/docs/eips/eip-1820.html
[9] 我的小专栏: https://xiaozhuanlan.com/topic/5920148376

关键词:365怎么买外围_外围 365_365彩票网外围币新闻 币牛牛

转载自365怎么买外围_外围 365_365彩票网外围币新闻网(www.btc268.com),提供365怎么买外围_外围 365_365彩票网外围币行情走势分析与数字货币投资炒币最新消息。

原文标题:ERC20 的问题和ERC777 功能型代币(通证)最佳实践

原文地址:http://www.btc268.com/news/qkl/15856.html

本文来源:币源编辑:币牛牛

本文仅代表作者个人观点,与本网站立场无关。

本网站转载信息目的在于传递更多信息。请读者仅作参考,投资有风险,入市须谨慎!

'); })();