foundry_cheatcodes/evm/
fork.rs

1use crate::{
2    json::json_value_to_token, Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, DatabaseExt,
3    Result, Vm::*,
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::{fork::CreateFork, AsEnvMut, ContextExt};
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 - 1")
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
269/// Creates and then also selects the new fork
270fn create_select_fork(ccx: &mut CheatsCtxt, url_or_alias: &str, block: Option<u64>) -> Result {
271    check_broadcast(ccx.state)?;
272
273    let fork = create_fork_request(ccx, url_or_alias, block)?;
274    let (db, journal, mut env) = ccx.ecx.as_db_env_and_journal();
275    let id = db.create_select_fork(fork, &mut env, journal)?;
276    Ok(id.abi_encode())
277}
278
279/// Creates a new fork
280fn create_fork(ccx: &mut CheatsCtxt, url_or_alias: &str, block: Option<u64>) -> Result {
281    let fork = create_fork_request(ccx, url_or_alias, block)?;
282    let id = ccx.ecx.journaled_state.database.create_fork(fork)?;
283    Ok(id.abi_encode())
284}
285
286/// Creates and then also selects the new fork at the given transaction
287fn create_select_fork_at_transaction(
288    ccx: &mut CheatsCtxt,
289    url_or_alias: &str,
290    transaction: &B256,
291) -> Result {
292    check_broadcast(ccx.state)?;
293
294    let fork = create_fork_request(ccx, url_or_alias, None)?;
295    let (db, journal, mut env) = ccx.ecx.as_db_env_and_journal();
296    let id = db.create_select_fork_at_transaction(fork, &mut env, journal, *transaction)?;
297    Ok(id.abi_encode())
298}
299
300/// Creates a new fork at the given transaction
301fn create_fork_at_transaction(
302    ccx: &mut CheatsCtxt,
303    url_or_alias: &str,
304    transaction: &B256,
305) -> Result {
306    let fork = create_fork_request(ccx, url_or_alias, None)?;
307    let id = ccx.ecx.journaled_state.database.create_fork_at_transaction(fork, *transaction)?;
308    Ok(id.abi_encode())
309}
310
311/// Creates the request object for a new fork request
312fn create_fork_request(
313    ccx: &mut CheatsCtxt,
314    url_or_alias: &str,
315    block: Option<u64>,
316) -> Result<CreateFork> {
317    persist_caller(ccx);
318
319    let rpc_endpoint = ccx.state.config.rpc_endpoint(url_or_alias)?;
320    let url = rpc_endpoint.url()?;
321    let mut evm_opts = ccx.state.config.evm_opts.clone();
322    evm_opts.fork_block_number = block;
323    evm_opts.fork_retries = rpc_endpoint.config.retries;
324    evm_opts.fork_retry_backoff = rpc_endpoint.config.retry_backoff;
325    if let Some(Ok(auth)) = rpc_endpoint.auth {
326        evm_opts.fork_headers = Some(vec![format!("Authorization: {auth}")]);
327    }
328    let fork = CreateFork {
329        enable_caching: !ccx.state.config.no_storage_caching &&
330            ccx.state.config.rpc_storage_caching.enable_for_endpoint(&url),
331        url,
332        env: ccx.ecx.as_env_mut().to_owned(),
333        evm_opts,
334    };
335    Ok(fork)
336}
337
338fn check_broadcast(state: &Cheatcodes) -> Result<()> {
339    if state.broadcast.is_none() {
340        Ok(())
341    } else {
342        Err(fmt_err!("cannot select forks during a broadcast"))
343    }
344}
345
346fn transact(
347    ccx: &mut CheatsCtxt,
348    executor: &mut dyn CheatcodesExecutor,
349    transaction: B256,
350    fork_id: Option<U256>,
351) -> Result {
352    let (db, journal, env) = ccx.ecx.as_db_env_and_journal();
353    db.transact(
354        fork_id,
355        transaction,
356        env.to_owned(),
357        journal,
358        &mut *executor.get_inspector(ccx.state),
359    )?;
360    Ok(Default::default())
361}
362
363// Helper to add the caller of fork cheat code as persistent account (in order to make sure that the
364// state of caller contract is not lost when fork changes).
365// Applies to create, select and roll forks actions.
366// https://github.com/foundry-rs/foundry/issues/8004
367fn persist_caller(ccx: &mut CheatsCtxt) {
368    ccx.ecx.journaled_state.database.add_persistent_account(ccx.caller);
369}
370
371/// Performs an Ethereum JSON-RPC request to the given endpoint.
372fn rpc_call(url: &str, method: &str, params: &str) -> Result {
373    let provider = ProviderBuilder::new(url).build()?;
374    let params_json: serde_json::Value = serde_json::from_str(params)?;
375    let result =
376        foundry_common::block_on(provider.raw_request(method.to_string().into(), params_json))
377            .map_err(|err| fmt_err!("{method:?}: {err}"))?;
378    let result_as_tokens = convert_to_bytes(
379        &json_value_to_token(&result).map_err(|err| fmt_err!("failed to parse result: {err}"))?,
380    );
381
382    Ok(result_as_tokens.abi_encode())
383}
384
385/// Convert fixed bytes and address values to bytes in order to prevent encoding issues.
386fn convert_to_bytes(token: &DynSolValue) -> DynSolValue {
387    match token {
388        // Convert fixed bytes to prevent encoding issues.
389        // See: <https://github.com/foundry-rs/foundry/issues/8287>
390        DynSolValue::FixedBytes(bytes, size) => {
391            DynSolValue::Bytes(bytes.as_slice()[..*size].to_vec())
392        }
393        DynSolValue::Address(addr) => DynSolValue::Bytes(addr.to_vec()),
394        //  Convert tuple values to prevent encoding issues.
395        // See: <https://github.com/foundry-rs/foundry/issues/7858>
396        DynSolValue::Tuple(vals) => DynSolValue::Tuple(vals.iter().map(convert_to_bytes).collect()),
397        val => val.clone(),
398    }
399}