Truffle测试报“tx 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

