Foundry Cheatcodes:cheatcode 的定义、调度与稳定 JSON 接口机制
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
本篇指南基于 Foundry 仓库中foundry-cheatcodescrate 的官方说明文档(crates/cheatcodes/README.md)及其配套开发者文档,讲清楚 Foundry cheatcode 体系的完整技术链路:cheatcode 如何在单个sol!宏调用中声明、如何借助内部Cheatcodederive 宏同时生成 Rust 绑定与 JSON 规范、Cheatcodesinspector 如何在 EVM 执行中拦截并分派 cheatcode 调用,以及cheatcodes.json这一对外稳定接口是如何保证与源码同步的。读完之后,你可以独立为 Foundry 新增一个 cheatcode,并能理解第三方工具(如 forge-std、Foundry book)如何基于同一份 JSON 接口消费 cheatcode 定义。
一、crate 结构与整体定位
根据 crates/cheatcodes/README.md,foundry-cheatcodes承担“Foundry cheatcodes 的定义与实现”这一职责,其目录结构分为三部分:
- assets/:JSON 接口与 schema 规范,即 cheatcodes.json 和 cheatcodes.schema.json;
- spec/:定义公共 trait 与结构体(cheatcode 的“规范层”,crate 名为
foundry-cheatcodes-spec); - src/:cheatcode 的 Rust 实现(“实现层”)。
这种“spec 与实现分离”的组织方式与源码目录一一对应:spec/src/vm.rs中用 Solidity 语法声明Vm接口,而src/下按功能域拆分为evm/、test/、inspector/、fs.rs、json.rs、crypto.rs、script.rs、tempo.rs、monad.rs等模块,每个模块对应一组 cheatcode 的实现。
二、Cheats 的本质:对固定地址的调用拦截
在讨论接口定义之前,先明确 cheatcode 在运行时的本质(见 docs/dev/cheatcodes.md):
Cheatcodes are calls to a specific address, the cheatcode handler address, defined as
address(uint160(uint256(keccak256("hevm cheat code"))))(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D).
在 Solidity 测试中这通常写作Vm constant vm = Vm(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);,一般则从forge-std/Test.sol继承而来。该常量在源码中的定义位置是 crates/evm/core/src/constants.rs:
pub const CHEATCODE_ADDRESS: Address = address!("0x7109709ECfa91a80626fF3989D68f67F5b1DD12D");由于 cheatcode 绑定在一个常量地址上,Cheatcodesinspector 只需监听这个地址。从 crates/cheatcodes/src/inspector.rs 的call回调可以看到拦截逻辑的核心判断:
if call.target_address == CHEATCODE_ADDRESS { // 拦截到 cheatcode 调用 // ... }interceptor 机制建立在 revm 的Inspectortrait 之上——该 trait 提供一系列回调,在 EVM 执行的特定阶段(如即将执行 call、create 时)被通知。Foundry 的evmcrate 中还有 coverage、tracing、debugger、logging 等多种 inspector,cheatcode inspector 是其中负责“状态操纵型调用拦截”的一种。
三、接口定义:单个sol!宏调用承载全部 cheatcode
crates/cheatcodes/README.md 的核心论点是:
All cheatcodes are defined in a single
sol!macro call inspec/src/vm.rs.
这个sol!调用来自 Alloy 生态的宏,负责生成 Solidity 接口的 Rust 绑定。实际的接口声明位于 crates/cheatcodes/spec/src/vm.rs(当前约 3500 行),开头如下:
sol! { // Cheatcodes are marked as view/pure/none using the following rules: // 0. A call's observable behaviour includes its return value, logs, reverts and state writes, // 1. If you can influence a later call's observable behaviour, you're neither `view` nor `pure` // (you are modifying some state be it the EVM, interpreter, filesystem, etc), // 2. Otherwise if you can be influenced by an earlier call, or if reading some state, you're `view`, // 3. Otherwise you're `pure`. /// Foundry cheatcodes interface. #[derive(Debug, Cheatcode)] // Keep this list small to avoid unnecessary bloat. #[sol(abi)] interface Vm { // ======== Types ======== /// Error thrown by cheatcodes. error CheatcodeError(string message); // ... } }文件头部注释同时给出了一条重要的规范:cheatcode 的 Solidity 可见性(view/pure/ 无)不是随意的,而是按“是否影响后续调用的可观察行为”、“是否受先前调用影响或读取状态”逐条判定。这条规则保证了接口元数据在语义上是一致的。
sol!宏在此承担了双重角色:
- 生成原始 Rust 绑定,包括
Vm类型、每个函数对应的 call struct,以及聚合的VmCalls枚举; - 允许在每个接口项(函数、struct)或整个接口上标注自定义属性——这里正是通过
#[derive(Cheatcode)]挂入内部 derive 宏的入口。
接口中定义的类型(如Log、Gas、CallerMode、ForgeContext等 struct/enum)与函数一起构成完整的 cheatcode 词汇表,其中Gas结构甚至细化到 EIP-8037 state gas 的单独计量字段,展示了该接口对现代 EVM 语义的覆盖深度。
四、Cheatcodederive 宏:编译期检查与规范生成
README 指出,sol!宏与内部Cheatcodederive 宏(实现位于 crates/macros/src/cheatcodes.rs)组合,使得“同时生成 Rust 定义与 JSON 规范”成为可能。derive 宏在Vm接口声明上派生一次,并递归作用于接口的所有项以及sol!生成的产物(如VmCalls枚举)。
它做了两类工作(详见 docs/dev/cheatcodes.md):
- 编译期检查:确保每个函数和 struct 都有文档注释、每个函数参数都有命名。缺少这些信息会直接编译失败——这是一种把文档质量变成硬约束的做法;
- 生成派发骨架:生成一个稍后用于实现
match { ... }分派函数的宏。新增 cheatcode 后编译失败,正是因为这个自动生成的match期望每个新 call struct 都已实现Cheatcodetrait,而下一步实现即可修复编译。
derive 宏还会解析函数上的#[cheatcode(...)]属性,这些属性用于指定 JSON 接口的附加元数据。当前支持的属性有三个:
| 属性 | 含义 | 规则 |
|---|---|---|
#[cheatcode(group = <ident>)] | cheatcode 所属分组 | 必填 |
#[cheatcode(status = <ident>)] | 当前状态(stable / experimental 等) | 默认Stable |
#[cheatcode(safety = <ident>)] | 在 script 中使用是否安全 | 未指定时取分组的安全级;分组语义不明时必须手动指定 |
多个属性用逗号分隔,例如#[cheatcode(group = Evm, status = Experimental)]。spec/src/vm.rs中大量函数都带着此类标注,例如:
/// Gets the address for a given private key. #[cheatcode(group = Evm, safety = Safe)] function addr(uint256 privateKey) external pure returns (address keyAddr); /// Gets the nonce of an account. #[cheatcode(group = Evm, safety = Safe)] function getNonce(address account) external view returns (uint64 nonce);元数据模型:Group、Status 与 Safety
这些属性最终落到 crates/cheatcodes/spec/src/cheatcode.rs 定义的规范结构上。每个 cheatcode 的规范由Cheatcode结构体承载,字段包括自动生成的 Solidity 函数声明func与手动指定的group、status、safety三个字段。
- Group枚举划分了 10 个分组:
Evm、Testing、Scripting、Filesystem、Environment、String、Json、Toml、Crypto、Utilities,每个分组的文档注释都给出了典型示例(如Testing组的assume、expectRevert;Scripting组的broadcast、startBroadcast); - Safety:分组自带安全级推断——
Evm与Testing组返回None(歧义,需逐个 cheatcode 判定),其余分组直接推导为Safe; - Status:
Stable/Experimental/Deprecated(Option<&str>)/Removed/Internal,分别对应无警告、使用警告、弃用警告、硬错误等不同的运行时反馈策略。
五、Cheatcodetrait:三级实现入口
README 强调:“Cheatcodes are manually implemented through theCheatcodetrait, which is called in theCheatcodesinspector implementation.” 该 trait 定义在 crates/cheatcodes/src/lib.rs,规定了三个可实现的入口方法,且只能实现其一:
| 方法 | 用途 | 适用场景 |
|---|---|---|
apply | 不依赖 EVM 数据的纯 cheatcode | 简单状态操纵 |
apply_stateful | 需要访问 EVM 数据 | 依赖当前 EVM 状态的操作 |
apply_full | 需要 EVM executor 访问 | 需要发起嵌套 EVM 调用的操作 |
trait 定义与默认降级链如下:
/// Cheatcode implementation. pub(crate) trait Cheatcode: CheatcodeDef { /// Applies this cheatcode to the given state. fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result { let _ = state; unimplemented!("{}", Self::CHEATCODE.func.id) } fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result { self.apply(ccx.state) } fn apply_full<FEN: FoundryEvmNetwork>( &self, ccx: &mut CheatsCtxt<'_, '_, FEN>, executor: &mut dyn CheatcodesExecutor<FEN>, ) -> Result { let _ = executor; self.apply_stateful(ccx) } }三级入口对应三种上下文能力:apply只拿到Cheatcodes状态;apply_stateful拿到CheatsCtxt——它聚合了 inspector 状态、EVM 上下文(FoundryContextFor)、原始msg.sender和当前 cheatcode 调用的 gas limit,并通过Deref/DerefMut直接转发到 EVM 上下文;apply_full额外提供CheatcodesExecutor,用于执行嵌套 EVM 操作(如exec类 cheatcode 需要在一个子 EVM 中回放调用)。
嵌套执行能力由 crates/cheatcodes/src/inspector.rs 中的CheatcodesExecutortrait 抽象,提供with_nested_evm、transact_on_db、with_fresh_nested_evm等方法。从源码结构看,inspector 内部刻意不向 cheatcode 实现直接暴露嵌套 EVM 的装配过程,而是通过闭包执行(NestedEvmClosureFor),这使 inspector 组合、fork 状态克隆等细节对上层实现保持封装。
六、Inspector 分派链路:从 calldata 到实现
把前几节串起来,一次 cheatcode 调用的完整链路是:
- 拦截:EVM 即将执行
call时,Cheatcodesinspector 的call回调检查call.target_address == CHEATCODE_ADDRESS; - 解码:将 calldata 解码为
sol!宏生成的VmCalls枚举; - 分派:对解码成功的 call struct 执行一个大型
match——该match由Cheatcodederive 宏部分自动生成,每个分支调用对应 call struct 的Cheatcode::apply_full入口; - 实现:落到
src/下各功能模块中手写的具体实现(如 evm/ 下的prank.rs、mock.rs、fork.rs,test/ 下的expect.rs、assert.rs等)。
这条链路解释了为什么新增 cheatcode 会“先编译失败再手工实现”:sol!与 derive 宏在编译期就生成了完备的match期望,而 trait 方法体留待开发者补齐。
七、JSON 接口:对外的稳定契约
crates/cheatcodes/README.md 中单独列出了 JSON 接口的地位:
The JSON interface is guaranteed to be stable, and can be used by third-party tools to interact with the Foundry cheatcodes externally.
文档列举了当前的实际消费方(以仓库内可验证的部分为准):
- Foundry 内部用它自动生成一份用于测试的 Solidity 接口,即 testdata/utils/Vm.sol;
- 社区项目
forge-std基于同一接口生成用户侧的Vm.sol; - Foundry book 基于它生成 cheatcodes 参考文档(文档标注为进行中)。
JSON 接口的内容模型对应 crates/cheatcodes/spec/src/lib.rs 中的Cheatcodes结构体:
pub struct Cheatcodes<'a> { pub errors: Cow<'a, [Error<'a>]>, pub events: Cow<'a, [Event<'a>]>, pub enums: Cow<'a, [Enum<'a>]>, pub structs: Cow<'a, [Struct<'a>]>, pub cheatcodes: Cow<'a, [Cheatcode<'a>]>, }Cheatcodes::new()从Vm上硬编码收集各 struct(Log、Rpc、Wallet、Gas、DebugStep等 17 个)、enum(CallerMode、AccountAccessKind、ForgeContext、BroadcastTxType)、errors 与全部 cheatcode 函数,注释坦承“技术尚未发展到可以从模块中自动收集某类全部项的地步,所以只能硬编码在这里”。
一致性如何被强制:cargo cheats的“先失败后通过”机制
README 提到 JSON 文件由运行cargo cheats自动生成。结合 crates/cheatcodes/spec/src/lib.rs 中的测试代码可以看到保证机制:测试spec_up_to_date、schema_up_to_date、iface_up_to_date会重新生成cheatcodes.json、schema 与testdata/utils/Vm.sol的内容并与磁盘文件比对,核心逻辑是:
/// Checks that the `file` has the specified `contents`. If that is not the /// case, updates the file and then fails the test. fn ensure_file_contents(file: &Path, contents: &String) { // 内容一致则通过;否则写回新内容并 panic,要求重跑 }这正是文档中描述的“第一次执行预期失败”的原因:新增 cheatcode 后首跑会把更新写回 cheatcodes.json 并失败,以便 CI 检测到变更;再次执行即通过,确认文件已同步。同一套测试还生成 testdata/utils/Vm.sol——文件头注释明确写着“Automatically generated fromfoundry-cheatcodesVm definitions. Do not modify manually.”
八、新增一个 cheatcode 的完整流程
综合 crates/cheatcodes/README.md 的指引与 docs/dev/cheatcodes.md 的详细步骤,标准流程共五步:
- 定义:在 crates/cheatcodes/spec/src/vm.rs 中新增 Solidity 函数/结构定义。要求:所有 struct 和函数必须有文档注释、所有参数必须命名。此步后编译会因自动生成的
match { ... }缺失分支而失败——这是预期行为; - 实现:在 crates/cheatcodes/ 中按分类进入对应模块(如 EVM 类进
src/evm/,测试类进src/test/),为新生成的 call struct 实现Cheatcodetrait(三选一:apply/apply_stateful/apply_full),参考相邻实现即可; - 登记类型:如果向
Vm增加了新的 struct、enum、error 或 event,需要同步更新 spec::Cheatcodes::new 中的硬编码清单; - 同步 JSON:连续运行
cargo cheats两次。第一次预期失败(写回更新后的 cheatcodes.json 等文件),第二次通过; - 测试:在 testdata/default/cheats/ 下为新 cheatcode 编写集成测试。该目录已包含 100 余个 cheat 场景的 Solidity 测试文件,新测试可直接对齐既有命名与组织方式。
九、一个实例:registerMappingSstoreHook的约束面
开发者文档 docs/dev/cheatcodes.md 还给出了一个实验性 cheatcode 的规格细节,可以展示这套体系如何表达复杂约束:registerMappingSstoreHook为某个具体 mapping 根注册 post-store 回调,回调参数为(account, computedSlot, rootSlot, keys, oldValue, newValue),其中keys为根到叶顺序的原始bytes32存储字;其要求目标在最新注册之后观察到完整的 64 字节 Keccak provenance、回调必须通过msg.sender == address(vm)鉴权以防外部伪造、且与 raw SSTORE hook 不能对同一目标叠加。这类约束说明 cheatcode 不只是“接口 + 一行实现”,其安全模型同样是 spec 的一部分。
十、要点回顾
- 单点定义:全部 cheatcode 集中于 spec/src/vm.rs 的一个
sol!调用,配合#[derive(Cheatcode)]同时产出 Rust 绑定与 JSON 规范,杜绝“实现与文档漂移”; - 编译期文档约束:derive 宏强制文档注释与参数命名,未实现的 cheatcode 会直接破坏编译,形成“定义即承诺”的工作流;
- 运行时拦截模型:cheatcode 是对
0x7109709ECfa91a80626fF3989D68f67F5b1DD12D(CHEATCODE_ADDRESS)的普通 call,由Cheatcodesinspector 在call回调中解码分派(src/inspector.rs); - 三级实现接口:
apply/apply_stateful/apply_full分别对应“仅状态 / EVM 数据 / executor(嵌套 EVM)”三种能力梯度(src/lib.rs); - 稳定对外契约:cheatcodes.json 声明为稳定接口,供 forge-std、Foundry book 等外部消费,并由 spec 层测试与
cargo cheats的双跑机制强制保持同步; - 完整扩展流程:定义 → 实现 trait → 登记类型 → 两次
cargo cheats→ 集成测试,每一步的失败/成功都是流程内建的质量门禁。
掌握这套机制后,无论是阅读任意 cheatcode 的实现(先查Vm接口找语义,再按 group 定位模块),还是为项目贡献新 cheatcode,都可以在 crates/cheatcodes/ 与 docs/dev/cheatcodes.md 之间找到闭环的参照系。
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考