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