如何使用ERC20代币实现买、卖功能并完成Dapp部署

1、目标

本文主要实现创建ERC20代币合约,以及实现通过BNB进行买卖功能,用户通过BNB购买您所创建的代币,以及卖出代币换回BNB
文章中使用OpenZeppelin快速搭建ERC20智能合约。

2、基础知识

2.1 ERC20
2.1.1 什么是ERC20?

ERC-20简单说就是一种代币的标准。
ERC-20引入了Fungible Tokens的标准,换句话说,它们具有使每个Token与另一个Token完全相同(在类型和值上)的属性。例如,一个ERC-20代币的行为就像ETH或BNB,这意味着1个代币将永远等于所有其他代币。
如果您想了解有关ERC-20代币的更多信息,可以查看链接:EIP-20代币标准
2.2 OpenZeppelin
2.2.1 什么是 OpenZeppelin?
OpenZeppelin提供一整套安全产品,用于构建、管理和检查以太坊项目软件开发和运营的各个方面。包括模块化、可重用、安全的智能合约库,使用Solidity编写。
2.2.2 OpenZeppelin官网
https://www.openzeppelin.com/
2.3 OpenZeppelin 4.6.0安装

$ npm install @openzeppelin/contracts@4.6.0
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'hardhat@2.9.6',
npm WARN EBADENGINE   required: { node: '^12.0.0 || ^14.0.0 || ^16.0.0' },
npm WARN EBADENGINE   current: { node: 'v18.2.0', npm: '8.9.0' }
npm WARN EBADENGINE }

added 1 package in 3s

50 packages are looking for funding

2.4 Hardhat
2.4.1 什么是Hardhat
Hardhat是一个用于编译、部署、测试和调试以太坊软件的开发环境。它可以帮助开发人员管理和自动化构建智能合约和dApp过程中固有的重复性任务,并围绕此工作流程轻松引入更多功能。这意味着在核心上编译、运行和测试智能合约。

Hardhat内置于 Hardhat Network,这是一个专为开发而设计的本地以太坊网络。它的功能侧重于Solidity调试,具有堆栈跟踪console.log()和事务失败时的显式错误消息。

Hardhat Runner是与Hardhat交互的CLI命令,是一个可扩展的任务运行器。它是围绕任务和插件的概念设计的。每次您从CLI运行Hardhat时,您都在运行一项任务。例如npx hardhat compile正在运行内置compile任务。任务可以调用其他任务,允许定义复杂的工作流。用户和插件可以覆盖现有任务,使这些工作流程可定制和可扩展。

Hardhat的许多功能都来自插件,作为开发人员,您可以自由选择要使用的插件。Hardhat对您最终使用的工具没有意见,但它确实带有一些内置的默认值。所有这些都可以被覆盖。
2.4.2 Hardhat 官网
https://hardhat.org/

2.4.3 Hardhat 2.9.6的安装

$ npm install --save-dev hardhat@2.9.6
npm WARN EBADENGINE Unsupported engine {
npm WARN EBADENGINE   package: 'hardhat@2.9.6',
npm WARN EBADENGINE   required: { node: '^12.0.0 || ^14.0.0 || ^16.0.0' },
npm WARN EBADENGINE   current: { node: 'v16.5.0', npm: '8.9.0' }
npm WARN EBADENGINE }

added 180 packages in 33s

默认情况下,Hardhat将始终在启动时启动Hardhat Network的内存实例。也可以以独立方式运行Hardhat Network,以便外部客户端可以连接到它。
要以这种方式运行Hardhat Network,请运行npx hardhat node:

$ npx hardhat node
You are using a version of Node.js that is not supported by Hardhat, and it may work incorrectly, or not work at all.

Please, make sure you are using a supported version of Node.js.

To learn more about which versions of Node.js are supported go to https://hardhat.org/nodejs-versions
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Account #1: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

Account #2: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a

Account #3: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 ETH)
Private Key: 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6

Account #4: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000 ETH)
Private Key: 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a

Account #5: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000 ETH)
Private Key: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba

Account #6: 0x976EA74026E726554dB657fA54763abd0C3a0aa9 (10000 ETH)
Private Key: 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e

Account #7: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 (10000 ETH)
Private Key: 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356

Account #8: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f (10000 ETH)
Private Key: 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97

Account #9: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 (10000 ETH)
Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

Account #10: 0xBcd4042DE499D14e55001CcbB24a551F3b954096 (10000 ETH)
Private Key: 0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897

