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