1use 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#[derive(Clone, Debug, Default, Serialize, Parser)]
41#[command(next_help_heading = "EVM options", about = None, long_about = None)] pub struct EvmArgs {
43 #[command(flatten)]
45 #[serde(flatten)]
46 pub rpc: RpcCommonOpts,
47
48 #[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 #[arg(long)]
61 #[serde(skip)]
62 pub fork_state_by_number: bool,
63
64 #[arg(long, requires = "rpc_url", value_name = "RETRIES")]
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub fork_retries: Option<u32>,
70
71 #[arg(long, requires = "rpc_url", value_name = "BACKOFF")]
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub fork_retry_backoff: Option<u64>,
77
78 #[arg(long)]
86 #[serde(skip)]
87 pub no_storage_caching: bool,
88
89 #[arg(long)]
93 #[serde(skip)]
94 pub no_fork_bal: bool,
95
96 #[arg(long)]
100 #[serde(skip)]
101 pub fork_prefetch: bool,
102
103 #[arg(long, value_name = "BALANCE")]
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub initial_balance: Option<U256>,
107
108 #[arg(long, value_name = "ADDRESS")]
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub sender: Option<Address>,
112
113 #[arg(long)]
115 #[serde(skip)]
116 pub ffi: bool,
117
118 #[arg(long)]
120 #[serde(skip)]
121 pub live_logs: bool,
122
123 #[arg(long)]
125 #[serde(skip)]
126 pub always_use_create_2_factory: bool,
127
128 #[arg(long, value_name = "ADDRESS")]
130 #[serde(skip_serializing_if = "Option::is_none")]
131 pub create2_deployer: Option<Address>,
132
133 #[command(flatten)]
135 #[serde(flatten)]
136 pub env: EnvArgs,
137
138 #[arg(long)]
142 #[serde(skip)]
143 pub isolate: bool,
144
145 #[arg(long, conflicts_with = "isolate")]
147 #[serde(skip)]
148 pub no_isolate: bool,
149
150 #[arg(long, value_name = "HARDFORK")]
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub hardfork: Option<FoundryHardfork>,
156
157 #[command(flatten)]
159 #[serde(skip)]
160 pub networks: NetworkConfigs,
161}
162
163impl Provider for EvmArgs {
165 fn metadata(&self) -> Metadata {
166 Metadata::named("Evm Opts Provider")
167 }
168
169 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
170 let value = Value::serialize(self)?;
171 let error = InvalidType(value.to_actual(), "map".into());
172 let mut dict = value.into_dict().ok_or(error)?;
173
174 if shell::verbosity() > 0 {
175 let verbosity = shell::verbosity();
177 dict.insert("verbosity".to_string(), verbosity.into());
178 }
179
180 if self.ffi {
181 dict.insert("ffi".to_string(), self.ffi.into());
182 }
183
184 if self.live_logs {
185 dict.insert("live_logs".to_string(), self.live_logs.into());
186 }
187
188 if self.no_isolate {
189 dict.insert("isolate".to_string(), false.into());
190 } else if self.isolate {
191 dict.insert("isolate".to_string(), self.isolate.into());
192 }
193
194 if self.always_use_create_2_factory {
195 dict.insert(
196 "always_use_create_2_factory".to_string(),
197 self.always_use_create_2_factory.into(),
198 );
199 }
200
201 if self.no_storage_caching {
202 dict.insert("no_storage_caching".to_string(), self.no_storage_caching.into());
203 }
204
205 if self.rpc.no_rpc_rate_limit {
207 dict.insert("no_rpc_rate_limit".to_string(), true.into());
208 }
209 if self.rpc.accept_invalid_certs {
210 dict.insert("eth_rpc_accept_invalid_certs".to_string(), true.into());
211 }
212 if self.rpc.no_proxy {
213 dict.insert("eth_rpc_no_proxy".to_string(), true.into());
214 }
215
216 if let Some(network) = self.networks.resolved_network() {
219 dict.insert("network".to_string(), network.name().into());
220 }
221 if self.networks.is_celo() {
222 dict.insert("celo".to_string(), true.into());
223 }
224
225 let mut data = Map::from([(Config::selected_profile(), dict)]);
226 if self.fork_state_by_number {
227 data.entry(Profile::Global)
228 .or_default()
229 .insert("fork_state_by_number".to_string(), true.into());
230 }
231 if self.no_fork_bal {
232 data.entry(Profile::Global).or_default().insert("no_fork_bal".to_string(), true.into());
234 }
235 if self.fork_prefetch {
236 data.entry(Profile::Global)
237 .or_default()
238 .insert("fork_prefetch".to_string(), true.into());
239 }
240 Ok(data)
241 }
242}
243
244#[derive(Clone, Debug, Default, Serialize, Parser)]
246#[command(next_help_heading = "Executor environment config")]
247pub struct EnvArgs {
248 #[arg(long, value_name = "CODE_SIZE")]
251 #[serde(skip_serializing_if = "Option::is_none")]
252 pub code_size_limit: Option<usize>,
253
254 #[arg(long, visible_alias = "chain-id", value_name = "CHAIN")]
256 #[serde(rename = "chain_id", skip_serializing_if = "Option::is_none", serialize_with = "id")]
257 pub chain: Option<Chain>,
258
259 #[arg(long, value_name = "GAS_PRICE")]
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub gas_price: Option<u64>,
263
264 #[arg(long, visible_alias = "base-fee", value_name = "FEE")]
266 #[serde(skip_serializing_if = "Option::is_none")]
267 pub block_base_fee_per_gas: Option<u64>,
268
269 #[arg(long, value_name = "ADDRESS")]
271 #[serde(skip_serializing_if = "Option::is_none")]
272 pub tx_origin: Option<Address>,
273
274 #[arg(long, value_name = "ADDRESS")]
276 #[serde(skip_serializing_if = "Option::is_none")]
277 pub block_coinbase: Option<Address>,
278
279 #[arg(long, value_name = "TIMESTAMP")]
281 #[serde(skip_serializing_if = "Option::is_none")]
282 pub block_timestamp: Option<u64>,
283
284 #[arg(long, value_name = "BLOCK")]
286 #[serde(skip_serializing_if = "Option::is_none")]
287 pub block_number: Option<u64>,
288
289 #[arg(long, value_name = "DIFFICULTY")]
291 #[serde(skip_serializing_if = "Option::is_none")]
292 pub block_difficulty: Option<u64>,
293
294 #[arg(long, value_name = "PREVRANDAO")]
296 #[serde(skip_serializing_if = "Option::is_none")]
297 pub block_prevrandao: Option<B256>,
298
299 #[arg(long, visible_alias = "gas-limit", value_name = "BLOCK_GAS_LIMIT")]
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub block_gas_limit: Option<u64>,
303
304 #[arg(long, value_name = "MEMORY_LIMIT")]
309 #[serde(skip_serializing_if = "Option::is_none")]
310 pub memory_limit: Option<u64>,
311
312 #[arg(long, visible_aliases = &["no-block-gas-limit", "no-gas-limit"])]
314 #[serde(skip_serializing_if = "std::ops::Not::not")]
315 pub disable_block_gas_limit: bool,
316
317 #[arg(long, visible_alias = "tx-gas-limit")]
319 #[serde(skip_serializing_if = "std::ops::Not::not")]
320 pub enable_tx_gas_limit: bool,
321}
322
323fn id<S: serde::Serializer>(chain: &Option<Chain>, s: S) -> Result<S::Ok, S::Error> {
326 if let Some(chain) = chain {
327 s.serialize_u64(chain.id())
328 } else {
329 unreachable!()
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use foundry_config::{
338 NamedChain,
339 figment::{Figment, providers::Serialized},
340 };
341
342 #[test]
343 fn fork_state_by_number_cli_preserves_config_unless_explicit() {
344 for profile in ["default", "ci"] {
345 for configured in [false, true] {
346 for environment in [None, Some(false), Some(true)] {
347 for flag in [false, true] {
348 let config =
349 Config { fork_state_by_number: configured, ..Default::default() };
350 let mut figment =
351 Figment::from(Serialized::defaults(config).profile(profile))
352 .select(profile);
353 if let Some(environment) = environment {
354 figment = figment
355 .merge(Serialized::global("fork_state_by_number", environment));
356 }
357 let args = EvmArgs::parse_from(
358 ["foundry-cli"]
359 .into_iter()
360 .chain(flag.then_some("--fork-state-by-number")),
361 );
362 let merged = Config::from_provider(figment.merge(args)).unwrap();
363 assert_eq!(
364 merged.fork_state_by_number,
365 flag || environment.unwrap_or(configured),
366 "profile={profile}, configured={configured}, environment={environment:?}, flag={flag}",
367 );
368 }
369 }
370 }
371 }
372 }
373
374 #[test]
375 fn fork_bal_cli_preserves_config_unless_explicit() {
376 for profile in ["default", "ci"] {
377 for configured in [false, true] {
378 for environment in [None, Some(false), Some(true)] {
379 for flag in [false, true] {
380 let config = Config { no_fork_bal: configured, ..Default::default() };
381 let mut figment =
382 Figment::from(Serialized::defaults(config).profile(profile))
383 .select(profile);
384 if let Some(environment) = environment {
385 figment = figment.merge(Serialized::global("no_fork_bal", environment));
386 }
387 let args = EvmArgs::parse_from(
388 ["foundry-cli"].into_iter().chain(flag.then_some("--no-fork-bal")),
389 );
390 let merged = Config::from_provider(figment.merge(args)).unwrap();
391 assert_eq!(
392 merged.no_fork_bal,
393 flag || environment.unwrap_or(configured),
394 "profile={profile}, configured={configured}, environment={environment:?}, flag={flag}",
395 );
396 }
397 }
398 }
399 }
400 }
401
402 #[test]
403 fn fork_prefetch_cli_preserves_config_unless_explicit() {
404 for profile in ["default", "ci"] {
405 for configured in [false, true] {
406 for flag in [false, true] {
407 let config = Config { fork_prefetch: configured, ..Default::default() };
408 let figment = Figment::from(Serialized::defaults(config).profile(profile))
409 .select(profile);
410 let args = EvmArgs::parse_from(
411 ["foundry-cli"].into_iter().chain(flag.then_some("--fork-prefetch")),
412 );
413 let merged = Config::from_provider(figment.merge(args)).unwrap();
414 assert_eq!(
415 merged.fork_prefetch,
416 flag || configured,
417 "profile={profile}, configured={configured}, flag={flag}",
418 );
419 }
420 }
421 }
422 }
423
424 #[test]
425 fn compute_units_per_second_skips_when_none() {
426 let args = EvmArgs::default();
427 let data = args.data().expect("provider data");
428 let dict = data.get(&Config::selected_profile()).expect("profile dict");
429 assert!(
430 !dict.contains_key("compute_units_per_second"),
431 "compute_units_per_second should be skipped when None"
432 );
433 }
434
435 #[test]
436 fn compute_units_per_second_present_when_some() {
437 let args = EvmArgs {
438 rpc: RpcCommonOpts { compute_units_per_second: Some(1000), ..Default::default() },
439 ..Default::default()
440 };
441 let data = args.data().expect("provider data");
442 let dict = data.get(&Config::selected_profile()).expect("profile dict");
443 let val = dict.get("compute_units_per_second").expect("cups present");
444 assert_eq!(val, &Value::from(1000u64));
445 }
446
447 #[test]
448 fn celo_network_is_included_in_provider_data() {
449 let args = EvmArgs { networks: NetworkConfigs::with_celo(), ..Default::default() };
450 let data = args.data().expect("provider data");
451 let dict = data.get(&Config::selected_profile()).expect("profile dict");
452
453 assert_eq!(dict.get("celo"), Some(&Value::from(true)));
454 assert!(!dict.contains_key("network"));
455 }
456
457 #[test]
458 fn explicit_ethereum_network_is_included_in_provider_data() {
459 let args = EvmArgs { networks: NetworkConfigs::with_ethereum(), ..Default::default() };
460 let data = args.data().expect("provider data");
461 let dict = data.get(&Config::selected_profile()).expect("profile dict");
462
463 assert_eq!(dict.get("network"), Some(&Value::from("ethereum")));
464 assert!(!dict.contains_key("celo"));
465 }
466
467 #[test]
468 fn rpc_url_arg_does_not_read_eth_rpc_url_env() {
469 use clap::CommandFactory;
470
471 let command = EvmArgs::command();
472 let rpc_url =
473 command.get_arguments().find(|arg| arg.get_id() == "rpc_url").expect("rpc_url arg");
474
475 assert!(rpc_url.get_env().is_none());
476 }
477
478 #[test]
479 fn can_parse_chain_id() {
480 let args = EvmArgs {
481 env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
482 ..Default::default()
483 };
484 let config = Config::from_provider(Config::figment().merge(args)).unwrap();
485 assert_eq!(config.chain, Some(NamedChain::Mainnet.into()));
486
487 let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "goerli"]);
488 assert_eq!(env.chain, Some(NamedChain::Goerli.into()));
489 }
490
491 #[cfg(feature = "base")]
492 #[test]
493 fn can_parse_namespaced_base_hardfork() {
494 let args = EvmArgs::parse_from(["foundry-cli", "--hardfork", "base:Beryl"]);
495 assert_eq!(args.hardfork.map(String::from).as_deref(), Some("base:Beryl"));
496
497 let config = Config::from_provider(Config::figment().merge(args)).unwrap();
498 assert!(config.networks.is_base());
499 assert_eq!(config.hardfork.map(String::from).as_deref(), Some("base:Beryl"));
500 }
501
502 #[test]
503 fn hardfork_arg_selects_network() {
504 let args = EvmArgs::parse_from(["foundry-cli", "--hardfork", "tempo:T5"]);
505 let hardfork = "tempo:T5".parse::<FoundryHardfork>().unwrap();
506 assert_eq!(args.hardfork, Some(hardfork));
507
508 let config = Config::from_provider(Config::figment().merge(args)).unwrap();
509 assert_eq!(config.hardfork, Some(hardfork));
510 assert!(config.networks.is_tempo());
511 }
512
513 #[test]
514 fn test_memory_limit() {
515 let args = EvmArgs {
516 env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
517 ..Default::default()
518 };
519 let config = Config::from_provider(Config::figment().merge(args)).unwrap();
520 assert_eq!(config.memory_limit, Config::default().memory_limit);
521
522 let env = EnvArgs::parse_from(["foundry-cli", "--memory-limit", "100"]);
523 assert_eq!(env.memory_limit, Some(100));
524 }
525
526 #[test]
527 fn test_chain_id() {
528 let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "1"]);
529 assert_eq!(env.chain, Some(Chain::mainnet()));
530
531 let env = EnvArgs::parse_from(["foundry-cli", "--chain-id", "mainnet"]);
532 assert_eq!(env.chain, Some(Chain::mainnet()));
533 let args = EvmArgs { env, ..Default::default() };
534 let config = Config::from_provider(Config::figment().merge(args)).unwrap();
535 assert_eq!(config.chain, Some(Chain::mainnet()));
536 }
537}