Account #11: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788 (10000 ETH)
Private Key: 0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82

Account #12: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a (10000 ETH)
Private Key: 0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1

Account #13: 0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec (10000 ETH)
Private Key: 0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd

Account #14: 0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097 (10000 ETH)
Private Key: 0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa

Account #15: 0xcd3B766CCDd6AE721141F452C550Ca635964ce71 (10000 ETH)
Private Key: 0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61

Account #16: 0x2546BcD3c84621e976D8185a91A922aE77ECEc30 (10000 ETH)
Private Key: 0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0

Account #17: 0xbDA5747bFD65F08deb54cb465eB87D40e51B197E (10000 ETH)
Private Key: 0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd

Account #18: 0xdD2FD4581271e230360230F9337D5c0430Bf44C0 (10000 ETH)
Private Key: 0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0

Account #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH)
Private Key: 0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

这里公开一个JSON-RPC接口。要使用它,可以将您的钱包或应用程序连接到http://127.0.0.1:8545
如果您想将Hardhat连接到此节点以针对它运行部署脚本,您只需使用--network localhost.
要尝试此操作,请使用以下选项启动一个节点npx hardhat node并重新运行示例脚本

$ npx hardhat run scripts/sample-script.js --network localhost

3 创建ERC20合约&部署
3.1 创建ERC20合约
在这里我们将创建一个继承自OpenZepllein的ERC20合约的代币合约。
在构造函数中,我们会创建1000个MSHK Token。
在Solidity中,ERC20代币有18位小数,并将它们发送到msg.sender(合约创建者的地址)
下面很简单的几行代码,我们就可以完成一个合约的部署,是不是很酷!

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// 了解有关 ERC20 实施的更多信息
// 在 OpenZeppelin 文档上:https://docs.openzeppelin.com/contracts/4.x/erc20
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract ERC20MSHKToken is ERC20 {
    constructor() ERC20("MSHK ERC20 Token", "MSHK") {
        // 向合约创建者发送 1000 个有18位小数的代币
        _mint(msg.sender, 1000 * 10 ** 18); // 总量 1000个
    }
}

上面的代码中我们从OpenZeppelin库中导入ERC20.sol合约。该合约是ERC20标准的OpenZeppelin实现,他们在安全性和优化方面都做得非常出色。
当constructor构造函数被调用时,我们也在调用ERC20构造函数并传递两个参数。第一个是name我们的Token,第二个是symbol.
其中_mint方法的代码来自ERC20.sol中,部分代码如下:

/** @dev Creates `amount` tokens and assigns them to `account`, increasing
 * the total supply.
 *
 * Emits a {Transfer} event with `from` set to the zero address.
 *
 * Requirements:
 *
 * - `account` cannot be the zero address.
 */
function _mint(address account, uint256 amount) internal virtual {
    require(account != address(0), "ERC20: mint to the zero address");

    _beforeTokenTransfer(address(0), account, amount);

    _totalSupply += amount;
    _balances[account] += amount;
    emit Transfer(address(0), account, amount);

    _afterTokenTransfer(address(0), account, amount);
}

其中_beforeTokenTransfer_afterTokenTransfer是调用方法前、后钩子方法。
上面代码中首先更新_totalSupply的代币总量(在我们的例子中,是1000个带有18位小数的代币),同时设置balance列表中当前帐号的总量,然后我们再发出一个Transfer事件。
3.2 创建ERC20 Vendor合约
在这部分练习中,我们将创建一个ERC20MSHKTokenVendor.sol合约。

这部分合约主要负责允许用户用 BNB 兑换我们的代币。为了做到这一点,我们需要:

  • 为我们的代币设置价格(1 BNB Token = 100 MSHK Token)
  • 实现支付buyToken()功能。
  • 发出一个BuyTokens事件,记录谁是买家、发送的 BNB 数量和购买的 MSHK Token 数量
  • 在部署时将所有 MSHK Token 转移到 Vendor 合约
  • 将 Vendor 合约的 Ownership 进行变更,方便以后对 Vendor 合约有操作权限

ERC20MSHKTokenVendor.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./ERC20MSHKToken.sol";

// Learn more about the ERC20 implementation 
// on OpenZeppelin docs: https://docs.openzeppelin.com/contracts/4.x/api/access#Ownable
import "@openzeppelin/contracts/access/Ownable.sol";

