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 driveinfo,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: install and first session.
- Concepts: what the tool does and the data pipeline.
- Solidity Backend: Solidity REPL, CLI, workflows.
- Solana Backend: Solana REPL, runtime commands, workflows.
- Reference: HTTP API and WebSocket events.
- MCP server: drive ilold from an LLM agent.
- Roadmap: known gaps and future work.
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
.solsources is treated as a Solidity project. - A directory containing
Anchor.toml(withidls/<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
| Backend | Input | Execution model | What 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 |
| Solana | Project root with Anchor.toml, idls/<program>.json, target/deploy/<program>.so | Concrete (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
| Crate | Role |
|---|---|
ilold-cli | Argument parsing, REPL, output formatting, key bindings (crates/ilold-cli/src/main.rs, explore.rs, help.rs) |
ilold-web | HTTP + WebSocket API consumed by both the REPL (via --attach) and the web canvas |
ilold-session-core | Shared session abstractions (steps, scenarios, canvas patches) |
ilold-core | Solidity model, CFG, slicer, narrative, sequence analysis |
ilold-solana-core | Anchor 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
.solfile is loaded as-is. - A directory is walked recursively; the directories
out,cache,node_modules,lib,target,.git,.svelte-kitand 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
| Surface | Purpose |
|---|---|
analyze | One-shot pretty-print of every contract: functions, CFG and path-tree stats, sequences up to --max-seq-depth, optional verbose function behavior breakdown |
context | Generate machine-readable narratives for a function or a comma-separated sequence |
serve | Start the HTTP/WS server only, no REPL: feed the web canvas |
explore | Interactive 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:
- Audit walkthrough: full session against a Staking contract.
- Taint analysis: forward slicing of user-controlled parameters.
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]
| Flag | Default | Description |
|---|---|---|
--contract <name> | (all contracts) | Restrict output to a single contract |
--max-seq-depth <N> | 3 | Depth bound for the sequence tree (call combinations up to N steps) |
--verbose | off | Per-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.fnwithinternal | 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
analyzedoes not require a configured project; it works on raw.solfiles.- 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]
| Flag | Description |
|---|---|
--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. |
--list | List 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
contextis read-only; it does not start the API server.--listshort-circuits before computing path trees, so it is cheap on large projects.- Use
explorewhen you need to iterate;contextis 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
| Icon | Meaning |
|---|---|
▶ | 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+Nflags 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/loadand in theexportreport.
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:
functionsshows which functions write state – you call the important ones first.c depositreportsbalances, totalStakedchanged – you runwho totalStakedto see the impact.whoshowsrewardPerTokenreadstotalStaked– you runsl rewardPerToken totalStakedortr rewardPerTokento trace the dependency.sliceshows modifier-origin statements – you trace the modifier withtr depositto see full execution order.stateaggregates writes across steps – variables with writes from multiple functions deservetimelineinspection.timelinereveals 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.
Related pages
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:
- A
requirecheck validates thatamountis positive. - An external call to
stakingToken.transferFromusesamountdirectly. balances[msg.sender]is incremented byamount.totalStakedis incremented byamount.
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 type | deposit | withdraw |
|---|---|---|
| Validation | amount > 0 | amount > 0, balances >= amount |
| External call | transferFrom(sender, this, amount) | transfer(sender, amount) |
| State writes | balances += amount, totalStaked += amount | balances -= 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:
balancesis written in both functions usingamountdirectly. Any arithmetic error in the+=or-=operations would let an attacker manipulate their balance.totalStakedis written in both functions usingamountdirectly. SincerewardPerTokenreadstotalStaked(visible viawho totalStaked), a corruptedtotalStakedwould affect reward calculations for all users.- Neither
rewardPerTokenStorednorrewardsappear in the forward slice ofamount. These variables are written by theupdateRewardmodifier, which derives values fromrewardPerToken()andearned()– not from the caller’samountparameter 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
- Run
fto list entry points. - For each external function with parameters, run
sl <func> <param> --forward. - Note which state variables appear in each forward slice.
- Run
who <var>on each affected state variable to find cross-function readers. - Run
sl <reader> <var> --forwardto trace second-order propagation. - Record findings with
fiwhen a tainted path reaches a sensitive sink without adequate validation.
Related pages
- Full Audit Walkthrough
- Known Limitations – forward slice caveats
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.
Related pages
- Taint Analysis – forward slice caveats in practice
- HTTP API Reference
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
| Concept | Solidity | Solana |
|---|---|---|
| Entry point | function on a contract | instruction on a program |
| Persistent state | contract state variables | accounts owned by the program |
| Caller identity | msg.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 variable | mutation history of an account pubkey, decoded |
step <i> | re-renders the persisted flow tree | re-prints CU, logs, account diffs |
slice / trace | full CFG-based analysis | not implemented (Phase 2) |
sequence | narrative with cross-step dependencies | aliased to session (no narrative engine yet) |
| Execution | symbolic (CFG + paths) | concrete (in-process LiteSVM execution) |
back | drops the step from the timeline | drops the step AND rewinds the VM to the pre-call snapshot |
save / load | step list + persisted paths | step 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 tosession).
Workflows
- Audit walkthrough: staking program end-to-end, paralleling the Solidity walkthrough.
- Scenarios and forks: branching VMs, rewinding the clock, persisting bundles.
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=valueandaccount_field=user_nametokens. Unmapped names become local keypairs. Signers are auto-resolved from the IDL. - JSON form:
{"args": {...}, "accounts": {...}, "signers": [...]}for full control.
Flags:
| Flag | Description |
|---|---|
--signer=a,b | Add signers on top of the IDL defaults |
--no-signer=name | Remove 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 tosessionon Solana; there is no cross-step narrative engine yet. See Roadmap. callis 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.
useclears 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.
inspectis the easiest way to confirm whatstateandtimelinewill 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:
| Command | Status | What it reads |
|---|---|---|
who <query> | works | IDL: account types, instructions, struct fields |
timeline <pubkey> | works | account diffs accumulated by the active scenario |
coupling | works | IDL + accounts metadata |
coverage | works | runtime metrics over the active scenario |
slice / trace | not implemented | requires 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/traceyet). - The Solidity counterparts of
whoandtimelinework 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:
| Flag | Description |
|---|---|
--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:
| Flag | Description |
|---|---|
--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
backandclearrewind the VM of the active scenario only, never the fork’s parent. Diverging viaforkis the only way to keep both timelines side by side.time-warpis a per-scenario side effect on theClocksysvar, but is not reverted byback.- 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:
| Flag | Description |
|---|---|
--with-keypairs | Bundle 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_blockincrates/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:
f/vto map the surface (instructions and account types instead of functions and state variables).users new+callto push the VM forward (vs.c <func>on a parsed CFG).state,step,timelineto inspect what changed.whoto navigate cross-instruction relationships (vs. cross-function in Solidity).finding,note,statusto record observations.export,save,loadto 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.
Related pages
- Session, Programs and IDL, Solana runtime, Analysis, Findings
- Scenarios and forks
- Solana: Limitations
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
| Pattern | Commands |
|---|---|
| “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> |
Related pages
- Scenarios: command reference.
- Workspace: save/load details.
- Solana: Limitations: what survives
save/loadand what doesn’t.
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 tosession; 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.
Related pages
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:
| Command | Payload | Description |
|---|---|---|
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:
| Param | Values | Default | Description |
|---|---|---|---|
direction | backward, forward, both, b, f, all | both | Slice 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:
| Param | Type | Default | Description |
|---|---|---|---|
depth | integer | 2 | Maximum inline depth for internal calls |
reverts | boolean | false | Include revert paths in the tree |
expand | string | empty | Comma-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:
| Endpoint | Description |
|---|---|
GET /api/program/{name}/view | Full ProgramView for the named program: instructions (with args, accounts, signers, PDAs, admin-gated flag, coupling hints), account types, discriminators. |
GET /api/program/{name}/overlay | Runtime overlay aggregated over the active scenario: calls-per-instruction, failures, CU stats, CPI edges. |
GET /api/users/{scenario}/labels | Returns the keypair labels for a given scenario (used by the web canvas to render users new <name> aliases). |
GET /api/scenarios | Scenario list for the active program (active marker, step counts). |
GET /api/scenarios/all | Scenario 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.
Related pages
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
| Event | Fields | Trigger |
|---|---|---|
session_add_node | scenario, function, access, step_index, optional runtime (CU + diffs + logs excerpt on Solana) | call adds a step |
session_remove_node | scenario | back rewinds the last step |
session_clear | scenario | clear wipes the scenario |
session_highlight | scenario, function | Auditor selects a step (web canvas) |
Scenario events
| Event | Fields | Trigger |
|---|---|---|
scenario_created | name | scenario new |
scenario_switched | from, to | scenario switch |
scenario_deleted | name | scenario delete |
scenario_forked | from, to, at_step | scenario fork |
scenario_store_reloaded | active | After load, when the entire scenario tree is rehydrated |
Solana-only events
| Event | Fields | Trigger |
|---|---|---|
solana_users_changed | scenario | users new, airdrop, or anything that mutates the keypair set |
session_overlay_update | scenario, ix_name, calls_added, failed_added, optional cu, cpi_targets_added | Runtime 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.
Related pages
- HTTP API
- Solana REPL: Scenarios: the source of the scenario events.
- Solidity REPL: Scenarios: same events on the Solidity side.
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:
-
Backend: an
ilold serveinstance pointing at the project to audit.ilold serve tests/fixtures/solana/staking --port 8080The MCP server defaults to
http://127.0.0.1:8080, so any free port works as long as--server-urlmatches. -
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]
| Flag | Required | Default | Description |
|---|---|---|---|
--server-url <URL> | no | http://127.0.0.1:8080 | Base URL of the ilold serve instance. Environment variable: ILOLD_SERVER_URL. |
--contract <NAME> | no | unset | Optional 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. |
--narration | no | off | Emit 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)
| Tool | Purpose |
|---|---|
ilold_programs | List every program detected in the workspace. |
ilold_funcs | List the instructions exposed by the active program. |
ilold_funcs_all | Same list with admin-gating and coupling hints. |
ilold_info | Detail one instruction: args, accounts, signers, PDAs, discriminator. |
ilold_vars | List declared account types with their Anchor discriminators. |
ilold_pda | List the PDAs declared by an instruction (seeds, bumps). |
ilold_who | Resolve a query against the IDL (account type, instruction, or field). |
ilold_coupling | List instruction pairs that share a writable account. |
Session (mutate the timeline)
| Tool | Purpose |
|---|---|
ilold_call | Run an Anchor instruction against LiteSVM and append the result as a step. |
ilold_back | Remove the last step from the active scenario and rewind the VM. |
ilold_clear | Reset the active scenario steps and the underlying VM state. |
ilold_state | Decoded view of every account mutated during the active scenario. |
ilold_session | Active scenario summary: steps, findings, notes. |
ilold_step | Re-inspect one step: CU, logs, decoded diffs. |
Runtime (mutate the VM)
| Tool | Purpose |
|---|---|
ilold_users | List every named keypair in the active scenario. |
ilold_users_new | Create a new keypair and airdrop the initial lamports. |
ilold_airdrop | Top up an existing keypair with extra lamports. |
ilold_time_warp | Advance or rewind the Clock sysvar. |
ilold_inspect | Read a VM account by pubkey and decode it via the Anchor discriminator. |
Analysis
| Tool | Purpose |
|---|---|
ilold_timeline | Cross-step mutation history of an account, decoded. |
ilold_coverage | Aggregated runtime metrics over the active scenario (calls, failures, CU stats, CPI edges). |
Scenarios
| Tool | Purpose |
|---|---|
ilold_scenario | Manage scenarios: create, list, switch, fork, delete. |
Findings and journal
| Tool | Purpose |
|---|---|
ilold_finding | Record a security finding tied to the latest step. |
ilold_findings | List every finding recorded in the active scenario. |
ilold_note | Attach a free-form annotation to the active scenario. |
ilold_status | Set the review status of an instruction: open, reviewed, finding. |
ilold_export | Generate the audit deliverable (Markdown). |
Workspace
| Tool | Purpose |
|---|---|
ilold_use | Set the active program for the rest of the MCP session. Every other tool call routes to this program. |
ilold_save | Serialise the active scenario to ~/.ilold/sessions/<name>.json. |
ilold_load | Restore 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:
ilold_programslists every program detected by the backend. The active one is marked.ilold_use <program>sets the active program. The handler validates the name against/api/project/mapand rejects unknown names.- 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
stakingprogram. Look for paths where the admin signer check can be bypassed. Create a useralice, runstakefor 1000 lamports, and produce a coverage report at the end.
The client typically resolves it as the following tool sequence:
ilold_funcs_allto enumerate instructions and admin-gating hints.ilold_infoon each instruction the model wants to inspect.ilold_users_newto createalice.ilold_callforinitialize_pooland thenstake.ilold_coverageto read aggregated runtime metrics.ilold_findingif the model identifies an issue, followed byilold_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_BLOCKSonce 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 containclear,delete,reset). - Narration is best-effort.
--narrationemits anotifications/progressmessage keyed by the requestprogressToken. 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
| Symptom | Likely cause |
|---|---|
Cannot reach Ilold server at <url> on startup | ilold 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 client | The 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_programs | The 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/traceyet), heuristicwho,time-warpsemantics, 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/andrecursive/fixtures are Foundry layouts) but does not invokeforge buildorforge test. Possible directions include usingforge buildartefacts as an alternative ingest path, replaying PoCs from findings viaforge 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.
Related
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.