Skip to content

fix(bifrost): do not create ~/.bash_profile during install - #712

Open
Jon-Becker wants to merge 1 commit into
mainfrom
fix/issue-707-bifrost-profile
Open

fix(bifrost): do not create ~/.bash_profile during install#712
Jon-Becker wants to merge 1 commit into
mainfrom
fix/issue-707-bifrost-profile

Conversation

@Jon-Becker

Copy link
Copy Markdown
Owner

What changed? Why?

Fixes #707.

The bifrost/install script appended the PATH export to both ~/.bashrc and ~/.bash_profile using >>. Because >> creates its target when absent, running the installer created ~/.bash_profile on systems that did not have one. On Debian/Ubuntu/WSL, ~/.profile normally sources ~/.bashrc, but a bash login shell reads ~/.bash_profile first and stops there — so once the installer created that file, the user's ~/.profile/~/.bashrc startup configuration was silently dropped after the next login.

The actual source matched the issue report (the bash case branch unconditionally wrote to both files). The fix loops over ~/.bashrc and ~/.bash_profile and only appends to a profile that already exists, so the installer never creates ~/.bash_profile as a side effect. The existing PATH string, the zsh (~/.zshrc) and fish (config.fish) branches, and the "already on PATH" guard are all unchanged.

Notes to reviewers

  • bifrost/install — replaced the two unconditional >> writes in the */bash) branch with a loop that skips non-existent profiles ([ -f "$SHELL_PROFILE" ] || continue) and quotes the profile path. Only the bash branch changed.
  • bifrost/tests/profile-test — new deterministic regression test (extensionless to match repo shell-script convention and avoid the *.sh entry in .gitignore). It runs the real install script against an isolated $HOME with curl/binary steps stubbed, so only the shell-profile logic executes.

How has it been tested?

  • bash -n bifrost/install and bash -n bifrost/tests/profile-test — syntax valid.
  • bash bifrost/tests/profile-test — 4/4 assertions pass:
    1. absent ~/.bash_profile is not created,
    2. existing ~/.bashrc receives the PATH export,
    3. ~/.bash_profile is still not created when only ~/.bashrc exists,
    4. existing ~/.bash_profile receives the PATH export.
  • Ran the same test against the pre-fix install script to confirm it is meaningful: it fails cases 1 and 3 (reproducing the reported bug) and passes after the fix.
  • Change is shell-only; no Rust sources were touched, so the workspace cargo checks are unaffected.

The installer appended the PATH export to both ~/.bashrc and
~/.bash_profile using >>, which created ~/.bash_profile when it did not
exist. On Debian/Ubuntu/WSL, bash login shells then read the new
~/.bash_profile instead of ~/.profile, silently dropping the user's
startup configuration.

Only append to bash profiles that already exist, matching the reporter's
suggested fix. zsh and fish handling is unchanged.

Adds bifrost/tests/profile-test which drives the real install script
against an isolated HOME with network/binary steps stubbed to prove the
absent ~/.bash_profile case does not create the file and that existing
profiles receive the PATH update.

Fixes #707
@github-actions

Copy link
Copy Markdown
Contributor

❌ Eval Report for fe7e96a

Test Case CFG Decompilation
NestedMappings 65 32
SimpleLoop 55 15
NestedLoop 45 12
TransientStorage 100 12
WhileLoop 62 15
NestedMapping 68 12
SimpleStorage 100 85
Mapping 50 76
WETH9 100 58
Events 62 55
Average 70 37
⚠️ 9 eval(s) scoring <70%

NestedMappings (CFG: 65, Decompilation: 32)

Decompilation

{
  "score": 32,
  "summary": "Only the approve() write is partially recovered, and even there the nested-mapping key derivation is wrong (single flat key from the spender argument, with msg.sender and both keccak256 slot hashes dropped). Both read paths (allowance() and the public allowances getter) are reduced to empty argument-validation stubs with no storage load and no return value, so two of the three externally observable behaviors are absent.",
  "differences": [
    "Nested mapping key computation is lost: original writes allowances[msg.sender][spender] via keccak256(spender . keccak256(msg.sender . slot0)); decompiled writes storage_map_a[address(arg0)] with no msg.sender component and no nested hashing, so the storage location is functionally different.",
    "approve() no longer depends on msg.sender at all, meaning all callers would collide on the same slot.",
    "allowance(address,address) (selector 0xdd62ed3e) lost its entire body: no storage read of the nested mapping and no return of the uint256 value; it is emitted as a no-op that only checks the argument encoding.",
    "The decompiled 0xdd62ed3e function takes only one address argument, while the original signature takes two (owner, spender); the second parameter is dropped.",
    "The public getter allowances(address,address) (selector 0x55b6ed5c) likewise lost its storage read and return value, and also only shows one of two address parameters.",
    "Mutability is misreported in a behaviorally relevant way: the view functions are marked pure (no state access recovered) and approve is marked payable while the original is non-payable (bytecode-level callvalue handling not represented).",
    "Storage is modeled as mapping(bytes32 => bytes32) storage_map_a rather than a two-level mapping, so no read/write pair in the decompilation targets the same slot the original does."
  ]
}

CFG

