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

如果你持有一个ERC777[1]代币,那么你就可以利用ERC777代币中的钩子函数方法,给自己布署一个账本合约来记录自己的账户每一次接收到的代币数量和对方账户等信息
钩子函数
ERC777代币是ERC20代币合约的升级版,其中的最重要的升级功能就是每次进行转账的时候都会调用钩子函数,具体的方法我们可以在ERC777的代码中找到
//ERC777.sol
function _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注册表是否同意成为账户的接口,同时还要实现指定的接口方法,例如tokensToSend和tokensReceived.ERC1820注册表也不是只为这两个接口服务的,你也可以利用这个原理制作出其他有趣的智能合约.
所以制作一个接口合约我们要做的事情:
•拥有一个tokensReceived方法满足ERC777合约的调用
•拥有一个canImplementInterfaceForAddress方法告诉ERC1820注册表同意成为账户的接口
•调用ERC1820合约的setInterfaceImplementer方法为你的账户注册接口合约
下面我们来看代码:
//TokensRecipient.sol
pragma 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.sender的TOKENS_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-helpers的await 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