Skip to main content

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