X Ray
pashov/skills
Generates an x-ray.md pre-audit report covering overview, enhanced threat model (protocol-type profiling, git-weighted attack surfaces, temporal risk analysis, composability dependency mapping)…
A skill your agent uses when preparing for a security audit, performing reconnaissance on a new codebase, or creating a protocol overview.
$ npx skills add ccashwell/evm-cortex --skill xray-pre-audit -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install ccashwell/evm-cortex xray-pre-audit --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/xray-pre-audit .claude/skills/xray-pre-audit && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "xray-pre-audit" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/xray-pre-audit into .claude/skills/xray-pre-audit/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "xray-pre-audit", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/ccashwell/evm-cortex/tree/main/skills/xray-pre-auditType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add ccashwell/evm-cortex --skill xray-pre-audit -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install ccashwell/evm-cortex xray-pre-audit --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .agents/skills && cp -r skills-src/skills/xray-pre-audit .agents/skills/xray-pre-audit && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "xray-pre-audit" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/xray-pre-audit into .agents/skills/xray-pre-audit/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "xray-pre-audit", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ccashwell/evm-cortex --skill xray-pre-audit -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install ccashwell/evm-cortex xray-pre-audit --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/skills/xray-pre-audit .cursor/skills/xray-pre-audit && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "xray-pre-audit" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/xray-pre-audit into .cursor/skills/xray-pre-audit/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "xray-pre-audit", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/ccashwell/evm-cortex.git --path skills/xray-pre-audit--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add ccashwell/evm-cortex --skill xray-pre-audit -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install ccashwell/evm-cortex xray-pre-audit --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/skills/xray-pre-audit .gemini/skills/xray-pre-audit && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "xray-pre-audit" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/xray-pre-audit into .gemini/skills/xray-pre-audit/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "xray-pre-audit", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install ccashwell/evm-cortex xray-pre-auditInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add ccashwell/evm-cortex --skill xray-pre-audit -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .github/skills && cp -r skills-src/skills/xray-pre-audit .github/skills/xray-pre-audit && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "xray-pre-audit" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/xray-pre-audit into .github/skills/xray-pre-audit/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "xray-pre-audit", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add ccashwell/evm-cortex --skill xray-pre-audit -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install ccashwell/evm-cortex xray-pre-audit --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/ccashwell/evm-cortex.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/skills/xray-pre-audit .opencode/skills/xray-pre-audit && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "xray-pre-audit" agent skill from https://github.com/ccashwell/evm-cortex/tree/main/skills/xray-pre-audit into .opencode/skills/xray-pre-audit/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "xray-pre-audit", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
xray-pre-auditA skill your agent uses when preparing for a security audit, performing reconnaissance on a new codebase, or creating a protocol overview.
Xray Pre Audit is an agent skill from ccashwell/evm-cortex. 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.
Its SKILL.md is about 25k tokens, which your agent loads only when the skill is triggered. The skill folder holds 5 other files, including scripts (for example `scripts/analyze_git_security.py`, `scripts/enumerate.sh` and `scripts/generate_svg.py`).
It sits in Security, covering Threat modeling, Git workflow and Diagrams. The repository describes itself as: Ethereum protocol engineering squad for AI coding assistants. The licence is MIT.
10 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit f8f3301. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Ships 3 files in scripts/ (Python and Shell), which the agent can run.
Shell commands in SKILL.md call:
python3gitbashcurlnpxFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
github.comraw.githubusercontent.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Xray Pre Audit loads about 25k tokens when it runs. Until then it costs about 100 tokens; SKILL.md has 10,466 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); the scripts in this folder are not scanned.
The full file from ccashwell/evm-cortex at commit f8f3301, republished under its MIT licence (© ccashwell). 10,466 words, ~24,704 tokens.
.claude/skills/xray-pre-audit/SKILL.md (or your agent's skills folder). This skill also uses 4 other files; get the full folder from GitHub.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.
audit-orchestrator agent kicks off a Light, Core, or Thorough audit| 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 |
Before doing anything else, call TodoWrite with these three todos, all pending:
Phase 1: Enumerate & measure codebasePhase 2: Read sources, classify entry points, synthesize invariantsPhase 3: Write x-ray report filesTransitions — update via TodoWrite, never batch:
in_progress immediately, before running enumeration.completed and Phase 2 in_progress.completed and Phase 3 in_progress.completed.Rule: exactly one todo is in_progress at any time. Status updates happen the moment a phase starts or ends.
Before reading any code, quantify the target. Numbers ground the analysis and calibrate effort.
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.
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-rayAll 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.
Run the bundled enumeration script as a single Bash call. It creates the output directory and emits labeled sections consumed by every later step.
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.
In the same parallel message as enumeration:
curl -sf https://raw.githubusercontent.com/pashov/skills/main/x-ray/VERSIONCompare 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.
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.
Launch coverage in the background. Do not wait for it.
# Foundry
forge coverage 2>&1 || (echo "RETRYING_WITH_IR_MINIMUM" && forge coverage --ir-minimum 2>&1)
# Hardhat
npx hardhat coverage 2>&1If 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.
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/nullGlob **/{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:
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).
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.
interfaces/ directories, or filenames matching I followed by an uppercase letter.FullMath/TickMath, OpenZeppelin, Solady, Solmate.Do NOT read test files or documentation files in this step.
Choose by in-scope source file count.
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.
| 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.
Both paths must produce the same facts per file:
contract, abstract contract, library, or interfacemsg.sender checks@invariant tags, require/assert statementsNONE — 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) = -amountGuard 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.
Issue both greps in the same parallel message as the source reads — they are independent.
# 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:]_]|$)'# 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.
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 |
| Role-gated | Role modifier (onlyRole, onlyOwner, onlyRouter, …) OR an internal msg.sender restriction. Record which role or address is required. | MEDIUM |
| Admin-only | DEFAULT_ADMIN_ROLE, onlyOwner pointing at protocol admin, or equivalent top-level authority | LOWER, but centralization risk |
| Initializer | initializer / reinitializer — one-time deployment entry points | Track separately |
A function without a modifier is NOT automatically permissionless. You MUST verify the body before classifying. In Path A the bodies are already in context — classify directly. In Path B, batch all candidate body reads into a SINGLE parallel message. Look for any of:
require(msg.sender == expected);
if (msg.sender != expected) revert Unauthorized();
if (msg.sender != a || cond) revert Unauthorized(); // compound
_checkCaller(); // internal checkA modifier-free function with an internal msg.sender check is role-gated. Common offenders: acceptOwnership(), acceptMsig(), confirmX() — no modifier, but restricted to a pending address.
nonReentrant alone is NOT access control.
Record per entry point:
| Field | Value |
|---|---|
| Contract / Function | Pool.sol — deposit(uint256 assets, address receiver) |
| Visibility | external, nonReentrant |
| Access level | Permissionless |
| Caller | User |
| Parameters | assets (user-controlled), receiver (user-controlled) |
| Call chain | → Pool._deposit() → Pool._mint() → Strategy.deploy() |
| State modified | balances[receiver], totalSupply, totalAssets |
| Value flow | in (tokens: sender → Pool) |
| Reentrancy guard | yes |
Parameter trust levels: (user-controlled) the caller picks the value freely, (user-signed) the value comes from a user's offchain signature, (keeper-provided) a keeper selects it, (protocol-derived) read from onchain state.
Call chains use concrete contract names, not interface names — write FuturesManager.addCollateral(), not ICollateralManager.addCollateral(). Interfaces describe how the caller references the target; the auditor needs to know which contract actually executes. The only exception is genuinely external third-party contracts (IERC20.safeTransfer()).
The grep scan is a hard gate: the permissionless list in the report must match the grep-verified, body-checked result. Where grep plus code reading conflicts with a subagent summary, grep wins.
Reorganize the Step 2b data into flow paths for entry-points.md. This is not a new analysis pass and needs no new tool calls.
For each major user-facing entry point (permissionless and role-gated functions that move value):
require statements and state variable checks.◄──.Output is simple arrow chains grouped by actor, 15-30 lines total. Reference earlier flows ([owner setup above]) instead of repeating them.
Watch for code that looks like the remnant of a removed mechanism, kept so the rest of the codebase still compiles. Signals: empty or trivial function bodies, state variables declared but never meaningfully read or written, comments containing "deprecated" / "legacy" / "backwards compat" / "no longer used", interface implementations that always return a default, storage variables preserved solely for proxy layout compatibility.
After reading ALL source files, cross-reference candidates against these mandatory checks. Batch every caller-check Grep for all candidates into a SINGLE parallel message — do not verify one at a time.
Classify as backwards-compatibility only when ALL of: no active callers, no NatSpec/comments documenting the behavior as intentional, and git history shows the mechanism it belonged to was removed.
Never describe backwards-compatibility code as an active feature. Note it explicitly in Section 1 so auditors do not burn time investigating dead code. If nothing survives the checks, omit the subsection entirely.
Two analyses that feed into existing report sections. Neither produces a standalone section.
Centralization analysis — for each privileged role (admin, owner, operator, keeper, service):
AccessControlDefaultAdminRules' 1-day delay) from operational action delays. A role transfer delay does NOT protect against a compromised holder using instant operational functions.emergencyWithdraw, setTreasury, transferFee).Integrate into Actors (Capabilities column states what is instant versus delayed), Trust Boundaries (what each boundary actually protects versus what bypasses it), and Key Attack Surfaces (framed as "Admin operational powers" or "[Role] compromise" — the surface is the role compromise, not the individual functions).
Pause coverage analysis — for each critical state-changing function, check whether whenNotPaused or an equivalent applies; note which functions are pausable and which are not. A function that should logically be pausable but is not (for example one callable by a bounded role that operates on user funds) is a detail that worsens that role's compromise scenario — fold it into that role's attack surface rather than raising it independently.
Anti-pattern: do NOT create a standalone "Centralization Risks" section. Centralization detail belongs distributed across Actors, Trust Boundaries, Key Attack Surfaces, and Protocol-Type Concerns. A dedicated section duplicates what those already say. Same for pause coverage.
Use the exact nSLOC TOTAL from Step 1 enumeration — no ~ prefix — in the report header and scope table.
Detect type from function signatures, state variables, and architectural patterns found during Step 2. A protocol may match multiple types. Rank by signal density; the type with the most matches is primary.
| Type | Detection Signals in Code |
|---|---|
| Lending / Borrowing | borrow(), repay(), liquidate(), liquidationBonus, healthFactor, collateralFactor, LTV, debtToken, interestRate, collateral ratio math |
| DEX / AMM | swap(), addLiquidity(), removeLiquidity(), constant-product math, stable-swap invariant, sqrtPriceX96, tick, LP mint/burn, fee tier, getAmountOut(), reserves |
| Yield Aggregator / Vault | ERC-4626 pattern (deposit/withdraw/convertToShares/convertToAssets), strategy pattern with harvest(), totalAssets(), strategyDebt, auto-compound |
| Stablecoin | Mint/burn against collateral, collateralRatio, stability fee, debtCeiling, redemption mechanism, PSM, anchor/peg/target price |
| Derivatives / Perps | openPosition(), closePosition(), increaseSize(), fundingRate, margin, leverage, PnL math, markPrice, indexPrice, position struct with size/collateral/entryPrice |
| Liquid Staking | stake() plus derivative token mint, requestWithdrawal(), exchange rate calculation, validator set management, withdrawal queue, rebasing or share-based token |
| Bridge | Cross-chain message passing, lock()/unlock() or burn()/mint(), relayer/validator set, message nonce, chain ID checks, merkle proof verification |
| Governance | propose(), vote(), execute(), queue(), quorum calculation, voting power snapshots, timelock, delegation, proposalThreshold |
| NFT / Marketplace | mint(), list(), buy(), offer(), royalty, tokenURI, ownerOf, setApprovalForAll |
A protocol with swap(), addLiquidity(), borrow(), liquidate() is Primary DEX/AMM, Secondary Lending.
Protocols face different dominant threats at different points in their lifecycle. Detect which phases are relevant from code signals; include only those in the threat model.
| Phase | Include When |
|---|---|
| Deployment & Initialization | Always — every protocol has this phase |
| Steady State | Always — this is the baseline, covered by the main threat model |
| Market Stress | Oracle integration, liquidation logic, collateral/debt tracking, or any price-dependent calculation exists |
| Governance & Upgrade Windows | Timelock, governance contract, proxy pattern (UUPS/transparent/beacon), or propose()/vote()/execute() exists |
| Deprecation & Wind-down | V2/migration in names or comments, migrate() exists, deprecated contract references, or multi-version architecture |
The threat model is the analytical core of X-Ray. It maps actors, trust boundaries, and attack surfaces.
The profiles below are a threat identification library, not prose to paste into the report. Use them to know what to look for and who threatens this protocol, then translate that into the report's format.
DO-NOT-EXPLOIT RULE (critical). Attack surfaces describe the concern area, not the exploit. The auditor's value is building the attack path; yours is finding the area fast. If a bullet contains "→ attacker drains X", "→ user trapped", "→ inflated share", "double-counts W", "leads to understated N" — cut it. Replace with "Worth checking…", "Worth tracing…", "Worth confirming…". Name the asymmetry, the divergence, the unusual pattern, the cross-path bookkeeping, then stop.
The exploit-chain prose inside this Step 4 library (for example "oracle manipulation → inflated collateral → drain the pool") is intentional here — it teaches the threat. It is forbidden in the report.
Only named roles from the code. Never "Anyone". Never "Semi-trusted" — use "Bounded (reason)".
| Actor | Trust Level | Capabilities |
|---|---|---|
| EOA User | Untrusted | All permissionless functions |
| Contract User | Untrusted, reentrancy-capable | Same as EOA, plus code execution on callbacks |
| Keeper | Bounded (can only complete CREATED swaps within constraints; not subject to whenNotPaused) | harvest, rebalance, liquidate |
| Admin (multisig) | Trusted | 11 instant setters plus pause. pause does NOT gate withdraw. |
| Timelock | Trusted, delayed | Delayed execution provides an exit window |
| Oracle | External trust | Price data consumed by protocol; manipulation is critical |
| Integrated Protocol | External trust | Protocol depends on their correct operation |
| Flashloan Attacker | Adversarial | Unlimited capital within one transaction |
Capabilities cells are a scannable reference, not a capability paragraph. For roles with many powers, summarize and enumerate only the dangerous ones inline. Aim for two lines per cell.
Then produce an Adversary Ranking ordered by threat level for this protocol type, adjusted by git evidence. Three to five entries, ONE sentence each stating WHO they are and WHY they matter here. Attack mechanics and code references belong in Key Attack Surfaces, not here.
┌─────────────────────────────────────────────┐
│ UNTRUSTED ZONE (public internet) │
│ Users, Flashloan attackers, MEV searchers │
├─────────────────────────────────────────────┤
│ PROTOCOL BOUNDARY │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Vault.sol │───▶│ Strategy.sol │ │
│ │ (entry point)│ │ (internal) │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
├─────────┼───────────────────┼───────────────┤
│ EXTERNAL TRUST BOUNDARY │ │
│ ┌──────▼───────┐ ┌──────▼───────┐ │
│ │ Chainlink │ │ Aave V3 │ │
│ │ (oracle) │ │ (lending) │ │
│ └──────────────┘ └──────────────┘ │
├─────────────────────────────────────────────┤
│ ADMIN TRUST BOUNDARY │
│ Multisig → Timelock → Parameter changes │
└─────────────────────────────────────────────┘For each boundary state what is trusted, the damage if compromised, and whether a timelock or multisig exists. Where a delay protects only the role seat and not operational actions, say so explicitly: "1-day delay protects the admin seat, but emergencyWithdraw and setFee execute instantly." If git analysis shows boundary code was frequently modified or carries fix-scored commits, append *Git signal: N modifications, M fix-scored commits — elevated risk.*
Adversaries, ranked: flash loan attacker (drives oracle manipulation cost to near zero) → oracle manipulator (the oracle is the sole source of truth for solvency) → liquidation MEV searcher (if extraction makes liquidation unprofitable for honest liquidators, bad debt accrues) → malicious first depositor (share-price manipulation on supply/debt tokens) → compromised admin (collateral factors, oracle address, rate model, pause).
Dominant patterns: oracle manipulation → inflated collateral → max borrow → drain pool. Flash borrow → manipulate spot → liquidate victim at wrong price. Bad debt via unliquidatable positions (oracle lag, gas spikes, illiquid collateral). Utilization manipulation to move interest rates. Recursive borrowing amplification.
Critical invariants: totalBorrows <= totalCollateral * LTV per market and per account. Every position is liquidatable before it can cause bad debt. Liquidation is profitable for liquidators. Oracle price is within acceptable deviation and freshness bounds. Interest accrual is monotonic and unmanipulable.
Look first at: the complete price path (oracle read → normalization → collateral value → health factor — every step is a manipulation point); whether one transaction can borrow, manipulate, and liquidate; liquidation math against gas plus slippage; share price when totalSupply == 0; what admin can change instantly versus through timelock.
Adversaries, ranked: MEV searcher / sandwich attacker (every swap without adequate slippage protection is a guaranteed extraction) → flash loan price manipulator → malicious first LP / empty pool attacker → liquidity manipulation attacker → compromised admin (fees, pause, routing, pool whitelist).
Dominant patterns: sandwich attacks. LP share inflation on empty pools via donation. Reentrancy through token callbacks (ERC-777, ERC-1155) while pool state is inconsistent. Spot price exploitation when other protocols read the pool as an oracle. Concentrated liquidity tick manipulation. Fee-on-transfer accounting breaks.
Critical invariants: pool invariant holds before and after every operation. LP share value is monotonically non-decreasing from fees. No tokens leave without proportional LP burn or valid swap math. Swap output matches invariant-derived calculation exactly. Tracked reserves match actual balances.
Look first at: swap math rounding direction; LP mint/burn at totalSupply == 0 and minimum liquidity enforcement; whether the pool exposes getPrice()/observe() (then manipulation has external blast radius); where slippage protection is enforced and whether it can be bypassed; state updates versus external calls in the swap path.
Adversaries, ranked: share inflation attacker (deposit 1 wei, donate a large amount, next depositor rounds to 0 shares) → malicious or compromised strategy (strategies hold the actual funds) → reentrancy through external protocol callbacks → donation / direct-transfer attacker → compromised admin (add strategies, allocation weights, harvester, migrate funds).
Dominant patterns: ERC-4626 share inflation. Strategy reports fake gain, attacker deposits at inflated price, previous depositors diluted. Strategy retains approvals after migration. Harvest sandwich. Accounting desync when the strategy's real balance drifts from recorded allocation (rebasing, slashing, reward accrual).
Critical invariants: totalAssets() accurately reflects real underlying value. convertToShares(convertToAssets(shares)) <= shares and the reverse. Strategy cannot extract more than allocated. Share price rises only from yield.
Look first at: virtual offset or minimum deposit protecting share math; what a strategy may report and who may add or remove strategies; whether totalAssets() uses balanceOf(this) (donation-exposed) or internal accounting; reentrancy protection and state-before-call ordering; whether migration revokes old approvals.
Adversaries, ranked: oracle manipulator → economic/governance attacker (collateral parameters, stability fees) → bank run attacker (strategic redemption drains the best collateral) → flash loan minter → compromised admin (collateral types, oracle, debt ceilings, pause redemptions).
Dominant patterns: manipulate collateral price, mint at inflated value, sell, leaving the protocol undercollateralized. Algorithmic death spiral. Redemption DoS draining liquid collateral. Governance-set undercollateralized ratios. Staleness arbitrage between mint and redeem.
Critical invariants: every unit is backed by at least the configured collateral ratio. Mint and redeem are inverse — round-trip preserves value. The peg mechanism converges under sell pressure rather than diverging. Liquidation can always restore individual position collateralization. Total supply is within the debt ceiling across all collateral types.
Look first at: the mint path (accepted collateral → valuation → ratio → who can change the ratio); the redeem path under stress and whether all units can redeem simultaneously; liquidation profitability; what governance can change and how fast; whether a 10% depeg self-corrects or amplifies.
Adversaries, ranked: oracle manipulator (leverage amplifies oracle error — 1% manipulation on 50x is a 50% PnL swing) → liquidation MEV searcher → funding rate manipulator → position size attacker (positions larger than the protocol can pay out) → compromised admin (max leverage, funding parameters, liquidation thresholds, oracle, pause liquidations).
Dominant patterns: oracle manipulation triggering cascade liquidation. Funding rate manipulation via concentrated one-sided open interest. Position size exceeding payout capacity. Stale oracle enabling a risk-free directional bet against offchain price. Cross-margin cascades within one account. ADL manipulation.
Critical invariants: sum of all PnL is zero minus fees. Available liquidity covers maximum payout under worst-case price movement. Liquidation triggers before any position causes bad debt. Funding rate converges open interest imbalance. Mark price cannot deviate from index price beyond safety bounds.
Look first at: PnL math at leverage limits and in both directions; the margin between liquidation trigger and insolvency; how mark price is derived and whether it moves within a block; whether open interest and position size limits are enforced; the maximum funding rate and how fast it can drain margin.
Adversaries, ranked: exchange rate manipulator → validator set attacker → withdrawal queue attacker → oracle/rate arbitrageur (exit at stale rate before a slashing report lands) → compromised admin (validator set, fees, oracle, pause withdrawals).
Dominant patterns: rewards or slashing reporting manipulation. Withdrawal queue griefing with spam requests. Rebasing integration bugs in downstream protocols. Validator collusion suppressing rewards. Share price manipulation via direct transfer to the contract.
Critical invariants: exchange rate reflects true underlying value (staked plus rewards minus slashing). Total derivative supply times exchange rate is at most total underlying staked. The withdrawal queue processes in fair order. Slashing is reflected in the rate before any user can exit at the stale rate.
Look first at: who reports rewards and slashing, how often, and whether it is manipulable; the withdrawal queue delay and griefability; who selects validators; whether the derivative rebases or uses shares; how a massive slashing event is socialized.
Adversaries, ranked: validator/relayer set attacker (the single largest bridge exploit vector by value lost) → message replay attacker → race condition exploiter (source/destination finality gaps, reorgs) → fake message crafter → compromised admin (validator set, pause, upgrade to drain locked funds).
Dominant patterns: validator key compromise forging messages to mint unbacked tokens. Message replay from a missing or overflowing nonce. Proof verification bypass. Chain ID confusion. Reorg exploitation after destination minting.
Critical invariants: locked tokens on source equal minted tokens on destination. Every message is processed exactly once. Messages cannot be forged without validator threshold consensus. Accounting is consistent across chains.
Look first at: validator count, threshold, and how the set changes; nonce presence, checking, and overflow; proof and signature verification edge cases; finality assumptions on the source chain; whether admin can drain locked funds.
Adversaries, ranked: flash loan governance attacker (trivial when voting power reads current balance instead of a snapshot) → governance capture attacker (patient accumulation) → proposal spam / griefing attacker → timelock exploitation attacker → compromised guardian (cancel, pause, emergency bypass).
Dominant patterns: flash loan → vote → return. Bribe attacks. Proposal obfuscation hiding malicious calldata. Timelock front-running to position before parameter changes execute. Guardian emergency powers used for non-emergencies.
Critical invariants: voting power is snapshotted at proposal creation. Quorum prevents minority capture. Timelock delay is long enough for users to exit. No single role bypasses governance for non-emergency actions. Proposal calldata matches its description.
Look first at: whether voting power is a snapshot or a current balance; quorum and threshold against actual token distribution; timelock duration; every parameter governance controls; who holds emergency powers and what they can do.
| Threat | Vector | Impact |
|---|---|---|
| Reentrancy | External call → callback → re-enter state-changing function | Double-spend, state corruption |
| Access control bypass | Missing modifier, wrong role check, initializer re-call | Unauthorized privileged operations |
| Rounding exploitation | Systematic rounding in the attacker's favor across many operations | Slow value drain |
| Griefing / DoS | Revert on transfer, gas griefing, storage bloat | Protocol unusable |
| Upgrade hijack | Unprotected initializer, storage collision, proxy admin takeover | Complete compromise |
| Centralization | Admin key compromise or malicious admin action | Total loss of funds |
Include a "Temporal Risk Profile" subsection covering only the phases that add NEW information beyond Actors, Attack Surfaces, and Upgrade Architecture. Skip Steady State — the main threat model covers it. One to three bullets per phase, each citing a code location and a mitigation status (mitigated / partially mitigated / unmitigated).
Deployment & Initialization — the most dangerous 24-48 hours.
| Threat | What to look for |
|---|---|
| Initialization front-running | initialize()/init() without access control or the initializer modifier; proxies where initialize runs in a separate transaction from deployment; permissionless pool or market creation |
| Parameter misconfiguration | Test values still active — zero delays, max-uint fees, known test addresses; constructor/initializer params without validation; insecure defaults |
| Ownership not transferred | Ownable with no transferOwnership() in the deploy script; two-step transfer not yet accepted; roles not granted to intended addresses |
| Empty-state exploitation | if (totalSupply == 0) branches; attacker-chosen initial prices or ratios; deposits when totalAssets == 0; missing minimum initial deposit |
| Deployment ordering bugs | Multi-transaction deploy scripts; circular contract references; approval and role-grant chains that must happen in a specific order |
Market Stress — where the largest DeFi losses have come from.
| Threat | What to look for |
|---|---|
| Oracle latency under volatility | The staleness threshold on latestRoundData() and whether it fits the asset's volatility; deviation checks; behavior when updatedAt is zero or in the future |
| Liquidation cascade | Does liquidation sell collateral on-market (price impact)? Circuit breaker? Throttling? Can it absorb a 30%+ drop in one block? |
| Liquidity evaporation | Liquidation profitability assumptions when depth is thin; assumed swap paths; minimum liquidity requirements |
| Correlated asset depeg | Hardcoded 1:1 price equivalences; using an underlying's oracle for a derivative asset; collateral factors that ignore depeg risk |
| Gas price spikes | Keeper-dependent flows; liquidation incentive versus gas cost; operations bounded by a time window; keeper-failure fallbacks |
| Withdrawal stampede | Withdrawal queues and rate limits; liquid versus deployed share of TVL; how fast strategies unwind; stress-scaling withdrawal fees |
Governance & Upgrade Windows — the transition between old and new state.
| Threat | What to look for |
|---|---|
| Timelock exploitation window | Whether the delay is long enough to exit; parameters exploitable once their pending value is public |
| Upgrade storage collision | _authorizeUpgrade protection; storage gap usage; whether upgrades are tested against the real layout |
| Flash loan governance | balanceOf(msg.sender) (vulnerable) versus snapshot at proposal creation (immune); borrowability of the governance token |
| Governance capture | Voting power concentration, quorum, cost to buy a passing stake, guardian veto |
| Migration window | V1→V2 transfer mechanisms; whether V1 retains fund access; deadlines; front-runnability |
Deprecation & Wind-down — include only with version-transition evidence.
| Threat | What to look for |
|---|---|
| Residual funds in deprecated contracts | Are old versions reachable? Do they still hold funds? Is migration forced? |
| Abandoned approval chains | Unlimited approve() versus permit(); any revocation mechanism during migration |
| Dependent protocol breakage | Does this protocol serve as an oracle or data source? Is there a deprecation flag integrators can read? |
| Frozen state exploitation | What breaks if no governance proposal passes for six months? Parameters requiring periodic updates? Automated fallback? |
Every external call extracted in Step 2 gets classified into the composability taxonomy. For each call determine:
Report each significant dependency as a blockquote block:
Chainlink ETH/USD — via
Oracle.getPrice()
- Assumes: fresh, positive, 8-decimal price
- Validates:
answeredInRoundonly — NO staleness or deviation check- Mutability: feed address settable by admin, no timelock
- On failure: reverts
Layer 1 — direct dependency risks. Oracle dependency chains (protocol → oracle → underlying sources; if any source is manipulable within the trust assumptions, the oracle is manipulable). Yield strategy dependencies (the external protocol holds the actual funds; check upgradeability, pausability, emergency withdrawal, loss socialization, retained approvals after migration). Callback reentrancy (guards that are per-function do not stop cross-function reentrancy; ERC-777 tokensReceived, ERC-1155 onERC1155Received, flash loan callbacks, swap callbacks).
Token behavior assumptions. Report only the ones the code does not validate.
| Assumption | Violating Tokens | Impact if Violated |
|---|---|---|
| Transfer sends the exact amount | Fee-on-transfer (PAXG, USDT with fee enabled) | Internal accounting exceeds real balance; protocol becomes insolvent |
| Balance changes only on transfer | Rebasing (stETH, AMPL, aTokens) | Accounting drift, share price manipulation |
| Transfer reverts on failure | USDT (returns false without reverting) | Silent transfer failure, lost funds |
| No callback on transfer | ERC-777, ERC-1155 | Reentrancy through the transfer hook |
| 18 decimals | USDC (6), USDT (6), WBTC (8), GUSD (2) | Math errors, massive over/under-valuation |
| No address blocking | USDC, USDT blacklists | Withdrawals blocked, funds trapped |
| Cannot be paused | USDC, USDT | All protocol operations halt |
| Immutable implementation | Upgradeable tokens behind proxies | Behavior changes post-deployment without consent |
Check whether the code uses the balanceOf before/after pattern (handles fee-on-transfer), uses SafeERC20 (handles non-reverting tokens), reads decimals() dynamically, handles rebasing, and whether arbitrary tokens can be used or a whitelist exists.
Layer 2 — shared state risks. Liquidity coupling (two protocols using the same pool; a large action here moves prices there within a block). Oracle sharing (the same feed triggering correlated liquidations, with feedback loops back into this protocol). Approval chain exposure (unlimited approvals to an upgradeable contract mean a future upgrade could drain them; deprecated contracts still holding approvals).
Layer 3 — temporal composability risks. Governance-induced behavior change in a dependency (no contract interaction changed, but economic assumptions broke — for example a lending market lowering a collateral factor under a strategy assuming the old one). Upgrade-induced interface change (same signature, different behavior, revert conditions, or gas). Deprecation without notification (a feed returns stale data silently, or starts reverting into a fail-open try/catch). Dependency-of-dependency upgrades — map the chain two to three levels deep and flag chains deeper than two levels, noting for each level whether it is upgradeable and governed.
Invariants are properties that must hold across ALL state transitions. This step is a reasoning pass over the Step 2 extractions — delta writes, guard predicates, transitions, invariant comments. It needs no new tool calls except the batched write-site Greps in the guard-lift pass.
The output is invariants.md, a catalog of blocks with stable IDs that x-ray.md cross-links into.
A guard is a per-call precondition enforced at a single callsite — require(amount >= MIN). It is not falsifiable: the code guarantees it at that callsite. It feeds §1 of invariants.md (Enforced Guards reference) only.
An invariant is a property that must hold globally across any sequence of calls — "every active position is at least MIN". Invariants that are lifted from guards, or stated in NatSpec, feed §2/§3/§4.
| ID | Section | Meaning |
|---|---|---|
G-N | §1 Enforced Guards | Verbatim per-call precondition with location and purpose |
I-N | §2 Inferred, Single-Contract | Conservation / Bound / Ratio / StateMachine / Temporal |
X-N | §3 Inferred, Cross-Contract | Caller assumption paired with callee write sites |
E-N | §4 Economic | Higher-order property derived from specific I-N / X-N |
IDs are stable within a run and are written as #### G-1, #### I-17, #### X-4 H4 headings — never as table rows. H4 headings produce slug anchors (#g-1, #i-17) that cross-file markdown links resolve reliably in VS Code and on GitHub. Inline <a id> anchors inside table cells do NOT work cross-file, which is why referenced IDs must never live in tables.
For each NatSpec @invariant tag or inline comment asserting a global property — "totalSupply always equals Σ balances", "fee never exceeds MAX_BP", "only one active epoch at a time" — route DIRECTLY to §2, or §3/§4 if the property spans contracts or derives from multiple primitives, classified by shape. Source tag: NatSpec: Contract.sol:LN. Do NOT place developer-stated global invariants in §1 — §1 is per-call guards only.
Doc-stated global invariants from Step 1's spec extraction route the same way.
After routing, still run the structural scans. They confirm (Onchain: Yes) or contradict (Onchain: No) the stated claim.
Each step uses the raw extraction data, not prior-step conclusions.
1. Conservation scan. For each function, find delta-write pairs where Δ(A) = +expr and Δ(B) = -expr (or Δ(B) = +expr for a mapping counterpart) in the same body. Each matched pair is a conservation candidate: A + B = const or A == Σ B[key].
mapping[key] += e plus scalar += e) imply scalar == Σ mapping[key]. Verify the pattern across ALL functions writing either variable. If any function writes one without the other, note it as partial conservation and split into separate Yes/No blocks.mapping[from] -= e, mapping[to] += e, no scalar change) confirm the mapping sum is self-conserving.2. Guard extraction and lift. Two passes over every require / assert / if-revert.
Pass A — extract verbatim. Every guard becomes a G-N block in §1, quoted verbatim with its source location and a purpose line. This is a mechanical dump of per-call preconditions — not falsifiable, not fuzzed. Skip guards that reference only function parameters with no storage tie-back AND carry no global implication.
Pass B — lift, then check all write sites. For each guard ask: does this imply a property that must hold across any sequence of calls, not just at this callsite?
require(amount >= MIN) at deposit implies "every active position ≥ MIN"; require(_fee <= 10) at a setter implies "fee ∈ [0, 10]") → rewrite it as a global property, then locate ALL write sites of the constrained storage variable with Grep on the variable name across scope files. Batch every write-site Grep for every lifted guard into a SINGLE parallel message.Onchain: Yes. Derivation cites the guard plus the confirmed write-site enumeration.Onchain: No, citing the unguarded write site as the gap. This is the high-signal output — the gap is simultaneously an invariant and a potential bug.Include setter-level bounds where a setter writes a storage variable constrained by its own parameter check. Run the same all-write-site check: if several setters write the same variable but only some enforce the bound, the property is Onchain: No.
3. Ratio scan. For each storage write of the form A = B * C / D where B, C, D are storage variables or function-scoped snapshots of storage, record the ratio. Note whether the snapshot is taken before or after other state changes in the same function — ordering matters (totalSupply snapshotted before _burn versus after).
4. State machine / one-shot scan. For each enum/uint/address in a require(var == X); ... var = Y pattern, record the transition and classify:
require(var == default); var = concrete with no path back (setStrategy, setLeverager). Keep.require(var == false); var = true but another function flips it back (freeze/unFreeze). NOT a state machine invariant — drop.false → true → false driven by timing or condition. Record as a cycle invariant.5. Temporal scan. For each block.timestamp or block.number comparison involving a storage variable (deadline, lastUpdate, lockPeriod, interval), extract the constraint. Note whether it is checked-then-updated (safe) or updated-then-checked (potential stale read).
6. Cross-contract scan. For each external call whose return value feeds arithmetic or a storage write, record what the caller assumes. Then find the callee's write sites for that state. If the callee can change it independently through another function, the assumption is unvalidated — record as X-N with Onchain: No. Include a block ONLY when BOTH sides are inside the scope files; never speculate about out-of-scope contracts.
Also include setter-versus-invariant mismatches — an admin setter writing a storage value without checking that existing invariants still hold (setReserveCapacity without checking current liquidity). These are cross-contract in the sense that the setter sits in one place and the invariant is enforced elsewhere.
7. Economic derivation. After steps 1-6, check whether any combination of single-contract and cross-contract invariants implies a higher-order property. Each E-N must cite the specific I-N / X-N IDs it derives from. If any source invariant in the chain is Onchain: No, the economic invariant is Onchain: No too.
| Type | Gate |
|---|---|
| Conservation | The Δ-pair exists at the cited lines, in the same function body |
| Guard (§1) | The require/assert/if-revert is verbatim from code |
| Guard lift (§2) | The lifted property references persistent storage, not a restated transient parameter. All write sites of the constrained variable are enumerated via Grep, and the Yes/No verdict matches the enumeration. A Yes with an unguarded write site is invalid. |
| NatSpec | The tag or comment exists verbatim at the cited location AND asserts a global property. A per-call note is dropped, not routed to §2. |
| Ratio | The formula is exact and snapshot ordering is noted |
| StateMachine | Both sides of the edge exist AND no reverse path exists. A reverse path means it is a togglable flag — drop. |
| Temporal | The comparison involves a storage variable, not just block.timestamp versus a parameter |
| Cross-contract | Both the caller usage and the callee write site exist in scope |
| Economic | Every referenced I-N / X-N is itself verified |
If you cannot verify, drop the block. "Could not verify" is not a valid block.
This is what makes the invariant map usable rather than decorative. When writing Key Attack Surfaces in x-ray.md Section 2, cross-reference each surface against the invariants.md blocks. If the surface's cited file:line falls within the Location / Derivation / Caller side / Callee side window of any G-N / I-N / X-N / E-N block, append the matching IDs as bracketed markdown links immediately after the surface title, using lowercase slug fragments:
- **`withdrawFromInvestment` unchecked subtraction** [[X-4](invariants.md#x-4), [I-17](invariants.md#i-17)] — ...Surfaces that are purely access-control or upgradeability concerns may be left unlinked — that is a healthy signal, not a gap. On a non-trivial protocol, expect at least 70% of surfaces to link to at least one invariant. A much lower rate means the invariant walk was too shallow.
The same IDs give downstream agents stable handles: invariant-tester names Foundry invariant tests after them, poc-writer references the Onchain: No blocks it is trying to falsify.
| Category | Shape |
|---|---|
| Conservation | Two or more storage variables change by equal-and-opposite amounts in the same body: Δ(A) = +x, Δ(B) = -x → A + B = const |
| Bound | A guard on a storage variable lifted to a global property and enforced at every write site: require(x <= MAX) at every writer → x ∈ [0, MAX] globally |
| Ratio | A storage variable defined as a formula of others: withdrawAmount = totalBalance * shares / totalSupply |
| StateMachine | A storage variable transitions through discrete values with guards preventing reversal |
| Temporal | A condition depends on block.timestamp, block.number, or a duration/deadline variable |
Git history reveals what the team worries about, where complexity lives, and what has been fixed.
The analysis is scoped to the current branch only (HEAD). Every git signal — fix candidates, hotspots, dangerous areas, late changes — reflects ONLY commits reachable from HEAD, not other branches.
[branch] at [commit]".Do NOT pass --all to any of these commands. --all silently pulls in other branches and breaks every claim above.
Run the bundled analyzer. It is dependency-free and emits structured JSON, which is far more reliable to reason over than a pile of ad-hoc git log output. Issue this in the Step 1 parallel batch:
cd {project-root} && python3 {SKILL_DIR}/scripts/analyze_git_security.py \
--repo . --src-dir {src-dir} --json x-ray/git-security-analysis.json 2>&1 \
&& cat x-ray/git-security-analysis.jsonThe JSON has eight sections:
| Section | Contents |
|---|---|
meta | Analyzed repo, source dir, branch, and commit — quote these in the report header |
repo_shape | Commit counts, age, contributor line attribution, squashed-import detection |
fix_candidates | Commits whose subjects suggest a bug or security fix, for scoring below |
dangerous_area_changes | Commits touching security-sensitive areas |
late_changes | Recent source changes — code the team has had least time to review |
forked_deps | Vendored or forked dependencies that diverge from upstream |
tech_debt | TODO / FIXME / HACK / XXX / BUG markers in scope |
dev_patterns | Bus factor, review patterns, merge behavior |
The script scopes itself to HEAD. Do not add --all or substitute hand-written git log commands — both break the branch-scoping guarantees above.
Enumeration (Step 1) already emits overlapping git statistics (git_hotspots, git_large_diffs, git_recent_30d, git_contributors). Use those for the coarse picture and this JSON for the security-specific reasoning; do not run a third set of commands to re-derive either.
| Classification | Detection | Consequence |
|---|---|---|
squashed_import | ≤1 commit touches source files, or ≤3 source commits within 7 days | No development history — skip fix and hotspot analysis, state this explicitly |
normal_dev with bulk import | One commit contributes >85% of all source lines added, but real development follows | Analyze only the post-import commits |
normal_dev | Neither of the above | Full analysis applies |
Score each source-touching commit. The score is a weighted sum, floored at zero. 10 or higher warrants reading the diff manually. Report commits scoring 5 or higher.
Intent from the commit subject — first match wins, sets the base:
| Signal | Base |
|---|---|
| security, vulnerab, exploit, attack, CVE-N, reentran, overflow, underflow, front-run, malleab | +8 |
| hotfix, emergency, critical | +6 |
| fix / fixes / fixed, bug, patch, broken | +4 |
| harden, mitigat, protect, restrict, sanitiz, validat | +2 |
| leading feat / add / implement / introduce / support | −1 |
| leading docs: / chore: / ci: / test: / style: / build:, or readme, typo, format, lint, rename, refactor, cleanup, comment | −3 |
Topic tags — checked independently, +2 each, applied only when the base is non-negative: oracle / price / liquidat / slippage / MEV; reentran / overflow / underflow / front-run; ecrecover / permit / signature / nonce.
Structural diff signals — read the diff and count added versus removed:
| Signal | Score |
|---|---|
Net addition of require/revert/assert | +3 |
Net removal of require/revert/assert | +3 |
| Equal-count rewrite of guards | +2 |
Net addition of onlyOwner/onlyRole/nonReentrant/whenNotPaused/initializer (tightening) | +3 |
| Net removal of the same (loosening) | +3 |
| Equal-count rewrite of access control | +2 |
Touches safeTransfer*/.transfer(/transferFrom/.call{value | +2 |
Touches ecrecover/permit/ECDSA/EIP-712/nonce | +2 |
Touches balance*/totalSupply/exchangeRate/index/reserve | +1 |
Both adding and removing a guard score equally. Adding one may fix a vulnerability; removing one may introduce it. Report the direction so the auditor knows which to expect.
Domain overlap: the commit's files span ≥2 security areas → +3; exactly 1 → +1.
Shape modifiers: 1-3 source files touched (focused) → +2; net code removal → +1; includes test changes → +1; source churn >500 lines → −2; >2000 lines → −4.
Classify each source file by grepping its content, then use the classification for both domain-overlap scoring and the Dangerous Area Evolution table.
| Area | Patterns |
|---|---|
access_control | onlyOwner, onlyRole, modifier only, AccessControl, Ownable2Step, require(msg.sender, hasRole, _checkRole |
fund_flows | .deposit(, .withdraw(, .transfer(, .mint(, .burn(, collateral, safeTransfer, balanceOf, allowance, approve |
oracle_price | oracle, price, feed, TWAP, markPrice, indexPrice, latestRoundData, getPrice, EMA |
liquidation | liquidat, backstop, ADL, deleverage, insurance, insolvenc, badDebt, isLiquidatable |
signatures | ecrecover, permit, signature, EIP-712, ECDSA, nonce, digest, _hashTypedData |
state_machines | status =, state =, Phase, Stage, lifecycle, transition, paused, frozen, isActive, whenNotPaused |
Areas with high commit counts warrant deeper review — frequent changes to security-critical code correlate with higher defect density.
Check lib/ and vendored directories against known upstreams (OpenZeppelin, Solady, Uniswap V2/V3, Aave, Chainlink, Permit2). Skip forge-std and ds-test.
A library present as a git submodule at a tagged upstream commit is normal. A library internalized into the repo — copied in, no submodule — is hidden attack surface: the team may have introduced bugs while adapting it, and upstream security fixes will not propagate. Compare pragmas against the expected upstream range; a pragma mismatch is strong evidence of modification.
| Signal | Meaning | Action |
|---|---|---|
| File changed 20+ times in 3 months | Unstable or complex — high bug density | Prioritize for depth analysis |
| Fix commits concentrated in one file | Known bug area | Check whether the fixes are complete |
| Single author on a critical contract | Bus factor 1, less review | Extra scrutiny |
| Zero merge commits | Likely no peer review process | Flag in Review Signals |
| Low test co-change rate | Source changed without test files changing | Flag as co-modification risk, NOT as a coverage claim |
| Reverted commits | Something went wrong | Investigate what and why |
| Large commit touching many files | Refactor or rushed feature | Check for regressions |
| Burst of commits right before audit | Late unreviewed changes | Highest-priority review targets |
| TODO/FIXME in a security-critical path | Acknowledged incomplete work | Cross-reference against attack surfaces |
The test co-change rate measures file co-modification within commits. It is NOT a coverage metric. Never claim "commits without tests" on the basis of a coverage tool failure.
If any git command fails, fall back to plain git log statistics. Never block on git analysis.
Build a machine-readable graph, then render and validate a diagram from it.
{
"title": "[Protocol] Architecture",
"nodes": [
{"id": "vault", "label": "Vault", "subtitle": "Coordinator", "type": "protocol", "row": 1}
],
"edges": [
{"from": "user", "to": "vault", "label": "deposit assets"}
],
"groups": [
{"label": "Vault Layer", "nodes": ["vault", "strategy"]}
]
}Node types: actor for users and roles, protocol for in-scope contracts, external for out-of-scope dependencies. subtitle is a short role line; for composite nodes list the individual contracts there.
Assign row to minimize edge distance, not by node type. Place each node on the row adjacent to its primary caller. Actors usually land at the top and leaf dependencies at the bottom, but an external node called only from row 1 belongs on row 2, not on a distant "externals" row.
Group containment rule. Every node must be either inside exactly one group, or on a row that has no group box at all. A group box spans all rows containing its nodes, so an ungrouped node sharing a row with a group will visually escape or overlap it. Fix by adding it to the right group or moving it to another row. Classify a node's group by its primary caller — an ACL contract called by the coordinator belongs in the coordinator's group, not a downstream infrastructure group.
| In-scope contracts | Max nodes | Max edges | Max per row |
|---|---|---|---|
| ≤10 | 12 | 14 | 4 |
| 11-20 | 16 | 18 | 4 |
| 21-35 | 20 | 22 | 5 |
| 36+ | 24 | 26 | 5 |
Prioritize completeness over compression. Every contract that holds funds, gates access, or sits on a critical call path must be visible — as its own node or clearly named in a composite node's subtitle.
Apply in order; use the first tier that fits the budget.
Never composite across trust levels. Merging permissionless and admin-only nodes hides trust boundaries. Combine actors only when they share both trust level and capabilities. External dependencies that are the sole data source for critical logic (oracles, price feeds) get their own node.
nodes ordering so arcs fan out cleanly.Do NOT hand-author the SVG. Write architecture.json, then generate the SVG with the bundled script:
python3 {SKILL_DIR}/scripts/generate_svg.py x-ray/architecture.json x-ray/architecture.svgThe generator is dependency-free and handles all styling and geometry: rounded node boxes, an accent stripe by node type (protocol, external, and pill-shaped actor), labeled arrows, and group enclosure rects. It also auto-assigns layers via longest-path when row is omitted, so hand-tuning rows is optional rather than required.
Because layout is deterministic, the failure modes that remain are content problems in the JSON, not rendering problems. Optionally rasterize for a sanity check — use the first renderer that works:
convert -density 300 x-ray/architecture.svg /tmp/architecture-preview.png
rsvg-convert x-ray/architecture.svg -o /tmp/architecture-preview.png
python3 -c "import cairosvg; cairosvg.svg2png(url='x-ray/architecture.svg', write_to='/tmp/architecture-preview.png', scale=3)"If no renderer is available, skip the visual check — the SVG is still valid. Note the skip in the report.
Content checklist — fix in architecture.json and regenerate, max 2 iterations:
Never edit the generated SVG directly. It is a build artifact; the next regeneration discards the edit.
rm -f x-ray/architecture.json /tmp/architecture-preview.pngThese are independent signals and conflating them produces false claims.
Test presence comes from Step 1 enumeration — test_files, test_functions, stateless_fuzz, foundry_invariant, echidna, medusa, hardhat_fuzz, fork, certora, halmos, hevm. These are file-scan results and are ALWAYS reliable, regardless of whether the toolchain can compile or run.
Coverage metrics (line and branch percentages) come from forge coverage or hardhat coverage, which need installed dependencies, successful compilation, and passing tests. Coverage fails for reasons unrelated to test quality: dependencies not installed, stack-too-deep, compiler version mismatch, missing RPC or fork config.
Rules:
test_files / test_functions from enumeration for ALL test existence claims. Never infer "no tests" from a coverage tool failure."[N] test files with [M] test functions detected; coverage metrics unavailable — [failure reason]". If it is still running, write Pending. Do not estimate, interpolate, or carry over a number from a previous run.stateless_fuzz = 0, foundry_invariant = 0, and so on). Never claim "no tests" when enumeration found test files. Prioritize by audit impact: missing stateful fuzz and formal verification on math-heavy financial logic outranks missing fork tests.test_co_change_rate measures file co-modification in commits — qualify it as such.Check the background coverage job's status once: include results if done, the failure reason if failed, Pending if still running. Do NOT wait.
Write x-ray.md, entry-points.md, invariants.md, and architecture.json in a SINGLE message so they are created concurrently.
# X-Ray Pre-Audit Report
> [Protocol] | [nSLOC] nSLOC | [short-hash] (`[branch]`) | [framework] | [DD/MM/YY]
---
## 1. Protocol Overview
**What it does:** [One sentence — the core mechanism.]
- **Users**: [Who interacts and why]
- **Core flow**: [The main user-facing operation]
- **Key mechanism**: [AMM type, vault model, oracle design]
- **Token model**: [What tokens exist and their roles]
- **Admin model**: [Who controls what]
For a visual overview see the [architecture diagram](architecture.svg).
### Contracts in Scope
[Group by subsystem — one row per subsystem, not per file.]
| Subsystem | Key Contracts | nSLOC | Role |
|-----------|--------------|------:|------|
[Only protocol-authored contracts and libraries. No interfaces, no vendored libs.]
### Backwards-Compatibility Code
[Only if remnants survived the Step 2c verification checks. Omit the subsection entirely otherwise.]
- `[contract:function]` — [what it belonged to, why retained, that it is not active functionality]
### How It Fits Together
[Start with "The core trick:" — one sentence on the fundamental mechanism. Then 3-5 key
flows as annotated call chains with tree branching, italic annotations on critical steps
(state changes, callback windows, payment verification). Use concrete contract names, not
interface names. Skip governance/admin/oracle flows — Section 2 covers those.]
---
## 2. Threat & Trust Model
### Protocol Threat Profile
> Protocol classified as **[Primary]** with **[Secondary]** characteristics
[1-2 sentences on why, from code signals.]
### Actors & Adversary Model
| Actor | Trust Level | Capabilities |
|-------|-------------|-------------|
[Only named roles from code. No "Anyone". Never "Semi-trusted" — use "Bounded (reason)".
Capabilities must distinguish instant from timelocked actions and note pause coverage gaps.]
**Adversary Ranking** (by threat level for this protocol type, adjusted by git evidence):
1. **[Adversary]** — [ONE sentence: who they are, why relevant here.]
[3-5 entries. No attack mechanics, no code refs — those live in Key Attack Surfaces.]
See [entry-points.md](entry-points.md) for the full permissionless entry point map.
### Trust Boundaries
- **[Boundary]** — [protection status + the single worst instant action it leaves open + code ref. Max 2 lines.]
### Key Attack Surfaces
[The single authoritative location for surface detail. Sorted by priority score: protocol-type
relevance + git hotspot + fix history + late changes + dangerous area churn. Not alphabetical.
No RISK labels. No mitigation analysis. Blank line between bullets. Hard cap 2 lines each.]
- **[Surface name]** [[X-N](invariants.md#x-n)] — [code ref + the concern + what to trace to confirm or dismiss it.]
### Upgrade Architecture Concerns
[Only if upgradeable contracts exist. Uninitialized implementations, storage gap consistency,
missing upgrade timelock, blast radius of shared contracts, placeholder proxy windows.]
### Protocol-Type Concerns
**As a [Primary type]:**
- [Code ref + technical concern: math precision, curve edge case, share rounding direction.]
[2-3 bullets per type. Skip anything already a Key Attack Surface. Every bullet cites a
specific contract and function — no generic protocol-type advice.]
### Temporal Risk Profile
[Only phases adding NEW information beyond Actors, Attack Surfaces, and Upgrade Architecture.
Skip Steady State. 1-3 bullets per phase, each with a code location and mitigation status.]
### Composability & Dependency Risks
> **[External Name]** — via `[contract:function]`
> - Assumes: [assumptions about return value and behavior]
> - Validates: [checks present, or NONE]
> - Mutability: [Immutable / Upgradeable by X / Governed by X]
> - On failure: [revert / fallback / fail-open]
**Token Assumptions** *(unvalidated only)*:
- [Token type]: assumes [unvalidated assumption] — impact if violated: [consequence]
**Shared State Exposure** *(if applicable)*:
- [Shared pools or oracles, who else shares them, whether this protocol's actions affect others]
---
## 3. Invariants
> ### Full invariant map: **[invariants.md](invariants.md)**
>
> - **[N] Enforced Guards** (`G-1` … `G-N`) — per-call preconditions with check, location, purpose
> - **[N] Single-Contract Invariants** (`I-1` … `I-N`) — Conservation, Bound, Ratio, StateMachine, Temporal
> - **[N] Cross-Contract Invariants** (`X-1` … `X-N`) — caller/callee pairs crossing scope boundaries
> - **[N] Economic Invariants** (`E-1` … `E-N`) — higher-order properties from `I-N` + `X-N`
>
> Every inferred block cites a concrete Δ-pair, guard-lift with write sites, state edge,
> temporal predicate, or NatSpec quote. The **Onchain: No** blocks are the high-signal ones —
> each is simultaneously an invariant and a potential bug. Attack surfaces above cross-link
> directly into the relevant blocks.
[Section 3 is a POINTER, not a catalog. Do NOT duplicate the invariants here.]
---
## 4. Documentation Quality
| Aspect | Status | Notes |
|--------|--------|-------|
| README | [Present/Missing] | |
| NatSpec | [~N annotations] | [Coverage notes] |
| Spec/Whitepaper | [Present/Missing] | |
| Inline Comments | [Sparse/Adequate/Thorough] | [Notable gaps] |
[Tag spec-derived claims `(per spec)` versus `(per code)`.]
---
## 5. Test Analysis
| Metric | Value | Source |
|--------|-------|--------|
| Test files | [N] | File scan (always reliable) |
| Test functions | [N] | File scan (always reliable) |
| Line coverage | [N% / Pending / Unavailable — reason] | Coverage tool (requires compilation) |
| Branch coverage | [N% / Pending / Unavailable — reason] | Coverage tool (requires compilation) |
### Test Depth
| Category | Count | Contracts Covered |
|----------|-------|-------------------|
| Unit | | |
| Integration | | |
| Fork | | |
| Stateless Fuzz | | |
| Stateful Fuzz (Foundry) | | |
| Stateful Fuzz (Echidna / Medusa) | | |
| Formal Verification (Certora / Halmos / HEVM) | | |
[Always include Unit, Stateless Fuzz, at least one Stateful Fuzz row, and at least one Formal
Verification row even at 0 — their absence is audit-relevant. Consolidate other zero rows into Gaps.]
### Gaps
[Missing categories only, prioritized by audit impact. Never claim "no tests" when files exist.
Reference specific untested functions, not just percentages.]
---
## 6. Developer & Git History
> Analyzed branch: `[branch]` at `[commit]`. Repo shape: [normal_dev / squashed_import] — [one sentence].
### Contributors
| Author | Commits | Source Lines (+/-) | % of Source Changes |
|--------|--------:|--------------------|--------------------:|
[Flag single-developer dominance (>90%), ghost contributors, uneven distribution.]
### Review & Process Signals
| Signal | Value | Assessment |
|--------|-------|------------|
| Unique contributors | | [Single-dev / Small team / Larger team] |
| Merge commits | [N] of [total] | [Formal review / No merge commits — likely no peer review] |
| Repo age | [first] → [last] | |
| Recent source activity (30d) | | [Active / Quiet / Late burst before audit] |
| Test co-change rate | [N%] | [Measures co-modification, NOT coverage] |
### File Hotspots
| File | Modifications | Note |
|------|-------------:|------|
### Security-Relevant Commits
[Only commits scoring ≥5. Skip for squashed imports. Score = weighted sum of message intent,
diff structure, security-domain overlap, and change shape. 10+ warrants a manual diff.]
| SHA | Date | Subject | Score | Key Signal |
|-----|------|---------|------:|------------|
### Dangerous Area Evolution
| Security Area | Commits | Key Files |
|--------------|--------:|-----------|
### Forked Dependencies
| Library | Path | Upstream | Status | Notes |
|---------|------|----------|--------|-------|
[Internalized libraries with pragma or logic drift are hidden attack surface — upstream
security fixes will not propagate.]
### Technical Debt Markers
| File:Line | Type | Text | Author | Date |
|-----------|------|------|--------|------|
### Security Observations
[4-8 bullets, ONE line each: `**Lead-in** — short fact + file/commit ref.`]
### Cross-Reference Synthesis
[2-4 bullets connecting git signals to Sections 2-3, one line each, using → to compress
cause and effect. Do not restate the findings.]
---
## 7. Static Analysis Summary
| Tool | High | Medium | Low | Notes |
|------|-----:|-------:|----:|-------|
| Slither | | | | [after false-positive triage] |
| Aderyn | | | | |
### Findings Requiring Manual Review
[Tool-flagged items needing human or agent judgment. Route to `slither-analyst`.]
---
## X-Ray Verdict
**[TIER]** — [one sentence justification]
### Readiness Criteria
| Criterion | Status | Notes |
|-----------|--------|-------|
| Build succeeds | | |
| Tests pass | | |
| Coverage measured on core | | |
| NatSpec on public functions | | |
| Invariants documented | | |
| Known issues listed | | |
| No critical Slither findings | | |
| Architecture documented | | |
### Structural Facts
1. [Verifiable structural fact — nSLOC across N subsystems, N upgradeable contracts, 2 devs wrote N%]
[3-5 items. ONLY measurable facts from Sections 1-7. No security claims, no speculation about
what "could" happen, no bug hypotheses, no attack scenarios. The verdict describes structural
posture — tests, docs, access control, complexity — NOT security. The auditor forms their own
security conclusions.]
### Top Concerns for Auditors
1. [Highest priority — what to look at first]
### Recommended Audit Mode
[Light / Core / Thorough — based on complexity, TVL risk, and readiness tier.]The tier is the lowest level across Tests, Docs, and Access Control. If Code Hygiene shows TODOs in security-critical paths, drop one tier. Absence of TODOs does NOT raise the tier.
| Tier | Tests | Docs | Access Control |
|---|---|---|---|
| EXPOSED | 0 test functions found | No NatSpec, no spec | Unclear roles |
| FRAGILE | Unit only | Sparse NatSpec | Roles exist, no timelock |
| ADEQUATE | Unit + fuzz OR invariant | NatSpec present | Roles plus clear boundaries |
| HARDENED | Unit + fuzz + invariant | Plus spec/whitepaper | Plus timelock or multisig |
| FORTIFIED | Plus formal verification | Plus thorough inline comments | Plus emergency pause |
The Tests tier is based on test existence from the Step 1 file scan, NOT on whether tests pass at runtime. If enumeration found 23 unit test functions, the Tests signal is "unit tests exist" regardless of compilation failures.
Map the tier to a recommended audit mode: EXPOSED or FRAGILE means the codebase is not audit-ready — recommend remediation before engaging, and run Light mode for early feedback. ADEQUATE maps to Core. HARDENED or FORTIFIED maps to Thorough, since the cheap findings are already gone and the remaining bugs are deep.
# Entry Point Map
> [Protocol] | [N] entry points | [N] permissionless | [N] role-gated | [N] admin-only
---
## Protocol Flow Paths
[Order entry points into expected execution flows — the story from deployment to steady state.
Group by actor. Trace backwards from each destination to deployment. Simple arrow chains, no
boxes. Branch with tree characters. Reference earlier flows instead of repeating them. Annotate
non-function preconditions with the left-arrow marker. 15-30 lines total.]
### Setup (Owner)
`initReserve()` → `setLeverager()` → `initVault()` → `setLeverageParams()`
### User Flow
`[owner setup above]` → `Lender.deposit()` → `openPosition()` ◄── liquidity must exist
├─→ `withdraw()`
└─→ `liquidatePosition()` ◄── position unhealthy
### Maintenance (Keeper)
`[deposit above]` → [rebalanceInterval passes] → [price in range] → `rebalance()`
---
## Permissionless
[Full detail block per entry point regardless of count. Sorted by value flow: tokens-in first,
tokens-out second, no movement last.]
### `Contract.functionName()`
| Aspect | Detail |
|--------|--------|
| Visibility | [external/public], [nonReentrant if present] |
| Caller | |
| Parameters | [name (user-controlled), name (protocol-derived)] |
| Call chain | `→ Contract.fn() → Contract.fn()` |
| State modified | |
| Value flow | [sender → Vault / Vault → recipient / None] |
| Reentrancy guard | [yes/no] |
---
## Role-Gated
[Group by role, then by value flow. Same detail block. If the protocol has >30 entry points
total, use compact tables here instead of per-function blocks.]
---
## Admin-Only
| Contract | Function | Parameters | State Modified |
|----------|----------|------------|----------------|
---
## Initialization
[One-time `initialize()` / `reinitializer()` entry points. Still attackable during deployment.]entry-points.md is purely structural. No threat analysis, no invariants, no git history, no test analysis — those belong in the readiness report.
# Invariant Map
> [Protocol] | [N] guards | [N] inferred | [N] not enforced onchain
---
## 1. Enforced Guards (Reference)
Per-call preconditions. Heading IDs are anchor targets from x-ray.md attack surfaces.
#### G-1
`require(_fee <= 10)` · `Vault.sol:206` · caps protocol fee so share price cannot be siphoned by an admin fee spike
[Repeat for every guard. Exactly two lines each: the H4 heading, then one body line with three
middot-separated fields — verbatim predicate in backticks, file:line in backticks, purpose prose.
The purpose must explain WHY the guard exists / which invariant or trust boundary it enforces,
not restate what the predicate checks. Blank line between guards, no horizontal rules.]
[NatSpec-stated global invariants do NOT belong here — they route to §2/§3/§4 by shape.]
---
## 2. Inferred Invariants (Single-Contract)
#### I-1
`Conservation` · Onchain: **Yes**
> totalSupply == Σ balanceOf[user]
**Derivation** — Δ-pair: `Vault.sol:142` ↔ `Vault.sol:143`; confirmed at all 4 write sites of `totalSupply`
**If violated** — [consequence]
---
[Repeat for every inferred invariant. Fields separated by blank lines. Category and Onchain
status sit between the heading and the claim so status is scannable.]
---
## 3. Inferred Invariants (Cross-Contract)
#### X-1
Onchain: **No**
> [what the caller assumes about the callee's return value or state]
**Caller side** — `Caller.sol:LN` — [how the value is used]
**Callee side** — `Callee.sol:LN` — [write sites that could break the assumption]
**If violated** — [consequence]
---
## 4. Economic Invariants
#### E-1
Onchain: **No**
> [economic property]
**Follows from** — `I-4` + `X-1`
**If violated** — [consequence]Rules for invariants.md:
<a id> anchors inside table cells do not work cross-file.Δ-pair: Contract.sol:Lx ↔ Contract.sol:Ly; guard-lift: <verbatim guard> + <write-site enumeration> (a single-callsite guard is not a valid lift); edge: State@Lx → State@Ly; temporal: <verbatim check>; NatSpec: Contract.sol:LN — "<verbatim comment>". Blocks that cannot cite one are dropped. No "implied by semantics."Follows from.invariants.md#... fragment matches a heading that actually exists in the file you just wrote, and that the fragment is lowercase.After all files are written and cleanup is done, read the ## X-Ray Verdict section from the generated x-ray/x-ray.md and print it verbatim to the terminal. Do NOT paraphrase, summarize, or rewrite — copy the exact tier, justification, and structural facts as they appear in the file.
X-Ray feeds directly into the EVM Cortex audit pipeline:
| Agent | Receives from X-Ray | Uses It For |
|---|---|---|
scout | Request to explore codebase | File discovery before X-Ray runs; Path B subsystem readers |
audit-orchestrator | Verdict tier + full report | Scoping audit mode, routing depth agents |
invariant-analyst | invariants.md with I-N / X-N / E-N IDs | Deepening the invariant set, especially Onchain: No blocks |
invariant-tester | Invariant IDs + entry points | Foundry invariant suites named after the IDs |
access-control-reviewer | Actors table + Step 2d centralization analysis | Privilege escalation and centralization review |
oracle-analyst | Composability dependency map | Oracle staleness, deviation, and sequencer checks |
mev-analyst | Permissionless entry points + value flows | Front-running and sandwich exposure |
slither-analyst | Static analysis output + scope table | Triaging findings against X-Ray context |
depth-state-trace | Entry points + delta writes | Tracing state mutations through high-risk paths |
depth-token-flow | Fund flow functions + token assumption gaps | Verifying token accounting correctness |
depth-edge-case | Entry points + invariants | Enumerating boundary conditions |
depth-external | External call classification | Reentrancy and callback risk analysis |
storage-layout-analyst | Upgrade Architecture Concerns | Storage gap and collision verification |
code-reviewer | Scope table + architecture diagram | Focused review with full context |
poc-writer | Onchain: No invariant blocks + attack surfaces | Exploit PoCs that falsify a specific invariant ID |
security-verifier | PoC candidates + severity inputs | Fork-test verification |
scribe | Full X-Ray report | Generating the final audit report |
1. scout → explores codebase, identifies src layout
2. xray-pre-audit → generates x-ray/ folder (report, entry points, invariants, diagram)
3. audit-orchestrator → reads verdict tier, picks Light/Core/Thorough
├── Onchain:No conservation blocks → depth-token-flow
├── delta writes + entry points → depth-state-trace
├── boundary invariants → depth-edge-case
├── external call classification → depth-external
├── Actors + centralization analysis → access-control-reviewer
└── dependency map → oracle-analyst
4. invariant-tester → tests named for I-N / X-N IDs
5. poc-writer → PoCs falsifying specific Onchain:No blocks
6. security-verifier→ fork-verifies each PoC
7. scribe → final report, invariant IDs carried throughx-ray.md. Protect the threat model, invariant pointer, test gaps, git analysis, and verdict; compress other sections if needed. Detail goes to entry-points.md and invariants.md.file:line reference already shows. Code references carry the evidence; prose must not duplicate them.poc-writer's jobformal-verifier's jobprotocol-qa's jobRun this checklist to determine whether a codebase is ready for audit. X-Ray populates the answers, and the results drive the verdict tier.
forge build succeeds with zero warningsforge test passes — all tests greenforge coverage runs without error (and the number in the report was actually produced)foundry.toml properly configured (src, test, script paths)@notice, @param, @return)require stringsforge fmt produces no diffReentrancyGuard on functions with external callsSafeERC20 used for all token interactions© ccashwell, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. The last 2 KB are left out here. Raw file
SKILL.md and 4 other files (scripts) in skills/xray-pre-audit of ccashwell/evm-cortex.
Open the folder on GitHubat commit f8f3301
Xray Pre Audit next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Xray Pre Audit this skillccashwell/evm-cortex | 131 | — | ~25k | Automated safety check: Pass | MIT | |
| X Raypashov/skills | 1.2k | 1 repos | ~10k | Automated safety check: Pass | MIT | |
| Audit PrepPlamenTSV/plamen | 303 | — | ~3.7k | Automated safety check: Pass | MIT | |
| Security Scanericrisco/rsc-harness | 174 | — | ~2.8k | Automated safety check: Notes | MIT | |
| Repo SentinelMathews-Tom/armory | 328 | — | ~2.2k | Automated safety check: Pass | MIT | |
| Security Ownership Mapdiegosouzapw/awesome-omni-skills | 159 | — | ~4.6k | Automated safety check: Pass | Apache-2.0 |
pashov/skills
Generates an x-ray.md pre-audit report covering overview, enhanced threat model (protocol-type profiling, git-weighted attack surfaces, temporal risk analysis, composability dependency mapping)…
PlamenTSV/plamen
Prepare Solidity projects for a security audit — test coverage, test quality, NatSpec docs, code hygiene, dependency health, best-practice enforcement, deployment readiness, and project…
ericrisco/rsc-harness
A skill your agent uses when automated scanners drive a security sweep of a repo or app — SAST, dependency/lockfile CVEs, secrets in the tree or git history, IaC misconfig — and the raw output has…
Mathews-Tom/armory
Full security audit for public repositories across 12 attack surfaces: git history, secrets, CI/CD, containers, dependencies, licenses.
diegosouzapw/awesome-omni-skills
Security Ownership Map workflow skill. An agent skill from diegosouzapw/awesome-omni-skills.
davila7/claude-code-templates
Senior ISMS Audit Expert for internal and external information security management system auditing.
ccashwell/evm-cortex
A skill your agent uses when integrating with Aave V3 for lending, borrowing, flash loans, or building on top of Aave markets.
ccashwell/evm-cortex
Access control design patterns for Solidity protocols. An agent skill from ccashwell/evm-cortex.
ccashwell/evm-cortex
A skill your agent uses when running a local Ethereum node with Anvil.
ccashwell/evm-cortex
A skill your agent uses when performing systematic breadth-first review of all contracts during a security audit.
ccashwell/evm-cortex
A skill your agent uses when performing deep analysis of specific findings or high-risk areas during a security audit.
ccashwell/evm-cortex
A skill your agent uses when preparing a codebase for security audit.
Categories
A skill your agent uses when preparing for a security audit, performing reconnaissance on a new codebase, or creating a protocol overview. Xray Pre Audit is an agent skill from ccashwell/evm-cortex. Use when preparing for a security audit, performing reconnaissance on a new codebase, or creating a protocol overview.
Xray Pre Audit fits situations like: preparing for a security audit; performing reconnaissance on a new codebase; creating a protocol overview.
Run `npx skills add ccashwell/evm-cortex --skill xray-pre-audit -a claude-code`. Or copy the skill folder (skills/xray-pre-audit in ccashwell/evm-cortex) into .claude/skills/xray-pre-audit in your project. Claude Code loads it when a task matches its description.
Run `npx skills add ccashwell/evm-cortex --skill xray-pre-audit -a codex`. Or copy the skill folder (skills/xray-pre-audit in ccashwell/evm-cortex) into .agents/skills/xray-pre-audit in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add ccashwell/evm-cortex --skill xray-pre-audit -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/xray-pre-audit, .gemini/skills/xray-pre-audit, .github/skills/xray-pre-audit and .opencode/skills/xray-pre-audit in your project.
Going by SKILL.md and its folder, Xray Pre Audit needs Python and a shell for the scripts in its folder and the command-line tools its instructions call (python3, git, bash, curl and npx). Our summary lists: Python 3; Node.js; A Bash shell.
SKILL.md names 2 domains. In commands or code: github.com and raw.githubusercontent.com; the agent is likely to contact these when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. The check reads SKILL.md only: the scripts in the folder are not scanned, so read them before running anything.
Xray Pre Audit is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 25k tokens (SKILL.md is roughly 99k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Xray Pre Audit: X Ray (pashov/skills, 1.2k stars), Audit Prep (PlamenTSV/plamen, 303 stars), Security Scan (ericrisco/rsc-harness, 174 stars) and Repo Sentinel (Mathews-Tom/armory, 328 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
ccashwell (a GitHub user) maintains it in ccashwell/evm-cortex, which has 131 GitHub stars. The repository holds 89 skills in this directory. The repository was last updated on September 30, 2026.
Source: ccashwell/evm-cortex on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.