Hardhat测试中Expect未捕获ERC20合约回滚原因问题
问题描述
我正在使用Hardhat框架测试一个ERC20合约,编写了如下测试用例:
it("testing with second", async () => { const amount = ethers.utils.parseEther("1"); await expect( ERC20Contract.connect(second).transferFrom( ERC20Contract.address, second, amount ) ).to.be.revertedWith("ERC20: insufficient allowance"); });
我期望合约回滚时返回错误信息ERC20: insufficient allowance,但实际收到如下错误:
Error: cannot estimate gas; transaction may fail or may require manual gas limit (error={"name":"ProviderError","_stack":"ProviderError: HttpProviderError\n at HttpProvider.request (/home/kadiemq/hh-tut/erc20-tut/node_modules/hardhat/src/internal/core/providers/http.ts:78:19)\n at LocalAccountsProvider.request (/home/kadiemq/hh-tut/erc20-tut/node_modules/hardhat/src/internal/core/providers/accounts.ts:187:34)\n at processTicksAndRejections (node:internal/process/task_queues:96:5)\n at async EthersProviderWrapper.send (/home/kadiemq/hh-tut/erc20-tut/node_modules/@nomiclabs/hardhat-ethers/src/internal/ethers-provider-wrapper.ts:13:20)","code":-32000,"_isProviderError":true,"data":{"stack":"RuntimeError: VM Exception while processing transaction: revert ERC20: insufficient allowance\n at Function.RuntimeError.fromResults (/tmp/.mount_ganachAQrIFW/resources/static/node/node_modules/ganache-core/lib/utils/runtimeerror.js:94:13)\n at module.exports (/tmp/.mount_ganachAQrIFW/resources/static/node/node_modules/ganache-core/lib/utils/gas/guestimation.js:142:32)","name":"RuntimeError"}}, tx={"data":"0x23b872dd000000000000000000000000624daf7e06c04e0ab541323b3d3e95b629745a6000000000000000000000000033757dfeda24de8dc2b46d348a035ad60bbc3a3f0000000000000000000000000000000000000000000000000de0b6b3a7640000","to":{},"from":"0x2c93fc47DC6aaF30CD5a6C47F59bD898842B0190","gasPrice":{"type":"BigNumber","hex":"0x04a817c800"},"type":0,"nonce":{},"gasLimit":{},"chainId":{}}, code=UNPREDICTABLE_GAS_LIMIT, version=abstract-signer/5.7.0)
虽然错误信息中包含ERC20: insufficient allowance,但expect函数并未捕获到该信息,导致测试失败。
相关代码
合约代码
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; contract OurToken is ERC20 { constructor(uint256 initialSupply) ERC20("OurToken", "OT") { _mint(address(this), initialSupply); _approve(address(this), msg.sender, initialSupply); } }
部署脚本
import { DeployFunction } from "hardhat-deploy/dist/types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { developmentChains, INITIAL_SUPPLY, networkConfig, } from "../helper-hardhat-config"; const DeployToken: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { const { getNamedAccounts, deployments, network } = hre; const { deploy, log } = deployments; const { deployer } = await getNamedAccounts(); const chainId = network.config.chainId!; const args = [INITIAL_SUPPLY]; const OurToken = await deploy("OurToken", { from: deployer, args: args, log: true, waitConfirmations: networkConfig[network.name].blockConfirmations || 1, }); }; export default DeployToken; DeployToken.tags = ["all", "token"];
完整测试代码
import { deployments, ethers, getNamedAccounts } from "hardhat"; import { ERC20 } from "../typechain-types"; import { assert, expect } from "chai"; describe("Testing", async () => { let deployer: string; let second: string; let ERC20Contract: ERC20; beforeEach(async () => { deployer = (await getNamedAccounts()).deployer; second = (await getNamedAccounts()).second; await deployments.fixture(["all"]); }); it("testing with deployer", async () => { ERC20Contract = await ethers.getContract("OurToken", deployer); const amount = ethers.utils.parseEther("1"); const previousBalance = await ERC20Contract.balanceOf(deployer); const tx = await ERC20Contract.transferFrom( ERC20Contract.address, deployer, amount ); tx.wait(1); const newBalance = await ERC20Contract.balanceOf(deployer); assert.equal( previousBalance.add(amount).toString(), newBalance.toString() ); }); it("testing with second", async () => { ERC20Contract = ERC20Contract.connect(second); const amount = ethers.utils.parseEther("1"); await expect( ERC20Contract.transferFrom(ERC20Contract.address, deployer, amount) ).to.be.revertedWith("ERC20: insufficient allowance"); }); });
问题原因及解决方案
问题根源
第二个测试用例中ERC20Contract变量未被正确初始化:
beforeEach钩子仅完成合约部署,未初始化ERC20Contract实例- 直接调用
ERC20Contract.connect(second)时,若单独运行该用例或beforeEach重置后变量为undefined,会触发连接失败,导致出现无法预估Gas的前置错误,而非预期的合约revert信息
修复方案
方案1:在beforeEach中统一初始化合约
修改beforeEach钩子,添加合约实例初始化:
beforeEach(async () => { deployer = (await getNamedAccounts()).deployer; second = (await getNamedAccounts()).second; await deployments.fixture(["all"]); // 初始化合约实例,默认关联deployer账号 ERC20Contract = await ethers.getContract("OurToken", deployer); });
修改第二个测试用例,单独创建second账号连接的合约实例:
it("testing with second", async () => { const secondConnectedContract = ERC20Contract.connect(second); const amount = ethers.utils.parseEther("1"); await expect( secondConnectedContract.transferFrom(ERC20Contract.address, deployer, amount) ).to.be.revertedWith("ERC20: insufficient allowance"); });
方案2:在测试用例中独立获取合约
直接在第二个测试用例中用second账号获取合约实例:
it("testing with second", async () => { const ERC20Contract = await ethers.getContract("OurToken", second); const amount = ethers.utils.parseEther("1"); await expect( ERC20Contract.transferFrom(ERC20Contract.address, deployer, amount) ).to.be.revertedWith("ERC20: insufficient allowance"); });
额外说明
修复后,expect就能正常捕获合约抛出的ERC20: insufficient allowance错误信息,测试可正常通过。
内容的提问来源于stack exchange,提问作者Kadiem Alqazzaz

