foundry-rs/foundry

Anvil debug_traceTransaction works differently for transactions mined by anvil vs ones mined before

Closed this issue · 7 comments

Component

Anvil

Have you ensured that all of these are up to date?

  • Foundry
  • Foundryup

What version of Foundry are you on?

forge 0.2.0 (75fc63b 2024-12-05T00:23:28.354134759Z)

What command(s) is the bug in?

anvil --steps-tracing --fork-url --port

Operating System

Linux

Describe the bug

Preface

It seems debug_traceTransaction does not work as intended if the transaction being mined was mined with anvil vs mined before anvil started:

Dependencies and versions

I wrote a thorough test for it using ethers.js (6.13.1) node 20.18.0 npm 10.8.2 anvil 0.2.0 (75fc63b 2024-12-05T00:23:28.348102608Z)

Prerequisits

To run the code below you need to do:

  • npm install ethers
  • Fill privateKey and providerUrl in the code provided below
    • ensure providerUrl u set supports debug_traceTransaction
    • the private key you have should have some funds cause it actually send a tx to sepolia chain
  • look at other fields notable defaultGas which is default 50gwei and default amount in is 0.001 eth
  • run npx ts-node ./test.ts

What will the code do:

  • Create a buy fixed input on sepolia v2 router
  • sign the tx with your private key
  • send the tx to normal sepolia provider
  • wait for the normal tx to be mined
  • print fields for debug_traceTransaction on the normal provider on the mined normal tx
  • spawn anvil
  • print fields for debug_traceTransaction using anvil provider on the mined normal tx
  • Create another buy fixed input on sepolia v2 router
  • sign the tx with your private key
  • send the tx to anvil provider
  • wait for anvil tx to be mined
  • print fields for debug_traceTransaction on the anvil provider for the mined anvil tx

Output:

------------------------------------------------
---------------NORMAL_PROVIDER------------------
------------------NORMAL_TX---------------------
TX Sent. TX Hash xxxxxx
TX Mined
TX Succeeded
debug_traceTransaction fields using normal provider for normal TX "xxxxxx":
Result keys [ 'post', 'pre' ]
------------------------------------------------

Stay Tuned there will be around 20 seconds wait

------------------------------------------------
---------------ANVIL_PROVIDER------------------
------------------NORMAL_TX---------------------
debug_traceTransaction fields using anvil provider for normal TX "xxxxxx":
Result keys [ 'post', 'pre' ]
------------------ANVIL_TX---------------------
TX Sent. TX Hash yyyyyy
TX Mined
TX Succeeded
debug_traceTransaction fields using anvil provider for anvil TX "yyyyyy":
Result keys []
------------------------------------------------

NOTE:

Please ping me if you have problems running the code

THE CODE:

// test.ts
import { spawn } from 'child_process';
import { Contract, type JsonRpcApiProvider, Transaction, Wallet, WebSocketProvider } from 'ethers';

const providerUrl = 'ws://FILL_THIS_DONT_RUN_WITHOUT_FILLING_THIS_THIS_IS_VERY_IMPORTANT_TO_NOTE_DO_NOT_FORGET_FILLING_THIS_FIELD_PLEEEEEEEEEEEEEEEEEEEEEASE'; // Should set it to a websocket rpc node that supports debug_traceTransaction. I couldnt find one on chainlist https://chainlist.org/?chain=97&search=11155111&testnets=true so I left this empty
const chainId = 11155111; // using sepolia for tests

const privateKey =
  '0xFILL_THIS_DONT_RUN_WITHOUT_FILLING_THIS_THIS_IS_VERY_IMPORTANT_TO_NOTE_DO_NOT_FORGET_FILLING_THIS_FIELD_PLEEEEEEEEEEEEEEEEEEEEEASE';

const amountIn = 1_000_000_000_000_000n;

