Skip to main content

foundry_cli/opts/
evm.rs

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