{
  "score": 65,
  "summary": "The CFG captures the full function dispatcher (all three external selectors: approve 0x095ea7b3, allowances getter 0x55b6ed5c, allowance 0xdd62ed3e), the fallback revert, and the complete body of approve() including the nested-mapping double-keccak slot computation, SSTORE and STOP. However, the two read paths (allowance() and the public allowances getter) are truncated: after calldata decoding, only their revert successors are present, so neither reaches its SLOAD / RETURN exit. The source contains no if/else, loops, or external calls, so no conditional or loop structure is missing, but 2 of 3 function bodies and their return exits are absent.",
  "missing_paths": [
    "allowance(address,address) body: nested mapping lookup (allowances[owner][spender]) SLOAD and the ABI-encode/RETURN exit block reachable from node 16 (0x8f -> 0xa9) are absent; node 16 only has an edge to the revert block 14.",
    "Public getter allowances(address,address) body: the storage read and RETURN exit reachable from node 12 (0x5f -> 0x79) are absent; the successful continuation of node 13 (fallthrough at 0x0226 JUMPDEST) is not present, only the revert edge to node 6.",
    "Successful (non-revert) fallthrough of the second address-cast validation inside the view-function calldata decoder (node 13 -> 0x0226), which is the only route to the read functions' return paths."
  ],
  "extra_paths": [
    "CALLVALUE / ISZERO non-payable guard branch at 0x08 with revert block at 0x0c (node 0 -> 1)",
    "CALLDATASIZE < 4 check at 0x15 with fallback revert (node 2 -> 17)",
    "Calldata slice length check SLT/ISZERO at 0x027a and 0x02b8 with abi-decode revert helper at 0x01df (nodes 10 and 14)",
    "address(uint160) dirty-bits validation branches at 0x021f and 0x0252 with revert blocks (nodes 6 and 8)",
    "Internal compiler dispatch/jump plumbing inlined into nodes 4, 5, 7, 9, 12, 13, 16"
  ],
  "observations": [
    "Node labels concatenate multiple basic blocks across internal JUMPs (e.g. node 9 spans 0x0259, 0x026a, 0x02a4, 0x58, 0xbf and 0x5d), so a single node covers several compiler-level blocks; graph structure was used rather than labels.",
    "The approve() write path is complete and correct: two MSTORE+SHA3 rounds for the nested mapping, then SSTORE, then STOP at 0x5e.",
    "The revert helper block at 0x01df is duplicated into two separate nodes (10 and 14) rather than shared, and the address-check revert at 0x0223 is shared (node 6 has predecessors 5 and 13) - shared-target handling is inconsistent but harmless for completeness.",
    "The dispatcher is a linear if-else chain (nodes 3 -> 11 -> 15 -> 17) and every selector comparison plus the final fallback revert is represented, so the switch-like pattern is fully captured.",
    "Source has no loops, ternaries, external calls, or require/assert statements, so all reverts in the CFG are compiler-generated and do not count against completeness."
  ]
}

SimpleLoop (CFG: 55, Decompilation: 15)

Decompilation

{
  "score": 15,
  "summary": "The decompilation fails to capture the fundamental behavior of the contract. The only function of substance, loop(uint256), is a bounded loop that increments a storage variable; the decompiled output contains no loop, no storage write, and instead emits tautological/nonsensical require statements. The function is also incorrectly typed as `view`, which contradicts the state mutation it performs. Only the public storage variable `number` (and its implicit getter) is correctly recovered.",
  "differences": [
    "The for-loop (`for (uint256 i = 0; i < loops; i++)`) is entirely absent — no iteration construct or loop counter appears in the decompiled output, so the repeated execution semantics are lost.",
    "The state write `number++` (SSTORE to slot 0) is missing; the decompiled function only reads `number` in a require expression and never modifies storage.",
    "The function is annotated `public view`, but the original mutates state — mutability behavior is incorrectly represented.",
    "`require(arg0 == arg0)` is a tautology with no counterpart in the original and does not correspond to any real check.",
    "`require(!0 < arg0)` misrepresents the loop entry condition `0 < loops`: the original skips the body when loops == 0 rather than reverting, so this converts a branch-not-taken into a revert condition that does not exist in the source.",
    "`require(number - 0xffff...ff)` is a garbled rendering of the loop's overflow/increment arithmetic and introduces a revert condition on the value of `number` that the original does not have."
  ]
}

CFG

{
  "score": 55,
  "summary": "The CFG correctly captures the dispatcher, both function entries (loop(uint256) and number()), the calldata/selector reject paths, and the for-loop's entry, condition test (i < loops) and exit. However the loop is rendered as an acyclic graph: there is no back edge from the body to the condition check at 0x77, and the block containing the storage write (SSTORE for number++) and the i++ increment is absent entirely. Node 8 terminates at the increment helper's overflow check with only the panic successor emitted, so the entire non-reverting continuation of the loop body is missing. Since the loop is essentially the whole logic of this contract, that is a significant structural omission even though every other source path is present.",
  "missing_paths": [
    "Loop back edge: after the body executes, control returns to the condition check at 0x77 (for (...; i < loops; i++)). The CFG has no edge back into node 7, so the loop appears to execute at most once.",
    "Loop body completion: the block performing the actual storage write for `number++` (SSTORE) is not present — node 8 stops after SLOAD and the increment helper call.",
    "Loop increment: the `i++` update block (and its own increment/overflow helper return) is not represented.",
    "Non-reverting successor of the JUMPI at 0x0195 (fallthrough to 0x019e) is missing; only the panic branch (node 9) is emitted, cutting off the normal-execution continuation of the loop."
  ],
  "extra_paths": [
    "Node 0 -> 1: compiler-generated non-payable CALLVALUE check and revert.",
    "Node 2 -> 14: CALLDATASIZE < 4 fallback revert (no fallback/receive in source).",
    "Node 12 -> 14: selector-miss fallback revert.",
    "Node 4 -> 11 and node 5 -> 6: ABI calldata decoding validation reverts for the uint256 argument.",
    "Node 8 -> 9: Panic(0x11) arithmetic overflow check inserted by Solidity 0.8.x for `number++` — not a source-level branch."
  ],
  "observations": [
    "Both public entry points are captured: loop(uint256) selector 0x0b7d796e (node 3 -> 4) and number() selector 0x8381f58a (node 12 -> 13), including the getter's RETURN at 0x71 and loop()'s STOP at 0x53.",
    "The if-else chain of the selector dispatcher is fully expanded — each EQ/JUMPI has both taken and fallthrough successors modeled.",
    "The loop exit path is correct: node 7 (condition, LT/ISZERO/JUMPI) -> node 10, which cleans the stack and falls into the function epilogue STOP.",
    "Inlined internal helpers (calldata decoding at 0xce/0xe2, the 0xaf identity/cast helper, and the increment helper at 0x0162) are flattened into their callers' basic blocks rather than represented as distinct call/return subgraphs, which is why the return-to-loop control flow is lost.",
    "Because the JUMP-based returns from inlined helpers are not resolved back to their call sites, the graph is a DAG with no cycles at all — a static analysis over this CFG would not identify any loop in the contract."
  ]
}

