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