Docs

debug_traceCall - Polygon RPC Method

Trace a call without creating a transaction on Polygon. Simulate transactions, debug contract interactions, and analyze gas usage with full execution traces.

Traces a call on Polygon without creating a transaction on-chain. This is a dry-run trace — it executes the call in the EVM at a specified block and returns detailed execution traces including opcodes, internal calls, and state changes, without any on-chain side effects.

Why Polygon? Build on the most adopted Ethereum scaling solution with 45,000+ dApps and enterprise partnerships with $4B+ TVL, sub-$0.01 transactions, 8M+ daily transactions, and zkEVM for enhanced security.

Archive Node Required

This method requires an archive node with debug APIs enabled when tracing against historical blocks. For "latest" or "pending" blocks, a full node with debug APIs may suffice. Dwellir provides archive node access for Polygon — ensure your plan includes debug namespace support.

When to Use This Method

debug_traceCall is powerful for enterprise developers, gaming studios, and teams building high-throughput applications:

  • Simulating Transactions Before Sending — Preview the full execution trace of a transaction before committing it on-chain, catching reverts and unexpected behavior before spending gas on Polygon
  • Debugging Contract Interactions — Step through contract execution at the opcode level to understand complex interactions, delegate calls, and proxy patterns for enterprise solutions (Starbucks, Disney, Reddit), gaming, DeFi, and stablecoin payments
  • Gas Estimation With Trace Details — Go beyond eth_estimateGas by seeing exactly which opcodes and internal calls consume gas, enabling targeted optimization
  • Security Analysis — Analyze how a contract would execute a specific call, detecting reentrancy, unexpected state modifications, and access control issues

Code Examples

Common Use Cases

1. Pre-Flight Transaction Simulation

Test a transaction before sending it on Polygon to catch reverts and estimate costs:

JavaScript
async function simulateTransaction(provider, txParams) {
  // Use callTracer to see the full call tree
  const trace = await provider.send('debug_traceCall', [
    {
      from: txParams.from,
      to: txParams.to,
      data: txParams.data,
      value: txParams.value || '0x0',
      gas: txParams.gasLimit || '0x1e8480' // 2M gas default
    },
    'latest',
    { tracer: 'callTracer' }
  ]);

  const gasUsed = parseInt(trace.gasUsed, 16);

  if (trace.error) {
    console.error('Transaction would revert!');
    console.error(`  Error: ${trace.error}`);
    console.error(`  Reason: ${trace.revertReason || 'unknown'}`);
    console.error(`  Gas wasted: ${gasUsed}`);
    return { success: false, error: trace.error, revertReason: trace.revertReason, gasUsed };
  }

  // Analyze internal calls for unexpected behavior
  const allCalls = flattenCalls(trace);
  const delegateCalls = allCalls.filter(c => c.type === 'DELEGATECALL');
  const creates = allCalls.filter(c => c.type === 'CREATE' || c.type === 'CREATE2');

  console.log('Simulation results:');
  console.log(`  Gas used: ${gasUsed}`);
  console.log(`  Internal calls: ${allCalls.length}`);
  console.log(`  Delegate calls: ${delegateCalls.length}`);
  console.log(`  Contract creations: ${creates.length}`);

  return { success: true, gasUsed, trace };
}

function flattenCalls(trace) {
  const calls = [trace];
  for (const sub of trace.calls || []) {
    calls.push(...flattenCalls(sub));
  }
  return calls;
}

2. Gas Optimization Analysis

Identify the most expensive opcodes in a contract call on Polygon:

JavaScript
async function analyzeGasHotspots(provider, callObj) {
  // Use default opcode tracer for step-by-step gas analysis
  const trace = await provider.send('debug_traceCall', [
    callObj,
    'latest',
    { disableStorage: false, enableReturnData: true }
  ]);

  const opcodeGas = {};

  for (const log of trace.structLogs) {
    if (!opcodeGas[log.op]) {
      opcodeGas[log.op] = { count: 0, totalGas: 0 };
    }
    opcodeGas[log.op].count++;
    opcodeGas[log.op].totalGas += log.gasCost;
  }

  // Sort by total gas cost
  const sorted = Object.entries(opcodeGas)
    .map(([op, stats]) => ({ op, ...stats, avgGas: Math.round(stats.totalGas / stats.count) }))
    .sort((a, b) => b.totalGas - a.totalGas);

  console.log('Gas hotspots:');
  console.log('Op'.padEnd(15), 'Count'.padStart(8), 'Total Gas'.padStart(12), 'Avg Gas'.padStart(10));
  for (const entry of sorted.slice(0, 10)) {
    console.log(
      entry.op.padEnd(15),
      String(entry.count).padStart(8),
      String(entry.totalGas).padStart(12),
      String(entry.avgGas).padStart(10)
    );
  }

  // Identify SSTORE/SLOAD hotspots (most expensive storage operations)
  const storageOps = trace.structLogs.filter(
    log => log.op === 'SSTORE' || log.op === 'SLOAD'
  );
  console.log(`\nStorage operations: ${storageOps.length} (${storageOps.filter(s => s.op === 'SSTORE').length} writes)`);

  return { opcodeGas: sorted, totalSteps: trace.structLogs.length, totalGas: trace.gas };
}

