mcpField
X
x-ray

x-ray

github.com/pashov安全1.3k326 次安装1 周前更新
代码仓库

关于 x-ray

x-ray 是用于分析 Solidity 项目并在 `x-ray/` 目录生成安全报告的技能,涵盖源码审查、入口分类、Git 历史分析与威胁评估。

主要功能

  1. 01三阶段顺序审计流程
  2. 02Solidity 源码与入口分析
  3. 03Git 安全及变更分析
  4. 04覆盖率与规格文档审查
  5. 05生成结构化安全报告

适用场景

  1. 01审查 Solidity 代码库的安全风险
  2. 02追踪访问控制与合约入口
  3. 03评估仓库历史和依赖变更

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/pashov/skills --skill x-ray

安装后在 Claude Code 中输入/x-ray确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
VERSION
references/
templates.md
threats.md
scripts/
analyze_git_security.py
entry_points.py
enumerate.sh
generate_svg.py

详细介绍

X-Ray

在项目根目录生成一个 x-ray/ 文件夹,其中包含所有输出文件。流水线:分 3 步,始终按顺序执行。

$SKILL_DIR = 包含此 SKILL.md 文件的目录。根据加载此技能时所用的路径确定其位置(例如,如果此文件位于 /path/to/x-ray/SKILL.md,则 $SKILL_DIR = /path/to/x-ray)。

进度跟踪(必须)

开始任何操作之前,调用 TodoWrite,创建以下 3 个待办事项(状态均为 pending):

  1. Phase 1: Enumerate & measure codebase
  2. Phase 2: Read sources, classify entry points, synthesize invariants
  3. Phase 3: Write x-ray report files

状态转换(通过 TodoWrite 更新 — 绝不批量更新):

  • 在运行 enumerate.sh 之前,立即将 Phase 1 标记为 in_progress。
  • Step 1 的并行批处理返回后,在一次 TodoWrite 调用中将 Phase 1 标记为 completed,并将 Phase 2 标记为 in_progress。
  • Step 2(包括 2b–2g)完成后,在一次 TodoWrite 调用中将 Phase 2 标记为 completed,并将 Phase 3 标记为 in_progress。
  • 写入所有 Step 3 输出文件后,将 Phase 3 标记为 completed。

规则:任意时刻恰好只能有一个待办事项处于 in_progress 状态。阶段开始或结束时,立即更新状态。

步骤 1:枚举与测量

如果用户指定了路径,则将其用作项目根目录。否则使用当前工作目录。如果根目录下没有 .sol 文件或 foundry.toml/hardhat.config.*,则检查下一级目录。

源目录检测:从 foundry.toml(src = "...")或 hardhat.config.* 自动检测。如果没有配置,则尝试 src/ 和 contracts/。如果存在 foundry.toml,先读取它。

运行枚举(单次 Bash 调用 — 包含创建输出目录):

mkdir -p [project-root]/x-ray && bash $SKILL_DIR/scripts/enumerate.sh [project-root] [src-dir]

紧接着,在同一条消息中并行启动以下所有任务:

0. 版本检查(前台):

  • 从 $SKILL_DIR/VERSION 读取本地 VERSION 文件
  • Bash curl -sf https://raw.githubusercontent.com/pashov/skills/main/x-ray/VERSION
  • 如果成功获取远程 VERSION 且与本地版本不同,则打印 ⚠️ You are not using the latest version. Please upgrade for best security coverage. See https://github.com/pashov/skills。如果获取失败,则静默跳过。

1. 覆盖率(run_in_background: true):

对于 Foundry:

cd [project-root] && forge coverage 2>&1 || (echo "RETRYING_WITH_IR_MINIMUM" && forge coverage --ir-minimum 2>&1)

对于 Hardhat:

cd [project-root] && npx hardhat coverage 2>&1

如果工具链未安装(例如,forge: command not found、npx: command not found、缺少 node_modules/),覆盖率命令将失败。这是预期情况 — 上一步的枚举已记录测试是否存在。覆盖率失败并不意味着没有测试。

2. Git 安全分析 + JSON 读取(前台,单次 Bash 调用):

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.json

JSON 包含 7 个部分:repo_shape、fix_candidates、dangerous_area_changes、late_changes、forked_deps、tech_debt、dev_patterns。

3. 预加载参考文件(并行执行 2 次 Read 调用 — 这些内容必须在步骤 2d/3a 之前进入上下文):

  • $SKILL_DIR/references/threats.md — 威胁特征、时间性威胁、可组合性威胁
  • $SKILL_DIR/references/templates.md — 输出模板、入口点模板、架构指南