NestedLoop (CFG: 45, Decompilation: 12)

Decompilation

{
  "score": 12,
  "summary": "The decompilation fails to capture the contract's fundamental behavior. The public `number` state variable and the function selector/signature are recovered correctly, but the entire nested-loop control flow and the storage write are gone. The output is a flat sequence of degenerate `require` statements with no iteration and no SSTORE, and the function is mislabeled `view` even though the original mutates state. A caller of the decompiled function would observe no state change at all.",
  "differences": [
    "Both `for` loops are absent: no loop structure, no induction variables (i, j), and no back-edges/JUMPI loop condition are represented. The comparisons `i < loops` and `j < loops` appear only as two straight-line `require(!0 < arg0)` statements evaluated once against the constant 0.",
    "The storage write `number += 1` is lost. There is no SSTORE/assignment to `number`; the increment appears only inside a nonsensical `require(!number > (number + 0x01))` predicate, so the decompiled function has no state effect.",
    "Mutability is wrong: the function is annotated `public view`, whereas the original is state-mutating. This also implies the total number of increments (loops * loops) is not represented anywhere.",
    "Spurious revert conditions are introduced (`require(arg0 == arg0)`, `require(!0 < arg0)`, `require(!number > number + 1)`) that do not correspond to any revert path in the original, which never reverts for any input.",
    "Loop-trip semantics are lost: for `loops == 0` the original is a no-op, and for `loops > 0` it performs loops*loops increments; the decompiled code cannot distinguish these cases and instead derives reverts from the loop guards."
  ]
}

CFG

{
  "score": 45,
  "summary": "The CFG captures function dispatch, both public entry points, the function exit, revert paths, and the condition tests of both nested loops, but it is entirely acyclic: neither loop has a back-edge, the inner loop has no exit path, the i++/j++ increment blocks are absent, and the loop body's state write (SSTORE for `number += 1`) never appears. For a contract whose sole logic is a nested for-loop, the loop structure is only half represented.",
  "missing_paths": [
    "Inner loop exit path: the taken branch of the inner condition test at 0x89 (JUMPI to 0xb1) is missing — node 8 has only one successor (the body), so `j < loops` failing has nowhere to go",
    "Inner loop back-edge: no block for `j++` and no edge returning to the inner condition header at 0x84, so the inner loop is not a cycle",
    "Outer loop back-edge: no block for `i++` and no edge returning to the outer condition header at 0x77, so the outer loop is not a cycle",
    "The non-reverting continuation of the loop body: the fall-through of the addition-overflow check at 0x01a3 (to 0x01ac) is absent, so the successful `number += 1` path — including the SSTORE that actually writes storage — is entirely missing from the graph",
    "Because the increment/back-edge blocks are missing, the loop body is reachable exactly once in the graph; the repeated-execution semantics of the source are not represented"
  ],
  "extra_paths": [
    "Non-payable guard: CALLVALUE/ISZERO check at 0x07 branching to the revert at 0x0c (compiler-added)",
    "Calldata size < 4 check at 0x14 branching to the fallback revert at 0x34 (compiler dispatch)",
    "ABI decoder bounds check: SLT/ISZERO at 0x0106 branching to the revert helper at 0xc8 (compiler-added)",
    "uint256 argument cleanup/validation check at 0xe0 (EQ) branching to the revert at 0xe5 (compiler-added)",
    "Panic(0x11) arithmetic-overflow revert block at 0x0152 reached from the addition check at 0x019e (compiler-added checked-arithmetic path)",
    "Selector-comparison chain fall-through for `number()` at 0x2a and the shared fallback revert at 0x34 (compiler dispatch)"
  ],
  "observations": [
    "The graph is a DAG — 16 nodes, 16 edges, zero cycles. No natural loop can be recovered from it, which is the central defect for this contract.",
    "Both public functions are reached correctly: 0x0b7d796e -> loop(uint256) (node 4) and 0x8381f58a -> number() getter (node 14), and node 14 terminates in RETURN while node 11 terminates in STOP, so both function exits are captured.",
    "The outer loop's exit path (0x77 -> 0xbf, node 7 -> node 11) IS present and correctly chains to the function's STOP, so the CFG models the zero-iteration case for the outer loop only.",
    "Basic blocks are heavily inlined across JUMPs (e.g. node 9 splices the SLOAD, two casting helpers at 0xcc, and the overflow check into one label), which makes internal helper returns invisible but does not by itself lose source-level paths.",
    "Blocks 0xb1, 0x01ac, and the loop-increment stubs referenced by JUMPI/PUSH targets inside existing nodes have no corresponding node in the graph — the missing edges are to jump targets the CFG builder evidently failed to enqueue, suggesting the traversal stopped at the first back-jump."
  ]
}

