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

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

目录

[TOC]

1、目标
2、基础知识
2.1、ERC20
2.1.1、什么是 ERC20?
2.2、OpenZeppelin
2.2.1、 什么是 OpenZeppelin?
2.2.2、 OpenZeppelin 官网
2.3、OpenZeppelin 4.6.0 安装
2.4、Hardhat
2.4.1、什么是 Hardhat
2.4.2、Hardhat 官网
2.4.3、Hardhat 2.9.6 的安装
3、创建 ERC20 合约&部署
3.1、创建 ERC20 合约
3.2、创建 ERC20 Vendor 合约
3.2.1、BuyToken 介绍
3.2.2、SellToken 介绍
3.2.3、Withdraw 介绍
3.3、编写测试文件
3.4、部署合约
4、部署前端 Dapp 程序
5、参考文章

1、目标

  本文主要实现创建 ERC20 代币合约,以及实现通过 BNB 进行买卖功能,用户通过 BNB 购买您所创建的代币,以及卖出代币换回 BNB

  文章中使用 OpenZeppelin 快速搭建 ERC20 智能合约。

开始写实例时准备部署到ETH主网,在写文章的时候主网的Gas费用单个合约部署需要$150+,后面改成部署到BSC主网。不知道哪一年会降下来。

2、基础知识

2.1、ERC20

2.1.1、什么是 ERC20?

mshk.top
  ERC-20 简单说就是一种代币的标准。

  ERC-20 引入了 Fungible Tokens 的标准,换句话说,它们具有使每个 Token 与另一个 Token 完全相同(在类型和值上)的属性。例如,一个 ERC-20 代币的行为就像 ETHBNB,这意味着 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 合约

  在这里我们将创建一个继承自 OpenZeplleinERC20 合约的代币合约。
在构造函数中,我们会创建 1000 个 MSHK Token

Solidity 中, ERC20 代币有 18 位小数,并将它们发送到msg.sender(合约创建者的地址)

  下面很简单的几行代码,我们就可以完成一个合约的部署,是不是很酷!
  ERC20MSHKToken.sol:

// 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关键字,允许接收 ETHBNB

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);
  • 当用户买入 MSHK Token 后,应该调用 approve 方法,设置用户最大的消费 MSHK Token 数量
  • 当调用 transferFrom 方法后,会从最大消费 Token 数量中扣除

  
  ERC20MSHKTokenVendor.sol 代码中的注释比较完整,就不做更多详细介绍。

3.3、编写测试文件

  测试是应用安全和优化的重要基础。您永远不应该跳过它们,它们是理解整个应用程序逻辑中涉及的操作流程的一种方式。

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

  测试完整的代码如下,文件位置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 localhostlocalhost 网络中部署智能合约:

$ 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,修改tokenContractAddressvendorContractAddress 为正确的合约地址,其他不要修改。
  
  mshk.top

  在线Demo:http://cointool.a246.vip/zh-CN/ContractTrade
  GitHub代码:https://github.com/idoall/nuxt-chain-contracts-trade

# 5、参考文章
  OpenZeppelin 文档
  Solidity 文档
  Ethers 文档
  Hardhat 使用指南
  
  


博文作者:迦壹
博客地址:如何使用ERC20代币实现买、卖功能并完成Dapp部署
转载声明:可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明,谢谢合作!


发表回复

您的电子邮箱地址不会被公开。