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