TransientStorage (CFG: 100, Decompilation: 12)

Decompilation

{
  "score": 12,
  "summary": "The decompilation fails to capture the contract's fundamental behavior. Five of the six functions (incrementCounter, lock, unlock, getCounter, isLocked) are emitted as bogus compile-time constants with no transient-storage reads or writes, so the increment arithmetic, the boolean lock/unlock writes, and both getters are entirely lost. Only setTempOwner survives as a real function with a transient store, and even that is mangled with a spurious argument-equality require and an incorrect bit-masking expression. The transient (TLOAD/TSTORE) semantics of the state variables are effectively not modeled at all.",
  "differences": [
    "incrementCounter(): the read-modify-write `counter = counter + 1` (TLOAD slot 0, add 1, TSTORE slot 0) is completely absent — the function is rendered as an empty `bytes public constant incrementCounter`, performing no state change.",
    "lock(): the write of `true` to the transient `locked` variable is missing — emitted as an empty `bytes public constant lock` with no store operation.",
    "unlock(): the write of `false` to the transient `locked` variable is missing — emitted as an empty `bytes public constant unlock` with no store operation.",
    "getCounter(): instead of loading and returning the transient counter, it is emitted as `uint256 public constant getCounter = 1`, i.e. a hardcoded constant 1 rather than a load of the current counter value.",
    "isLocked(): instead of loading and returning the transient `locked` flag, it is emitted as `bool public constant isLocked = true`, a hardcoded value that ignores the actual stored state.",
    "setTempOwner(): adds a nonexistent guard `require(arg0 == address(arg0))` that has no counterpart in the source.",
    "setTempOwner(): the store expression `transient[0x01] = (address(arg0) * 0x01) | uint96(transient[0x01])` misrepresents the write — the original overwrites the whole slot with the address, whereas the decompiled form ORs in low-order bits of the existing slot value and applies a nonsensical multiply-by-one/uint96 mask.",
    "The functions are declared as constants/`pure` rather than state-mutating or `view`, so mutability behavior is not equivalent (incrementCounter, lock, unlock all mutate transient state; getCounter/isLocked read it).",
    "No transient-storage load operations (TLOAD) appear anywhere in the output, so slot assignment for counter, tempOwner, and locked cannot be correlated with the original state variables (tempOwner's slot 0x01 is the only one represented)."
  ]
}

WhileLoop (CFG: 62, Decompilation: 15)

Decompilation

{
  "score": 15,
  "summary": "The decompilation fails to capture the contract's fundamental behavior. The while loop is entirely absent: there is no iteration construct, no loop counter, and no storage write to `number`. The function is even annotated `view`, which is the opposite of the original's state-mutating behavior. Only fragments of the loop's internals survive as nonsensical require statements (`!0 < arg0` echoing the `i < loops` condition, `number + 0x01` echoing the increment), and those fragments invert the semantics by reverting when the loop body would execute rather than executing it. The public `number` state variable/getter is the one thing preserved.",
  "differences": [
    "The `while (i < loops)` loop is completely missing — no loop or backward control flow of any kind is emitted, so the repeated execution that is the entire point of the function is lost",
    "The storage write `number = number + 1` is missing; `number` is never assigned, so the contract's only state change is absent",
    "The loop counter `i` and its increment `i = i + 1` are absent, so there is no termination/progress logic",
    "The function is declared `public view`, incorrectly implying no state mutation, whereas the original is state-mutating (non-view, non-payable)",
    "The loop condition is turned into a revert guard: `require(!0 < arg0)` means the function reverts whenever `loops > 0`, i.e. exactly the inputs for which the original does work. The original never reverts for any input.",
    "`require(!number > (number + 0x01))` is a spurious revert condition with no counterpart in the original; the original has no require/revert statements at all",
    "`require(arg0 == arg0)` is a meaningless tautology with no counterpart in the source (likely a mis-lifted calldata/argument check)"
  ]
}

CFG

