Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(decompile): resolve external calls, simplify internal logic around external calls #507

Merged
merged 18 commits into from
Dec 7, 2024
Merged
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- run: cargo fmt --check --all
- run: cargo +nightly fmt --check --all

check:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 7 additions & 10 deletions crates/cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,7 @@ pub fn delete_cache(key: &str) -> Result<(), Error> {
#[allow(deprecated)]
pub fn read_cache<T>(key: &str) -> Result<Option<T>, Error>
where
T: 'static + DeserializeOwned,
{
T: 'static + DeserializeOwned, {
let home = home_dir().ok_or(Error::Generic(
"failed to get home directory. does your os support `std::env::home_dir()`?".to_string(),
))?;
Expand All @@ -239,8 +238,8 @@ where
.map_err(|e| Error::Generic(format!("failed to deserialize cache object: {:?}", e)))?;

// check if the cache has expired, if so, delete it and return None
if cache.expiry
< std::time::SystemTime::now()
if cache.expiry <
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| Error::Generic(format!("failed to get current time: {:?}", e)))?
.as_secs()
Expand All @@ -267,8 +266,7 @@ where
#[allow(deprecated)]
pub fn store_cache<T>(key: &str, value: T, expiry: Option<u64>) -> Result<(), Error>
where
T: Serialize,
{
T: Serialize, {
let home = home_dir().ok_or(Error::Generic(
"failed to get home directory. does your os support `std::env::home_dir()`?".to_string(),
))?;
Expand All @@ -280,8 +278,8 @@ where
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_err(|e| Error::Generic(format!("failed to get current time: {:?}", e)))?
.as_secs()
+ 60 * 60 * 24 * 90,
.as_secs() +
60 * 60 * 24 * 90,
);

let cache = Cache { value, expiry };
Expand All @@ -306,8 +304,7 @@ pub async fn with_cache<T, F, Fut>(key: &str, func: F) -> eyre::Result<T>
where
T: 'static + Serialize + DeserializeOwned + Send + Sync,
F: FnOnce() -> Fut + Send,
Fut: std::future::Future<Output = Result<T, eyre::Report>> + Send,
{
Fut: std::future::Future<Output = Result<T, eyre::Report>> + Send, {
// Try to read from cache
match read_cache::<T>(key) {
Ok(Some(cached_value)) => {
Expand Down
4 changes: 2 additions & 2 deletions crates/cfg/src/core/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ pub fn build_cfg(
.first()
.ok_or_eyre("failed to get first operation")?
.last_instruction
.opcode
== JUMPDEST,
.opcode ==
JUMPDEST,
)?;
}

Expand Down
10 changes: 5 additions & 5 deletions crates/cfg/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ use heimdall_vm::core::vm::VM;
use petgraph::{dot::Dot, Graph};
use std::time::{Duration, Instant};

use super::CFGArgs;
use super::CfgArgs;

use crate::{core::graph::build_cfg, error::Error};
use tracing::{debug, info};

#[derive(Debug, Clone)]
pub struct CFGResult {
pub struct CfgResult {
pub graph: Graph<String, String>,
}

impl CFGResult {
impl CfgResult {
pub fn as_dot(&self, color_edges: bool) -> String {
let output = format!("{}", Dot::with_config(&self.graph, &[]));

Expand All @@ -44,7 +44,7 @@ impl CFGResult {
}
}

pub async fn cfg(args: CFGArgs) -> Result<CFGResult, Error> {
pub async fn cfg(args: CfgArgs) -> Result<CfgResult, Error> {
// init
let start_time = Instant::now();

Expand Down Expand Up @@ -99,5 +99,5 @@ pub async fn cfg(args: CFGArgs) -> Result<CFGResult, Error> {
debug!("cfg generated in {:?}", start_time.elapsed());
info!("generated cfg successfully");

Ok(CFGResult { graph: contract_cfg })
Ok(CfgResult { graph: contract_cfg })
}
8 changes: 4 additions & 4 deletions crates/cfg/src/interfaces/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use heimdall_config::parse_url_arg;
after_help = "For more information, read the wiki: https://jbecker.dev/r/heimdall-rs/wiki",
override_usage = "heimdall cfg <TARGET> [OPTIONS]"
)]
pub struct CFGArgs {
/// The target to generate a CFG for, either a file, bytecode, contract address, or ENS name.
pub struct CfgArgs {
/// The target to generate a Cfg for, either a file, bytecode, contract address, or ENS name.
#[clap(required = true)]
pub target: String,

Expand Down Expand Up @@ -42,13 +42,13 @@ pub struct CFGArgs {
pub timeout: u64,
}

impl CFGArgs {
impl CfgArgs {
pub async fn get_bytecode(&self) -> Result<Vec<u8>> {
get_bytecode_from_target(&self.target, &self.rpc_url).await
}
}

impl CFGArgsBuilder {
impl CfgArgsBuilder {
pub fn new() -> Self {
Self {
target: Some(String::new()),
Expand Down
2 changes: 1 addition & 1 deletion crates/cfg/src/interfaces/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
mod args;

// re-export the public interface
pub use args::{CFGArgs, CFGArgsBuilder};
pub use args::{CfgArgs, CfgArgsBuilder};
4 changes: 2 additions & 2 deletions crates/cfg/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ mod core;
mod interfaces;

// re-export the public interface
pub use core::{cfg, CFGResult};
pub use core::{cfg, CfgResult};
pub use error::Error;
pub use interfaces::{CFGArgs, CFGArgsBuilder};
pub use interfaces::{CfgArgs, CfgArgsBuilder};
4 changes: 2 additions & 2 deletions crates/cli/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use clap::{ArgAction, Args, ValueEnum};
use heimdall_cache::CacheArgs;
use heimdall_config::ConfigArgs;
use heimdall_core::{
heimdall_cfg::CFGArgs, heimdall_decoder::DecodeArgs, heimdall_decompiler::DecompilerArgs,
heimdall_cfg::CfgArgs, heimdall_decoder::DecodeArgs, heimdall_decompiler::DecompilerArgs,
heimdall_disassembler::DisassemblerArgs, heimdall_dump::DumpArgs,
heimdall_inspect::InspectArgs,
};
Expand Down Expand Up @@ -42,7 +42,7 @@ pub enum Subcommands {
Decompile(DecompilerArgs),

#[clap(name = "cfg", about = "Generate a visual control flow graph for EVM bytecode")]
CFG(CFGArgs),
Cfg(CfgArgs),

#[clap(name = "decode", about = "Decode calldata into readable types")]
Decode(DecodeArgs),
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ async fn main() -> Result<()> {
result.display()
}

Subcommands::CFG(mut cmd) => {
Subcommands::Cfg(mut cmd) => {
// if the user has not specified a rpc url, use the default
if cmd.rpc_url.as_str() == "" {
cmd.rpc_url = configuration.rpc_url;
Expand Down
43 changes: 35 additions & 8 deletions crates/common/src/ether/calldata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ use alloy::primitives::TxHash;
use eyre::{bail, eyre, Result};

/// Given a target, return calldata of the target.
pub async fn get_calldata_from_target(target: &str, rpc_url: &str) -> Result<Vec<u8>> {
pub async fn get_calldata_from_target(target: &str, raw: bool, rpc_url: &str) -> Result<Vec<u8>> {
// If the target is a transaction hash, fetch the calldata from the RPC provider.
if let Ok(address) = target.parse::<TxHash>() {
return get_transaction(address, rpc_url)
.await
.map(|tx| tx.input.to_vec())
.map_err(|_| eyre!("failed to fetch transaction from RPC provider"));
// if raw is true, the user specified that the target is raw calldata. skip fetching the
// transaction.
if !raw {
return get_transaction(address, rpc_url)
.await
.map(|tx| tx.input.to_vec())
.map_err(|_| eyre!("failed to fetch transaction from RPC provider"));
}
}

// If the target is not a transaction hash, it could be calldata.
Expand All @@ -34,6 +38,7 @@ mod tests {

let calldata = get_calldata_from_target(
"0x317907eeece00619fd4418c18a4ec4ebe5c87cdbff808f4b01cc2c6384799837",
false,
&rpc_url,
)
.await
Expand All @@ -51,6 +56,7 @@ mod tests {

let calldata = get_calldata_from_target(
"0xf14fcbc8bf9eac48d61719f80efb268ef1099a248fa332ed639041337954647ec6583f2e",
false,
&rpc_url,
)
.await
Expand All @@ -66,10 +72,31 @@ mod tests {
std::process::exit(0);
});

let calldata =
get_calldata_from_target("asfnsdalkfasdlfnlasdkfnalkdsfndaskljfnasldkjfnasf", &rpc_url)
.await;
let calldata = get_calldata_from_target(
"asfnsdalkfasdlfnlasdkfnalkdsfndaskljfnasldkjfnasf",
false,
&rpc_url,
)
.await;

assert!(calldata.is_err());
}

#[tokio::test]
async fn test_get_calldata_when_target_is_calldata_that_is_exactly_32_bytes() {
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| {
println!("RPC_URL not set, skipping test");
std::process::exit(0);
});

let calldata = get_calldata_from_target(
"0x317907eeece00619fd4418c18a4ec4ebe5c87cdbff808f4b01cc2c6384799837",
true,
&rpc_url,
)
.await
.expect("failed to get calldata from target");

assert!(calldata.len() == 32);
}
}
10 changes: 5 additions & 5 deletions crates/common/src/ether/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,18 +339,18 @@ pub fn score_signature(signature: &str, num_words: Option<usize>) -> u32 {

// prioritize signatures with less numbers
score -= (signature.split('(').next().unwrap_or("").matches(|c: char| c.is_numeric()).count()
as u32)
* 3;
as u32) *
3;

// prioritize signatures with parameters
let num_params = signature.matches(',').count() + 1;
score += num_params as u32 * 10;

// count the number of parameters in the signature, if enabled
if let Some(num_words) = num_words {
let num_dyn_params = signature.matches("bytes").count()
+ signature.matches("string").count()
+ signature.matches('[').count();
let num_dyn_params = signature.matches("bytes").count() +
signature.matches("string").count() +
signature.matches('[').count();
let num_static_params = num_params - num_dyn_params;

// reduce the score if the signature has less static parameters than there are words in the
Expand Down
28 changes: 14 additions & 14 deletions crates/common/src/ether/tokenize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,17 +130,17 @@ pub fn tokenize(s: &str) -> Token {
let mut op = ch.to_string();
iter.next();
if let Some(&next_ch) = iter.peek() {
if (ch == '=' && (next_ch == '=' || next_ch == '>'))
|| (ch == '&' && next_ch == '&')
|| (ch == '|' && next_ch == '|')
|| (ch == '<' && next_ch == '=')
|| (ch == '>' && next_ch == '=')
|| (ch == '!' && next_ch == '=')
|| (ch == '+' && next_ch == '+')
|| (ch == '-' && next_ch == '-')
|| (ch == '*' && next_ch == '*')
|| (ch == '>' && next_ch == '>')
|| (ch == '<' && next_ch == '<')
if (ch == '=' && (next_ch == '=' || next_ch == '>')) ||
(ch == '&' && next_ch == '&') ||
(ch == '|' && next_ch == '|') ||
(ch == '<' && next_ch == '=') ||
(ch == '>' && next_ch == '=') ||
(ch == '!' && next_ch == '=') ||
(ch == '+' && next_ch == '+') ||
(ch == '-' && next_ch == '-') ||
(ch == '*' && next_ch == '*') ||
(ch == '>' && next_ch == '>') ||
(ch == '<' && next_ch == '<')
{
op.push(next_ch);
iter.next();
Expand Down Expand Up @@ -188,9 +188,9 @@ fn parse_literal(iter: &mut std::iter::Peekable<std::str::Chars>) -> String {
}

// literal validation
if literal.starts_with("0x")
&& literal.len() > 2
&& literal[2..].chars().all(|c| c.is_ascii_hexdigit())
if literal.starts_with("0x") &&
literal.len() > 2 &&
literal[2..].chars().all(|c| c.is_ascii_hexdigit())
{
return literal;
}
Expand Down
10 changes: 2 additions & 8 deletions crates/common/src/utils/hex.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::strings::encode_hex;
use alloy::primitives::{Address, Bytes, FixedBytes, I256, U256};
use alloy::primitives::{Address, Bytes, FixedBytes, U256};

/// A convenience function which encodes a given EVM type into a sized, lowercase hex string.
pub trait ToLowerHex {
Expand All @@ -20,13 +20,7 @@ impl ToLowerHex for bytes::Bytes {

impl ToLowerHex for U256 {
fn to_lower_hex(&self) -> String {
format!("{:#032x}", self)
}
}

impl ToLowerHex for I256 {
fn to_lower_hex(&self) -> String {
format!("{:#032x}", self)
encode_hex(&self.to_be_bytes_vec())
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/common/src/utils/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,8 @@ pub fn tokenize(s: &str) -> Vec<String> {
// Check if current character and last character form a compound operator (like "==",
// ">=", "&&", "||")
if let Some(last) = last_char {
if compound_operator_first_chars.contains(&last)
&& (c == '=' || c == '&' || c == '|')
if compound_operator_first_chars.contains(&last) &&
(c == '=' || c == '&' || c == '|')
{
// Remove the last character as a single token
tokens.pop();
Expand Down
7 changes: 5 additions & 2 deletions crates/common/src/utils/sync.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::{future::Future, pin::Pin};

/// Take in a non-async function and await it. This functions should be blocking.
pub fn blocking_await<F, T>(f: F) -> T
where
F: FnOnce() -> T,
{
F: FnOnce() -> T, {
tokio::task::block_in_place(f)
}

pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
Loading
Loading