Skip to content

docs: add 2 new academic citations to README - #704

Merged
Jon-Becker merged 1 commit into
mainfrom
docs/add-citations-jul-2026
Jul 4, 2026
Merged

docs: add 2 new academic citations to README#704
Jon-Becker merged 1 commit into
mainfrom
docs/add-citations-jul-2026

Conversation

@Jon-Becker

Copy link
Copy Markdown
Owner

What changed? Why?

added 2 new academic citations to the README that reference heimdall-rs:

  • Liao et al. (2025) - "Augmenting smart contract decompiler output through fine-grained dependency analysis and LLM-facilitated semantic recovery" (arXiv:2501.08670, accepted at IEEE TSE). uses heimdall as one of three mainstream decompilers (alongside Gigahorse/Elipmoc and Panoramix) to evaluate their SmartHalo framework for improving decompiled output quality.

  • Wang et al. (2026) - "TxRay: Agentic postmortem of live blockchain attacks" (arXiv:2602.01317). uses heimdall as a bytecode-level fallback for decompilation and disassembly when contract source code is unavailable, as part of their LLM-based agentic postmortem system for DeFi exploit analysis.

Notes to reviewers

readme-only change, 2 new lines inserted in alphabetical order within the existing academic citations section.

How has it been tested?

verified both papers actually cite heimdall-rs by checking the full paper text. confirmed citations are not duplicates of any existing entries in the README.

- Liao et al. (2025), SmartHalo decompiler output augmentation (arXiv:2501.08670)
- Wang et al. (2026), TxRay agentic postmortem system (arXiv:2602.01317)
@Jon-Becker
Jon-Becker merged commit b1fa157 into main Jul 4, 2026
12 of 13 checks passed
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

❌ Eval Report for dafd499

Test Case CFG Decompilation
TransientStorage 100 25
NestedMapping 100 15
NestedMappings 100 32
SimpleStorage 100 92
Mapping 100 95
SimpleLoop 72 20
Events 100 68
WhileLoop 68 15
NestedLoop 60 15
WETH9 95 64
Average 89 44
⚠️ 8 eval(s) scoring <70%

TransientStorage (CFG: 100, Decompilation: 25)

Decompilation

{
  "score": 25,
  "summary": "The decompiler fails to recover the logic of five of the six functions. Only setTempOwner is represented as an actual function with a transient-storage write; incrementCounter, lock, unlock, getCounter, and isLocked are all collapsed into meaningless constant declarations that preserve neither their control flow nor their state changes. Transient storage (TSTORE/TLOAD) appears to have broken the analysis.",
  "differences": [
    "incrementCounter() is lost entirely — the transient counter read, +1 increment, and write-back are not represented; it appears only as an empty 'bytes public constant incrementCounter'.",
    "getCounter() does not read transient storage — its return of the counter is replaced by a hardcoded 'uint256 public constant getCounter = 1', so the actual load/return logic is gone.",
    "lock() and unlock() are lost — the transient stores of true/false to the 'locked' variable are not represented, appearing only as empty 'bytes public constant' declarations.",
    "isLocked() does not read transient storage — its return of 'locked' is replaced by a constant 'bool public constant isLocked = 0xBool(true)', losing the load/return logic.",
    "setTempOwner is mislabeled as 'pure' and includes a spurious 'require(arg0 == address(arg0))' check plus an unexpected read-modify-write mask '(address(arg0) * 0x01) | uint96(transient[0x01])' rather than a plain store of the owner address."
  ]
}

NestedMapping (CFG: 100, Decompilation: 15)

Decompilation

