foundry_config/
lib.rs

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