使用 Certora 对 OpenZeppelin Contracts 进行形式化验证(WTF-Solidity 仓库实战指南)

使用 Certora 对 OpenZeppelin Contracts 进行形式化验证(WTF-Solidity 仓库实战指南) 使用 Certora 对 OpenZeppelin Contracts 进行形式化验证WTF-Solidity 仓库实战指南【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity在 WTF-Solidity 仓库中随 lib/openzeppelin-contracts 一起引入了一套完整的形式化验证Formal Verification工程位于 Topics/Tools/TOOL07_Foundry/hello_wtf/lib/openzeppelin-contracts/fv 目录下。它以 Certora Prover 为引擎用数学证明的方式验证AccessControl、ERC20、ERC721等核心合约的关键安全性质。读完本文你将掌握如何读懂这套验证工程的目录结构、如何用node fv/run.js提交验证任务、如何阅读.conf配置与.spec规范文件以及如何通过make apply/make record维护针对原合约的补丁从而在自己维护的合约项目中复现同样的形式化验证流程。一、什么是形式化验证为什么要做形式化验证与单元测试、模糊测试不同测试只能证明「某些输入下程序行为正确」而形式化验证通过数学推理对所有可能的输入路径证明或证伪给定的性质。在智能合约领域常见做法是使用 Certora ProverCVT配合其专用规范语言Specification Language把合约的字节码行为转化为逻辑命题交给后端求解器证明。OpenZeppelin Contracts 之所以被广泛使用正是因为其核心实现经过了这类严格的验证。本仓库中 fv/README.md 明确说明该目录的全部指令就是「Running Formal Verification Tool」即如何在 OpenZeppelin Contracts 上运行形式化验证工具。验证工作按.conf配置文件定义的 spec 逐合约提交到 Certora 验证服务云端的 Prover并支持在 GitHub Actions CI 环境中对选定的 Pull Request 自动执行。从源码结构看这套 fv 工程是 OpenZeppelin 官方仓库中验证体系的原样嵌入WTF-Solidity 通过 vendor 依赖的方式将其纳入lib/openzeppelin-contracts因此你可以直接在本地复现整个验证过程并把它作为学习形式化验证规范的活教材。二、fv 目录结构总览先从整体上认识这套验证工程目录内容如下相对仓库根目录fv/README.md本文所依据的核心使用说明fv/run.js验证任务提交脚本Node.jsfv/Makefile补丁生成与应用的 make 目标fv/specs/每个受验合约的.conf配置与.spec规范fv/specs/helpers/可复用的规范定义helpers.specfv/specs/methods/各接口的methods摘要声明fv/harnesses/harness 合约继承并简化原合约fv/diff/对原合约的补丁文件.patchfv/reports/历史审计验证报告PDF。从 fv/Makefile 可以看出fv/patched目录并不在仓库中而是由make apply生成的工作产物验证时使用的是打补丁后的fv/patched目录中的合约而不是直接验证原合约。三、前置条件安装 Certora Prover 与获取 API Key按照 fv/README.md 的要求运行本地验证前需要完成两步准备安装 Certora Prover 包按 Certora 官方安装指南获取certoraRun命令行工具并确保solc可执行文件目录已加入系统PATH。获取 API Key本地测试必须提供 API Key在 Certora 平台申请。与此同时验证也会在 GitHub Actions 的 CI 环境中、对选定的 Pull Request 自动运行CI 环境下密钥由平台注入。这一环节有两个关键点值得注意提交任务的命令是certoraRun它由 fv/run.js 通过exec(certoraRun ...)调用因此该命令必须在 PATH 中API Key 通过环境变量注入fv/run.js 使用yargs的.env()配置允许从环境变量读取参数默认并行数parallel为 4。四、运行验证node fv/run.js 全参数详解验证脚本 fv/run.js 是提交验证任务的统一入口从仓库根目录执行node fv/run.js [SPEC_NAME | fv/specs/NAME.conf] [--all] [-p N] [-v]参数含义如下参数含义示例SPEC_NAMEfv/specs/下某个配置文件的基本名不含扩展名AccessControl会映射到fv/specs/AccessControl.conffv/specs/NAME.conf也可以直接传.conf文件的显式路径node fv/run.js fv/specs/ERC721.conf--all运行fv/specs/*.conf下的全部配置node fv/run.js --all-p N/--parallel N并行提交的任务数默认 4node fv/run.js --all -p 8-v/--verbose输出详细日志可叠加count类型node fv/run.js ERC721 -vv官方示例node fv/run.js AccessControl # 运行 AccessControl 配置fv/specs/AccessControl.conf及其 harness 与 spec脚本行为细节源码级从 fv/run.js 源码可以确认以下几点任务名解析argv._.map(name (fs.existsSync(name) ? name : pattern.replace(*, name)))—— 如果传入的参数是已存在的文件路径则直接使用否则按fv/specs/*.conf模板补齐为完整路径fv/run.js忘记--all的提示当既没有指定 spec 名又没有--all时脚本打印Warning: No specs requested. Did you forget to toggle --all?并设置退出码 1fv/run.js并发控制使用p-limit限制同时运行的certoraRun进程数数量由-p决定fv/run.js结果解析脚本从certoraRun的标准输出中正则匹配https://prover.certora.com/output/...形式的报告链接并打印匹配失败或执行出错时打印错误并以退出码 1 结束fv/run.jsverbose 进度-v时打印[i/N] Running conf形式的进度fv/run.js。注意一个 spec 可能被配置为对多个合约运行而一个合约也可能运行多个 spec。二者是多对多关系全部由.conf文件的verify字段决定。五、.conf 配置文件验证任务的装配清单每个 spec 由一个.conf文件描述。以 fv/specs/AccessControl.conf 为例{ files: [ fv/harnesses/AccessControlHarness.sol ], process: emv, url_visibility: public, verify: AccessControlHarness:fv/specs/AccessControl.spec }关键字段files参与验证的 Solidity 源文件列表。这里指向 harness 合约 fv/harnesses/AccessControlHarness.sol而该 harness 内部import {AccessControl} from ../patched/access/AccessControl.sol即引用的是打补丁后的fv/patched目录版本这正是「先 apply 补丁、再验证」流程的落地体现process指定处理方式此处为emvEVM 字节码级验证url_visibility验证报告链接的可见性public表示公开可访问verify合约名:规范文件路径的配对表示用fv/specs/AccessControl.spec中的规则验证AccessControlHarness合约。同目录下还有ERC20.conf、ERC721.conf、Ownable.conf、TimelockController.conf等 21 个配置覆盖了 OpenZeppelin 中最常用的权限、代币与治理模块均可仿照上面的方式阅读。六、.spec 规范文件规则如何描述「正确性」.spec文件用 Certora 规范语言描述待证明的性质。以 fv/specs/AccessControl.spec 为例它通过import引入两类共享定义import helpers/helpers.spec; import methods/IAccessControl.spec;6.1 helpers.spec通用定义fv/specs/helpers/helpers.spec 提供了环境与数学辅助定义例如definition nonzero(address account) returns bool account ! 0; definition nonpayable(env e) returns bool e.msg.value 0; definition nonzerosender(env e) returns bool nonzero(e.msg.sender); definition sanity(env e) returns bool clock(e) 0 clock(e) max_uint48; definition min(mathint a, mathint b) returns mathint a b ? a : b; definition clock(env e) returns mathint to_mathint(e.block.timestamp); definition isSetAndPast(env e, uint48 timepoint) returns bool timepoint ! 0 to_mathint(timepoint) clock(e);这些definition把「调用非 payable」「发送者非零」「时间点已设置且已过去」等条件抽象成可复用的布尔表达式供各规则直接引用。6.2 methods 摘要声明可调用函数fv/specs/methods/IAccessControl.spec 声明了规则中可调用的外部函数及其属性methods { function DEFAULT_ADMIN_ROLE() external returns (bytes32) envfree; function hasRole(bytes32, address) external returns(bool) envfree; function getRoleAdmin(bytes32) external returns(bytes32) envfree; function grantRole(bytes32, address) external; function revokeRole(bytes32, address) external; function renounceRole(bytes32, address) external; }其中envfree表示该函数返回结果与交易环境msg.sender、block.timestamp 等无关这让 Prover 可以更高效地推理。6.3 规则示例只有 grantRole 能授予权限AccessControl.spec中第一条规则onlyGrantCanGrant验证「只有grantRole能改变角色的授予状态」rule onlyGrantCanGrant(env e, method f, bytes32 role, address account) { calldataarg args; bool hasRoleBefore hasRole(role, account); f(e, args); bool hasRoleAfter hasRole(role, account); assert ( !hasRoleBefore hasRoleAfter ) ( f.selector sig:grantRole(bytes32, address).selector ); assert ( hasRoleBefore !hasRoleAfter ) ( f.selector sig:revokeRole(bytes32, address).selector || f.selector sig:renounceRole(bytes32, address).selector ); }这条规则用method f概括任意函数调用如果在调用f前后hasRole(role, account)从 false 变为 true那么f必然是grantRole如果从 true 变为 false则f必然是revokeRole或renounceRole。也就是说规则断言「除这三个入口外没有任何方法可以改变权限状态」。grantRoleEffect、revokeRoleEffect、renounceRoleEffect三条规则则进一步验证每个方法的「正确性三元组」liveness活性调用是否成功与调用者权限严格对应。例如grantRoleEffect断言success isCallerAdmin即只有角色管理员调用才可能成功fv/specs/AccessControl.specrenounceRoleEffect则断言success account e.msg.sender即只能对自己执行放弃角色fv/specs/AccessControl.speceffect效果成功后状态确实发生变化。如grantRole成功后hasRole(role, account)为真fv/specs/AccessControl.specno side effect无副作用调用不会影响无关角色/账户的组合。如hasOtherRoleBefore ! hasOtherRoleAfter蕴含(role otherRole account otherAccount)fv/specs/AccessControl.spec。规则中大量使用withrevert如grantRolewithrevert(e, role, account)与lastReverted用于捕获「调用回滚」这一分支确保对失败路径同样进行推理。七、Harness 机制为可验证性改造合约部分规则要求对原始代码做各种简化OpenZeppelin 的主要手段是验证一个继承自原合约、并覆写部分方法的 harness 合约它们统一存放在 fv/harnesses/ 目录共 20 个例如AccessControlHarness.sol、ERC20PermitHarness.sol、ERC721Harness.sol、TimelockControllerHarness.sol等。以 fv/harnesses/AccessControlHarness.sol 为例// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {AccessControl} from ../patched/access/AccessControl.sol; contract AccessControlHarness is AccessControl {}它直接继承fv/patched下的AccessControl。注意这里的 pragma 为^0.8.20且 import 的是补丁目录而非原始contracts/与.conf中files字段及 README 中「在fv/patched目录上运行验证」的说明完全吻合。八、补丁机制make apply / make record / make cleanharness 模式需要对原代码做少量修改例如某些方法需要从private改为internal或改为virtual。这些改动通过补丁在验证前应用由 fv/Makefile 管理。8.1 应用补丁make applymake -C fv apply执行流程fv/Makefile删除旧的fv/patched目录并从contracts/即../contracts完整复制生成新的fv/patched逐个遍历 fv/diff/ 下的.patch文件用patch -p0应用到fv/patched中对应的 Solidity 文件。补丁文件命名规则为「路径下划线化」例如access_manager_AccessManager.sol.patch→ 对应access/manager/AccessManager.soltoken_ERC721_ERC721.sol.patch→ 对应token/ERC721/ERC721.sol。以 fv/diff/token_ERC721_ERC721.sol.patch 为例它展示了典型的 FV 化改造——把私有状态变量改为 internal 以便 harness 访问- mapping(address owner uint256) private _balances; mapping(address owner uint256) internal _balances; // private → internal for FV注意make apply是每次运行验证前的必做步骤。README 明确要求在运行fv/run.js之前必须先把对应补丁应用到contracts目录把输出放到fv/patched目录然后对fv/patched目录执行验证。8.2 记录新补丁make record当原合约发生变更时直接重新应用旧补丁可能产生冲突。解决流程是手动在fv/patched中合并/修正冲突验证脚本会报错并在patched目录输出被拒绝的改动合并完成后执行make -C fv recordrecord会用diff -ruN对比contracts/SRC与fv/patchedDST的差异重新生成fv/diff/下的补丁文件fv/Makefile并将空补丁自动删除[ -s $ ] || rm $。生成的新补丁即可提交进 git。8.3 查看帮助与清理make -C fv help输出三个目标说明make apply通过把补丁应用到contracts/创建fv/patched目录make record记录contracts/与fv/patched之间差异对应的补丁make clean移除所有生成文件即被 git 忽略的文件底层执行git clean -fdXfv/Makefile。九、完整实战流程从零跑通一次验证综合以上各部分在本仓库中跑通一次形式化验证的完整顺序如下# 1. 安装 Certora ProvercertoraRun 入 PATH并配置 API Key 环境变量 # 2. 应用补丁生成 fv/patched 目录每次验证前必须执行 make -C fv apply # 3. 运行单个 spec例如 AccessControl node fv/run.js AccessControl # 等价写法node fv/run.js fv/specs/AccessControl.conf # 4. 并行运行全部 specs node fv/run.js --all -p 4 # 5. 验证成功后若修改了原合约用 record 重新生成补丁并提交 make -C fv record其中第 2 步会先生成fv/patched/access/AccessControl.sol等补丁文件第 3 步的AccessControl.conf通过AccessControlHarness它 import../patched/access/AccessControl.sol加载这些文件因此跳过make apply直接运行 run.js 会失败——这是新手最容易踩的坑。十、小结本仓库lib/openzeppelin-contracts/fv目录是一套完整、可直接复用的 Certora 形式化验证工程其工作流可归纳为三条主线装配.conf文件把 harness 合约与.spec规范配对run.js据此调用certoraRun向云端 Prover 提交任务并支持--all全量、-p并行、-v详细日志改造fv/harnesses/中的继承式 harness 与fv/diff/中的补丁如private → internal解决「原合约不可直接验证」的问题make apply/make record负责补丁的生成与维护证明.spec文件用规则rule表达活性、效果与无副作用等性质helpers.spec与methods/目录提供共享的抽象定义如 AccessControl 中「只有 grantRole/revokeRole/renounceRole 能改变权限状态」这类强性质均由数学证明保证。对正在学习 WTF-Solidity 的开发者而言这套工程的价值在于你可以把任何一个.spec文件当作 Certora 规范语言的教学样例把任何一个.conf与 harness 的组合当作「如何为一个合约编写可验证入口」的参考模板。当你在自己的合约项目中需要同样的安全保证时完全可以照搬这套目录结构与命令流程。本文基于 Topics/Tools/TOOL07_Foundry/hello_wtf/lib/openzeppelin-contracts/fv/README.md 撰写相关实现细节以 fv/run.js、fv/Makefile、fv/specs/AccessControl.conf、fv/specs/AccessControl.spec、fv/harnesses/AccessControlHarness.sol、fv/diff/token_ERC721_ERC721.sol.patch 等文件为准。验证的实际运行需要 Certora Prover 环境与 API Key本仓库仅提供脚本与规范不附带运行结果。【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考