From d8a4229fb1140af078b924a4913e26e228805931 Mon Sep 17 00:00:00 2001 From: Daniel Perez Date: Sat, 14 Sep 2024 21:53:38 +0100 Subject: [PATCH 1/4] Add trace_call function --- Cargo.lock | 20 ++++++++- Cargo.toml | 1 + src/interpreter/env.rs | 12 +++--- src/interpreter/functions/contract.rs | 58 +++++++++++++++++++++++++-- 4 files changed, 80 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f447e4..807a5f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -398,7 +398,7 @@ dependencies = [ "alloy-primitives", "alloy-rpc-client 0.1.0", "alloy-rpc-types 0.1.0", - "alloy-rpc-types-trace", + "alloy-rpc-types-trace 0.1.0", "alloy-transport 0.1.0", "async-stream", "async-trait", @@ -432,6 +432,7 @@ dependencies = [ "alloy-rpc-client 0.2.1", "alloy-rpc-types-anvil", "alloy-rpc-types-eth", + "alloy-rpc-types-trace 0.2.1", "alloy-signer-local", "alloy-transport 0.2.1", "alloy-transport-http 0.2.1", @@ -582,6 +583,7 @@ dependencies = [ "alloy-rpc-types-anvil", "alloy-rpc-types-engine 0.2.1", "alloy-rpc-types-eth", + "alloy-rpc-types-trace 0.2.1", "alloy-serde 0.2.1", "serde", ] @@ -663,6 +665,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "alloy-rpc-types-trace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a86eeb49ea0cc79f249faa1d35c20541bb1c317a59b5962cb07b1890355b0064" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde 0.2.1", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "alloy-serde" version = "0.1.0" @@ -5192,7 +5208,7 @@ source = "git+https://github.com/paradigmxyz/revm-inspectors?rev=0d3f1f4#0d3f1f4 dependencies = [ "alloy-primitives", "alloy-rpc-types 0.1.0", - "alloy-rpc-types-trace", + "alloy-rpc-types-trace 0.1.0", "alloy-sol-types", "anstyle", "colorchoice", diff --git a/Cargo.toml b/Cargo.toml index 146bfc2..34f29b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ alloy = { version = "0.2.0", features = [ "signer-keystore", "node-bindings", "provider-anvil-api", + "provider-debug-api", ] } itertools = "0.13.0" rpassword = "7.3.1" diff --git a/src/interpreter/env.rs b/src/interpreter/env.rs index 187c2d3..31889b4 100644 --- a/src/interpreter/env.rs +++ b/src/interpreter/env.rs @@ -125,21 +125,21 @@ impl Env { } pub async fn get_chain_id(&self) -> Result { - self.provider - .root() - .get_chain_id() - .await - .map_err(Into::into) + self.provider.get_chain_id().await.map_err(Into::into) } pub fn fork(&mut self, url: &str) -> Result<()> { - let anvil = Anvil::new().fork(url).try_spawn()?; + let anvil = Anvil::new().arg("--steps-tracing").fork(url).try_spawn()?; let endpoint = anvil.endpoint(); self.set_provider_url(endpoint.as_str())?; self.anvil = Some(anvil); Ok(()) } + pub fn is_fork(&self) -> bool { + self.anvil.is_some() + } + pub async fn impersonate(&mut self, address: Address) -> Result<()> { if let Some(addr) = self.impersonating { bail!("already impersonating {}", addr); diff --git a/src/interpreter/functions/contract.rs b/src/interpreter/functions/contract.rs index e7cc341..69b1898 100644 --- a/src/interpreter/functions/contract.rs +++ b/src/interpreter/functions/contract.rs @@ -2,12 +2,15 @@ use std::{hash::Hash, sync::Arc}; use alloy::{ contract::{CallBuilder, ContractInstance, Interface}, - eips::BlockId, + eips::{BlockId, BlockNumberOrTag}, json_abi::StateMutability, network::{Network, TransactionBuilder}, primitives::{keccak256, Address, FixedBytes, U256}, - providers::Provider, - rpc::types::{TransactionInput, TransactionRequest}, + providers::{ext::DebugApi, Provider}, + rpc::types::{ + trace::geth::GethDebugTracingCallOptions, BlockTransactionsKind, TransactionInput, + TransactionRequest, + }, transports::Transport, }; use anyhow::{anyhow, bail, Result}; @@ -23,6 +26,7 @@ pub enum ContractCallMode { Default, Encode, Call, + TraceCall, Send, } @@ -32,6 +36,7 @@ impl std::fmt::Display for ContractCallMode { ContractCallMode::Default => write!(f, "default"), ContractCallMode::Encode => write!(f, "encode"), ContractCallMode::Call => write!(f, "call"), + ContractCallMode::TraceCall => write!(f, "trace_call"), ContractCallMode::Send => write!(f, "send"), } } @@ -44,6 +49,7 @@ impl TryFrom<&str> for ContractCallMode { match s { "encode" => Ok(ContractCallMode::Encode), "call" => Ok(ContractCallMode::Call), + "trace_call" => Ok(ContractCallMode::TraceCall), "send" => Ok(ContractCallMode::Send), _ => bail!("{} does not exist for contract call", s), } @@ -229,6 +235,8 @@ impl FunctionDef for ContractFunction { if self.mode == ContractCallMode::Encode { let encoded = func.calldata(); Ok(Value::Bytes(encoded[..].to_vec())) + } else if self.mode == ContractCallMode::TraceCall { + _execute_contract_trace_call(&addr, func, env).await } else if self.mode == ContractCallMode::Call || (self.mode == ContractCallMode::Default && is_view) { @@ -327,3 +335,47 @@ where Ok(Value::Tuple(return_values)) } } + +async fn _execute_contract_trace_call( + addr: &Address, + func: CallBuilder, + env: &mut Env, +) -> Result +where + T: Transport + Clone, + P: Provider, + N: Network, +{ + let data = func.calldata(); + let input = TransactionInput::new(data.clone()); + let mut tx_req = TransactionRequest::default().with_to(*addr).input(input); + if let Some(acc) = env.get_default_sender() { + tx_req = tx_req.with_from(acc); + } + + let (provider, previous_url) = if env.is_fork() { + (env.get_provider(), None) + } else { + let url = env.get_rpc_url(); + env.fork(url.as_str())?; + (env.get_provider(), Some(url)) + }; + + let options = GethDebugTracingCallOptions::default(); + let block_tag = env.block(); + let block = provider + .get_block(block_tag, BlockTransactionsKind::Hashes) + .await? + .ok_or(anyhow!("could not get block {:?}", block_tag))?; + let block_num = + BlockNumberOrTag::Number(block.header.number.ok_or(anyhow!("no block number"))?); + + let maybe_tx = provider.debug_trace_call(tx_req, block_num, options).await; + if let Some(url) = previous_url { + env.set_provider_url(url.as_str())?; + } + let traces = maybe_tx?; + + println!("{:?}", traces); + Ok(Value::Null) +} From 24434dccbcee3491781a5f18f9d6dcdf7e971be2 Mon Sep 17 00:00:00 2001 From: Daniel Perez Date: Sun, 15 Sep 2024 21:29:46 +0100 Subject: [PATCH 2/4] Add logic to decode error --- src/interpreter/builtins/abi.rs | 73 +++++++++++++++++++++++---- src/interpreter/builtins/mod.rs | 1 + src/interpreter/env.rs | 41 ++++++++++++--- src/interpreter/functions/contract.rs | 60 +++++++++++++++------- src/interpreter/mod.rs | 1 + src/interpreter/utils.rs | 25 +++++++-- 6 files changed, 163 insertions(+), 38 deletions(-) diff --git a/src/interpreter/builtins/abi.rs b/src/interpreter/builtins/abi.rs index e727be4..9c692a2 100644 --- a/src/interpreter/builtins/abi.rs +++ b/src/interpreter/builtins/abi.rs @@ -4,39 +4,85 @@ use crate::interpreter::{ functions::{FunctionDef, FunctionParam, SyncMethod}, ContractInfo, Env, Type, Value, }; -use alloy::dyn_abi::{DynSolType, DynSolValue, JsonAbiExt}; -use anyhow::{bail, Result}; +use alloy::{ + dyn_abi::{DynSolType, DynSolValue, JsonAbiExt}, + json_abi::{self, JsonAbi}, + primitives::FixedBytes, +}; +use anyhow::{anyhow, bail, Result}; use lazy_static::lazy_static; -fn abi_decode_calldata(_env: &mut Env, receiver: &Value, args: &[Value]) -> Result { +trait Decodable: JsonAbiExt { + fn signature(&self) -> String; + fn selector(&self) -> FixedBytes<4>; +} + +impl Decodable for json_abi::Function { + fn signature(&self) -> String { + json_abi::Function::signature(self) + } + + fn selector(&self) -> FixedBytes<4> { + json_abi::Function::selector(self) + } +} +impl Decodable for json_abi::Error { + fn signature(&self) -> String { + json_abi::Error::signature(self) + } + + fn selector(&self) -> FixedBytes<4> { + json_abi::Error::selector(self) + } +} + +fn _generic_abi_decode( + receiver: &Value, + args: &[Value], + type_: &str, + get_options: F, +) -> Result +where + F: Fn(&JsonAbi) -> Vec<&D>, +{ let (name, abi) = match receiver { Value::TypeObject(Type::Contract(ContractInfo(name, abi))) => (name, abi), - _ => bail!("decode function expects contract type as first argument"), + _ => bail!("decode {} expects contract type as first argument", type_), }; let data = match args.first() { Some(Value::Bytes(bytes)) => bytes, - _ => bail!("decode function expects bytes as argument"), + _ => bail!("decode {} expects bytes as argument", type_), }; let selector = alloy::primitives::FixedBytes::<4>::from_slice(&data[..4]); - let function = abi - .functions() + let options = get_options(abi); + let error = options + .iter() .find(|f| f.selector() == selector) - .ok_or(anyhow::anyhow!( - "function with selector {} not found for {}", + .ok_or(anyhow!( + "{} with selector {} not found for {}", + type_, selector, name ))?; - let decoded = function.abi_decode_input(&data[4..], true)?; + let decoded = error.abi_decode_input(&data[4..], true)?; let values = decoded .into_iter() .map(Value::try_from) .collect::>>()?; Ok(Value::Tuple(vec![ - Value::Str(function.signature()), + Value::Str(error.signature()), Value::Tuple(values), ])) } +fn abi_decode_calldata(_env: &mut Env, receiver: &Value, args: &[Value]) -> Result { + _generic_abi_decode(receiver, args, "function", |abi| abi.functions().collect()) +} + +fn abi_decode_error(_env: &mut Env, receiver: &Value, args: &[Value]) -> Result { + _generic_abi_decode(receiver, args, "error", |abi| abi.errors().collect()) +} + fn value_to_soltype(value: &Value) -> Result { match value { Value::TypeObject(ty) => Ok(DynSolType::try_from(ty.clone())?), @@ -101,6 +147,11 @@ lazy_static! { abi_decode_calldata, vec![vec![FunctionParam::new("calldata", Type::Bytes)]] ); + pub static ref ABI_DECODE_ERROR: Arc = SyncMethod::arc( + "decode_error", + abi_decode_error, + vec![vec![FunctionParam::new("data", Type::Bytes)]] + ); } #[cfg(test)] diff --git a/src/interpreter/builtins/mod.rs b/src/interpreter/builtins/mod.rs index 84d3fb9..c365732 100644 --- a/src/interpreter/builtins/mod.rs +++ b/src/interpreter/builtins/mod.rs @@ -117,6 +117,7 @@ lazy_static! { let mut contract_methods = HashMap::new(); contract_methods.insert("decode".to_string(), abi::ABI_DECODE_CALLDATA.clone()); + contract_methods.insert("decode_error".to_string(), abi::ABI_DECODE_ERROR.clone()); m.insert(NonParametricType::Contract, contract_methods); let mut abi_methods = HashMap::new(); diff --git a/src/interpreter/env.rs b/src/interpreter/env.rs index 31889b4..eba1538 100644 --- a/src/interpreter/env.rs +++ b/src/interpreter/env.rs @@ -8,10 +8,10 @@ use url::Url; use alloy::{ eips::BlockId, - json_abi::{Event, JsonAbi}, + json_abi, network::{AnyNetwork, Ethereum, EthereumWallet, NetworkWallet, TxSigner}, node_bindings::{Anvil, AnvilInstance}, - primitives::{Address, B256}, + primitives::{Address, FixedBytes, B256}, providers::{ ext::AnvilApi, fillers::{FillProvider, JoinFill, RecommendedFiller}, @@ -42,7 +42,9 @@ pub struct Env { is_wallet_connected: bool, ledger: Option>>, block_id: BlockId, - events: HashMap, + events: HashMap, + errors: HashMap, json_abi::Error>, + functions: HashMap, json_abi::Function>, impersonating: Option
, anvil: Option, pub config: Config, @@ -65,6 +67,8 @@ impl Env { ledger: None, block_id: BlockId::latest(), events: HashMap::new(), + errors: HashMap::new(), + functions: HashMap::new(), impersonating: None, anvil: None, config, @@ -87,27 +91,49 @@ impl Env { self.config.debug } - pub fn get_event(&self, selector: &B256) -> Option<&Event> { + pub fn get_event(&self, selector: &B256) -> Option<&json_abi::Event> { self.events.get(selector) } - pub fn add_contract(&mut self, name: &str, abi: JsonAbi) -> ContractInfo { + pub fn get_error(&self, selector: &FixedBytes<4>) -> Option<&json_abi::Error> { + self.errors.get(selector) + } + + pub fn get_function(&self, selector: &FixedBytes<4>) -> Option<&json_abi::Function> { + self.functions.get(selector) + } + + pub fn add_contract(&mut self, name: &str, abi: json_abi::JsonAbi) -> ContractInfo { for event in abi.events() { self.register_event(event.clone()); } + for error in abi.errors() { + self.register_error(error.clone()); + } + for function in abi.functions() { + self.register_function(function.clone()); + } let contract_info = ContractInfo(name.to_string(), abi); self.set_type(name, Type::Contract(contract_info.clone())); contract_info } - pub fn list_events(&mut self) -> Vec<&Event> { + pub fn list_events(&mut self) -> Vec<&json_abi::Event> { self.events.values().collect() } - pub fn register_event(&mut self, event: Event) { + pub fn register_event(&mut self, event: json_abi::Event) { self.events.insert(event.selector(), event); } + pub fn register_error(&mut self, error: json_abi::Error) { + self.errors.insert(error.selector(), error); + } + + pub fn register_function(&mut self, function: json_abi::Function) { + self.functions.insert(function.selector(), function); + } + pub fn set_block(&mut self, block: BlockId) { self.block_id = block; } @@ -316,6 +342,7 @@ impl Env { .filler(wallet_filler) .on_http(rpc_url); self.provider = provider; + self.anvil = None; Ok(()) } diff --git a/src/interpreter/functions/contract.rs b/src/interpreter/functions/contract.rs index 69b1898..b6f0a2e 100644 --- a/src/interpreter/functions/contract.rs +++ b/src/interpreter/functions/contract.rs @@ -5,7 +5,7 @@ use alloy::{ eips::{BlockId, BlockNumberOrTag}, json_abi::StateMutability, network::{Network, TransactionBuilder}, - primitives::{keccak256, Address, FixedBytes, U256}, + primitives::{keccak256, Address, Bytes, FixedBytes, U256}, providers::{ext::DebugApi, Provider}, rpc::types::{ trace::geth::GethDebugTracingCallOptions, BlockTransactionsKind, TransactionInput, @@ -17,7 +17,9 @@ use anyhow::{anyhow, bail, Result}; use futures::{future::BoxFuture, FutureExt}; use itertools::Itertools; -use crate::interpreter::{types::HashableIndexMap, ContractInfo, Env, Type, Value}; +use crate::interpreter::{ + types::HashableIndexMap, utils::decode_error, ContractInfo, Env, Type, Value, +}; use super::{Function, FunctionDef, FunctionParam}; @@ -236,7 +238,7 @@ impl FunctionDef for ContractFunction { let encoded = func.calldata(); Ok(Value::Bytes(encoded[..].to_vec())) } else if self.mode == ContractCallMode::TraceCall { - _execute_contract_trace_call(&addr, func, env).await + _execute_contract_trace_call(&addr, func, &call_options, env).await } else if self.mode == ContractCallMode::Call || (self.mode == ContractCallMode::Default && is_view) { @@ -305,6 +307,27 @@ where Ok(Value::Transaction(*tx.tx_hash())) } +fn _decode_output( + return_bytes: Bytes, + func: CallBuilder, +) -> Result +where + T: Transport + Clone, + P: Provider, + N: Network, +{ + let result = func.decode_output(return_bytes, true)?; + let return_values = result + .into_iter() + .map(Value::try_from) + .collect::>>()?; + if return_values.len() == 1 { + Ok(return_values.into_iter().next().unwrap()) + } else { + Ok(Value::Tuple(return_values)) + } +} + async fn _execute_contract_call( addr: &Address, func: CallBuilder, @@ -324,21 +347,13 @@ where let block = opts.block.unwrap_or(env.block()); let provider = env.get_provider(); let return_bytes = provider.call(&tx_req).block(block).await?; - let result = func.decode_output(return_bytes, true)?; - let return_values = result - .into_iter() - .map(Value::try_from) - .collect::>>()?; - if return_values.len() == 1 { - Ok(return_values.into_iter().next().unwrap()) - } else { - Ok(Value::Tuple(return_values)) - } + _decode_output(return_bytes, func) } async fn _execute_contract_trace_call( addr: &Address, func: CallBuilder, + opts: &CallOptions, env: &mut Env, ) -> Result where @@ -349,7 +364,10 @@ where let data = func.calldata(); let input = TransactionInput::new(data.clone()); let mut tx_req = TransactionRequest::default().with_to(*addr).input(input); - if let Some(acc) = env.get_default_sender() { + + if let Some(from_) = opts.from { + tx_req = tx_req.with_from(from_); + } else if let Some(acc) = env.get_default_sender() { tx_req = tx_req.with_from(acc); } @@ -374,8 +392,16 @@ where if let Some(url) = previous_url { env.set_provider_url(url.as_str())?; } - let traces = maybe_tx?; + let traces = maybe_tx?.try_into_default_frame()?; - println!("{:?}", traces); - Ok(Value::Null) + if traces.failed { + let err = traces.return_value; + if let Ok(err_val) = decode_error(env, &err) { + bail!("revert: {}", err_val); + } else { + bail!("revert: {:?}", err); + } + } else { + _decode_output(traces.return_value, func) + } } diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index 297eda8..f41a2dc 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -6,6 +6,7 @@ mod functions; #[allow(clippy::module_inception)] mod interpreter; mod parsing; +pub mod tracing; mod types; mod utils; mod value; diff --git a/src/interpreter/utils.rs b/src/interpreter/utils.rs index c93d833..65f27f3 100644 --- a/src/interpreter/utils.rs +++ b/src/interpreter/utils.rs @@ -1,12 +1,12 @@ -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; use indexmap::IndexMap; use itertools::{Either, Itertools}; use std::str::FromStr; use alloy::{ - dyn_abi::EventExt, + dyn_abi::{EventExt, JsonAbiExt}, json_abi::Event, - primitives::U256, + primitives::{FixedBytes, U256}, rpc::types::{Log, TransactionReceipt}, }; @@ -82,6 +82,25 @@ pub fn decode_log_args(log: &Log, event: &Event) -> Result { )) } +pub fn decode_error(env: &Env, data: &[u8]) -> Result { + if data.len() < 4 { + bail!("error data is too short"); + } + let selector = FixedBytes::from_slice(&data[..4]); + let error = env + .get_error(&selector) + .ok_or(anyhow!("error with selector {} not found", selector))?; + let decoded = error.abi_decode_input(&data[4..], true)?; + let values = decoded + .into_iter() + .map(Value::try_from) + .collect::>>()?; + Ok(Value::Tuple(vec![ + Value::Str(error.signature()), + Value::Tuple(values), + ])) +} + pub fn log_to_value(env: &Env, log: Log) -> Result { let mut fields = IndexMap::new(); fields.insert("address".to_string(), Value::Addr(log.address())); From 7443885670ce32e0ce4289cc9ac32a49fcf335bf Mon Sep 17 00:00:00 2001 From: Daniel Perez Date: Sun, 15 Sep 2024 22:22:28 +0100 Subject: [PATCH 3/4] Use call trace and add basic trace formatting --- Cargo.lock | 80 +++++++++++++++++-- Cargo.toml | 1 + src/interpreter/env.rs | 12 +++ src/interpreter/functions/contract.rs | 37 ++++++--- src/interpreter/tracing.rs | 109 ++++++++++++++++++++++++++ src/interpreter/value.rs | 12 +++ 6 files changed, 232 insertions(+), 19 deletions(-) create mode 100644 src/interpreter/tracing.rs diff --git a/Cargo.lock b/Cargo.lock index 807a5f5..9cd5ec4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1840,7 +1840,7 @@ dependencies = [ "anstyle", "clap_lex", "strsim 0.11.1", - "terminal_size", + "terminal_size 0.3.0", "unicase", "unicode-width", ] @@ -2488,6 +2488,7 @@ dependencies = [ "serde_json", "shellexpand", "solang-parser", + "textwrap", "tokio", "url", ] @@ -2688,7 +2689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" dependencies = [ "cfg-if", - "rustix", + "rustix 0.38.34", "windows-sys 0.48.0", ] @@ -2699,7 +2700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e5768da2206272c81ef0b5e951a41862938a6070da63bcea197899942d3b947" dependencies = [ "cfg-if", - "rustix", + "rustix 0.38.34", "windows-sys 0.52.0", ] @@ -3235,7 +3236,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8" dependencies = [ - "rustix", + "rustix 0.38.34", "windows-sys 0.52.0", ] @@ -3819,6 +3820,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -4069,6 +4081,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -5422,6 +5440,20 @@ dependencies = [ "semver 1.0.23", ] +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "0.38.34" @@ -5431,7 +5463,7 @@ dependencies = [ "bitflags 2.6.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.14", "windows-sys 0.52.0", ] @@ -5969,6 +6001,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.5.7" @@ -6185,7 +6223,7 @@ dependencies = [ "cfg-if", "fastrand", "once_cell", - "rustix", + "rustix 0.38.34", "windows-sys 0.52.0", ] @@ -6200,16 +6238,38 @@ dependencies = [ "winapi", ] +[[package]] +name = "terminal_size" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +dependencies = [ + "rustix 0.37.27", + "windows-sys 0.48.0", +] + [[package]] name = "terminal_size" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "rustix", + "rustix 0.38.34", "windows-sys 0.48.0", ] +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "terminal_size 0.2.6", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.63" @@ -6709,6 +6769,12 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.23" diff --git a/Cargo.toml b/Cargo.toml index 34f29b6..ce22e79 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ semver = "1.0.23" shellexpand = { version = "3.1.0", features = ["path"] } indexmap = "2.2.6" lazy_static = "1.5.0" +textwrap = { version = "0.16.1", features = ["terminal_size"] } [build-dependencies] git2-rs = { version = "0.19.0", package = "git2", default-features = false } diff --git a/src/interpreter/env.rs b/src/interpreter/env.rs index eba1538..b6d8f84 100644 --- a/src/interpreter/env.rs +++ b/src/interpreter/env.rs @@ -42,6 +42,7 @@ pub struct Env { is_wallet_connected: bool, ledger: Option>>, block_id: BlockId, + contract_names: HashMap, events: HashMap, errors: HashMap, json_abi::Error>, functions: HashMap, json_abi::Function>, @@ -66,6 +67,7 @@ impl Env { is_wallet_connected: false, ledger: None, block_id: BlockId::latest(), + contract_names: HashMap::new(), events: HashMap::new(), errors: HashMap::new(), functions: HashMap::new(), @@ -258,6 +260,10 @@ impl Env { Vec::from_iter(vars) } + pub fn get_contract_name(&self, addr: &Address) -> Option<&String> { + self.contract_names.get(addr) + } + pub fn get_var(&self, name: &str) -> Option<&Value> { for scope in self.variables.iter().rev() { if let Some(value) = scope.get(name) { @@ -286,7 +292,13 @@ impl Env { } pub fn set_var(&mut self, name: &str, value: Value) { + let is_top_level = self.variables.len() == 1; let scope = self.variables.last_mut().unwrap(); + if is_top_level { + if let Value::Contract(ContractInfo(name, _), addr) = &value { + self.contract_names.insert(*addr, name.clone()); + } + } scope.insert(name.to_string(), value); } diff --git a/src/interpreter/functions/contract.rs b/src/interpreter/functions/contract.rs index b6f0a2e..c6d0f35 100644 --- a/src/interpreter/functions/contract.rs +++ b/src/interpreter/functions/contract.rs @@ -8,8 +8,8 @@ use alloy::{ primitives::{keccak256, Address, Bytes, FixedBytes, U256}, providers::{ext::DebugApi, Provider}, rpc::types::{ - trace::geth::GethDebugTracingCallOptions, BlockTransactionsKind, TransactionInput, - TransactionRequest, + trace::geth::{self, GethDebugTracingCallOptions}, + BlockTransactionsKind, TransactionInput, TransactionRequest, }, transports::Transport, }; @@ -18,7 +18,8 @@ use futures::{future::BoxFuture, FutureExt}; use itertools::Itertools; use crate::interpreter::{ - types::HashableIndexMap, utils::decode_error, ContractInfo, Env, Type, Value, + tracing::format_call_frame, types::HashableIndexMap, utils::decode_error, ContractInfo, Env, + Type, Value, }; use super::{Function, FunctionDef, FunctionParam}; @@ -379,7 +380,13 @@ where (env.get_provider(), Some(url)) }; - let options = GethDebugTracingCallOptions::default(); + let mut options = GethDebugTracingCallOptions::default(); + let mut tracing_options = options.tracing_options.clone(); + tracing_options = tracing_options.with_tracer(geth::GethDebugTracerType::BuiltInTracer( + geth::GethDebugBuiltInTracerType::CallTracer, + )); + options = options.with_tracing_options(tracing_options); + // options.with_tracing_options(options) let block_tag = env.block(); let block = provider .get_block(block_tag, BlockTransactionsKind::Hashes) @@ -392,16 +399,22 @@ where if let Some(url) = previous_url { env.set_provider_url(url.as_str())?; } - let traces = maybe_tx?.try_into_default_frame()?; + let call_frame = maybe_tx?.try_into_call_frame()?; - if traces.failed { - let err = traces.return_value; - if let Ok(err_val) = decode_error(env, &err) { - bail!("revert: {}", err_val); - } else { - bail!("revert: {:?}", err); + println!("{}", format_call_frame(env, &call_frame)); + + if let Some(err) = call_frame.error { + if let Some(output) = call_frame.output { + if let Ok(err_val) = decode_error(env, &output) { + bail!("revert: {}", err_val); + } else { + bail!("revert: {}", output); + } } + bail!("revert: {}", err); + } else if let Some(output) = call_frame.output { + _decode_output(output, func) } else { - _decode_output(traces.return_value, func) + Ok(Value::Null) } } diff --git a/src/interpreter/tracing.rs b/src/interpreter/tracing.rs new file mode 100644 index 0000000..0667988 --- /dev/null +++ b/src/interpreter/tracing.rs @@ -0,0 +1,109 @@ +use alloy::{ + dyn_abi::{FunctionExt, JsonAbiExt}, + json_abi::Function, + primitives::{Bytes, FixedBytes}, + rpc::types::trace::geth::CallFrame, +}; +use anyhow::Result; +use itertools::Itertools; + +use crate::interpreter::utils::decode_error; + +use super::{Env, Value}; + +fn try_format_func( + env: &Env, + func: &Function, + input: &[u8], + output: &Option, + is_error: bool, +) -> Result { + let decoded = func.abi_decode_input(input, true)?; + let values = Value::try_from(decoded)?; + let result = format!("{}{}", func.name, values); + if let Some(output) = output { + let value_output = if is_error { + decode_error(env, output)? + } else { + let decoded = func.abi_decode_output(output, true)?; + if decoded.len() == 1 { + Value::try_from(decoded[0].clone())? + } else { + Value::try_from(decoded)? + } + }; + Ok(format!("{} -> {}", result, value_output)) + } else { + Ok(result) + } +} + +fn get_formatted_function( + env: &Env, + input: &Bytes, + output: &Option, + is_error: bool, +) -> String { + if input.len() >= 4 { + let selector = FixedBytes::<4>::from_slice(&input[..4]); + if let Some(func) = env.get_function(&selector) { + if let Ok(result) = try_format_func(env, func, &input[4..], output, is_error) { + return result; + } + } + } + if let Some(output) = output { + format!("{} -> {}", input, output) + } else { + format!("{}", input) + } +} + +fn get_formatted_call(env: &Env, frame: &CallFrame) -> String { + let mut formatted = "".to_string(); + if let Some(addr) = frame.to { + if let Some(contract) = env.get_contract_name(&addr) { + formatted.push_str(&format!("{}({})", &contract, addr)); + } else { + formatted.push_str(&format!("{}", addr)); + } + } + formatted.push_str("::"); + formatted.push_str(&get_formatted_function( + env, + &frame.input, + &frame.output, + frame.error.is_some(), + )); + + formatted +} + +fn format_call( + env: &Env, + frame: &CallFrame, + depth: usize, + wrap_opts: &textwrap::Options, +) -> String { + let indent = format!("{:indent$}", "", indent = depth * 4); + let subsequent_indent = format!("{:indent$}", "", indent = depth * 4 + 2); + let opts = wrap_opts + .clone() + .initial_indent(&indent) + .subsequent_indent(&subsequent_indent); + let call_str = get_formatted_call(env, frame); + let rows = textwrap::wrap(&call_str, opts); + let mut result = rows.iter().join("\n"); + + for call in &frame.calls { + result.push('\n'); + result.push_str(&format_call(env, call, depth + 1, wrap_opts)); + } + + result +} + +pub fn format_call_frame(env: &Env, frame: &CallFrame) -> String { + let wrap_opts = textwrap::Options::new(textwrap::termwidth() - 16).break_words(true); + format_call(env, frame, 0, &wrap_opts) +} diff --git a/src/interpreter/value.rs b/src/interpreter/value.rs index 53d1537..5529d4b 100644 --- a/src/interpreter/value.rs +++ b/src/interpreter/value.rs @@ -189,6 +189,18 @@ impl TryFrom for Value { } } +impl TryFrom> for Value { + type Error = anyhow::Error; + + fn try_from(value: Vec) -> std::result::Result { + let values = value + .into_iter() + .map(Value::try_from) + .collect::>>()?; + Ok(Value::Tuple(values)) + } +} + impl From for Value { fn from(n: i32) -> Self { Value::Int(n.try_into().unwrap(), 256) From c2e18fff880c147a2f73a5fc92eaa2e0376a01a2 Mon Sep 17 00:00:00 2001 From: Daniel Perez Date: Mon, 16 Sep 2024 00:12:09 +0100 Subject: [PATCH 4/4] Update docs --- CHANGELOG.md | 1 + docs/src/interacting_with_contracts.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 683681d..7226332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Add `repl.fork` to start run and use an Anvil instance as a fork of the current URL * Add `repl.startPrank` / `repl.stopPrank` to start/stop impersonating an address +* Add `FUNC.trace_call` method to contract functions ### Other changes diff --git a/docs/src/interacting_with_contracts.md b/docs/src/interacting_with_contracts.md index a86d340..beeea97 100644 --- a/docs/src/interacting_with_contracts.md +++ b/docs/src/interacting_with_contracts.md @@ -12,6 +12,7 @@ Sending a transaction requires an [account to be loaded](./account_management.md The behavior can be changed by using one of the following method on the returned function object: * `call`: Call the function and return the result +* `trace_call`: Same as call but also prints the trace of the call (also potentially shows better error messages) * `send`: Sends a transaction to the function and return the result * `encode`: ABI-encodes the function call