{
  "score": 62,
  "summary": "The CFG captures both function entry points, the full selector dispatch chain, all revert paths, and the while-loop condition with both its body-entry and loop-exit successors. However, the loop body is truncated: the successful (non-overflow) continuation of `number = number + 1` at 0x0193 has no node, so the SSTORE, the `i = i + 1` increment, and the back edge from the body to the loop header at 0x77 are all absent. The resulting graph is a DAG with no cycle, so the loop is not actually represented as a loop.",
  "missing_paths": [
    "Back edge from the while-loop body to the loop condition header (0x77) - the CFG is acyclic, so the iterative nature of `while (i < loops)` is lost",
    "Successful (non-overflow) branch of the addition check at 0x018a -> 0x0193; the entire remainder of the loop body after the arithmetic check is unreachable in the CFG",
    "The `number = number + 1` state write (SSTORE) block, which lives in the missing 0x0193 successor",
    "The `i = i + 1` increment statement and its own arithmetic/overflow check block"
  ],
  "extra_paths": [
    "Node 0: CALLVALUE non-payable check -> revert at 0x0c (compiler-added)",
    "Node 2: CALLDATASIZE < 4 -> fallback revert at 0x34 (compiler dispatch)",
    "Node 3/12: selector equality comparisons and fall-through to the no-match revert (compiler dispatch)",
    "Node 4 -> 11: calldata SLT/ISZERO ABI-decode sanity check -> revert at 0xaf (compiler-added)",
    "Node 5 -> 6: uint256 cleanup/validation check -> revert at 0xcc (compiler-added)",
    "Node 8 -> 9: Panic(0x11) arithmetic overflow revert for `number + 1` (compiler-added checked-arithmetic path)"
  ],
  "observations": [
    "Both public entry points are present and correct: `loop(uint256)` (0x0b7d796e) reaching node 4 and `number()` (0x8381f58a) reaching node 13 with its SLOAD/RETURN path.",
    "The loop condition block (node 7: DUP2 DUP2 LT ISZERO JUMPI) correctly forks to the body (node 8) and the exit/STOP path (node 10), so loop entry, guard, and exit are modeled.",
    "The only terminator reachable from node 8 is the Panic revert; there is no non-reverting way out of the loop body, which is a structural inconsistency the recovery should have caught.",
    "Internal helper routines (ABI decode at 0xe6/0xd2, uint256 cleanup at 0xb3, checked add at 0x0166, ABI encode at 0x0120) are inlined into the calling basic blocks rather than shared, which is acceptable for completeness scoring but inflates block size.",
    "The source contains no if/else chains, ternaries, external calls, or try/catch, so the only substantive control-flow construct to recover is the while loop."
  ]
}

NestedMapping (CFG: 68, Decompilation: 12)

Decompilation

{
  "score": 12,
  "summary": "The decompiler recovered the correct number of external selectors (7, matching the 4 explicit functions plus 3 auto-generated public mapping getters) and the argument types/arity of each, but captured essentially none of the actual behavior. Every function is emitted as `pure` with only tautological `require(x == x)` / `require(arg == address(arg))` calldata-masking checks. All nested-mapping keccak256 slot derivations, every SSTORE in setAllowance/setGrid/setDeepNested, every SLOAD in the getters, and the return value of getAllowance are entirely absent. The output conveys no information about the contract's state model or logic.",
  "differences": [
    "setAllowance: the storage write `allowances[owner][spender] = amount` is missing entirely — no keccak256 slot derivation and no SSTORE appear; the function is even marked `pure`.",
    "setGrid: the storage write `grid[x][y] = value` is missing; additionally the third parameter (`bool value`) is not recovered at all (decompiled signature takes only one uint256 arg).",
    "setDeepNested: the three-level `deepNested[a][b][c] = value` storage write is missing; only 2 of the 4 parameters are recovered (address, uint256 — the trailing address and uint256 value are dropped).",
    "getAllowance: the SLOAD of the nested mapping and the `return` of the uint256 value are missing; the function returns nothing and is marked `pure` instead of `view`.",
    "Public getters for `allowances`, `grid`, and `deepNested`: all storage reads and return values are missing; each is reduced to argument masking only, and the two-key getters (allowances, grid) are shown with a single argument.",
    "No state variables or storage slots are declared or referenced anywhere in the output, so the mapping layout (slots 0, 1, 2 with nested keccak256 hashing) is unrecoverable from the decompilation.",
    "Mutability is wrong across the board: all seven functions are labeled `pure` although four write storage and three read it."
  ]
}

CFG

{
  "score": 68,
  "summary": "The CFG fully reconstructs the function dispatcher: all 7 externally reachable entry points (setAllowance, setGrid, setDeepNested, getAllowance, and the three auto-generated mapping getters allowances/grid/deepNested) appear as distinct selector-comparison branches, and the fallback revert plus the non-payable value check are present. However, the graph stops inside the ABI calldata decoders — no node reaches a function body (no SLOAD/SSTORE/KECCAK256 for the nested mapping slot derivation) and there is no RETURN or STOP terminator anywhere, so none of the 7 functions has a captured exit path. Successful-decode continuation edges are also inconsistently traced (e.g. node 6 only records the revert branch of its JUMPI while node 16 records both), which is why the graph dead-ends. The source contains no if/else, loops, ternaries, external calls, or explicit require/revert, so the only source-level control flow at stake is entry/exit per function; entries are complete, exits are entirely absent.",
  "missing_paths": [
    "Function body and exit (SSTORE + implicit return) for setAllowance(address,address,uint256) — dispatch reaches the decoder but never the storage write or a STOP/RETURN",
    "Function body and exit for setGrid(uint256,uint256,bool)",
    "Function body and exit for setDeepNested(address,uint256,address,uint256)",
    "Function body and exit (SLOAD + RETURN of the ABI-encoded uint256) for getAllowance(address,address)",
    "Function body and exit for the public getter allowances(address,address)",
    "Function body and exit for the public getter grid(uint256,uint256)",
    "Function body and exit for the public getter deepNested(address,uint256,address)",
    "Success (non-revert) continuation edge out of several decoder blocks — e.g. node 6 (JUMPI at 0x041a) and node 11 (JUMPI at 0x04b3) only record the reverting branch, so no path leads past argument decoding"
  ],
  "extra_paths": [
    "CALLVALUE ISZERO check at 0x05-0x0b with revert at 0x0c (compiler non-payable guard) — informational",
    "CALLDATASIZE < 4 check at 0x11-0x18 falling through to the fallback revert at 0x7b — informational",
    "Binary-search selector split on `selector > 0x55b6ed5c` (node 3 -> node 23) — optimizer-generated dispatch structure",
    "Calldata-size sanity checks (`SLT` on `DUP4 DUP6 SUB`) at 0x043d, 0x05c2, 0x0613, 0x0676, 0x04d7, 0x0526 with reverts to the shared 0x03d7 revert stub — compiler ABI-decoder guards",
    "address clean-word validation (`PUSH20 0xff..ff AND` then `EQ`, revert at 0x041b / 0x04b4) — compiler ABI type checks",
    "bool clean-word validation via 0x0473 (ISZERO-style truncation path) — compiler ABI type check"
  ],
  "observations": [
    "All 7 selector equality comparisons are present and each has both taken and fall-through edges, so the dispatch chain (a switch-like pattern) is captured completely — this is the bulk of the source's real branching.",
    "Every terminal node in the graph is a REVERT (0x0e, 0x7e, 0x041d, 0x04b6, 0x03da); there is no RETURN or STOP node, indicating the traversal terminated at the ABI-decoder stage rather than following the internal JUMPs back into the dispatcher's body (e.g. 0x99, 0xc9, 0xf9, 0x0129, 0x0159, 0x0175, 0x0191 are pushed as return targets but never expanded into nodes).",
    "Shared helper blocks are inlined into multiple nodes rather than deduplicated (the 0x03d7 revert stub, 0x0421/0x040b address decoder, and 0x04ba/0x04a4 bool decoder each appear in several node labels), so the graph is a partially unrolled trace rather than a canonical basic-block CFG.",
    "Nodes 7 and 12 are correctly shared as convergence points across multiple decoder paths, showing some cross-path node reuse does occur.",
    "The nested-mapping slot computation (repeated KECCAK256 over key || slot) — the defining logic of this contract — appears nowhere in the CFG, though it is straight-line code and therefore not a branch omission per se; it is the absence of the containing blocks entirely that costs points via missing exits.",
    "Because the source has no loops, conditionals, ternaries, try/catch, or external calls, no branch-shaped source construct is misrepresented; the deduction is solely for missing function-exit coverage."
  ]
}