contract ERC20MSHKTokenVendor is Ownable {

  // Our Token Contract
  ERC20MSHKToken mshkToken;

  // token price for ETH
  uint256 public tokensPerEth = 100;

  // 定义买卖事件
  event BuyTokens(address buyer, uint256 amountOfETH, uint256 amountOfTokens);
  event SellTokens(address seller, uint256 amountOfTokens, uint256 amountOfETH);

  constructor(address tokenAddress) {
    //创建 ERC20合约实例
    mshkToken = ERC20MSHKToken(tokenAddress);
  }

  /**
  * 允许用户使用BNB购买 Token
  */
  function buyTokens() public payable returns (uint256 tokenAmount) {
    // 发送的数量必须大于0
    require(msg.value > 0, "Send ETH to buy some tokens");

    // 计算后的代币买入数量
    uint256 amountToBuy = msg.value * tokensPerEth;

    // 检查合约中的代币是否足够
    // address(this) 合约实例的地址
    // msg.sender 合约调用的地址
    // 以上两个概念要区分开,参考: https://docs.soliditylang.org/en/develop/units-and-global-variables.html
    uint256 vendorBalance = mshkToken.balanceOf(address(this));
    require(vendorBalance >= amountToBuy, "Vendor contract has not enough tokens in its balance");

    // 向合约的调用者发送代币 
    (bool sent) = mshkToken.transfer(msg.sender, amountToBuy);
    require(sent, "Failed to transfer token to user");

    // 注册事件
    emit BuyTokens(msg.sender, msg.value, amountToBuy);

    return amountToBuy;
  }

  /**
  * 允许用户卖出 Token 换回 BNB
  */
  function sellTokens(uint256 tokenAmountToSell) public {
    // 检查数量是否大于0
    require(tokenAmountToSell > 0, "Specify an amount of token greater than zero");

    // 检测调用合约者的代币是否足够
    uint256 userBalance = mshkToken.balanceOf(msg.sender);
    require(userBalance >= tokenAmountToSell, "Your balance is lower than the amount of tokens you want to sell");

    // 检查该合约中的ETH余额是否足够
    uint256 amountOfETHToTransfer = tokenAmountToSell / tokensPerEth;
    uint256 ownerETHBalance = address(this).balance;
    require(ownerETHBalance >= amountOfETHToTransfer, "Vendor has not enough funds to accept the sell request");

    // 从合约调用者向合约发送代币
    (bool sent) = mshkToken.transferFrom(msg.sender, address(this), tokenAmountToSell);
    require(sent, "Failed to transfer tokens from user to vendor");

    // 向合约调用者发送指定的 BNB
    (sent,) = msg.sender.call{value: amountOfETHToTransfer}("");
    require(sent, "Failed to send ETH to the user");

    // 注册事件
    emit SellTokens(msg.sender, tokenAmountToSell, amountOfETHToTransfer);

  }

  /**
  * 允许我们转出所有的BNB,测试时使用
  */
  function withdraw() public onlyOwner {
    uint256 ownerBalance = address(this).balance;
    require(ownerBalance > 0, "Owner has not balance to withdraw");

    // 将合约中的全部 BNB 转出到调用者,且只能是 owner
    (bool sent,) = msg.sender.call{value: address(this).balance}("");
    require(sent, "Failed to send user balance back to the owner");
  }
}

注意交易的方法使用了payable关键字,允许接收主网链上的代币。
合约部署在ETH Chain上接收的是ETH,部署到BNB Chain上接收的是BNB
3.2.1 BuyToken介绍
buyTokens 方法主要做以下操作:

  • 对用户传入的 BNB 做检查是否合法
  • 根据接收的 BNB 数量计算可以给用户多少 MSHK Token
  • 并查 Vendor 合约的 MSHK Token 余额是否足够
  • 触发 transfer 事件,向用户发送 MSHK Token,返回一个 bool 用于判断转帐是否成功
  • 发出 BuyTokens 事件以通知区块链我们达成了交易

3.2.2 SellToken介绍
当用户购买了MSHK Token以后,我们也应该允许用户卖出MSHK Token换回他们的BNB。
sellTokens的方法声明中,同样也使用了payable关键字,允许接收ETH或BNB。

