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_primitives::{B256, U256};
7use alloy_provider::Provider;
8use alloy_rpc_types::Filter;
9use alloy_sol_types::SolValue;
10use foundry_common::provider::ProviderBuilder;
11use foundry_evm_core::{AsEnvMut, ContextExt, fork::CreateFork};
12
13impl Cheatcode for activeForkCall {
14    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
15        let Self {} = self;
16        ccx.ecx
17            .journaled_state
18            .database
19            .active_fork_id()
20            .map(|id| id.abi_encode())
21            .ok_or_else(|| fmt_err!("no active fork"))
22    }
23}
24
25impl Cheatcode for createFork_0Call {
26    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
27        let Self { urlOrAlias } = self;
28        create_fork(ccx, urlOrAlias, None)
29    }
30}
31
32impl Cheatcode for createFork_1Call {
33    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
34        let Self { urlOrAlias, blockNumber } = self;
35        create_fork(ccx, urlOrAlias, Some(blockNumber.saturating_to()))
36    }
37}
38
39impl Cheatcode for createFork_2Call {
40    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
41        let Self { urlOrAlias, txHash } = self;
42        create_fork_at_transaction(ccx, urlOrAlias, txHash)
43    }
44}
45
46impl Cheatcode for createSelectFork_0Call {
47    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
48        let Self { urlOrAlias } = self;
49        create_select_fork(ccx, urlOrAlias, None)
50    }
51}
52
53impl Cheatcode for createSelectFork_1Call {
54    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
55        let Self { urlOrAlias, blockNumber } = self;
56        create_select_fork(ccx, urlOrAlias, Some(blockNumber.saturating_to()))
57    }
58}
59
60impl Cheatcode for createSelectFork_2Call {
61    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
62        let Self { urlOrAlias, txHash } = self;
63        create_select_fork_at_transaction(ccx, urlOrAlias, txHash)
64    }
65}
66
67impl Cheatcode for rollFork_0Call {
68    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
69        let Self { blockNumber } = self;
70        persist_caller(ccx);
71        let (db, journal, mut env) = ccx.ecx.as_db_env_and_journal();
72        db.roll_fork(None, (*blockNumber).to(), &mut env, journal)?;
73        Ok(Default::default())
74    }
75}
76
77impl Cheatcode for rollFork_1Call {
78    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
79        let Self { txHash } = self;
80        persist_caller(ccx);
81        let (db, journal, mut env) = ccx.ecx.as_db_env_and_journal();
82        db.roll_fork_to_transaction(None, *txHash, &mut env, journal)?;
83        Ok(Default::default())
84    }
85}
86
87impl Cheatcode for rollFork_2Call {
88    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
89        let Self { forkId, blockNumber } = self;
90        persist_caller(ccx);
91        let (db, journal, mut env) = ccx.ecx.as_db_env_and_journal();
92        db.roll_fork(Some(*forkId), (*blockNumber).to(), &mut env, journal)?;
93        Ok(Default::default())
94    }
95}
96
97impl Cheatcode for rollFork_3Call {
98    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
99        let Self { forkId, txHash } = self;
100        persist_caller(ccx);
101        let (db, journal, mut env) = ccx.ecx.as_db_env_and_journal();
102        db.roll_fork_to_transaction(Some(*forkId), *txHash, &mut env, journal)?;
103        Ok(Default::default())
104    }
105}
106
107impl Cheatcode for selectForkCall {
108    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
109        let Self { forkId } = self;
110        persist_caller(ccx);
111        check_broadcast(ccx.state)?;
112        let (db, journal, mut env) = ccx.ecx.as_db_env_and_journal();
113        db.select_fork(*forkId, &mut env, journal)?;
114        Ok(Default::default())
115    }
116}
117
118impl Cheatcode for transact_0Call {
119    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
120        let Self { txHash } = *self;
121        transact(ccx, executor, txHash, None)
122    }
123}
124
125impl Cheatcode for transact_1Call {
126    fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result {
127        let Self { forkId, txHash } = *self;
128        transact(ccx, executor, txHash, Some(forkId))
129    }
130}
131
132impl Cheatcode for allowCheatcodesCall {
133    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
134        let Self { account } = self;
135        ccx.ecx.journaled_state.database.allow_cheatcode_access(*account);
136        Ok(Default::default())
137    }
138}
139
140impl Cheatcode for makePersistent_0Call {
141    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
142        let Self { account } = self;
143        ccx.ecx.journaled_state.database.add_persistent_account(*account);
144        Ok(Default::default())
145    }
146}
147
148impl Cheatcode for makePersistent_1Call {
149    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
150        let Self { account0, account1 } = self;
151        ccx.ecx.journaled_state.database.add_persistent_account(*account0);
152        ccx.ecx.journaled_state.database.add_persistent_account(*account1);
153        Ok(Default::default())
154    }
155}
156
157impl Cheatcode for makePersistent_2Call {
158    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
159        let Self { account0, account1, account2 } = self;
160        ccx.ecx.journaled_state.database.add_persistent_account(*account0);
161        ccx.ecx.journaled_state.database.add_persistent_account(*account1);
162        ccx.ecx.journaled_state.database.add_persistent_account(*account2);
163        Ok(Default::default())
164    }
165}
166
167impl Cheatcode for makePersistent_3Call {
168    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
169        let Self { accounts } = self;
170        for account in accounts {
171            ccx.ecx.journaled_state.database.add_persistent_account(*account);
172        }
173        Ok(Default::default())
174    }
175}
176
177impl Cheatcode for revokePersistent_0Call {
178    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
179        let Self { account } = self;
180        ccx.ecx.journaled_state.database.remove_persistent_account(account);
181        Ok(Default::default())
182    }
183}
184
185impl Cheatcode for revokePersistent_1Call {
186    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
187        let Self { accounts } = self;
188        for account in accounts {
189            ccx.ecx.journaled_state.database.remove_persistent_account(account);
190        }
191        Ok(Default::default())
192    }
193}
194
195impl Cheatcode for isPersistentCall {
196    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
197        let Self { account } = self;
198        Ok(ccx.ecx.journaled_state.database.is_persistent(account).abi_encode())
199    }
200}
201
202impl Cheatcode for rpc_0Call {
203    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
204        let Self { method, params } = self;
205        let url = ccx
206            .ecx
207            .journaled_state
208            .database
209            .active_fork_url()
210            .ok_or_else(|| fmt_err!("no active fork URL found"))?;
211        rpc_call(&url, method, params)
212    }
213}
214
215impl Cheatcode for rpc_1Call {
216    fn apply(&self, state: &mut Cheatcodes) -> Result {
217        let Self { urlOrAlias, method, params } = self;
218        let url = state.config.rpc_endpoint(urlOrAlias)?.url()?;
219        rpc_call(&url, method, params)
220    }
221}
222
223impl Cheatcode for eth_getLogsCall {
224    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
225        let Self { fromBlock, toBlock, target, topics } = self;
226        let (Ok(from_block), Ok(to_block)) = (u64::try_from(fromBlock), u64::try_from(toBlock))
227        else {
228            bail!("blocks in block range must be less than 2^64")
229        };
230
231        if topics.len() > 4 {
232            bail!("topics array must contain at most 4 elements")
233        }
234
235        let url = ccx
236            .ecx
237            .journaled_state
238            .database
239            .active_fork_url()
240            .ok_or_else(|| fmt_err!("no active fork URL found"))?;
241        let provider = ProviderBuilder::new(&url).build()?;
242        let mut filter = Filter::new().address(*target).from_block(from_block).to_block(to_block);
243        for (i, &topic) in topics.iter().enumerate() {
244            filter.topics[i] = topic.into();
245        }
246
247        let logs = foundry_common::block_on(provider.get_logs(&filter))
248            .map_err(|e| fmt_err!("failed to get logs: {e}"))?;
249
250        let eth_logs = logs
251            .into_iter()
252            .map(|log| EthGetLogs {
253                emitter: log.address(),
254                topics: log.topics().to_vec(),
255                data: log.inner.data.data,
256                blockHash: log.block_hash.unwrap_or_default(),
257                blockNumber: log.block_number.unwrap_or_default(),
258                transactionHash: log.transaction_hash.unwrap_or_default(),
259                transactionIndex: log.transaction_index.unwrap_or_default(),
260                logIndex: U256::from(log.log_index.unwrap_or_default()),
261                removed: log.removed,
262            })
263            .collect::<Vec<_>>();
264
265        Ok(eth_logs.abi_encode())
266    }
267}
268
269impl Cheatcode for getRawBlockHeaderCall {
270    fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result {
271        let Self { blockNumber } = self;
272        let url = ccx
273            .ecx
274            .journaled_state
275            .database
276            .active_fork_url()
277            .ok_or_else(|| fmt_err!("no active fork"))?;
278        let provider = ProviderBuilder::new(&url).build()?;
279        let block_number = u64::try_from(blockNumber)
280            .map_err(|_| fmt_err!("block number must be less than 2^64"))?;
281        let block =
282            foundry_common::block_on(async move { provider.get_block(block_number.into()).await })
283                .map_err(|e| fmt_err!("failed to get block: {e}"))?
284                .ok_or_else(|| fmt_err!("block {block_number} not found"))?;
285
286        let header: alloy_consensus::Header = block
287            .into_inner()
288            .header
289            .inner
290            .try_into_header()
291            .map_err(|e| fmt_err!("failed to convert to header: {e}"))?;
292        Ok(alloy_rlp::encode(&header).abi_encode())
293    }
294}
295
296/// Creates and then also selects the new fork
297fn create_select_fork(ccx: &mut CheatsCtxt, url_or_alias: &str, block: Option<u64>) -> Result {
298    check_broadcast(ccx.state)?;
299
300    let fork = create_fork_request(ccx, url_or_alias, block)?;
301    let (db, journal, mut env) = ccx.ecx.as_db_env_and_journal();
302    let id = db.create_select_fork(fork, &mut env, journal)?;
303    Ok(id.abi_encode())
304}
305
306/// Creates a new fork
307fn create_fork(ccx: &mut CheatsCtxt, url_or_alias: &str, block: Option<u64>) -> Result {
308    let fork = create_fork_request(ccx, url_or_alias, block)?;
309    let id = ccx.ecx.journaled_state.database.create_fork(fork)?;
310    Ok(id.abi_encode())
311}
312
313/// Creates and then also selects the new fork at the given transaction
314fn create_select_fork_at_transaction(
315    ccx: &mut CheatsCtxt,
316    url_or_alias: &str,
317    transaction: &B256,
318) -> Result {
319    check_broadcast(ccx.state)?;
320
321    let fork = create_fork_request(ccx, url_or_alias, None)?;
322    let (db, journal, mut env) = ccx.ecx.as_db_env_and_journal();
323    let id = db.create_select_fork_at_transaction(fork, &mut env, journal, *transaction)?;
324    Ok(id.abi_encode())
325}
326
327/// Creates a new fork at the given transaction
328fn create_fork_at_transaction(
329    ccx: &mut CheatsCtxt,
330    url_or_alias: &str,
331    transaction: &B256,
332) -> Result {
333    let fork = create_fork_request(ccx, url_or_alias, None)?;
334    let id = ccx.ecx.journaled_state.database.create_fork_at_transaction(fork, *transaction)?;
335    Ok(id.abi_encode())
336}
337
338/// Creates the request object for a new fork request
339fn create_fork_request(
340    ccx: &mut CheatsCtxt,
341    url_or_alias: &str,
342    block: Option<u64>,
343) -> Result<CreateFork> {
344    persist_caller(ccx);
345
346    let rpc_endpoint = ccx.state.config.rpc_endpoint(url_or_alias)?;
347    let url = rpc_endpoint.url()?;
348    let mut evm_opts = ccx.state.config.evm_opts.clone();
349    evm_opts.fork_block_number = block;
350    evm_opts.fork_retries = rpc_endpoint.config.retries;
351    evm_opts.fork_retry_backoff = rpc_endpoint.config.retry_backoff;
352    if let Some(Ok(auth)) = rpc_endpoint.auth {
353        evm_opts.fork_headers = Some(vec![format!("Authorization: {auth}")]);
354    }
355    let fork = CreateFork {
356        enable_caching: !ccx.state.config.no_storage_caching
357            && ccx.state.config.rpc_storage_caching.enable_for_endpoint(&url),
358        url,
359        env: ccx.ecx.as_env_mut().to_owned(),
360        evm_opts,
361    };
362    Ok(fork)
363}
364
365fn check_broadcast(state: &Cheatcodes) -> Result<()> {
366    if state.broadcast.is_none() {
367        Ok(())
368    } else {
369        Err(fmt_err!("cannot select forks during a broadcast"))
370    }
371}
372
373fn transact(
374    ccx: &mut CheatsCtxt,
375    executor: &mut dyn CheatcodesExecutor,
376    transaction: B256,
377    fork_id: Option<U256>,
378) -> Result {
379    let (db, journal, env) = ccx.ecx.as_db_env_and_journal();
380    db.transact(
381        fork_id,
382        transaction,
383        env.to_owned(),
384        journal,
385        &mut *executor.get_inspector(ccx.state),
386    )?;
387    Ok(Default::default())
388}
389
390// Helper to add the caller of fork cheat code as persistent account (in order to make sure that the
391// state of caller contract is not lost when fork changes).
392// Applies to create, select and roll forks actions.
393// https://github.com/foundry-rs/foundry/issues/8004
394fn persist_caller(ccx: &mut CheatsCtxt) {
395    ccx.ecx.journaled_state.database.add_persistent_account(ccx.caller);
396}
397
398/// Performs an Ethereum JSON-RPC request to the given endpoint.
399fn rpc_call(url: &str, method: &str, params: &str) -> Result {
400    let provider = ProviderBuilder::new(url).build()?;
401    let params_json: serde_json::Value = serde_json::from_str(params)?;
402    let result =
403        foundry_common::block_on(provider.raw_request(method.to_string().into(), params_json))
404            .map_err(|err| fmt_err!("{method:?}: {err}"))?;
405    let result_as_tokens = convert_to_bytes(
406        &json_value_to_token(&result).map_err(|err| fmt_err!("failed to parse result: {err}"))?,
407    );
408
409    Ok(result_as_tokens.abi_encode())
410}
411
412/// Convert fixed bytes and address values to bytes in order to prevent encoding issues.
413fn convert_to_bytes(token: &DynSolValue) -> DynSolValue {
414    match token {
415        // Convert fixed bytes to prevent encoding issues.
416        // See: <https://github.com/foundry-rs/foundry/issues/8287>
417        DynSolValue::FixedBytes(bytes, size) => {
418            DynSolValue::Bytes(bytes.as_slice()[..*size].to_vec())
419        }
420        DynSolValue::Address(addr) => DynSolValue::Bytes(addr.to_vec()),
421        //  Convert tuple values to prevent encoding issues.
422        // See: <https://github.com/foundry-rs/foundry/issues/7858>
423        DynSolValue::Tuple(vals) => DynSolValue::Tuple(vals.iter().map(convert_to_bytes).collect()),
424        val => val.clone(),
425    }
426}