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

Truffle测试报“tx nonce不正确”错误,求原因与解决方案

问题:Truffle测试中Nonce不一致导致交易失败

场景说明

我正在开发去中心化交易所应用,为核心合约编写测试用例时遇到问题。我创建了多个Mock ERC20代币用于测试(示例代码如下),希望在每个测试执行前完成代币铸造,但测试始终在approve调用处失败。

Mock ERC20代币示例(Dai合约)

//SPDX-License-Identifier: MIT

pragma solidity 0.8.17;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Dai is ERC20 {
    constructor() ERC20("Dai Stablecoin", "DAI") {}

    function faucet(address to, uint256 amount) external {
        _mint(to, amount);
    }
}

测试代码

const expectRevert = require('@openzeppelin/test-helpers/src/expectRevert');

const Dai = artifacts.require('mocks/Dai.sol');
const Bat = artifacts.require('mocks/Bat.sol');
const Rep = artifacts.require('mocks/Rep.sol');
const Zrx = artifacts.require('mocks/Zrx.sol');
const Dex = artifacts.require('Dex.sol');

contract('Dex', (accounts) => {
  let tokenList;
  let dai, bat, rep, zrx, dex;

  const [trader1, trader2] = [accounts[1], accounts[2]];
  const [DAI, BAT, REP, ZRX] = ['DAI', 'BAT', 'REP', 'ZRX'].map((ticker) =>
    web3.utils.fromAscii(ticker)
  );

  beforeEach(async () => {
    tokenList = await Promise.all([Dai.new(), Bat.new(), Rep.new(), Zrx.new()]);
    [dai, bat, rep, zrx] = tokenList;

    dex = await Dex.new();

    await Promise.all([
      dex.addToken(DAI, dai.address),
      dex.addToken(BAT, bat.address),
      dex.addToken(REP, rep.address),
      dex.addToken(ZRX, zrx.address),
    ]);

    const amount = web3.utils.toWei('1000');
    const seedTokenBalance = async (token, trader) => {
      await token.faucet(trader, amount);
      await token.approve(dex.address, amount, {
        from: trader,
      });
    };

    await Promise.all(
      tokenList.map((token) => seedTokenBalance(token, trader1))
    );
    await Promise.all(
      tokenList.map((token) => seedTokenBalance(token, trader2))
    );
  });

  describe('deposit', () => {
    it('should deposit token', async () => {
      const amount = web3.utils.toWei('100');

      await dex.deposit(amount, DAI, {
        from: trader1,
      });

      const balance = await dex.traderBalances(trader1, DAI);

      assert(balance.toString() === amount);
    });

    it('should NOT deposit tokens if token does not exist', async () => {
      await expectRevert(
        dex.deposit(
          web3.utils.toWei('100'),
          web3.utils.fromAscii('TOKEN-DOES-NOT-EXIST'),
          { from: trader1 }
        ),
        'Token does not exist.'
      );
    });
  });
});

报错位置

测试在以下代码处失败:

await token.approve(dex.address, amount, {
        from: trader,
      });

报错信息

Error: Returned error: VM Exception while processing transaction: the tx doesn't have the correct nonce. account has nonce of: 1 tx has nonce of: 0 (vm hf=london -> block -> tx) -- Reason given: Custom error (could not decode).

truffle-config.js配置

// require('dotenv').config();
// const { MNEMONIC, PROJECT_ID } = process.env;

// const HDWalletProvider = require('@truffle/hdwallet-provider');

module.exports = {
  networks: {
  },

  // Set default mocha options here, use special reporters, etc.
  mocha: {
    // timeout: 100000
  },

  // Configure your compilers
  compilers: {
    solc: {
      version: '0.8.17', // Fetch exact version from solc-bin (default: truffle's version)
    },
  },
};

解答

错误含义解释

Nonce是以太坊账户的交易计数器,核心规则:

  • 账户的nonce代表该账户已确认的交易数量,初始值为0,每完成一笔交易nonce自动加1
  • 提交交易时必须使用与账户当前nonce匹配的值,若使用的nonce小于账户当前nonce,说明该nonce对应的交易已被处理,会触发此错误

你的测试中,问题根源是并行执行的Promise.all导致同一账户的交易nonce冲突:对同一个trader账户同时发起多笔faucet和approve交易时,Web3/Truffle没有正确管理nonce,多笔交易使用了相同的nonce值。其中一笔交易确认后账户nonce递增,另一笔使用旧nonce的交易就会失败。

修复方法

核心是避免同一账户的交易并行执行,将同一账户的代币操作改为串行处理:

方案1:完全串行处理同一账户的所有代币操作

修改beforeEach中的代币播种逻辑,把Promise.all改为for...of循环串行执行:

// 替换原来的两个Promise.all
// 先处理trader1的所有代币
for (const token of tokenList) {
  await seedTokenBalance(token, trader1);
}
// 再处理trader2的所有代币
for (const token of tokenList) {
  await seedTokenBalance(token, trader2);
}

方案2:账户间并行,账户内串行

如果想保留不同账户间的并行效率,可将同一账户的操作封装为串行任务,再让不同账户的任务并行:

// trader1和trader2的任务并行,但各自内部的代币操作串行
await Promise.all([
  (async () => {
    for (const token of tokenList) {
      await seedTokenBalance(token, trader1);
    }
  })(),
  (async () => {
    for (const token of tokenList) {
      await seedTokenBalance(token, trader2);
    }
  })()
]);

补充说明

Truffle测试环境在处理同一账户的并行交易时,不会自动等待前一笔交易确认再发送下一笔,因此会出现nonce重复问题。改为串行执行后,每一笔交易都会等待上一笔完成,账户nonce会正确递增,彻底避免冲突。


内容的提问来源于stack exchange,提问作者Karen

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 08:50:26