sellTokens方法主要做以下操作:

  • 对用户传入的 Token 做检查是否合法
  • 判断用户的 Token 是否足够卖出
  • 根据接收的 Token 比例,计算需要给用户多少 BNB.判断 Vendor 合约中的 BNB 是否足够支付给用户
  • 调用 transferFrom 方法接收用户传入的 Token 转移到 Vendor 合约钱包
  • Vendor 合约向用户的钱包转移等量的 BNB
  • 发出 SellTokens 事件以通知区块链我们达成了交易

3.2.3 Withdraw介绍
withdraw方法非常简单。它依赖于onlyOwner function modifier继承自Ownable合约。该修饰符检查msg.sender是合同的所有者。我们不希望其他用户提取我们收集的BNB。在函数内部我们将BNB转移给所有者并检查操作是否成功。
最后需要注意智能合约定义了两个特别定义的事件,当用户被授予从帐户中提取代币的权利时,以及代币实际转移后,这些事件将被调用或发出:

event Approval(address indexed tokenOwner, address indexed spender,
 uint tokens);
event Transfer(address indexed from, address indexed to,
 uint tokens);
  1. 当用户买入MSHK Token后,应该调用approve方法,设置用户最大的消费MSHK Token数量
  2. 当调用transferFrom方法后,会从最大消费Token数量中扣除

ERC20MSHKTokenVendor.sol代码中的注释比较完整,就不做更多详细介绍。
3.3 编写测试文件
测试是应用安全和优化的重要基础。您永远不应该跳过它们,它们是理解整个应用程序逻辑中涉及的操作流程的一种方式。

测试我们主要在hardhat中进行,主要用到ethers和chai两个代码库。

测试完整的代码如下,文件位置hardhat/test/MSHKContractTest.js:

// https://docs.ethers.io/v5/
const { ethers } = require("hardhat");

// https://www.chaijs.com/
// Chai 是一个 BDD / TDD 断言库,适用于节点和浏览器,可以与任何 javascript 测试框架完美搭配
const {use, expect} = require('chai');

