搭建一个可众筹的ERC777代币

假设有这样一个需求:你为了实现一个伟大的理想,需要筹集100个ETH作为启动资金,所以你发行了一个ERC777代币作为凭证,同时布署了一个众筹合约,你的支持者可以通过众筹合约向你购买代币,兑换比例为1ETH:100ERC777

分析

大家都知道,ERC777向后兼容ERC20,ERC20的接口方法在ERC777中同样适用. 所以在Openzeppelin的众筹合约中,通过buyTokens()逻辑进行购买代币的操作,buyTokens()方法又会触发一个_deliverTokens()的内部方法进行转账.来看一下代码:

function _deliverTokens(address beneficiary, uint256 tokenAmount) internal {
    _token.safeTransfer(beneficiary, tokenAmount);
}

代码中的_token就是众筹所发售的代币,而safeTransfer()方法是一个套用在token身上的library

//Crowdsale.sol
using SafeERC20 for IERC20;
IERC20 private _token;

在SafeERC20.sol的library中可以找到safeTransfer的实现方法:

//SafeERC20.sol
function safeTransfer(IERC20 token, address to, uint256 value) internal {
    callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}

所以从代码中可以看到,众筹合约还是通过token.transfer方法进行购买代币后的转账的.所以我们如果使用ERC777进行众筹,肯定是可以兼容的.

注意,下面的例子中使用Openzeppelin的AllowanceCrowdsale众筹方法,前面讲的_deliverTokens()方法中将会被重写,转而使用token.transferFrom方法

ERC777合约

请先初始化truffle环境和安装了openzeppelin

ERC777合约代码

$ vim contracts/ERC777Token.sol
pragma solidity ^0.5.0;

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

contract ERC777Token is ERC777{
    constructor(
        string memory name, //代币名称
        string memory symbol, //代币缩写
        uint256 initialSupply, //发行总量
        address[] memory defaultOperators //默认操作员数组
    ) public ERC777(name, symbol, defaultOperators) {
        _mint(msg.sender, msg.sender, initialSupply, "", "");
    }
}

众筹合约

我们使用Openzeppelin提供的AllowanceCrowdsale众筹合约为演示,这个合约的功能是众筹的代币将可以从一个发行者账户中发送给购买者账户,所以在布署合约之后,需要发行者将全部代币批准给众筹合约.

合约代码

$ vim contracts/AllowanceCrowdsale.sol
pragma solidity ^0.5.0;

import "@openzeppelin/contracts/crowdsale/Crowdsale.sol";
import "@openzeppelin/contracts/crowdsale/emission/AllowanceCrowdsale.sol";

contract AllowanceCrowdsaleContract is Crowdsale, AllowanceCrowdsale {
    constructor(
        uint256 rate,           // 兑换比例
        address payable wallet, // 接收ETH受益人地址
        IERC20 token,           // 代币地址
        address tokenWallet     // 代币从这个地址发送
    )
        AllowanceCrowdsale(tokenWallet)
        Crowdsale(rate, wallet, token)
        public{}
}

编译合约

$ truffle compile

布署合约

布署脚本

$ vim migrations/2_deploy_ERC777Token.js
const ERC777Token = artifacts.require("ERC777Token");
const AllowanceCrowdsaleContract = artifacts.require("AllowanceCrowdsaleContract");
const { singletons } = require('@openzeppelin/test-helpers');

module.exports = async (deployer, network, accounts) => {
    await singletons.ERC1820Registry(accounts[0]);//布署ERC1820注册表(仅测试环境)
    const totalSupply = web3.utils.toWei('10000');
    const defaultOperators = accounts[1];
    const param = [
        "My ERC777 Token",   //代币名称
        "M7T",              //代币缩写
        totalSupply,        //发行总量
        [defaultOperators]    //默认操作员[可选],或传入空数组
    ]
    const ERC777TokenInstance = await deployer.deploy(ERC777Token, ...param);
    const crowdsaleParam = [  
        100,                        //兑换比例1ETH:100ERC777
        accounts[1],                //接收ETH受益人地址
        ERC777TokenInstance.address,//代币地址
        accounts[0]                 //代币从这个地址发送
    ]
    const AllowanceCrowdsaleInstance = await deployer.deploy(AllowanceCrowdsaleContract, ...crowdsaleParam);
    //在布署之后必须将发送者账户中的代币批准给众筹合约
    ERC777TokenInstance.approve(AllowanceCrowdsaleInstance.address, web3.utils.toWei(totalSupply.toString(),'ether'));
};

在测试环境中布署脚本中必须引用@openzeppelin/test-helpers测试助手,使用它来模拟一个ERC1820注册表,因为ERC777必须向ERC1820注册表注册自己的接口方法;在正式网或Ropsten测试网布署时需要取消掉.

