foundry_cli/opts/
evm.rs

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