describe('Test dApp', () => {
    let owner;
    let addr1;
    let addr2;
    let addrs;

    let vendorContract;
    let tokenContract;
    let tokenFactory;

    let vendorTokensSupply;
    let tokensPerEth;

    // 每个测试执行前,运行的通用方法
    beforeEach(async () => {
        // 获取帐号列表
        [owner, addr1, addr2, ...addrs] = await ethers.getSigners();

        // console.log("\towner:", owner.address);
        // console.log("\taddr1:", addr1.address);
        // console.log("\taddr2:", addr2.address);
        // console.log("\taddrs:", addrs.length);
        // Deploy ExampleExternalContract contract
        // YourTokenFactory = await ethers.getContractFactory('YourToken');
        // mshk = await YourTokenFactory.deploy();

        // // Deploy Staker Contract
        // const VendorContract = await ethers.getContractFactory('Vendor');
        // mshkVendor = await VendorContract.deploy(mshk.address);

        tokenFactory = await hre.ethers.getContractFactory("ERC20MSHKToken");
        tokenContract = await tokenFactory.deploy();

        const VendorContract = await hre.ethers.getContractFactory("ERC20MSHKTokenVendor");
        vendorContract = await VendorContract.deploy(tokenContract.address);

        // 向交易合约转帐 1000 个代币,所有代币
        // parseUnits("1.0");
        // { BigNumber: "1000000000000000000" }
        await tokenContract.transfer(vendorContract.address, ethers.utils.parseEther('1000'));

        // 设置 合约所有者
        await vendorContract.transferOwnership(owner.address);

        // 合约代币总量 
        vendorTokensSupply = await tokenContract.balanceOf(vendorContract.address);

        // 获取 代币替换比例
        tokensPerEth = await vendorContract.tokensPerEth();

        // console.log("\ttokenContract deployed to:", tokenContract.address);
        // console.log("\tvendorContract deployed to:", vendorContract.address);
        // console.log('\tvendorContract余额[%s]:%s',vendorContract.address,vendorTokensSupply);
    });


    describe('Test buyTokens() method', () => {
        it('buyTokens 测试没有发送 ETH 代币', async () => {
            const amount = ethers.utils.parseEther('0'); // 测试 0 个代币

            // 使用 connect 方法,连接到 addr1 帐号测试是否可以购买合约
            // 使用 revertedWith 匹配是否包含指定消息
            await expect(
                vendorContract.connect(addr1).buyTokens({
                    value: amount,
                }),
            ).to.be.revertedWith('Send ETH to buy some tokens');
        });

        it('buyTokens 测试没有有足够的 Token 可供购买', async () => {
            const amount = ethers.utils.parseEther('11'); // 发送大于1
            await expect(
                vendorContract.connect(addr1).buyTokens({
                    value: amount,
                }),
            ).to.be.revertedWith('Vendor contract has not enough tokens in its balance');
        });

        it('buyTokens 购买成功!', async () => {
            const buyAmount = 1
            const amount = ethers.utils.parseEther(buyAmount.toString());

            // 测试购买代币 ,并发送事件
            // https://ethereum-waffle.readthedocs.io/en/latest/matchers.html#emitting-events
            await expect(
                vendorContract.connect(addr1).buyTokens({
                    value: amount,
                }),
            )
                .to.emit(vendorContract, 'BuyTokens')   // 发送事件
                .withArgs(addr1.address, amount, amount.mul(tokensPerEth)); // 发送事件参数

            // 验证 addr1 的余额和数量是否一致
            const userTokenBalance = await tokenContract.balanceOf(addr1.address);
            const userTokenAmount = ethers.utils.parseEther((buyAmount * tokensPerEth).toString());
            expect(userTokenBalance).to.equal(userTokenAmount);

            // 验证合约中的余额是否 900
            const vendorTokenBalance = await tokenContract.balanceOf(vendorContract.address);
            expect(vendorTokenBalance).to.equal(vendorTokensSupply.sub(userTokenAmount));

            // 查看合约中是否有 1 ETH
            // https://docs.ethers.io/v5/api/providers/provider/
            const vendorBalance = await ethers.provider.getBalance(vendorContract.address);
            expect(vendorBalance).to.equal(amount);
        });
    });

    describe('Test withdraw() method', () => {
        it('转帐帐号是否为合约拥有者', async () => {
            await expect(vendorContract.connect(addr1).withdraw()).to.be.revertedWith('Ownable: caller is not the owner');
        });

        it('不有足够的余额可转出', async () => {
            await expect(vendorContract.connect(owner).withdraw()).to.be.revertedWith('Owner has not balance to withdraw');
        });

        it('withdraw 转出所有ETH成功', async () => {
            const ethOfTokenToBuy = ethers.utils.parseEther('1');

            // 买入 Token
            await vendorContract.connect(addr1).buyTokens({
                value: ethOfTokenToBuy,
            });

            // withdraw operation
            const txWithdraw = await vendorContract.connect(owner).withdraw();

            // Check that the Vendor's balance has 0 eth
            const vendorBalance = await ethers.provider.getBalance(vendorContract.address);
            expect(vendorBalance).to.equal(0);

            // 测试交易是否改变账户余额 为 1 eth
            await expect(txWithdraw).to.changeEtherBalance(owner, ethOfTokenToBuy);
        });
    });

    describe('Test sellTokens() method', () => {
        it('测试卖出代币为0', async () => {
            const amountToSell = ethers.utils.parseEther('0');
            await expect(vendorContract.connect(addr1).sellTokens(amountToSell)).to.be.revertedWith(
                'Specify an amount of token greater than zero',
            );
        });

        it('测试没有足够的代币卖出', async () => {
            const amountToSell = ethers.utils.parseEther('1');
            await expect(vendorContract.connect(addr1).sellTokens(amountToSell)).to.be.revertedWith(
                'Your balance is lower than the amount of tokens you want to sell',
            );
        });

        it('测试 owner 没有足够的ETH供卖出代币', async () => {
            // User 1 buy
            const ethOfTokenToBuy = ethers.utils.parseEther('1');

            // 使用 add1 买入 1 ether 的代币
            await vendorContract.connect(addr1).buyTokens({
                value: ethOfTokenToBuy,
            });

            // 将所有 ETH 转出
            await vendorContract.connect(owner).withdraw();

            const amountToSell = ethers.utils.parseEther('100');
            await expect(vendorContract.connect(addr1).sellTokens(amountToSell)).to.be.revertedWith(
                'Vendor has not enough funds to accept the sell request',
            );
        });

        it('买入代币,未设置可花费代币是否有异常', async () => {
            // User 1 buy
            const ethOfTokenToBuy = ethers.utils.parseEther('1');

            // 使用 add1 买入 1 ether 的代币
            await vendorContract.connect(addr1).buyTokens({
                value: ethOfTokenToBuy,
            });

            const amountToSell = ethers.utils.parseEther('100');

            await expect(vendorContract.connect(addr1).sellTokens(amountToSell)).to.be.revertedWith(
                'ERC20: insufficient allowance',
            );
        });

        it('买、卖代币以及余额测试', async () => {
            // addr1 buy 1 ETH of tokens
            const ethOfTokenToBuy = ethers.utils.parseEther('1');

            // 使用 add1 买入 1 ether 的代币
            await vendorContract.connect(addr1).buyTokens({
                value: ethOfTokenToBuy,
            });

            // 设置 addr1 可拥有 vendor 合约的数量 为 1 ETH 比例的代币数量
            const amountToSell = ethers.utils.parseEther('100');
            await tokenContract.connect(addr1).approve(vendorContract.address, amountToSell);

            // 获取 addr1 中可花费的代币数量
            const vendorAllowance = await tokenContract.allowance(addr1.address, vendorContract.address);
            // 检查 vendor 合约是否有足够的代币可以出售
            expect(vendorAllowance).to.equal(amountToSell);

            // 卖出 代币
            const sellTx = await vendorContract.connect(addr1).sellTokens(amountToSell);

            // 获取 vendor 持有的代币数量
            const vendorTokenBalance = await tokenContract.balanceOf(vendorContract.address);
            // 检查卖出后的代币数量是否还是 1000
            expect(vendorTokenBalance).to.equal(ethers.utils.parseEther('1000'));

            // 检查 addr1 的代币数量是否为0
            const userTokenBalance = await tokenContract.balanceOf(addr1.address);
            expect(userTokenBalance).to.equal(0);

            // Check that the user's ETH balance is 1
            const userEthBalance = ethers.utils.parseEther('1');
            await expect(sellTx).to.changeEtherBalance(addr1, userEthBalance);
        });
    });

});

