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