如果你持有一个 ERC777[1] 代币 , 那么你就可以利用 ERC777 代币中的钩子函数方法 , 给自己布署一个账本合约来记录自己的账户每一次接收到的代币数量和对方账户等信息

钩子函数

ERC777 代币是 ERC20 代币合约的升级版 , 其中的最重要的升级功能就是每次进行转账的时候都会调用钩子函数 , 具体的方法我们可以在 ERC777 的代码中找到

             *
    //ERC777.solfunction_send(address from,address to,uint256 amount,bytes memory userData,bytes memory operatorData,bool requireReceptionAck) internal {    require(from != address(0), "ERC777: send from the zero address");    require(to != address(0), "ERC777: send to the zero address");    address operator =_msgSender();    // 调用发送钩子   _callTokensToSend(operator, from, to, amount, userData, operatorData);   _move(operator, from, to, amount, userData, operatorData);    // 调用接收钩子   _callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck);}

从上面的代码中我们看到在执行发送方法时会调用两次钩子方法 , 一次是调用发送钩子 , 一次是调用接收钩子 . 这两个钩子方法的具体实现我们看以下的代码 :

    //ERC777.sol// 发送钩子 function_callTokensToSend(address operator,address from,address to,uint256 amount,bytes memory userData,bytes memory operatorData) private {    // 获取发送账户的接口地址    address implementer =_ERC1820_REGISTRY.getInterfaceImplementer(from,_TOKENS_SENDER_INTERFACE_HASH);    if (implementer != address(0)) {        // 执行接口地址的 tokensToSend 方法        IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);    }}// 接收钩子 function_callTokensReceived(address operator,address from,address to,uint256 amount,bytes memory userData,bytes memory operatorData,bool requireReceptionAck) private {    // 获取接收账户的接口地址    address implementer =_ERC1820_REGISTRY.getInterfaceImplementer(to,_TOKENS_RECIPIENT_INTERFACE_HASH);    if (implementer != address(0)) {        // 执行接口地址的 tokensReceived 方法        IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);    } else if (requireReceptionAck) {        // 如果 requireReceptionAck  true 则必须执行接口方法 , 以防止代币被锁死        require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient");    }}

以上就是 ERC777 合约的钩子调用方法 , 我们在两个钩子的调用方法中都看到了通过 ERC1820 注册表 [2] 获取账户的接口地址的方法 , 那么这个接口地址又是怎么注册的呢 ? 我们可以在 ERC1820 的合约 [3] 代码中找到答案 :

