Skip to main content

foundry_cheatcodes/evm/
fork.rs

1use crate::{
2    Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, DatabaseExt, Result, Vm::*,
3    json::json_value_to_token,
4};
5use alloy_dyn_abi::DynSolValue;
6use alloy_evm::EvmEnv;
7use alloy_network::AnyNetwork;
8use alloy_primitives::{Address, B256, U256};
9use alloy_provider::Provider;
10use alloy_rpc_types::Filter;
11use alloy_sol_types::SolValue;
12use foundry_common::provider::ProviderBuilder;
13use foundry_evm_core::{
14    FoundryContextExt, backend::JournaledState, evm::FoundryEvmNetwork, fork::CreateFork,
15};
16use revm::context::ContextTr;
17
18impl Cheatcode for activeForkCall {
19    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
20        let Self {} = self;
21        ccx.ecx
22            .db()
23            .active_fork_id()
24            .map(|id| id.abi_encode())
25            .ok_or_else(|| fmt_err!("no active fork"))
26    }
27}
28
29impl Cheatcode for createFork_0Call {
30    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
31        let Self { urlOrAlias } = self;
32        create_fork(ccx, urlOrAlias, None)
33    }
34}
35
36impl Cheatcode for createFork_1Call {
37    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
38        let Self { urlOrAlias, blockNumber } = self;
39        create_fork(ccx, urlOrAlias, Some(blockNumber.saturating_to()))
40    }
41}
42
43impl Cheatcode for createFork_2Call {
44    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
45        let Self { urlOrAlias, txHash } = self;
46        create_fork_at_transaction(ccx, urlOrAlias, txHash)
47    }
48}
49
50impl Cheatcode for createSelectFork_0Call {
51    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
52        let Self { urlOrAlias } = self;
53        create_select_fork(ccx, urlOrAlias, None)
54    }
55}
56
57impl Cheatcode for createSelectFork_1Call {
58    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
59        let Self { urlOrAlias, blockNumber } = self;
60        create_select_fork(ccx, urlOrAlias, Some(blockNumber.saturating_to()))
61    }
62}
63
64impl Cheatcode for createSelectFork_2Call {
65    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
66        let Self { urlOrAlias, txHash } = self;
67        create_select_fork_at_transaction(ccx, urlOrAlias, txHash)
68    }
69}
70
71impl Cheatcode for rollFork_0Call {
72    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
73        let Self { blockNumber } = self;
74        persist_caller(ccx);
75        fork_env_op(ccx.ecx, |db, evm_env, _, inner| {
76            db.roll_fork(None, (*blockNumber).to(), evm_env, inner)
77        })
78    }
79}
80
81impl Cheatcode for rollFork_1Call {
82    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
83        let Self { txHash } = self;
84        persist_caller(ccx);
85        fork_env_op(ccx.ecx, |db, evm_env, _, inner| {
86            db.roll_fork_to_transaction(None, *txHash, evm_env, inner)
87        })
88    }
89}
90
91impl Cheatcode for rollFork_2Call {
92    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
93        let Self { forkId, blockNumber } = self;
94        persist_caller(ccx);
95        fork_env_op(ccx.ecx, |db, evm_env, _, inner| {
96            db.roll_fork(Some(*forkId), (*blockNumber).to(), evm_env, inner)
97        })
98    }
99}
100
101impl Cheatcode for rollFork_3Call {
102    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
103        let Self { forkId, txHash } = self;
104        persist_caller(ccx);
105        fork_env_op(ccx.ecx, |db, evm_env, _, inner| {
106            db.roll_fork_to_transaction(Some(*forkId), *txHash, evm_env, inner)
107        })
108    }
109}
110
111impl Cheatcode for selectForkCall {
112    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
113        let Self { forkId } = self;
114        persist_caller(ccx);
115        check_broadcast(ccx.state)?;
116        fork_env_op(ccx.ecx, |db, evm_env, tx_env, inner| {
117            db.select_fork(*forkId, evm_env, tx_env, inner)
118        })
119    }
120}
121
122impl Cheatcode for transact_0Call {
123    fn apply_full<FEN: FoundryEvmNetwork>(
124        &self,
125        ccx: &mut CheatsCtxt<'_, '_, FEN>,
126        executor: &mut dyn CheatcodesExecutor<FEN>,
127    ) -> Result {
128        let Self { txHash } = *self;
129        transact(ccx, executor, txHash, None)
130    }
131}
132
133impl Cheatcode for transact_1Call {
134    fn apply_full<FEN: FoundryEvmNetwork>(
135        &self,
136        ccx: &mut CheatsCtxt<'_, '_, FEN>,
137        executor: &mut dyn CheatcodesExecutor<FEN>,
138    ) -> Result {
139        let Self { forkId, txHash } = *self;
140        transact(ccx, executor, txHash, Some(forkId))
141    }
142}
143
144impl Cheatcode for allowCheatcodesCall {
145    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
146        let Self { account } = self;
147        ccx.ecx.db_mut().allow_cheatcode_access(*account);
148        Ok(Default::default())
149    }
150}
151
152impl Cheatcode for makePersistent_0Call {
153    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
154        let Self { account } = self;
155        ccx.ecx.db_mut().add_persistent_account(*account);
156        Ok(Default::default())
157    }
158}
159
160impl Cheatcode for makePersistent_1Call {
161    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
162        let Self { account0, account1 } = self;
163        ccx.ecx.db_mut().add_persistent_account(*account0);
164        ccx.ecx.db_mut().add_persistent_account(*account1);
165        Ok(Default::default())
166    }
167}
168
169impl Cheatcode for makePersistent_2Call {
170    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
171        let Self { account0, account1, account2 } = self;
172        ccx.ecx.db_mut().add_persistent_account(*account0);
173        ccx.ecx.db_mut().add_persistent_account(*account1);
174        ccx.ecx.db_mut().add_persistent_account(*account2);
175        Ok(Default::default())
176    }
177}
178
179impl Cheatcode for makePersistent_3Call {
180    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
181        let Self { accounts } = self;
182        for account in accounts {
183            ccx.ecx.db_mut().add_persistent_account(*account);
184        }
185        Ok(Default::default())
186    }
187}
188
189impl Cheatcode for revokePersistent_0Call {
190    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
191        let Self { account } = self;
192        ccx.ecx.db_mut().remove_persistent_account(account);
193        Ok(Default::default())
194    }
195}
196
197impl Cheatcode for revokePersistent_1Call {
198    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
199        let Self { accounts } = self;
200        for account in accounts {
201            ccx.ecx.db_mut().remove_persistent_account(account);
202        }
203        Ok(Default::default())
204    }
205}
206
207impl Cheatcode for isPersistentCall {
208    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
209        let Self { account } = self;
210        Ok(ccx.ecx.db().is_persistent(account).abi_encode())
211    }
212}
213
214impl Cheatcode for rpc_0Call {
215    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
216        let Self { method, params } = self;
217        let url =
218            ccx.ecx.db().active_fork_url().ok_or_else(|| fmt_err!("no active fork URL found"))?;
219        let result = rpc_call(&url, method, params)?;
220        invalidate_active_fork_cache(ccx, method, params);
221        Ok(result)
222    }
223}
224
225impl Cheatcode for rpc_1Call {
226    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
227        let Self { urlOrAlias, method, params } = self;
228        let url = state.config.rpc_endpoint(urlOrAlias)?.url()?;
229        rpc_call(&url, method, params)
230    }
231}
232
233impl Cheatcode for rpcJson_0Call {
234    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
235        let Self { method, params } = self;
236        let url =
237            ccx.ecx.db().active_fork_url().ok_or_else(|| fmt_err!("no active fork URL found"))?;
238        let result = rpc_json_call(&url, method, params)?;
239        invalidate_active_fork_cache(ccx, method, params);
240        Ok(result)
241    }
242}
243
244impl Cheatcode for rpcJson_1Call {
245    fn apply<FEN: FoundryEvmNetwork>(&self, state: &mut Cheatcodes<FEN>) -> Result {
246        let Self { urlOrAlias, method, params } = self;
247        let url = state.config.rpc_endpoint(urlOrAlias)?.url()?;
248        rpc_json_call(&url, method, params)
249    }
250}
251
252impl Cheatcode for eth_getLogsCall {
253    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
254        let Self { fromBlock, toBlock, target, topics } = self;
255        let (Ok(from_block), Ok(to_block)) = (u64::try_from(fromBlock), u64::try_from(toBlock))
256        else {
257            bail!("blocks in block range must be less than 2^64")
258        };
259
260        if topics.len() > 4 {
261            bail!("topics array must contain at most 4 elements")
262        }
263
264        let url =
265            ccx.ecx.db().active_fork_url().ok_or_else(|| fmt_err!("no active fork URL found"))?;
266        let provider = ProviderBuilder::<AnyNetwork>::new(&url).build()?;
267        let mut filter = Filter::new().address(*target).from_block(from_block).to_block(to_block);
268        for (i, &topic) in topics.iter().enumerate() {
269            filter.topics[i] = topic.into();
270        }
271
272        let logs = foundry_common::block_on(provider.get_logs(&filter))
273            .map_err(|e| fmt_err!("failed to get logs: {e}"))?;
274
275        let eth_logs = logs
276            .into_iter()
277            .map(|log| EthGetLogs {
278                emitter: log.address(),
279                topics: log.topics().to_vec(),
280                data: log.inner.data.data,
281                blockHash: log.block_hash.unwrap_or_default(),
282                blockNumber: log.block_number.unwrap_or_default(),
283                transactionHash: log.transaction_hash.unwrap_or_default(),
284                transactionIndex: log.transaction_index.unwrap_or_default(),
285                logIndex: U256::from(log.log_index.unwrap_or_default()),
286                removed: log.removed,
287            })
288            .collect::<Vec<_>>();
289
290        Ok(eth_logs.abi_encode())
291    }
292}
293
294impl Cheatcode for getRawBlockHeaderCall {
295    fn apply_stateful<FEN: FoundryEvmNetwork>(&self, ccx: &mut CheatsCtxt<'_, '_, FEN>) -> Result {
296        let Self { blockNumber } = self;
297        let url = ccx.ecx.db().active_fork_url().ok_or_else(|| fmt_err!("no active fork"))?;
298        let provider = ProviderBuilder::<AnyNetwork>::new(&url).build()?;
299        let block_number = u64::try_from(blockNumber)
300            .map_err(|_| fmt_err!("block number must be less than 2^64"))?;
301        let block =
302            foundry_common::block_on(async move { provider.get_block(block_number.into()).await })
303                .map_err(|e| fmt_err!("failed to get block: {e}"))?
304                .ok_or_else(|| fmt_err!("block {block_number} not found"))?;
305
306        let header: alloy_consensus::Header = block
307            .into_inner()
308            .header
309            .inner
310            .try_into_header()
311            .map_err(|e| fmt_err!("failed to convert to header: {e}"))?;
312        Ok(alloy_rlp::encode(&header).abi_encode())
313    }
314}
315
316/// Creates and then also selects the new fork
317fn create_select_fork<FEN: FoundryEvmNetwork>(
318    ccx: &mut CheatsCtxt<'_, '_, FEN>,
319    url_or_alias: &str,
320    block: Option<u64>,
321) -> Result {
322    check_broadcast(ccx.state)?;
323
324    let fork = create_fork_request(ccx, url_or_alias, block)?;
325    fork_env_op(ccx.ecx, |db, evm_env, tx_env, inner| {
326        db.create_select_fork(fork, evm_env, tx_env, inner)
327    })
328}
329
330/// Creates a new fork
331fn create_fork<FEN: FoundryEvmNetwork>(
332    ccx: &mut CheatsCtxt<'_, '_, FEN>,
333    url_or_alias: &str,
334    block: Option<u64>,
335) -> Result {
336    let fork = create_fork_request(ccx, url_or_alias, block)?;
337    let id = ccx.ecx.db_mut().create_fork(fork)?;
338    Ok(id.abi_encode())
339}
340
341/// Creates and then also selects the new fork at the given transaction
342fn create_select_fork_at_transaction<FEN: FoundryEvmNetwork>(
343    ccx: &mut CheatsCtxt<'_, '_, FEN>,
344    url_or_alias: &str,
345    transaction: &B256,
346) -> Result {
347    check_broadcast(ccx.state)?;
348
349    let fork = create_fork_request(ccx, url_or_alias, None)?;
350    fork_env_op(ccx.ecx, |db, evm_env, tx_env, inner| {
351        db.create_select_fork_at_transaction(fork, evm_env, tx_env, inner, *transaction)
352    })
353}
354
355/// Creates a new fork at the given transaction
356fn create_fork_at_transaction<FEN: FoundryEvmNetwork>(
357    ccx: &mut CheatsCtxt<'_, '_, FEN>,
358    url_or_alias: &str,
359    transaction: &B256,
360) -> Result {
361    let fork = create_fork_request(ccx, url_or_alias, None)?;
362    let id = ccx.ecx.db_mut().create_fork_at_transaction(fork, *transaction)?;
363    Ok(id.abi_encode())
364}
365
366/// Creates the request object for a new fork request
367fn create_fork_request<FEN: FoundryEvmNetwork>(
368    ccx: &mut CheatsCtxt<'_, '_, FEN>,
369    url_or_alias: &str,
370    block: Option<u64>,
371) -> Result<CreateFork> {
372    persist_caller(ccx);
373
374    let rpc_endpoint = ccx.state.config.rpc_endpoint(url_or_alias)?;
375    let url = rpc_endpoint.url()?;
376    let mut evm_opts = ccx.state.config.evm_opts.clone();
377    evm_opts.fork_block_number = block;
378    evm_opts.fork_retries = rpc_endpoint.config.retries;
379    evm_opts.fork_retry_backoff = rpc_endpoint.config.retry_backoff;
380    if let Some(Ok(auth)) = rpc_endpoint.auth {
381        evm_opts.fork_headers = Some(vec![format!("Authorization: {auth}")]);
382    }
383    let fork = CreateFork {
384        enable_caching: !ccx.state.config.no_storage_caching
385            && ccx.state.config.rpc_storage_caching.enable_for_endpoint(&url),
386        url,
387        evm_opts,
388    };
389    Ok(fork)
390}
391
392/// Clones the EVM and tx environments, runs a fork operation that may modify them, then writes
393/// them back. This is the common pattern for all fork-switching cheatcodes (rollFork, selectFork,
394/// createSelectFork).
395fn fork_env_op<CTX: FoundryContextExt, T: SolValue>(
396    ecx: &mut CTX,
397    f: impl FnOnce(
398        &mut CTX::Db,
399        &mut EvmEnv<CTX::Spec, CTX::Block>,
400        &mut CTX::Tx,
401        &mut JournaledState,
402    ) -> eyre::Result<T>,
403) -> Result {
404    let mut evm_env = ecx.evm_clone();
405    let mut tx_env = ecx.tx_clone();
406    let (db, inner) = ecx.db_journal_inner_mut();
407    let result = f(db, &mut evm_env, &mut tx_env, inner)?;
408    ecx.set_evm(evm_env);
409    ecx.set_tx(tx_env);
410    Ok(result.abi_encode())
411}
412
413fn check_broadcast<FEN: FoundryEvmNetwork>(state: &Cheatcodes<FEN>) -> Result<()> {
414    if state.broadcast.is_none() {
415        Ok(())
416    } else {
417        Err(fmt_err!("cannot select forks during a broadcast"))
418    }
419}
420
421fn transact<FEN: FoundryEvmNetwork>(
422    ccx: &mut CheatsCtxt<'_, '_, FEN>,
423    executor: &mut dyn CheatcodesExecutor<FEN>,
424    transaction: B256,
425    fork_id: Option<U256>,
426) -> Result {
427    executor.transact_on_db(ccx.state, ccx.ecx, fork_id, transaction)?;
428    Ok(Default::default())
429}
430
431// Helper to add the caller of fork cheat code as persistent account (in order to make sure that the
432// state of caller contract is not lost when fork changes).
433// Applies to create, select and roll forks actions.
434// https://github.com/foundry-rs/foundry/issues/8004
435fn persist_caller<FEN: FoundryEvmNetwork>(ccx: &mut CheatsCtxt<'_, '_, FEN>) {
436    ccx.ecx.db_mut().add_persistent_account(ccx.caller);
437}
438
439/// Performs an Ethereum JSON-RPC request to the given endpoint.
440fn rpc_call(url: &str, method: &str, params: &str) -> Result {
441    let result = rpc_result(url, method, params)?;
442    let result_as_tokens = convert_to_bytes(
443        &json_value_to_token(&result, None)
444            .map_err(|err| fmt_err!("failed to parse result: {err}"))?,
445    );
446
447    let payload = match &result_as_tokens {
448        DynSolValue::Bytes(b) => b.clone(),
449        _ => result_as_tokens.abi_encode(),
450    };
451    Ok(DynSolValue::Bytes(payload).abi_encode())
452}
453
454/// Performs an Ethereum JSON-RPC request to the given endpoint and returns the JSON result.
455fn rpc_json_call(url: &str, method: &str, params: &str) -> Result {
456    let result = rpc_result(url, method, params)?;
457    Ok(serde_json::to_string(&result)?.abi_encode())
458}
459
460/// Invalidates the fork DB cache after a `vm.rpc` call on the active fork that mutates the node
461/// directly (bypassing the cache), so later DB reads (e.g. the broadcast simulation runner)
462/// re-fetch the new value. Only well-known Anvil/Hardhat account/storage setters are handled;
463/// chain-advancing methods (e.g. `eth_sendTransaction`) need re-forking, not eviction.
464fn invalidate_active_fork_cache<FEN: FoundryEvmNetwork>(
465    ccx: &mut CheatsCtxt<'_, '_, FEN>,
466    method: &str,
467    params: &str,
468) {
469    const ACCOUNT_SETTERS: &[&str] = &[
470        "anvil_setBalance",
471        "hardhat_setBalance",
472        "tenderly_setBalance",
473        "anvil_addBalance",
474        "hardhat_addBalance",
475        "tenderly_addBalance",
476        "anvil_setNonce",
477        "hardhat_setNonce",
478        "evm_setAccountNonce",
479        "anvil_setCode",
480        "hardhat_setCode",
481    ];
482    let is_storage_setter = matches!(method, "anvil_setStorageAt" | "hardhat_setStorageAt");
483    if !is_storage_setter && !ACCOUNT_SETTERS.contains(&method) {
484        return;
485    }
486
487    let Ok(params) = serde_json::from_str::<serde_json::Value>(params) else { return };
488    let Some(address) =
489        params.get(0).and_then(|v| v.as_str()).and_then(|s| s.parse::<Address>().ok())
490    else {
491        return;
492    };
493
494    if is_storage_setter {
495        let Some(slot) =
496            params.get(1).and_then(|v| v.as_str()).and_then(|s| s.parse::<U256>().ok())
497        else {
498            return;
499        };
500        ccx.ecx.db_mut().invalidate_fork_cache_storage(address, slot);
501    } else {
502        ccx.ecx.db_mut().invalidate_fork_cache_account(address);
503    }
504}
505
506fn rpc_result(url: &str, method: &str, params: &str) -> Result<serde_json::Value> {
507    let provider = ProviderBuilder::<AnyNetwork>::new(url).build()?;
508    let params_json: serde_json::Value = serde_json::from_str(params)?;
509    foundry_common::block_on(provider.raw_request(method.to_string().into(), params_json))
510        .map_err(|err| fmt_err!("{method:?}: {err}"))
511}
512
513/// Convert fixed bytes and address values to bytes in order to prevent encoding issues.
514fn convert_to_bytes(token: &DynSolValue) -> DynSolValue {
515    match token {
516        // Convert fixed bytes to prevent encoding issues.
517        // See: <https://github.com/foundry-rs/foundry/issues/8287>
518        DynSolValue::FixedBytes(bytes, size) => {
519            DynSolValue::Bytes(bytes.as_slice()[..*size].to_vec())
520        }
521        DynSolValue::Address(addr) => DynSolValue::Bytes(addr.to_vec()),
522        val => val.clone(),
523    }
524}