Mapping (CFG: 50, Decompilation: 76)

CFG

{
  "score": 50,
  "summary": "The CFG fully reconstructs the function-selector dispatch chain (all 7 external entry points from the source, including the three auto-generated public mapping getters) and the fallback/non-payable revert paths. However, only one of the seven functions (setOwner) is traced through to its body and exit. For the other six, the graph stops inside the ABI decoder and follows only the revert branch; the decode-success continuation (0x0422 JUMPDEST) and every corresponding function body, SSTORE/SLOAD, and RETURN/STOP terminator are absent. So while the source has no conditionals or loops, 6 of the 7 logical entry->body->exit paths are truncated.",
  "missing_paths": [
    "setBalance(address,uint256): decoder success path from 0x04db JUMPI to 0x04e7, the body writing balances[account] = amount (SSTORE into mapping slot 0), and its STOP exit",
    "register(address): decoder success path from 0x0419 JUMPI to 0x0422, the body writing registered[account] = true (SSTORE into mapping slot 2), and its STOP exit",
    "getBalance(address): decoder success path, the SLOAD of balances[account], the ABI-encode return block, and its RETURN exit",
    "balances(address) public getter: body and RETURN exit (dispatch entry present at node 24, body missing)",
    "owners(uint256) public getter: decoder success path from 0x036c JUMPI, body and RETURN exit (dispatch entry present at node 22, body missing)",
    "registered(address) public getter: body and RETURN exit (dispatch entry present at node 26, body missing)",
    "Shared decoder continuation block 0x0422 JUMPDEST is never emitted, so five callers (nodes 13, 19, 25, 27 and the 0x040d decoder generally) have only their failure successor",
    "No RETURN-terminated node exists anywhere in the graph, so no view-function exit is represented"
  ],
  "extra_paths": [
    "Non-payable CALLVALUE/ISZERO check at 0x05-0x0b with its revert (node 1)",
    "CALLDATASIZE < 4 check at 0x11-0x18 routing to the fallback revert (node 28)",
    "Compiler ABI-decoder SLT calldata-length sanity checks at 0x0469, 0x0415, 0x04da, 0x0368 and their shared revert stub 0x0329",
    "address type-validation paths (PUSH20 mask + EQ check at 0x03ee) and uint256 cast validation (EQ check at 0x0341) with their reverts (nodes 7, 9)",
    "Duplicated fallback revert block 0x7b appearing as both node 20 and node 28",
    "Selector range split (GT against 0x7fd39247 at node 3) used by the compiler to binary-search the dispatch table"
  ],
  "observations": [
    "Switch-like dispatch is complete: all 7 selectors (0x025e7c27, 0x27e235e3, 0x4420e486, 0x7fd39247, 0xb2dd5c07, 0xe30443bc, 0xf8b2cb4f) each have an EQ test with both taken and not-taken successors, and the chain terminates correctly in the fallback revert.",
    "setOwner is the only fully traced function: nodes 5 -> 6 -> 8 -> 10 cover both decoder params, the mapping SHA3 slot computation, the SSTORE with address masking, and the STOP at 0x0116.",
    "The truncation pattern is consistent: wherever a decoder JUMPI is reached a second time (shared blocks 0x040d, 0x04d1, 0x0360), only the fall-through revert successor is expanded, suggesting the traversal does not re-expand already-visited jump targets for their success branch.",
    "Node 14 is reused as the successor of four distinct decoder sites (13, 19, 25, 27), which is structurally correct for the shared revert stub but leaves those four paths with no non-error exit.",
    "The source contains no if/else, ternary, loop, external call, or require/assert, so no user-written branch is missing - the deficiency is entirely missing function bodies and exit points."
  ]
}