布署合约

请确保一个测试节点在运行

$ truffle migrate

测试合约

编写测试脚本

$ vim test/ERC777Crowdsale.js
const assert = require('assert');
const { contract, accounts, web3 } = require('@openzeppelin/test-environment');
const { ether, makeInterfaceId, constants, singletons, expectEvent } = require('@openzeppelin/test-helpers');
const CrowdsaleContract = contract.fromArtifact("AllowanceCrowdsaleContract");
const ERC777Token = contract.fromArtifact("ERC777Token");

const totalSupply = '10000';//发行总量
const [owner, sender, receiver, purchaser, beneficiary] = accounts;
const EthValue = '1';
const rate = '100';
const TokenValue = (EthValue * rate).toString();
const defaultOperators = [sender];
const userData = web3.utils.toHex('A gift');

describe("布署合约", function () {
    it('实例化ERC1820注册表', async function () {
        ERC1820RegistryInstance = await singletons.ERC1820Registry(owner);
    });
    it('布署ERC777代币合约', async function () {
        ERC777Param = [
            "My ERC777 Token",   //代币名称
            "M7T",              //代币缩写
            ether(totalSupply),      //发行总量
            defaultOperators    //默认操作员
        ];
        ERC777Instance = await ERC777Token.new(...ERC777Param, { from: owner });
    });
    it('布署众筹合约', async function () {
        CrowdsaleParam = [
            rate,                                       //兑换比例1ETH:100ERC777
            beneficiary,                                //接收ETH受益人地址
            ERC777Instance.address,                     //代币地址
            owner,                                      //代币从这个地址发送
        ]
        CrowdsaleInstance = await CrowdsaleContract.new(...CrowdsaleParam, { from: owner });
    });
});
describe("布署后首先执行", function () {
    it('将代币批准给众筹合约', async function () {
        await ERC777Instance.approve(CrowdsaleInstance.address, ether(totalSupply), { from: owner });
    });
});
describe("测试ERC777合约基本信息", function () {
    it('代币名称: name()', async function () {
        assert.equal(ERC777Param[0], await ERC777Instance.name());
    });
    it('代币缩写: symbol()', async function () {
        assert.equal(ERC777Param[1], await ERC777Instance.symbol());
    });
    it('代币精度: decimals()', async function () {
        assert.equal('18', (await ERC777Instance.decimals()).toString());
    });
    it('代币总量: totalSupply()', async function () {
        assert.equal(ERC777Param[2].toString(), (await ERC777Instance.totalSupply()).toString());
    });
    it('代币最小单位: granularity()', async function () {
        assert.equal('1', (await ERC777Instance.granularity()).toString());
    });
});
describe("测试通用的众筹方法", function () {
    it('ERC777代币地址: token()', async function () {
        assert.equal(ERC777Instance.address, await CrowdsaleInstance.token());
    });
    it('ETH受益人地址: wallet()', async function () {
        assert.equal(beneficiary, await CrowdsaleInstance.wallet());
    });
    it('兑换比例: rate()', async function () {
        assert.equal(rate, await CrowdsaleInstance.rate());
    });
    it('代币现存地址: tokenWallet()', async function () {
        assert.equal(owner, await CrowdsaleInstance.tokenWallet());
    });

    it('测试购买代币方法: buyTokens()', async function () {
        let receipt = await CrowdsaleInstance.buyTokens(sender, { value: ether(EthValue), from: sender });
        expectEvent(receipt, 'TokensPurchased', {
            purchaser: sender,
            beneficiary: sender,
            value: ether(EthValue),
            amount: ether(TokenValue)
        });
    });
    it('测试众筹收入: weiRaised()', async function () {
        assert.equal(ether(EthValue).toString(), (await CrowdsaleInstance.weiRaised()).toString());
    });

    it('购买者账户余额为: balanceOf()', async function () {
        assert.equal(ether(TokenValue).toString(), (await ERC777Instance.balanceOf(sender)).toString());
    });
    it('测试剩余代币数量: remainingTokens()', async function () {
        assert.equal(ether((parseInt(totalSupply) - parseInt(TokenValue)).toString()).toString(), (await CrowdsaleInstance.remainingTokens()).toString());
    });
});

ERC777代币还可以为自己设置一个发布和收币的钩子合约,每当发送和接收方法被触发时都会调用,具体方法请看我的Github

本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

  • 发表于 2020-05-07 15:14
  • 阅读 ( 565 )
  • 学分 ( 194 )
  • 分类:智能合约

2 条评论

请先 登录 后评论
崔棉大师
崔棉大师

4 篇文章, 553 学分