Hardhat测试ERC20合约转账报余额不足错误排查
问题描述
正在学习Solidity编写简易ERC20代币合约(包含转账、余额更新基础功能),运行转账功能测试用例时抛出如下报错:
npx hardhat test No need to generate any newer typings. MyERC20Contract when I transfer 10 tokens 1) sould transfer tokens correctly 0 passing (728ms) 1 failing 1) MyERC20Contract when I transfer 10 tokens sould transfer tokens correctly: Error: VM Exception while processing transaction: reverted with reason string 'ERC20: transfer amount exceeds balance' at ERC20._transfer (contracts/ERC20.sol:49) at ERC20.transfer (contracts/ERC20.sol:25) at async HardhatNode._mineBlockWithPendingTxs (node_modules/hardhat/src/internal/hardhat-network/provider/node.ts:1773:23) at async HardhatNode.mineBlock (node_modules/hardhat/src/internal/hardhat-network/provider/node.ts:466:16) at async EthModule._sendTransactionAndReturnHash (node_modules/hardhat/src/internal/hardhat-network/provider/modules/eth.ts:1504:18) at async HardhatNetworkProvider.request (node_modules/hardhat/src/internal/hardhat-network/provider/provider.ts:118:18) at async EthersProviderWrapper.send (node_modules/@nomiclabs/hardhat-ethers/src/internal/ethers-provider-wrapper.ts:13:20)
涉及代码
测试文件代码
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; import { expect } from "chai"; import { ethers } from "hardhat"; import { ERC20 } from "../typechain"; describe("MyERC20Contract", function() { let MyERC20Contract: ERC20; let someAddress: SignerWithAddress; let someOtherAddress: SignerWithAddress; beforeEach(async function() { const ERC20ContractFactory = await ethers.getContractFactory("ERC20"); MyERC20Contract = await ERC20ContractFactory.deploy("Hello","SYM"); await MyERC20Contract.deployed(); someAddress = (await ethers.getSigners())[1]; someOtherAddress = (await ethers.getSigners())[2]; }); describe("When I have 10 tokens", function() { beforeEach(async function() { await MyERC20Contract.transfer(someAddress.address, 10); }); }); describe("when I transfer 10 tokens", function() { it("sould transfer tokens correctly", async function() { await MyERC20Contract .connect(someAddress) .transfer(someOtherAddress.address, 10); expect( await MyERC20Contract.balanceOf(someOtherAddress.address) ).to.equal(10); }); }); });
Solidity合约代码
//SPDX-License-Identifier: Unlicense: MIT pragma solidity ^0.8.0; contract ERC20 { uint256 public totalSupply; string public name; string public symbol; mapping (address => uint256) public balanceOf; mapping (address => mapping (address => uint256)) public allowance; constructor(string memory name_, string memory symbol_) { name = name_; symbol = symbol_; _mint(msg.sender, 100e18); } function decimals() external pure returns (uint8) { return 18; } function transfer(address recipient, uint256 amount) external returns (bool) { return _transfer(msg.sender, recipient, amount); } function transferFrom(address sender, address recipient, uint256 amount) external returns (bool) { uint256 currentAllowance = allowance[sender][msg.sender]; require(currentAllowance >= amount, "ERC20: Transfer amount exceeds allowance" ) ; allowance[sender][msg.sender] = currentAllowance - amount; return _transfer(sender, recipient, amount); } function approve(address spender, uint256 amount) external returns (bool) { allowance[msg.sender][spender] = amount; return true; } function _transfer(address sender, address recipient, uint256 amount) private returns (bool) { require(recipient != address(0), "ERC20: transfer to zero address"); uint256 senderBalance = balanceOf[sender]; require(senderBalance >= amount, "ERC20: transfer amount exceeds balance"); balanceOf[sender] = senderBalance - amount; balanceOf[recipient] += amount; return true; } function _mint(address to, uint256 amount) internal { require(recipient != address(0), "ERC20: transfer to zero address"); totalSupply += amount; balanceOf[to] +=amount; } }
问题根因
代码共存在3处直接导致测试失败的问题,另有2处规范/拼写问题:
- 测试块结构逻辑错误:Mocha框架的
beforeEach钩子仅对同块级、嵌套子块内的测试用例生效。你给someAddress转10代币的前置逻辑写在独立的describe("When I have 10 tokens")块下,和后续转账测试块为平级关系,转账测试运行前不会执行给someAddress转币的操作,someAddress初始余额为0,发起转账自然触发余额不足回滚。 - 合约
_mint函数语法错误:函数定义的接收地址参数为to,零地址校验时错误引用了不存在的变量recipient,代码无法正常编译。 - 代币单位不匹配:合约设置小数位为18,部署时给部署者铸造的是
100e18基础单位的代币(对应100个整币),但测试中转账、断言使用的金额都是无小数的10,仅对应10个最小单位(即0.000...001个整币),不符合转10个整币的测试预期。 - 规范问题:SPDX许可证声明写法错误,正确写法应为
// SPDX-License-Identifier: MIT。 - 拼写问题:测试用例描述中
sould为拼写错误,正确应为should。
修复方案
测试代码修复
- 调整测试块嵌套结构,将转账测试块放到"When I have 10 tokens"块内部,确保前置转币逻辑正常执行。
- 所有涉及代币金额的位置统一使用18位小数的标准单位,可直接调用
ethers.utils.parseEther("10")表示10个整币。
修复后的测试核心代码如下:
describe("MyERC20Contract", function() { let MyERC20Contract: ERC20; let someAddress: SignerWithAddress; let someOtherAddress: SignerWithAddress; beforeEach(async function() { const ERC20ContractFactory = await ethers.getContractFactory("ERC20"); MyERC20Contract = await ERC20ContractFactory.deploy("Hello","SYM"); await MyERC20Contract.deployed(); someAddress = (await ethers.getSigners())[1]; someOtherAddress = (await ethers.getSigners())[2]; }); describe("When I have 10 tokens", function() { beforeEach(async function() { // 转10个标准单位的代币 await MyERC20Contract.transfer(someAddress.address, ethers.utils.parseEther("10")); }); // 嵌套转账测试块,继承前置逻辑 describe("when I transfer 10 tokens", function() { it("should transfer tokens correctly", async function() { await MyERC20Contract .connect(someAddress) .transfer(someOtherAddress.address, ethers.utils.parseEther("10")); expect( await MyERC20Contract.balanceOf(someOtherAddress.address) ).to.equal(ethers.utils.parseEther("10")); }); }); }); });
合约代码修复
- 修正SPDX头写法。
- 修改
_mint函数的零地址校验逻辑,将错误的recipient变量替换为参数to,同时修正错误提示信息。
修复后的_mint函数代码如下:
// 修正SPDX头 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; // ... 其他合约代码不变 function _mint(address to, uint256 amount) internal { require(to != address(0), "ERC20: mint to zero address"); totalSupply += amount; balanceOf[to] += amount; }
完成以上修改后重新运行测试即可正常通过。
内容的提问来源于stack exchange,提问作者PinPiguin
相关产品推荐
相关产品推荐

