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 serde::Serialize;
14
15use foundry_common::shell;
16
17/// `EvmArgs` and `EnvArgs` take the highest precedence in the Config/Figment hierarchy.
18///
19/// All vars are opt-in, their default values are expected to be set by the
20/// [`foundry_config::Config`], and are always present ([`foundry_config::Config::default`])
21///
22/// Both have corresponding types in the `evm_adapters` crate which have mandatory fields.
23/// The expected workflow is
24///   1. load the [`foundry_config::Config`]
25///   2. merge with `EvmArgs` into a `figment::Figment`
26///   3. extract `evm_adapters::Opts` from the merged `Figment`
27///
28/// # Example
29///
30/// ```ignore
31/// use foundry_config::Config;
32/// use forge::executor::opts::EvmOpts;
33/// use foundry_cli::opts::EvmArgs;
34/// # fn t(args: EvmArgs) {
35/// let figment = Config::figment_with_root(".").merge(args);
36/// let opts = figment.extract::<EvmOpts>().unwrap();
37/// # }
38/// ```
39#[derive(Clone, Debug, Default, Serialize, Parser)]
40#[command(next_help_heading = "EVM options", about = None, long_about = None)] // override doc
41pub struct EvmArgs {
42    /// Fetch state over a remote endpoint instead of starting from an empty state.
43    ///
44    /// If you want to fetch state from a specific block number, see --fork-block-number.
45    #[arg(long, short, visible_alias = "rpc-url", value_name = "URL")]
46    #[serde(rename = "eth_rpc_url", skip_serializing_if = "Option::is_none")]
47    pub fork_url: Option<String>,
48
49    /// Fetch state from a specific block number over a remote endpoint.
50    ///
51    /// See --fork-url.
52    #[arg(long, requires = "fork_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 --fork-url.
59    #[arg(long, requires = "fork_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 --fork-url.
66    #[arg(long, requires = "fork_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 --fork-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    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
97    #[arg(long)]
98    #[serde(skip)]
99    pub always_use_create_2_factory: bool,
100
101    /// The CREATE2 deployer address to use, this will override the one in the config.
102    #[arg(long, value_name = "ADDRESS")]
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub create2_deployer: Option<Address>,
105
106    /// Sets the number of assumed available compute units per second for this provider
107    ///
108    /// default value: 330
109    ///
110    /// See also --fork-url and <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
111    #[arg(long, alias = "cups", value_name = "CUPS", help_heading = "Fork config")]
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub compute_units_per_second: Option<u64>,
114
115    /// Disables rate limiting for this node's provider.
116    ///
117    /// See also --fork-url and <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
118    #[arg(
119        long,
120        value_name = "NO_RATE_LIMITS",
121        help_heading = "Fork config",
122        visible_alias = "no-rate-limit"
123    )]
124    #[serde(skip)]
125    pub no_rpc_rate_limit: bool,
126
127    /// All ethereum environment related arguments
128    #[command(flatten)]
129    #[serde(flatten)]
130    pub env: EnvArgs,
131
132    /// Whether to enable isolation of calls.
133    /// In isolation mode all top-level calls are executed as a separate transaction in a separate
134    /// EVM context, enabling more precise gas accounting and transaction state changes.
135    #[arg(long)]
136    #[serde(skip)]
137    pub isolate: bool,
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            dict.insert("verbosity".to_string(), shell::verbosity().into());
154        }
155
156        if self.ffi {
157            dict.insert("ffi".to_string(), self.ffi.into());
158        }
159
160        if self.isolate {
161            dict.insert("isolate".to_string(), self.isolate.into());
162        }
163
164        if self.always_use_create_2_factory {
165            dict.insert(
166                "always_use_create_2_factory".to_string(),
167                self.always_use_create_2_factory.into(),
168            );
169        }
170
171        if self.no_storage_caching {
172            dict.insert("no_storage_caching".to_string(), self.no_storage_caching.into());
173        }
174
175        if self.no_rpc_rate_limit {
176            dict.insert("no_rpc_rate_limit".to_string(), self.no_rpc_rate_limit.into());
177        }
178
179        Ok(Map::from([(Config::selected_profile(), dict)]))
180    }
181}
182
183/// Configures the executor environment during tests.
184#[derive(Clone, Debug, Default, Serialize, Parser)]
185#[command(next_help_heading = "Executor environment config")]
186pub struct EnvArgs {
187    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests. By
188    /// default, it is 0x6000 (~25kb).
189    #[arg(long, value_name = "CODE_SIZE")]
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub code_size_limit: Option<usize>,
192
193    /// The chain name or EIP-155 chain ID.
194    #[arg(long, visible_alias = "chain-id", value_name = "CHAIN")]
195    #[serde(rename = "chain_id", skip_serializing_if = "Option::is_none", serialize_with = "id")]
196    pub chain: Option<Chain>,
197
198    /// The gas price.
199    #[arg(long, value_name = "GAS_PRICE")]
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub gas_price: Option<u64>,
202
203    /// The base fee in a block.
204    #[arg(long, visible_alias = "base-fee", value_name = "FEE")]
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub block_base_fee_per_gas: Option<u64>,
207
208    /// The transaction origin.
209    #[arg(long, value_name = "ADDRESS")]
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub tx_origin: Option<Address>,
212
213    /// The coinbase of the block.
214    #[arg(long, value_name = "ADDRESS")]
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub block_coinbase: Option<Address>,
217
218    /// The timestamp of the block.
219    #[arg(long, value_name = "TIMESTAMP")]
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub block_timestamp: Option<u64>,
222
223    /// The block number.
224    #[arg(long, value_name = "BLOCK")]
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub block_number: Option<u64>,
227
228    /// The block difficulty.
229    #[arg(long, value_name = "DIFFICULTY")]
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub block_difficulty: Option<u64>,
232
233    /// The block prevrandao value. NOTE: Before merge this field was mix_hash.
234    #[arg(long, value_name = "PREVRANDAO")]
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub block_prevrandao: Option<B256>,
237
238    /// The block gas limit.
239    #[arg(long, visible_alias = "gas-limit", value_name = "BLOCK_GAS_LIMIT")]
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub block_gas_limit: Option<u64>,
242
243    /// The memory limit per EVM execution in bytes.
244    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
245    ///
246    /// The default is 128MiB.
247    #[arg(long, value_name = "MEMORY_LIMIT")]
248    #[serde(skip_serializing_if = "Option::is_none")]
249    pub memory_limit: Option<u64>,
250
251    /// Whether to disable the block gas limit checks.
252    #[arg(long, visible_aliases = &["no-block-gas-limit", "no-gas-limit"])]
253    #[serde(skip_serializing_if = "std::ops::Not::not")]
254    pub disable_block_gas_limit: bool,
255
256    /// Whether to enable tx gas limit checks as imposed by Osaka (EIP-7825).
257    #[arg(long, visible_alias = "tx-gas-limit")]
258    #[serde(skip_serializing_if = "std::ops::Not::not")]
259    pub enable_tx_gas_limit: bool,
260}
261
262/// We have to serialize chain IDs and not names because when extracting an EVM `Env`, it expects
263/// `chain_id` to be `u64`.
264fn id<S: serde::Serializer>(chain: &Option<Chain>, s: S) -> Result<S::Ok, S::Error> {
265    if let Some(chain) = chain {
266        s.serialize_u64(chain.id())
267    } else {
268        // skip_serializing_if = "Option::is_none" should prevent this branch from being taken
269        unreachable!()
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use foundry_config::NamedChain;
277
278    #[test]
279    fn compute_units_per_second_skips_when_none() {
280        let args = EvmArgs::default();
281        let data = args.data().expect("provider data");
282        let dict = data.get(&Config::selected_profile()).expect("profile dict");
283        assert!(
284            !dict.contains_key("compute_units_per_second"),
285            "compute_units_per_second should be skipped when None"
286        );
287    }
288
289    #[test]
290    fn compute_units_per_second_present_when_some() {
291        let args = EvmArgs { compute_units_per_second: Some(1000), ..Default::default() };
292        let data = args.data().expect("provider data");
293        let dict = data.get(&Config::selected_profile()).expect("profile dict");
294        let val = dict.get("compute_units_per_second").expect("cups present");
295        assert_eq!(val, &Value::from(1000u64));
296    }
297
298    #[test]
299    fn can_parse_chain_id() {
300        let args = EvmArgs {
301            env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
302            ..Default::default()
303        };
304        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
305        assert_eq!(config.chain, Some(NamedChain::Mainnet.into()));
306
307        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "goerli"]);
308        assert_eq!(env.chain, Some(NamedChain::Goerli.into()));
309    }
310
311    #[test]
312    fn test_memory_limit() {
313        let args = EvmArgs {
314            env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
315            ..Default::default()
316        };
317        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
318        assert_eq!(config.memory_limit, Config::default().memory_limit);
319
320        let env = EnvArgs::parse_from(["foundry-cli", "--memory-limit", "100"]);
321        assert_eq!(env.memory_limit, Some(100));
322    }
323
324    #[test]
325    fn test_chain_id() {
326        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "1"]);
327        assert_eq!(env.chain, Some(Chain::mainnet()));
328
329        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "mainnet"]);
330        assert_eq!(env.chain, Some(Chain::mainnet()));
331        let args = EvmArgs { env, ..Default::default() };
332        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
333        assert_eq!(config.chain, Some(Chain::mainnet()));
334    }
335}