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
141// Make this set of options a `figment::Provider` so that it can be merged into the `Config`
142impl Provider for EvmArgs {
143    fn metadata(&self) -> Metadata {
144        Metadata::named("Evm Opts Provider")
145    }
146
147    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
148        let value = Value::serialize(self)?;
149        let error = InvalidType(value.to_actual(), "map".into());
150        let mut dict = value.into_dict().ok_or(error)?;
151
152        if shell::verbosity() > 0 {
153            // need to merge that manually otherwise `from_occurrences` does not work
154            dict.insert("verbosity".to_string(), shell::verbosity().into());
155        }
156
157        if self.ffi {
158            dict.insert("ffi".to_string(), self.ffi.into());
159        }
160
161        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        if self.no_rpc_rate_limit {
177            dict.insert("no_rpc_rate_limit".to_string(), self.no_rpc_rate_limit.into());
178        }
179
180        if let Some(fork_url) = &self.fork_url {
181            dict.insert("eth_rpc_url".to_string(), fork_url.clone().into());
182        }
183
184        Ok(Map::from([(Config::selected_profile(), dict)]))
185    }
186}
187
188/// Configures the executor environment during tests.
189#[derive(Clone, Debug, Default, Serialize, Parser)]
190#[command(next_help_heading = "Executor environment config")]
191pub struct EnvArgs {
192    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests. By
193    /// default, it is 0x6000 (~25kb).
194    #[arg(long, value_name = "CODE_SIZE")]
195    #[serde(skip_serializing_if = "Option::is_none")]
196    pub code_size_limit: Option<usize>,
197
198    /// The chain name or EIP-155 chain ID.
199    #[arg(long, visible_alias = "chain-id", value_name = "CHAIN")]
200    #[serde(rename = "chain_id", skip_serializing_if = "Option::is_none", serialize_with = "id")]
201    pub chain: Option<Chain>,
202
203    /// The gas price.
204    #[arg(long, value_name = "GAS_PRICE")]
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub gas_price: Option<u64>,
207
208    /// The base fee in a block.
209    #[arg(long, visible_alias = "base-fee", value_name = "FEE")]
210    #[serde(skip_serializing_if = "Option::is_none")]
211    pub block_base_fee_per_gas: Option<u64>,
212
213    /// The transaction origin.
214    #[arg(long, value_name = "ADDRESS")]
215    #[serde(skip_serializing_if = "Option::is_none")]
216    pub tx_origin: Option<Address>,
217
218    /// The coinbase of the block.
219    #[arg(long, value_name = "ADDRESS")]
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub block_coinbase: Option<Address>,
222
223    /// The timestamp of the block.
224    #[arg(long, value_name = "TIMESTAMP")]
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub block_timestamp: Option<u64>,
227
228    /// The block number.
229    #[arg(long, value_name = "BLOCK")]
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub block_number: Option<u64>,
232
233    /// The block difficulty.
234    #[arg(long, value_name = "DIFFICULTY")]
235    #[serde(skip_serializing_if = "Option::is_none")]
236    pub block_difficulty: Option<u64>,
237
238    /// The block prevrandao value. NOTE: Before merge this field was mix_hash.
239    #[arg(long, value_name = "PREVRANDAO")]
240    #[serde(skip_serializing_if = "Option::is_none")]
241    pub block_prevrandao: Option<B256>,
242
243    /// The block gas limit.
244    #[arg(long, visible_aliases = &["block-gas-limit", "gas-limit"], value_name = "BLOCK_GAS_LIMIT")]
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub block_gas_limit: Option<u64>,
247
248    /// The memory limit per EVM execution in bytes.
249    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
250    ///
251    /// The default is 128MiB.
252    #[arg(long, value_name = "MEMORY_LIMIT")]
253    #[serde(skip_serializing_if = "Option::is_none")]
254    pub memory_limit: Option<u64>,
255
256    /// Whether to disable the block gas limit checks.
257    #[arg(long, visible_aliases = &["no-block-gas-limit", "no-gas-limit"])]
258    #[serde(skip_serializing_if = "std::ops::Not::not")]
259    pub disable_block_gas_limit: bool,
260
261    /// Whether to enable tx gas limit checks as imposed by Osaka (EIP-7825).
262    #[arg(long, visible_alias = "tx-gas-limit")]
263    #[serde(skip_serializing_if = "std::ops::Not::not")]
264    pub enable_tx_gas_limit: bool,
265}
266
267impl EvmArgs {
268    /// Ensures that fork url exists and returns its reference.
269    pub fn ensure_fork_url(&self) -> eyre::Result<&String> {
270        self.fork_url.as_ref().wrap_err("Missing `--fork-url` field.")
271    }
272}
273
274/// We have to serialize chain IDs and not names because when extracting an EVM `Env`, it expects
275/// `chain_id` to be `u64`.
276fn id<S: serde::Serializer>(chain: &Option<Chain>, s: S) -> Result<S::Ok, S::Error> {
277    if let Some(chain) = chain {
278        s.serialize_u64(chain.id())
279    } else {
280        // skip_serializing_if = "Option::is_none" should prevent this branch from being taken
281        unreachable!()
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use foundry_config::NamedChain;
289
290    #[test]
291    fn compute_units_per_second_skips_when_none() {
292        let args = EvmArgs::default();
293        let data = args.data().expect("provider data");
294        let dict = data.get(&Config::selected_profile()).expect("profile dict");
295        assert!(
296            !dict.contains_key("compute_units_per_second"),
297            "compute_units_per_second should be skipped when None"
298        );
299    }
300
301    #[test]
302    fn compute_units_per_second_present_when_some() {
303        let args = EvmArgs { compute_units_per_second: Some(1000), ..Default::default() };
304        let data = args.data().expect("provider data");
305        let dict = data.get(&Config::selected_profile()).expect("profile dict");
306        let val = dict.get("compute_units_per_second").expect("cups present");
307        assert_eq!(val, &Value::from(1000u64));
308    }
309
310    #[test]
311    fn can_parse_chain_id() {
312        let args = EvmArgs {
313            env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
314            ..Default::default()
315        };
316        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
317        assert_eq!(config.chain, Some(NamedChain::Mainnet.into()));
318
319        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "goerli"]);
320        assert_eq!(env.chain, Some(NamedChain::Goerli.into()));
321    }
322
323    #[test]
324    fn test_memory_limit() {
325        let args = EvmArgs {
326            env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
327            ..Default::default()
328        };
329        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
330        assert_eq!(config.memory_limit, Config::default().memory_limit);
331
332        let env = EnvArgs::parse_from(["foundry-cli", "--memory-limit", "100"]);
333        assert_eq!(env.memory_limit, Some(100));
334    }
335
336    #[test]
337    fn test_chain_id() {
338        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "1"]);
339        assert_eq!(env.chain, Some(Chain::mainnet()));
340
341        let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "mainnet"]);
342        assert_eq!(env.chain, Some(Chain::mainnet()));
343        let args = EvmArgs { env, ..Default::default() };
344        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
345        assert_eq!(config.chain, Some(Chain::mainnet()));
346    }
347}