foundry_common/
evm.rs

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