const v2RouterAddress = '0xC532a74256D3Db42D0Bf7a0400fEFDbad7694008';
const wethAddress = '0x7b79995e5f793A07Bc00c21412e50Ecae098E7f9';
const usdtAddress = '0xaA8E23Fb1079EA71e0a56F48a2aA51851D8433D0';

const defaultGas = 50_000_000_000n; // 50 gwei
const maxFeePerGas = defaultGas;
const maxPriorityFeePerGas = defaultGas;
const gasLimit = 250_000n; // should be enough

const signerWallet = new Wallet(privateKey);
const signingKey = signerWallet.signingKey;
const wallet = signerWallet.address;

const wait = (ms: number) => new Promise((res) => setTimeout(res, ms));

export const getBalanceChangeDueToTransaction = async (
  provider: JsonRpcApiProvider,
  transactionHash: string
): Promise<null | { [address: string]: { preBalance: bigint; postBalance: bigint } }> =>
  provider
    .send('debug_traceTransaction', [
      transactionHash,
      {
        tracer: 'prestateTracer',
        tracerConfig: {
          onlyTopCall: false,
          diffMode: true,
        },
      },
    ])
    .then((result) => {
      // console.log('Result', result);
      console.log('Result keys', Object.keys(result ?? {}));
      return result;
    })
    .catch((error) => {
      console.error('ERROR:', error);
      throw error;
    });

(async () => {
  const normalProvider = new WebSocketProvider(providerUrl, {
    name: 'eth',
    chainId,
  });

  const v2Router = new Contract(v2RouterAddress, [
    {
      inputs: [
        { internalType: 'uint256', name: 'amountOutMin', type: 'uint256' },
        { internalType: 'address[]', name: 'path', type: 'address[]' },
        { internalType: 'address', name: 'to', type: 'address' },
        { internalType: 'uint256', name: 'deadline', type: 'uint256' },
      ],
      name: 'swapExactETHForTokensSupportingFeeOnTransferTokens',
      outputs: [],
      stateMutability: 'payable',
      type: 'function',
    },
  ]);

  const contractTx =
    await v2Router.swapExactETHForTokensSupportingFeeOnTransferTokens.populateTransaction(
      0n, // min amount out = 0 => 100% slippage
      [wethAddress, usdtAddress], // buyPath
      wallet, // deposit to = wallet
      Date.now() + 600 * 1000, // deadline
      {
        from: wallet,
        value: amountIn,
      }
    );
  contractTx.maxFeePerGas = maxFeePerGas;
  contractTx.maxPriorityFeePerGas = maxPriorityFeePerGas;
  contractTx.type = 2;
  contractTx.gasLimit = gasLimit;
  contractTx.nonce = await normalProvider.getTransactionCount(wallet);
  delete contractTx.from;
  contractTx.to = v2RouterAddress;

  const normalTx = Transaction.from(contractTx);
  normalTx.chainId = chainId;
  normalTx.signature = signingKey.sign(normalTx.unsignedHash);

  const normalRawTx = normalTx.serialized;
  const normalTxHash = normalTx.hash!;

  console.log('------------------------------------------------');
  console.log('---------------NORMAL_PROVIDER------------------');
  console.log('------------------NORMAL_TX---------------------');
  await normalProvider.broadcastTransaction(normalRawTx);

  console.log('TX Sent. TX Hash', normalTxHash);

  await normalProvider.waitForTransaction(normalTxHash, 1, 30_000);

  console.log('TX Mined');

  if ((await normalProvider.getTransactionReceipt(normalTxHash))?.status !== 1) {
    console.log('TX Failed');
    throw new Error('Transaction failed on chain');
  }

  console.log('TX Succeeded');

  console.log(
    `debug_traceTransaction fields using normal provider for normal TX "${normalTxHash}":`
  );
  await getBalanceChangeDueToTransaction(normalProvider, normalTxHash);
  console.log('------------------------------------------------');

  console.log('\nStay Tuned there will be around 20 seconds wait\n');

  await wait(15000);

  const anvilProcess = spawn('anvil', ['--steps-tracing', '--fork-url', providerUrl]);

  await wait(1000);

  const anvilProvider = new WebSocketProvider('ws://127.0.0.1:8545', {
    name: 'eth',
    chainId,
  });

  await wait(1000);

  await anvilProvider.send('evm_setAutomine', [true]);

  console.log('------------------------------------------------');
  console.log('---------------ANVIL_PROVIDER------------------');
  console.log('------------------NORMAL_TX---------------------');
  console.log(
    `debug_traceTransaction fields using anvil provider for normal TX "${normalTxHash}":`
  );
  await getBalanceChangeDueToTransaction(anvilProvider, normalTxHash);

  console.log('------------------ANVIL_TX---------------------');
  contractTx.nonce = await anvilProvider.getTransactionCount(wallet);

  const anvilTx = Transaction.from(contractTx);
  anvilTx.chainId = chainId;
  anvilTx.signature = signingKey.sign(anvilTx.unsignedHash);

  const anvilTxRawTx = anvilTx.serialized;
  const anvilTxHash = anvilTx.hash!;

  await anvilProvider.broadcastTransaction(anvilTxRawTx);

  console.log('TX Sent. TX Hash', anvilTxHash);

  await anvilProvider
    .waitForTransaction(anvilTxHash, 1, 1_000)
    .catch(() => anvilProvider.getTransactionReceipt(anvilTxHash));

  console.log('TX Mined');

  if ((await anvilProvider.getTransactionReceipt(anvilTxHash))?.status !== 1) {
    console.log('TX Failed');
    throw new Error('Transaction failed on anvil');
  }

  console.log('TX Succeeded');

  console.log(`debug_traceTransaction fields using anvil provider for anvil TX "${anvilTxHash}":`);
  await getBalanceChangeDueToTransaction(anvilProvider, anvilTxHash);
  console.log('------------------------------------------------');

  await anvilProvider.destroy();
  anvilProcess.kill('SIGTERM');
  await normalProvider.destroy();
})();

