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    /// Number of retries.
56    ///
57    /// See --rpc-url.
58    #[arg(long, requires = "rpc_url", value_name = "RETRIES")]
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub fork_retries: Option<u32>,
61
62    /// Initial retry backoff on encountering errors.
63    ///
64    /// See --rpc-url.
65    #[arg(long, requires = "rpc_url", value_name = "BACKOFF")]
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub fork_retry_backoff: Option<u64>,
68
69    /// Explicitly disables the use of RPC caching.
70    ///
71    /// All storage slots are read entirely from the endpoint.
72    ///
73    /// This flag overrides the project's configuration file.
74    ///
75    /// See --rpc-url.
76    #[arg(long)]
77    #[serde(skip)]
78    pub no_storage_caching: bool,
79
80    /// The initial balance of deployed test contracts.
81    #[arg(long, value_name = "BALANCE")]
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub initial_balance: Option<U256>,
84
85    /// The address which will be executing tests/scripts.
86    #[arg(long, value_name = "ADDRESS")]
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub sender: Option<Address>,
89
90    /// Enable the FFI cheatcode.
91    #[arg(long)]
92    #[serde(skip)]
93    pub ffi: bool,
94
95    /// Whether to show `console.log` outputs in realtime during script/test execution
96    #[arg(long)]
97    #[serde(skip)]
98    pub live_logs: bool,
99
100    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
101    #[arg(long)]
102    #[serde(skip)]
103    pub always_use_create_2_factory: bool,
104
105    /// The CREATE2 deployer address to use, this will override the one in the config.
106    #[arg(long, value_name = "ADDRESS")]
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub create2_deployer: Option<Address>,
109
110    /// All ethereum environment related arguments
111    #[command(flatten)]
112    #[serde(flatten)]
113    pub env: EnvArgs,
114
115    /// Whether to enable isolation of calls.
116    /// In isolation mode all top-level calls are executed as a separate transaction in a separate
117    /// EVM context, enabling more precise gas accounting and transaction state changes.
118    #[arg(long)]
119    #[serde(skip)]
120    pub isolate: bool,
121
122    /// Whether to disable isolation of calls.
123    #[arg(long, conflicts_with = "isolate")]
124    #[serde(skip)]
125    pub no_isolate: bool,
126
127    /// The runtime EVM hardfork to use.
128    ///
129    /// Network-specific hardforks must be namespaced, for example `tempo:T5`.
130    #[arg(long, value_name = "HARDFORK")]
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub hardfork: Option<FoundryHardfork>,
133
134    /// Network selection.
135    #[command(flatten)]
136    #[serde(skip)]
137    pub networks: NetworkConfigs,
138}
139
140// Make this set of options a `figment::Provider` so that it can be merged into the `Config`
141impl Provider for EvmArgs {
142    fn metadata(&self) -> Metadata {
143        Metadata::named("Evm Opts Provider")
144    }
145
146    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
147        let value = Value::serialize(self)?;
148        let error = InvalidType(value.to_actual(), "map".into());
149        let mut dict = value.into_dict().ok_or(error)?;
150
151        if shell::verbosity() > 0 {
152            // need to merge that manually otherwise `from_occurrences` does not work
153            let verbosity = shell::verbosity();
154            dict.insert("verbosity".to_string(), verbosity.into());
155        }
156
157        if self.ffi {
158            dict.insert("ffi".to_string(), self.ffi.into());
159        }
160
161        if self.live_logs {
162            dict.insert("live_logs".to_string(), self.live_logs.into());
163        }
164
165        if self.no_isolate {
166            dict.insert("isolate".to_string(), false.into());
167        } else if self.isolate {
168            dict.insert("isolate".to_string(), self.isolate.into());
169        }
170
171        if self.always_use_create_2_factory {
172            dict.insert(
173                "always_use_create_2_factory".to_string(),
174                self.always_use_create_2_factory.into(),
175            );
176        }
177
178        if self.no_storage_caching {
179            dict.insert("no_storage_caching".to_string(), self.no_storage_caching.into());
180        }
181
182        // Merge serde-skipped fields from the common RPC options.
183        if self.rpc.no_rpc_rate_limit {
184            dict.insert("no_rpc_rate_limit".to_string(), true.into());
185        }
186        if self.rpc.accept_invalid_certs {
187            dict.insert("eth_rpc_accept_invalid_certs".to_string(), true.into());
188        }
189        if self.rpc.no_proxy {
190            dict.insert("eth_rpc_no_proxy".to_string(), true.into());
191        }
192
193        // Only insert network flags when explicitly set via CLI to avoid overriding
194        // values from foundry.toml (NetworkConfigs is flattened in Config).
195        if let Some(network) = self.networks.resolved_network() {
196            dict.insert("network".to_string(), network.name().into());
197        }
198        if self.networks.is_celo() {
199            dict.insert("celo".to_string(), true.into());
200        }
201
202        Ok(Map::from([(Config::selected_profile(), dict)]))
203    }
204}
205
206/// Configures the executor environment during tests.
207#[derive(Clone, Debug, Default, Serialize, Parser)]
208#[command(next_help_heading = "Executor environment config")]
209pub struct EnvArgs {
210    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests. By
211    /// default, it is 0x6000 (~25kb).
212    #[arg(long, value_name = "CODE_SIZE")]
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub code_size_limit: Option<usize>,
215
216    /// The chain name or EIP-155 chain ID.
217    #[arg(long, visible_alias = "chain-id", value_name = "CHAIN")]
218    #[serde(rename = "chain_id", skip_serializing_if = "Option::is_none", serialize_with = "id")]
219    pub chain: Option<Chain>,
220
221    /// The gas price.
222    #[arg(long, value_name = "GAS_PRICE")]
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub gas_price: Option<u64>,
225
226    /// The base fee in a block.
227    #[arg(long, visible_alias = "base-fee", value_name = "FEE")]
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub block_base_fee_per_gas: Option<u64>,
230
231    /// The transaction origin.
232    #[arg(long, value_name = "ADDRESS")]
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub tx_origin: Option<Address>,
235
236    /// The coinbase of the block.
237    #[arg(long, value_name = "ADDRESS")]
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub block_coinbase: Option<Address>,
240
241    /// The timestamp of the block.
242    #[arg(long, value_name = "TIMESTAMP")]
243    #[serde(skip_serializing_if = "Option::is_none")]
244    pub block_timestamp: Option<u64>,
245
246    /// The block number.
247    #[arg(long, value_name = "BLOCK")]
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub block_number: Option<u64>,
250
251    /// The block difficulty.
252    #[arg(long, value_name = "DIFFICULTY")]
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub block_difficulty: Option<u64>,
255
256    /// The block prevrandao value. NOTE: Before merge this field was mix_hash.
257    #[arg(long, value_name = "PREVRANDAO")]
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub block_prevrandao: Option<B256>,
260
261    /// The block gas limit.
262    #[arg(long, visible_alias = "gas-limit", value_name = "BLOCK_GAS_LIMIT")]
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub block_gas_limit: Option<u64>,
265
266    /// The memory limit per EVM execution in bytes.
267    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
268    ///
269    /// The default is 128MiB.
270    #[arg(long, value_name = "MEMORY_LIMIT")]
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub memory_limit: Option<u64>,
273
274    /// Whether to disable the block gas limit checks.
275    #[arg(long, visible_aliases = &["no-block-gas-limit", "no-gas-limit"])]
276    #[serde(skip_serializing_if = "std::ops::Not::not")]
277    pub disable_block_gas_limit: bool,
278
279    /// Whether to enable tx gas limit checks as imposed by Osaka (EIP-7825).
280    #[arg(long, visible_alias = "tx-gas-limit")]
281    #[serde(skip_serializing_if = "std::ops::Not::not")]
282    pub enable_tx_gas_limit: bool,
283}
284
285/// We have to serialize chain IDs and not names because when extracting an EVM `Env`, it expects
286/// `chain_id` to be `u64`.
287fn id<S: serde::Serializer>(chain: &Option<Chain>, s: S) -> Result<S::Ok, S::Error> {
288    if let Some(chain) = chain {
289        s.serialize_u64(chain.id())
290    } else {
291        // skip_serializing_if = "Option::is_none" should prevent this branch from being taken
292        unreachable!()
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299    use foundry_config::NamedChain;
300
301    #[test]
302    fn compute_units_per_second_skips_when_none() {
303        let args = EvmArgs::default();
304        let data = args.data().expect("provider data");
305        let dict = data.get(&Config::selected_profile()).expect("profile dict");
306        assert!(
307            !dict.contains_key("compute_units_per_second"),
308            "compute_units_per_second should be skipped when None"
309        );
310    }
311
312    #[test]
313    fn compute_units_per_second_present_when_some() {
314        let args = EvmArgs {
315            rpc: RpcCommonOpts { compute_units_per_second: Some(1000), ..Default::default() },
316            ..Default::default()
317        };
318        let data = args.data().expect("provider data");
319        let dict = data.get(&Config::selected_profile()).expect("profile dict");
320        let val = dict.get("compute_units_per_second").expect("cups present");
321        assert_eq!(val, &Value::from(1000u64));
322    }
323
324    #[test]
325    fn celo_network_is_included_in_provider_data() {
326        let args = EvmArgs { networks: NetworkConfigs::with_celo(), ..Default::default() };
327        let data = args.data().expect("provider data");
328        let dict = data.get(&Config::selected_profile()).expect("profile dict");
329
330        assert_eq!(dict.get("celo"), Some(&Value::from(true)));
331        assert!(!dict.contains_key("network"));
332    }
333
334    #[test]
335    fn explicit_ethereum_network_is_included_in_provider_data() {
336        let args = EvmArgs { networks: NetworkConfigs::with_ethereum(), ..Default::default() };
337        let data = args.data().expect("provider data");
338        let dict = data.get(&Config::selected_profile()).expect("profile dict");
339
340        assert_eq!(dict.get("network"), Some(&Value::from("ethereum")));
341        assert!(!dict.contains_key("celo"));
342    }
343
344    #[test]
345    fn rpc_url_arg_does_not_read_eth_rpc_url_env() {
346        use clap::CommandFactory;
347
348        let command = EvmArgs::command();
349        let rpc_url =
350            command.get_arguments().find(|arg| arg.get_id() == "rpc_url").expect("rpc_url arg");
351
352        assert!(rpc_url.get_env().is_none());
353    }
354
355    #[test]
356    fn can_parse_chain_id() {
357        let args = EvmArgs {
358            env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
359            ..Default::default()
360        };
361        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
362        assert_eq!(config.chain, Some(NamedChain::Mainnet.into()));
363
364        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "goerli"]);
365        assert_eq!(env.chain, Some(NamedChain::Goerli.into()));
366    }
367
368    #[cfg(feature = "base")]
369    #[test]
370    fn can_parse_namespaced_base_hardfork() {
371        let args = EvmArgs::parse_from(["foundry-cli", "--hardfork", "base:Beryl"]);
372        assert_eq!(args.hardfork.map(String::from).as_deref(), Some("base:Beryl"));
373
374        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
375        assert!(config.networks.is_base());
376        assert_eq!(config.hardfork.map(String::from).as_deref(), Some("base:Beryl"));
377    }
378
379    #[test]
380    fn hardfork_arg_selects_network() {
381        let args = EvmArgs::parse_from(["foundry-cli", "--hardfork", "tempo:T5"]);
382        let hardfork = "tempo:T5".parse::<FoundryHardfork>().unwrap();
383        assert_eq!(args.hardfork, Some(hardfork));
384
385        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
386        assert_eq!(config.hardfork, Some(hardfork));
387        assert!(config.networks.is_tempo());
388    }
389
390    #[test]
391    fn test_memory_limit() {
392        let args = EvmArgs {
393            env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
394            ..Default::default()
395        };
396        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
397        assert_eq!(config.memory_limit, Config::default().memory_limit);
398
399        let env = EnvArgs::parse_from(["foundry-cli", "--memory-limit", "100"]);
400        assert_eq!(env.memory_limit, Some(100));
401    }
402
403    #[test]
404    fn test_chain_id() {
405        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "1"]);
406        assert_eq!(env.chain, Some(Chain::mainnet()));
407
408        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "mainnet"]);
409        assert_eq!(env.chain, Some(Chain::mainnet()));
410        let args = EvmArgs { env, ..Default::default() };
411        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
412        assert_eq!(config.chain, Some(Chain::mainnet()));
413    }
414}