Sepolia测试中Solidity合约调用mint后余额未变更的问题排查
ERC20合约mint测试异常问题分析
合约代码(Solidity)
// SPDX-License-Identifier: MIT // Compatible with OpenZeppelin Contracts ^5.0.0 pragma solidity ^0.8.20; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import {AggregatorV3Interface} from "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol"; contract MyTokenERC20 is ERC20, ERC20Permit { AggregatorV3Interface internal dataFeed = AggregatorV3Interface(0x694AA1769357215DE4FAC081bf1f309aDC325306); constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {} event Test(string val); uint256 min_num = 10 * 10 ** 18; function mint() public payable { require( convertEthAmountToUsdValue(msg.value) >= 1 * 10 * 10 ** 18, "not usd enough" ); _mint(msg.sender, 10 * min_num); } function convertEthAmountToUsdValue( uint256 amountVal ) public view returns (uint256) { ( , /* uint80 roundID */ int answer /*uint startedAt*/ /*uint timeStamp*/ /*uint80 answeredInRound*/, , , ) = dataFeed.latestRoundData(); return amountVal * (uint256(answer) / 10 ** 8); } }
测试代码(Hardhat)
const { network, getNamedAccounts, deployments, ethers } = require("hardhat"); describe("MyToken test", function () { let deployer, myToken, MyTokenBurn; beforeEach(async function () { deployer = (await getNamedAccounts()).deployer; await deployments.fixture(["all"]); MyTokenBurn = await ethers.getContract("MyTokenERC20", deployer); }); describe("test", function () { it("test1", async function () { const BASE_FEE = ethers.parseEther("0.01"); const total = await MyTokenBurn.totalSupply(); console.log("total:" + total); const st = await MyTokenBurn.balanceOf(deployer); console.log("st:" + st); // 注释掉事件监听后结果异常 // await new Promise(async (resolve, reject) => { // MyTokenBurn.once('Transfer', async (index1, index2, index3) => { // console.log('index1:' + index1 + ", index2:" + index2 + ", index3:" + index3); // resolve(); // }); // await MyTokenBurn.mint({ value: BASE_FEE }); // }); await MyTokenBurn.mint({ value: BASE_FEE }); const totalEnd = await MyTokenBurn.totalSupply(); console.log("totalEnd:" + totalEnd); const end = await MyTokenBurn.balanceOf(deployer); console.log("end:" + end); }); }); });
问题现象
调用mint函数后,预期totalEnd和end应输出10 * 10 * 10 ** 18(即1e20),但移除Transfer事件监听代码后,输出变为0;保留监听时结果符合预期。
原因分析
- Chainlink价格喂料未做有效性校验:合约调用
latestRoundData时,未验证返回数据的合法性(如是否为最新数据、价格是否为正)。当喂料返回 stale 或无效数据(如answer为0)时,convertEthAmountToUsdValue计算结果为0,不满足require条件,导致mint函数执行失败(revert),余额和总供应量保持为0。 - 事件监听的偶然生效:保留事件监听时,测试代码通过Promise等待事件触发,相当于延长了交易执行前的等待时间,此时Chainlink喂料恰好返回有效数据,
mint正常执行。这是偶然现象,并非事件监听本身解决了问题。
解决方案
方案1:修复合约的价格校验与单位逻辑
修改convertEthAmountToUsdValue函数,添加喂料数据有效性校验,并修正单位计算错误:
function convertEthAmountToUsdValue( uint256 amountVal ) public view returns (uint256) { ( uint80 roundID, int answer, , uint timeStamp, uint80 answeredInRound ) = dataFeed.latestRoundData(); // 校验价格数据有效性 require(answeredInRound >= roundID, "Stale price data"); require(timeStamp != 0, "Incomplete round"); require(answer > 0, "Invalid price value"); uint256 ethUsdPrice = uint256(answer) / 10 ** 8; // 正确计算:将ETH wei转换为USD(以1e18为单位,1USD=1e18) return (amountVal * ethUsdPrice) / 10 ** 18; }
同时,合约的require条件可保持不变,修正后计算逻辑能准确反映ETH对应的USD价值。
方案2:调整测试用ETH金额
临时调高测试发送的ETH金额,确保即使价格波动也能满足require条件:
const BASE_FEE = ethers.parseEther("0.1"); // 从0.01ETH改为0.1ETH
方案3:本地测试模拟Chainlink喂料
如果在本地Hardhat环境测试,使用Chainlink Mock合约模拟价格数据,避免依赖真实网络:
- 修改合约,允许构造函数传入喂料地址:
contract MyTokenERC20 is ERC20, ERC20Permit { AggregatorV3Interface internal dataFeed; constructor(address _dataFeed) ERC20("MyToken", "MTK") ERC20Permit("MyToken") { dataFeed = AggregatorV3Interface(_dataFeed); } // ... 其他代码不变 }
- 修改测试代码,部署Mock喂料并传入合约:
const { ethers } = require("hardhat"); describe("MyToken test", function () { let deployer, MyTokenBurn, mockFeed; beforeEach(async function () { deployer = (await getNamedAccounts()).deployer; // 部署Mock价格喂料,设置ETH/USD价格为2000(8位小数) mockFeed = await ethers.deployContract("MockV3Aggregator", [8, ethers.parseUnits("2000", 8)]); // 部署MyTokenERC20合约,传入Mock喂料地址 MyTokenBurn = await ethers.deployContract("MyTokenERC20", [await mockFeed.getAddress()], { from: deployer }); }); // ... 测试逻辑不变 });
内容的提问来源于stack exchange,提问作者ingram
相关产品推荐
相关产品推荐