3. Security Analysis of Contract Interactions

Detect potentially dangerous patterns when calling a contract on Polygon:

Python
import requests

def security_trace_call(call_object, block='latest'):
    response = requests.post('https://api-polygon-mainnet-full.n.dwellir.com/YOUR_API_KEY', json={
        'jsonrpc': '2.0',
        'method': 'debug_traceCall',
        'params': [call_object, block, {'tracer': 'callTracer'}],
        'id': 1
    })
    trace = response.json()['result']

    warnings = []
    all_calls = flatten_calls(trace)

    for call in all_calls:
        # Detect unexpected delegate calls
        if call['type'] == 'DELEGATECALL':
            warnings.append(f'DELEGATECALL to {call["to"]} — could modify caller storage')

        # Detect value transfers to unexpected addresses
        value = int(call.get('value', '0x0'), 16)
        if value > 0 and call['to'] != call_object.get('to', '').lower():
            warnings.append(
                f'Value transfer of {value} wei to unexpected address {call["to"]}'
            )

        # Detect selfdestruct (CALL with no input to EOA after value)
        if call.get('error'):
            warnings.append(f'Internal revert at {call["to"]}: {call["error"]}')

    if trace.get('error'):
        print(f'TOP-LEVEL REVERT: {trace["error"]}')
        if trace.get('revertReason'):
            print(f'  Reason: {trace["revertReason"]}')
    else:
        gas_used = int(trace['gasUsed'], 16)
        print(f'Call succeeded: {gas_used} gas used')

    if warnings:
        print(f'\nSecurity warnings ({len(warnings)}):')
        for w in warnings:
            print(f'  - {w}')
    else:
        print('No security warnings detected')

    return {'success': not trace.get('error'), 'warnings': warnings}

def flatten_calls(trace):
    calls = [trace]
    for sub in trace.get('calls', []):
        calls.extend(flatten_calls(sub))
    return calls

# Example: analyze a token approval
security_trace_call({
    'from': '0x1234567890abcdef1234567890abcdef12345678',
    'to': '0x8D97689C9818892B700e27F316cc3E41e17fBeb9',
    'data': '0x095ea7b3000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcdffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
})

Error Handling

Common errors and solutions:

Error CodeDescriptionSolution
-32000Execution revertedThe call would revert — check the revertReason in the trace result for details
-32000Missing trie nodeHistorical block state not available — use an archive node endpoint
-32601Method not foundDebug namespace is not enabled on the node
-32603Internal errorCall may be too complex — try increasing timeout or use disableStorage
-32005Rate limit exceededReduce request frequency or upgrade your plan
-32602Invalid paramsVerify the call object fields and block number format
-32000Insufficient fundsThe from address lacks the balance for the value field — note this is simulated, not real funds
JavaScript
async function safeTraceCall(provider, callObj, blockNumber = 'latest', options = {}) {
  const defaultOptions = {
    tracer: 'callTracer',
    timeout: '30s',
    ...options
  };

  try {
    return await provider.send('debug_traceCall', [callObj, blockNumber, defaultOptions]);
  } catch (error) {
    if (error.code === -32601) {
      throw new Error('debug_traceCall not available — ensure archive node with debug API');
    }
    if (error.code === -32000 && error.message?.includes('trie node')) {
      throw new Error(`State unavailable for block ${blockNumber} — use an archive endpoint`);
    }
    if (error.message?.includes('timeout')) {
      console.warn('Trace timed out — retrying with reduced detail');
      return await provider.send('debug_traceCall', [
        callObj,
        blockNumber,
        { tracer: 'callTracer', tracerConfig: { onlyTopCall: true } }
      ]);
    }
    throw error;
  }
}