From 8b651184def82c8b23663e282694ecc6302b237c Mon Sep 17 00:00:00 2001 From: Jon-Becker Date: Fri, 8 Dec 2023 20:36:12 -0500 Subject: [PATCH] feat(inspect): tracing, `Parameterize` trait --- cli/src/main.rs | 5 + common/src/ether/rpc.rs | 32 +++-- common/src/resources/transpose.rs | 59 +++++++- common/src/utils/hex.rs | 43 ++++++ common/src/utils/io/file.rs | 9 +- common/src/utils/io/logging.rs | 42 ++++++ common/src/utils/io/types.rs | 192 ++++++++++++++++++++++++++ common/src/utils/mod.rs | 1 + common/src/utils/strings.rs | 29 ---- core/src/decode/core/abi.rs | 1 + core/src/decode/mod.rs | 31 +++-- core/src/inspect/core/mod.rs | 1 + core/src/inspect/core/storage.rs | 3 + core/src/inspect/core/tracing.rs | 219 ++++++++++++++++++++++++++++++ core/src/inspect/mod.rs | 131 ++++-------------- core/tests/test_decode.rs | 6 + 16 files changed, 640 insertions(+), 164 deletions(-) create mode 100644 common/src/utils/hex.rs create mode 100644 core/src/inspect/core/mod.rs create mode 100644 core/src/inspect/core/storage.rs create mode 100644 core/src/inspect/core/tracing.rs diff --git a/cli/src/main.rs b/cli/src/main.rs index 14aeb727..246cb3e3 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -344,6 +344,11 @@ async fn main() -> Result<(), Box> { cmd.rpc_url = configuration.rpc_url; } + // if the user has not specified a transpose api key, use the default + if cmd.transpose_api_key.is_none() { + cmd.transpose_api_key = Some(configuration.transpose_api_key); + } + inspect(cmd).await?; } diff --git a/common/src/ether/rpc.rs b/common/src/ether/rpc.rs index 4f507064..7d0a8f17 100644 --- a/common/src/ether/rpc.rs +++ b/common/src/ether/rpc.rs @@ -39,7 +39,7 @@ pub async fn chain_id(rpc_url: &str) -> Result> // make sure the RPC provider isn't empty if rpc_url.is_empty() { logger.error("reading on-chain data requires an RPC provider. Use `heimdall --help` for more information."); - std::process::exit(1); + return Err(backoff::Error::Permanent(())) } // create new provider @@ -47,7 +47,7 @@ pub async fn chain_id(rpc_url: &str) -> Result> Ok(provider) => provider, Err(_) => { logger.error(&format!("failed to connect to RPC provider '{}' .", &rpc_url)); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }; @@ -107,7 +107,7 @@ pub async fn get_code( // make sure the RPC provider isn't empty if rpc_url.is_empty() { logger.error("reading on-chain data requires an RPC provider. Use `heimdall --help` for more information."); - std::process::exit(1); + return Err(backoff::Error::Permanent(())) } // create new provider @@ -115,7 +115,7 @@ pub async fn get_code( Ok(provider) => provider, Err(_) => { logger.error(&format!("failed to connect to RPC provider '{}' .", &rpc_url)); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }; @@ -124,7 +124,7 @@ pub async fn get_code( Ok(address) => address, Err(_) => { logger.error(&format!("failed to parse address '{}' .", &contract_address)); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }; @@ -181,7 +181,7 @@ pub async fn get_transaction( // make sure the RPC provider isn't empty if rpc_url.is_empty() { logger.error("reading on-chain data requires an RPC provider. Use `heimdall --help` for more information."); - std::process::exit(1); + return Err(backoff::Error::Permanent(())); } // create new provider @@ -189,7 +189,7 @@ pub async fn get_transaction( Ok(provider) => provider, Err(_) => { logger.error(&format!("failed to connect to RPC provider '{}' .", &rpc_url)); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }; @@ -198,7 +198,7 @@ pub async fn get_transaction( Ok(transaction_hash) => transaction_hash, Err(_) => { logger.error(&format!("failed to parse transaction hash '{}' .", &transaction_hash)); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }; @@ -208,7 +208,7 @@ pub async fn get_transaction( Some(tx) => tx, None => { logger.error(&format!("transaction '{}' doesn't exist.", &transaction_hash)); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }, Err(_) => { @@ -265,7 +265,7 @@ pub async fn get_storage_diff( Ok(provider) => provider, Err(_) => { logger.error(&format!("failed to connect to RPC provider '{}' .", &rpc_url)); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }; @@ -277,7 +277,7 @@ pub async fn get_storage_diff( "failed to parse transaction hash '{}' .", &transaction_hash )); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }; @@ -293,7 +293,7 @@ pub async fn get_storage_diff( &transaction_hash )); logger.error(&format!("error: '{e}' .")); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }; @@ -316,6 +316,7 @@ pub async fn get_storage_diff( }, ) .await + .map_err(|_| Box::from("failed to get storage diff")) } /// Get the raw trace data of the provided transaction hash @@ -350,7 +351,7 @@ pub async fn get_trace( Ok(provider) => provider, Err(_) => { logger.error(&format!("failed to connect to RPC provider '{}' .", &rpc_url)); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }; @@ -362,7 +363,7 @@ pub async fn get_trace( "failed to parse transaction hash '{}' .", &transaction_hash )); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }; @@ -381,7 +382,7 @@ pub async fn get_trace( &transaction_hash )); logger.error(&format!("error: '{e}' .")); - std::process::exit(1) + return Err(backoff::Error::Permanent(())) } }; @@ -391,6 +392,7 @@ pub async fn get_trace( }, ) .await + .map_err(|_| Box::from("failed to get trace")) } // TODO: add tests diff --git a/common/src/resources/transpose.rs b/common/src/resources/transpose.rs index 930d9c77..4688cc1d 100644 --- a/common/src/resources/transpose.rs +++ b/common/src/resources/transpose.rs @@ -21,7 +21,7 @@ struct TransposeResponse { } /// executes a transpose SQL query and returns the response -async fn _call_transpose(query: &str, api_key: &str) -> Option { +async fn call_transpose(query: &str, api_key: &str) -> Option { // get a new logger let logger = Logger::default(); @@ -115,7 +115,7 @@ pub async fn get_transaction_list( bounds.1 ); - let response = match _call_transpose(&query, api_key).await { + let response = match call_transpose(&query, api_key).await { Some(response) => response, None => { logger.error("failed to get transaction list from Transpose"); @@ -197,7 +197,7 @@ pub async fn get_contract_creation( "{{\"sql\":\"SELECT block_number, transaction_hash FROM {chain}.transactions WHERE TIMESTAMP = ( SELECT created_timestamp FROM {chain}.accounts WHERE address = '{address}' ) AND contract_address = '{address}'\",\"parameters\":{{}},\"options\":{{\"timeout\": 999999999}}}}", ); - let response = match _call_transpose(&query, api_key).await { + let response = match call_transpose(&query, api_key).await { Some(response) => response, None => { logger.error("failed to get creation tx from Transpose"); @@ -237,7 +237,58 @@ pub async fn get_contract_creation( } }; - return Some((block_number, transaction_hash)) + return Some((block_number, transaction_hash)); + }; + + None +} + +/// Get the label for the given address. +/// +/// ``` +/// use heimdall_common::resources::transpose::get_label; +/// +/// let address = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; +/// let api_key = "YOUR_API_KEY"; +/// +/// // let label = get_label(address, api_key).await; +/// ``` +pub async fn get_label(address: &str, api_key: &str) -> Option { + // get a new logger + let logger = Logger::default(); + let start_time = Instant::now(); + + // build the SQL query + let query = format!( + "{{\"sql\":\"SELECT COALESCE( (SELECT name FROM ethereum.contract_labels WHERE contract_address = '{address}' ), (SELECT ens_name FROM ethereum.ens_names WHERE primary_address = '{address}' LIMIT 1), (SELECT protocol_name FROM ethereum.protocols WHERE contract_address = '{address}' ), (SELECT symbol FROM ethereum.tokens WHERE contract_address = '{address}' ), (SELECT symbol FROM ethereum.collections WHERE contract_address = '{address}' ) ) as label\",\"parameters\":{{}},\"options\":{{\"timeout\": 999999999}}}}", + ); + + let response = match call_transpose(&query, api_key).await { + Some(response) => response, + None => { + logger.error("failed to get contract label from Transpose"); + return None; + } + }; + + logger.debug(&format!("fetching contract creation took {:?}", start_time.elapsed())); + + // parse the results + if let Some(result) = response.results.into_iter().next() { + let label: String = match result.get("label") { + Some(label) => match label.as_str() { + Some(label) => label.to_string(), + None => { + logger.error("failed to parse label from Transpose"); + return None; + } + }, + None => { + logger.error("failed to fetch label from Transpose response"); + return None; + } + }; + return Some(label); }; None diff --git a/common/src/utils/hex.rs b/common/src/utils/hex.rs new file mode 100644 index 00000000..e0ef2b71 --- /dev/null +++ b/common/src/utils/hex.rs @@ -0,0 +1,43 @@ +use ethers::types::{Bloom, Bytes, H160, H256, H64}; + +use super::strings::encode_hex; + +pub trait ToLowerHex { + fn to_lower_hex(&self) -> String; +} + +impl ToLowerHex for H256 { + fn to_lower_hex(&self) -> String { + format!("{:#032x}", self) + } +} + +impl ToLowerHex for H160 { + fn to_lower_hex(&self) -> String { + format!("{:#020x}", self) + } +} + +impl ToLowerHex for H64 { + fn to_lower_hex(&self) -> String { + format!("{:#016x}", self) + } +} + +impl ToLowerHex for Bloom { + fn to_lower_hex(&self) -> String { + format!("{:#064x}", self) + } +} + +impl ToLowerHex for Bytes { + fn to_lower_hex(&self) -> String { + format!("{:#0x}", self) + } +} + +impl ToLowerHex for Vec { + fn to_lower_hex(&self) -> String { + encode_hex(self.to_vec()) + } +} diff --git a/common/src/utils/io/file.rs b/common/src/utils/io/file.rs index 4395c07c..bb5edf64 100644 --- a/common/src/utils/io/file.rs +++ b/common/src/utils/io/file.rs @@ -17,11 +17,10 @@ use std::{ /// assert_eq!(short_path, "./something.json"); /// ``` pub fn short_path(path: &str) -> String { - let current_dir = match env::current_dir() { - Ok(dir) => dir.into_os_string().into_string().unwrap(), - Err(_) => std::process::exit(1), - }; - path.replace(¤t_dir, ".") + match env::current_dir() { + Ok(dir) => path.replace(&dir.into_os_string().into_string().unwrap(), "."), + Err(_) => path.to_owned(), + } } /// Write contents to a file on the disc diff --git a/common/src/utils/io/logging.rs b/common/src/utils/io/logging.rs index 71023abb..20438c4f 100644 --- a/common/src/utils/io/logging.rs +++ b/common/src/utils/io/logging.rs @@ -255,6 +255,26 @@ impl TraceFactory { self.add("call", parent_index, instruction, vec![title, returns]) } + pub fn add_call_with_extra( + &mut self, + parent_index: u32, + instruction: u32, + origin: String, + function_name: String, + args: Vec, + returns: String, + extra: Vec, + ) -> u32 { + let title = format!( + "{}::{}({}) {}", + origin.bright_cyan(), + function_name.bright_cyan(), + args.join(", "), + extra.iter().map(|s| format!("[{}]", s)).collect::>().join(" ").dimmed() + ); + self.add("call", parent_index, instruction, vec![title, returns]) + } + /// adds a contract creation trace pub fn add_creation( &mut self, @@ -375,6 +395,28 @@ impl Default for Logger { } } +impl Default for TraceFactory { + fn default() -> Self { + // get the environment variable RUST_LOG and parse it + let level = match std::env::var("RUST_LOG") { + Ok(level) => match level.to_lowercase().as_str() { + "silent" => -1, + "error" => 0, + "warn" => 1, + "info" => 2, + "debug" => 3, + "trace" => 4, + "all" => 5, + "max" => 6, + _ => 1, + }, + Err(_) => 2, + }; + + TraceFactory::new(level) + } +} + impl Logger { /// create a new logger with the given verbosity pub fn new(verbosity: &str) -> (Logger, TraceFactory) { diff --git a/common/src/utils/io/types.rs b/common/src/utils/io/types.rs index 41e8e3d7..2a999471 100644 --- a/common/src/utils/io/types.rs +++ b/common/src/utils/io/types.rs @@ -1,6 +1,8 @@ use colored::Colorize; use ethers::abi::Token; +use crate::utils::hex::ToLowerHex; + /// A helper function used by the decode module to pretty format decoded tokens. pub fn display(inputs: Vec, prefix: &str) -> Vec { let mut output = Vec::new(); @@ -59,3 +61,193 @@ pub fn display(inputs: Vec, prefix: &str) -> Vec { output } + +pub trait Parameterize { + fn parameterize(&self) -> String; + fn to_type(&self) -> String; +} + +/// A helper function used by the decode module to pretty format decoded tokens. +/// +/// ``` +/// use ethers::abi::Token; +/// use ethers::types::Address; +/// use heimdall_common::utils::io::types::Parameterize; +/// +/// let output = Token::Address(Address::zero()).parameterize(); +/// assert_eq!(output, "address: 0x0000000000000000000000000000000000000000".to_string()); +/// ``` +impl Parameterize for Token { + fn parameterize(&self) -> String { + match self { + Token::Address(val) => format!("address: {}", val.to_lower_hex()), + Token::Int(val) => format!("int: {}", val), + Token::Uint(val) => format!("uint: {}", val), + Token::String(val) => format!("string: {}", val), + Token::Bool(val) => format!("bool: {}", val), + Token::Bytes(val) => format!("bytes: 0x{}", val.to_lower_hex()), + Token::FixedBytes(val) => format!("bytes{}: 0x{}", val.len(), val.to_lower_hex()), + Token::Array(val) => { + // get type of array + let array_type = match val.first() { + Some(token) => token.to_type(), + None => String::new(), + }; + + // parametrize all array elements, remove their `type: ` prefix, and join them + let elements = val + .iter() + .map(|token| token.parameterize().replace(&format!("{}: ", array_type), "")) + .collect::>() + .join(", "); + + // return array type and elements + format!("{}[]: [{}]", array_type, elements) + } + Token::FixedArray(val) => { + // get type of array + let array_type = match val.first() { + Some(token) => token.to_type(), + None => String::new(), + }; + + // parametrize all array elements, remove their `type: ` prefix, and join them + let elements = val + .iter() + .map(|token| token.parameterize().replace(&format!("{}: ", array_type), "")) + .collect::>() + .join(", "); + + // return array type and elements + format!("{}[{}]: [{}]", array_type, val.len(), elements) + } + Token::Tuple(val) => { + // return tuple type and elements + format!( + "({})", + val.iter() + .map(|token| token.parameterize()) + .collect::>() + .join(", ") + ) + } + } + } + + fn to_type(&self) -> String { + match self { + Token::Address(_) => "address".to_string(), + Token::Int(_) => "int".to_string(), + Token::Uint(_) => "uint".to_string(), + Token::String(_) => "string".to_string(), + Token::Bool(_) => "bool".to_string(), + Token::Bytes(_) => "bytes".to_string(), + Token::FixedBytes(val) => format!("bytes{}", val.len()), + Token::Array(val) => { + // get type of array + let array_type = match val.first() { + Some(token) => token.to_type(), + None => String::new(), + }; + format!("{}[]", array_type) + } + Token::FixedArray(val) => { + // get type of array + let array_type = match val.first() { + Some(token) => token.to_type(), + None => String::new(), + }; + format!("{}[{}]", array_type, val.len()) + } + Token::Tuple(val) => { + // get all internal types + let types = + val.iter().map(|token| token.to_type()).collect::>().join(", "); + + // return tuple type + format!("({})", types) + } + } + } +} + +#[cfg(test)] +mod tests { + use ethers::types::Address; + + use super::*; + + #[test] + fn test_parameterize_address() { + let output = Token::Address(Address::zero()).parameterize(); + assert_eq!(output, "address: 0x0000000000000000000000000000000000000000".to_string()); + } + + #[test] + fn test_parameterize_int() { + let output = Token::Int(1.into()).parameterize(); + assert_eq!(output, "int: 1".to_string()); + } + + #[test] + fn test_parameterize_uint() { + let output = Token::Uint(1.into()).parameterize(); + assert_eq!(output, "uint: 1".to_string()); + } + + #[test] + fn test_parameterize_string() { + let output = Token::String("test".to_string()).parameterize(); + assert_eq!(output, "string: test".to_string()); + } + + #[test] + fn test_parameterize_bool() { + let output = Token::Bool(true).parameterize(); + assert_eq!(output, "bool: true".to_string()); + } + + #[test] + fn test_parameterize_bytes() { + let output = Token::Bytes(vec![0x01, 0x02, 0x03]).parameterize(); + assert_eq!(output, "bytes: 0x010203".to_string()); + } + + #[test] + fn test_parameterize_fixed_bytes() { + let output = Token::FixedBytes(vec![0x01, 0x02, 0x03]).parameterize(); + assert_eq!(output, "bytes3: 0x010203".to_string()); + } + + #[test] + fn test_parameterize_array() { + let output = + Token::Array(vec![Token::Uint(1.into()), Token::Uint(2.into())]).parameterize(); + assert_eq!(output, "uint[]: [1, 2]".to_string()); + } + + #[test] + fn test_parameterize_fixed_array() { + let output = + Token::FixedArray(vec![Token::Uint(1.into()), Token::Uint(2.into())]).parameterize(); + assert_eq!(output, "uint[2]: [1, 2]".to_string()); + } + + #[test] + fn test_parameterize_tuple() { + let output = + Token::Tuple(vec![Token::Uint(1.into()), Token::Uint(2.into()), Token::Uint(3.into())]) + .parameterize(); + assert_eq!(output, "(uint: 1, uint: 2, uint: 3)".to_string()); + } + + #[test] + fn test_parameterize_nested_array() { + let output = Token::Array(vec![ + Token::Array(vec![Token::Uint(1.into()), Token::Uint(2.into())]), + Token::Array(vec![Token::Uint(3.into()), Token::Uint(4.into())]), + ]) + .parameterize(); + assert_eq!(output, "uint[][]: [[1, 2], [3, 4]]".to_string()); + } +} diff --git a/common/src/utils/mod.rs b/common/src/utils/mod.rs index bfa2992f..c817f031 100644 --- a/common/src/utils/mod.rs +++ b/common/src/utils/mod.rs @@ -1,3 +1,4 @@ +pub mod hex; pub mod http; pub mod integers; pub mod io; diff --git a/common/src/utils/strings.rs b/common/src/utils/strings.rs index 81820ae6..168093d4 100644 --- a/common/src/utils/strings.rs +++ b/common/src/utils/strings.rs @@ -3,7 +3,6 @@ use std::{fmt::Write, num::ParseIntError}; use ethers::{ abi::AbiEncode, prelude::{I256, U256}, - types::{Bloom, H160, H256, H64}, }; use fancy_regex::Regex; @@ -310,34 +309,6 @@ pub fn tokenize(s: &str) -> Vec { tokens } -pub trait ToLowerHex { - fn to_lower_hex(&self) -> String; -} - -impl ToLowerHex for H256 { - fn to_lower_hex(&self) -> String { - format!("{:#032x}", self) - } -} - -impl ToLowerHex for H160 { - fn to_lower_hex(&self) -> String { - format!("{:#020x}", self) - } -} - -impl ToLowerHex for H64 { - fn to_lower_hex(&self) -> String { - format!("{:#016x}", self) - } -} - -impl ToLowerHex for Bloom { - fn to_lower_hex(&self) -> String { - format!("{:#064x}", self) - } -} - #[derive(Debug, PartialEq)] pub enum TokenType { Control, diff --git a/core/src/decode/core/abi.rs b/core/src/decode/core/abi.rs index f6c7da94..f71a2928 100644 --- a/core/src/decode/core/abi.rs +++ b/core/src/decode/core/abi.rs @@ -21,6 +21,7 @@ pub fn is_parameter_abi_encoded( parameter_index: usize, calldata_words: &[&str], ) -> Result, Error> { + debug_max!("calldata_words: {:#?}", calldata_words); let mut coverages = HashSet::from([parameter_index]); // convert this word to a U256 diff --git a/core/src/decode/mod.rs b/core/src/decode/mod.rs index 871d19af..afd9a1ed 100644 --- a/core/src/decode/mod.rs +++ b/core/src/decode/mod.rs @@ -69,6 +69,10 @@ pub struct DecodeArgs { /// Whether to truncate nonstandard sized calldata. #[clap(long, short)] pub truncate_calldata: bool, + + /// Whether to skip resolving selectors. Heimdall will attempt to guess types. + #[clap(long = "skip-resolving")] + pub skip_resolving: bool, } impl DecodeArgsBuilder { @@ -81,6 +85,7 @@ impl DecodeArgsBuilder { explain: Some(false), default: Some(true), truncate_calldata: Some(false), + skip_resolving: Some(false), } } } @@ -109,7 +114,9 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { // check if we require an OpenAI API key if args.explain && args.openai_api_key.is_empty() { logger.error("OpenAI API key is required for explaining calldata. Use `heimdall decode --help` for more information."); - std::process::exit(1); + return Err(Error::GenericError( + "OpenAI API key is required for explaining calldata.".to_string(), + )); } // init variables @@ -128,13 +135,15 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { calldata = args.target.to_string().replacen("0x", "", 1); } else { logger.error("invalid target. must be a transaction hash or calldata (bytes)."); - std::process::exit(1); + return Err(Error::GenericError( + "invalid target. must be a transaction hash or calldata (bytes).".to_string(), + )); } // check if the calldata length is a standard length if calldata.len() % 2 != 0 || calldata.len() < 8 { logger.error("calldata is not a valid hex string."); - std::process::exit(1); + return Err(Error::GenericError("calldata is not a valid hex string.".to_string())); } // if calldata isn't a multiple of 64, it may be harder to decode. @@ -156,16 +165,20 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { Ok(byte_args) => byte_args, Err(_) => { logger.error("failed to parse bytearray from calldata."); - std::process::exit(1) + return Err(Error::DecodeError); } }; // get the function signature possibilities - let potential_matches = match ResolvedFunction::resolve(&function_selector).await { - Some(signatures) => signatures, - None => Vec::new(), + let potential_matches = if !args.skip_resolving { + match ResolvedFunction::resolve(&function_selector).await { + Some(signatures) => signatures, + None => Vec::new(), + } + } else { + Vec::new() }; - if potential_matches.is_empty() { + if potential_matches.is_empty() && !args.skip_resolving { logger.warn("couldn't resolve potential matches for the given function selector."); } @@ -359,7 +372,7 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { Some(selected_match) => selected_match, None => { logger.error("invalid selection."); - std::process::exit(1) + return Err(Error::GenericError("invalid selection.".to_string())); } }; diff --git a/core/src/inspect/core/mod.rs b/core/src/inspect/core/mod.rs new file mode 100644 index 00000000..5ee45fbc --- /dev/null +++ b/core/src/inspect/core/mod.rs @@ -0,0 +1 @@ +pub mod tracing; diff --git a/core/src/inspect/core/storage.rs b/core/src/inspect/core/storage.rs new file mode 100644 index 00000000..968dd9f1 --- /dev/null +++ b/core/src/inspect/core/storage.rs @@ -0,0 +1,3 @@ + + +/// Converts a raw [`StateDiff`] to a human-readable diff. diff --git a/core/src/inspect/core/tracing.rs b/core/src/inspect/core/tracing.rs new file mode 100644 index 00000000..cbfb854a --- /dev/null +++ b/core/src/inspect/core/tracing.rs @@ -0,0 +1,219 @@ +use std::collections::HashMap; + +use ethers::types::{Transaction, TransactionTrace}; +use heimdall_common::{ + resources::transpose::get_label, + utils::{ + hex::ToLowerHex, + io::{logging::TraceFactory, types::Parameterize}, + }, +}; + +use crate::{decode::DecodeArgsBuilder, error::Error, inspect::InspectArgs}; + +/// Converts raw [`TransactionTrace`]s to human-readable [`TraceFactory`] +pub async fn build_trace_display( + args: &InspectArgs, + transaction: &Transaction, + transaction_traces: Vec, + address_labels: &mut HashMap>, +) -> Result { + let mut trace = TraceFactory::default(); + let decode_call = trace.add_call_with_extra( + 0, + transaction.gas.as_u32(), // panicky + "heimdall".to_string(), + "inspect".to_string(), + vec![transaction.hash.to_lower_hex()], + "()".to_string(), + vec![format!("{} wei", transaction.value)], + ); + + let mut trace_indices = HashMap::new(); + + for transaction_trace in transaction_traces { + let trace_address = transaction_trace + .trace_address + .iter() + .map(|address| address.to_string()) + .collect::>() + .join("."); + let parent_address = trace_address + .split('.') + .take(trace_address.split('.').count() - 1) + .collect::>() + .join("."); + + // get trace index from parent_address + let parent_index = trace_indices.get(&parent_address).unwrap_or(&decode_call); + + // get result + let mut result_str = "()".to_string(); + if let Some(result) = transaction_trace.result { + result_str = match result { + ethers::types::Res::Call(res) => { + // we can attempt to decode this as if it is calldata, we just need to add some + // 4byte prefix. + let output = + format!("0x00000000{}", res.output.to_string().replacen("0x", "", 1)); + let result = crate::decode::decode( + DecodeArgsBuilder::new() + .target(output) + .skip_resolving(true) + .build() + .map_err(|_e| Error::DecodeError)?, + ) + .await?; + + // get first result + if let Some(resolved_function) = result.first() { + resolved_function + .decoded_inputs + .clone() + .unwrap_or_default() + .iter() + .map(|token| token.parameterize()) + .collect::>() + .join(", ") + } else { + res.output.to_string() + } + } + ethers::types::Res::Create(res) => res.address.to_lower_hex(), + ethers::types::Res::None => "()".to_string(), + } + } + if result_str.replacen("0x", "", 1).is_empty() { + result_str = "()".to_string(); + } + + // get action + match transaction_trace.action { + ethers::types::Action::Call(call) => { + // add address label. we will use this to display the address in the trace, if + // available. (requires `transpose_api_key`) + if let Some(transpose_api_key) = &args.transpose_api_key { + if let std::collections::hash_map::Entry::Vacant(e) = + address_labels.entry(call.to.to_lower_hex()) + { + e.insert(get_label(&call.to.to_lower_hex(), transpose_api_key).await); + } + } + let address_label = address_labels + .get(&call.to.to_lower_hex()) + .unwrap_or(&None) + .clone() + .unwrap_or(call.to.to_lower_hex()) + .to_string(); + + // build extra_data, which will be used to display the call type and value transfer + // information + let mut extra_data = vec![]; + let call_type = match call.call_type { + ethers::types::CallType::Call => "call", + ethers::types::CallType::DelegateCall => "delegatecall", + ethers::types::CallType::StaticCall => "staticcall", + ethers::types::CallType::CallCode => "callcode", + ethers::types::CallType::None => "none", + } + .to_string(); + extra_data.push(call_type.clone()); + if !call.value.is_zero() { + extra_data.push(format!("{} wei", call.value)); + } + + // attempt to decode calldata + let calldata = call.input.to_string(); + if !calldata.replacen("0x", "", 1).is_empty() { + let result = crate::decode::decode( + DecodeArgsBuilder::new() + .target(calldata) + .build() + .map_err(|_e| Error::DecodeError)?, + ) + .await?; + + // get first result + if let Some(resolved_function) = result.first() { + // convert decoded inputs Option> to Vec + let decoded_inputs = + resolved_function.decoded_inputs.clone().unwrap_or_default(); + + // get index of parent + let parent_index = trace.add_call_with_extra( + *parent_index, + call.gas.as_u32(), // panicky + address_label, + resolved_function.name.clone(), + vec![decoded_inputs + .iter() + .map(|token| token.parameterize()) + .collect::>() + .join(", ")], + result_str, + extra_data, + ); + + // add trace_address to trace_indices + trace_indices.insert(trace_address.clone(), parent_index); + } else { + // get index of parent + let parent_index = trace.add_call_with_extra( + *parent_index, + call.gas.as_u32(), // panicky + address_label, + "unknown".to_string(), + vec![format!("bytes: {}", call.input.to_string())], + result_str, + extra_data, + ); + + // add trace_address to trace_indices + trace_indices.insert(trace_address.clone(), parent_index); + } + } else { + // value transfer + trace.add_call_with_extra( + *parent_index, + call.gas.as_u32(), // panicky + call.to.to_lower_hex(), + "fallback".to_string(), + vec![], + result_str, + extra_data, + ); + } + } + ethers::types::Action::Create(create) => { + // add address label. we will use this to display the address in the trace, if + // available. (requires `transpose_api_key`) + if let Some(transpose_api_key) = &args.transpose_api_key { + if !address_labels.contains_key(&result_str) { + address_labels.insert( + result_str.clone(), + get_label(&result_str, transpose_api_key).await, + ); + } + } + let address_label = address_labels + .get(&result_str) + .unwrap_or(&None) + .clone() + .unwrap_or("NewContract".to_string()) + .to_string(); + + trace.add_creation( + *parent_index, + create.gas.as_u32(), + address_label, + result_str, + create.init.len().try_into().map_err(|_e| Error::DecodeError)?, + ); + } + ethers::types::Action::Suicide(_suicide) => {} + ethers::types::Action::Reward(_) => todo!(), + } + } + + Ok(trace) +} diff --git a/core/src/inspect/mod.rs b/core/src/inspect/mod.rs index 7d332368..0a5181fc 100644 --- a/core/src/inspect/mod.rs +++ b/core/src/inspect/mod.rs @@ -1,15 +1,17 @@ +mod core; + use std::collections::HashMap; use clap::{AppSettings, Parser}; -use clap_verbosity_flag::Verbosity; + use derive_builder::Builder; use heimdall_common::{ - ether::rpc::get_trace, - utils::{io::logging::Logger, strings::ToLowerHex}, + ether::rpc::{get_trace, get_transaction}, + utils::io::logging::Logger, }; -use crate::{decode::DecodeArgsBuilder, error::Error}; +use crate::{error::Error, inspect::core::tracing::build_trace_display}; #[derive(Debug, Clone, Parser, Builder)] #[clap( @@ -34,6 +36,10 @@ pub struct InspectArgs { /// When prompted, always select the default value. #[clap(long, short)] pub default: bool, + + /// Your OPTIONAL Transpose.io API Key, used for labeling contract addresses. + #[clap(long = "transpose-api-key", short, hide_default_value = true)] + pub transpose_api_key: Option, } impl InspectArgsBuilder { @@ -43,6 +49,7 @@ impl InspectArgsBuilder { verbose: Some(clap_verbosity_flag::Verbosity::new(0, 1)), rpc_url: Some(String::new()), default: Some(true), + transpose_api_key: None, } } } @@ -52,6 +59,9 @@ impl InspectArgsBuilder { /// visualization, and more. #[allow(deprecated)] pub async fn inspect(args: InspectArgs) -> Result<(), Error> { + // define + let mut address_labels = HashMap::new(); + // set logger environment variable if not already set // TODO: abstract this to a heimdall_common util if std::env::var("RUST_LOG").is_err() { @@ -65,115 +75,32 @@ pub async fn inspect(args: InspectArgs) -> Result<(), Error> { } // get a new logger and trace - let (_logger, mut trace) = Logger::new(match args.verbose.log_level() { + let (_logger, _trace) = Logger::new(match args.verbose.log_level() { Some(level) => level.as_str(), None => "SILENT", }); // get calldata from RPC - // let transaction = get_transaction(&args.target, &args.rpc_url) - // .await - // .map_err(|e| Error::RpcError(e.to_string()))?; + let transaction = get_transaction(&args.target, &args.rpc_url) + .await + .map_err(|e| Error::RpcError(e.to_string()))?; // get trace let block_trace = get_trace(&args.target, &args.rpc_url).await.map_err(|e| Error::RpcError(e.to_string()))?; - let decode_call = trace.add_call( - 0, - line!(), - "heimdall".to_string(), - "inspect".to_string(), - vec![args.target.clone()], - "()".to_string(), - ); - - if let Some(transaction_traces) = block_trace.trace { - let mut trace_indices = HashMap::new(); - - for transaction_trace in transaction_traces { - println!("trace: {:?}", transaction_trace); - let trace_address = transaction_trace - .trace_address - .iter() - .map(|address| address.to_string()) - .collect::>() - .join("."); - let parent_address = trace_address - .split('.') - .take(trace_address.split('.').count() - 1) - .collect::>() - .join("."); - - // get trace index from parent_address - let parent_index = trace_indices.get(&parent_address).unwrap_or(&decode_call); - - // get action - match transaction_trace.action { - ethers::types::Action::Call(call) => { - // attempt to decode calldata - let calldata = call.input.to_string(); - - if !calldata.replacen("0x", "", 1).is_empty() { - let result = crate::decode::decode( - DecodeArgsBuilder::new() - .target(calldata) - .rpc_url("https://eth.llamarpc.com".to_string()) - .verbose(Verbosity::new(2, 0)) - .build() - .map_err(|_e| Error::DecodeError)?, - ) - .await?; - - // get first result - if let Some(resolved_function) = result.first() { - // convert decoded inputs Option> to Vec - let decoded_inputs = - resolved_function.decoded_inputs.clone().unwrap_or_default(); - - // get index of parent - let parent_index = trace.add_call( - *parent_index, - line!(), - call.to.to_lower_hex(), - resolved_function.name.clone(), - decoded_inputs - .iter() - .map(|token| format!("{:?}", token)) - .collect::>(), - "()".to_string(), - ); - - // add trace_address to trace_indices - trace_indices.insert(trace_address.clone(), parent_index); - trace.add_info( - parent_index, - line!(), - &format!("trace_address: {:?}", trace_address), - ); - } else { - unimplemented!(); - } - } else { - // value transfer - trace.add_call( - *parent_index, - line!(), - call.to.to_lower_hex(), - "transfer".to_string(), - vec![format!("{} wei", call.value)], - "()".to_string(), - ); - } - } - ethers::types::Action::Create(_) => todo!(), - ethers::types::Action::Suicide(_) => todo!(), - ethers::types::Action::Reward(_) => todo!(), - } - } - } + // build displayable trace + let transaction_trace_display = build_trace_display( + &args, + &transaction, + block_trace.trace.unwrap_or_default(), + &mut address_labels, + ) + .await?; + + transaction_trace_display.display(); - trace.display(); + println!("{:#?}", block_trace.state_diff); Ok(()) } diff --git a/core/tests/test_decode.rs b/core/tests/test_decode.rs index 0566a366..79548ff2 100644 --- a/core/tests/test_decode.rs +++ b/core/tests/test_decode.rs @@ -16,6 +16,7 @@ mod benchmark { explain: false, default: true, truncate_calldata: false, + skip_resolving: false, }; let _ = heimdall_core::decode::decode(args).await; } @@ -34,6 +35,7 @@ mod benchmark { explain: false, default: true, truncate_calldata: false, + skip_resolving: false, }; let _ = heimdall_core::decode::decode(args).await; } @@ -52,6 +54,7 @@ mod benchmark { explain: false, default: true, truncate_calldata: false, + skip_resolving: false, }; let _ = heimdall_core::decode::decode(args).await; } @@ -70,6 +73,7 @@ mod benchmark { explain: false, default: true, truncate_calldata: false, + skip_resolving: false, }; let _ = heimdall_core::decode::decode(args).await; } @@ -93,6 +97,7 @@ mod tests { explain: false, default: true, truncate_calldata: false, + skip_resolving: false, }; let _ = heimdall_core::decode::decode(args).await; } @@ -107,6 +112,7 @@ mod tests { explain: false, default: true, truncate_calldata: false, + skip_resolving: false, }; let _ = heimdall_core::decode::decode(args).await; }