验证接口

    //ERC1820.sol/// @notice 如果合约代表某个其他地址实现接口,则返回 Magic 值。bytes32 constant internal ERC1820_ACCEPT_MAGIC = keccak256(abi.encodePacked("ERC1820_ACCEPT_MAGIC"));  
    /// @notice 设置某个地址的接口由哪个合约实现,需要由管理员来设置。(每个地址是他自己的管理员,直到设置了一个新的地址)。/// @param_addr 待设置的关联接口的地址(如果 '_addr' 是零地址,则假定为 'msg.sender')/// @param_interfaceHash 接口,它是接口名称字符串的 keccak256 哈希值 /// 例如 : 'web3.utils.keccak256("ERC777TokensRecipient")' 表示 'ERC777TokensRecipient' 接口。/// @param_implementer 为地址 '_addr' 实现了 '_interfaceHash' 接口的合约地址  
    function setInterfaceImplementer(address_addr, bytes32_interfaceHash, address_implementer) external {    address addr =_addr == address(0) ? msg.sender :_addr;    require(getManager(addr) == msg.sender, "Not the manager");  
        require(!isERC165Interface(_interfaceHash), "Must not be an ERC165 hash");    if (_implementer != address(0) &&_implementer != msg.sender) {        // 调用接口合约的 canImplementInterfaceForAddress 方法 , 验证合约是否同意成为账户的接口        require(            ERC1820ImplementerInterface(_implementer)                .canImplementInterfaceForAddress(_interfaceHash, addr) == ERC1820_ACCEPT_MAGIC,                "Does not implement the interface"        );    }    interfaces[addr][_interfaceHash] =_implementer;    emit InterfaceImplementerSet(addr,_interfaceHash,_implementer);}

以上代码就是 ERC1820 注册表合约的注册接口地址的方法 , 通过向这个方法传递三个参数 (_addr,_interfaceHash,_implementer) 来为一个账户注册一个接口合约地址 . 代码中的 ERC1820ImplementerInterface(_implementer).canImplementInterfaceForAddress(_interfaceHash, addr) 这句最为核心 , 目的是调用参数中的 _implementer 接口合约的 canImplementInterfaceForAddress 方法来验证接口合约是否同意成为 _addr 账户的 _interfaceHash 这个方法的接口合约 , 如果 canImplementInterfaceForAddress 方法返回的是 ERC1820_ACCEPT_MAGIC 这个固定值 (keccak256(abi.encodePacked("ERC1820_ACCEPT_MAGIC"))) 则表示同意 .

接口合约

从前面的代码中我们看到了 , 接口合约必须实现 canImplementInterfaceForAddress 方法来告诉 ERC1820 注册表是否同意成为账户的接口 , 同时还要实现指定的接口方法 , 例如 tokensToSendtokensReceived.ERC1820 注册表也不是只为这两个接口服务的 , 你也可以利用这个原理制作出其他有趣的智能合约 .

所以制作一个接口合约我们要做的事情 :

•拥有一个 tokensReceived 方法满足 ERC777 合约的调用•拥有一个 canImplementInterfaceForAddress 方法告诉 ERC1820 注册表同意成为账户的接口•调用 ERC1820 合约的 setInterfaceImplementer 方法为你的账户注册接口合约

下面我们来看代码 :

    //TokensRecipient.solpragma solidity ^0.5.0;  
    import "@openzeppelin/contracts/ownership/Ownable.sol";import "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol";import "@openzeppelin/contracts/introspection/ERC1820Implementer.sol";import "@openzeppelin/contracts/introspection/IERC1820Registry.sol";import "@openzeppelin/contracts/token/ERC777/IERC777.sol";import "@openzeppelin/contracts/math/SafeMath.sol";  

    contract TokensRecipient is ERC1820Implementer, IERC777Recipient, Ownable {    bool private allowTokensReceived;    using SafeMath for uint256;    // keccak256("ERC777TokensRecipient")    bytes32 private constant TOKENS_RECIPIENT_INTERFACE_HASH = 0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b;  
        mapping(address => address) public token;    mapping(address => address) public operator;    mapping(address => address) public from;    mapping(address => address) public to;    mapping(address => uint256) public amount;    mapping(address => bytes) public data;    mapping(address => bytes) public operatorData;    mapping(address => uint256) public balanceOf;    //ERC1820 注册表合约地址 , 全网统一    IERC1820Registry internal constant ERC1820_REGISTRY = IERC1820Registry(        0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24    );  
        constructor(bool_setInterface) public {        if (_setInterface) {            // 为合约自身也注册一个接口 , 如果这个合约可以接收代币就用得到            ERC1820_REGISTRY.setInterfaceImplementer(                address(this),                TOKENS_RECIPIENT_INTERFACE_HASH,                address(this)            );        }       _registerInterfaceForAddress(TOKENS_RECIPIENT_INTERFACE_HASH, msg.sender);        allowTokensReceived = true;    }  
        function tokensReceived(        address_operator,        address_from,        address_to,        uint256_amount,        bytes calldata_data,        bytes calldata_operatorData    ) external {        require(allowTokensReceived, "Receive not allowed");        token[_from] = msg.sender;        operator[_from] =_operator;        from[_from] =_from;        to[_from] =_to;        amount[_from] = amount[_from].add(_amount);        data[_from] =_data;        operatorData[_from] =_operatorData;        balanceOf[_from] = IERC777(msg.sender).balanceOf(_from);        balanceOf[_to] = IERC777(msg.sender).balanceOf(_to);    }  
        function acceptTokens() public onlyOwner {        allowTokensReceived = true;    }  
        function rejectTokens() public onlyOwner {        allowTokensReceived = false;    }}  

以上我们使用了一些 Openzeppelin 的标准库 ,canImplementInterfaceForAddress 方法在 ERC1820Implementer.sol 合约文件中 , 通过第 40 行 _registerInterfaceForAddress 方法向 canImplementInterfaceForAddress 方法注册了同意成为发送账户 msg.senderTOKENS_RECIPIENT_INTERFACE_HASH 接口 . 在 tokensReceived 方法中我们将传入的交易数据一一记录在合约的变量中 , 例如通过 amount[_from] = amount[_from].add(_amount); 记录了发送账户累计向你的账户发送过多少代币 . acceptTokens()rejectTokens() 两个方法作为合约的开关 , 如果设置 allowTokensReceived 值为 false 则你的账户将会停止接收代币 , 这个方法也是很有用的 , 在之前的 ERC20 代币中很难实现 .

布署合约

布署合约的方法没有特别需要讲的 , 如果对布署合约不熟悉 , 请参考崔棉大师的花式发币法 [4]

测试合约

在接口合约布署之后 , 合约的功能并不会马上生效 , 因为你还需要调用 ERC1820 注册表合约去注册你的接口合约 我们通过写一个测试脚本来模拟这个过程 :

    const assert = require('assert');const { contract, accounts, web3 } = require('@openzeppelin/test-environment');const { ether, makeInterfaceId, singletons, expectEvent } = require('@openzeppelin/test-helpers');const ERC777Contract = contract.fromArtifact("ERC777Contract");const TokensRecipient = contract.fromArtifact("TokensRecipient");  
    [owner, sender, receiver] = accounts;const initialSupply = '1000000000';const defaultOperators = [sender];let amount = '100';const userData = web3.utils.toHex('A gift');describe("ERC777 代币 ", function () {    it(' 实例化 ERC1820 注册表 ', async function () {        ERC1820RegistryInstance = await singletons.ERC1820Registry(owner);    });    it(' 布署代币合约 ', async function () {        ERC777Param = [            // 构造函数的参数            "My Golden Coin",   // 代币名称            "MGC",              // 代币缩写            ether(initialSupply),      // 发行总量            defaultOperators    // 默认操作员        ]        ERC777Instance = await ERC777Contract.new(...ERC777Param, { from: owner });    });    it(' 布署接受接口合约 ', async function () {        TokensRecipientInstance = await TokensRecipient.new(true, { from: receiver });    });});  
    describe(" 注册 ERC1820 接口 ", function () {    it(' 注册代币接收接口 : setInterfaceImplementer() ERC777TokensRecipient', async function () {        await ERC1820RegistryInstance.setInterfaceImplementer(            receiver,            makeInterfaceId.ERC1820('ERC777TokensRecipient'),            TokensRecipientInstance.address,            { from: receiver }        );    });    it(' 验证代币接收接口 : setInterfaceImplementer() ERC777TokensRecipient', async function () {        assert.equal(TokensRecipientInstance.address, await ERC1820RegistryInstance.getInterfaceImplementer(            receiver,            makeInterfaceId.ERC1820('ERC777TokensRecipient')        ))    });});  
    describe(" 测试 ERC777 合约的方法 ", function () {    //send()    it(' 发送方法 : send()', async function () {        let receipt = await ERC777Instance.send(receiver, ether(amount), userData, { from: owner });        expectEvent(receipt, 'Sent', {            operator: owner,            from: owner,            to: receiver,            amount: ether(amount),            data: userData,            operatorData: null        });        expectEvent(receipt, 'Transfer', {            from: owner,            to: receiver,            value: ether(amount),        });    });    it(' 验证接收接口 : TokensRecipient()', async function () {        assert.equal(ERC777Instance.address, await TokensRecipientInstance.token(owner));        assert.equal(owner, await TokensRecipientInstance.operator(owner));        assert.equal(owner, await TokensRecipientInstance.from(owner));        assert.equal(receiver, await TokensRecipientInstance.to(owner));        assert.equal(ether(amount).toString(), (await TokensRecipientInstance.amount(owner)).toString());        assert.equal(userData, await TokensRecipientInstance.data(owner));        assert.equal(null, await TokensRecipientInstance.operatorData(owner));        assert.equal(ether((parseInt(initialSupply) - parseInt(amount)).toString()).toString(), (await TokensRecipientInstance.balanceOf(owner)).toString());        assert.equal(ether(amount), (await TokensRecipientInstance.balanceOf(receiver)).toString());    });});describe(" 测试发送和接收接口的拒绝方法 ", function () {    it(' 设置拒绝接收 : rejectTokens()', async function () {        await TokensRecipientInstance.rejectTokens({ from: receiver });    });    it(' 验证代币接收者拒绝接收 : transfer()', async function () {        await assert.rejects(ERC777Instance.transfer(receiver, ether(amount), { from: owner }), /Receive not allowed/);    });});  

在这个测试脚本中 , 我们首先通过 @openzeppelin/test-helpersawait singletons.ERC1820Registry(owner) 方法模拟出一个 ERC1820 注册表 . 之后布署了一个 ERC777 合约 , 在实际应用中如果你已经有了某个 ERC777 代币 , 则不需要这一步 , 这一步仅仅是为了测试而设置的 . 下一步为 receiver 账户布署了接收接口的合约 . 在合约布署之后 , 要向 ERC1820 合约为 receiver 账户注册接收接口合约的地址 , 通过 makeInterfaceId.ERC1820('ERC777TokensRecipient') 这个方法将 ERC777TokensRecipient 字符串取哈希值 , 这样 ERC1820 合约就知道了接口合约地址成为了 receiver 账户的 ERC777TokensRecipient 这个方法的接口 . 之后我们进行了转账的测试 ,ERC777 代币合约的 send 方法也要向 ERC1820 注册表合约查询 receiver 账户是否注册了 ERC777TokensRecipient 这个方法的接口合约地址 , 如果注册了 , 就必须要调用接口合约 以上就是实现了一个属于你自己的 ERC777 代币接收账本 .

欢迎关注 : 崔棉大师的花式发币法 [5]

References

[1] ERC777: https://learnblockchain.cn/docs/eips/eip-777.html
[2] ERC1820 注册表 : https://learnblockchain.cn/docs/eips/eip-1820.html
[3] ERC1820 的合约 : https://learnblockchain.cn/docs/eips/eip-1820.html#%E8%A7%84%E8%8C%83
[4] 崔棉大师的花式发币法 : https://github.com/Fankouzu/MintCoin
[5] 崔棉大师的花式发币法 : https://github.com/Fankouzu/MintCoin

给你的 ERC777 代币制作一个自己的专属账本

来源链接:mp.weixin.qq.com