Skip to main content

foundry_evm/inspectors/
logs.rs

1use alloy_primitives::Log;
2use alloy_sol_types::{SolEvent, SolInterface, SolValue};
3use foundry_common::{ErrorExt, fmt::ConsoleFmt, sh_println};
4use foundry_evm_core::{
5    InspectorExt, abi::console, constants::HARDHAT_CONSOLE_ADDRESS, decode::decode_console_log,
6};
7use revm::{
8    Inspector,
9    context::ContextTr,
10    interpreter::{
11        CallInputs, CallOutcome, Gas, InstructionResult, InterpreterResult,
12        interpreter::EthInterpreter,
13    },
14};
15
16/// An inspector that collects logs during execution.
17///
18/// The inspector collects logs from the `LOG` opcodes as well as Hardhat-style `console.sol` logs.
19#[derive(Clone, Debug)]
20pub enum LogCollector {
21    /// The collected logs. Includes both `LOG` opcodes and Hardhat-style `console.sol` logs.
22    Capture { logs: Vec<Log> },
23    /// Print logs directly to stdout.
24    LiveLogs,
25}
26
27impl LogCollector {
28    pub fn into_captured_logs(self) -> Option<Vec<Log>> {
29        match self {
30            Self::Capture { logs } => Some(logs),
31            Self::LiveLogs => None,
32        }
33    }
34
35    #[cold]
36    fn do_hardhat_log<CTX>(&mut self, context: &mut CTX, inputs: &CallInputs) -> Option<CallOutcome>
37    where
38        CTX: ContextTr,
39    {
40        if let Err(err) = self.hardhat_log(&inputs.input.bytes(context)) {
41            let result = InstructionResult::Revert;
42            let output = err.abi_encode_revert();
43            return Some(CallOutcome {
44                result: InterpreterResult { result, output, gas: Gas::new(inputs.gas_limit) },
45                memory_offset: inputs.return_memory_offset.clone(),
46                was_precompile_called: true,
47                precompile_call_logs: vec![],
48            });
49        }
50        None
51    }
52
53    fn hardhat_log(&mut self, data: &[u8]) -> alloy_sol_types::Result<()> {
54        let decoded = console::hh::ConsoleCalls::abi_decode(data)?;
55        self.push_msg(&decoded.fmt(Default::default()));
56        Ok(())
57    }
58
59    fn push_raw_log(&mut self, log: Log) {
60        match self {
61            Self::Capture { logs } => logs.push(log),
62            Self::LiveLogs => {
63                if let Some(msg) = decode_console_log(&log) {
64                    sh_println!("{msg}").expect("fail printing to stdout");
65                } else {
66                    // This case should not happen if the users call through forge-std.
67                    // We print the log data for the user nonetheless.
68                    sh_println!("console.log({:?}, {})", log.data.topics(), log.data.data)
69                        .expect("fail printing to stdout");
70                }
71            }
72        }
73    }
74
75    fn push_msg(&mut self, msg: &str) {
76        match self {
77            Self::Capture { logs } => logs.push(new_console_log(msg)),
78            Self::LiveLogs => sh_println!("{msg}").expect("fail printing to stdout"),
79        }
80    }
81}
82
83impl<CTX> Inspector<CTX, EthInterpreter> for LogCollector
84where
85    CTX: ContextTr,
86{
87    fn log(&mut self, _context: &mut CTX, log: Log) {
88        self.push_raw_log(log);
89    }
90
91    fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option<CallOutcome> {
92        if inputs.target_address == HARDHAT_CONSOLE_ADDRESS {
93            return self.do_hardhat_log(context, inputs);
94        }
95        None
96    }
97}
98
99impl InspectorExt for LogCollector {
100    fn console_log(&mut self, msg: &str) {
101        self.push_msg(msg);
102    }
103}
104
105/// Creates a `console.log(string)` event.
106fn new_console_log(msg: &str) -> Log {
107    Log::new_unchecked(
108        HARDHAT_CONSOLE_ADDRESS,
109        vec![console::ds::log::SIGNATURE_HASH],
110        msg.abi_encode().into(),
111    )
112}