Skip to content

feat(decompile): loop detection and analysis#674

Open
Jon-Becker wants to merge 14 commits into
mainfrom
jon-becker/loops
Open

feat(decompile): loop detection and analysis#674
Jon-Becker wants to merge 14 commits into
mainfrom
jon-becker/loops

Conversation

@Jon-Becker
Copy link
Copy Markdown
Owner

Motivation

Solution

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Dec 23, 2025

✅ Coverage Report for 592674d

Metric Value
Base branch 66.57%
PR branch 69.54%
Diff +2.97%

// Early filter: skip loop detection for conditions that can't be loops
// (tautological or constant-only comparisons like overflow checks)
let condition_could_be_loop =
jump_condition.as_ref().map_or(true, |cond| {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [clippy] reported by reviewdog 🐶

warning: this `map_or` can be simplified
   --> crates/vm/src/ext/exec/mod.rs:252:29
    |
252 | / ...                   jump_condition.as_ref().map_or(true, |cond| {
253 | | ...                       !is_tautologically_false_condition(cond) &&
254 | | ...                           !is_tautologically_true_condition(cond)
255 | | ...                   });
    | |________________________^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.91.0/index.html#unnecessary_map_or
    = note: `#[warn(clippy::unnecessary_map_or)]` on by default
help: use is_none_or instead
    |
252 -                             jump_condition.as_ref().map_or(true, |cond| {
252 +                             jump_condition.as_ref().is_none_or(|cond| {
    |

}

// check for mutated memory accesses in the jump condition
if jump_condition_contains_mutated_memory_access(&diff, cond) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [clippy] reported by reviewdog 🐶

warning: this `if` statement can be collapsed
   --> crates/vm/src/ext/exec/mod.rs:299:37
    |
299 | / ...                   if jump_condition_contains_mutated_memory_access(&diff, cond) {
300 | | ...                       if stack_diff_shows_iteration(&diff, cond) {
301 | | ...                           detected_loop_info = Some((diff, cond.clone()));
302 | | ...                           break;
303 | | ...                       }
304 | | ...                   }
    | |_______________________^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.91.0/index.html#collapsible_if
    = note: `#[warn(clippy::collapsible_if)]` on by default
help: collapse nested if block
    |
299 ~                                     if jump_condition_contains_mutated_memory_access(&diff, cond)
300 ~                                         && stack_diff_shows_iteration(&diff, cond) {
301 |                                             detected_loop_info = Some((diff, cond.clone()));
302 |                                             break;
303 ~                                         }
    |

}

// check for mutated storage accesses in the jump condition
if jump_condition_contains_mutated_storage_access(&diff, cond) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [clippy] reported by reviewdog 🐶

warning: this `if` statement can be collapsed
   --> crates/vm/src/ext/exec/mod.rs:307:37
    |
307 | / ...                   if jump_condition_contains_mutated_storage_access(&diff, cond) {
308 | | ...                       if stack_diff_shows_iteration(&diff, cond) {
309 | | ...                           detected_loop_info = Some((diff, cond.clone()));
310 | | ...                           break;
311 | | ...                       }
312 | | ...                   }
    | |_______________________^
    |
    = help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.91.0/index.html#collapsible_if
help: collapse nested if block
    |
307 ~                                     if jump_condition_contains_mutated_storage_access(&diff, cond)
308 ~                                         && stack_diff_shows_iteration(&diff, cond) {
309 |                                             detected_loop_info = Some((diff, cond.clone()));
310 |                                             break;
311 ~                                         }
    |

@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Eval Report for 592674d

Test Case CFG Decompilation
WhileLoop 100 100
NestedMappings 100 30
SimpleLoop 100 100
TransientStorage 100 15
Mapping 100 25
NestedLoop 100 100
Events 100 70
SimpleStorage 100 35
NestedMapping 100 35
WETH9 100 45
Average 100 55
⚠️ 6 eval(s) scoring <70%

NestedMappings (CFG: 100, Decompilation: 30)

Decompilation

{
  "score": 30,
  "summary": "Decompilation fails to preserve nested mapping structure, resulting in incorrect storage access patterns that fundamentally change program behavior",
  "differences": [
    "Nested mapping storage access is simplified to single-key mapping lookups, losing the two-dimensional structure required for allowances[owner][spender]",
    "allowance() function ignores the owner (arg0) parameter and only uses spender (arg1) for storage lookup, when it should compute storage slot from both addresses",
    "approve() function ignores msg.sender in storage calculation and only uses spender (arg0), when it should compute storage slot from both msg.sender and spender",
    "The public allowances() getter uses incorrect logic (var_b = arg0; var_b = arg1;) that overwrites the first address with the second, losing the owner parameter entirely"
  ]
}

TransientStorage (CFG: 100, Decompilation: 15)

Decompilation

{
  "score": 15,
  "summary": "Critical decompilation failure - only 1 of 6 functions partially recovered, with incorrect logic. Transient storage operations not properly handled, resulting in missing functions and incorrect constant declarations instead of function implementations.",
  "differences": [
    "incrementCounter function missing - declared as empty constant instead of function that increments transient counter",
    "lock function missing - declared as empty constant instead of function that sets transient locked to true",
    "unlock function missing - declared as empty constant instead of function that sets transient locked to false",
    "getCounter function missing - declared as constant uint256=1 instead of view function returning transient counter value",
    "isLocked function missing - declared as constant bool=true instead of view function returning transient locked state",
    "setTempOwner has incorrect logic with unnecessary bit manipulation and redundant require check - should be simple assignment to transient storage",
    "All transient storage read/write operations failed to decompile correctly"
  ]
}

Mapping (CFG: 100, Decompilation: 25)

Decompilation

{
  "score": 25,
  "summary": "Critical storage mapping errors cause fundamental logic failure. Getter functions read from wrong storage locations, and setter functions use incorrect bit-packing operations instead of simple assignments.",
  "differences": [
    "Public getter functions (balances, owners, registered) read from storage_map_b while setter functions write to storage_map_a, causing complete disconnect between writes and reads",
    "setOwner uses incorrect bit-packing logic (address * 0x01 | uint96(...)) instead of simple address assignment to owners[tokenId]",
    "register uses incorrect bit-packing logic (0x01 * 0x01 | uint248(...)) instead of simple boolean assignment to registered[address]",
    "Storage slot calculations are fundamentally wrong - contract conflates three distinct mappings into two storage maps with incorrect access patterns",
    "getBalance correctly reads from storage_map_a matching setBalance, but the public balances() getter reads from storage_map_b, creating inconsistent state access"
  ]
}

SimpleStorage (CFG: 100, Decompilation: 35)

Decompilation

{
  "score": 35,
  "summary": "Decompilation captures only 1 of 4 functions correctly. Critical failures in storage layout interpretation lead to fundamentally incorrect logic in setOwner, initialize, and reset functions. The decompiler appears to incorrectly pack storage variables and generates nonsensical bitwise operations instead of simple assignments.",
  "differences": [
    "Missing 'initialized' boolean state variable - not declared in decompiled contract",
    "setOwner function uses incorrect bitwise operations (preserving upper bits) instead of clean address assignment",
    "initialize function incorrectly modifies owner variable with bitwise operations instead of setting a separate boolean to true",
    "reset function has fundamentally broken logic: performs 'owner = value | (uint96(owner))' which incorrectly mixes value and owner variables",
    "reset function fails to properly reset owner to address(0), using incorrect bitwise operations instead",
    "reset function does not reset the initialized boolean (missing entirely from decompiled output)"
  ]
}

NestedMapping (CFG: 100, Decompilation: 35)

Decompilation

{
  "score": 35,
  "summary": "Decompilation fails to correctly handle nested mapping storage calculations in setter functions, causing critical functional bugs. While public getter functions appear correct, the corresponding setter functions only use partial keys for storage slot computation, which would cause storage collisions and incorrect state updates.",
  "differences": [
    "setAllowance function only uses the spender address (arg1) for storage calculation, completely omitting the owner address (arg0) from the nested mapping key computation. This breaks the two-level mapping structure.",
    "getAllowance function has the same issue as setAllowance - only considers spender (arg1) and ignores owner (arg0) in storage lookup, causing incorrect reads.",
    "setGrid function only uses the y-coordinate (arg1) for storage calculation, missing the x-coordinate (arg0). Different x values with the same y would overwrite each other instead of occupying separate storage slots.",
    "setDeepNested function only uses the last two keys (arg1 and arg2) in storage calculation, omitting the first key (arg0) from the three-level nested mapping computation."
  ]
}

WETH9 (CFG: 100, Decompilation: 45)

Decompilation

{
  "score": 45,
  "summary": "Decompilation captures basic structure but has critical logic errors in transfer functions, incorrect storage mapping in approve, and malformed control flow with unreachable code.",
  "differences": [
    "transfer() function has severely corrupted logic: includes a nonsensical 'for' loop with unreachable code after early return, and incorrect condition checks that don't match the original transferFrom delegation",
    "transferFrom() has similar corruption with unreachable for-loop code after return statement and malformed conditional logic that doesn't properly represent the allowance check for non-self transfers",
    "approve() function incorrectly writes to storage_map_c instead of storage_map_d (allowance mapping), causing it to modify balances instead of allowances",
    "Both balanceOf() and allowance() incorrectly read from storage_map_d when they should read from different mappings (balanceOf should use storage_map_c based on other function usage)",
    "withdraw() uses incorrect transfer method syntax - should be a low-level call, not .transfer() which returns void, not (bool, bytes)",
    "transfer() and transferFrom() have inverted logic conditions (using '!storage_map_c[var_a] < arg1' instead of 'storage_map_c[var_a] >= arg1'), though these may be functionally equivalent",
    "transferFrom() has incorrect allowance check logic - the condition 'require(address(arg0) == (address(msg.sender)))' followed by uint max check doesn't properly represent the original 'if (src != msg.sender && allowance != uint(-1))' pattern",
    "Multiple redundant and unreachable code blocks after return statements in both transfer functions indicate severe control flow decompilation failure"
  ]
}

@github-actions
Copy link
Copy Markdown
Contributor

Benchmark for 592674d

Click to view benchmark
Test Base PR %
heimdall_cfg/complex 10.3±0.59ms 16.0±1.63ms +55.34%
heimdall_cfg/simple 1148.9±106.72µs 1296.1±96.61µs +12.81%
heimdall_decoder/seaport 43.1±2.70µs 47.1±7.92µs +9.28%
heimdall_decoder/transfer 3.5±0.39µs 3.0±0.25µs -14.29%
heimdall_decoder/uniswap 12.6±1.64µs 12.2±0.91µs -3.17%
heimdall_decompiler/abi_complex 44.9±2.96ms 108.8±5.52ms +142.32%
heimdall_decompiler/abi_simple 1284.6±169.10µs 1714.2±185.31µs +33.44%
heimdall_decompiler/sol_complex 62.1±3.12ms 163.4±10.74ms +163.12%
heimdall_decompiler/sol_simple 1963.3±303.21µs 3.0±0.30ms +52.80%
heimdall_decompiler/yul_complex 53.3±4.42ms 116.3±10.75ms +118.20%
heimdall_decompiler/yul_simple 1913.2±151.32µs 1873.8±130.71µs -2.06%
heimdall_disassembler/complex 1150.1±90.79µs 1142.3±57.44µs -0.68%
heimdall_disassembler/simple 56.5±7.49µs 55.6±4.90µs -1.59%
heimdall_vm/erc20_transfer 242.3±50.78µs 227.3±33.79µs -6.19%
heimdall_vm/fib 742.5±10.22µs 772.0±59.34µs +3.97%
heimdall_vm/ten_thousand_hashes 4.6±1.17s 566.6±25.17ms -87.68%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant