- name
- xray-pre-audit
- description
- Use when preparing for a security audit, performing reconnaissance on a new codebase, or creating a protocol overview. Generates a structured pre-audit readiness report covering architecture overview, threat model, cross-linked protocol invariants, entry point classification and flow paths, composability analysis, test coverage gaps, git history signals, and an architecture diagram.
# X-Ray Pre-Audit Reconnaissance
## Overview
X-Ray generates a structured pre-audit **readiness report** that gives auditors (human or AI) a fast, accurate understanding of a protocol before diving into line-by-line review. The output is an `x-ray/` folder at the project root containing:
| File | Contents | Lifetime |
|------|----------|----------|
| `x-ray.md` | Main readiness report — overview, threat model, docs/test analysis, git history, verdict. Under 500 lines. | Kept |
| `entry-points.md` | Protocol flow paths + full entry point classification with call chains and parameter trust levels | Kept |
| `invariants.md` | Complete invariant catalog with stable IDs (`G-N`, `I-N`, `X-N`, `E-N`) that `x-ray.md` cross-links into | Kept |
| `architecture.svg` | Rendered architecture diagram | Kept |
| `architecture.json` | Intermediate machine-readable architecture graph used to build the SVG | Deleted at cleanup |
Adapted for the EVM Cortex squad from the Pashov Audit Group `x-ray` skill (upstream: `github.com/pashov/skills`, skill `x-ray`, **VERSION 2** — see the `VERSION` file alongside this one). The three scripts under `scripts/` are upstream code carried over intact and are the source of truth for enumeration, git security analysis, and SVG generation — the prose here tells you when to call them and how to read their output, never how to reimplement them.
X-Ray is fully autonomous. It runs without user interaction, produces concrete artifacts, and never fabricates findings. When something cannot be determined from the code, it says so explicitly.
### When to Invoke
- A new audit engagement begins and you need to orient quickly
- You are asked to "review", "audit", or "assess" a Solidity codebase
- The `audit-orchestrator` agent kicks off a Light, Core, or Thorough audit
- A developer asks "is this codebase audit-ready?"
- You need to build a threat model or protocol overview from scratch
### Relationship to Other Audit Skills
| Skill | Phase | X-Ray's Role |
|-------|-------|-------------|
| `audit-prep` | Before audit | X-Ray validates audit-prep deliverables |
| `audit-recon` | Phase 1 | X-Ray IS enhanced recon — superset of audit-recon |
| `audit-breadth-scan` | Phase 2 | X-Ray feeds prioritized leads into breadth scan |
| `audit-depth-analysis` | Phase 3 | X-Ray's threat model guides depth agent routing |
| `invariant-testing` | Testing | X-Ray's `invariants.md` IDs become invariant test names |
| `pashov-audit-pipeline` | Full review | X-Ray runs first; its 8 agents consume the threat model and invariant map |
---
## Progress Tracking (Mandatory)
Before doing anything else, call TodoWrite with these three todos, all `pending`:
1. `Phase 1: Enumerate & measure codebase`
2. `Phase 2: Read sources, classify entry points, synthesize invariants`
3. `Phase 3: Write x-ray report files`
Transitions — update via TodoWrite, never batch:
- Mark Phase 1 `in_progress` immediately, before running enumeration.
- When Step 1's parallel batch returns, in ONE TodoWrite call mark Phase 1 `completed` and Phase 2 `in_progress`.
- When Step 2 (including all sub-steps) finishes, in ONE TodoWrite call mark Phase 2 `completed` and Phase 3 `in_progress`.
- After all Step 8 output files are written and the diagram is validated, mark Phase 3 `completed`.
Rule: exactly one todo is `in_progress` at any time. Status updates happen the moment a phase starts or ends.
---
## Step 1: Enumerate & Measure
Before reading any code, quantify the target. Numbers ground the analysis and calibrate effort.
### Project Root & Source Directory Detection
If the user specifies a path, use it as project root. Otherwise use cwd. If no `.sol` files or `foundry.toml` / `hardhat.config.*` at root, check one level deep.
```bash
ROOT="${1:-.}"
cd "$ROOT" || exit 1
# Toolchain
if [ -f foundry.toml ]; then TOOLCHAIN=foundry
elif [ -f hardhat.config.js ] || [ -f hardhat.config.ts ]; then TOOLCHAIN=hardhat
else TOOLCHAIN=unknown; fi
echo "toolchain: $TOOLCHAIN"
# Source dir: foundry.toml `src = "..."`, else hardhat `contracts/`, else try both
SRC=$(sed -nE 's/^[[:space:]]*src[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' foundry.toml 2>/dev/null | head -1)
[ -z "$SRC" ] && { [ -d src ] && SRC=src; }
[ -z "$SRC" ] && { [ -d contracts ] && SRC=contracts; }
[ -d "$SRC" ] || { echo "ERROR: source directory not found"; exit 1; }
echo "src: $SRC"
mkdir -p x-ray
```
All shell in this skill uses **POSIX ERE only** — no `-P` / PCRE, no GNU-only escapes (`\s`, `\w`, `\b`). This keeps every command identical on macOS BSD grep, Linux GNU grep, and ripgrep. Use `[[:space:]]`, `[[:alnum:]_]`, and explicit alternation instead.
### Enumeration
Run the bundled enumeration script as a single Bash call. It creates the output directory and emits labeled sections consumed by every later step.
```bash
mkdir -p {project-root}/x-ray && bash {SKILL_DIR}/scripts/enumerate.sh {project-root} {src-dir}
```
`{SKILL_DIR}` is this skill's directory. The script covers, in one pass: toolchain detection, source files with line counts, per-file and total nSLOC, NatSpec density, test presence, fuzz/formal-verification detection, docs, current commit, and git history statistics. Do not reimplement any of it inline — the script handles quoting and exclusion edge cases that ad-hoc greps get wrong.
### Version Check
In the same parallel message as enumeration:
```bash
curl -sf https://raw.githubusercontent.com/pashov/skills/main/x-ray/VERSION
```
Compare against this skill's local `VERSION` file. If the remote value is higher, print `⚠️ Upstream x-ray is at version N, this skill is vendored at 2. See https://github.com/pashov/skills`. If the fetch fails, skip silently — a network failure is not an audit finding.
### Reading the Test Signals
Test **presence** comes from the file scan above. It is always reliable, even when the toolchain cannot compile — which is exactly why it is measured separately from coverage.
Multi-signal categories (`echidna`, `medusa`, `halmos`, `certora`) report as `functions:configs`. `5:1` means 5 test functions plus 1 config file. A config with zero functions is worth flagging on its own: the harness exists but nobody wrote properties for it.
| Signal | Reads as |
|--------|----------|
| `test_functions` | Unit/integration breadth (Foundry `function test*` plus Hardhat `it(...)`) |
| `stateless_fuzz` | `testFuzz*` — per-call property testing |
| `foundry_invariant` | `invariant_*` — stateful campaigns in Foundry |
| `echidna` / `medusa` | External stateful fuzzing, as `functions:configs` |
| `certora` / `halmos` / `hevm` | Formal verification: specs/CVL, `check_*`, `prove_*` |
| `fork` | Fork-test count — whether live integration state is exercised at all |
A protocol with high `test_functions` and zero stateful or formal signals is a specific, reportable readiness gap: the state space has never been searched. Route that to `fizz` for an Echidna/Medusa suite.
### Coverage (Background)
Launch coverage in the background. Do not wait for it.
```bash
# Foundry
forge coverage 2>&1 || (echo "RETRYING_WITH_IR_MINIMUM" && forge coverage --ir-minimum 2>&1)
# Hardhat
npx hardhat coverage 2>&1
```
If the toolchain is not installed (`forge: command not found`, missing `node_modules/`), this fails. That is expected and carries **no information about test quality** — see the test-existence rules in Step 8.
### Dependency Snapshot
```bash
forge tree 2>/dev/null | head -40
grep -rE 'openzeppelin|solady|solmate' foundry.toml remappings.txt 2>/dev/null | head -10
ls lib/ 2>/dev/null
```
### Spec / Whitepaper Detection
Glob `**/{whitepaper,spec,design,protocol,architecture,overview,README}*.{pdf,md}`, excluding `node_modules/`, `lib/`, `x-ray/`, `test/`. Skip user-facing docs — tutorials, API references, changelogs, contribution guides. Then apply size-aware handling:
- **Path A (≤5 docs, each ≤300 lines)** — read them directly as part of the Step 2 parallel message. No subagent needed.
- **Path B (>5 docs, or any doc >300 lines)** — launch a single `scout` subagent that reads all doc files and returns a structured extraction of at most 200 lines, with these headings only: Doc-Stated Global Invariants, Actor Definitions, Trust Assumptions, Cross-System Flows, Economic Properties, Key Design Decisions. Require a source quote per claim; omit empty headings.
Extract only: doc-stated global invariants, actor definitions, cross-system flows, trust assumptions, economic properties, key design decisions. Tag every spec-derived claim in the report with `(per spec)` so auditors can tell code-verified from spec-stated. Doc-stated global invariants feed the NatSpec routing pass in Step 5 — they route to §2/§3/§4 of `invariants.md` by shape, never to §1 (which is per-call guards only).
### Parallel Batch Rule
Issue enumeration, background coverage, git analysis (Step 6), the reference doc reads, and the spec glob **in the same message**. Proceed to Step 2 without waiting for coverage.
---
## Step 2: Source Analysis
### Scope Filtering
- Skip interfaces: `interfaces/` directories, or filenames matching `I` followed by an uppercase letter.
- Skip vendored libraries: copies of Uniswap `FullMath`/`TickMath`, OpenZeppelin, Solady, Solmate.
- Skip mocks and test doubles.
- When uncertain, read the file but exclude it from the scope table.
Do NOT read test files or documentation files in this step.
### Two Scan Paths
Choose by in-scope source file count.
#### Path A — ≤20 source files: direct reads
One Read call per file, all in a single parallel message alongside the entry point grep scan. Do not re-read README, docs, or `foundry.toml` — Step 1 already covered them.
#### Path B — >20 source files: fan out to parallel subagents
| Tier | Files | Handling |
|------|-------|----------|
| 1 | ≤120 lines | Batch into a single Bash `cat` call |
| 2 | >120 lines | Group by subsystem; launch one `scout` subagent per subsystem (up to 5 subagents, ~10 files each) |
Each Tier 2 subagent extracts facts only — no analysis — and returns the per-file structure below.
### Per-File Extraction Contract
Both paths must produce the same facts per file:
- **Type** — `contract`, `abstract contract`, `library`, or `interface`
- **Inherits** — full linearization (affects function resolution order)
- **Imports** — libraries and contracts pulled in
- **Roles / access control** — modifiers, role constants, `msg.sender` checks
- **Value-holding state** — mappings and variables holding balances, collateral, stakes, reserves, debt
- **External calls** — every call to another contract, including token transfers
- **Fund flows** — deposit / withdraw / mint / burn / transfer / borrow / repay / liquidate paths
- **Invariant comments** — NatSpec `@invariant` tags, `require`/`assert` statements
- **Backwards-compatibility indicators** — see Step 2c
- **Key logic** — one or two sentences on what the contract does
- **Function-level access map** (contracts only, skip libraries) — every public/external non-view non-pure function with its modifier, or `NONE — permissionless`. For functions with no modifier, also list the external calls they make.
Three extractions carry the invariant synthesis in Step 5 and must be captured precisely:
**Delta writes** — for each non-view non-pure function, the storage variables that change and the symbolic delta applied. Format `Δ(var) = +expr` or `Δ(var) = -expr`. Same-basic-block only; report a pair only when both writes appear in the same function body with no intervening call to an unknown external contract. Do not chase writes through inherited or imported functions unless the semantic effect is unambiguous — OpenZeppelin `_mint` touching `balanceOf` and `_totalSupply` is fine, custom internal helpers are not. List a custom helper's deltas under that helper's own entry.
```
deposit(): Δ(totalSupply) = +shares, Δ(balanceOf[msg.sender]) = +shares
borrow(): Δ(totalBorrows) = +amount, Δ(underlyingBalance) = -amount
```
**Guard predicates** — every `require` / `assert` / `if-revert` that references a storage variable, quoted verbatim with its line number. Skip guards that reference only function parameters.
```
Vault.sol:206: require(_fee <= 10, "fee is capped at 0.1%")
```
**Enum / one-shot transitions** — every `require(var == X); ...; var = Y` pair where `var` is a storage enum, uint, or address. Record as `X@Lx → Y@Ly`. Include one-shot latches such as `require(addr == address(0)); addr = concrete`.
### Entry Point Grep Scan
Issue both greps in the **same parallel message** as the source reads — they are independent.
```bash
# 1. Single-line signatures: name and visibility on the same line
grep -rnE 'function[[:space:]]+[[:alnum:]_]+[[:space:]]*\([^)]*\)[[:space:]]+(external|public)' "$SRC"/ --include='*.sol' \
| grep -v '/interfaces/' | grep -v '/mock/' \
| grep -Ev '(^|[^[:alnum:]_])(view|pure)([^[:alnum:]_]|$)'
```
```bash
# 2. Multiline signatures: visibility on the closing-paren line (covers 90%+ of multiline cases)
grep -rnE '^[[:space:]]*\)[[:space:]]+(external|public)' "$SRC"/ --include='*.sol' -B5 \
| grep -v '/interfaces/' | grep -v '/mock/' \
| grep -Ev '(^|[^[:alnum:]_])(view|pure)([^[:alnum:]_]|$)'
```
Combine both result sets. The multiline grep is not optional — Solidity routinely splits parameters across lines, leaving `external`/`public` on the `)` line while `function name(` sits several lines above. The trailing filter is the POSIX-portable substitute for `\b(view|pure)\b`: it drops lines where `view` or `pure` appears as a standalone identifier while preserving identifiers like `view_param`.
### Step 2b: Entry Point Classification
Classify **all** entry points using the grep results plus the function bodies. Do not rely on subagent summaries alone — subagents extract facts at contract level and can misattribute which function makes which call or carries which modifier.
Exclude: view/pure functions, interface-only declarations, library internal functions (downstream calls, not entry points), mock contracts.
| Access Level | Criteria | Priority |
|-------------|----------|----------|
| **Permissionless** | No access-control modifier AND no caller restriction anywhere in the function body | HIGHEST |
GitHubで見る