关于 x-ray
x-ray 是用于分析 Solidity 项目并在 `x-ray/` 目录生成安全报告的技能,涵盖源码审查、入口分类、Git 历史分析与威胁评估。
主要功能
- 01三阶段顺序审计流程
- 02Solidity 源码与入口分析
- 03Git 安全及变更分析
- 04覆盖率与规格文档审查
- 05生成结构化安全报告
适用场景
- 01审查 Solidity 代码库的安全风险
- 02追踪访问控制与合约入口
- 03评估仓库历史和依赖变更
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ pashov/ skills --skill x-ray安装后在 Claude Code 中输入/x-ray确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.mdVERSIONreferences/templates.mdthreats.mdscripts/analyze_git_security.pyentry_points.pyenumerate.shgenerate_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):
Phase 1: Enumerate & measure codebasePhase 2: Read sources, classify entry points, synthesize invariantsPhase 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 提示词:
将此子 Agent 纳入步骤 1 的并行消息。其输出将用于撰写步骤 3 的报告。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.
对于两条路径,仅提取:文档中声明的全局不变量、参与者定义、跨系统流程、信任假设、经济属性、关键设计决策。在报告中,所有源自规范的声明都标注 (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 函数、仅接口声明、库内部函数(它们是下游调用,不是入口点)、模拟合约。
将每个结果归入以下三类之一:
- 无权限限制——没有访问控制修饰符,且函数体内没有内部调用者限制。将函数分类为无权限限制前,必须检查函数体。对于 路径 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将调用者限制为特定的待定地址。
- 角色限制——具有角色修饰符(
onlyRole(X)、onlyOwner、onlyRouter等),或存在内部msg.sender限制(通过require、if/revert或委托检查)。记录所需角色或地址。 - 仅管理员——由
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 构建流程路径。这不是单独的分析阶段——只是重新整理已有数据。
对于每个面向主要用户的入口点(无权限限制和会转移价值的角色限制函数):
- 找出其
require语句和状态变量检查 - 对于每项检查,查找写入该状态变量的函数(已知于其他入口点的“修改的状态”字段)
- 反向串联:目标 ← 写入其前置条件的函数 ← 写入该前置条件的函数 ← ... ← 部署
- 使用
◄──注释标记非函数前置条件(时间推移、市场状况、外部状态)
输出:按参与者流程分组的简单箭头链。引用前面的流程,不要重复。总计 15–30 行。确切格式请参见 entry-points.md 模板中的“协议流程路径”部分。
入口点扫描是一个硬性门槛:报告中的无权限限制入口点部分必须与经过扫描验证的列表一致,而非子 Agent 摘要。如果两者冲突,以扫描和代码阅读结果为准。
步骤 2c:向后兼容代码检测
阅读源文件时,留意那些看起来是已移除机制的残留代码,其保留目的在于避免剩余代码库出错。常见信号包括:函数体为空或仅包含简单操作、声明后从未被有意义地读取或写入的状态变量、包含「deprecated」「legacy」「backwards compat」或「no longer used」的注释、实现接口但始终返回默认值的函数,以及仅为代理存储布局兼容性而保留的存储变量。
阅读完所有源文件后,在将候选项归类为向后兼容代码之前,请对照以下强制验证检查逐一核实。必须在一条并行消息中批量执行所有候选项的调用方检查 Grep 调用,不得逐个验证:
- 调用方检查(必需):使用 Grep 确认当前代码库中没有活跃调用方调用该函数或变量。如果它在活跃代码路径中被调用,则它不是向后兼容代码,而是当前设计的一部分,无论它是否返回默认值或零值。
- NatSpec/注释检查(必需):如果代码中有 NatSpec 或行内注释说明其行为方式的原因(例如「为 X 模式简化」「按设计如此」「特意设为零」),则这是有文档说明的有意设计,不是向后兼容代码。不要用启发式模式匹配推翻开发者明确记录的说明。
- 接口义务检查:如果某个函数因接口要求而返回默认值,并且仍被活跃调用,则它属于当前架构的一部分,而不是残留代码。
只有同时满足以下所有条件时,才能将代码归类为向后兼容代码:(a) 不存在活跃调用方;(b) 没有 NatSpec/注释将该行为说明为有意设计;(c) Git 历史表明其所属机制已被移除。
不要在报告中将向后兼容代码描述为活跃功能。相反,应在第 1 节(见输出模板)中明确指出这些代码,让审计人员了解代码库中哪些部分是为兼容性而保留的,而非实际运行的功能。如果经过上述验证后不存在仍保留的向后兼容代码,则完全省略该小节。
步骤 2d:中心化与暂停覆盖分析
阅读源文件并分类入口点后,执行两项分析,为 Actors 表、信任边界和关键攻击面(第 2 节)提供依据。这些分析不是独立章节——其结果应整合到现有报告章节中。
中心化分析——对于每个特权角色(admin、owner、operator、keeper、service 等):
- 列出该角色可以执行的所有操作(参照函数级访问权限映射)。
- 对于每项操作,注明是否设有 timelock、多签或延迟。请区分角色转移延迟(例如,
AccessControlDefaultAdminRules的 1 天延迟)和操作性操作延迟——两者并不相同。角色转移延迟无法防止被攻陷的持有者使用即时操作函数。 - 指明哪些操作可以提取或转移用户资金(例如,
emergencyWithdraw、setTreasury、transferFee)。
整合到:Actors 表(Capabilities 列应明确说明哪些操作是即时执行、哪些受 timelock 限制)、信任边界(说明每个边界实际保护的内容,以及哪些操作可以绕过保护)、关键攻击面(以“管理员操作权限”或“[Role] 被攻陷”为主题——攻击面是角色被攻陷,而非单个函数)。
暂停覆盖分析——对于每个关键状态变更函数:
- 检查是否应用了
whenNotPaused(或等效机制)。 - 注明哪些函数可暂停,哪些不可暂停。
- 如果逻辑上应可暂停的函数实际上不可暂停(例如,由权限受限的角色调用、且会操作用户资金的函数),请将此发现整合到该角色的相关攻击面中。缺少暂停机制本身不是攻击面——它只是会加剧该角色被攻陷时风险的因素。
反模式:不要创建独立的“中心化风险”小节。 中心化详情应分散在 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 声明。
遍历顺序(每一步都使用原始提取数据,而非前一步的结论):
-
守恒扫描:对每个函数,查找同一函数体中的增量写入对,其中
Δ(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,且标量未变化),确认映射总和具有自守恒性。 - 负向守恒(重要):如果某个理应跟踪资金流的函数(例如,闪电贷提取 / 返还、接收 / 转发)存储增量为零,则将其记录为守恒负向发现。缺少增量本身就是一种不变量观察。
- 对于映射写入(
-
守卫提取与提升(对每个
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,作为链上=否的边界不变量,并将未受守卫约束的写入位置列为差异。这是高信号输出——该差异同时是不变量和潜在漏洞。
纳入设置器级别的边界条件,即设置器依据自身参数检查写入存储变量的情况。执行相同的全写入位置检查——如果多个设置器写入同一变量,但只有部分设置器执行边界检查,则该属性为链上=否。
-
比例扫描:对于每个形式为
A = B * C / D的存储写入,其中 B、C、D 是存储变量或函数作用域内的存储快照,记录该比例。注明快照是在同一函数中的其他状态变更之前还是之后取得的(顺序很重要——例如,totalSupply是在_burn之前还是之后取得快照)。 -
状态机/单次操作扫描:对于
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)。记录为循环不变量。
- 单次锁存:
-
时间扫描:对于涉及存储变量(deadline、lastUpdate、lockPeriod、interval)的每个
block.timestamp或block.number比较,提取时间约束。注明约束是先检查后更新(安全),还是先更新后检查(可能读取过时值)。 -
跨合约扫描:对于返回值被用于算术运算或存储写入的每个外部调用,记录调用方的假设。然后查找被调用方对此状态的写入位置。如果被调用方可通过另一个函数独立更改该状态,则该假设未经验证——记录为链上=否的跨合约不变量。仅包括调用方假设和被调用方写入位置均在范围文件内的条目。不要推测范围外合约的行为。
- 还要包括:设置器与不变量不匹配——管理员设置器写入存储值时,未检查现有不变量是否仍然成立(例如,
setReserveCapacity未检查当前流动性)。这类情况属于跨合约,因为设置器与不变量是在不同的合约/函数中。
- 还要包括:设置器与不变量不匹配——管理员设置器写入存储值时,未检查现有不变量是否仍然成立(例如,
-
经济推导:完成步骤 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 枚举得到的
test_files/test_functions。绝不要因为覆盖率工具失败就推断“没有测试”。 - 如果覆盖率失败,但枚举结果显示存在测试,请报告:
"检测到 [N] 个测试文件和 [M] 个测试函数;覆盖率指标不可用——[失败原因]"。 - 在“缺口”小节中,只标记缺失的测试类别(
stateless_fuzz=0、foundry_invariant=0、echidna=0、medusa=0、certora=0、halmos=0、hevm=0、fork=0);如果枚举结果显示存在测试,绝不要标记“没有测试”。按审计影响排列缺口优先级:对于数学密集型/金融逻辑,缺少有状态模糊测试和形式化验证的优先级高于缺少分叉测试。 - 在 Git 历史的“安全观察”中,绝不要基于覆盖率失败就声称“提交没有测试”。Git 分析中的
test_co_change_rate衡量的是提交中文件的共同修改情况,而非覆盖率——应对此加以说明。 - 如果覆盖率失败,不要让失败影响威胁模型或风险评估。测试是否存在(来自枚举)与覆盖率指标(来自工具)是相互独立的信号。
检查 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 片段:- **攻击面名称** [[X-4](invariants.md#x-4), [I-17](invariants.md#i-17)] — ...。每条攻击面列表项之间空一行。纯粹涉及访问控制或升级能力的攻击面可以不添加链接——这属于正常情况,并非遗漏。非平凡协议的典型命中率:≥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 可到达的提交,不包括其他分支。
规则:
- 在报告标题或 Git 历史记录部分注明分析的分支:“Analyzed branch:
[branch]at `[commit]”。 - 描述 Git 历史中的修复提交或代码变更时,始终说明当前分支代码的行为;如果无法查看此分支上的前后状态,不要描述修复“改变了”什么。
- 绝不要描述其他分支上的代码状态。你在步骤 2 中读取的源文件属于当前分支;Git 历史只描述这些文件在此分支上的演变过程。
- 如果仓库形态为
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 统计方法。绝不要因缺少脚本而阻塞。
开始任何操作之前,请原样输出:
██████╗ █████╗ ███████╗██╗ ██╗ ██████╗ ██╗ ██╗ ███████╗██╗ ██╗██╗██╗ ██╗ ███████╗
██╔══██╗██╔══██╗██╔════╝██║ ██║██╔═══██╗██║ ██║ ██╔════╝██║ ██╔╝██║██║ ██║ ██╔════╝
██████╔╝███████║███████╗███████║██║ ██║██║ ██║ ███████╗█████╔╝ ██║██║ ██║ ███████╗
██╔═══╝ ██╔══██║╚════██║██╔══██║██║ ██║╚██╗ ██╔╝ ╚════██║██╔═██╗ ██║██║ ██║ ╚════██║
██║ ██║ ██║███████║██║ ██║╚██████╔╝ ╚████╔╝ ███████║██║ ██╗██║███████╗███████╗███████║
╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚══════╝