{
  "score": 15,
  "summary": "The decompilation fails to capture the fundamental behavior of the contract. All original functions perform nested-mapping storage writes (setAllowance, setGrid, setDeepNested) or a nested-mapping storage read (getAllowance), but every decompiled function is emitted as 'public pure' with only trivial tautological type-check requires (e.g. require(arg0 == arg0)) and no storage access, no mapping slot computation, no state changes, and no return values. The core logic — computing nested keccak storage slots and performing SSTORE/SLOAD — is entirely absent.",
  "differences": [
    "All storage write operations are missing: setAllowance, setGrid, and setDeepNested each assign to a nested mapping (SSTORE with keccak-based slot derivation), but no decompiled function contains any storage write.",
    "The storage read in getAllowance (return allowances[owner][spender]) is missing; no decompiled function reads storage or returns a value.",
    "Functions are marked 'pure' whereas the setters mutate contract state, misrepresenting mutability/state-change behavior.",
    "Nested mapping slot computation (keccak256 hashing of keys with parent slots) is not represented at all.",
    "The auto-generated public getter logic for the mappings is not captured.",
    "Function bodies contain only tautological checks (arg0 == arg0, arg == address(arg)) that bear no relation to the original arithmetic, control flow, or state operations."
  ]
}

NestedMappings (CFG: 100, Decompilation: 32)

Decompilation

{
  "score": 32,
  "summary": "Selectors for all three functions are correctly identified, but the core nested-mapping logic is almost entirely lost. Both view functions (allowance and the allowances getter) are reduced to empty stubs that perform no storage read and return no value, and approve writes to a flat single-key mapping using only the spender argument while ignoring the msg.sender outer key, so the two-level mapping slot computation is not preserved.",
  "differences": [
    "allowance (0xdd62ed3e) does not read storage or return the nested mapping value; it is an empty stub containing only a trivial require, so the return of allowances[owner][spender] is missing entirely",
    "The allowances public getter (0x55b6ed5c) is likewise reduced to an empty stub that performs no storage read and returns nothing",
    "approve (0x095ea7b3) stores to a flat mapping keyed only by the spender (storage_map_a[arg0] = arg1) and omits the msg.sender outer key, so the nested mapping slot calculation allowances[msg.sender][spender] is not preserved and the value is written to the wrong slot",
    "The two-level nested mapping storage layout (hash of inner key combined with hash of outer key/slot) is not represented anywhere in the decompiled output"
  ]
}

SimpleLoop (CFG: 72, Decompilation: 20)

Decompilation

{
  "score": 20,
  "summary": "The decompilation fails to capture the core behavior of the original function. The original loops `loops` times, incrementing the storage variable `number` each iteration (a state-mutating operation). The decompiled output is incorrectly marked `view`, contains no loop, performs no storage write to `number`, and reduces the function to a series of meaningless tautological/degenerate require statements. The essential logic — the loop and the repeated increment of state — is entirely absent.",
  "differences": [
    "The for-loop iterating `loops` times is completely missing from the decompilation.",
    "The storage write incrementing `number` (number++) on each iteration is not present; no SSTORE / state change is captured.",
    "Function mutability is wrong: original mutates state, decompiled output is marked `view` (non-mutating).",
    "The require statements produced (require(arg0 == arg0), require(!0 < arg0), require(number - 0xff...ff)) are degenerate/tautological and do not correspond to any logic in the original function.",
    "The loop counter comparison `i < loops` is not represented as an actual iterative control flow condition."
  ]
}

Events (CFG: 100, Decompilation: 68)

Decompilation

{
  "score": 68,
  "summary": "Five of seven event-emitting functions are decompiled correctly (emitDeposit, emitWithdrawal, emitLog, emitLogBytes, and emitMultiple all capture their emissions with correct parameters, including the Transfer(address(0), account, amount) inside emitMultiple). However, the two three-argument functions (emitTransfer at 0x23de6651 and emitApproval at 0x5687f2b8) are badly recovered: each detects only a single address argument and, critically, emits no event at all. The Approval event is entirely missing from the output. Since the contract's sole purpose is emitting events, losing two of the emissions is a meaningful functional gap.",
  "differences": [
    "emitTransfer (selector 0x23de6651) does not emit the Transfer event; the decompiled function only contains a type-check require and detects one address argument instead of (address, address, uint256).",
    "emitApproval (selector 0x5687f2b8) does not emit the Approval event; the decompiled function only contains a type-check require and detects one address argument instead of (address, address, uint256).",
    "The Approval event is absent from the decompiled output entirely, so the emitApproval emission has no corresponding representation."
  ]
}

WhileLoop (CFG: 68, Decompilation: 15)

Decompilation

