diff --git a/cli/src/cmd/cast/call.rs b/cli/src/cmd/cast/call.rs index 65fece91fc13d..df367c2b9ddfd 100644 --- a/cli/src/cmd/cast/call.rs +++ b/cli/src/cmd/cast/call.rs @@ -1,15 +1,24 @@ // cast estimate subcommands use crate::{ + cmd::utils::{handle_traces, TraceResult}, opts::{EthereumOpts, TransactionOpts}, utils::{self, parse_ether_value}, }; use cast::{Cast, TxBuilder}; use clap::Parser; -use ethers::types::{BlockId, NameOrAddress, U256}; +use ethers::{ + solc::EvmVersion, + types::{BlockId, NameOrAddress, U256}, +}; use eyre::WrapErr; -use foundry_config::Config; +use forge::executor::opts::EvmOpts; +use foundry_config::{find_project_root_path, Config}; +use foundry_evm::trace::TracingExecutor; use std::str::FromStr; +type Provider = + ethers::providers::Provider>; + /// CLI arguments for `cast call`. #[derive(Debug, Parser)] pub struct CallArgs { @@ -31,13 +40,41 @@ pub struct CallArgs { )] data: Option, + /// Forks the remote rpc, executes the transaction locally and prints a trace + #[clap(long, default_value_t = false)] + trace: bool, + + /// Can only be used with "--trace" + /// + /// opens an interactive debugger + #[clap(long, requires = "trace")] + debug: bool, + + /// Can only be used with "--trace" + /// + /// prints a more verbose trace + #[clap(long, requires = "trace")] + verbose: bool, + + /// Can only be used with "--trace" + /// Labels to apply to the traces. + /// + /// Format: `address:label` + #[clap(long, requires = "trace")] + labels: Vec, + + /// Can only be used with "--trace" + /// + /// The EVM Version to use. + #[clap(long, requires = "trace")] + evm_version: Option, + /// The block height to query at. /// /// Can also be the tags earliest, finalized, safe, latest, or pending. #[clap(long, short)] block: Option, - /// Simulate a contract deployment. #[clap(subcommand)] command: Option, @@ -50,6 +87,7 @@ pub struct CallArgs { #[derive(Debug, Parser)] pub enum CallSubcommands { + /// ignores the address field and simulates creating a contract #[clap(name = "--create")] Create { /// Bytecode of contract. @@ -73,54 +111,158 @@ pub enum CallSubcommands { impl CallArgs { pub async fn run(self) -> eyre::Result<()> { - let CallArgs { to, sig, args, data, tx, eth, command, block } = self; + let CallArgs { + to, + sig, + args, + data, + tx, + eth, + command, + block, + trace, + evm_version, + debug, + verbose, + labels, + } = self; let config = Config::from(ð); let provider = utils::get_provider(&config)?; let chain = utils::get_chain(config.chain_id, &provider).await?; let sender = eth.wallet.sender().await; - let mut builder = TxBuilder::new(&provider, sender, to, chain, tx.legacy).await?; + let mut builder: TxBuilder<'_, Provider> = + TxBuilder::new(&provider, sender, to, chain, tx.legacy).await?; + builder .gas(tx.gas_limit) .etherscan_api_key(config.get_etherscan_api_key(Some(chain))) .gas_price(tx.gas_price) .priority_gas_price(tx.priority_gas_price) .nonce(tx.nonce); + match command { Some(CallSubcommands::Create { code, sig, args, value }) => { - builder.value(value); + if trace { + let figment = Config::figment_with_root(find_project_root_path(None).unwrap()) + .merge(eth.rpc); + + let evm_opts = figment.extract::()?; + + let (env, fork, chain) = + TracingExecutor::get_fork_material(&config, evm_opts).await?; - let mut data = hex::decode(code.strip_prefix("0x").unwrap_or(&code))?; + let mut executor = + foundry_evm::trace::TracingExecutor::new(env, fork, evm_version, debug) + .await; - if let Some(s) = sig { - let (mut sigdata, _func) = builder.create_args(&s, args).await?; - data.append(&mut sigdata); + let trace = match executor.deploy( + sender, + code.into(), + value.unwrap_or(U256::zero()), + None, + ) { + Ok(deploy_result) => TraceResult::from(deploy_result), + Err(evm_err) => TraceResult::try_from(evm_err)?, + }; + + handle_traces(trace, &config, chain, labels, verbose, debug).await?; + + return Ok(()) } - builder.set_data(data); + // fill the builder after the conditional so we dont move values + fill_create(&mut builder, value, code, sig, args).await?; } _ => { - builder.value(tx.value); + // fill first here because we need to use the builder in the conditional + fill_tx(&mut builder, tx.value, sig, args, data).await?; - if let Some(sig) = sig { - builder.set_args(sig.as_str(), args).await?; - } - if let Some(data) = data { - // Note: `sig+args` and `data` are mutually exclusive - builder.set_data( - hex::decode(data).wrap_err("Expected hex encoded function data")?, - ); + if trace { + let figment = Config::figment_with_root(find_project_root_path(None).unwrap()) + .merge(eth.rpc); + + let evm_opts = figment.extract::()?; + + let (env, fork, chain) = + TracingExecutor::get_fork_material(&config, evm_opts).await?; + + let mut executor = + foundry_evm::trace::TracingExecutor::new(env, fork, evm_version, debug) + .await; + + let (tx, _) = builder.build(); + + let trace = TraceResult::from(executor.call_raw_committing( + sender, + tx.to_addr().copied().expect("an address to be here"), + tx.data().cloned().unwrap_or_default().to_vec().into(), + tx.value().copied().unwrap_or_default(), + )?); + + handle_traces(trace, &config, chain, labels, verbose, debug).await?; + + return Ok(()) } } }; - let builder_output = builder.build(); + let builder_output: ( + ethers::types::transaction::eip2718::TypedTransaction, + Option, + ) = builder.build(); + println!("{}", Cast::new(provider).call(builder_output, block).await?); + Ok(()) } } +/// fills the builder from create arg +async fn fill_create( + builder: &mut TxBuilder<'_, Provider>, + value: Option, + code: String, + sig: Option, + args: Vec, +) -> eyre::Result<()> { + builder.value(value); + + let mut data = hex::decode(code.strip_prefix("0x").unwrap_or(&code))?; + + if let Some(s) = sig { + let (mut sigdata, _func) = builder.create_args(&s, args).await?; + data.append(&mut sigdata); + } + + builder.set_data(data); + + Ok(()) +} + +/// fills the builder from args +async fn fill_tx( + builder: &mut TxBuilder<'_, Provider>, + value: Option, + sig: Option, + args: Vec, + data: Option, +) -> eyre::Result<()> { + builder.value(value); + + if let Some(sig) = sig { + builder.set_args(sig.as_str(), args).await?; + } + + if let Some(data) = data { + // Note: `sig+args` and `data` are mutually exclusive + builder.set_data(hex::decode(data).wrap_err("Expected hex encoded function data")?); + } + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/cli/src/cmd/cast/run.rs b/cli/src/cmd/cast/run.rs index 47b395def368c..0722f01b281c2 100644 --- a/cli/src/cmd/cast/run.rs +++ b/cli/src/cmd/cast/run.rs @@ -1,32 +1,21 @@ -use crate::{init_progress, opts::RpcOpts, update_progress, utils}; -use cast::{ - executor::{EvmError, ExecutionErr}, - trace::{identifier::SignaturesIdentifier, CallTraceDecoder, Traces}, +use crate::{ + cmd::utils::{handle_traces, TraceResult}, + init_progress, + opts::RpcOpts, + update_progress, utils, }; + use clap::Parser; -use ethers::{ - abi::Address, - prelude::{artifacts::ContractBytecodeSome, ArtifactId, Middleware}, - solc::EvmVersion, - types::H160, -}; +use ethers::{prelude::Middleware, solc::EvmVersion, types::H160}; use eyre::WrapErr; use forge::{ - debug::DebugArena, - executor::{ - inspector::cheatcodes::util::configure_tx_env, opts::EvmOpts, Backend, DeployResult, - ExecutorBuilder, RawCallResult, - }, + executor::{inspector::cheatcodes::util::configure_tx_env, opts::EvmOpts}, revm::primitives::U256 as rU256, - trace::{identifier::EtherscanIdentifier, CallTraceDecoderBuilder, TraceKind}, utils::h256_to_b256, }; use foundry_config::{find_project_root_path, Config}; -use foundry_evm::utils::evm_spec; -use std::{collections::BTreeMap, str::FromStr}; +use foundry_evm::{executor::EvmError, trace::TracingExecutor}; use tracing::trace; -use ui::{TUIExitReason, Tui, Ui}; -use yansi::Paint; const ARBITRUM_SENDER: H160 = H160([ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -97,8 +86,8 @@ impl RunArgs { pub async fn run(self) -> eyre::Result<()> { let figment = Config::figment_with_root(find_project_root_path(None).unwrap()).merge(self.rpc); - let mut evm_opts = figment.extract::()?; - let config = Config::from_provider(figment).sanitized(); + let evm_opts = figment.extract::()?; + let mut config = Config::from_provider(figment).sanitized(); let compute_units_per_second = if self.no_rate_limit { Some(u64::MAX) } else { self.compute_units_per_second }; @@ -117,27 +106,14 @@ impl RunArgs { .block_number .ok_or_else(|| eyre::eyre!("tx may still be pending: {:?}", tx_hash))? .as_u64(); - evm_opts.fork_url = Some(config.get_rpc_url_or_localhost_http()?.into_owned()); - // we need to set the fork block to the previous block, because that's the state at - // which we access the data in order to execute the transaction(s) - evm_opts.fork_block_number = Some(tx_block_number - 1); - - // Set up the execution environment - let mut env = evm_opts.evm_env().await?; - // can safely disable base fee checks on replaying txs because can - // assume those checks already passed on confirmed txs - env.cfg.disable_base_fee = true; - let db = Backend::spawn(evm_opts.get_fork(&config, env.clone())).await; - - // configures a bare version of the evm executor: no cheatcode inspector is enabled, - // tracing will be enabled only for the targeted transaction - let builder = ExecutorBuilder::default() - .with_config(env) - .with_spec(evm_spec(&self.evm_version.unwrap_or(config.evm_version))); - - let mut executor = builder.build(db); - - let mut env = executor.env().clone(); + + config.fork_block_number = Some(tx_block_number - 1); + + let (mut env, fork, chain) = TracingExecutor::get_fork_material(&config, evm_opts).await?; + + let mut executor = + TracingExecutor::new(env.clone(), fork, self.evm_version, self.debug).await; + env.block.number = rU256::from(tx_block_number); let block = provider.get_block_with_txs(tx_block_number).await?; @@ -197,153 +173,25 @@ impl RunArgs { } // Execute our transaction - let mut result = { - executor - .set_tracing(true) - .set_debugger(self.debug) - .set_trace_printer(self.trace_printer); + let result = { + executor.set_trace_printer(self.trace_printer); configure_tx_env(&mut env, &tx); if let Some(to) = tx.to { trace!(tx=?tx.hash,to=?to, "executing call transaction"); - let RawCallResult { - reverted, - gas_used: gas, - traces, - debug: run_debug, - exit_reason: _, - .. - } = executor.commit_tx_with_env(env)?; - - RunResult { - success: !reverted, - traces: vec![(TraceKind::Execution, traces.unwrap_or_default())], - debug: run_debug.unwrap_or_default(), - gas_used: gas, - } + TraceResult::from(executor.commit_tx_with_env(env)?) } else { trace!(tx=?tx.hash, "executing create transaction"); match executor.deploy_with_env(env, None) { - Ok(DeployResult { gas_used, traces, debug: run_debug, .. }) => RunResult { - success: true, - traces: vec![(TraceKind::Execution, traces.unwrap_or_default())], - debug: run_debug.unwrap_or_default(), - gas_used, - }, - Err(EvmError::Execution(inner)) => { - let ExecutionErr { reverted, gas_used, traces, debug: run_debug, .. } = - *inner; - RunResult { - success: !reverted, - traces: vec![(TraceKind::Execution, traces.unwrap_or_default())], - debug: run_debug.unwrap_or_default(), - gas_used, - } - } - Err(err) => { - eyre::bail!("unexpected error when running create transaction: {:?}", err) - } + Ok(res) => TraceResult::from(res), + Err(err) => TraceResult::try_from(err)?, } } }; - let mut etherscan_identifier = - EtherscanIdentifier::new(&config, evm_opts.get_remote_chain_id())?; - - let labeled_addresses = self.label.iter().filter_map(|label_str| { - let mut iter = label_str.split(':'); - - if let Some(addr) = iter.next() { - if let (Ok(address), Some(label)) = (Address::from_str(addr), iter.next()) { - return Some((address, label.to_string())) - } - } - None - }); - - let mut decoder = CallTraceDecoderBuilder::new().with_labels(labeled_addresses).build(); - - decoder.add_signature_identifier(SignaturesIdentifier::new( - Config::foundry_cache_dir(), - config.offline, - )?); - - for (_, trace) in &mut result.traces { - decoder.identify(trace, &mut etherscan_identifier); - } + handle_traces(result, &config, chain, self.label, self.verbose, self.debug).await?; - if self.debug { - let (sources, bytecode) = etherscan_identifier.get_compiled_contracts().await?; - run_debugger(result, decoder, bytecode, sources)?; - } else { - print_traces(&mut result, decoder, self.verbose).await?; - } Ok(()) } } - -fn run_debugger( - result: RunResult, - decoder: CallTraceDecoder, - known_contracts: BTreeMap, - sources: BTreeMap, -) -> eyre::Result<()> { - let calls: Vec = vec![result.debug]; - let flattened = calls.last().expect("we should have collected debug info").flatten(0); - let tui = Tui::new( - flattened, - 0, - decoder.contracts, - known_contracts.into_iter().map(|(id, artifact)| (id.name, artifact)).collect(), - sources - .into_iter() - .map(|(id, source)| { - let mut sources = BTreeMap::new(); - sources.insert(0, source); - (id.name, sources) - }) - .collect(), - Default::default(), - )?; - match tui.start().expect("Failed to start tui") { - TUIExitReason::CharExit => Ok(()), - } -} - -async fn print_traces( - result: &mut RunResult, - decoder: CallTraceDecoder, - verbose: bool, -) -> eyre::Result<()> { - if result.traces.is_empty() { - eyre::bail!("Unexpected error: No traces. Please report this as a bug: https://github.com/foundry-rs/foundry/issues/new?assignees=&labels=T-bug&template=BUG-FORM.yml"); - } - - println!("Traces:"); - for (_, trace) in &mut result.traces { - decoder.decode(trace).await; - if !verbose { - println!("{trace}"); - } else { - println!("{trace:#}"); - } - } - println!(); - - if result.success { - println!("{}", Paint::green("Transaction successfully executed.")); - } else { - println!("{}", Paint::red("Transaction failed.")); - } - - println!("Gas used: {}", result.gas_used); - Ok(()) -} - -struct RunResult { - pub success: bool, - pub traces: Traces, - pub debug: DebugArena, - pub gas_used: u64, -} diff --git a/cli/src/cmd/utils.rs b/cli/src/cmd/utils.rs index 6ab7890ee3862..118c4abb6a196 100644 --- a/cli/src/cmd/utils.rs +++ b/cli/src/cmd/utils.rs @@ -3,19 +3,28 @@ use ethers::{ abi::Abi, core::types::Chain, solc::{ - artifacts::{CompactBytecode, CompactDeployedBytecode}, + artifacts::{CompactBytecode, CompactDeployedBytecode, ContractBytecodeSome}, cache::{CacheEntry, SolFilesCache}, info::ContractInfo, utils::read_json_file, - Artifact, ProjectCompileOutput, + Artifact, ArtifactId, ProjectCompileOutput, }, }; use eyre::WrapErr; use forge::executor::opts::EvmOpts; use foundry_common::{cli_warn, fs, TestFunctionExt}; use foundry_config::{error::ExtractConfigError, figment::Figment, Chain as ConfigChain, Config}; -use std::{fmt::Write, path::PathBuf}; +use foundry_evm::{ + debug::DebugArena, + executor::{DeployResult, EvmError, ExecutionErr, RawCallResult}, + trace::{ + identifier::{EtherscanIdentifier, SignaturesIdentifier}, + CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces, + }, +}; +use std::{collections::BTreeMap, fmt::Write, path::PathBuf, str::FromStr}; use tracing::trace; +use ui::{TUIExitReason, Tui, Ui}; use yansi::Paint; /// Common trait for all cli commands @@ -299,3 +308,159 @@ pub fn read_constructor_args_file(constructor_args_path: PathBuf) -> eyre::Resul }; Ok(args) } + +/// A slimmed down return from the executor used for returning minimal trace + gas metering info +pub struct TraceResult { + pub success: bool, + pub traces: Traces, + pub debug: DebugArena, + pub gas_used: u64, +} + +impl From for TraceResult { + fn from(result: RawCallResult) -> Self { + let RawCallResult { gas_used, traces, reverted, debug, .. } = result; + + Self { + success: !reverted, + traces: vec![(TraceKind::Execution, traces.expect("traces is None"))], + debug: debug.unwrap_or_default(), + gas_used, + } + } +} + +impl From for TraceResult { + fn from(result: DeployResult) -> Self { + let DeployResult { gas_used, traces, debug, .. } = result; + + Self { + success: true, + traces: vec![(TraceKind::Execution, traces.expect("traces is None"))], + debug: debug.unwrap_or_default(), + gas_used, + } + } +} + +impl TryFrom for TraceResult { + type Error = EvmError; + + fn try_from(err: EvmError) -> Result { + match err { + EvmError::Execution(err) => { + let ExecutionErr { reverted, gas_used, traces, debug: run_debug, .. } = *err; + Ok(TraceResult { + success: !reverted, + traces: vec![(TraceKind::Execution, traces.expect("traces is None"))], + debug: run_debug.unwrap_or_default(), + gas_used, + }) + } + _ => Err(err), + } + } +} + +/// labels the traces, conditionally prints them or opens the debugger +pub async fn handle_traces( + mut result: TraceResult, + config: &Config, + chain: Option, + labels: Vec, + verbose: bool, + debug: bool, +) -> eyre::Result<()> { + let mut etherscan_identifier = EtherscanIdentifier::new(config, chain)?; + + let labeled_addresses = labels.iter().filter_map(|label_str| { + let mut iter = label_str.split(':'); + + if let Some(addr) = iter.next() { + if let (Ok(address), Some(label)) = + (ethers::types::Address::from_str(addr), iter.next()) + { + return Some((address, label.to_string())) + } + } + None + }); + + let mut decoder = CallTraceDecoderBuilder::new().with_labels(labeled_addresses).build(); + + decoder.add_signature_identifier(SignaturesIdentifier::new( + Config::foundry_cache_dir(), + config.offline, + )?); + + for (_, trace) in &mut result.traces { + decoder.identify(trace, &mut etherscan_identifier); + } + + if debug { + let (sources, bytecode) = etherscan_identifier.get_compiled_contracts().await?; + run_debugger(result, decoder, bytecode, sources)?; + } else { + print_traces(&mut result, decoder, verbose).await?; + } + + Ok(()) +} + +pub async fn print_traces( + result: &mut TraceResult, + decoder: CallTraceDecoder, + verbose: bool, +) -> eyre::Result<()> { + if result.traces.is_empty() { + panic!("No traces found") + } + + println!("Traces:"); + for (_, trace) in &mut result.traces { + decoder.decode(trace).await; + if !verbose { + println!("{trace}"); + } else { + println!("{trace:#}"); + } + } + println!(); + + if result.success { + println!("{}", Paint::green("Transaction successfully executed.")); + } else { + println!("{}", Paint::red("Transaction failed.")); + } + + println!("Gas used: {}", result.gas_used); + Ok(()) +} + +pub fn run_debugger( + result: TraceResult, + decoder: CallTraceDecoder, + known_contracts: BTreeMap, + sources: BTreeMap, +) -> eyre::Result<()> { + let calls: Vec = vec![result.debug]; + let flattened = calls.last().expect("we should have collected debug info").flatten(0); + let tui = Tui::new( + flattened, + 0, + decoder.contracts, + known_contracts.into_iter().map(|(id, artifact)| (id.name, artifact)).collect(), + sources + .into_iter() + .map(|(id, source)| { + let mut sources = BTreeMap::new(); + sources.insert(0, source); + (id.name, sources) + }) + .collect(), + Default::default(), + )?; + match tui.start().expect("Failed to start tui") { + TUIExitReason::CharExit => Ok(()), + } +} diff --git a/evm/src/trace/executor.rs b/evm/src/trace/executor.rs new file mode 100644 index 0000000000000..6d941aa4435ef --- /dev/null +++ b/evm/src/trace/executor.rs @@ -0,0 +1,65 @@ +use crate::{ + executor::{fork::CreateFork, opts::EvmOpts, Backend, Executor, ExecutorBuilder}, + utils::evm_spec, +}; +use ethers::solc::EvmVersion; +use foundry_config::Config; +use revm::primitives::Env; +use std::ops::{Deref, DerefMut}; + +/// A default executor with tracing enabled +pub struct TracingExecutor { + executor: Executor, +} + +impl TracingExecutor { + pub async fn new( + env: revm::primitives::Env, + fork: Option, + version: Option, + debug: bool, + ) -> Self { + let db = Backend::spawn(fork).await; + + // configures a bare version of the evm executor: no cheatcode inspector is enabled, + // tracing will be enabled only for the targeted transaction + let builder = ExecutorBuilder::default() + .with_config(env) + .with_spec(evm_spec(&version.unwrap_or_default())); + + let mut executor = builder.build(db); + + executor.set_tracing(true).set_debugger(debug); + + Self { executor } + } + + /// uses the fork block number from the config + pub async fn get_fork_material( + config: &Config, + mut evm_opts: EvmOpts, + ) -> eyre::Result<(Env, Option, Option)> { + evm_opts.fork_url = Some(config.get_rpc_url_or_localhost_http()?.into_owned()); + evm_opts.fork_block_number = config.fork_block_number; + + let env = evm_opts.evm_env().await?; + + let fork = evm_opts.get_fork(config, env.clone()); + + Ok((env, fork, evm_opts.get_remote_chain_id())) + } +} + +impl Deref for TracingExecutor { + type Target = Executor; + + fn deref(&self) -> &Self::Target { + &self.executor + } +} + +impl DerefMut for TracingExecutor { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.executor + } +} diff --git a/evm/src/trace/mod.rs b/evm/src/trace/mod.rs index 9f276479be3b4..23b5d1fa47a69 100644 --- a/evm/src/trace/mod.rs +++ b/evm/src/trace/mod.rs @@ -7,6 +7,7 @@ use ethers::{ core::utils::to_checksum, types::{Bytes, DefaultFrame, GethDebugTracingOptions, StructLog, H256, U256}, }; +pub use executor::TracingExecutor; use foundry_common::contracts::{ContractsByAddress, ContractsByArtifact}; use hashbrown::HashMap; use node::CallTraceNode; @@ -24,8 +25,9 @@ use yansi::{Color, Paint}; pub mod identifier; mod decoder; +mod executor; pub mod node; -mod utils; +pub mod utils; pub type Traces = Vec<(TraceKind, CallTraceArena)>;