Foundry Cheatcodes:cheatcode 的定义、调度与稳定 JSON 接口机制
2026/9/16 16:25:23 网站建设 项目流程

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.rsjson.rscrypto.rsscript.rstempo.rsmonad.rs等模块,每个模块对应一组 cheatcode 的实现。

二、Cheats 的本质:对固定地址的调用拦截

在讨论接口定义之前,先明确 cheatcode 在运行时的本质(见 docs/dev/cheatcodes.md):

Cheatcodes are calls to a specific address, the cheatcode handler address, defined asaddress(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 singlesol!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!宏在此承担了双重角色:

  1. 生成原始 Rust 绑定,包括Vm类型、每个函数对应的 call struct,以及聚合的VmCalls枚举;
  2. 允许在每个接口项(函数、struct)或整个接口上标注自定义属性——这里正是通过#[derive(Cheatcode)]挂入内部 derive 宏的入口。

接口中定义的类型(如LogGasCallerModeForgeContext等 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与手动指定的groupstatussafety三个字段。

  • Group枚举划分了 10 个分组:EvmTestingScriptingFilesystemEnvironmentStringJsonTomlCryptoUtilities,每个分组的文档注释都给出了典型示例(如Testing组的assumeexpectRevertScripting组的broadcaststartBroadcast);
  • Safety:分组自带安全级推断——EvmTesting组返回None(歧义,需逐个 cheatcode 判定),其余分组直接推导为Safe
  • StatusStable/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_evmtransact_on_dbwith_fresh_nested_evm等方法。从源码结构看,inspector 内部刻意不向 cheatcode 实现直接暴露嵌套 EVM 的装配过程,而是通过闭包执行(NestedEvmClosureFor),这使 inspector 组合、fork 状态克隆等细节对上层实现保持封装。

六、Inspector 分派链路:从 calldata 到实现

把前几节串起来,一次 cheatcode 调用的完整链路是:

  1. 拦截:EVM 即将执行call时,Cheatcodesinspector 的call回调检查call.target_address == CHEATCODE_ADDRESS
  2. 解码:将 calldata 解码为sol!宏生成的VmCalls枚举;
  3. 分派:对解码成功的 call struct 执行一个大型match——该matchCheatcodederive 宏部分自动生成,每个分支调用对应 call struct 的Cheatcode::apply_full入口;
  4. 实现:落到src/下各功能模块中手写的具体实现(如 evm/ 下的prank.rsmock.rsfork.rs,test/ 下的expect.rsassert.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(LogRpcWalletGasDebugStep等 17 个)、enum(CallerModeAccountAccessKindForgeContextBroadcastTxType)、errors 与全部 cheatcode 函数,注释坦承“技术尚未发展到可以从模块中自动收集某类全部项的地步,所以只能硬编码在这里”。

一致性如何被强制:cargo cheats的“先失败后通过”机制

README 提到 JSON 文件由运行cargo cheats自动生成。结合 crates/cheatcodes/spec/src/lib.rs 中的测试代码可以看到保证机制:测试spec_up_to_dateschema_up_to_dateiface_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 的详细步骤,标准流程共五步:

  1. 定义:在 crates/cheatcodes/spec/src/vm.rs 中新增 Solidity 函数/结构定义。要求:所有 struct 和函数必须有文档注释、所有参数必须命名。此步后编译会因自动生成的match { ... }缺失分支而失败——这是预期行为;
  2. 实现:在 crates/cheatcodes/ 中按分类进入对应模块(如 EVM 类进src/evm/,测试类进src/test/),为新生成的 call struct 实现Cheatcodetrait(三选一:apply/apply_stateful/apply_full),参考相邻实现即可;
  3. 登记类型:如果向Vm增加了新的 struct、enum、error 或 event,需要同步更新 spec::Cheatcodes::new 中的硬编码清单;
  4. 同步 JSON:连续运行cargo cheats两次。第一次预期失败(写回更新后的 cheatcodes.json 等文件),第二次通过;
  5. 测试:在 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 是对0x7109709ECfa91a80626fF3989D68f67F5b1DD12DCHEATCODE_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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询