WETH9 (CFG: 100, Decompilation: 58)

Decompilation

{
  "score": 58,
  "summary": "Balance-side accounting is recovered well: deposit, withdraw, totalSupply, and the balance debit/credit plus all four event emissions in transfer/transferFrom match the original. The allowance subsystem is substantially wrong, however — approve writes to the balance map with a single (non-nested) key, the allowance and balanceOf getters both read the same map (which is not the map the mutating functions write), and the conditional allowance check/decrement in transferFrom is inverted and misattributed to the balance map. Control flow is also emitted as several linearized, mutually contradictory paths after unconditional returns, one of which double-subtracts the balance, and the payable fallback that forwards to deposit() is absent entirely.",
  "differences": [
    "approve() writes `storage_map_c[guy] = wad` — the balance mapping with a single key — instead of the nested allowance mapping `allowance[msg.sender][guy]`; the two-level slot derivation (keccak of guy over keccak of msg.sender) is lost, so the approval is recorded in the wrong storage location.",
    "allowance(src, guy) returns `storage_map_d[guy]`, dropping the first key entirely: the nested lookup collapses to a single-key read, so the returned value does not depend on the owner argument.",
    "balanceOf(arg0) reads `storage_map_d[arg0]`, but deposit/withdraw/transfer/transferFrom all write `storage_map_c[...]`. The getter and the mutators are attributed to different mappings, so balances written are not the balances read back.",
    "transferFrom's allowance branch is inverted: the original executes the allowance check only when `src != msg.sender && allowance[src][msg.sender] != uint(-1)`, whereas the decompiled output emits `require(arg0 == msg.sender)` followed by `require(storage_map_c[msg.sender] == 0xff..ff)` — turning an escape condition into a mandatory precondition that would revert every delegated transfer.",
    "The allowance decrement `allowance[src][msg.sender] -= wad` is rendered as `storage_map_c[msg.sender] = storage_map_c[msg.sender] - arg2` — a subtraction from the spender's balance rather than from the nested allowance entry, and the `require(allowance >= wad)` guard preceding it is not represented against the allowance value.",
    "transfer() emits `require(msg.sender == msg.sender)` (tautology) and then `require(storage_map_c[msg.sender] == 0xff..ff)` on the primary path; since the original reaches transfer via transferFrom with src == msg.sender, that allowance path is unreachable in the source, so the decompilation adds a spurious always-reverting precondition to the main path.",
    "transfer() and transferFrom() each contain multiple code paths after unconditional `return 0x01` statements, and the second path subtracts `arg1`/`arg2` from the balance twice (`storage_map_c[msg.sender] -= arg` followed by another `-= arg`), which does not correspond to any behavior in the original single-decrement logic.",
    "The payable fallback function (`function() public payable { deposit(); }`) is not present in the decompiled output, so plain ETH sends to the contract have no recovered behavior.",
    "withdraw() renders the ETH send as `(bool success, bytes memory ret0) = address(msg.sender).transfer(arg0)` without any check on `success`; the original relies on `transfer`'s implicit revert-on-failure, which is only implied by the method name here rather than expressed as a revert condition."
  ]
}

Events (CFG: 62, Decompilation: 55)

Decompilation

{
  "score": 55,
  "summary": "All 7 selectors are recovered and 5 of 7 functions faithfully reproduce their event emissions (Deposit, Withdrawal, Log, LogBytes, and the three-event emitMultiple including the exact string literal \"Multiple events emitted\" and the address(0) Transfer). However, the two three-argument functions emitTransfer (0x23de6651) and emitApproval (0x5687f2b8) are decompiled as single-argument bodies containing only a calldata sanity check and no LOG at all, so their entire observable behavior is lost, and the Approval event is absent from the contract altogether.",
  "differences": [
    "emitTransfer (selector 0x23de6651) is decompiled as a no-op that only checks its first argument; the `emit Transfer(from, to, value)` LOG3 is completely missing, along with the `to` and `value` parameters.",
    "emitApproval (selector 0x5687f2b8) is decompiled as a no-op that only checks its first argument; the `emit Approval(owner, spender, value)` LOG3 is completely missing, along with the `spender` and `value` parameters.",
    "The Approval event is never declared or emitted anywhere in the decompiled output, so one of the contract's six log types is entirely unrepresented.",
    "The dynamic-data events (emit Log / emit LogBytes in emitLog and emitLogBytes) are rendered with raw memory offset/length expressions referencing an undefined `var_b` instead of the calldata-copied payload, so the source of the logged bytes is not recoverable from the output.",
    "All emitting functions are annotated `pure` even though they write logs, so state-mutability behavior is misrepresented (a genuinely pure function cannot emit).",
    "Event declarations omit `indexed` on the address/account parameters, losing the topic-vs-data layout of the emitted logs."
  ]
}

CFG

