智能合约审计中大模型的适用边界

大模型在 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 在智能合约开发里帮忙而不添乱,必须建立起死板的分工规则:

  1. AI 负责产生用例与变体(Generator):利用 LLM 根据合约的业务要求生成高覆盖率的 Solidity 接口测试桩和极端边界伪数据。
  2. 静态工具负责阻断语法与控制流缺陷(Static Checker):Slither、Mythril 必须作为 CI Pipeline 的强卡口,AI 生成的代码不通过 Slither 扫描不允许进入下一步。
  3. Foundry/Echidna 负责状态空间证明(Verifier):一切逻辑正确性只看 forge test 的实际执行结果与符号执行(Symbolic Execution)的反例输出。

把 AI 放在“测试用例生成者”的低风险位置,把智能合约的真正死活交给确定性的编译与模糊测试工具。认清这个边界,合约安全审计才算真正落到了实处。

把模型输出变成可审查的输入

向大模型提问时,输入中应包含合约版本、继承关系、角色权限、资产流向和已知的不变量,而不只是粘贴某个函数。模型给出的结论应被保存为候选检查项,例如“外部调用前是否更新状态”“升级入口是否受权限保护”,不能直接作为审计结论。这样做的收益是让人工审计员能追溯每条建议来自什么上下文,也能快速删掉与当前协议无关的泛化回答。

测试生成后还要检查测试本身是否真的覆盖了目标路径。一个 expectRevert 只能证明调用失败,不能说明失败原因符合预期;涉及资产守恒时,应同时断言调用前后的余额、份额和总供给。对于可升级合约,还应把初始化、暂停、角色迁移和实现替换纳入同一份测试矩阵。不同网络、不同代币标准和不同预言机实现也可能改变外部调用语义,不能用单一模拟合约替代全部情况。

最后,审计报告要明确证据等级:编译器和测试工具给出的可复现结果、人工代码阅读的判断、以及模型提出但尚未验证的假设应分开写。这个边界比让模型输出一长串“安全建议”更有用。

对每一次模型协作,还应固定提示词版本和输入文件清单。合约发生改动后,旧结论不应自动沿用;重新运行测试并记录差异,才知道安全判断是否仍适用于当前版本。

更多推荐