{
  "score": 15,
  "summary": "The decompilation fails to capture the fundamental behavior of the contract. The original increments the storage variable `number` by 1 inside a while loop that runs `loops` times. The decompiled output contains no loop, no storage write, and is incorrectly marked as `view`. The core logic (iterated state mutation) is entirely lost, replaced by three unrelated require statements.",
  "differences": [
    "The while loop control flow is completely absent — there is no iteration construct in the decompiled output",
    "The storage write `number = number + 1` is missing; `number` is never modified, and the function is incorrectly marked `view` (mutability is wrong)",
    "The loop counter `i` and its increment/comparison (`i < loops`, `i = i + 1`) are not represented",
    "The decompiled body consists of require/assertion statements (e.g. require(!number > (number + 1))) that do not correspond to any operation in the original source",
    "No state change occurs in the decompiled function, whereas the original mutates contract storage on every iteration"
  ]
}

CFG

{
  "score": 68,
  "summary": "The CFG captures the function dispatcher, both function entry/exit points, all revert paths, and the while-loop entry, condition, body-entry, and exit. However, the loop's back-edge (iteration path) is missing: the loop body's normal completion (which stores `number`, increments `i`, and jumps back to the condition at 0x77) is not represented — only the compiler's overflow-panic branch out of the body is captured. Main control flow is intact but the loop iteration is not fully closed.",
  "missing_paths": [
    "Loop back-edge: the normal (no-overflow) continuation of the loop body at 0x0193, which stores `number = number + 1`, computes `i = i + 1`, and jumps back to the loop condition at 0x77, is absent. Node 8 (body) only has an edge to node 9 (overflow panic revert); the fall-back-to-condition iteration path is not captured, so the while loop's back-edge is not represented.",
    "The block at 0x0193 (loop body normal continuation containing the i-increment and SSTORE of number) has no node in the CFG."
  ],
  "extra_paths": [
    "Node 1: CALLVALUE non-zero revert (compiler payable guard)",
    "Node 14: CALLDATASIZE < 4 revert (compiler dispatcher fallback)",
    "Nodes 5/6/11: ABI calldata-decode length/bounds validation reverts (compiler-added argument decoding)",
    "Node 9: Panic(0x11) arithmetic-overflow revert on `number + 1` (compiler 0.8.x overflow check)"
  ],
  "observations": [
    "The if/else selector-matching chain in the dispatcher is fully captured: node 3 branches on selector 0x0b7d796e (loop) vs. fall-through, and node 12 branches on 0x8381f58a (number getter) vs. revert.",
    "Both function entry points (loop, number) and their exits (STOP at node 10, RETURN at node 13) are present.",
    "The loop condition check `i < loops` (LT/ISZERO at 0x7a-0x7c) with its two outcomes — enter body (node 8) and exit loop (node 10) — is correctly represented.",
    "The single missing edge is the loop's iteration back-edge, which is the one non-compiler control-flow element absent; it is a meaningful omission because iteration is the core semantics of a while loop, though the loop's entry/condition/exit skeleton is otherwise intact."
  ]
}

NestedLoop (CFG: 60, Decompilation: 15)

Decompilation

{
  "score": 15,
  "summary": "The decompilation fails to capture the fundamental behavior of the contract. The original performs a nested loop that increments the storage variable `number` loops*loops times, but the decompiled output contains no loop structure, no storage writes, and marks the function as `view`. The core logic (iteration and state mutation) is entirely lost.",
  "differences": [
    "No loop/iteration structure is present; the nested for-loops that drive the number of increments are completely absent.",
    "The storage write `number += 1` is not performed; the function is incorrectly marked `view` and does not mutate state, whereas the original mutates storage.",
    "The loop condition comparisons are reduced to meaningless tautological/inverted require statements (e.g. `require(arg0 == arg0)`, `require(!0 < arg0)`) that do not reflect the original control flow.",
    "Function mutability is wrong: original is state-modifying (non-view), decompiled is `view`.",
    "The increment expression `number + 1` appears only inside a nonsensical `require(!number > (number + 0x01))` rather than as an actual assignment back to storage."
  ]
}

CFG

