foundry_config/
lib.rs

1//! # foundry-config
2//!
3//! Foundry configuration.
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use crate::cache::StorageCachingConfig;
12use alloy_primitives::{Address, B256, FixedBytes, U256, address, map::AddressHashMap};
13use eyre::{ContextCompat, WrapErr};
14use figment::{
15    Error, Figment, Metadata, Profile, Provider,
16    providers::{Env, Format, Serialized, Toml},
17    value::{Dict, Map, Value},
18};
19use filter::GlobMatcher;
20use foundry_compilers::{
21    ArtifactOutput, ConfigurableArtifacts, Graph, Project, ProjectPathsConfig,
22    RestrictionsWithVersion, VyperLanguage,
23    artifacts::{
24        BytecodeHash, DebuggingSettings, EvmVersion, Libraries, ModelCheckerSettings,
25        ModelCheckerTarget, Optimizer, OptimizerDetails, RevertStrings, Settings, SettingsMetadata,
26        Severity,
27        output_selection::{ContractOutputSelection, OutputSelection},
28        remappings::{RelativeRemapping, Remapping},
29        serde_helpers,
30    },
31    cache::SOLIDITY_FILES_CACHE_FILENAME,
32    compilers::{
33        Compiler,
34        multi::{MultiCompiler, MultiCompilerSettings},
35        solc::{Solc, SolcCompiler},
36        vyper::{Vyper, VyperSettings},
37    },
38    error::SolcError,
39    multi::{MultiCompilerParser, MultiCompilerRestrictions},
40    solc::{CliSettings, SolcSettings},
41};
42use regex::Regex;
43use revm::primitives::hardfork::SpecId;
44use semver::Version;
45use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
46use std::{
47    borrow::Cow,
48    collections::BTreeMap,
49    fs,
50    path::{Path, PathBuf},
51    str::FromStr,
52};
53
54mod macros;
55
56pub mod utils;
57pub use utils::*;
58
59mod endpoints;
60pub use endpoints::{
61    ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
62};
63
64mod etherscan;
65pub use etherscan::EtherscanConfigError;
66use etherscan::{EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig};
67
68pub mod resolve;
69pub use resolve::UnresolvedEnvVarError;
70
71pub mod cache;
72use cache::{Cache, ChainCache};
73
74pub mod fmt;
75pub use fmt::FormatterConfig;
76
77pub mod lint;
78pub use lint::{LinterConfig, Severity as LintSeverity};
79
80pub mod fs_permissions;
81pub use fs_permissions::FsPermissions;
82use fs_permissions::PathPermission;
83
84pub mod error;
85use error::ExtractConfigError;
86pub use error::SolidityErrorCode;
87
88pub mod doc;
89pub use doc::DocConfig;
90
91pub mod filter;
92pub use filter::SkipBuildFilters;
93
94mod warning;
95pub use warning::*;
96
97pub mod fix;
98
99// reexport so cli types can implement `figment::Provider` to easily merge compiler arguments
100pub use alloy_chains::{Chain, NamedChain};
101pub use figment;
102
103pub mod providers;
104pub use providers::Remappings;
105use providers::*;
106
107mod fuzz;
108pub use fuzz::{FuzzConfig, FuzzCorpusConfig, FuzzDictionaryConfig};
109
110mod invariant;
111pub use invariant::InvariantConfig;
112
113mod inline;
114pub use inline::{InlineConfig, InlineConfigError, NatSpec};
115
116pub mod soldeer;
117use soldeer::{SoldeerConfig, SoldeerDependencyConfig};
118
119mod vyper;
120pub use vyper::VyperConfig;
121
122mod bind_json;
123use bind_json::BindJsonConfig;
124
125mod compilation;
126pub use compilation::{CompilationRestrictions, SettingsOverrides};
127
128pub mod extend;
129use extend::Extends;
130
131pub use semver;
132
133/// Foundry configuration
134///
135/// # Defaults
136///
137/// All configuration values have a default, documented in the [fields](#fields)
138/// section below. [`Config::default()`] returns the default values for
139/// the default profile while [`Config::with_root()`] returns the values based on the given
140/// directory. [`Config::load()`] starts with the default profile and merges various providers into
141/// the config, same for [`Config::load_with_root()`], but there the default values are determined
142/// by [`Config::with_root()`]
143///
144/// # Provider Details
145///
146/// `Config` is a Figment [`Provider`] with the following characteristics:
147///
148///   * **Profile**
149///
150///     The profile is set to the value of the `profile` field.
151///
152///   * **Metadata**
153///
154///     This provider is named `Foundry Config`. It does not specify a
155///     [`Source`](figment::Source) and uses default interpolation.
156///
157///   * **Data**
158///
159///     The data emitted by this provider are the keys and values corresponding
160///     to the fields and values of the structure. The dictionary is emitted to
161///     the "default" meta-profile.
162///
163/// Note that these behaviors differ from those of [`Config::figment()`].
164#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
165pub struct Config {
166    /// The selected profile. **(default: _default_ `default`)**
167    ///
168    /// **Note:** This field is never serialized nor deserialized. When a
169    /// `Config` is merged into a `Figment` as a `Provider`, this profile is
170    /// selected on the `Figment`. When a `Config` is extracted, this field is
171    /// set to the extracting Figment's selected `Profile`.
172    #[serde(skip)]
173    pub profile: Profile,
174    /// The list of all profiles defined in the config.
175    ///
176    /// See `profile`.
177    #[serde(skip)]
178    pub profiles: Vec<Profile>,
179
180    /// The root path where the config detection started from, [`Config::with_root`].
181    // We're skipping serialization here, so it won't be included in the [`Config::to_string()`]
182    // representation, but will be deserialized from the `Figment` so that forge commands can
183    // override it.
184    #[serde(default = "root_default", skip_serializing)]
185    pub root: PathBuf,
186
187    /// Configuration for extending from another foundry.toml (base) file.
188    ///
189    /// Can be either a string path or an object with path and strategy.
190    /// Base files cannot extend (inherit) other files.
191    #[serde(default, skip_serializing)]
192    pub extends: Option<Extends>,
193
194    /// path of the source contracts dir, like `src` or `contracts`
195    pub src: PathBuf,
196    /// path of the test dir
197    pub test: PathBuf,
198    /// path of the script dir
199    pub script: PathBuf,
200    /// path to where artifacts shut be written to
201    pub out: PathBuf,
202    /// all library folders to include, `lib`, `node_modules`
203    pub libs: Vec<PathBuf>,
204    /// `Remappings` to use for this repo
205    pub remappings: Vec<RelativeRemapping>,
206    /// Whether to autodetect remappings by scanning the `libs` folders recursively
207    pub auto_detect_remappings: bool,
208    /// library addresses to link
209    pub libraries: Vec<String>,
210    /// whether to enable cache
211    pub cache: bool,
212    /// whether to dynamically link tests
213    pub dynamic_test_linking: bool,
214    /// where the cache is stored if enabled
215    pub cache_path: PathBuf,
216    /// where the gas snapshots are stored
217    pub snapshots: PathBuf,
218    /// whether to check for differences against previously stored gas snapshots
219    pub gas_snapshot_check: bool,
220    /// whether to emit gas snapshots to disk
221    pub gas_snapshot_emit: bool,
222    /// where the broadcast logs are stored
223    pub broadcast: PathBuf,
224    /// additional solc allow paths for `--allow-paths`
225    pub allow_paths: Vec<PathBuf>,
226    /// additional solc include paths for `--include-path`
227    pub include_paths: Vec<PathBuf>,
228    /// glob patterns to skip
229    pub skip: Vec<GlobMatcher>,
230    /// whether to force a `project.clean()`
231    pub force: bool,
232    /// evm version to use
233    #[serde(with = "from_str_lowercase")]
234    pub evm_version: EvmVersion,
235    /// list of contracts to report gas of
236    pub gas_reports: Vec<String>,
237    /// list of contracts to ignore for gas reports
238    pub gas_reports_ignore: Vec<String>,
239    /// Whether to include gas reports for tests.
240    pub gas_reports_include_tests: bool,
241    /// The Solc instance to use if any.
242    ///
243    /// This takes precedence over `auto_detect_solc`, if a version is set then this overrides
244    /// auto-detection.
245    ///
246    /// **Note** for backwards compatibility reasons this also accepts solc_version from the toml
247    /// file, see `BackwardsCompatTomlProvider`.
248    ///
249    /// Avoid using this field directly; call the related `solc` methods instead.
250    #[doc(hidden)]
251    pub solc: Option<SolcReq>,
252    /// Whether to autodetect the solc compiler version to use.
253    pub auto_detect_solc: bool,
254    /// Offline mode, if set, network access (downloading solc) is disallowed.
255    ///
256    /// Relationship with `auto_detect_solc`:
257    ///    - if `auto_detect_solc = true` and `offline = true`, the required solc version(s) will
258    ///      be auto detected but if the solc version is not installed, it will _not_ try to
259    ///      install it
260    pub offline: bool,
261    /// Whether to activate optimizer
262    pub optimizer: Option<bool>,
263    /// The number of runs specifies roughly how often each opcode of the deployed code will be
264    /// executed across the life-time of the contract. This means it is a trade-off parameter
265    /// between code size (deploy cost) and code execution cost (cost after deployment).
266    /// An `optimizer_runs` parameter of `1` will produce short but expensive code. In contrast, a
267    /// larger `optimizer_runs` parameter will produce longer but more gas efficient code. The
268    /// maximum value of the parameter is `2**32-1`.
269    ///
270    /// A common misconception is that this parameter specifies the number of iterations of the
271    /// optimizer. This is not true: The optimizer will always run as many times as it can
272    /// still improve the code.
273    pub optimizer_runs: Option<usize>,
274    /// Switch optimizer components on or off in detail.
275    /// The "enabled" switch above provides two defaults which can be
276    /// tweaked here. If "details" is given, "enabled" can be omitted.
277    pub optimizer_details: Option<OptimizerDetails>,
278    /// Model checker settings.
279    pub model_checker: Option<ModelCheckerSettings>,
280    /// verbosity to use
281    pub verbosity: u8,
282    /// url of the rpc server that should be used for any rpc calls
283    pub eth_rpc_url: Option<String>,
284    /// Whether to accept invalid certificates for the rpc server.
285    pub eth_rpc_accept_invalid_certs: bool,
286    /// JWT secret that should be used for any rpc calls
287    pub eth_rpc_jwt: Option<String>,
288    /// Timeout that should be used for any rpc calls
289    pub eth_rpc_timeout: Option<u64>,
290    /// Headers that should be used for any rpc calls
291    ///
292    /// # Example
293    ///
294    /// rpc_headers = ["x-custom-header:value", "x-another-header:another-value"]
295    ///
296    /// You can also the ETH_RPC_HEADERS env variable like so:
297    /// `ETH_RPC_HEADERS="x-custom-header:value x-another-header:another-value"`
298    pub eth_rpc_headers: Option<Vec<String>>,
299    /// etherscan API key, or alias for an `EtherscanConfig` in `etherscan` table
300    pub etherscan_api_key: Option<String>,
301    /// Multiple etherscan api configs and their aliases
302    #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
303    pub etherscan: EtherscanConfigs,
304    /// list of solidity error codes to always silence in the compiler output
305    pub ignored_error_codes: Vec<SolidityErrorCode>,
306    /// list of file paths to ignore
307    #[serde(rename = "ignored_warnings_from")]
308    pub ignored_file_paths: Vec<PathBuf>,
309    /// Diagnostic level (minimum) at which the process should finish with a non-zero exit.
310    pub deny: DenyLevel,
311    /// DEPRECATED: use `deny` instead.
312    #[serde(default, skip_serializing)]
313    pub deny_warnings: bool,
314    /// Only run test functions matching the specified regex pattern.
315    #[serde(rename = "match_test")]
316    pub test_pattern: Option<RegexWrapper>,
317    /// Only run test functions that do not match the specified regex pattern.
318    #[serde(rename = "no_match_test")]
319    pub test_pattern_inverse: Option<RegexWrapper>,
320    /// Only run tests in contracts matching the specified regex pattern.
321    #[serde(rename = "match_contract")]
322    pub contract_pattern: Option<RegexWrapper>,
323    /// Only run tests in contracts that do not match the specified regex pattern.
324    #[serde(rename = "no_match_contract")]
325    pub contract_pattern_inverse: Option<RegexWrapper>,
326    /// Only run tests in source files matching the specified glob pattern.
327    #[serde(rename = "match_path", with = "from_opt_glob")]
328    pub path_pattern: Option<globset::Glob>,
329    /// Only run tests in source files that do not match the specified glob pattern.
330    #[serde(rename = "no_match_path", with = "from_opt_glob")]
331    pub path_pattern_inverse: Option<globset::Glob>,
332    /// Only show coverage for files that do not match the specified regex pattern.
333    #[serde(rename = "no_match_coverage")]
334    pub coverage_pattern_inverse: Option<RegexWrapper>,
335    /// Path where last test run failures are recorded.
336    pub test_failures_file: PathBuf,
337    /// Max concurrent threads to use.
338    pub threads: Option<usize>,
339    /// Whether to show test execution progress.
340    pub show_progress: bool,
341    /// Configuration for fuzz testing
342    pub fuzz: FuzzConfig,
343    /// Configuration for invariant testing
344    pub invariant: InvariantConfig,
345    /// Whether to allow ffi cheatcodes in test
346    pub ffi: bool,
347    /// Whether to allow `expectRevert` for internal functions.
348    pub allow_internal_expect_revert: bool,
349    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
350    pub always_use_create_2_factory: bool,
351    /// Sets a timeout in seconds for vm.prompt cheatcodes
352    pub prompt_timeout: u64,
353    /// The address which will be executing all tests
354    pub sender: Address,
355    /// The tx.origin value during EVM execution
356    pub tx_origin: Address,
357    /// the initial balance of each deployed test contract
358    pub initial_balance: U256,
359    /// the block.number value during EVM execution
360    #[serde(
361        deserialize_with = "crate::deserialize_u64_to_u256",
362        serialize_with = "crate::serialize_u64_or_u256"
363    )]
364    pub block_number: U256,
365    /// pins the block number for the state fork
366    pub fork_block_number: Option<u64>,
367    /// The chain name or EIP-155 chain ID.
368    #[serde(rename = "chain_id", alias = "chain")]
369    pub chain: Option<Chain>,
370    /// Block gas limit.
371    pub gas_limit: GasLimit,
372    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests.
373    pub code_size_limit: Option<usize>,
374    /// `tx.gasprice` value during EVM execution.
375    ///
376    /// This is an Option, so we can determine in fork mode whether to use the config's gas price
377    /// (if set by user) or the remote client's gas price.
378    pub gas_price: Option<u64>,
379    /// The base fee in a block.
380    pub block_base_fee_per_gas: u64,
381    /// The `block.coinbase` value during EVM execution.
382    pub block_coinbase: Address,
383    /// The `block.timestamp` value during EVM execution.
384    #[serde(
385        deserialize_with = "crate::deserialize_u64_to_u256",
386        serialize_with = "crate::serialize_u64_or_u256"
387    )]
388    pub block_timestamp: U256,
389    /// The `block.difficulty` value during EVM execution.
390    pub block_difficulty: u64,
391    /// Before merge the `block.max_hash`, after merge it is `block.prevrandao`.
392    pub block_prevrandao: B256,
393    /// The `block.gaslimit` value during EVM execution.
394    pub block_gas_limit: Option<GasLimit>,
395    /// The memory limit per EVM execution in bytes.
396    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
397    ///
398    /// The default is 128MiB.
399    pub memory_limit: u64,
400    /// Additional output selection for all contracts, such as "ir", "devdoc", "storageLayout",
401    /// etc.
402    ///
403    /// See the [Solc Compiler Api](https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-api) for more information.
404    ///
405    /// The following values are always set because they're required by `forge`:
406    /// ```json
407    /// {
408    ///   "*": [
409    ///       "abi",
410    ///       "evm.bytecode",
411    ///       "evm.deployedBytecode",
412    ///       "evm.methodIdentifiers"
413    ///     ]
414    /// }
415    /// ```
416    #[serde(default)]
417    pub extra_output: Vec<ContractOutputSelection>,
418    /// If set, a separate JSON file will be emitted for every contract depending on the
419    /// selection, eg. `extra_output_files = ["metadata"]` will create a `metadata.json` for
420    /// each contract in the project.
421    ///
422    /// See [Contract Metadata](https://docs.soliditylang.org/en/latest/metadata.html) for more information.
423    ///
424    /// The difference between `extra_output = ["metadata"]` and
425    /// `extra_output_files = ["metadata"]` is that the former will include the
426    /// contract's metadata in the contract's json artifact, whereas the latter will emit the
427    /// output selection as separate files.
428    #[serde(default)]
429    pub extra_output_files: Vec<ContractOutputSelection>,
430    /// Whether to print the names of the compiled contracts.
431    pub names: bool,
432    /// Whether to print the sizes of the compiled contracts.
433    pub sizes: bool,
434    /// If set to true, changes compilation pipeline to go through the Yul intermediate
435    /// representation.
436    pub via_ir: bool,
437    /// Whether to include the AST as JSON in the compiler output.
438    pub ast: bool,
439    /// RPC storage caching settings determines what chains and endpoints to cache
440    pub rpc_storage_caching: StorageCachingConfig,
441    /// Disables storage caching entirely. This overrides any settings made in
442    /// `rpc_storage_caching`
443    pub no_storage_caching: bool,
444    /// Disables rate limiting entirely. This overrides any settings made in
445    /// `compute_units_per_second`
446    pub no_rpc_rate_limit: bool,
447    /// Multiple rpc endpoints and their aliases
448    #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
449    pub rpc_endpoints: RpcEndpoints,
450    /// Whether to store the referenced sources in the metadata as literal data.
451    pub use_literal_content: bool,
452    /// Whether to include the metadata hash.
453    ///
454    /// The metadata hash is machine dependent. By default, this is set to [BytecodeHash::None] to allow for deterministic code, See: <https://docs.soliditylang.org/en/latest/metadata.html>
455    #[serde(with = "from_str_lowercase")]
456    pub bytecode_hash: BytecodeHash,
457    /// Whether to append the metadata hash to the bytecode.
458    ///
459    /// If this is `false` and the `bytecode_hash` option above is not `None` solc will issue a
460    /// warning.
461    pub cbor_metadata: bool,
462    /// How to treat revert (and require) reason strings.
463    #[serde(with = "serde_helpers::display_from_str_opt")]
464    pub revert_strings: Option<RevertStrings>,
465    /// Whether to compile in sparse mode
466    ///
467    /// If this option is enabled, only the required contracts/files will be selected to be
468    /// included in solc's output selection, see also [`OutputSelection`].
469    pub sparse_mode: bool,
470    /// Generates additional build info json files for every new build, containing the
471    /// `CompilerInput` and `CompilerOutput`.
472    pub build_info: bool,
473    /// The path to the `build-info` directory that contains the build info json files.
474    pub build_info_path: Option<PathBuf>,
475    /// Configuration for `forge fmt`
476    pub fmt: FormatterConfig,
477    /// Configuration for `forge lint`
478    pub lint: LinterConfig,
479    /// Configuration for `forge doc`
480    pub doc: DocConfig,
481    /// Configuration for `forge bind-json`
482    pub bind_json: BindJsonConfig,
483    /// Configures the permissions of cheat codes that touch the file system.
484    ///
485    /// This includes what operations can be executed (read, write)
486    pub fs_permissions: FsPermissions,
487
488    /// Whether to enable call isolation.
489    ///
490    /// Useful for more correct gas accounting and EVM behavior in general.
491    pub isolate: bool,
492
493    /// Whether to disable the block gas limit checks.
494    pub disable_block_gas_limit: bool,
495
496    /// Whether to enable the tx gas limit checks as imposed by Osaka (EIP-7825).
497    pub enable_tx_gas_limit: bool,
498
499    /// Address labels
500    pub labels: AddressHashMap<String>,
501
502    /// Whether to enable safety checks for `vm.getCode` and `vm.getDeployedCode` invocations.
503    /// If disabled, it is possible to access artifacts which were not recompiled or cached.
504    pub unchecked_cheatcode_artifacts: bool,
505
506    /// CREATE2 salt to use for the library deployment in scripts.
507    pub create2_library_salt: B256,
508
509    /// The CREATE2 deployer address to use.
510    pub create2_deployer: Address,
511
512    /// Configuration for Vyper compiler
513    pub vyper: VyperConfig,
514
515    /// Soldeer dependencies
516    pub dependencies: Option<SoldeerDependencyConfig>,
517
518    /// Soldeer custom configs
519    pub soldeer: Option<SoldeerConfig>,
520
521    /// Whether failed assertions should revert.
522    ///
523    /// Note that this only applies to native (cheatcode) assertions, invoked on Vm contract.
524    pub assertions_revert: bool,
525
526    /// Whether `failed()` should be invoked to check if the test have failed.
527    pub legacy_assertions: bool,
528
529    /// Optional additional CLI arguments to pass to `solc` binary.
530    #[serde(default, skip_serializing_if = "Vec::is_empty")]
531    pub extra_args: Vec<String>,
532
533    /// Whether to enable Celo precompiles.
534    #[serde(default)]
535    pub celo: bool,
536
537    /// Timeout for transactions in seconds.
538    pub transaction_timeout: u64,
539
540    /// Warnings gathered when loading the Config. See [`WarningsProvider`] for more information.
541    #[serde(rename = "__warnings", default, skip_serializing)]
542    pub warnings: Vec<Warning>,
543
544    /// Additional settings profiles to use when compiling.
545    #[serde(default)]
546    pub additional_compiler_profiles: Vec<SettingsOverrides>,
547
548    /// Restrictions on compilation of certain files.
549    #[serde(default)]
550    pub compilation_restrictions: Vec<CompilationRestrictions>,
551
552    /// Whether to enable script execution protection.
553    pub script_execution_protection: bool,
554
555    /// PRIVATE: This structure may grow, As such, constructing this structure should
556    /// _always_ be done using a public constructor or update syntax:
557    ///
558    /// ```ignore
559    /// use foundry_config::Config;
560    ///
561    /// let config = Config { src: "other".into(), ..Default::default() };
562    /// ```
563    #[doc(hidden)]
564    #[serde(skip)]
565    pub _non_exhaustive: (),
566}
567
568/// Diagnostic level (minimum) at which the process should finish with a non-zero exit.
569#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Default, Serialize)]
570#[serde(rename_all = "lowercase")]
571pub enum DenyLevel {
572    /// Always exit with zero code.
573    #[default]
574    Never,
575    /// Exit with a non-zero code if any warnings are found.
576    Warnings,
577    /// Exit with a non-zero code if any notes or warnings are found.
578    Notes,
579}
580
581// Custom deserialization to make `DenyLevel` parsing case-insensitive and backwards compatible with
582// booleans.
583impl<'de> Deserialize<'de> for DenyLevel {
584    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
585    where
586        D: Deserializer<'de>,
587    {
588        struct DenyLevelVisitor;
589
590        impl<'de> de::Visitor<'de> for DenyLevelVisitor {
591            type Value = DenyLevel;
592
593            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
594                formatter.write_str("one of the following strings: `never`, `warnings`, `notes`")
595            }
596
597            fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
598            where
599                E: de::Error,
600            {
601                Ok(DenyLevel::from(value))
602            }
603
604            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
605            where
606                E: de::Error,
607            {
608                DenyLevel::from_str(value).map_err(de::Error::custom)
609            }
610        }
611
612        deserializer.deserialize_any(DenyLevelVisitor)
613    }
614}
615
616impl FromStr for DenyLevel {
617    type Err = String;
618
619    fn from_str(s: &str) -> Result<Self, Self::Err> {
620        match s.to_lowercase().as_str() {
621            "warnings" | "warning" | "w" => Ok(Self::Warnings),
622            "notes" | "note" | "n" => Ok(Self::Notes),
623            "never" | "false" | "f" => Ok(Self::Never),
624            _ => Err(format!(
625                "unknown variant: found `{s}`, expected one of `never`, `warnings`, `notes`"
626            )),
627        }
628    }
629}
630
631impl From<bool> for DenyLevel {
632    fn from(deny: bool) -> Self {
633        if deny { Self::Warnings } else { Self::Never }
634    }
635}
636
637impl DenyLevel {
638    /// Returns `true` if the deny level includes warnings.
639    pub fn warnings(&self) -> bool {
640        match self {
641            Self::Never => false,
642            Self::Warnings | Self::Notes => true,
643        }
644    }
645
646    /// Returns `true` if the deny level includes notes.
647    pub fn notes(&self) -> bool {
648        match self {
649            Self::Never | Self::Warnings => false,
650            Self::Notes => true,
651        }
652    }
653
654    /// Returns `true` if the deny level is set to never (only errors).
655    pub fn never(&self) -> bool {
656        match self {
657            Self::Never => true,
658            Self::Warnings | Self::Notes => false,
659        }
660    }
661}
662
663/// Mapping of fallback standalone sections. See [`FallbackProfileProvider`].
664pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
665
666/// Deprecated keys and their replacements.
667///
668/// See [Warning::DeprecatedKey]
669pub const DEPRECATIONS: &[(&str, &str)] =
670    &[("cancun", "evm_version = Cancun"), ("deny_warnings", "deny = warnings")];
671
672impl Config {
673    /// The default profile: "default"
674    pub const DEFAULT_PROFILE: Profile = Profile::Default;
675
676    /// The hardhat profile: "hardhat"
677    pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
678
679    /// TOML section for profiles
680    pub const PROFILE_SECTION: &'static str = "profile";
681
682    /// Standalone sections in the config which get integrated into the selected profile
683    pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
684        "rpc_endpoints",
685        "etherscan",
686        "fmt",
687        "lint",
688        "doc",
689        "fuzz",
690        "invariant",
691        "labels",
692        "dependencies",
693        "soldeer",
694        "vyper",
695        "bind_json",
696    ];
697
698    /// File name of config toml file
699    pub const FILE_NAME: &'static str = "foundry.toml";
700
701    /// The name of the directory foundry reserves for itself under the user's home directory: `~`
702    pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
703
704    /// Default address for tx.origin
705    ///
706    /// `0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38`
707    pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
708
709    /// Default salt for create2 library deployments
710    pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
711
712    /// Default create2 deployer
713    pub const DEFAULT_CREATE2_DEPLOYER: Address =
714        address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");
715
716    /// Loads the `Config` from the current directory.
717    ///
718    /// See [`figment`](Self::figment) for more details.
719    pub fn load() -> Result<Self, ExtractConfigError> {
720        Self::from_provider(Self::figment())
721    }
722
723    /// Loads the `Config` with the given `providers` preset.
724    ///
725    /// See [`figment`](Self::figment) for more details.
726    pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
727        Self::from_provider(Self::default().to_figment(providers))
728    }
729
730    /// Loads the `Config` from the given root directory.
731    ///
732    /// See [`figment_with_root`](Self::figment_with_root) for more details.
733    #[track_caller]
734    pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
735        Self::from_provider(Self::figment_with_root(root.as_ref()))
736    }
737
738    /// Attempts to extract a `Config` from `provider`, returning the result.
739    ///
740    /// # Example
741    ///
742    /// ```rust
743    /// use figment::providers::{Env, Format, Toml};
744    /// use foundry_config::Config;
745    ///
746    /// // Use foundry's default `Figment`, but allow values from `other.toml`
747    /// // to supersede its values.
748    /// let figment = Config::figment().merge(Toml::file("other.toml").nested());
749    ///
750    /// let config = Config::from_provider(figment);
751    /// ```
752    #[doc(alias = "try_from")]
753    pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
754        trace!("load config with provider: {:?}", provider.metadata());
755        Self::from_figment(Figment::from(provider))
756    }
757
758    #[doc(hidden)]
759    #[deprecated(note = "use `Config::from_provider` instead")]
760    pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
761        Self::from_provider(provider)
762    }
763
764    fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
765        let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
766        config.profile = figment.profile().clone();
767
768        // The `"profile"` profile contains all the profiles as keys.
769        let mut add_profile = |profile: &Profile| {
770            if !config.profiles.contains(profile) {
771                config.profiles.push(profile.clone());
772            }
773        };
774        let figment = figment.select(Self::PROFILE_SECTION);
775        if let Ok(data) = figment.data()
776            && let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION))
777        {
778            for profile in profiles.keys() {
779                add_profile(&Profile::new(profile));
780            }
781        }
782        add_profile(&Self::DEFAULT_PROFILE);
783        add_profile(&config.profile);
784
785        config.normalize_optimizer_settings();
786
787        Ok(config)
788    }
789
790    /// Returns the populated [Figment] using the requested [FigmentProviders] preset.
791    ///
792    /// This will merge various providers, such as env,toml,remappings into the figment if
793    /// requested.
794    pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
795        // Note that `Figment::from` here is a method on `Figment` rather than the `From` impl below
796
797        if providers.is_none() {
798            return Figment::from(self);
799        }
800
801        let root = self.root.as_path();
802        let profile = Self::selected_profile();
803        let mut figment = Figment::default().merge(DappHardhatDirProvider(root));
804
805        // merge global foundry.toml file
806        if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
807            figment = Self::merge_toml_provider(
808                figment,
809                TomlFileProvider::new(None, global_toml),
810                profile.clone(),
811            );
812        }
813        // merge local foundry.toml file
814        figment = Self::merge_toml_provider(
815            figment,
816            TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)),
817            profile.clone(),
818        );
819
820        // merge environment variables
821        figment = figment
822            .merge(
823                Env::prefixed("DAPP_")
824                    .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
825                    .global(),
826            )
827            .merge(
828                Env::prefixed("DAPP_TEST_")
829                    .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
830                    .global(),
831            )
832            .merge(DappEnvCompatProvider)
833            .merge(EtherscanEnvProvider::default())
834            .merge(
835                Env::prefixed("FOUNDRY_")
836                    .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
837                    .map(|key| {
838                        let key = key.as_str();
839                        if Self::STANDALONE_SECTIONS.iter().any(|section| {
840                            key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
841                        }) {
842                            key.replacen('_', ".", 1).into()
843                        } else {
844                            key.into()
845                        }
846                    })
847                    .global(),
848            )
849            .select(profile.clone());
850
851        // only resolve remappings if all providers are requested
852        if providers.is_all() {
853            // we try to merge remappings after we've merged all other providers, this prevents
854            // redundant fs lookups to determine the default remappings that are eventually updated
855            // by other providers, like the toml file
856            let remappings = RemappingsProvider {
857                auto_detect_remappings: figment
858                    .extract_inner::<bool>("auto_detect_remappings")
859                    .unwrap_or(true),
860                lib_paths: figment
861                    .extract_inner::<Vec<PathBuf>>("libs")
862                    .map(Cow::Owned)
863                    .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
864                root,
865                remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
866            };
867            figment = figment.merge(remappings);
868        }
869
870        // normalize defaults
871        figment = self.normalize_defaults(figment);
872
873        Figment::from(self).merge(figment).select(profile)
874    }
875
876    /// The config supports relative paths and tracks the root path separately see
877    /// `Config::with_root`
878    ///
879    /// This joins all relative paths with the current root and attempts to make them canonic
880    #[must_use]
881    pub fn canonic(self) -> Self {
882        let root = self.root.clone();
883        self.canonic_at(root)
884    }
885
886    /// Joins all relative paths with the given root so that paths that are defined as:
887    ///
888    /// ```toml
889    /// [profile.default]
890    /// src = "src"
891    /// out = "./out"
892    /// libs = ["lib", "/var/lib"]
893    /// ```
894    ///
895    /// Will be made canonic with the given root:
896    ///
897    /// ```toml
898    /// [profile.default]
899    /// src = "<root>/src"
900    /// out = "<root>/out"
901    /// libs = ["<root>/lib", "/var/lib"]
902    /// ```
903    #[must_use]
904    pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
905        let root = canonic(root);
906
907        fn p(root: &Path, rem: &Path) -> PathBuf {
908            canonic(root.join(rem))
909        }
910
911        self.src = p(&root, &self.src);
912        self.test = p(&root, &self.test);
913        self.script = p(&root, &self.script);
914        self.out = p(&root, &self.out);
915        self.broadcast = p(&root, &self.broadcast);
916        self.cache_path = p(&root, &self.cache_path);
917        self.snapshots = p(&root, &self.snapshots);
918
919        if let Some(build_info_path) = self.build_info_path {
920            self.build_info_path = Some(p(&root, &build_info_path));
921        }
922
923        self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
924
925        self.remappings =
926            self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
927
928        self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
929
930        self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
931
932        self.fs_permissions.join_all(&root);
933
934        if let Some(model_checker) = &mut self.model_checker {
935            model_checker.contracts = std::mem::take(&mut model_checker.contracts)
936                .into_iter()
937                .map(|(path, contracts)| {
938                    (format!("{}", p(&root, path.as_ref()).display()), contracts)
939                })
940                .collect();
941        }
942
943        self
944    }
945
946    /// Normalizes the evm version if a [SolcReq] is set
947    pub fn normalized_evm_version(mut self) -> Self {
948        self.normalize_evm_version();
949        self
950    }
951
952    /// Normalizes optimizer settings.
953    /// See <https://github.com/foundry-rs/foundry/issues/9665>
954    pub fn normalized_optimizer_settings(mut self) -> Self {
955        self.normalize_optimizer_settings();
956        self
957    }
958
959    /// Normalizes the evm version if a [SolcReq] is set to a valid version.
960    pub fn normalize_evm_version(&mut self) {
961        self.evm_version = self.get_normalized_evm_version();
962    }
963
964    /// Normalizes optimizer settings:
965    /// - with default settings, optimizer is set to false and optimizer runs to 200
966    /// - if optimizer is set and optimizer runs not specified, then optimizer runs is set to 200
967    /// - enable optimizer if not explicitly set and optimizer runs set to a value greater than 0
968    pub fn normalize_optimizer_settings(&mut self) {
969        match (self.optimizer, self.optimizer_runs) {
970            // Default: set the optimizer to false and optimizer runs to 200.
971            (None, None) => {
972                self.optimizer = Some(false);
973                self.optimizer_runs = Some(200);
974            }
975            // Set the optimizer runs to 200 if the `optimizer` config set.
976            (Some(_), None) => self.optimizer_runs = Some(200),
977            // Enables optimizer if the `optimizer_runs` has been set with a value greater than 0.
978            (None, Some(runs)) => self.optimizer = Some(runs > 0),
979            _ => {}
980        }
981    }
982
983    /// Returns the normalized [EvmVersion] for the current solc version, or the configured one.
984    pub fn get_normalized_evm_version(&self) -> EvmVersion {
985        if let Some(version) = self.solc_version()
986            && let Some(evm_version) = self.evm_version.normalize_version_solc(&version)
987        {
988            return evm_version;
989        }
990        self.evm_version
991    }
992
993    /// Returns a sanitized version of the Config where are paths are set correctly and potential
994    /// duplicates are resolved
995    ///
996    /// See [`Self::canonic`]
997    #[must_use]
998    pub fn sanitized(self) -> Self {
999        let mut config = self.canonic();
1000
1001        config.sanitize_remappings();
1002
1003        config.libs.sort_unstable();
1004        config.libs.dedup();
1005
1006        config
1007    }
1008
1009    /// Cleans up any duplicate `Remapping` and sorts them
1010    ///
1011    /// On windows this will convert any `\` in the remapping path into a `/`
1012    pub fn sanitize_remappings(&mut self) {
1013        #[cfg(target_os = "windows")]
1014        {
1015            // force `/` in remappings on windows
1016            use path_slash::PathBufExt;
1017            self.remappings.iter_mut().for_each(|r| {
1018                r.path.path = r.path.path.to_slash_lossy().into_owned().into();
1019            });
1020        }
1021    }
1022
1023    /// Returns the directory in which dependencies should be installed
1024    ///
1025    /// Returns the first dir from `libs` that is not `node_modules` or `lib` if `libs` is empty
1026    pub fn install_lib_dir(&self) -> &Path {
1027        self.libs
1028            .iter()
1029            .find(|p| !p.ends_with("node_modules"))
1030            .map(|p| p.as_path())
1031            .unwrap_or_else(|| Path::new("lib"))
1032    }
1033
1034    /// Serves as the entrypoint for obtaining the project.
1035    ///
1036    /// Returns the `Project` configured with all `solc` and path related values.
1037    ///
1038    /// *Note*: this also _cleans_ [`Project::cleanup`] the workspace if `force` is set to true.
1039    ///
1040    /// # Example
1041    ///
1042    /// ```
1043    /// use foundry_config::Config;
1044    /// let config = Config::load_with_root(".")?.sanitized();
1045    /// let project = config.project()?;
1046    /// # Ok::<_, eyre::Error>(())
1047    /// ```
1048    pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1049        self.create_project(self.cache, false)
1050    }
1051
1052    /// Same as [`Self::project()`] but sets configures the project to not emit artifacts and ignore
1053    /// cache.
1054    pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1055        self.create_project(false, true)
1056    }
1057
1058    /// A cached, in-memory project that does not request any artifacts.
1059    ///
1060    /// Use this when you just want the source graph or the Solar compiler context.
1061    pub fn solar_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1062        let ui_testing = std::env::var_os("FOUNDRY_LINT_UI_TESTING").is_some();
1063        let mut project = self.create_project(self.cache && !ui_testing, false)?;
1064        project.update_output_selection(|selection| {
1065            // We have to request something to populate `contracts` in the output and thus
1066            // artifacts.
1067            *selection = OutputSelection::common_output_selection(["abi".into()]);
1068        });
1069        Ok(project)
1070    }
1071
1072    /// Builds mapping with additional settings profiles.
1073    fn additional_settings(
1074        &self,
1075        base: &MultiCompilerSettings,
1076    ) -> BTreeMap<String, MultiCompilerSettings> {
1077        let mut map = BTreeMap::new();
1078
1079        for profile in &self.additional_compiler_profiles {
1080            let mut settings = base.clone();
1081            profile.apply(&mut settings);
1082            map.insert(profile.name.clone(), settings);
1083        }
1084
1085        map
1086    }
1087
1088    /// Resolves globs and builds a mapping from individual source files to their restrictions
1089    #[expect(clippy::disallowed_macros)]
1090    fn restrictions(
1091        &self,
1092        paths: &ProjectPathsConfig,
1093    ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
1094    {
1095        let mut map = BTreeMap::new();
1096        if self.compilation_restrictions.is_empty() {
1097            return Ok(BTreeMap::new());
1098        }
1099
1100        let graph = Graph::<MultiCompilerParser>::resolve(paths)?;
1101        let (sources, _) = graph.into_sources();
1102
1103        for res in &self.compilation_restrictions {
1104            for source in sources.keys().filter(|path| {
1105                if res.paths.is_match(path) {
1106                    true
1107                } else if let Ok(path) = path.strip_prefix(&paths.root) {
1108                    res.paths.is_match(path)
1109                } else {
1110                    false
1111                }
1112            }) {
1113                let res: RestrictionsWithVersion<_> =
1114                    res.clone().try_into().map_err(SolcError::msg)?;
1115                if !map.contains_key(source) {
1116                    map.insert(source.clone(), res);
1117                } else {
1118                    let value = map.remove(source.as_path()).unwrap();
1119                    if let Some(merged) = value.clone().merge(res) {
1120                        map.insert(source.clone(), merged);
1121                    } else {
1122                        // `sh_warn!` is a circular dependency, preventing us from using it here.
1123                        eprintln!(
1124                            "{}",
1125                            yansi::Paint::yellow(&format!(
1126                                "Failed to merge compilation restrictions for {}",
1127                                source.display()
1128                            ))
1129                        );
1130                        map.insert(source.clone(), value);
1131                    }
1132                }
1133            }
1134        }
1135
1136        Ok(map)
1137    }
1138
1139    /// Creates a [`Project`] with the given `cached` and `no_artifacts` flags.
1140    ///
1141    /// Prefer using [`Self::project`] or [`Self::ephemeral_project`] instead.
1142    pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
1143        let settings = self.compiler_settings()?;
1144        let paths = self.project_paths();
1145        let mut builder = Project::builder()
1146            .artifacts(self.configured_artifacts_handler())
1147            .additional_settings(self.additional_settings(&settings))
1148            .restrictions(self.restrictions(&paths)?)
1149            .settings(settings)
1150            .paths(paths)
1151            .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
1152            .ignore_paths(self.ignored_file_paths.clone())
1153            .set_compiler_severity_filter(if self.deny.warnings() {
1154                Severity::Warning
1155            } else {
1156                Severity::Error
1157            })
1158            .set_offline(self.offline)
1159            .set_cached(cached)
1160            .set_build_info(!no_artifacts && self.build_info)
1161            .set_no_artifacts(no_artifacts);
1162
1163        if !self.skip.is_empty() {
1164            let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
1165            builder = builder.sparse_output(filter);
1166        }
1167
1168        let project = builder.build(self.compiler()?)?;
1169
1170        if self.force {
1171            self.cleanup(&project)?;
1172        }
1173
1174        Ok(project)
1175    }
1176
1177    /// Cleans the project.
1178    pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
1179        &self,
1180        project: &Project<C, T>,
1181    ) -> Result<(), SolcError> {
1182        project.cleanup()?;
1183
1184        // Remove last test run failures file.
1185        let _ = fs::remove_file(&self.test_failures_file);
1186
1187        // Remove fuzz and invariant cache directories.
1188        let remove_test_dir = |test_dir: &Option<PathBuf>| {
1189            if let Some(test_dir) = test_dir {
1190                let path = project.root().join(test_dir);
1191                if path.exists() {
1192                    let _ = fs::remove_dir_all(&path);
1193                }
1194            }
1195        };
1196        remove_test_dir(&self.fuzz.failure_persist_dir);
1197        remove_test_dir(&self.fuzz.corpus.corpus_dir);
1198        remove_test_dir(&self.invariant.corpus.corpus_dir);
1199        remove_test_dir(&self.invariant.failure_persist_dir);
1200
1201        Ok(())
1202    }
1203
1204    /// Ensures that the configured version is installed if explicitly set
1205    ///
1206    /// If `solc` is [`SolcReq::Version`] then this will download and install the solc version if
1207    /// it's missing, unless the `offline` flag is enabled, in which case an error is thrown.
1208    ///
1209    /// If `solc` is [`SolcReq::Local`] then this will ensure that the path exists.
1210    fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
1211        if let Some(solc) = &self.solc {
1212            let solc = match solc {
1213                SolcReq::Version(version) => {
1214                    if let Some(solc) = Solc::find_svm_installed_version(version)? {
1215                        solc
1216                    } else {
1217                        if self.offline {
1218                            return Err(SolcError::msg(format!(
1219                                "can't install missing solc {version} in offline mode"
1220                            )));
1221                        }
1222                        Solc::blocking_install(version)?
1223                    }
1224                }
1225                SolcReq::Local(solc) => {
1226                    if !solc.is_file() {
1227                        return Err(SolcError::msg(format!(
1228                            "`solc` {} does not exist",
1229                            solc.display()
1230                        )));
1231                    }
1232                    Solc::new(solc)?
1233                }
1234            };
1235            return Ok(Some(solc));
1236        }
1237
1238        Ok(None)
1239    }
1240
1241    /// Returns the [SpecId] derived from the configured [EvmVersion]
1242    pub fn evm_spec_id(&self) -> SpecId {
1243        evm_spec_id(self.evm_version)
1244    }
1245
1246    /// Returns whether the compiler version should be auto-detected
1247    ///
1248    /// Returns `false` if `solc_version` is explicitly set, otherwise returns the value of
1249    /// `auto_detect_solc`
1250    pub fn is_auto_detect(&self) -> bool {
1251        if self.solc.is_some() {
1252            return false;
1253        }
1254        self.auto_detect_solc
1255    }
1256
1257    /// Whether caching should be enabled for the given chain id
1258    pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
1259        !self.no_storage_caching
1260            && self.rpc_storage_caching.enable_for_chain_id(chain_id.into())
1261            && self.rpc_storage_caching.enable_for_endpoint(endpoint)
1262    }
1263
1264    /// Returns the `ProjectPathsConfig` sub set of the config.
1265    ///
1266    /// **NOTE**: this uses the paths as they are and does __not__ modify them, see
1267    /// `[Self::sanitized]`
1268    ///
1269    /// # Example
1270    ///
1271    /// ```
1272    /// use foundry_compilers::solc::Solc;
1273    /// use foundry_config::Config;
1274    /// let config = Config::load_with_root(".")?.sanitized();
1275    /// let paths = config.project_paths::<Solc>();
1276    /// # Ok::<_, eyre::Error>(())
1277    /// ```
1278    pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
1279        let mut builder = ProjectPathsConfig::builder()
1280            .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
1281            .sources(&self.src)
1282            .tests(&self.test)
1283            .scripts(&self.script)
1284            .artifacts(&self.out)
1285            .libs(self.libs.iter())
1286            .remappings(self.get_all_remappings())
1287            .allowed_path(&self.root)
1288            .allowed_paths(&self.libs)
1289            .allowed_paths(&self.allow_paths)
1290            .include_paths(&self.include_paths);
1291
1292        if let Some(build_info_path) = &self.build_info_path {
1293            builder = builder.build_infos(build_info_path);
1294        }
1295
1296        builder.build_with_root(&self.root)
1297    }
1298
1299    /// Returns configuration for a compiler to use when setting up a [Project].
1300    pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
1301        if let Some(solc) = self.ensure_solc()? {
1302            Ok(SolcCompiler::Specific(solc))
1303        } else {
1304            Ok(SolcCompiler::AutoDetect)
1305        }
1306    }
1307
1308    /// Returns the solc version, if any.
1309    pub fn solc_version(&self) -> Option<Version> {
1310        self.solc.as_ref().and_then(|solc| solc.try_version().ok())
1311    }
1312
1313    /// Returns configured [Vyper] compiler.
1314    pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
1315        // Only instantiate Vyper if there are any Vyper files in the project.
1316        if !self.project_paths::<VyperLanguage>().has_input_files() {
1317            return Ok(None);
1318        }
1319        let vyper = if let Some(path) = &self.vyper.path {
1320            Some(Vyper::new(path)?)
1321        } else {
1322            Vyper::new("vyper").ok()
1323        };
1324        Ok(vyper)
1325    }
1326
1327    /// Returns configuration for a compiler to use when setting up a [Project].
1328    pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
1329        Ok(MultiCompiler { solc: Some(self.solc_compiler()?), vyper: self.vyper_compiler()? })
1330    }
1331
1332    /// Returns configured [MultiCompilerSettings].
1333    pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
1334        Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
1335    }
1336
1337    /// Returns all configured remappings.
1338    pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
1339        self.remappings.iter().map(|m| m.clone().into())
1340    }
1341
1342    /// Returns the configured rpc jwt secret
1343    ///
1344    /// Returns:
1345    ///    - The jwt secret, if configured
1346    ///
1347    /// # Example
1348    ///
1349    /// ```
1350    /// use foundry_config::Config;
1351    /// # fn t() {
1352    /// let config = Config::with_root("./");
1353    /// let rpc_jwt = config.get_rpc_jwt_secret().unwrap().unwrap();
1354    /// # }
1355    /// ```
1356    pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1357        Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1358    }
1359
1360    /// Returns the configured rpc url
1361    ///
1362    /// Returns:
1363    ///    - the matching, resolved url of  `rpc_endpoints` if `eth_rpc_url` is an alias
1364    ///    - the `eth_rpc_url` as-is if it isn't an alias
1365    ///
1366    /// # Example
1367    ///
1368    /// ```
1369    /// use foundry_config::Config;
1370    /// # fn t() {
1371    /// let config = Config::with_root("./");
1372    /// let rpc_url = config.get_rpc_url().unwrap().unwrap();
1373    /// # }
1374    /// ```
1375    pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1376        let maybe_alias = self.eth_rpc_url.as_ref().or(self.etherscan_api_key.as_ref())?;
1377        if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1378            Some(alias)
1379        } else {
1380            Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1381        }
1382    }
1383
1384    /// Resolves the given alias to a matching rpc url
1385    ///
1386    /// # Returns
1387    ///
1388    /// In order of resolution:
1389    ///
1390    /// - the matching, resolved url of `rpc_endpoints` if `maybe_alias` is an alias
1391    /// - a mesc resolved url if `maybe_alias` is a known alias in mesc
1392    /// - `None` otherwise
1393    ///
1394    /// # Note on mesc
1395    ///
1396    /// The endpoint is queried for in mesc under the `foundry` profile, allowing users to customize
1397    /// endpoints for Foundry specifically.
1398    ///
1399    /// # Example
1400    ///
1401    /// ```
1402    /// use foundry_config::Config;
1403    /// # fn t() {
1404    /// let config = Config::with_root("./");
1405    /// let rpc_url = config.get_rpc_url_with_alias("mainnet").unwrap().unwrap();
1406    /// # }
1407    /// ```
1408    pub fn get_rpc_url_with_alias(
1409        &self,
1410        maybe_alias: &str,
1411    ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1412        let mut endpoints = self.rpc_endpoints.clone().resolved();
1413        if let Some(endpoint) = endpoints.remove(maybe_alias) {
1414            return Some(endpoint.url().map(Cow::Owned));
1415        }
1416
1417        if let Some(mesc_url) = self.get_rpc_url_from_mesc(maybe_alias) {
1418            return Some(Ok(Cow::Owned(mesc_url)));
1419        }
1420
1421        None
1422    }
1423
1424    /// Attempts to resolve the URL for the given alias from [`mesc`](https://github.com/paradigmxyz/mesc)
1425    pub fn get_rpc_url_from_mesc(&self, maybe_alias: &str) -> Option<String> {
1426        // Note: mesc requires a MESC_PATH in the env, which the user can configure and is expected
1427        // to be part of the shell profile, default is ~/mesc.json
1428        let mesc_config = mesc::load::load_config_data()
1429            .inspect_err(|err| debug!(%err, "failed to load mesc config"))
1430            .ok()?;
1431
1432        if let Ok(Some(endpoint)) =
1433            mesc::query::get_endpoint_by_query(&mesc_config, maybe_alias, Some("foundry"))
1434        {
1435            return Some(endpoint.url);
1436        }
1437
1438        if maybe_alias.chars().all(|c| c.is_numeric()) {
1439            // try to lookup the mesc network by chain id if alias is numeric
1440            // This only succeeds if the chain id has a default:
1441            // "network_defaults": {
1442            //    "50104": "sophon_50104"
1443            // }
1444            if let Ok(Some(endpoint)) =
1445                mesc::query::get_endpoint_by_network(&mesc_config, maybe_alias, Some("foundry"))
1446            {
1447                return Some(endpoint.url);
1448            }
1449        }
1450
1451        None
1452    }
1453
1454    /// Returns the configured rpc, or the fallback url
1455    ///
1456    /// # Example
1457    ///
1458    /// ```
1459    /// use foundry_config::Config;
1460    /// # fn t() {
1461    /// let config = Config::with_root("./");
1462    /// let rpc_url = config.get_rpc_url_or("http://localhost:8545").unwrap();
1463    /// # }
1464    /// ```
1465    pub fn get_rpc_url_or<'a>(
1466        &'a self,
1467        fallback: impl Into<Cow<'a, str>>,
1468    ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
1469        if let Some(url) = self.get_rpc_url() { url } else { Ok(fallback.into()) }
1470    }
1471
1472    /// Returns the configured rpc or `"http://localhost:8545"` if no `eth_rpc_url` is set
1473    ///
1474    /// # Example
1475    ///
1476    /// ```
1477    /// use foundry_config::Config;
1478    /// # fn t() {
1479    /// let config = Config::with_root("./");
1480    /// let rpc_url = config.get_rpc_url_or_localhost_http().unwrap();
1481    /// # }
1482    /// ```
1483    pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1484        self.get_rpc_url_or("http://localhost:8545")
1485    }
1486
1487    /// Returns the `EtherscanConfig` to use, if any
1488    ///
1489    /// Returns
1490    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if `etherscan_api_key` is
1491    ///    an alias
1492    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if a `chain` is
1493    ///    configured. an alias
1494    ///  - the Mainnet  `ResolvedEtherscanConfig` if `etherscan_api_key` is set, `None` otherwise
1495    ///
1496    /// # Example
1497    ///
1498    /// ```
1499    /// use foundry_config::Config;
1500    /// # fn t() {
1501    /// let config = Config::with_root("./");
1502    /// let etherscan_config = config.get_etherscan_config().unwrap().unwrap();
1503    /// let client = etherscan_config.into_client().unwrap();
1504    /// # }
1505    /// ```
1506    pub fn get_etherscan_config(
1507        &self,
1508    ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1509        self.get_etherscan_config_with_chain(None).transpose()
1510    }
1511
1512    /// Same as [`Self::get_etherscan_config()`] but optionally updates the config with the given
1513    /// `chain`, and `etherscan_api_key`
1514    ///
1515    /// If not matching alias was found, then this will try to find the first entry in the table
1516    /// with a matching chain id. If an etherscan_api_key is already set it will take precedence
1517    /// over the chain's entry in the table.
1518    pub fn get_etherscan_config_with_chain(
1519        &self,
1520        chain: Option<Chain>,
1521    ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1522        if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref())
1523            && self.etherscan.contains_key(maybe_alias)
1524        {
1525            return self.etherscan.clone().resolved().remove(maybe_alias).transpose();
1526        }
1527
1528        // try to find by comparing chain IDs after resolving
1529        if let Some(res) = chain
1530            .or(self.chain)
1531            .and_then(|chain| self.etherscan.clone().resolved().find_chain(chain))
1532        {
1533            match (res, self.etherscan_api_key.as_ref()) {
1534                (Ok(mut config), Some(key)) => {
1535                    // we update the key, because if an etherscan_api_key is set, it should take
1536                    // precedence over the entry, since this is usually set via env var or CLI args.
1537                    config.key.clone_from(key);
1538                    return Ok(Some(config));
1539                }
1540                (Ok(config), None) => return Ok(Some(config)),
1541                (Err(err), None) => return Err(err),
1542                (Err(_), Some(_)) => {
1543                    // use the etherscan key as fallback
1544                }
1545            }
1546        }
1547
1548        // etherscan fallback via API key
1549        if let Some(key) = self.etherscan_api_key.as_ref() {
1550            return Ok(ResolvedEtherscanConfig::create(
1551                key,
1552                chain.or(self.chain).unwrap_or_default(),
1553            ));
1554        }
1555        Ok(None)
1556    }
1557
1558    /// Helper function to just get the API key
1559    ///
1560    /// Optionally updates the config with the given `chain`.
1561    ///
1562    /// See also [Self::get_etherscan_config_with_chain]
1563    pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1564        self.get_etherscan_config_with_chain(chain).ok().flatten().map(|c| c.key)
1565    }
1566
1567    /// Returns the remapping for the project's _src_ directory
1568    ///
1569    /// **Note:** this will add an additional `<src>/=<src path>` remapping here so imports that
1570    /// look like `import {Foo} from "src/Foo.sol";` are properly resolved.
1571    ///
1572    /// This is due the fact that `solc`'s VFS resolves [direct imports](https://docs.soliditylang.org/en/develop/path-resolution.html#direct-imports) that start with the source directory's name.
1573    pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1574        get_dir_remapping(&self.src)
1575    }
1576
1577    /// Returns the remapping for the project's _test_ directory, but only if it exists
1578    pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1579        if self.root.join(&self.test).exists() { get_dir_remapping(&self.test) } else { None }
1580    }
1581
1582    /// Returns the remapping for the project's _script_ directory, but only if it exists
1583    pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1584        if self.root.join(&self.script).exists() { get_dir_remapping(&self.script) } else { None }
1585    }
1586
1587    /// Returns the `Optimizer` based on the configured settings
1588    ///
1589    /// Note: optimizer details can be set independently of `enabled`
1590    /// See also: <https://github.com/foundry-rs/foundry/issues/7689>
1591    /// and  <https://github.com/ethereum/solidity/blob/bbb7f58be026fdc51b0b4694a6f25c22a1425586/docs/using-the-compiler.rst?plain=1#L293-L294>
1592    pub fn optimizer(&self) -> Optimizer {
1593        Optimizer {
1594            enabled: self.optimizer,
1595            runs: self.optimizer_runs,
1596            // we always set the details because `enabled` is effectively a specific details profile
1597            // that can still be modified
1598            details: self.optimizer_details.clone(),
1599        }
1600    }
1601
1602    /// returns the [`foundry_compilers::ConfigurableArtifacts`] for this config, that includes the
1603    /// `extra_output` fields
1604    pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1605        let mut extra_output = self.extra_output.clone();
1606
1607        // Sourcify verification requires solc metadata output. Since, it doesn't
1608        // affect the UX & performance of the compiler, output the metadata files
1609        // by default.
1610        // For more info see: <https://github.com/foundry-rs/foundry/issues/2795>
1611        // Metadata is not emitted as separate file because this breaks typechain support: <https://github.com/foundry-rs/foundry/issues/2969>
1612        if !extra_output.contains(&ContractOutputSelection::Metadata) {
1613            extra_output.push(ContractOutputSelection::Metadata);
1614        }
1615
1616        ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().copied())
1617    }
1618
1619    /// Parses all libraries in the form of
1620    /// `<file>:<lib>:<addr>`
1621    pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1622        Libraries::parse(&self.libraries)
1623    }
1624
1625    /// Returns all libraries with applied remappings. Same as `self.solc_settings()?.libraries`.
1626    pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1627        let paths: ProjectPathsConfig = self.project_paths();
1628        Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1629    }
1630
1631    /// Returns the configured `solc` `Settings` that includes:
1632    /// - all libraries
1633    /// - the optimizer (including details, if configured)
1634    /// - evm version
1635    pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
1636        // By default if no targets are specifically selected the model checker uses all targets.
1637        // This might be too much here, so only enable assertion checks.
1638        // If users wish to enable all options they need to do so explicitly.
1639        let mut model_checker = self.model_checker.clone();
1640        if let Some(model_checker_settings) = &mut model_checker
1641            && model_checker_settings.targets.is_none()
1642        {
1643            model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1644        }
1645
1646        let mut settings = Settings {
1647            libraries: self.libraries_with_remappings()?,
1648            optimizer: self.optimizer(),
1649            evm_version: Some(self.evm_version),
1650            metadata: Some(SettingsMetadata {
1651                use_literal_content: Some(self.use_literal_content),
1652                bytecode_hash: Some(self.bytecode_hash),
1653                cbor_metadata: Some(self.cbor_metadata),
1654            }),
1655            debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1656                revert_strings: Some(revert_strings),
1657                // Not used.
1658                debug_info: Vec::new(),
1659            }),
1660            model_checker,
1661            via_ir: Some(self.via_ir),
1662            // Not used.
1663            stop_after: None,
1664            // Set in project paths.
1665            remappings: Vec::new(),
1666            // Set with `with_extra_output` below.
1667            output_selection: Default::default(),
1668        }
1669        .with_extra_output(self.configured_artifacts_handler().output_selection());
1670
1671        // We're keeping AST in `--build-info` for backwards compatibility with HardHat.
1672        if self.ast || self.build_info {
1673            settings = settings.with_ast();
1674        }
1675
1676        let cli_settings =
1677            CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };
1678
1679        Ok(SolcSettings { settings, cli_settings })
1680    }
1681
1682    /// Returns the configured [VyperSettings] that includes:
1683    /// - evm version
1684    pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
1685        Ok(VyperSettings {
1686            evm_version: Some(self.evm_version),
1687            optimize: self.vyper.optimize,
1688            bytecode_metadata: None,
1689            // TODO: We don't yet have a way to deserialize other outputs correctly, so request only
1690            // those for now. It should be enough to run tests and deploy contracts.
1691            output_selection: OutputSelection::common_output_selection([
1692                "abi".to_string(),
1693                "evm.bytecode".to_string(),
1694                "evm.deployedBytecode".to_string(),
1695            ]),
1696            search_paths: None,
1697            experimental_codegen: self.vyper.experimental_codegen,
1698        })
1699    }
1700
1701    /// Returns the default figment
1702    ///
1703    /// The default figment reads from the following sources, in ascending
1704    /// priority order:
1705    ///
1706    ///   1. [`Config::default()`] (see [defaults](#defaults))
1707    ///   2. `foundry.toml` _or_ filename in `FOUNDRY_CONFIG` environment variable
1708    ///   3. `FOUNDRY_` prefixed environment variables
1709    ///
1710    /// The profile selected is the value set in the `FOUNDRY_PROFILE`
1711    /// environment variable. If it is not set, it defaults to `default`.
1712    ///
1713    /// # Example
1714    ///
1715    /// ```rust
1716    /// use foundry_config::Config;
1717    /// use serde::Deserialize;
1718    ///
1719    /// let my_config = Config::figment().extract::<Config>();
1720    /// ```
1721    pub fn figment() -> Figment {
1722        Self::default().into()
1723    }
1724
1725    /// Returns the default figment enhanced with additional context extracted from the provided
1726    /// root, like remappings and directories.
1727    ///
1728    /// # Example
1729    ///
1730    /// ```rust
1731    /// use foundry_config::Config;
1732    /// use serde::Deserialize;
1733    ///
1734    /// let my_config = Config::figment_with_root(".").extract::<Config>();
1735    /// ```
1736    pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
1737        Self::with_root(root.as_ref()).into()
1738    }
1739
1740    #[doc(hidden)]
1741    #[track_caller]
1742    pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
1743        let root = match root {
1744            Some(root) => root,
1745            None => &find_project_root(None).expect("could not determine project root"),
1746        };
1747        Self::figment_with_root(root)
1748    }
1749
1750    /// Creates a new Config that adds additional context extracted from the provided root.
1751    ///
1752    /// # Example
1753    ///
1754    /// ```rust
1755    /// use foundry_config::Config;
1756    /// let my_config = Config::with_root(".");
1757    /// ```
1758    pub fn with_root(root: impl AsRef<Path>) -> Self {
1759        Self::_with_root(root.as_ref())
1760    }
1761
1762    fn _with_root(root: &Path) -> Self {
1763        // autodetect paths
1764        let paths = ProjectPathsConfig::builder().build_with_root::<()>(root);
1765        let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
1766        Self {
1767            root: paths.root,
1768            src: paths.sources.file_name().unwrap().into(),
1769            out: artifacts.clone(),
1770            libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
1771            remappings: paths
1772                .remappings
1773                .into_iter()
1774                .map(|r| RelativeRemapping::new(r, root))
1775                .collect(),
1776            fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
1777            ..Self::default()
1778        }
1779    }
1780
1781    /// Returns the default config but with hardhat paths
1782    pub fn hardhat() -> Self {
1783        Self {
1784            src: "contracts".into(),
1785            out: "artifacts".into(),
1786            libs: vec!["node_modules".into()],
1787            ..Self::default()
1788        }
1789    }
1790
1791    /// Extracts a basic subset of the config, used for initialisations.
1792    ///
1793    /// # Example
1794    ///
1795    /// ```rust
1796    /// use foundry_config::Config;
1797    /// let my_config = Config::with_root(".").into_basic();
1798    /// ```
1799    pub fn into_basic(self) -> BasicConfig {
1800        BasicConfig {
1801            profile: self.profile,
1802            src: self.src,
1803            out: self.out,
1804            libs: self.libs,
1805            remappings: self.remappings,
1806        }
1807    }
1808
1809    /// Updates the `foundry.toml` file for the given `root` based on the provided closure.
1810    ///
1811    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
1812    /// [Self::get_config_path()] and if the closure returns `true`.
1813    pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
1814    where
1815        F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
1816    {
1817        let config = Self::load_with_root(root)?.sanitized();
1818        config.update(|doc| f(&config, doc))
1819    }
1820
1821    /// Updates the `foundry.toml` file this `Config` ias based on with the provided closure.
1822    ///
1823    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
1824    /// [Self::get_config_path()] and if the closure returns `true`
1825    pub fn update<F>(&self, f: F) -> eyre::Result<()>
1826    where
1827        F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
1828    {
1829        let file_path = self.get_config_path();
1830        if !file_path.exists() {
1831            return Ok(());
1832        }
1833        let contents = fs::read_to_string(&file_path)?;
1834        let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
1835        if f(&mut doc) {
1836            fs::write(file_path, doc.to_string())?;
1837        }
1838        Ok(())
1839    }
1840
1841    /// Sets the `libs` entry inside a `foundry.toml` file but only if it exists
1842    ///
1843    /// # Errors
1844    ///
1845    /// An error if the `foundry.toml` could not be parsed.
1846    pub fn update_libs(&self) -> eyre::Result<()> {
1847        self.update(|doc| {
1848            let profile = self.profile.as_str().as_str();
1849            let root = &self.root;
1850            let libs: toml_edit::Value = self
1851                .libs
1852                .iter()
1853                .map(|path| {
1854                    let path =
1855                        if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
1856                    toml_edit::Value::from(&*path.to_string_lossy())
1857                })
1858                .collect();
1859            let libs = toml_edit::value(libs);
1860            doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
1861            true
1862        })
1863    }
1864
1865    /// Serialize the config type as a String of TOML.
1866    ///
1867    /// This serializes to a table with the name of the profile
1868    ///
1869    /// ```toml
1870    /// [profile.default]
1871    /// src = "src"
1872    /// out = "out"
1873    /// libs = ["lib"]
1874    /// # ...
1875    /// ```
1876    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
1877        // serializing to value first to prevent `ValueAfterTable` errors
1878        let mut value = toml::Value::try_from(self)?;
1879        // Config map always gets serialized as a table
1880        let value_table = value.as_table_mut().unwrap();
1881        // remove standalone sections from inner table
1882        let standalone_sections = Self::STANDALONE_SECTIONS
1883            .iter()
1884            .filter_map(|section| {
1885                let section = section.to_string();
1886                value_table.remove(&section).map(|value| (section, value))
1887            })
1888            .collect::<Vec<_>>();
1889        // wrap inner table in [profile.<profile>]
1890        let mut wrapping_table = [(
1891            Self::PROFILE_SECTION.into(),
1892            toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
1893        )]
1894        .into_iter()
1895        .collect::<toml::map::Map<_, _>>();
1896        // insert standalone sections
1897        for (section, value) in standalone_sections {
1898            wrapping_table.insert(section, value);
1899        }
1900        // stringify
1901        toml::to_string_pretty(&toml::Value::Table(wrapping_table))
1902    }
1903
1904    /// Returns the path to the `foundry.toml` of this `Config`.
1905    pub fn get_config_path(&self) -> PathBuf {
1906        self.root.join(Self::FILE_NAME)
1907    }
1908
1909    /// Returns the selected profile.
1910    ///
1911    /// If the `FOUNDRY_PROFILE` env variable is not set, this returns the `DEFAULT_PROFILE`.
1912    pub fn selected_profile() -> Profile {
1913        // Can't cache in tests because the env var can change.
1914        #[cfg(test)]
1915        {
1916            Self::force_selected_profile()
1917        }
1918        #[cfg(not(test))]
1919        {
1920            static CACHE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();
1921            CACHE.get_or_init(Self::force_selected_profile).clone()
1922        }
1923    }
1924
1925    fn force_selected_profile() -> Profile {
1926        Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
1927    }
1928
1929    /// Returns the path to foundry's global TOML file: `~/.foundry/foundry.toml`.
1930    pub fn foundry_dir_toml() -> Option<PathBuf> {
1931        Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
1932    }
1933
1934    /// Returns the path to foundry's config dir: `~/.foundry/`.
1935    pub fn foundry_dir() -> Option<PathBuf> {
1936        dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
1937    }
1938
1939    /// Returns the path to foundry's cache dir: `~/.foundry/cache`.
1940    pub fn foundry_cache_dir() -> Option<PathBuf> {
1941        Self::foundry_dir().map(|p| p.join("cache"))
1942    }
1943
1944    /// Returns the path to foundry rpc cache dir: `~/.foundry/cache/rpc`.
1945    pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
1946        Some(Self::foundry_cache_dir()?.join("rpc"))
1947    }
1948    /// Returns the path to foundry chain's cache dir: `~/.foundry/cache/rpc/<chain>`
1949    pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1950        Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
1951    }
1952
1953    /// Returns the path to foundry's etherscan cache dir: `~/.foundry/cache/etherscan`.
1954    pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
1955        Some(Self::foundry_cache_dir()?.join("etherscan"))
1956    }
1957
1958    /// Returns the path to foundry's keystores dir: `~/.foundry/keystores`.
1959    pub fn foundry_keystores_dir() -> Option<PathBuf> {
1960        Some(Self::foundry_dir()?.join("keystores"))
1961    }
1962
1963    /// Returns the path to foundry's etherscan cache dir for `chain_id`:
1964    /// `~/.foundry/cache/etherscan/<chain>`
1965    pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1966        Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
1967    }
1968
1969    /// Returns the path to the cache dir of the `block` on the `chain`:
1970    /// `~/.foundry/cache/rpc/<chain>/<block>`
1971    pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1972        Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
1973    }
1974
1975    /// Returns the path to the cache file of the `block` on the `chain`:
1976    /// `~/.foundry/cache/rpc/<chain>/<block>/storage.json`
1977    pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1978        Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
1979    }
1980
1981    /// Returns the path to `foundry`'s data directory inside the user's data directory.
1982    ///
1983    /// | Platform | Value                                         | Example                                          |
1984    /// | -------  | --------------------------------------------- | ------------------------------------------------ |
1985    /// | Linux    | `$XDG_CONFIG_HOME` or `$HOME`/.config/foundry | /home/alice/.config/foundry                      |
1986    /// | macOS    | `$HOME`/Library/Application Support/foundry   | /Users/Alice/Library/Application Support/foundry |
1987    /// | Windows  | `{FOLDERID_RoamingAppData}/foundry`           | C:\Users\Alice\AppData\Roaming/foundry           |
1988    pub fn data_dir() -> eyre::Result<PathBuf> {
1989        let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
1990        std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
1991        Ok(path)
1992    }
1993
1994    /// Returns the path to the `foundry.toml` file, the file is searched for in
1995    /// the current working directory and all parent directories until the root,
1996    /// and the first hit is used.
1997    ///
1998    /// If this search comes up empty, then it checks if a global `foundry.toml` exists at
1999    /// `~/.foundry/foundry.toml`, see [`Self::foundry_dir_toml`].
2000    pub fn find_config_file() -> Option<PathBuf> {
2001        fn find(path: &Path) -> Option<PathBuf> {
2002            if path.is_absolute() {
2003                return match path.is_file() {
2004                    true => Some(path.to_path_buf()),
2005                    false => None,
2006                };
2007            }
2008            let cwd = std::env::current_dir().ok()?;
2009            let mut cwd = cwd.as_path();
2010            loop {
2011                let file_path = cwd.join(path);
2012                if file_path.is_file() {
2013                    return Some(file_path);
2014                }
2015                cwd = cwd.parent()?;
2016            }
2017        }
2018        find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
2019            .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
2020    }
2021
2022    /// Clears the foundry cache.
2023    pub fn clean_foundry_cache() -> eyre::Result<()> {
2024        if let Some(cache_dir) = Self::foundry_cache_dir() {
2025            let path = cache_dir.as_path();
2026            let _ = fs::remove_dir_all(path);
2027        } else {
2028            eyre::bail!("failed to get foundry_cache_dir");
2029        }
2030
2031        Ok(())
2032    }
2033
2034    /// Clears the foundry cache for `chain`.
2035    pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<()> {
2036        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2037            let path = cache_dir.as_path();
2038            let _ = fs::remove_dir_all(path);
2039        } else {
2040            eyre::bail!("failed to get foundry_chain_cache_dir");
2041        }
2042
2043        Ok(())
2044    }
2045
2046    /// Clears the foundry cache for `chain` and `block`.
2047    pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<()> {
2048        if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
2049            let path = cache_dir.as_path();
2050            let _ = fs::remove_dir_all(path);
2051        } else {
2052            eyre::bail!("failed to get foundry_block_cache_dir");
2053        }
2054
2055        Ok(())
2056    }
2057
2058    /// Clears the foundry etherscan cache.
2059    pub fn clean_foundry_etherscan_cache() -> eyre::Result<()> {
2060        if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
2061            let path = cache_dir.as_path();
2062            let _ = fs::remove_dir_all(path);
2063        } else {
2064            eyre::bail!("failed to get foundry_etherscan_cache_dir");
2065        }
2066
2067        Ok(())
2068    }
2069
2070    /// Clears the foundry etherscan cache for `chain`.
2071    pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<()> {
2072        if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
2073            let path = cache_dir.as_path();
2074            let _ = fs::remove_dir_all(path);
2075        } else {
2076            eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
2077        }
2078
2079        Ok(())
2080    }
2081
2082    /// List the data in the foundry cache.
2083    pub fn list_foundry_cache() -> eyre::Result<Cache> {
2084        if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
2085            let mut cache = Cache { chains: vec![] };
2086            if !cache_dir.exists() {
2087                return Ok(cache);
2088            }
2089            if let Ok(entries) = cache_dir.as_path().read_dir() {
2090                for entry in entries.flatten().filter(|x| x.path().is_dir()) {
2091                    match Chain::from_str(&entry.file_name().to_string_lossy()) {
2092                        Ok(chain) => cache.chains.push(Self::list_foundry_chain_cache(chain)?),
2093                        Err(_) => continue,
2094                    }
2095                }
2096                Ok(cache)
2097            } else {
2098                eyre::bail!("failed to access foundry_cache_dir");
2099            }
2100        } else {
2101            eyre::bail!("failed to get foundry_cache_dir");
2102        }
2103    }
2104
2105    /// List the cached data for `chain`.
2106    pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
2107        let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
2108            Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
2109            None => {
2110                warn!("failed to access foundry_etherscan_chain_cache_dir");
2111                0
2112            }
2113        };
2114
2115        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2116            let blocks = Self::get_cached_blocks(&cache_dir)?;
2117            Ok(ChainCache {
2118                name: chain.to_string(),
2119                blocks,
2120                block_explorer: block_explorer_data_size,
2121            })
2122        } else {
2123            eyre::bail!("failed to get foundry_chain_cache_dir");
2124        }
2125    }
2126
2127    /// The path provided to this function should point to a cached chain folder.
2128    fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
2129        let mut blocks = vec![];
2130        if !chain_path.exists() {
2131            return Ok(blocks);
2132        }
2133        for block in chain_path.read_dir()?.flatten() {
2134            let file_type = block.file_type()?;
2135            let file_name = block.file_name();
2136            let filepath = if file_type.is_dir() {
2137                block.path().join("storage.json")
2138            } else if file_type.is_file()
2139                && file_name.to_string_lossy().chars().all(char::is_numeric)
2140            {
2141                block.path()
2142            } else {
2143                continue;
2144            };
2145            blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
2146        }
2147        Ok(blocks)
2148    }
2149
2150    /// The path provided to this function should point to the etherscan cache for a chain.
2151    fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
2152        if !chain_path.exists() {
2153            return Ok(0);
2154        }
2155
2156        fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
2157            dir.try_fold(0, |acc, file| {
2158                let file = file?;
2159                let size = match file.metadata()? {
2160                    data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
2161                    data => data.len(),
2162                };
2163                Ok(acc + size)
2164            })
2165        }
2166
2167        dir_size_recursive(fs::read_dir(chain_path)?)
2168    }
2169
2170    fn merge_toml_provider(
2171        mut figment: Figment,
2172        toml_provider: impl Provider,
2173        profile: Profile,
2174    ) -> Figment {
2175        figment = figment.select(profile.clone());
2176
2177        // add warnings
2178        figment = {
2179            let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
2180            figment.merge(warnings)
2181        };
2182
2183        // use [profile.<profile>] as [<profile>]
2184        let mut profiles = vec![Self::DEFAULT_PROFILE];
2185        if profile != Self::DEFAULT_PROFILE {
2186            profiles.push(profile.clone());
2187        }
2188        let provider = toml_provider.strict_select(profiles);
2189
2190        // apply any key fixes
2191        let provider = &BackwardsCompatTomlProvider(ForcedSnakeCaseData(provider));
2192
2193        // merge the default profile as a base
2194        if profile != Self::DEFAULT_PROFILE {
2195            figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
2196        }
2197        // merge special keys into config
2198        for standalone_key in Self::STANDALONE_SECTIONS {
2199            if let Some((_, fallback)) =
2200                STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
2201            {
2202                figment = figment.merge(
2203                    provider
2204                        .fallback(standalone_key, fallback)
2205                        .wrap(profile.clone(), standalone_key),
2206                );
2207            } else {
2208                figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
2209            }
2210        }
2211        // merge the profile
2212        figment = figment.merge(provider);
2213        figment
2214    }
2215
2216    /// Check if any defaults need to be normalized.
2217    ///
2218    /// This normalizes the default `evm_version` if a `solc` was provided in the config.
2219    ///
2220    /// See also <https://github.com/foundry-rs/foundry/issues/7014>
2221    #[expect(clippy::disallowed_macros)]
2222    fn normalize_defaults(&self, mut figment: Figment) -> Figment {
2223        let evm_version = figment.extract_inner::<EvmVersion>("evm_version").ok().or_else(|| {
2224            figment
2225                .extract_inner::<String>("evm_version")
2226                .ok()
2227                .and_then(|s| s.parse::<EvmVersion>().ok())
2228        });
2229
2230        let solc_version = figment
2231            .extract_inner::<SolcReq>("solc")
2232            .ok()
2233            .and_then(|solc| solc.try_version().ok())
2234            .and_then(|v| self.evm_version.normalize_version_solc(&v));
2235
2236        if figment.contains("evm_version") {
2237            // Check compatibility if both evm_version and solc are provided
2238            // First try to extract as EvmVersion directly, then fallback to string parsing for
2239            // case-insensitive support
2240            if let Some(evm_version) = evm_version {
2241                figment = figment.merge(("evm_version", evm_version));
2242
2243                if let Some(solc_version) = solc_version
2244                    && solc_version != evm_version
2245                {
2246                    eprintln!(
2247                        "{}",
2248                        yansi::Paint::yellow(&format!(
2249                            "Warning: evm_version '{evm_version}' may be incompatible with solc version. Consider using '{solc_version}'"
2250                        ))
2251                    );
2252                }
2253            }
2254            return figment;
2255        }
2256
2257        // Normalize `evm_version` based on the provided solc version.
2258        if let Some(version) = solc_version {
2259            figment = figment.merge(("evm_version", version));
2260        }
2261
2262        // Normalize `deny` based on the provided `deny_warnings` version.
2263        if self.deny_warnings
2264            && let Ok(DenyLevel::Never) = figment.extract_inner("deny")
2265        {
2266            figment = figment.merge(("deny", DenyLevel::Warnings));
2267        }
2268
2269        figment
2270    }
2271}
2272
2273impl From<Config> for Figment {
2274    fn from(c: Config) -> Self {
2275        (&c).into()
2276    }
2277}
2278impl From<&Config> for Figment {
2279    fn from(c: &Config) -> Self {
2280        c.to_figment(FigmentProviders::All)
2281    }
2282}
2283
2284/// Determines what providers should be used when loading the [`Figment`] for a [`Config`].
2285#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2286pub enum FigmentProviders {
2287    /// Include all providers.
2288    #[default]
2289    All,
2290    /// Only include necessary providers that are useful for cast commands.
2291    ///
2292    /// This will exclude more expensive providers such as remappings.
2293    Cast,
2294    /// Only include necessary providers that are useful for anvil.
2295    ///
2296    /// This will exclude more expensive providers such as remappings.
2297    Anvil,
2298    /// Don't include any providers.
2299    None,
2300}
2301
2302impl FigmentProviders {
2303    /// Returns true if all providers should be included.
2304    pub const fn is_all(&self) -> bool {
2305        matches!(self, Self::All)
2306    }
2307
2308    /// Returns true if this is the cast preset.
2309    pub const fn is_cast(&self) -> bool {
2310        matches!(self, Self::Cast)
2311    }
2312
2313    /// Returns true if this is the anvil preset.
2314    pub const fn is_anvil(&self) -> bool {
2315        matches!(self, Self::Anvil)
2316    }
2317
2318    /// Returns true if no providers should be included.
2319    pub const fn is_none(&self) -> bool {
2320        matches!(self, Self::None)
2321    }
2322}
2323
2324/// Wrapper type for [`regex::Regex`] that implements [`PartialEq`] and [`serde`] traits.
2325#[derive(Clone, Debug, Serialize, Deserialize)]
2326#[serde(transparent)]
2327pub struct RegexWrapper {
2328    #[serde(with = "serde_regex")]
2329    inner: regex::Regex,
2330}
2331
2332impl std::ops::Deref for RegexWrapper {
2333    type Target = regex::Regex;
2334
2335    fn deref(&self) -> &Self::Target {
2336        &self.inner
2337    }
2338}
2339
2340impl std::cmp::PartialEq for RegexWrapper {
2341    fn eq(&self, other: &Self) -> bool {
2342        self.as_str() == other.as_str()
2343    }
2344}
2345
2346impl Eq for RegexWrapper {}
2347
2348impl From<RegexWrapper> for regex::Regex {
2349    fn from(wrapper: RegexWrapper) -> Self {
2350        wrapper.inner
2351    }
2352}
2353
2354impl From<regex::Regex> for RegexWrapper {
2355    fn from(re: Regex) -> Self {
2356        Self { inner: re }
2357    }
2358}
2359
2360mod serde_regex {
2361    use regex::Regex;
2362    use serde::{Deserialize, Deserializer, Serializer};
2363
2364    pub(crate) fn serialize<S>(value: &Regex, serializer: S) -> Result<S::Ok, S::Error>
2365    where
2366        S: Serializer,
2367    {
2368        serializer.serialize_str(value.as_str())
2369    }
2370
2371    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
2372    where
2373        D: Deserializer<'de>,
2374    {
2375        let s = String::deserialize(deserializer)?;
2376        Regex::new(&s).map_err(serde::de::Error::custom)
2377    }
2378}
2379
2380/// Ser/de `globset::Glob` explicitly to handle `Option<Glob>` properly
2381pub(crate) mod from_opt_glob {
2382    use serde::{Deserialize, Deserializer, Serializer};
2383
2384    pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
2385    where
2386        S: Serializer,
2387    {
2388        match value {
2389            Some(glob) => serializer.serialize_str(glob.glob()),
2390            None => serializer.serialize_none(),
2391        }
2392    }
2393
2394    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
2395    where
2396        D: Deserializer<'de>,
2397    {
2398        let s: Option<String> = Option::deserialize(deserializer)?;
2399        if let Some(s) = s {
2400            return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
2401        }
2402        Ok(None)
2403    }
2404}
2405
2406/// Parses a config profile
2407///
2408/// All `Profile` date is ignored by serde, however the `Config::to_string_pretty` includes it and
2409/// returns a toml table like
2410///
2411/// ```toml
2412/// #[profile.default]
2413/// src = "..."
2414/// ```
2415/// This ignores the `#[profile.default]` part in the toml
2416pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
2417    s: &str,
2418) -> Result<Option<(Profile, T)>, Error> {
2419    let figment = Config::merge_toml_provider(
2420        Figment::new(),
2421        Toml::string(s).nested(),
2422        Config::DEFAULT_PROFILE,
2423    );
2424    if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
2425        Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
2426    } else {
2427        Ok(None)
2428    }
2429}
2430
2431impl Provider for Config {
2432    fn metadata(&self) -> Metadata {
2433        Metadata::named("Foundry Config")
2434    }
2435
2436    #[track_caller]
2437    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2438        let mut data = Serialized::defaults(self).data()?;
2439        if let Some(entry) = data.get_mut(&self.profile) {
2440            entry.insert("root".to_string(), Value::serialize(self.root.clone())?);
2441        }
2442        Ok(data)
2443    }
2444
2445    fn profile(&self) -> Option<Profile> {
2446        Some(self.profile.clone())
2447    }
2448}
2449
2450impl Default for Config {
2451    fn default() -> Self {
2452        Self {
2453            profile: Self::DEFAULT_PROFILE,
2454            profiles: vec![Self::DEFAULT_PROFILE],
2455            fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2456            isolate: cfg!(feature = "isolate-by-default"),
2457            root: root_default(),
2458            extends: None,
2459            src: "src".into(),
2460            test: "test".into(),
2461            script: "script".into(),
2462            out: "out".into(),
2463            libs: vec!["lib".into()],
2464            cache: true,
2465            dynamic_test_linking: false,
2466            cache_path: "cache".into(),
2467            broadcast: "broadcast".into(),
2468            snapshots: "snapshots".into(),
2469            gas_snapshot_check: false,
2470            gas_snapshot_emit: true,
2471            allow_paths: vec![],
2472            include_paths: vec![],
2473            force: false,
2474            evm_version: EvmVersion::Prague,
2475            gas_reports: vec!["*".to_string()],
2476            gas_reports_ignore: vec![],
2477            gas_reports_include_tests: false,
2478            solc: None,
2479            vyper: Default::default(),
2480            auto_detect_solc: true,
2481            offline: false,
2482            optimizer: None,
2483            optimizer_runs: None,
2484            optimizer_details: None,
2485            model_checker: None,
2486            extra_output: Default::default(),
2487            extra_output_files: Default::default(),
2488            names: false,
2489            sizes: false,
2490            test_pattern: None,
2491            test_pattern_inverse: None,
2492            contract_pattern: None,
2493            contract_pattern_inverse: None,
2494            path_pattern: None,
2495            path_pattern_inverse: None,
2496            coverage_pattern_inverse: None,
2497            test_failures_file: "cache/test-failures".into(),
2498            threads: None,
2499            show_progress: false,
2500            fuzz: FuzzConfig::new("cache/fuzz".into()),
2501            invariant: InvariantConfig::new("cache/invariant".into()),
2502            always_use_create_2_factory: false,
2503            ffi: false,
2504            allow_internal_expect_revert: false,
2505            prompt_timeout: 120,
2506            sender: Self::DEFAULT_SENDER,
2507            tx_origin: Self::DEFAULT_SENDER,
2508            initial_balance: U256::from((1u128 << 96) - 1),
2509            block_number: U256::from(1),
2510            fork_block_number: None,
2511            chain: None,
2512            gas_limit: (1u64 << 30).into(), // ~1B
2513            code_size_limit: None,
2514            gas_price: None,
2515            block_base_fee_per_gas: 0,
2516            block_coinbase: Address::ZERO,
2517            block_timestamp: U256::from(1),
2518            block_difficulty: 0,
2519            block_prevrandao: Default::default(),
2520            block_gas_limit: None,
2521            disable_block_gas_limit: false,
2522            enable_tx_gas_limit: false,
2523            memory_limit: 1 << 27, // 2**27 = 128MiB = 134_217_728 bytes
2524            eth_rpc_url: None,
2525            eth_rpc_accept_invalid_certs: false,
2526            eth_rpc_jwt: None,
2527            eth_rpc_timeout: None,
2528            eth_rpc_headers: None,
2529            etherscan_api_key: None,
2530            verbosity: 0,
2531            remappings: vec![],
2532            auto_detect_remappings: true,
2533            libraries: vec![],
2534            ignored_error_codes: vec![
2535                SolidityErrorCode::SpdxLicenseNotProvided,
2536                SolidityErrorCode::ContractExceeds24576Bytes,
2537                SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2538                SolidityErrorCode::TransientStorageUsed,
2539            ],
2540            ignored_file_paths: vec![],
2541            deny: DenyLevel::Never,
2542            deny_warnings: false,
2543            via_ir: false,
2544            ast: false,
2545            rpc_storage_caching: Default::default(),
2546            rpc_endpoints: Default::default(),
2547            etherscan: Default::default(),
2548            no_storage_caching: false,
2549            no_rpc_rate_limit: false,
2550            use_literal_content: false,
2551            bytecode_hash: BytecodeHash::Ipfs,
2552            cbor_metadata: true,
2553            revert_strings: None,
2554            sparse_mode: false,
2555            build_info: false,
2556            build_info_path: None,
2557            fmt: Default::default(),
2558            lint: Default::default(),
2559            doc: Default::default(),
2560            bind_json: Default::default(),
2561            labels: Default::default(),
2562            unchecked_cheatcode_artifacts: false,
2563            create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
2564            create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
2565            skip: vec![],
2566            dependencies: Default::default(),
2567            soldeer: Default::default(),
2568            assertions_revert: true,
2569            legacy_assertions: false,
2570            warnings: vec![],
2571            extra_args: vec![],
2572            celo: false,
2573            transaction_timeout: 120,
2574            additional_compiler_profiles: Default::default(),
2575            compilation_restrictions: Default::default(),
2576            script_execution_protection: true,
2577            _non_exhaustive: (),
2578        }
2579    }
2580}
2581
2582/// Wrapper for the config's `gas_limit` value necessary because toml-rs can't handle larger number
2583/// because integers are stored signed: <https://github.com/alexcrichton/toml-rs/issues/256>
2584///
2585/// Due to this limitation this type will be serialized/deserialized as String if it's larger than
2586/// `i64`
2587#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
2588pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);
2589
2590impl From<u64> for GasLimit {
2591    fn from(gas: u64) -> Self {
2592        Self(gas)
2593    }
2594}
2595
2596impl From<GasLimit> for u64 {
2597    fn from(gas: GasLimit) -> Self {
2598        gas.0
2599    }
2600}
2601
2602impl Serialize for GasLimit {
2603    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2604    where
2605        S: Serializer,
2606    {
2607        if self.0 == u64::MAX {
2608            serializer.serialize_str("max")
2609        } else if self.0 > i64::MAX as u64 {
2610            serializer.serialize_str(&self.0.to_string())
2611        } else {
2612            serializer.serialize_u64(self.0)
2613        }
2614    }
2615}
2616
2617/// Variants for selecting the [`Solc`] instance
2618#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2619#[serde(untagged)]
2620pub enum SolcReq {
2621    /// Requires a specific solc version, that's either already installed (via `svm`) or will be
2622    /// auto installed (via `svm`)
2623    Version(Version),
2624    /// Path to an existing local solc installation
2625    Local(PathBuf),
2626}
2627
2628impl SolcReq {
2629    /// Tries to get the solc version from the `SolcReq`
2630    ///
2631    /// If the `SolcReq` is a `Version` it will return the version, if it's a path to a binary it
2632    /// will try to get the version from the binary.
2633    fn try_version(&self) -> Result<Version, SolcError> {
2634        match self {
2635            Self::Version(version) => Ok(version.clone()),
2636            Self::Local(path) => Solc::new(path).map(|solc| solc.version),
2637        }
2638    }
2639}
2640
2641impl<T: AsRef<str>> From<T> for SolcReq {
2642    fn from(s: T) -> Self {
2643        let s = s.as_ref();
2644        if let Ok(v) = Version::from_str(s) { Self::Version(v) } else { Self::Local(s.into()) }
2645    }
2646}
2647
2648/// A subset of the foundry `Config`
2649/// used to initialize a `foundry.toml` file
2650///
2651/// # Example
2652///
2653/// ```rust
2654/// use foundry_config::{BasicConfig, Config};
2655/// use serde::Deserialize;
2656///
2657/// let my_config = Config::figment().extract::<BasicConfig>();
2658/// ```
2659#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2660pub struct BasicConfig {
2661    /// the profile tag: `[profile.default]`
2662    #[serde(skip)]
2663    pub profile: Profile,
2664    /// path of the source contracts dir, like `src` or `contracts`
2665    pub src: PathBuf,
2666    /// path to where artifacts shut be written to
2667    pub out: PathBuf,
2668    /// all library folders to include, `lib`, `node_modules`
2669    pub libs: Vec<PathBuf>,
2670    /// `Remappings` to use for this repo
2671    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2672    pub remappings: Vec<RelativeRemapping>,
2673}
2674
2675impl BasicConfig {
2676    /// Serialize the config as a String of TOML.
2677    ///
2678    /// This serializes to a table with the name of the profile
2679    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2680        let s = toml::to_string_pretty(self)?;
2681        Ok(format!(
2682            "\
2683[profile.{}]
2684{s}
2685# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n",
2686            self.profile
2687        ))
2688    }
2689}
2690
2691pub(crate) mod from_str_lowercase {
2692    use serde::{Deserialize, Deserializer, Serializer};
2693    use std::str::FromStr;
2694
2695    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
2696    where
2697        T: std::fmt::Display,
2698        S: Serializer,
2699    {
2700        serializer.collect_str(&value.to_string().to_lowercase())
2701    }
2702
2703    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
2704    where
2705        D: Deserializer<'de>,
2706        T: FromStr,
2707        T::Err: std::fmt::Display,
2708    {
2709        String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
2710    }
2711}
2712
2713fn canonic(path: impl Into<PathBuf>) -> PathBuf {
2714    let path = path.into();
2715    foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
2716}
2717
2718fn root_default() -> PathBuf {
2719    ".".into()
2720}
2721
2722#[cfg(test)]
2723mod tests {
2724    use super::*;
2725    use crate::{
2726        cache::{CachedChains, CachedEndpoints},
2727        endpoints::RpcEndpointType,
2728        etherscan::ResolvedEtherscanConfigs,
2729        fmt::IndentStyle,
2730    };
2731    use NamedChain::Moonbeam;
2732    use endpoints::{RpcAuth, RpcEndpointConfig};
2733    use figment::error::Kind::InvalidType;
2734    use foundry_compilers::artifacts::{
2735        ModelCheckerEngine, YulDetails, vyper::VyperOptimizationMode,
2736    };
2737    use similar_asserts::assert_eq;
2738    use soldeer_core::remappings::RemappingsLocation;
2739    use std::{fs::File, io::Write};
2740    use tempfile::tempdir;
2741
2742    // Helper function to clear `__warnings` in config, since it will be populated during loading
2743    // from file, causing testing problem when comparing to those created from `default()`, etc.
2744    fn clear_warning(config: &mut Config) {
2745        config.warnings = vec![];
2746    }
2747
2748    #[test]
2749    fn default_sender() {
2750        assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
2751    }
2752
2753    #[test]
2754    fn test_caching() {
2755        let mut config = Config::default();
2756        let chain_id = NamedChain::Mainnet;
2757        let url = "https://eth-mainnet.alchemyapi";
2758        assert!(config.enable_caching(url, chain_id));
2759
2760        config.no_storage_caching = true;
2761        assert!(!config.enable_caching(url, chain_id));
2762
2763        config.no_storage_caching = false;
2764        assert!(!config.enable_caching(url, NamedChain::Dev));
2765    }
2766
2767    #[test]
2768    fn test_install_dir() {
2769        figment::Jail::expect_with(|jail| {
2770            let config = Config::load().unwrap();
2771            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2772            jail.create_file(
2773                "foundry.toml",
2774                r"
2775                [profile.default]
2776                libs = ['node_modules', 'lib']
2777            ",
2778            )?;
2779            let config = Config::load().unwrap();
2780            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2781
2782            jail.create_file(
2783                "foundry.toml",
2784                r"
2785                [profile.default]
2786                libs = ['custom', 'node_modules', 'lib']
2787            ",
2788            )?;
2789            let config = Config::load().unwrap();
2790            assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
2791
2792            Ok(())
2793        });
2794    }
2795
2796    #[test]
2797    fn test_figment_is_default() {
2798        figment::Jail::expect_with(|_| {
2799            let mut default: Config = Config::figment().extract()?;
2800            let default2 = Config::default();
2801            default.profile = default2.profile.clone();
2802            default.profiles = default2.profiles.clone();
2803            assert_eq!(default, default2);
2804            Ok(())
2805        });
2806    }
2807
2808    #[test]
2809    fn figment_profiles() {
2810        figment::Jail::expect_with(|jail| {
2811            jail.create_file(
2812                "foundry.toml",
2813                r"
2814                [foo.baz]
2815                libs = ['node_modules', 'lib']
2816
2817                [profile.default]
2818                libs = ['node_modules', 'lib']
2819
2820                [profile.ci]
2821                libs = ['node_modules', 'lib']
2822
2823                [profile.local]
2824                libs = ['node_modules', 'lib']
2825            ",
2826            )?;
2827
2828            let config = crate::Config::load().unwrap();
2829            let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
2830            assert_eq!(config.profiles, expected);
2831
2832            Ok(())
2833        });
2834    }
2835
2836    #[test]
2837    fn test_default_round_trip() {
2838        figment::Jail::expect_with(|_| {
2839            let original = Config::figment();
2840            let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
2841            for figment in &[original, roundtrip] {
2842                let config = Config::from_provider(figment).unwrap();
2843                assert_eq!(config, Config::default().normalized_optimizer_settings());
2844            }
2845            Ok(())
2846        });
2847    }
2848
2849    #[test]
2850    fn ffi_env_disallowed() {
2851        figment::Jail::expect_with(|jail| {
2852            jail.set_env("FOUNDRY_FFI", "true");
2853            jail.set_env("FFI", "true");
2854            jail.set_env("DAPP_FFI", "true");
2855            let config = Config::load().unwrap();
2856            assert!(!config.ffi);
2857
2858            Ok(())
2859        });
2860    }
2861
2862    #[test]
2863    fn test_profile_env() {
2864        figment::Jail::expect_with(|jail| {
2865            jail.set_env("FOUNDRY_PROFILE", "default");
2866            let figment = Config::figment();
2867            assert_eq!(figment.profile(), "default");
2868
2869            jail.set_env("FOUNDRY_PROFILE", "hardhat");
2870            let figment: Figment = Config::hardhat().into();
2871            assert_eq!(figment.profile(), "hardhat");
2872
2873            jail.create_file(
2874                "foundry.toml",
2875                r"
2876                [profile.default]
2877                libs = ['lib']
2878                [profile.local]
2879                libs = ['modules']
2880            ",
2881            )?;
2882            jail.set_env("FOUNDRY_PROFILE", "local");
2883            let config = Config::load().unwrap();
2884            assert_eq!(config.libs, vec![PathBuf::from("modules")]);
2885
2886            Ok(())
2887        });
2888    }
2889
2890    #[test]
2891    fn test_default_test_path() {
2892        figment::Jail::expect_with(|_| {
2893            let config = Config::default();
2894            let paths_config = config.project_paths::<Solc>();
2895            assert_eq!(paths_config.tests, PathBuf::from(r"test"));
2896            Ok(())
2897        });
2898    }
2899
2900    #[test]
2901    fn test_default_libs() {
2902        figment::Jail::expect_with(|jail| {
2903            let config = Config::load().unwrap();
2904            assert_eq!(config.libs, vec![PathBuf::from("lib")]);
2905
2906            fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
2907            let config = Config::load().unwrap();
2908            assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
2909
2910            fs::create_dir_all(jail.directory().join("lib")).unwrap();
2911            let config = Config::load().unwrap();
2912            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2913
2914            Ok(())
2915        });
2916    }
2917
2918    #[test]
2919    fn test_inheritance_from_default_test_path() {
2920        figment::Jail::expect_with(|jail| {
2921            jail.create_file(
2922                "foundry.toml",
2923                r#"
2924                [profile.default]
2925                test = "defaulttest"
2926                src  = "defaultsrc"
2927                libs = ['lib', 'node_modules']
2928
2929                [profile.custom]
2930                src = "customsrc"
2931            "#,
2932            )?;
2933
2934            let config = Config::load().unwrap();
2935            assert_eq!(config.src, PathBuf::from("defaultsrc"));
2936            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2937
2938            jail.set_env("FOUNDRY_PROFILE", "custom");
2939            let config = Config::load().unwrap();
2940            assert_eq!(config.src, PathBuf::from("customsrc"));
2941            assert_eq!(config.test, PathBuf::from("defaulttest"));
2942            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2943
2944            Ok(())
2945        });
2946    }
2947
2948    #[test]
2949    fn test_custom_test_path() {
2950        figment::Jail::expect_with(|jail| {
2951            jail.create_file(
2952                "foundry.toml",
2953                r#"
2954                [profile.default]
2955                test = "mytest"
2956            "#,
2957            )?;
2958
2959            let config = Config::load().unwrap();
2960            let paths_config = config.project_paths::<Solc>();
2961            assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
2962            Ok(())
2963        });
2964    }
2965
2966    #[test]
2967    fn test_remappings() {
2968        figment::Jail::expect_with(|jail| {
2969            jail.create_file(
2970                "foundry.toml",
2971                r#"
2972                [profile.default]
2973                src = "some-source"
2974                out = "some-out"
2975                cache = true
2976            "#,
2977            )?;
2978            let config = Config::load().unwrap();
2979            assert!(config.remappings.is_empty());
2980
2981            jail.create_file(
2982                "remappings.txt",
2983                r"
2984                file-ds-test/=lib/ds-test/
2985                file-other/=lib/other/
2986            ",
2987            )?;
2988
2989            let config = Config::load().unwrap();
2990            assert_eq!(
2991                config.remappings,
2992                vec![
2993                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
2994                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
2995                ],
2996            );
2997
2998            jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
2999            let config = Config::load().unwrap();
3000
3001            assert_eq!(
3002                config.remappings,
3003                vec![
3004                    // From environment (should have precedence over remapping.txt)
3005                    Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
3006                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3007                    // From remapping.txt (should have less precedence than remapping.txt)
3008                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3009                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3010                ],
3011            );
3012
3013            Ok(())
3014        });
3015    }
3016
3017    #[test]
3018    fn test_remappings_override() {
3019        figment::Jail::expect_with(|jail| {
3020            jail.create_file(
3021                "foundry.toml",
3022                r#"
3023                [profile.default]
3024                src = "some-source"
3025                out = "some-out"
3026                cache = true
3027            "#,
3028            )?;
3029            let config = Config::load().unwrap();
3030            assert!(config.remappings.is_empty());
3031
3032            jail.create_file(
3033                "remappings.txt",
3034                r"
3035                ds-test/=lib/ds-test/
3036                other/=lib/other/
3037            ",
3038            )?;
3039
3040            let config = Config::load().unwrap();
3041            assert_eq!(
3042                config.remappings,
3043                vec![
3044                    Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
3045                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3046                ],
3047            );
3048
3049            jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
3050            let config = Config::load().unwrap();
3051
3052            // Remappings should now be:
3053            // - ds-test from environment (lib/ds-test/src/)
3054            // - other from remappings.txt (lib/other/)
3055            // - env-lib from environment (lib/env-lib/)
3056            assert_eq!(
3057                config.remappings,
3058                vec![
3059                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
3060                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
3061                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3062                ],
3063            );
3064
3065            // contains additional remapping to the source dir
3066            assert_eq!(
3067                config.get_all_remappings().collect::<Vec<_>>(),
3068                vec![
3069                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
3070                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
3071                    Remapping::from_str("other/=lib/other/").unwrap(),
3072                ],
3073            );
3074
3075            Ok(())
3076        });
3077    }
3078
3079    #[test]
3080    fn test_can_update_libs() {
3081        figment::Jail::expect_with(|jail| {
3082            jail.create_file(
3083                "foundry.toml",
3084                r#"
3085                [profile.default]
3086                libs = ["node_modules"]
3087            "#,
3088            )?;
3089
3090            let mut config = Config::load().unwrap();
3091            config.libs.push("libs".into());
3092            config.update_libs().unwrap();
3093
3094            let config = Config::load().unwrap();
3095            assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
3096            Ok(())
3097        });
3098    }
3099
3100    #[test]
3101    fn test_large_gas_limit() {
3102        figment::Jail::expect_with(|jail| {
3103            let gas = u64::MAX;
3104            jail.create_file(
3105                "foundry.toml",
3106                &format!(
3107                    r#"
3108                [profile.default]
3109                gas_limit = "{gas}"
3110            "#
3111                ),
3112            )?;
3113
3114            let config = Config::load().unwrap();
3115            assert_eq!(
3116                config,
3117                Config {
3118                    gas_limit: gas.into(),
3119                    ..Config::default().normalized_optimizer_settings()
3120                }
3121            );
3122
3123            Ok(())
3124        });
3125    }
3126
3127    #[test]
3128    #[should_panic]
3129    fn test_toml_file_parse_failure() {
3130        figment::Jail::expect_with(|jail| {
3131            jail.create_file(
3132                "foundry.toml",
3133                r#"
3134                [profile.default]
3135                eth_rpc_url = "https://example.com/
3136            "#,
3137            )?;
3138
3139            let _config = Config::load().unwrap();
3140
3141            Ok(())
3142        });
3143    }
3144
3145    #[test]
3146    #[should_panic]
3147    fn test_toml_file_non_existing_config_var_failure() {
3148        figment::Jail::expect_with(|jail| {
3149            jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3150
3151            let _config = Config::load().unwrap();
3152
3153            Ok(())
3154        });
3155    }
3156
3157    #[test]
3158    fn test_resolve_etherscan_with_chain() {
3159        figment::Jail::expect_with(|jail| {
3160            let env_key = "__BSC_ETHERSCAN_API_KEY";
3161            let env_value = "env value";
3162            jail.create_file(
3163                "foundry.toml",
3164                r#"
3165                [profile.default]
3166
3167                [etherscan]
3168                bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3169            "#,
3170            )?;
3171
3172            let config = Config::load().unwrap();
3173            assert!(
3174                config
3175                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3176                    .is_err()
3177            );
3178
3179            unsafe {
3180                std::env::set_var(env_key, env_value);
3181            }
3182
3183            assert_eq!(
3184                config
3185                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3186                    .unwrap()
3187                    .unwrap()
3188                    .key,
3189                env_value
3190            );
3191
3192            let mut with_key = config;
3193            with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3194
3195            assert_eq!(
3196                with_key
3197                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3198                    .unwrap()
3199                    .unwrap()
3200                    .key,
3201                "via etherscan_api_key"
3202            );
3203
3204            unsafe {
3205                std::env::remove_var(env_key);
3206            }
3207            Ok(())
3208        });
3209    }
3210
3211    #[test]
3212    fn test_resolve_etherscan() {
3213        figment::Jail::expect_with(|jail| {
3214            jail.create_file(
3215                "foundry.toml",
3216                r#"
3217                [profile.default]
3218
3219                [etherscan]
3220                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3221                moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3222            "#,
3223            )?;
3224
3225            let config = Config::load().unwrap();
3226
3227            assert!(config.etherscan.clone().resolved().has_unresolved());
3228
3229            jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3230
3231            let configs = config.etherscan.resolved();
3232            assert!(!configs.has_unresolved());
3233
3234            let mb_urls = Moonbeam.etherscan_urls().unwrap();
3235            let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3236            assert_eq!(
3237                configs,
3238                ResolvedEtherscanConfigs::new([
3239                    (
3240                        "mainnet",
3241                        ResolvedEtherscanConfig {
3242                            api_url: mainnet_urls.0.to_string(),
3243                            chain: Some(NamedChain::Mainnet.into()),
3244                            browser_url: Some(mainnet_urls.1.to_string()),
3245                            key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3246                        }
3247                    ),
3248                    (
3249                        "moonbeam",
3250                        ResolvedEtherscanConfig {
3251                            api_url: mb_urls.0.to_string(),
3252                            chain: Some(Moonbeam.into()),
3253                            browser_url: Some(mb_urls.1.to_string()),
3254                            key: "123456789".to_string(),
3255                        }
3256                    ),
3257                ])
3258            );
3259
3260            Ok(())
3261        });
3262    }
3263
3264    #[test]
3265    fn test_resolve_etherscan_with_versions() {
3266        figment::Jail::expect_with(|jail| {
3267            jail.create_file(
3268                "foundry.toml",
3269                r#"
3270                [profile.default]
3271
3272                [etherscan]
3273                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
3274                moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
3275            "#,
3276            )?;
3277
3278            let config = Config::load().unwrap();
3279
3280            assert!(config.etherscan.clone().resolved().has_unresolved());
3281
3282            jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3283
3284            let configs = config.etherscan.resolved();
3285            assert!(!configs.has_unresolved());
3286
3287            let mb_urls = Moonbeam.etherscan_urls().unwrap();
3288            let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3289            assert_eq!(
3290                configs,
3291                ResolvedEtherscanConfigs::new([
3292                    (
3293                        "mainnet",
3294                        ResolvedEtherscanConfig {
3295                            api_url: mainnet_urls.0.to_string(),
3296                            chain: Some(NamedChain::Mainnet.into()),
3297                            browser_url: Some(mainnet_urls.1.to_string()),
3298                            key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3299                        }
3300                    ),
3301                    (
3302                        "moonbeam",
3303                        ResolvedEtherscanConfig {
3304                            api_url: mb_urls.0.to_string(),
3305                            chain: Some(Moonbeam.into()),
3306                            browser_url: Some(mb_urls.1.to_string()),
3307                            key: "123456789".to_string(),
3308                        }
3309                    ),
3310                ])
3311            );
3312
3313            Ok(())
3314        });
3315    }
3316
3317    #[test]
3318    fn test_resolve_etherscan_chain_id() {
3319        figment::Jail::expect_with(|jail| {
3320            jail.create_file(
3321                "foundry.toml",
3322                r#"
3323                [profile.default]
3324                chain_id = "sepolia"
3325
3326                [etherscan]
3327                sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3328            "#,
3329            )?;
3330
3331            let config = Config::load().unwrap();
3332            let etherscan = config.get_etherscan_config().unwrap().unwrap();
3333            assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3334            assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3335
3336            Ok(())
3337        });
3338    }
3339
3340    #[test]
3341    fn test_resolve_rpc_url() {
3342        figment::Jail::expect_with(|jail| {
3343            jail.create_file(
3344                "foundry.toml",
3345                r#"
3346                [profile.default]
3347                [rpc_endpoints]
3348                optimism = "https://example.com/"
3349                mainnet = "${_CONFIG_MAINNET}"
3350            "#,
3351            )?;
3352            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3353
3354            let mut config = Config::load().unwrap();
3355            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3356
3357            config.eth_rpc_url = Some("mainnet".to_string());
3358            assert_eq!(
3359                "https://eth-mainnet.alchemyapi.io/v2/123455",
3360                config.get_rpc_url_or_localhost_http().unwrap()
3361            );
3362
3363            config.eth_rpc_url = Some("optimism".to_string());
3364            assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
3365
3366            Ok(())
3367        })
3368    }
3369
3370    #[test]
3371    fn test_resolve_rpc_url_if_etherscan_set() {
3372        figment::Jail::expect_with(|jail| {
3373            jail.create_file(
3374                "foundry.toml",
3375                r#"
3376                [profile.default]
3377                etherscan_api_key = "dummy"
3378                [rpc_endpoints]
3379                optimism = "https://example.com/"
3380            "#,
3381            )?;
3382
3383            let config = Config::load().unwrap();
3384            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3385
3386            Ok(())
3387        })
3388    }
3389
3390    #[test]
3391    fn test_resolve_rpc_url_alias() {
3392        figment::Jail::expect_with(|jail| {
3393            jail.create_file(
3394                "foundry.toml",
3395                r#"
3396                [profile.default]
3397                [rpc_endpoints]
3398                polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
3399            "#,
3400            )?;
3401            let mut config = Config::load().unwrap();
3402            config.eth_rpc_url = Some("polygonAmoy".to_string());
3403            assert!(config.get_rpc_url().unwrap().is_err());
3404
3405            jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
3406
3407            let mut config = Config::load().unwrap();
3408            config.eth_rpc_url = Some("polygonAmoy".to_string());
3409            assert_eq!(
3410                "https://polygon-amoy.g.alchemy.com/v2/123455",
3411                config.get_rpc_url().unwrap().unwrap()
3412            );
3413
3414            Ok(())
3415        })
3416    }
3417
3418    #[test]
3419    fn test_resolve_rpc_aliases() {
3420        figment::Jail::expect_with(|jail| {
3421            jail.create_file(
3422                "foundry.toml",
3423                r#"
3424               [profile.default]
3425               [etherscan]
3426               arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
3427               [rpc_endpoints]
3428               arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
3429            "#,
3430            )?;
3431
3432            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
3433            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
3434
3435            let config = Config::load().unwrap();
3436
3437            let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
3438            assert!(config.is_err());
3439            assert_eq!(
3440                config.unwrap_err().to_string(),
3441                "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`"
3442            );
3443
3444            Ok(())
3445        });
3446    }
3447
3448    #[test]
3449    fn test_resolve_rpc_config() {
3450        figment::Jail::expect_with(|jail| {
3451            jail.create_file(
3452                "foundry.toml",
3453                r#"
3454                [rpc_endpoints]
3455                optimism = "https://example.com/"
3456                mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
3457            "#,
3458            )?;
3459            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3460
3461            let config = Config::load().unwrap();
3462            assert_eq!(
3463                RpcEndpoints::new([
3464                    (
3465                        "optimism",
3466                        RpcEndpointType::String(RpcEndpointUrl::Url(
3467                            "https://example.com/".to_string()
3468                        ))
3469                    ),
3470                    (
3471                        "mainnet",
3472                        RpcEndpointType::Config(RpcEndpoint {
3473                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3474                            config: RpcEndpointConfig {
3475                                retries: Some(3),
3476                                retry_backoff: Some(1000),
3477                                compute_units_per_second: Some(1000),
3478                            },
3479                            auth: None,
3480                        })
3481                    ),
3482                ]),
3483                config.rpc_endpoints
3484            );
3485
3486            let resolved = config.rpc_endpoints.resolved();
3487            assert_eq!(
3488                RpcEndpoints::new([
3489                    (
3490                        "optimism",
3491                        RpcEndpointType::String(RpcEndpointUrl::Url(
3492                            "https://example.com/".to_string()
3493                        ))
3494                    ),
3495                    (
3496                        "mainnet",
3497                        RpcEndpointType::Config(RpcEndpoint {
3498                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3499                            config: RpcEndpointConfig {
3500                                retries: Some(3),
3501                                retry_backoff: Some(1000),
3502                                compute_units_per_second: Some(1000),
3503                            },
3504                            auth: None,
3505                        })
3506                    ),
3507                ])
3508                .resolved(),
3509                resolved
3510            );
3511            Ok(())
3512        })
3513    }
3514
3515    #[test]
3516    fn test_resolve_auth() {
3517        figment::Jail::expect_with(|jail| {
3518            jail.create_file(
3519                "foundry.toml",
3520                r#"
3521                [profile.default]
3522                eth_rpc_url = "optimism"
3523                [rpc_endpoints]
3524                optimism = "https://example.com/"
3525                mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
3526            "#,
3527            )?;
3528
3529            let config = Config::load().unwrap();
3530
3531            jail.set_env("_CONFIG_AUTH", "123456");
3532            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3533
3534            assert_eq!(
3535                RpcEndpoints::new([
3536                    (
3537                        "optimism",
3538                        RpcEndpointType::String(RpcEndpointUrl::Url(
3539                            "https://example.com/".to_string()
3540                        ))
3541                    ),
3542                    (
3543                        "mainnet",
3544                        RpcEndpointType::Config(RpcEndpoint {
3545                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3546                            config: RpcEndpointConfig {
3547                                retries: Some(3),
3548                                retry_backoff: Some(1000),
3549                                compute_units_per_second: Some(1000)
3550                            },
3551                            auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
3552                        })
3553                    ),
3554                ]),
3555                config.rpc_endpoints
3556            );
3557            let resolved = config.rpc_endpoints.resolved();
3558            assert_eq!(
3559                RpcEndpoints::new([
3560                    (
3561                        "optimism",
3562                        RpcEndpointType::String(RpcEndpointUrl::Url(
3563                            "https://example.com/".to_string()
3564                        ))
3565                    ),
3566                    (
3567                        "mainnet",
3568                        RpcEndpointType::Config(RpcEndpoint {
3569                            endpoint: RpcEndpointUrl::Url(
3570                                "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3571                            ),
3572                            config: RpcEndpointConfig {
3573                                retries: Some(3),
3574                                retry_backoff: Some(1000),
3575                                compute_units_per_second: Some(1000)
3576                            },
3577                            auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
3578                        })
3579                    ),
3580                ])
3581                .resolved(),
3582                resolved
3583            );
3584
3585            Ok(())
3586        });
3587    }
3588
3589    #[test]
3590    fn test_resolve_endpoints() {
3591        figment::Jail::expect_with(|jail| {
3592            jail.create_file(
3593                "foundry.toml",
3594                r#"
3595                [profile.default]
3596                eth_rpc_url = "optimism"
3597                [rpc_endpoints]
3598                optimism = "https://example.com/"
3599                mainnet = "${_CONFIG_MAINNET}"
3600                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
3601                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
3602            "#,
3603            )?;
3604
3605            let config = Config::load().unwrap();
3606
3607            assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
3608
3609            assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
3610
3611            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3612            jail.set_env("_CONFIG_API_KEY1", "123456");
3613            jail.set_env("_CONFIG_API_KEY2", "98765");
3614
3615            let endpoints = config.rpc_endpoints.resolved();
3616
3617            assert!(!endpoints.has_unresolved());
3618
3619            assert_eq!(
3620                endpoints,
3621                RpcEndpoints::new([
3622                    ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3623                    (
3624                        "mainnet",
3625                        RpcEndpointUrl::Url(
3626                            "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3627                        )
3628                    ),
3629                    (
3630                        "mainnet_2",
3631                        RpcEndpointUrl::Url(
3632                            "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
3633                        )
3634                    ),
3635                    (
3636                        "mainnet_3",
3637                        RpcEndpointUrl::Url(
3638                            "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
3639                        )
3640                    ),
3641                ])
3642                .resolved()
3643            );
3644
3645            Ok(())
3646        });
3647    }
3648
3649    #[test]
3650    fn test_extract_etherscan_config() {
3651        figment::Jail::expect_with(|jail| {
3652            jail.create_file(
3653                "foundry.toml",
3654                r#"
3655                [profile.default]
3656                etherscan_api_key = "optimism"
3657
3658                [etherscan]
3659                optimism = { key = "https://etherscan-optimism.com/" }
3660                amoy = { key = "https://etherscan-amoy.com/" }
3661            "#,
3662            )?;
3663
3664            let mut config = Config::load().unwrap();
3665
3666            let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
3667            assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
3668
3669            config.etherscan_api_key = Some("amoy".to_string());
3670
3671            let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
3672            assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
3673
3674            Ok(())
3675        });
3676    }
3677
3678    #[test]
3679    fn test_extract_etherscan_config_by_chain() {
3680        figment::Jail::expect_with(|jail| {
3681            jail.create_file(
3682                "foundry.toml",
3683                r#"
3684                [profile.default]
3685
3686                [etherscan]
3687                amoy = { key = "https://etherscan-amoy.com/", chain = 80002 }
3688            "#,
3689            )?;
3690
3691            let config = Config::load().unwrap();
3692
3693            let amoy = config
3694                .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
3695                .unwrap()
3696                .unwrap();
3697            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3698
3699            Ok(())
3700        });
3701    }
3702
3703    #[test]
3704    fn test_extract_etherscan_config_by_chain_with_url() {
3705        figment::Jail::expect_with(|jail| {
3706            jail.create_file(
3707                "foundry.toml",
3708                r#"
3709                [profile.default]
3710
3711                [etherscan]
3712                amoy = { key = "https://etherscan-amoy.com/", chain = 80002 , url =  "https://verifier-url.com/"}
3713            "#,
3714            )?;
3715
3716            let config = Config::load().unwrap();
3717
3718            let amoy = config
3719                .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
3720                .unwrap()
3721                .unwrap();
3722            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3723            assert_eq!(amoy.api_url, "https://verifier-url.com/".to_string());
3724
3725            Ok(())
3726        });
3727    }
3728
3729    #[test]
3730    fn test_extract_etherscan_config_by_chain_and_alias() {
3731        figment::Jail::expect_with(|jail| {
3732            jail.create_file(
3733                "foundry.toml",
3734                r#"
3735                [profile.default]
3736                eth_rpc_url = "amoy"
3737
3738                [etherscan]
3739                amoy = { key = "https://etherscan-amoy.com/" }
3740
3741                [rpc_endpoints]
3742                amoy = "https://polygon-amoy.g.alchemy.com/v2/amoy"
3743            "#,
3744            )?;
3745
3746            let config = Config::load().unwrap();
3747
3748            let amoy = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
3749            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3750
3751            let amoy_rpc = config.get_rpc_url().unwrap().unwrap();
3752            assert_eq!(amoy_rpc, "https://polygon-amoy.g.alchemy.com/v2/amoy");
3753            Ok(())
3754        });
3755    }
3756
3757    #[test]
3758    fn test_toml_file() {
3759        figment::Jail::expect_with(|jail| {
3760            jail.create_file(
3761                "foundry.toml",
3762                r#"
3763                [profile.default]
3764                src = "some-source"
3765                out = "some-out"
3766                cache = true
3767                eth_rpc_url = "https://example.com/"
3768                verbosity = 3
3769                remappings = ["ds-test=lib/ds-test/"]
3770                via_ir = true
3771                rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
3772                use_literal_content = false
3773                bytecode_hash = "ipfs"
3774                cbor_metadata = true
3775                revert_strings = "strip"
3776                allow_paths = ["allow", "paths"]
3777                build_info_path = "build-info"
3778                always_use_create_2_factory = true
3779
3780                [rpc_endpoints]
3781                optimism = "https://example.com/"
3782                mainnet = "${RPC_MAINNET}"
3783                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3784                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3785            "#,
3786            )?;
3787
3788            let config = Config::load().unwrap();
3789            assert_eq!(
3790                config,
3791                Config {
3792                    src: "some-source".into(),
3793                    out: "some-out".into(),
3794                    cache: true,
3795                    eth_rpc_url: Some("https://example.com/".to_string()),
3796                    remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
3797                    verbosity: 3,
3798                    via_ir: true,
3799                    rpc_storage_caching: StorageCachingConfig {
3800                        chains: CachedChains::Chains(vec![
3801                            Chain::mainnet(),
3802                            Chain::optimism_mainnet(),
3803                            Chain::from_id(999999)
3804                        ]),
3805                        endpoints: CachedEndpoints::All,
3806                    },
3807                    use_literal_content: false,
3808                    bytecode_hash: BytecodeHash::Ipfs,
3809                    cbor_metadata: true,
3810                    revert_strings: Some(RevertStrings::Strip),
3811                    allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
3812                    rpc_endpoints: RpcEndpoints::new([
3813                        ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3814                        ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3815                        (
3816                            "mainnet_2",
3817                            RpcEndpointUrl::Env(
3818                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3819                            )
3820                        ),
3821                        (
3822                            "mainnet_3",
3823                            RpcEndpointUrl::Env(
3824                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3825                                    .to_string()
3826                            )
3827                        ),
3828                    ]),
3829                    build_info_path: Some("build-info".into()),
3830                    always_use_create_2_factory: true,
3831                    ..Config::default().normalized_optimizer_settings()
3832                }
3833            );
3834
3835            Ok(())
3836        });
3837    }
3838
3839    #[test]
3840    fn test_load_remappings() {
3841        figment::Jail::expect_with(|jail| {
3842            jail.create_file(
3843                "foundry.toml",
3844                r"
3845                [profile.default]
3846                remappings = ['nested/=lib/nested/']
3847            ",
3848            )?;
3849
3850            let config = Config::load_with_root(jail.directory()).unwrap();
3851            assert_eq!(
3852                config.remappings,
3853                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3854            );
3855
3856            Ok(())
3857        });
3858    }
3859
3860    #[test]
3861    fn test_load_full_toml() {
3862        figment::Jail::expect_with(|jail| {
3863            jail.create_file(
3864                "foundry.toml",
3865                r#"
3866                [profile.default]
3867                auto_detect_solc = true
3868                block_base_fee_per_gas = 0
3869                block_coinbase = '0x0000000000000000000000000000000000000000'
3870                block_difficulty = 0
3871                block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
3872                block_number = 1
3873                block_timestamp = 1
3874                use_literal_content = false
3875                bytecode_hash = 'ipfs'
3876                cbor_metadata = true
3877                cache = true
3878                cache_path = 'cache'
3879                evm_version = 'london'
3880                extra_output = []
3881                extra_output_files = []
3882                always_use_create_2_factory = false
3883                ffi = false
3884                force = false
3885                gas_limit = 9223372036854775807
3886                gas_price = 0
3887                gas_reports = ['*']
3888                ignored_error_codes = [1878]
3889                ignored_warnings_from = ["something"]
3890                deny = "never"
3891                initial_balance = '0xffffffffffffffffffffffff'
3892                libraries = []
3893                libs = ['lib']
3894                memory_limit = 134217728
3895                names = false
3896                no_storage_caching = false
3897                no_rpc_rate_limit = false
3898                offline = false
3899                optimizer = true
3900                optimizer_runs = 200
3901                out = 'out'
3902                remappings = ['nested/=lib/nested/']
3903                sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3904                sizes = false
3905                sparse_mode = false
3906                src = 'src'
3907                test = 'test'
3908                tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3909                verbosity = 0
3910                via_ir = false
3911
3912                [profile.default.rpc_storage_caching]
3913                chains = 'all'
3914                endpoints = 'all'
3915
3916                [rpc_endpoints]
3917                optimism = "https://example.com/"
3918                mainnet = "${RPC_MAINNET}"
3919                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3920
3921                [fuzz]
3922                runs = 256
3923                seed = '0x3e8'
3924                max_test_rejects = 65536
3925
3926                [invariant]
3927                runs = 256
3928                depth = 500
3929                fail_on_revert = false
3930                call_override = false
3931                shrink_run_limit = 5000
3932            "#,
3933            )?;
3934
3935            let config = Config::load_with_root(jail.directory()).unwrap();
3936
3937            assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
3938            assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
3939            assert_eq!(
3940                config.remappings,
3941                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3942            );
3943
3944            assert_eq!(
3945                config.rpc_endpoints,
3946                RpcEndpoints::new([
3947                    ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3948                    ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3949                    (
3950                        "mainnet_2",
3951                        RpcEndpointUrl::Env(
3952                            "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3953                        )
3954                    ),
3955                ]),
3956            );
3957
3958            Ok(())
3959        });
3960    }
3961
3962    #[test]
3963    fn test_solc_req() {
3964        figment::Jail::expect_with(|jail| {
3965            jail.create_file(
3966                "foundry.toml",
3967                r#"
3968                [profile.default]
3969                solc_version = "0.8.12"
3970            "#,
3971            )?;
3972
3973            let config = Config::load().unwrap();
3974            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3975
3976            jail.create_file(
3977                "foundry.toml",
3978                r#"
3979                [profile.default]
3980                solc = "0.8.12"
3981            "#,
3982            )?;
3983
3984            let config = Config::load().unwrap();
3985            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3986
3987            jail.create_file(
3988                "foundry.toml",
3989                r#"
3990                [profile.default]
3991                solc = "path/to/local/solc"
3992            "#,
3993            )?;
3994
3995            let config = Config::load().unwrap();
3996            assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
3997
3998            jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
3999            let config = Config::load().unwrap();
4000            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
4001            Ok(())
4002        });
4003    }
4004
4005    // ensures the newer `solc` takes precedence over `solc_version`
4006    #[test]
4007    fn test_backwards_solc_version() {
4008        figment::Jail::expect_with(|jail| {
4009            jail.create_file(
4010                "foundry.toml",
4011                r#"
4012                [default]
4013                solc = "0.8.12"
4014                solc_version = "0.8.20"
4015            "#,
4016            )?;
4017
4018            let config = Config::load().unwrap();
4019            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4020
4021            Ok(())
4022        });
4023
4024        figment::Jail::expect_with(|jail| {
4025            jail.create_file(
4026                "foundry.toml",
4027                r#"
4028                [default]
4029                solc_version = "0.8.20"
4030            "#,
4031            )?;
4032
4033            let config = Config::load().unwrap();
4034            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
4035
4036            Ok(())
4037        });
4038    }
4039
4040    #[test]
4041    fn test_toml_casing_file() {
4042        figment::Jail::expect_with(|jail| {
4043            jail.create_file(
4044                "foundry.toml",
4045                r#"
4046                [profile.default]
4047                src = "some-source"
4048                out = "some-out"
4049                cache = true
4050                eth-rpc-url = "https://example.com/"
4051                evm-version = "berlin"
4052                auto-detect-solc = false
4053            "#,
4054            )?;
4055
4056            let config = Config::load().unwrap();
4057            assert_eq!(
4058                config,
4059                Config {
4060                    src: "some-source".into(),
4061                    out: "some-out".into(),
4062                    cache: true,
4063                    eth_rpc_url: Some("https://example.com/".to_string()),
4064                    auto_detect_solc: false,
4065                    evm_version: EvmVersion::Berlin,
4066                    ..Config::default().normalized_optimizer_settings()
4067                }
4068            );
4069
4070            Ok(())
4071        });
4072    }
4073
4074    #[test]
4075    fn test_output_selection() {
4076        figment::Jail::expect_with(|jail| {
4077            jail.create_file(
4078                "foundry.toml",
4079                r#"
4080                [profile.default]
4081                extra_output = ["metadata", "ir-optimized"]
4082                extra_output_files = ["metadata"]
4083            "#,
4084            )?;
4085
4086            let config = Config::load().unwrap();
4087
4088            assert_eq!(
4089                config.extra_output,
4090                vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
4091            );
4092            assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
4093
4094            Ok(())
4095        });
4096    }
4097
4098    #[test]
4099    fn test_precedence() {
4100        figment::Jail::expect_with(|jail| {
4101            jail.create_file(
4102                "foundry.toml",
4103                r#"
4104                [profile.default]
4105                src = "mysrc"
4106                out = "myout"
4107                verbosity = 3
4108            "#,
4109            )?;
4110
4111            let config = Config::load().unwrap();
4112            assert_eq!(
4113                config,
4114                Config {
4115                    src: "mysrc".into(),
4116                    out: "myout".into(),
4117                    verbosity: 3,
4118                    ..Config::default().normalized_optimizer_settings()
4119                }
4120            );
4121
4122            jail.set_env("FOUNDRY_SRC", r"other-src");
4123            let config = Config::load().unwrap();
4124            assert_eq!(
4125                config,
4126                Config {
4127                    src: "other-src".into(),
4128                    out: "myout".into(),
4129                    verbosity: 3,
4130                    ..Config::default().normalized_optimizer_settings()
4131                }
4132            );
4133
4134            jail.set_env("FOUNDRY_PROFILE", "foo");
4135            let val: Result<String, _> = Config::figment().extract_inner("profile");
4136            assert!(val.is_err());
4137
4138            Ok(())
4139        });
4140    }
4141
4142    #[test]
4143    fn test_extract_basic() {
4144        figment::Jail::expect_with(|jail| {
4145            jail.create_file(
4146                "foundry.toml",
4147                r#"
4148                [profile.default]
4149                src = "mysrc"
4150                out = "myout"
4151                verbosity = 3
4152                evm_version = 'berlin'
4153
4154                [profile.other]
4155                src = "other-src"
4156            "#,
4157            )?;
4158            let loaded = Config::load().unwrap();
4159            assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4160            let base = loaded.into_basic();
4161            let default = Config::default();
4162            assert_eq!(
4163                base,
4164                BasicConfig {
4165                    profile: Config::DEFAULT_PROFILE,
4166                    src: "mysrc".into(),
4167                    out: "myout".into(),
4168                    libs: default.libs.clone(),
4169                    remappings: default.remappings.clone(),
4170                }
4171            );
4172            jail.set_env("FOUNDRY_PROFILE", r"other");
4173            let base = Config::figment().extract::<BasicConfig>().unwrap();
4174            assert_eq!(
4175                base,
4176                BasicConfig {
4177                    profile: Config::DEFAULT_PROFILE,
4178                    src: "other-src".into(),
4179                    out: "myout".into(),
4180                    libs: default.libs.clone(),
4181                    remappings: default.remappings,
4182                }
4183            );
4184            Ok(())
4185        });
4186    }
4187
4188    #[test]
4189    #[should_panic]
4190    fn test_parse_invalid_fuzz_weight() {
4191        figment::Jail::expect_with(|jail| {
4192            jail.create_file(
4193                "foundry.toml",
4194                r"
4195                [fuzz]
4196                dictionary_weight = 101
4197            ",
4198            )?;
4199            let _config = Config::load().unwrap();
4200            Ok(())
4201        });
4202    }
4203
4204    #[test]
4205    fn test_fallback_provider() {
4206        figment::Jail::expect_with(|jail| {
4207            jail.create_file(
4208                "foundry.toml",
4209                r"
4210                [fuzz]
4211                runs = 1
4212                include_storage = false
4213                dictionary_weight = 99
4214
4215                [invariant]
4216                runs = 420
4217
4218                [profile.ci.fuzz]
4219                dictionary_weight = 5
4220
4221                [profile.ci.invariant]
4222                runs = 400
4223            ",
4224            )?;
4225
4226            let invariant_default = InvariantConfig::default();
4227            let config = Config::load().unwrap();
4228
4229            assert_ne!(config.invariant.runs, config.fuzz.runs);
4230            assert_eq!(config.invariant.runs, 420);
4231
4232            assert_ne!(
4233                config.fuzz.dictionary.include_storage,
4234                invariant_default.dictionary.include_storage
4235            );
4236            assert_eq!(
4237                config.invariant.dictionary.include_storage,
4238                config.fuzz.dictionary.include_storage
4239            );
4240
4241            assert_ne!(
4242                config.fuzz.dictionary.dictionary_weight,
4243                invariant_default.dictionary.dictionary_weight
4244            );
4245            assert_eq!(
4246                config.invariant.dictionary.dictionary_weight,
4247                config.fuzz.dictionary.dictionary_weight
4248            );
4249
4250            jail.set_env("FOUNDRY_PROFILE", "ci");
4251            let ci_config = Config::load().unwrap();
4252            assert_eq!(ci_config.fuzz.runs, 1);
4253            assert_eq!(ci_config.invariant.runs, 400);
4254            assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4255            assert_eq!(
4256                ci_config.invariant.dictionary.dictionary_weight,
4257                config.fuzz.dictionary.dictionary_weight
4258            );
4259
4260            Ok(())
4261        })
4262    }
4263
4264    #[test]
4265    fn test_standalone_profile_sections() {
4266        figment::Jail::expect_with(|jail| {
4267            jail.create_file(
4268                "foundry.toml",
4269                r"
4270                [fuzz]
4271                runs = 100
4272
4273                [invariant]
4274                runs = 120
4275
4276                [profile.ci.fuzz]
4277                runs = 420
4278
4279                [profile.ci.invariant]
4280                runs = 500
4281            ",
4282            )?;
4283
4284            let config = Config::load().unwrap();
4285            assert_eq!(config.fuzz.runs, 100);
4286            assert_eq!(config.invariant.runs, 120);
4287
4288            jail.set_env("FOUNDRY_PROFILE", "ci");
4289            let config = Config::load().unwrap();
4290            assert_eq!(config.fuzz.runs, 420);
4291            assert_eq!(config.invariant.runs, 500);
4292
4293            Ok(())
4294        });
4295    }
4296
4297    #[test]
4298    fn can_handle_deviating_dapp_aliases() {
4299        figment::Jail::expect_with(|jail| {
4300            let addr = Address::ZERO;
4301            jail.set_env("DAPP_TEST_NUMBER", 1337);
4302            jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4303            jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4304            jail.set_env("DAPP_TEST_DEPTH", 20);
4305            jail.set_env("DAPP_FORK_BLOCK", 100);
4306            jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4307            jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4308
4309            let config = Config::load().unwrap();
4310
4311            assert_eq!(config.block_number, U256::from(1337));
4312            assert_eq!(config.sender, addr);
4313            assert_eq!(config.fuzz.runs, 420);
4314            assert_eq!(config.invariant.depth, 20);
4315            assert_eq!(config.fork_block_number, Some(100));
4316            assert_eq!(config.optimizer_runs, Some(999));
4317            assert!(!config.optimizer.unwrap());
4318
4319            Ok(())
4320        });
4321    }
4322
4323    #[test]
4324    fn can_parse_libraries() {
4325        figment::Jail::expect_with(|jail| {
4326            jail.set_env(
4327                "DAPP_LIBRARIES",
4328                "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
4329            );
4330            let config = Config::load().unwrap();
4331            assert_eq!(
4332                config.libraries,
4333                vec![
4334                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4335                        .to_string()
4336                ]
4337            );
4338
4339            jail.set_env(
4340                "DAPP_LIBRARIES",
4341                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4342            );
4343            let config = Config::load().unwrap();
4344            assert_eq!(
4345                config.libraries,
4346                vec![
4347                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4348                        .to_string(),
4349                ]
4350            );
4351
4352            jail.set_env(
4353                "DAPP_LIBRARIES",
4354                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4355            );
4356            let config = Config::load().unwrap();
4357            assert_eq!(
4358                config.libraries,
4359                vec![
4360                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4361                        .to_string(),
4362                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4363                        .to_string()
4364                ]
4365            );
4366
4367            Ok(())
4368        });
4369    }
4370
4371    #[test]
4372    fn test_parse_many_libraries() {
4373        figment::Jail::expect_with(|jail| {
4374            jail.create_file(
4375                "foundry.toml",
4376                r"
4377                [profile.default]
4378               libraries= [
4379                        './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4380                        './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4381                        './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4382                        './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4383                        './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4384                    ]
4385            ",
4386            )?;
4387            let config = Config::load().unwrap();
4388
4389            let libs = config.parsed_libraries().unwrap().libs;
4390
4391            similar_asserts::assert_eq!(
4392                libs,
4393                BTreeMap::from([
4394                    (
4395                        PathBuf::from("./src/SizeAuctionDiscount.sol"),
4396                        BTreeMap::from([
4397                            (
4398                                "Chainlink".to_string(),
4399                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4400                            ),
4401                            (
4402                                "Math".to_string(),
4403                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4404                            )
4405                        ])
4406                    ),
4407                    (
4408                        PathBuf::from("./src/SizeAuction.sol"),
4409                        BTreeMap::from([
4410                            (
4411                                "ChainlinkTWAP".to_string(),
4412                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4413                            ),
4414                            (
4415                                "Math".to_string(),
4416                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4417                            )
4418                        ])
4419                    ),
4420                    (
4421                        PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
4422                        BTreeMap::from([(
4423                            "ChainlinkTWAP".to_string(),
4424                            "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4425                        )])
4426                    ),
4427                ])
4428            );
4429
4430            Ok(())
4431        });
4432    }
4433
4434    #[test]
4435    fn config_roundtrip() {
4436        figment::Jail::expect_with(|jail| {
4437            let default = Config::default().normalized_optimizer_settings();
4438            let basic = default.clone().into_basic();
4439            jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
4440
4441            let mut other = Config::load().unwrap();
4442            clear_warning(&mut other);
4443            assert_eq!(default, other);
4444
4445            let other = other.into_basic();
4446            assert_eq!(basic, other);
4447
4448            jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
4449            let mut other = Config::load().unwrap();
4450            clear_warning(&mut other);
4451            assert_eq!(default, other);
4452
4453            Ok(())
4454        });
4455    }
4456
4457    #[test]
4458    fn test_fs_permissions() {
4459        figment::Jail::expect_with(|jail| {
4460            jail.create_file(
4461                "foundry.toml",
4462                r#"
4463                [profile.default]
4464                fs_permissions = [{ access = "read-write", path = "./"}]
4465            "#,
4466            )?;
4467            let loaded = Config::load().unwrap();
4468
4469            assert_eq!(
4470                loaded.fs_permissions,
4471                FsPermissions::new(vec![PathPermission::read_write("./")])
4472            );
4473
4474            jail.create_file(
4475                "foundry.toml",
4476                r#"
4477                [profile.default]
4478                fs_permissions = [{ access = "none", path = "./"}]
4479            "#,
4480            )?;
4481            let loaded = Config::load().unwrap();
4482            assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
4483
4484            Ok(())
4485        });
4486    }
4487
4488    #[test]
4489    fn test_optimizer_settings_basic() {
4490        figment::Jail::expect_with(|jail| {
4491            jail.create_file(
4492                "foundry.toml",
4493                r"
4494                [profile.default]
4495                optimizer = true
4496
4497                [profile.default.optimizer_details]
4498                yul = false
4499
4500                [profile.default.optimizer_details.yulDetails]
4501                stackAllocation = true
4502            ",
4503            )?;
4504            let mut loaded = Config::load().unwrap();
4505            clear_warning(&mut loaded);
4506            assert_eq!(
4507                loaded.optimizer_details,
4508                Some(OptimizerDetails {
4509                    yul: Some(false),
4510                    yul_details: Some(YulDetails {
4511                        stack_allocation: Some(true),
4512                        ..Default::default()
4513                    }),
4514                    ..Default::default()
4515                })
4516            );
4517
4518            let s = loaded.to_string_pretty().unwrap();
4519            jail.create_file("foundry.toml", &s)?;
4520
4521            let mut reloaded = Config::load().unwrap();
4522            clear_warning(&mut reloaded);
4523            assert_eq!(loaded, reloaded);
4524
4525            Ok(())
4526        });
4527    }
4528
4529    #[test]
4530    fn test_model_checker_settings_basic() {
4531        figment::Jail::expect_with(|jail| {
4532            jail.create_file(
4533                "foundry.toml",
4534                r"
4535                [profile.default]
4536
4537                [profile.default.model_checker]
4538                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4539                engine = 'chc'
4540                targets = [ 'assert', 'outOfBounds' ]
4541                timeout = 10000
4542            ",
4543            )?;
4544            let mut loaded = Config::load().unwrap();
4545            clear_warning(&mut loaded);
4546            assert_eq!(
4547                loaded.model_checker,
4548                Some(ModelCheckerSettings {
4549                    contracts: BTreeMap::from([
4550                        ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
4551                        ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
4552                    ]),
4553                    engine: Some(ModelCheckerEngine::CHC),
4554                    targets: Some(vec![
4555                        ModelCheckerTarget::Assert,
4556                        ModelCheckerTarget::OutOfBounds
4557                    ]),
4558                    timeout: Some(10000),
4559                    invariants: None,
4560                    show_unproved: None,
4561                    div_mod_with_slacks: None,
4562                    solvers: None,
4563                    show_unsupported: None,
4564                    show_proved_safe: None,
4565                })
4566            );
4567
4568            let s = loaded.to_string_pretty().unwrap();
4569            jail.create_file("foundry.toml", &s)?;
4570
4571            let mut reloaded = Config::load().unwrap();
4572            clear_warning(&mut reloaded);
4573            assert_eq!(loaded, reloaded);
4574
4575            Ok(())
4576        });
4577    }
4578
4579    #[test]
4580    fn test_model_checker_settings_relative_paths() {
4581        figment::Jail::expect_with(|jail| {
4582            jail.create_file(
4583                "foundry.toml",
4584                r"
4585                [profile.default]
4586
4587                [profile.default.model_checker]
4588                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4589                engine = 'chc'
4590                targets = [ 'assert', 'outOfBounds' ]
4591                timeout = 10000
4592            ",
4593            )?;
4594            let loaded = Config::load().unwrap().sanitized();
4595
4596            // NOTE(onbjerg): We have to canonicalize the path here using dunce because figment will
4597            // canonicalize the jail path using the standard library. The standard library *always*
4598            // transforms Windows paths to some weird extended format, which none of our code base
4599            // does.
4600            let dir = foundry_compilers::utils::canonicalize(jail.directory())
4601                .expect("Could not canonicalize jail path");
4602            assert_eq!(
4603                loaded.model_checker,
4604                Some(ModelCheckerSettings {
4605                    contracts: BTreeMap::from([
4606                        (
4607                            format!("{}", dir.join("a.sol").display()),
4608                            vec!["A1".to_string(), "A2".to_string()]
4609                        ),
4610                        (
4611                            format!("{}", dir.join("b.sol").display()),
4612                            vec!["B1".to_string(), "B2".to_string()]
4613                        ),
4614                    ]),
4615                    engine: Some(ModelCheckerEngine::CHC),
4616                    targets: Some(vec![
4617                        ModelCheckerTarget::Assert,
4618                        ModelCheckerTarget::OutOfBounds
4619                    ]),
4620                    timeout: Some(10000),
4621                    invariants: None,
4622                    show_unproved: None,
4623                    div_mod_with_slacks: None,
4624                    solvers: None,
4625                    show_unsupported: None,
4626                    show_proved_safe: None,
4627                })
4628            );
4629
4630            Ok(())
4631        });
4632    }
4633
4634    #[test]
4635    fn test_fmt_config() {
4636        figment::Jail::expect_with(|jail| {
4637            jail.create_file(
4638                "foundry.toml",
4639                r#"
4640                [fmt]
4641                line_length = 100
4642                tab_width = 2
4643                bracket_spacing = true
4644                style = "space"
4645            "#,
4646            )?;
4647            let loaded = Config::load().unwrap().sanitized();
4648            assert_eq!(
4649                loaded.fmt,
4650                FormatterConfig {
4651                    line_length: 100,
4652                    tab_width: 2,
4653                    bracket_spacing: true,
4654                    style: IndentStyle::Space,
4655                    ..Default::default()
4656                }
4657            );
4658
4659            Ok(())
4660        });
4661    }
4662
4663    #[test]
4664    fn test_lint_config() {
4665        figment::Jail::expect_with(|jail| {
4666            jail.create_file(
4667                "foundry.toml",
4668                r"
4669                [lint]
4670                severity = ['high', 'medium']
4671                exclude_lints = ['incorrect-shift']
4672                ",
4673            )?;
4674            let loaded = Config::load().unwrap().sanitized();
4675            assert_eq!(
4676                loaded.lint,
4677                LinterConfig {
4678                    severity: vec![LintSeverity::High, LintSeverity::Med],
4679                    exclude_lints: vec!["incorrect-shift".into()],
4680                    ..Default::default()
4681                }
4682            );
4683
4684            Ok(())
4685        });
4686    }
4687
4688    #[test]
4689    fn test_invariant_config() {
4690        figment::Jail::expect_with(|jail| {
4691            jail.create_file(
4692                "foundry.toml",
4693                r"
4694                [invariant]
4695                runs = 512
4696                depth = 10
4697            ",
4698            )?;
4699
4700            let loaded = Config::load().unwrap().sanitized();
4701            assert_eq!(
4702                loaded.invariant,
4703                InvariantConfig {
4704                    runs: 512,
4705                    depth: 10,
4706                    failure_persist_dir: Some(PathBuf::from("cache/invariant")),
4707                    ..Default::default()
4708                }
4709            );
4710
4711            Ok(())
4712        });
4713    }
4714
4715    #[test]
4716    fn test_standalone_sections_env() {
4717        figment::Jail::expect_with(|jail| {
4718            jail.create_file(
4719                "foundry.toml",
4720                r"
4721                [fuzz]
4722                runs = 100
4723
4724                [invariant]
4725                depth = 1
4726            ",
4727            )?;
4728
4729            jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
4730            jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
4731            jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
4732
4733            let config = Config::load().unwrap();
4734            assert_eq!(config.fmt.line_length, 95);
4735            assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
4736            assert_eq!(config.invariant.depth, 5);
4737
4738            Ok(())
4739        });
4740    }
4741
4742    #[test]
4743    fn test_parse_with_profile() {
4744        let foundry_str = r"
4745            [profile.default]
4746            src = 'src'
4747            out = 'out'
4748            libs = ['lib']
4749
4750            # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
4751        ";
4752        assert_eq!(
4753            parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
4754            (
4755                Config::DEFAULT_PROFILE,
4756                BasicConfig {
4757                    profile: Config::DEFAULT_PROFILE,
4758                    src: "src".into(),
4759                    out: "out".into(),
4760                    libs: vec!["lib".into()],
4761                    remappings: vec![]
4762                }
4763            )
4764        );
4765    }
4766
4767    #[test]
4768    fn test_implicit_profile_loads() {
4769        figment::Jail::expect_with(|jail| {
4770            jail.create_file(
4771                "foundry.toml",
4772                r"
4773                [default]
4774                src = 'my-src'
4775                out = 'my-out'
4776            ",
4777            )?;
4778            let loaded = Config::load().unwrap().sanitized();
4779            assert_eq!(loaded.src.file_name().unwrap(), "my-src");
4780            assert_eq!(loaded.out.file_name().unwrap(), "my-out");
4781            assert_eq!(
4782                loaded.warnings,
4783                vec![Warning::UnknownSection {
4784                    unknown_section: Profile::new("default"),
4785                    source: Some("foundry.toml".into())
4786                }]
4787            );
4788
4789            Ok(())
4790        });
4791    }
4792
4793    #[test]
4794    fn test_etherscan_api_key() {
4795        figment::Jail::expect_with(|jail| {
4796            jail.create_file(
4797                "foundry.toml",
4798                r"
4799                [default]
4800            ",
4801            )?;
4802            jail.set_env("ETHERSCAN_API_KEY", "");
4803            let loaded = Config::load().unwrap().sanitized();
4804            assert!(loaded.etherscan_api_key.is_none());
4805
4806            jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
4807            let loaded = Config::load().unwrap().sanitized();
4808            assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
4809
4810            Ok(())
4811        });
4812    }
4813
4814    #[test]
4815    fn test_etherscan_api_key_figment() {
4816        figment::Jail::expect_with(|jail| {
4817            jail.create_file(
4818                "foundry.toml",
4819                r"
4820                [default]
4821                etherscan_api_key = 'DUMMY'
4822            ",
4823            )?;
4824            jail.set_env("ETHERSCAN_API_KEY", "ETHER");
4825
4826            let figment = Config::figment_with_root(jail.directory())
4827                .merge(("etherscan_api_key", "USER_KEY"));
4828
4829            let loaded = Config::from_provider(figment).unwrap();
4830            assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
4831
4832            Ok(())
4833        });
4834    }
4835
4836    #[test]
4837    fn test_normalize_defaults() {
4838        figment::Jail::expect_with(|jail| {
4839            jail.create_file(
4840                "foundry.toml",
4841                r"
4842                [default]
4843                solc = '0.8.13'
4844            ",
4845            )?;
4846
4847            let loaded = Config::load().unwrap().sanitized();
4848            assert_eq!(loaded.evm_version, EvmVersion::London);
4849            Ok(())
4850        });
4851    }
4852
4853    // a test to print the config, mainly used to update the example config in the README
4854    #[expect(clippy::disallowed_macros)]
4855    #[test]
4856    #[ignore]
4857    fn print_config() {
4858        let config = Config {
4859            optimizer_details: Some(OptimizerDetails {
4860                peephole: None,
4861                inliner: None,
4862                jumpdest_remover: None,
4863                order_literals: None,
4864                deduplicate: None,
4865                cse: None,
4866                constant_optimizer: Some(true),
4867                yul: Some(true),
4868                yul_details: Some(YulDetails {
4869                    stack_allocation: None,
4870                    optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
4871                }),
4872                simple_counter_for_loop_unchecked_increment: None,
4873            }),
4874            ..Default::default()
4875        };
4876        println!("{}", config.to_string_pretty().unwrap());
4877    }
4878
4879    #[test]
4880    fn can_use_impl_figment_macro() {
4881        #[derive(Default, Serialize)]
4882        struct MyArgs {
4883            #[serde(skip_serializing_if = "Option::is_none")]
4884            root: Option<PathBuf>,
4885        }
4886        impl_figment_convert!(MyArgs);
4887
4888        impl Provider for MyArgs {
4889            fn metadata(&self) -> Metadata {
4890                Metadata::default()
4891            }
4892
4893            fn data(&self) -> Result<Map<Profile, Dict>, Error> {
4894                let value = Value::serialize(self)?;
4895                let error = InvalidType(value.to_actual(), "map".into());
4896                let dict = value.into_dict().ok_or(error)?;
4897                Ok(Map::from([(Config::selected_profile(), dict)]))
4898            }
4899        }
4900
4901        let _figment: Figment = From::from(&MyArgs::default());
4902
4903        #[derive(Default)]
4904        struct Outer {
4905            start: MyArgs,
4906            other: MyArgs,
4907            another: MyArgs,
4908        }
4909        impl_figment_convert!(Outer, start, other, another);
4910
4911        let _figment: Figment = From::from(&Outer::default());
4912    }
4913
4914    #[test]
4915    fn list_cached_blocks() -> eyre::Result<()> {
4916        fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
4917            let block_path = chain_path.join(block_number);
4918            fs::create_dir(block_path.as_path()).unwrap();
4919            let file_path = block_path.join("storage.json");
4920            let mut file = File::create(file_path).unwrap();
4921            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4922        }
4923
4924        fn fake_block_cache_block_path_as_file(
4925            chain_path: &Path,
4926            block_number: &str,
4927            size_bytes: usize,
4928        ) {
4929            let block_path = chain_path.join(block_number);
4930            let mut file = File::create(block_path).unwrap();
4931            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4932        }
4933
4934        let chain_dir = tempdir()?;
4935
4936        fake_block_cache(chain_dir.path(), "1", 100);
4937        fake_block_cache(chain_dir.path(), "2", 500);
4938        fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
4939        // Pollution file that should not show up in the cached block
4940        let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
4941        writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
4942
4943        let result = Config::get_cached_blocks(chain_dir.path())?;
4944
4945        assert_eq!(result.len(), 3);
4946        let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
4947        let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
4948        let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
4949
4950        assert_eq!(block1.0, "1");
4951        assert_eq!(block1.1, 100);
4952        assert_eq!(block2.0, "2");
4953        assert_eq!(block2.1, 500);
4954        assert_eq!(block3.0, "3");
4955        assert_eq!(block3.1, 900);
4956
4957        chain_dir.close()?;
4958        Ok(())
4959    }
4960
4961    #[test]
4962    fn list_etherscan_cache() -> eyre::Result<()> {
4963        fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
4964            let metadata_path = chain_path.join("sources");
4965            let abi_path = chain_path.join("abi");
4966            let _ = fs::create_dir(metadata_path.as_path());
4967            let _ = fs::create_dir(abi_path.as_path());
4968
4969            let metadata_file_path = metadata_path.join(address);
4970            let mut metadata_file = File::create(metadata_file_path).unwrap();
4971            writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4972                .unwrap();
4973
4974            let abi_file_path = abi_path.join(address);
4975            let mut abi_file = File::create(abi_file_path).unwrap();
4976            writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4977                .unwrap();
4978        }
4979
4980        let chain_dir = tempdir()?;
4981
4982        fake_etherscan_cache(chain_dir.path(), "1", 100);
4983        fake_etherscan_cache(chain_dir.path(), "2", 500);
4984
4985        let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
4986
4987        assert_eq!(result, 600);
4988
4989        chain_dir.close()?;
4990        Ok(())
4991    }
4992
4993    #[test]
4994    fn test_parse_error_codes() {
4995        figment::Jail::expect_with(|jail| {
4996            jail.create_file(
4997                "foundry.toml",
4998                r#"
4999                [default]
5000                ignored_error_codes = ["license", "unreachable", 1337]
5001            "#,
5002            )?;
5003
5004            let config = Config::load().unwrap();
5005            assert_eq!(
5006                config.ignored_error_codes,
5007                vec![
5008                    SolidityErrorCode::SpdxLicenseNotProvided,
5009                    SolidityErrorCode::Unreachable,
5010                    SolidityErrorCode::Other(1337)
5011                ]
5012            );
5013
5014            Ok(())
5015        });
5016    }
5017
5018    #[test]
5019    fn test_parse_file_paths() {
5020        figment::Jail::expect_with(|jail| {
5021            jail.create_file(
5022                "foundry.toml",
5023                r#"
5024                [default]
5025                ignored_warnings_from = ["something"]
5026            "#,
5027            )?;
5028
5029            let config = Config::load().unwrap();
5030            assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
5031
5032            Ok(())
5033        });
5034    }
5035
5036    #[test]
5037    fn test_parse_optimizer_settings() {
5038        figment::Jail::expect_with(|jail| {
5039            jail.create_file(
5040                "foundry.toml",
5041                r"
5042                [default]
5043                [profile.default.optimizer_details]
5044            ",
5045            )?;
5046
5047            let config = Config::load().unwrap();
5048            assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
5049
5050            Ok(())
5051        });
5052    }
5053
5054    #[test]
5055    fn test_parse_labels() {
5056        figment::Jail::expect_with(|jail| {
5057            jail.create_file(
5058                "foundry.toml",
5059                r#"
5060                [labels]
5061                0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
5062                0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
5063            "#,
5064            )?;
5065
5066            let config = Config::load().unwrap();
5067            assert_eq!(
5068                config.labels,
5069                AddressHashMap::from_iter(vec![
5070                    (
5071                        address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
5072                        "Uniswap V3: Factory".to_string()
5073                    ),
5074                    (
5075                        address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
5076                        "Uniswap V3: Positions NFT".to_string()
5077                    ),
5078                ])
5079            );
5080
5081            Ok(())
5082        });
5083    }
5084
5085    #[test]
5086    fn test_parse_vyper() {
5087        figment::Jail::expect_with(|jail| {
5088            jail.create_file(
5089                "foundry.toml",
5090                r#"
5091                [vyper]
5092                optimize = "codesize"
5093                path = "/path/to/vyper"
5094                experimental_codegen = true
5095            "#,
5096            )?;
5097
5098            let config = Config::load().unwrap();
5099            assert_eq!(
5100                config.vyper,
5101                VyperConfig {
5102                    optimize: Some(VyperOptimizationMode::Codesize),
5103                    path: Some("/path/to/vyper".into()),
5104                    experimental_codegen: Some(true),
5105                }
5106            );
5107
5108            Ok(())
5109        });
5110    }
5111
5112    #[test]
5113    fn test_parse_soldeer() {
5114        figment::Jail::expect_with(|jail| {
5115            jail.create_file(
5116                "foundry.toml",
5117                r#"
5118                [soldeer]
5119                remappings_generate = true
5120                remappings_regenerate = false
5121                remappings_version = true
5122                remappings_prefix = "@"
5123                remappings_location = "txt"
5124                recursive_deps = true
5125            "#,
5126            )?;
5127
5128            let config = Config::load().unwrap();
5129
5130            assert_eq!(
5131                config.soldeer,
5132                Some(SoldeerConfig {
5133                    remappings_generate: true,
5134                    remappings_regenerate: false,
5135                    remappings_version: true,
5136                    remappings_prefix: "@".to_string(),
5137                    remappings_location: RemappingsLocation::Txt,
5138                    recursive_deps: true,
5139                })
5140            );
5141
5142            Ok(())
5143        });
5144    }
5145
5146    // <https://github.com/foundry-rs/foundry/issues/10926>
5147    #[test]
5148    fn test_resolve_mesc_by_chain_id() {
5149        let s = r#"{
5150    "mesc_version": "0.2.1",
5151    "default_endpoint": null,
5152    "endpoints": {
5153        "sophon_50104": {
5154            "name": "sophon_50104",
5155            "url": "https://rpc.sophon.xyz",
5156            "chain_id": "50104",
5157            "endpoint_metadata": {}
5158        }
5159    },
5160    "network_defaults": {
5161    },
5162    "network_names": {},
5163    "profiles": {
5164        "foundry": {
5165            "name": "foundry",
5166            "default_endpoint": "local_ethereum",
5167            "network_defaults": {
5168                "50104": "sophon_50104"
5169            },
5170            "profile_metadata": {},
5171            "use_mesc": true
5172        }
5173    },
5174    "global_metadata": {}
5175}"#;
5176
5177        let config = serde_json::from_str(s).unwrap();
5178        let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5179            .unwrap()
5180            .unwrap();
5181        assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5182
5183        let s = r#"{
5184    "mesc_version": "0.2.1",
5185    "default_endpoint": null,
5186    "endpoints": {
5187        "sophon_50104": {
5188            "name": "sophon_50104",
5189            "url": "https://rpc.sophon.xyz",
5190            "chain_id": "50104",
5191            "endpoint_metadata": {}
5192        }
5193    },
5194    "network_defaults": {
5195        "50104": "sophon_50104"
5196    },
5197    "network_names": {},
5198    "profiles": {},
5199    "global_metadata": {}
5200}"#;
5201
5202        let config = serde_json::from_str(s).unwrap();
5203        let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5204            .unwrap()
5205            .unwrap();
5206        assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5207    }
5208
5209    #[test]
5210    fn test_get_etherscan_config_with_unknown_chain() {
5211        figment::Jail::expect_with(|jail| {
5212            jail.create_file(
5213                "foundry.toml",
5214                r#"
5215                [etherscan]
5216                mainnet = { chain = 3658348, key = "api-key"}
5217            "#,
5218            )?;
5219            let config = Config::load().unwrap();
5220            let unknown_chain = Chain::from_id(3658348);
5221            let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5222            assert!(result.is_err());
5223            let error_msg = result.unwrap_err().to_string();
5224            assert!(error_msg.contains("No known Etherscan API URL for chain `3658348`"));
5225            assert!(error_msg.contains("Specify a `url`"));
5226            assert!(error_msg.contains("Verify the chain `3658348` is correct"));
5227
5228            Ok(())
5229        });
5230    }
5231
5232    #[test]
5233    fn test_get_etherscan_config_with_existing_chain_and_url() {
5234        figment::Jail::expect_with(|jail| {
5235            jail.create_file(
5236                "foundry.toml",
5237                r#"
5238                [etherscan]
5239                mainnet = { chain = 1, key = "api-key" }
5240            "#,
5241            )?;
5242            let config = Config::load().unwrap();
5243            let unknown_chain = Chain::from_id(1);
5244            let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5245            assert!(result.is_ok());
5246            Ok(())
5247        });
5248    }
5249
5250    #[test]
5251    fn test_can_inherit_a_base_toml() {
5252        figment::Jail::expect_with(|jail| {
5253            // Create base config file with optimizer_runs = 800
5254            jail.create_file(
5255                "base-config.toml",
5256                r#"
5257                    [profile.default]
5258                    optimizer_runs = 800
5259
5260                    [invariant]
5261                    runs = 1000
5262
5263                    [rpc_endpoints]
5264                    mainnet = "https://example.com"
5265                    optimism = "https://example-2.com/"
5266                    "#,
5267            )?;
5268
5269            // Create local config that inherits from base-config.toml
5270            jail.create_file(
5271                "foundry.toml",
5272                r#"
5273                    [profile.default]
5274                    extends = "base-config.toml"
5275
5276                    [invariant]
5277                    runs = 333
5278                    depth = 15
5279
5280                    [rpc_endpoints]
5281                    mainnet = "https://test.xyz/rpc"
5282                    "#,
5283            )?;
5284
5285            let config = Config::load().unwrap();
5286            assert_eq!(config.extends, Some(Extends::Path("base-config.toml".to_string())));
5287
5288            // optimizer_runs should be inherited from base-config.toml
5289            assert_eq!(config.optimizer_runs, Some(800));
5290
5291            // invariant settings should be overridden by local config
5292            assert_eq!(config.invariant.runs, 333);
5293            assert_eq!(config.invariant.depth, 15);
5294
5295            // rpc_endpoints.mainnet should be overridden by local config
5296            // optimism should be inherited from base config
5297            let endpoints = config.rpc_endpoints.resolved();
5298            assert!(
5299                endpoints.get("mainnet").unwrap().url().unwrap().contains("https://test.xyz/rpc")
5300            );
5301            assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("example-2.com"));
5302
5303            Ok(())
5304        });
5305    }
5306
5307    #[test]
5308    fn test_inheritance_validation() {
5309        figment::Jail::expect_with(|jail| {
5310            // Test 1: Base file with 'extends' should fail
5311            jail.create_file(
5312                "base-with-inherit.toml",
5313                r#"
5314                    [profile.default]
5315                    extends = "another.toml"
5316                    optimizer_runs = 800
5317                    "#,
5318            )?;
5319
5320            jail.create_file(
5321                "foundry.toml",
5322                r#"
5323                    [profile.default]
5324                    extends = "base-with-inherit.toml"
5325                    "#,
5326            )?;
5327
5328            // Should fail because base file has 'extends'
5329            let result = Config::load();
5330            assert!(result.is_err());
5331            assert!(result.unwrap_err().to_string().contains("Nested inheritance is not allowed"));
5332
5333            // Test 2: Circular reference should fail
5334            jail.create_file(
5335                "foundry.toml",
5336                r#"
5337                    [profile.default]
5338                    extends = "foundry.toml"
5339                    "#,
5340            )?;
5341
5342            let result = Config::load();
5343            assert!(result.is_err());
5344            assert!(result.unwrap_err().to_string().contains("cannot inherit from itself"));
5345
5346            // Test 3: Non-existent base file should fail
5347            jail.create_file(
5348                "foundry.toml",
5349                r#"
5350                    [profile.default]
5351                    extends = "non-existent.toml"
5352                    "#,
5353            )?;
5354
5355            let result = Config::load();
5356            assert!(result.is_err());
5357            let err_msg = result.unwrap_err().to_string();
5358            assert!(
5359                err_msg.contains("does not exist")
5360                    || err_msg.contains("Failed to resolve inherited config path"),
5361                "Error message: {err_msg}"
5362            );
5363
5364            Ok(())
5365        });
5366    }
5367
5368    #[test]
5369    fn test_complex_inheritance_merging() {
5370        figment::Jail::expect_with(|jail| {
5371            // Create a comprehensive base config
5372            jail.create_file(
5373                "base.toml",
5374                r#"
5375                    [profile.default]
5376                    optimizer = true
5377                    optimizer_runs = 1000
5378                    via_ir = false
5379                    solc = "0.8.19"
5380
5381                    [invariant]
5382                    runs = 500
5383                    depth = 100
5384
5385                    [fuzz]
5386                    runs = 256
5387                    seed = "0x123"
5388
5389                    [rpc_endpoints]
5390                    mainnet = "https://base-mainnet.com"
5391                    optimism = "https://base-optimism.com"
5392                    arbitrum = "https://base-arbitrum.com"
5393                    "#,
5394            )?;
5395
5396            // Create local config that overrides some values
5397            jail.create_file(
5398                "foundry.toml",
5399                r#"
5400                    [profile.default]
5401                    extends = "base.toml"
5402                    optimizer_runs = 200  # Override
5403                    via_ir = true        # Override
5404                    # optimizer and solc are inherited
5405
5406                    [invariant]
5407                    runs = 333  # Override
5408                    # depth is inherited
5409
5410                    # fuzz section is fully inherited
5411
5412                    [rpc_endpoints]
5413                    mainnet = "https://local-mainnet.com"  # Override
5414                    # optimism and arbitrum are inherited
5415                    polygon = "https://local-polygon.com"  # New
5416                    "#,
5417            )?;
5418
5419            let config = Config::load().unwrap();
5420
5421            // Check profile.default values
5422            assert_eq!(config.optimizer, Some(true));
5423            assert_eq!(config.optimizer_runs, Some(200));
5424            assert_eq!(config.via_ir, true);
5425            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
5426
5427            // Check invariant section
5428            assert_eq!(config.invariant.runs, 333);
5429            assert_eq!(config.invariant.depth, 100);
5430
5431            // Check fuzz section (fully inherited)
5432            assert_eq!(config.fuzz.runs, 256);
5433            assert_eq!(config.fuzz.seed, Some(U256::from(0x123)));
5434
5435            // Check rpc_endpoints
5436            let endpoints = config.rpc_endpoints.resolved();
5437            assert!(endpoints.get("mainnet").unwrap().url().unwrap().contains("local-mainnet"));
5438            assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("base-optimism"));
5439            assert!(endpoints.get("arbitrum").unwrap().url().unwrap().contains("base-arbitrum"));
5440            assert!(endpoints.get("polygon").unwrap().url().unwrap().contains("local-polygon"));
5441
5442            Ok(())
5443        });
5444    }
5445
5446    #[test]
5447    fn test_inheritance_with_different_profiles() {
5448        figment::Jail::expect_with(|jail| {
5449            // Create base config with multiple profiles
5450            jail.create_file(
5451                "base.toml",
5452                r#"
5453                    [profile.default]
5454                    optimizer = true
5455                    optimizer_runs = 200
5456
5457                    [profile.ci]
5458                    optimizer = true
5459                    optimizer_runs = 10000
5460                    via_ir = true
5461
5462                    [profile.dev]
5463                    optimizer = false
5464                    "#,
5465            )?;
5466
5467            // Local config inherits from base - only for default profile
5468            jail.create_file(
5469                "foundry.toml",
5470                r#"
5471                    [profile.default]
5472                    extends = "base.toml"
5473                    verbosity = 3
5474
5475                    [profile.ci]
5476                    optimizer_runs = 5000  # This doesn't inherit from base.toml's ci profile
5477                    "#,
5478            )?;
5479
5480            // Test default profile
5481            let config = Config::load().unwrap();
5482            assert_eq!(config.optimizer, Some(true));
5483            assert_eq!(config.optimizer_runs, Some(200));
5484            assert_eq!(config.verbosity, 3);
5485
5486            // Test CI profile (NO 'extends', so doesn't inherit from base)
5487            jail.set_env("FOUNDRY_PROFILE", "ci");
5488            let config = Config::load().unwrap();
5489            assert_eq!(config.optimizer_runs, Some(5000));
5490            assert_eq!(config.optimizer, Some(true));
5491            // via_ir is not set in local ci profile and there's no 'extends', so default
5492            assert_eq!(config.via_ir, false);
5493
5494            Ok(())
5495        });
5496    }
5497
5498    #[test]
5499    fn test_inheritance_with_env_vars() {
5500        figment::Jail::expect_with(|jail| {
5501            jail.create_file(
5502                "base.toml",
5503                r#"
5504                    [profile.default]
5505                    optimizer_runs = 500
5506                    sender = "0x0000000000000000000000000000000000000001"
5507                    verbosity = 1
5508                    "#,
5509            )?;
5510
5511            jail.create_file(
5512                "foundry.toml",
5513                r#"
5514                    [profile.default]
5515                    extends = "base.toml"
5516                    verbosity = 2
5517                    "#,
5518            )?;
5519
5520            // Environment variables should override both base and local values
5521            jail.set_env("FOUNDRY_OPTIMIZER_RUNS", "999");
5522            jail.set_env("FOUNDRY_VERBOSITY", "4");
5523
5524            let config = Config::load().unwrap();
5525            assert_eq!(config.optimizer_runs, Some(999));
5526            assert_eq!(config.verbosity, 4);
5527            assert_eq!(
5528                config.sender,
5529                "0x0000000000000000000000000000000000000001"
5530                    .parse::<alloy_primitives::Address>()
5531                    .unwrap()
5532            );
5533
5534            Ok(())
5535        });
5536    }
5537
5538    #[test]
5539    fn test_inheritance_with_subdirectories() {
5540        figment::Jail::expect_with(|jail| {
5541            // Create base config in a subdirectory
5542            jail.create_dir("configs")?;
5543            jail.create_file(
5544                "configs/base.toml",
5545                r#"
5546                    [profile.default]
5547                    optimizer_runs = 800
5548                    src = "contracts"
5549                    "#,
5550            )?;
5551
5552            // Reference it with relative path
5553            jail.create_file(
5554                "foundry.toml",
5555                r#"
5556                    [profile.default]
5557                    extends = "configs/base.toml"
5558                    test = "tests"
5559                    "#,
5560            )?;
5561
5562            let config = Config::load().unwrap();
5563            assert_eq!(config.optimizer_runs, Some(800));
5564            assert_eq!(config.src, PathBuf::from("contracts"));
5565            assert_eq!(config.test, PathBuf::from("tests"));
5566
5567            // Test with parent directory reference
5568            jail.create_dir("project")?;
5569            jail.create_file(
5570                "shared-base.toml",
5571                r#"
5572                    [profile.default]
5573                    optimizer_runs = 1500
5574                    "#,
5575            )?;
5576
5577            jail.create_file(
5578                "project/foundry.toml",
5579                r#"
5580                    [profile.default]
5581                    extends = "../shared-base.toml"
5582                    "#,
5583            )?;
5584
5585            std::env::set_current_dir(jail.directory().join("project")).unwrap();
5586            let config = Config::load().unwrap();
5587            assert_eq!(config.optimizer_runs, Some(1500));
5588
5589            Ok(())
5590        });
5591    }
5592
5593    #[test]
5594    fn test_inheritance_with_empty_files() {
5595        figment::Jail::expect_with(|jail| {
5596            // Empty base file
5597            jail.create_file(
5598                "base.toml",
5599                r#"
5600                    [profile.default]
5601                    "#,
5602            )?;
5603
5604            jail.create_file(
5605                "foundry.toml",
5606                r#"
5607                    [profile.default]
5608                    extends = "base.toml"
5609                    optimizer_runs = 300
5610                    "#,
5611            )?;
5612
5613            let config = Config::load().unwrap();
5614            assert_eq!(config.optimizer_runs, Some(300));
5615
5616            // Empty local file (only 'extends')
5617            jail.create_file(
5618                "base2.toml",
5619                r#"
5620                    [profile.default]
5621                    optimizer_runs = 400
5622                    via_ir = true
5623                    "#,
5624            )?;
5625
5626            jail.create_file(
5627                "foundry.toml",
5628                r#"
5629                    [profile.default]
5630                    extends = "base2.toml"
5631                    "#,
5632            )?;
5633
5634            let config = Config::load().unwrap();
5635            assert_eq!(config.optimizer_runs, Some(400));
5636            assert!(config.via_ir);
5637
5638            Ok(())
5639        });
5640    }
5641
5642    #[test]
5643    fn test_inheritance_array_and_table_merging() {
5644        figment::Jail::expect_with(|jail| {
5645            jail.create_file(
5646                "base.toml",
5647                r#"
5648                    [profile.default]
5649                    libs = ["lib", "node_modules"]
5650                    ignored_error_codes = [5667, 1878]
5651                    extra_output = ["metadata", "ir"]
5652
5653                    [profile.default.model_checker]
5654                    engine = "chc"
5655                    timeout = 10000
5656                    targets = ["assert"]
5657
5658                    [profile.default.optimizer_details]
5659                    peephole = true
5660                    inliner = true
5661                    "#,
5662            )?;
5663
5664            jail.create_file(
5665                "foundry.toml",
5666                r#"
5667                    [profile.default]
5668                    extends = "base.toml"
5669                    libs = ["custom-lib"]  # Concatenates with base array
5670                    ignored_error_codes = [2018]  # Concatenates with base array
5671
5672                    [profile.default.model_checker]
5673                    timeout = 5000  # Overrides base value
5674                    # engine and targets are inherited
5675
5676                    [profile.default.optimizer_details]
5677                    jumpdest_remover = true  # Adds new field
5678                    # peephole and inliner are inherited
5679                    "#,
5680            )?;
5681
5682            let config = Config::load().unwrap();
5683
5684            // Arrays are now concatenated with admerge (base + local)
5685            assert_eq!(
5686                config.libs,
5687                vec![
5688                    PathBuf::from("lib"),
5689                    PathBuf::from("node_modules"),
5690                    PathBuf::from("custom-lib")
5691                ]
5692            );
5693            assert_eq!(
5694                config.ignored_error_codes,
5695                vec![
5696                    SolidityErrorCode::UnusedFunctionParameter, // 5667 from base.toml
5697                    SolidityErrorCode::SpdxLicenseNotProvided,  // 1878 from base.toml
5698                    SolidityErrorCode::FunctionStateMutabilityCanBeRestricted  // 2018 from local
5699                ]
5700            );
5701
5702            // Tables are deep-merged
5703            assert_eq!(config.model_checker.as_ref().unwrap().timeout, Some(5000));
5704            assert_eq!(
5705                config.model_checker.as_ref().unwrap().engine,
5706                Some(ModelCheckerEngine::CHC)
5707            );
5708            assert_eq!(
5709                config.model_checker.as_ref().unwrap().targets,
5710                Some(vec![ModelCheckerTarget::Assert])
5711            );
5712
5713            // optimizer_details table is actually merged, not replaced
5714            assert_eq!(config.optimizer_details.as_ref().unwrap().peephole, Some(true));
5715            assert_eq!(config.optimizer_details.as_ref().unwrap().inliner, Some(true));
5716            assert_eq!(config.optimizer_details.as_ref().unwrap().jumpdest_remover, None);
5717
5718            Ok(())
5719        });
5720    }
5721
5722    #[test]
5723    fn test_inheritance_with_special_sections() {
5724        figment::Jail::expect_with(|jail| {
5725            jail.create_file(
5726                "base.toml",
5727                r#"
5728                    [profile.default]
5729                    # Base file should not have 'extends' to avoid nested inheritance
5730
5731                    [labels]
5732                    "0x0000000000000000000000000000000000000001" = "Alice"
5733                    "0x0000000000000000000000000000000000000002" = "Bob"
5734
5735                    [[profile.default.fs_permissions]]
5736                    access = "read"
5737                    path = "./src"
5738
5739                    [[profile.default.fs_permissions]]
5740                    access = "read-write"
5741                    path = "./cache"
5742                    "#,
5743            )?;
5744
5745            jail.create_file(
5746                "foundry.toml",
5747                r#"
5748                    [profile.default]
5749                    extends = "base.toml"
5750
5751                    [labels]
5752                    "0x0000000000000000000000000000000000000002" = "Bob Updated"
5753                    "0x0000000000000000000000000000000000000003" = "Charlie"
5754
5755                    [[profile.default.fs_permissions]]
5756                    access = "read"
5757                    path = "./test"
5758                    "#,
5759            )?;
5760
5761            let config = Config::load().unwrap();
5762
5763            // Labels should be merged
5764            assert_eq!(
5765                config.labels.get(
5766                    &"0x0000000000000000000000000000000000000001"
5767                        .parse::<alloy_primitives::Address>()
5768                        .unwrap()
5769                ),
5770                Some(&"Alice".to_string())
5771            );
5772            assert_eq!(
5773                config.labels.get(
5774                    &"0x0000000000000000000000000000000000000002"
5775                        .parse::<alloy_primitives::Address>()
5776                        .unwrap()
5777                ),
5778                Some(&"Bob Updated".to_string())
5779            );
5780            assert_eq!(
5781                config.labels.get(
5782                    &"0x0000000000000000000000000000000000000003"
5783                        .parse::<alloy_primitives::Address>()
5784                        .unwrap()
5785                ),
5786                Some(&"Charlie".to_string())
5787            );
5788
5789            // fs_permissions array is now concatenated with addmerge (base + local)
5790            assert_eq!(config.fs_permissions.permissions.len(), 3); // 2 from base + 1 from local
5791            // Check that all permissions are present
5792            assert!(
5793                config
5794                    .fs_permissions
5795                    .permissions
5796                    .iter()
5797                    .any(|p| p.path.to_str().unwrap() == "./src")
5798            );
5799            assert!(
5800                config
5801                    .fs_permissions
5802                    .permissions
5803                    .iter()
5804                    .any(|p| p.path.to_str().unwrap() == "./cache")
5805            );
5806            assert!(
5807                config
5808                    .fs_permissions
5809                    .permissions
5810                    .iter()
5811                    .any(|p| p.path.to_str().unwrap() == "./test")
5812            );
5813
5814            Ok(())
5815        });
5816    }
5817
5818    #[test]
5819    fn test_inheritance_with_compilation_settings() {
5820        figment::Jail::expect_with(|jail| {
5821            jail.create_file(
5822                "base.toml",
5823                r#"
5824                    [profile.default]
5825                    solc = "0.8.19"
5826                    evm_version = "paris"
5827                    via_ir = false
5828                    optimizer = true
5829                    optimizer_runs = 200
5830
5831                    [profile.default.optimizer_details]
5832                    peephole = true
5833                    inliner = false
5834                    jumpdest_remover = true
5835                    order_literals = false
5836                    deduplicate = true
5837                    cse = true
5838                    constant_optimizer = true
5839                    yul = true
5840
5841                    [profile.default.optimizer_details.yul_details]
5842                    stack_allocation = true
5843                    optimizer_steps = "dhfoDgvulfnTUtnIf"
5844                    "#,
5845            )?;
5846
5847            jail.create_file(
5848                "foundry.toml",
5849                r#"
5850                    [profile.default]
5851                    extends = "base.toml"
5852                    evm_version = "shanghai"  # Override
5853                    optimizer_runs = 1000  # Override
5854
5855                    [profile.default.optimizer_details]
5856                    inliner = true  # Override
5857                    # Rest inherited
5858                    "#,
5859            )?;
5860
5861            let config = Config::load().unwrap();
5862
5863            // Check compilation settings
5864            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
5865            assert_eq!(config.evm_version, EvmVersion::Shanghai);
5866            assert_eq!(config.via_ir, false);
5867            assert_eq!(config.optimizer, Some(true));
5868            assert_eq!(config.optimizer_runs, Some(1000));
5869
5870            // Check optimizer details - the table is actually merged
5871            let details = config.optimizer_details.as_ref().unwrap();
5872            assert_eq!(details.peephole, Some(true));
5873            assert_eq!(details.inliner, Some(true));
5874            assert_eq!(details.jumpdest_remover, None);
5875            assert_eq!(details.order_literals, None);
5876            assert_eq!(details.deduplicate, Some(true));
5877            assert_eq!(details.cse, Some(true));
5878            assert_eq!(details.constant_optimizer, None);
5879            assert_eq!(details.yul, Some(true));
5880
5881            // Check yul details - inherited from base
5882            if let Some(yul_details) = details.yul_details.as_ref() {
5883                assert_eq!(yul_details.stack_allocation, Some(true));
5884                assert_eq!(yul_details.optimizer_steps, Some("dhfoDgvulfnTUtnIf".to_string()));
5885            }
5886
5887            Ok(())
5888        });
5889    }
5890
5891    #[test]
5892    fn test_inheritance_with_remappings() {
5893        figment::Jail::expect_with(|jail| {
5894            jail.create_file(
5895                "base.toml",
5896                r#"
5897                    [profile.default]
5898                    remappings = [
5899                        "forge-std/=lib/forge-std/src/",
5900                        "@openzeppelin/=lib/openzeppelin-contracts/",
5901                        "ds-test/=lib/ds-test/src/"
5902                    ]
5903                    auto_detect_remappings = false
5904                    "#,
5905            )?;
5906
5907            jail.create_file(
5908                "foundry.toml",
5909                r#"
5910                    [profile.default]
5911                    extends = "base.toml"
5912                    remappings = [
5913                        "@custom/=lib/custom/",
5914                        "ds-test/=lib/forge-std/lib/ds-test/src/"  # Note: This will be added alongside base remappings
5915                    ]
5916                    "#,
5917            )?;
5918
5919            let config = Config::load().unwrap();
5920
5921            // Remappings array is now concatenated with admerge (base + local)
5922            assert!(config.remappings.iter().any(|r| r.to_string().contains("@custom/")));
5923            assert!(config.remappings.iter().any(|r| r.to_string().contains("ds-test/")));
5924            assert!(config.remappings.iter().any(|r| r.to_string().contains("forge-std/")));
5925            assert!(config.remappings.iter().any(|r| r.to_string().contains("@openzeppelin/")));
5926
5927            // auto_detect_remappings should be inherited
5928            assert!(!config.auto_detect_remappings);
5929
5930            Ok(())
5931        });
5932    }
5933
5934    #[test]
5935    fn test_inheritance_with_multiple_profiles_and_single_file() {
5936        figment::Jail::expect_with(|jail| {
5937            // Create base config with prod and test profiles
5938            jail.create_file(
5939                "base.toml",
5940                r#"
5941                    [profile.prod]
5942                    optimizer = true
5943                    optimizer_runs = 10000
5944                    via_ir = true
5945
5946                    [profile.test]
5947                    optimizer = false
5948
5949                    [profile.test.fuzz]
5950                    runs = 100
5951                    "#,
5952            )?;
5953
5954            // Local config inherits from base for prod profile
5955            jail.create_file(
5956                "foundry.toml",
5957                r#"
5958                    [profile.prod]
5959                    extends = "base.toml"
5960                    evm_version = "shanghai"  # Additional setting
5961
5962                    [profile.test]
5963                    extends = "base.toml"
5964
5965                    [profile.test.fuzz]
5966                    runs = 500  # Override
5967                    "#,
5968            )?;
5969
5970            // Test prod profile
5971            jail.set_env("FOUNDRY_PROFILE", "prod");
5972            let config = Config::load().unwrap();
5973            assert_eq!(config.optimizer, Some(true));
5974            assert_eq!(config.optimizer_runs, Some(10000));
5975            assert_eq!(config.via_ir, true);
5976            assert_eq!(config.evm_version, EvmVersion::Shanghai);
5977
5978            // Test test profile
5979            jail.set_env("FOUNDRY_PROFILE", "test");
5980            let config = Config::load().unwrap();
5981            assert_eq!(config.optimizer, Some(false));
5982            assert_eq!(config.fuzz.runs, 500);
5983
5984            Ok(())
5985        });
5986    }
5987
5988    #[test]
5989    fn test_inheritance_with_multiple_profiles_and_files() {
5990        figment::Jail::expect_with(|jail| {
5991            jail.create_file(
5992                "prod.toml",
5993                r#"
5994                    [profile.prod]
5995                    optimizer = true
5996                    optimizer_runs = 20000
5997                    gas_limit = 50000000
5998                    "#,
5999            )?;
6000            jail.create_file(
6001                "dev.toml",
6002                r#"
6003                    [profile.dev]
6004                    optimizer = true
6005                    optimizer_runs = 333
6006                    gas_limit = 555555
6007                    "#,
6008            )?;
6009
6010            // Local config with only both profiles
6011            jail.create_file(
6012                "foundry.toml",
6013                r#"
6014                    [profile.dev]
6015                    extends = "dev.toml"
6016                    sender = "0x0000000000000000000000000000000000000001"
6017
6018                    [profile.prod]
6019                    extends = "prod.toml"
6020                    sender = "0x0000000000000000000000000000000000000002"
6021                    "#,
6022            )?;
6023
6024            // Test that prod profile correctly inherits even without a default profile
6025            jail.set_env("FOUNDRY_PROFILE", "dev");
6026            let config = Config::load().unwrap();
6027            assert_eq!(config.optimizer, Some(true));
6028            assert_eq!(config.optimizer_runs, Some(333));
6029            assert_eq!(config.gas_limit, 555555.into());
6030            assert_eq!(
6031                config.sender,
6032                "0x0000000000000000000000000000000000000001"
6033                    .parse::<alloy_primitives::Address>()
6034                    .unwrap()
6035            );
6036
6037            // Test that prod profile correctly inherits even without a default profile
6038            jail.set_env("FOUNDRY_PROFILE", "prod");
6039            let config = Config::load().unwrap();
6040            assert_eq!(config.optimizer, Some(true));
6041            assert_eq!(config.optimizer_runs, Some(20000));
6042            assert_eq!(config.gas_limit, 50000000.into());
6043            assert_eq!(
6044                config.sender,
6045                "0x0000000000000000000000000000000000000002"
6046                    .parse::<alloy_primitives::Address>()
6047                    .unwrap()
6048            );
6049
6050            Ok(())
6051        });
6052    }
6053
6054    #[test]
6055    fn test_extends_strategy_extend_arrays() {
6056        figment::Jail::expect_with(|jail| {
6057            // Create base config with arrays
6058            jail.create_file(
6059                "base.toml",
6060                r#"
6061                    [profile.default]
6062                    libs = ["lib", "node_modules"]
6063                    ignored_error_codes = [5667, 1878]
6064                    optimizer_runs = 200
6065                    "#,
6066            )?;
6067
6068            // Local config extends with extend-arrays strategy (concatenates arrays)
6069            jail.create_file(
6070                "foundry.toml",
6071                r#"
6072                    [profile.default]
6073                    extends = "base.toml"
6074                    libs = ["mylib", "customlib"]
6075                    ignored_error_codes = [1234]
6076                    optimizer_runs = 500
6077                    "#,
6078            )?;
6079
6080            let config = Config::load().unwrap();
6081
6082            // Arrays should be concatenated (base + local)
6083            assert_eq!(config.libs.len(), 4);
6084            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6085            assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6086            assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
6087            assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
6088
6089            assert_eq!(config.ignored_error_codes.len(), 3);
6090            assert!(
6091                config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
6092            ); // 5667
6093            assert!(
6094                config.ignored_error_codes.contains(&SolidityErrorCode::SpdxLicenseNotProvided)
6095            ); // 1878
6096            assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); // 1234 - generic
6097
6098            // Non-array values should be replaced
6099            assert_eq!(config.optimizer_runs, Some(500));
6100
6101            Ok(())
6102        });
6103    }
6104
6105    #[test]
6106    fn test_extends_strategy_replace_arrays() {
6107        figment::Jail::expect_with(|jail| {
6108            // Create base config with arrays
6109            jail.create_file(
6110                "base.toml",
6111                r#"
6112                    [profile.default]
6113                    libs = ["lib", "node_modules"]
6114                    ignored_error_codes = [5667, 1878]
6115                    optimizer_runs = 200
6116                    "#,
6117            )?;
6118
6119            // Local config extends with replace-arrays strategy (replaces arrays entirely)
6120            jail.create_file(
6121                "foundry.toml",
6122                r#"
6123                    [profile.default]
6124                    extends = { path = "base.toml", strategy = "replace-arrays" }
6125                    libs = ["mylib", "customlib"]
6126                    ignored_error_codes = [1234]
6127                    optimizer_runs = 500
6128                    "#,
6129            )?;
6130
6131            let config = Config::load().unwrap();
6132
6133            // Arrays should be replaced entirely (only local values)
6134            assert_eq!(config.libs.len(), 2);
6135            assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
6136            assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
6137            assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
6138            assert!(!config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6139
6140            assert_eq!(config.ignored_error_codes.len(), 1);
6141            assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); // 1234
6142            assert!(
6143                !config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
6144            ); // 5667
6145
6146            // Non-array values should be replaced
6147            assert_eq!(config.optimizer_runs, Some(500));
6148
6149            Ok(())
6150        });
6151    }
6152
6153    #[test]
6154    fn test_extends_strategy_no_collision_success() {
6155        figment::Jail::expect_with(|jail| {
6156            // Create base config
6157            jail.create_file(
6158                "base.toml",
6159                r#"
6160                    [profile.default]
6161                    optimizer = true
6162                    optimizer_runs = 200
6163                    src = "src"
6164                    "#,
6165            )?;
6166
6167            // Local config extends with no-collision strategy and no conflicts
6168            jail.create_file(
6169                "foundry.toml",
6170                r#"
6171                    [profile.default]
6172                    extends = { path = "base.toml", strategy = "no-collision" }
6173                    test = "tests"
6174                    libs = ["lib"]
6175                    "#,
6176            )?;
6177
6178            let config = Config::load().unwrap();
6179
6180            // Values from base should be present
6181            assert_eq!(config.optimizer, Some(true));
6182            assert_eq!(config.optimizer_runs, Some(200));
6183            assert_eq!(config.src, PathBuf::from("src"));
6184
6185            // Values from local should be present
6186            assert_eq!(config.test, PathBuf::from("tests"));
6187            assert_eq!(config.libs.len(), 1);
6188            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6189
6190            Ok(())
6191        });
6192    }
6193
6194    #[test]
6195    fn test_extends_strategy_no_collision_error() {
6196        figment::Jail::expect_with(|jail| {
6197            // Create base config
6198            jail.create_file(
6199                "base.toml",
6200                r#"
6201                    [profile.default]
6202                    optimizer = true
6203                    optimizer_runs = 200
6204                    libs = ["lib", "node_modules"]
6205                    "#,
6206            )?;
6207
6208            // Local config extends with no-collision strategy but has conflicts
6209            jail.create_file(
6210                "foundry.toml",
6211                r#"
6212                    [profile.default]
6213                    extends = { path = "base.toml", strategy = "no-collision" }
6214                    optimizer_runs = 500
6215                    libs = ["mylib"]
6216                    "#,
6217            )?;
6218
6219            // Loading should fail due to key collision
6220            let result = Config::load();
6221
6222            if let Ok(config) = result {
6223                panic!(
6224                    "Expected error but got config with optimizer_runs: {:?}, libs: {:?}",
6225                    config.optimizer_runs, config.libs
6226                );
6227            }
6228
6229            let err = result.unwrap_err();
6230            let err_str = err.to_string();
6231            assert!(
6232                err_str.contains("Key collision detected") || err_str.contains("collision"),
6233                "Error message doesn't mention collision: {err_str}"
6234            );
6235
6236            Ok(())
6237        });
6238    }
6239
6240    #[test]
6241    fn test_extends_both_syntaxes() {
6242        figment::Jail::expect_with(|jail| {
6243            // Create base config
6244            jail.create_file(
6245                "base.toml",
6246                r#"
6247                    [profile.default]
6248                    libs = ["lib"]
6249                    optimizer = true
6250                    "#,
6251            )?;
6252
6253            // Test 1: Simple string syntax (should use default extend-arrays)
6254            jail.create_file(
6255                "foundry_string.toml",
6256                r#"
6257                    [profile.default]
6258                    extends = "base.toml"
6259                    libs = ["custom"]
6260                    "#,
6261            )?;
6262
6263            // Test 2: Object syntax with explicit strategy
6264            jail.create_file(
6265                "foundry_object.toml",
6266                r#"
6267                    [profile.default]
6268                    extends = { path = "base.toml", strategy = "replace-arrays" }
6269                    libs = ["custom"]
6270                    "#,
6271            )?;
6272
6273            // Test string syntax (default extend-arrays)
6274            jail.set_env("FOUNDRY_CONFIG", "foundry_string.toml");
6275            let config = Config::load().unwrap();
6276            assert_eq!(config.libs.len(), 2); // Should concatenate
6277            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6278            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6279
6280            // Test object syntax (replace-arrays)
6281            jail.set_env("FOUNDRY_CONFIG", "foundry_object.toml");
6282            let config = Config::load().unwrap();
6283            assert_eq!(config.libs.len(), 1); // Should replace
6284            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6285            assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
6286
6287            Ok(())
6288        });
6289    }
6290
6291    #[test]
6292    fn test_extends_strategy_default_is_extend_arrays() {
6293        figment::Jail::expect_with(|jail| {
6294            // Create base config
6295            jail.create_file(
6296                "base.toml",
6297                r#"
6298                    [profile.default]
6299                    libs = ["lib", "node_modules"]
6300                    optimizer = true
6301                    "#,
6302            )?;
6303
6304            // Local config extends without specifying strategy (should default to extend-arrays)
6305            jail.create_file(
6306                "foundry.toml",
6307                r#"
6308                    [profile.default]
6309                    extends = "base.toml"
6310                    libs = ["custom"]
6311                    optimizer = false
6312                    "#,
6313            )?;
6314
6315            // Should work with default extend-arrays strategy
6316            let config = Config::load().unwrap();
6317
6318            // Arrays should be concatenated by default
6319            assert_eq!(config.libs.len(), 3);
6320            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6321            assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6322            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6323
6324            // Non-array values should be replaced
6325            assert_eq!(config.optimizer, Some(false));
6326
6327            Ok(())
6328        });
6329    }
6330
6331    #[test]
6332    fn test_deprecated_deny_warnings_is_handled() {
6333        figment::Jail::expect_with(|jail| {
6334            jail.create_file(
6335                "foundry.toml",
6336                r#"
6337                [profile.default]
6338                deny_warnings = true
6339                "#,
6340            )?;
6341            let config = Config::load().unwrap();
6342
6343            // Assert that the deprecated flag is correctly interpreted
6344            assert_eq!(config.deny, DenyLevel::Warnings);
6345            Ok(())
6346        });
6347    }
6348
6349    #[test]
6350    fn test_evm_version_solc_compatibility_warning() {
6351        figment::Jail::expect_with(|jail| {
6352            // Create a config with incompatible evm_version and solc version
6353            // Using Cancun with an older solc version (Berlin) that doesn't support it
6354            jail.create_file(
6355                "foundry.toml",
6356                r#"
6357            [profile.default]
6358            evm_version = "Cancun"
6359            solc = "0.8.5"
6360        "#,
6361            )?;
6362
6363            let _config = Config::load().unwrap();
6364            Ok(())
6365        });
6366    }
6367}