{
  "score": 62,
  "summary": "The dispatcher is fully captured: all 7 external functions from the source have their selector comparison and entry edge present (0x23de6651, 0x28ba84ca, 0x2e3c2a4d, 0x5687f2b8, 0xd0062fbb, 0xe3a379c5, 0xfc4ae4ba), and the fallback/non-payable revert paths exist. However, only 2 of the 7 functions (the string/bytes emitters) are traced through to their event-emission body and STOP terminator. For the other 5 functions the CFG stops inside ABI decoding: the successful (JUMPI-taken) branch out of the decode validity checks is absent, so those decode blocks lead only to revert nodes. The result is that 5 of 7 function bodies and normal exit points are missing, even though the source is entirely branch-free (no if/else, loops, ternaries, external calls, or try/catch).",
  "missing_paths": [
    "emitTransfer: no path from the decoder to the Transfer LOG3 emit or to a STOP exit — node for 0x04b6 decode reaches only the address dirty-bits revert (0x0469) and the calldata-bounds revert (0x0421); the taken branch 0x0468 JUMPI -> 0x046c is not in the graph",
    "emitApproval: same truncation — dispatch at 0x7f/0xd3 reaches the shared 3-argument decoder but never the Approval LOG3 block or STOP",
    "emitDeposit: 2-argument decode block (0x0506) has only the revert successor at 0x0514; the success branch 0x0513 JUMPI -> 0x051c and the Deposit LOG2 emit + STOP are absent",
    "emitWithdrawal: same as emitDeposit — entry and decode present, Withdrawal LOG2 emit body and normal exit missing",
    "emitMultiple: entry present, but none of its three sequential emits (Deposit LOG2, Transfer LOG3, Log LOG1) nor its STOP exit appear; the straight-line sequence of three emissions is entirely absent",
    "Normal (non-revert) function exit points for 5 of 7 functions — only two STOP terminators exist in the whole CFG"
  ],
  "extra_paths": [
    "Non-payable guard at 0x05 CALLVALUE/ISZERO -> revert at 0x0c (compiler-added)",
    "calldatasize < 4 check at 0x11 -> 0x7b revert (dispatcher prologue)",
    "Unmatched-selector fallback revert at 0x7b and the 0x55 -> 0x7b jump (compiler dispatch)",
    "Selector binary-search split at 0x1f (PUSH4 0x5687f2b8 GT) — optimizer-generated dispatch tree",
    "Calldata-region SLT bounds checks at 0x04bf, 0x050e, 0x05ad, 0x064d -> reverts at 0x0421",
    "uint64 offset/length sanity checks at 0x066a and 0x05ca -> reverts at 0x0425",
    "Dynamic-array 0x1f SLT checks at 0x05f8/0x0558 -> reverts at 0x0544",
    "Copy-length overflow checks at 0x0630/0x0590 -> reverts at 0x054c and length checks at 0x0614/0x0574 -> reverts at 0x0548",
    "address dirty-high-bits masking check at 0x0464 EQ -> revert at 0x0469"
  ],
  "observations": [
    "Switch-like selector dispatch is complete: all 7 source functions are distinguished, including the optimizer's GT-based binary split, so no function is unreachable in the graph.",
    "The truncation pattern is systematic rather than random: every JUMPI whose taken target is 0x046c or 0x051c is missing that successor, while other conditional blocks (0x066f, 0x0619, 0x0635, 0x05cf, 0x0579, 0x0595) correctly have both successors. This looks like the explorer never queued those two JUMPDESTs, orphaning the 2-argument and 3-argument emit bodies.",
    "Only the two dynamic-argument functions (emitLog(string) and emitLogBytes(bytes)) are traced end to end, each reaching a LOG1 with topics 0x532fd6ea... and 0xcf34ef53... and terminating in STOP at 0x010a and 0xd2.",
    "The source contains no conditionals, loops, ternaries, external calls, require/assert, or try/catch, so no branch-shaped source construct is missing — the deficiency is unexplored reachable code, which still removes the normal-completion path for 5 functions.",
    "Shared decoder blocks are correctly modeled as convergent nodes (e.g. 0x0506 reached from three dispatch sites, 0x04b6 from two), which is faithful to the compiler's internal function reuse.",
    "44 basic blocks and 46 edges; 13 distinct revert terminators versus 2 STOP terminators, an imbalance that reflects the missing success paths."
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

✅ Coverage Report for fe7e96a

Metric Value
Base branch 66.26%
PR branch 66.27%
Diff +0.01%

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark for fe7e96a

Click to view benchmark
Test Base PR %
heimdall_cfg/complex 13.1±0.10ms 13.1±0.40ms 0.00%
heimdall_cfg/simple 1356.2±9.25µs 1359.1±12.17µs +0.21%
heimdall_decoder/seaport 56.1±2.38µs 57.2±2.67µs +1.96%
heimdall_decoder/transfer 3.9±0.29µs 3.8±0.33µs -2.56%
heimdall_decoder/uniswap 16.0±1.53µs 15.6±1.24µs -2.50%
heimdall_decompiler/abi_complex 54.6±0.48ms 54.1±0.79ms -0.92%
heimdall_decompiler/abi_simple 1440.4±22.08µs 1449.2±20.00µs +0.61%
heimdall_decompiler/sol_complex 77.2±1.38ms 77.3±1.27ms +0.13%
heimdall_decompiler/sol_simple 2.2±0.02ms 2.2±0.01ms 0.00%
heimdall_decompiler/yul_complex 58.9±0.69ms 59.1±1.08ms +0.34%
heimdall_decompiler/yul_simple 1582.5±21.65µs 1590.8±11.95µs +0.52%
heimdall_disassembler/complex 1279.8±35.25µs 1283.6±40.93µs +0.30%
heimdall_disassembler/simple 63.0±6.08µs 63.7±6.11µs +1.11%
heimdall_vm/erc20_transfer 260.4±9.69µs 259.8±8.76µs -0.23%
heimdall_vm/fib 865.2±12.90µs 854.2±38.01µs -1.27%
heimdall_vm/ten_thousand_hashes 758.2±60.75ms 835.6±676.40ms +10.21%

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.

bifrost install script creates ~/.bash_profile on Debian/Ubuntu, silently breaking the login-shell startup chain

1 participant