代码中的注释比较完整,就不做详细介绍。通过下面的命令,查看测试效果:

# 进入 hardhat 目录
$ cd hardhat

# 编译合约
$ npx hardhat compile
Compiled 7 Solidity files successfully

# 对测试文件中的代码进行测试
# npx hardhat test


  Test dApp
    Test buyTokens() method
      ✔ buyTokens 测试没有发送 ETH 代币 (54ms)
      ✔ buyTokens 测试没有有足够的 Token 可供购买
      ✔ buyTokens 购买成功! (46ms)
    Test withdraw() method
      ✔ 转帐帐号是否为合约拥有者
      ✔ 不有足够的余额可转出
      ✔ withdraw 转出所有ETH成功
    Test sellTokens() method
      ✔ 测试卖出代币为0
      ✔ 测试没有足够的代币卖出
      ✔ 测试 owner 没有足够的ETH供卖出代币 (42ms)
      ✔ 买入代币,未设置可花费代币是否有异常
      ✔ 买、卖代币以及余额测试 (67ms)


  11 passing (3s)

3.4 部署合约
如果你和我一样,上面的测试全是 ✔ 代表测试通过,说明我们的测试覆盖了每一个边缘情况,接下来我们可以测试将程序部署到Hardhot测试网络上。

部署到其他测试网络原理一样,可以参考链接:https://hardhat.org/guides/deploying

新开一个终端,执行命令npx hardhat node开启一个本地节点:

$ npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

Account #1: 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 (10000 ETH)
Private Key: 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d

Account #2: 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC (10000 ETH)
Private Key: 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a

Account #3: 0x90F79bf6EB2c4f870365E785982E1f101E93b906 (10000 ETH)
Private Key: 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6

Account #4: 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65 (10000 ETH)
Private Key: 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a

Account #5: 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc (10000 ETH)
Private Key: 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba

Account #6: 0x976EA74026E726554dB657fA54763abd0C3a0aa9 (10000 ETH)
Private Key: 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e

Account #7: 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955 (10000 ETH)
Private Key: 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356

Account #8: 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f (10000 ETH)
Private Key: 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97

Account #9: 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720 (10000 ETH)
Private Key: 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6

Account #10: 0xBcd4042DE499D14e55001CcbB24a551F3b954096 (10000 ETH)
Private Key: 0xf214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897

Account #11: 0x71bE63f3384f5fb98995898A86B02Fb2426c5788 (10000 ETH)
Private Key: 0x701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82

Account #12: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a (10000 ETH)
Private Key: 0xa267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1

Account #13: 0x1CBd3b2770909D4e10f157cABC84C7264073C9Ec (10000 ETH)
Private Key: 0x47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd

