Skip to content

Commit

Permalink
feat: parse CALL/CALLCODE value as ether, if possible
Browse files Browse the repository at this point in the history
  • Loading branch information
Jon-Becker committed Nov 15, 2024
1 parent c3fee86 commit bbab941
Show file tree
Hide file tree
Showing 20 changed files with 101 additions and 85 deletions.
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
3 changes: 2 additions & 1 deletion crates/common/src/ether/calldata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use eyre::{bail, eyre, Result};
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>() {
// if raw is true, the user specified that the target is raw calldata. skip fetching the transaction.
// 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
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
2 changes: 1 addition & 1 deletion crates/common/src/utils/threading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub fn task_pool<
let mut handles = Vec::new();

// Split items into chunks for each thread to process
let chunk_size = (items.len() + num_threads - 1) / num_threads;
let chunk_size = items.len().div_ceil(num_threads);
let chunks = items.chunks(chunk_size);

// Share ownership of f across threads with Arc
Expand Down
6 changes: 3 additions & 3 deletions crates/core/benches/bench_cfg.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use heimdall_cfg::{cfg, CFGArgsBuilder};
use heimdall_cfg::{cfg, CfgArgsBuilder};
use tokio::runtime::Runtime;

fn test_cfg(c: &mut Criterion) {
Expand All @@ -17,10 +17,10 @@ fn test_cfg(c: &mut Criterion) {
group.bench_with_input(BenchmarkId::from_parameter(name), &contract, |b, c| {
b.to_async::<Runtime>(Runtime::new().unwrap()).iter(|| async {
let start = std::time::Instant::now();
let args = CFGArgsBuilder::new()
let args = CfgArgsBuilder::new()
.target(c.to_string())
.build()
.expect("Failed to build CFGArgs");
.expect("Failed to build CfgArgs");
let _ = cfg(args).await;
start.elapsed()
});
Expand Down
10 changes: 5 additions & 5 deletions crates/core/tests/test_cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod integration_tests {
use memory_stats::memory_stats;
use std::path::PathBuf;

use heimdall_cfg::{cfg, CFGArgs, CFGArgsBuilder};
use heimdall_cfg::{cfg, CfgArgs, CfgArgsBuilder};
use petgraph::dot::Dot;
use serde_json::Value;

Expand All @@ -14,7 +14,7 @@ mod integration_tests {
std::process::exit(0);
});

let result = heimdall_cfg::cfg(CFGArgs {
let result = heimdall_cfg::cfg(CfgArgs {
target: String::from("0x1bf797219482a29013d804ad96d1c6f84fba4c45"),
rpc_url,
default: true,
Expand Down Expand Up @@ -43,7 +43,7 @@ mod integration_tests {
std::process::exit(0);
});

let result = heimdall_cfg::cfg(CFGArgs {
let result = heimdall_cfg::cfg(CfgArgs {
target: String::from("0xE90d8Fb7B79C8930B5C8891e61c298b412a6e81a"),
rpc_url,
default: true,
Expand Down Expand Up @@ -110,8 +110,8 @@ mod integration_tests {
let mut fail_count = 0;

for (contract_address, bytecode) in contracts {
println!("Generating CFG for contract {contract_address}");
let args = CFGArgsBuilder::new()
println!("Generating Cfg for contract {contract_address}");
let args = CfgArgsBuilder::new()
.target(bytecode)
.timeout(10000)
.build()
Expand Down
3 changes: 2 additions & 1 deletion crates/decode/src/interfaces/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ pub struct DecodeArgs {
#[clap(long = "skip-resolving")]
pub skip_resolving: bool,

/// Whether to treat the target as a raw calldata string. Useful if the target is exactly 32 bytes.
/// Whether to treat the target as a raw calldata string. Useful if the target is exactly 32
/// bytes.
#[clap(long, short)]
pub raw: bool,
}
Expand Down
8 changes: 4 additions & 4 deletions crates/decompile/src/core/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ impl Analyzer {
}

// check if the ending brackets are needed
if analyzer_state.jumped_conditional.is_some()
&& analyzer_state.conditional_stack.contains(
if analyzer_state.jumped_conditional.is_some() &&
analyzer_state.conditional_stack.contains(
analyzer_state
.jumped_conditional
.as_ref()
Expand All @@ -167,8 +167,8 @@ impl Analyzer {
{
// remove the conditional
for (i, conditional) in analyzer_state.conditional_stack.iter().enumerate() {
if conditional
== analyzer_state.jumped_conditional.as_ref().expect(
if conditional ==
analyzer_state.jumped_conditional.as_ref().expect(
"impossible case: should have short-circuited in previous conditional",
)
{
Expand Down
91 changes: 55 additions & 36 deletions crates/decompile/src/utils/heuristics/extcall.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use alloy::primitives::U256;
use eyre::eyre;
use heimdall_common::utils::{hex::ToLowerHex, sync::blocking_await};
use heimdall_vm::{
core::{opcodes::opcode_name, vm::State},
w_gas, w_push0,
};
use tracing::trace;

use crate::{
core::analyze::AnalyzerState, interfaces::AnalyzedFunction,
Expand All @@ -24,11 +23,27 @@ pub fn extcall_heuristic(
0xf1 | 0xf2 => {
let address = instruction.input_operations[1].solidify();
let memory = function.get_memory_range(instruction.inputs[3], instruction.inputs[4]);

let extcalldata = memory
.iter()
.map(|x| x.value.to_lower_hex().trim_start_matches("0x").to_owned())
.map(|x| x.value.to_lower_hex().to_owned())
.collect::<Vec<String>>()
.join("");
let gas_solidified = instruction.input_operations[0].solidify();
let value_solidified = instruction.input_operations[2].solidify();

// if gas is 2,300, this is a value transfer
if gas_solidified.contains("0x08fc") {
trace!(
"instruction {} ({}) with 2300 gas indicates a value transfer",
instruction.instruction,
opcode_name(instruction.opcode)
);
function
.logic
.push(format!("address({}).transfer({});", address, value_solidified));
return Ok(());
}

let extcalldata_clone = extcalldata.clone();
let decoded = blocking_await(move || {
Expand All @@ -52,10 +67,18 @@ pub fn extcall_heuristic(
// - if value is just the default (0), we don't need to include it
let mut modifiers = vec![];
if instruction.input_operations[0] != w_gas!() {
modifiers.push(format!("gas: {}", instruction.input_operations[0].solidify()));
modifiers.push(format!("gas: {}", gas_solidified));
}
if instruction.input_operations[2] != w_push0!() {
modifiers.push(format!("value: {}", instruction.input_operations[2].solidify()));
// if the value is just a hex string, we can parse it as ether for readability
if let Ok(value) =
u128::from_str_radix(value_solidified.trim_start_matches("0x"), 16)
{
let ether_value = value as f64 / 10_f64.powi(18);
modifiers.push(format!("value: {} ether", ether_value));
} else {
modifiers.push(format!("value: {}", value_solidified));
}
}
let modifier = if modifiers.is_empty() {
"".to_string()
Expand All @@ -68,21 +91,19 @@ pub fn extcall_heuristic(
decode_precompile(instruction.inputs[1], &memory, &instruction.input_operations[5])
{
function.logic.push(precompile_logic);
} else if let Some(decoded) = decoded {
function.logic.push(format!(
"(bool success, bytes memory ret0) = address({}).{}{}(...); // {}",
address,
modifier,
decoded.decoded.name,
opcode_name(instruction.opcode).to_lowercase(),
));
} else {
if let Some(decoded) = decoded {
function.logic.push(format!(
"(bool success, bytes memory ret0) = address({}).{}{}(...); // {}",
address,
modifier,
decoded.decoded.name,
opcode_name(instruction.opcode).to_lowercase(),
));
} else {
function.logic.push(format!(
"(bool success, bytes memory ret0) = address({}).call{}(abi.encode({}));",
address, modifier, extcalldata
));
}
function.logic.push(format!(
"(bool success, bytes memory ret0) = address({}).call{}(abi.encode({}));",
address, modifier, extcalldata
));
}
}

Expand Down Expand Up @@ -127,24 +148,22 @@ pub fn extcall_heuristic(
decode_precompile(instruction.inputs[1], &memory, &instruction.input_operations[4])
{
function.logic.push(precompile_logic);
} else if let Some(decoded) = decoded {
function.logic.push(format!(
"(bool success, bytes memory ret0) = address({}).{}{}(...); // {}",
address,
modifier,
decoded.decoded.name,
opcode_name(instruction.opcode).to_lowercase(),
));
} else {
if let Some(decoded) = decoded {
function.logic.push(format!(
"(bool success, bytes memory ret0) = address({}).{}{}(...); // {}",
address,
modifier,
decoded.decoded.name,
opcode_name(instruction.opcode).to_lowercase(),
));
} else {
function.logic.push(format!(
"(bool success, bytes memory ret0) = address({}).{}{}(abi.encode({}));",
address,
opcode_name(instruction.opcode).to_lowercase(),
modifier,
extcalldata
));
}
function.logic.push(format!(
"(bool success, bytes memory ret0) = address({}).{}{}(abi.encode({}));",
address,
opcode_name(instruction.opcode).to_lowercase(),
modifier,
extcalldata
));
}
}

Expand Down
8 changes: 4 additions & 4 deletions crates/decompile/src/utils/heuristics/solidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,10 @@ pub fn solidity_heuristic(

// perform a series of checks to determine if the condition
// is added by the compiler and can be ignored
if (conditional.contains("msg.data.length") && conditional.contains("0x04"))
|| VARIABLE_SIZE_CHECK_REGEX.is_match(&conditional).unwrap_or(false)
|| (conditional.replace('!', "") == "success")
|| (conditional == "!msg.value")
if (conditional.contains("msg.data.length") && conditional.contains("0x04")) ||
VARIABLE_SIZE_CHECK_REGEX.is_match(&conditional).unwrap_or(false) ||
(conditional.replace('!', "") == "success") ||
(conditional == "!msg.value")
{
return Ok(());
}
Expand Down
4 changes: 2 additions & 2 deletions crates/decompile/src/utils/heuristics/yul.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ pub fn yul_heuristic(
// CALLDATACOPY, CODECOPY, EXTCODECOPY, RETURNDATACOPY, TSTORE,
// SSTORE, RETURN, SELFDESTRUCT, LOG0, LOG1, LOG2, LOG3, LOG4
// we simply want to add the operation to the function's logic
0x37 | 0x39 | 0x3c | 0x3e | 0x55 | 0x5d | 0xf0 | 0xf1 | 0xf2 | 0xf4 | 0xf5 | 0xfa
| 0xff | 0xA0 | 0xA1 | 0xA2 | 0xA3 | 0xA4 => {
0x37 | 0x39 | 0x3c | 0x3e | 0x55 | 0x5d | 0xf0 | 0xf1 | 0xf2 | 0xf4 | 0xf5 | 0xfa |
0xff | 0xA0 | 0xA1 | 0xA2 | 0xA3 | 0xA4 => {
function.logic.push(format!(
"{}({})",
opcode_name(instruction.opcode).to_lowercase(),
Expand Down
Loading

0 comments on commit bbab941

Please sign in to comment.