4. 规范/白皮书检测(1 次 Glob:**/{whitepaper,spec,design,protocol,architecture,overview,README}*.{pdf,md},排除 node_modules/、lib/、x-ray/、test/)。跳过面向用户的文档(教程、API 参考、变更日志、贡献指南)。然后根据文档大小进行处理:

  • 路径 A(≤5 份文档,且每份 ≤300 行): 在步骤 2 的并行消息中将它们作为 Read 调用。直接读取 — 无需子 Agent。
  • 路径 B(>5 份文档,或任意文档 >300 行): 启动一个子 Agent(model: "sonnet"),读取所有文档并返回结构化提取结果(最多 200 行)。子 Agent 提示词:
    Read each doc file listed below. Extract ONLY security-relevant information into this format:
    Files: [list of doc file paths]
    
    Return this exact structure:
    ### Doc-Stated Global Invariants
    [Bullet list of every invariant, constraint, or guarantee the docs claim must hold globally across calls. Treated like NatSpec-stated invariants by Step 2g — routed directly to §2 / §3 / §4 of invariants.md by shape, NOT into §1 (Enforced Guards, which is per-call preconditions only).]
    ### Actor Definitions
    [Each actor/role with stated permissions and trust level]
    ### Trust Assumptions
    [What the protocol assumes about external systems, oracles, admins, users]
    ### Cross-System Flows
    [How value/data moves between contracts or external systems]
    ### Economic Properties
    [Fee structures, reward mechanisms, tokenomics, bounded parameters]
    ### Key Design Decisions
    [Explicit "we chose X over Y because Z" statements]
    
    Rules: Quote the source doc for each claim. Omit sections with no relevant content. Max 200 lines total.
    
    将此子 Agent 纳入步骤 1 的并行消息。其输出将用于撰写步骤 3 的报告。

对于两条路径,仅提取:文档中声明的全局不变量、参与者定义、跨系统流程、信任假设、经济属性、关键设计决策。在报告中,所有源自规范的声明都标注 (per spec),以便审计人员区分代码验证的内容与规范声明的内容。文档中声明的全局不变量将用于步骤 2g 的 NatSpec 路由步骤 — 根据其形式路由至 invariants.md 的 §2 / §3 / §4,而不是 §1(强制执行的守卫)。

所有调用(覆盖率、Git 分析、参考文件读取、规范 Glob)都必须出现在同一条消息中。无需等待 forge 覆盖率完成,即可继续步骤 2。

步骤 2:读取源文件 + 入口点扫描(单条消息,所有工具调用并行)

重要:每个工具调用 — Bash、Agent、Read、Grep — 都必须在一条消息中发出,以便并发运行。这包括源文件读取、入口点扫描,以及步骤 1 中检测到的任何规范文档(它们彼此独立)。

范围筛选
  • 跳过接口:interfaces/ 目录或文件名以 I + 大写字母开头
  • 跳过厂商库:Uniswap FullMath/TickMath、OZ 副本
  • 如无法确定,则纳入,但从范围表中排除
路径 A:≤20 个源文件(直接读取)

每个文件调用一次 Read。不要读取 README、文档或 foundry.toml(已在步骤 1 中读取)。

**每个文件提取:**合约类型与继承关系、角色与访问控制、持有价值的状态变量、外部调用、资金流、不变量注释、assert/require、向后兼容性指标(见下文)、增量写入(逐个函数记录:存储变量及其应用的符号增量——例如 Δ(totalSupply) = +shares, Δ(balanceOf[msg.sender]) = +shares——仅限同一基本块,不跨函数推断;只有在对 balanceOf/_totalSupply 的影响在语义上明确时,才能解析 OZ _mint/_burn 等继承的辅助函数)、守卫谓词(每个引用存储变量的 require/assert/if-revert,逐字引用并标注行号;跳过仅引用函数参数的守卫)、枚举/单次转换(记录每一组 require(state == X); ...; state = Y,格式为 X@Lx → Y@Ly——包括 require(addr == address(0)); addr = concrete 这类单次锁存转换)。

路径 B:>20 个源文件(并行子 Agent)

**第 1 层——小文件(≤120 行):**使用单次 Bash cat 调用批量读取。

**第 2 层——大文件(>120 行):**按子系统分组。每个子系统启动一个子 Agent(model: "sonnet",最多 5 个,每个约 10 个文件)。子 Agent 提示词:

