Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: all build build-release check clean fmt format lint lint-fix test test-doc test-heavy test-cov test-cov-json bench install help
.PHONY: all build build-release check clean fmt format lint lint-fix test test-doc test-heavy test-cov test-cov-json bench install help eval

# Clippy flags used across the project
CLIPPY_ALLOW := --allow clippy::new_without_default \
Expand Down Expand Up @@ -74,3 +74,8 @@ bench:

install:
cargo install --path crates/cli --locked

eval:
# note: needs heimdall-eval cloned locally next to heimdall-rs
@ cd ../heimdall-eval && make eval-all DEV=1 > /dev/null 2>&1
@ cat ../heimdall-eval/heimdall/evals.json
2 changes: 1 addition & 1 deletion crates/cfg/src/core/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use heimdall_vm::{
core::opcodes::{opcode_name, JUMPDEST},
ext::exec::VMTrace,
};
use petgraph::{matrix_graph::NodeIndex, Graph};
use petgraph::{graph::NodeIndex, Graph};
use std::collections::HashSet;

/// convert a symbolic execution [`VMTrace`] into a [`Graph`] of blocks, illustrating the
Expand Down
66 changes: 61 additions & 5 deletions crates/decompile/src/core/analyze.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
use std::{fmt::Display, time::Instant};

use futures::future::BoxFuture;
use heimdall_vm::ext::exec::VMTrace;
use heimdall_vm::ext::exec::{LoopInfo, VMTrace};
use tracing::debug;

use crate::{
interfaces::AnalyzedFunction,
utils::heuristics::{
argument_heuristic, event_heuristic, extcall_heuristic, modifier_heuristic,
solidity_heuristic, yul_heuristic, Heuristic,
argument_heuristic, event_heuristic, extcall_heuristic, is_loop_overhead,
is_overflow_panic, loop_heuristic, modifier_heuristic, solidity_heuristic, yul_heuristic,
Heuristic, LoopAnalyzerState,
},
Error,
};
Expand Down Expand Up @@ -59,6 +60,10 @@ pub(crate) struct AnalyzerState {
pub analyzer_type: AnalyzerType,
/// Whether to skip resolving internal calls
pub skip_resolving: bool,
/// Loop-related state for tracking active loops
pub loop_state: LoopAnalyzerState,
/// Flag to skip JUMPI that's a loop condition
pub skip_next_jumpi: bool,
}

/// The analyzer, which will analyze a [`VMTrace`] generated by symbolic execution and build an
Expand Down Expand Up @@ -126,10 +131,12 @@ impl Analyzer {
conditional_stack: Vec::new(),
analyzer_type: self.typ,
skip_resolving: self.skip_resolving,
loop_state: LoopAnalyzerState::default(),
skip_next_jumpi: false,
};

// Perform analysis
self.analyze_inner(&trace_root, &mut analyzer_state).await?;
self.analyze_inner(&trace_root, &mut analyzer_state, &[]).await?;

debug!(
"analysis for '{}' completed in {:?}",
Expand All @@ -145,22 +152,71 @@ impl Analyzer {
&'a mut self,
branch: &'a VMTrace,
analyzer_state: &'a mut AnalyzerState,
parent_loops: &'a [LoopInfo],
) -> BoxFuture<'a, Result<(), Error>> {
Box::pin(async move {
// reset jumped conditional, we dont propagate conditionals across branches
analyzer_state.jumped_conditional = None;

// Combine parent loops with this branch's loops (avoiding duplicates)
// Pre-allocate with capacity for both parent and potential new loops
let mut all_loops: Vec<LoopInfo> =
Vec::with_capacity(parent_loops.len() + branch.detected_loops.len());
all_loops.extend_from_slice(parent_loops);

for loop_info in &branch.detected_loops {
let key = (loop_info.header_pc, loop_info.condition_pc);
if !all_loops.iter().any(|l| (l.header_pc, l.condition_pc) == key) {
all_loops.push(loop_info.clone());
}
}

// Debug: Log detected loops for this branch
if !all_loops.is_empty() {
debug!("branch has {} detected loops", all_loops.len());
for loop_info in &all_loops {
debug!(
" loop: header_pc={}, condition_pc={}, condition='{}'",
loop_info.header_pc, loop_info.condition_pc, loop_info.condition
);
}
}

// for each operation in the current trace branch, peform analysis with registerred
// heuristics
for operation in &branch.operations {
// Skip overflow panic paths from Solidity 0.8+
if is_overflow_panic(operation) {
continue;
}

// Skip loop overhead operations (induction variable updates, etc.)
if is_loop_overhead(operation, &analyzer_state.loop_state.active_loops) {
continue;
}

// Run loop heuristic FIRST if there are detected loops
if !all_loops.is_empty() {
loop_heuristic(&mut self.function, operation, analyzer_state, &all_loops)
.await?;
}

// Check if we should skip this JUMPI (it's a loop condition)
if analyzer_state.skip_next_jumpi && operation.last_instruction.opcode == 0x57 {
analyzer_state.skip_next_jumpi = false;
continue;
}

// Run other heuristics
for heuristic in &self.heuristics {
heuristic.run(&mut self.function, operation, analyzer_state).await?;
}
}

// recurse into the children of the current trace branch
// Pass all_loops to children so they have access to parent loop context
for child in &branch.children {
self.analyze_inner(child, analyzer_state).await?;
self.analyze_inner(child, analyzer_state, &all_loops).await?;
}

// check if the ending brackets are needed
Expand Down
11 changes: 9 additions & 2 deletions crates/decompile/src/core/postprocess.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ use crate::{
constants::STORAGE_ACCESS_REGEX,
postprocessors::{
arithmetic_postprocessor, bitwise_mask_postprocessor, eliminate_dead_variables,
memory_postprocessor, remove_empty_lines, storage_postprocessor,
transient_postprocessor, variable_postprocessor, Pass,
loop_postprocessor, memory_postprocessor, remove_empty_lines, remove_overflow_checks,
storage_postprocessor, transient_postprocessor, variable_postprocessor, Pass,
},
},
Error,
Expand Down Expand Up @@ -69,13 +69,17 @@ impl PostprocessOrchestrator {
match self.typ {
AnalyzerType::Solidity => {
// Line-level postprocessors that run on each line
// Run overflow check removal early to clean up spurious requires
self.passes.push(Pass::line_level(vec![
remove_overflow_checks,
bitwise_mask_postprocessor,
arithmetic_postprocessor,
memory_postprocessor,
storage_postprocessor,
transient_postprocessor,
variable_postprocessor,
// Run loop postprocessor last to rename loop variables
loop_postprocessor,
]));

// Function-level passes that run on the entire function
Expand Down Expand Up @@ -181,6 +185,9 @@ impl PostprocessOrchestrator {
pass.run(function, &mut state)?;
}

// Remove empty lines that were cleared by postprocessors
function.logic.retain(|line| !line.trim().is_empty());

// wherever storage_map contains a value that doesnt exist in storage_type_map, add it with
// a default value
state.storage_map.iter().for_each(|(_, v)| {
Expand Down
Loading
Loading