Skip to content

Commit cd0e9de

Browse files
authored
refactor(cli): move EvmArgs to foundry_cli (#11741)
1 parent d7f8b36 commit cd0e9de

File tree

7 files changed

+368
-368
lines changed

7 files changed

+368
-368
lines changed

crates/chisel/src/opts.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
use clap::{Parser, Subcommand};
2-
use foundry_cli::opts::{BuildOpts, GlobalArgs};
3-
use foundry_common::{
4-
evm::EvmArgs,
5-
version::{LONG_VERSION, SHORT_VERSION},
6-
};
2+
use foundry_cli::opts::{BuildOpts, EvmArgs, GlobalArgs};
3+
use foundry_common::version::{LONG_VERSION, SHORT_VERSION};
74
use std::path::PathBuf;
85

96
foundry_config::impl_figment_convert!(Chisel, build, evm);

crates/cli/src/opts/evm.rs

Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
//! CLI arguments for configuring the EVM settings.
2+
3+
use alloy_primitives::{Address, B256, U256};
4+
use clap::Parser;
5+
use eyre::ContextCompat;
6+
use foundry_config::{
7+
Chain, Config,
8+
figment::{
9+
self, Metadata, Profile, Provider,
10+
error::Kind::InvalidType,
11+
value::{Dict, Map, Value},
12+
},
13+
};
14+
use serde::Serialize;
15+
16+
use 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
42+
pub 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`
147+
impl 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")]
200+
pub 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+
276+
impl 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`.
285+
fn 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)]
295+
mod 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+
}

crates/cli/src/opts/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
mod build;
22
mod chain;
33
mod dependency;
4+
mod evm;
45
mod global;
56
mod rpc;
67
mod transaction;
78

89
pub use build::*;
910
pub use chain::*;
1011
pub use dependency::*;
12+
pub use evm::*;
1113
pub use global::*;
1214
pub use rpc::*;
1315
pub use transaction::*;

0 commit comments

Comments
 (0)