From f9264cc60b78a9e56bdaaaab629da0dd38b3be69 Mon Sep 17 00:00:00 2001 From: Jon-Becker Date: Sun, 19 Nov 2023 17:53:06 -0600 Subject: [PATCH 1/9] feat(decode): init calldata type guessing --- core/src/decode/mod.rs | 43 ++++++------------------------------------ 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/core/src/decode/mod.rs b/core/src/decode/mod.rs index 77db2bf9..c80e3f91 100644 --- a/core/src/decode/mod.rs +++ b/core/src/decode/mod.rs @@ -255,7 +255,11 @@ pub async fn decode(args: DecodeArgs) -> Result, Box Result, Box Result, Box = Vec::new(); - for (i, input) in calldata[8..] - .chars() - .collect::>() - .chunks(64) - .map(|c| c.iter().collect::()) - .collect::>() - .iter() - .enumerate() - { - inputs.push( - format!( - "{} {}:{}{}", - if i == 0 { "input" } else { " " }, - i, - if i.to_string().len() <= 3 { - " ".repeat(3 - i.to_string().len()) - } else { - "".to_string() - }, - input - ) - .to_string(), - ) - } - trace.add_message(decode_call, line!(), inputs); - // force the trace to display trace.level = 4; trace.display(); From 3c7a25e662447e6c275350838aa97c221525623e Mon Sep 17 00:00:00 2001 From: Jon-Becker Date: Thu, 30 Nov 2023 02:14:59 -0500 Subject: [PATCH 2/9] feat(decode): pretty accurate type guessing --- common/src/constants.rs | 3 + common/src/ether/evm/core/types.rs | 218 ++++++++++++++++++- common/src/utils/io/logging.rs | 6 +- common/src/utils/iter.rs | 35 ++++ common/src/utils/mod.rs | 1 + core/src/decode/core/abi.rs | 247 ++++++++++++++++++++++ core/src/decode/core/mod.rs | 1 + core/src/decode/mod.rs | 324 +++++++++++++++++++---------- core/src/decompile/resolve.rs | 2 +- core/src/snapshot/resolve.rs | 2 +- 10 files changed, 721 insertions(+), 118 deletions(-) create mode 100644 common/src/utils/iter.rs create mode 100644 core/src/decode/core/abi.rs create mode 100644 core/src/decode/core/mod.rs diff --git a/common/src/constants.rs b/common/src/constants.rs index 3cc66cbb..11c31b87 100644 --- a/common/src/constants.rs +++ b/common/src/constants.rs @@ -14,6 +14,9 @@ lazy_static! { /// contract on Ethereum. pub static ref BYTECODE_REGEX: Regex = Regex::new(r"^(0x)?[0-9a-fA-F]{0,50000}$").unwrap(); + /// The following regex is used to validate raw calldata + pub static ref CALLDATA_REGEX: Regex = Regex::new(r"^(0x)?[0-9a-fA-F]*$").unwrap(); + /// The following regex is used to reduce null byte prefixes pub static ref REDUCE_HEX_REGEX: Regex = Regex::new(r"^0x(00)*").unwrap(); diff --git a/common/src/ether/evm/core/types.rs b/common/src/ether/evm/core/types.rs index 317257c2..0cbdf509 100644 --- a/common/src/ether/evm/core/types.rs +++ b/common/src/ether/evm/core/types.rs @@ -1,10 +1,20 @@ use colored::Colorize; use ethers::abi::{AbiEncode, ParamType, Token}; -use crate::{constants::TYPE_CAST_REGEX, utils::strings::find_balanced_encapsulator}; +use crate::{ + constants::TYPE_CAST_REGEX, + utils::strings::{decode_hex, find_balanced_encapsulator}, +}; use super::vm::Instruction; +#[derive(Debug, Clone, PartialEq)] +pub enum Padding { + Left, + Right, + None, +} + /// Parse function parameters [`ParamType`]s from a function signature. /// /// ``` @@ -171,7 +181,7 @@ fn is_first_type_tuple(string: &str) -> bool { /// A helper function used by [`extract_types_from_string`] that converts a string type to a /// ParamType. For example, "address" will be converted to [`ParamType::Address`]. -fn to_type(string: &str) -> ParamType { +pub fn to_type(string: &str) -> ParamType { let is_array = string.ends_with(']'); // get size of array @@ -359,11 +369,115 @@ pub fn find_cast(line: &str) -> (usize, usize, Option) { } } +/// Given a string of bytes, determine if it is left or right padded. +pub fn get_padding(bytes: &str) -> Padding { + let decoded = match decode_hex(bytes) { + Ok(decoded) => decoded, + Err(_) => return Padding::None, + }; + + let size = decoded.len(); + + // get indices of null bytes in the decoded bytes + let null_byte_indices = decoded + .iter() + .enumerate() + .filter(|(_, byte)| **byte == 0) + .map(|(index, _)| index) + .collect::>(); + + // we can avoid doing a full check if any of the following are true: + // there are no null bytes OR + // neither first nor last byte is a null byte, it is not padded + if null_byte_indices.is_empty() || + null_byte_indices[0] != 0 && null_byte_indices[null_byte_indices.len() - 1] != size - 1 + { + return Padding::None + } + + // the first byte is a null byte AND the last byte is not a null byte, it is left padded + if null_byte_indices[0] == 0 && null_byte_indices[null_byte_indices.len() - 1] != size - 1 { + return Padding::Left + } + + // the first byte is not a null byte AND the last byte is a null byte, it is right padded + if null_byte_indices[0] != 0 && null_byte_indices[null_byte_indices.len() - 1] == size - 1 { + return Padding::Right + } + + // get non-null byte indices + let non_null_byte_indices = decoded + .iter() + .enumerate() + .filter(|(_, byte)| **byte != 0) + .map(|(index, _)| index) + .collect::>(); + + if non_null_byte_indices.is_empty() { + return Padding::None + } + + // check if the there are more null-bytes before the first non-null byte than after the last + // non-null byte + let left_hand_padding = + null_byte_indices.iter().filter(|index| **index < non_null_byte_indices[0]).count(); + let right_hand_padding = null_byte_indices + .iter() + .filter(|index| **index > non_null_byte_indices[non_null_byte_indices.len() - 1]) + .count(); + + if left_hand_padding > right_hand_padding { + return Padding::Left + } else if left_hand_padding < right_hand_padding { + return Padding::Right + } + + Padding::None +} + +/// Given a string of bytes, get the max padding size for the data +pub fn get_padding_size(bytes: &str) -> usize { + match get_padding(bytes) { + Padding::Left => { + // count number of null-bytes at the start of the data + bytes + .chars() + .collect::>() + .chunks(2) + .map(|c| c.iter().collect::()) + .take_while(|c| c == "00") + .count() + } + Padding::Right => { + // count number of null-bytes at the end of the data + bytes + .chars() + .collect::>() + .chunks(2) + .map(|c| c.iter().collect::()) + .rev() + .take_while(|c| c == "00") + .count() + } + _ => 0, + } +} + +// Get minimum size needed to store the given word +pub fn get_potential_types_for_word(word: &str) -> (usize, Vec) { + // get padding of the word, note this is a maximum + let padding_size = get_padding_size(word); + + // get number of bytes padded + let data_size = (word.len() / 2) - padding_size; + byte_size_to_type(data_size) +} + #[cfg(test)] mod tests { use ethers::abi::ParamType; - use crate::ether::evm::core::types::parse_function_parameters; + use crate::ether::evm::core::types::{get_padding, parse_function_parameters, Padding}; #[test] fn test_simple_signature() { @@ -566,4 +680,102 @@ mod tests { ]) ); } + + #[test] + fn test_get_padding_no_padding() { + // No padding, input contains no null bytes + let input = "11".repeat(32); + assert_eq!(get_padding(&input), Padding::None); + } + + #[test] + fn test_get_padding_left_padding() { + // Left padded, first byte is null + let input = "00".repeat(31) + "11"; + assert_eq!(get_padding(&input), Padding::Left); + } + + #[test] + fn test_get_padding_right_padding() { + // Right padding, last byte is null + let input = "11".to_owned() + &"00".repeat(31); + assert_eq!(get_padding(&input), Padding::Right); + } + + #[test] + fn test_get_padding_skewed_left_padding() { + // Both left and right null-bytes, but still left padded + let input = "00".repeat(30) + "1100"; + assert_eq!(get_padding(&input), Padding::Left); + } + + #[test] + fn test_get_padding_skewed_right_padding() { + // Both left and right null-bytes, but still right padded + let input = "0011".to_owned() + &"00".repeat(30); + assert_eq!(get_padding(&input), Padding::Right); + } + + #[test] + fn test_get_padding_empty_input() { + // Empty input should result in no padding + let input = ""; + assert_eq!(get_padding(input), Padding::None); + } + + #[test] + fn test_get_padding_single_byte() { + // Single-byte input with null byte + let input = "00"; + assert_eq!(get_padding(input), Padding::None); + } + + #[test] + fn test_get_padding_single_byte_left_padding() { + // Single-byte input with left padding + let input = "0011"; + assert_eq!(get_padding(input), Padding::Left); + } + + #[test] + fn test_get_padding_single_byte_right_padding() { + // Single-byte input with right padding + let input = "1100"; + assert_eq!(get_padding(input), Padding::Right); + } + + #[test] + fn test_get_padding_single_byte_both_padding() { + // Single-byte input with both left and right padding + let input = "001100"; + assert_eq!(get_padding(input), Padding::None); + } + + #[test] + fn test_get_padding_mixed_padding() { + // Mixed padding, some null bytes in the middle + let input = "00".repeat(10) + "1122330000332211" + &"00".repeat(10); + assert_eq!(get_padding(&input), Padding::None); + } + + #[test] + fn test_get_padding_mixed_padding_skewed_left() { + // Mixed padding, some null bytes in the middle + let input = "00".repeat(10) + "001122330000332211" + &"00".repeat(10); + assert_eq!(get_padding(&input), Padding::Left); + } + + #[test] + fn test_get_padding_mixed_padding_skewed_right() { + // Mixed padding, some null bytes in the middle + let input = "00".repeat(10) + "112233000033221100" + &"00".repeat(10); + assert_eq!(get_padding(&input), Padding::Right); + } + + #[test] + fn test_get_padding_invalid_hex_input() { + // Invalid hex input, should result in no padding + let input = "XYZ"; + assert_eq!(get_padding(input), Padding::None); + } } diff --git a/common/src/utils/io/logging.rs b/common/src/utils/io/logging.rs index 8d587ecf..f813aadd 100644 --- a/common/src/utils/io/logging.rs +++ b/common/src/utils/io/logging.rs @@ -108,7 +108,7 @@ impl TraceFactory { "{} {} {}", replace_last(prefix, "│ ", " ├─").bold().bright_white(), format!("[{}]", trace.instruction).bold().bright_white(), - trace.message.get(0).expect("Failed to build trace.") + trace.message.first().expect("Failed to build trace.") ); // print the children @@ -136,7 +136,7 @@ impl TraceFactory { println!( "{} emit {}", replace_last(prefix, "│ ", " ├─").bold().bright_white(), - trace.message.get(0).expect("Failed to build trace.") + trace.message.first().expect("Failed to build trace.") ); } TraceCategory::LogUnknown => { @@ -211,7 +211,7 @@ impl TraceFactory { "{} {} create → {}", replace_last(prefix, "│ ", " ├─").bold().bright_white(), format!("[{}]", trace.instruction).bold().bright_white(), - trace.message.get(0).expect("Failed to build trace.") + trace.message.first().expect("Failed to build trace.") ); // print the children diff --git a/common/src/utils/iter.rs b/common/src/utils/iter.rs new file mode 100644 index 00000000..dc7f2fe0 --- /dev/null +++ b/common/src/utils/iter.rs @@ -0,0 +1,35 @@ +pub fn remove_sorted_indices( + v: impl IntoIterator, + indices: impl IntoIterator, +) -> Vec { + let v = v.into_iter(); + let mut indices = indices.into_iter(); + let mut i = match indices.next() { + None => return v.collect(), + Some(i) => i, + }; + let (min, max) = v.size_hint(); + let mut result = Vec::with_capacity(max.unwrap_or(min)); + + for (j, x) in v.into_iter().enumerate() { + if j == i { + if let Some(idx) = indices.next() { + i = idx; + } + } else { + result.push(x); + } + } + + result +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_remove_sorted_indices() { + assert_eq!(remove_sorted_indices(vec![1, 2, 3, 4, 5], vec![0, 2, 4]), vec![2, 4]); + } +} diff --git a/common/src/utils/mod.rs b/common/src/utils/mod.rs index 910066c1..bfa2992f 100644 --- a/common/src/utils/mod.rs +++ b/common/src/utils/mod.rs @@ -1,6 +1,7 @@ pub mod http; pub mod integers; pub mod io; +pub mod iter; pub mod strings; pub mod sync; pub mod testing; diff --git a/core/src/decode/core/abi.rs b/core/src/decode/core/abi.rs new file mode 100644 index 00000000..a9c34a78 --- /dev/null +++ b/core/src/decode/core/abi.rs @@ -0,0 +1,247 @@ +use std::collections::HashSet; + +use ethers::types::U256; +use heimdall_common::ether::evm::core::types::{ + get_padding, get_padding_size, get_potential_types_for_word, Padding, +}; + +/// Finds the offsets of all ABI-encoded items in the given calldata. +pub fn is_parameter_abiencoded( + parameter_index: usize, + calldata_words: &[&str], +) -> (bool, Option, Option>) { + let mut coverages = HashSet::from([parameter_index]); + + // convert this word to a U256 + let word = U256::from_str_radix(calldata_words[parameter_index], 16) + .unwrap() + .max(U256::from(u128::max_value())); + + // if the word is a multiple of 32, it may be an offset pointing to the start of an + // ABI-encoded item + if word % 32 != U256::zero() { + return (false, None, None); + } + + // check if the pointer is pointing to a valid location in the calldata + let word_offset = word / 32; + if word_offset >= U256::from(calldata_words.len()) { + return (false, None, None); + } + + // note: `size` is the size of the ABI-encoded item. It varies depending on the type of the + // item. + let size = U256::from_str_radix( + calldata_words.get(word_offset.as_usize()).expect("word_offset out of bounds"), + 16, + ) + .unwrap(); + + // check if there are enough words left in the calldata to contain the ABI-encoded item. + // if there aren't, it doesn't necessarily mean that the calldata is invalid, but it does + // indicate that they aren't an array of items. + let data_start_word_offset = word_offset + 1; + let data_end_word_offset = data_start_word_offset + size; + if data_end_word_offset >= U256::from(calldata_words.len()) { + // this could still be bytes, string, or a non ABI-encoded item + + // join words AFTER the word_offset. these are where potential data is. + let data_words = &calldata_words[data_start_word_offset.as_usize()..]; + + // check if there are enough remaining bytes to contain the ABI-encoded item. + if data_words.join("").len() / 2 < size.as_usize() { + return (false, None, None); + } + + // if `size` is less than 32 bytes, we only need to check the first word for `32 - size` + // null-bytes. tricky because sometimes a null byte could be part of the + // data. + if size <= U256::from(32) { + let potential_data = data_words[0]; + + // get the padding of the data + let padding_size = get_padding_size(potential_data); + + // if the padding is greater than `32 - size`, then this is not an ABI-encoded item. + if padding_size > 32 - size.as_usize() { + return (false, None, None); + } + + // insert the word offset into the coverage set + coverages.insert(data_start_word_offset.as_usize()); + coverages.insert(word_offset.as_usize()); + (true, Some(String::from("bytes")), Some(coverages)) + } else { + // recalculate data_end_word_offset based on `size` + // size is in bytes, and one word is 32 bytes. find out how many words we need to + // cover `size` bytes. + let word_count_for_size = U256::from((size.as_u32() as f32 / 32f32).ceil() as u32); // wont panic unless calldata is huge + let data_end_word_offset = data_start_word_offset + word_count_for_size; + + // get the last word of the data + let last_word = data_words + .get(word_count_for_size.as_usize() - 1) + .expect("word_count_for_size out of bounds"); + + // how many bytes should be in the last word? + let last_word_size = size.as_usize() % 32; + + // if the padding is greater than `32 - last_word_size`, then this is not an ABI-encoded + // item. + let padding_size = get_padding_size(last_word); + if padding_size > 32 - last_word_size { + return (false, None, None); + } + + // insert all word offsets from `data_start_word_offset` to `data_end_word_offset` into + // the coverage set + for i in word_offset.as_usize()..data_end_word_offset.as_usize() { + coverages.insert(i); + } + + (true, Some(String::from("bytes")), Some(coverages)) + } + } else { + // this could be an array of items. + let data_words = + &calldata_words[data_start_word_offset.as_usize()..data_end_word_offset.as_usize()]; + + let (_min_size, potential_type) = data_words + .iter() + .map(|w| { + let (padding_size, mut potential_types) = get_potential_types_for_word(w); + + // perform heuristics + // - if we use right-padding, this is probably bytesN + // - if we use left-padding, this is probably uintN or intN + // - if we use no padding, this is probably bytes32 + match get_padding(w) { + Padding::Left => potential_types.retain(|t| t.starts_with("uint")), + _ => potential_types.retain(|t| t.starts_with("bytes")), + } + + (padding_size, potential_types) + }) + .fold((0, String::from("")), |(max_size, mut potential_types), (size, types)| { + if size > max_size { + potential_types = types + .first() + .expect("potential types is empty when decoding abi.encoded array") + .clone(); + (size, potential_types) + } else { + (max_size, potential_types) + } + }); + + // insert all word offsets from `data_start_word_offset` to `data_end_word_offset` into the + // coverage set + for i in word_offset.as_usize()..data_end_word_offset.as_usize() { + coverages.insert(i); + } + (true, Some(format!("{potential_type}[]")), Some(coverages)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_detect_abi_encoded_parameters_nominal() { + // calldata from https://docs.soliditylang.org/en/develop/abi-spec.html + // * Signature: `f(uint256,uint32[],bytes10,bytes)` + // * Values: `(0x123, [0x456, 0x789], "1234567890", "Hello, world!")` + // 0x8be65246 + // 0. 0000000000000000000000000000000000000000000000000000000000000123 + // 1. 0000000000000000000000000000000000000000000000000000000000000080 + // 2. 3132333435363738393000000000000000000000000000000000000000000000 + // 3. 00000000000000000000000000000000000000000000000000000000000000e0 + // 4. 0000000000000000000000000000000000000000000000000000000000000002 + // 5. 0000000000000000000000000000000000000000000000000000000000000456 + // 6. 0000000000000000000000000000000000000000000000000000000000000789 + // 7. 000000000000000000000000000000000000000000000000000000000000000d + // 8. 48656c6c6f2c20776f726c642100000000000000000000000000000000000000 + let calldata = "0x8be6524600000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000080313233343536373839300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000004560000000000000000000000000000000000000000000000000000000000000789000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20776f726c642100000000000000000000000000000000000000"; + + let calldata = &calldata[10..]; + + // chunk in blocks of 32 bytes (64 hex chars) + let calldata_words = calldata + .as_bytes() + .chunks(64) + .map(|chunk| { + let s = std::str::from_utf8(chunk).unwrap(); + s + }) + .collect::>(); + + for i in 0..calldata_words.len() { + let (is_abi_encoded, _, coverages) = is_parameter_abiencoded(i, &calldata_words); + println!( + "{i} - is_abi_encoded: {}, with word coverage: {:?}", + is_abi_encoded, coverages + ); + + if i == 1 { + assert!(is_abi_encoded); + assert_eq!(coverages, Some(HashSet::from([1, 4, 5, 6]))); + } else if i == 3 { + assert!(is_abi_encoded); + assert_eq!(coverages, Some(HashSet::from([3, 7, 8]))); + } else { + assert!(!is_abi_encoded); + assert_eq!(coverages, None); + } + } + } + #[test] + fn test_detect_abi_encoded_parameters_2() { + // calldata from https://docs.soliditylang.org/en/develop/abi-spec.html + // * Signature: `f(uint256,uint32[],bytes10,bytes)` + // * Values: `(0x123, [0x456, 0x789], "1234567890", "Hello, world!")` + // 0x8be65246 + // 0. 0000000000000000000000000000000000000000000000000000000000000123 + // 1. 0000000000000000000000000000000000000000000000000000000000000080 + // 2. 3132333435363738393000000000000000000000000000000000000000000000 + // 3. 00000000000000000000000000000000000000000000000000000000000000e0 + // 4. 0000000000000000000000000000000000000000000000000000000000000002 + // 5. 0000000000000000000000000000000000000000000000000000000000000456 + // 6. 0000000000000000000000000000000000000000000000000000000000000789 + // 7. 000000000000000000000000000000000000000000000000000000000000002d + // 8. 48656c6c6f2c20776f726c642148656c6c6f2c20776f726c642148656c6c6f2c + // 9. 48656c6c6f2c20776f726c642100000000000000000000000000000000000000 + let calldata = "0x8be6524600000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000080313233343536373839300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000004560000000000000000000000000000000000000000000000000000000000000789000000000000000000000000000000000000000000000000000000000000002d48656c6c6f2c20776f726c642148656c6c6f2c20776f726c642148656c6c6f2c48656c6c6f2c20776f726c642100000000000000000000000000000000000000"; + + let calldata = &calldata[10..]; + + // chunk in blocks of 32 bytes (64 hex chars) + let calldata_words = calldata + .as_bytes() + .chunks(64) + .map(|chunk| { + let s = std::str::from_utf8(chunk).unwrap(); + s + }) + .collect::>(); + + for i in 0..calldata_words.len() { + let (is_abi_encoded, _, coverages) = is_parameter_abiencoded(i, &calldata_words); + println!( + "{i} - is_abi_encoded: {}, with word coverage: {:?}", + is_abi_encoded, coverages + ); + + if i == 1 { + assert!(is_abi_encoded); + assert_eq!(coverages, Some(HashSet::from([1, 4, 5, 6]))); + } else if i == 3 { + assert!(is_abi_encoded); + assert_eq!(coverages, Some(HashSet::from([3, 7, 8, 9]))); + } else { + assert!(!is_abi_encoded); + assert_eq!(coverages, None); + } + } + } +} diff --git a/core/src/decode/core/mod.rs b/core/src/decode/core/mod.rs new file mode 100644 index 00000000..69ab6724 --- /dev/null +++ b/core/src/decode/core/mod.rs @@ -0,0 +1 @@ +pub mod abi; diff --git a/core/src/decode/mod.rs b/core/src/decode/mod.rs index c80e3f91..096ef976 100644 --- a/core/src/decode/mod.rs +++ b/core/src/decode/mod.rs @@ -1,6 +1,7 @@ +mod core; mod util; -use std::time::Duration; +use std::{collections::HashSet, time::Duration}; use clap::{AppSettings, Parser}; use derive_builder::Builder; @@ -10,9 +11,12 @@ use ethers::{ }; use heimdall_common::{ - constants::TRANSACTION_HASH_REGEX, + constants::{CALLDATA_REGEX, TRANSACTION_HASH_REGEX}, ether::{ - evm::core::types::{display, parse_function_parameters}, + evm::core::types::{ + display, get_padding, get_potential_types_for_word, parse_function_parameters, to_type, + Padding, + }, rpc::get_transaction, signatures::{score_signature, ResolveSelector, ResolvedFunction}, }, @@ -22,7 +26,7 @@ use heimdall_common::{ use indicatif::ProgressBar; use strsim::normalized_damerau_levenshtein as similarity; -use crate::decode::util::get_explanation; +use crate::decode::{core::abi::is_parameter_abiencoded, util::get_explanation}; #[derive(Debug, Clone, Parser, Builder)] #[clap( @@ -90,22 +94,22 @@ pub async fn decode(args: DecodeArgs) -> Result, Box level.as_str(), None => "SILENT", }); - // init variables - let mut raw_transaction: Transaction = Transaction::default(); - let calldata; - // 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); } + // init variables + let mut raw_transaction: Transaction = Transaction::default(); + let mut calldata; + // determine whether or not the target is a transaction hash if TRANSACTION_HASH_REGEX.is_match(&args.target).unwrap() { // We are decoding a transaction hash, so we need to fetch the calldata from the RPC @@ -113,19 +117,14 @@ pub async fn decode(args: DecodeArgs) -> Result, Box Result, Box Result, Box signatures, None => Vec::new(), }; - let mut matches: Vec = Vec::new(); + if potential_matches.is_empty() { + logger.warn("couldn't resolve potential matches for the given function selector."); + } + let mut matches: Vec = Vec::new(); for potential_match in &potential_matches { // convert the string inputs into a vector of decoded types let mut inputs: Vec = Vec::new(); @@ -175,74 +173,71 @@ pub async fn decode(args: DecodeArgs) -> Result, Box { - // convert tokens to params - let mut params: Vec = Vec::new(); - for (i, input) in inputs.iter().enumerate() { - params.push(Param { - name: format!("arg{i}"), - kind: input.to_owned(), - internal_type: None, - }); - } - // build the decoded function to verify it's a match - let decoded_function_call = Function { - name: potential_match.name.to_string(), - inputs: params, - outputs: Vec::new(), - constant: None, - state_mutability: StateMutability::NonPayable, - } - .encode_input(&result); - match decoded_function_call { - Ok(decoded_function_call) => { - // decode the function call in trimmed bytes, removing 0s, because contracts - // can use nonstandard sized words and padding is - // hard - let cleaned_bytes = decoded_function_call.encode_hex().replace('0', ""); - let decoded_function_call = match cleaned_bytes - .split_once(&function_selector.replace('0', "")) - { - Some(decoded_function_call) => decoded_function_call.1, - None => { - logger.debug(&format!("potential match '{}' ignored. decoded inputs differed from provided calldata.", &potential_match.signature).to_string()); - continue - } - }; - - // if the decoded function call matches (95%) the function signature, add it - // to the list of matches - if similarity(decoded_function_call, &calldata[8..].replace('0', "")).abs() >= - 0.90 - { - let mut found_match = potential_match.clone(); - found_match.decoded_inputs = Some(result); - matches.push(found_match); - } else { + if let Ok(result) = decode_abi(&inputs, &byte_args) { + // convert tokens to params + let mut params: Vec = Vec::new(); + for (i, input) in inputs.iter().enumerate() { + params.push(Param { + name: format!("arg{i}"), + kind: input.to_owned(), + internal_type: None, + }); + } + // build the decoded function to verify it's a match + let decoded_function_call = Function { + name: potential_match.name.to_string(), + inputs: params, + outputs: Vec::new(), + constant: None, + state_mutability: StateMutability::NonPayable, + } + .encode_input(&result); + match decoded_function_call { + Ok(decoded_function_call) => { + // decode the function call in trimmed bytes, removing 0s, because contracts + // can use nonstandard sized words and padding is + // hard + let cleaned_bytes = decoded_function_call.encode_hex().replace('0', ""); + let decoded_function_call = match cleaned_bytes + .split_once(&function_selector.replace('0', "")) + { + Some(decoded_function_call) => decoded_function_call.1, + None => { logger.debug(&format!("potential match '{}' ignored. decoded inputs differed from provided calldata.", &potential_match.signature).to_string()); + continue; } - } - Err(_) => { - logger.debug( - &format!( - "potential match '{}' ignored. type checking failed", - &potential_match.signature - ) - .to_string(), - ); + }; + + // if the decoded function call matches (95%) the function signature, add it + // to the list of matches + if similarity(decoded_function_call, &calldata[8..].replace('0', "")).abs() >= + 0.90 + { + let mut found_match = potential_match.clone(); + found_match.decoded_inputs = Some(result); + matches.push(found_match); + } else { + logger.debug(&format!("potential match '{}' ignored. decoded inputs differed from provided calldata.", &potential_match.signature).to_string()); } } + Err(_) => { + logger.debug( + &format!( + "potential match '{}' ignored. type checking failed", + &potential_match.signature + ) + .to_string(), + ); + } } - Err(_) => { - logger.debug( - &format!( - "potential match '{}' ignored. decoding types failed", - &potential_match.signature - ) - .to_string(), - ); - } + } else { + logger.debug( + &format!( + "potential match '{}' ignored. decoding types failed", + &potential_match.signature + ) + .to_string(), + ); } } @@ -257,31 +252,140 @@ pub async fn decode(args: DecodeArgs) -> Result, Box of all possible types for each + let mut potential_inputs: Vec = Vec::new(); + + // chunk in blocks of 32 bytes (64 hex chars) + let calldata_words = calldata[8..] + .as_bytes() + .chunks(64) + .map(|chunk| { + let s = std::str::from_utf8(chunk).unwrap(); + s + }) + .collect::>(); + + // while calldata_words is not empty, iterate over it + let mut i = 0; + let mut covered_words = HashSet::new(); + while covered_words.len() != calldata_words.len() { + let word = calldata_words[i]; + + // check if the first word is abiencoded + let (is_abi_encoded, ty, abi_encoded_word_coverage) = + is_parameter_abiencoded(i, &calldata_words); + + // if is_abi_encoded, add the type to potential_inputs + if is_abi_encoded { + let potential_type = to_type(&ty.expect("no abi encoded types found")); + potential_inputs.push(potential_type); + covered_words.extend(abi_encoded_word_coverage.unwrap()); + } else { + let (_, mut potential_types) = get_potential_types_for_word(word); + + // perform heuristics + // - if we use right-padding, this is probably bytesN + // - if we use left-padding, this is probably uintN or intN + // - if we use no padding, this is probably bytes32 + match get_padding(word) { + Padding::Left => potential_types.retain(|t| t.starts_with("uint")), + _ => potential_types.retain(|t| t.starts_with("bytes")), + } - // build a trace of the calldata - let decode_call = trace.add_call( - 0, - line!(), - "heimdall".to_string(), - "decode".to_string(), - vec![shortened_target], - "()".to_string(), - ); - trace.br(decode_call); - trace.add_message(decode_call, line!(), vec![format!("selector: 0x{function_selector}",)]); - trace.add_message( - decode_call, - line!(), - vec![format!("calldata: {} bytes", calldata.len() / 2usize)], - ); - trace.br(decode_call); + let potential_type = + to_type(potential_types.first().expect("no potential types found")); + potential_inputs.push(potential_type); + covered_words.insert(i); + } - // force the trace to display - trace.level = 4; - trace.display(); + i += 1; + } + + // use potential_inputs to decode calldata + if let Ok(decoded_inputs) = decode_abi(&potential_inputs, &byte_args) { + // convert tokens to params + let mut params: Vec = Vec::new(); + for (i, input) in potential_inputs.iter().enumerate() { + params.push(Param { + name: format!("arg{i}"), + kind: input.to_owned(), + internal_type: None, + }); + } + + // build a trace of the calldata + let decode_call = trace.add_call( + 0, + line!(), + "heimdall".to_string(), + "decode".to_string(), + vec![shortened_target], + "()".to_string(), + ); + trace.br(decode_call); + trace.add_message(decode_call, line!(), vec!["name: unknown".to_string()]); + trace.add_message( + decode_call, + line!(), + vec![format!("signature: Unresolved_{}", function_selector)], + ); + trace.add_message( + decode_call, + line!(), + vec![format!("selector: 0x{function_selector}",)], + ); + trace.add_message( + decode_call, + line!(), + vec![format!("calldata: {} bytes", calldata.len() / 2usize)], + ); + trace.br(decode_call); + + // build decoded string for --explain + let decoded_string = &mut format!( + "{}\n{}\n{}\n{}", + format!("name: unknown"), + format!("signature: Unresolved_{}", function_selector), + format!("selector: 0x{function_selector}"), + format!("calldata: {} bytes", calldata.len() / 2usize) + ); + + // build inputs + for (i, input) in decoded_inputs.iter().enumerate() { + let mut decoded_inputs_as_message = display(vec![input.to_owned()], " "); + if decoded_inputs_as_message.is_empty() { + break; + } + + if i == 0 { + decoded_inputs_as_message[0] = format!( + "input {}:{}{}", + i, + " ".repeat(4 - i.to_string().len()), + decoded_inputs_as_message[0].replacen(" ", "", 1) + ) + } else { + decoded_inputs_as_message[0] = format!( + " {}:{}{}", + i, + " ".repeat(4 - i.to_string().len()), + decoded_inputs_as_message[0].replacen(" ", "", 1) + ) + } + + // add to trace and decoded string + trace.add_message(decode_call, 1, decoded_inputs_as_message.clone()); + decoded_string + .push_str(&format!("\n{}", decoded_inputs_as_message.clone().join("\n"))); + } + + // force the trace to display + trace.level = 4; + trace.display(); + } else { + logger.error("failed to dynamically decode calldata."); + } } else { let mut selection: u8 = 0; @@ -350,7 +454,7 @@ pub async fn decode(args: DecodeArgs) -> Result, Box>() .join(",") )); diff --git a/core/src/snapshot/resolve.rs b/core/src/snapshot/resolve.rs index a4c6508d..62863da9 100644 --- a/core/src/snapshot/resolve.rs +++ b/core/src/snapshot/resolve.rs @@ -22,7 +22,7 @@ pub fn match_parameters( &function .arguments .values() - .map(|(_, types)| types.get(0).unwrap().clone()) + .map(|(_, types)| types.first().unwrap().clone()) .collect::>() .join(",") )); From 5da9820a15b707fbfb5d777e2086a2c54937eb5f Mon Sep 17 00:00:00 2001 From: Jon-Becker Date: Thu, 30 Nov 2023 02:19:37 -0500 Subject: [PATCH 3/9] fix(decode): fix type guessing overflow check --- core/src/decode/core/abi.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/decode/core/abi.rs b/core/src/decode/core/abi.rs index a9c34a78..61ff5f15 100644 --- a/core/src/decode/core/abi.rs +++ b/core/src/decode/core/abi.rs @@ -13,9 +13,8 @@ pub fn is_parameter_abiencoded( let mut coverages = HashSet::from([parameter_index]); // convert this word to a U256 - let word = U256::from_str_radix(calldata_words[parameter_index], 16) - .unwrap() - .max(U256::from(u128::max_value())); + // TODO: this can panic. make this entire function a `Result` + let word = U256::from_str_radix(calldata_words[parameter_index], 16).unwrap(); // if the word is a multiple of 32, it may be an offset pointing to the start of an // ABI-encoded item From 71e8374c4ad0210eec6c209e24a0ffce52ba5e92 Mon Sep 17 00:00:00 2001 From: Jon-Becker Date: Thu, 30 Nov 2023 18:59:00 -0500 Subject: [PATCH 4/9] feat(decode): handle nested types, strings --- Cargo.lock | 1 + common/src/ether/evm/core/types.rs | 147 ++++++++++-- common/src/utils/io/logging.rs | 2 +- common/src/utils/io/macros.rs | 9 + common/src/utils/io/mod.rs | 1 + core/Cargo.toml | 1 + core/src/decode/core/abi.rs | 364 ++++++++++++++++++++++++++--- core/src/decode/mod.rs | 25 +- core/src/error.rs | 11 + core/src/lib.rs | 1 + 10 files changed, 500 insertions(+), 62 deletions(-) create mode 100644 common/src/utils/io/macros.rs create mode 100644 core/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index c28eb2f4..42b98c34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1728,6 +1728,7 @@ dependencies = [ "serde", "serde_json", "strsim", + "thiserror", "tokio", "tui", ] diff --git a/common/src/ether/evm/core/types.rs b/common/src/ether/evm/core/types.rs index 0cbdf509..a1b472c1 100644 --- a/common/src/ether/evm/core/types.rs +++ b/common/src/ether/evm/core/types.rs @@ -1,3 +1,5 @@ +use std::collections::VecDeque; + use colored::Colorize; use ethers::abi::{AbiEncode, ParamType, Token}; @@ -183,27 +185,27 @@ fn is_first_type_tuple(string: &str) -> bool { /// ParamType. For example, "address" will be converted to [`ParamType::Address`]. pub fn to_type(string: &str) -> ParamType { let is_array = string.ends_with(']'); + let mut array_size: VecDeque> = VecDeque::new(); + let mut string = string.to_string(); - // get size of array - let array_size = if is_array { - let (start, end, valid) = find_balanced_encapsulator(string, ('[', ']')); + // while string contains a [..] + while string.ends_with(']') { + let (start, end, valid) = find_balanced_encapsulator(&string, ('[', ']')); if !valid { - return ParamType::Bytes + return ParamType::Bytes // default to bytes if invalid } let size = string[start + 1..end - 1].to_string(); - match size.parse::() { + + array_size.push_back(match size.parse::() { Ok(size) => Some(size), Err(_) => None, - } - } else { - None - }; + }); - // if array, remove the [..] from the string - let string = if is_array { string.splitn(2, '[').collect::>()[0] } else { string }; + string = string.replacen(&format!("[{}]", &size), "", 1); + } - let arg_type = match string { + let arg_type = match string.as_str() { "address" => ParamType::Address, "bool" => ParamType::Bool, "string" => ParamType::String, @@ -219,20 +221,29 @@ pub fn to_type(string: &str) -> ParamType { let size = stripped.parse::().unwrap(); ParamType::FixedBytes(size) } else { - panic!("Invalid type: '{}'", string); + // default to bytes if invalid + ParamType::Bytes } } }; if is_array { - if let Some(size) = array_size { - ParamType::FixedArray(Box::new(arg_type), size) - } else { - ParamType::Array(Box::new(arg_type)) + let mut arg_type = arg_type; + + // while array_size is not empty + while !array_size.is_empty() { + // pop off first element of array_size + if let Some(size) = array_size.pop_front().unwrap() { + arg_type = ParamType::FixedArray(Box::new(arg_type), size); + } else { + arg_type = ParamType::Array(Box::new(arg_type)); + } } - } else { - arg_type + + return arg_type; } + + arg_type } /// A helper function used by the decode module to pretty format decoded tokens. @@ -343,6 +354,8 @@ pub fn byte_size_to_type(byte_size: usize) -> (usize, Vec) { _ => {} } + // TODO: add a check for addresses with null-byte prefixes + // push arbitrary types to the array potential_types.push(format!("uint{}", byte_size * 8)); potential_types.push(format!("bytes{byte_size}")); @@ -778,4 +791,100 @@ mod tests { let input = "XYZ"; assert_eq!(get_padding(input), Padding::None); } + + #[test] + fn test_to_type_address() { + let input = "address"; + assert_eq!(super::to_type(input), ParamType::Address); + } + + #[test] + fn test_to_type_bool() { + let input = "bool"; + assert_eq!(super::to_type(input), ParamType::Bool); + } + + #[test] + fn test_to_type_string() { + let input = "string"; + assert_eq!(super::to_type(input), ParamType::String); + } + + #[test] + fn test_to_type_bytes() { + let input = "bytes"; + assert_eq!(super::to_type(input), ParamType::Bytes); + } + + #[test] + fn test_to_type_uint256() { + let input = "uint256"; + assert_eq!(super::to_type(input), ParamType::Uint(256)); + } + + #[test] + fn test_to_type_int() { + let input = "int256"; + assert_eq!(super::to_type(input), ParamType::Int(256)); + } + + #[test] + fn test_to_type_bytes1() { + let input = "bytes1"; + assert_eq!(super::to_type(input), ParamType::FixedBytes(1)); + } + + #[test] + fn test_to_type_uint() { + let input = "uint"; + assert_eq!(super::to_type(input), ParamType::Uint(256)); + } + + #[test] + fn test_to_type_array() { + let input = "uint8[]"; + assert_eq!(super::to_type(input), ParamType::Array(Box::new(ParamType::Uint(8)))); + } + + #[test] + fn test_to_type_nested_array() { + let input = "uint8[][]"; + assert_eq!( + super::to_type(input), + ParamType::Array(Box::new(ParamType::Array(Box::new(ParamType::Uint(8))))) + ); + } + + #[test] + fn test_to_type_fixed_array() { + let input = "uint8[2]"; + assert_eq!(super::to_type(input), ParamType::FixedArray(Box::new(ParamType::Uint(8)), 2)); + } + + #[test] + fn test_to_type_nested_fixed_array() { + let input = "uint8[2][2]"; + assert_eq!( + super::to_type(input), + ParamType::FixedArray( + Box::new(ParamType::FixedArray(Box::new(ParamType::Uint(8)), 2)), + 2 + ) + ); + } + + #[test] + fn test_to_type_nested_fixed_array_ordering() { + let input = "uint8[2][3][2]"; + assert_eq!( + super::to_type(input), + ParamType::FixedArray( + Box::new(ParamType::FixedArray( + Box::new(ParamType::FixedArray(Box::new(ParamType::Uint(8)), 2)), + 3 + )), + 2 + ) + ); + } } diff --git a/common/src/utils/io/logging.rs b/common/src/utils/io/logging.rs index f813aadd..518e3b54 100644 --- a/common/src/utils/io/logging.rs +++ b/common/src/utils/io/logging.rs @@ -473,7 +473,7 @@ impl Logger { "{} {}: {}", pretty_timestamp().dimmed(), "debug".bright_white().bold(), - message + message.replace('\n', &("\n".to_owned() + &" ".repeat(31))) ); } } diff --git a/common/src/utils/io/macros.rs b/common/src/utils/io/macros.rs new file mode 100644 index 00000000..af47580a --- /dev/null +++ b/common/src/utils/io/macros.rs @@ -0,0 +1,9 @@ +#[macro_export] +macro_rules! debug_max { + ($message:expr) => { + heimdall_common::utils::io::logging::Logger::default().debug_max($message); + }; + ($message:expr, $($arg:tt)*) => { + heimdall_common::utils::io::logging::Logger::default().debug_max(&format!($message, $($arg)*)); + }; +} diff --git a/common/src/utils/io/mod.rs b/common/src/utils/io/mod.rs index 62f9fc36..00fe6325 100644 --- a/common/src/utils/io/mod.rs +++ b/common/src/utils/io/mod.rs @@ -1,2 +1,3 @@ pub mod file; pub mod logging; +pub mod macros; diff --git a/core/Cargo.toml b/core/Cargo.toml index d83ae3eb..3becf013 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -8,6 +8,7 @@ readme = "README.md" version = "0.6.4" [dependencies] +thiserror = "1.0.50" backtrace = "0.3" clap = {version = "3.1.18", features = ["derive"]} clap-verbosity-flag = "1.0.0" diff --git a/core/src/decode/core/abi.rs b/core/src/decode/core/abi.rs index 61ff5f15..6efab0fe 100644 --- a/core/src/decode/core/abi.rs +++ b/core/src/decode/core/abi.rs @@ -1,55 +1,78 @@ use std::collections::HashSet; use ethers::types::U256; -use heimdall_common::ether::evm::core::types::{ - get_padding, get_padding_size, get_potential_types_for_word, Padding, +use heimdall_common::{ + debug_max, + ether::evm::core::types::{ + get_padding, get_padding_size, get_potential_types_for_word, Padding, + }, }; +use crate::error::Error; + /// Finds the offsets of all ABI-encoded items in the given calldata. pub fn is_parameter_abiencoded( parameter_index: usize, calldata_words: &[&str], -) -> (bool, Option, Option>) { +) -> Result<(bool, Option, Option>), Error> { let mut coverages = HashSet::from([parameter_index]); // convert this word to a U256 // TODO: this can panic. make this entire function a `Result` - let word = U256::from_str_radix(calldata_words[parameter_index], 16).unwrap(); + let word = U256::from_str_radix(calldata_words[parameter_index], 16)?; // if the word is a multiple of 32, it may be an offset pointing to the start of an // ABI-encoded item - if word % 32 != U256::zero() { - return (false, None, None); + if word % 32 != U256::zero() || word == U256::zero() { + debug_max!( + "parameter {}: '{}' doesnt appear to be an offset ptr", + parameter_index, + calldata_words[parameter_index] + ); + return Ok((false, None, None)); } // check if the pointer is pointing to a valid location in the calldata let word_offset = word / 32; if word_offset >= U256::from(calldata_words.len()) { - return (false, None, None); + debug_max!( + "parameter {}: '{}' is out of bounds (offset check)", + parameter_index, + calldata_words[parameter_index] + ); + return Ok((false, None, None)); } + coverages.insert(word_offset.as_usize()); // note: `size` is the size of the ABI-encoded item. It varies depending on the type of the // item. - let size = U256::from_str_radix( - calldata_words.get(word_offset.as_usize()).expect("word_offset out of bounds"), - 16, - ) - .unwrap(); + let size_word = calldata_words.get(word_offset.as_usize()).ok_or(Error::BoundsError)?; + let size = U256::from_str_radix(size_word, 16)?.min(U256::from(usize::MAX)); // check if there are enough words left in the calldata to contain the ABI-encoded item. // if there aren't, it doesn't necessarily mean that the calldata is invalid, but it does // indicate that they aren't an array of items. let data_start_word_offset = word_offset + 1; let data_end_word_offset = data_start_word_offset + size; - if data_end_word_offset >= U256::from(calldata_words.len()) { + if data_end_word_offset > U256::from(calldata_words.len()) { // this could still be bytes, string, or a non ABI-encoded item + debug_max!( + "parameter {}: '{}' may be bytes, string", + parameter_index, + calldata_words[parameter_index] + ); // join words AFTER the word_offset. these are where potential data is. let data_words = &calldata_words[data_start_word_offset.as_usize()..]; // check if there are enough remaining bytes to contain the ABI-encoded item. if data_words.join("").len() / 2 < size.as_usize() { - return (false, None, None); + debug_max!( + "parameter {}: '{}' is out of bounds (bytes check)", + parameter_index, + calldata_words[parameter_index] + ); + return Ok((false, None, None)); } // if `size` is less than 32 bytes, we only need to check the first word for `32 - size` @@ -57,30 +80,38 @@ pub fn is_parameter_abiencoded( // data. if size <= U256::from(32) { let potential_data = data_words[0]; + debug_max!("with data: {}", potential_data); // get the padding of the data let padding_size = get_padding_size(potential_data); // if the padding is greater than `32 - size`, then this is not an ABI-encoded item. if padding_size > 32 - size.as_usize() { - return (false, None, None); + debug_max!("parameter {}: '{}' with size {} cannot fit into word with padding of {} bytes (bytes)", parameter_index, calldata_words[parameter_index], size, padding_size); + return Ok((false, None, None)); } // insert the word offset into the coverage set coverages.insert(data_start_word_offset.as_usize()); coverages.insert(word_offset.as_usize()); - (true, Some(String::from("bytes")), Some(coverages)) + debug_max!( + "parameter {}: '{}' is bytes", + parameter_index, + calldata_words[parameter_index] + ); + Ok((true, Some(String::from("bytes")), Some(coverages))) } else { // recalculate data_end_word_offset based on `size` // size is in bytes, and one word is 32 bytes. find out how many words we need to // cover `size` bytes. let word_count_for_size = U256::from((size.as_u32() as f32 / 32f32).ceil() as u32); // wont panic unless calldata is huge let data_end_word_offset = data_start_word_offset + word_count_for_size; + let data_words = &calldata_words[data_start_word_offset.as_usize()..]; + debug_max!("with data: {:#?}", data_words.join("")); // get the last word of the data - let last_word = data_words - .get(word_count_for_size.as_usize() - 1) - .expect("word_count_for_size out of bounds"); + let last_word = + data_words.get(word_count_for_size.as_usize() - 1).ok_or(Error::BoundsError)?; // how many bytes should be in the last word? let last_word_size = size.as_usize() % 32; @@ -89,7 +120,8 @@ pub fn is_parameter_abiencoded( // item. let padding_size = get_padding_size(last_word); if padding_size > 32 - last_word_size { - return (false, None, None); + debug_max!("parameter {}: '{}' with size {} cannot fit into last word with padding of {} bytes (bytes)", parameter_index, calldata_words[parameter_index], size, padding_size); + return Ok((false, None, None)); } // insert all word offsets from `data_start_word_offset` to `data_end_word_offset` into @@ -98,16 +130,145 @@ pub fn is_parameter_abiencoded( coverages.insert(i); } - (true, Some(String::from("bytes")), Some(coverages)) + debug_max!( + "parameter {}: '{}' is bytes", + parameter_index, + calldata_words[parameter_index] + ); + Ok((true, Some(String::from("bytes")), Some(coverages))) } } else { // this could be an array of items. + debug_max!( + "parameter {}: '{}' may be an array", + parameter_index, + calldata_words[parameter_index] + ); let data_words = &calldata_words[data_start_word_offset.as_usize()..data_end_word_offset.as_usize()]; + debug_max!("potential array items: {:#?}", data_words); + + // check if the data words all have conforming padding + // we do this check because strings will typically be of the form: + // 0000000000000000000000000000000000000000000000000000000000000003 // length of 3 + // 6f6e650000000000000000000000000000000000000000000000000000000000 // "one" + // + // so, if the data words have conforming padding, we can assume that this is not a string + // and is instead an array. + let padding_matches: bool = data_words + .iter() + .map(|word| get_padding(word)) + .all(|padding| padding == get_padding(data_words[0])); + if !padding_matches { + // size is in bytes now, we just need to do the same as bytes bound checking + debug_max!( + "parameter {}: '{}' may be string", + parameter_index, + calldata_words[parameter_index] + ); + + // if `size` is less than 32 bytes, we only need to check the first word for `32 - size` + // null-bytes. tricky because sometimes a null byte could be part of the + // data. + if size <= U256::from(32) { + let potential_data = data_words[0]; + debug_max!("with data: {}", potential_data); + + // get the padding of the data + let padding_size = get_padding_size(potential_data); + + // if the padding is greater than `32 - size`, then this is not an ABI-encoded item. + if padding_size > 32 - size.as_usize() { + debug_max!("parameter {}: '{}' with size {} cannot fit into word with padding of {} bytes (string)", parameter_index, calldata_words[parameter_index], size, padding_size); + return Ok((false, None, None)); + } + + // yay! we have a string! + // insert the word offset into the coverage set + coverages.insert(data_start_word_offset.as_usize()); + coverages.insert(word_offset.as_usize()); + } else { + // recalculate data_end_word_offset based on `size` + // size is in bytes, and one word is 32 bytes. find out how many words we need to + // cover `size` bytes. + let word_count_for_size = U256::from((size.as_u32() as f32 / 32f32).ceil() as u32); // wont panic unless calldata is huge + let data_end_word_offset = data_start_word_offset + word_count_for_size; + debug_max!( + "with data: {:#?}", + calldata_words + [data_start_word_offset.as_usize()..data_end_word_offset.as_usize()] + .join("") + ); + + // get the last word of the data + let last_word = + data_words.get(word_count_for_size.as_usize() - 1).ok_or(Error::BoundsError)?; + + // how many bytes should be in the last word? + let last_word_size = size.as_usize() % 32; + + // if the padding is greater than `32 - last_word_size`, then this is not an + // ABI-encoded item. + let padding_size = get_padding_size(last_word); + if padding_size > 32 - last_word_size { + debug_max!("parameter {}: '{}' with size {} cannot fit into last word with padding of {} bytes (string)", parameter_index, calldata_words[parameter_index], size, padding_size); + return Ok((false, None, None)); + } + + // yay! we have a string! + // insert all word offsets from `data_start_word_offset` to `data_end_word_offset` + // into the coverage set + for i in word_offset.as_usize()..data_end_word_offset.as_usize() { + coverages.insert(i); + } + } + + debug_max!( + "parameter {}: '{}' is string", + parameter_index, + calldata_words[parameter_index] + ); + return Ok((true, Some(String::from("string")), Some(coverages))); + } - let (_min_size, potential_type) = data_words + // map over the array of words and get the potential types for each word, then, + // get the type that best fits the array. + let (_, potential_type) = data_words .iter() - .map(|w| { + .enumerate() + .map(|(i, w)| { + // we need to get a slice of calldata_words from `data_start_word_offset` to the end + // of the calldata_words. this is because nested abi-encoded items + // reset the offsets of the words. + let data_words = &calldata_words[data_start_word_offset.as_usize()..]; + + // first, check if this word *could* be a nested abi-encoded item + debug_max!( + "parameter {}: '{}' checking for nested abi-encoded data", + parameter_index, + calldata_words[parameter_index] + ); + if let Ok((is_abi_encoded, ty, nested_coverages)) = + is_parameter_abiencoded(i, data_words) + { + // we need to add data_start_word_offset to all the offsets in nested_coverages + // because they are relative to the start of the nested abi-encoded item. + let nested_coverages = nested_coverages.map(|nested_coverages| { + nested_coverages + .into_iter() + .map(|nested_coverage| { + nested_coverage + data_start_word_offset.as_usize() + }) + .collect::>() + }); + + if is_abi_encoded { + // merge coverages and nested_coverages + coverages.extend(nested_coverages.unwrap()); + return (32, vec![ty.unwrap()]); + } + } + let (padding_size, mut potential_types) = get_potential_types_for_word(w); // perform heuristics @@ -115,21 +276,27 @@ pub fn is_parameter_abiencoded( // - if we use left-padding, this is probably uintN or intN // - if we use no padding, this is probably bytes32 match get_padding(w) { - Padding::Left => potential_types.retain(|t| t.starts_with("uint")), - _ => potential_types.retain(|t| t.starts_with("bytes")), + Padding::Left => potential_types + .retain(|t| t.starts_with("uint") || t.starts_with("address")), + _ => potential_types + .retain(|t| t.starts_with("bytes") || t.starts_with("string")), } (padding_size, potential_types) }) - .fold((0, String::from("")), |(max_size, mut potential_types), (size, types)| { + .fold((0, String::from("")), |(max_size, mut potential_type), (size, types)| { + // "address" and "string" are priority types + if types.contains(&String::from("string")) { + return (32, String::from("string")); + } else if types.contains(&String::from("address")) { + return (32, String::from("address")); + } + if size > max_size { - potential_types = types - .first() - .expect("potential types is empty when decoding abi.encoded array") - .clone(); - (size, potential_types) + potential_type = types.first().expect("types is empty").clone(); + (max_size, potential_type) } else { - (max_size, potential_types) + (max_size, potential_type) } }); @@ -138,7 +305,14 @@ pub fn is_parameter_abiencoded( for i in word_offset.as_usize()..data_end_word_offset.as_usize() { coverages.insert(i); } - (true, Some(format!("{potential_type}[]")), Some(coverages)) + let type_str = format!("{potential_type}[]"); + debug_max!( + "parameter {}: '{}' is {}", + parameter_index, + calldata_words[parameter_index], + type_str + ); + Ok((true, Some(type_str), Some(coverages))) } } @@ -176,7 +350,8 @@ mod tests { .collect::>(); for i in 0..calldata_words.len() { - let (is_abi_encoded, _, coverages) = is_parameter_abiencoded(i, &calldata_words); + let (is_abi_encoded, _, coverages) = + is_parameter_abiencoded(i, &calldata_words).unwrap(); println!( "{i} - is_abi_encoded: {}, with word coverage: {:?}", is_abi_encoded, coverages @@ -194,6 +369,7 @@ mod tests { } } } + #[test] fn test_detect_abi_encoded_parameters_2() { // calldata from https://docs.soliditylang.org/en/develop/abi-spec.html @@ -225,7 +401,8 @@ mod tests { .collect::>(); for i in 0..calldata_words.len() { - let (is_abi_encoded, _, coverages) = is_parameter_abiencoded(i, &calldata_words); + let (is_abi_encoded, _, coverages) = + is_parameter_abiencoded(i, &calldata_words).unwrap(); println!( "{i} - is_abi_encoded: {}, with word coverage: {:?}", is_abi_encoded, coverages @@ -243,4 +420,121 @@ mod tests { } } } + + #[test] + fn test_detect_abi_encoded_complex() { + // calldata from https://docs.soliditylang.org/en/develop/abi-spec.html + // * Signature: `g(uint256[][],string[])` + // * Values: `([[1, 2], [3]], ["one", "two", "three"])` + // - **Coverages** + // - uint256[][]: [0, 2, 3, 4, 5, 6, 7, 8, 9] + // - string[]: [1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] + // 0x2289b18c + // 0. 0000000000000000000000000000000000000000000000000000000000000040 + // 1. 0000000000000000000000000000000000000000000000000000000000000140 + // 2. 0000000000000000000000000000000000000000000000000000000000000002 + // 3. 0000000000000000000000000000000000000000000000000000000000000040 + // 4. 00000000000000000000000000000000000000000000000000000000000000a0 + // 5. 0000000000000000000000000000000000000000000000000000000000000002 + // 6. 0000000000000000000000000000000000000000000000000000000000000001 + // 7. 0000000000000000000000000000000000000000000000000000000000000002 + // 8. 0000000000000000000000000000000000000000000000000000000000000001 + // 9. 0000000000000000000000000000000000000000000000000000000000000003 + // 10. 0000000000000000000000000000000000000000000000000000000000000003 + // 11. 0000000000000000000000000000000000000000000000000000000000000060 + // 12. 00000000000000000000000000000000000000000000000000000000000000a0 + // 13. 00000000000000000000000000000000000000000000000000000000000000e0 + // 14. 0000000000000000000000000000000000000000000000000000000000000003 + // 15. 6f6e650000000000000000000000000000000000000000000000000000000000 + // 16. 0000000000000000000000000000000000000000000000000000000000000003 + // 17. 74776f0000000000000000000000000000000000000000000000000000000000 + // 18. 0000000000000000000000000000000000000000000000000000000000000005 + // 19. 7468726565000000000000000000000000000000000000000000000000000000 + let calldata = "0x2289b18c000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000036f6e650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000374776f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000057468726565000000000000000000000000000000000000000000000000000000"; + + let calldata = &calldata[10..]; + + // chunk in blocks of 32 bytes (64 hex chars) + let calldata_words = calldata + .as_bytes() + .chunks(64) + .map(|chunk| { + let s = std::str::from_utf8(chunk).unwrap(); + s + }) + .collect::>(); + + for i in 0..calldata_words.len() { + let (is_abi_encoded, ty, coverages) = + is_parameter_abiencoded(i, &calldata_words).unwrap(); + println!( + "{i} - is_abi_encoded: {}, ty: {:?}, with word coverage: {:?}", + is_abi_encoded, ty, coverages + ); + + if i == 0 { + assert!(is_abi_encoded); + assert_eq!(coverages, Some(HashSet::from([0, 2, 3, 4, 5, 6, 7, 8, 9]))); + } else if i == 1 { + assert!(is_abi_encoded); + assert_eq!( + coverages, + Some(HashSet::from([1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])) + ); + } + } + } + + #[test] + fn test_detect_abi_encoded_mixed() { + // calldata from https://docs.soliditylang.org/en/develop/abi-spec.html + // * Signature: `sam(bytes,bool,uint256[])` + // * Values: `("dave", true, [1, 2, 3])` + // - **Coverages** + // - bytes: [0, 3, 4] + // - bool: [1] + // - uint256[]: [2, 5, 6, 7, 8] + // 0xa5643bf2 + // 0. 0000000000000000000000000000000000000000000000000000000000000060- + // 1. 0000000000000000000000000000000000000000000000000000000000000001- + // 2. 00000000000000000000000000000000000000000000000000000000000000a0- + // 3. 0000000000000000000000000000000000000000000000000000000000000004- + // 4. 6461766500000000000000000000000000000000000000000000000000000000- + // 5. 0000000000000000000000000000000000000000000000000000000000000003 + // 6. 0000000000000000000000000000000000000000000000000000000000000001 + // 7. 0000000000000000000000000000000000000000000000000000000000000002 + // 8. 0000000000000000000000000000000000000000000000000000000000000003 + let calldata = "0xa5343bf20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000000000000000000464617665000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003"; + + let calldata = &calldata[10..]; + + // chunk in blocks of 32 bytes (64 hex chars) + let calldata_words = calldata + .as_bytes() + .chunks(64) + .map(|chunk| { + let s = std::str::from_utf8(chunk).unwrap(); + s + }) + .collect::>(); + + for i in 0..calldata_words.len() { + let (is_abi_encoded, ty, coverages) = + is_parameter_abiencoded(i, &calldata_words).unwrap(); + println!( + "{i} - is_abi_encoded: {}, ty: {:?}, with word coverage: {:?}", + is_abi_encoded, ty, coverages + ); + + if i == 0 { + assert!(is_abi_encoded); + assert_eq!(coverages, Some(HashSet::from([0, 3, 4]))); + } else if i == 1 { + assert!(!is_abi_encoded); + } else if i == 2 { + assert!(is_abi_encoded); + assert_eq!(coverages, Some(HashSet::from([2, 5, 6, 7, 8]))); + } + } + } } diff --git a/core/src/decode/mod.rs b/core/src/decode/mod.rs index 096ef976..b5bf14f6 100644 --- a/core/src/decode/mod.rs +++ b/core/src/decode/mod.rs @@ -26,7 +26,10 @@ use heimdall_common::{ use indicatif::ProgressBar; use strsim::normalized_damerau_levenshtein as similarity; -use crate::decode::{core::abi::is_parameter_abiencoded, util::get_explanation}; +use crate::{ + decode::{core::abi::is_parameter_abiencoded, util::get_explanation}, + error::Error, +}; #[derive(Debug, Clone, Parser, Builder)] #[clap( @@ -82,7 +85,7 @@ impl DecodeArgsBuilder { /// The entrypoint for the decode module. This will attempt to decode the arguments of the target /// calldata, without the ABI of the target contract. #[allow(deprecated)] -pub async fn decode(args: DecodeArgs) -> Result, Box> { +pub async fn decode(args: DecodeArgs) -> Result, Error> { // set logger environment variable if not already set if std::env::var("RUST_LOG").is_err() { std::env::set_var( @@ -270,17 +273,22 @@ pub async fn decode(args: DecodeArgs) -> Result, Box>(); + tmp.sort(); + let word = calldata_words[i]; // check if the first word is abiencoded let (is_abi_encoded, ty, abi_encoded_word_coverage) = - is_parameter_abiencoded(i, &calldata_words); + is_parameter_abiencoded(i, &calldata_words)?; // if is_abi_encoded, add the type to potential_inputs if is_abi_encoded { let potential_type = to_type(&ty.expect("no abi encoded types found")); potential_inputs.push(potential_type); - covered_words.extend(abi_encoded_word_coverage.unwrap()); + covered_words.extend(abi_encoded_word_coverage.unwrap()); // safe to unwrap because + // is_abi_encoded is true } else { let (_, mut potential_types) = get_potential_types_for_word(word); @@ -289,12 +297,15 @@ pub async fn decode(args: DecodeArgs) -> Result, Box potential_types.retain(|t| t.starts_with("uint")), - _ => potential_types.retain(|t| t.starts_with("bytes")), + Padding::Left => potential_types + .retain(|t| t.starts_with("uint") || t.starts_with("address")), + _ => potential_types + .retain(|t| t.starts_with("bytes") || t.starts_with("string")), } let potential_type = - to_type(potential_types.first().expect("no potential types found")); + to_type(potential_types.first().expect("potential types is empty")); + potential_inputs.push(potential_type); covered_words.insert(i); } diff --git a/core/src/error.rs b/core/src/error.rs new file mode 100644 index 00000000..26d0cd6a --- /dev/null +++ b/core/src/error.rs @@ -0,0 +1,11 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Json error: {0}")] + SerdeError(#[from] serde_json::Error), + #[error("FromStrRadixError: {0}")] + FromStrRadixError(#[from] ethers::abi::ethereum_types::FromStrRadixErr), + #[error("BoundsError")] + BoundsError, + #[error("Error: {0}")] + GenericError(String), +} diff --git a/core/src/lib.rs b/core/src/lib.rs index 9ba0b185..23a2a4f3 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -3,4 +3,5 @@ pub mod decode; pub mod decompile; pub mod disassemble; pub mod dump; +pub mod error; pub mod snapshot; From eb2fc2a028769ea0a9c1d16489ae76416954ab53 Mon Sep 17 00:00:00 2001 From: Jon-Becker Date: Thu, 30 Nov 2023 19:53:20 -0500 Subject: [PATCH 5/9] feat(decode): update return type for `is_parameter_abi_encoded` --- core/src/decode/core/abi.rs | 108 +++++++++++++++++++----------------- core/src/decode/mod.rs | 13 ++--- 2 files changed, 61 insertions(+), 60 deletions(-) diff --git a/core/src/decode/core/abi.rs b/core/src/decode/core/abi.rs index 6efab0fe..bb293485 100644 --- a/core/src/decode/core/abi.rs +++ b/core/src/decode/core/abi.rs @@ -10,15 +10,20 @@ use heimdall_common::{ use crate::error::Error; +#[derive(Debug, Clone)] +pub struct AbiEncoded { + pub ty: String, + pub coverages: HashSet, +} + /// Finds the offsets of all ABI-encoded items in the given calldata. -pub fn is_parameter_abiencoded( +pub fn is_parameter_abi_encoded( parameter_index: usize, calldata_words: &[&str], -) -> Result<(bool, Option, Option>), Error> { +) -> Result, Error> { let mut coverages = HashSet::from([parameter_index]); // convert this word to a U256 - // TODO: this can panic. make this entire function a `Result` let word = U256::from_str_radix(calldata_words[parameter_index], 16)?; // if the word is a multiple of 32, it may be an offset pointing to the start of an @@ -29,7 +34,7 @@ pub fn is_parameter_abiencoded( parameter_index, calldata_words[parameter_index] ); - return Ok((false, None, None)); + return Ok(None); } // check if the pointer is pointing to a valid location in the calldata @@ -40,7 +45,7 @@ pub fn is_parameter_abiencoded( parameter_index, calldata_words[parameter_index] ); - return Ok((false, None, None)); + return Ok(None); } coverages.insert(word_offset.as_usize()); @@ -72,7 +77,7 @@ pub fn is_parameter_abiencoded( parameter_index, calldata_words[parameter_index] ); - return Ok((false, None, None)); + return Ok(None); } // if `size` is less than 32 bytes, we only need to check the first word for `32 - size` @@ -88,7 +93,7 @@ pub fn is_parameter_abiencoded( // if the padding is greater than `32 - size`, then this is not an ABI-encoded item. if padding_size > 32 - size.as_usize() { debug_max!("parameter {}: '{}' with size {} cannot fit into word with padding of {} bytes (bytes)", parameter_index, calldata_words[parameter_index], size, padding_size); - return Ok((false, None, None)); + return Ok(None); } // insert the word offset into the coverage set @@ -99,7 +104,7 @@ pub fn is_parameter_abiencoded( parameter_index, calldata_words[parameter_index] ); - Ok((true, Some(String::from("bytes")), Some(coverages))) + Ok(Some(AbiEncoded { ty: String::from("bytes"), coverages })) } else { // recalculate data_end_word_offset based on `size` // size is in bytes, and one word is 32 bytes. find out how many words we need to @@ -121,7 +126,7 @@ pub fn is_parameter_abiencoded( let padding_size = get_padding_size(last_word); if padding_size > 32 - last_word_size { debug_max!("parameter {}: '{}' with size {} cannot fit into last word with padding of {} bytes (bytes)", parameter_index, calldata_words[parameter_index], size, padding_size); - return Ok((false, None, None)); + return Ok(None); } // insert all word offsets from `data_start_word_offset` to `data_end_word_offset` into @@ -135,7 +140,7 @@ pub fn is_parameter_abiencoded( parameter_index, calldata_words[parameter_index] ); - Ok((true, Some(String::from("bytes")), Some(coverages))) + Ok(Some(AbiEncoded { ty: String::from("bytes"), coverages })) } } else { // this could be an array of items. @@ -180,7 +185,7 @@ pub fn is_parameter_abiencoded( // if the padding is greater than `32 - size`, then this is not an ABI-encoded item. if padding_size > 32 - size.as_usize() { debug_max!("parameter {}: '{}' with size {} cannot fit into word with padding of {} bytes (string)", parameter_index, calldata_words[parameter_index], size, padding_size); - return Ok((false, None, None)); + return Ok(None); } // yay! we have a string! @@ -212,7 +217,7 @@ pub fn is_parameter_abiencoded( let padding_size = get_padding_size(last_word); if padding_size > 32 - last_word_size { debug_max!("parameter {}: '{}' with size {} cannot fit into last word with padding of {} bytes (string)", parameter_index, calldata_words[parameter_index], size, padding_size); - return Ok((false, None, None)); + return Ok(None); } // yay! we have a string! @@ -228,7 +233,7 @@ pub fn is_parameter_abiencoded( parameter_index, calldata_words[parameter_index] ); - return Ok((true, Some(String::from("string")), Some(coverages))); + return Ok(Some(AbiEncoded { ty: String::from("string"), coverages })); } // map over the array of words and get the potential types for each word, then, @@ -248,25 +253,19 @@ pub fn is_parameter_abiencoded( parameter_index, calldata_words[parameter_index] ); - if let Ok((is_abi_encoded, ty, nested_coverages)) = - is_parameter_abiencoded(i, data_words) + if let Ok(Some(nested_abi_encoded_param)) = is_parameter_abi_encoded(i, data_words) { // we need to add data_start_word_offset to all the offsets in nested_coverages // because they are relative to the start of the nested abi-encoded item. - let nested_coverages = nested_coverages.map(|nested_coverages| { - nested_coverages - .into_iter() - .map(|nested_coverage| { - nested_coverage + data_start_word_offset.as_usize() - }) - .collect::>() - }); - - if is_abi_encoded { - // merge coverages and nested_coverages - coverages.extend(nested_coverages.unwrap()); - return (32, vec![ty.unwrap()]); - } + let nested_coverages = nested_abi_encoded_param + .coverages + .into_iter() + .map(|nested_coverage| nested_coverage + data_start_word_offset.as_usize()) + .collect::>(); + + // merge coverages and nested_coverages + coverages.extend(nested_coverages); + return (32, vec![nested_abi_encoded_param.ty]); } let (padding_size, mut potential_types) = get_potential_types_for_word(w); @@ -312,7 +311,7 @@ pub fn is_parameter_abiencoded( calldata_words[parameter_index], type_str ); - Ok((true, Some(type_str), Some(coverages))) + Ok(Some(AbiEncoded { ty: type_str, coverages })) } } @@ -350,8 +349,10 @@ mod tests { .collect::>(); for i in 0..calldata_words.len() { - let (is_abi_encoded, _, coverages) = - is_parameter_abiencoded(i, &calldata_words).unwrap(); + let abi_encoded_params = is_parameter_abi_encoded(i, &calldata_words).unwrap(); + let is_abi_encoded = abi_encoded_params.is_some(); + let coverages = abi_encoded_params.map(|p| p.coverages).unwrap_or_default(); + println!( "{i} - is_abi_encoded: {}, with word coverage: {:?}", is_abi_encoded, coverages @@ -359,13 +360,13 @@ mod tests { if i == 1 { assert!(is_abi_encoded); - assert_eq!(coverages, Some(HashSet::from([1, 4, 5, 6]))); + assert_eq!(coverages, HashSet::from([1, 4, 5, 6])); } else if i == 3 { assert!(is_abi_encoded); - assert_eq!(coverages, Some(HashSet::from([3, 7, 8]))); + assert_eq!(coverages, HashSet::from([3, 7, 8])); } else { assert!(!is_abi_encoded); - assert_eq!(coverages, None); + assert_eq!(coverages, HashSet::new()); } } } @@ -401,8 +402,10 @@ mod tests { .collect::>(); for i in 0..calldata_words.len() { - let (is_abi_encoded, _, coverages) = - is_parameter_abiencoded(i, &calldata_words).unwrap(); + let abi_encoded_params = is_parameter_abi_encoded(i, &calldata_words).unwrap(); + let is_abi_encoded = abi_encoded_params.is_some(); + let coverages = abi_encoded_params.map(|p| p.coverages).unwrap_or_default(); + println!( "{i} - is_abi_encoded: {}, with word coverage: {:?}", is_abi_encoded, coverages @@ -410,13 +413,13 @@ mod tests { if i == 1 { assert!(is_abi_encoded); - assert_eq!(coverages, Some(HashSet::from([1, 4, 5, 6]))); + assert_eq!(coverages, HashSet::from([1, 4, 5, 6])); } else if i == 3 { assert!(is_abi_encoded); - assert_eq!(coverages, Some(HashSet::from([3, 7, 8, 9]))); + assert_eq!(coverages, HashSet::from([3, 7, 8, 9])); } else { assert!(!is_abi_encoded); - assert_eq!(coverages, None); + assert_eq!(coverages, HashSet::new()); } } } @@ -465,8 +468,11 @@ mod tests { .collect::>(); for i in 0..calldata_words.len() { - let (is_abi_encoded, ty, coverages) = - is_parameter_abiencoded(i, &calldata_words).unwrap(); + let abi_encoded_params = is_parameter_abi_encoded(i, &calldata_words).unwrap(); + let is_abi_encoded = abi_encoded_params.is_some(); + let coverages = abi_encoded_params.clone().map(|p| p.coverages).unwrap_or_default(); + let ty = abi_encoded_params.map(|p| p.ty).unwrap_or_default(); + println!( "{i} - is_abi_encoded: {}, ty: {:?}, with word coverage: {:?}", is_abi_encoded, ty, coverages @@ -474,13 +480,10 @@ mod tests { if i == 0 { assert!(is_abi_encoded); - assert_eq!(coverages, Some(HashSet::from([0, 2, 3, 4, 5, 6, 7, 8, 9]))); + assert_eq!(coverages, HashSet::from([0, 2, 3, 4, 5, 6, 7, 8, 9])); } else if i == 1 { assert!(is_abi_encoded); - assert_eq!( - coverages, - Some(HashSet::from([1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])) - ); + assert_eq!(coverages, HashSet::from([1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19])); } } } @@ -519,8 +522,11 @@ mod tests { .collect::>(); for i in 0..calldata_words.len() { - let (is_abi_encoded, ty, coverages) = - is_parameter_abiencoded(i, &calldata_words).unwrap(); + let abi_encoded_params = is_parameter_abi_encoded(i, &calldata_words).unwrap(); + let is_abi_encoded = abi_encoded_params.is_some(); + let coverages = abi_encoded_params.clone().map(|p| p.coverages).unwrap_or_default(); + let ty = abi_encoded_params.map(|p| p.ty).unwrap_or_default(); + println!( "{i} - is_abi_encoded: {}, ty: {:?}, with word coverage: {:?}", is_abi_encoded, ty, coverages @@ -528,12 +534,12 @@ mod tests { if i == 0 { assert!(is_abi_encoded); - assert_eq!(coverages, Some(HashSet::from([0, 3, 4]))); + assert_eq!(coverages, HashSet::from([0, 3, 4])); } else if i == 1 { assert!(!is_abi_encoded); } else if i == 2 { assert!(is_abi_encoded); - assert_eq!(coverages, Some(HashSet::from([2, 5, 6, 7, 8]))); + assert_eq!(coverages, HashSet::from([2, 5, 6, 7, 8])); } } } diff --git a/core/src/decode/mod.rs b/core/src/decode/mod.rs index b5bf14f6..72ac36e9 100644 --- a/core/src/decode/mod.rs +++ b/core/src/decode/mod.rs @@ -27,7 +27,7 @@ use indicatif::ProgressBar; use strsim::normalized_damerau_levenshtein as similarity; use crate::{ - decode::{core::abi::is_parameter_abiencoded, util::get_explanation}, + decode::{core::abi::is_parameter_abi_encoded, util::get_explanation}, error::Error, }; @@ -280,15 +280,10 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { let word = calldata_words[i]; // check if the first word is abiencoded - let (is_abi_encoded, ty, abi_encoded_word_coverage) = - is_parameter_abiencoded(i, &calldata_words)?; - - // if is_abi_encoded, add the type to potential_inputs - if is_abi_encoded { - let potential_type = to_type(&ty.expect("no abi encoded types found")); + if let Some(abi_encoded) = is_parameter_abi_encoded(i, &calldata_words)? { + let potential_type = to_type(&abi_encoded.ty); potential_inputs.push(potential_type); - covered_words.extend(abi_encoded_word_coverage.unwrap()); // safe to unwrap because - // is_abi_encoded is true + covered_words.extend(abi_encoded.coverages); } else { let (_, mut potential_types) = get_potential_types_for_word(word); From 410f24451a52fb672d8663c318ec6201d07664ac Mon Sep 17 00:00:00 2001 From: Jon-Becker Date: Fri, 1 Dec 2023 00:12:08 -0500 Subject: [PATCH 6/9] chore(decode): cleanup code, add integration tests --- common/src/ether/evm/core/memory.rs | 6 + common/src/ether/evm/core/stack.rs | 6 + common/src/ether/evm/core/storage.rs | 6 + common/src/ether/evm/core/types.rs | 72 +--- common/src/utils/io/logging.rs | 9 +- common/src/utils/io/mod.rs | 1 + common/src/utils/io/types.rs | 61 +++ core/src/decode/core/abi.rs | 518 +++++++++++++++----------- core/src/decode/mod.rs | 340 ++++++++--------- core/src/decompile/out/solidity.rs | 2 +- core/src/error.rs | 2 + core/src/snapshot/structures/state.rs | 6 + core/tests/test_decode.rs | 71 ++++ core/tests/testdata/txids.json | 1 + 14 files changed, 616 insertions(+), 485 deletions(-) create mode 100644 common/src/utils/io/types.rs create mode 100644 core/tests/testdata/txids.json diff --git a/common/src/ether/evm/core/memory.rs b/common/src/ether/evm/core/memory.rs index 25718a0a..f2741153 100644 --- a/common/src/ether/evm/core/memory.rs +++ b/common/src/ether/evm/core/memory.rs @@ -5,6 +5,12 @@ pub struct Memory { // TODO: add bit-tracking for memory } +impl Default for Memory { + fn default() -> Self { + Self::new() + } +} + impl Memory { /// Creates a new [`Memory`] with an empty memory vector. pub fn new() -> Memory { diff --git a/common/src/ether/evm/core/stack.rs b/common/src/ether/evm/core/stack.rs index ea7bf32f..d3909e48 100644 --- a/common/src/ether/evm/core/stack.rs +++ b/common/src/ether/evm/core/stack.rs @@ -25,6 +25,12 @@ pub struct StackFrame { pub operation: WrappedOpcode, } +impl Default for Stack { + fn default() -> Self { + Self::new() + } +} + impl Stack { /// Creates a new [`Stack`]. /// diff --git a/common/src/ether/evm/core/storage.rs b/common/src/ether/evm/core/storage.rs index f9bba6d5..889d8ba2 100644 --- a/common/src/ether/evm/core/storage.rs +++ b/common/src/ether/evm/core/storage.rs @@ -10,6 +10,12 @@ pub struct Storage { access_set: HashSet<[u8; 32]>, } +impl Default for Storage { + fn default() -> Self { + Self::new() + } +} + impl Storage { /// Creates a new [`Storage`] struct. /// diff --git a/common/src/ether/evm/core/types.rs b/common/src/ether/evm/core/types.rs index a1b472c1..a69a1868 100644 --- a/common/src/ether/evm/core/types.rs +++ b/common/src/ether/evm/core/types.rs @@ -1,7 +1,6 @@ use std::collections::VecDeque; -use colored::Colorize; -use ethers::abi::{AbiEncode, ParamType, Token}; +use ethers::abi::{AbiEncode, ParamType}; use crate::{ constants::TYPE_CAST_REGEX, @@ -246,65 +245,6 @@ pub fn to_type(string: &str) -> ParamType { arg_type } -/// 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(); - let prefix = prefix.to_string(); - - for input in inputs { - match input { - Token::Address(_) => output.push(format!("{prefix}{} 0x{input}", "address".blue())), - Token::Int(val) => output.push(format!("{prefix}{} {}", "int ".blue(), val)), - Token::Uint(val) => output.push(format!("{prefix}{} {}", "uint ".blue(), val)), - Token::String(val) => output.push(format!("{prefix}{} {val}", "string ".blue())), - Token::Bool(val) => { - if val { - output.push(format!("{prefix}{} true", "bool ".blue())); - } else { - output.push(format!("{prefix}{} false", "bool ".blue())); - } - } - Token::FixedBytes(_) | Token::Bytes(_) => { - let bytes = input - .to_string() - .chars() - .collect::>() - .chunks(64) - .map(|c| c.iter().collect::()) - .collect::>(); - - for (i, byte) in bytes.iter().enumerate() { - if i == 0 { - output.push(format!("{prefix}{} 0x{}", "bytes ".blue(), byte)); - } else { - output.push(format!("{prefix}{} {}", " ".blue(), byte)); - } - } - } - Token::FixedArray(val) | Token::Array(val) => { - if val.is_empty() { - output.push(format!("{prefix}[]")); - } else { - output.push(format!("{prefix}[")); - output.extend(display(val.to_vec(), &format!("{prefix} "))); - output.push(format!("{prefix}]")); - } - } - Token::Tuple(val) => { - if val.is_empty() { - output.push(format!("{prefix}()")); - } else { - output.push(format!("{prefix}(")); - output.extend(display(val.to_vec(), &format!("{prefix} "))); - output.push(format!("{prefix})")); - } - } - }; - } - - output -} - /// Convert a bitwise masking operation to a tuple containing: \ /// 1. The size of the type being masked \ /// 2. Potential types that the type being masked could be. @@ -439,13 +379,11 @@ pub fn get_padding(bytes: &str) -> Padding { .filter(|index| **index > non_null_byte_indices[non_null_byte_indices.len() - 1]) .count(); - if left_hand_padding > right_hand_padding { - return Padding::Left - } else if left_hand_padding < right_hand_padding { - return Padding::Right + match left_hand_padding.cmp(&right_hand_padding) { + std::cmp::Ordering::Greater => Padding::Left, + std::cmp::Ordering::Less => Padding::Right, + std::cmp::Ordering::Equal => Padding::None, } - - Padding::None } /// Given a string of bytes, get the max padding size for the data diff --git a/common/src/utils/io/logging.rs b/common/src/utils/io/logging.rs index 518e3b54..8c1f88ff 100644 --- a/common/src/utils/io/logging.rs +++ b/common/src/utils/io/logging.rs @@ -439,7 +439,14 @@ impl Logger { /// log a warning message pub fn warn(&self, message: &str) { - println!("{} {}: {}", pretty_timestamp().dimmed(), "warn".bright_yellow().bold(), message); + if self.level >= 0 { + println!( + "{} {}: {}", + pretty_timestamp().dimmed(), + "warn".bright_yellow().bold(), + message + ); + } } /// log a debug message diff --git a/common/src/utils/io/mod.rs b/common/src/utils/io/mod.rs index 00fe6325..53919abe 100644 --- a/common/src/utils/io/mod.rs +++ b/common/src/utils/io/mod.rs @@ -1,3 +1,4 @@ pub mod file; pub mod logging; pub mod macros; +pub mod types; diff --git a/common/src/utils/io/types.rs b/common/src/utils/io/types.rs new file mode 100644 index 00000000..41e8e3d7 --- /dev/null +++ b/common/src/utils/io/types.rs @@ -0,0 +1,61 @@ +use colored::Colorize; +use ethers::abi::Token; + +/// 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(); + let prefix = prefix.to_string(); + + for input in inputs { + match input { + Token::Address(_) => output.push(format!("{prefix}{} 0x{input}", "address".blue())), + Token::Int(val) => output.push(format!("{prefix}{} {}", "int ".blue(), val)), + Token::Uint(val) => output.push(format!("{prefix}{} {}", "uint ".blue(), val)), + Token::String(val) => output.push(format!("{prefix}{} {val}", "string ".blue())), + Token::Bool(val) => { + if val { + output.push(format!("{prefix}{} true", "bool ".blue())); + } else { + output.push(format!("{prefix}{} false", "bool ".blue())); + } + } + Token::FixedBytes(_) | Token::Bytes(_) => { + let bytes = input + .to_string() + .chars() + .collect::>() + .chunks(64) + .map(|c| c.iter().collect::()) + .collect::>(); + + for (i, byte) in bytes.iter().enumerate() { + if i == 0 { + output.push(format!("{prefix}{} 0x{}", "bytes ".blue(), byte)); + } else { + output.push(format!("{prefix}{} {}", " ".blue(), byte)); + } + } + } + Token::FixedArray(val) | Token::Array(val) => { + if val.is_empty() { + output.push(format!("{prefix}[]")); + } else { + output.push(format!("{prefix}[")); + output.extend(display(val.to_vec(), &format!("{prefix} "))); + output.push(format!("{prefix}]")); + } + } + Token::Tuple(val) => { + if val.is_empty() { + output.push(format!("{prefix}()")); + } else { + output.push(format!("{prefix}(")); + output.extend(display(val.to_vec(), &format!("{prefix} "))); + output.push(format!("{prefix})")); + } + } + }; + } + + output +} diff --git a/core/src/decode/core/abi.rs b/core/src/decode/core/abi.rs index bb293485..86ec51de 100644 --- a/core/src/decode/core/abi.rs +++ b/core/src/decode/core/abi.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::{cmp::Ordering, collections::HashSet}; use ethers::types::U256; use heimdall_common::{ @@ -24,62 +24,225 @@ pub fn is_parameter_abi_encoded( let mut coverages = HashSet::from([parameter_index]); // convert this word to a U256 + let (word, word_offset) = match process_and_validate_word(parameter_index, calldata_words) { + Ok((word, word_offset)) => (word, word_offset), + Err(_) => return Ok(None), + }; + coverages.insert(word_offset.as_usize()); + + // note: `size` is the size of the ABI-encoded item. It varies depending on the type of the + // item. + let size_word = calldata_words.get(word_offset.as_usize()).ok_or(Error::BoundsError)?; + let size = U256::from_str_radix(size_word, 16)?.min(U256::from(usize::MAX)); + + // check if there are enough words left in the calldata to contain the ABI-encoded item. + // if there aren't, it doesn't necessarily mean that the calldata is invalid, but it does + // indicate that they aren't an array of items. + let data_start_word_offset = word_offset + 1; + let data_end_word_offset = data_start_word_offset + size; + match data_end_word_offset.cmp(&U256::from(calldata_words.len())) { + Ordering::Greater => is_parameter_abi_encoded_bytes( + parameter_index, + calldata_words, + word, + word_offset, + data_start_word_offset, + size, + coverages, + ), + _ => is_parameter_abi_encoded_array( + parameter_index, + calldata_words, + word, + word_offset, + data_start_word_offset, + data_end_word_offset, + size, + coverages, + ), + } +} + +/// Processes a word and validates that it is could be a ptr to an ABI-encoded item. +fn process_and_validate_word( + parameter_index: usize, + calldata_words: &[&str], +) -> Result<(U256, U256), Error> { let word = U256::from_str_radix(calldata_words[parameter_index], 16)?; // if the word is a multiple of 32, it may be an offset pointing to the start of an // ABI-encoded item if word % 32 != U256::zero() || word == U256::zero() { - debug_max!( - "parameter {}: '{}' doesnt appear to be an offset ptr", - parameter_index, - calldata_words[parameter_index] - ); - return Ok(None); + debug_max!("parameter {}: '{}' doesnt appear to be an offset ptr", parameter_index, word); + return Err(Error::BoundsError); } // check if the pointer is pointing to a valid location in the calldata let word_offset = word / 32; if word_offset >= U256::from(calldata_words.len()) { - debug_max!( - "parameter {}: '{}' is out of bounds (offset check)", - parameter_index, - calldata_words[parameter_index] - ); + debug_max!("parameter {}: '{}' is out of bounds (offset check)", parameter_index, word); + return Err(Error::BoundsError); + } + + Ok((word, word_offset)) +} + +// Handle ABI-encoded bytes +fn is_parameter_abi_encoded_bytes( + parameter_index: usize, + calldata_words: &[&str], + word: U256, + word_offset: U256, + data_start_word_offset: U256, + size: U256, + coverages: HashSet, +) -> Result, Error> { + let mut coverages = coverages; + debug_max!("parameter {}: '{}' may be bytes, string", parameter_index, word); + + // join words AFTER the word_offset. these are where potential data is. + let data_words = &calldata_words[data_start_word_offset.as_usize()..]; + + // check if there are enough remaining bytes to contain the ABI-encoded item. + if data_words.join("").len() / 2 < size.as_usize() { + debug_max!("parameter {}: '{}' is out of bounds (bytes check)", parameter_index, word); return Ok(None); } - coverages.insert(word_offset.as_usize()); - // note: `size` is the size of the ABI-encoded item. It varies depending on the type of the - // item. - let size_word = calldata_words.get(word_offset.as_usize()).ok_or(Error::BoundsError)?; - let size = U256::from_str_radix(size_word, 16)?.min(U256::from(usize::MAX)); + // if `size` is less than 32 bytes, we only need to check the first word for `32 - size` + // null-bytes. tricky because sometimes a null byte could be part of the + // data. + if size <= U256::from(32) { + let potential_data = data_words[0]; + debug_max!("with data: {}", potential_data); - // check if there are enough words left in the calldata to contain the ABI-encoded item. - // if there aren't, it doesn't necessarily mean that the calldata is invalid, but it does - // indicate that they aren't an array of items. - let data_start_word_offset = word_offset + 1; - let data_end_word_offset = data_start_word_offset + size; - if data_end_word_offset > U256::from(calldata_words.len()) { - // this could still be bytes, string, or a non ABI-encoded item - debug_max!( - "parameter {}: '{}' may be bytes, string", - parameter_index, - calldata_words[parameter_index] - ); + // get the padding of the data + let padding_size = get_padding_size(potential_data); - // join words AFTER the word_offset. these are where potential data is. + // if the padding is greater than `32 - size`, then this is not an ABI-encoded item. + if padding_size > 32 - size.as_usize() { + debug_max!("parameter {}: '{}' with size {} cannot fit into word with padding of {} bytes (bytes)", parameter_index, word, size, padding_size); + return Ok(None); + } + + // insert the word offset into the coverage set + coverages.insert(data_start_word_offset.as_usize()); + coverages.insert(word_offset.as_usize()); + debug_max!("parameter {}: '{}' is bytes", parameter_index, word); + Ok(Some(AbiEncoded { ty: String::from("bytes"), coverages })) + } else { + // recalculate data_end_word_offset based on `size` + // size is in bytes, and one word is 32 bytes. find out how many words we need to + // cover `size` bytes. + let word_count_for_size = U256::from((size.as_u32() as f32 / 32f32).ceil() as u32); // wont panic unless calldata is huge + let data_end_word_offset = data_start_word_offset + word_count_for_size; let data_words = &calldata_words[data_start_word_offset.as_usize()..]; + debug_max!("with data: {:#?}", data_words.join("")); - // check if there are enough remaining bytes to contain the ABI-encoded item. - if data_words.join("").len() / 2 < size.as_usize() { - debug_max!( - "parameter {}: '{}' is out of bounds (bytes check)", - parameter_index, - calldata_words[parameter_index] - ); + // get the last word of the data + let last_word = + data_words.get(word_count_for_size.as_usize() - 1).ok_or(Error::BoundsError)?; + + // how many bytes should be in the last word? + let last_word_size = size.as_usize() % 32; + + // if the padding is greater than `32 - last_word_size`, then this is not an ABI-encoded + // item. + let padding_size = get_padding_size(last_word); + if padding_size > 32 - last_word_size { + debug_max!("parameter {}: '{}' with size {} cannot fit into last word with padding of {} bytes (bytes)", parameter_index, word, size, padding_size); return Ok(None); } + // insert all word offsets from `data_start_word_offset` to `data_end_word_offset` into + // the coverage set + for i in word_offset.as_usize()..data_end_word_offset.as_usize() { + coverages.insert(i); + } + + debug_max!("parameter {}: '{}' is bytes", parameter_index, word); + Ok(Some(AbiEncoded { ty: String::from("bytes"), coverages })) + } +} + +// Handle ABI-encoded bytes +fn is_parameter_abi_encoded_array( + parameter_index: usize, + calldata_words: &[&str], + word: U256, + word_offset: U256, + data_start_word_offset: U256, + data_end_word_offset: U256, + size: U256, + coverages: HashSet, +) -> Result, Error> { + let mut coverages = coverages; + debug_max!("parameter {}: '{}' may be an array", parameter_index, word); + let data_words = + &calldata_words[data_start_word_offset.as_usize()..data_end_word_offset.as_usize()]; + debug_max!("potential array items: {:#?}", data_words); + + // check if this array is a string (of bytes) + if let Ok(Some(abi_encoded)) = is_parameter_abi_encoded_string( + data_words, + parameter_index, + calldata_words, + word, + word_offset, + data_start_word_offset, + size, + coverages.clone(), + ) { + return Ok(Some(abi_encoded)); + } + // this is an array! + + // insert all word offsets from `data_start_word_offset` to `data_end_word_offset` into the + // coverage set + for i in word_offset.as_usize()..data_end_word_offset.as_usize() { + coverages.insert(i); + } + + // get most-likely potential type of the array + let potential_type = get_potential_type( + data_words, + parameter_index, + calldata_words, + word, + data_start_word_offset, + &mut coverages, + ); + let type_str = format!("{potential_type}[]"); + debug_max!("parameter {}: '{}' is {}", parameter_index, word, type_str); + Ok(Some(AbiEncoded { ty: type_str, coverages })) +} + +fn is_parameter_abi_encoded_string( + data_words: &[&str], + parameter_index: usize, + calldata_words: &[&str], + word: U256, + word_offset: U256, + data_start_word_offset: U256, + size: U256, + coverages: HashSet, +) -> Result, Error> { + let mut coverages = coverages; + // check if the data words all have conforming padding + // we do this check because strings will typically be of the form: + // 0000000000000000000000000000000000000000000000000000000000000003 // length of 3 + // 6f6e650000000000000000000000000000000000000000000000000000000000 // "one" + // + // so, if the data words have conforming padding, we can assume that this is not a string + // and is instead an array. + let padding_matches: bool = data_words + .iter() + .map(|word| get_padding(word)) + .all(|padding| padding == get_padding(data_words[0])); + if !padding_matches { + // size is in bytes now, we just need to do the same as bytes bound checking + debug_max!("parameter {}: '{}' may be string", parameter_index, word); + // if `size` is less than 32 bytes, we only need to check the first word for `32 - size` // null-bytes. tricky because sometimes a null byte could be part of the // data. @@ -92,27 +255,29 @@ pub fn is_parameter_abi_encoded( // if the padding is greater than `32 - size`, then this is not an ABI-encoded item. if padding_size > 32 - size.as_usize() { - debug_max!("parameter {}: '{}' with size {} cannot fit into word with padding of {} bytes (bytes)", parameter_index, calldata_words[parameter_index], size, padding_size); + debug_max!("parameter {}: '{}' with size {} cannot fit into word with padding of {} bytes (string)", parameter_index, word, size, padding_size); return Ok(None); } + // yay! we have a string! // insert the word offset into the coverage set coverages.insert(data_start_word_offset.as_usize()); coverages.insert(word_offset.as_usize()); - debug_max!( - "parameter {}: '{}' is bytes", - parameter_index, - calldata_words[parameter_index] - ); - Ok(Some(AbiEncoded { ty: String::from("bytes"), coverages })) + return Ok(Some(AbiEncoded { + ty: String::from("string"), + coverages: coverages.clone(), + })); } else { // recalculate data_end_word_offset based on `size` // size is in bytes, and one word is 32 bytes. find out how many words we need to // cover `size` bytes. let word_count_for_size = U256::from((size.as_u32() as f32 / 32f32).ceil() as u32); // wont panic unless calldata is huge let data_end_word_offset = data_start_word_offset + word_count_for_size; - let data_words = &calldata_words[data_start_word_offset.as_usize()..]; - debug_max!("with data: {:#?}", data_words.join("")); + debug_max!( + "with data: {:#?}", + calldata_words[data_start_word_offset.as_usize()..data_end_word_offset.as_usize()] + .join("") + ); // get the last word of the data let last_word = @@ -121,198 +286,99 @@ pub fn is_parameter_abi_encoded( // how many bytes should be in the last word? let last_word_size = size.as_usize() % 32; - // if the padding is greater than `32 - last_word_size`, then this is not an ABI-encoded - // item. + // if the padding is greater than `32 - last_word_size`, then this is not an + // ABI-encoded item. let padding_size = get_padding_size(last_word); if padding_size > 32 - last_word_size { - debug_max!("parameter {}: '{}' with size {} cannot fit into last word with padding of {} bytes (bytes)", parameter_index, calldata_words[parameter_index], size, padding_size); + debug_max!("parameter {}: '{}' with size {} cannot fit into last word with padding of {} bytes (string)", parameter_index, word, size, padding_size); return Ok(None); } - // insert all word offsets from `data_start_word_offset` to `data_end_word_offset` into - // the coverage set + // yay! we have a string! + // insert all word offsets from `data_start_word_offset` to `data_end_word_offset` + // into the coverage set for i in word_offset.as_usize()..data_end_word_offset.as_usize() { coverages.insert(i); } - - debug_max!( - "parameter {}: '{}' is bytes", - parameter_index, - calldata_words[parameter_index] - ); - Ok(Some(AbiEncoded { ty: String::from("bytes"), coverages })) } - } else { - // this could be an array of items. - debug_max!( - "parameter {}: '{}' may be an array", - parameter_index, - calldata_words[parameter_index] - ); - let data_words = - &calldata_words[data_start_word_offset.as_usize()..data_end_word_offset.as_usize()]; - debug_max!("potential array items: {:#?}", data_words); - - // check if the data words all have conforming padding - // we do this check because strings will typically be of the form: - // 0000000000000000000000000000000000000000000000000000000000000003 // length of 3 - // 6f6e650000000000000000000000000000000000000000000000000000000000 // "one" - // - // so, if the data words have conforming padding, we can assume that this is not a string - // and is instead an array. - let padding_matches: bool = data_words - .iter() - .map(|word| get_padding(word)) - .all(|padding| padding == get_padding(data_words[0])); - if !padding_matches { - // size is in bytes now, we just need to do the same as bytes bound checking - debug_max!( - "parameter {}: '{}' may be string", - parameter_index, - calldata_words[parameter_index] - ); - - // if `size` is less than 32 bytes, we only need to check the first word for `32 - size` - // null-bytes. tricky because sometimes a null byte could be part of the - // data. - if size <= U256::from(32) { - let potential_data = data_words[0]; - debug_max!("with data: {}", potential_data); - // get the padding of the data - let padding_size = get_padding_size(potential_data); - - // if the padding is greater than `32 - size`, then this is not an ABI-encoded item. - if padding_size > 32 - size.as_usize() { - debug_max!("parameter {}: '{}' with size {} cannot fit into word with padding of {} bytes (string)", parameter_index, calldata_words[parameter_index], size, padding_size); - return Ok(None); - } + debug_max!("parameter {}: '{}' is string", parameter_index, word); + return Ok(Some(AbiEncoded { ty: String::from("string"), coverages: coverages.clone() })); + } - // yay! we have a string! - // insert the word offset into the coverage set - coverages.insert(data_start_word_offset.as_usize()); - coverages.insert(word_offset.as_usize()); - } else { - // recalculate data_end_word_offset based on `size` - // size is in bytes, and one word is 32 bytes. find out how many words we need to - // cover `size` bytes. - let word_count_for_size = U256::from((size.as_u32() as f32 / 32f32).ceil() as u32); // wont panic unless calldata is huge - let data_end_word_offset = data_start_word_offset + word_count_for_size; - debug_max!( - "with data: {:#?}", - calldata_words - [data_start_word_offset.as_usize()..data_end_word_offset.as_usize()] - .join("") - ); - - // get the last word of the data - let last_word = - data_words.get(word_count_for_size.as_usize() - 1).ok_or(Error::BoundsError)?; - - // how many bytes should be in the last word? - let last_word_size = size.as_usize() % 32; - - // if the padding is greater than `32 - last_word_size`, then this is not an - // ABI-encoded item. - let padding_size = get_padding_size(last_word); - if padding_size > 32 - last_word_size { - debug_max!("parameter {}: '{}' with size {} cannot fit into last word with padding of {} bytes (string)", parameter_index, calldata_words[parameter_index], size, padding_size); - return Ok(None); - } + Ok(None) +} - // yay! we have a string! - // insert all word offsets from `data_start_word_offset` to `data_end_word_offset` - // into the coverage set - for i in word_offset.as_usize()..data_end_word_offset.as_usize() { - coverages.insert(i); - } - } +/// Handle determining the most potential type of an abi-encoded item. +fn get_potential_type( + data_words: &[&str], + parameter_index: usize, + calldata_words: &[&str], + word: U256, + data_start_word_offset: U256, + coverages: &mut HashSet, +) -> String { + let (_, potential_type) = data_words + .iter() + .enumerate() + .map(|(i, w)| { + // we need to get a slice of calldata_words from `data_start_word_offset` to the end + // of the calldata_words. this is because nested abi-encoded items + // reset the offsets of the words. + let data_words = &calldata_words[data_start_word_offset.as_usize()..]; + // first, check if this word *could* be a nested abi-encoded item debug_max!( - "parameter {}: '{}' is string", + "parameter {}: '{}' checking for nested abi-encoded data", parameter_index, - calldata_words[parameter_index] + word ); - return Ok(Some(AbiEncoded { ty: String::from("string"), coverages })); - } + if let Ok(Some(nested_abi_encoded_param)) = is_parameter_abi_encoded(i, data_words) { + // we need to add data_start_word_offset to all the offsets in nested_coverages + // because they are relative to the start of the nested abi-encoded item. + let nested_coverages = nested_abi_encoded_param + .coverages + .into_iter() + .map(|nested_coverage| nested_coverage + data_start_word_offset.as_usize()) + .collect::>(); + + // merge coverages and nested_coverages + coverages.extend(nested_coverages); + return (32, vec![nested_abi_encoded_param.ty]); + } - // map over the array of words and get the potential types for each word, then, - // get the type that best fits the array. - let (_, potential_type) = data_words - .iter() - .enumerate() - .map(|(i, w)| { - // we need to get a slice of calldata_words from `data_start_word_offset` to the end - // of the calldata_words. this is because nested abi-encoded items - // reset the offsets of the words. - let data_words = &calldata_words[data_start_word_offset.as_usize()..]; - - // first, check if this word *could* be a nested abi-encoded item - debug_max!( - "parameter {}: '{}' checking for nested abi-encoded data", - parameter_index, - calldata_words[parameter_index] - ); - if let Ok(Some(nested_abi_encoded_param)) = is_parameter_abi_encoded(i, data_words) - { - // we need to add data_start_word_offset to all the offsets in nested_coverages - // because they are relative to the start of the nested abi-encoded item. - let nested_coverages = nested_abi_encoded_param - .coverages - .into_iter() - .map(|nested_coverage| nested_coverage + data_start_word_offset.as_usize()) - .collect::>(); - - // merge coverages and nested_coverages - coverages.extend(nested_coverages); - return (32, vec![nested_abi_encoded_param.ty]); - } + let (padding_size, mut potential_types) = get_potential_types_for_word(w); - let (padding_size, mut potential_types) = get_potential_types_for_word(w); - - // perform heuristics - // - if we use right-padding, this is probably bytesN - // - if we use left-padding, this is probably uintN or intN - // - if we use no padding, this is probably bytes32 - match get_padding(w) { - Padding::Left => potential_types - .retain(|t| t.starts_with("uint") || t.starts_with("address")), - _ => potential_types - .retain(|t| t.starts_with("bytes") || t.starts_with("string")), + // perform heuristics + // - if we use right-padding, this is probably bytesN + // - if we use left-padding, this is probably uintN or intN + // - if we use no padding, this is probably bytes32 + match get_padding(w) { + Padding::Left => { + potential_types.retain(|t| t.starts_with("uint") || t.starts_with("address")) } + _ => potential_types.retain(|t| t.starts_with("bytes") || t.starts_with("string")), + } - (padding_size, potential_types) - }) - .fold((0, String::from("")), |(max_size, mut potential_type), (size, types)| { - // "address" and "string" are priority types - if types.contains(&String::from("string")) { - return (32, String::from("string")); - } else if types.contains(&String::from("address")) { - return (32, String::from("address")); - } + (padding_size, potential_types) + }) + .fold((0, String::from("")), |(max_size, mut potential_type), (size, types)| { + // "address" and "string" are priority types + if types.contains(&String::from("string")) { + return (32, String::from("string")); + } else if types.contains(&String::from("address")) { + return (32, String::from("address")); + } - if size > max_size { - potential_type = types.first().expect("types is empty").clone(); - (max_size, potential_type) - } else { - (max_size, potential_type) - } - }); + if size > max_size { + potential_type = types.first().expect("types is empty").clone(); + (max_size, potential_type) + } else { + (max_size, potential_type) + } + }); - // insert all word offsets from `data_start_word_offset` to `data_end_word_offset` into the - // coverage set - for i in word_offset.as_usize()..data_end_word_offset.as_usize() { - coverages.insert(i); - } - let type_str = format!("{potential_type}[]"); - debug_max!( - "parameter {}: '{}' is {}", - parameter_index, - calldata_words[parameter_index], - type_str - ); - Ok(Some(AbiEncoded { ty: type_str, coverages })) - } + potential_type } #[cfg(test)] @@ -433,16 +499,16 @@ mod tests { // - uint256[][]: [0, 2, 3, 4, 5, 6, 7, 8, 9] // - string[]: [1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19] // 0x2289b18c - // 0. 0000000000000000000000000000000000000000000000000000000000000040 - // 1. 0000000000000000000000000000000000000000000000000000000000000140 - // 2. 0000000000000000000000000000000000000000000000000000000000000002 - // 3. 0000000000000000000000000000000000000000000000000000000000000040 - // 4. 00000000000000000000000000000000000000000000000000000000000000a0 - // 5. 0000000000000000000000000000000000000000000000000000000000000002 - // 6. 0000000000000000000000000000000000000000000000000000000000000001 - // 7. 0000000000000000000000000000000000000000000000000000000000000002 - // 8. 0000000000000000000000000000000000000000000000000000000000000001 - // 9. 0000000000000000000000000000000000000000000000000000000000000003 + // 0. 0000000000000000000000000000000000000000000000000000000000000040 + // 1. 0000000000000000000000000000000000000000000000000000000000000140 + // 2. 0000000000000000000000000000000000000000000000000000000000000002 + // 3. 0000000000000000000000000000000000000000000000000000000000000040 + // 4. 00000000000000000000000000000000000000000000000000000000000000a0 + // 5. 0000000000000000000000000000000000000000000000000000000000000002 + // 6. 0000000000000000000000000000000000000000000000000000000000000001 + // 7. 0000000000000000000000000000000000000000000000000000000000000002 + // 8. 0000000000000000000000000000000000000000000000000000000000000001 + // 9. 0000000000000000000000000000000000000000000000000000000000000003 // 10. 0000000000000000000000000000000000000000000000000000000000000003 // 11. 0000000000000000000000000000000000000000000000000000000000000060 // 12. 00000000000000000000000000000000000000000000000000000000000000a0 diff --git a/core/src/decode/mod.rs b/core/src/decode/mod.rs index 72ac36e9..3227e279 100644 --- a/core/src/decode/mod.rs +++ b/core/src/decode/mod.rs @@ -6,21 +6,24 @@ use std::{collections::HashSet, time::Duration}; use clap::{AppSettings, Parser}; use derive_builder::Builder; use ethers::{ - abi::{decode as decode_abi, AbiEncode, Function, Param, ParamType, StateMutability}, + abi::{decode as decode_abi, AbiEncode, Function, Param, ParamType, StateMutability, Token}, types::Transaction, }; use heimdall_common::{ constants::{CALLDATA_REGEX, TRANSACTION_HASH_REGEX}, + debug_max, ether::{ evm::core::types::{ - display, get_padding, get_potential_types_for_word, parse_function_parameters, to_type, - Padding, + get_padding, get_potential_types_for_word, parse_function_parameters, to_type, Padding, }, rpc::get_transaction, signatures::{score_signature, ResolveSelector, ResolvedFunction}, }, - utils::{io::logging::Logger, strings::decode_hex}, + utils::{ + io::{logging::Logger, types::display}, + strings::decode_hex, + }, }; use indicatif::ProgressBar; @@ -129,7 +132,7 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { } // check if the calldata length is a standard length - if calldata.len() % 2 != 0 { + if calldata.len() % 2 != 0 || calldata.len() < 8 { logger.error("calldata is not a valid hex string."); std::process::exit(1); } @@ -308,206 +311,163 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { i += 1; } - // use potential_inputs to decode calldata - if let Ok(decoded_inputs) = decode_abi(&potential_inputs, &byte_args) { - // convert tokens to params - let mut params: Vec = Vec::new(); - for (i, input) in potential_inputs.iter().enumerate() { - params.push(Param { - name: format!("arg{i}"), - kind: input.to_owned(), - internal_type: None, - }); - } + debug_max!( + "potential parameter inputs, ({:?})", + potential_inputs.iter().map(|x| x.to_string()).collect::>() + ); - // build a trace of the calldata - let decode_call = trace.add_call( - 0, - line!(), - "heimdall".to_string(), - "decode".to_string(), - vec![shortened_target], - "()".to_string(), - ); - trace.br(decode_call); - trace.add_message(decode_call, line!(), vec!["name: unknown".to_string()]); - trace.add_message( - decode_call, - line!(), - vec![format!("signature: Unresolved_{}", function_selector)], - ); - trace.add_message( - decode_call, - line!(), - vec![format!("selector: 0x{function_selector}",)], - ); - trace.add_message( - decode_call, - line!(), - vec![format!("calldata: {} bytes", calldata.len() / 2usize)], - ); - trace.br(decode_call); - - // build decoded string for --explain - let decoded_string = &mut format!( - "{}\n{}\n{}\n{}", - format!("name: unknown"), - format!("signature: Unresolved_{}", function_selector), - format!("selector: 0x{function_selector}"), - format!("calldata: {} bytes", calldata.len() / 2usize) - ); + if let Ok((decoded_inputs, params)) = try_decode(&potential_inputs, &byte_args) { + // build a ResolvedFunction to add to matches + let resolved_function = ResolvedFunction { + name: format!("Unresolved_{}", function_selector), + signature: format!( + "Unresolved_{}({})", + function_selector, + params.iter().map(|x| x.kind.to_string()).collect::>().join(", ") + ), + inputs: params.iter().map(|x| x.kind.to_string()).collect::>(), + decoded_inputs: Some(decoded_inputs), + }; - // build inputs - for (i, input) in decoded_inputs.iter().enumerate() { - let mut decoded_inputs_as_message = display(vec![input.to_owned()], " "); - if decoded_inputs_as_message.is_empty() { - break; - } + matches.push(resolved_function); + } else { + logger.error("failed to dynamically decode calldata."); + return Err(Error::DecodeError); + } + } - if i == 0 { - decoded_inputs_as_message[0] = format!( - "input {}:{}{}", - i, - " ".repeat(4 - i.to_string().len()), - decoded_inputs_as_message[0].replacen(" ", "", 1) - ) - } else { - decoded_inputs_as_message[0] = format!( - " {}:{}{}", - i, - " ".repeat(4 - i.to_string().len()), - decoded_inputs_as_message[0].replacen(" ", "", 1) - ) - } + let mut selection: u8 = 0; - // add to trace and decoded string - trace.add_message(decode_call, 1, decoded_inputs_as_message.clone()); - decoded_string - .push_str(&format!("\n{}", decoded_inputs_as_message.clone().join("\n"))); - } + // sort matches by signature using score heuristic from `score_signature` + matches.sort_by(|a, b| { + let a_score = score_signature(&a.signature); + let b_score = score_signature(&b.signature); + b_score.cmp(&a_score) + }); - // force the trace to display - trace.level = 4; - trace.display(); - } else { - logger.error("failed to dynamically decode calldata."); + if matches.len() > 1 { + selection = logger.option( + "warn", + "multiple possible matches found. select an option below", + matches.iter().map(|x| x.signature.clone()).collect(), + Some(0u8), + args.default, + ); + } + + let selected_match = match matches.get(selection as usize) { + Some(selected_match) => selected_match, + None => { + logger.error("invalid selection."); + std::process::exit(1) } - } else { - let mut selection: u8 = 0; - - // sort matches by signature using score heuristic from `score_signature` - matches.sort_by(|a, b| { - let a_score = score_signature(&a.signature); - let b_score = score_signature(&b.signature); - b_score.cmp(&a_score) - }); - - if matches.len() > 1 { - selection = logger.option( - "warn", - "multiple possible matches found. select an option below", - matches.iter().map(|x| x.signature.clone()).collect(), - Some(0u8), - args.default, - ); + }; + + let decode_call = trace.add_call( + 0, + line!(), + "heimdall".to_string(), + "decode".to_string(), + vec![shortened_target], + "()".to_string(), + ); + trace.br(decode_call); + trace.add_message(decode_call, line!(), vec![format!("name: {}", selected_match.name)]); + trace.add_message( + decode_call, + line!(), + vec![format!("signature: {}", selected_match.signature)], + ); + trace.add_message(decode_call, line!(), vec![format!("selector: 0x{function_selector}")]); + trace.add_message( + decode_call, + line!(), + vec![format!("calldata: {} bytes", calldata.len() / 2usize)], + ); + trace.br(decode_call); + + // build decoded string for --explain + let decoded_string = &mut format!( + "{}\n{}\n{}\n{}", + format!("name: {}", selected_match.name), + format!("signature: {}", selected_match.signature), + format!("selector: 0x{function_selector}"), + format!("calldata: {} bytes", calldata.len() / 2usize) + ); + + // build inputs + for (i, input) in selected_match.decoded_inputs.as_ref().unwrap().iter().enumerate() { + let mut decoded_inputs_as_message = display(vec![input.to_owned()], " "); + if decoded_inputs_as_message.is_empty() { + break; } - let selected_match = match matches.get(selection as usize) { - Some(selected_match) => selected_match, - None => { - logger.error("invalid selection."); - std::process::exit(1) - } - }; + if i == 0 { + decoded_inputs_as_message[0] = format!( + "input {}:{}{}", + i, + " ".repeat(4 - i.to_string().len()), + decoded_inputs_as_message[0].replacen(" ", "", 1) + ) + } else { + decoded_inputs_as_message[0] = format!( + " {}:{}{}", + i, + " ".repeat(4 - i.to_string().len()), + decoded_inputs_as_message[0].replacen(" ", "", 1) + ) + } - let decode_call = trace.add_call( - 0, - line!(), - "heimdall".to_string(), - "decode".to_string(), - vec![shortened_target], - "()".to_string(), - ); - trace.br(decode_call); - trace.add_message( - decode_call, - line!(), - vec![format!("name: {}", selected_match.name)], - ); - trace.add_message( - decode_call, - line!(), - vec![format!("signature: {}", selected_match.signature)], - ); - trace.add_message(decode_call, line!(), vec![format!("selector: 0x{function_selector}")]); - trace.add_message( - decode_call, - line!(), - vec![format!("calldata: {} bytes", calldata.len() / 2usize)], - ); - trace.br(decode_call); - - // build decoded string for --explain - let decoded_string = &mut format!( - "{}\n{}\n{}\n{}", - format!("name: {}", selected_match.name), - format!("signature: {}", selected_match.signature), - format!("selector: 0x{function_selector}"), - format!("calldata: {} bytes", calldata.len() / 2usize) - ); + // add to trace and decoded string + trace.add_message(decode_call, 1, decoded_inputs_as_message.clone()); + decoded_string.push_str(&format!("\n{}", decoded_inputs_as_message.clone().join("\n"))); + } - // build inputs - for (i, input) in selected_match.decoded_inputs.as_ref().unwrap().iter().enumerate() { - let mut decoded_inputs_as_message = display(vec![input.to_owned()], " "); - if decoded_inputs_as_message.is_empty() { - break; + // display trace (pretty print decoded calldata) + trace.display(); + + if args.explain { + // get a new progress bar + let explain_progress = ProgressBar::new_spinner(); + explain_progress.enable_steady_tick(Duration::from_millis(100)); + explain_progress.set_style(logger.info_spinner()); + explain_progress.set_message("attempting to explain calldata..."); + + match get_explanation(decoded_string.to_string(), raw_transaction, &args.openai_api_key) + .await + { + Some(explanation) => { + explain_progress.finish_and_clear(); + logger.success(&format!("Transaction explanation: {}", explanation.trim())); } - - if i == 0 { - decoded_inputs_as_message[0] = format!( - "input {}:{}{}", - i, - " ".repeat(4 - i.to_string().len()), - decoded_inputs_as_message[0].replacen(" ", "", 1) - ) - } else { - decoded_inputs_as_message[0] = format!( - " {}:{}{}", - i, - " ".repeat(4 - i.to_string().len()), - decoded_inputs_as_message[0].replacen(" ", "", 1) - ) + None => { + explain_progress.finish_and_clear(); + logger.error("failed to get explanation from OpenAI."); } + }; + } - // add to trace and decoded string - trace.add_message(decode_call, 1, decoded_inputs_as_message.clone()); - decoded_string.push_str(&format!("\n{}", decoded_inputs_as_message.clone().join("\n"))); - } + Ok(matches) +} - // display trace (pretty print decoded calldata) - trace.display(); - - if args.explain && !matches.is_empty() { - // get a new progress bar - let explain_progress = ProgressBar::new_spinner(); - explain_progress.enable_steady_tick(Duration::from_millis(100)); - explain_progress.set_style(logger.info_spinner()); - explain_progress.set_message("attempting to explain calldata..."); - - match get_explanation(decoded_string.to_string(), raw_transaction, &args.openai_api_key) - .await - { - Some(explanation) => { - explain_progress.finish_and_clear(); - logger.success(&format!("Transaction explanation: {}", explanation.trim())); - } - None => { - explain_progress.finish_and_clear(); - logger.error("failed to get explanation from OpenAI."); - } - }; +// Attempt to decode the given calldata with the given types. +fn try_decode( + inputs: &Vec, + byte_args: &Vec, +) -> Result<(Vec, Vec), Error> { + if let Ok(result) = decode_abi(inputs, byte_args) { + // convert tokens to params + let mut params: Vec = Vec::new(); + for (i, input) in inputs.iter().enumerate() { + params.push(Param { + name: format!("arg{i}"), + kind: input.to_owned(), + internal_type: None, + }); } + + return Ok((result, params)); } - Ok(matches) + Err(Error::DecodeError) } diff --git a/core/src/decompile/out/solidity.rs b/core/src/decompile/out/solidity.rs index 23dbd8ba..6e2c13a1 100644 --- a/core/src/decompile/out/solidity.rs +++ b/core/src/decompile/out/solidity.rs @@ -27,7 +27,7 @@ use super::{ /// Solidity contract. pub fn build_solidity_output( args: &DecompilerArgs, - abi: &Vec, + abi: &[ABIStructure], functions: Vec, all_resolved_errors: HashMap, all_resolved_events: HashMap, diff --git a/core/src/error.rs b/core/src/error.rs index 26d0cd6a..755fd66c 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -6,6 +6,8 @@ pub enum Error { FromStrRadixError(#[from] ethers::abi::ethereum_types::FromStrRadixErr), #[error("BoundsError")] BoundsError, + #[error("DecodeError")] + DecodeError, #[error("Error: {0}")] GenericError(String), } diff --git a/core/src/snapshot/structures/state.rs b/core/src/snapshot/structures/state.rs index 9416a90b..106d8ad0 100644 --- a/core/src/snapshot/structures/state.rs +++ b/core/src/snapshot/structures/state.rs @@ -21,6 +21,12 @@ pub struct State { pub scroll: bool, } +impl Default for State { + fn default() -> Self { + Self::new() + } +} + impl State { pub fn new() -> Self { Self { diff --git a/core/tests/test_decode.rs b/core/tests/test_decode.rs index 312d3e36..8605a616 100644 --- a/core/tests/test_decode.rs +++ b/core/tests/test_decode.rs @@ -111,3 +111,74 @@ mod tests { let _ = heimdall_core::decode::decode(args).await; } } + +mod integration_tests { + use clap_verbosity_flag::Verbosity; + use heimdall_common::utils::{sync::blocking_await, threading::task_pool}; + use heimdall_core::decode::DecodeArgsBuilder; + use serde_json::Value; + + /// Thorough testing for decode across a large number of transactions. + #[test] + #[ignore] + fn heavy_test_decode_thorough() { + // load ./tests/testdata/txids.json into a vector using serde + let txids = serde_json::from_str::( + &std::fs::read_to_string("./tests/testdata/txids.json").unwrap(), + ) + .unwrap() + .get("txids") + .unwrap() + .as_array() + .unwrap() + .iter() + .map(|v| v.as_str().unwrap().to_string()) + .collect::>(); + let total = txids.len(); + + // task_pool(items, num_threads, f) + let results = task_pool(txids, 10, |txid: String| { + let args = DecodeArgsBuilder::new() + .target(txid.to_string()) + .verbose(Verbosity::new(-1, 0)) + .rpc_url("https://eth.llamarpc.com".to_string()) + .build() + .unwrap(); + + blocking_await(move || { + // get new blocking runtime + let rt = tokio::runtime::Runtime::new().unwrap(); + + // get the storage diff for this transaction + println!("decoding txid: {}", txid); + match rt.block_on(heimdall_core::decode::decode(args)) { + Ok(resolved_functions) => { + // check if any resolved_function is named Unresolved_{} + if resolved_functions.iter().any(|rf| rf.name.starts_with("Unresolved_")) { + println!("decoding txid: {} ... unresolved succeeded", txid); + } + + println!("decoding txid: {} ... succeeded", txid); + 1 + } + Err(_) => { + println!("decoding txid: {} ... failed", txid); + 0 + } + } + }) + }); + let success_count = results.iter().filter(|r| **r == 1).count(); + + // assert 95% of the transactions were successful + let success_rate = (success_count as f64) / (total as f64); + println!( + "heavy_test_decode_thorough:\n * total: {}\n * failed: {}\n * success rate: {}", + total, + total - success_count, + success_rate * 100.0 + ); + + assert!(success_rate > 0.95); + } +} diff --git a/core/tests/testdata/txids.json b/core/tests/testdata/txids.json new file mode 100644 index 00000000..963fe205 --- /dev/null +++ b/core/tests/testdata/txids.json @@ -0,0 +1 @@ +{"txids": ["0xd9b3af20b0f33696c69b4755cf77837b96244e49074e68a3be93fa05824894f2","0xdb8d1800beb42f9021ee29600eb6e435838f44544dbd3d80544173faaaaecd2b","0x2f0f061d2beb1af060c4b19e59f7c0fa4fe3d3c93055426afe1b39f15f2165d3","0x1e02bbf43b37268a2730f75bda2abc79eac29616e3b98ff001811f014b147a00","0xf4b7cf2cbc0ae9280e2820a71fb71b0127e4a4ddbe9e9208599734f00c4506ef","0x48e5823b018795ee1199b644183e604ea8fc79ea1412971aeb8d6134fcd65481","0x739ed3d56287d90e79c58008549a3c22bda075e4fe1a6ec1dc54a99059e93f83","0x861c49f55b1d5e129e3f0c0e00dc36092ecf81a7c9e90140b218adb6d758a728","0x2d531798cd36fc39e42c809b005f57c4fd3742f364c1be7f327420e8463618d2","0xc68fd547098c4d0cf9ab667c19e2258dc4ade33c8f17e1f9aa8358a77c1a0c25","0xa752b56c9647651c4786b17e71491b44c07eca3cea3412d4b4774fd8b792c4fb","0xaa724c8d316786510ddce9770559cbcc15ba06f4db3a82eed245c0a0fa1a9784","0xddfae6d65ba90b0f2ca43b50dd80631adfedb54fa40467f269f0e78406300478","0x054b2440671f4300d285c13702b7cb57d9d7ac15b1803485620665330f745467","0xe9bb100159d05f805bcb6bef92252f41cddd63d40c2f171f88873cc991762428","0xdd2c15a8bc17b91a2f63e5025c2f1ea65b52667630faca39844b7078f006951b","0x6c16130ab4cfdac161a078ed40a4be8f2f80c8b2e582eaa490a489bbe1e6acb8","0xbb7b5ac622e0ae15aa8df02d3ca5e0de46c74c3c37b863696c7593bb31037ba6","0x16a114e0087d0a5223f8257efd3f8c20de5ee312672816779e43cf42b092dc10","0x20e1c2382ff2de2c266a41507873fe5253d14376a6b6dc5d4ece3a6680ea1f80","0x8f6e588d872138cb5b2af171880fc399312e3fea08f94aa6c3b6684a57f2eb50","0x6ad74be0458ae55a65692f80df3595f27ff51c77e8636ed1834c6057746cc49e","0xe65654ed24e3855b75f9ddea229f2cfbb922a2fae234d229c8f93c3b75ea77bf","0x4075bae03ddf9ae4507293cb8b4cecdf7a0b4ff0922058af550435188567700d","0x4a83ace324e93dabca9d20ebd5533845c004f82cae46015ccf277184953251b3","0x7fca0b8e63367cd598be0e44f34fbacd598a538efa9d82c7582feb9c9577538d","0x1778c3824b85311d62dc6458dc1782e2822ba95d0aa70415b797fdc4daddd6e1","0x4c211ad251096645357790abca539cca488508c5c662e3a83fcc9ed98555aabf","0x6c90f9fc512da3bfbe0c8cb0d8bc674dfe4fd404a79d7c8dde6eada2b547bc6f","0xfbfc7c359ab1b2b89eb900f727bd69a9a0eb1c9095fbb78cea184962287ef286","0x7e31d14cb861cac3884655e6b3c75a18085eb89e9fc46303c9c81713835b380c","0x898a8d331a6b73cd73214d398992667b261cbe9343abba0759ac3496c23a89bc","0x84aed1213de2381aa6ba487b222cd37915a24aaa72592545060fcb6f87d003af","0xf4cb42bd432f67f89e4bde0683ee47128a77fe5ddb242fbccecf89f11df92fd2","0xbf949f9758fb4f93b68a1464bb22cd2f1f865ccb9deb595eae95c1e28a9761ba","0xd9691e7300b10d98787fba0e5742d2ee983042f3918c95bec25edddfd0026e2c","0xd6fc36397e90acb3c410abe98dc82a5a7157b54973d3a326c7b563dd8223a38d","0x4a4936100f1866dc49af02463fcf35dc3b38331c1da9247b234ca268a865b41c","0x2e8f4e33f3ae4964dc71557311e741987e7f77f4dba67e4b147653869503f6c8","0x2de4c63fa18e9cb60eebfdc25563dd25c19c7b7780e07065f52ccd18071d18bc","0x54462ffd16bf586c1aea817658fefc0596b40ac2b76863c2e511ee130d914492","0x7e75fe92c47fe1a23f30011b60dca6b5535c78ccd4f2e8ed55ef7eeee3a470fe","0x7a6f38cc6aa63501a7352750916f3d5431fb69690932a8b00265b9f4a5e2edf6","0x6b37c77c759488beb5fdfcee1e591741e0b00e7e4d786c14a6a2e3063218cb97","0xb766ce5eab85f25b0af41aa8fe004f190e42dfe555dd4212d7a2c0aa9302e41a","0x4d9c91255de40d37b2aebc2271311c8faf54d8da13602e9279e0e495fb404f09","0x34d5c2736ab01e1f3607228dd5df63f0040e6cb4014a3585e722418bfd79c8ab","0x3c6c8dc0f7ff4e760e4ac0c35a44a34d63c6b12e8a3f6603765980b671b6d85e","0x2b4eb82e411d9de446e67310f6133a803b4d0b166c2e0992221dcb3ef1525dbb","0x69dc4a8abd7c614ae110c1d3f97f0984cc3909281a94d1d6adf1fc9717acf1b5","0x678bb015724be16c9a04e14dd3d65cc24c23f038062a7b85882282a93bf2afad","0x373bec32ef0d7dd8797dfaf01fd1b5012e28a39faba7a4b837053b6bdee6caf9","0xbfebce06dde0250b1fc6c12a7cbd15730dab2099c257f62d0ddce8af9fb9bd0a","0x5f5f254d1fec73acfe55fa8a114143d2ddf3472c3f0ba4d3fac709dba27c9d42","0x2bea99d32cef287256eade50ef051f7a85d430f1219ac79ebc90989b1f22eae9","0xe760d1414b66fc63f6bd2ffee88093f9de69c7c1e0e4d12dbf0d53bce0634f6f","0xc452d4419d5208e8ea52abef8449b3c56aeb4cf53819cb86a38ae80f6844ba3e","0xbc6b5bc5321ec66926c40e257f7477b90e9600dd6081ac0017d96e83ded2269b","0xc356bd9b43d3e7c5d5d5083edfab7df9a2f911bddbc93654abff73bb1e7b36c1","0x28e7532d494dbaf284065234f6106805303a47ee5f39364c783d5b0666615a26","0x9dfd379a5517f75f43ebd203ea0a53540f600c2927182ab2d03326801633de73","0x30edc63abaa127708b26637bc479730965c09b17a3abf65f0e57aaa998d6e0d3","0x309fa50f037f0c0ae8da2a82d54a67974ae75634f84031f94f986c5428e541b9","0xe36d09823cfda918dd56e7e7eb36e3be43d97930b85fd25155efdb77de57093a","0xed8d41f25dd8a8fc6735152da5b2e9f5d21dca409c2a75aa0d5c8336318c1206","0xf9f465fdfe49c5d042cf68823bc7b283c9c0ac62b593275da725e196f2622c57","0x6449cb78d81dcb2452db45f39055528f0870744aa02df69049f403e82dfdf1ee","0xa8f7400d50f6a4991e3f7aeb0615ffd42b29f3f115e39bef9016123f4717e23b","0x5566314441cd5f2a0c22838a523b9be7eb33f167db8762f1d83eb92165012b87","0x8a6af1d0c0b2f57b7c6e87e9c264159b1fd664fe72f671958576f1a51013fa28","0xfcd4ac817d0cdbbd540c720c388da8ffc2208f58937da95d8d0ec9515c2bd8c0","0x776c86ad58872a09473776cd82b5b58672b551ea2325108cf190ff0935d2b9cc","0x0ae101b0769f93854aedb95239536c67acb96a8fcd9f5c426ea78e4df9115301","0xb2c0a42fe40f06979f187f719cf9bc8e9e4089fdffcd815d06a4a3740cd6e3bf","0x1929674446973de25e73c295c7cebd356c14a2b79da81e8081a9ce19a25b0306","0xc81d419d782c0540f0f5f70432d2af54339f5a16f48af97d98b9a085f4247dea","0xf533137235440c652435bed933fad4464feecc85b292a2d4361fcbea1ca12fcf","0x38dc44545915e62352299634efcfd478249f0575761b6b3029c9bb2d7c41973c","0x867fcfa702180cbc70eeba088dba731669b1ee208c7250d13d253ab036d3fcac","0x3dedb26bef4bfd20c7feec6ff6041c91d80c3e5552e550c4419ae8bd902f29e4","0xedf26b3e79b602dc01ac01d66d0e6f3bf285a1ff1babe12d7fe07eead4d44f21","0x08535ff648aa646de4e86d02f04f54c985f52d73cc9ba9cb2672f961a7a565cf","0x63e5c54ac945120b15979c3189f12af09f5f96bd80be89a58b454564f927457e","0x4987fcfda92159e336c0d746f13a2aa0009a67bc3465fda0ee6517fb727b4bfe","0x7578078878bb81460866dc29a69299434eec7333fd35ea45dc44e732c63add03","0xb8bfb4d8d92326dcf5d24074b98dd211d68ddc1b3b718c8d3faa5d8a8531f4d5","0x440de295647e961a40a08887c08f51e0ecc5bcb8d81abbb75b50c8cdfd14b164","0xd4cfe4068e88b71ec3e29820a37c769190ed2e54cdfc70ac78b825bb26e43ea7","0xf2ba06a7bcbe1589856ba22a5e6a9204c0d8fec55711d2538a224e6b2d7af1d6","0x5f8e50131263392b453ffa12cbb461a95b20e78615bca73b15fba5af5f8defe2","0x2b2cb49db49154149e1623f00ba365776ccfbba3a6dc47f6a8c793a09bd431ea","0x4d5e731434961708ae829840ea598f2bde5eeb8d7bdb17462831fad984f6bcda","0x5093f3d1f52fcb3da66d427fbe818f037b9628a8f19d691e2ba7b01cbfa3d140","0xcfaa50894a49aaabe00f3f6e5f0aaf7c301cf6699c7af71685b002ee5d4915ba","0x42c98cde69210d1a07372aa2a0ef6a81282bf035c87755655005081d4ce5a034","0xb50439324491e68d000743ac99c4f4e301bf21ba9d84372dfdc89a0290a49069","0xfbc619b802a32c04a39e4937c4b9d8882abf8ec3a77506b9dbc7e4e6b9f89e8d","0x404006b29ea67df67812c0f1313334d994467dc37939d55be7d3df4399472754","0x84ac8fbf7b199de6e01c4148cdb2c1ee121c7430174e4ee704cde349ee5437d0","0xe17a11c675fb295030186864e0662a4e03b91bf1d08f0f1159916d9bb6b453c1","0x67cffb7ef293a1d798484c8c4b06814b728405afea187c17a5950581ecd42b36","0xcbeae17f909b389ac71c8b81a6ce342703bf536e29e8d97d81b86f35151face2","0x2627f0faa2a0505b89629475e19316213349e18b489f11c44c510ec4956bad99","0x3db07dc014a1ea130f3bfb5eaea16cf4eeb3ede1a0eed48260bf382c91f9e3bc","0x3b051486b952fcb05efc7984bfcf3bb1361c1283af43e96589448f0db0d7a8ce","0x2443f5d833938a3437581719909873a3eac869793bcadb8f8bdbee6c2cafcab8","0xd4dc08902bfff0365382c333a017d923438502e480fd7feca8a669d357158f40","0xf4e11c1cd45e77bbbdfddacefe926a7696384a5e3c14e83dea1328575c4365e3","0x6ee2cb419938dd061e94ba061a86636702dd43fde7e25b6ac3b0cfa1ead6c59b","0xfd225fa1de68eb2c6d8bb10f680066d987e55e0759c33e9b079d002680cbddee","0xf5d428bd01bb07d02318e67229e2c8ce478f215c637ee437498d0dfc17705f5a","0x9a2b9dcd580335f8a3b0fb95e2631fe6c4fd7226796f5d15740f9ba120020f56","0x007325a4d096aac4712acc3ca2e73dec3c7ad0306b891ea7e6ad3350c40cd920","0x0ac39bab6174bc27f7cfb4e8b64bdb9934b499045d336d53cc055eaaa638ea29","0xdaa45d725d02f0a80454620c53ca771129c23b799bb1b2d3254ea9c00a1c97f9","0xe5283486e84a67458467d8bbf66760169668965d6697d7cd4dca3ba6ad72410f","0xf86e0f65d7ebb20cc6275c7b1e2a3ca7a1f2a5d5c23afe56744dc86130169481","0xf60ed5416d0d452298a00ee345f698b3c01387178de847a83f86834e87358f18","0xb617700ec17fbe41f447aaf5bebc75722e8a4c231c265e4c1bfcbb537010fbff","0xd315662117044fcbc7c5a7874cc0801122dc2ce9b3727bfc4a02e0de94e2a4b4","0x8ae8e679dd23d72d0f034160ee5e755f48de21a241b3f9fde8f8dcc90c9b0471","0xce4180e4f5f34aca0111846d176307cf115f5442b4d34087bc0780d80363a834","0x036925b92d0ffefbf008e81964bc9fadd4855bb713d513dcbc17b17c1d6fc3b5","0xd983f70a7d66d3daadb85c92d86494ee149532f448eb6ad2be339c199c79aaf5","0x9247b37a982919d623eb9fa1f5fbebabd2b98105ab9a9fab2628faace1981415","0x110289d7583b4676b264c0885f359d73851ef6cd26f6006649b8b2a523239a03","0xbee19ef828a83b0d431520e418521e4b32db246b2dd01e2cbea4884a89db4705","0x2028e4eab315ad0563cd9f8be3478a49c8e8065a2d2b05452988b684473f2a72","0x504fa398b85ee12c97a956e3bff21d1cb7357e19fcfe2642fb3b63b02e8e7be1","0x3acb87d4239995835a497d3480928e9f33704bb8fcb8e3fae52d2b29d02bd06e","0x882f8be1878c191a0362a43b5382dfa779b2ffd1f1e9337eb7ca7224d92ae68a","0x813b76b97fb4bb8515a5ea538c140835dddf2dea449cd3f5ea14b1b487a0b51f","0x44ba4b758189ccaa71bbcf33530322afc85fbb94714f1cd3ccacfe9eaa73eea8","0x37482543cfd9beb2623948d21f43b0586f3afcd7b75ad2a064bb2e743ad95d4a","0x1640593e5f5df9d262b85a2337821299b01c5422f569c537f4c546180f1e6789","0xcba3527db9c9dd899d0cbd3c1ff10bf68a07db909578f31da4cf270a1098c384","0x6b9232ddbf192532c19aea6b84a57e10e8edda8f25cbf762a296658cb192da2c","0xc2b306434ac0ef17cd39a3624f503d7cdd8102d82b5ae7bb5b3de71d6d5f9f72","0x18233705eadeecda942da737a24420ad4901513b233cdf656ac65ebf5864312e","0x26bdaf5db0695a56f74b33b8ffc3babc9a02cd6daf609604788aed8189d92346","0x34540aafea480598bc84df58d8126ba8bdbf890e25db7f065055f44a541f823d","0x765b8710551fcbd0c849056a02d97c39325f1808398e6e0bfa2bc202f21eadb4","0x9ed39882c8b352ec2e246a99ed074af89756739dc39d6762a4f2fc6bfc30156a","0x68f78adc98da69c6f7e3a0814c1a0a4d8f95f579a77e04017f2e2a0cca06af1e","0xd15c65beb4da055c926c7020b4c20a433c682026dc2d83c3c64cf78dad84000b","0x1d6e99bd29cde1fb271214f3ad8725d8ac94be272c4cd4830475eeb2e5005d7b","0x54325ea3c42568b90ff7a9549d6be29f295919d144b8d8dafcd1a89591eeecd6","0x5e94ba5d51a0c9e0bdc2abe24f7476c098fcef309258a8ec8a87038700667108","0x7749e63f3e43b959ee20cc9e1428f8718cc41c743b637911bab85c06b076d817","0x4b4cf58ddae21c9437641778ef29f6f952eae11155e1eedea6d2fdd79077ad8a","0x475b1f0abcc2ab89243ff0a1c139b9089a49a314630786a44b100424dc034852","0xac1e6aeba6ea37fecffa4cd1d66aeb02b090494a5a591ad6325717431ecb03f7","0x12fdaf40609f3207523c9ab0868ad3fc47b7e421035b08f93f4bed76a9ccc219","0x292044684636cafb8edb86bb9b5134c988e3b8927e0d91a40d681c854fde433d","0x2e73d06a759a2488fc85f8c45d598f00ba78e1b7c8d807a07977f51b48f92403","0xb309a9d0166a2632eb619f51ed08a14a600ef333a2e4fa5b7f84fb618ecee73e","0xac2f8a42e5091f6266722f81ad6d02f2382740dbc8e2c4d42bfbd12f39cd3616","0x66632bf158879fa3dab84f4ff7a6c9a98d4c522c92ada002a7828edd9da7b95f","0xf1cf3dbe026baf4ff4edc6c8e80a8ca52fc4e74c0718aef7366f2eb26e970a69","0x6db76401b6d24b517bef777b32b228b801a12e914e559083e774b2e13f983781","0x9b561344f67cd2316058f06e46a8a7a2a971dabf4577fb9e2a3b5519746c5ba3","0x97571bce71d51b498aec375c931e837da64da1840c44e3d33341dbbe733b2946","0x68cecaa010d5cddfcb4d903ba43339b3979bf56cade737db64c0b9bc81d95d4e","0x96a126286192988c0a11b07a6625ba205ba993cd69e5c7baa42c8d7304c3302d","0x7af919a6fcaf0821f0f49823be97872c5e5830695d20d901b3d1e2b8e200a78d","0x224913d83a49f2d19d4e0f36ca215cac81c2baf6bb950bf3b39e802da5744444","0x1ae1b60ae77c2804d38588b38f7e756992692be4782da2cb3eb8328e5bdbe993","0x1894b609b8a60f95ef568a77399f97f2ca090e25de4a8d0f7c73cd867ddf3331","0xbcbbee8008409d762d348803576399e4746b1ebae73802f2e87894af8e3a1397","0xdf4c6ec1f5f7e8293ae3afa3ee783f49cc506ea7afdc26d2ecd252591398b9eb","0x093d78465ab1081504e47f4ad71a35904ad7b84e86e18259168f5fe97a362e95","0x57b013520eb608f45752f8494b2757cdf0aeef342e752add6b1d7b22fe83582f","0xe01d2ff7c3f0c432da37ac3282c46a296c311844eba1e14832248e89dc5a4489","0xc1a94777f4e58cc9ad571a745857ec8d0ca9ae835237047ba75197f2a68df65c","0x2220638778114158d3b7adcfa3390c298600b2d6ed4c3c6f45cfeb1e5f1f061f","0xb5d8455c374c70a9e98f59cb97d1db2f9dfa76fbaec2f3854dae04d9364b96fb","0xe8d1de7e560b51e0251da38abafa1a7b13e8da3367b8a6a678d5deac351c44ef","0xebd6725d210c109db1c14a946ba84680f3ade4587fa560817d9db70b934e019f","0x036cbb3577e4f0c40f7c9025740afc3faacc0a1e08bba3b5dcdd3edf6da2322d","0x845960e9fd28bb7b7f48bab602cb48e9686cb9341584d5cbd5714bfb7765093e","0x4b25fb17973e1edf172fa7d1d490cb4098158cb3c5b0ed19d7e98e08ccaf421b","0x0c4dfe72142a8667b16441a686dea6db32e7c8ce5c24f8d2b5d00c142271e3e7","0xf05592deda2237a5011e951067214c80dfb81e6c067e04cba525422b753daa65","0x25c130ae6105470723262a88efcd40d6368006b42b20cc10bdcb97bd7004f9a3","0x876323e25290899e2d59b96bdb4be9aa59bbd1ad3dbbbe4f0e8b719cd932af0b","0xf691a8d43b07d107b58a9d850f9dc7685f53fb5204eb5a4fa771e7e5f0235953","0x10f14de47c9ba5ee39eeb2223995cba360579e7caf18bbc6388106c296033587","0x452ed644b9239537d14b77aa1602da4ae6575f38d455dfad8c3cb8e5f59019f7","0x5471280d6629f886c6383896792333c3685a3e41ac0e8a1214e9b4d7cacdd999","0xa116a120f6e9223361890e675849925b14027d277f622be444b3930a5ec52fc8","0x46ab4d508fbf201fc62c0f8267aa831a7cc3bc6fddc3cf5bfc33d80377c58659","0xd8cacdc12f6b9d8880fcfcb6d44a5ea8802f9d22b38e6fa0b799e8c0e1e77b38","0x1b54a6bb2140224935a690474eade8dacb6e6808f4462e4fe4c698f890db303c","0x2a705348bb2341f75af1abba6128cb8706a6e052f6eb0cce042e32bf0a240ad0","0x9da1a6a6eba2c0cd2f192063aa1d7d6cc92e3c85aca6932bc9b0159fb91a88a7","0xdd3b737e0c0659e381ad2ee774b7cb308395957ada95f1edcfdbb4b204dbddff","0x02779c7fe199410db01a842aa142f95464e1b19e28ef26fd5add3914a4699413","0xa20478285225787d8b82654e84b181ab53dc6dedae75ea8cdf7d9e7cc40ee0a5","0x4f50efaff3f16148d5ec4480aad0aaafe5cb4cf1f7c524e47fd9ad6e4b47394e","0x1dbacc913384c66689e85535085754f6dddb1eeea04ae3986bf475f02d523a45","0x0a34fffa2397b610ca2de2c30b6188c01ae2a3cc2581a4afce4015cc44170cfb","0xdf795434f890d9399f823f48caff110c7a64f8ccf5056820b30ccab5e9fff693","0xf1973469ec57aeaec9dc0c8d66771309cce2fd7f76e41a5291ef5d28055afcdb","0xa24c3e75dac1e75b421fb4a0962423bcae98e2f875f6b52254c2860a28cc27c4","0xd13bae939a5f3e89773918d48f34c1f4abfc3169b6da706b31284223db390423","0xc7f8fc8b8ba6208e2117abbafdd31b6821fbe95304602a5a21ea18af66eaa32f","0x5c6896c1a3bbe2b5e5497dd585eaf00920e5ee3bc43abec13b3dceb6658e9247","0xf19d8a662d49ee71cdb6ec66fd49bd3371202d065ca04ccc259aaf2eaa667120","0x7095c5154465d149964cd61483abfc3a0ad376fa7eaf5d33612f57936966c58b","0x9e894f6ae338908c448061ec54108a618fa00bcdb29ff2596a725b2a874c364c","0xa47310b0585f5eb16efa1ca9cdb938fedb59c0e256f5cd98439158c1d88bdd58","0x668cd2acc68d3f74ad098b5b5419af2f533963e6c0aed2c1a65d666f168fbb23","0x361442bd8db1514985086e1b92283df33982ef421084debdbe406ed0153b84d9","0xb7c1a7ec6f67ad0a7561ac6b0863e1aa5a09f57c01dc6013a71b585b49a3f466","0x1e4864c066c08b994cf728f19b0306102311b8ecb44930377c2d10b65398a68e","0x17f4093ecbaa47cb958b7fdbdf9b92f5df354d45dda194477b67ec955cf3e50c","0xe802e379aa306b9670a4b782f72c4359a8afe5df4c8e3843d79b8d48915dabba","0x9a34d1094cebd0f012d9f1dadea508bf8ca641e4a7aee8b4043fe094103a2814","0xb63f185289d99add87af7c50cc64a82191c66ebd963b3c76bcf0ba2de09561bd","0xd7bb6f9b4d14b9f7704488d36b90116e5934fe9e7c224a1facba999f043c3241","0xb03f94c101ff2b5d77e302b204c6a2b57aa38a5cfb89f38761c54d2ca7403619","0x2e53f27563775efef4c9c64d37a313dc0b3399f025b72f1075c8d01ae5667538","0xb3d27a5c1f00d067f78136806a78a2bddcc919ddb3ccd070f6bdd07c1d9fc82a","0x97ef8f33b46ad8c2a857e607d47b2b716592655fef39a4cf874959e38613f4e4","0x17bb12a67bbf8a1df9ee426774529ce16ce9478f2e09bb26ff80f3640abf5073","0xe2ecbe138b54ad422f0062eadd02e957d290d6ca244f93d01e9c25b96a20be75","0x7d37c929c6e601e1ff056b4689a13b4a5a285990a8e26b9c6d72cf8457b91f83","0x430f0fc1e2ac17b06b17d9be9757099b28bf3b836be6f254909387666e1fc520","0xb60e01e3833780c66822f7e5611f9b138a1bfb3941af509a6083c98fd309b519","0x8b5f16053eff81b87dd9e5b7fc86cf712be8cc9a0f4a17b7233d67e13097a6da","0x4128dc0de2c6b0a3cc5ae9767ddde4f829fa5484b2f32ae10c008cb9e20cb5cb","0x082158b7099657e50aa1bd4a4454216aad7de0ea7e00b6a89ee37679a1c283c4","0x3a9ddd628aca085e71d1a637038a59103ca6ff4722b9694d7c7d2053817e2ddd","0x4c491ec0eb6f58997974c6dc47c4c132a43614e1472048831a4dff75a9762139","0xc34c524e28a6b79479252ac7070f557cb1196125b961f8cef3ddd9b7fce049f5","0xccdd7db39277b3bf7b1ecbbf9d9f2a5504df85cf149d69606b5b8b6496ae3595","0xb6e28428b32712cf212480fac5577f5abade94f1982ed0c86e345d9ca400c6a9","0xf5986379b5f208d9cf2654eae83e1a1a38da68c0a7feed3dabc765646f45c304","0x89a7368925fc68b683d0e7a1e339f0f8ab62ee0fb1c4dc89b87f1d24783550f3","0x645d6141802581e82b76e43881e00ac3f20eb74d4b33f08e7d18be939cefde1c","0x4c9c754d21c86a868094bd30e490285a08a3202a23bda410c97fd064a2555d7f","0xb2f2ff9a7da82b96a18e11f19f82b642b92895647ebc34795e66c28897498622","0xdaba1da926ba65c63b4a50517c7af5bdf2377d10453c2ca652d36d1d572ddadd","0x28e81d7e9e5922d92be281125e640be92e77f23aa177ea45eadba2635b0f9d23","0xe196dd492a3531f07b630a0a8267be724beb1612b138bb236f83729fdc4bf2c2","0x848b3a2ce32bddd470ad2cd40688bd5b56efe28908df12489da3b5c8ad204e9d","0xd5fa02cc43f136508cd180c77d5622fb988310fa12f951f424f5cf26cd9d1a39","0x73f32c7000206cd1d0ba85e9dabc5263bbba7a0dec5cf232f7423eb65e6b84e2","0xd61575bc11f883fc103de5ecc2f4e9de46d13268cbfe14b9124db0bb9ec489d1","0x97bcbe968c9583db075e652e1786ac1ea85c3d74e2def280112c62b196fdc183","0x3864016378e10d3daa3ed68fb6ab2010b314bad099b9f3c86ebef4861f82865c","0xdaf9fd780a6000a8ff0494d899bd9294f399853f6e0f7524dac4183e81c31c87","0x6bf38734ff5ce30258d7fc20ec55697b22b7d302e86b07f121c6fdf76bc27cfd","0xb46db2c2f8961b84f13cebc04da4bf98aaa62b793984431a9cd149acd26cce1e","0xc6d0b333f4701e8c52bfa8788fdbca55f8ec4ccb34fb89234e970fff5b372b46","0xc136ccaa259023690f8b2041b837be36158d1407a7eeaa5645506c31302f50f8","0x262fdab13be9a2a6642d9a355f7c649767058e308f0ce6e580973eaf6af9f5dd","0xed782aff7d53a4e8e69cab950df50118b566f3bd7529b67bee68560f23736867","0xae3720bac1702b91ee54596a130434f9c722f60696f5efc0b16ba9a0b0847bb9","0x1c7fe6b80d38b3e8b4ef4077e69107f8b4c6ebc00df6d00e20c647ca2394be43","0x2786a558ece6fb6c4522e3b900ec4985f4b758a994a9016f00b95fc1371f6b4f","0xbb3088318ff197ea93d267e791aeb1be188fd129072881688d1b5a89a79b7012","0x760bf1b0a51e903182b1fdf3b2bba0c8568e121ca2a6bd696ac2250c4039d76b","0x1259dc309e0a764040282a4494a2ce6329cb3ad56f5587909b184148ab85c8fc","0x413ed4d9e574dc568f81fafabd60c7f4c00aedf1bbe67ed779c4507ced3fab73","0xdcfff6e29dac6b94a0924c58128f2ab4abe85d3d534052cdb8c15cc19aae3fbf","0x3765c09b331fbe510e4b0ee739705f53b6244822575b3fa06b38aac4908f629b","0xc79ae2849bead579a71b92fce50e9728c7fd0033b8abe34fc7b67f292c271710","0x0d2b103b164a026eeebce2ec0194f347dfb33ec38680c3767e54df975015723c","0xca9b2e166f7a82994cecbeb29377bacc51e91881690d7c1ac445c81cb6347332","0x404820a6855da9f5fb9c5a7b1392ccf30d7878b72e408513d98a7b6f51803124","0xf1b6ca7e41a68c474c6a806955d6d57bd3dfc6b16efe0d70a4e51f3ffbf0f555","0x978679d2715006415d1d7364ba8c73a8882c601f1ce2020cb66fc9f797685f11","0x65228c8e173ad237b2a0e710b424e5ced94cc842f34395d3cfd467158dd080e5","0x752bef7772b9d4b2d0d2fcfb1e305246b4bf9f4e40d60773907cf5ae12d23622","0x7508d0038499b673b475c816c2a769bfc5abd7b86bf55eadab5b814dfd798b38","0x8c9fa3f0de3c03b27de254fd8cb04651d512db5b806b68d8847c9e9f10bc0a9c","0x9cb9cc056205dbb872d4632c529db54791c72fa2c7a53cd30d8e61190527f3af","0xd4359b5e6a2efe44752dfdaa287e9f4d53d36a3ea9c535e3802396c779849d3a","0x567c81c416a676767fdf05c7a5c10c04bd9cd77987ad381dbe05b503b77ed883","0xd93a9c23545d1f65fe32572f32474d688c6ee52835a73d2f68e7127aac2c0cb5","0xffe44d389ab183014037993c20de715a85389b13b9a87005be80c64332218867","0xd80df9ea39310a25e19c177487d10f09bba10c22fa07d5ba45db7456ff83519c","0xf7bd1e678af54d5784c2aaa97bae27deb6df4cc2104895ecc08d71b42854bb0c","0x0fe9eaa8c98ea6e4e01077a920475f36b664516e7040a2671fb92298c05c3189","0x13bd21d3bee4b7b7e73cd2156fe63f0e4b5f4953af16d362b28a93528b55607f","0x9e74cc4bb543866c19094659a32c026335c384ae75d4759cae113122d4165f52","0xdba520479c44abac6dd0eb42e714e2ec9ced52d8555cc80e5b118cfbaa4f4d3d","0xa934e01b4288d1b50b37dec9b1fe76b120f3e2f86e9cc105c20f095d81c85e72","0x8348d48565e91faaf01836d3d8e8ccc86a02ae9b53c16bf30ce57b6342940bb2","0xc2b54c41378fbf6c630994ee13133454d9a3af8d986c1b6c11ee875db2f5a724","0xf213ca2f9240735d88d0d0ce0369b59941b8a5d5373c334e722cfd25e27f7b4c","0xb632832776b0c23a12d46830b2dc7184861fa30f288756792766606bdb658955","0x0a1615a57bc5eb4cca555b5a5697b49ea91c18c8149f4e468121495053cb4513","0xb18c373b1e69aa61d3aa94be84137f1d9d12393fc71c25b705d1d63f3e3bbd16","0xeca3fda21a12a32d57df7b220aa0ce2cbb30958279d72b02e05fbc52988de2ed","0x97f3fc0d09eb5327c46500f4a52231a4ca280d1abc90aab57dfe3ca01c5ed59e","0xaa67641ca34dd0daac2319f6998237dfe58a25bca18d1cdce71621d77251ecdd","0xd02360a770c5fcb8fcb9c326e5fd472e6a3c3df73d230581863760ffa1849e02","0xba5c161ff40a4074249125f6f81caa60ca3c46f139da4d9f13d4f23c6a8a34f4","0x118c9bdcb5bdd1af4e2ad82316d6755fc366267d1ae9125d11a943063be696ac","0xf2bc0534be63bf7a00562f0c70fb69f6e1cf3dd20892a37baad4010687e0b2d9","0xef51a6336e9ae63be9fb654da06924f36b498806da83775f231f57906d5efb98","0xbcbff941b238c2a73f5242102ab94a8d96797a3bf264e68c23ec0a2b10ec2a9b","0xdb203ac3423a8189b3af864af8178cbcc4c68e8a285333b20d76d587515a4cb6","0x70f7a38491c8e40408f460ca1953bad9bcfc0040ce721f2bd847738599f972ba","0xb0c4ed9ed3f7ab2498986ce1359f517110d4b12439a47384400c620d45627137","0x8b10ea570f3e6b17275815bae36d721d4ee76ddd886d84c38f5baab448a779d6","0xeb23a96bd7344a37a1ef73d7ce76f2e509771e04ea6b135c8d1aceccdd71a703","0x235083debfe9afd67eaf472edf66053cadbca681a9b75db17a7393d51c7f1107","0x349e9d875d7af060238548948e906026fb0f4ddd225dd7dbf405a36ba7848f04","0x1caf4443b7658cbb19a43b2936ea2c3f7d6b59e07e9ad9b72debe9b74d3f22ce","0x1acd974ebc3aaa41b2dcd6ed08ad21d2f741359dad04e3b5fb9a771cfe50ed8c","0x19f5d7eaf0c72e0d6a979f2e7d7993009a3f5d83dc9062e74df9d2510164322e","0x6cd515e99f2297ad59c94d88964a172824ab9a13abea6b18605da157deff6f05","0x837d5314a934f60cc476305b823c287184aab9ce5aeb83aa01843e1348251307","0x524b704ac5ef92c870b16f3d3b4702e4d1f095d1ee0acc566b50c54f398a368f","0xb87d55209674e01ce01985337761cac7b80658f1dd38d3fecdfa6965e92aa42b","0xb59114a29e7ddbcdba658ee62c5e357bbc55ddc844b553ccad361b3f4028faa9","0xda5b7830462ff793499a970b321e72254872c3a3521f5449571003103679e2fb","0x40002f327dbd92fb86366a04b8bf6fcef6eea1af48aca913ee2972886c3e423c","0xa5e651e8f537a5d01645d6b41b567ac5acc66c080c83b02185feac046a233cec","0xaf1e225c4839730d0eccfa22370e40123a5d23f5c333a930fccbc1ff384072dd","0x2cf1f80efb1d88eea1e77d3259cb8012e15d189c10d20a4c9e5a5af328dbe027","0xcbd40b3a5d6c859240d8d72369eed6d8e1ccaa61bb7065d22d9df20add2547df","0xad261f430d054983788e89c01954832b417456b0af759ff914ea004164d92b35","0xea520313d504adcde482d3ba2c6429e47eac0c34c3a6319c120816f6123f231f","0x0f9b78c70f9845bab44bb96899864fb17c9c43605c3508f8a3cefbc53ec7b4ea","0x35a3810b289e238b678f2572b43521760d64f7036a14a3e21897ae7cb134f115","0x069dfb639a1612eb6d58d21c1f5b29c1c23dbc54d0f7278dc8938b562919791d","0x23527d33579de88cd9af370c500dfdabf8f0126efacd1c82a1e1bf8d63d7b6ef","0x305252ff151507305ba9c4d3b7521c76294379dc185856749344172a7dd51c9a","0x2bba76f13441aee4197d46cd4a07a65ba42765cc52957abd71046f008fe72dc1","0x3df293559007ee02b1af5f0c5e2110740132fc83be0a3367365dc3e376fa8383","0xdaef534423452905ddb8d44514dd00a4922f6ba3c62f5dd98d84b46428d5f848","0x8b8d1c2a47d2c476a6db47a985a44a845fb916c66c2b74d2a5ee89d44e842572","0x69b9b02431dcd42c9d150d30c34415c936478b70ed2a6044923bf51847e4532c","0x641dc287127e535dfdc4132c4aab93980b46d296bfbf8de4abdd4e59638d853b","0x387c258416e1d1417599c68236b575e35dc1a353fcb4aec0c640c8d77cdf8b63","0x5ecc20257d054d38c0d0e5f62b53b774b831c21506866158e074172018729b81","0x65f553297f30ffeb361d74e73dfee360e43a466b290cfb4bd30c1cfb4ab088a8","0xbbe78947d7bff2b743dd512787a09c9f2558fd5ece613742915ab0fc21ed8074","0x0b8561fe3c95da6a2fc9b959df9a7f33ac47d9f69e6036b42ab0a2179d887e91","0x4b72817422b169c21e50e5fc6080b4dcabfcbaf3a31fbd6ea6fb50139bf604d4","0xc7f08ce2a35c8e113ed9ad573b7218344b1c5e73afd5045ac4c489162390c033","0x060d9c92175bd4755e07c6f2c8a9e9cd55f80fcef5431964b29657ffc37f9ee2","0x60ca1362868589bc8161a225afe2d39645ac4d6e3c0a98c10f85fe918f7a1de8","0x9d5f3838a5cc41077a82abc9ec7cfa2ff45195edd34e7c98830c044832740b16","0x45e5f4b927f8e2e606225f71657ccc9c1108ddda4fb4fe3b9435c88a4cf56eb3","0xe8998d3660c899a40a44d159c521a2c0a07cff35c3d665461b74c43535c67e4c","0xc712d4fa621d23e43918e2f4451510b345c9ab4f5193da9d0b2ff7342b1e6927","0xf14c05e4dd35baaa697150a8a073c2c618b0b3ac35e23212b782fd86756a3e1d","0x5ae934c117b59dbf7b82d50c8b6f4edb19dbe0b16945f3740daaf472d58b8d48","0x744b027763c6da3a79d192b2af8ccde72225db7401b112e3292e223f891d21c8","0x7f54ed6d0fc8057a512da69b09da70122ee967f03d38354566a21df4d7a9eda2","0x114cb310b7da996c4dca9bed82188935e706d521437a13f215d29a69b3fa357b","0x3263b2d2cf2329693b287cd7894db9d6d95d16b7b9d3026256fc042bb14ea008","0xf01a4ff43bdf9b1577cf1f1354ae9e7406ff5448deeacc42c1008bf7474d1ec9","0xe11868bfd8b4850659e361794ef048c021829b3e2dbdc59aae2489f23440f171","0xbbd5bc64a42fc032da86e627bc0cdd26ce68d5fabe26bdae8d28c19b1bee6a19","0xc1029961a4f482a7669c1e85c14190a423f3955408b63827eed0aab3dca73faf","0xeab4723cc211093e54a63c8055b5b344bf5719c166677c5302bd68a8f0009bec","0x3d08d68637b623a982c4601843924654a149dbd30cb15695d42ba296e8aa87b0","0xff54c782417eec1ce2b2d506866d9b14a451aea041db2e29d47b0b6eea53fc46","0x08ac2ab87a5173448f6184ae989818145cde096ab20651f868d1f952f6956010","0xc5d9b276feb64b71647d2e1f6da25b6028e98d082a6514a85d68325921dd63c8","0x538888682d1e5ebe82d8fc415c7b772dd7704fd52391f8ed2bee86569108d801","0xa371056851d7589956aade3867d138ee03493f042c0d7a2a1f39b6665b73fd03","0xe29845328abfe0ac66a882bde6edb9962e7dd6bd75066a677d93b9e62304a828","0x54e8148298773ad1263e08ce2aa2cb241cba728e7cb7b8582e71ad8da5d70136","0xd3b0157098939bdf0056fe5f86e82a8673d8367f425ebbe350188e4f74ae11b4","0xb7514ababfb9f6c8edee8dcefecc9d202a9dadd10d1e7908cbdcee872ca28506","0xf4b33cf8fed2a296a821badf9bb416d13448676d7fc80c59b9fb2428a173157f","0x1ff2e6e67a384a87baee35332008449192ecc96fc3a78c6389e18c52af72c14a","0xbdf5fbfc0faf4d8474325ac4831899beca310ecf18a786f1ca75f92c18d68252","0x2cca1123669ae94f8177492f9d684a80bf4e6693d7035d88abfbd8969c005d09","0x5952b58f68c190a777ff0e17c7d0332e0fa799a503ee7eaeaf6ef7627a01388a","0xd6ce80b661434e94a93ca26c14e49a33728b18ae06f1b97c2dadc5b01592fc52","0x63a7933eddccfa78bbb5752ab7b1cdb8db497185d736fef4966e35239d158be9","0x98499bfbd80335c48823e855190fdd5a9bd35f9adacbd21fad8a86ecefd495a9","0xeb3ae71fbd2b80fe3427b33c7e3789cd9e82ec2a5b65ee73afb46dd5218cbede","0x8178b7652faa1268cd78f8c753277d68a9b802f5498b026a6eed6a28ef8eec4c","0xaba191b5c0d7126c557ba3831bd0209837b0b07816a8284ca62ef01d4011c68e","0x90843b329e55615dc7f374cfbe6ac659d5c3f7fa5e2aa4f29e88c0fff76c2759","0x56b4de26f2e422f4f9ef01fe3f0b58e0d454f743af8b90c375b2a0035358081b","0xb24b780a353fd4352358fdf252beecf25519ebe2524f277673b340e7c445239b","0x5669286d22b75de3c04e374a69383f4b4868a7ab207bcb3423e65884212ea33d","0x9294bc101fbdcfc732683a4486eef2398d49a2629eaa7ae211162e245086aa74","0x7d9c03f46856fa2cc5232995b35b1d2f65fea5b4a3390519466ea48e23774566","0x4dd72c81dab6ff98a0b2451d637367082890429048d936f8e169af66370070d5","0x559b55d197cb30da52ef3102161b2cb7c6a157f12618ddcd72fcc1a1450ecb8d","0x5f9dd47e9a7bd4fe986e8de903cd809cfb1be0a74cb41926e5af181a64d34a5e","0xcceb1bce8a335d0dec6b0045deb470b4a020baa9b702ec3930eb6402aa5792af","0xb8a339d6656996e799cbb4bdeb9c76e91a14566d288db517a51c0a31d1521fee","0x7fd73b3aa516f12edfbb220174cf32d76129036fb18ea8bddc19ccb82ea95209","0xd22eff23ca747e13d4ca4f9ce3bd4f920ab759f3737677645409d538ff1ebba0","0xedd4f1e64248669f09056054960be7688978ea820880a8b0a46e4032abd0375d","0x2ac1bde4f0bfdd3abe42a36eae34c7973b7902a871b9954fc90a5fca94fcec0e","0x95936c4819dc4258ffaa021cd281a9fc9593bd9510f29a3058f63a9312e8ec1a","0xa832d7deb1881308c4679e0bdb0435678a49bc5a211d67ba8684e2b44f920e8b","0x9977e4c6930b27aff6de5b87321e95933b0a6051b707269b049029a016080863","0x92b4d8b27e6c4f70ed2f32e46a6bc6e6859f0b89d4f1a9447a40f78266061cf8","0xf0d36ac9e3dbfa926af9f0d43b35caad74e2a4a209eeb2c79d061df049dcc86d","0x09ea773de7f861c46b559ac367fbd6ada58d1666669580f1ed5ad959ab54bc6a","0xb3f27e2d48df85c5507c22fb062e11cc960b540cdb43225af2da11ab5d7e043b","0x971c763e3f6ef6b8ffdb26dbf1b6c5b364df1f3999f0b33bb9408b493a7ca66c","0x7e200d4b2ab899d7f501623b3dba33d11c7f687ae8a399b70a8059e926bac40f","0x14e851def368276787b48c5b78558edcf1e581f0624e1e7e6f247208b37b5991","0x783dcbdea22d28cb6495dfc6feba91e1e5a7e499e84fb0ec821bdb0259b9cadd","0x3df4d143fc06e1ea552a0412aa8c887aed9a2d265cc92a37f9a7b59af74d1898","0x181bc19b3e943035f9d2d2bfdcd64cbb70f2d5fab4618a62a543b615028e8b84","0x5d30b78ab67929ce31dda59e85913d0c6ad09c9c70493d7bee0fca3d5b7143d5","0xefc7d1a57178db328e8d548921139ce8aac39505d0303af62d4c4542c07648c8","0xd14b613c1a7f2aebfea341bbf28c45061f4c073830957ab998d143d8ffe830c5","0xc9a4a8b80cafdd2b9ebf8c4f898a7554b4315f6813d6319bc7a588d6d188b472","0xce1e32a7b8ebb414dd6539ff5c810b09c397c3f58a06654661cd39a6fb1d249f","0x217acf629ca5d971aec3d92af732d7f393fd57f1dba36283f4fadd4951842b5e","0x921b42c6593d7836042c38989a94aae9bca0ff184db6dd93f7a9fdc594eab14a","0x064001e03d02f1559aaac1f485411b329ebdc58b0725bc18a257f8c6922f33cd","0x709a059dec52c5afe642aad27d89344d31e236f4bc4b5604e8a9902fbec23018","0xc7de8ae6eca7faa6109d719c08ee61745bf64635815576c510cfe89982fabfe2","0x8a15aa253818bf89091ea3c9a3dad8be3c2c7137736983fcbd4ecbfa413af35d","0x6130e66287c954f9e780331bca36749ff41b9a6b2abe6fad7e5f7962ee1ca975","0xfbb26c73dcfc3f6c31e3e02266ad3f96eeb6783f45563625ba918683b5dfe4bc","0xbd1939976d3132fcb6706eb73974ed764e9304862cc1b6f811352e3b739e3d6c","0xcb32e256d32956a9b8db6ef7636387d8ff18ad9d8c5c270e7d29a43e735c1437","0x1d83253f67c6a2f654734c9a1e6f558f07c5ce86c849140c1bf893e8afc6c41d","0xce5e41c9616e0272065f67431f657864885e978e0c2ced5a0103c5cec807fd16","0xd5ef4e1fb77fd00cda53c98b7f1f3a3599b5a87fc32950736cd5e9953c0238b5","0x75071f115872d12f59d549c75c65b5b109357bdae45400360081a1bd5dda4c94","0x32c339369f0c79ae166c54cd303a98bb4feb552ccf2b787d60812b982db7ccc7","0xf36774b1e033c47d66e021f7dfb02f140b1b4518824169efccafbc323451d956","0x3f3669876e4148c02a97d14dc636f6204c0727908f8204d737a649eb1167ada0","0xcf9d7f9257697a1ba0fa112af469f4c537c5229530b99a6cbdf42db96117785b","0xd34444016c2d9d1590d673679ab21d93c505da23be6b6007352784aabd746652","0x9db74d1ed75408849287c799e56d27e4c519579d309c38d17419d6aa26a44bec","0xba50b5f8f8d266afc0651c3e658ae4a04672ec25fc72ddd98ba245e2a7a1d863","0x9d32499b7255779b96c6c460e34fddc8502c0c11275e312bef68fd36fff01729","0x4ddb683cc8a9f5ea0ea69b065b43eeea6ba87c242c805135940b01898285ee97","0x1b5c339e1c62aa0b13646461874f0d0375bc82296b7df1ac1c43935b7230dde7","0x352ac26c7d8664fcb73b230adc8d06cc96efab527f3861b64e7a999b35fb52a2","0xcc0b549ae2b47fd10149d157676aeffbf264c6e9f44508a0bf5021ba9dbba7dc","0x7471abc19e4f0a2ea3278e005fb32935a40cbac82aa3f54103a13a548ecee144","0x6d5dd9705ab278fb4074e2aafe5469a6e1e49281b9d5aca98d24e7af8df2d988","0x7a619f26621befeea427c4828cbe77370fc6712bab77a36ac4a9606d1e96a1e2","0xecb307f7fdf2a3c1724387e4a3a0dbdbcc7b9b266377987669240f43b77327a5","0x72c0c757aa746c85c6ac5726ea0f73e8ba7ee0180d0495d0f86b27d9256b5393","0xd4846dba8626ea67b0158a60aaa29e146deaf1597c5ad00d7c431f82cf291a87","0x9f1fbba71181a76ebe5ab99b7aea690f8814188e32fd6b8dc1a68192d12339be","0x183fa064166de66d1171efc3f87891509b34332d662f810487926d3f67c8e383","0x82a4695c079f6891ccafe4aae579624084443f5f41cf8f462ce0769253c80471","0xddf07644c4c6300076fd030578aaff12d053fd4a94df66aadf6693e87d51d7a7"]} From 9c1e21dcb8fec8f27bd97a7f20fd1c6df7b18c83 Mon Sep 17 00:00:00 2001 From: Jon-Becker Date: Fri, 1 Dec 2023 00:18:09 -0500 Subject: [PATCH 7/9] perf(evm): accept addresses with reasonable leading null-bytes --- common/src/ether/evm/core/types.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/common/src/ether/evm/core/types.rs b/common/src/ether/evm/core/types.rs index a69a1868..9060eff9 100644 --- a/common/src/ether/evm/core/types.rs +++ b/common/src/ether/evm/core/types.rs @@ -290,12 +290,10 @@ pub fn byte_size_to_type(byte_size: usize) -> (usize, Vec) { match byte_size { 1 => potential_types.push("bool".to_string()), - 20 => potential_types.push("address".to_string()), + 15..=20 => potential_types.push("address".to_string()), _ => {} } - // TODO: add a check for addresses with null-byte prefixes - // push arbitrary types to the array potential_types.push(format!("uint{}", byte_size * 8)); potential_types.push(format!("bytes{byte_size}")); From 25efebe207ecd99d02383f404e525f8ef1b2e43d Mon Sep 17 00:00:00 2001 From: Jon-Becker Date: Fri, 1 Dec 2023 00:20:01 -0500 Subject: [PATCH 8/9] chore(decode): `&Vec<_>` => `&[_]` --- core/src/decode/mod.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/src/decode/mod.rs b/core/src/decode/mod.rs index 3227e279..d9947a3e 100644 --- a/core/src/decode/mod.rs +++ b/core/src/decode/mod.rs @@ -451,10 +451,7 @@ pub async fn decode(args: DecodeArgs) -> Result, Error> { } // Attempt to decode the given calldata with the given types. -fn try_decode( - inputs: &Vec, - byte_args: &Vec, -) -> Result<(Vec, Vec), Error> { +fn try_decode(inputs: &[ParamType], byte_args: &[u8]) -> Result<(Vec, Vec), Error> { if let Ok(result) = decode_abi(inputs, byte_args) { // convert tokens to params let mut params: Vec = Vec::new(); From 2089c088567c8137856306c5bc38eb435738a510 Mon Sep 17 00:00:00 2001 From: Jon-Becker Date: Fri, 1 Dec 2023 00:28:03 -0500 Subject: [PATCH 9/9] chore(decode): add docstrings --- core/src/decode/core/abi.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/decode/core/abi.rs b/core/src/decode/core/abi.rs index 86ec51de..f6c7da94 100644 --- a/core/src/decode/core/abi.rs +++ b/core/src/decode/core/abi.rs @@ -87,7 +87,7 @@ fn process_and_validate_word( Ok((word, word_offset)) } -// Handle ABI-encoded bytes +/// Handle ABI-encoded bytes fn is_parameter_abi_encoded_bytes( parameter_index: usize, calldata_words: &[&str], @@ -165,7 +165,7 @@ fn is_parameter_abi_encoded_bytes( } } -// Handle ABI-encoded bytes +/// Handle ABI-encoded bytes fn is_parameter_abi_encoded_array( parameter_index: usize, calldata_words: &[&str], @@ -217,6 +217,7 @@ fn is_parameter_abi_encoded_array( Ok(Some(AbiEncoded { ty: type_str, coverages })) } +/// Determine if the given word is an abi-encoded string. fn is_parameter_abi_encoded_string( data_words: &[&str], parameter_index: usize,