智能合约审计中大模型的适用边界
智能合约审计中大模型的适用边界
大模型在 Solidity 代码生成和审计中的能力边界需要谨慎评估。有些开发者将大量 Solidity 文件直接交给 LLM 查找重入或跨合约逻辑缺陷,结果可能漏掉高风险问题,也可能产生大量误报。
在智能合约开发与审计链路里,AI 有非常清晰的技术适用边界。不讲适用条件直接用 AI 帮智能合约把关,往往是灾难的开始。
不宜让大模型单独下结论的场景
跨合约状态依赖、复杂 DeFi 协议的数学经济曲线(如 AMM 恒定乘积公式与流动性浓缩计算)、以及涉及到闪电贷攻击(Flashloan Attack)的逻辑组合,AI 目前的表现极不可靠。原因在于大模型缺乏对全全局状态空间图的精准推导能力,它看到的只是文本标记的上下文概率。
容易被表面模式掩盖的重入问题
看下面这段看似使用了“常见防重入模式”的 Solidity 代码。如果我们让 AI 检查,它常常会因为看到了 nonReentrant 修饰符或者标准的 balances[msg.sender] -= amount 就判断“安全”:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
interface IERC20 {
function transfer(address to, uint256 amount) external returns (bool);
}
contract FlawedVault {
mapping(address => uint256) public shares;
mapping(address => uint256) public rewards;
IERC20 public immutable token;
bool private locked;
modifier nonReentrant() {
require(!locked, "REENTRANCY");
locked = true;
_;
locked = false;
}
constructor(address _token) {
token = IERC20(_token);
}
// 隐蔽的跨函数重入缺陷 (Cross-function Reentrancy)
function withdraw(uint256 shareAmount) external nonReentrant {
require(shares[msg.sender] >= shareAmount, "INSUFFICIENT_BALANCE");
uint256 rewardAmount = rewards[msg.sender];
// 外部转账触发回调 (如果 token 是 ERC777 或带 Hook 的代币)
require(token.transfer(msg.sender, shareAmount), "TRANSFER_FAILED");
// 状态更新延迟到转账之后
shares[msg.sender] -= shareAmount;
rewards[msg.sender] = 0;
}
function claimRewardOnly() external nonReentrant {
uint256 reward = rewards[msg.sender];
require(reward > 0, "NO_REWARD");
// 这里的 locked 标示只能防止单独递归调用 claimRewardOnly
// 但如果 withdraw 内部转账时回调了此函数,由于 locked 状态在 withdraw 执行完毕前都为 true,
// 看起来被挡住了,但如果开发者把锁设计在不同变量上,就立刻沦陷。
rewards[msg.sender] = 0;
payable(msg.sender).transfer(reward);
}
}
AI 可能忽略代币标准变体(如 ERC-777 的 tokensToSend 钩子)对外部调用的侧通道影响。不能仅凭 AI 的“Pass”结论判断生产环境风险。
大模型更适合参与的测试工作
AI 在 Solidity 开发中的高价值定位,绝非替代审计员,而是充当“Foundry 测试用例推导器”。
编写基于属性的测试(Property-based Testing)需要定义系统不变性(Invariants)。大模型擅长根据 Solidity 接口定义,生成穷尽极端参数的 Foundry/Forge 测试骨架。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "forge-std/Test.sol";
import "../src/FlawedVault.sol";
contract VaultInvariantTest is Test {
FlawedVault internal vault;
MockERC20Hook internal token;
function setUp() public {
token = new MockERC20Hook();
vault = new FlawedVault(address(token));
}
// 由 AI 辅助分析并生成的 Foundry Invariant 断言
function invariant_SolvencyAlwaysHolds() public {
uint256 totalVaultTokenBalance = token.balanceOf(address(vault));
// 系统总质押份额不应当在任何异常状态下超越实际持有的代币数
assertGe(totalVaultTokenBalance, vault.shares(address(this)));
}
// 针对边界值的 Fuzzing 测试
function testFuzz_WithdrawAmount(uint256 amount) public {
vm.assume(amount > 0 && amount <= 1000 ether);
token.mint(address(vault), amount);
// 模拟外部恶意重入攻击动作
token.setReentrantTarget(address(vault));
vm.expectRevert();
vault.withdraw(amount);
}
}
contract MockERC20Hook {
mapping(address => uint256) public balanceOf;
address internal reentrantTarget;
function setReentrantTarget(address target) external {
reentrantTarget = target;
}
function mint(address to, uint256 amount) external {
balanceOf[to] += amount;
}
function transfer(address to, uint256 amount) external returns (bool) {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
// 模拟恶意回调
if (reentrantTarget != address(0)) {
(bool success, ) = reentrantTarget.call(
abi.encodeWithSignature("withdraw(uint256)", amount)
);
// 忽略报错以测试主流程防御力
}
return true;
}
}
审计流程中的职责划分
要想让 AI 在智能合约开发里帮忙而不添乱,必须建立起死板的分工规则:
- AI 负责产生用例与变体(Generator):利用 LLM 根据合约的业务要求生成高覆盖率的 Solidity 接口测试桩和极端边界伪数据。
- 静态工具负责阻断语法与控制流缺陷(Static Checker):Slither、Mythril 必须作为 CI Pipeline 的强卡口,AI 生成的代码不通过 Slither 扫描不允许进入下一步。
- Foundry/Echidna 负责状态空间证明(Verifier):一切逻辑正确性只看
forge test的实际执行结果与符号执行(Symbolic Execution)的反例输出。
把 AI 放在“测试用例生成者”的低风险位置,把智能合约的真正死活交给确定性的编译与模糊测试工具。认清这个边界,合约安全审计才算真正落到了实处。
把模型输出变成可审查的输入
向大模型提问时,输入中应包含合约版本、继承关系、角色权限、资产流向和已知的不变量,而不只是粘贴某个函数。模型给出的结论应被保存为候选检查项,例如“外部调用前是否更新状态”“升级入口是否受权限保护”,不能直接作为审计结论。这样做的收益是让人工审计员能追溯每条建议来自什么上下文,也能快速删掉与当前协议无关的泛化回答。
测试生成后还要检查测试本身是否真的覆盖了目标路径。一个 expectRevert 只能证明调用失败,不能说明失败原因符合预期;涉及资产守恒时,应同时断言调用前后的余额、份额和总供给。对于可升级合约,还应把初始化、暂停、角色迁移和实现替换纳入同一份测试矩阵。不同网络、不同代币标准和不同预言机实现也可能改变外部调用语义,不能用单一模拟合约替代全部情况。
最后,审计报告要明确证据等级:编译器和测试工具给出的可复现结果、人工代码阅读的判断、以及模型提出但尚未验证的假设应分开写。这个边界比让模型输出一长串“安全建议”更有用。
对每一次模型协作,还应固定提示词版本和输入文件清单。合约发生改动后,旧结论不应自动沿用;重新运行测试并记录差异,才知道安全判断是否仍适用于当前版本。
更多推荐
所有评论(0)