Foundry 中 Monad reserve-balance 跟踪器与 deal / cloneAccount / loadAllocs 状态同步机制解析
【免费下载链接】foundryFoundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.项目地址: https://gitcode.com/GitHub_Trending/fo/foundry
Foundry 在 .changelog/monad-state-splice-tracker.md 这一 patch 级变更记录中,修复了一个与 Monad 网络支持相关的关键一致性问题:让 Monad 的 reserve-balance 跟踪与deal、cloneAccount、loadAllocs三个 cheatcode 造成的账户变更及其回滚保持同步。本文以该变更记录为骨架,结合foundry-evm-core、foundry-cheatcodes、foundry-evm等 crate 的源码实现,深入讲解这一同步机制的来龙去脉:什么是 reserve-balance 跟踪、为什么 cheatcode 直改账户会破坏它、修复如何在源码层面落地,以及快照回滚与嵌套 EVM 场景下的完整处理路径。读完本文,你将能理解 Foundry 多网络 EVM 抽象中"派生状态(derived state)"与"外部状态注入"之间的一致性保障思路,并掌握FoundryJournal、refresh_chain_journal等核心抽象的实际用法。
一、变更记录原文与波及范围
该变更记录全文如下:
Kept Monad reserve-balance tracking synchronized with account changes and rollbacks from the
deal,cloneAccount, andloadAllocscheatcodes.
即:保持 Monad 的 reserve-balance 跟踪与deal、cloneAccount、loadAllocs三个 cheatcode 造成的账户变更及其回滚同步。
其 frontmatter 声明了四个受影响包均为patch级别:
| 包名 | 版本变更 | 职责 |
|---|---|---|
forge | patch | Forge 主程序,承载vm.deal等 cheatcode 的 CLI 入口 |
foundry-cheatcodes | patch | cheatcode 的实现层,直接处理deal/cloneAccount/loadAllocs调用 |
foundry-evm | patch | EVM 执行与 inspector 栈,处理嵌套 EVM、隔离调用等场景 |
foundry-evm-core | patch | 核心抽象层,定义FoundryJournal等 trait |
根据 .changelog/README.md 的约定,每个.changelog/*.md条目将包名映射到patch/minor/major并提供一条发布说明——本条即是一次标准的缺陷修复记录,因此统一使用patch级别。
二、背景:Monad 网络支持与 reserve-balance 跟踪
2.1 Monad 网络进入 Foundry
.changelog/monad-network.md 记录了 Monad 网络支持作为minor特性进入 Foundry:Anvil、Cast、Chisel、Forge 四个工具同时获得 Monad 网络支持。从源码结构看,Monad 支持被组织为:
crates/evm/networks/src/lib.rs中注册了 Monad 网络的相关定义;crates/evm/core/src/evm/monad.rs提供了MonadEvmFactory、MonadChainContext、MonadContext等 Monad 专属类型;- 相关实现通过
#[cfg(feature = "monad")]特性门控,仅在启用该特性时编译(参见 .changelog/cleanup-monad-feature-imports.md、.changelog/gate-monad-fork-context-update.md 等同系列条目)。
2.2 什么是 reserve-balance 跟踪
Monad 的 EVM 实现(monad_revm)在事务执行期间维护一个reserve-balance tracker(monad_revm::reserve_balance::tracker::ReserveBalanceTracker),它记录与当前链上下文(block 参与者)相关的账户储备余额会计状态。在 Anvil 的集成测试 crates/anvil/tests/it/monad.rs 中可以看到,Monad 通过一个reserve-balance 预编译合约向合约暴露储备余额的查询能力(测试辅助函数reserve_balance_call()位于该文件第 2304 行附近,大量用例如monad_nine_exposes_reserve_balance_precompile_for_calls、monad_eight_config_filters_reserve_balance_precompile都围绕它展开)。
从 crates/evm/core/src/evm/monad.rs 中MonadChainContext::refresh_journal的实现可以看出该跟踪器的本质:
fn refresh_journal<J: FoundryJournal>(&self, journal: &mut J) { let mut tracker = journal.capture_reserve_balance(); tracker.rebase(self, journal.evm_state()); journal.restore_reserve_balance(tracker); }即:tracker 是缓存于 journal 中的派生结构,它根据链上下文(self,即 block 参与者集合)与当前 EVM 账户状态(journal.evm_state())通过rebase重新计算。正常情况下,事务执行走 revm 的标准记账路径,tracker 与账户状态自然保持一致。
三、问题本质:cheatcode 直改账户绕过常规记账路径
deal、cloneAccount、loadAllocs三个 cheatcode 有一个共同点:它们直接修改底层数据库(backend)中的账户状态,而不是通过 revm 的事务执行路径。这会导致缓存于 journal 中的 reserve-balance tracker 变成陈旧数据(stale)。
3.1deal:直接替换账户余额
crates/cheatcodes/src/evm.rs 中dealCall::apply_stateful的实现:
impl Cheatcode for dealCall { fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result { let Self { account: address, newBalance: new_balance } = *self; let account = journaled_account(ccx.ecx, address)?; let old_balance = std::mem::replace(&mut account.info.balance, new_balance); let record = DealRecord { address, old_balance, new_balance }; ccx.state.eth_deals.push(record); refresh_chain_journal(ccx.ecx); Ok(Default::default()) } }它直接用std::mem::replace把账户余额换成new_balance,并记录DealRecord以便测试结束后恢复。deal是对账户余额最直接的"外科手术式"修改,跟踪器若未随之更新,预编译查询到的储备余额就会与实际余额脱节。
3.2cloneAccount:克隆整个账户
crates/cheatcodes/src/evm.rs 中cloneAccountCall::apply_stateful:
impl Cheatcode for cloneAccountCall { fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result { let Self { source, target } = self; let account = ccx.ecx.journal_mut().load_account(*source)?; let genesis = genesis_account(account.data); let (db, inner) = ccx.ecx.db_journal_inner_mut(); db.clone_account(&genesis, target, inner)?; // Cloned account should persist in forked envs. ccx.ecx.db_mut().add_persistent_account(*target); refresh_chain_journal(ccx.ecx); Ok(Default::default()) } }它把源账户的余额、nonce、存储等完整克隆到目标地址,同样是在 backend 层面新增账户数据。
3.3loadAllocs:批量装载账户
crates/cheatcodes/src/evm.rs 中loadAllocsCall::apply_stateful读取 allocs(或 genesis)文件后一次性db.load_allocs(&allocs, inner)装载大量账户。
3.4 统一的修复落点:refresh_chain_journal
注意这三个实现都以refresh_chain_journal(ccx.ecx)收尾。这正是本次修复的核心落点:在每一次直改账户的 cheatcode 完成后,立即基于新的链上下文与 EVM 状态重新计算(rebase)reserve-balance tracker,使其与账户变更保持同步。
refresh_chain_journal定义在 crates/evm/core/src/env.rs:
pub fn refresh_chain_journal<CTX: FoundryContextExt>(context: &mut CTX) { let chain = context.chain().clone(); chain.refresh_journal(context.journal_mut()); }它克隆当前链上下文,再调用FoundryChain::refresh_journal。对普通以太坊网络而言impl FoundryChain<Tx> for ()是空操作;对 Monad 网络则触发上面看到的 trackerrebase。这样设计让通用 cheatcode 代码只需调用一个统一的刷新函数,具体语义由各网络族(EVM family)自行决定。
四、FoundryJournaltrait:Monad 专有逻辑的隔离
与本次修复配套的 .changelog/foundry-journal-reserve-balance.md 记录了同系列重构:将FoundryEvmFactory/NestedEvm上零散的交易状态捕获/恢复方法,替换为通过 revm 原生ContextTr::journal/journal_mut直接访问,并新增FoundryJournaltrait。其要点是:Monad 的 reserve-balance tracker 方法在 trait 上是具体实现且以#[cfg(feature = "monad")]门控,从而把最后一批 Monad 专属方法与关联类型从通用 trait 中移除。
crates/evm/core/src/env.rs 中FoundryJournal的定义清晰地体现了这一点:
/// Foundry extension for Journal type pub trait FoundryJournal: JournalExt { /// Mutable access to the database and journal inner. fn db_journal_inner_mut(&mut self) -> (&mut Self::Database, &mut JournaledState); /// Reference to the journal inner. fn journal_inner(&self) -> &JournaledState; /// Captures Monad's reserve-balance tracker for the active transaction. #[cfg(feature = "monad")] fn capture_reserve_balance( &self, ) -> monad_revm::reserve_balance::tracker::ReserveBalanceTracker { monad_revm::reserve_balance::tracker::ReserveBalanceTracker::default() } /// Restores Monad's reserve-balance tracker for the active transaction. #[cfg(feature = "monad")] fn restore_reserve_balance( &mut self, _tracker: monad_revm::reserve_balance::tracker::ReserveBalanceTracker, ) { } /// Whether transaction boundaries currently preserve the reserve-balance tracker... #[cfg(feature = "monad")] fn preserves_reserve_balance(&self) -> bool { false } /// Sets whether transaction boundaries preserve the reserve-balance tracker. #[cfg(feature = "monad")] fn set_preserve_reserve_balance(&mut self, _preserve: bool) {} }其设计要点:
- trait 提供默认实现:
capture_reserve_balance默认返回ReserveBalanceTracker::default(),restore_reserve_balance默认空操作。因此普通以太坊 journal(impl FoundryJournal for Journal<DB>)无需任何 Monad 逻辑即可编译。 - Monad journal 覆盖实现:
#[cfg(feature = "monad")] impl FoundryJournal for monad_revm::MonadJournal<DB>中,捕获通过monad_revm::MonadJournalTr::reserve_balance(self).clone()克隆 tracker,恢复通过reserve_balance_mut(self)写回,preserves_reserve_balance则透传 revm 原生判断。 - 通用代码无感知:
FoundryContextExt(crates/evm/core/src/env.rs)只依赖Journal: FoundryJournal这一约束,即可获得db_journal_inner_mut()、journal_inner()、set_journal_inner()等能力,cheatcode 层完全不需要关心当前是否运行在 Monad 上。
五、快照与回滚路径上的同步
变更记录强调"and rollbacks"——不仅账户变更时要同步,vm.snapshot/vm.revertTo回滚时也要让 tracker 回到一致状态。这一部分在 crates/cheatcodes/src/evm.rs 的快照处理函数中体现。
5.1 快照:连带保存 tracker
inner_snapshot_state(crates/cheatcodes/src/evm.rs)在保存journaled_state、环境覆盖(env_overrides)与 fork 块号覆盖的同时,Monad 门控地额外保存一份(chain 上下文, reserve-balance tracker)组合:
#[cfg(feature = "monad")] { use foundry_evm_core::FoundryJournal as _; ccx.state .context_snapshots .insert(id, (ccx.ecx.chain().clone(), ccx.ecx.journal().capture_reserve_balance())); }这里capture_reserve_balance克隆出 tracker 的当前副本,与链上下文一起存入context_snapshots,快照 id 与后端快照保持一致。
5.2 回滚:先还原 tracker 再刷新
inner_revert_to_state(crates/cheatcodes/src/evm.rs)和inner_revert_to_state_and_delete(同文件第 1725 行起)在调用后端revert_state恢复journaled_state之后:
if let Some((context, state)) = ccx.state.context_snapshots.get(&snapshot_id) { *ccx.ecx.chain_mut() = context.clone(); ccx.ecx.journal_mut().restore_reserve_balance(state.clone()); } ... refresh_chain_journal(ccx.ecx);即先还原快照时刻的 tracker 副本,再调用refresh_chain_journal做一次 rebase 校准,最后才恢复 EVM 环境。两个函数分别对应RevertKeep(保留快照以便再次回滚,用.get不删除)与RevertRemove(删除快照,用.remove)。这样即使快照期间发生了deal/cloneAccount/loadAllocs造成的余额变化,回滚后 tracker 也能与还原后的账户状态严格一致。
六、嵌套 EVM 与隔离调用中的 capture / restore
除 cheatcode 直改账户外,Foundry 的测试框架还会创建嵌套 EVM(nested EVM,例如 cheatcode 内部再执行一段逻辑)与隔离调用(isolated call,例如vm.ffi、startPrank相关的隔离执行)。这些子执行体共享同一个数据库,因此 tracker 需要在进入/退出子 EVM 时被捕获并恢复,否则子事务对账户的修改会污染父事务的跟踪状态。
6.1 inspector 栈中的嵌套 EVM
crates/evm/evm/src/inspectors/stack.rs 中InspectorStackInner::with_nested_evm的流程:
- 进入前:
capture_reserve_balance(ecx.journal())保存父上下文 tracker; - 构建嵌套 EVM 后:
restore_reserve_balance(evm.journal_mut(), state)把 tracker 注入子 EVM,并调用refresh_nested_chain_journal; - 子执行体运行完毕:再次
capture_reserve_balance(evm.journal_mut())取回子结果; - 回到父上下文:把子结果
restore_reserve_balance写回父 journal。
TransparentCheatcodesExecutor(crates/cheatcodes/src/inspector.rs)遵循完全相同的模式。这套"进入捕获 → 注入子环境 → 退出回收 → 回写父环境"的对称结构,保证了嵌套执行期间 tracker 不会丢失或错乱。
6.2 隔离调用的 preserve 语义
隔离调用(isolated call)稍有不同:crates/evm/evm/src/inspectors/stack.rs 中创建嵌套 EVM 时额外设置了:
foundry_evm_core::FoundryJournal::set_preserve_reserve_balance( evm.journal_mut(), true, );结合 trait 注释(crates/evm/core/src/env.rs)可以理解:隔离调用建模的是外层事务的内部调用(inner call),而非一条新事务,因此事务边界应保留 tracker,而不是按新事务重置。执行失败时(该文件第 1168-1174 行)同样会restore_reserve_balance并把结果按Revert处理,确保异常路径下父上下文也保持一致。这从侧面印证了该修复不只覆盖三个 cheatcode 本身,还覆盖了所有会派生子执行体的周边路径。
七、验证:Anvil 集成测试中的 reserve-balance 预编译
Monad 相关行为有完整的集成测试覆盖,集中在 crates/anvil/tests/it/monad.rs。测试通过 HTTP provider 构造对 reserve-balance 预编译的调用(辅助函数reserve_balance_call(),第 2304 行起),并断言返回值:
- 未配置/未启用时返回空(如
monad_eight_config_filters_reserve_balance_precompile); - 启用后返回确定的 32 字节结果(如
assert_eq!(provider.call(reserve_balance_call()).await.unwrap(), Bytes::from(vec![0; 32]))); - 覆盖普通节点、嵌套节点、fork 节点等多种执行形态(文件内大量
.call(reserve_balance_call())断言)。
这些测试是本次同步修复的验收底线:无论账户状态如何被 cheatcode 改写或回滚,预编译读到的储备余额都必须与真实状态一致。
八、小结
本次monad-state-splice-tracker变更虽然只是一条 patch 级修复记录,但背后的工程模式值得借鉴:
- 统一刷新入口:所有直改账户的 cheatcode(
deal、cloneAccount、loadAllocs)统一以refresh_chain_journal收尾,由FoundryChain::refresh_journal按网络族决定是否需要重算派生状态; - trait 默认实现 + 特性门控:
FoundryJournal把 Monad 专有方法做成#[cfg(feature = "monad")]的具体默认实现,通用代码零感知,Monad 分支只在不影响主路径的前提下编译; - 派生状态与快照同生命周期:快照时连 tracker 一起保存,回滚时先还原再 rebase,保证"账户状态 ↔ 跟踪器"在任意回滚点都严格一致;
- 子执行体边界对称处理:嵌套 EVM 与隔离调用采用 capture / restore 对称结构,并用
preserve_reserve_balance区分"内部调用"与"新事务"两种语义。
理解这一机制,有助于你在使用 Foundry 的 Monad 网络支持(或为其贡献多网络 EVM 能力)时,准确把握 cheatcode 直改状态与派生会计结构之间的同步约束。相关源码入口:FoundryJournal定义见 crates/evm/core/src/env.rs,Monad 链上下文实现见 crates/evm/core/src/evm/monad.rs,cheatcode 实现见 crates/cheatcodes/src/evm.rs。
【免费下载链接】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),仅供参考