Skip to main content

foundry_cli/opts/
evm.rs

1//! CLI arguments for configuring the EVM settings.
2
3use crate::opts::RpcCommonOpts;
4use alloy_primitives::{Address, B256, U256};
5use clap::Parser;
6use foundry_common::shell;
7use foundry_config::{
8    Chain, Config, FoundryHardfork,
9    figment::{
10        self, Metadata, Profile, Provider,
11        error::Kind::InvalidType,
12        value::{Dict, Map, Value},
13    },
14};
15use foundry_evm_networks::NetworkConfigs;
16use serde::Serialize;
17
18/// `EvmArgs` and `EnvArgs` take the highest precedence in the Config/Figment hierarchy.
19///
20/// All vars are opt-in, their default values are expected to be set by the
21/// [`foundry_config::Config`], and are always present ([`foundry_config::Config::default`])
22///
23/// Both have corresponding types in the `evm_adapters` crate which have mandatory fields.
24/// The expected workflow is
25///   1. load the [`foundry_config::Config`]
26///   2. merge with `EvmArgs` into a `figment::Figment`
27///   3. extract `evm_adapters::Opts` from the merged `Figment`
28///
29/// # Example
30///
31/// ```ignore
32/// use foundry_config::Config;
33/// use forge::executor::opts::EvmOpts;
34/// use foundry_cli::opts::EvmArgs;
35/// # fn t(args: EvmArgs) {
36/// let figment = Config::figment_with_root(".").merge(args);
37/// let opts = figment.extract::<EvmOpts>().unwrap();
38/// # }
39/// ```
40#[derive(Clone, Debug, Default, Serialize, Parser)]
41#[command(next_help_heading = "EVM options", about = None, long_about = None)] // override doc
42pub struct EvmArgs {
43    /// Common RPC options (URL, timeout, rate limiting, etc.).
44    #[command(flatten)]
45    #[serde(flatten)]
46    pub rpc: RpcCommonOpts,
47
48    /// Fetch state from a specific block number over a remote endpoint.
49    ///
50    /// See --rpc-url.
51    #[arg(long, requires = "rpc_url", value_name = "BLOCK")]
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub fork_block_number: Option<u64>,
54
55    /// Fetch fork state by block number instead of hash.
56    ///
57    /// Use for RPC endpoints that cannot serve state by block hash. Number-based reads
58    /// cannot guarantee a consistent snapshot if the remote chain reorganizes and do not
59    /// use the disk state cache. Transaction replay still requires hash-addressed state.
60    #[arg(long)]
61    #[serde(skip)]
62    pub fork_state_by_number: bool,
63
64    /// Number of retries.
65    ///
66    /// See --rpc-url.
67    #[arg(long, requires = "rpc_url", value_name = "RETRIES")]
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub fork_retries: Option<u32>,
70
71    /// Initial retry backoff on encountering errors.
72    ///
73    /// See --rpc-url.
74    #[arg(long, requires = "rpc_url", value_name = "BACKOFF")]
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub fork_retry_backoff: Option<u64>,
77
78    /// Explicitly disables the use of RPC caching.
79    ///
80    /// All storage slots are read entirely from the endpoint.
81    ///
82    /// This flag overrides the project's configuration file.
83    ///
84    /// See --rpc-url.
85    #[arg(long)]
86    #[serde(skip)]
87    pub no_storage_caching: bool,
88
89    /// Disable parent-block BAL cache prewarming for transaction-hash fork cheatcodes.
90    ///
91    /// Preceding transactions are still replayed when prewarming is enabled.
92    #[arg(long)]
93    #[serde(skip)]
94    pub no_fork_bal: bool,
95
96    /// Prefetch a new fork block's cache from the keys of an older cached block on the same chain.
97    ///
98    /// Helps forks of `latest`, which start with an empty cache on each new head.
99    #[arg(long)]
100    #[serde(skip)]
101    pub fork_prefetch: bool,
102
103    /// The initial balance of deployed test contracts.
104    #[arg(long, value_name = "BALANCE")]
105    #[serde(skip_serializing_if = "Option::is_none")]
106    pub initial_balance: Option<U256>,
107
108    /// The address which will be executing tests/scripts.
109    #[arg(long, value_name = "ADDRESS")]
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub sender: Option<Address>,
112
113    /// Enable the FFI cheatcode.
114    #[arg(long)]
115    #[serde(skip)]
116    pub ffi: bool,
117
118    /// Whether to show `console.log` outputs in realtime during script/test execution
119    #[arg(long)]
120    #[serde(skip)]
121    pub live_logs: bool,
122
123    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
124    #[arg(long)]
125    #[serde(skip)]
126    pub always_use_create_2_factory: bool,
127
128    /// The CREATE2 deployer address to use, this will override the one in the config.
129    #[arg(long, value_name = "ADDRESS")]
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub create2_deployer: Option<Address>,
132
133    /// All ethereum environment related arguments
134    #[command(flatten)]
135    #[serde(flatten)]
136    pub env: EnvArgs,
137
138    /// Whether to enable isolation of calls.
139    /// In isolation mode all top-level calls are executed as a separate transaction in a separate
140    /// EVM context, enabling more precise gas accounting and transaction state changes.
141    #[arg(long)]
142    #[serde(skip)]
143    pub isolate: bool,
144
145    /// Whether to disable isolation of calls.
146    #[arg(long, conflicts_with = "isolate")]
147    #[serde(skip)]
148    pub no_isolate: bool,
149
150    /// The runtime EVM hardfork to use.
151    ///
152    /// Network-specific hardforks must be namespaced, for example `tempo:T5`.
153    #[arg(long, value_name = "HARDFORK")]
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub hardfork: Option<FoundryHardfork>,
156
157    /// Network selection.
158    #[command(flatten)]
159    #[serde(skip)]
160    pub networks: NetworkConfigs,
161}
162
163// Make this set of options a `figment::Provider` so that it can be merged into the `Config`
164impl Provider for EvmArgs {
165    fn metadata(&self) -> Metadata {
166        Metadata::named("Evm Opts Provider")
167    }
168
169    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
170        let value = Value::serialize(self)?;
171        let error = InvalidType(value.to_actual(), "map".into());
172        let mut dict = value.into_dict().ok_or(error)?;
173
174        if shell::verbosity() > 0 {
175            // need to merge that manually otherwise `from_occurrences` does not work
176            let verbosity = shell::verbosity();
177            dict.insert("verbosity".to_string(), verbosity.into());
178        }
179
180        if self.ffi {
181            dict.insert("ffi".to_string(), self.ffi.into());
182        }
183
184        if self.live_logs {
185            dict.insert("live_logs".to_string(), self.live_logs.into());
186        }
187
188        if self.no_isolate {
189            dict.insert("isolate".to_string(), false.into());
190        } else if self.isolate {
191            dict.insert("isolate".to_string(), self.isolate.into());
192        }
193
194        if self.always_use_create_2_factory {
195            dict.insert(
196                "always_use_create_2_factory".to_string(),
197                self.always_use_create_2_factory.into(),
198            );
199        }
200
201        if self.no_storage_caching {
202            dict.insert("no_storage_caching".to_string(), self.no_storage_caching.into());
203        }
204
205        // Merge serde-skipped fields from the common RPC options.
206        if self.rpc.no_rpc_rate_limit {
207            dict.insert("no_rpc_rate_limit".to_string(), true.into());
208        }
209        if self.rpc.accept_invalid_certs {
210            dict.insert("eth_rpc_accept_invalid_certs".to_string(), true.into());
211        }
212        if self.rpc.no_proxy {
213            dict.insert("eth_rpc_no_proxy".to_string(), true.into());
214        }
215
216        // Only insert network flags when explicitly set via CLI to avoid overriding
217        // values from foundry.toml (NetworkConfigs is flattened in Config).
218        if let Some(network) = self.networks.resolved_network() {
219            dict.insert("network".to_string(), network.name().into());
220        }
221        if self.networks.is_celo() {
222            dict.insert("celo".to_string(), true.into());
223        }
224
225        let mut data = Map::from([(Config::selected_profile(), dict)]);
226        if self.fork_state_by_number {
227            data.entry(Profile::Global)
228                .or_default()
229                .insert("fork_state_by_number".to_string(), true.into());
230        }
231        if self.no_fork_bal {
232            // Environment values use the global profile, which overrides the selected profile.
233            data.entry(Profile::Global).or_default().insert("no_fork_bal".to_string(), true.into());
234        }
235        if self.fork_prefetch {
236            data.entry(Profile::Global)
237                .or_default()
238                .insert("fork_prefetch".to_string(), true.into());
239        }
240        Ok(data)
241    }
242}
243
244/// Configures the executor environment during tests.
245#[derive(Clone, Debug, Default, Serialize, Parser)]
246#[command(next_help_heading = "Executor environment config")]
247pub struct EnvArgs {
248    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests. By
249    /// default, it is 0x6000 (~25kb).
250    #[arg(long, value_name = "CODE_SIZE")]
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub code_size_limit: Option<usize>,
253
254    /// The chain name or EIP-155 chain ID.
255    #[arg(long, visible_alias = "chain-id", value_name = "CHAIN")]
256    #[serde(rename = "chain_id", skip_serializing_if = "Option::is_none", serialize_with = "id")]
257    pub chain: Option<Chain>,
258
259    /// The gas price.
260    #[arg(long, value_name = "GAS_PRICE")]
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub gas_price: Option<u64>,
263
264    /// The base fee in a block.
265    #[arg(long, visible_alias = "base-fee", value_name = "FEE")]
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub block_base_fee_per_gas: Option<u64>,
268
269    /// The transaction origin.
270    #[arg(long, value_name = "ADDRESS")]
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub tx_origin: Option<Address>,
273
274    /// The coinbase of the block.
275    #[arg(long, value_name = "ADDRESS")]
276    #[serde(skip_serializing_if = "Option::is_none")]
277    pub block_coinbase: Option<Address>,
278
279    /// The timestamp of the block.
280    #[arg(long, value_name = "TIMESTAMP")]
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub block_timestamp: Option<u64>,
283
284    /// The block number.
285    #[arg(long, value_name = "BLOCK")]
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub block_number: Option<u64>,
288
289    /// The block difficulty.
290    #[arg(long, value_name = "DIFFICULTY")]
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub block_difficulty: Option<u64>,
293
294    /// The block prevrandao value. NOTE: Before merge this field was mix_hash.
295    #[arg(long, value_name = "PREVRANDAO")]
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub block_prevrandao: Option<B256>,
298
299    /// The block gas limit.
300    #[arg(long, visible_alias = "gas-limit", value_name = "BLOCK_GAS_LIMIT")]
301    #[serde(skip_serializing_if = "Option::is_none")]
302    pub block_gas_limit: Option<u64>,
303
304    /// The memory limit per EVM execution in bytes.
305    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
306    ///
307    /// The default is 128MiB.
308    #[arg(long, value_name = "MEMORY_LIMIT")]
309    #[serde(skip_serializing_if = "Option::is_none")]
310    pub memory_limit: Option<u64>,
311
312    /// Whether to disable the block gas limit checks.
313    #[arg(long, visible_aliases = &["no-block-gas-limit", "no-gas-limit"])]
314    #[serde(skip_serializing_if = "std::ops::Not::not")]
315    pub disable_block_gas_limit: bool,
316
317    /// Whether to enable tx gas limit checks as imposed by Osaka (EIP-7825).
318    #[arg(long, visible_alias = "tx-gas-limit")]
319    #[serde(skip_serializing_if = "std::ops::Not::not")]
320    pub enable_tx_gas_limit: bool,
321}
322
323/// We have to serialize chain IDs and not names because when extracting an EVM `Env`, it expects
324/// `chain_id` to be `u64`.
325fn id<S: serde::Serializer>(chain: &Option<Chain>, s: S) -> Result<S::Ok, S::Error> {
326    if let Some(chain) = chain {
327        s.serialize_u64(chain.id())
328    } else {
329        // skip_serializing_if = "Option::is_none" should prevent this branch from being taken
330        unreachable!()
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337    use foundry_config::{
338        NamedChain,
339        figment::{Figment, providers::Serialized},
340    };
341
342    #[test]
343    fn fork_state_by_number_cli_preserves_config_unless_explicit() {
344        for profile in ["default", "ci"] {
345            for configured in [false, true] {
346                for environment in [None, Some(false), Some(true)] {
347                    for flag in [false, true] {
348                        let config =
349                            Config { fork_state_by_number: configured, ..Default::default() };
350                        let mut figment =
351                            Figment::from(Serialized::defaults(config).profile(profile))
352                                .select(profile);
353                        if let Some(environment) = environment {
354                            figment = figment
355                                .merge(Serialized::global("fork_state_by_number", environment));
356                        }
357                        let args = EvmArgs::parse_from(
358                            ["foundry-cli"]
359                                .into_iter()
360                                .chain(flag.then_some("--fork-state-by-number")),
361                        );
362                        let merged = Config::from_provider(figment.merge(args)).unwrap();
363                        assert_eq!(
364                            merged.fork_state_by_number,
365                            flag || environment.unwrap_or(configured),
366                            "profile={profile}, configured={configured}, environment={environment:?}, flag={flag}",
367                        );
368                    }
369                }
370            }
371        }
372    }
373
374    #[test]
375    fn fork_bal_cli_preserves_config_unless_explicit() {
376        for profile in ["default", "ci"] {
377            for configured in [false, true] {
378                for environment in [None, Some(false), Some(true)] {
379                    for flag in [false, true] {
380                        let config = Config { no_fork_bal: configured, ..Default::default() };
381                        let mut figment =
382                            Figment::from(Serialized::defaults(config).profile(profile))
383                                .select(profile);
384                        if let Some(environment) = environment {
385                            figment = figment.merge(Serialized::global("no_fork_bal", environment));
386                        }
387                        let args = EvmArgs::parse_from(
388                            ["foundry-cli"].into_iter().chain(flag.then_some("--no-fork-bal")),
389                        );
390                        let merged = Config::from_provider(figment.merge(args)).unwrap();
391                        assert_eq!(
392                            merged.no_fork_bal,
393                            flag || environment.unwrap_or(configured),
394                            "profile={profile}, configured={configured}, environment={environment:?}, flag={flag}",
395                        );
396                    }
397                }
398            }
399        }
400    }
401
402    #[test]
403    fn fork_prefetch_cli_preserves_config_unless_explicit() {
404        for profile in ["default", "ci"] {
405            for configured in [false, true] {
406                for flag in [false, true] {
407                    let config = Config { fork_prefetch: configured, ..Default::default() };
408                    let figment = Figment::from(Serialized::defaults(config).profile(profile))
409                        .select(profile);
410                    let args = EvmArgs::parse_from(
411                        ["foundry-cli"].into_iter().chain(flag.then_some("--fork-prefetch")),
412                    );
413                    let merged = Config::from_provider(figment.merge(args)).unwrap();
414                    assert_eq!(
415                        merged.fork_prefetch,
416                        flag || configured,
417                        "profile={profile}, configured={configured}, flag={flag}",
418                    );
419                }
420            }
421        }
422    }
423
424    #[test]
425    fn compute_units_per_second_skips_when_none() {
426        let args = EvmArgs::default();
427        let data = args.data().expect("provider data");
428        let dict = data.get(&Config::selected_profile()).expect("profile dict");
429        assert!(
430            !dict.contains_key("compute_units_per_second"),
431            "compute_units_per_second should be skipped when None"
432        );
433    }
434
435    #[test]
436    fn compute_units_per_second_present_when_some() {
437        let args = EvmArgs {
438            rpc: RpcCommonOpts { compute_units_per_second: Some(1000), ..Default::default() },
439            ..Default::default()
440        };
441        let data = args.data().expect("provider data");
442        let dict = data.get(&Config::selected_profile()).expect("profile dict");
443        let val = dict.get("compute_units_per_second").expect("cups present");
444        assert_eq!(val, &Value::from(1000u64));
445    }
446
447    #[test]
448    fn celo_network_is_included_in_provider_data() {
449        let args = EvmArgs { networks: NetworkConfigs::with_celo(), ..Default::default() };
450        let data = args.data().expect("provider data");
451        let dict = data.get(&Config::selected_profile()).expect("profile dict");
452
453        assert_eq!(dict.get("celo"), Some(&Value::from(true)));
454        assert!(!dict.contains_key("network"));
455    }
456
457    #[test]
458    fn explicit_ethereum_network_is_included_in_provider_data() {
459        let args = EvmArgs { networks: NetworkConfigs::with_ethereum(), ..Default::default() };
460        let data = args.data().expect("provider data");
461        let dict = data.get(&Config::selected_profile()).expect("profile dict");
462
463        assert_eq!(dict.get("network"), Some(&Value::from("ethereum")));
464        assert!(!dict.contains_key("celo"));
465    }
466
467    #[test]
468    fn rpc_url_arg_does_not_read_eth_rpc_url_env() {
469        use clap::CommandFactory;
470
471        let command = EvmArgs::command();
472        let rpc_url =
473            command.get_arguments().find(|arg| arg.get_id() == "rpc_url").expect("rpc_url arg");
474
475        assert!(rpc_url.get_env().is_none());
476    }
477
478    #[test]
479    fn can_parse_chain_id() {
480        let args = EvmArgs {
481            env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
482            ..Default::default()
483        };
484        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
485        assert_eq!(config.chain, Some(NamedChain::Mainnet.into()));
486
487        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "goerli"]);
488        assert_eq!(env.chain, Some(NamedChain::Goerli.into()));
489    }
490
491    #[cfg(feature = "base")]
492    #[test]
493    fn can_parse_namespaced_base_hardfork() {
494        let args = EvmArgs::parse_from(["foundry-cli", "--hardfork", "base:Beryl"]);
495        assert_eq!(args.hardfork.map(String::from).as_deref(), Some("base:Beryl"));
496
497        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
498        assert!(config.networks.is_base());
499        assert_eq!(config.hardfork.map(String::from).as_deref(), Some("base:Beryl"));
500    }
501
502    #[test]
503    fn hardfork_arg_selects_network() {
504        let args = EvmArgs::parse_from(["foundry-cli", "--hardfork", "tempo:T5"]);
505        let hardfork = "tempo:T5".parse::<FoundryHardfork>().unwrap();
506        assert_eq!(args.hardfork, Some(hardfork));
507
508        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
509        assert_eq!(config.hardfork, Some(hardfork));
510        assert!(config.networks.is_tempo());
511    }
512
513    #[test]
514    fn test_memory_limit() {
515        let args = EvmArgs {
516            env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
517            ..Default::default()
518        };
519        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
520        assert_eq!(config.memory_limit, Config::default().memory_limit);
521
522        let env = EnvArgs::parse_from(["foundry-cli", "--memory-limit", "100"]);
523        assert_eq!(env.memory_limit, Some(100));
524    }
525
526    #[test]
527    fn test_chain_id() {
528        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "1"]);
529        assert_eq!(env.chain, Some(Chain::mainnet()));
530
531        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "mainnet"]);
532        assert_eq!(env.chain, Some(Chain::mainnet()));
533        let args = EvmArgs { env, ..Default::default() };
534        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
535        assert_eq!(config.chain, Some(Chain::mainnet()));
536    }
537}