{
  "score": 60,
  "summary": "The CFG captures the function dispatch for both functions, the outer loop's two-way condition branch, the inner loop condition, the loop body, and all revert/require paths. However, the loop iteration structure is incompletely captured: the inner loop's exit branch, both loops' back-edges (increment -> re-test), and the normal (non-overflow) continuation of the body are absent, leaving the nested loops as an acyclic straight-line structure rather than true cycles.",
  "missing_paths": [
    "Inner loop exit branch: node 8 (inner condition `j < loops`) only has an edge to the body (node 9); the edge to the inner-loop exit block (0xb1, which returns to the outer increment) is missing.",
    "Inner loop back-edge / iteration: the normal (non-overflow) path out of the body (node 9, jump to 0x01ac after the `number += 1` add) leads nowhere in the graph; there is no edge back to the inner condition (0x84) for the j++ re-test.",
    "Outer loop back-edge / iteration: there is no edge from the inner-loop exit back through the outer increment (i++) to the outer condition (0x77), so the outer for-loop's iteration cycle is not represented.",
    "Loop increment blocks (i++ and j++) and the inner-loop exit block (0xb1) are not present as nodes."
  ],
  "extra_paths": [
    "Node 10: SafeMath/0.8 overflow panic (0x4e487b71, code 0x11) revert path for `number += 1` — compiler-added, informational only.",
    "Node 0 -> 1: CALLVALUE nonpayable check revert — compiler-added dispatch guard.",
    "Node 12: calldata SLT bounds/type check revert during argument decoding.",
    "Node 15: fallback revert when no selector matches."
  ],
  "observations": [
    "Both public functions are fully represented in the dispatcher: `loop(uint256)` selector 0x0b7d796e (node 3 -> 4) and the auto-generated `number()` getter selector 0x8381f58a (node 13 -> 14 -> RETURN).",
    "Both nested-loop condition tests appear as distinct nodes (node 7 = outer `i < loops`, node 8 = inner `j < loops`) with the characteristic LT + ISZERO + JUMPI pattern, and the loop body incrementing storage `number` is captured in node 9.",
    "The outer loop is captured more completely than the inner: node 7 has both the body-entry edge (->8) and the exit edge (->11 -> STOP), whereas node 8 has only the body-entry edge, so one side of the inner condition's branch is missing.",
    "The overall graph is acyclic — no back-edges exist — so while the loop entry/condition/body/exit decision points are largely present, the iterative nature of the two for-loops (the defining feature of this contract) is not fully reflected in the extracted control flow."
  ]
}

WETH9 (CFG: 95, Decompilation: 64)

Decompilation

{
  "score": 64,
  "summary": "Core value-movement logic of WETH9 is largely preserved: deposit credits balance and emits Deposit, withdraw checks/decrements balance and sends ETH via transfer with a Withdrawal event, transfer/transferFrom perform the balance subtract/add and emit Transfer, and totalSupply returns this.balance. However the allowance/approve nested-mapping handling is mangled and there is an internal storage-slot inconsistency, causing several real functional deviations.",
  "differences": [
    "approve() writes the allowance to a single-keyed mapping (keyed only on the spender arg0) and to the balance map (storage_map_c), instead of the nested allowance[msg.sender][guy]; the msg.sender dimension of the mapping key is lost.",
    "transferFrom() does not correctly implement the allowance decrement: the original conditionally requires allowance[src][msg.sender] >= wad and decrements it when src != msg.sender and allowance != uint(-1). The decompiled version replaces this with a require reading the balance map (storage_map_c) equal to uint(-1) and omits the actual allowance subtraction.",
    "The src != msg.sender / allowance == uint(-1) branch logic is flattened into unconditional require statements plus unreachable duplicated code blocks after early returns, so the conditional structure of the allowance path is not faithfully represented.",
    "Storage map inconsistency: deposit/transfer/withdraw write balances to storage_map_c, but balanceOf reads from storage_map_d and allowance also reads storage_map_d, so getters read a different slot than the writers use, misassigning the balanceOf vs allowance mappings.",
    "allowance() getter returns storage_map_d keyed only on arg1 and ignores arg0, dropping the outer key of the nested allowance mapping."
  ]
}

@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

⚠️ Coverage Report for dafd499

Metric Value
Base branch 66.26%
PR branch 66.15%
Diff -0.11%

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