foundry_common/
evm.rs

1//! CLI arguments for configuring the EVM settings.
2
3use alloy_primitives::{Address, B256, U256, map::HashMap};
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 crate::shell;
17
18/// Map keyed by breakpoints char to their location (contract address, pc)
19pub type Breakpoints = HashMap<char, (Address, usize)>;
20
21/// `EvmArgs` and `EnvArgs` take the highest precedence in the Config/Figment hierarchy.
22///
23/// All vars are opt-in, their default values are expected to be set by the
24/// [`foundry_config::Config`], and are always present ([`foundry_config::Config::default`])
25///
26/// Both have corresponding types in the `evm_adapters` crate which have mandatory fields.
27/// The expected workflow is
28///   1. load the [`foundry_config::Config`]
29///   2. merge with `EvmArgs` into a `figment::Figment`
30///   3. extract `evm_adapters::Opts` from the merged `Figment`
31///
32/// # Example
33///
34/// ```ignore
35/// use foundry_config::Config;
36/// use forge::executor::opts::EvmOpts;
37/// use foundry_common::evm::EvmArgs;
38/// # fn t(args: EvmArgs) {
39/// let figment = Config::figment_with_root(".").merge(args);
40/// let opts = figment.extract::<EvmOpts>().unwrap();
41/// # }
42/// ```
43#[derive(Clone, Debug, Default, Serialize, Parser)]
44#[command(next_help_heading = "EVM options", about = None, long_about = None)] // override doc
45pub struct EvmArgs {
46    /// Fetch state over a remote endpoint instead of starting from an empty state.
47    ///
48    /// If you want to fetch state from a specific block number, see --fork-block-number.
49    #[arg(long, short, visible_alias = "rpc-url", value_name = "URL")]
50    #[serde(rename = "eth_rpc_url", skip_serializing_if = "Option::is_none")]
51    pub fork_url: Option<String>,
52
53    /// Fetch state from a specific block number over a remote endpoint.
54    ///
55    /// See --fork-url.
56    #[arg(long, requires = "fork_url", value_name = "BLOCK")]
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub fork_block_number: Option<u64>,
59
60    /// Number of retries.
61    ///
62    /// See --fork-url.
63    #[arg(long, requires = "fork_url", value_name = "RETRIES")]
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub fork_retries: Option<u32>,
66
67    /// Initial retry backoff on encountering errors.
68    ///
69    /// See --fork-url.
70    #[arg(long, requires = "fork_url", value_name = "BACKOFF")]
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub fork_retry_backoff: Option<u64>,
73
74    /// Explicitly disables the use of RPC caching.
75    ///
76    /// All storage slots are read entirely from the endpoint.
77    ///
78    /// This flag overrides the project's configuration file.
79    ///
80    /// See --fork-url.
81    #[arg(long)]
82    #[serde(skip)]
83    pub no_storage_caching: bool,
84
85    /// The initial balance of deployed test contracts.
86    #[arg(long, value_name = "BALANCE")]
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub initial_balance: Option<U256>,
89
90    /// The address which will be executing tests/scripts.
91    #[arg(long, value_name = "ADDRESS")]
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub sender: Option<Address>,
94
95    /// Enable the FFI cheatcode.
96    #[arg(long)]
97    #[serde(skip)]
98    pub ffi: bool,
99
100    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
101    #[arg(long)]
102    #[serde(skip)]
103    pub always_use_create_2_factory: bool,
104
105    /// The CREATE2 deployer address to use, this will override the one in the config.
106    #[arg(long, value_name = "ADDRESS")]
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub create2_deployer: Option<Address>,
109
110    /// Sets the number of assumed available compute units per second for this provider
111    ///
112    /// default value: 330
113    ///
114    /// See also --fork-url and <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
115    #[arg(long, alias = "cups", value_name = "CUPS", help_heading = "Fork config")]
116    pub compute_units_per_second: Option<u64>,
117
118    /// Disables rate limiting for this node's provider.
119    ///
120    /// See also --fork-url and <https://docs.alchemy.com/reference/compute-units#what-are-cups-compute-units-per-second>
121    #[arg(
122        long,
123        value_name = "NO_RATE_LIMITS",
124        help_heading = "Fork config",
125        visible_alias = "no-rate-limit"
126    )]
127    #[serde(skip)]
128    pub no_rpc_rate_limit: bool,
129
130    /// All ethereum environment related arguments
131    #[command(flatten)]
132    #[serde(flatten)]
133    pub env: EnvArgs,
134
135    /// Whether to enable isolation of calls.
136    /// In isolation mode all top-level calls are executed as a separate transaction in a separate
137    /// EVM context, enabling more precise gas accounting and transaction state changes.
138    #[arg(long)]
139    #[serde(skip)]
140    pub isolate: bool,
141
142    /// Whether to enable Odyssey features.
143    #[arg(long, alias = "alphanet")]
144    #[serde(skip)]
145    pub odyssey: bool,
146}
147
148// Make this set of options a `figment::Provider` so that it can be merged into the `Config`
149impl Provider for EvmArgs {
150    fn metadata(&self) -> Metadata {
151        Metadata::named("Evm Opts Provider")
152    }
153
154    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
155        let value = Value::serialize(self)?;
156        let error = InvalidType(value.to_actual(), "map".into());
157        let mut dict = value.into_dict().ok_or(error)?;
158
159        if shell::verbosity() > 0 {
160            // need to merge that manually otherwise `from_occurrences` does not work
161            dict.insert("verbosity".to_string(), shell::verbosity().into());
162        }
163
164        if self.ffi {
165            dict.insert("ffi".to_string(), self.ffi.into());
166        }
167
168        if self.isolate {
169            dict.insert("isolate".to_string(), self.isolate.into());
170        }
171
172        if self.odyssey {
173            dict.insert("odyssey".to_string(), self.odyssey.into());
174        }
175
176        if self.always_use_create_2_factory {
177            dict.insert(
178                "always_use_create_2_factory".to_string(),
179                self.always_use_create_2_factory.into(),
180            );
181        }
182
183        if self.no_storage_caching {
184            dict.insert("no_storage_caching".to_string(), self.no_storage_caching.into());
185        }
186
187        if self.no_rpc_rate_limit {
188            dict.insert("no_rpc_rate_limit".to_string(), self.no_rpc_rate_limit.into());
189        }
190
191        if let Some(fork_url) = &self.fork_url {
192            dict.insert("eth_rpc_url".to_string(), fork_url.clone().into());
193        }
194
195        Ok(Map::from([(Config::selected_profile(), dict)]))
196    }
197}
198
199/// Configures the executor environment during tests.
200#[derive(Clone, Debug, Default, Serialize, Parser)]
201#[command(next_help_heading = "Executor environment config")]
202pub struct EnvArgs {
203    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests. By
204    /// default, it is 0x6000 (~25kb).
205    #[arg(long, value_name = "CODE_SIZE")]
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub code_size_limit: Option<usize>,
208
209    /// The chain name or EIP-155 chain ID.
210    #[arg(long, visible_alias = "chain-id", value_name = "CHAIN")]
211    #[serde(rename = "chain_id", skip_serializing_if = "Option::is_none", serialize_with = "id")]
212    pub chain: Option<Chain>,
213
214    /// The gas price.
215    #[arg(long, value_name = "GAS_PRICE")]
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub gas_price: Option<u64>,
218
219    /// The base fee in a block.
220    #[arg(long, visible_alias = "base-fee", value_name = "FEE")]
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub block_base_fee_per_gas: Option<u64>,
223
224    /// The transaction origin.
225    #[arg(long, value_name = "ADDRESS")]
226    #[serde(skip_serializing_if = "Option::is_none")]
227    pub tx_origin: Option<Address>,
228
229    /// The coinbase of the block.
230    #[arg(long, value_name = "ADDRESS")]
231    #[serde(skip_serializing_if = "Option::is_none")]
232    pub block_coinbase: Option<Address>,
233
234    /// The timestamp of the block.
235    #[arg(long, value_name = "TIMESTAMP")]
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub block_timestamp: Option<u64>,
238
239    /// The block number.
240    #[arg(long, value_name = "BLOCK")]
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub block_number: Option<u64>,
243
244    /// The block difficulty.
245    #[arg(long, value_name = "DIFFICULTY")]
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub block_difficulty: Option<u64>,
248
249    /// The block prevrandao value. NOTE: Before merge this field was mix_hash.
250    #[arg(long, value_name = "PREVRANDAO")]
251    #[serde(skip_serializing_if = "Option::is_none")]
252    pub block_prevrandao: Option<B256>,
253
254    /// The block gas limit.
255    #[arg(long, visible_alias = "gas-limit", value_name = "GAS_LIMIT")]
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub block_gas_limit: Option<u64>,
258
259    /// The memory limit per EVM execution in bytes.
260    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
261    ///
262    /// The default is 128MiB.
263    #[arg(long, value_name = "MEMORY_LIMIT")]
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub memory_limit: Option<u64>,
266
267    /// Whether to disable the block gas limit checks.
268    #[arg(long, visible_alias = "no-gas-limit")]
269    #[serde(skip_serializing_if = "std::ops::Not::not")]
270    pub disable_block_gas_limit: bool,
271}
272
273impl EvmArgs {
274    /// Ensures that fork url exists and returns its reference.
275    pub fn ensure_fork_url(&self) -> eyre::Result<&String> {
276        self.fork_url.as_ref().wrap_err("Missing `--fork-url` field.")
277    }
278}
279
280/// We have to serialize chain IDs and not names because when extracting an EVM `Env`, it expects
281/// `chain_id` to be `u64`.
282fn id<S: serde::Serializer>(chain: &Option<Chain>, s: S) -> Result<S::Ok, S::Error> {
283    if let Some(chain) = chain {
284        s.serialize_u64(chain.id())
285    } else {
286        // skip_serializing_if = "Option::is_none" should prevent this branch from being taken
287        unreachable!()
288    }
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294    use foundry_config::NamedChain;
295
296    #[test]
297    fn can_parse_chain_id() {
298        let args = EvmArgs {
299            env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
300            ..Default::default()
301        };
302        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
303        assert_eq!(config.chain, Some(NamedChain::Mainnet.into()));
304
305        let env = EnvArgs::parse_from(["foundry-common", "--chain-id", "goerli"]);
306        assert_eq!(env.chain, Some(NamedChain::Goerli.into()));
307    }
308
309    #[test]
310    fn test_memory_limit() {
311        let args = EvmArgs {
312            env: EnvArgs { chain: Some(NamedChain::Mainnet.into()), ..Default::default() },
313            ..Default::default()
314        };
315        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
316        assert_eq!(config.memory_limit, Config::default().memory_limit);
317
318        let env = EnvArgs::parse_from(["foundry-common", "--memory-limit", "100"]);
319        assert_eq!(env.memory_limit, Some(100));
320    }
321
322    #[test]
323    fn test_chain_id() {
324        let env = EnvArgs::parse_from(["foundry-common", "--chain-id", "1"]);
325        assert_eq!(env.chain, Some(Chain::mainnet()));
326
327        let env = EnvArgs::parse_from(["foundry-common", "--chain-id", "mainnet"]);
328        assert_eq!(env.chain, Some(Chain::mainnet()));
329        let args = EvmArgs { env, ..Default::default() };
330        let config = Config::from_provider(Config::figment().merge(args)).unwrap();
331        assert_eq!(config.chain, Some(Chain::mainnet()));
332    }
333}