edb_engine/
core.rs

1// EDB - Ethereum Debugger
2// Copyright (C) 2024 Zhuo Zhang and Wuqi Zhang
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! Core engine functionality for transaction analysis and debugging.
18//!
19//! This module provides the main engine implementation that orchestrates the complete
20//! debugging workflow for Ethereum transactions. It handles source code analysis,
21//! contract instrumentation, transaction execution with debugging inspectors,
22//! and RPC server management.
23//!
24//! # Workflow Overview
25//!
26//! 1. **Preparation**: Accept forked database and transaction configuration
27//! 2. **Analysis**: Download and analyze contract source code
28//! 3. **Instrumentation**: Inject debugging hooks into contract bytecode
29//! 4. **Execution**: Replay transaction with comprehensive debugging inspectors
30//! 5. **Collection**: Gather execution snapshots and trace data
31//! 6. **API**: Start RPC server for debugging interface
32//!
33//! # Key Components
34//!
35//! - [`EngineConfig`] - Engine configuration and settings
36//! - [`run_transaction_analysis`] - Main analysis workflow function
37//! - Inspector coordination for comprehensive data collection
38//! - Source code fetching and compilation management
39//! - Snapshot generation and organization
40//!
41//! # Supported Features
42//!
43//! - **Multi-contract analysis**: Analyze all contracts involved in execution
44//! - **Source fetching**: Automatic download from Etherscan and verification
45//! - **Quick mode**: Fast analysis with reduced operations
46//! - **Instrumentation**: Automatic debugging hook injection
47//! - **Comprehensive inspection**: Opcode and source-level snapshot collection
48
49use alloy_primitives::{Address, Bytes, TxHash};
50use eyre::Result;
51use foundry_block_explorers::Client;
52use foundry_compilers::{
53    artifacts::{Contract, SolcInput},
54    solc::Solc,
55};
56use indicatif::{ProgressBar, ProgressStyle};
57use revm::{
58    context::{
59        result::{ExecutionResult, HaltReason},
60        Host, TxEnv,
61    },
62    database::CacheDB,
63    Database, DatabaseCommit, DatabaseRef, InspectEvm, MainBuilder,
64};
65use semver::Version;
66use std::{
67    collections::{HashMap, HashSet},
68    fs,
69    path::PathBuf,
70    time::Duration,
71};
72use tracing::{debug, error, info, warn};
73
74use edb_common::{
75    relax_evm_constraints, types::Trace, CachePath, EdbCachePath, EdbContext, ForkResult,
76    DEFAULT_ETHERSCAN_CACHE_TTL,
77};
78
79use crate::{
80    analysis::AnalysisResult,
81    analyze,
82    inspector::{CallTracer, TraceReplayResult},
83    instrument,
84    rpc::RpcServerHandle,
85    start_debug_server,
86    utils::{next_etherscan_api_key, Artifact, OnchainCompiler},
87    CodeTweaker, EngineContext, HookSnapshotInspector, HookSnapshots, OpcodeSnapshotInspector,
88    OpcodeSnapshots, SnapshotAnalysis, Snapshots,
89};
90
91/// Configuration for the EDB debugging engine.
92///
93/// Contains settings that control the engine's behavior during transaction analysis,
94/// source code fetching, and debugging operations.
95#[derive(Debug, Clone)]
96pub struct EngineConfig {
97    /// RPC provider URL for blockchain interaction (typically a proxy or archive node)
98    pub rpc_proxy_url: String,
99    /// Optional Etherscan API key for automatic source code downloading and verification
100    pub etherscan_api_key: Option<String>,
101    /// Quick mode flag - when enabled, skips time-intensive operations for faster analysis
102    pub quick: bool,
103}
104
105impl Default for EngineConfig {
106    fn default() -> Self {
107        Self {
108            rpc_proxy_url: "http://localhost:8545".into(),
109            etherscan_api_key: None,
110            quick: false,
111        }
112    }
113}
114
115impl EngineConfig {
116    /// Set the Etherscan API key for source code download
117    pub fn with_etherscan_api_key(mut self, key: String) -> Self {
118        self.etherscan_api_key = Some(key);
119        self
120    }
121
122    /// Enable or disable quick mode for faster analysis
123    pub fn with_quick_mode(mut self, quick: bool) -> Self {
124        self.quick = quick;
125        self
126    }
127
128    /// Set the RPC proxy URL for blockchain interactions
129    pub fn with_rpc_proxy_url(mut self, url: String) -> Self {
130        self.rpc_proxy_url = url;
131        self
132    }
133}
134
135/// The main Engine struct that performs transaction analysis
136#[derive(Debug)]
137pub struct Engine {
138    /// RPC Provider URL
139    pub rpc_proxy_url: String,
140    /// Port for the JSON-RPC server
141    pub host_port: Option<u16>,
142    /// Etherscan API key for source code download
143    pub etherscan_api_key: Option<String>,
144    /// Quick mode - skip certain operations for faster analysis
145    pub quick: bool,
146}
147
148impl Default for Engine {
149    fn default() -> Self {
150        Self::new(EngineConfig::default())
151    }
152}
153
154impl Engine {
155    /// Create a new Engine instance from configuration
156    pub fn new(config: EngineConfig) -> Self {
157        let EngineConfig { rpc_proxy_url, etherscan_api_key, quick } = config;
158        Self { rpc_proxy_url, host_port: None, etherscan_api_key, quick }
159    }
160
161    /// Main preparation method for the engine
162    ///
163    /// This method accepts a forked database and EVM configuration prepared by the edb binary.
164    /// It focuses on the core debugging workflow:
165    /// 1. Replays the target transaction to collect touched contracts
166    /// 2. Downloads verified source code for each contract
167    /// 3. Analyzes the source code to identify instrumentation points
168    /// 4. Instruments and recompiles the source code
169    /// 5. Collect opcode-level step execution results
170    /// 6. Re-executes the transaction with state snapshots
171    /// 7. Starts a JSON-RPC server with the analysis results and snapshots
172    pub async fn prepare<DB>(&self, fork_result: ForkResult<DB>) -> Result<RpcServerHandle>
173    where
174        DB: Database + DatabaseCommit + DatabaseRef + Clone + Send + Sync + 'static,
175        <CacheDB<DB> as Database>::Error: Clone + Send + Sync,
176        <DB as Database>::Error: Clone + Send + Sync,
177    {
178        info!("Starting engine preparation for transaction: {:?}", fork_result.target_tx_hash);
179
180        // Step 0: Initialize context and database
181        let ForkResult { context: mut ctx, target_tx_env: tx, target_tx_hash: tx_hash, fork_info } =
182            fork_result;
183
184        // Step 1: Replay the target transaction to collect call trace and touched contracts
185        info!("Replaying transaction to collect call trace and touched contracts");
186        let replay_result = self.replay_and_collect_trace(ctx.clone(), tx.clone())?;
187
188        // Step 2: Download verified source code for each contract
189        info!("Downloading verified source code for each contract");
190        let artifacts =
191            self.download_verified_source_code(&replay_result, ctx.chain_id().to::<u64>()).await?;
192
193        // Step 3: Analyze source code to identify instrumentation points
194        info!("Analyzing source code");
195        let analysis_results = self.analyze_source_code(&artifacts)?;
196
197        // Step 4: Instrument source code
198        info!("Instrumenting source code");
199        let recompiled_artifacts =
200            self.instrument_and_recompile_source_code(&artifacts, &analysis_results)?;
201
202        // Step 5: Collect opcode-level step execution results
203        info!("Collecting opcode-level step execution results");
204        let opcode_snapshots = self.capture_opcode_level_snapshots(
205            ctx.clone(),
206            tx.clone(),
207            artifacts.keys().cloned().collect(),
208            &replay_result.execution_trace,
209        )?;
210
211        // Step 6: Replace original bytecode with instrumented versions
212        info!("Tweaking bytecode");
213        let contracts_in_tx =
214            self.tweak_bytecode(&mut ctx, &artifacts, &recompiled_artifacts, tx_hash).await?;
215
216        // Step 7: Re-execute the transaction with snapshot collection
217        info!("Re-executing transaction with snapshot collection");
218        let hook_creation =
219            self.collect_creation_hooks(&artifacts, &recompiled_artifacts, contracts_in_tx)?;
220        let hook_snapshots = self.capture_hook_snapshots(
221            ctx.clone(),
222            tx.clone(),
223            hook_creation,
224            &replay_result.execution_trace,
225            &analysis_results,
226        )?;
227
228        // Step 8: Start RPC server with analysis results and snapshots
229        info!("Starting RPC server with analysis results and snapshots");
230        let mut snapshots = self.get_time_travel_snapshots(opcode_snapshots, hook_snapshots)?;
231        snapshots.analyze(&replay_result.execution_trace, &analysis_results)?;
232        // Let's pack the debug context
233        let context = EngineContext::build(
234            fork_info,
235            ctx.cfg.clone(),
236            ctx.block.clone(),
237            tx,
238            tx_hash,
239            snapshots,
240            artifacts,
241            recompiled_artifacts,
242            analysis_results,
243            replay_result.execution_trace,
244        )?;
245
246        let rpc_handle = start_debug_server(context).await?;
247        info!("Debug RPC server started on port {}", rpc_handle.port());
248
249        Ok(rpc_handle)
250    }
251
252    /// Replay the target transaction and collect call trace with all touched addresses
253    fn replay_and_collect_trace<DB>(
254        &self,
255        ctx: EdbContext<DB>,
256        tx: TxEnv,
257    ) -> Result<TraceReplayResult>
258    where
259        DB: Database + DatabaseCommit + DatabaseRef + Clone,
260        <CacheDB<DB> as Database>::Error: Clone,
261        <DB as Database>::Error: Clone,
262    {
263        info!("Replaying transaction to collect call trace and touched addresses");
264
265        let mut tracer = CallTracer::new();
266        let mut evm = ctx.build_mainnet_with_inspector(&mut tracer);
267
268        let result = evm
269            .inspect_one_tx(tx)
270            .map_err(|e| eyre::eyre!("Failed to inspect the target transaction: {:?}", e))?;
271
272        if let ExecutionResult::Halt { reason, .. } = result {
273            if matches!(reason, HaltReason::OutOfGas { .. }) {
274                error!("EDB cannot debug out-of-gas errors. Proceed at your own risk.")
275            }
276        }
277
278        let result = tracer.into_replay_result();
279
280        for (address, deployed) in &result.visited_addresses {
281            if *deployed {
282                debug!("Contract {} was deployed during transaction replay", address);
283            } else {
284                debug!("Address {} was touched during transaction replay", address);
285            }
286        }
287
288        // Print the trace tree structure
289        result.execution_trace.print_trace_tree();
290
291        Ok(result)
292    }
293
294    /// Download and compile verified source code for each contract
295    async fn download_verified_source_code(
296        &self,
297        replay_result: &TraceReplayResult,
298        chain_id: u64,
299    ) -> Result<HashMap<Address, Artifact>> {
300        info!("Downloading verified source code for touched contracts");
301
302        let compiler = OnchainCompiler::new(None)?;
303
304        let compiler_cache_root =
305            EdbCachePath::new(None as Option<PathBuf>).compiler_chain_cache_dir(chain_id);
306
307        // Create fancy progress bar with blockchain-themed styling
308        // NOTE: For multi-threaded usage, wrap in Arc<ProgressBar> to share across threads
309        // Example: let console_bar = Arc::new(ProgressBar::new(...));
310        // Then clone the Arc for each thread: let bar_clone = console_bar.clone();
311        let addresses: Vec<_> = replay_result.visited_addresses.keys().copied().collect();
312        let total_contracts = addresses.len();
313
314        let console_bar = std::sync::Arc::new(ProgressBar::new(total_contracts as u64));
315        console_bar.set_style(
316            ProgressStyle::with_template(
317                "{spinner:.green} 📜 Downloading & compiling contracts [{bar:40.cyan/blue}] {pos:>3}/{len:3} 🔧 {msg}"
318            )?
319            .progress_chars("🟩🟦⬜")
320            .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
321        );
322
323        let mut artifacts = HashMap::new();
324        for (i, address) in addresses.iter().enumerate() {
325            let short_addr = &address.to_string()[2..10]; // Skip 0x, take 8 chars
326            console_bar.set_message(format!("contract {}: 0x{}...", i + 1, short_addr));
327
328            // We use the default API key if none is provided
329            let api_key = self.get_etherscan_api_key();
330
331            let etherscan = Client::builder()
332                .with_api_key(api_key)
333                .with_cache(
334                    compiler_cache_root.clone(),
335                    Duration::from_secs(DEFAULT_ETHERSCAN_CACHE_TTL),
336                ) // 24 hours
337                .chain(chain_id.into())?
338                .build()?;
339
340            match compiler.compile(&etherscan, *address).await {
341                Ok(Some(artifact)) => {
342                    console_bar.set_message(format!("✅ 0x{short_addr}... compiled"));
343                    artifacts.insert(*address, artifact);
344                }
345                Ok(None) => {
346                    console_bar.set_message(format!("⚠️  0x{short_addr}... no source"));
347                    debug!("No source code available for contract {}", address);
348                }
349                Err(e) => {
350                    console_bar.set_message(format!("❌ 0x{short_addr}... failed"));
351                    warn!("Failed to compile contract {}: {:?}", address, e);
352                }
353            }
354
355            console_bar.inc(1);
356        }
357
358        console_bar.finish_with_message(format!(
359            "✨ Done! Compiled {} out of {} contracts",
360            artifacts.len(),
361            total_contracts
362        ));
363
364        Ok(artifacts)
365    }
366
367    /// Analyze the source code for instrumentation points and variable usage
368    fn analyze_source_code(
369        &self,
370        artifacts: &HashMap<Address, Artifact>,
371    ) -> Result<HashMap<Address, AnalysisResult>> {
372        info!("Analyzing source code to identify instrumentation points");
373
374        let mut analysis_result = HashMap::new();
375        for (address, artifact) in artifacts {
376            debug!("Analyzing contract at address: {}", address);
377            let analysis = analyze(artifact)?;
378            analysis_result.insert(*address, analysis);
379        }
380
381        Ok(analysis_result)
382    }
383
384    /// Time travel (i.e., snapshotting) at hooks for contracts we have source code
385    fn capture_hook_snapshots<'a, DB>(
386        &self,
387        mut ctx: EdbContext<DB>,
388        mut tx: TxEnv,
389        creation_hooks: Vec<(&'a Contract, &'a Contract, &'a Bytes)>,
390        trace: &Trace,
391        analysis_results: &HashMap<Address, AnalysisResult>,
392    ) -> Result<HookSnapshots<DB>>
393    where
394        DB: Database + DatabaseCommit + DatabaseRef + Clone,
395        <CacheDB<DB> as Database>::Error: Clone,
396        <DB as Database>::Error: Clone,
397    {
398        // We need to relax execution constraints for hook snapshots
399        relax_evm_constraints(&mut ctx, &mut tx);
400
401        info!("Collecting hook snapshots for source code contracts");
402
403        let mut inspector = HookSnapshotInspector::new(trace, analysis_results);
404        inspector.with_creation_hooks(creation_hooks)?;
405        let mut evm = ctx.build_mainnet_with_inspector(&mut inspector);
406
407        evm.inspect_one_tx(tx)
408            .map_err(|e| eyre::eyre!("Failed to inspect the target transaction: {:?}", e))?;
409
410        let snapshots = inspector.into_snapshots();
411
412        snapshots.print_summary();
413
414        Ok(snapshots)
415    }
416
417    /// Time travel (i.e., snapshotting) at the opcode level for contracts we do not
418    /// have source code.
419    fn capture_opcode_level_snapshots<DB>(
420        &self,
421        ctx: EdbContext<DB>,
422        tx: TxEnv,
423        excluded_addresses: HashSet<Address>,
424        trace: &Trace,
425    ) -> Result<OpcodeSnapshots<DB>>
426    where
427        DB: Database + DatabaseCommit + DatabaseRef + Clone,
428        <CacheDB<DB> as Database>::Error: Clone,
429        <DB as Database>::Error: Clone,
430    {
431        info!("Collecting opcode-level step execution results");
432
433        let mut inspector = OpcodeSnapshotInspector::new(&ctx, trace);
434        inspector.with_excluded_addresses(excluded_addresses);
435        let mut evm = ctx.build_mainnet_with_inspector(&mut inspector);
436
437        evm.inspect_one_tx(tx)
438            .map_err(|e| eyre::eyre!("Failed to inspect the target transaction: {:?}", e))?;
439
440        let snapshots = inspector.into_snapshots();
441
442        snapshots.print_summary();
443
444        Ok(snapshots)
445    }
446
447    /// Instrument and recompile the source code
448    fn instrument_and_recompile_source_code(
449        &self,
450        artifacts: &HashMap<Address, Artifact>,
451        analysis_result: &HashMap<Address, AnalysisResult>,
452    ) -> Result<HashMap<Address, Artifact>> {
453        info!("Instrumenting source code based on analysis results");
454
455        let mut recompiled_artifacts = HashMap::new();
456        for (address, artifact) in artifacts {
457            let compiler_version =
458                Version::parse(artifact.compiler_version().trim_start_matches('v'))?;
459
460            let analysis = analysis_result
461                .get(address)
462                .ok_or_else(|| eyre::eyre!("No analysis result found for address {}", address))?;
463
464            let input = instrument(&compiler_version, &artifact.input, analysis)?;
465            let meta = artifact.meta.clone();
466
467            // prepare the compiler
468            let version = meta.compiler_version()?;
469            let compiler = Solc::find_or_install(&version)?;
470
471            // compile the source code
472            let output = match compiler.compile_exact(&input) {
473                Ok(output) => output,
474                Err(e) => {
475                    // Dump source code for debugging
476                    let (original_dir, instrumented_dir) =
477                        dump_source_for_debugging(address, &artifact.input, &input)?;
478
479                    return Err(eyre::eyre!(
480                        "Failed to recompile contract {}\n\nCompiler error: {}\n\nDebug info:\n  Original source: {}\n  Instrumented source: {}",
481                        address,
482                        e,
483                        original_dir.display(),
484                        instrumented_dir.display()
485                    ));
486                }
487            };
488            if output.errors.iter().any(|e| e.is_error()) {
489                // Dump source code for debugging
490                let (original_dir, instrumented_dir) =
491                    dump_source_for_debugging(address, &artifact.input, &input)?;
492
493                // Format errors with better source location info
494                let formatted_errors = format_compiler_errors(&output.errors, &instrumented_dir);
495
496                return Err(eyre::eyre!(
497                    "Recompilation failed for contract {}\n\nCompilation errors:{}\n\nDebug info:\n  Original source: {}\n  Instrumented source: {}",
498                    address,
499                    formatted_errors,
500                    original_dir.display(),
501                    instrumented_dir.display()
502                ));
503            }
504
505            debug!(
506                "Recompiled Contract {}: {} vs {}",
507                address,
508                artifact.output.contracts.len(),
509                output.contracts.len()
510            );
511
512            recompiled_artifacts.insert(*address, Artifact { meta, input, output });
513        }
514
515        Ok(recompiled_artifacts)
516    }
517
518    /// Tweak the bytecode of the contracts
519    async fn tweak_bytecode<DB>(
520        &self,
521        ctx: &mut EdbContext<DB>,
522        artifacts: &HashMap<Address, Artifact>,
523        recompiled_artifacts: &HashMap<Address, Artifact>,
524        tx_hash: TxHash,
525    ) -> Result<Vec<Address>>
526    where
527        DB: Database + DatabaseCommit + DatabaseRef + Clone,
528        <CacheDB<DB> as Database>::Error: Clone,
529        <DB as Database>::Error: Clone,
530    {
531        let mut tweaker =
532            CodeTweaker::new(ctx, self.rpc_proxy_url.clone(), self.etherscan_api_key.clone());
533
534        let mut contracts_in_tx = Vec::new();
535
536        for (address, recompiled_artifact) in recompiled_artifacts {
537            let creation_tx_hash = tweaker.get_creation_tx(address).await?;
538            if creation_tx_hash == tx_hash {
539                debug!("Skip tweaking contract {}, since it was created by the transaction under investigation", address);
540                contracts_in_tx.push(*address);
541                continue;
542            }
543
544            let artifact = artifacts
545                .get(address)
546                .ok_or_else(|| eyre::eyre!("No original artifact found for address {}", address))?;
547
548            tweaker.tweak(address, artifact, recompiled_artifact, self.quick).await.map_err(
549                |e| eyre::eyre!("Failed to tweak bytecode for contract {}: {}", address, e),
550            )?;
551        }
552
553        Ok(contracts_in_tx)
554    }
555
556    // Collect creation code that will be hooked
557    fn collect_creation_hooks<'a>(
558        &self,
559        artifacts: &'a HashMap<Address, Artifact>,
560        recompiled_artifacts: &'a HashMap<Address, Artifact>,
561        contracts_in_tx: Vec<Address>,
562    ) -> Result<Vec<(&'a Contract, &'a Contract, &'a Bytes)>> {
563        info!("Collecting creation hooks for contracts in transaction");
564
565        let mut hook_creation = Vec::new();
566        for address in contracts_in_tx {
567            let Some(artifact) = artifacts.get(&address) else {
568                eyre::bail!("No original artifact found for address {}", address);
569            };
570
571            let Some(recompiled_artifact) = recompiled_artifacts.get(&address) else {
572                eyre::bail!("No recompiled artifact found for address {}", address);
573            };
574
575            hook_creation.extend(artifact.find_creation_hooks(recompiled_artifact));
576        }
577
578        Ok(hook_creation)
579    }
580
581    // Create snapshots for time travel
582    fn get_time_travel_snapshots<DB>(
583        &self,
584        opcode_snapshots: OpcodeSnapshots<DB>,
585        hook_snapshots: HookSnapshots<DB>,
586    ) -> Result<Snapshots<DB>>
587    where
588        DB: Database + DatabaseCommit + DatabaseRef + Clone,
589        <CacheDB<DB> as Database>::Error: Clone,
590        <DB as Database>::Error: Clone,
591    {
592        let snapshots = Snapshots::merge(opcode_snapshots, hook_snapshots);
593        snapshots.print_summary();
594
595        Ok(snapshots)
596    }
597}
598
599// Helper functions
600impl Engine {
601    fn get_etherscan_api_key(&self) -> String {
602        self.etherscan_api_key.clone().unwrap_or(next_etherscan_api_key())
603    }
604}
605
606/// Sanitize a path to prevent directory traversal attacks
607fn sanitize_path(path: &std::path::Path) -> PathBuf {
608    use std::path::Component;
609
610    let mut sanitized = PathBuf::new();
611
612    for component in path.components() {
613        match component {
614            Component::Normal(name) => {
615                // Only add normal path components (no .., ., or absolute paths)
616                sanitized.push(name);
617            }
618            Component::CurDir => {
619                // Skip "." components
620            }
621            Component::ParentDir => {
622                // Skip ".." components - don't allow traversal
623                warn!("Skipping parent directory component in path: {:?}", path);
624            }
625            Component::RootDir => {
626                // Skip root directory "/" - don't allow absolute paths
627                warn!("Skipping root directory component in path: {:?}", path);
628            }
629            Component::Prefix(_) => {
630                // Skip Windows drive prefixes like "C:"
631                warn!("Skipping prefix component in path: {:?}", path);
632            }
633        }
634    }
635
636    // If the path was completely stripped, use a default name
637    if sanitized.as_os_str().is_empty() {
638        sanitized.push("unnamed_source");
639    }
640
641    sanitized
642}
643
644/// Extract code context around an error position
645fn extract_code_context(
646    file_path: &std::path::Path,
647    start_pos: i32,
648    end_pos: i32,
649    context_lines: usize,
650) -> Option<String> {
651    use std::io::{BufRead, BufReader};
652
653    let file = fs::File::open(file_path).ok()?;
654    let reader = BufReader::new(file);
655    let lines: Vec<String> = reader.lines().map_while(Result::ok).collect();
656
657    // Convert byte positions to line/column
658    let mut current_pos = 0i32;
659    let mut start_line = 0;
660    let mut start_col = 0;
661    let mut end_line = 0;
662    let mut end_col = 0;
663
664    for (line_num, line) in lines.iter().enumerate() {
665        let line_start = current_pos;
666        let line_end = current_pos + line.len() as i32 + 1; // +1 for newline
667
668        if start_pos >= line_start && start_pos < line_end {
669            start_line = line_num;
670            start_col = (start_pos - line_start) as usize;
671        }
672
673        if end_pos >= line_start && end_pos <= line_end {
674            end_line = line_num;
675            end_col = (end_pos - line_start) as usize;
676        }
677
678        current_pos = line_end;
679    }
680
681    // Build context
682    let mut context = String::new();
683    let context_start = start_line.saturating_sub(context_lines);
684    let context_end = (end_line + context_lines + 1).min(lines.len());
685
686    for line_num in context_start..context_end {
687        if line_num >= lines.len() {
688            break;
689        }
690
691        let line_number = line_num + 1; // 1-indexed
692        let line = &lines[line_num];
693
694        // Format line with line number
695        if line_num >= start_line && line_num <= end_line {
696            // Error line - highlight it
697            context.push_str(&format!("  {line_number} | {line}\n"));
698
699            // Add underline for the error position on the first error line
700            if line_num == start_line {
701                let padding = format!("  {line_number} | ").len();
702                let mut underline = " ".repeat(padding + start_col);
703                let underline_len = if start_line == end_line {
704                    (end_col - start_col).max(1)
705                } else {
706                    line.len() - start_col
707                };
708                underline.push_str(&"^".repeat(underline_len));
709                context.push_str(&format!("{underline}\n"));
710            }
711        } else {
712            // Context line
713            context.push_str(&format!("  {line_number} | {line}\n"));
714        }
715    }
716
717    Some(context)
718}
719
720/// Format compiler errors with better source location information
721fn format_compiler_errors(
722    errors: &[foundry_compilers::artifacts::Error],
723    dump_dir: &std::path::Path,
724) -> String {
725    let mut formatted = String::new();
726
727    for error in errors.iter().filter(|e| e.is_error()) {
728        formatted.push_str("\n\n");
729
730        // Add error severity and type
731        if let Some(error_code) = &error.error_code {
732            formatted.push_str(&format!("Error [{error_code}]: "));
733        } else {
734            formatted.push_str("Error: ");
735        }
736
737        // Add the main error message
738        formatted.push_str(&error.message);
739
740        // Add source location and code context if available
741        if let Some(loc) = &error.source_location {
742            formatted.push_str(&format!("\n  --> {}:{}:{}", loc.file.as_str(), loc.start, loc.end));
743
744            // Try to extract code context from the dumped files
745            let sanitized_path = sanitize_path(std::path::Path::new(&loc.file));
746            let source_file = dump_dir.join(&sanitized_path);
747
748            if let Some(context) = extract_code_context(&source_file, loc.start, loc.end, 5) {
749                formatted.push_str("\n\n");
750                formatted.push_str(&context);
751            }
752        }
753
754        // If we have a formatted message with context, also include it
755        // (as it might have additional information)
756        if let Some(formatted_msg) = &error.formatted_message {
757            if !formatted_msg.trim().is_empty() {
758                formatted.push_str("\n\nCompiler's formatted output:\n");
759                formatted.push_str(formatted_msg);
760            }
761        }
762
763        // Add secondary locations if any
764        if !error.secondary_source_locations.is_empty() {
765            for sec_loc in &error.secondary_source_locations {
766                if let Some(msg) = &sec_loc.message {
767                    formatted.push_str(&format!("\n  Note: {msg}"));
768                }
769                if let Some(file) = &sec_loc.file {
770                    formatted.push_str(&format!(
771                        "\n    --> {}:{}:{}",
772                        file,
773                        sec_loc.start.map(|s| s.to_string()).unwrap_or_else(|| "?".to_string()),
774                        sec_loc.end.map(|e| e.to_string()).unwrap_or_else(|| "?".to_string())
775                    ));
776
777                    // Try to show context for secondary locations too
778                    if let (Some(start), Some(end)) = (sec_loc.start, sec_loc.end) {
779                        let sanitized_path = sanitize_path(std::path::Path::new(file));
780                        let source_file = dump_dir.join(&sanitized_path);
781
782                        if let Some(context) = extract_code_context(&source_file, start, end, 1) {
783                            formatted.push('\n');
784                            formatted.push_str(&context);
785                        }
786                    }
787                }
788            }
789        }
790    }
791
792    if formatted.is_empty() {
793        formatted.push_str("\nNo specific error details available");
794    }
795
796    formatted
797}
798
799/// Dump source code to a temporary directory for debugging
800fn dump_source_for_debugging(
801    address: &Address,
802    original_input: &SolcInput,
803    instrumented_input: &SolcInput,
804) -> Result<(PathBuf, PathBuf)> {
805    use std::io::Write;
806
807    // Create temp directories
808    let temp_dir = std::env::temp_dir();
809    let debug_dir = temp_dir.join(format!("edb_debug_{address}"));
810    let original_dir = debug_dir.join("original");
811    let instrumented_dir = debug_dir.join("instrumented");
812
813    // Create directories
814    fs::create_dir_all(&original_dir)?;
815    fs::create_dir_all(&instrumented_dir)?;
816
817    // Write original sources
818    for (path_str, source) in &original_input.sources {
819        let path = std::path::Path::new(path_str);
820        let sanitized_path = sanitize_path(path);
821        let file_path = original_dir.join(&sanitized_path);
822
823        // Safety check: verify the resulting path is still within our directory
824        // We use a path-based check rather than canonicalize to avoid TOCTOU issues
825        if !file_path.starts_with(&original_dir) {
826            return Err(eyre::eyre!(
827                "Path traversal detected in source path: {}",
828                path_str.display()
829            ));
830        }
831
832        // Create parent directories if needed
833        if let Some(parent) = file_path.parent() {
834            fs::create_dir_all(parent)?;
835        }
836
837        let mut file = fs::File::create(&file_path)?;
838        file.write_all(source.content.as_bytes())?;
839    }
840
841    // Write original settings.json
842    let settings_path = original_dir.join("settings.json");
843    let mut settings_file = fs::File::create(&settings_path)?;
844    settings_file.write_all(serde_json::to_string_pretty(&original_input.settings)?.as_bytes())?;
845
846    // Write instrumented sources
847    for (path_str, source) in &instrumented_input.sources {
848        let path = std::path::Path::new(path_str);
849        let sanitized_path = sanitize_path(path);
850        let file_path = instrumented_dir.join(&sanitized_path);
851
852        // Safety check: verify the resulting path is still within our directory
853        // We use a path-based check rather than canonicalize to avoid TOCTOU issues
854        if !file_path.starts_with(&instrumented_dir) {
855            return Err(eyre::eyre!(
856                "Path traversal detected in source path: {}",
857                path_str.display()
858            ));
859        }
860
861        // Create parent directories if needed
862        if let Some(parent) = file_path.parent() {
863            fs::create_dir_all(parent)?;
864        }
865
866        let mut file = fs::File::create(&file_path)?;
867        file.write_all(source.content.as_bytes())?;
868    }
869
870    // Write instrumented settings.json
871    let settings_path = instrumented_dir.join("settings.json");
872    let mut settings_file = fs::File::create(&settings_path)?;
873    settings_file
874        .write_all(serde_json::to_string_pretty(&instrumented_input.settings)?.as_bytes())?;
875
876    Ok((original_dir, instrumented_dir))
877}