ChainScope is a local-first code graph for blockchain and protocol security research.
It indexes repositories into SQLite knowledge graphs and gives humans and AI agents fast structural queries over:
- call paths
- state readers and writers
- trust boundaries
- state transitions
- sink reachability
- ranked hotspots
- research-vs-production provenance
High-severity protocol bugs rarely live in one function. They emerge across:
- public entry points
- internal call chains
- external calls
- admin and upgrade surfaces
- state mutations
- helper scripts, PoCs, and fuzz harnesses
ChainScope gives you that structural map before you start deep manual reading.
It is also built for agentic research. Instead of forcing an agent to read a large repository linearly, ChainScope lets it ask high-signal questions first:
- What are the riskiest functions?
- Who writes
balances? - Is there a path from
deposittodelegatecall? - Which functions cross trust boundaries?
- Which findings come from production code vs research scaffolding?
That means more context goes to exploitability and impact, and less to rebuilding repository structure from scratch.
| Audience | What ChainScope helps with |
|---|---|
| Protocol security researchers | Fast graph-backed triage and path tracing |
| Bug bounty hunters | Exploit-surface-first target selection |
| Auditors | Structural navigation through large contract repos |
| AI-agent workflows | High-signal code relations without linear reading |
| Protocol/backend engineers | Trust-boundary and state-flow investigation |
| Signal | Note |
|---|---|
| Role | Map, not researcher |
| Best use | Graph-backed targeting, not verdicts |
| Speed | Finds risky intersections fast |
| Output quality | Good for hypothesis generation |
| Limitation | Manual exploitability still required |
| Area | What ChainScope gives you |
|---|---|
| Triage | Workspace profiling and exploit-surface-first target selection |
| Graphing | Functions, state vars, calls, reads/writes, transitions, sinks |
| Discovery | Hotspots, DeFi patterns, unsafe backend patterns |
| Tracing | Paths, state access, cross-boundary calls, state machines |
| Provenance | Research-mode indexing plus production-only query scope |
| Interfaces | MCP server for agents and CLI wrappers for local workflows |
ChainScope is strongest on:
- smart contract repos
- multi-repo blockchain workspaces
- protocol backends, keepers, relayers, indexers, and node code
- mixed Solidity/Rust/Go/Java/Python/TypeScript blockchain systems
- cross-chain messaging and bridge-style repos
Current language and ecosystem coverage includes:
- Solidity
- Vyper
- Move
- Clarity
- TON
- Cairo
- Sway
- Rust
- Go
- Java
- Python
- TypeScript / JavaScript
- C / C++
- protobuf
- Stellar XDR
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtThe exported MCP server name is chainscope:
{
"mcpServers": {
"chainscope": {
"command": "/opt/ChainScope/run_mcp.sh",
"args": []
}
}
}docker build -f Dockerfile.sandbox -t chainscope-sandbox .The Dockerfile copies the project to /opt/ChainScope/.
When Codex or Claude starts in a blockchain repo with the ChainScope MCP config loaded, the tools are available natively. Useful prompts include:
Profile this blockchain folder and recommend which subrepos to build firstBuild the knowledge graph for this repo, then show me the attack surfaceTrace who writes to balancesPath from deposit to the delegatecallTell me everything about deposit()Run all ChainScope tools and output a summarized report
Typical tool flow:
cs_profileto choose the right targetcs_buildto create the graphcs_summaryto confirm graph health, build metadata, and scopecs_auditorcs_hotspotsto rank the attack surfacecs_paths,cs_trace,cs_cross_summary,cs_cross, andcs_stateto validate the structure
graph.db is written to the current working directory unless you pass --db. After .mcp.json changes, start a fresh Codex session so the tool list reloads. In Claude Code, /mcp is the quickest way to confirm that the server is connected.
- Copy
.mcp.jsoninto the target project folder. - From that project folder, run
docker sandbox run --template chainscope-sandbox claude.
The custom image includes:
- Python plus the runtime dependencies from
Dockerfile.sandbox - tree-sitter parsers for the supported languages
- the ChainScope source tree at
/opt/ChainScope/
Rebuild the image after local ChainScope changes:
docker build -f Dockerfile.sandbox -t chainscope-sandbox .cs_profile
->
pick target repo or package
->
cs_build
->
cs_summary
->
cs_hotspots / cs_audit
->
cs_paths / cs_trace / cs_cross_summary / cs_cross / cs_state
->
manual source review
->
PoC or report
Profile a repo or workspace first:
python cs_profile.py /path/to/workspace --strategy bounty --jsonWhy:
--strategy bountyprioritizes exploit surface over raw repository size- build plans come back with per-target graph DB paths
- you avoid indexing a giant workspace blindly
Build a graph:
python cs_build.py /path/to/repo --db graph.dbMCP cs_profile, cs_build, and graph query tools do not self-timeout by
default. If you want a time-limited partial build or query, pass
timeout_seconds; otherwise long workspace profiles, graph builds, and broad
graph reads continue until the client or host stops them.
MCP cs_profile also caps large output sections with max_output_items; set
max_output_items=0 only when an exhaustive workspace inventory is intentional.
MCP cs_build caps extractor failure_examples with max_failure_examples;
set max_failure_examples=0 only when the full stored failure sample is needed.
MCP responses are compact JSON by default so agents spend context on graph
signal rather than indentation whitespace. CLI wrappers still pretty-print JSON
when their --json output mode is used.
If a build is time-limited or partial, ChainScope prioritizes production
protocol roots such as src/, contracts/, programs/, pallets/, and
crates/ before lower-signal operational or config code. This keeps partial
graphs useful for agent triage.
Query it:
python cs_summary.py --db graph.db
python cs_paths.py --db graph.db --from deposit --to withdraw
python cs_trace.py --db graph.db --var balances
python cs_cross.py --db graph.db --summary
python cs_cross.py --db graph.db --external-calls
python cs_state.py --db graph.db --allFor agents, the usual loop is:
cs_profileto choose the right subrepocs_buildto create the graphcs_summaryvia MCP to confirm the DB is populated and scoped correctlycs_hotspotsorcs_auditvia MCP to identify promising surfacescs_paths,cs_trace,cs_cross_summary,cs_cross, andcs_stateto validate structure- direct source reading only where the graph indicates it matters
For common function names, prefer qualified cs_lookup queries such as
Vault.deposit or TokenMessaging.send. Broad lookups are capped by default
so agents get candidates instead of an oversized response. max_matches caps
full function profiles and max_candidates caps ambiguous candidate lists; set
either value to 0 only when exhaustive output is intentional. Individual
lookup relation lists such as callers, callees, state reads/writes, and other
edges are capped by max_relation_items; set max_relation_items=0 only for
exhaustive profiles. Capped relation totals use edge-only counts before fetching
shown node details. Large parsed metadata blobs are compacted by
max_metadata_bytes; set max_metadata_bytes=0 only when the full raw metadata
is needed. Relation edge attributes are capped by max_attribute_bytes=2048;
set max_attribute_bytes=0 only when full call-site or edge attributes are
needed. Hidden relation rows are counted but do not format attributes after the
relation cap is reached. Shared ambiguous-match helpers also count hidden
candidate rows without materializing preview dictionaries after candidate caps.
cs_paths also caps ambiguous endpoint matches, endpoint candidate lists, and
returned paths. Use qualified endpoint names first; set
max_endpoint_matches=0, max_endpoint_candidates=0, or max_paths=0 only
when exhaustive path search is intentional. Bounded path search streams tuple
paths directly into the final capped result instead of building a separate
per-endpoint-pair path list. Returned path label disambiguation batches label
counts for the capped result instead of issuing one count query per path node.
Optional guard and state annotations are capped per path node by
max_guards_per_node=20 and max_state_access_per_node=25; set either to 0
only when exhaustive annotations are needed.
The same rule applies to broad cs_trace variable queries. Names like
total, owner, or balance are capped by default and return compact
candidates when ambiguous. max_matches caps fully traced variables and
max_candidates caps the candidate list; set either value to 0 only for
exhaustive trace output. Writer and reader lists are capped independently by
max_accessors_per_relation; set it to 0 only when exhaustive accessor output
is intentional. Single-variable capped traces use indexed distinct counts plus
limited accessor rows instead of scanning every accessor. When using
show_callers, caller lists are capped by max_callers_per_accessor; capped
caller totals use edge-only counts before fetching shown caller details. Full
parsed variable metadata is omitted by default; set include_metadata=true only
when that raw metadata is needed. Included variable metadata is capped by
max_metadata_bytes=4096; set max_metadata_bytes=0 only when full variable
metadata is needed.
cs_summary caps source-context counters by max_source_contexts=20.
Set max_source_contexts=0 only when every custom provenance bucket is needed.
cs_summary --attack-surface is also a bounded overview. Its _summary reports
the total entry points, how many were shown, and whether the list was truncated;
increase top when you need a broader entry-point inventory. All-source
attack-surface summaries fetch and format function metadata only for retained
top rows.
Scanner category output from cs_defi and cs_unsafe is capped by
max_per_category=25 by default. The summary still reports full category
totals; set max_per_category=0 only when an exhaustive category dump is
intentional. Per-finding scanner detail lists such as risks, sinks,
operations, and unchecked_calls are capped by max_detail_items=10; set
max_detail_items=0 only when exhaustive detector detail is needed.
Narrow category scans use SQL metadata prefilters. Broad all-category scans
stream function rows and index top-level metadata keys once, avoiding wide
metadata LIKE ... OR ... chains that can be slower on large graphs.
cs_audit is a top-N overview. Its _summary reports full section totals and
which sections were truncated so agents can drill down with specialized tools
instead of treating the overview as exhaustive. Raw attack-surface metadata is
omitted by default for MCP context; set include_metadata=true only when that
raw JSON is needed. Included attack-surface metadata is capped by
max_metadata_bytes=4096; set max_metadata_bytes=0 only when full metadata
is needed. Source-context counters are capped by max_source_contexts=20; set
max_source_contexts=0 only when every provenance bucket is needed. Detailed
dead-code rows are also omitted by default; set
include_dead_code_details=true when dead-code investigation is the current task.
Append-style audit sections such as access gaps and detailed dead code count
hidden rows without building their detail payloads after top is reached.
Broad cs_hotspots scans use one streaming function-row pass that retains only
scoreable candidates before guard counting, use indexed edge aggregates for
state-write and external-call counts, and screen metadata with cached top-level
key sets instead of wide SQL metadata LIKE ... OR ... chains. This keeps
normal broad scans usable on larger graphs while preserving exact scoring and
false-positive rejection.
Write-count and external-call aggregate helpers use the source/relation edge
index directly and fall back only when older graph DBs lack it.
Traversal relation scans, guard-label lookups, and writable-entry guard counts
use composite relation indexes the same way, so audit-style broad scans avoid
avoidable edge-table walks.
Older graph DBs that still have single-column edge indexes use those directly
instead of first attempting missing composite indexes and retrying after an
SQLite error. Preferred/fallback edge-index groups are resolved with one schema
lookup so broad MCP scans do not repeatedly probe sqlite_master.
The core graph API used by local workflows also filters traversal, reverse-call,
accessor, guard, and attack-surface edge queries through composite indexes, with
direct single-column index fallback for older graph DBs. Local graph path
formatting batches node-label loads and ambiguity counts for capped paths.
MCP broad scans also cache repeated raw metadata probes so common fields such as
source_context, is_sink, and sink_type do not require repeated JSON scans.
Top-level metadata key sets are cached too, while oversized metadata blobs are
scanned uncached so long-lived MCP servers do not retain them. Repeated detector
key groups are cached as sets for broad audit and scanner passes.
Follow-up tools such as cs_cross, cs_sinks, cs_paths, cs_trace, and
cs_lookup use composite edge indexes when present, but fall back cleanly for
older graph DBs that were built before those indexes existed, including direct
use of older source/target edge indexes for relation-filtered follow-up queries.
Reachability follow-ups such as cs_cross(from_func=...) and cs_sinks batch
node cache lookups per frontier instead of issuing one node query per traversed
edge.
Broad cs_state output is also capped by entity groups, transitions per entity,
warnings, and summary entity total counters. Raw transition metadata is retained
only for transitions that can be shown after max_transitions_per_entity.
All-source broad state scans fetch function metadata only for shown transitions;
exclude_research=true keeps metadata in the stream because filtering needs it.
Prefer entity= when investigating one state machine, or set the state caps to
0 when you intentionally need every transition or entity total.
For large graphs, prefer cs_cross_summary before broad cs_cross. It returns
totals, top source files, top targets, and bounded sample calls so agents can
choose where to inspect without dumping every trust-boundary edge. Sample calls
are capped by top; source-context, source-file, and target counters are capped
by max_counter_items.
Broad cross-boundary discovery uses the edge relation index when available and
falls back cleanly for older graph DBs.
cs_cross_summary(from_func=...) also streams the reachable boundary calls
directly instead of expanding an exhaustive raw cs_cross result first.
Raw cs_cross output is capped by max_results=50 by default; ambiguous
from_func candidates are capped by max_start_candidates and require a more
qualified name instead of silently picking the first match. Set max_results=0
only when an exhaustive edge list is intentional. Cross-boundary call attribute
payloads are capped by max_attribute_bytes=2048; set
max_attribute_bytes=0 only when full call attributes are needed. Broad raw
rows omit source and target graph IDs by default; set include_node_ids=true
when exact graph IDs are needed. Hidden raw cross rows do not format source
context after max_results is reached; cs_cross_summary still formats
contexts for all rows because its counters depend on them.
Broad cs_sinks output is capped by sink count and reachable callers per sink
(max_results=50, max_callers_per_sink=10 by default). The response still
reports total sinks, type counts, and caller truncation. Full parsed sink
metadata is omitted by default for MCP context; set include_metadata=true only
when the raw sink metadata is needed. Raw sink metadata is not retained in
default sink buffers, and hidden sink rows do not format source context after
the sink cap is reached. Included sink metadata is capped by
max_metadata_bytes=4096 and formatted only for shown sinks; set
max_metadata_bytes=0 only when full sink metadata is needed. Caller rows omit
verbose fields by default; set
include_caller_details=true when caller signatures and line_end are needed.
Set max_results=0 or max_callers_per_sink=0 only when exhaustive sink
expansion is intentional.
cs_profilecs_buildcs_summarycs_hotspotscs_auditcs_defics_unsafecs_pathscs_tracecs_cross_summarycs_crosscs_sinkscs_statecs_lookup
python cs_profile.py ...python cs_profile.py --max-output-items 0 ...python cs_build.py ...python cs_build.py --json ...python cs_build.py --max-failure-examples 0 --json ...python cs_summary.py ...python cs_summary.py --max-source-contexts 0 --json ...python cs_summary.py --attack-surface --top 10 ...python cs_paths.py ...python cs_paths.py --max-paths 0 --max-endpoint-matches 0 ...python cs_paths.py --max-endpoint-candidates 0 ...python cs_paths.py --show-guards --max-guards-per-node 0 ...python cs_paths.py --show-state --max-state-access-per-node 0 ...python cs_trace.py ...python cs_trace.py --max-matches 0 ...python cs_trace.py --max-candidates 0 ...python cs_trace.py --max-accessors-per-relation 0 ...python cs_trace.py --show-callers --max-callers-per-accessor 0 ...python cs_trace.py --include-metadata --json ...python cs_trace.py --include-metadata --max-metadata-bytes 0 --json ...python cs_cross.py --summary ...python cs_cross.py --summary --max-counter-items 0 ...python cs_cross.py ...python cs_cross.py --max-results 0 ...python cs_cross.py --max-attribute-bytes 0 ...python cs_cross.py --include-node-ids --json ...python cs_cross.py --max-start-candidates 0 ...python cs_state.py ...python cs_state.py --max-entities 0 --max-transitions-per-entity 0 --max-warnings 0 ...python cs_state.py --max-entity-totals 0 --json ...python cs_sinks.py ...python cs_sinks.py --max-results 0 --max-callers-per-sink 0 ...python cs_sinks.py --include-metadata --max-metadata-bytes 0 --json ...python cs_sinks.py --include-metadata --include-caller-details --json ...
By default, ChainScope stays production-first and skips low-signal or research-only paths such as:
scripts/poc/fuzz/invariant/certora/echidna/
If you want those artifacts included:
python cs_profile.py /path/to/repo --include-research --json
python cs_build.py /path/to/repo --include-researchMixed graphs preserve provenance with source_context tags such as:
productionscriptpocfuzzinvariant
When you build a mixed graph, you can still query just production code:
python cs_summary.py --db graph.db --exclude-research
python cs_paths.py --db graph.db --from start --to finish --exclude-research
python cs_trace.py --db graph.db --var total --exclude-research
python cs_cross.py --db graph.db --summary --exclude-research
python cs_cross.py --db graph.db --external-calls --exclude-research
python cs_state.py --db graph.db --all --exclude-research
python cs_sinks.py --db graph.db --type self_destruct --exclude-researchThe MCP server exposes the same scope control through exclude_research=true.
ChainScope is high-signal, but it is not a verdict engine.
Keep these limits in mind:
- exploitability still requires manual verification
- live state, balances, roles, and deployment wiring are outside pure static structure
- business-logic intent is not inferred reliably from graph shape alone
- some findings are intentionally noisy because they are meant to prioritize investigation, not replace it
- it is not yet a strong general web application security platform
ChainScope does not currently model:
- HTTP routes and middleware stacks
- session and cookie flows
- CSRF and browser-side auth semantics
- template rendering and XSS sinks
- file upload pipelines
- framework-specific ORM behavior
- cloud/IAM/runtime policy boundaries
You can still use it on backend code, and it already catches useful cross-language patterns such as command execution, deserialization, weak crypto, SQL injection, unsafe blocks, and race-like behavior. But the highest-signal heuristics are still blockchain-first.
{
"workspace_mode": true,
"strategy": "bounty",
"recommended_clusters": [
{
"name": "bridges-and-messaging",
"reason": "high trust-boundary density and externally reachable execution"
}
],
"build_plan": [
{
"label": "gmx-synthetics",
"tool_call": {
"tool": "cs_build",
"repo_path": "/path/to/repo",
"db": "graphs/gmx-synthetics.db"
}
}
]
}{
"query_scope": "production_only",
"variable_matches": 1,
"variables": [
{
"variable": "balances",
"writers": [
"Vault.deposit",
"Vault.withdraw"
],
"readers": [
"Vault.previewWithdraw",
"Vault.totalAssets"
]
}
]
}These outputs are intentionally structural. They tell you where to look next, not whether something is exploitable.
Contributions are welcome, especially in:
- parser quality for blockchain ecosystems
- protocol semantics and higher-signal heuristics
- CLI and MCP parity
- test fixtures for real protocol patterns
Start with CONTRIBUTING.md.
Near-term expansion areas:
- richer protocol semantics for roles, upgrades, assets, and config surfaces
- broader parser-grade support for additional blockchain ecosystems
- stronger runtime and fork-aware workflows for validating live exploit paths
- better backend/web framework understanding beyond protocol-heavy repos
- more publish-ready CLI parity for every MCP query surface
The current exported copy was verified with:
pytest -qResult at the time of this README update:
565 passed