Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

ilold is an execution path analyzer and interactive security workbench for smart contracts. It maps every possible path through a protocol (every branch, function combination, and state mutation) and lets users navigate them visually, branch by branch, with an LLM reasoning over each path.

The tool loads a project, builds an in-memory model, and drops the auditor into a REPL backed by a live canvas. Each command answers a question about the protocol: list entry points, add a call to a session, inspect state changes, trace execution flow, slice data dependencies, record findings, export a report. The canvas reflects the same state in a visual graph that the user navigates by clicking, expanding, and forking.

ilold supports two backends:

  • Solidity: static analysis on top of solar-compiler. Contracts are parsed into a typed model; per-function CFGs and path trees drive info, trace, slice, timeline, and sequence narratives.
  • Solana: concrete execution on top of LiteSVM. Programs run inside an in-process VM, accounts are decoded from the program IDL, and timelines are reconstructed from per-step account diffs.

The REPL surface is the same shell for both backends, with backend-specific commands documented in their respective sections.

Core concept

An audit is a conversation with the code. ilold models that conversation as a session: the auditor adds entry-point calls one at a time, and the tool tracks how state accumulates across the sequence. Every analysis command operates either on a single entry point or on the accumulated session state.

Sessions can be branched into named scenarios. A scenario is an independent timeline with its own state; on Solana each scenario also owns its own VM and user keypairs.

Key differentiator

ilold does not detect vulnerabilities automatically. It gives the auditor primitives to drive the analysis: build a sequence, inspect state changes, trace execution flow, slice dependencies, and record findings. The auditor leads, the tool answers questions grounded in the actual structure of the code (Solidity) or the actual runtime behaviour (Solana).

Where to start

Getting Started

Installation

Clone the repository and build from source:

git clone https://github.com/scab24/ilold.git
cd ilold
cargo build --release

The binary is at target/release/ilold. The four subcommands are analyze, context, serve, and explore (see crates/ilold-cli/src/main.rs).

Backend detection

serve and explore auto-detect the backend from the path:

  • A directory or file containing .sol sources is treated as a Solidity project.
  • A directory containing Anchor.toml (with idls/<program>.json) is treated as a Solana project.

analyze and context are Solidity-only.

Running against a Solidity project

cargo run -- explore tests/fixtures/staking.sol

ilold parses the files, builds the model and per-function CFGs, and drops the auditor into the REPL:

  ╭──────────────────────────────────────────╮
  │ ilold explore — Staking                  │
  │ 8 functions | Type ? for help            │
  │ Web UI: http://localhost:52431           │
  ╰──────────────────────────────────────────╯

ilold[Staking]>

The port defaults to 0 (auto-assigned) for explore and 8080 for serve. Override with --port.

Running against a Solana project

cargo run -- explore tests/fixtures/solana/staking

ilold loads the IDL under idls/<program>.json, boots a LiteSVM with the program binary from target/deploy/<program>.so (or bin/<program>.so), and opens the REPL:

ilold[staking]>

Without a compiled .so the REPL still starts and IDL navigation (f, i, pda, vars) works, but commands that drive the VM (call, state, inspect) fail until the program is built.

First session

A typical first exploration of the Solidity staking contract:

ilold[Staking]> f

  [P] deposit         writes state, external calls
  [P] withdraw        writes state, external calls
  [P] claimRewards    writes state, external calls
  [R] setRewardRate   writes state
  [R] pause           writes state
  [R] unpause         writes state
  [P] rewardPerToken  view
  [P] earned          view
ilold[Staking]> c deposit

  + Step 0: deposit [P] external
    State writes:
      · balances[msg.sender]
      · lastUpdateTime
      · rewardPerTokenStored
      · rewards[account]
      · totalStaked
      · userRewardPerTokenPaid[account]
    Sequence: deposit
ilold[Staking → deposit]> s

  ════════════════════════════════════════════[ STATE ]═════════════════════════════════════════════
  balances[msg.sender]
    += amount (step 0:15, deposit)
  lastUpdateTime
    = block.timestamp (step 0:8, deposit)
  rewardPerTokenStored
    = rewardPerToken() (step 0:7, deposit)
  rewards[account]
    = earned(account) (step 0:11, deposit)
  totalStaked
    += amount (step 0:16, deposit)
  userRewardPerTokenPaid[account]
    = rewardPerTokenStored (step 0:12, deposit)

The full audit flow (who, tr, sl, tl, scenarios, findings, export) is covered in Solidity: Audit walkthrough. For the Solana equivalent (users new, call <ix>, state, step, timeline <pubkey>) see Solana: Audit walkthrough.

Inline help

Type ? at the prompt for the full command reference. Append ? to any command for its usage:

ilold[Staking]> sl?
  slice <func> <var> [--backward]  Dataflow slice. Example: sl deposit totalStaked --backward

On Solana, appending ? renders the structured help block for the command, including syntax, flags, examples, return shape, and related commands.

What ilold does

ilold is a smart-contract audit explorer. It loads a project, builds an internal model, and exposes that model through an interactive REPL. The auditor adds entry-point calls to a session, the tool tracks accumulated effects, and analysis commands answer questions about the code without requiring a separate run.

Two backends, one shell

BackendInputExecution modelWhat you get
Solidity.sol sources (file or directory)Symbolic (parser, CFG, path tree, slicer)Function narratives, execution trees with modifier inlining, backward/forward dataflow slices, cross-step timelines
SolanaProject root with Anchor.toml, idls/<program>.json, target/deploy/<program>.soConcrete (in-process execution via LiteSVM)Per-call CU and logs, account diffs, decoded timelines, scenario-isolated VMs, time-warp on the Clock sysvar

The REPL command surface is the same shell. Backend-specific commands are documented in Solana: Solana runtime and Solana: Scenarios.

Sessions and scenarios

A session is the active scenario inside the active project. Adding a step means calling an entry point and recording its effects. A scenario is a named branch of the session timeline; scenarios can be created from scratch (scenario new) or forked from an existing one at a step boundary (scenario fork). On Solana, each scenario owns its own VM and user keypairs, so forks produce independent state.

What the tool does not do

ilold has no built-in vulnerability detectors. There is no checklist that fires “this is a reentrancy” or “this is a missing access control” automatically. The auditor uses who, info, trace, slice, timeline, state, step to investigate, and records findings via finding and note. See Roadmap for the Phase 2 detector engine and AST extractor.

Architecture

ilold is split into a handful of crates with clear responsibilities. The diagram below shows the pipeline for each backend; both meet at the shared web layer (ilold-web) and the REPL frontend (ilold-cli).

Solidity pipeline

.sol files
  │
  ▼
ilold-core::parse::solar_frontend   (SolarParser)
  │
  ▼
ilold-core::model                   (Project, ContractDef, FunctionDef, ...)
  │
  ▼
ilold-core::cfg::builder            (CfgBuilder)
  │
  ▼
ilold-core::pathtree::walker        (build_path_tree)
  │
  ▼
ilold-core::sequence::analysis      (analyze_sequences, analyze_project)
  │
  ▼
ilold-core::narrative + slicing     (info, trace, slice, timeline)

analyze and context run this pipeline once and print to stdout. serve and explore keep the model in memory and expose it through the HTTP/WS API in ilold-web.

Solana pipeline

Anchor.toml + idls/<program>.json + target/deploy/<program>.so
  │
  ▼
ilold-solana-core::ingest           (detect, AnchorProject)
  │
  ▼
ilold-solana-core::runtime          (LiteSVM-backed engine)
  │
  ▼
ilold-solana-core::exploration      (SolanaCommand → SolanaCommandResult)
  │                                  per-step CU, logs, account diffs, decoded timelines
  ▼