Related Issue:

#8678

@maa105 thank you, will give it a try, would it be possible to create a gh repo with the test driver, could save time if we have a repo to clone, npm install and then run (as https://github.com/mshakeg/anvil-backtester within #7039 and https://github.com/vlad-blana/foundry-anvil-lock-repro in #7275)

@maa105 thank you, will give it a try, would it be possible to create a gh repo with the test driver, could save time if we have a repo to clone, npm install and then run (as https://github.com/mshakeg/anvil-backtester within #7039 and https://github.com/vlad-blana/foundry-anvil-lock-repro in #7275)

Sure will do

Done run:

git clone git@github.com:maa105/AnvilDebugTraceTransactionIssue.git
cd AnvilDebugTraceTransactionIssue
npm start

or one liner:

git clone git@github.com:maa105/AnvilDebugTraceTransactionIssue.git&&cd AnvilDebugTraceTransactionIssue&&npm start

thank you, that's awesome. will check!

@maa105 which provider you use, I don't have debug_traceTransaction for Sepolia available with infura nor alchemy providers

ah, looking at the code, I see what's the issue here is that you're using the prestateTracer https://github.com/maa105/AnvilDebugTraceTransactionIssue/blob/master/index.ts#L35 which is not supported in Anvil (e.g. if you change this line to tracer: "callTracer" you will get the result keys). this is a dupe of #8443 which we're going to add support in post v1. Thank you!

maa105 commented

@maa105 which provider you use, I don't have debug_traceTransaction for Sepolia available with infura nor alchemy providers

I have my own private node running sorry cant help there :|

ah, looking at the code, I see what's the issue here is that you're using the prestateTracer https://github.com/maa105/AnvilDebugTraceTransactionIssue/blob/master/index.ts#L35 which is not supported in Anvil (e.g. if you change this line to tracer: "callTracer" you will get the result keys). this is a dupe of #8443 which we're going to add support in post v1. Thank you!

Alright I'll give it a shot when I have time and report back. Thanks for your time man