2
月
在社区成长 2 月
LEVEL 1
1 XP
一、背景:为什么安全审计不能只依赖 AI
大语言模型已经可以解释 Solidity 代码、归纳静态分析告警、生成测试用例,并辅助审计人员梳理调用链。但在真实审计中,“能够给出答案”不等于“答案可以被采信”。模型可能忽略跨合约状态变化、误解代理模式、引用不存在的函数,或者把理论风险描述成可利用漏洞。
因此,AI 在智能合约审计中的合理定位应当是:提高代码阅读和假设生成效率,但不直接充当漏洞裁决者。每一项结论都应落到可定位的代码、明确的前置条件,以及能够重复执行的静态分析结果或本地测试上。本文给出一套仅面向本地项目和授权靶场的工作流,将 AI、Slither、Foundry 与人工复核组合起来,形成“发现—验证—修复—回归”的证据闭环。
二、基本原理:把自然语言判断转换为可证伪假设
可靠流程可以分为四层:
1. 确定性工具层:编译器、Slither、单元测试和模糊测试负责提供可重复结果。
2. AI 分析层:解释复杂函数、聚类告警、提出攻击前置条件和测试思路。
3. 验证层:将模型输出改写为 Foundry 测试或不变量,由本地 EVM 执行。
4. 人工裁决层:结合业务权限、部署参数和协议设计,判断影响范围与严重性。
关键原则是让 AI 输出“可证伪假设”,而不是笼统地说“这里可能存在重入”。一个合格假设至少包含:代码位置、危险状态、攻击者所需权限、触发顺序、预期违反的不变量、可能的反例。例如:“withdraw 在更新 balances 前执行外部调用;如果调用者是可重入合约且余额未设置为零,则同一余额可能被重复提取。”这类描述可以直接转化为本地测试。
三、前置条件
建议在隔离的开发环境中准备以下工具:
- Git,用于固定待审计版本并记录修复差异。
- Solidity 编译器,版本应与项目配置一致。
- Foundry,用于单元测试、模糊测试和不变量测试。
- Python 3 与 Slither,用于静态分析。
- 可选的本地模型服务;如果使用外部模型,必须先处理源代码、密钥和客户信息的泄露风险。
示例安装与初始化命令如下,执行对象仅限本地实验目录:
python3 -m venv .venv
source .venv/bin/activate
pip install slither-analyzer
forge init ai-audit-lab
cd ai-audit-lab
git init
git add . && git commit -m "baseline"
如果被审计项目包含依赖,应锁定依赖提交或版本,避免今天与明天的扫描结果对应不同代码。可执行:
git rev-parse HEAD
solc --version
forge --version
slither --version
将这些版本信息一并保存到审计记录中。
四、分步骤实践
步骤 1:建立资产、权限和不变量清单
在让 AI 阅读代码之前,先人工回答三个问题:资金或关键资产在哪里;谁能改变关键参数;哪些性质在任何交易序列后都必须成立。常见不变量包括:用户领取量不能超过记账余额;总份额与资产换算不能凭空增值;暂停状态下禁止敏感操作;只有治理账户可以升级实现;签名不能被重复消费。
可以要求 AI 按固定结构整理,但必须提供真实源码作为上下文:
“请只根据给定代码列出外部入口、状态写入、外部调用和权限检查。每项必须标注合约名及函数名;无法确认的内容写未知,不要推测部署参数。”
这种约束能减少模型补全不存在细节的概率。
步骤 2:运行确定性静态分析并保留原始结果
先编译,再执行 Slither:
forge build
slither . --json slither-result.json
slither . --print call-graph
slither . --print inheritance
不要把静态分析告警直接等同于漏洞。例如,重入检测可能只反映“外部调用后写状态”,但如果函数有可靠的重入锁,或者外部目标完全不可控,实际风险会下降。反过来,工具未告警也不代表安全,因为经济模型缺陷、签名域分离错误和跨函数业务问题经常需要定制验证。
步骤 3:让 AI 做告警归并,而不是替代扫描器
将源码片段、编译器版本和相关 Slither 告警一起交给模型,要求输出如下字段:
- hypothesis:待验证假设;
- location:合约、函数及代码范围;
- preconditions:攻击者权限、余额、时序等条件;
- violated_invariant:可能违反的不变量;
- evidence_for:支持该判断的代码证据;
- evidence_against:可能使问题不成立的保护措施;
- test_plan:本地验证步骤;
- confidence:仅表示分析把握,不代表漏洞严重性。
特别要强调:模型找不到证据时必须返回“证据不足”,不能用常见漏洞模板填空。
步骤 4:在本地靶场验证重入假设
以下是一个故意存在检查—交互—效果顺序错误的本地教学合约,禁止部署到生产环境:
contract LocalVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "empty");
(bool ok,) = msg.sender.call{value: amount}("");
require(ok, "send failed");
balances[msg.sender] = 0;
}
}
这里可以提出可验证假设:当调用者合约在 receive 中再次调用 withdraw,且金库中还有其他测试账户存入的资金时,同一笔攻击者余额可能被重复支付。验证应只在 Foundry 本地 EVM 中进行,并使用测试资金。
测试重点不是展示针对真实目标的操作,而是证明不变量是否被破坏。可以断言:一次提现后,调用者资产增量不得超过提现前的 balances 记录;任何调用序列结束后,合约的会计负债与可支付资产应保持预期关系。运行命令:
forge test -vvv
forge test --match-test test_ReentrantWithdrawalIsRejected -vvvv
如果修复前测试观察到支付增量超过账面余额,而修复后同一测试无法再破坏不变量,就形成了可重复的证据链。
步骤 5:实施最小修复并检查副作用
该案例可优先采用检查—效果—交互模式,在外部调用前清零余额:
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "empty");
balances[msg.sender] = 0;
(bool ok,) = msg.sender.call{value: amount}("");
require(ok, "send failed");
}
也可以结合成熟的重入保护组件,但不应只机械添加修饰器。需要检查代理存储布局、继承顺序、初始化过程和项目依赖版本。外部调用失败会使整笔交易回滚,因此预先清零不会在失败后永久丢失余额;这一点也应由测试确认,而不是只靠文字推理。
步骤 6:增加模糊测试与不变量测试
单一案例只能覆盖一条路径。可使用 Foundry fuzzing 随机化存款额、调用者和调用顺序:
forge test --fuzz-runs 5000
建议验证以下性质:
- 任意用户成功领取的累计金额不大于其合法入账金额;
- 未授权地址不能改变管理员或实现地址;
- 暂停后所有受控入口均按设计拒绝;
- 零值、最大值和多次调用不会破坏会计关系;
- 外部调用失败时,用户账面权益不会意外消失。
AI 可以协助生成测试骨架,但必须人工检查断言方向。错误断言会造成“测试通过但安全性质没有被验证”的假象。
步骤 7:生成可追溯审计记录
每个确认问题至少保存以下信息:代码提交哈希、工具版本、问题位置、前置条件、影响描述、最小复现测试、修复提交以及回归结果。建议使用类似格式:
ID: SC-REENTRANCY-001
Status: Confirmed / Fixed / Retested
Commit: 待审计提交哈希
Location: LocalVault.withdraw
Preconditions: 可控接收合约、正余额、金库存在足够测试资金
Evidence: Foundry 测试名称与执行结果
Fix: 先更新状态,再进行外部调用
Residual Risk: 其他外部调用入口尚需独立检查
“Confirmed”只能用于测试或严格代码推理已经证实的问题;无法确认部署参数、角色配置或跨链状态时,应明确标记为“待环境验证”。
五、风险与注意事项
第一,源代码泄露风险。未公开合约、审计报告、私钥、RPC 凭据和客户信息不应直接发送给公共模型。应优先使用本地部署、脱敏片段或经过合同允许的企业环境。
第二,提示注入风险。代码注释、README、依赖包文本都可能包含诱导模型忽略规则的内容。应把仓库内容视为不可信数据,禁止模型根据仓库文本自动执行命令、上传文件或修改权限。
第三,自动修复风险。模型生成的补丁可能改变 ABI、存储布局、舍入方向或 Gas 行为。特别是可升级合约,新增状态变量必须遵循存储布局约束,并使用布局检查和升级测试。
第四,误报与漏报。AI 的置信度不是统计意义上的安全概率,也不能替代人工风险评级。严重性应根据可利用条件、资金规模、权限边界、可恢复性和链上部署状态判断。
第五,测试环境偏差。模拟环境中的余额、预言机、区块时间和外部协议行为可能与生产不同。报告必须区分“本地已证明”“配置相关”和“链上尚未验证”。本文流程不授权对任何真实第三方地址进行探测或交易。
六、常见问题
问:AI 判断没有漏洞,是否可以结束审计?
答:不可以。模型输出只是一种辅助意见。还需要静态分析、测试、业务规则审查、权限检查和依赖审查。
问:Slither 与 AI 的结论冲突怎么办?
答:回到代码与执行结果。先确认编译版本和扫描范围,再构造最小测试。能够重复执行的证据优先于自然语言判断。
问:能否让 AI 自动提交修复?
答:可以在隔离分支生成候选补丁,但不应自动合并或部署。补丁需要代码审查、完整回归、存储布局检查和权限复核。
问:为什么要求模型同时给出反对证据?
答:只要求寻找漏洞容易产生确认偏差。列出重入锁、访问控制、调用目标不可控等反证,有助于区分真实问题与模式匹配误报。
问:如何判断流程具有可重复性?
答:在固定提交、依赖、编译器和工具版本后,由另一名审计人员执行同一命令,应得到一致的核心结果。若结论依赖未记录的聊天上下文,就不能算完整证据。
七、总结
AI 在智能合约安全中的可靠价值,不是替审计人员给出“安全”结论,而是加速入口梳理、告警归并、假设生成和测试草拟。高质量流程必须坚持风险边界:源码输入可控、模型权限最小化、测试仅在本地或授权环境执行、所有关键结论均有代码位置和可重复证据。将静态分析作为线索来源,将 AI 输出改写为可证伪假设,再用 Foundry 测试和人工业务判断完成裁决,才能把模型的语言能力转化为真正可审计、可复核、可回归的安全工程成果。
大语言模型已经可以解释 Solidity 代码、归纳静态分析告警、生成测试用例,并辅助审计人员梳理调用链。但在真实审计中,“能够给出答案”不等于“答案可以被采信”。模型可能忽略跨合约状态变化、误解代理模式、引用不存在的函数,或者把理论风险描述成可利用漏洞。
因此,AI 在智能合约审计中的合理定位应当是:提高代码阅读和假设生成效率,但不直接充当漏洞裁决者。每一项结论都应落到可定位的代码、明确的前置条件,以及能够重复执行的静态分析结果或本地测试上。本文给出一套仅面向本地项目和授权靶场的工作流,将 AI、Slither、Foundry 与人工复核组合起来,形成“发现—验证—修复—回归”的证据闭环。
二、基本原理:把自然语言判断转换为可证伪假设
可靠流程可以分为四层:
1. 确定性工具层:编译器、Slither、单元测试和模糊测试负责提供可重复结果。
2. AI 分析层:解释复杂函数、聚类告警、提出攻击前置条件和测试思路。
3. 验证层:将模型输出改写为 Foundry 测试或不变量,由本地 EVM 执行。
4. 人工裁决层:结合业务权限、部署参数和协议设计,判断影响范围与严重性。
关键原则是让 AI 输出“可证伪假设”,而不是笼统地说“这里可能存在重入”。一个合格假设至少包含:代码位置、危险状态、攻击者所需权限、触发顺序、预期违反的不变量、可能的反例。例如:“withdraw 在更新 balances 前执行外部调用;如果调用者是可重入合约且余额未设置为零,则同一余额可能被重复提取。”这类描述可以直接转化为本地测试。
三、前置条件
建议在隔离的开发环境中准备以下工具:
- Git,用于固定待审计版本并记录修复差异。
- Solidity 编译器,版本应与项目配置一致。
- Foundry,用于单元测试、模糊测试和不变量测试。
- Python 3 与 Slither,用于静态分析。
- 可选的本地模型服务;如果使用外部模型,必须先处理源代码、密钥和客户信息的泄露风险。
示例安装与初始化命令如下,执行对象仅限本地实验目录:
python3 -m venv .venv
source .venv/bin/activate
pip install slither-analyzer
forge init ai-audit-lab
cd ai-audit-lab
git init
git add . && git commit -m "baseline"
如果被审计项目包含依赖,应锁定依赖提交或版本,避免今天与明天的扫描结果对应不同代码。可执行:
git rev-parse HEAD
solc --version
forge --version
slither --version
将这些版本信息一并保存到审计记录中。
四、分步骤实践
步骤 1:建立资产、权限和不变量清单
在让 AI 阅读代码之前,先人工回答三个问题:资金或关键资产在哪里;谁能改变关键参数;哪些性质在任何交易序列后都必须成立。常见不变量包括:用户领取量不能超过记账余额;总份额与资产换算不能凭空增值;暂停状态下禁止敏感操作;只有治理账户可以升级实现;签名不能被重复消费。
可以要求 AI 按固定结构整理,但必须提供真实源码作为上下文:
“请只根据给定代码列出外部入口、状态写入、外部调用和权限检查。每项必须标注合约名及函数名;无法确认的内容写未知,不要推测部署参数。”
这种约束能减少模型补全不存在细节的概率。
步骤 2:运行确定性静态分析并保留原始结果
先编译,再执行 Slither:
forge build
slither . --json slither-result.json
slither . --print call-graph
slither . --print inheritance
不要把静态分析告警直接等同于漏洞。例如,重入检测可能只反映“外部调用后写状态”,但如果函数有可靠的重入锁,或者外部目标完全不可控,实际风险会下降。反过来,工具未告警也不代表安全,因为经济模型缺陷、签名域分离错误和跨函数业务问题经常需要定制验证。
步骤 3:让 AI 做告警归并,而不是替代扫描器
将源码片段、编译器版本和相关 Slither 告警一起交给模型,要求输出如下字段:
- hypothesis:待验证假设;
- location:合约、函数及代码范围;
- preconditions:攻击者权限、余额、时序等条件;
- violated_invariant:可能违反的不变量;
- evidence_for:支持该判断的代码证据;
- evidence_against:可能使问题不成立的保护措施;
- test_plan:本地验证步骤;
- confidence:仅表示分析把握,不代表漏洞严重性。
特别要强调:模型找不到证据时必须返回“证据不足”,不能用常见漏洞模板填空。
步骤 4:在本地靶场验证重入假设
以下是一个故意存在检查—交互—效果顺序错误的本地教学合约,禁止部署到生产环境:
contract LocalVault {
mapping(address => uint256) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "empty");
(bool ok,) = msg.sender.call{value: amount}("");
require(ok, "send failed");
balances[msg.sender] = 0;
}
}
这里可以提出可验证假设:当调用者合约在 receive 中再次调用 withdraw,且金库中还有其他测试账户存入的资金时,同一笔攻击者余额可能被重复支付。验证应只在 Foundry 本地 EVM 中进行,并使用测试资金。
测试重点不是展示针对真实目标的操作,而是证明不变量是否被破坏。可以断言:一次提现后,调用者资产增量不得超过提现前的 balances 记录;任何调用序列结束后,合约的会计负债与可支付资产应保持预期关系。运行命令:
forge test -vvv
forge test --match-test test_ReentrantWithdrawalIsRejected -vvvv
如果修复前测试观察到支付增量超过账面余额,而修复后同一测试无法再破坏不变量,就形成了可重复的证据链。
步骤 5:实施最小修复并检查副作用
该案例可优先采用检查—效果—交互模式,在外部调用前清零余额:
function withdraw() external {
uint256 amount = balances[msg.sender];
require(amount > 0, "empty");
balances[msg.sender] = 0;
(bool ok,) = msg.sender.call{value: amount}("");
require(ok, "send failed");
}
也可以结合成熟的重入保护组件,但不应只机械添加修饰器。需要检查代理存储布局、继承顺序、初始化过程和项目依赖版本。外部调用失败会使整笔交易回滚,因此预先清零不会在失败后永久丢失余额;这一点也应由测试确认,而不是只靠文字推理。
步骤 6:增加模糊测试与不变量测试
单一案例只能覆盖一条路径。可使用 Foundry fuzzing 随机化存款额、调用者和调用顺序:
forge test --fuzz-runs 5000
建议验证以下性质:
- 任意用户成功领取的累计金额不大于其合法入账金额;
- 未授权地址不能改变管理员或实现地址;
- 暂停后所有受控入口均按设计拒绝;
- 零值、最大值和多次调用不会破坏会计关系;
- 外部调用失败时,用户账面权益不会意外消失。
AI 可以协助生成测试骨架,但必须人工检查断言方向。错误断言会造成“测试通过但安全性质没有被验证”的假象。
步骤 7:生成可追溯审计记录
每个确认问题至少保存以下信息:代码提交哈希、工具版本、问题位置、前置条件、影响描述、最小复现测试、修复提交以及回归结果。建议使用类似格式:
ID: SC-REENTRANCY-001
Status: Confirmed / Fixed / Retested
Commit: 待审计提交哈希
Location: LocalVault.withdraw
Preconditions: 可控接收合约、正余额、金库存在足够测试资金
Evidence: Foundry 测试名称与执行结果
Fix: 先更新状态,再进行外部调用
Residual Risk: 其他外部调用入口尚需独立检查
“Confirmed”只能用于测试或严格代码推理已经证实的问题;无法确认部署参数、角色配置或跨链状态时,应明确标记为“待环境验证”。
五、风险与注意事项
第一,源代码泄露风险。未公开合约、审计报告、私钥、RPC 凭据和客户信息不应直接发送给公共模型。应优先使用本地部署、脱敏片段或经过合同允许的企业环境。
第二,提示注入风险。代码注释、README、依赖包文本都可能包含诱导模型忽略规则的内容。应把仓库内容视为不可信数据,禁止模型根据仓库文本自动执行命令、上传文件或修改权限。
第三,自动修复风险。模型生成的补丁可能改变 ABI、存储布局、舍入方向或 Gas 行为。特别是可升级合约,新增状态变量必须遵循存储布局约束,并使用布局检查和升级测试。
第四,误报与漏报。AI 的置信度不是统计意义上的安全概率,也不能替代人工风险评级。严重性应根据可利用条件、资金规模、权限边界、可恢复性和链上部署状态判断。
第五,测试环境偏差。模拟环境中的余额、预言机、区块时间和外部协议行为可能与生产不同。报告必须区分“本地已证明”“配置相关”和“链上尚未验证”。本文流程不授权对任何真实第三方地址进行探测或交易。
六、常见问题
问:AI 判断没有漏洞,是否可以结束审计?
答:不可以。模型输出只是一种辅助意见。还需要静态分析、测试、业务规则审查、权限检查和依赖审查。
问:Slither 与 AI 的结论冲突怎么办?
答:回到代码与执行结果。先确认编译版本和扫描范围,再构造最小测试。能够重复执行的证据优先于自然语言判断。
问:能否让 AI 自动提交修复?
答:可以在隔离分支生成候选补丁,但不应自动合并或部署。补丁需要代码审查、完整回归、存储布局检查和权限复核。
问:为什么要求模型同时给出反对证据?
答:只要求寻找漏洞容易产生确认偏差。列出重入锁、访问控制、调用目标不可控等反证,有助于区分真实问题与模式匹配误报。
问:如何判断流程具有可重复性?
答:在固定提交、依赖、编译器和工具版本后,由另一名审计人员执行同一命令,应得到一致的核心结果。若结论依赖未记录的聊天上下文,就不能算完整证据。
七、总结
AI 在智能合约安全中的可靠价值,不是替审计人员给出“安全”结论,而是加速入口梳理、告警归并、假设生成和测试草拟。高质量流程必须坚持风险边界:源码输入可控、模型权限最小化、测试仅在本地或授权环境执行、所有关键结论均有代码位置和可重复证据。将静态分析作为线索来源,将 AI 输出改写为可证伪假设,再用 Foundry 测试和人工业务判断完成裁决,才能把模型的语言能力转化为真正可审计、可复核、可回归的安全工程成果。