You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;保留监听时结果符合预期。

原因分析

  1. Chainlink价格喂料未做有效性校验:合约调用latestRoundData时,未验证返回数据的合法性(如是否为最新数据、价格是否为正)。当喂料返回 stale 或无效数据(如answer为0)时,convertEthAmountToUsdValue计算结果为0,不满足require条件,导致mint函数执行失败(revert),余额和总供应量保持为0。
  2. 事件监听的偶然生效:保留事件监听时,测试代码通过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合约模拟价格数据,避免依赖真实网络:

  1. 修改合约,允许构造函数传入喂料地址:
contract MyTokenERC20 is ERC20, ERC20Permit {
    AggregatorV3Interface internal dataFeed;

    constructor(address _dataFeed) ERC20("MyToken", "MTK") ERC20Permit("MyToken") {
        dataFeed = AggregatorV3Interface(_dataFeed);
    }

    // ... 其他代码不变
}
  1. 修改测试代码,部署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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 11:33:09