debug_traceCall - Base RPC Method
Trace a call without creating a transaction on Base. Simulate transactions, debug contract interactions, and analyze gas usage with full execution traces.
Traces a call on Base 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 Base? Build on Coinbase's L2 with 54% of L2 market revenue and direct access to 110M+ Coinbase users with $8B+ TVL, $0.08 gas fees, built-in Coinbase distribution, and seamless fiat rails.
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 Base — ensure your plan includes debug namespace support.
When to Use This Method
debug_traceCall is powerful for consumer app developers, SocialFi builders, and teams seeking easy fiat onramps:
- 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 Base
- Debugging Contract Interactions — Step through contract execution at the opcode level to understand complex interactions, delegate calls, and proxy patterns for consumer dApps, SocialFi, NFT marketplaces, and merchant payment integrations
- Gas Estimation With Trace Details — Go beyond
eth_estimateGasby 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 Base to catch reverts and estimate costs:
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 Base:
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 Base:
import requests
def security_trace_call(call_object, block='latest'):
response = requests.post('https://api-base-mainnet-archive.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': '0x4200000000000000000000000000000000000006',
'data': '0x095ea7b3000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcdffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'
})Error Handling
Common errors and solutions:
| Error Code | Description | Solution |
|---|---|---|
| -32000 | Execution reverted | The call would revert — check the revertReason in the trace result for details |
| -32000 | Missing trie node | Historical block state not available — use an archive node endpoint |
| -32601 | Method not found | Debug namespace is not enabled on the node |
| -32603 | Internal error | Call may be too complex — try increasing timeout or use disableStorage |
| -32005 | Rate limit exceeded | Reduce request frequency or upgrade your plan |
| -32602 | Invalid params | Verify the call object fields and block number format |
| -32000 | Insufficient funds | The from address lacks the balance for the value field — note this is simulated, not real funds |
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;
}
}Related Methods
eth_call— Execute a call without trace (returns only the result, not execution details)debug_traceTransaction— Trace an already-executed transaction by hasheth_estimateGas— Estimate gas for a call (without trace details)debug_traceBlockByNumber— Trace all transactions in a block by numberdebug_traceBlockByHash— Trace all transactions in a block by hash
debug_traceBlockByNumber
Trace all transactions in a block by number on Base. Analyze gas consumption, debug state transitions, and inspect historical block execution.
debug_traceTransaction
Trace a transaction execution on Base. Requires archive node for debugging consumer dApps, SocialFi, NFT marketplaces, and merchant payment integrations.