Account #14: 0xdF3e18d64BC6A983f673Ab319CCaE4f1a57C7097 (10000 ETH)
Private Key: 0xc526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa

Account #15: 0xcd3B766CCDd6AE721141F452C550Ca635964ce71 (10000 ETH)
Private Key: 0x8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61

Account #16: 0x2546BcD3c84621e976D8185a91A922aE77ECEc30 (10000 ETH)
Private Key: 0xea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0

Account #17: 0xbDA5747bFD65F08deb54cb465eB87D40e51B197E (10000 ETH)
Private Key: 0x689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd

Account #18: 0xdD2FD4581271e230360230F9337D5c0430Bf44C0 (10000 ETH)
Private Key: 0xde9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0

Account #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH)
Private Key: 0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

打开一个新的终端,运行命令 npx hardhat run scripts/deploy.js –network localhost 在 localhost 网络中部署智能合约:

$ npx hardhat run scripts/deploy.js --network localhost
MSHKToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
MSHKTokenVendor deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512

  
此时在之前的节点终点终端中,可以看到合约创建的输出:

...
...
Account #19: 0x8626f6940E2eb28930eFb4CeF49B2d1F2C9C1199 (10000 ETH)
Private Key: 0xdf57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e

WARNING: These accounts, and their private keys, are publicly known.
Any funds sent to them on Mainnet or any other live network WILL BE LOST.

web3_clientVersion
eth_chainId
eth_accounts
eth_blockNumber
eth_chainId (2)
eth_estimateGas
eth_getBlockByNumber
eth_feeHistory
eth_sendTransaction
  Contract deployment: ERC20MSHKToken
  Contract address:    0x5fbdb2315678afecb367f032d93f642f64180aa3
  Transaction:         0x3186c85eacb01eb0cbcd5e2ae090fbb83bc4db50998cc15b1f6552d7efe4b12b
  From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  Value:               0 ETH
  Gas used:            1169491 of 1169491
  Block #1:            0x135b0986c8b9d4609714bf006f3483e169b4e5653473c44f8ec60734ec02fe53

eth_chainId
eth_getTransactionByHash
eth_accounts
eth_chainId
eth_estimateGas
eth_feeHistory
eth_sendTransaction
  Contract deployment: ERC20MSHKTokenVendor
  Contract address:    0xe7f1725e7734ce288f8367e1bb143e90bb3f0512
  Transaction:         0x90fe1443a5f4c8eb4eaeb66b41fb05992be786ff50d109ecc23e8e2592cd2f7a
  From:                0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
  Value:               0 ETH
  Gas used:            1306973 of 1306973
  Block #2:            0x594369832a040a37c3ad82dd26813bf85619232991f2ca8606dc36e2905e4494

eth_chainId
eth_getTransactionByHash
eth_chainId
eth_getTransactionReceipt
eth_chainId
eth_getTransactionReceipt

可以看到合约部署终端中的输出合约地址MSHKToken deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3/MSHKTokenVendor deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512与节点终端中输出的合约地址Contract address是一致的,这说明我们部署功能了。

4、部署前端 Dapp 程序

在Github的代码库中nuxt-app目录中,运行npm run dev,然后浏览http://localhost:3000可以看到效果,修改前端代码中有以下几点需要注意:

  • 如果你在主网上部署,你应该在 Etherscan/Bscscan 上验证你的合约。此过程将增加您的应用程序的可信度和信任度。
  • 关闭调试模式(它会打印大量的 console.log,这是您不想在 Chrome 开发人员控制台中看到的)。打开 nuxt-app/plugins/main.js,找到 this.Debug = true; 更改为 this.Debug = false;
  • 确保您的 Vue 应用程序指向正确的网络(您刚刚用于部署合同的网络)。打开 nuxt-app/store/StateAccount.js,修改tokenContractAddress和vendorContractAddress 为正确的合约地址,其他不要修改。


GitHub代码:GitHub.com

5、参考文章

OpenZeppelin 文档
Solidity 文档
Ethers 文档
Hardhat 使用指南

给TA打赏
共{{data.count}}人
人已打赏
区块链

批量转账 如何将ERC20代币在一次交易中发送到多个地址?

2022-9-8 23:48:37

区块链

Vue+NodeJs在线获取BSC链上代币最新价格

2022-9-9 0:28:29

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
有新私信 私信列表
搜索