Kevin Riedl

7 分钟 阅读 · 2026年5月26日

智能合约安全清单:外部审计前我们核对的 30 项

我们不做审计。我们交付并加固智能合约代码,然后由外部机构审计。在那段审计窗口打开之前,我们跑这份30 项审前清单,分为6 类:编译器与工具链、访问控制与角色、算术与溢出、外部调用与重入、可升级性与存储、Gas 与 DoS 面。下面每一项都在我们或同行的真实项目里烧过。把 30 项全部清掉,按我们的项目历史,能把审计师在我们移交品上的发现数减少大约 60% 到 80%,意味着更便宜的审计和更快的主网。

这是我们希望在 Scramble PayQuivrLightbridge 以及 Account Abstraction 工作的第一天就有人递给我们的列表。我们不称之为审计。它是一次加固评审,目的是让审计变得无聊。

马上要进审计?

 预约免费咨询

智能合约审前加固评审实际检查什么?

6 类,每类 5 项,共 30 项。我们把清单当成关卡。任一项失败,合约不会离开我们的仓库,除非被修复或被客户书面豁免并说明原因。我们把它和 TDD 工作流以及 QA 服务 一起搭配,使每个修复都带回归测试落地。

类别 1:编译器与工具链(第 1 到 5 项)

  1. 固定的 Solidity 版本。foundry.tomlhardhat.config 中锁定单一编译器版本。^0.8.20 这种浮动 pragma 意味着两位开发者能从同一份源码编译出两份不同字节码。
  2. 优化器设置公开声明且可复现。记录 optimizer runs 值。Etherscan 验证在不一致时会失败,审计师在标记它之前会浪费一小时。
  3. SMTChecker 和 Slither 跑过且零未抑制发现。每一处抑制都有内联注释注明是哪位审计师或工程师签字豁免。无静默抑制。
  4. Foundry fuzz 和 invariant 测试在 CI 中跑且 seed 数量高。移交审计前我们要求每个关键函数至少 10,000 fuzz 跑和 256 invariant 跑。
  5. 依赖 lockfile 已提交并审过。OpenZeppelin、Solady 版本固定。没有指向 maingit 依赖。每次依赖升级我们都做 diff。

类别 2:访问控制与角色(第 6 到 10 项)

  1. 每个特权函数都有显式角色检查。不是默认 onlyOwner。我们把每个函数映射到角色并记录部署时谁持有它。
  2. 主网上线时无 EOA 持有 admin 密钥。多签或 timelock,没有讨论余地。我们见过同行项目因单密钥 admin 被攻破而终结。
  3. 两步所有权转移。Ownable2Step 或等价方案。一步转移上的笔误是不可恢复的。
  4. 角色 renounce 和 revoke 路径有测试。DEFAULT_ADMIN_ROLE 实际能在不锁死协议的情况下被 renounce 吗?请测一次。
  5. 可升级合约的 initializer 保护。每个实现合约的 constructor 中放 _disableInitializers(),无例外。

类别 3:算术与溢出(第 11 到 15 项)

  1. 使用 Solidity 0.8+ 且 unchecked 块就地说明理由。每个 unchecked 块附注释解释为何在调用点不可能溢出。
  2. 除法顺序核验。在精度要紧的地方先乘后除。我们在金融路径 grep / 并逐一评审。
  3. 费用与百分比数学使用 basis points 或定义好的精度常量。不用原始小数。把常量写文档并到处使用。
  4. 处理代币 decimal 不一致。USDC 是 6 位 decimal,WETH 是 18。我们对每一对在 mainnet 代币地址上做 fork test。
  5. 类型转换检查。每一处 uint256uint128 或更小的转换都有显式上限检查或 SafeCast。

类别 4:外部调用与重入(第 16 到 20 项)

  1. 强制 Checks-Effects-Interactions。状态更新在任何外部调用之前发生。每个外部调用我们逐个人评审。
  2. 对每个执行外部调用且修改状态的函数都加 ReentrancyGuard。默认开启,只有有理由时才关闭。
  3. 跨函数重入有测试。对一个共享状态的姐妹函数进行重入调用是经典漏点。我们做 fuzz。
  4. 处理不可信代币回调。ERC777、ERC1155 onERC1155Received、ERC721 onERC721Received。假定任何外部代币都是敌对的。
  5. 低层调用上检查返回数据长度。(bool ok, ) = target.call(...) 不检查返回数据是一个等着发生的静默失败。
Kevin Riedl

"加固让审计变得无聊。无聊的审计能上线。"

类别 5:可升级性与存储(第 21 到 25 项)

  1. 存储布局有文档,每次升级都 diff。升级前后跑 forge inspect 存储布局,拒绝任何被重排的 slot。
  2. 每个可升级父合约保留 storage gap。按 OpenZeppelin 惯例 uint256[50] private __gap;
  3. 代理合约把 constructor 逻辑搬到 initializer。可升级合约里 constructor 里的任何内容都是 bug。
  4. UUPS 的 authorizeUpgrade 带角色检查。OpenZeppelin 默认脚手架是抽象的。确保已实现并受保护。
  5. 在 fork 上端到端测试升级路径。部署 V1、填充状态、升级到 V2、验证状态完整。强制。

类别 6:Gas 与 DoS 面(第 26 到 30 项)

  1. 移除或分页无界循环。任何用户可增长的数组上的循环都是 DoS 向量。我们重构成 pull-based 模式。
  2. 核验区块 gas 上限余量。在实际状态规模下,单个函数都不应超过约 30% 的区块 gas 上限。
  3. 处理外部调用 gas stipend。把全部 gas 转给不可信合约会让 grief 成为可能。我们设上限或用 try/catch
  4. 热路径上最小化存储写入。热存储 slot 打包。用 forge snapshot 测量,超过 5% 的回归就驳回。
  5. 前置抢跑与 MEV 面有文档。必要时使用 commit-reveal、slippage 参数、deadline 参数。如果协议会被三明治攻击,用户在主网前就知道。

为什么有这份清单而不是干脆雇审计师?

审计师按代码行和天数收费。把未加固的代码送审,就是花 每天 €2,000 到 €4,000 让资深工程师去找那些你自己团队用 Slither 和 fuzz 框架就能抓到的 bug。在我们的项目历史里,移交审计前完成全部 30 项的项目,审计师发现数回来时只是个位数,大多是 informational。跳过清单的项目通常回来 30+ 发现,包括 critical,然后要重新审计、付全价。

这是 Wavect 与一家通用型外包的差别。完整背景请见 Wavect vs 一家通用型开发外包。我们用一种把安全放在第一位的节奏构建 Web3 系统,这也内建在我们的区块链服务里,因此审计变成签字而非救火。

项目中由谁负责这份清单?

该项目的 Wavect 技术负责人。他们对每一项以提交引用加测试引用签字。客户在审计移交包里收到签字清单,外部审计师会和代码一起拿到这份清单。审计师喜欢这样,因为这告诉他们把注意力集中到哪里。

最终思考

30 项、6 类、每次发布一份签字文档。这就是审前加固评审。我们不替代外部审计师,也从不这样声称。我们做的是把已经清掉明显向量的代码递给他们,让他们的费用买到深层发现,而不是显然的发现。

如果你即将把一份 Solidity 代码库送上主网,却没跑过结构化加固,你就在用审计的价做工程的活。书审计档期之前,先来找我们,不是之后。

在交付智能合约?

 预约免费咨询
Kevin Riedl

7 分钟 阅读 · 2026年5月26日