Read each file listed below and return a structured summary. Do NOT analyze — just extract facts.
Files: [list of file paths]
For EACH file, return this exact format:
### [filename]
- **Type**: contract | library | abstract
- **Inherits**: [parent contracts]
- **Imports**: [imported libraries/contracts]
- **Roles/Access**: [onlyOwner, role constants, modifiers]
- **State vars (value-holding)**: [mappings/vars that hold balances, collateral, etc.]
- **External calls**: [calls to other contracts, ERC20 transfers, etc.]
- **Fund flows**: [deposit/withdraw/mint/burn/transfer paths]
- **Invariants**: [require/assert statements, NatSpec invariant comments]
- **Delta writes**: For EACH non-view non-pure function, list storage variables that change and the symbolic delta applied. Use format `Δ(var) = +expr` or `Δ(var) = -expr`. Only report pairs where BOTH writes appear in the same function body with no intervening call to an unknown external contract. Do NOT chase writes through inherited/imported functions unless the semantic effect is unambiguous (OZ `_mint` → `balanceOf` + `_totalSupply` is fine; custom internal helpers are NOT — list those deltas only in the internal helper's own entry). Example:
  - `deposit()`: `Δ(totalSupply) = +shares`, `Δ(balanceOf[msg.sender]) = +shares`
  - `borrow()`: `Δ(totalBorrows) = +amount`, `Δ(underlyingBalance) = -amount`
- **Guard predicates**: Every `require`/`assert`/`if-revert` in the file that references a storage variable. Quote verbatim with 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 pattern of `require(var == X); ...; var = Y` where `var` is a storage enum, uint, or address. Record as `X@Lx → Y@Ly`. Include one-shot latches like `require(addr == address(0)); addr = concrete`.
- **Key logic**: [1-2 sentences on what the contract does]
- **Function-level access map** (REQUIRED for contracts, skip for libraries):
  List every public/external non-view non-pure function with its access control:
  - `functionName()` — [modifier name, e.g. `onlyRole(OCT_KEEPER)`] or [NONE — permissionless]
  For functions with NO modifier, also list which external calls they make:
  - `functionName()` — NONE — calls `ContractName.method()`
入口点扫描(与源文件读取一并在同一条并行消息中执行)

在与上述源文件读取同一条消息中启动此 Bash 调用——它独立运行,可并发执行:

python3 $SKILL_DIR/scripts/entry_points.py [src-dir]

该脚本会在清除注释和字符串字面量后,完整读取每个函数头——从 function(或 receive/fallback)到其左花括号 {。不要改用逐行 grep:格式化工具经常会将函数头拆分到多行(forge fmt 的默认 multiline_func_header = "attributes_first" 会把 external 放在单独一行;每行一个参数的函数头则会把 function name( 与 ) external 分隔到数行),而面向行的模式会悄然漏掉这些函数。脚本只需 python3,步骤 1 的 git 分析也会用到。如果脚本无法运行,请自行读取每个范围内文件的函数头(不要依赖子 Agent 摘要)并整理列表。

每个入口点输出一行,格式为 path:line: [abstract|library] Container.name(params) attributes,随后输出 # N entry point(s) in M of K .sol file(s) 汇总。如果 K 为 0,请先检查 [src-dir] 再继续。无法读取的文件或目录会报告到 stderr,并计入汇总。脚本会列出所有非 view/非 pure 的 external 或 public 函数,包括 receive、fallback 和初始化函数;跳过接口、无函数体声明、internal/private 函数、自由函数,以及路径包含 /interfaces/ 或 /mock/ 的文件。标记说明:

  • [abstract]——定义于抽象合约中:将其归属到所有继承它的具体合约,除非继承层级中更下层的合约重写了该函数(此时适用重写函数的修饰符)。
  • [library]——库函数,不是入口点:直接调用非 view 库函数会回滚,因此只能通过调用它们的合约访问。将它们视为下游调用。
  • // ordinary function named receive(或 fallback)——只是恰好以该名称命名的普通函数;它不会接收普通 ETH 转账,也不会处理未知调用。

所有工具调用(源文件读取/Bash/子 Agent、入口点扫描)必须在同一条消息中执行。

不要读取测试文件或文档文件。

第 2b 步:入口点分类

使用第 2 步并行消息中已返回的入口点扫描结果,对所有入口点进行分类。不要仅依赖子 Agent 摘要——子 Agent 会按合约层面提取事实,可能会错误归因哪个函数发起了哪个外部调用,或哪个函数具有哪个修饰符。

从入口点中排除:view/pure 函数、仅接口声明、库内部函数(它们是下游调用,不是入口点)、模拟合约。

将每个结果归入以下三类之一:

  1. 无权限限制——没有访问控制修饰符,且函数体内没有内部调用者限制。将函数分类为无权限限制前,必须检查函数体。对于 路径 A(≤20 个文件),第 2 步读取的内容已在上下文中——直接分类,无需额外的 Read 调用。对于 路径 B,将所有候选函数体读取合并到一条并行消息中。查找以下任何限制调用者的模式:
    • require(msg.sender == X) 或 if (msg.sender != X) revert ...
    • if (msg.sender != X || ...) revert ...(复合条件)
    • 调用检查 msg.sender 的内部函数 没有修饰符但有内部 msg.sender 检查的函数属于角色限制,而非无权限限制。常见示例:acceptOwnership()、acceptMsig()、confirmX()——这些函数通常没有修饰符,但会通过 if (msg.sender != pendingX) revert 将调用者限制为特定的待定地址。
  2. 角色限制——具有角色修饰符(onlyRole(X)、onlyOwner、onlyRouter 等),或存在内部 msg.sender 限制(通过 require、if/revert 或委托检查)。记录所需角色或地址。
  3. 仅管理员——由 DEFAULT_ADMIN_ROLE、指向协议管理员的 onlyOwner 或类似的顶级权限控制。

注意:单独使用 nonReentrant 不属于访问控制。initializer/reinitializer 是一次性部署函数——需单独跟踪。

对每个入口点,记录:

  • 合约名称和函数名称
  • 访问级别(无权限限制 / 角色名称 / 管理员)
  • 调用者(用户、Keeper、管理员、LP 等)
  • 参数及信任级别:(user-controlled)、(user-signed)、(keeper-provided)、(protocol-derived)
  • 调用链:使用子 Agent 摘要和函数级访问映射追踪下游调用。格式:→ Contract.fn() → Contract.fn()
  • 修改的状态:发生变化的存储变量/映射
  • 价值流:in(存入代币)、out(提取代币)、none
  • 重入保护:是/否

这些数据将用于两项输出:

  • x-ray.md(第 2 节)中的无权限限制入口点列表——仅使用无权限限制的子集
  • 完整的 entry-points.md 文件(第 3c 步)——包含所有类别
第 2b 步流程:协议流程路径构建

使用第 2b 步已收集的入口点数据,为 entry-points.md 构建流程路径。这不是单独的分析阶段——只是重新整理已有数据。

对于每个面向主要用户的入口点(无权限限制和会转移价值的角色限制函数):

  1. 找出其 require 语句和状态变量检查
  2. 对于每项检查,查找写入该状态变量的函数(已知于其他入口点的“修改的状态”字段)
  3. 反向串联:目标 ← 写入其前置条件的函数 ← 写入该前置条件的函数 ← ... ← 部署
  4. 使用 ◄── 注释标记非函数前置条件(时间推移、市场状况、外部状态)

输出:按参与者流程分组的简单箭头链。引用前面的流程,不要重复。总计 15–30 行。确切格式请参见 entry-points.md 模板中的“协议流程路径”部分。

入口点扫描是一个硬性门槛:报告中的无权限限制入口点部分必须与经过扫描验证的列表一致,而非子 Agent 摘要。如果两者冲突,以扫描和代码阅读结果为准。

步骤 2c:向后兼容代码检测

阅读源文件时,留意那些看起来是已移除机制的残留代码,其保留目的在于避免剩余代码库出错。常见信号包括:函数体为空或仅包含简单操作、声明后从未被有意义地读取或写入的状态变量、包含「deprecated」「legacy」「backwards compat」或「no longer used」的注释、实现接口但始终返回默认值的函数,以及仅为代理存储布局兼容性而保留的存储变量。

阅读完所有源文件后,在将候选项归类为向后兼容代码之前,请对照以下强制验证检查逐一核实。必须在一条并行消息中批量执行所有候选项的调用方检查 Grep 调用,不得逐个验证:

  1. 调用方检查(必需):使用 Grep 确认当前代码库中没有活跃调用方调用该函数或变量。如果它在活跃代码路径中被调用,则它不是向后兼容代码,而是当前设计的一部分,无论它是否返回默认值或零值。
  2. NatSpec/注释检查(必需):如果代码中有 NatSpec 或行内注释说明其行为方式的原因(例如「为 X 模式简化」「按设计如此」「特意设为零」),则这是有文档说明的有意设计,不是向后兼容代码。不要用启发式模式匹配推翻开发者明确记录的说明。
  3. 接口义务检查:如果某个函数因接口要求而返回默认值,并且仍被活跃调用,则它属于当前架构的一部分,而不是残留代码。

只有同时满足以下所有条件时,才能将代码归类为向后兼容代码:(a) 不存在活跃调用方;(b) 没有 NatSpec/注释将该行为说明为有意设计;(c) Git 历史表明其所属机制已被移除。

不要在报告中将向后兼容代码描述为活跃功能。相反,应在第 1 节(见输出模板)中明确指出这些代码,让审计人员了解代码库中哪些部分是为兼容性而保留的,而非实际运行的功能。如果经过上述验证后不存在仍保留的向后兼容代码,则完全省略该小节。

步骤 2d:中心化与暂停覆盖分析

阅读源文件并分类入口点后,执行两项分析,为 Actors 表、信任边界和关键攻击面(第 2 节)提供依据。这些分析不是独立章节——其结果应整合到现有报告章节中。

中心化分析——对于每个特权角色(admin、owner、operator、keeper、service 等):

  1. 列出该角色可以执行的所有操作(参照函数级访问权限映射)。
  2. 对于每项操作,注明是否设有 timelock、多签或延迟。请区分角色转移延迟(例如,AccessControlDefaultAdminRules 的 1 天延迟)和操作性操作延迟——两者并不相同。角色转移延迟无法防止被攻陷的持有者使用即时操作函数。
  3. 指明哪些操作可以提取或转移用户资金(例如,emergencyWithdraw、setTreasury、transferFee)。

整合到:Actors 表(Capabilities 列应明确说明哪些操作是即时执行、哪些受 timelock 限制)、信任边界(说明每个边界实际保护的内容,以及哪些操作可以绕过保护)、关键攻击面(以“管理员操作权限”或“[Role] 被攻陷”为主题——攻击面是角色被攻陷,而非单个函数)。

暂停覆盖分析——对于每个关键状态变更函数:

  1. 检查是否应用了 whenNotPaused(或等效机制)。
  2. 注明哪些函数可暂停,哪些不可暂停。
  3. 如果逻辑上应可暂停的函数实际上不可暂停(例如,由权限受限的角色调用、且会操作用户资金的函数),请将此发现整合到该角色的相关攻击面中。缺少暂停机制本身不是攻击面——它只是会加剧该角色被攻陷时风险的因素。

反模式:不要创建独立的“中心化风险”小节。 中心化详情应分散在 Actors、信任边界、关键攻击面和协议类型关注点中。单独设置专门章节会重复已有章节中的信息。暂停覆盖分析也一样——应整合到相关角色的攻击面描述中。

步骤 2e:协议分类

阅读源代码后,按照 references/threats.md 中的流程对协议进行分类(类型检测 + 混合分类、阶段检测和外部调用分类,均在同一个文件中)。

步骤 2f:nSLOC

在报告标题和范围表中使用步骤 1 enumerate 输出中的准确 nSLOC TOTAL(不带 ~ 前缀)。

步骤 2g:不变量综合

使用步骤 2 中提取的 delta 写入、守卫谓词、枚举值/一次性状态转换和不变量注释(来自路径 A 的直接读取,或路径 B 的子 Agent 输出),系统地遍历以下分类法,生成候选不变量。这是一个推理步骤——无需调用新工具(以下步骤 2 的 Pass B 中的 Grep 批量搜索除外)。

术语:守卫是单个调用点强制执行的逐次调用前置条件(例如,require(amount >= MIN))。它不是可证伪的不变量——代码会在该调用点保证此条件成立。不变量是必须在任意调用序列中全局成立的属性(例如,“每个活跃仓位均 ≥ MIN”)。守卫仅供 §1 的 invariants.md(已强制执行的守卫参考)使用。从守卫提升而来的不变量(见下方步骤 2)或 NatSpec 中声明的不变量,供 §2 / §3 / §4 使用。 NatSpec 路由(在结构化遍历前运行):对于每个 NatSpec @invariant 标签或断言全局属性的行内注释(例如,“totalSupply 始终等于 Σ balances”、“fee 永不超过 MAX_BP”、“同一时间最多只有一个活动 epoch”),按类别形态(守恒 / 边界 / 比率 / 状态机 / 时序)直接路由至 §2(如果属性跨越多个合约或由多个原语推导而来,则路由至 §3/§4)。来源标签:NatSpec: Contract.sol:LN。不要将开发者声明的全局不变量放在 §1 中——§1 仅包含逐次调用的守卫谓词。完成路由后,仍需运行以下结构扫描——这些扫描可能证实(链上=是)或反驳(链上=否)NatSpec 声明。

遍历顺序(每一步都使用原始提取数据,而非前一步的结论):

  1. 守恒扫描:对每个函数,查找同一函数体中的增量写入对,其中 Δ(A) = +expr 且 Δ(B) = -expr(或者映射对应项的 Δ(B) = +expr)。每组匹配对都是一个守恒候选:A + B = const 或 A == Σ B[key]。

    • 对于映射写入(mapping[key] += e 与 scalar += e 配对),推断 scalar == Σ mapping[key]。验证该模式是否适用于写入任一变量的所有函数——如果有任何函数只写入其中一个变量,则将差异记为“部分守恒”,并拆分为是 / 否两行。
    • 对于转账模式(mapping[from] -= e、mapping[to] += e,且标量未变化),确认映射总和具有自守恒性。
    • 负向守恒(重要):如果某个理应跟踪资金流的函数(例如,闪电贷提取 / 返还、接收 / 转发)存储增量为零,则将其记录为守恒负向发现。缺少增量本身就是一种不变量观察。
  2. 守卫提取与提升(对每个 require/assert/if-revert 执行两轮扫描):

    A 轮——原样提取(强制执行的守卫参考):每个 require/assert/if-revert 都要在 invariants.md 的 §1 中成为一个 G-N 行。逐字引用谓词并注明源位置。这是逐次调用前置条件的机械摘录——不可证伪,也不进行模糊测试。跳过仅引用函数参数、未关联存储且没有全局含义的守卫(纯粹的本地输入验证,除 B 轮外没有审计价值)。

    B 轮——提升为全局属性,然后检查所有写入位置:针对每个守卫,询问:“这是否意味着一个必须在任意调用序列中都成立的属性,而不仅仅是在此调用点成立?”

    • 如果否(守卫仅约束函数消耗的临时参数,且未关联持久化存储)→ 仅保留在 §1 中。不要提升。
    • 如果是(守卫意味着一个持久化属性——例如,存款中的 require(amount >= MIN) 意味着“每个活动头寸 ≥ MIN”;设置器中的 require(_fee <= 10) 意味着“fee ∈ [0, 10]”)→ 将守卫改写为全局属性,然后使用 Grep 在作用域文件中按变量名查找受约束存储变量的所有写入位置。将所有已提升守卫的写入位置 Grep 合并到一条并行消息中——不要逐个验证:
      • 如果所有写入位置都执行等效守卫 → 将其提升至 §2,作为链上=是的边界不变量。推导依据:引用该守卫 + 确认所有写入位置。
      • 如果任何写入位置在没有等效守卫的情况下写入该变量 → 将其提升至 §2,作为链上=否的边界不变量,并将未受守卫约束的写入位置列为差异。这是高信号输出——该差异同时是不变量和潜在漏洞。

    纳入设置器级别的边界条件,即设置器依据自身参数检查写入存储变量的情况。执行相同的全写入位置检查——如果多个设置器写入同一变量,但只有部分设置器执行边界检查,则该属性为链上=否。

  3. 比例扫描:对于每个形式为 A = B * C / D 的存储写入,其中 B、C、D 是存储变量或函数作用域内的存储快照,记录该比例。注明快照是在同一函数中的其他状态变更之前还是之后取得的(顺序很重要——例如,totalSupply 是在 _burn 之前还是之后取得快照)。

  4. 状态机/单次操作扫描:对于 require(var == X); ... var = Y 模式中的每个 enum/uint/address 变量,记录状态转换。区分:

    • 单次锁存:require(var == default); var = concrete,且不存在返回路径(例如 setStrategy、setLeverager)。
    • 可切换标志:require(var == false); var = true,但另一个函数会将其改回(例如 freeze/unFreeze、toggleVaultLeverage)。这不是状态机不变量——跳过。
    • 循环状态:由时间/条件驱动的 false → true → false(例如 ongoingVestingPosition)。记录为循环不变量。
  5. 时间扫描:对于涉及存储变量(deadline、lastUpdate、lockPeriod、interval)的每个 block.timestamp 或 block.number 比较,提取时间约束。注明约束是先检查后更新(安全),还是先更新后检查(可能读取过时值)。

  6. 跨合约扫描:对于返回值被用于算术运算或存储写入的每个外部调用,记录调用方的假设。然后查找被调用方对此状态的写入位置。如果被调用方可通过另一个函数独立更改该状态,则该假设未经验证——记录为链上=否的跨合约不变量。仅包括调用方假设和被调用方写入位置均在范围文件内的条目。不要推测范围外合约的行为。

    • 还要包括:设置器与不变量不匹配——管理员设置器写入存储值时,未检查现有不变量是否仍然成立(例如,setReserveCapacity 未检查当前流动性)。这类情况属于跨合约,因为设置器与不变量是在不同的合约/函数中。
  7. 经济推导:完成步骤 1–6 后,检查单合约不变量和跨合约不变量的组合是否能推导出更高阶属性。每个经济不变量都必须引用其推导所依据的具体 I-N / X-N ID。如果推导链存在缺口(某个源不变量的链上=否),则该经济不变量的链上状态也为否。

验证关卡(纳入任何推导不变量前必须执行):

  • 守恒:确认引用行中存在 Δ 配对(位于同一函数体内)。
  • 守卫(Pass A,§1 行):确认 require/assert/if-revert 与代码中的内容逐字一致。
  • 守卫提升(Pass B,§2 行):确认提升后的全局属性引用持久存储(而不仅仅是对临时参数的复述)。通过 Grep 确认已枚举受约束变量的所有写入位置,并确认链上=是/否的判定与枚举结果一致——如果有任何写入位置缺少守卫,而该行标记为链上=是,则该行无效。
  • NatSpec:确认引用位置存在逐字一致的 @invariant 标签或注释,且断言的是全局属性(而非单次调用的说明)。如果只是单次调用说明,则删除——不要转到 §2。
  • 比例:确认公式准确,并注明快照相对于同一函数中其他写入的顺序。
  • 状态机:确认状态边的两端均存在,并确认没有反向路径。如果存在反向路径,则属于可切换标志——删除。
  • 时间:确认比较涉及存储变量,而不只是 block.timestamp 与参数的比较。
  • 跨合约:确认调用方用法和被调用方写入位置均存在于范围内。
  • 经济:确认所有引用的 I-N / X-N ID 均已验证。
  • 如果无法验证 → 删除该行。“无法验证”不是有效条目。 输出:不变量候选项直接写入 invariants.md(步骤 3a)。x-ray.md 第 3 节包含强制执行的防护条件(参考)及排名前 3–5 的推断结果(优先列出守恒性分析中 On-chain=No、跨合约及防护条件提取遗漏;并纳入一条高信号的 Yes 行,用于结构性覆盖,例如比率或状态机闩锁)。

步骤 3:编写输出

测试存在性与覆盖率执行情况(重要)

测试是否存在由步骤 1 的枚举结果决定(test_files、test_functions、stateless_fuzz、foundry_invariant、echidna、medusa、hardhat_fuzz、fork、certora、halmos、hevm 计数)。这些是文件扫描结果,无论工具链能否编译或运行,都始终可靠。多信号类别(echidna、medusa、certora、halmos)以 functions:configs 的形式输出——例如,5:1 表示检测到 5 个测试函数和 1 个配置文件。

覆盖率指标(行覆盖率/分支覆盖率)来自 forge coverage 或 hardhat coverage,需要已安装依赖、编译成功且测试通过。覆盖率可能因许多与测试质量无关的原因而失败:

  • 未安装依赖(未运行 npm install / forge install)
  • 编译器错误(堆栈过深、版本不匹配)
  • 测试执行失败(缺少 RPC、分叉配置)

规则:

  1. 对所有关于测试是否存在的判断,使用步骤 1 枚举得到的 test_files / test_functions。绝不要因为覆盖率工具失败就推断“没有测试”。
  2. 如果覆盖率失败,但枚举结果显示存在测试,请报告:"检测到 [N] 个测试文件和 [M] 个测试函数;覆盖率指标不可用——[失败原因]"。
  3. 在“缺口”小节中,只标记缺失的测试类别(stateless_fuzz=0、foundry_invariant=0、echidna=0、medusa=0、certora=0、halmos=0、hevm=0、fork=0);如果枚举结果显示存在测试,绝不要标记“没有测试”。按审计影响排列缺口优先级:对于数学密集型/金融逻辑,缺少有状态模糊测试和形式化验证的优先级高于缺少分叉测试。
  4. 在 Git 历史的“安全观察”中,绝不要基于覆盖率失败就声称“提交没有测试”。Git 分析中的 test_co_change_rate 衡量的是提交中文件的共同修改情况,而非覆盖率——应对此加以说明。
  5. 如果覆盖率失败,不要让失败影响威胁模型或风险评估。测试是否存在(来自枚举)与覆盖率指标(来自工具)是相互独立的信号。

检查 forge coverage 状态:如果已完成,则包含结果;如果失败,则说明失败原因;如果仍在运行,则标记为“pending”。不要等待。

3a. 写入所有输出文件(在同一条消息中并行调用 4 次 Write)

所有输出文件都放入 x-ray/ 目录。在同一条消息中写入全部四个文件,以便同时创建:

1. x-ray/architecture.json — 遵循 references/templates.md 中架构指南部分的格式和规则(已在步骤 1 加载)。

2. x-ray/x-ray.md — 遵循 references/templates.md 中输出模板部分的模板(已在步骤 1 加载)。少于 500 行。不得捏造。第 3 节(不变量)仅作指针,指向 invariants.md——不要包含防护条件表,也不要列出推断出的顶级不变量。§3 的全部内容只能是一条包含计数(防护条件/单合约/跨合约/经济)的引用块提示,以及一个指向 invariants.md 文件的醒目链接。不变量目录仅存在于 invariants.md;在 x-ray.md 中重复列出是旧版 V2 的做法,现已不再正确。 关键攻击面交叉引用要求:编写第 2 节「关键攻击面」时,将每个攻击面与刚刚生成的 invariants.md 块交叉引用。如果攻击面引用的 file:line 位于任意 G-N / I-N / X-N / E-N 块的 Location / Derivation / Caller side / Callee side 范围内,请在攻击面标题后立即追加匹配 ID 的方括号 Markdown 链接,并使用小写 slug 片段:- **攻击面名称** &nbsp;&#91;[X-4](invariants.md#x-4), [I-17](invariants.md#i-17)&#93; — ...。每条攻击面列表项之间空一行。纯粹涉及访问控制或升级能力的攻击面可以不添加链接——这属于正常情况,并非遗漏。非平凡协议的典型命中率:≥70% 的攻击面至少链接一个不变量。

3. x-ray/entry-points.md —— 使用第 2b 步收集的完整入口点数据和 Step 2b-flow 中的流程路径,遵循 references/templates.md 的入口点模板部分(已在第 1 步加载)。从 Protocol Flow Paths 部分开始(以箭头链展示各主要入口点的前置条件序列),然后是访问级别详情部分。只陈述事实——不进行威胁分析(威胁分析应放在 x-ray.md 中)。如果协议的入口点超过 30 个,则角色门控和管理员部分使用紧凑表格,而非逐函数详情块。无论入口点数量多少,只有无需许可的入口点才使用完整详情块。

4. x-ray/invariants.md —— 遵循 references/templates.md 的不变量映射模板部分(已在第 1 步加载)。分为四个部分:Enforced Guards(Reference)、Inferred(Single-Contract)、Inferred(Cross-Contract)、Economic。使用 #### G-N / #### I-N / #### X-N / #### E-N 标题块——不要使用表格。 标题锚点(slug #g-1、#i-17 等)是 x-ray.md 攻击面跨文件 Markdown 链接的目标;表格单元格中的内联 <a id> 锚点在 VS Code 中无法跨文件使用。每个 G-N 块都必须包含一行 Purpose,说明该保护条件所保护的内容(而不只是它检查的内容)。每个推断块都必须引用一个具体的 Δ 对、guard-lift + write-sites、edge、temporal predicate 或 NatSpec 声明——不符合要求的块应删除。每个跨合约块都必须同时引用调用方假设和被调用方写入位置(两者都必须位于范围文件内)。每个经济块都必须基于特定的 I-N / X-N ID 推导。块数量不限。只陈述事实——不进行威胁分析。

编写第 2 节(威胁与信任模型) —— 遵循输出模板的结构。威胁画像、时序威胁和可组合性威胁内容使用 references/threats.md(所有内容都在同一个文件中,已在第 1 步加载)。对于混合型协议,合并时先列主要攻击者,再列独有的次要威胁(去重重叠项)。

验证规则(编写第 2 节时适用):

  • 无需许可的入口点:仅使用第 2b 步扫描验证过的列表。第 2b 步流程是事实依据——不要依赖子 Agent 摘要。
  • 安全声明:在写出任何关于安全检查缺失、不完整或可绕过的声明前,必须通过阅读相关代码追踪实际数据流。具体来说:(1) 找出该变量的所有写入位置(使用 Grep);(2) 对照这些写入位置确认声明成立。子 Agent 摘要不足以作为依据。如果无法验证,请在声明中注明「无法确认」,而不要将其陈述为事实。

第 7 节(Git 历史):将 x-ray/git-security-analysis.json 中的内容整合到以下部分:Contributors、Review Signals、Hotspots、Security-Relevant Commits(分数 >= 5)、Dangerous Area Evolution、Forked Dependencies、Tech Debt、Cross-Reference Synthesis(用 2-4 个要点将 Git 信号与第 2-3 节关联起来)。

分支范围(重要)

Git 分析仅针对当前分支(HEAD)。JSON 元数据中的 git_branch 字段会告知你分析的是哪个分支。所有 Git 信号(修复候选项、热点、危险区域、近期变更)都仅反映从 HEAD 可到达的提交,不包括其他分支。

规则:

  1. 在报告标题或 Git 历史记录部分注明分析的分支:“Analyzed branch: [branch] at `[commit]”。
  2. 描述 Git 历史中的修复提交或代码变更时,始终说明当前分支代码的行为;如果无法查看此分支上的前后状态,不要描述修复“改变了”什么。
  3. 绝不要描述其他分支上的代码状态。你在步骤 2 中读取的源文件属于当前分支;Git 历史只描述这些文件在此分支上的演变过程。
  4. 如果仓库形态为 squashed_import(1 个提交),则不存在有意义的演变过程;请说明这一点,并跳过修复项/热点分析。
3b. 生成并验证架构 SVG
python3 $SKILL_DIR/scripts/generate_svg.py x-ray/architecture.json x-ray/architecture.svg

然后按照 references/templates.md 中架构指南部分的渲染、审查清单和修复循环进行操作。最多迭代 3 次。之后清理临时文件(包括 x-ray/git-security-analysis.json)。

3c. 最终结论

写入所有文件并完成清理后,从生成的 x-ray/x-ray.md 中读取 ## X-Ray Verdict 部分,并原样输出到终端。不要转述、总结或改写——准确复制文件中出现的等级、理由和关键观察。

约束

  • 少于 500 行。保护威胁模型、不变量、测试缺口、Git 分析和结论;必要时压缩其他部分。
  • 不得编造。不确定时写明“无法确定”。
  • 步骤 1–3 完全自主运行,无需用户交互。
  • 在范围表中始终按子系统归类合约。
  • 单次完成。不得输出部分结果。
  • 绝不要提及审计平台、竞赛规则或漏洞赏金计划相关措辞;保持报告与供应商无关。
  • 如果 Git 安全分析脚本失败,则退回到仅使用 bash 的 Git 统计方法。绝不要因缺少脚本而阻塞。

开始任何操作之前,请原样输出:

██████╗  █████╗ ███████╗██╗  ██╗ ██████╗ ██╗   ██╗     ███████╗██╗  ██╗██╗██╗     ██╗     ███████╗
██╔══██╗██╔══██╗██╔════╝██║  ██║██╔═══██╗██║   ██║     ██╔════╝██║ ██╔╝██║██║     ██║     ██╔════╝
██████╔╝███████║███████╗███████║██║   ██║██║   ██║     ███████╗█████╔╝ ██║██║     ██║     ███████╗
██╔═══╝ ██╔══██║╚════██║██╔══██║██║   ██║╚██╗ ██╔╝     ╚════██║██╔═██╗ ██║██║     ██║     ╚════██║
██║     ██║  ██║███████║██║  ██║╚██████╔╝ ╚████╔╝      ███████║██║  ██╗██║███████╗███████╗███████║
╚═╝     ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝ ╚═════╝   ╚═══╝       ╚══════╝╚═╝  ╚═╝╚═╝╚══════╝╚══════╝╚══════╝

评论

支持 Markdown