ilold-web (shared HTTP/WS layer)    (/api/cmd, /api/program/*, /ws)
  │
  ▼
ilold-cli::explore                  (REPL, parses input, prints results)

There is no static CFG or path tree on Solana today: programs are bytecode at this point. Anything that requires control-flow analysis (slice, trace, sequence narrative) is listed in Solana: Limitations and tracked under Phase 2 in the Roadmap.

Shared layer

CrateRole
ilold-cliArgument parsing, REPL, output formatting, key bindings (crates/ilold-cli/src/main.rs, explore.rs, help.rs)
ilold-webHTTP + WebSocket API consumed by both the REPL (via --attach) and the web canvas
ilold-session-coreShared session abstractions (steps, scenarios, canvas patches)
ilold-coreSolidity model, CFG, slicer, narrative, sequence analysis
ilold-solana-coreAnchor IDL ingest, LiteSVM runtime, instruction execution, timeline reconstruction

The web canvas (crates/ilold-web/frontend) subscribes to the /ws stream and stays in sync with whatever the REPL does.

Solidity Backend Overview

The Solidity backend is built on top of solar-compiler (parser) and a set of analysis passes in ilold-core. It supports both a one-shot CLI (analyze, context) and the interactive REPL (explore, serve).

What it parses

crates/ilold-cli/src/main.rs::collect_sol_files walks the input path:

  • A single .sol file is loaded as-is.
  • A directory is walked recursively; the directories out, cache, node_modules, lib, target, .git, .svelte-kit and any dot-prefixed directory are skipped.

Once the project is parsed, ilold builds a per-function CFG via CfgBuilder::build_with_project and a path tree via build_path_tree (see crates/ilold-core/src/cfg/builder.rs and pathtree/walker.rs). Inheritance is resolved transitively, so inherited functions and state variables show up in funcs-all / vars-all and in info output.

What you can do with it

SurfacePurpose
analyzeOne-shot pretty-print of every contract: functions, CFG and path-tree stats, sequences up to --max-seq-depth, optional verbose function behavior breakdown
contextGenerate machine-readable narratives for a function or a comma-separated sequence
serveStart the HTTP/WS server only, no REPL: feed the web canvas
exploreInteractive REPL, with the HTTP/WS API running alongside

REPL command groups

The REPL has six command groups, all documented in their own pages:

  • Session: c/call, b/back, cl/clear, s/state, seq/sequence, st/step, ss/session.
  • Analysis: w/who, i/info, tr/trace, tl/timeline, sl/slice.
  • Contract: f/functions, fa/funcs-all, v/vars, va/vars-all, ct/contracts, use.
  • Findings: fi/finding, n/note, status, fl/findings, ex/export.
  • Scenarios: sc/scenario (new, list, switch, fork, delete).
  • Workspace: save, load, browser, q/quit/exit, ?/help.

Workflows

Two end-to-end walkthroughs are included:

CLI: analyze

ilold analyze parses a Solidity project, runs the full static-analysis pipeline (parser → CFG → path tree → sequence analysis with cross-contract transitive effects), and prints a structured summary to stdout. It is the one-shot view of what explore keeps in memory.

Synopsis

ilold analyze <path> [--contract <name>] [--max-seq-depth <N>] [--verbose]
FlagDefaultDescription
--contract <name>(all contracts)Restrict output to a single contract
--max-seq-depth <N>3Depth bound for the sequence tree (call combinations up to N steps)
--verboseoffPer-block CFG layout, call-graph edges, full function-behavior breakdown

<path> may be a single .sol file or a directory; the walker skips out, cache, node_modules, lib, target, and dot-prefixed directories.

Example: default output

$ ilold analyze tests/fixtures/staking.sol
Parsed 1 file(s), 2 contract(s)

interface IERC20 (3 functions, 0 state vars)
  [P] external transfer — 1 blocks, 0 edges, 0 paths (0 happy, 0 revert)
  [P] external transferFrom — 1 blocks, 0 edges, 0 paths (0 happy, 0 revert)
  [P] external balanceOf — 1 blocks, 0 edges, 0 paths (0 happy, 0 revert)

contract Staking (9 functions, 11 state vars)
  [S] internal constructor — 2 blocks, 1 edges, 1 paths (1 happy, 0 revert)
  [P] external deposit — 8 blocks, 8 edges, 5 paths (2 happy, 3 revert)
  [P] external withdraw — 8 blocks, 8 edges, 6 paths (2 happy, 4 revert)
  [P] external claimRewards — 6 blocks, 7 edges, 4 paths (4 happy, 0 revert)
  [R] external setRewardRate — 4 blocks, 3 edges, 2 paths (1 happy, 1 revert)
  [R] external pause — 4 blocks, 3 edges, 2 paths (1 happy, 1 revert)
  [R] external unpause — 4 blocks, 3 edges, 2 paths (1 happy, 1 revert)
  [P] public rewardPerToken — 5 blocks, 4 edges, 2 paths (2 happy, 0 revert)
  [P] public earned — 2 blocks, 1 edges, 1 paths (1 happy, 0 revert)
  Sequences (depth 3): 584 total (8 functions: 6 state-changing, 2 read-only)

Each function line prints an access badge ([P] public/external, [R] restricted/admin-gated, [S] system/internal), visibility, block/edge counts from the CFG, and the path-tree breakdown (total, happy, revert).

Example: --max-seq-depth

The sequence tree enumerates ordered combinations of entry-point calls up to depth N. Raising the bound surfaces longer interaction patterns that the static analyzer reasons about:

$ ilold analyze tests/fixtures/staking.sol --max-seq-depth 5
...
Sequences (depth 5): 37448 total (8 functions: 6 state-changing, 2 read-only)

The sequence tree is consumed by the cross-step transitive-effect pass and feeds seq in the REPL.

Example: --verbose

--verbose adds:

  • One line per CFG block ([id] BlockKind (N stmts)).
  • One line per CFG edge (src → dst EdgeKind).
  • The intra-contract call graph (fn → contract.fn with internal | external | inherited).
  • A per-function behavior tree with requires, writes, calls, emits, and transitions to other functions, including the shared state variables that link them.

The output is meant to be read top-down by a human; context (next page) is the machine-readable counterpart.

Notes

  • analyze does not require a configured project; it works on raw .sol files.
  • Interfaces are listed in the contract header but have no sequence tree.
  • Errors building the CFG for a single function are reported inline and do not abort the run.

See Solidity: Limitations for the analysis boundaries (intraprocedural slicing, modifier placeholder split, etc.).

CLI: context

ilold context produces a structured narrative for a single function or for a comma-separated sequence of functions. It runs the same pipeline as analyze (parser → CFG → path tree → sequence analysis with cross-contract transitive effects) but emits a focused narrative instead of the project-wide pretty-print.

Synopsis

ilold context <path> [--contract <name>] [--function <name>]
                     [--sequence <f1,f2,...>] [--list]
FlagDescription
--contract <name>Pick the active contract. Required when the project has more than one.
--function <name>Build a function narrative: paths, state reads/writes, internal/external calls, transitive effects, observations.
--sequence <f1,f2,...>Build a sequence narrative across the listed functions.
--listList functions of the resolved contract with access level and tags, then exit.

The function/sequence narratives are the same data structures the REPL renders for i <func> and seq respectively (see crates/ilold-core/src/narrative/function.rs and narrative/sequence.rs).

Example: list mode

$ ilold context tests/fixtures/staking.sol --list
  Staking — 9 functions

  [P] deposit              external
  [P] withdraw             external
  [P] claimRewards         external
  [R] setRewardRate        external
  [R] pause                external
  [R] unpause              external
  [P] rewardPerToken       public
  [P] earned               public

  Usage:
    ilold context <path> --function <name>
    ilold context <path> --sequence "fn1,fn2"

  Example:
    ilold context <path> --function deposit
    ilold context <path> --sequence "deposit,withdraw"

The badges line up with analyze: [P] public/external entry point, [R] restricted/admin-gated, [S] system/internal.

Example: single function

$ ilold context tests/fixtures/staking.sol --function withdraw

Output is the same FunctionNarrative printed by the REPL’s i withdraw, including transitive effects through the call chain.

Example: a sequence

$ ilold context tests/fixtures/staking.sol --sequence deposit,withdraw,claimRewards

The output narrates the per-step writes and the cross-step dependencies (variables shared between consecutive steps).

Notes

  • context is read-only; it does not start the API server.
  • --list short-circuits before computing path trees, so it is cheap on large projects.
  • Use explore when you need to iterate; context is meant for scripts and one-off questions.

Session Commands

Session commands manage the call sequence – the ordered list of function calls that represents an execution scenario you want to analyze.

call

c <function> or call <function>

Adds a function call to the session sequence. Only external and public functions are accepted; internal and private functions are rejected since they cannot be entry points for a real transaction.

ilold[Staking]> c deposit

  + Step 0: deposit [P] external
    State writes:
      · balances
      · totalStaked
    Sequence: deposit
ilold[→ deposit]> c withdraw

  + Step 1: withdraw [P] external
    State writes:
      · balances
      · totalStaked
    Sequence: deposit → withdraw

Returns: StepAdded { step_index, function, access, state_changed }. access is one of Public, Restricted { role }, Internal, Special { kind }.

Attempting to call an internal function:

ilold[Staking]> c _updateRewards

  '_updateRewards' is internal and cannot be called from outside the contract —
  not a valid session entry point. Use `tr _updateRewards` to view its flow,
  or `c <public_caller>` to trace a real entry point.

back

b or back

Removes the last step from the session sequence.

ilold[→ deposit → withdraw]> b

  - Step removed. 1 remaining.
    Sequence: deposit

clear

cl or clear

Resets the session, removing all steps. Prompts for confirmation if steps exist.

ilold[→ deposit → withdraw]> cl
  Clear 2 steps? (y/n)
  y
  Session cleared.

state

s or state

Shows the accumulated state mutations across all steps in the session. Each variable lists every mutation with the operator symbol (+=, -=, =) and the originating function.

ilold[→ deposit → withdraw]> s

  ═══════════════════[ STATE ]═══════════════════
  balances
    += msg.value (step 0, deposit)
    -= amount (step 1, withdraw)
  totalStaked
    += msg.value (step 0, deposit)
    -= amount (step 1, withdraw)

Each change line is <operator> <value_expr> (step <N>, <function>), with an optional via <modifier> suffix when the mutation comes from a modifier body.

Returns: StateView { summary: [VariableSummary { variable, changes }] }. If the session is empty, state tells you to add steps first.

sequence

seq or sequence

Displays a narrative of the current call sequence, including dependencies between steps and observations about the interaction pattern. Requires at least 2 steps.

ilold[→ deposit → withdraw]> seq

  Step 0: deposit
    writes: balances, totalStaked

  Step 1: withdraw
    writes: balances, totalStaked
    depends on: deposit (shared state: balances, totalStaked)

  Observations:
    · deposit and withdraw modify the same variables (balances, totalStaked)

step

st <index> or step <index>

Re-inspects a specific session step, showing its full function narrative (same output as info). You can also write st0, st1 without a space.

ilold[→ deposit → withdraw]> st 0

  deposit [public] — whenNotPaused
  ├── Paths: 2 total, 1 happy, 1 revert
  ├── State reads: balances
  ├── State writes: balances, totalStaked
  └── Events: Deposited

session

ss or session

Shows the full session overview: active contract, current step sequence, and findings count.

ilold[→ deposit → withdraw]> ss

  Contract: Staking
  Steps:    deposit → withdraw
  Findings: 0

Analysis Commands

Analysis commands query the contract model without modifying the session. Each command produces cross-reference hints at the bottom of its output, suggesting related commands to run next.

who

w <variable> or who <variable>

Shows which functions read and write a state variable, with their access level. Searches across the active contract and its ancestors.

ilold[Staking]> who totalStaked

  who: totalStaked
    Writers:
      [P] deposit
      [P] withdraw
    Readers:
      [P] rewardPerToken
  → sl deposit totalStaked, sl withdraw totalStaked
  → tl totalStaked

The cross-reference hints suggest running slice for each writer and timeline for the variable. Access badges: [P] public/external, [R] restricted (admin-gated), [I] internal, [S] special.

Returns: VariableInfo { variable, writers, readers } where each writer/reader is a (String, AccessLevel) pair.

info

i <function> or info <function>

Displays a full function narrative: execution paths, state reads/writes, internal and external calls, transitive effects through the call chain, and observations. Works on any function including internal ones.

ilold[Staking]> i withdraw

  withdraw [public] — whenNotPaused, nonReentrant
  ├── Paths: 3 total, 1 happy, 2 revert
  ├── State reads: balances, totalStaked
  ├── State writes: balances, totalStaked
  ├── External calls: msg.sender.call{value: amount}
  ├── Transitive effects:
  │     via _updateRewards:
  │       writes: rewardDebt
  │       reads: rewardPerToken
  └── Observations:
        · External call after state writes (checks-effects-interactions followed)
  → c withdraw, tr withdraw

The info command does not require the function to be in the session. It analyzes the function in isolation.

trace

tr <function> [--depth N] [--reverts] [+N...] [-i] tr step <N>

Renders the execution flow tree for a function. Modifier bodies are inlined into the tree with [from: modifier] annotations. Internal calls are expanded up to the depth limit (default 2).

ilold[Staking]> tr withdraw

  ╭──────────────────────────────────────╮
  │ Staking::withdraw(uint256)           │
  │ modifiers: whenNotPaused, nonReentrant│
  │ max inlining depth: 2               │
  ╰──────────────────────────────────────╯

  001 │ ▶ withdraw(uint256)
  002 │ ├─ ◇ require(!paused, "Paused")  [from: whenNotPaused]
  003 │ ├─ ◇ require(!locked)  [from: nonReentrant]
  004 │ ├─ ✏ locked = true  [from: nonReentrant]
  005 │ ├─ ◇ require(amount <= balances[msg.sender])
  006 │ ├─ ○ _updateRewards(msg.sender)  [+8 ops, depth limited]
  007 │ ├─ ✏ balances[msg.sender] -= amount
  008 │ ├─ ✏ totalStaked -= amount
  009 │ ├─ → msg.sender.call{value: amount}
  010 │ ├─ ◆ emit Withdrawn(msg.sender, amount)
  011 │ └─ ✏ locked = false  [from: nonReentrant]

  tip: expand with `tr <func> +N` — candidates: 6
  → sl withdraw balances, sl withdraw totalStaked

Icon legend

IconMeaning
Function entry
require/assert
State write
State read
Internal call
External call
Event emission
?Branch (if/else)
Loop header
Return
Revert

Options

  • --depth N – Set max inlining depth for internal calls. Default is 2.
  • --reverts – Include revert paths in the tree.
  • +N – Force-expand a depth-limited internal call at step N. Multiple +N flags allowed.
  • -i – Open the trace in an interactive TUI with keyboard navigation. Increases default depth to 4.
  • step N – Re-render the persisted flow tree from session step N (depth/expand flags are ignored).
ilold[Staking]> tr withdraw +6

  (same tree with _updateRewards fully expanded at step 6)
ilold[Staking]> tr step 0

  (renders the persisted trace from session step 0)

timeline

tl <variable> or timeline <variable>

Shows the cross-step mutation history of a variable across the current session. Each mutation includes the operator, value expression, and path conditions (reached-when).

ilold[→ deposit → withdraw]> tl totalStaked

  totalStaked — mutation timeline
  ════════════════════════════════════════════════════════════
  [state]
    session step 0 deposit
      ✏ totalStaked += msg.value [trace step 5]
    session step 1 withdraw
      ✏ totalStaked -= amount [trace step 8]
        reached when:
          · amount <= balances[msg.sender]
  → sl deposit totalStaked, sl withdraw totalStaked

If the variable has no mutations in the current session, timeline tells you to add steps with c <func> first.

slice

sl <function> <variable> [--backward|--forward|--both]

Performs dataflow analysis on a variable within a function. Walks the function body and modifier bodies to find definitions (backward) and uses (forward) of the variable. Modifier entries are prefixed with [mod name].

The direction flags can appear in any position: sl --backward deposit totalStaked and sl deposit totalStaked --backward are equivalent. The default direction is --both.

ilold[Staking]> sl withdraw balances --backward

  withdraw · balances — dataflow slice
  ════════════════════════════════════════════════════════════
  [backward]
    L31   require(amount <= balances[msg.sender])
    L34   balances[msg.sender] -= amount
  → tr withdraw | tl balances
ilold[Staking]> sl withdraw totalStaked --both

  withdraw · totalStaked — dataflow slice
  ════════════════════════════════════════════════════════════
  [backward]
    L35   totalStaked -= amount
  [forward]
    L38   emit Withdrawn(msg.sender, amount)
  → tr withdraw | tl totalStaked

Short flags are also accepted: -b for --backward, -f for --forward.

When a variable is defined inside a modifier body, the entry shows its origin:

  [backward]
    L12   [mod whenNotPaused] require(!paused, "Paused")
    L35   totalStaked -= amount

Contract Commands

Contract commands inspect the structure of the loaded contracts without modifying the session.

functions

f or functions

Lists the callable functions in the active contract with their access level and tags.

ilold[Staking]> f

  [P] deposit           writes state, external calls
  [P] withdraw          writes state, external calls
  [P] claimRewards      writes state, external calls
  [R] setRewardRate     writes state
  [R] pause             writes state
  [R] unpause           writes state
  [P] rewardPerToken    view
  [P] earned            view

Badges: [P] public/external, [R] restricted (admin-gated), [I] internal, [S] special. Tags indicate writes state, external calls, or view (read-only, no external calls).

Returns: FunctionList { functions: [FunctionEntry { name, access, writes_state, has_external_calls, is_read_only }] }.

funcs-all

fa or funcs-all

Lists all accessible functions including those inherited from parent contracts.

ilold[Staking]> fa

  [P] deposit           writes state, external calls
  [P] withdraw          writes state, external calls
  [P] claimRewards      writes state, external calls
  [R] setRewardRate     writes state
  [R] pause             writes state
  [R] unpause           writes state
  [P] rewardPerToken    view
  [P] earned            view

  inherited:
  [P] owner              from Ownable
  [P] transferOwnership  from Ownable

Inherited functions are listed separately with their origin contract.

Returns: FunctionListAll { functions: [AccessibleFunctionEntry { name, access, writes_state, has_external_calls, is_read_only, origin, is_inherited }] }.

vars

v or vars

Lists the state variables of the active contract with their type and mutability tag.

ilold[Staking]> v

  mutable   owner                   address
  mutable   paused                  bool
  mutable   rewardRate              uint256
  mutable   lastUpdateTime          uint256
  mutable   rewardPerTokenStored    uint256
  mutable   balances                mapping(address => uint256)
  mutable   userRewardPerTokenPaid  mapping(address => uint256)
  mutable   rewards                 mapping(address => uint256)
  mutable   totalStaked             uint256

Tags are mutable, const, or immutable.

vars-all

va or vars-all

Lists all accessible state variables including inherited ones.

ilold[Staking]> va

  mutable   owner                   address
  mutable   paused                  bool
  mutable   rewardRate              uint256
  ...

Returns: StateVarListAll { state_vars: [AccessibleStateVarEntry { name, type_name, is_constant, is_immutable, origin, is_inherited }] }. Inherited entries print under an inherited: section with from <origin>.

contracts

ct or contracts

Lists all contracts in the loaded project with their type badge, function count, state variable count, and inherits clause when present.

ilold[Staking]> ct

  [I] IERC20    3 functions, 0 state vars
  [C] Staking   9 functions, 11 state vars  ← current

Type badges: [C] contract, [I] interface, [L] library, [A] abstract.

use

use <contract>

Switches the active contract. Clears the current session steps.

ilold[Staking]> use Ownable

  ✓ Now using: Ownable
  Cleared 2 step(s) from previous contract

After switching, all session and analysis commands operate on the new contract.

Findings Commands

Findings commands let you record security observations during an audit session. Findings are tied to the current session state and can be exported as a markdown report.

finding

fi [severity] [title] or finding [severity] [title]

Records a security finding. Can be used in two modes:

Inline mode – pass severity and title directly:

ilold[→ deposit → withdraw]> fi high Reentrancy in withdraw before balance update

  ✓ Finding F-001 added

Interactive mode – run fi with no arguments to be prompted:

ilold[→ deposit → withdraw]> fi
  Severity (critical/high/medium/low/info):
  > high
  Title:
  > Reentrancy in withdraw before balance update
  Description (optional):
  > The external call on L38 occurs before totalStaked is decremented.
  ✓ Finding F-001 added

Valid severities: critical, high, medium, low, info (or informational).

The finding captures the current session sequence automatically.

note

n <text> or note <text>

Attaches a free-text note to the current session step. Notes are included in the exported report.

ilold[→ deposit → withdraw]> n Check if msg.value can be zero here

  ✓ Note added

Scenarios are managed by the dedicated sc | scenario command family (scenario new <name>, scenario fork <name> [at <N>], scenario switch <name>, scenario list, scenario delete <name>). See Scenarios for the full reference.

status

status <function> <status>

Sets the review status for a function. Useful for tracking audit progress.

ilold[Staking]> status deposit reviewed

  ✓ Status updated

Valid statuses: reviewed, suspicious, vulnerable, clean, inprogress, notreviewed.

findings

fl or findings

Lists the count of recorded findings. Use export to see full details.

ilold[Staking]> fl

  2 finding(s) recorded. Use export to export.

export

ex or export

Exports all findings, notes, and status changes as a markdown report. The file is written to the current directory.

ilold[Staking]> ex

  ✓ Exported to ilold-report-Staking.md

Scenario Commands

A scenario is a named branch of the session timeline. Every session starts on the default scenario main; the auditor can create more scenarios, switch between them, fork an existing one at a specific step, or delete a scenario that is no longer needed. The prompt shows the active scenario as ilold[Contract/scenario] when it is not main.

scenario new

scenario new <name> (alias: sc new)

Creates an empty scenario with no steps. The new scenario is not activated automatically. Use scenario switch to make it the active one.

ilold[Staking]> sc new reentrancy
  ✓ Created scenario 'reentrancy'

Returns: ScenarioCreated { name }.

scenario list

scenario list (aliases: scenario ls, sc list)

Lists every scenario in the active session, marking the active one.

ilold[Staking]> sc list
  scenarios — 2 total, active: main
        name         steps
    →   main         2
        reentrancy   0

Returns: ScenarioList { items: [ScenarioInfo { name, step_count, active }] }. The CLI renders it inside a framed header box.

scenario switch

scenario switch <name> (alias: sc switch <name>)

Activates an existing scenario. All subsequent session and analysis commands operate against its step list. The prompt updates to reflect the new active scenario.

ilold[Staking]> sc switch reentrancy
  ✓ Switched: 'main' → 'reentrancy'
ilold[Staking/reentrancy]>

Returns: ScenarioSwitched { from, to }. Switching to the active scenario is idempotent and prints · Already on scenario '<name>'.

scenario fork

scenario fork <name> [at <N>] (alias: sc fork)

Creates a new scenario branching from the active one. With at <N>, the new scenario inherits steps 0..N from the source scenario; without it, the new scenario inherits the full step list of the source. After forking, the new scenario is activated.

ilold[Staking → deposit → withdraw → claimRewards]> sc fork attack-v2 at 1
  ✓ Forked 'main' → 'attack-v2' at step 1
ilold[Staking/attack-v2 → deposit]>

Returns: ScenarioForked { from, to, at_step }.

Forks are useful when the auditor wants to keep an existing line of reasoning intact while testing a divergent path.

scenario delete

scenario delete <name> (aliases: scenario rm <name>, sc delete, sc rm)

Removes a scenario. The active scenario cannot be deleted; switch first.

ilold[Staking]> sc delete reentrancy
  ✓ Deleted scenario 'reentrancy'

Returns: ScenarioDeleted { name }.

Notes

  • Solidity scenarios share the same parsed model, so analysis commands stay cheap across forks.
  • For the Solana counterpart (each scenario carries its own VM, signers and PDAs), see Solana: Scenarios.
  • The full scenario tree is included in save / load and in the export report.

Workspace Commands

Workspace commands handle session persistence and external tools.

save

save <name>

Saves the current session (steps, findings, notes, statuses) to a JSON file under ~/.ilold/sessions/.

ilold[→ deposit → withdraw]> save staking-audit

  ✓ Saved to /Users/you/.ilold/sessions/staking-audit.json

You can resume this session later with load, even across different ilold runs, as long as the same contract files are loaded.

load

load <name>

Loads a previously saved session from ~/.ilold/sessions/.

ilold[Staking]> load staking-audit

  ✓ Session loaded (2 steps)

The prompt updates to reflect the loaded steps. The session replaces whatever is currently in memory.

browser

browser

Prints the base URL of the HTTP API the REPL is talking to. explore runs the API in-process by default; pass --attach <url> to point the REPL at a separate serve instance instead.

ilold[Staking]> browser

  API running at http://127.0.0.1:52431/api/

The web canvas (when running serve and opening the URL in a browser) subscribes to the same HTTP/WS endpoints. See HTTP API Reference for the full surface.

quit

q, quit, or exit

Exits the REPL. Ctrl+D and Ctrl+C also work.

Unsaved session data is lost on exit. Use save before quitting if the session needs to survive.

Full Audit Walkthrough

This walkthrough demonstrates a realistic audit of a Staking contract using the ilold interactive REPL. The contract allows users to deposit tokens, withdraw them, and claim rewards, with owner-only administrative functions and a pause mechanism.

Starting the session

Launch ilold against the Staking contract source files:

ilold explore contracts/Staking.sol

The REPL starts and auto-selects the main contract:

ilold explore -- Staking
8 functions | Type ? for help
Web UI: http://localhost:3001

Staking >

List available functions and state variables to orient yourself:

Staking > f
  deposit          External   writes state   
  withdraw         External   writes state   
  claimRewards     External   writes state   
  setRewardRate    External   writes state   onlyOwner
  pause            External   writes state   onlyOwner
  unpause          External   writes state   onlyOwner
  rewardPerToken   Public     read-only
  earned           Public     read-only

Staking > v
  stakingToken       address
  rewardToken        address
  owner              address
  paused             bool
  rewardRate         uint256
  lastUpdateTime     uint256
  rewardPerTokenStored  uint256
  balances           mapping(address => uint256)
  userRewardPerTokenPaid  mapping(address => uint256)
  rewards            mapping(address => uint256)
  totalStaked        uint256

The function list shows access level, whether a function writes state, and which modifiers restrict access. Read-only functions (view/pure) are marked separately. This is your starting map.

Calling deposit – observing state writes

Staking > c deposit
  Step 0: deposit [External]
  State changed: balances, totalStaked

The session records deposit as step 0. The engine analyzed the CFG and reports which state variables are mutated. Two writes: balances and totalStaked.

Investigating totalStaked with who, slice, trace

The who command reveals which functions read and write a variable across the entire contract:

Staking > who totalStaked
  Writers:
    deposit          External
    withdraw         External
  Readers:
    rewardPerToken   Public

Both deposit and withdraw modify totalStaked, and rewardPerToken reads it. This tells you the impact surface: any bug in how totalStaked is updated will propagate to reward calculations.

Now use slice to see the dataflow within deposit:

Staking > sl deposit totalStaked
  Backward slice (sources -> totalStaked):
    [0] stakingToken.transferFrom(msg.sender, address(this), amount)
    [2] totalStaked += amount

  Forward slice (totalStaked -> sinks):
    [2] totalStaked += amount

The backward slice shows that the transferFrom call precedes the state write. The forward slice is short because totalStaked is only written, not read within this function. The real consumers are in rewardPerToken – the who output already told you that.

Use trace to see the full execution flow:

Staking > tr deposit
  1. [mod whenNotPaused] require(!paused, "paused")
  2. [mod updateReward] rewardPerTokenStored = rewardPerToken()
  3. [mod updateReward] lastUpdateTime = block.timestamp
  4. [mod updateReward] userRewardPerTokenPaid[account] = rewardPerTokenStored
  5. [mod updateReward] rewards[account] = earned(account)
  6. require(amount > 0, "Cannot stake 0")
  7. stakingToken.transferFrom(msg.sender, address(this), amount)
  8. balances[msg.sender] += amount
  9. totalStaked += amount

Lines prefixed with [mod ...] come from modifiers inlined before the function body. The updateReward modifier writes four state variables before deposit’s own body runs. This is critical context: if you only looked at the function body, you would miss these writes.

Calling withdraw – observing the sequence

Staking > c withdraw
  Step 1: withdraw [External]
  State changed: balances, totalStaked

Staking[deposit > withdraw] >

The prompt now shows both steps. Use state to see the accumulated picture:

Staking[deposit > withdraw] > s
  balances             written by: deposit (+= amount), withdraw (-= amount)
  totalStaked          written by: deposit (+= amount), withdraw (-= amount)
  rewardPerTokenStored written by: deposit (via updateReward), withdraw (via updateReward)
  lastUpdateTime       written by: deposit (via updateReward), withdraw (via updateReward)
  userRewardPerTokenPaid written by: deposit (via updateReward), withdraw (via updateReward)
  rewards              written by: deposit (via updateReward), withdraw (via updateReward)

The state view aggregates every write from every step. Mutations introduced by modifiers are tagged with via <modifier>. You can see that updateReward runs in both deposit and withdraw, updating the reward accounting state each time.

Checking claimRewards – modifier writes in the slice

Staking > c claimRewards
  Step 2: claimRewards [External]
  State changed: rewards

Slice claimRewards for the rewards variable:

Staking[deposit > withdraw > claimRewards] > sl claimRewards rewards --both
  Backward slice (sources -> rewards):
    [mod updateReward] rewards[account] = earned(account)
    [5] uint256 reward = rewards[msg.sender]

  Forward slice (rewards -> sinks):
    [mod updateReward] rewards[account] = earned(account)
    [5] uint256 reward = rewards[msg.sender]
    [7] rewards[msg.sender] = 0
    [8] rewardToken.transfer(msg.sender, reward)

Entries tagged [mod updateReward] are statements from the modifier body that touch rewards. The slicer walks both the function body and every applied modifier, so nothing is hidden. The forward slice shows that rewards[msg.sender] is read into a local, zeroed, and then transferred – the standard claim pattern.

Using timeline to track a variable across all steps

Staking[deposit > withdraw > claimRewards] > tl balances
  Variable: balances

  Step 0  deposit       balances[msg.sender] += amount
  Step 1  withdraw      balances[msg.sender] -= amount
Staking[deposit > withdraw > claimRewards] > tl rewardPerTokenStored
  Variable: rewardPerTokenStored

  Step 0  deposit       rewardPerTokenStored = rewardPerToken()   via updateReward
  Step 1  withdraw      rewardPerTokenStored = rewardPerToken()   via updateReward
  Step 2  claimRewards  rewardPerTokenStored = rewardPerToken()   via updateReward

The timeline shows every mutation of a variable across the entire session in chronological order. Each entry includes the step index, the function that caused it, the assignment expression, and whether it came from a modifier. Path conditions (from branching logic) are included when the function has conditional writes.

This gives you a cross-function view that no single-function analysis can provide.

Recording findings and exporting

Record an observation while looking at the claim flow:

Staking[deposit > withdraw > claimRewards] > n claimRewards zeroes rewards before transfer -- CEI pattern followed

If you spot an issue, record a finding with severity:

Staking[deposit > withdraw > claimRewards] > fi medium No check that reward > 0 before transfer call
  Finding F-001 added (Medium)

Mark functions as reviewed:

Staking[deposit > withdraw > claimRewards] > status deposit reviewed
Staking[deposit > withdraw > claimRewards] > status claimRewards suspicious

Export the session to a markdown report:

Staking[deposit > withdraw > claimRewards] > export
  Exported to ilold-report-Staking.md

Save the session for later:

Staking[deposit > withdraw > claimRewards] > save staking-audit-day1
  Saved to ~/.ilold/sessions/staking-audit-day1.json

How cross-reference hints guide exploration

The output of each command naturally points to the next:

  1. functions shows which functions write state – you call the important ones first.
  2. c deposit reports balances, totalStaked changed – you run who totalStaked to see the impact.
  3. who shows rewardPerToken reads totalStaked – you run sl rewardPerToken totalStaked or tr rewardPerToken to trace the dependency.
  4. slice shows modifier-origin statements – you trace the modifier with tr deposit to see full execution order.
  5. state aggregates writes across steps – variables with writes from multiple functions deserve timeline inspection.
  6. timeline reveals the chronological mutation history – unexpected patterns become findings.

Each command’s output contains the variable names, function names, and modifier names needed for the next query. The workflow is: call, observe, investigate, record, repeat.

Taint Analysis – Tracing User Input

Forward slicing traces how a variable’s value propagates through a function body. When the starting variable is a user-controlled parameter, the slice acts as a taint analysis: it reveals every state variable, local variable, and external call that the attacker-controlled input can reach.

Identifying entry points

List functions and note which ones are externally callable:

Staking > f
  deposit          External   writes state
  withdraw         External   writes state
  claimRewards     External   writes state
  setRewardRate    External   writes state   onlyOwner
  pause            External   writes state   onlyOwner
  unpause          External   writes state   onlyOwner
  rewardPerToken   Public     read-only
  earned           Public     read-only

Functions marked External accept parameters from untrusted callers. The onlyOwner annotation means the function has an access-control modifier, but the parameter values themselves are still caller-supplied. For taint analysis, focus on functions where user-controlled parameters flow into state writes or external calls.

In this contract, deposit(uint256 amount) and withdraw(uint256 amount) both take an amount parameter from the caller and write state.

Forward-slicing amount in deposit

Use the forward slice to trace where amount goes:

Staking > sl deposit amount --forward
  Forward slice (amount -> sinks):
    [0] require(amount > 0, "Cannot stake 0")
    [1] stakingToken.transferFrom(msg.sender, address(this), amount)
    [2] balances[msg.sender] += amount
    [3] totalStaked += amount

The slice shows four statements that depend on amount:

  1. A require check validates that amount is positive.
  2. An external call to stakingToken.transferFrom uses amount directly.
  3. balances[msg.sender] is incremented by amount.
  4. totalStaked is incremented by amount.

The user-controlled value reaches two state variables (balances, totalStaked) and one external call (transferFrom). The require at line 0 is the only validation gate.

Forward-slicing amount in withdraw

Staking > sl withdraw amount --forward
  Forward slice (amount -> sinks):
    [0] require(amount > 0, "Cannot withdraw 0")
    [1] require(balances[msg.sender] >= amount, "Insufficient balance")
    [2] balances[msg.sender] -= amount
    [3] totalStaked -= amount
    [4] stakingToken.transfer(msg.sender, amount)

The withdraw slice is similar but has an additional check: balances[msg.sender] >= amount. This prevents withdrawing more than deposited. The subtraction operations mirror the additions in deposit.

Compare the two slices side by side:

Statement typedepositwithdraw
Validationamount > 0amount > 0, balances >= amount
External calltransferFrom(sender, this, amount)transfer(sender, amount)
State writesbalances += amount, totalStaked += amountbalances -= amount, totalStaked -= amount

The asymmetry in validation is expected here: deposit relies on the ERC-20 transferFrom to enforce that the caller actually has the tokens, while withdraw must check the internal balance explicitly.

What the slice reveals about state variable control

The forward slice of a user parameter tells you which state variables are directly controlled by external input. From the two slices above:

  • balances is written in both functions using amount directly. Any arithmetic error in the += or -= operations would let an attacker manipulate their balance.
  • totalStaked is written in both functions using amount directly. Since rewardPerToken reads totalStaked (visible via who totalStaked), a corrupted totalStaked would affect reward calculations for all users.
  • Neither rewardPerTokenStored nor rewards appear in the forward slice of amount. These variables are written by the updateReward modifier, which derives values from rewardPerToken() and earned() – not from the caller’s amount parameter directly.

Mapping to vulnerability patterns

Forward slice results map to common vulnerability classes:

Unchecked arithmetic. If balances[msg.sender] += amount or totalStaked += amount can overflow, the attacker controls the input that triggers it. For Solidity 0.8+, the compiler inserts overflow checks automatically. For older versions, look for SafeMath usage in the slice.

Missing validation. If the slice shows a state write or external call with no preceding require that bounds the parameter, the input flows unchecked. In deposit, the only check is amount > 0 – there is no upper bound. Whether this is a problem depends on the token’s transferFrom behavior.

External call with user input. Both slices show external calls (transferFrom, transfer) that use amount. If the token contract is untrusted or implements callbacks (e.g., ERC-777), the attacker controls the value passed to a potentially re-entrant call. Check whether the state writes happen before or after the external call (CEI pattern).

Cross-function impact. Use who to find all readers of a tainted state variable, then forward-slice the reader to see downstream effects. For example, rewardPerToken reads totalStaked, so a manipulated totalStaked propagates into every user’s reward calculation.

Practical workflow

  1. Run f to list entry points.
  2. For each external function with parameters, run sl <func> <param> --forward.
  3. Note which state variables appear in each forward slice.
  4. Run who <var> on each affected state variable to find cross-function readers.
  5. Run sl <reader> <var> --forward to trace second-order propagation.
  6. Record findings with fi when a tainted path reaches a sensitive sink without adequate validation.

Known Limitations

This page documents the current analysis boundaries of ilold. Understanding these limitations is necessary for interpreting slice, timeline, and trace results correctly.

Intraprocedural slicing only

The dataflow slicer operates within a single function body (plus its inlined modifiers). It does not follow values across function call boundaries. If deposit calls an internal helper _updateBalance(amount), the slice for amount in deposit will show the call site but not the writes inside _updateBalance. Use tr <func> to inspect internal call bodies separately.

Assignment-only DEF extraction

Only Assignment expressions (x = ..., x += ..., x -= ...) produce DEF entries in the slicer’s use-def analysis. Solidity mutations that are not modeled as assignments – specifically x++, x--, ++x, --x, delete x, arr.push(v), and arr.pop() – are captured as USEs of the target variable but not as DEFs. A backward slice on a variable mutated exclusively through .push() will miss the mutating statement as a definition point.

Modifier placeholder split

Modifier bodies are split at the first top-level _; (placeholder) statement to separate “before” code from “after” code. If the placeholder appears inside a nested block (e.g., inside an if branch), the entire modifier body is treated as “before” code. This is over-inclusive: statements that should execute after the function body will appear before it in the flattened view. In practice, most modifiers place _; at the top level, so this rarely triggers.

Forward slice over-tainting via ancestor merge

When a statement is included in a forward slice, its lexical ancestors (enclosing if, for, while blocks) are also included so that the rendered slice shows control-flow context. The ancestor’s condition variables are merged into the tainted set. This means an if (unrelatedCondition) enclosing a tainted write will add unrelatedCondition to the taint set, potentially pulling in unrelated statements in subsequent iterations. The result is a conservative (larger) slice rather than a precise one.

Tuple destructuring

Tuple destructuring assignments such as (a, b) = foo() may not be recognized as DEFs depending on how the Solidity frontend lowers them. If the frontend does not emit a top-level Assignment node, the individual targets (a, b) are treated as USEs only. This can cause a backward slice to miss the destructuring as a definition point for a or b.

Timeline tracks state mutations only

The timeline command tracks writes to state variables across session steps. Local variable assignments within a function body are recorded separately (local_entries) but are not visible in the default timeline output. If you need to trace a local variable, use slice within the specific function instead.

Session requires at least one call

The timeline, state, and sequence endpoints require an active session with at least one Call step. The timeline and state commands return empty results if no steps have been added. The sequence command requires at least two steps. Use tr <func> for read-only inspection of a function’s flow without adding it to the session.

Internal and private functions cannot be session entry points

Session steps model real external transactions. Functions with internal or private visibility cannot be called from outside the contract, so they cannot be added as session steps via c <func>. Use tr <func> to inspect their execution flow, or call a public/external function that invokes them to see their effects through the modifier and internal-call inlining in the trace.

Solana Backend Overview

The Solana backend runs Anchor programs against a LiteSVM-backed engine. Every call runs in the VM, so the auditor sees real compute units, real logs, and real account state. Static control-flow analysis is not available yet; anything that requires it (slice, trace, sequence narrative) is deferred to Phase 2 (see Roadmap).

Two CLI entry points cover Solana: ilold explore <project> (REPL + API) and ilold serve <project> (API only). Both auto-detect the Solana backend when the path resolves to an Anchor workspace.

Project layout the loader expects

<root>/
  Anchor.toml
  idls/
    <program>.json          # Anchor IDL (required)
  target/deploy/<program>.so  # compiled program (or bin/<program>.so)

crates/ilold-solana-core/src/ingest resolves these paths. Without the .so, IDL navigation (f, i, pda, vars, who) still works; everything that drives the VM (call, state, inspect, timeline) fails until the program is compiled.

The committed fixtures live under tests/fixtures/solana/staking (single program) and tests/fixtures/solana/cpi (two programs that talk to each other through CPI). Both ship pre-built bin/<program>.so binaries so the suite runs without the Anchor toolchain.

Solidity vs Solana mental model

ConceptSoliditySolana
Entry pointfunction on a contractinstruction on a program
Persistent statecontract state variablesaccounts owned by the program
Caller identitymsg.sender (implicit)signers passed by the client
who <X>reads/writes of a state variable (CFG-based)instructions that touch an account type (IDL heuristic)
timeline <X>mutation history of a state variablemutation history of an account pubkey, decoded
step <i>re-renders the persisted flow treere-prints CU, logs, account diffs
slice / tracefull CFG-based analysisnot implemented (Phase 2)
sequencenarrative with cross-step dependenciesaliased to session (no narrative engine yet)
Executionsymbolic (CFG + paths)concrete (in-process LiteSVM execution)
backdrops the step from the timelinedrops the step AND rewinds the VM to the pre-call snapshot
save / loadstep list + persisted pathsstep list + replay-driven VM reconstruction

REPL command groups

The REPL command surface mirrors the Solidity one with backend-specific extensions. Each group has its own page:

  • Session: c/call, b/back, cl/clear, s/session, state, st/step.
  • Programs and IDL: ct/programs, use, f/funcs, fa/funcs-all, i/info, v/vars, va/vars-all.
  • Solana runtime: users, airdrop, tw/time-warp, pda, inspect.
  • Analysis: who, tl/timeline, cp/coupling, cov/coverage.
  • Findings: fi/finding, n/note, status, fl/findings, ex/export.
  • Scenarios: sc/scenario (new, list, switch, fork, delete).
  • Workspace: save, load, browser.
  • Help and control: ?/help, <cmd>?, q/quit/exit, seq (aliased to session).

Workflows

Session Commands

Session commands drive the active scenario: append a call, rewind, inspect what changed. Every state-changing command updates the LiteSVM as a side effect; back and clear rewind the VM to the corresponding pre-step snapshot.

call

c <ix> [arg=val ...] [account=user_or_pubkey ...] or call <ix> {json} (alias: c)

Runs an Anchor instruction against the VM and appends the result to the active scenario. Two payload forms are supported:

  • Concise key=value form: positional arg=value and account_field=user_name tokens. Unmapped names become local keypairs. Signers are auto-resolved from the IDL.
  • JSON form: {"args": {...}, "accounts": {...}, "signers": [...]} for full control.

Flags:

FlagDescription
--signer=a,bAdd signers on top of the IDL defaults
--no-signer=nameRemove a default signer (for negative cases)
ilold[staking]> c initialize_pool reward_rate=10 pool=pool admin=admin
  ✓ step 0 [ok]: initialize_pool (12400 CU, 1 diffs)
ilold[staking → initialize_pool]> c stake amount=1000 pool=pool user_stake=alice_stake user=alice
  ✓ step 1 [ok]: stake (18700 CU, 2 diffs)
ilold[staking]> c stake {"args":{"amount":1000},"accounts":{"pool":"pool","user_stake":"alice_stake","user":"alice"}}

When the VM rejects the call (Anchor constraint, custom require!, etc.) no step is appended and the CLI prints the error inline:

ilold[staking]> c stake amount=0 pool=pool user_stake=alice_stake user=alice
  ✗ FAILED: stake (4200 CU, not recorded)
    error: AnchorError: Amount must be > 0

Returns: StepAdded { step_index, instruction, logs_excerpt, account_diffs_count, compute_units, error } on success, CallFailed { instruction, logs_excerpt, compute_units, error } when the VM rejects.

See also: info, pda, state, step, back.

back

b or back

Removes the last step from the active scenario and rewinds the VM to the pre-call snapshot of that step. Re-issuing the same call after back produces fresh CU and diffs.

ilold[staking → initialize_pool → stake]> b
  ✓ step undone (1 remaining)

Returns: StepRemoved { remaining }.

time-warp is a global side effect on the Clock sysvar and is not undone by back. Reset the clock manually if a test relies on a specific timestamp.

clear

cl or clear

Drops every step in the active scenario and rewinds the VM to the genesis snapshot of that scenario.

ilold[staking → initialize_pool → stake]> cl
  ✓ session cleared

Returns: Cleared.

state

state

Decoded view of every account mutated during the active scenario. Each entry shows the pubkey, the owning program, and the decoded fields from the latest step.

ilold[staking → initialize_pool → stake]> state
  [A] pool (2039280 lamports) 7XzG…ABCd
      admin          AdminPubkey…
      reward_rate    10
      total_staked   1000
      last_update_ts 1714060800
  [A] alice_stake (1559040 lamports) 6Hj…Pq
      user           AlicePubkey…
      amount         1000
      reward_debt    0

Returns: StateView { accounts: [AccountSummary { pubkey, label, lamports, decoded }] }. decoded is the Anchor-decoded JSON snapshot, None for accounts whose discriminator does not match a known type.

session

s or session

Prints the active scenario summary: ordered steps, findings, notes, and the current scenario name.

ilold[staking → initialize_pool → stake]> s
  program=staking scenario=main steps=2 findings=0
    0. initialize_pool
    1. stake

Returns: SessionView { program, scenario, steps, findings_count }.

step

st <index> or step <index> (no-space shortcut: st0, step1)

Re-inspects a specific step of the active scenario, printing the persisted CU, logs, and decoded account diffs.

ilold[staking → initialize_pool → stake]> step 1
  · step 1 · stake
    compute units: 18700
    logs: (4 lines)
    diffs (2):
      pool (Pool)
        total_staked  0 → 1000
      alice_stake (UserStake)
        amount        — → 1000

Returns: StepDetail { step_index, instruction, runtime_trace, diff_summary }. runtime_trace is a JSON blob carrying compute_units, logs, and (when present) error; diff_summary is a list of { address, name, lamports_delta, data_changed, decoded_before, decoded_after }.

Notes

  • The Solidity equivalent (seq / sequence) is currently aliased to session on Solana; there is no cross-step narrative engine yet. See Roadmap.
  • call is the only command that drives the VM forward; everything else inspects or rewinds.

Programs and IDL Commands

These commands inspect the static surface of the active program: instruction list, instruction detail, account types, and project-level navigation.

programs / contracts

ct or programs (aliases: contracts, progs)

Lists every program detected in the workspace. Multi-program Anchor workspaces (e.g. tests/fixtures/solana/cpi) show one entry per program.

ilold[staking]> ct
  staking          ← current
  reward_oracle

use

use <program>

Switches the active program. Subsequent commands target the new program; the prompt label updates.

ilold[staking]> use reward_oracle
  ✓ now using reward_oracle

functions

f or funcs (alias: functions)

Lists the instructions exposed by the active program, with arg / account counts and signer names. PDA-bearing instructions are tagged [PDA].

ilold[staking]> f
  [PDA] initialize_pool (args:1 accounts:3) signers: admin
  [PDA] stake (args:1 accounts:4) signers: user
  [PDA] unstake (args:1 accounts:4) signers: user
  [ix]  add_rewards (args:1 accounts:2) signers: admin
  [PDA] claim_rewards (args:0 accounts:4) signers: user

Returns: InstructionList { items: [InstructionEntry { name, args_count, accounts_count, has_pdas, signers }] }.

funcs-all

fa or funcs-all

Currently identical to funcs: both dispatch to the same Funcs command and return InstructionList. The admin-gating and coupling hints surface separately via info (admin_gated flag) and coupling.

ilold[staking]> fa
  [PDA] initialize_pool (args:1 accounts:3) signers: admin
  [PDA] stake (args:1 accounts:4) signers: user
  ...

Returns: InstructionList { items: [...] }: same shape as funcs.

info

i <ix> or info <ix>

Full detail of an instruction: discriminator, typed args, accounts with their signer / writable / optional flags and kind (system, sysvar, program, pda, other), declared PDAs with their seeds, and the admin-gating heuristic.

ilold[staking]> i stake
  instruction stake
  discriminator 0xa1b2c3...

  args (1)
    · amount u64

  accounts (4)
    · pool         other  writable
    · user_stake   pda    writable
    · user         other  signer writable
    · system_program  program  const 11111111111111111111111111111111

  pdas (1)
    · user_stake seeds=["user-stake", user] program=self

  admin_gated false

Returns: IxInfo { ix: IxView, admin_gated: bool }. IxView carries name, discriminator_hex, args, accounts, and per-account pda metadata.

See also: funcs-all, pda, who, call.

vars

v or vars

Lists the account types declared in the IDL, with their Anchor discriminator and field layout. Solana does not split vars / vars-all: both alias to the same Vars command.

ilold[staking]> v
  [T] Pool 0x4e2a...
    · admin           Pubkey
    · reward_rate     u64
    · total_staked    u64
    · last_update_ts  i64
  [T] UserStake 0x8c91...
    · user            Pubkey
    · amount          u64
    · reward_debt     u64

Returns: AccountTypes { accounts: [AccountView { name, discriminator_hex, fields: [FieldView { name, ty }] }] }.

vars-all

va or vars-all

Aliased to vars at the dispatcher level: same command, same output.

Notes

  • The Solidity counterpart of these commands is documented under Contract. The shapes line up so an auditor moving between backends sees the same structure.
  • use clears the displayed step list for the previous program. The underlying scenario state for that program is preserved and reappears when switching back.

Solana Runtime Commands

These commands operate directly on the LiteSVM owned by the active scenario. They have no Solidity counterpart.

users

users: list keypairs in the active scenario.

users new <name> [lamports]: create a keypair and airdrop it. Default airdrop is 10_000_000_000 lamports (10 SOL).

ilold[staking]> users new alice
  ✓ user alice created at 8H7…Pq with 10000000000 lamports

ilold[staking]> users new bob 5000000000
  ✓ user bob created at 6Yg…Tx with 5000000000 lamports

ilold[staking]> users
  [U] alice 8H7…Pq 10000000000 lamports
  [U] bob 6Yg…Tx 5000000000 lamports

Returns: UserList { users: [{ name, pubkey, lamports }] } on listing, UserCreated { name, pubkey, lamports } on users new.

airdrop

airdrop <user> <lamports> (alias: air)

Tops up an existing keypair with extra lamports.

ilold[staking]> airdrop alice 1000000000
  ✓ alice now 11000000000 lamports 8H7…Pq

Returns: Airdropped { name, pubkey, total_lamports }.

time-warp

tw <delta_seconds> or time-warp <delta_seconds>

Advances (or rewinds) the Clock sysvar so vesting / reward / lockup logic can be exercised. Positive deltas move forward, negative deltas move backward.

ilold[staking]> tw 86400
  ✓ clock now ts=1714147200 slot=12345

ilold[staking]> tw -3600
  ✓ clock now ts=1714143600 slot=12345

Returns: TimeWarped { unix_timestamp, slot }.

time-warp is a global side effect on the Clock sysvar and is not undone by back. It is the auditor’s responsibility to reset the clock manually if a later test expects a specific timestamp. Negative deltas adjust unix_timestamp linearly but do not move the slot counter backwards.

pda

pda <ix>

Lists the PDAs declared by an instruction (Anchor seeds plus bump). Read directly from the IDL, no VM execution required.

ilold[staking]> pda stake
  [PDA] user_stake seeds=["user-stake", user] program=self

Returns: PdaList { instruction, pdas: [{ account_name, seeds, program }] }.

inspect

inspect <pubkey> (alias: acc)

Reads an account from the VM and decodes it via the Anchor discriminator. The pubkey can be a named keypair, a named PDA from a previous step, or a raw base58 string.

ilold[staking]> inspect alice
  8H7…Pq owner=11111111111111111111111111111111 lamports=10000000000 data_len=0

ilold[staking]> inspect 6Yg7...
  6Yg7…ABCd owner=StakingProgram… lamports=2039280 data_len=72
    {
      "admin": "AdminPubkey…",
      "reward_rate": 10,
      "total_staked": 1000,
      "last_update_ts": 1714060800
    }

Returns: AccountInspected { pubkey, owner, lamports, data_len, decoded }. decoded is Some(Value) when the Anchor discriminator matches a known account type, otherwise None.

Notes

  • All runtime commands are scoped to the active scenario. Forks own their own VM and their own keypair set; switching scenarios swaps the runtime.
  • inspect is the easiest way to confirm what state and timeline will surface for a given account.

Analysis Commands

The Solana analysis surface is partial: there is no static CFG yet, so slice, trace, and the dedicated sequence narrative are not implemented (sequence is aliased to session). The following commands are available today:

CommandStatusWhat it reads
who <query>worksIDL: account types, instructions, struct fields
timeline <pubkey>worksaccount diffs accumulated by the active scenario
couplingworksIDL + accounts metadata
coverageworksruntime metrics over the active scenario
slice / tracenot implementedrequires the Anchor handler AST (Phase 2)

who

who <AccountType | ix_name | field_name>

Resolves a query against the IDL. The same command answers three different questions depending on the input.

Account type: list instructions that reference accounts of that type. The lookup is case-insensitive with a snake_case → PascalCase fallback, so who pool and who Pool both work.

ilold[staking]> who Pool
  · 'Pool' (account type)
    fields: admin: Pubkey, reward_rate: u64, total_staked: u64, last_update_ts: i64

  Referenced by 5 instructions:

    · initialize_pool (as pool) writable
        args: reward_rate: u64
    · stake (as pool) writable
        args: amount: u64
    · unstake (as pool) writable
        args: amount: u64
    · add_rewards (as pool) writable
        args: amount: u64
    · claim_rewards (as pool) writable
        args: (none)

Instruction: list accounts the instruction touches, plus its args and discriminator:

ilold[staking]> who claim_rewards
  · 'claim_rewards' (instruction)
    args: (none)
    discriminator 0xa1b2c3...

  Touches 4 accounts:

    · pool (Pool) writable
    · user_stake (UserStake) writable
    · user signer
    · reward_vault writable

Field: identify the owning type and the instructions that write the owner account (heuristic without source-level analysis we cannot tell which writer actually mutates this field).

ilold[staking]> who total_staked
  · 'total_staked' (field of Pool, type u64)
    Pool struct: admin: Pubkey, reward_rate: u64, total_staked: u64, last_update_ts: i64

  Heuristic: the following instructions write the owner account.
  Without source-level analysis we cannot tell which one(s)
  actually mutate this field; cross-check with `step <idx>`.

    · stake (as pool) writable
    · unstake (as pool) writable

Returns: WhoList { account_type, instructions, query_kind, field_owner, field_type, owner_fields, ix_args, ix_discriminator_hex, ix_accounts }. query_kind is one of AccountType, Field, Instruction, NotFound.

See also: info, funcs, vars, coupling.

timeline

tl <pubkey> or timeline <pubkey>

Shows the cross-step mutation history of an account, decoded. The pubkey can be a named keypair, a named PDA, or a raw base58 string.

ilold[staking → initialize_pool → stake]> tl pool
  · timeline for pool (7XzG…ABCd)
    · #0 initialize_pool (main) data
        {"admin":"AdminPubkey…","reward_rate":10,"total_staked":0,"last_update_ts":0}
    · #1 stake (main) data
        {"admin":"AdminPubkey…","reward_rate":10,"total_staked":1000,"last_update_ts":1714060800}

Returns: TimelineView { pubkey, label, entries: [{ step_index, instruction, scenario, lamports_delta, data_changed, before_decoded, after_decoded }] }.

coupling

cp or coupling

Lists instruction pairs that share a writable account. Surfaces instructions that may interfere through shared writable state (ProgramView heuristic).

ilold[staking]> coupling
  · stake  ↔  unstake          [pool, user_stake]
  · stake  ↔  claim_rewards    [pool, user_stake]
  · add_rewards  ↔  claim_rewards    [pool]

Returns: CouplingList { pairs: [{ a, b, shared_writable: [..] }] }.

coverage

cov or coverage

Aggregated runtime metrics over the active scenario: calls, failures, CU stats, CPI edges (RuntimeOverlay).

ilold[staking → initialize_pool → stake]> cov
  Coverage for program staking (scenario main)

  Instruction        Calls Failed CU avg CU max CPIs
  initialize_pool    1     0      12400  12400  0
  stake              1     0      18700  18700  0

  Total: 2 calls, 0 failed

Returns: Coverage { overlay: { program, scenario, calls_per_ix, failed_per_ix, cu_stats_per_ix, cpi_edges } }.

Coverage is the closest current surrogate for “have I exercised every instruction?”: it makes it easy to spot instructions never called, instructions that always fail, and programs reached only through CPI.

Notes

  • See Solana: Limitations for the static-analysis gap (no CFG → no slice / trace yet).
  • The Solidity counterparts of who and timeline work on state variables; the Solana versions work on account types and pubkeys respectively. The mental shift is the same (“what touches this piece of state?”) but the units of state are different.

Findings Commands

Findings, notes, and per-instruction status flags are recorded against the active scenario and aggregated by export. The Solana export adds runtime metadata (CU, logs, account diffs) to each step in the report.

finding

fi <severity> <title> or finding <severity> <title> (alias: fi)

Records a security finding tied to the latest step of the active scenario.

Flags:

FlagDescription
--rec="..."Optional remediation recommendation. Quote it if it contains spaces.

Valid severities: critical, high, medium, low, info.

ilold[staking → … → stake]> fi high reentrancy via stake
  ✓ finding F-001
ilold[staking → … → claim_rewards]> finding critical missing signer --rec="require admin signature"
  ✓ finding F-002

Returns: FindingAdded { id }.

findings

fl or findings

Lists every finding recorded in the active scenario, with severity, title, the step it is attached to, and the optional remediation.

ilold[staking]> fl
  F-001 high [2026-05-09T10:12:00Z] reentrancy via stake
  F-002 critical [2026-05-09T10:14:00Z] missing signer
    require admin signature

Returns: FindingsList { items: [{ id, severity, title, description, created_at }] }.

note

n <text> or note <text>

Attaches a free-form annotation to the active scenario. Notes are stored alongside findings and surface in the exported report.

ilold[staking → … → stake]> n suspicious admin path here
  ✓ note recorded

Returns: NoteAdded.

status

status <ix> <open | reviewed | finding>

Sets the review status of an instruction. Useful for tracking audit progress.

ilold[staking]> status stake reviewed
  ✓ status updated
ilold[staking]> status claim_rewards finding
  ✓ status updated

Note: Solana statuses are intentionally narrower than the Solidity equivalent: only open, reviewed, finding (alias found). Solidity supports reviewed, suspicious, vulnerable, clean, inprogress, notreviewed.

Returns: StatusUpdated.

export

ex or export

Generates a Markdown deliverable aggregating audit metadata, severity matrix, methodology, findings (with step index, recommendation, and runtime metadata) and per-scenario step lists across all scenarios.

Flags:

FlagDescription
--auditor=<name>Auditor identity in the report metadata
--version=<v>Project version pinned in the report
--date=<YYYY-MM-DD>Audit date override (defaults to today)
ilold[staking]> export
  ✓ markdown report (4321 bytes)

  # ilold audit report
  ...

ilold[staking]> export --auditor="Alba S." --version=v1.2 --date=2026-05-09
  ✓ markdown report (4567 bytes)

Returns: Exported { markdown, bytes }. The CLI prints the full Markdown body after the header line.

Notes

  • Findings are scoped to the scenario they were recorded in but the export merges all of them.
  • The Solidity equivalent (see Solidity: Findings) does not support the --rec=, --auditor=, --version=, --date= flags; the report there is simpler.

Scenario Commands

Scenarios are independent branches of the session. On Solana, each scenario owns its own VM and its own keypair set: a fork carries the parent’s VM state up to the fork step, then diverges. This is the core mechanism for testing “what happens if step 2 fails differently?” without losing the original timeline.

scenario new

sc new <name> or scenario new <name>

Creates an empty scenario with a fresh VM (the program is reloaded, no users yet). The new scenario is not activated automatically.

ilold[staking]> sc new attack
  ✓ scenario attack created

Returns: ScenarioCreated { name }.

scenario list

sc list (aliases: sc ls, scenario list, bare sc)

Lists every scenario in the active session with the active marker and step count.

ilold[staking]> sc list
  [S] main (3 steps) ← active
  [S] attack (0 steps)

Returns: ScenarioList { items: [ScenarioInfo { name, step_count, active }] }.

scenario switch

sc switch <name>

Activates an existing scenario. The VM, keypairs, and step list are swapped to that scenario’s state.

ilold[staking]> sc switch attack
  → main → attack
ilold[staking/attack]>

Returns: ScenarioSwitched { from, to }.

scenario fork

sc fork <name> [step]

Creates a new scenario branching from the active one. With [step], the new scenario inherits steps 0..step and the VM is rewound to the pre-call snapshot of that step. Without [step], the fork inherits the full step list and the VM state at HEAD. The new scenario is activated after forking.

ilold[staking → … → claim_rewards]> sc fork attack-v2 1
  ✓ forked main → attack-v2 at step 1
ilold[staking/attack-v2 → initialize_pool]>

Returns: ScenarioForked { from, to, at_step }.

The fork keeps the parent’s keypair definitions so PDAs derived from them resolve to the same addresses (provided you opt into deterministic keypairs via save --with-keypairs; see Workspace).

scenario delete

sc delete <name> (aliases: sc rm <name>)

Removes a scenario. The active scenario cannot be deleted; switch first.

ilold[staking]> sc delete attack
  ✓ scenario attack deleted

Returns: ScenarioDeleted { name }.

Notes

  • back and clear rewind the VM of the active scenario only, never the fork’s parent. Diverging via fork is the only way to keep both timelines side by side.
  • time-warp is a per-scenario side effect on the Clock sysvar, but is not reverted by back.
  • See Solana: Scenarios and forks for an end-to-end workflow.
  • The Solidity counterpart is documented at Solidity: Scenarios; the command surface is identical, but Solidity scenarios share the parsed model (no VM clone needed).

Workspace Commands

Workspace commands handle persistence and external tooling. On Solana, save / load cover not just the step list but the entire scenario tree, including runtime traces, findings, fork origins, and the original call payloads needed to replay each call against a fresh VM.

save

save <name>

Serialises the active session to ~/.ilold/sessions/<name>.json.

Flags:

FlagDescription
--with-keypairsBundle plaintext test keypairs for deterministic reload. Do NOT commit the resulting file.
ilold[staking]> save reentrancy-attack
  ✓ session JSON (8421 bytes)

ilold[staking]> save reentrancy-attack --with-keypairs
  ✓ session JSON (9532 bytes)

Returns: SessionSaved { json }. The CLI prints the JSON byte count. The file is written by the dispatch layer; warnings about bundled keypairs come from the surrounding CLI flow, not the result variant itself.

Without --with-keypairs, load regenerates user keypairs and any PDA derived from a signer pubkey will resolve to different addresses on reload. With the flag, the JSON embeds the keypairs in plaintext so the next load reproduces the exact same pubkeys.

load

load <name>

Reads ~/.ilold/sessions/<name>.json, boots a fresh VM per scenario, re-airdrops the in-memory users, and replays each call from the persisted payload.

ilold[staking]> load reentrancy-attack
  ✓ loaded program=staking steps=3

Returns: SessionLoaded { program, steps } where steps is the list of instruction names that were replayed.

The reconstructed VM state matches the saved snapshot for typical Anchor flows. Programs with non-deterministic behaviour or balances above the default replay cap may diverge; see Solana: Limitations.

browser

browser

Prints the base URL of the local HTTP API. The web canvas (crates/ilold-web/frontend) subscribes to the same /api and /ws endpoints, so anything the REPL runs surfaces there live.

ilold[staking]> browser
  · Web UI not yet available in explore mode.
  · API running at http://127.0.0.1:8080/api/

See HTTP API Reference for the full surface and WebSocket events for the live update stream.

quit

q, quit, or exit

Exits the REPL. Ctrl+D and Ctrl+C also work.

Unsaved scenarios are lost on exit. Use save before quitting if the session needs to survive.

Help and Control

These commands print the command menu, the structured help block for an individual command, or exit the REPL.

help

?, h, or help

Prints the top-level command menu grouped by category (Session, Programs, Solana runtime, Analysis, Findings, Workspace). Hint at the bottom reminds the auditor that appending ? to any command prints the full reference block for that command.

ilold[staking]> ?

  ilold explore — append ? to any command for inline help (e.g. sl?)

  Session
    c  | call <ix> arg=val acc=user   Concise: keys auto-distributed; signers auto from IDL
    b  | back                          Remove last step from active scenario
    cl | clear                         Reset active scenario steps
       | state                         Decoded view of accounts mutated this session
    s  | session                       Active scenario summary (steps + findings)
  ...

inline help

<command>?

Renders the structured help block for that command: purpose, syntax, flags, examples, return shape, and related commands. The block lives in crates/ilold-cli/src/help.rs::SOLANA_HELP_BLOCKS and is the canonical reference for every Solana command.

ilold[staking]> call?

  c | call

  Purpose
    Run an Anchor instruction against the LiteSVM and append the result as
    a step on the active scenario.

  Syntax
    c <ix> arg=val acc=user   Concise key=value form (signers auto-resolved from IDL)
    c <ix> {json}             Full JSON form: {"args":{...},"accounts":{...},"signers":[...]}

  Flags
    --signer=a,b      Add signers (override IDL defaults)
    --no-signer=name  Remove a default signer (test negative cases)

  Examples
    c stake amount=1000 pool=pool user_stake=alice_stake user=alice
    c initialize_pool reward_rate=10 pool=pool admin=admin
    c stake {"args":{"amount":1000},"accounts":{"pool":"pool", ...}}

  Returns
    StepAdded { step_index, instruction, logs_excerpt, account_diffs_count, compute_units }
    on success, or CallFailed { ... } when the VM rejects.

  See also
    info, pda, state, step, back

Lookup is case-insensitive: CALL?, call?, and c? all return the same block.

sequence

seq or sequence

Solana has no dedicated cross-step narrative engine yet (Phase 2, see Roadmap). seq is aliased to session: it prints the active scenario summary so an auditor who reaches for seq out of habit still gets a useful view.

quit

q, quit, or exit

Exits the REPL. Ctrl+D and Ctrl+C also work.

Notes

  • The full list of registered command aliases is enforced by the test every_solana_command_has_a_help_block in crates/ilold-cli/src/help.rs. New commands without a corresponding HelpBlock break the build.
  • The Solidity REPL uses a flat one-line inline-help table (see print_inline_help); Solana uses the structured HelpBlock format above. Both respond to the <cmd>? trailing syntax.

Solana Audit Walkthrough

This walkthrough mirrors the Solidity audit walkthrough against the canonical Solana staking fixture under tests/fixtures/solana/staking. The program exposes five instructions — initialize_pool, stake, unstake, add_rewards, claim_rewards — and ships with a pre-built bin/staking.so so the suite runs without the Anchor toolchain.

Starting the session

Launch ilold against the Anchor workspace:

ilold explore tests/fixtures/solana/staking

The REPL boots a LiteSVM with bin/staking.so and auto-selects the program:

ilold[staking]>

List the instructions and account types to orient yourself:

ilold[staking]> f
  initialize_pool   args=1   accounts=3   signers=1   pdas=1
  stake             args=1   accounts=4   signers=1   pdas=1
  unstake           args=1   accounts=4   signers=1   pdas=1
  add_rewards       args=1   accounts=2   signers=1   pdas=0
  claim_rewards     args=0   accounts=4   signers=1   pdas=1

ilold[staking]> v
  Pool          disc=0x4e2a...
  UserStake     disc=0x8c91...

This is the starting map: five entry points, two account types.

Creating users and bootstrapping the pool

Solana sessions need accounts to drive. Mint the keypairs you need:

ilold[staking]> users new admin 100000000
  ✓ admin   pubkey=AdminPk…   balance=0.1 SOL

ilold[staking]> users new pool 2000000
ilold[staking]> users new alice 50000000
ilold[staking]> users new alice_stake 2000000

users new creates a keypair and airdrops it. Unmapped account names in a later call will be coerced to local keypairs as well, but pre-creating them makes the session deterministic.

Now initialize the pool:

ilold[staking]> call initialize_pool reward_rate=10 pool=pool admin=admin
  + Step 0: initialize_pool   CU 12.4k   1 account written

call runs the instruction in the VM and appends the result. The output lists CU consumed and the number of mutated accounts.

Staking and observing state

ilold[staking → initialize_pool]> call stake amount=1000 pool=pool user_stake=alice_stake user=alice
  + Step 1: stake   CU 18.7k   2 accounts written

Check the accumulated state:

ilold[staking → … → stake]> state
  Pool 7XzG…ABCd
    admin           = AdminPk…
    reward_rate     = 10
    total_staked    = 1000
  UserStake 6Hj…Pq
    user            = AlicePk…
    amount          = 1000
    reward_debt     = 0

Inspecting a step in detail

step <i> re-prints the persisted CU, logs, and decoded diffs for a specific step:

ilold[staking → … → stake]> step 1
  step 1   stake
    CU       18.7k
    accounts written:
      Pool 7XzG…ABCd   total_staked: 0 → 1000
      UserStake 6Hj…Pq amount: — → 1000
    logs:
      Program log: Instruction: Stake
      ...

Cross-step questions

who resolves a query against the IDL. Use it to find which instructions touch a given account type:

ilold[staking → … → stake]> who Pool
  AccountType: Pool
  Instructions:
    initialize_pool   accounts: pool [init, mut]
    stake             accounts: pool [mut]
    unstake           accounts: pool [mut]
    add_rewards       accounts: pool [mut]
    claim_rewards     accounts: pool [mut]

timeline <pubkey> shows how a specific account has evolved across the session, with decoded field diffs:

ilold[staking → … → stake]> timeline pool
  Pool 7XzG…ABCd
    step 0  initialize_pool
      admin         = AdminPk…   (— → AdminPk…)
      reward_rate   = 10          (— → 10)
      total_staked  = 0           (— → 0)
    step 1  stake
      total_staked  = 1000        (0 → 1000)

Recording findings and exporting

Capture an observation as a note and record a finding tied to the latest step:

ilold[staking → … → stake]> n staking does not enforce min stake amount
  ✓ Note added

ilold[staking → … → stake]> finding High "missing reentrancy guard" --rec="Apply checks-effects-interactions"
  ✓ Finding F-001 added (High)

Mark instructions as you go:

ilold[staking]> status stake reviewed
ilold[staking]> status claim_rewards finding

Export the deliverable:

ilold[staking]> export --auditor="Demo Auditor" --version="v0.1.0" --date=2026-05-09
  ✓ Exported

Persisting the session

ilold[staking]> save my-audit --with-keypairs
  ✓ Saved to ~/.ilold/sessions/my-audit.json
  ⚠  bundle includes plaintext test keypairs — do NOT commit it

ilold[staking]> clear
  Cleared 2 step(s).

ilold[staking]> load my-audit
  ⚠  bundle contains plaintext test keypairs — do NOT commit *.json files like this
  ✓ Session loaded (2 steps)

--with-keypairs is mandatory when the audit relies on deterministic PDAs (which depend on signer pubkeys): without it, load regenerates fresh keypairs and PDAs come back at different addresses.

Parallel to the Solidity walkthrough

The flow is structurally identical to the Solidity one:

  1. f / v to map the surface (instructions and account types instead of functions and state variables).
  2. users new + call to push the VM forward (vs. c <func> on a parsed CFG).
  3. state, step, timeline to inspect what changed.
  4. who to navigate cross-instruction relationships (vs. cross-function in Solidity).
  5. finding, note, status to record observations.
  6. export, save, load to ship the deliverable and resume later.

What is not available yet on Solana: slice and trace. Both require the Anchor handler AST and are tracked in Roadmap: Solana Phase 2.

Scenarios and Forks

Scenarios are the auditor’s tool for asking “what if?” against a real VM without losing the current line of reasoning. This page walks through a forking session against tests/fixtures/solana/staking and shows how back, clear, fork, and time-warp interact.

Setup

Boot the REPL with the staking fixture and set up the happy path:

ilold[staking]> users new admin 100000000
ilold[staking]> users new pool 2000000
ilold[staking]> users new alice 50000000
ilold[staking]> users new alice_stake 2000000

ilold[staking]> call initialize_pool reward_rate=10 pool=pool admin=admin
  + Step 0: initialize_pool   CU 12.4k

ilold[staking]> call stake amount=1000 pool=pool user_stake=alice_stake user=alice
  + Step 1: stake   CU 18.7k

ilold[staking → initialize_pool → stake]>

Branching with scenario fork

Now diverge: what happens if a malicious caller tries unstake for more than the staked amount?

ilold[staking → initialize_pool → stake]> sc fork over-unstake 2
  ✓ Forked 'main' → 'over-unstake' at step 2
ilold[staking/over-unstake → initialize_pool → stake]>

The fork inherits steps 0..2 from main and rewinds the VM to the snapshot taken just before step 2 would have executed. The new scenario is active. The main scenario keeps its original state untouched.

Try the attack on the fork:

ilold[staking/over-unstake → initialize_pool → stake]> call unstake amount=999999 pool=pool user_stake=alice_stake user=alice
  ✗ Step rejected   CU 7.2k
    error: Custom { code: 6001 }   "InsufficientStake"

CallFailed is recorded as a step; the VM stays at the pre-call snapshot.

Rewinding with back

If you want to retry, back drops the failed step and rewinds the VM:

ilold[staking/over-unstake → … → stake]> b
  - Step removed. 2 remaining.

ilold[staking/over-unstake → … → stake]> call unstake amount=500 pool=pool user_stake=alice_stake user=alice
  + Step 2: unstake   CU 16.1k

The replayed call produces fresh CU and diffs because the VM was rewound, not a cached replay.

Switching back to main

ilold[staking/over-unstake → … → unstake]> sc switch main
ilold[staking → initialize_pool → stake]>

main still has its original two steps; the fork’s state never leaked. Each scenario carries its own VM, signers, and PDAs.

Time-warping vesting / reward logic

time-warp advances the Clock sysvar. It is scenario-local but step-independent: back does not rewind the clock. Use it to exercise reward accrual:

ilold[staking → … → stake]> tw 86400
  ✓ Clock unix_timestamp += 86400

ilold[staking → … → stake]> call claim_rewards pool=pool user_stake=alice_stake user=alice
  + Step 2: claim_rewards   CU 22.3k

If a later step needs a different clock, undo the offset manually with tw -86400.

Persisting the scenario tree

save and load cover the whole scenario tree, not just the active one. Use --with-keypairs whenever a PDA depends on a signer pubkey (most Anchor programs):

ilold[staking]> save staking-attack --with-keypairs
  ✓ Saved to ~/.ilold/sessions/staking-attack.json
  ⚠  bundle includes plaintext test keypairs — do NOT commit it

ilold[staking]> clear
ilold[staking]> load staking-attack
  ⚠  bundle contains plaintext test keypairs — do NOT commit *.json files like this
  ✓ Session loaded (2 steps)

load reboots a fresh VM per scenario, re-airdrops the users, and replays each call. The final state matches the saved snapshot for typical Anchor flows. Non-deterministic programs may diverge, see Solana: Limitations.

Practical patterns

PatternCommands
“Keep the original timeline, try a divergent path”sc fork <name> <step>, then continue with call
“Retry the last failed call”b, edit args, call again
“Start over without losing other scenarios”cl on the active scenario, then re-issue calls
“Reproduce later, same addresses”save <name> --with-keypairs, load <name>
“Reproduce later, fresh randomness”save <name>, load <name>

Solana Known Limitations

These boundaries reflect the current Solana backend. The corresponding Roadmap entry tracks the Phase 2 work that will lift each of them.

No static control-flow analysis

Solana programs are loaded as compiled binaries. There is no parsed handler AST and no per-instruction CFG, so the commands that depend on it are not implemented:

  • slice <fn> <var>: needs Anchor handler AST (see Solana roadmap).
  • trace <fn>: same reason.
  • sequence: aliased to session; no narrative engine with cross-step dependencies yet.

who is heuristic

who <field_name> infers the owning account type via a snake_case → PascalCase fallback against the IDL. Programs with non-conventional naming will miss matches. who <AccountType> and who <ix_name> use exact lookups and are reliable.

time-warp is one-way for slot

time-warp <delta> advances unix_timestamp linearly for both positive and negative deltas. The slot counter only moves forward; negative deltas do not rewind it. Programs that key off slot rather than unix_timestamp may see inconsistent values after a backward warp.

time-warp is not rewound by back

back drops the last step and rewinds the VM to the pre-call snapshot of that step, but time-warp is a separate side effect on the Clock sysvar and is not undone. The auditor must reset the clock manually with an inverse tw if a later step expects a specific timestamp.

save / load regenerates keypairs by default

Without --with-keypairs, save does not embed the test keypairs. On load, a fresh keypair is generated for each user; any PDA derived from a signer pubkey resolves to a different address than the original session.

Pass save <name> --with-keypairs to opt into deterministic reload. The resulting JSON contains plaintext keypairs and must not be committed to public repositories. The CLI prints a reminder at both save and load time.

Legacy load without call_payload

LoadSession is best-effort for legacy saves missing the call_payload field. The timeline restores, but the VM stays at genesis (no replay).

CPI visibility in the UI

Cross-program CPI calls are exercised correctly by the VM and surface in logs, but the web canvas does not yet have a dedicated visualisation for CPI edges. See Solana roadmap for the dedicated CPI view.

Flat bipartite CFG visual

The web canvas renders Solana state as a flat bipartite graph (instructions ↔ accounts). The Solidity-style CFG visual is not implemented; See Solana roadmap for CFG visual parity.

HTTP API Reference

ilold exposes an HTTP API on the configured port (default 3001). All endpoints return JSON. The API is split into three groups: command bus, session queries, and contract queries.

Command bus

POST /api/cmd

Execute a session command. This is the single entry point for all state-mutating session operations.

Request body:

{
  "contract": "Staking",          // optional, defaults to session contract
  "command": { "Call": { "func": "deposit" } }
}

Supported commands:

CommandPayloadDescription
Call{ "func": "deposit", "trace_config": null }Add a function call as a session step
Back"Back"Remove the last step
Clear"Clear"Remove all steps
State"State"Return accumulated state variable summary
Functions"Functions"List functions of the current contract
FunctionsAll"FunctionsAll"List all accessible functions including inherited
StateVarsAll"StateVarsAll"List all accessible state variables including inherited
Who{ "variable": "totalStaked" }Find all writers and readers of a variable
Finding{ "severity": "High", "title": "...", "description": "..." }Record an audit finding
Note{ "text": "..." }Record a free-text note
Status{ "func": "deposit", "status": "Reviewed" }Set review status for a function
Session"Session"Return session overview (contract, steps, finding count)
Export"Export"Export session journal as markdown
SaveSession"SaveSession"Serialize session to JSON
LoadSession{ "json": "..." }Restore session from serialized JSON

Response: A CommandResult variant matching the command type. Key variants:

  • StepAdded: step_index, function, access, state_changed (list of variable names)
  • StateView: summary (list of variable summaries with writers per step)
  • FunctionList: functions (name, access level, writes_state, has_external_calls)
  • VariableInfo: variable, writers (name + access), readers (name + access)
  • Error: message

Session query endpoints

These endpoints read from the active session. They return 404 if no session exists (no Call command has been issued).

GET /api/session/state

Returns the accumulated state variable summary across all session steps.

Response: Array of VariableSummary objects with variable name, type, and per-step write details.

GET /api/session/sequence

Returns a sequence narrative describing the relationship between session steps. Requires at least 2 steps.

Response: SequenceNarrative with per-step summaries and flow summaries derived from persisted flow trees.

GET /api/session/step/{index}/narrative

Returns the function narrative for a specific session step.

Path params: index – zero-based step index.

Response: FunctionNarrative with paths, state writes, external calls, and conditions.

GET /api/session/step/{index}/trace

Returns the persisted FlowTree of a session step. This is the tree captured when Call was executed, not a recomputation.

Path params: index – zero-based step index.

Response: FlowTree with step nodes, each containing operation type, target, conditions, and child steps. Returns 404 if the step has no persisted tree (pre-Phase-2a sessions).

GET /api/session/timeline/

Returns a chronological timeline of every write to variable across all session steps. Matches by base name (e.g., balances matches balances[msg.sender]).

Path params: variable – state variable name.

Response: VariableTimeline with state_entries and local_entries. Each entry contains: session_step_index, function, target, operator, value_expr, reached_when (path conditions), via (modifier name if applicable), scope.

GET /api/session/slice/{function}/

Returns a dataflow slice for variable inside function of the session’s current contract.

Path params: function – function name, variable – variable name.

Query params:

ParamValuesDefaultDescription
directionbackward, forward, both, b, f, allbothSlice direction

Response: SliceResult with backward and forward arrays. Each entry contains: path, span (source location), text (rendered statement), origin (FunctionBody or Modifier(name)).

GET /api/session/trace/{contract}/

Returns a FlowTree for the given function, computed on demand from the current analysis data.

Path params: contract – contract name, func – function name.

Query params:

ParamTypeDefaultDescription
depthinteger2Maximum inline depth for internal calls
revertsbooleanfalseInclude revert paths in the tree
expandstringemptyComma-separated step IDs to force-inline beyond max depth (e.g., 17,24)

Response: FlowTree with nested step nodes representing the execution flow.

GET /api/session/function/{contract}/

Returns a function narrative without requiring a session step. Useful for inspecting functions that have not been called in the session.

Path params: contract – contract name, func – function name.

Response: FunctionNarrative with paths, state effects, and behavioral summary.

Contract query endpoints

These endpoints read from the static analysis data and do not require an active session.

GET /api/project

Returns a project summary with file count and a list of contracts (name, kind, function count, state variable count, inheritance).

GET /api/project/map

Returns the full project map with all contracts, their functions (with path counts and external call flags), state variables, and cross-contract relationships extracted from call graphs.

GET /api/contract/

Returns contract detail: name, kind, inheritance chain, functions (with path stats), state variables, and inherited functions and state variables.

GET /api/contract/{name}/callgraph

Returns the call graph for a contract in Cytoscape-compatible JSON format. Nodes represent functions (with contract, type, external flag). Edges represent calls (with kind and count).

GET /api/contract/{name}/{func}/cfg

Returns the control flow graph for a function in Cytoscape-compatible JSON format. Nodes represent basic blocks (entry, normal, return, revert, assembly, loop). Edges represent control flow transitions.

GET /api/contract/{name}/{func}/paths

Returns the path tree for a function. Contains all execution paths with stats (total, happy, revert) and per-path annotations (state writes, external calls, events).

GET /api/contract/{name}/sequences

Returns the sequence tree for a contract, showing function interaction patterns.

GET /api/contract/{name}/analysis

Returns the sequence analysis for a contract: per-function behavior summaries (state writes, state reads, external calls, conditions) and inter-function transition information.

GET /api/contract/{name}/suggestions

Returns search suggestions for the contract: function names, state variable names, event names, external call targets, and predefined categories (revert, return, assembly).

Solana-specific endpoints

Solana shares /api/cmd, /api/project, /api/project/map, /ws, and most session endpoints with the Solidity backend. The following routes are Solana-only:

EndpointDescription
GET /api/program/{name}/viewFull ProgramView for the named program: instructions (with args, accounts, signers, PDAs, admin-gated flag, coupling hints), account types, discriminators.
GET /api/program/{name}/overlayRuntime overlay aggregated over the active scenario: calls-per-instruction, failures, CU stats, CPI edges.
GET /api/users/{scenario}/labelsReturns the keypair labels for a given scenario (used by the web canvas to render users new <name> aliases).
GET /api/scenariosScenario list for the active program (active marker, step counts).
GET /api/scenarios/allScenario list across every program in the workspace.

POST /api/cmd carries a SolanaCommand payload (Call, Users, UsersNew, Airdrop, TimeWarp, Pda, Inspect, Scenario, SaveSession, LoadSession, …; see crates/ilold-solana-core/src/exploration/commands.rs). The response is a SolanaCommandResult variant (StepAdded, CallFailed, StateView, Timeline, Coverage, etc.).

WebSocket

GET /ws upgrades to a WebSocket connection. See WebSocket events for the full event vocabulary and payload shapes.

A second WebSocket route GET /ws/pty provides a PTY bridge used by the embedded REPL in the web canvas.

WebSocket Events

The /ws route emits ServerMessage events (JSON, tagged on a type field) whenever the active session changes. The full mapping from internal CanvasPatch variants to wire events lives in crates/ilold-web/src/ws/handler.rs.

Session events

EventFieldsTrigger
session_add_nodescenario, function, access, step_index, optional runtime (CU + diffs + logs excerpt on Solana)call adds a step
session_remove_nodescenarioback rewinds the last step
session_clearscenarioclear wipes the scenario
session_highlightscenario, functionAuditor selects a step (web canvas)

Scenario events

EventFieldsTrigger
scenario_creatednamescenario new
scenario_switchedfrom, toscenario switch
scenario_deletednamescenario delete
scenario_forkedfrom, to, at_stepscenario fork
scenario_store_reloadedactiveAfter load, when the entire scenario tree is rehydrated

Solana-only events

EventFieldsTrigger
solana_users_changedscenariousers new, airdrop, or anything that mutates the keypair set
session_overlay_updatescenario, ix_name, calls_added, failed_added, optional cu, cpi_targets_addedRuntime overlay aggregates updated after a call

Client → server

The only message the client can send is a search query consumed by crates/ilold-web/src/ws/search.rs. Responses come back as search_result (one per match) and search_complete (total count).

PTY bridge

GET /ws/pty opens a PTY for the embedded REPL in the web canvas. The protocol is binary-passthrough; the wire format is documented inline in crates/ilold-web/src/ws/pty.rs.

MCP Server

ilold ships an MCP (Model Context Protocol) server that exposes the Solana REPL as a set of typed tools. Any MCP-compatible client (Claude Code, Claude Desktop, Cursor, Continue) can invoke those tools to drive an audit programmatically: list instructions, call them against the live LiteSVM, inspect state, record findings, and export the deliverable. The MCP server is a thin transport on top of the existing HTTP API; it adds no new domain logic.

Architecture

LLM client  ──── stdio ────►  ilold mcp  ──── HTTP ────►  ilold serve  ────►  LiteSVM
                                                              │
                                                              └──── WebSocket ────►  Web canvas (optional)

The MCP client launches ilold mcp as a local subprocess and talks to it over stdio (newline-delimited JSON-RPC). The MCP process is stateless: each tools/call translates the arguments into a SolanaCommand and forwards it to a running ilold serve instance via POST /api/cmd. The same backend broadcasts canvas patches over WebSocket, so a browser tab connected to the web canvas reflects every step the LLM takes.

Only Solana is supported in v1. The MCP server refuses to start when the backend reports kind != "solana".

Setup

Two processes need to be running:

  1. Backend: an ilold serve instance pointing at the project to audit.

    ilold serve tests/fixtures/solana/staking --port 8080
    

    The MCP server defaults to http://127.0.0.1:8080, so any free port works as long as --server-url matches.

  2. MCP client: configure the LLM client to spawn ilold mcp (see the client snippets below). The client launches the subprocess on demand and tears it down when the session ends.

The ilold binary must be on the client’s PATH. If it is not, use the absolute path returned by which ilold in the command field.

CLI reference

ilold mcp [OPTIONS]
FlagRequiredDefaultDescription
--server-url <URL>nohttp://127.0.0.1:8080Base URL of the ilold serve instance. Environment variable: ILOLD_SERVER_URL.
--contract <NAME>nounsetOptional initial active program. When unset the LLM (or the user) must call ilold_use <program> before any other tool. Pre-setting it is handy when the workspace has a single program. Environment variable: ILOLD_CONTRACT.
--narrationnooffEmit a notifications/progress MCP message before each tool call describing intent (for example Calling \stake` with amount=1000). Environment variable: ILOLD_NARRATION`.

The MCP server is agnostic to the active contract. A single registration in the client works against multi-program workspaces: the LLM lists programs with ilold_programs and then fixes the active one with ilold_use.

The MCP transport reserves stdout for JSON-RPC; logs and panics go to stderr.

Client configuration

Every snippet below assumes the backend is running on http://127.0.0.1:8080. The MCP server is registered once and stays agnostic to the active program — the LLM calls ilold_use <program> to switch contract during the session. Pre-setting --contract <name> is optional and only seeds the initial value.

Claude Code

Two options. The first is project-scoped (.mcp.json at the repository root, checked into version control); the second is the claude mcp add CLI which writes to ~/.claude.json by default.

.mcp.json:

{
  "mcpServers": {
    "ilold": {
      "command": "ilold",
      "args": [
        "mcp",
        "--server-url", "http://127.0.0.1:8080"
      ]
    }
  }
}

Add "--contract", "<name>" to the args list to pre-set the initial active program.

Equivalent CLI form:

claude mcp add --transport stdio ilold -- ilold mcp --server-url http://127.0.0.1:8080

Claude Desktop

Edit claude_desktop_config.json (Developer → Edit Config in the desktop settings):

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
{
  "mcpServers": {
    "ilold": {
      "command": "ilold",
      "args": [
        "mcp",
        "--server-url", "http://127.0.0.1:8080"
      ]
    }
  }
}

Restart Claude Desktop after saving. The MCP indicator in the input box lists ilold and its tools when the connection is healthy. Append "--contract", "<name>" to args to pre-set an initial program.

Cursor

Place the file at .cursor/mcp.json (project) or ~/.cursor/mcp.json (global):

{
  "mcpServers": {
    "ilold": {
      "command": "ilold",
      "args": [
        "mcp",
        "--server-url", "http://127.0.0.1:8080"
      ]
    }
  }
}

Optional env and envFile keys are supported by Cursor for passing environment variables. Append "--contract", "<name>" to args to pre-set an initial program.

Continue

Continue uses YAML. Edit ~/.continue/config.yaml:

mcpServers:
  - name: ilold
    type: stdio
    command: ilold
    args:
      - mcp
      - --server-url
      - http://127.0.0.1:8080

Append - --contract and - <name> to args to pre-set an initial program.

Tools

The registry is derived at startup from crates/ilold-help/src/lib.rs::SOLANA_HELP_BLOCKS. The table below lists every exposed tool with a one-line summary. Each tool returns the matching SolanaCommandResult variant as structured JSON plus a pretty-printed text block identical to the REPL output.

Discovery (read-only)

ToolPurpose
ilold_programsList every program detected in the workspace.
ilold_funcsList the instructions exposed by the active program.
ilold_funcs_allSame list with admin-gating and coupling hints.
ilold_infoDetail one instruction: args, accounts, signers, PDAs, discriminator.
ilold_varsList declared account types with their Anchor discriminators.
ilold_pdaList the PDAs declared by an instruction (seeds, bumps).
ilold_whoResolve a query against the IDL (account type, instruction, or field).
ilold_couplingList instruction pairs that share a writable account.

Session (mutate the timeline)

ToolPurpose
ilold_callRun an Anchor instruction against LiteSVM and append the result as a step.
ilold_backRemove the last step from the active scenario and rewind the VM.
ilold_clearReset the active scenario steps and the underlying VM state.
ilold_stateDecoded view of every account mutated during the active scenario.
ilold_sessionActive scenario summary: steps, findings, notes.
ilold_stepRe-inspect one step: CU, logs, decoded diffs.

Runtime (mutate the VM)

ToolPurpose
ilold_usersList every named keypair in the active scenario.
ilold_users_newCreate a new keypair and airdrop the initial lamports.
ilold_airdropTop up an existing keypair with extra lamports.
ilold_time_warpAdvance or rewind the Clock sysvar.
ilold_inspectRead a VM account by pubkey and decode it via the Anchor discriminator.

Analysis

ToolPurpose
ilold_timelineCross-step mutation history of an account, decoded.
ilold_coverageAggregated runtime metrics over the active scenario (calls, failures, CU stats, CPI edges).

Scenarios

ToolPurpose
ilold_scenarioManage scenarios: create, list, switch, fork, delete.

Findings and journal

ToolPurpose
ilold_findingRecord a security finding tied to the latest step.
ilold_findingsList every finding recorded in the active scenario.
ilold_noteAttach a free-form annotation to the active scenario.
ilold_statusSet the review status of an instruction: open, reviewed, finding.
ilold_exportGenerate the audit deliverable (Markdown).

Workspace

ToolPurpose
ilold_useSet the active program for the rest of the MCP session. Every other tool call routes to this program.
ilold_saveSerialise the active scenario to ~/.ilold/sessions/<name>.json.
ilold_loadRestore a scenario JSON from disk and replay it into the VM.

Total: 30 tools. The REPL meta commands (?, help, quit, browser, seq) are intentionally excluded: the MCP client discovers tools via tools/list, the subprocess exits on stdin EOF, and the canvas URL is already on the human side.

Switching programs

Multi-program workspaces are handled at runtime, not at registration time:

  1. ilold_programs lists every program detected by the backend. The active one is marked.
  2. ilold_use <program> sets the active program. The handler validates the name against /api/project/map and rejects unknown names.
  3. Subsequent tool calls (ilold_funcs, ilold_call, etc.) route to the active program automatically.

If no contract is active (no --contract flag and no prior ilold_use call), every tool other than ilold_programs and ilold_use returns a clear error asking the LLM to set one. ilold_use can be called any number of times in the same session to switch back and forth between programs.

Example session

A natural-language prompt for an MCP-aware client looks like this:

Audit the staking program. Look for paths where the admin signer check can be bypassed. Create a user alice, run stake for 1000 lamports, and produce a coverage report at the end.

The client typically resolves it as the following tool sequence:

  1. ilold_funcs_all to enumerate instructions and admin-gating hints.
  2. ilold_info on each instruction the model wants to inspect.
  3. ilold_users_new to create alice.
  4. ilold_call for initialize_pool and then stake.
  5. ilold_coverage to read aggregated runtime metrics.
  6. ilold_finding if the model identifies an issue, followed by ilold_export.

Every step also fires a WebSocket patch from ilold serve, so a browser tab pointed at the canvas reflects the graph evolving in real time.

Limitations

  • Solana only. The MCP server refuses to start when the backend is a Solidity project. Solidity support is in the cross-cutting roadmap.
  • Single active program at a time. The handler tracks one active program. Call ilold_use <program> to switch — the MCP subprocess does not need to be restarted to point at a different program in the same workspace.
  • Static tool registry. Tools are derived from SOLANA_HELP_BLOCKS once at startup. Reloading the backend project does not change the tool set; only the data behind the tools.
  • No sandbox over the LLM. Every tool that mutates the VM (ilold_call, ilold_clear, ilold_back, ilold_scenario) is invocable without confirmation from the server. Sandboxing is delegated to the MCP client: mature clients prompt the human before destructive tools (those whose names contain clear, delete, reset).
  • Narration is best-effort. --narration emits a notifications/progress message keyed by the request progressToken. Clients that do not declare a progress token in the request silently drop the notification.
  • stdio only. SSE and streamable HTTP transports are out of scope for v1. Every supported client uses stdio.

Troubleshooting

SymptomLikely cause
Cannot reach Ilold server at <url> on startupilold serve is not running, or --server-url points to the wrong port.
Server at <url> is not a Solana project (kind=solidity)The backend was started against a Solidity workspace. Point ilold serve at a Solana project.
Tools do not appear in the clientThe client could not spawn ilold. Check that the binary is on PATH or use an absolute path in command. Inspect the client log (~/Library/Logs/Claude/mcp-server-ilold.log for Claude Desktop on macOS).
No active contract from every tool but ilold_programsThe session has no active program. Call ilold_use <program> (or restart the subprocess with --contract <name>).
Tool call returns Error: ...The backend rejected the SolanaCommand. The error text is the same as the REPL would print; check the active program (ilold_programs) and the instruction arguments.

Known Limitations

Limitations are documented per backend, since the boundaries are very different:

  • Solidity: Limitations: intraprocedural slicing, assignment-only DEF extraction, modifier placeholder split, tuple destructuring, etc.
  • Solana: Limitations: no static CFG (no slice / trace yet), heuristic who, time-warp semantics, keypair persistence, CPI visibility.

For the planned remediations, see the Roadmap.

Solidity Roadmap

Solidity covers the MVP scope. The items below are tracked enhancements; the section after them is open to ideas.

Slicer precision

The slicer is intraprocedural and assignment-only. Mutations via x++, delete x, arr.push(v) show up as USEs but not DEFs; tuple destructuring may not surface; forward slices include lexical ancestors and can over-taint.

Cross-function dataflow

The slicer stops at call boundaries. Following a value through a helper requires tr <func> (manual inlining) or a separate run on the helper.

Modifier placeholder split

Modifier bodies are split at the first top-level _;. Nested placeholders fall back to “before” code.

Sequence depth bound

--max-seq-depth defaults to 3. Deeper bounds grow combinatorially; no change planned.

Open to ideas

The roadmap is not closed. Examples of integrations we have considered but not started:

  • Foundry: today ilold reads Foundry projects (the multi/ and recursive/ fixtures are Foundry layouts) but does not invoke forge build or forge test. Possible directions include using forge build artefacts as an alternative ingest path, replaying PoCs from findings via forge test --debug, or cross-linking traces.
  • Cross-tool reports: emitting findings in a format consumable by other audit pipelines.
  • New analysis passes: anything that fits the CFG + path-tree model.

If you have a use case the current backend does not cover, open an issue or reach out.

Solana Roadmap

Items below are tracked work without committed dates.

AST via Elozer

Plug Elozer, our in-house static analyzer, into ilold to produce a typed AST for the program source: account validation, state writes, constraints, CPI sites. Foundation for everything below.

CFG on top of the AST

Build the control-flow graph layer on the Elozer AST. Brings Solana to parity with the Solidity CFG view and unlocks slice, trace, and structural narratives.

Detector engine

Detectors for known Sealevel attack patterns (missing signer checks, missing owner checks, account confusion, arithmetic overflow, reinit, PDA seed collision) measured against the public sealevel-attacks corpus. Depends on AST + CFG.

LiteSVM register-tracing bridge

Record concrete values at each VM instruction boundary so the dynamic trace can confirm or refute hypotheses produced by the static layer.

CFG visual parity on the canvas

The web canvas renders Solana state today as a flat bipartite graph (instructions ↔ accounts). The redesigned view will mirror the Solidity CFG: per-instruction control flow, branch nodes, constraint annotations.

CPI graph in the UI

The runtime already records CPI edges (coverage surfaces them in text). A dedicated CPI view in the canvas is the next visual step.

Sequence narrative

sequence is aliased to session on Solana today. A true narrative engine reuses the existing coupling aggregate plus a renderer mirroring the Solidity output.

Open to ideas

The Solana side is younger and the roadmap above is the current shape, not a fixed plan. Examples of directions we are open to:

  • New analysis passes once Elozer’s AST and the CFG layer are in place.
  • Integrations with other Solana tooling (anchor-cli, sealevel-attacks corpus consumers, custom IDL extensions).
  • Alternative VMs or replay engines beyond LiteSVM if a use case justifies it.

If you have a concrete use case the current backend does not cover, open an issue or reach out.

Cross-cutting Roadmap

Elozer integration

Elozer is our in-house static analyzer. It produces a typed AST for smart-contract source today; a CFG layer needs to be built on top before slicing, taint analysis, and detectors can run on the Solana side. Wiring Elozer into ilold provides that AST foundation and unblocks the items listed in the Solana roadmap.