顺序不是排版上的安排。每一层都消费上一层产出的东西,而每一层的定义既来自它能抓到什么,也同样来自它抓不到什么。这些盲区被印在这里,理由和它们被印在报告里是同一个:一个隐藏自己射程的工具,是一个你无法校准的工具。
201 个检测器扫过 Solidity 源码:未检查的返回值、重入形状、增发与清仓路径、owner 的权力、代理与升级路径,以及一次模拟的买入卖出——在你陷进貔貅之前就把它认出来。引擎跑在你的机器上,未发布的合约不会为了被读一遍而上传。盲区:某条路径在合约真正会遇到的数值下是否可达。这一层什么都没有执行,它下面的每一层也都没有。
源码是一种说法,部署的字节码才是事实。这一层读取分发器,得到合约真正暴露的选择器;对着签名语料给原始 calldata 起名;减去库代码,只留下手写的那些行——审计师应该最先读的正是它们;并报告一个地址的资金从哪来,每个答案都带着产出它的那份名单的出处。盲区:意图,以及任何没人加载过的名单。这个产品里没有编译进任何制裁名单;当没有标签名单被加载时,报告会用这句话直说,而不是返回一个你可能误当作「已筛查」的干净结果。
每条链都带着一个问题——「我能不能让一份额贵到让一笔真实存款买到零份?」——一份前置条件清单,以及每个条件所占的权重。只有当扫描确实证明了它必需的前置条件时,这条链才会被发出,而且它会附上每一条的证据。如果代码撑不起这条链,就不会出现这条链。盲区:链上实时状态——余额、流动性深度、私钥在谁手里、owner 是不是多签。这里的置信度是「已被证明的前置条件所占的比例」,绝不是「一次攻击会成功的概率」。
模型的工作只在第 03 层已经确立了一条链之后才开始:用读者自己的语言解释那条链,讲到一个防守者足以把它堵上的程度。它从不会被要求生成场景,也无法添加场景——因为一份自信、无法证伪的臆造攻击清单,会把那一条真实发现埋在下面。盲区:意图。它无法告诉你一个特权设计究竟是失误,还是某个人有意做出的决定。
每条发现连同它的严重程度、证据和修复方式。一个已交付报告的公开档案。一个把「读自链上的实况」和「别人回报的分数」严格分开、并给后者标上日期的徽章。以及在报告签字之后继续盯着这个地址的监控——因为一份合约完全可能在通过的下一周就换了 owner。不包含:任何关于「安全」的断言。一份报告陈述的是测了什么、以及什么成立了——一份承诺安全的报告,是一份该被怀疑的报告。
每条链都是由代码从检测器输出组合出来的,并为它声称的每一条前置条件附上证据。AI 解释一条已经存在的链,它从来没有能力增加一条。规模、深度和克制是三种不同的主张,这里把它们分开讲。
工具本身(扫描器、控制台、SAFI 工作区)目前仍是英文界面。这些页面用中文把一切讲清楚,方便你进去之前先看明白。
ATK-VAULT-INFLATION:两个被接受的弱点,一个空池子。这就是第 03 层拿第 01 层的产出做的事——下面两半单独看都不是严重问题,而这恰恰是一份按严重程度排序的发现清单会把它藏起来的原因。
Euler Finance 因为可以被同样方式撬动的记账方式损失了 1.97 亿美元。
先成为第一个存款人并铸造恰好一份份额。然后直接把一大笔钱打给合约,于是背后的资产增加了而没有新份额产生。下一笔诚实的存款小于新的单份额价格,于是他们能拿到的份额向下取整为零。他们的钱并没有被转走——它被算术吸收了,去支撑你那一份份额。
一个份额操纵检测器和一个取整检测器触发了,而且这份合约可以直接接收价值。三条发现,没有一条是严重级别,分散坐在清单的不同位置。
在函数表里,任何叫 MIN_、deadShares 或 initialMint 的东西都不存在。
读取已部署的选择器,正是「不存在」和「没读到」之间的分界:那道死份额防御是缺失的,而不只是没被读到。
必需的前置条件全部满足,这条链被发出,并为每一条附上证据,而它的置信度精确说明了扫描究竟证明了其中多大比例。
修复方式:在部署时铸造死份额,或在内部单独记账;把份额发放的取整方向改为对存款人有利;拒绝那些会铸造出零份额的存款。
上面这四步,用读者自己的语言,讲到足以把这条路径堵上的程度。
这一步之所以放在最后,是因为它是唯一一个模型参与的环节——而它拿到的输入,是前面三层已经证明过的东西。