給你的ERC777代幣制作一個自己的專屬賬本

買賣虛擬貨幣
如果你持有一個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登錄檔是否同意成為賬戶的介面,同時還要實現指定的介面方法,例如tokensToSend和tokensReceived.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.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

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;