1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use crate::cache::StorageCachingConfig;
12use alloy_primitives::{Address, B256, FixedBytes, U256, address, map::AddressHashMap};
13use eyre::{ContextCompat, WrapErr};
14use figment::{
15 Error, Figment, Metadata, Profile, Provider,
16 providers::{Env, Format, Serialized, Toml},
17 value::{Dict, Map, Value},
18};
19use filter::GlobMatcher;
20use foundry_compilers::{
21 ArtifactOutput, ConfigurableArtifacts, Graph, Project, ProjectPathsConfig,
22 RestrictionsWithVersion, VyperLanguage,
23 artifacts::{
24 BytecodeHash, DebuggingSettings, EvmVersion, Libraries, ModelCheckerSettings,
25 ModelCheckerTarget, Optimizer, OptimizerDetails, RevertStrings, Settings, SettingsMetadata,
26 Severity,
27 output_selection::{ContractOutputSelection, OutputSelection},
28 remappings::{RelativeRemapping, Remapping},
29 serde_helpers,
30 },
31 cache::SOLIDITY_FILES_CACHE_FILENAME,
32 compilers::{
33 Compiler,
34 multi::{MultiCompiler, MultiCompilerSettings},
35 solc::{Solc, SolcCompiler},
36 vyper::{Vyper, VyperSettings},
37 },
38 error::SolcError,
39 multi::{MultiCompilerParser, MultiCompilerRestrictions},
40 solc::{CliSettings, SolcLanguage, SolcSettings},
41};
42use regex::Regex;
43use semver::Version;
44use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
45use std::{
46 borrow::Cow,
47 collections::BTreeMap,
48 fs, io,
49 path::{Path, PathBuf},
50 str::FromStr,
51};
52
53mod macros;
54
55pub mod utils;
56pub use foundry_evm_hardforks::{
57 ExecutionSpec, FoundryHardfork, FromEvmVersion, evm_spec_id, evm_spec_id_from_str,
58};
59pub use utils::*;
60
61mod endpoints;
62pub use endpoints::{
63 ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
64 builtin_rpc_url,
65};
66
67mod etherscan;
68pub use etherscan::EtherscanConfigError;
69use etherscan::{EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig};
70
71pub mod resolve;
72pub use resolve::UnresolvedEnvVarError;
73
74pub mod cache;
75use cache::{Cache, ChainCache};
76
77pub mod fmt;
78pub use fmt::FormatterConfig;
79
80pub mod lint;
81pub use lint::{LinterConfig, Severity as LintSeverity};
82
83pub mod fs_permissions;
84pub use fs_permissions::FsPermissions;
85use fs_permissions::PathPermission;
86
87pub mod error;
88use error::ExtractConfigError;
89pub use error::SolidityErrorCode;
90
91pub mod doc;
92pub use doc::DocConfig;
93
94pub mod filter;
95pub use filter::SkipBuildFilters;
96
97mod warning;
98pub use warning::*;
99
100pub mod fix;
101
102pub use alloy_chains::{Chain, NamedChain};
104pub use figment;
105
106pub mod providers;
107pub use providers::Remappings;
108use providers::*;
109
110mod fuzz;
111pub use fuzz::{FuzzConfig, FuzzCorpusConfig, FuzzCorpusMutationWeights, FuzzDictionaryConfig};
112
113mod invariant;
114pub use invariant::{InvariantConfig, InvariantDepthMode, InvariantWorkers};
115
116mod symbolic;
117pub use symbolic::{SymbolicConfig, SymbolicExplorationOrder, SymbolicStorageLayout};
118
119mod coverage;
120pub use coverage::{CoverageConfig, CoverageReportKind, parse_lcov_version};
121
122mod fee;
123pub use fee::Eip1559FeeEstimatePreset;
124
125pub mod mutation;
126pub use mutation::{MutationConfig, MutatorType};
127
128mod inline;
129pub use inline::{InlineConfig, InlineConfigError, NatSpec};
130
131pub mod soldeer;
132use soldeer::{SoldeerConfig, SoldeerDependencyConfig};
133
134mod vyper;
135pub use vyper::VyperConfig;
136
137mod bind_json;
138use bind_json::BindJsonConfig;
139
140mod compilation;
141pub use compilation::{CompilationRestrictions, SettingsOverrides};
142
143pub mod extend;
144use extend::Extends;
145
146use foundry_evm_networks::NetworkConfigs;
147pub use semver;
148
149#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
181pub struct Config {
182 #[serde(skip)]
189 pub profile: Profile,
190 #[serde(skip)]
194 pub profiles: Vec<Profile>,
195
196 #[serde(default = "root_default", skip_serializing)]
201 pub root: PathBuf,
202
203 #[serde(default, skip_serializing)]
208 pub extends: Option<Extends>,
209
210 pub src: PathBuf,
214 pub test: PathBuf,
216 pub script: PathBuf,
218 pub out: PathBuf,
220 pub libs: Vec<PathBuf>,
222 pub remappings: Vec<RelativeRemapping>,
224 pub auto_detect_remappings: bool,
226 pub libraries: Vec<String>,
228 pub cache: bool,
230 pub cache_path: PathBuf,
232 pub dynamic_test_linking: bool,
234 pub snapshots: PathBuf,
236 pub gas_snapshot_check: bool,
238 pub gas_snapshot_emit: bool,
240 pub broadcast: PathBuf,
242 pub allow_paths: Vec<PathBuf>,
244 pub include_paths: Vec<PathBuf>,
246 pub skip: Vec<GlobMatcher>,
248 pub force: bool,
250 #[serde(with = "from_str_lowercase")]
252 pub evm_version: EvmVersion,
253 pub hardfork: Option<FoundryHardfork>,
255 pub gas_reports: Vec<String>,
257 pub gas_reports_ignore: Vec<String>,
259 pub gas_reports_include_tests: bool,
261 #[doc(hidden)]
271 pub solc: Option<SolcReq>,
272 pub auto_detect_solc: bool,
274 pub offline: bool,
281 pub optimizer: Option<bool>,
283 pub optimizer_runs: Option<usize>,
294 pub optimizer_details: Option<OptimizerDetails>,
298 pub model_checker: Option<ModelCheckerSettings>,
300 pub verbosity: u8,
302 pub eth_rpc_url: Option<String>,
304 pub eth_rpc_accept_invalid_certs: bool,
306 pub eth_rpc_no_proxy: bool,
311 pub eth_rpc_jwt: Option<String>,
313 pub eth_rpc_timeout: Option<u64>,
315 pub eth_rpc_headers: Option<Vec<String>>,
324 pub eth_rpc_curl: bool,
326 pub etherscan_api_key: Option<String>,
328 #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
330 pub etherscan: EtherscanConfigs,
331 pub ignored_error_codes: Vec<SolidityErrorCode>,
333 pub ignored_error_codes_from: Vec<(PathBuf, Vec<SolidityErrorCode>)>,
335 #[serde(rename = "ignored_warnings_from")]
337 pub ignored_file_paths: Vec<PathBuf>,
338 pub deny: DenyLevel,
340 #[serde(default, skip_serializing)]
342 pub deny_warnings: bool,
343 #[serde(rename = "match_test")]
345 pub test_pattern: Option<RegexWrapper>,
346 #[serde(rename = "no_match_test")]
348 pub test_pattern_inverse: Option<RegexWrapper>,
349 #[serde(rename = "match_contract")]
351 pub contract_pattern: Option<RegexWrapper>,
352 #[serde(rename = "no_match_contract")]
354 pub contract_pattern_inverse: Option<RegexWrapper>,
355 #[serde(rename = "match_path", with = "from_opt_glob")]
357 pub path_pattern: Option<globset::Glob>,
358 #[serde(rename = "no_match_path", with = "from_opt_glob")]
360 pub path_pattern_inverse: Option<globset::Glob>,
361 #[serde(rename = "no_match_coverage")]
363 pub coverage_pattern_inverse: Option<RegexWrapper>,
364 pub test_failures_file: PathBuf,
366 pub mutation_dir: PathBuf,
368 pub threads: Option<usize>,
370 pub show_progress: bool,
372 pub fuzz: FuzzConfig,
374 pub invariant: InvariantConfig,
376 pub symbolic: SymbolicConfig,
378 pub coverage: CoverageConfig,
380 pub mutation: MutationConfig,
382 pub ffi: bool,
384 pub live_logs: bool,
386 pub allow_internal_expect_revert: bool,
388 pub always_use_create_2_factory: bool,
390 #[serde(default)]
394 pub eip1559_fee_estimate: Eip1559FeeEstimatePreset,
395 pub prompt_timeout: u64,
397 pub sender: Address,
399 pub tx_origin: Address,
401 pub initial_balance: U256,
403 #[serde(
405 deserialize_with = "crate::deserialize_u64_to_u256",
406 serialize_with = "crate::serialize_u64_or_u256"
407 )]
408 pub block_number: U256,
409 pub fork_block_number: Option<u64>,
411 #[serde(rename = "chain_id", alias = "chain")]
413 pub chain: Option<Chain>,
414 pub gas_limit: GasLimit,
416 pub code_size_limit: Option<usize>,
418 pub gas_price: Option<u64>,
423 pub block_base_fee_per_gas: u64,
425 pub block_coinbase: Address,
427 #[serde(
429 deserialize_with = "crate::deserialize_u64_to_u256",
430 serialize_with = "crate::serialize_u64_or_u256"
431 )]
432 pub block_timestamp: U256,
433 pub block_difficulty: u64,
435 pub block_prevrandao: B256,
437 pub block_gas_limit: Option<GasLimit>,
439 pub memory_limit: u64,
444 #[serde(default)]
461 pub extra_output: Vec<ContractOutputSelection>,
462 #[serde(default)]
473 pub extra_output_files: Vec<ContractOutputSelection>,
474 pub names: bool,
476 pub sizes: bool,
478 pub via_ir: bool,
481 pub via_ssa_cfg: bool,
485 pub experimental: bool,
490 pub ast: bool,
492 pub rpc_storage_caching: StorageCachingConfig,
494 pub no_storage_caching: bool,
497 pub no_rpc_rate_limit: bool,
500 #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
502 pub rpc_endpoints: RpcEndpoints,
503 pub use_literal_content: bool,
505 #[serde(with = "from_str_lowercase")]
509 pub bytecode_hash: BytecodeHash,
510 pub cbor_metadata: bool,
515 #[serde(with = "serde_helpers::display_from_str_opt")]
517 pub revert_strings: Option<RevertStrings>,
518 pub sparse_mode: bool,
523 pub build_info: bool,
526 pub build_info_path: Option<PathBuf>,
528 pub fmt: FormatterConfig,
530 pub lint: LinterConfig,
532 pub doc: DocConfig,
534 pub bind_json: BindJsonConfig,
536 pub fs_permissions: FsPermissions,
540
541 pub isolate: bool,
545
546 pub disable_block_gas_limit: bool,
548
549 pub enable_tx_gas_limit: bool,
551
552 pub labels: AddressHashMap<String>,
554
555 pub unchecked_cheatcode_artifacts: bool,
558
559 pub create2_library_salt: B256,
561
562 pub create2_deployer: Address,
564
565 pub vyper: VyperConfig,
567
568 pub dependencies: Option<SoldeerDependencyConfig>,
570
571 pub soldeer: Option<SoldeerConfig>,
573
574 pub assertions_revert: bool,
578
579 pub legacy_assertions: bool,
581
582 #[serde(default, skip_serializing_if = "Vec::is_empty")]
584 pub extra_args: Vec<String>,
585
586 #[serde(flatten)]
588 pub networks: NetworkConfigs,
589
590 pub transaction_timeout: u64,
592
593 #[serde(rename = "__warnings", default, skip_serializing)]
595 pub warnings: Vec<Warning>,
596
597 #[serde(default)]
599 pub additional_compiler_profiles: Vec<SettingsOverrides>,
600
601 #[serde(default)]
603 pub compilation_restrictions: Vec<CompilationRestrictions>,
604
605 pub script_execution_protection: bool,
607
608 #[doc(hidden)]
617 #[serde(skip)]
618 pub _non_exhaustive: (),
619}
620
621#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Default, Serialize)]
623#[serde(rename_all = "lowercase")]
624pub enum DenyLevel {
625 #[default]
627 Never,
628 Warnings,
630 Notes,
632}
633
634impl<'de> Deserialize<'de> for DenyLevel {
637 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
638 where
639 D: Deserializer<'de>,
640 {
641 struct DenyLevelVisitor;
642
643 impl<'de> de::Visitor<'de> for DenyLevelVisitor {
644 type Value = DenyLevel;
645
646 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
647 formatter.write_str("one of the following strings: `never`, `warnings`, `notes`")
648 }
649
650 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
651 where
652 E: de::Error,
653 {
654 Ok(DenyLevel::from(value))
655 }
656
657 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
658 where
659 E: de::Error,
660 {
661 DenyLevel::from_str(value).map_err(de::Error::custom)
662 }
663 }
664
665 deserializer.deserialize_any(DenyLevelVisitor)
666 }
667}
668
669impl FromStr for DenyLevel {
670 type Err = String;
671
672 fn from_str(s: &str) -> Result<Self, Self::Err> {
673 match s.to_lowercase().as_str() {
674 "warnings" | "warning" | "w" => Ok(Self::Warnings),
675 "notes" | "note" | "n" => Ok(Self::Notes),
676 "never" | "false" | "f" => Ok(Self::Never),
677 _ => Err(format!(
678 "unknown variant: found `{s}`, expected one of `never`, `warnings`, `notes`"
679 )),
680 }
681 }
682}
683
684impl From<bool> for DenyLevel {
685 fn from(deny: bool) -> Self {
686 if deny { Self::Warnings } else { Self::Never }
687 }
688}
689
690impl DenyLevel {
691 pub const fn warnings(&self) -> bool {
693 match self {
694 Self::Never => false,
695 Self::Warnings | Self::Notes => true,
696 }
697 }
698
699 pub const fn notes(&self) -> bool {
701 match self {
702 Self::Never | Self::Warnings => false,
703 Self::Notes => true,
704 }
705 }
706
707 pub const fn never(&self) -> bool {
709 match self {
710 Self::Never => true,
711 Self::Warnings | Self::Notes => false,
712 }
713 }
714}
715
716pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
718
719pub const DEPRECATIONS: &[(&str, &str)] =
723 &[("cancun", "evm_version = Cancun"), ("deny_warnings", "deny = warnings")];
724
725impl Config {
726 pub const DEFAULT_PROFILE: Profile = Profile::Default;
728
729 pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
731
732 pub const PROFILE_SECTION: &'static str = "profile";
734
735 pub const EXTERNAL_SECTION: &'static str = "external";
737
738 pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
740 "rpc_endpoints",
741 "etherscan",
742 "fmt",
743 "lint",
744 "doc",
745 "fuzz",
746 "invariant",
747 "symbolic",
748 "coverage",
749 "mutation",
750 "labels",
751 "dependencies",
752 "soldeer",
753 "vyper",
754 "bind_json",
755 ];
756
757 pub(crate) fn is_standalone_section<T: ?Sized + PartialEq<str>>(section: &T) -> bool {
758 section == Self::PROFILE_SECTION
759 || section == Self::EXTERNAL_SECTION
760 || Self::STANDALONE_SECTIONS.iter().any(|s| section == *s)
761 }
762
763 pub const FILE_NAME: &'static str = "foundry.toml";
765
766 pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
768
769 pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
773
774 pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
776
777 pub const DEFAULT_CREATE2_DEPLOYER: Address =
779 address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");
780
781 pub fn load() -> Result<Self, ExtractConfigError> {
785 Self::from_provider(Self::figment())
786 }
787
788 pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
792 Self::from_provider(Self::default().to_figment(providers))
793 }
794
795 #[track_caller]
799 pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
800 Self::from_provider(Self::figment_with_root(root.as_ref()))
801 }
802
803 #[track_caller]
810 pub fn load_with_root_and_fallback(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
811 let figment = Self::figment_with_root(root.as_ref());
812 Self::from_figment_fallback(Figment::from(figment))
813 }
814
815 #[doc(alias = "try_from")]
830 pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
831 trace!("load config with provider: {:?}", provider.metadata());
832 Self::from_figment(Figment::from(provider))
833 }
834
835 pub fn merge_inline_provider<T: Provider>(&self, provider: T) -> Result<Self, Error> {
838 let provider = Figment::from(provider).select(self.profile.clone());
839 let invariant_corpus_random_sequence_weight_configured =
840 self.invariant.corpus_random_sequence_weight_configured
841 || provider.contains("invariant.corpus_random_sequence_weight")
842 || provider
843 .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
844 .unwrap_or(false);
845 let invariant_workers_configured = self.invariant.workers_configured
846 || provider.contains("invariant.workers")
847 || provider.extract_inner::<bool>("invariant.workers_configured").unwrap_or(false)
848 || provider.extract_inner::<InvariantWorkers>("invariant.workers").is_ok();
849 let figment = self.to_figment(FigmentProviders::None).merge(provider);
850 let mut config = figment.extract::<Self>()?;
851 config.profile = self.profile.clone();
852 config.profiles = self.profiles.clone();
853 config.invariant.corpus_random_sequence_weight_configured =
854 invariant_corpus_random_sequence_weight_configured;
855 config.invariant.workers_configured = invariant_workers_configured;
856 config.normalize_hardfork_settings()?;
857
858 Ok(config)
859 }
860
861 #[doc(hidden)]
862 #[deprecated(note = "use `Config::from_provider` instead")]
863 pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
864 Self::from_provider(provider)
865 }
866
867 fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
868 Self::from_figment_inner(figment, true)
869 }
870
871 fn from_figment_fallback(figment: Figment) -> Result<Self, ExtractConfigError> {
874 Self::from_figment_inner(figment, false)
875 }
876
877 fn from_figment_inner(
878 figment: Figment,
879 strict_profile: bool,
880 ) -> Result<Self, ExtractConfigError> {
881 let invariant_corpus_random_sequence_weight_configured = figment
882 .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
883 .unwrap_or_else(|_| {
884 figment_value_is_configured(&figment, "invariant.corpus_random_sequence_weight")
885 });
886 let invariant_workers_configured = figment
887 .extract_inner::<bool>("invariant.workers_configured")
888 .unwrap_or_else(|_| figment_value_is_configured(&figment, "invariant.workers"));
889 let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
890 config.invariant.corpus_random_sequence_weight_configured =
891 invariant_corpus_random_sequence_weight_configured;
892 config.invariant.workers_configured = invariant_workers_configured;
893 let selected_profile = figment.profile().clone();
894
895 fn add_profile(profiles: &mut Vec<Profile>, profile: &Profile) {
897 if !profiles.contains(profile) {
898 profiles.push(profile.clone());
899 }
900 }
901 let figment = figment.select(Self::PROFILE_SECTION);
902 if let Ok(data) = figment.data()
903 && let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION))
904 {
905 for profile in profiles.keys() {
906 add_profile(&mut config.profiles, &Profile::new(profile));
907 }
908 }
909 add_profile(&mut config.profiles, &Self::DEFAULT_PROFILE);
910
911 if config.profiles.contains(&selected_profile) {
913 config.profile = selected_profile;
914 } else {
915 if strict_profile {
919 config
920 .warnings
921 .push(Warning::UnknownProfile { profile: selected_profile.to_string() });
922 }
923 config.profile = Self::DEFAULT_PROFILE;
924 }
925
926 config.normalize_optimizer_settings();
927 config.normalize_hardfork_settings().map_err(ExtractConfigError::new)?;
928
929 if let Some(runs) = config.optimizer_runs
931 && runs > u32::MAX as usize
932 {
933 return Err(ExtractConfigError::new(Error::from(format!(
934 "`optimizer_runs` value {} exceeds maximum allowed value of {}",
935 runs,
936 u32::MAX
937 ))));
938 }
939
940 Ok(config)
941 }
942
943 fn normalize_hardfork_settings(&mut self) -> Result<(), Error> {
944 let Some(hardfork) = self.hardfork else { return Ok(()) };
945 self.networks = self.networks.normalize_for_hardfork(hardfork).map_err(Error::from)?;
946 Ok(())
947 }
948
949 pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
954 if providers.is_none() {
957 return Figment::from(self);
958 }
959
960 let root = self.root.as_path();
961 let profile = Self::selected_profile();
962 let mut figment = Figment::default().merge(DappHardhatDirProvider(root));
963
964 if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
966 figment = Self::merge_toml_provider(
967 figment,
968 TomlFileProvider::new(None, global_toml),
969 profile.clone(),
970 );
971 }
972 figment = Self::merge_toml_provider(
974 figment,
975 TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)),
976 profile.clone(),
977 );
978
979 figment = figment
981 .merge(
982 Env::prefixed("DAPP_")
983 .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
984 .global(),
985 )
986 .merge(
987 Env::prefixed("DAPP_TEST_")
988 .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
989 .global(),
990 )
991 .merge(DappEnvCompatProvider)
992 .merge(EtherscanEnvProvider::default())
993 .merge(
994 Env::prefixed("FOUNDRY_")
995 .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
996 .map(|key| {
997 let key = key.as_str();
998 if Self::STANDALONE_SECTIONS.iter().any(|section| {
999 key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
1000 }) {
1001 key.replacen('_', ".", 1).into()
1002 } else {
1003 key.into()
1004 }
1005 })
1006 .global(),
1007 )
1008 .select(profile.clone());
1009
1010 if providers.is_all() {
1012 let remappings = RemappingsProvider {
1016 auto_detect_remappings: figment
1017 .extract_inner::<bool>("auto_detect_remappings")
1018 .unwrap_or(true),
1019 lib_paths: figment
1020 .extract_inner::<Vec<PathBuf>>("libs")
1021 .map(Cow::Owned)
1022 .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
1023 root,
1024 remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
1025 };
1026 figment = figment.merge(remappings);
1027 }
1028
1029 let invariant_corpus_random_sequence_weight_configured = self
1030 .invariant
1031 .corpus_random_sequence_weight_configured
1032 || figment
1033 .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
1034 .unwrap_or_else(|_| {
1035 figment_value_is_configured(&figment, "invariant.corpus_random_sequence_weight")
1036 });
1037 let invariant_workers_configured = self.invariant.workers_configured
1038 || figment.extract_inner::<bool>("invariant.workers_configured").unwrap_or_else(|_| {
1039 figment.extract_inner::<InvariantWorkers>("invariant.workers").is_ok()
1040 });
1041
1042 figment = self.normalize_defaults(figment);
1044 if invariant_corpus_random_sequence_weight_configured {
1045 figment = figment.merge(("invariant.corpus_random_sequence_weight_configured", true));
1046 }
1047 if invariant_workers_configured {
1048 figment = figment.merge(("invariant.workers_configured", true));
1049 }
1050
1051 Figment::from(self).merge(figment).select(profile)
1052 }
1053
1054 #[must_use]
1059 pub fn canonic(self) -> Self {
1060 let root = self.root.clone();
1061 self.canonic_at(root)
1062 }
1063
1064 #[must_use]
1082 pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
1083 let root = canonic(root);
1084
1085 fn p(root: &Path, rem: &Path) -> PathBuf {
1086 canonic(root.join(rem))
1087 }
1088
1089 self.src = p(&root, &self.src);
1090 self.test = p(&root, &self.test);
1091 self.script = p(&root, &self.script);
1092 self.out = p(&root, &self.out);
1093 self.broadcast = p(&root, &self.broadcast);
1094 self.cache_path = p(&root, &self.cache_path);
1095 self.snapshots = p(&root, &self.snapshots);
1096 self.test_failures_file = p(&root, &self.test_failures_file);
1097
1098 if let Some(build_info_path) = self.build_info_path {
1099 self.build_info_path = Some(p(&root, &build_info_path));
1100 }
1101
1102 self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
1103
1104 self.remappings =
1105 self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
1106
1107 self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
1108
1109 self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
1110
1111 self.fs_permissions.join_all(&root);
1112
1113 if let Some(model_checker) = &mut self.model_checker {
1114 model_checker.contracts = std::mem::take(&mut model_checker.contracts)
1115 .into_iter()
1116 .map(|(path, contracts)| {
1117 (format!("{}", p(&root, path.as_ref()).display()), contracts)
1118 })
1119 .collect();
1120 }
1121
1122 self
1123 }
1124
1125 pub fn normalized_evm_version(mut self) -> Self {
1127 self.normalize_evm_version();
1128 self
1129 }
1130
1131 pub const fn normalized_optimizer_settings(mut self) -> Self {
1134 self.normalize_optimizer_settings();
1135 self
1136 }
1137
1138 pub fn normalize_evm_version(&mut self) {
1140 self.evm_version = self.get_normalized_evm_version();
1141 }
1142
1143 pub const fn normalize_optimizer_settings(&mut self) {
1148 match (self.optimizer, self.optimizer_runs) {
1149 (None, None) => {
1151 self.optimizer = Some(false);
1152 self.optimizer_runs = Some(200);
1153 }
1154 (Some(_), None) => self.optimizer_runs = Some(200),
1156 (None, Some(runs)) => self.optimizer = Some(runs > 0),
1158 _ => {}
1159 }
1160 }
1161
1162 pub fn get_normalized_evm_version(&self) -> EvmVersion {
1164 if let Some(version) = self.solc_version()
1165 && let Some(evm_version) = self.evm_version.normalize_version_solc(&version)
1166 {
1167 return evm_version;
1168 }
1169 self.evm_version
1170 }
1171
1172 #[must_use]
1177 pub fn sanitized(self) -> Self {
1178 let mut config = self.canonic();
1179
1180 config.sanitize_remappings();
1181
1182 config.libs.sort_unstable();
1183 config.libs.dedup();
1184
1185 config
1186 }
1187
1188 #[allow(clippy::missing_const_for_fn)]
1192 pub fn sanitize_remappings(&mut self) {
1193 #[cfg(target_os = "windows")]
1194 {
1195 use path_slash::PathBufExt;
1197 self.remappings.iter_mut().for_each(|r| {
1198 r.path.path = r.path.path.to_slash_lossy().into_owned().into();
1199 });
1200 }
1201 }
1202
1203 pub fn install_lib_dir(&self) -> &Path {
1207 self.libs
1208 .iter()
1209 .find(|p| !p.ends_with("node_modules"))
1210 .map(|p| p.as_path())
1211 .unwrap_or_else(|| Path::new("lib"))
1212 }
1213
1214 pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1229 self.create_project(self.cache, false)
1230 }
1231
1232 pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1235 self.create_project(false, true)
1236 }
1237
1238 pub fn solar_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1242 let ui_testing = std::env::var_os("FOUNDRY_LINT_UI_TESTING").is_some();
1243 let mut project = self.create_project(self.cache && !ui_testing, false)?;
1244 project.update_output_selection(|selection| {
1245 *selection = OutputSelection::common_output_selection(["abi".into()]);
1248 });
1249 Ok(project)
1250 }
1251
1252 fn additional_settings(
1254 &self,
1255 base: &MultiCompilerSettings,
1256 ) -> BTreeMap<String, MultiCompilerSettings> {
1257 let mut map = BTreeMap::new();
1258
1259 for profile in &self.additional_compiler_profiles {
1260 let mut settings = base.clone();
1261 profile.apply(&mut settings);
1262 map.insert(profile.name.clone(), settings);
1263 }
1264
1265 map
1266 }
1267
1268 #[expect(clippy::disallowed_macros)]
1270 fn restrictions(
1271 &self,
1272 paths: &ProjectPathsConfig,
1273 ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
1274 {
1275 let mut map: BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>> =
1276 BTreeMap::new();
1277 if self.compilation_restrictions.is_empty() {
1278 return Ok(BTreeMap::new());
1279 }
1280
1281 let graph = Graph::<MultiCompilerParser>::resolve(paths)?;
1282 let (sources, _) = graph.into_sources();
1283
1284 for res in &self.compilation_restrictions {
1285 for source in sources.keys().filter(|path| {
1286 if res.paths.is_match(path) {
1287 true
1288 } else if let Ok(path) = path.strip_prefix(&paths.root) {
1289 res.paths.is_match(path)
1290 } else {
1291 false
1292 }
1293 }) {
1294 let res: RestrictionsWithVersion<_> =
1295 res.clone().try_into().map_err(SolcError::msg)?;
1296 if map.contains_key(source) {
1297 let value = map.remove(source.as_path()).unwrap();
1298 if let Some(merged) = value.clone().merge(res) {
1299 map.insert(source.clone(), merged);
1300 } else {
1301 eprintln!(
1303 "{}",
1304 yansi::Paint::yellow(&format!(
1305 "Failed to merge compilation restrictions for {}",
1306 source.display()
1307 ))
1308 );
1309 map.insert(source.clone(), value);
1310 }
1311 } else {
1312 map.insert(source.clone(), res);
1313 }
1314 }
1315 }
1316
1317 Ok(map)
1318 }
1319
1320 pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
1324 let settings = self.compiler_settings()?;
1325 let paths = self.project_paths();
1326
1327 let parse_path = |path: &PathBuf| path.strip_prefix("./").unwrap_or(path).to_path_buf();
1329
1330 let mut builder = Project::builder()
1331 .artifacts(self.configured_artifacts_handler())
1332 .additional_settings(self.additional_settings(&settings))
1333 .restrictions(self.restrictions(&paths)?)
1334 .settings(settings)
1335 .paths(paths)
1336 .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
1337 .ignore_error_codes_from(self.ignored_error_codes_from.iter().map(|(path, codes)| {
1338 (parse_path(path), codes.iter().copied().map(Into::into).collect())
1339 }))
1340 .ignore_paths(self.ignored_file_paths.iter().map(parse_path).collect())
1341 .set_compiler_severity_filter(if self.deny.warnings() {
1342 Severity::Warning
1343 } else {
1344 Severity::Error
1345 })
1346 .set_offline(self.offline)
1347 .set_cached(cached)
1348 .set_build_info(!no_artifacts && self.build_info)
1349 .set_no_artifacts(no_artifacts);
1350
1351 if !self.skip.is_empty() {
1352 let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
1353 builder = builder.sparse_output(filter);
1354 }
1355
1356 let project = builder.build(self.compiler()?)?;
1357
1358 if self.force {
1359 let _ = self.cleanup(&project);
1362 }
1363
1364 Ok(project)
1365 }
1366
1367 pub fn disable_optimizations(&self, project: &mut Project, ir_minimum: bool) {
1369 if ir_minimum {
1370 project.settings.solc.settings = std::mem::take(&mut project.settings.solc.settings)
1373 .with_via_ir_minimum_optimization();
1374
1375 let evm_version = project.settings.solc.evm_version;
1378 let version = self.solc_version().unwrap_or_else(|| Version::new(0, 8, 4));
1379 project.settings.solc.settings.sanitize(&version, SolcLanguage::Solidity);
1380 project.settings.solc.evm_version = evm_version;
1381 } else {
1382 project.settings.solc.optimizer.disable();
1383 project.settings.solc.optimizer.runs = None;
1384 project.settings.solc.optimizer.details = None;
1385 project.settings.solc.via_ir = None;
1386 }
1387 }
1388
1389 pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
1394 &self,
1395 project: &Project<C, T>,
1396 ) -> Result<Vec<String>, SolcError> {
1397 let mut warnings = Vec::new();
1398
1399 if let Err(err) = project.cleanup() {
1400 warnings.push(format!("failed to clean project artifacts: {err}"));
1401 }
1402
1403 if let Err(err) = fs::remove_file(&self.test_failures_file)
1405 && err.kind() != io::ErrorKind::NotFound
1406 {
1407 warnings.push(format!(
1408 "failed to remove test failures file {}: {err}",
1409 self.test_failures_file.display()
1410 ));
1411 }
1412
1413 let _ = fs::remove_dir_all(project.root().join(&self.mutation_dir));
1415
1416 let mut remove_test_dir = |test_dir: &Option<PathBuf>| {
1418 if let Some(test_dir) = test_dir {
1419 let path = project.root().join(test_dir);
1420 if let Err(err) = fs::remove_dir_all(&path)
1421 && err.kind() != io::ErrorKind::NotFound
1422 {
1423 warnings.push(format!(
1424 "failed to remove test cache directory {}: {err}",
1425 path.display()
1426 ));
1427 }
1428 }
1429 };
1430 remove_test_dir(&self.fuzz.failure_persist_dir);
1431 remove_test_dir(&self.fuzz.corpus.corpus_dir);
1432 remove_test_dir(&self.fuzz.corpus.frontier_dir);
1433 remove_test_dir(&self.invariant.corpus.corpus_dir);
1434 remove_test_dir(&self.invariant.failure_persist_dir);
1435
1436 Ok(warnings)
1437 }
1438
1439 fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
1446 if let Some(solc) = &self.solc {
1447 let solc = match solc {
1448 SolcReq::Version(version) => {
1449 if let Some(solc) = Solc::find_svm_installed_version(version)? {
1450 solc
1451 } else {
1452 if self.offline {
1453 return Err(SolcError::msg(format!(
1454 "can't install missing solc {version} in offline mode"
1455 )));
1456 }
1457 Solc::blocking_install(version)?
1458 }
1459 }
1460 SolcReq::Local(solc) => {
1461 if !solc.is_file() {
1462 return Err(SolcError::msg(format!(
1463 "`solc` {} does not exist",
1464 solc.display()
1465 )));
1466 }
1467 Solc::new(solc)?
1468 }
1469 };
1470 return Ok(Some(solc));
1471 }
1472
1473 Ok(None)
1474 }
1475
1476 pub fn evm_spec_id<SPEC: FromEvmVersion>(&self) -> SPEC {
1478 self.hardfork.map(Into::into).unwrap_or_else(|| evm_spec_id(self.evm_version))
1479 }
1480
1481 pub const fn is_auto_detect(&self) -> bool {
1486 if self.solc.is_some() {
1487 return false;
1488 }
1489 self.auto_detect_solc
1490 }
1491
1492 pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
1494 !self.no_storage_caching
1495 && self.rpc_storage_caching.enable_for_chain_id(chain_id.into())
1496 && self.rpc_storage_caching.enable_for_endpoint(endpoint)
1497 }
1498
1499 pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
1514 let mut builder = ProjectPathsConfig::builder()
1515 .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
1516 .sources(&self.src)
1517 .tests(&self.test)
1518 .scripts(&self.script)
1519 .artifacts(&self.out)
1520 .libs(self.libs.iter())
1521 .remappings(self.get_all_remappings())
1522 .allowed_path(&self.root)
1523 .allowed_paths(&self.libs)
1524 .allowed_paths(&self.allow_paths)
1525 .include_paths(&self.include_paths);
1526
1527 if let Some(build_info_path) = &self.build_info_path {
1528 builder = builder.build_infos(build_info_path);
1529 }
1530
1531 builder.build_with_root(&self.root)
1532 }
1533
1534 pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
1536 if let Some(solc) = self.ensure_solc()? {
1537 Ok(SolcCompiler::Specific(solc))
1538 } else {
1539 Ok(SolcCompiler::AutoDetect)
1540 }
1541 }
1542
1543 pub fn solc_version(&self) -> Option<Version> {
1545 self.solc.as_ref().and_then(|solc| solc.try_version().ok())
1546 }
1547
1548 pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
1550 if !self.project_paths::<VyperLanguage>().has_input_files() {
1552 return Ok(None);
1553 }
1554 let vyper = if let Some(path) = &self.vyper.path {
1555 Some(Vyper::new(path)?)
1556 } else {
1557 Vyper::new("vyper").ok()
1558 };
1559 Ok(vyper)
1560 }
1561
1562 pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
1564 Ok(MultiCompiler { solc: Some(self.solc_compiler()?), vyper: self.vyper_compiler()? })
1565 }
1566
1567 pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
1569 Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
1570 }
1571
1572 pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
1574 self.remappings.iter().map(|m| m.clone().into())
1575 }
1576
1577 pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1592 Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1593 }
1594
1595 pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1611 let maybe_alias = self.eth_rpc_url.as_deref()?;
1612 if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1613 Some(alias)
1614 } else {
1615 Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1616 }
1617 }
1618
1619 pub fn get_rpc_url_with_alias(
1644 &self,
1645 maybe_alias: &str,
1646 ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1647 let mut endpoints = self.rpc_endpoints.clone().resolved();
1648 if let Some(endpoint) = endpoints.remove(maybe_alias) {
1649 return Some(endpoint.url().map(Cow::Owned));
1650 }
1651
1652 if let Some(mesc_url) = self.get_rpc_url_from_mesc(maybe_alias) {
1653 return Some(Ok(Cow::Owned(mesc_url)));
1654 }
1655
1656 if let Some(builtin) = crate::endpoints::builtin_rpc_url(maybe_alias) {
1657 return Some(Ok(Cow::Borrowed(builtin)));
1658 }
1659
1660 None
1661 }
1662
1663 pub fn get_rpc_url_from_mesc(&self, maybe_alias: &str) -> Option<String> {
1665 let mesc_config = mesc::load::load_config_data()
1668 .inspect_err(|err| debug!(%err, "failed to load mesc config"))
1669 .ok()?;
1670
1671 if let Ok(Some(endpoint)) =
1672 mesc::query::get_endpoint_by_query(&mesc_config, maybe_alias, Some("foundry"))
1673 {
1674 return Some(endpoint.url);
1675 }
1676
1677 if maybe_alias.chars().all(|c| c.is_numeric()) {
1678 if let Ok(Some(endpoint)) =
1684 mesc::query::get_endpoint_by_network(&mesc_config, maybe_alias, Some("foundry"))
1685 {
1686 return Some(endpoint.url);
1687 }
1688 }
1689
1690 None
1691 }
1692
1693 pub fn get_rpc_url_or<'a>(
1705 &'a self,
1706 fallback: impl Into<Cow<'a, str>>,
1707 ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
1708 if let Some(url) = self.get_rpc_url() { url } else { Ok(fallback.into()) }
1709 }
1710
1711 pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1723 self.get_rpc_url_or("http://localhost:8545")
1724 }
1725
1726 pub fn get_etherscan_config(
1746 &self,
1747 ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1748 self.get_etherscan_config_with_chain(None).transpose()
1749 }
1750
1751 pub fn get_etherscan_config_with_chain(
1758 &self,
1759 chain: Option<Chain>,
1760 ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1761 if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref())
1762 && self.etherscan.contains_key(maybe_alias)
1763 {
1764 return self.etherscan.clone().resolved().remove(maybe_alias).transpose();
1765 }
1766
1767 if let Some(res) = chain
1769 .or(self.chain)
1770 .and_then(|chain| self.etherscan.clone().resolved().find_chain(chain))
1771 {
1772 match (res, self.etherscan_api_key.as_ref()) {
1773 (Ok(mut config), Some(key)) => {
1774 config.key.clone_from(key);
1777 return Ok(Some(config));
1778 }
1779 (Ok(config), None) => return Ok(Some(config)),
1780 (Err(err), None) => return Err(err),
1781 (Err(_), Some(_)) => {
1782 }
1784 }
1785 }
1786
1787 if let Some(key) = self.etherscan_api_key.as_ref() {
1789 return Ok(ResolvedEtherscanConfig::create(
1790 key,
1791 chain.or(self.chain).unwrap_or_default(),
1792 ));
1793 }
1794 Ok(None)
1795 }
1796
1797 #[expect(clippy::disallowed_macros)]
1803 pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1804 self.get_etherscan_config_with_chain(chain)
1805 .map_err(|e| {
1806 eprintln!(
1808 "{}: failed getting etherscan config: {e}",
1809 yansi::Paint::yellow("Warning"),
1810 );
1811 })
1812 .ok()
1813 .flatten()
1814 .map(|c| c.key)
1815 }
1816
1817 pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1824 get_dir_remapping(&self.src)
1825 }
1826
1827 pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1829 if self.root.join(&self.test).exists() { get_dir_remapping(&self.test) } else { None }
1830 }
1831
1832 pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1834 if self.root.join(&self.script).exists() { get_dir_remapping(&self.script) } else { None }
1835 }
1836
1837 pub fn optimizer(&self) -> Optimizer {
1843 Optimizer {
1844 enabled: self.optimizer,
1845 runs: self.optimizer_runs,
1846 details: self.optimizer_details.clone(),
1849 }
1850 }
1851
1852 pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1855 let mut extra_output = self.extra_output.clone();
1856
1857 if !extra_output.contains(&ContractOutputSelection::Metadata) {
1863 extra_output.push(ContractOutputSelection::Metadata);
1864 }
1865
1866 ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().copied())
1867 }
1868
1869 pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1872 Libraries::parse(&self.libraries)
1873 }
1874
1875 pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1877 let paths: ProjectPathsConfig = self.project_paths();
1878 Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1879 }
1880
1881 pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
1886 let mut model_checker = self.model_checker.clone();
1890 if let Some(model_checker_settings) = &mut model_checker
1891 && model_checker_settings.targets.is_none()
1892 {
1893 model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1894 }
1895
1896 let mut settings = Settings {
1897 libraries: self.libraries_with_remappings()?,
1898 optimizer: self.optimizer(),
1899 evm_version: Some(self.evm_version),
1900 metadata: Some(SettingsMetadata {
1901 use_literal_content: Some(self.use_literal_content),
1902 bytecode_hash: Some(self.bytecode_hash),
1903 cbor_metadata: Some(self.cbor_metadata),
1904 }),
1905 debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1906 revert_strings: Some(revert_strings),
1907 debug_info: Vec::new(),
1909 }),
1910 model_checker,
1911 via_ir: Some(self.via_ir || self.via_ssa_cfg),
1914 via_ssa_cfg: Some(self.via_ssa_cfg),
1915 experimental: Some(self.experimental),
1916 stop_after: None,
1918 remappings: Vec::new(),
1920 output_selection: Default::default(),
1922 }
1923 .with_extra_output(self.configured_artifacts_handler().output_selection());
1924
1925 if self.ast || self.build_info {
1927 settings = settings.with_ast();
1928 }
1929
1930 let cli_settings =
1931 CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };
1932
1933 Ok(SolcSettings { settings, cli_settings })
1934 }
1935
1936 pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
1939 let optimize = if self.vyper.opt_level.is_some() { None } else { self.vyper.optimize };
1942
1943 Ok(VyperSettings {
1944 evm_version: Some(self.evm_version),
1945 optimize,
1946 opt_level: self.vyper.opt_level,
1947 bytecode_metadata: None,
1948 output_selection: OutputSelection::common_output_selection([
1951 "abi".to_string(),
1952 "evm.bytecode".to_string(),
1953 "evm.deployedBytecode".to_string(),
1954 ]),
1955 search_paths: None,
1956 experimental_codegen: self.vyper.experimental_codegen,
1957 debug: self.vyper.debug,
1958 enable_decimals: self.vyper.enable_decimals,
1959 venom_experimental: self.vyper.venom_experimental,
1960 venom: self.vyper.venom.clone(),
1961 })
1962 }
1963
1964 pub fn figment() -> Figment {
1985 Self::default().into()
1986 }
1987
1988 pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
2000 Self::with_root(root.as_ref()).into()
2001 }
2002
2003 #[doc(hidden)]
2004 #[track_caller]
2005 pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
2006 let root = match root {
2007 Some(root) => root,
2008 None => &find_project_root(None).expect("could not determine project root"),
2009 };
2010 Self::figment_with_root(root)
2011 }
2012
2013 pub fn with_root(root: impl AsRef<Path>) -> Self {
2022 Self::_with_root(root.as_ref())
2023 }
2024
2025 fn _with_root(root: &Path) -> Self {
2026 let paths = ProjectPathsConfig::builder().build_with_root::<()>(root);
2028 let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
2029 Self {
2030 root: paths.root,
2031 src: paths.sources.file_name().unwrap().into(),
2032 out: artifacts.clone(),
2033 libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
2034 fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
2035 ..Self::default()
2036 }
2037 }
2038
2039 pub fn hardhat() -> Self {
2041 Self {
2042 src: "contracts".into(),
2043 out: "artifacts".into(),
2044 libs: vec!["node_modules".into()],
2045 ..Self::default()
2046 }
2047 }
2048
2049 pub fn into_basic(self) -> BasicConfig {
2058 BasicConfig {
2059 profile: self.profile,
2060 src: self.src,
2061 out: self.out,
2062 libs: self.libs,
2063 remappings: self.remappings,
2064 network: self.networks.active_network_name().map(String::from),
2065 }
2066 }
2067
2068 pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
2073 where
2074 F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
2075 {
2076 let config = Self::load_with_root(root)?.sanitized();
2077 config.update(|doc| f(&config, doc))
2078 }
2079
2080 pub fn update<F>(&self, f: F) -> eyre::Result<()>
2085 where
2086 F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
2087 {
2088 let file_path = self.get_config_path();
2089 if !file_path.exists() {
2090 return Ok(());
2091 }
2092 let contents = fs::read_to_string(&file_path)?;
2093 let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
2094 if f(&mut doc) {
2095 fs::write(file_path, doc.to_string())?;
2096 }
2097 Ok(())
2098 }
2099
2100 pub fn update_libs(&self) -> eyre::Result<()> {
2106 self.update(|doc| {
2107 let profile = self.profile.as_str().as_str();
2108 let root = &self.root;
2109 let libs: toml_edit::Value = self
2110 .libs
2111 .iter()
2112 .map(|path| {
2113 let path =
2114 if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
2115 toml_edit::Value::from(&*path.to_string_lossy())
2116 })
2117 .collect();
2118 let libs = toml_edit::value(libs);
2119 doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
2120 true
2121 })
2122 }
2123
2124 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2136 let mut value = toml::Value::try_from(self)?;
2138 let value_table = value.as_table_mut().unwrap();
2140 let standalone_sections = Self::STANDALONE_SECTIONS
2142 .iter()
2143 .filter_map(|section| {
2144 let section = section.to_string();
2145 value_table.remove(§ion).map(|value| (section, value))
2146 })
2147 .collect::<Vec<_>>();
2148 let mut wrapping_table = [(
2150 Self::PROFILE_SECTION.into(),
2151 toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
2152 )]
2153 .into_iter()
2154 .collect::<toml::map::Map<_, _>>();
2155 for (section, value) in standalone_sections {
2157 wrapping_table.insert(section, value);
2158 }
2159 toml::to_string_pretty(&toml::Value::Table(wrapping_table))
2161 }
2162
2163 pub fn get_config_path(&self) -> PathBuf {
2165 self.root.join(Self::FILE_NAME)
2166 }
2167
2168 pub fn selected_profile() -> Profile {
2172 #[cfg(test)]
2174 {
2175 Self::force_selected_profile()
2176 }
2177 #[cfg(not(test))]
2178 {
2179 static CACHE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();
2180 CACHE.get_or_init(Self::force_selected_profile).clone()
2181 }
2182 }
2183
2184 fn force_selected_profile() -> Profile {
2185 Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
2186 }
2187
2188 pub fn foundry_dir_toml() -> Option<PathBuf> {
2190 Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
2191 }
2192
2193 pub fn foundry_dir() -> Option<PathBuf> {
2195 dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
2196 }
2197
2198 pub fn foundry_cache_dir() -> Option<PathBuf> {
2200 Self::foundry_dir().map(|p| p.join("cache"))
2201 }
2202
2203 pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
2205 Some(Self::foundry_cache_dir()?.join("rpc"))
2206 }
2207 pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
2209 Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
2210 }
2211
2212 pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
2214 Some(Self::foundry_cache_dir()?.join("etherscan"))
2215 }
2216
2217 pub fn foundry_keystores_dir() -> Option<PathBuf> {
2219 Some(Self::foundry_dir()?.join("keystores"))
2220 }
2221
2222 pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
2225 Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
2226 }
2227
2228 pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2231 Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
2232 }
2233
2234 pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2237 Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
2238 }
2239
2240 pub fn data_dir() -> eyre::Result<PathBuf> {
2248 let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
2249 std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
2250 Ok(path)
2251 }
2252
2253 pub fn find_config_file() -> Option<PathBuf> {
2260 fn find(path: &Path) -> Option<PathBuf> {
2261 if path.is_absolute() {
2262 return match path.is_file() {
2263 true => Some(path.to_path_buf()),
2264 false => None,
2265 };
2266 }
2267 let cwd = std::env::current_dir().ok()?;
2268 let mut cwd = cwd.as_path();
2269 loop {
2270 let file_path = cwd.join(path);
2271 if file_path.is_file() {
2272 return Some(file_path);
2273 }
2274 cwd = cwd.parent()?;
2275 }
2276 }
2277 find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
2278 .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
2279 }
2280
2281 pub fn clean_foundry_cache() -> eyre::Result<Vec<String>> {
2285 if let Some(cache_dir) = Self::foundry_cache_dir() {
2286 let path = cache_dir.as_path();
2287 if let Err(err) = fs::remove_dir_all(path)
2288 && err.kind() != io::ErrorKind::NotFound
2289 {
2290 return Ok(vec![format!(
2291 "failed to remove foundry cache at {}: {err}",
2292 path.display()
2293 )]);
2294 }
2295 } else {
2296 eyre::bail!("failed to get foundry_cache_dir");
2297 }
2298
2299 Ok(vec![])
2300 }
2301
2302 pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<Vec<String>> {
2306 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2307 let path = cache_dir.as_path();
2308 if let Err(err) = fs::remove_dir_all(path)
2309 && err.kind() != io::ErrorKind::NotFound
2310 {
2311 return Ok(vec![format!(
2312 "failed to remove foundry cache for chain {chain} at {}: {err}",
2313 path.display()
2314 )]);
2315 }
2316 } else {
2317 eyre::bail!("failed to get foundry_chain_cache_dir");
2318 }
2319
2320 Ok(vec![])
2321 }
2322
2323 pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<Vec<String>> {
2327 if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
2328 let path = cache_dir.as_path();
2329 if let Err(err) = fs::remove_dir_all(path)
2330 && err.kind() != io::ErrorKind::NotFound
2331 {
2332 return Ok(vec![format!(
2333 "failed to remove foundry cache for chain {chain} block {block} at {}: {err}",
2334 path.display()
2335 )]);
2336 }
2337 } else {
2338 eyre::bail!("failed to get foundry_block_cache_dir");
2339 }
2340
2341 Ok(vec![])
2342 }
2343
2344 pub fn clean_foundry_etherscan_cache() -> eyre::Result<Vec<String>> {
2348 if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
2349 let path = cache_dir.as_path();
2350 if let Err(err) = fs::remove_dir_all(path)
2351 && err.kind() != io::ErrorKind::NotFound
2352 {
2353 return Ok(vec![format!(
2354 "failed to remove foundry etherscan cache at {}: {err}",
2355 path.display()
2356 )]);
2357 }
2358 } else {
2359 eyre::bail!("failed to get foundry_etherscan_cache_dir");
2360 }
2361
2362 Ok(vec![])
2363 }
2364
2365 pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<Vec<String>> {
2369 if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
2370 let path = cache_dir.as_path();
2371 if let Err(err) = fs::remove_dir_all(path)
2372 && err.kind() != io::ErrorKind::NotFound
2373 {
2374 return Ok(vec![format!(
2375 "failed to remove foundry etherscan cache for chain {chain} at {}: {err}",
2376 path.display()
2377 )]);
2378 }
2379 } else {
2380 eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
2381 }
2382
2383 Ok(vec![])
2384 }
2385
2386 pub fn list_foundry_cache() -> eyre::Result<Cache> {
2388 if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
2389 let mut cache = Cache { chains: vec![] };
2390 if !cache_dir.exists() {
2391 return Ok(cache);
2392 }
2393 if let Ok(entries) = cache_dir.as_path().read_dir() {
2394 for entry in entries.flatten().filter(|x| x.path().is_dir()) {
2395 if let Ok(chain) = Chain::from_str(&entry.file_name().to_string_lossy()) {
2396 cache.chains.push(Self::list_foundry_chain_cache(chain)?);
2397 }
2398 }
2399 Ok(cache)
2400 } else {
2401 eyre::bail!("failed to access foundry_cache_dir");
2402 }
2403 } else {
2404 eyre::bail!("failed to get foundry_cache_dir");
2405 }
2406 }
2407
2408 pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
2410 let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
2411 Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
2412 None => {
2413 warn!("failed to access foundry_etherscan_chain_cache_dir");
2414 0
2415 }
2416 };
2417
2418 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2419 let blocks = Self::get_cached_blocks(&cache_dir)?;
2420 Ok(ChainCache {
2421 name: chain.to_string(),
2422 blocks,
2423 block_explorer: block_explorer_data_size,
2424 })
2425 } else {
2426 eyre::bail!("failed to get foundry_chain_cache_dir");
2427 }
2428 }
2429
2430 fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
2432 let mut blocks = vec![];
2433 if !chain_path.exists() {
2434 return Ok(blocks);
2435 }
2436 for block in chain_path.read_dir()?.flatten() {
2437 let file_type = block.file_type()?;
2438 let file_name = block.file_name();
2439 let filepath = if file_type.is_dir() {
2440 block.path().join("storage.json")
2441 } else if file_type.is_file()
2442 && file_name.to_string_lossy().chars().all(char::is_numeric)
2443 {
2444 block.path()
2445 } else {
2446 continue;
2447 };
2448 blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
2449 }
2450 Ok(blocks)
2451 }
2452
2453 fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
2455 if !chain_path.exists() {
2456 return Ok(0);
2457 }
2458
2459 fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
2460 dir.try_fold(0, |acc, file| {
2461 let file = file?;
2462 let size = match file.metadata()? {
2463 data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
2464 data => data.len(),
2465 };
2466 Ok(acc + size)
2467 })
2468 }
2469
2470 dir_size_recursive(fs::read_dir(chain_path)?)
2471 }
2472
2473 fn merge_toml_provider(
2474 mut figment: Figment,
2475 toml_provider: impl Provider,
2476 profile: Profile,
2477 ) -> Figment {
2478 figment = figment.select(profile.clone());
2479
2480 figment = {
2482 let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
2483 figment.merge(warnings)
2484 };
2485
2486 let mut profiles = vec![Self::DEFAULT_PROFILE];
2488 if profile != Self::DEFAULT_PROFILE {
2489 profiles.push(profile.clone());
2490 }
2491 let provider = ForcedSnakeCaseData(toml_provider).strict_select(profiles);
2494 let provider = &BackwardsCompatTomlProvider(provider);
2495
2496 if profile != Self::DEFAULT_PROFILE {
2498 figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
2499 }
2500 for standalone_key in Self::STANDALONE_SECTIONS {
2502 if let Some((_, fallback)) =
2503 STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
2504 {
2505 figment = figment.merge(
2506 provider
2507 .fallback(standalone_key, fallback)
2508 .wrap(profile.clone(), standalone_key),
2509 );
2510 } else {
2511 figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
2512 }
2513 }
2514 figment = figment.merge(provider);
2516 figment
2517 }
2518
2519 fn normalize_defaults(&self, mut figment: Figment) -> Figment {
2525 if figment.contains("evm_version") {
2526 return figment;
2527 }
2528
2529 if let Ok(solc) = figment.extract_inner::<SolcReq>("solc")
2531 && let Some(version) = solc
2532 .try_version()
2533 .ok()
2534 .and_then(|version| self.evm_version.normalize_version_solc(&version))
2535 {
2536 figment = figment.merge(("evm_version", version));
2537 }
2538
2539 if figment.extract_inner::<bool>("deny_warnings").unwrap_or(false)
2541 && figment.extract_inner("deny") == Ok(DenyLevel::Never)
2542 {
2543 figment = figment.merge(("deny", DenyLevel::Warnings));
2544 }
2545
2546 figment
2547 }
2548}
2549
2550impl From<Config> for Figment {
2551 fn from(c: Config) -> Self {
2552 (&c).into()
2553 }
2554}
2555impl From<&Config> for Figment {
2556 fn from(c: &Config) -> Self {
2557 c.to_figment(FigmentProviders::All)
2558 }
2559}
2560
2561#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2563pub enum FigmentProviders {
2564 #[default]
2566 All,
2567 Cast,
2571 Anvil,
2575 None,
2577}
2578
2579impl FigmentProviders {
2580 pub const fn is_all(&self) -> bool {
2582 matches!(self, Self::All)
2583 }
2584
2585 pub const fn is_cast(&self) -> bool {
2587 matches!(self, Self::Cast)
2588 }
2589
2590 pub const fn is_anvil(&self) -> bool {
2592 matches!(self, Self::Anvil)
2593 }
2594
2595 pub const fn is_none(&self) -> bool {
2597 matches!(self, Self::None)
2598 }
2599}
2600
2601fn figment_value_is_configured(figment: &Figment, key: &str) -> bool {
2602 figment.find_metadata(key).is_some_and(|metadata| metadata.name.as_ref() != "Foundry Config")
2603}
2604
2605#[derive(Clone, Debug, Serialize, Deserialize)]
2607#[serde(transparent)]
2608pub struct RegexWrapper {
2609 #[serde(with = "serde_regex")]
2610 inner: regex::Regex,
2611}
2612
2613impl std::ops::Deref for RegexWrapper {
2614 type Target = regex::Regex;
2615
2616 fn deref(&self) -> &Self::Target {
2617 &self.inner
2618 }
2619}
2620
2621impl std::cmp::PartialEq for RegexWrapper {
2622 fn eq(&self, other: &Self) -> bool {
2623 self.as_str() == other.as_str()
2624 }
2625}
2626
2627impl Eq for RegexWrapper {}
2628
2629impl From<RegexWrapper> for regex::Regex {
2630 fn from(wrapper: RegexWrapper) -> Self {
2631 wrapper.inner
2632 }
2633}
2634
2635impl From<regex::Regex> for RegexWrapper {
2636 fn from(re: Regex) -> Self {
2637 Self { inner: re }
2638 }
2639}
2640
2641mod serde_regex {
2642 use regex::Regex;
2643 use serde::{Deserialize, Deserializer, Serializer};
2644
2645 pub(crate) fn serialize<S>(value: &Regex, serializer: S) -> Result<S::Ok, S::Error>
2646 where
2647 S: Serializer,
2648 {
2649 serializer.serialize_str(value.as_str())
2650 }
2651
2652 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
2653 where
2654 D: Deserializer<'de>,
2655 {
2656 let s = String::deserialize(deserializer)?;
2657 Regex::new(&s).map_err(serde::de::Error::custom)
2658 }
2659}
2660
2661pub(crate) mod from_opt_glob {
2663 use serde::{Deserialize, Deserializer, Serializer};
2664
2665 pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
2666 where
2667 S: Serializer,
2668 {
2669 match value {
2670 Some(glob) => serializer.serialize_str(glob.glob()),
2671 None => serializer.serialize_none(),
2672 }
2673 }
2674
2675 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
2676 where
2677 D: Deserializer<'de>,
2678 {
2679 let s: Option<String> = Option::deserialize(deserializer)?;
2680 if let Some(s) = s {
2681 return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
2682 }
2683 Ok(None)
2684 }
2685}
2686
2687pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
2698 s: &str,
2699) -> Result<Option<(Profile, T)>, Error> {
2700 let figment = Config::merge_toml_provider(
2701 Figment::new(),
2702 Toml::string(s).nested(),
2703 Config::DEFAULT_PROFILE,
2704 );
2705 if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
2706 Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
2707 } else {
2708 Ok(None)
2709 }
2710}
2711
2712impl Provider for Config {
2713 fn metadata(&self) -> Metadata {
2714 Metadata::named("Foundry Config")
2715 }
2716
2717 #[track_caller]
2718 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2719 let mut data = Serialized::defaults(self).data()?;
2720 let root = Value::serialize(self.root.clone())?;
2721 if let Some(entry) = data.get_mut(&Self::DEFAULT_PROFILE) {
2722 entry.insert("root".to_string(), root.clone());
2723 }
2724 if let Some(entry) = data.get_mut(&self.profile) {
2725 entry.insert("root".to_string(), root);
2726 }
2727 Ok(data)
2728 }
2729
2730 fn profile(&self) -> Option<Profile> {
2731 Some(self.profile.clone())
2732 }
2733}
2734
2735impl Default for Config {
2736 fn default() -> Self {
2737 Self {
2738 profile: Self::DEFAULT_PROFILE,
2739 profiles: vec![Self::DEFAULT_PROFILE],
2740 fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2741 isolate: true,
2742 root: root_default(),
2743 extends: None,
2744 src: "src".into(),
2745 test: "test".into(),
2746 script: "script".into(),
2747 out: "out".into(),
2748 libs: vec!["lib".into()],
2749 cache: true,
2750 dynamic_test_linking: true,
2751 cache_path: "cache".into(),
2752 broadcast: "broadcast".into(),
2753 snapshots: "snapshots".into(),
2754 gas_snapshot_check: false,
2755 gas_snapshot_emit: true,
2756 allow_paths: vec![],
2757 include_paths: vec![],
2758 force: false,
2759 evm_version: EvmVersion::Osaka,
2760 hardfork: None,
2761 gas_reports: vec!["*".to_string()],
2762 gas_reports_ignore: vec![],
2763 gas_reports_include_tests: false,
2764 solc: None,
2765 vyper: Default::default(),
2766 auto_detect_solc: true,
2767 offline: false,
2768 optimizer: None,
2769 optimizer_runs: None,
2770 optimizer_details: None,
2771 model_checker: None,
2772 extra_output: Default::default(),
2773 extra_output_files: Default::default(),
2774 names: false,
2775 sizes: false,
2776 test_pattern: None,
2777 test_pattern_inverse: None,
2778 contract_pattern: None,
2779 contract_pattern_inverse: None,
2780 path_pattern: None,
2781 path_pattern_inverse: None,
2782 coverage_pattern_inverse: None,
2783 test_failures_file: "cache/test-failures".into(),
2784 mutation_dir: "cache/mutation".into(),
2785 threads: None,
2786 show_progress: false,
2787 fuzz: FuzzConfig::new("cache/fuzz".into()),
2788 invariant: InvariantConfig::new("cache/invariant".into()),
2789 symbolic: SymbolicConfig::default(),
2790 coverage: CoverageConfig::default(),
2791 mutation: MutationConfig::default(),
2792 always_use_create_2_factory: false,
2793 eip1559_fee_estimate: Eip1559FeeEstimatePreset::default(),
2794 ffi: false,
2795 live_logs: false,
2796 allow_internal_expect_revert: false,
2797 prompt_timeout: 120,
2798 sender: Self::DEFAULT_SENDER,
2799 tx_origin: Self::DEFAULT_SENDER,
2800 initial_balance: U256::from((1u128 << 96) - 1),
2801 block_number: U256::from(1),
2802 fork_block_number: None,
2803 chain: None,
2804 gas_limit: (1u64 << 30).into(), code_size_limit: None,
2806 gas_price: None,
2807 block_base_fee_per_gas: 0,
2808 block_coinbase: Address::ZERO,
2809 block_timestamp: U256::from(1),
2810 block_difficulty: 0,
2811 block_prevrandao: Default::default(),
2812 block_gas_limit: None,
2813 disable_block_gas_limit: false,
2814 enable_tx_gas_limit: false,
2815 memory_limit: 1 << 27, eth_rpc_url: None,
2817 eth_rpc_accept_invalid_certs: false,
2818 eth_rpc_no_proxy: false,
2819 eth_rpc_jwt: None,
2820 eth_rpc_timeout: None,
2821 eth_rpc_headers: None,
2822 eth_rpc_curl: false,
2823 etherscan_api_key: None,
2824 verbosity: 0,
2825 remappings: vec![],
2826 auto_detect_remappings: true,
2827 libraries: vec![],
2828 ignored_error_codes: vec![
2829 SolidityErrorCode::SpdxLicenseNotProvided,
2830 SolidityErrorCode::ContractExceeds24576Bytes,
2831 SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2832 SolidityErrorCode::TransientStorageUsed,
2833 SolidityErrorCode::TransferDeprecated,
2834 SolidityErrorCode::NatspecMemorySafeAssemblyDeprecated,
2835 ],
2836 ignored_error_codes_from: vec![],
2837 ignored_file_paths: vec![],
2838 deny: DenyLevel::Never,
2839 deny_warnings: false,
2840 via_ir: false,
2841 via_ssa_cfg: false,
2842 experimental: false,
2843 ast: false,
2844 rpc_storage_caching: Default::default(),
2845 rpc_endpoints: Default::default(),
2846 etherscan: Default::default(),
2847 no_storage_caching: false,
2848 no_rpc_rate_limit: false,
2849 use_literal_content: false,
2850 bytecode_hash: BytecodeHash::Ipfs,
2851 cbor_metadata: true,
2852 revert_strings: None,
2853 sparse_mode: false,
2854 build_info: false,
2855 build_info_path: None,
2856 fmt: Default::default(),
2857 lint: Default::default(),
2858 doc: Default::default(),
2859 bind_json: Default::default(),
2860 labels: Default::default(),
2861 unchecked_cheatcode_artifacts: false,
2862 create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
2863 create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
2864 skip: vec![],
2865 dependencies: Default::default(),
2866 soldeer: Default::default(),
2867 assertions_revert: true,
2868 legacy_assertions: false,
2869 warnings: vec![],
2870 extra_args: vec![],
2871 networks: Default::default(),
2872 transaction_timeout: 120,
2873 additional_compiler_profiles: Default::default(),
2874 compilation_restrictions: Default::default(),
2875 script_execution_protection: true,
2876 _non_exhaustive: (),
2877 }
2878 }
2879}
2880
2881#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
2887pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);
2888
2889impl From<u64> for GasLimit {
2890 fn from(gas: u64) -> Self {
2891 Self(gas)
2892 }
2893}
2894
2895impl From<GasLimit> for u64 {
2896 fn from(gas: GasLimit) -> Self {
2897 gas.0
2898 }
2899}
2900
2901impl Serialize for GasLimit {
2902 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2903 where
2904 S: Serializer,
2905 {
2906 if self.0 == u64::MAX {
2907 serializer.serialize_str("max")
2908 } else if self.0 > i64::MAX as u64 {
2909 serializer.serialize_str(&self.0.to_string())
2910 } else {
2911 serializer.serialize_u64(self.0)
2912 }
2913 }
2914}
2915
2916#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2918#[serde(untagged)]
2919pub enum SolcReq {
2920 Version(Version),
2923 Local(PathBuf),
2925}
2926
2927impl SolcReq {
2928 fn try_version(&self) -> Result<Version, SolcError> {
2933 match self {
2934 Self::Version(version) => Ok(version.clone()),
2935 Self::Local(path) => Solc::new(path).map(|solc| solc.version),
2936 }
2937 }
2938}
2939
2940impl<T: AsRef<str>> From<T> for SolcReq {
2941 fn from(s: T) -> Self {
2942 let s = s.as_ref();
2943 if let Ok(v) = Version::from_str(s) { Self::Version(v) } else { Self::Local(s.into()) }
2944 }
2945}
2946
2947#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2959pub struct BasicConfig {
2960 #[serde(skip)]
2962 pub profile: Profile,
2963 pub src: PathBuf,
2965 pub out: PathBuf,
2967 pub libs: Vec<PathBuf>,
2969 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2971 pub remappings: Vec<RelativeRemapping>,
2972 #[serde(skip)]
2974 pub network: Option<String>,
2975}
2976
2977impl BasicConfig {
2978 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2982 let mut profile_body = toml::Value::try_from(self)?;
2983 if let Some(ref network) = self.network
2984 && let toml::Value::Table(ref mut table) = profile_body
2985 {
2986 table.insert("network".to_string(), toml::Value::String(network.clone()));
2987 }
2988
2989 let mut profile_section = toml::value::Table::new();
2990 profile_section.insert(self.profile.to_string(), profile_body);
2991
2992 let mut document = toml::value::Table::new();
2993 document.insert("profile".to_string(), toml::Value::Table(profile_section));
2994
2995 if self.network.as_deref() == Some("tempo") {
2996 let mut endpoints = toml::value::Table::new();
2997 endpoints.insert(
2998 "tempo".to_string(),
2999 toml::Value::String("https://rpc.tempo.xyz/".to_string()),
3000 );
3001 endpoints.insert(
3002 "moderato".to_string(),
3003 toml::Value::String("https://rpc.moderato.tempo.xyz/".to_string()),
3004 );
3005 document.insert("rpc_endpoints".to_string(), toml::Value::Table(endpoints));
3006 }
3007
3008 let body = toml::to_string_pretty(&toml::Value::Table(document))?;
3009 Ok(format!(
3010 "{body}\n# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n"
3011 ))
3012 }
3013}
3014
3015pub(crate) mod from_str_lowercase {
3016 use serde::{Deserialize, Deserializer, Serializer};
3017 use std::str::FromStr;
3018
3019 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
3020 where
3021 T: std::fmt::Display,
3022 S: Serializer,
3023 {
3024 serializer.collect_str(&value.to_string().to_lowercase())
3025 }
3026
3027 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
3028 where
3029 D: Deserializer<'de>,
3030 T: FromStr,
3031 T::Err: std::fmt::Display,
3032 {
3033 String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
3034 }
3035}
3036
3037fn canonic(path: impl Into<PathBuf>) -> PathBuf {
3038 let path = path.into();
3039 foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
3040}
3041
3042fn root_default() -> PathBuf {
3043 ".".into()
3044}
3045
3046#[cfg(test)]
3047mod tests {
3048 use super::*;
3049 use crate::{
3050 cache::{CachedChains, CachedEndpoints},
3051 endpoints::RpcEndpointType,
3052 etherscan::ResolvedEtherscanConfigs,
3053 fmt::IndentStyle,
3054 };
3055 use NamedChain::Moonbeam;
3056 use endpoints::{RpcAuth, RpcEndpointConfig};
3057 use figment::error::Kind::InvalidType;
3058 use foundry_compilers::artifacts::{
3059 ModelCheckerEngine, YulDetails,
3060 vyper::{VyperOptimizationLevel, VyperOptimizationMode, VyperVenomSettings},
3061 };
3062 use foundry_evm_hardforks::{TempoHardfork, latest_active_tempo_hardfork};
3063 use similar_asserts::assert_eq;
3064 use soldeer_core::remappings::RemappingsLocation;
3065 use std::{fs::File, io::Write, num::NonZeroUsize};
3066 use tempfile::tempdir;
3067
3068 fn clear_warning(config: &mut Config) {
3071 config.warnings = vec![];
3072 }
3073
3074 fn mark_serialized_invariant_provenance(config: &mut Config) {
3075 config.invariant.corpus_random_sequence_weight_configured = true;
3076 config.invariant.workers_configured = true;
3077 }
3078
3079 #[test]
3080 fn default_sender() {
3081 assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
3082 }
3083
3084 #[test]
3085 fn test_caching() {
3086 let mut config = Config::default();
3087 let chain_id = NamedChain::Mainnet;
3088 let url = "https://eth-mainnet.alchemyapi";
3089 assert!(config.enable_caching(url, chain_id));
3090
3091 config.no_storage_caching = true;
3092 assert!(!config.enable_caching(url, chain_id));
3093
3094 config.no_storage_caching = false;
3095 assert!(!config.enable_caching(url, NamedChain::Dev));
3096 }
3097
3098 #[test]
3099 fn test_install_dir() {
3100 figment::Jail::expect_with(|jail| {
3101 let config = Config::load().unwrap();
3102 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
3103 jail.create_file(
3104 "foundry.toml",
3105 r"
3106 [profile.default]
3107 libs = ['node_modules', 'lib']
3108 ",
3109 )?;
3110 let config = Config::load().unwrap();
3111 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
3112
3113 jail.create_file(
3114 "foundry.toml",
3115 r"
3116 [profile.default]
3117 libs = ['custom', 'node_modules', 'lib']
3118 ",
3119 )?;
3120 let config = Config::load().unwrap();
3121 assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
3122
3123 Ok(())
3124 });
3125 }
3126
3127 #[test]
3128 fn test_figment_is_default() {
3129 figment::Jail::expect_with(|_| {
3130 let mut default: Config = Config::figment().extract()?;
3131 let default2 = Config::default();
3132 default.profile = default2.profile.clone();
3133 default.profiles = default2.profiles.clone();
3134 assert_eq!(default, default2);
3135 Ok(())
3136 });
3137 }
3138
3139 #[test]
3140 fn figment_profiles() {
3141 figment::Jail::expect_with(|jail| {
3142 jail.create_file(
3143 "foundry.toml",
3144 r"
3145 [foo.baz]
3146 libs = ['node_modules', 'lib']
3147
3148 [profile.default]
3149 libs = ['node_modules', 'lib']
3150
3151 [profile.ci]
3152 libs = ['node_modules', 'lib']
3153
3154 [profile.local]
3155 libs = ['node_modules', 'lib']
3156 ",
3157 )?;
3158
3159 let config = crate::Config::load().unwrap();
3160 let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
3161 assert_eq!(config.profiles, expected);
3162
3163 Ok(())
3164 });
3165 }
3166
3167 #[test]
3168 fn test_default_round_trip() {
3169 figment::Jail::expect_with(|_| {
3170 let original = Config::figment();
3171 let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
3172 for figment in &[original, roundtrip] {
3173 let config = Config::from_provider(figment).unwrap();
3174 assert_eq!(config, Config::default().normalized_optimizer_settings());
3175 }
3176 Ok(())
3177 });
3178 }
3179
3180 #[test]
3181 fn ffi_env_disallowed() {
3182 figment::Jail::expect_with(|jail| {
3183 jail.set_env("FOUNDRY_FFI", "true");
3184 jail.set_env("FFI", "true");
3185 jail.set_env("DAPP_FFI", "true");
3186 let config = Config::load().unwrap();
3187 assert!(!config.ffi);
3188
3189 Ok(())
3190 });
3191 }
3192
3193 #[test]
3194 fn test_profile_env() {
3195 figment::Jail::expect_with(|jail| {
3196 jail.set_env("FOUNDRY_PROFILE", "default");
3197 let figment = Config::figment();
3198 assert_eq!(figment.profile(), "default");
3199
3200 jail.set_env("FOUNDRY_PROFILE", "hardhat");
3201 let figment: Figment = Config::hardhat().into();
3202 assert_eq!(figment.profile(), "hardhat");
3203
3204 jail.create_file(
3205 "foundry.toml",
3206 r"
3207 [profile.default]
3208 libs = ['lib']
3209 [profile.local]
3210 libs = ['modules']
3211 ",
3212 )?;
3213 jail.set_env("FOUNDRY_PROFILE", "local");
3214 let config = Config::load().unwrap();
3215 assert_eq!(config.libs, vec![PathBuf::from("modules")]);
3216
3217 Ok(())
3218 });
3219 }
3220
3221 #[test]
3222 fn test_default_test_path() {
3223 figment::Jail::expect_with(|_| {
3224 let config = Config::default();
3225 let paths_config = config.project_paths::<Solc>();
3226 assert_eq!(paths_config.tests, PathBuf::from(r"test"));
3227 Ok(())
3228 });
3229 }
3230
3231 #[test]
3232 fn test_default_libs() {
3233 figment::Jail::expect_with(|jail| {
3234 let config = Config::load().unwrap();
3235 assert_eq!(config.libs, vec![PathBuf::from("lib")]);
3236
3237 fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
3238 let config = Config::load().unwrap();
3239 assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
3240
3241 fs::create_dir_all(jail.directory().join("lib")).unwrap();
3242 let config = Config::load().unwrap();
3243 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3244
3245 Ok(())
3246 });
3247 }
3248
3249 #[test]
3250 fn test_inheritance_from_default_test_path() {
3251 figment::Jail::expect_with(|jail| {
3252 jail.create_file(
3253 "foundry.toml",
3254 r#"
3255 [profile.default]
3256 test = "defaulttest"
3257 src = "defaultsrc"
3258 libs = ['lib', 'node_modules']
3259
3260 [profile.custom]
3261 src = "customsrc"
3262 "#,
3263 )?;
3264
3265 let config = Config::load().unwrap();
3266 assert_eq!(config.src, PathBuf::from("defaultsrc"));
3267 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3268
3269 jail.set_env("FOUNDRY_PROFILE", "custom");
3270 let config = Config::load().unwrap();
3271 assert_eq!(config.src, PathBuf::from("customsrc"));
3272 assert_eq!(config.test, PathBuf::from("defaulttest"));
3273 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3274
3275 Ok(())
3276 });
3277 }
3278
3279 #[test]
3280 fn test_custom_test_path() {
3281 figment::Jail::expect_with(|jail| {
3282 jail.create_file(
3283 "foundry.toml",
3284 r#"
3285 [profile.default]
3286 test = "mytest"
3287 "#,
3288 )?;
3289
3290 let config = Config::load().unwrap();
3291 let paths_config = config.project_paths::<Solc>();
3292 assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
3293 Ok(())
3294 });
3295 }
3296
3297 #[test]
3298 fn test_remappings() {
3299 figment::Jail::expect_with(|jail| {
3300 jail.create_file(
3301 "foundry.toml",
3302 r#"
3303 [profile.default]
3304 src = "some-source"
3305 out = "some-out"
3306 cache = true
3307 "#,
3308 )?;
3309 let config = Config::load().unwrap();
3310 assert!(config.remappings.is_empty());
3311
3312 jail.create_file(
3313 "remappings.txt",
3314 r"
3315 file-ds-test/=lib/ds-test/
3316 file-other/=lib/other/
3317 ",
3318 )?;
3319
3320 let config = Config::load().unwrap();
3321 assert_eq!(
3322 config.remappings,
3323 vec![
3324 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3325 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3326 ],
3327 );
3328
3329 jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
3330 let config = Config::load().unwrap();
3331
3332 assert_eq!(
3333 config.remappings,
3334 vec![
3335 Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
3337 Remapping::from_str("other/=lib/other/").unwrap().into(),
3338 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3340 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3341 ],
3342 );
3343
3344 Ok(())
3345 });
3346 }
3347
3348 #[test]
3349 fn test_remappings_override() {
3350 figment::Jail::expect_with(|jail| {
3351 jail.create_file(
3352 "foundry.toml",
3353 r#"
3354 [profile.default]
3355 src = "some-source"
3356 out = "some-out"
3357 cache = true
3358 "#,
3359 )?;
3360 let config = Config::load().unwrap();
3361 assert!(config.remappings.is_empty());
3362
3363 jail.create_file(
3364 "remappings.txt",
3365 r"
3366 ds-test/=lib/ds-test/
3367 other/=lib/other/
3368 ",
3369 )?;
3370
3371 let config = Config::load().unwrap();
3372 assert_eq!(
3373 config.remappings,
3374 vec![
3375 Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
3376 Remapping::from_str("other/=lib/other/").unwrap().into(),
3377 ],
3378 );
3379
3380 jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
3381 let config = Config::load().unwrap();
3382
3383 assert_eq!(
3388 config.remappings,
3389 vec![
3390 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
3391 Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
3392 Remapping::from_str("other/=lib/other/").unwrap().into(),
3393 ],
3394 );
3395
3396 assert_eq!(
3398 config.get_all_remappings().collect::<Vec<_>>(),
3399 vec![
3400 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
3401 Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
3402 Remapping::from_str("other/=lib/other/").unwrap(),
3403 ],
3404 );
3405
3406 Ok(())
3407 });
3408 }
3409
3410 #[test]
3411 fn test_can_update_libs() {
3412 figment::Jail::expect_with(|jail| {
3413 jail.create_file(
3414 "foundry.toml",
3415 r#"
3416 [profile.default]
3417 libs = ["node_modules"]
3418 "#,
3419 )?;
3420
3421 let mut config = Config::load().unwrap();
3422 config.libs.push("libs".into());
3423 config.update_libs().unwrap();
3424
3425 let config = Config::load().unwrap();
3426 assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
3427 Ok(())
3428 });
3429 }
3430
3431 #[test]
3432 fn test_large_gas_limit() {
3433 figment::Jail::expect_with(|jail| {
3434 let gas = u64::MAX;
3435 jail.create_file(
3436 "foundry.toml",
3437 &format!(
3438 r#"
3439 [profile.default]
3440 gas_limit = "{gas}"
3441 "#
3442 ),
3443 )?;
3444
3445 let config = Config::load().unwrap();
3446 assert_eq!(
3447 config,
3448 Config {
3449 gas_limit: gas.into(),
3450 ..Config::default().normalized_optimizer_settings()
3451 }
3452 );
3453
3454 Ok(())
3455 });
3456 }
3457
3458 #[test]
3459 #[should_panic]
3460 fn test_toml_file_parse_failure() {
3461 figment::Jail::expect_with(|jail| {
3462 jail.create_file(
3463 "foundry.toml",
3464 r#"
3465 [profile.default]
3466 eth_rpc_url = "https://example.com/
3467 "#,
3468 )?;
3469
3470 let _config = Config::load().unwrap();
3471
3472 Ok(())
3473 });
3474 }
3475
3476 #[test]
3477 #[should_panic]
3478 fn test_toml_file_non_existing_config_var_failure() {
3479 figment::Jail::expect_with(|jail| {
3480 jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3481
3482 let _config = Config::load().unwrap();
3483
3484 Ok(())
3485 });
3486 }
3487
3488 #[test]
3489 fn test_resolve_etherscan_with_chain() {
3490 figment::Jail::expect_with(|jail| {
3491 let env_key = "__BSC_ETHERSCAN_API_KEY";
3492 let env_value = "env value";
3493 jail.create_file(
3494 "foundry.toml",
3495 r#"
3496 [profile.default]
3497
3498 [etherscan]
3499 bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3500 "#,
3501 )?;
3502
3503 let config = Config::load().unwrap();
3504 assert!(
3505 config
3506 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3507 .is_err()
3508 );
3509
3510 unsafe {
3511 std::env::set_var(env_key, env_value);
3512 }
3513
3514 assert_eq!(
3515 config
3516 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3517 .unwrap()
3518 .unwrap()
3519 .key,
3520 env_value
3521 );
3522
3523 let mut with_key = config;
3524 with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3525
3526 assert_eq!(
3527 with_key
3528 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3529 .unwrap()
3530 .unwrap()
3531 .key,
3532 "via etherscan_api_key"
3533 );
3534
3535 unsafe {
3536 std::env::remove_var(env_key);
3537 }
3538 Ok(())
3539 });
3540 }
3541
3542 #[test]
3543 fn test_resolve_etherscan() {
3544 figment::Jail::expect_with(|jail| {
3545 jail.create_file(
3546 "foundry.toml",
3547 r#"
3548 [profile.default]
3549
3550 [etherscan]
3551 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3552 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3553 "#,
3554 )?;
3555
3556 let config = Config::load().unwrap();
3557
3558 assert!(config.etherscan.clone().resolved().has_unresolved());
3559
3560 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3561
3562 let configs = config.etherscan.resolved();
3563 assert!(!configs.has_unresolved());
3564
3565 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3566 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3567 assert_eq!(
3568 configs,
3569 ResolvedEtherscanConfigs::new([
3570 (
3571 "mainnet",
3572 ResolvedEtherscanConfig {
3573 api_url: mainnet_urls.0.to_string(),
3574 chain: Some(NamedChain::Mainnet.into()),
3575 browser_url: Some(mainnet_urls.1.to_string()),
3576 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3577 }
3578 ),
3579 (
3580 "moonbeam",
3581 ResolvedEtherscanConfig {
3582 api_url: mb_urls.0.to_string(),
3583 chain: Some(Moonbeam.into()),
3584 browser_url: Some(mb_urls.1.to_string()),
3585 key: "123456789".to_string(),
3586 }
3587 ),
3588 ])
3589 );
3590
3591 Ok(())
3592 });
3593 }
3594
3595 #[test]
3596 fn test_resolve_etherscan_with_versions() {
3597 figment::Jail::expect_with(|jail| {
3598 jail.create_file(
3599 "foundry.toml",
3600 r#"
3601 [profile.default]
3602
3603 [etherscan]
3604 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
3605 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
3606 "#,
3607 )?;
3608
3609 let config = Config::load().unwrap();
3610
3611 assert!(config.etherscan.clone().resolved().has_unresolved());
3612
3613 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3614
3615 let configs = config.etherscan.resolved();
3616 assert!(!configs.has_unresolved());
3617
3618 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3619 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3620 assert_eq!(
3621 configs,
3622 ResolvedEtherscanConfigs::new([
3623 (
3624 "mainnet",
3625 ResolvedEtherscanConfig {
3626 api_url: mainnet_urls.0.to_string(),
3627 chain: Some(NamedChain::Mainnet.into()),
3628 browser_url: Some(mainnet_urls.1.to_string()),
3629 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3630 }
3631 ),
3632 (
3633 "moonbeam",
3634 ResolvedEtherscanConfig {
3635 api_url: mb_urls.0.to_string(),
3636 chain: Some(Moonbeam.into()),
3637 browser_url: Some(mb_urls.1.to_string()),
3638 key: "123456789".to_string(),
3639 }
3640 ),
3641 ])
3642 );
3643
3644 Ok(())
3645 });
3646 }
3647
3648 #[test]
3649 fn test_resolve_etherscan_chain_id() {
3650 figment::Jail::expect_with(|jail| {
3651 jail.create_file(
3652 "foundry.toml",
3653 r#"
3654 [profile.default]
3655 chain_id = "sepolia"
3656
3657 [etherscan]
3658 sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3659 "#,
3660 )?;
3661
3662 let config = Config::load().unwrap();
3663 let etherscan = config.get_etherscan_config().unwrap().unwrap();
3664 assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3665 assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3666
3667 Ok(())
3668 });
3669 }
3670
3671 #[test]
3673 fn test_resolve_etherscan_with_invalid_name() {
3674 figment::Jail::expect_with(|jail| {
3675 jail.create_file(
3676 "foundry.toml",
3677 r#"
3678 [etherscan]
3679 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3680 an_invalid_name = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3681 "#,
3682 )?;
3683
3684 let config = Config::load().unwrap();
3685 let etherscan_config = config.get_etherscan_config();
3686 assert!(etherscan_config.is_none());
3687
3688 Ok(())
3689 });
3690 }
3691
3692 #[test]
3693 fn test_resolve_rpc_url() {
3694 figment::Jail::expect_with(|jail| {
3695 jail.create_file(
3696 "foundry.toml",
3697 r#"
3698 [profile.default]
3699 [rpc_endpoints]
3700 optimism = "https://example.com/"
3701 mainnet = "${_CONFIG_MAINNET}"
3702 "#,
3703 )?;
3704 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3705
3706 let mut config = Config::load().unwrap();
3707 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3708
3709 config.eth_rpc_url = Some("mainnet".to_string());
3710 assert_eq!(
3711 "https://eth-mainnet.alchemyapi.io/v2/123455",
3712 config.get_rpc_url_or_localhost_http().unwrap()
3713 );
3714
3715 config.eth_rpc_url = Some("optimism".to_string());
3716 assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
3717
3718 Ok(())
3719 })
3720 }
3721
3722 #[test]
3723 fn test_resolve_rpc_url_if_etherscan_set() {
3724 figment::Jail::expect_with(|jail| {
3725 jail.create_file(
3726 "foundry.toml",
3727 r#"
3728 [profile.default]
3729 etherscan_api_key = "dummy"
3730 [rpc_endpoints]
3731 optimism = "https://example.com/"
3732 "#,
3733 )?;
3734
3735 let config = Config::load().unwrap();
3736 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3737
3738 Ok(())
3739 })
3740 }
3741
3742 #[test]
3743 fn test_resolve_rpc_url_alias() {
3744 figment::Jail::expect_with(|jail| {
3745 jail.create_file(
3746 "foundry.toml",
3747 r#"
3748 [profile.default]
3749 [rpc_endpoints]
3750 polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
3751 "#,
3752 )?;
3753 let mut config = Config::load().unwrap();
3754 config.eth_rpc_url = Some("polygonAmoy".to_string());
3755 assert!(config.get_rpc_url().unwrap().is_err());
3756
3757 jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
3758
3759 let mut config = Config::load().unwrap();
3760 config.eth_rpc_url = Some("polygonAmoy".to_string());
3761 assert_eq!(
3762 "https://polygon-amoy.g.alchemy.com/v2/123455",
3763 config.get_rpc_url().unwrap().unwrap()
3764 );
3765
3766 Ok(())
3767 })
3768 }
3769
3770 #[test]
3771 fn test_resolve_rpc_aliases() {
3772 figment::Jail::expect_with(|jail| {
3773 jail.create_file(
3774 "foundry.toml",
3775 r#"
3776 [profile.default]
3777 [etherscan]
3778 arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
3779 [rpc_endpoints]
3780 arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
3781 "#,
3782 )?;
3783
3784 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
3785 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
3786
3787 let config = Config::load().unwrap();
3788
3789 let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
3790 assert!(config.is_err());
3791 assert_eq!(
3792 config.unwrap_err().to_string(),
3793 "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`"
3794 );
3795
3796 Ok(())
3797 });
3798 }
3799
3800 #[test]
3801 fn test_resolve_rpc_config() {
3802 figment::Jail::expect_with(|jail| {
3803 jail.create_file(
3804 "foundry.toml",
3805 r#"
3806 [rpc_endpoints]
3807 optimism = "https://example.com/"
3808 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
3809 "#,
3810 )?;
3811 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3812
3813 let config = Config::load().unwrap();
3814 assert_eq!(
3815 RpcEndpoints::new([
3816 (
3817 "optimism",
3818 RpcEndpointType::String(RpcEndpointUrl::Url(
3819 "https://example.com/".to_string()
3820 ))
3821 ),
3822 (
3823 "mainnet",
3824 RpcEndpointType::Config(RpcEndpoint {
3825 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3826 extra_endpoints: vec![],
3827 config: RpcEndpointConfig {
3828 retries: Some(3),
3829 retry_backoff: Some(1000),
3830 compute_units_per_second: Some(1000),
3831 },
3832 auth: None,
3833 })
3834 ),
3835 ]),
3836 config.rpc_endpoints
3837 );
3838
3839 let resolved = config.rpc_endpoints.resolved();
3840 assert_eq!(
3841 RpcEndpoints::new([
3842 (
3843 "optimism",
3844 RpcEndpointType::String(RpcEndpointUrl::Url(
3845 "https://example.com/".to_string()
3846 ))
3847 ),
3848 (
3849 "mainnet",
3850 RpcEndpointType::Config(RpcEndpoint {
3851 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3852 extra_endpoints: vec![],
3853 config: RpcEndpointConfig {
3854 retries: Some(3),
3855 retry_backoff: Some(1000),
3856 compute_units_per_second: Some(1000),
3857 },
3858 auth: None,
3859 })
3860 ),
3861 ])
3862 .resolved(),
3863 resolved
3864 );
3865 Ok(())
3866 })
3867 }
3868
3869 #[test]
3870 fn test_resolve_auth() {
3871 figment::Jail::expect_with(|jail| {
3872 jail.create_file(
3873 "foundry.toml",
3874 r#"
3875 [profile.default]
3876 eth_rpc_url = "optimism"
3877 [rpc_endpoints]
3878 optimism = "https://example.com/"
3879 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
3880 "#,
3881 )?;
3882
3883 let config = Config::load().unwrap();
3884
3885 jail.set_env("_CONFIG_AUTH", "123456");
3886 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3887
3888 assert_eq!(
3889 RpcEndpoints::new([
3890 (
3891 "optimism",
3892 RpcEndpointType::String(RpcEndpointUrl::Url(
3893 "https://example.com/".to_string()
3894 ))
3895 ),
3896 (
3897 "mainnet",
3898 RpcEndpointType::Config(RpcEndpoint {
3899 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3900 extra_endpoints: vec![],
3901 config: RpcEndpointConfig {
3902 retries: Some(3),
3903 retry_backoff: Some(1000),
3904 compute_units_per_second: Some(1000)
3905 },
3906 auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
3907 })
3908 ),
3909 ]),
3910 config.rpc_endpoints
3911 );
3912 let resolved = config.rpc_endpoints.resolved();
3913 assert_eq!(
3914 RpcEndpoints::new([
3915 (
3916 "optimism",
3917 RpcEndpointType::String(RpcEndpointUrl::Url(
3918 "https://example.com/".to_string()
3919 ))
3920 ),
3921 (
3922 "mainnet",
3923 RpcEndpointType::Config(RpcEndpoint {
3924 endpoint: RpcEndpointUrl::Url(
3925 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3926 ),
3927 extra_endpoints: vec![],
3928 config: RpcEndpointConfig {
3929 retries: Some(3),
3930 retry_backoff: Some(1000),
3931 compute_units_per_second: Some(1000)
3932 },
3933 auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
3934 })
3935 ),
3936 ])
3937 .resolved(),
3938 resolved
3939 );
3940
3941 Ok(())
3942 });
3943 }
3944
3945 #[test]
3946 fn test_resolve_endpoints() {
3947 figment::Jail::expect_with(|jail| {
3948 jail.create_file(
3949 "foundry.toml",
3950 r#"
3951 [profile.default]
3952 eth_rpc_url = "optimism"
3953 [rpc_endpoints]
3954 optimism = "https://example.com/"
3955 mainnet = "${_CONFIG_MAINNET}"
3956 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
3957 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
3958 "#,
3959 )?;
3960
3961 let config = Config::load().unwrap();
3962
3963 assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
3964
3965 assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
3966
3967 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3968 jail.set_env("_CONFIG_API_KEY1", "123456");
3969 jail.set_env("_CONFIG_API_KEY2", "98765");
3970
3971 let endpoints = config.rpc_endpoints.resolved();
3972
3973 assert!(!endpoints.has_unresolved());
3974
3975 assert_eq!(
3976 endpoints,
3977 RpcEndpoints::new([
3978 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3979 (
3980 "mainnet",
3981 RpcEndpointUrl::Url(
3982 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3983 )
3984 ),
3985 (
3986 "mainnet_2",
3987 RpcEndpointUrl::Url(
3988 "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
3989 )
3990 ),
3991 (
3992 "mainnet_3",
3993 RpcEndpointUrl::Url(
3994 "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
3995 )
3996 ),
3997 ])
3998 .resolved()
3999 );
4000
4001 Ok(())
4002 });
4003 }
4004
4005 #[test]
4006 fn test_extract_etherscan_config() {
4007 figment::Jail::expect_with(|jail| {
4008 jail.create_file(
4009 "foundry.toml",
4010 r#"
4011 [profile.default]
4012 etherscan_api_key = "optimism"
4013
4014 [etherscan]
4015 optimism = { key = "https://etherscan-optimism.com/" }
4016 amoy = { key = "https://etherscan-amoy.com/" }
4017 "#,
4018 )?;
4019
4020 let mut config = Config::load().unwrap();
4021
4022 let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
4023 assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
4024
4025 config.etherscan_api_key = Some("amoy".to_string());
4026
4027 let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
4028 assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
4029
4030 Ok(())
4031 });
4032 }
4033
4034 #[test]
4035 fn test_extract_etherscan_config_by_chain() {
4036 figment::Jail::expect_with(|jail| {
4037 jail.create_file(
4038 "foundry.toml",
4039 r#"
4040 [profile.default]
4041
4042 [etherscan]
4043 amoy = { key = "https://etherscan-amoy.com/", chain = 80002 }
4044 "#,
4045 )?;
4046
4047 let config = Config::load().unwrap();
4048
4049 let amoy = config
4050 .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
4051 .unwrap()
4052 .unwrap();
4053 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
4054
4055 Ok(())
4056 });
4057 }
4058
4059 #[test]
4060 fn test_extract_etherscan_config_by_chain_with_url() {
4061 figment::Jail::expect_with(|jail| {
4062 jail.create_file(
4063 "foundry.toml",
4064 r#"
4065 [profile.default]
4066
4067 [etherscan]
4068 amoy = { key = "https://etherscan-amoy.com/", chain = 80002 , url = "https://verifier-url.com/"}
4069 "#,
4070 )?;
4071
4072 let config = Config::load().unwrap();
4073
4074 let amoy = config
4075 .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
4076 .unwrap()
4077 .unwrap();
4078 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
4079 assert_eq!(amoy.api_url, "https://verifier-url.com/".to_string());
4080
4081 Ok(())
4082 });
4083 }
4084
4085 #[test]
4086 fn test_extract_etherscan_config_by_chain_and_alias() {
4087 figment::Jail::expect_with(|jail| {
4088 jail.create_file(
4089 "foundry.toml",
4090 r#"
4091 [profile.default]
4092 eth_rpc_url = "amoy"
4093
4094 [etherscan]
4095 amoy = { key = "https://etherscan-amoy.com/" }
4096
4097 [rpc_endpoints]
4098 amoy = "https://polygon-amoy.g.alchemy.com/v2/amoy"
4099 "#,
4100 )?;
4101
4102 let config = Config::load().unwrap();
4103
4104 let amoy = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
4105 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
4106
4107 let amoy_rpc = config.get_rpc_url().unwrap().unwrap();
4108 assert_eq!(amoy_rpc, "https://polygon-amoy.g.alchemy.com/v2/amoy");
4109 Ok(())
4110 });
4111 }
4112
4113 #[test]
4114 fn test_toml_file() {
4115 figment::Jail::expect_with(|jail| {
4116 jail.create_file(
4117 "foundry.toml",
4118 r#"
4119 [profile.default]
4120 src = "some-source"
4121 out = "some-out"
4122 cache = true
4123 eth_rpc_url = "https://example.com/"
4124 verbosity = 3
4125 remappings = ["ds-test=lib/ds-test/"]
4126 via_ir = true
4127 rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
4128 use_literal_content = false
4129 bytecode_hash = "ipfs"
4130 cbor_metadata = true
4131 revert_strings = "strip"
4132 allow_paths = ["allow", "paths"]
4133 build_info_path = "build-info"
4134 always_use_create_2_factory = true
4135
4136 [rpc_endpoints]
4137 optimism = "https://example.com/"
4138 mainnet = "${RPC_MAINNET}"
4139 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
4140 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
4141 "#,
4142 )?;
4143
4144 let config = Config::load().unwrap();
4145 assert_eq!(
4146 config,
4147 Config {
4148 src: "some-source".into(),
4149 out: "some-out".into(),
4150 cache: true,
4151 eth_rpc_url: Some("https://example.com/".to_string()),
4152 remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
4153 verbosity: 3,
4154 via_ir: true,
4155 rpc_storage_caching: StorageCachingConfig {
4156 chains: CachedChains::Chains(vec![
4157 Chain::mainnet(),
4158 Chain::optimism_mainnet(),
4159 Chain::from_id(999999)
4160 ]),
4161 endpoints: CachedEndpoints::All,
4162 },
4163 use_literal_content: false,
4164 bytecode_hash: BytecodeHash::Ipfs,
4165 cbor_metadata: true,
4166 revert_strings: Some(RevertStrings::Strip),
4167 allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
4168 rpc_endpoints: RpcEndpoints::new([
4169 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
4170 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
4171 (
4172 "mainnet_2",
4173 RpcEndpointUrl::Env(
4174 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
4175 )
4176 ),
4177 (
4178 "mainnet_3",
4179 RpcEndpointUrl::Env(
4180 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
4181 .to_string()
4182 )
4183 ),
4184 ]),
4185 build_info_path: Some("build-info".into()),
4186 always_use_create_2_factory: true,
4187 ..Config::default().normalized_optimizer_settings()
4188 }
4189 );
4190
4191 Ok(())
4192 });
4193 }
4194
4195 #[test]
4196 fn test_load_remappings() {
4197 figment::Jail::expect_with(|jail| {
4198 jail.create_file(
4199 "foundry.toml",
4200 r"
4201 [profile.default]
4202 remappings = ['nested/=lib/nested/']
4203 ",
4204 )?;
4205
4206 let config = Config::load_with_root(jail.directory()).unwrap();
4207 assert_eq!(
4208 config.remappings,
4209 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
4210 );
4211
4212 Ok(())
4213 });
4214 }
4215
4216 #[test]
4217 fn test_load_full_toml() {
4218 figment::Jail::expect_with(|jail| {
4219 jail.create_file(
4220 "foundry.toml",
4221 r#"
4222 [profile.default]
4223 auto_detect_solc = true
4224 block_base_fee_per_gas = 0
4225 block_coinbase = '0x0000000000000000000000000000000000000000'
4226 block_difficulty = 0
4227 block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
4228 block_number = 1
4229 block_timestamp = 1
4230 use_literal_content = false
4231 bytecode_hash = 'ipfs'
4232 cbor_metadata = true
4233 cache = true
4234 cache_path = 'cache'
4235 evm_version = 'london'
4236 extra_output = []
4237 extra_output_files = []
4238 always_use_create_2_factory = false
4239 ffi = false
4240 force = false
4241 gas_limit = 9223372036854775807
4242 gas_price = 0
4243 gas_reports = ['*']
4244 ignored_error_codes = [1878]
4245 ignored_warnings_from = ["something"]
4246 deny = "never"
4247 initial_balance = '0xffffffffffffffffffffffff'
4248 libraries = []
4249 libs = ['lib']
4250 memory_limit = 134217728
4251 names = false
4252 no_storage_caching = false
4253 no_rpc_rate_limit = false
4254 offline = false
4255 optimizer = true
4256 optimizer_runs = 200
4257 out = 'out'
4258 remappings = ['nested/=lib/nested/']
4259 sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
4260 sizes = false
4261 sparse_mode = false
4262 src = 'src'
4263 test = 'test'
4264 tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
4265 verbosity = 0
4266 via_ir = false
4267
4268 [profile.default.rpc_storage_caching]
4269 chains = 'all'
4270 endpoints = 'all'
4271
4272 [rpc_endpoints]
4273 optimism = "https://example.com/"
4274 mainnet = "${RPC_MAINNET}"
4275 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
4276
4277 [fuzz]
4278 runs = 256
4279 seed = '0x3e8'
4280 max_test_rejects = 65536
4281
4282 [invariant]
4283 runs = 256
4284 depth = 500
4285 workers = 1
4286 fail_on_revert = false
4287 call_override = false
4288 shrink_run_limit = 5000
4289 "#,
4290 )?;
4291
4292 let config = Config::load_with_root(jail.directory()).unwrap();
4293
4294 assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
4295 assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
4296 assert_eq!(
4297 config.remappings,
4298 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
4299 );
4300
4301 assert_eq!(
4302 config.rpc_endpoints,
4303 RpcEndpoints::new([
4304 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
4305 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
4306 (
4307 "mainnet_2",
4308 RpcEndpointUrl::Env(
4309 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
4310 )
4311 ),
4312 ]),
4313 );
4314
4315 Ok(())
4316 });
4317 }
4318
4319 #[test]
4320 fn test_solc_req() {
4321 figment::Jail::expect_with(|jail| {
4322 jail.create_file(
4323 "foundry.toml",
4324 r#"
4325 [profile.default]
4326 solc_version = "0.8.12"
4327 "#,
4328 )?;
4329
4330 let config = Config::load().unwrap();
4331 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4332
4333 jail.create_file(
4334 "foundry.toml",
4335 r#"
4336 [profile.default]
4337 solc = "0.8.12"
4338 "#,
4339 )?;
4340
4341 let config = Config::load().unwrap();
4342 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4343
4344 jail.create_file(
4345 "foundry.toml",
4346 r#"
4347 [profile.default]
4348 solc = "path/to/local/solc"
4349 "#,
4350 )?;
4351
4352 let config = Config::load().unwrap();
4353 assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
4354
4355 jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
4356 let config = Config::load().unwrap();
4357 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
4358 Ok(())
4359 });
4360 }
4361
4362 #[test]
4364 fn test_backwards_solc_version() {
4365 figment::Jail::expect_with(|jail| {
4366 jail.create_file(
4367 "foundry.toml",
4368 r#"
4369 [default]
4370 solc = "0.8.12"
4371 solc_version = "0.8.20"
4372 "#,
4373 )?;
4374
4375 let config = Config::load().unwrap();
4376 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4377
4378 Ok(())
4379 });
4380
4381 figment::Jail::expect_with(|jail| {
4382 jail.create_file(
4383 "foundry.toml",
4384 r#"
4385 [default]
4386 solc_version = "0.8.20"
4387 "#,
4388 )?;
4389
4390 let config = Config::load().unwrap();
4391 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
4392
4393 Ok(())
4394 });
4395 }
4396
4397 #[test]
4398 fn test_toml_casing_file() {
4399 figment::Jail::expect_with(|jail| {
4400 jail.create_file(
4401 "foundry.toml",
4402 r#"
4403 [profile.default]
4404 src = "some-source"
4405 out = "some-out"
4406 cache = true
4407 eth-rpc-url = "https://example.com/"
4408 evm-version = "berlin"
4409 auto-detect-solc = false
4410 "#,
4411 )?;
4412
4413 let config = Config::load().unwrap();
4414 assert_eq!(
4415 config,
4416 Config {
4417 src: "some-source".into(),
4418 out: "some-out".into(),
4419 cache: true,
4420 eth_rpc_url: Some("https://example.com/".to_string()),
4421 auto_detect_solc: false,
4422 evm_version: EvmVersion::Berlin,
4423 ..Config::default().normalized_optimizer_settings()
4424 }
4425 );
4426
4427 Ok(())
4428 });
4429 }
4430
4431 #[test]
4432 fn test_output_selection() {
4433 figment::Jail::expect_with(|jail| {
4434 jail.create_file(
4435 "foundry.toml",
4436 r#"
4437 [profile.default]
4438 extra_output = ["metadata", "ir-optimized"]
4439 extra_output_files = ["metadata"]
4440 "#,
4441 )?;
4442
4443 let config = Config::load().unwrap();
4444
4445 assert_eq!(
4446 config.extra_output,
4447 vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
4448 );
4449 assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
4450
4451 Ok(())
4452 });
4453 }
4454
4455 #[test]
4456 fn test_precedence() {
4457 figment::Jail::expect_with(|jail| {
4458 jail.create_file(
4459 "foundry.toml",
4460 r#"
4461 [profile.default]
4462 src = "mysrc"
4463 out = "myout"
4464 verbosity = 3
4465 "#,
4466 )?;
4467
4468 let config = Config::load().unwrap();
4469 assert_eq!(
4470 config,
4471 Config {
4472 src: "mysrc".into(),
4473 out: "myout".into(),
4474 verbosity: 3,
4475 ..Config::default().normalized_optimizer_settings()
4476 }
4477 );
4478
4479 jail.set_env("FOUNDRY_SRC", r"other-src");
4480 let config = Config::load().unwrap();
4481 assert_eq!(
4482 config,
4483 Config {
4484 src: "other-src".into(),
4485 out: "myout".into(),
4486 verbosity: 3,
4487 ..Config::default().normalized_optimizer_settings()
4488 }
4489 );
4490
4491 jail.set_env("FOUNDRY_PROFILE", "foo");
4492 let val: Result<String, _> = Config::figment().extract_inner("profile");
4493 assert!(val.is_err());
4494
4495 Ok(())
4496 });
4497 }
4498
4499 #[test]
4500 fn test_extract_basic() {
4501 figment::Jail::expect_with(|jail| {
4502 jail.create_file(
4503 "foundry.toml",
4504 r#"
4505 [profile.default]
4506 src = "mysrc"
4507 out = "myout"
4508 verbosity = 3
4509 evm_version = 'berlin'
4510
4511 [profile.other]
4512 src = "other-src"
4513 "#,
4514 )?;
4515 let loaded = Config::load().unwrap();
4516 assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4517 let base = loaded.into_basic();
4518 let default = Config::default();
4519 assert_eq!(
4520 base,
4521 BasicConfig {
4522 profile: Config::DEFAULT_PROFILE,
4523 src: "mysrc".into(),
4524 out: "myout".into(),
4525 libs: default.libs.clone(),
4526 remappings: default.remappings.clone(),
4527 network: None,
4528 }
4529 );
4530 jail.set_env("FOUNDRY_PROFILE", r"other");
4531 let base = Config::figment().extract::<BasicConfig>().unwrap();
4532 assert_eq!(
4533 base,
4534 BasicConfig {
4535 profile: Config::DEFAULT_PROFILE,
4536 src: "other-src".into(),
4537 out: "myout".into(),
4538 libs: default.libs.clone(),
4539 remappings: default.remappings,
4540 network: None,
4541 }
4542 );
4543 Ok(())
4544 });
4545 }
4546
4547 #[test]
4548 #[should_panic]
4549 fn test_parse_invalid_fuzz_weight() {
4550 figment::Jail::expect_with(|jail| {
4551 jail.create_file(
4552 "foundry.toml",
4553 r"
4554 [fuzz]
4555 dictionary_weight = 101
4556 ",
4557 )?;
4558 let _config = Config::load().unwrap();
4559 Ok(())
4560 });
4561 }
4562
4563 #[test]
4564 fn test_fallback_provider() {
4565 figment::Jail::expect_with(|jail| {
4566 jail.create_file(
4567 "foundry.toml",
4568 r"
4569 [fuzz]
4570 runs = 1
4571 include_storage = false
4572 dictionary_weight = 99
4573
4574 [invariant]
4575 runs = 420
4576
4577 [profile.ci.fuzz]
4578 dictionary_weight = 5
4579
4580 [profile.ci.invariant]
4581 runs = 400
4582 ",
4583 )?;
4584
4585 let invariant_default = InvariantConfig::default();
4586 let config = Config::load().unwrap();
4587
4588 assert_ne!(config.invariant.runs, config.fuzz.runs);
4589 assert_eq!(config.invariant.runs, 420);
4590
4591 assert_ne!(
4592 config.fuzz.dictionary.include_storage,
4593 invariant_default.dictionary.include_storage
4594 );
4595 assert_eq!(
4596 config.invariant.dictionary.include_storage,
4597 config.fuzz.dictionary.include_storage
4598 );
4599
4600 assert_ne!(
4601 config.fuzz.dictionary.dictionary_weight,
4602 invariant_default.dictionary.dictionary_weight
4603 );
4604 assert_eq!(
4605 config.invariant.dictionary.dictionary_weight,
4606 config.fuzz.dictionary.dictionary_weight
4607 );
4608
4609 jail.set_env("FOUNDRY_PROFILE", "ci");
4610 let ci_config = Config::load().unwrap();
4611 assert_eq!(ci_config.fuzz.runs, 1);
4612 assert_eq!(ci_config.invariant.runs, 400);
4613 assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4614 assert_eq!(
4615 ci_config.invariant.dictionary.dictionary_weight,
4616 config.fuzz.dictionary.dictionary_weight
4617 );
4618
4619 Ok(())
4620 })
4621 }
4622
4623 #[test]
4624 fn test_standalone_profile_sections() {
4625 figment::Jail::expect_with(|jail| {
4626 jail.create_file(
4627 "foundry.toml",
4628 r#"
4629 [fuzz]
4630 runs = 100
4631
4632 [invariant]
4633 runs = 120
4634
4635 [symbolic]
4636 enabled = true
4637 max_paths = 12
4638 storage_layout = "generic"
4639
4640 [profile.ci.fuzz]
4641 runs = 420
4642
4643 [profile.ci.invariant]
4644 runs = 500
4645
4646 [profile.ci.symbolic]
4647 max_paths = 34
4648 dump_smt = true
4649 "#,
4650 )?;
4651
4652 let config = Config::load().unwrap();
4653 assert_eq!(config.fuzz.runs, 100);
4654 assert_eq!(config.invariant.runs, 120);
4655 assert!(config.symbolic.enabled);
4656 assert_eq!(config.symbolic.max_paths, 12);
4657 assert_eq!(config.symbolic.storage_layout, SymbolicStorageLayout::Generic);
4658 assert!(!config.symbolic.dump_smt);
4659
4660 jail.set_env("FOUNDRY_PROFILE", "ci");
4661 let config = Config::load().unwrap();
4662 assert_eq!(config.fuzz.runs, 420);
4663 assert_eq!(config.invariant.runs, 500);
4664 assert!(config.symbolic.enabled);
4665 assert_eq!(config.symbolic.max_paths, 34);
4666 assert_eq!(config.symbolic.storage_layout, SymbolicStorageLayout::Generic);
4667 assert!(config.symbolic.dump_smt);
4668
4669 Ok(())
4670 });
4671 }
4672
4673 #[test]
4674 fn can_handle_deviating_dapp_aliases() {
4675 figment::Jail::expect_with(|jail| {
4676 let addr = Address::ZERO;
4677 jail.set_env("DAPP_TEST_NUMBER", 1337);
4678 jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4679 jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4680 jail.set_env("DAPP_TEST_DEPTH", 20);
4681 jail.set_env("DAPP_FORK_BLOCK", 100);
4682 jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4683 jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4684
4685 let config = Config::load().unwrap();
4686
4687 assert_eq!(config.block_number, U256::from(1337));
4688 assert_eq!(config.sender, addr);
4689 assert_eq!(config.fuzz.runs, 420);
4690 assert_eq!(config.invariant.depth, 20);
4691 assert_eq!(config.fork_block_number, Some(100));
4692 assert_eq!(config.optimizer_runs, Some(999));
4693 assert!(!config.optimizer.unwrap());
4694
4695 Ok(())
4696 });
4697 }
4698
4699 #[test]
4700 fn can_parse_libraries() {
4701 figment::Jail::expect_with(|jail| {
4702 jail.set_env(
4703 "DAPP_LIBRARIES",
4704 "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
4705 );
4706 let config = Config::load().unwrap();
4707 assert_eq!(
4708 config.libraries,
4709 vec![
4710 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4711 .to_string()
4712 ]
4713 );
4714
4715 jail.set_env(
4716 "DAPP_LIBRARIES",
4717 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4718 );
4719 let config = Config::load().unwrap();
4720 assert_eq!(
4721 config.libraries,
4722 vec![
4723 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4724 .to_string(),
4725 ]
4726 );
4727
4728 jail.set_env(
4729 "DAPP_LIBRARIES",
4730 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4731 );
4732 let config = Config::load().unwrap();
4733 assert_eq!(
4734 config.libraries,
4735 vec![
4736 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4737 .to_string(),
4738 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4739 .to_string()
4740 ]
4741 );
4742
4743 Ok(())
4744 });
4745 }
4746
4747 #[test]
4748 fn test_parse_many_libraries() {
4749 figment::Jail::expect_with(|jail| {
4750 jail.create_file(
4751 "foundry.toml",
4752 r"
4753 [profile.default]
4754 libraries= [
4755 './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4756 './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4757 './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4758 './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4759 './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4760 ]
4761 ",
4762 )?;
4763 let config = Config::load().unwrap();
4764
4765 let libs = config.parsed_libraries().unwrap().libs;
4766
4767 similar_asserts::assert_eq!(
4768 libs,
4769 BTreeMap::from([
4770 (
4771 PathBuf::from("./src/SizeAuctionDiscount.sol"),
4772 BTreeMap::from([
4773 (
4774 "Chainlink".to_string(),
4775 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4776 ),
4777 (
4778 "Math".to_string(),
4779 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4780 )
4781 ])
4782 ),
4783 (
4784 PathBuf::from("./src/SizeAuction.sol"),
4785 BTreeMap::from([
4786 (
4787 "ChainlinkTWAP".to_string(),
4788 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4789 ),
4790 (
4791 "Math".to_string(),
4792 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4793 )
4794 ])
4795 ),
4796 (
4797 PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
4798 BTreeMap::from([(
4799 "ChainlinkTWAP".to_string(),
4800 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4801 )])
4802 ),
4803 ])
4804 );
4805
4806 Ok(())
4807 });
4808 }
4809
4810 #[test]
4811 fn config_roundtrip() {
4812 figment::Jail::expect_with(|jail| {
4813 let default = Config::default().normalized_optimizer_settings();
4814 let basic = default.clone().into_basic();
4815 jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
4816
4817 let mut other = Config::load().unwrap();
4818 clear_warning(&mut other);
4819 assert_eq!(default, other);
4820
4821 let other = other.into_basic();
4822 assert_eq!(basic, other);
4823
4824 jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
4825 let mut other = Config::load().unwrap();
4826 clear_warning(&mut other);
4827 let mut serialized_default = default;
4828 mark_serialized_invariant_provenance(&mut serialized_default);
4829 assert_eq!(serialized_default, other);
4830
4831 Ok(())
4832 });
4833 }
4834
4835 #[test]
4836 fn test_fs_permissions() {
4837 figment::Jail::expect_with(|jail| {
4838 jail.create_file(
4839 "foundry.toml",
4840 r#"
4841 [profile.default]
4842 fs_permissions = [{ access = "read-write", path = "./"}]
4843 "#,
4844 )?;
4845 let loaded = Config::load().unwrap();
4846
4847 assert_eq!(
4848 loaded.fs_permissions,
4849 FsPermissions::new(vec![PathPermission::read_write("./")])
4850 );
4851
4852 jail.create_file(
4853 "foundry.toml",
4854 r#"
4855 [profile.default]
4856 fs_permissions = [{ access = "none", path = "./"}]
4857 "#,
4858 )?;
4859 let loaded = Config::load().unwrap();
4860 assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
4861
4862 Ok(())
4863 });
4864 }
4865
4866 #[test]
4867 fn test_optimizer_settings_basic() {
4868 figment::Jail::expect_with(|jail| {
4869 jail.create_file(
4870 "foundry.toml",
4871 r"
4872 [profile.default]
4873 optimizer = true
4874
4875 [profile.default.optimizer_details]
4876 yul = false
4877
4878 [profile.default.optimizer_details.yulDetails]
4879 stackAllocation = true
4880 ",
4881 )?;
4882 let mut loaded = Config::load().unwrap();
4883 clear_warning(&mut loaded);
4884 assert_eq!(
4885 loaded.optimizer_details,
4886 Some(OptimizerDetails {
4887 yul: Some(false),
4888 yul_details: Some(YulDetails {
4889 stack_allocation: Some(true),
4890 ..Default::default()
4891 }),
4892 ..Default::default()
4893 })
4894 );
4895
4896 let s = loaded.to_string_pretty().unwrap();
4897 jail.create_file("foundry.toml", &s)?;
4898 mark_serialized_invariant_provenance(&mut loaded);
4899
4900 let mut reloaded = Config::load().unwrap();
4901 clear_warning(&mut reloaded);
4902 assert_eq!(loaded, reloaded);
4903
4904 Ok(())
4905 });
4906 }
4907
4908 #[test]
4909 fn test_model_checker_settings_basic() {
4910 figment::Jail::expect_with(|jail| {
4911 jail.create_file(
4912 "foundry.toml",
4913 r"
4914 [profile.default]
4915
4916 [profile.default.model_checker]
4917 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4918 engine = 'chc'
4919 targets = [ 'assert', 'outOfBounds' ]
4920 timeout = 10000
4921 ",
4922 )?;
4923 let mut loaded = Config::load().unwrap();
4924 clear_warning(&mut loaded);
4925 assert_eq!(
4926 loaded.model_checker,
4927 Some(ModelCheckerSettings {
4928 contracts: BTreeMap::from([
4929 ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
4930 ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
4931 ]),
4932 engine: Some(ModelCheckerEngine::CHC),
4933 targets: Some(vec![
4934 ModelCheckerTarget::Assert,
4935 ModelCheckerTarget::OutOfBounds
4936 ]),
4937 timeout: Some(10000),
4938 invariants: None,
4939 show_unproved: None,
4940 div_mod_with_slacks: None,
4941 solvers: None,
4942 show_unsupported: None,
4943 show_proved_safe: None,
4944 })
4945 );
4946
4947 let s = loaded.to_string_pretty().unwrap();
4948 jail.create_file("foundry.toml", &s)?;
4949 mark_serialized_invariant_provenance(&mut loaded);
4950
4951 let mut reloaded = Config::load().unwrap();
4952 clear_warning(&mut reloaded);
4953 assert_eq!(loaded, reloaded);
4954
4955 Ok(())
4956 });
4957 }
4958
4959 #[test]
4960 fn test_model_checker_settings_with_bool_flags() {
4961 figment::Jail::expect_with(|jail| {
4962 jail.create_file(
4963 "foundry.toml",
4964 r"
4965 [profile.default]
4966
4967 [profile.default.model_checker]
4968 engine = 'chc'
4969 show_unproved = true
4970 show_unsupported = true
4971 show_proved_safe = false
4972 div_mod_with_slacks = true
4973 ",
4974 )?;
4975 let mut loaded = Config::load().unwrap();
4976 clear_warning(&mut loaded);
4977
4978 let mc = loaded.model_checker.as_ref().unwrap();
4979 assert_eq!(mc.show_unproved, Some(true));
4980 assert_eq!(mc.show_unsupported, Some(true));
4981 assert_eq!(mc.show_proved_safe, Some(false));
4982 assert_eq!(mc.div_mod_with_slacks, Some(true));
4983
4984 let s = loaded.to_string_pretty().unwrap();
4986 jail.create_file("foundry.toml", &s)?;
4987
4988 let mut reloaded = Config::load().unwrap();
4989 clear_warning(&mut reloaded);
4990
4991 let mc_reloaded = reloaded.model_checker.as_ref().unwrap();
4992 assert_eq!(mc_reloaded.show_unproved, Some(true));
4993 assert_eq!(mc_reloaded.show_unsupported, Some(true));
4994 assert_eq!(mc_reloaded.show_proved_safe, Some(false));
4995 assert_eq!(mc_reloaded.div_mod_with_slacks, Some(true));
4996
4997 Ok(())
4998 });
4999 }
5000
5001 #[test]
5002 fn test_model_checker_settings_relative_paths() {
5003 figment::Jail::expect_with(|jail| {
5004 jail.create_file(
5005 "foundry.toml",
5006 r"
5007 [profile.default]
5008
5009 [profile.default.model_checker]
5010 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
5011 engine = 'chc'
5012 targets = [ 'assert', 'outOfBounds' ]
5013 timeout = 10000
5014 ",
5015 )?;
5016 let loaded = Config::load().unwrap().sanitized();
5017
5018 let dir = foundry_compilers::utils::canonicalize(jail.directory())
5023 .expect("Could not canonicalize jail path");
5024 assert_eq!(
5025 loaded.model_checker,
5026 Some(ModelCheckerSettings {
5027 contracts: BTreeMap::from([
5028 (
5029 format!("{}", dir.join("a.sol").display()),
5030 vec!["A1".to_string(), "A2".to_string()]
5031 ),
5032 (
5033 format!("{}", dir.join("b.sol").display()),
5034 vec!["B1".to_string(), "B2".to_string()]
5035 ),
5036 ]),
5037 engine: Some(ModelCheckerEngine::CHC),
5038 targets: Some(vec![
5039 ModelCheckerTarget::Assert,
5040 ModelCheckerTarget::OutOfBounds
5041 ]),
5042 timeout: Some(10000),
5043 invariants: None,
5044 show_unproved: None,
5045 div_mod_with_slacks: None,
5046 solvers: None,
5047 show_unsupported: None,
5048 show_proved_safe: None,
5049 })
5050 );
5051
5052 Ok(())
5053 });
5054 }
5055
5056 #[test]
5057 fn test_fmt_config() {
5058 figment::Jail::expect_with(|jail| {
5059 jail.create_file(
5060 "foundry.toml",
5061 r#"
5062 [fmt]
5063 line_length = 100
5064 tab_width = 2
5065 bracket_spacing = true
5066 style = "space"
5067 "#,
5068 )?;
5069 let loaded = Config::load().unwrap().sanitized();
5070 assert_eq!(
5071 loaded.fmt,
5072 FormatterConfig {
5073 line_length: 100,
5074 tab_width: 2,
5075 bracket_spacing: true,
5076 style: IndentStyle::Space,
5077 ..Default::default()
5078 }
5079 );
5080
5081 Ok(())
5082 });
5083 }
5084
5085 #[test]
5086 fn test_lint_config() {
5087 figment::Jail::expect_with(|jail| {
5088 jail.create_file(
5089 "foundry.toml",
5090 r"
5091 [lint]
5092 severity = ['high', 'medium']
5093 exclude_lints = ['incorrect-shift']
5094 ",
5095 )?;
5096 let loaded = Config::load().unwrap().sanitized();
5097 assert_eq!(
5098 loaded.lint,
5099 LinterConfig {
5100 severity: vec![LintSeverity::High, LintSeverity::Med],
5101 exclude_lints: vec!["incorrect-shift".into()],
5102 ..Default::default()
5103 }
5104 );
5105
5106 Ok(())
5107 });
5108 }
5109
5110 #[test]
5111 fn test_invariant_config() {
5112 figment::Jail::expect_with(|jail| {
5113 jail.create_file(
5114 "foundry.toml",
5115 r#"
5116 [invariant]
5117 runs = 512
5118 depth = 10
5119 min_depth = 2
5120 depth_mode = "random"
5121 workers = 4
5122 corpus_random_sequence_weight = 30
5123 payable_value_weight = 12
5124 mutation_weight_cmp = 7
5125 "#,
5126 )?;
5127
5128 let loaded = Config::load().unwrap().sanitized();
5129 assert_eq!(
5130 loaded.invariant,
5131 InvariantConfig {
5132 runs: 512,
5133 depth: 10,
5134 min_depth: 2,
5135 depth_mode: InvariantDepthMode::Random,
5136 workers: InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap()),
5137 corpus: FuzzCorpusConfig {
5138 corpus_random_sequence_weight: 30,
5139 payable_value_weight: 12,
5140 mutation_weights: FuzzCorpusMutationWeights {
5141 mutation_weight_cmp: 7,
5142 ..Default::default()
5143 },
5144 ..Default::default()
5145 },
5146 corpus_random_sequence_weight_configured: true,
5147 workers_configured: true,
5148 failure_persist_dir: Some(PathBuf::from("cache/invariant")),
5149 ..Default::default()
5150 }
5151 );
5152 assert!(loaded.invariant.corpus_random_sequence_weight_configured);
5153
5154 Ok(())
5155 });
5156 }
5157
5158 #[test]
5159 fn test_invariant_corpus_random_sequence_weight_provenance() {
5160 figment::Jail::expect_with(|jail| {
5161 jail.create_file(
5162 "foundry.toml",
5163 r#"
5164 [invariant]
5165 depth = 10
5166 "#,
5167 )?;
5168
5169 let loaded = Config::load().unwrap();
5170 assert_eq!(
5171 loaded.invariant.corpus.corpus_random_sequence_weight,
5172 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
5173 );
5174 assert!(!loaded.invariant.corpus_random_sequence_weight_configured);
5175 assert!(!loaded.invariant.workers_configured);
5176
5177 jail.create_file(
5178 "foundry.toml",
5179 r#"
5180 [invariant]
5181 corpus_random_sequence_weight = 10
5182 workers = 4
5183 "#,
5184 )?;
5185
5186 let loaded = Config::load().unwrap();
5187 assert_eq!(
5188 loaded.invariant.corpus.corpus_random_sequence_weight,
5189 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
5190 );
5191 assert!(loaded.invariant.corpus_random_sequence_weight_configured);
5192 assert_eq!(
5193 loaded.invariant.workers,
5194 InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap())
5195 );
5196 assert!(loaded.invariant.workers_configured);
5197
5198 Ok(())
5199 });
5200 }
5201
5202 #[test]
5203 fn test_fuzz_corpus_random_sequence_weight_fallback_does_not_mark_invariant_configured() {
5204 figment::Jail::expect_with(|jail| {
5205 jail.create_file(
5206 "foundry.toml",
5207 r#"
5208 [fuzz]
5209 corpus_random_sequence_weight = 10
5210 "#,
5211 )?;
5212
5213 let loaded = Config::load().unwrap();
5214 assert_eq!(
5215 loaded.invariant.corpus.corpus_random_sequence_weight,
5216 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
5217 );
5218 assert!(!loaded.invariant.corpus_random_sequence_weight_configured);
5219
5220 Ok(())
5221 });
5222 }
5223
5224 #[test]
5225 fn test_standalone_sections_env() {
5226 figment::Jail::expect_with(|jail| {
5227 jail.create_file(
5228 "foundry.toml",
5229 r"
5230 [fuzz]
5231 runs = 100
5232
5233 [invariant]
5234 depth = 1
5235 ",
5236 )?;
5237
5238 jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
5239 jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
5240 jail.set_env("FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_VALUES", "max");
5241 jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
5242 jail.set_env("FOUNDRY_INVARIANT_MIN_DEPTH", "2");
5243 jail.set_env("FOUNDRY_INVARIANT_DEPTH_MODE", "random");
5244 jail.set_env("FOUNDRY_INVARIANT_WORKERS", "3");
5245 jail.set_env("FOUNDRY_INVARIANT_CORPUS_RANDOM_SEQUENCE_WEIGHT", "30");
5246 jail.set_env("FOUNDRY_INVARIANT_PAYABLE_VALUE_WEIGHT", "12");
5247 jail.set_env("FOUNDRY_INVARIANT_MUTATION_WEIGHT_CMP", "7");
5248 jail.set_env("FOUNDRY_SYMBOLIC_MAX_PATHS", "64");
5249 jail.set_env("FOUNDRY_SYMBOLIC_DUMP_SMT", "true");
5250
5251 let config = Config::load().unwrap();
5252 assert_eq!(config.fmt.line_length, 95);
5253 assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
5254 assert_eq!(config.fuzz.dictionary.max_fuzz_dictionary_values, usize::MAX);
5255 assert_eq!(config.invariant.depth, 5);
5256 assert_eq!(config.invariant.min_depth, 2);
5257 assert_eq!(config.invariant.depth_mode, InvariantDepthMode::Random);
5258 assert_eq!(
5259 config.invariant.workers,
5260 InvariantWorkers::Fixed(NonZeroUsize::new(3).unwrap())
5261 );
5262 assert_eq!(config.invariant.corpus.corpus_random_sequence_weight, 30);
5263 assert!(config.invariant.corpus_random_sequence_weight_configured);
5264 assert_eq!(config.invariant.corpus.payable_value_weight, 12);
5265 assert_eq!(config.invariant.corpus.mutation_weights.mutation_weight_cmp, 7);
5266 assert_eq!(config.symbolic.max_paths, 64);
5267 assert!(config.symbolic.dump_smt);
5268
5269 Ok(())
5270 });
5271 }
5272
5273 #[test]
5274 fn test_invariant_workers_env_accepts_auto() {
5275 figment::Jail::expect_with(|jail| {
5276 jail.create_file(
5277 "foundry.toml",
5278 r"
5279 [invariant]
5280 workers = 3
5281 ",
5282 )?;
5283
5284 jail.set_env("FOUNDRY_INVARIANT_WORKERS", "auto");
5285
5286 let config = Config::load().unwrap();
5287 assert_eq!(config.invariant.workers, InvariantWorkers::Auto);
5288
5289 Ok(())
5290 });
5291 }
5292
5293 #[test]
5294 fn test_parse_with_profile() {
5295 let foundry_str = r"
5296 [profile.default]
5297 src = 'src'
5298 out = 'out'
5299 libs = ['lib']
5300
5301 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
5302 ";
5303 assert_eq!(
5304 parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
5305 (
5306 Config::DEFAULT_PROFILE,
5307 BasicConfig {
5308 profile: Config::DEFAULT_PROFILE,
5309 src: "src".into(),
5310 out: "out".into(),
5311 libs: vec!["lib".into()],
5312 remappings: vec![],
5313 network: None,
5314 }
5315 )
5316 );
5317 }
5318
5319 #[test]
5320 fn test_implicit_profile_loads() {
5321 figment::Jail::expect_with(|jail| {
5322 jail.create_file(
5323 "foundry.toml",
5324 r"
5325 [default]
5326 src = 'my-src'
5327 out = 'my-out'
5328 ",
5329 )?;
5330 let loaded = Config::load().unwrap().sanitized();
5331 assert_eq!(loaded.src.file_name().unwrap(), "my-src");
5332 assert_eq!(loaded.out.file_name().unwrap(), "my-out");
5333 assert_eq!(
5334 loaded.warnings,
5335 vec![Warning::UnknownSection {
5336 unknown_section: Profile::new("default"),
5337 source: Some("foundry.toml".into())
5338 }]
5339 );
5340
5341 Ok(())
5342 });
5343 }
5344
5345 #[test]
5346 fn hardfork_overrides_spec_id() {
5347 let config = Config {
5348 hardfork: Some(FoundryHardfork::Tempo(TempoHardfork::T3)),
5349 ..Config::default()
5350 };
5351
5352 assert_eq!(config.evm_spec_id::<TempoHardfork>(), TempoHardfork::T3);
5353 }
5354
5355 #[test]
5356 fn solc_settings_include_experimental_setting() {
5357 let config = Config { experimental: true, ..Config::default() };
5358 let settings = config.solc_settings().unwrap();
5359 assert_eq!(settings.settings.experimental, Some(true));
5360 assert!(settings.cli_settings.extra_args.is_empty());
5361
5362 let config = Config {
5363 experimental: false,
5364 extra_args: vec!["--experimental".to_string()],
5365 ..Config::default()
5366 };
5367 let settings = config.solc_settings().unwrap();
5368 assert_eq!(settings.settings.experimental, Some(false));
5369 assert_eq!(settings.cli_settings.extra_args, vec!["--experimental".to_string()]);
5370 }
5371
5372 #[test]
5373 fn solc_settings_include_via_ssa_cfg_setting() {
5374 let config = Config { via_ssa_cfg: true, ..Config::default() };
5375 let settings = config.solc_settings().unwrap();
5376 assert_eq!(settings.settings.via_ssa_cfg, Some(true));
5377 assert!(settings.cli_settings.extra_args.is_empty());
5378
5379 let config = Config {
5380 via_ssa_cfg: false,
5381 extra_args: vec!["--via-ssa-cfg".to_string()],
5382 ..Config::default()
5383 };
5384 let settings = config.solc_settings().unwrap();
5385 assert_eq!(settings.settings.via_ssa_cfg, Some(false));
5386 assert_eq!(settings.cli_settings.extra_args, vec!["--via-ssa-cfg".to_string()]);
5387 }
5388
5389 #[test]
5390 fn tempo_network_defaults_to_latest_tempo_hardfork() {
5391 figment::Jail::expect_with(|jail| {
5392 jail.create_file(
5393 "foundry.toml",
5394 r#"
5395 [profile.default]
5396 network = "tempo"
5397 "#,
5398 )?;
5399
5400 let config = Config::load().unwrap();
5401 assert!(config.networks.is_tempo());
5402 assert_eq!(config.evm_spec_id::<TempoHardfork>(), latest_active_tempo_hardfork());
5403
5404 Ok(())
5405 });
5406 }
5407
5408 #[test]
5409 fn tempo_hardfork_infers_tempo_network() {
5410 figment::Jail::expect_with(|jail| {
5411 jail.create_file(
5412 "foundry.toml",
5413 r#"
5414 [profile.default]
5415 hardfork = "tempo:T3"
5416 "#,
5417 )?;
5418
5419 let config = Config::load().unwrap();
5420 assert_eq!(config.hardfork, Some(FoundryHardfork::Tempo(TempoHardfork::T3)));
5421 assert!(config.networks.is_tempo());
5422
5423 Ok(())
5424 });
5425 }
5426
5427 #[test]
5428 fn hardfork_rejects_conflicting_network() {
5429 figment::Jail::expect_with(|jail| {
5430 jail.create_file(
5431 "foundry.toml",
5432 r#"
5433 [profile.default]
5434 tempo = true
5435 hardfork = "shanghai"
5436 "#,
5437 )?;
5438
5439 let err = Config::load().unwrap_err();
5440 assert!(
5441 err.to_string()
5442 .to_lowercase()
5443 .contains("hardfork `shanghai` conflicts with network config `tempo`")
5444 );
5445
5446 Ok(())
5447 });
5448 }
5449
5450 #[test]
5451 fn test_etherscan_api_key() {
5452 figment::Jail::expect_with(|jail| {
5453 jail.create_file(
5454 "foundry.toml",
5455 r"
5456 [default]
5457 ",
5458 )?;
5459 jail.set_env("ETHERSCAN_API_KEY", "");
5460 let loaded = Config::load().unwrap().sanitized();
5461 assert!(loaded.etherscan_api_key.is_none());
5462
5463 jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
5464 let loaded = Config::load().unwrap().sanitized();
5465 assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
5466
5467 Ok(())
5468 });
5469 }
5470
5471 #[test]
5472 fn test_etherscan_api_key_figment() {
5473 figment::Jail::expect_with(|jail| {
5474 jail.create_file(
5475 "foundry.toml",
5476 r"
5477 [default]
5478 etherscan_api_key = 'DUMMY'
5479 ",
5480 )?;
5481 jail.set_env("ETHERSCAN_API_KEY", "ETHER");
5482
5483 let figment = Config::figment_with_root(jail.directory())
5484 .merge(("etherscan_api_key", "USER_KEY"));
5485
5486 let loaded = Config::from_provider(figment).unwrap();
5487 assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
5488
5489 Ok(())
5490 });
5491 }
5492
5493 #[test]
5494 fn test_normalize_defaults() {
5495 figment::Jail::expect_with(|jail| {
5496 jail.create_file(
5497 "foundry.toml",
5498 r"
5499 [default]
5500 solc = '0.8.13'
5501 ",
5502 )?;
5503
5504 let loaded = Config::load().unwrap().sanitized();
5505 assert_eq!(loaded.evm_version, EvmVersion::London);
5506 Ok(())
5507 });
5508 }
5509
5510 #[expect(clippy::disallowed_macros)]
5512 #[test]
5513 #[ignore]
5514 fn print_config() {
5515 let config = Config {
5516 optimizer_details: Some(OptimizerDetails {
5517 peephole: None,
5518 inliner: None,
5519 jumpdest_remover: None,
5520 order_literals: None,
5521 deduplicate: None,
5522 cse: None,
5523 constant_optimizer: Some(true),
5524 yul: Some(true),
5525 yul_details: Some(YulDetails {
5526 stack_allocation: None,
5527 optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
5528 }),
5529 simple_counter_for_loop_unchecked_increment: None,
5530 }),
5531 ..Default::default()
5532 };
5533 println!("{}", config.to_string_pretty().unwrap());
5534 }
5535
5536 #[test]
5537 fn can_use_impl_figment_macro() {
5538 #[derive(Default, Serialize)]
5539 struct MyArgs {
5540 #[serde(skip_serializing_if = "Option::is_none")]
5541 root: Option<PathBuf>,
5542 }
5543 impl_figment_convert!(MyArgs);
5544
5545 impl Provider for MyArgs {
5546 fn metadata(&self) -> Metadata {
5547 Metadata::default()
5548 }
5549
5550 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
5551 let value = Value::serialize(self)?;
5552 let error = InvalidType(value.to_actual(), "map".into());
5553 let dict = value.into_dict().ok_or(error)?;
5554 Ok(Map::from([(Config::selected_profile(), dict)]))
5555 }
5556 }
5557
5558 let _figment: Figment = From::from(&MyArgs::default());
5559
5560 #[derive(Default)]
5561 struct Outer {
5562 start: MyArgs,
5563 other: MyArgs,
5564 another: MyArgs,
5565 }
5566 impl_figment_convert!(Outer, start, other, another);
5567
5568 let _figment: Figment = From::from(&Outer::default());
5569 }
5570
5571 #[test]
5572 fn list_cached_blocks() -> eyre::Result<()> {
5573 fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
5574 let block_path = chain_path.join(block_number);
5575 fs::create_dir(block_path.as_path()).unwrap();
5576 let file_path = block_path.join("storage.json");
5577 let mut file = File::create(file_path).unwrap();
5578 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
5579 }
5580
5581 fn fake_block_cache_block_path_as_file(
5582 chain_path: &Path,
5583 block_number: &str,
5584 size_bytes: usize,
5585 ) {
5586 let block_path = chain_path.join(block_number);
5587 let mut file = File::create(block_path).unwrap();
5588 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
5589 }
5590
5591 let chain_dir = tempdir()?;
5592
5593 fake_block_cache(chain_dir.path(), "1", 100);
5594 fake_block_cache(chain_dir.path(), "2", 500);
5595 fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
5596 let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
5598 writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
5599
5600 let result = Config::get_cached_blocks(chain_dir.path())?;
5601
5602 assert_eq!(result.len(), 3);
5603 let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
5604 let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
5605 let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
5606
5607 assert_eq!(block1.0, "1");
5608 assert_eq!(block1.1, 100);
5609 assert_eq!(block2.0, "2");
5610 assert_eq!(block2.1, 500);
5611 assert_eq!(block3.0, "3");
5612 assert_eq!(block3.1, 900);
5613
5614 chain_dir.close()?;
5615 Ok(())
5616 }
5617
5618 #[test]
5619 fn list_etherscan_cache() -> eyre::Result<()> {
5620 fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
5621 let metadata_path = chain_path.join("sources");
5622 let abi_path = chain_path.join("abi");
5623 let _ = fs::create_dir(metadata_path.as_path());
5624 let _ = fs::create_dir(abi_path.as_path());
5625
5626 let metadata_file_path = metadata_path.join(address);
5627 let mut metadata_file = File::create(metadata_file_path).unwrap();
5628 writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
5629 .unwrap();
5630
5631 let abi_file_path = abi_path.join(address);
5632 let mut abi_file = File::create(abi_file_path).unwrap();
5633 writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
5634 .unwrap();
5635 }
5636
5637 let chain_dir = tempdir()?;
5638
5639 fake_etherscan_cache(chain_dir.path(), "1", 100);
5640 fake_etherscan_cache(chain_dir.path(), "2", 500);
5641
5642 let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
5643
5644 assert_eq!(result, 600);
5645
5646 chain_dir.close()?;
5647 Ok(())
5648 }
5649
5650 #[test]
5651 fn test_parse_error_codes() {
5652 figment::Jail::expect_with(|jail| {
5653 jail.create_file(
5654 "foundry.toml",
5655 r#"
5656 [default]
5657 ignored_error_codes = ["license", "unreachable", 1337]
5658 "#,
5659 )?;
5660
5661 let config = Config::load().unwrap();
5662 assert_eq!(
5663 config.ignored_error_codes,
5664 vec![
5665 SolidityErrorCode::SpdxLicenseNotProvided,
5666 SolidityErrorCode::Unreachable,
5667 SolidityErrorCode::Other(1337)
5668 ]
5669 );
5670
5671 Ok(())
5672 });
5673 }
5674
5675 #[test]
5676 fn test_parse_file_paths() {
5677 figment::Jail::expect_with(|jail| {
5678 jail.create_file(
5679 "foundry.toml",
5680 r#"
5681 [default]
5682 ignored_warnings_from = ["something"]
5683 "#,
5684 )?;
5685
5686 let config = Config::load().unwrap();
5687 assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
5688
5689 Ok(())
5690 });
5691 }
5692
5693 #[test]
5694 fn test_parse_optimizer_settings() {
5695 figment::Jail::expect_with(|jail| {
5696 jail.create_file(
5697 "foundry.toml",
5698 r"
5699 [default]
5700 [profile.default.optimizer_details]
5701 ",
5702 )?;
5703
5704 let config = Config::load().unwrap();
5705 assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
5706
5707 Ok(())
5708 });
5709 }
5710
5711 #[test]
5712 fn test_parse_labels() {
5713 figment::Jail::expect_with(|jail| {
5714 jail.create_file(
5715 "foundry.toml",
5716 r#"
5717 [labels]
5718 0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
5719 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
5720 "#,
5721 )?;
5722
5723 let config = Config::load().unwrap();
5724 assert_eq!(
5725 config.labels,
5726 AddressHashMap::from_iter(vec![
5727 (
5728 address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
5729 "Uniswap V3: Factory".to_string()
5730 ),
5731 (
5732 address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
5733 "Uniswap V3: Positions NFT".to_string()
5734 ),
5735 ])
5736 );
5737
5738 Ok(())
5739 });
5740 }
5741
5742 #[test]
5743 fn test_parse_vyper() {
5744 figment::Jail::expect_with(|jail| {
5745 jail.create_file(
5746 "foundry.toml",
5747 r#"
5748 [vyper]
5749 optimize = "O1"
5750 path = "/path/to/vyper"
5751 experimental_codegen = true
5752 venom_experimental = false
5753 debug = true
5754 enable_decimals = true
5755
5756 [vyper.venom]
5757 disable_inlining = true
5758 disable_cse = true
5759 disable_sccp = false
5760 disable_load_elimination = true
5761 disable_dead_store_elimination = false
5762 disable_algebraic_optimization = true
5763 disable_branch_optimization = false
5764 disable_assert_elimination = true
5765 disable_mem2var = false
5766 disable_simplify_cfg = true
5767 disable_remove_unused_variables = false
5768 inline_threshold = 15
5769 "#,
5770 )?;
5771
5772 let config = Config::load().unwrap();
5773 assert_eq!(
5774 config.vyper,
5775 VyperConfig {
5776 optimize: Some(VyperOptimizationMode::O1),
5777 opt_level: None,
5778 path: Some("/path/to/vyper".into()),
5779 experimental_codegen: Some(true),
5780 venom_experimental: Some(false),
5781 debug: Some(true),
5782 enable_decimals: Some(true),
5783 venom: Some(VyperVenomSettings {
5784 disable_inlining: Some(true),
5785 disable_cse: Some(true),
5786 disable_sccp: Some(false),
5787 disable_load_elimination: Some(true),
5788 disable_dead_store_elimination: Some(false),
5789 disable_algebraic_optimization: Some(true),
5790 disable_branch_optimization: Some(false),
5791 disable_assert_elimination: Some(true),
5792 disable_mem2var: Some(false),
5793 disable_simplify_cfg: Some(true),
5794 disable_remove_unused_variables: Some(false),
5795 inline_threshold: Some(15),
5796 }),
5797 }
5798 );
5799
5800 Ok(())
5801 });
5802 }
5803
5804 #[test]
5805 fn test_vyper_settings_include_extended_config() {
5806 let config = Config {
5807 vyper: VyperConfig {
5808 opt_level: Some(VyperOptimizationLevel::O3),
5809 experimental_codegen: Some(true),
5810 venom_experimental: Some(false),
5811 debug: Some(true),
5812 enable_decimals: Some(true),
5813 venom: Some(VyperVenomSettings {
5814 disable_cse: Some(true),
5815 inline_threshold: Some(15),
5816 ..Default::default()
5817 }),
5818 ..Default::default()
5819 },
5820 ..Config::default().normalized_optimizer_settings()
5821 };
5822
5823 let settings = config.vyper_settings().unwrap();
5824 assert_eq!(settings.optimize, None);
5825 assert_eq!(settings.opt_level, Some(VyperOptimizationLevel::O3));
5826 assert_eq!(settings.experimental_codegen, Some(true));
5827 assert_eq!(settings.venom_experimental, Some(false));
5828 assert_eq!(settings.debug, Some(true));
5829 assert_eq!(settings.enable_decimals, Some(true));
5830 assert_eq!(
5831 settings.venom,
5832 Some(VyperVenomSettings {
5833 disable_cse: Some(true),
5834 inline_threshold: Some(15),
5835 ..Default::default()
5836 })
5837 );
5838 }
5839
5840 #[test]
5841 fn vyper_opt_level_overrides_optimize() {
5842 let config = Config {
5843 vyper: VyperConfig {
5844 optimize: Some(VyperOptimizationMode::Gas),
5845 opt_level: Some(VyperOptimizationLevel::O3),
5846 ..Default::default()
5847 },
5848 ..Config::default().normalized_optimizer_settings()
5849 };
5850
5851 let settings = config.vyper_settings().unwrap();
5852 assert_eq!(settings.optimize, None);
5853 assert_eq!(settings.opt_level, Some(VyperOptimizationLevel::O3));
5854 }
5855
5856 #[test]
5857 fn test_parse_soldeer() {
5858 figment::Jail::expect_with(|jail| {
5859 jail.create_file(
5860 "foundry.toml",
5861 r#"
5862 [soldeer]
5863 remappings_generate = true
5864 remappings_regenerate = false
5865 remappings_version = true
5866 remappings_prefix = "@"
5867 remappings_location = "txt"
5868 recursive_deps = true
5869 "#,
5870 )?;
5871
5872 let config = Config::load().unwrap();
5873
5874 assert_eq!(
5875 config.soldeer,
5876 Some(SoldeerConfig {
5877 remappings_generate: true,
5878 remappings_regenerate: false,
5879 remappings_version: true,
5880 remappings_prefix: "@".to_string(),
5881 remappings_location: RemappingsLocation::Txt,
5882 recursive_deps: true,
5883 })
5884 );
5885
5886 Ok(())
5887 });
5888 }
5889
5890 #[test]
5892 fn test_resolve_mesc_by_chain_id() {
5893 let s = r#"{
5894 "mesc_version": "0.2.1",
5895 "default_endpoint": null,
5896 "endpoints": {
5897 "sophon_50104": {
5898 "name": "sophon_50104",
5899 "url": "https://rpc.sophon.xyz",
5900 "chain_id": "50104",
5901 "endpoint_metadata": {}
5902 }
5903 },
5904 "network_defaults": {
5905 },
5906 "network_names": {},
5907 "profiles": {
5908 "foundry": {
5909 "name": "foundry",
5910 "default_endpoint": "local_ethereum",
5911 "network_defaults": {
5912 "50104": "sophon_50104"
5913 },
5914 "profile_metadata": {},
5915 "use_mesc": true
5916 }
5917 },
5918 "global_metadata": {}
5919}"#;
5920
5921 let config = serde_json::from_str(s).unwrap();
5922 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5923 .unwrap()
5924 .unwrap();
5925 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5926
5927 let s = r#"{
5928 "mesc_version": "0.2.1",
5929 "default_endpoint": null,
5930 "endpoints": {
5931 "sophon_50104": {
5932 "name": "sophon_50104",
5933 "url": "https://rpc.sophon.xyz",
5934 "chain_id": "50104",
5935 "endpoint_metadata": {}
5936 }
5937 },
5938 "network_defaults": {
5939 "50104": "sophon_50104"
5940 },
5941 "network_names": {},
5942 "profiles": {},
5943 "global_metadata": {}
5944}"#;
5945
5946 let config = serde_json::from_str(s).unwrap();
5947 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5948 .unwrap()
5949 .unwrap();
5950 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5951 }
5952
5953 #[test]
5954 fn test_get_etherscan_config_with_unknown_chain() {
5955 figment::Jail::expect_with(|jail| {
5956 jail.create_file(
5957 "foundry.toml",
5958 r#"
5959 [etherscan]
5960 mainnet = { chain = 3658348, key = "api-key"}
5961 "#,
5962 )?;
5963 let config = Config::load().unwrap();
5964 let unknown_chain = Chain::from_id(3658348);
5965 let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5966 assert!(result.is_err());
5967 let error_msg = result.unwrap_err().to_string();
5968 assert!(error_msg.contains("No known Etherscan API URL for chain `3658348`"));
5969 assert!(error_msg.contains("Specify a `url`"));
5970 assert!(error_msg.contains("Verify the chain `3658348` is correct"));
5971
5972 Ok(())
5973 });
5974 }
5975
5976 #[test]
5977 fn test_get_etherscan_config_with_existing_chain_and_url() {
5978 figment::Jail::expect_with(|jail| {
5979 jail.create_file(
5980 "foundry.toml",
5981 r#"
5982 [etherscan]
5983 mainnet = { chain = 1, key = "api-key" }
5984 "#,
5985 )?;
5986 let config = Config::load().unwrap();
5987 let unknown_chain = Chain::from_id(1);
5988 let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5989 assert!(result.is_ok());
5990 Ok(())
5991 });
5992 }
5993
5994 #[test]
5995 fn test_can_inherit_a_base_toml() {
5996 figment::Jail::expect_with(|jail| {
5997 jail.create_file(
5999 "base-config.toml",
6000 r#"
6001 [profile.default]
6002 optimizer_runs = 800
6003
6004 [invariant]
6005 runs = 1000
6006
6007 [rpc_endpoints]
6008 mainnet = "https://example.com"
6009 optimism = "https://example-2.com/"
6010 "#,
6011 )?;
6012
6013 jail.create_file(
6015 "foundry.toml",
6016 r#"
6017 [profile.default]
6018 extends = "base-config.toml"
6019
6020 [invariant]
6021 runs = 333
6022 depth = 15
6023
6024 [rpc_endpoints]
6025 mainnet = "https://test.xyz/rpc"
6026 "#,
6027 )?;
6028
6029 let config = Config::load().unwrap();
6030 assert_eq!(config.extends, Some(Extends::Path("base-config.toml".to_string())));
6031
6032 assert_eq!(config.optimizer_runs, Some(800));
6034
6035 assert_eq!(config.invariant.runs, 333);
6037 assert_eq!(config.invariant.depth, 15);
6038
6039 let endpoints = config.rpc_endpoints.resolved();
6042 assert!(
6043 endpoints.get("mainnet").unwrap().url().unwrap().contains("https://test.xyz/rpc")
6044 );
6045 assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("example-2.com"));
6046
6047 Ok(())
6048 });
6049 }
6050
6051 #[test]
6052 fn inherited_symbolic_sections_preserve_source_precedence() {
6053 figment::Jail::expect_with(|jail| {
6054 jail.create_file(
6055 "base.toml",
6056 r#"
6057 [profile.default.symbolic]
6058 max_paths = 10
6059 depth = 100
6060 "#,
6061 )?;
6062 jail.create_file(
6063 "foundry.toml",
6064 r#"
6065 [profile.default]
6066 extends = "base.toml"
6067
6068 [symbolic]
6069 max_paths = 20
6070 "#,
6071 )?;
6072
6073 let config = Config::load().unwrap();
6074 assert_eq!(config.symbolic.max_paths, 20);
6075 assert_eq!(config.symbolic.depth, Some(100));
6076
6077 jail.create_file(
6078 "base.toml",
6079 r#"
6080 [symbolic]
6081 max_paths = 30
6082 depth = 200
6083 "#,
6084 )?;
6085 jail.create_file(
6086 "foundry.toml",
6087 r#"
6088 [profile.default]
6089 extends = "base.toml"
6090
6091 [profile.default.symbolic]
6092 max_paths = 40
6093 "#,
6094 )?;
6095
6096 let config = Config::load().unwrap();
6097 assert_eq!(config.symbolic.max_paths, 40);
6098 assert_eq!(config.symbolic.depth, Some(200));
6099
6100 Ok(())
6101 });
6102 }
6103
6104 #[test]
6105 fn inherited_symbolic_sections_detect_effective_collisions() {
6106 figment::Jail::expect_with(|jail| {
6107 jail.create_file(
6108 "base.toml",
6109 r#"
6110 [profile.default.symbolic]
6111 max_paths = 10
6112 "#,
6113 )?;
6114 jail.create_file(
6115 "foundry.toml",
6116 r#"
6117 [profile.default]
6118 extends = { path = "base.toml", strategy = "no-collision" }
6119
6120 [symbolic]
6121 max_paths = 20
6122 "#,
6123 )?;
6124
6125 let err = Config::load().unwrap_err().to_string();
6126 assert!(err.contains("Key collision detected"), "unexpected error: {err}");
6127 assert!(err.contains("symbolic"), "unexpected error: {err}");
6128
6129 Ok(())
6130 });
6131 }
6132
6133 #[test]
6134 fn inherited_fuzz_section_remains_invariant_fallback() {
6135 figment::Jail::expect_with(|jail| {
6136 jail.create_file(
6137 "base.toml",
6138 r#"
6139 [fuzz]
6140 include_storage = false
6141 dictionary_weight = 99
6142 "#,
6143 )?;
6144 jail.create_file(
6145 "foundry.toml",
6146 r#"
6147 [profile.default]
6148 extends = "base.toml"
6149
6150 [invariant]
6151 runs = 420
6152 "#,
6153 )?;
6154
6155 let config = Config::load().unwrap();
6156 assert_eq!(config.invariant.runs, 420);
6157 assert!(!config.invariant.dictionary.include_storage);
6158 assert_eq!(config.invariant.dictionary.dictionary_weight, 99);
6159
6160 Ok(())
6161 });
6162 }
6163
6164 #[test]
6165 fn test_inheritance_validation() {
6166 figment::Jail::expect_with(|jail| {
6167 jail.create_file(
6169 "base-with-inherit.toml",
6170 r#"
6171 [profile.default]
6172 extends = "another.toml"
6173 optimizer_runs = 800
6174 "#,
6175 )?;
6176
6177 jail.create_file(
6178 "foundry.toml",
6179 r#"
6180 [profile.default]
6181 extends = "base-with-inherit.toml"
6182 "#,
6183 )?;
6184
6185 let result = Config::load();
6187 assert!(result.is_err());
6188 assert!(result.unwrap_err().to_string().contains("Nested inheritance is not allowed"));
6189
6190 jail.create_file(
6192 "foundry.toml",
6193 r#"
6194 [profile.default]
6195 extends = "foundry.toml"
6196 "#,
6197 )?;
6198
6199 let result = Config::load();
6200 assert!(result.is_err());
6201 assert!(result.unwrap_err().to_string().contains("cannot inherit from itself"));
6202
6203 jail.create_file(
6205 "foundry.toml",
6206 r#"
6207 [profile.default]
6208 extends = "non-existent.toml"
6209 "#,
6210 )?;
6211
6212 let result = Config::load();
6213 assert!(result.is_err());
6214 let err_msg = result.unwrap_err().to_string();
6215 assert!(
6216 err_msg.contains("does not exist")
6217 || err_msg.contains("Failed to resolve inherited config path"),
6218 "Error message: {err_msg}"
6219 );
6220
6221 Ok(())
6222 });
6223 }
6224
6225 #[test]
6226 fn test_complex_inheritance_merging() {
6227 figment::Jail::expect_with(|jail| {
6228 jail.create_file(
6230 "base.toml",
6231 r#"
6232 [profile.default]
6233 optimizer = true
6234 optimizer_runs = 1000
6235 via_ir = false
6236 solc = "0.8.19"
6237
6238 [invariant]
6239 runs = 500
6240 depth = 100
6241
6242 [fuzz]
6243 runs = 256
6244 seed = "0x123"
6245
6246 [rpc_endpoints]
6247 mainnet = "https://base-mainnet.com"
6248 optimism = "https://base-optimism.com"
6249 arbitrum = "https://base-arbitrum.com"
6250 "#,
6251 )?;
6252
6253 jail.create_file(
6255 "foundry.toml",
6256 r#"
6257 [profile.default]
6258 extends = "base.toml"
6259 optimizer_runs = 200 # Override
6260 via_ir = true # Override
6261 # optimizer and solc are inherited
6262
6263 [invariant]
6264 runs = 333 # Override
6265 # depth is inherited
6266
6267 # fuzz section is fully inherited
6268
6269 [rpc_endpoints]
6270 mainnet = "https://local-mainnet.com" # Override
6271 # optimism and arbitrum are inherited
6272 polygon = "https://local-polygon.com" # New
6273 "#,
6274 )?;
6275
6276 let config = Config::load().unwrap();
6277
6278 assert_eq!(config.optimizer, Some(true));
6280 assert_eq!(config.optimizer_runs, Some(200));
6281 assert_eq!(config.via_ir, true);
6282 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
6283
6284 assert_eq!(config.invariant.runs, 333);
6286 assert_eq!(config.invariant.depth, 100);
6287
6288 assert_eq!(config.fuzz.runs, 256);
6290 assert_eq!(config.fuzz.seed, Some(U256::from(0x123)));
6291
6292 let endpoints = config.rpc_endpoints.resolved();
6294 assert!(endpoints.get("mainnet").unwrap().url().unwrap().contains("local-mainnet"));
6295 assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("base-optimism"));
6296 assert!(endpoints.get("arbitrum").unwrap().url().unwrap().contains("base-arbitrum"));
6297 assert!(endpoints.get("polygon").unwrap().url().unwrap().contains("local-polygon"));
6298
6299 Ok(())
6300 });
6301 }
6302
6303 #[test]
6304 fn test_inheritance_with_different_profiles() {
6305 figment::Jail::expect_with(|jail| {
6306 jail.create_file(
6308 "base.toml",
6309 r#"
6310 [profile.default]
6311 optimizer = true
6312 optimizer_runs = 200
6313
6314 [profile.ci]
6315 optimizer = true
6316 optimizer_runs = 10000
6317 via_ir = true
6318
6319 [profile.dev]
6320 optimizer = false
6321 "#,
6322 )?;
6323
6324 jail.create_file(
6326 "foundry.toml",
6327 r#"
6328 [profile.default]
6329 extends = "base.toml"
6330 verbosity = 3
6331
6332 [profile.ci]
6333 optimizer_runs = 5000 # This doesn't inherit from base.toml's ci profile
6334 "#,
6335 )?;
6336
6337 let config = Config::load().unwrap();
6339 assert_eq!(config.optimizer, Some(true));
6340 assert_eq!(config.optimizer_runs, Some(200));
6341 assert_eq!(config.verbosity, 3);
6342
6343 jail.set_env("FOUNDRY_PROFILE", "ci");
6345 let config = Config::load().unwrap();
6346 assert_eq!(config.optimizer_runs, Some(5000));
6347 assert_eq!(config.optimizer, Some(true));
6348 assert_eq!(config.via_ir, false);
6350
6351 Ok(())
6352 });
6353 }
6354
6355 #[test]
6356 fn test_inheritance_with_env_vars() {
6357 figment::Jail::expect_with(|jail| {
6358 jail.create_file(
6359 "base.toml",
6360 r#"
6361 [profile.default]
6362 optimizer_runs = 500
6363 sender = "0x0000000000000000000000000000000000000001"
6364 verbosity = 1
6365 "#,
6366 )?;
6367
6368 jail.create_file(
6369 "foundry.toml",
6370 r#"
6371 [profile.default]
6372 extends = "base.toml"
6373 verbosity = 2
6374 "#,
6375 )?;
6376
6377 jail.set_env("FOUNDRY_OPTIMIZER_RUNS", "999");
6379 jail.set_env("FOUNDRY_VERBOSITY", "4");
6380
6381 let config = Config::load().unwrap();
6382 assert_eq!(config.optimizer_runs, Some(999));
6383 assert_eq!(config.verbosity, 4);
6384 assert_eq!(
6385 config.sender,
6386 "0x0000000000000000000000000000000000000001"
6387 .parse::<alloy_primitives::Address>()
6388 .unwrap()
6389 );
6390
6391 Ok(())
6392 });
6393 }
6394
6395 #[test]
6396 fn test_inheritance_with_subdirectories() {
6397 figment::Jail::expect_with(|jail| {
6398 jail.create_dir("configs")?;
6400 jail.create_file(
6401 "configs/base.toml",
6402 r#"
6403 [profile.default]
6404 optimizer_runs = 800
6405 src = "contracts"
6406 "#,
6407 )?;
6408
6409 jail.create_file(
6411 "foundry.toml",
6412 r#"
6413 [profile.default]
6414 extends = "configs/base.toml"
6415 test = "tests"
6416 "#,
6417 )?;
6418
6419 let config = Config::load().unwrap();
6420 assert_eq!(config.optimizer_runs, Some(800));
6421 assert_eq!(config.src, PathBuf::from("contracts"));
6422 assert_eq!(config.test, PathBuf::from("tests"));
6423
6424 jail.create_dir("project")?;
6426 jail.create_file(
6427 "shared-base.toml",
6428 r#"
6429 [profile.default]
6430 optimizer_runs = 1500
6431 "#,
6432 )?;
6433
6434 jail.create_file(
6435 "project/foundry.toml",
6436 r#"
6437 [profile.default]
6438 extends = "../shared-base.toml"
6439 "#,
6440 )?;
6441
6442 std::env::set_current_dir(jail.directory().join("project")).unwrap();
6443 let config = Config::load().unwrap();
6444 assert_eq!(config.optimizer_runs, Some(1500));
6445
6446 Ok(())
6447 });
6448 }
6449
6450 #[test]
6451 fn test_inheritance_with_empty_files() {
6452 figment::Jail::expect_with(|jail| {
6453 jail.create_file(
6455 "base.toml",
6456 r#"
6457 [profile.default]
6458 "#,
6459 )?;
6460
6461 jail.create_file(
6462 "foundry.toml",
6463 r#"
6464 [profile.default]
6465 extends = "base.toml"
6466 optimizer_runs = 300
6467 "#,
6468 )?;
6469
6470 let config = Config::load().unwrap();
6471 assert_eq!(config.optimizer_runs, Some(300));
6472
6473 jail.create_file(
6475 "base2.toml",
6476 r#"
6477 [profile.default]
6478 optimizer_runs = 400
6479 via_ir = true
6480 "#,
6481 )?;
6482
6483 jail.create_file(
6484 "foundry.toml",
6485 r#"
6486 [profile.default]
6487 extends = "base2.toml"
6488 "#,
6489 )?;
6490
6491 let config = Config::load().unwrap();
6492 assert_eq!(config.optimizer_runs, Some(400));
6493 assert!(config.via_ir);
6494
6495 Ok(())
6496 });
6497 }
6498
6499 #[test]
6500 fn test_inheritance_array_and_table_merging() {
6501 figment::Jail::expect_with(|jail| {
6502 jail.create_file(
6503 "base.toml",
6504 r#"
6505 [profile.default]
6506 libs = ["lib", "node_modules"]
6507 ignored_error_codes = [5667, 1878]
6508 extra_output = ["metadata", "ir"]
6509
6510 [profile.default.model_checker]
6511 engine = "chc"
6512 timeout = 10000
6513 targets = ["assert"]
6514
6515 [profile.default.optimizer_details]
6516 peephole = true
6517 inliner = true
6518 "#,
6519 )?;
6520
6521 jail.create_file(
6522 "foundry.toml",
6523 r#"
6524 [profile.default]
6525 extends = "base.toml"
6526 libs = ["custom-lib"] # Concatenates with base array
6527 ignored_error_codes = [2018] # Concatenates with base array
6528
6529 [profile.default.model_checker]
6530 timeout = 5000 # Overrides base value
6531 # engine and targets are inherited
6532
6533 [profile.default.optimizer_details]
6534 jumpdest_remover = true # Adds new field
6535 # peephole and inliner are inherited
6536 "#,
6537 )?;
6538
6539 let config = Config::load().unwrap();
6540
6541 assert_eq!(
6543 config.libs,
6544 vec![
6545 PathBuf::from("lib"),
6546 PathBuf::from("node_modules"),
6547 PathBuf::from("custom-lib")
6548 ]
6549 );
6550 assert_eq!(
6551 config.ignored_error_codes,
6552 vec![
6553 SolidityErrorCode::UnusedFunctionParameter, SolidityErrorCode::SpdxLicenseNotProvided, SolidityErrorCode::FunctionStateMutabilityCanBeRestricted ]
6557 );
6558
6559 assert_eq!(config.model_checker.as_ref().unwrap().timeout, Some(5000));
6561 assert_eq!(
6562 config.model_checker.as_ref().unwrap().engine,
6563 Some(ModelCheckerEngine::CHC)
6564 );
6565 assert_eq!(
6566 config.model_checker.as_ref().unwrap().targets,
6567 Some(vec![ModelCheckerTarget::Assert])
6568 );
6569
6570 assert_eq!(config.optimizer_details.as_ref().unwrap().peephole, Some(true));
6572 assert_eq!(config.optimizer_details.as_ref().unwrap().inliner, Some(true));
6573 assert_eq!(config.optimizer_details.as_ref().unwrap().jumpdest_remover, None);
6574
6575 Ok(())
6576 });
6577 }
6578
6579 #[test]
6580 fn test_inheritance_with_special_sections() {
6581 figment::Jail::expect_with(|jail| {
6582 jail.create_file(
6583 "base.toml",
6584 r#"
6585 [profile.default]
6586 # Base file should not have 'extends' to avoid nested inheritance
6587
6588 [labels]
6589 "0x0000000000000000000000000000000000000001" = "Alice"
6590 "0x0000000000000000000000000000000000000002" = "Bob"
6591
6592 [[profile.default.fs_permissions]]
6593 access = "read"
6594 path = "./src"
6595
6596 [[profile.default.fs_permissions]]
6597 access = "read-write"
6598 path = "./cache"
6599 "#,
6600 )?;
6601
6602 jail.create_file(
6603 "foundry.toml",
6604 r#"
6605 [profile.default]
6606 extends = "base.toml"
6607
6608 [labels]
6609 "0x0000000000000000000000000000000000000002" = "Bob Updated"
6610 "0x0000000000000000000000000000000000000003" = "Charlie"
6611
6612 [[profile.default.fs_permissions]]
6613 access = "read"
6614 path = "./test"
6615 "#,
6616 )?;
6617
6618 let config = Config::load().unwrap();
6619
6620 assert_eq!(
6622 config.labels.get(
6623 &"0x0000000000000000000000000000000000000001"
6624 .parse::<alloy_primitives::Address>()
6625 .unwrap()
6626 ),
6627 Some(&"Alice".to_string())
6628 );
6629 assert_eq!(
6630 config.labels.get(
6631 &"0x0000000000000000000000000000000000000002"
6632 .parse::<alloy_primitives::Address>()
6633 .unwrap()
6634 ),
6635 Some(&"Bob Updated".to_string())
6636 );
6637 assert_eq!(
6638 config.labels.get(
6639 &"0x0000000000000000000000000000000000000003"
6640 .parse::<alloy_primitives::Address>()
6641 .unwrap()
6642 ),
6643 Some(&"Charlie".to_string())
6644 );
6645
6646 assert_eq!(config.fs_permissions.permissions.len(), 3); assert!(
6650 config
6651 .fs_permissions
6652 .permissions
6653 .iter()
6654 .any(|p| p.path.to_str().unwrap() == "./src")
6655 );
6656 assert!(
6657 config
6658 .fs_permissions
6659 .permissions
6660 .iter()
6661 .any(|p| p.path.to_str().unwrap() == "./cache")
6662 );
6663 assert!(
6664 config
6665 .fs_permissions
6666 .permissions
6667 .iter()
6668 .any(|p| p.path.to_str().unwrap() == "./test")
6669 );
6670
6671 Ok(())
6672 });
6673 }
6674
6675 #[test]
6676 fn test_inheritance_with_compilation_settings() {
6677 figment::Jail::expect_with(|jail| {
6678 jail.create_file(
6679 "base.toml",
6680 r#"
6681 [profile.default]
6682 solc = "0.8.19"
6683 evm_version = "paris"
6684 via_ir = false
6685 optimizer = true
6686 optimizer_runs = 200
6687
6688 [profile.default.optimizer_details]
6689 peephole = true
6690 inliner = false
6691 jumpdest_remover = true
6692 order_literals = false
6693 deduplicate = true
6694 cse = true
6695 constant_optimizer = true
6696 yul = true
6697
6698 [profile.default.optimizer_details.yul_details]
6699 stack_allocation = true
6700 optimizer_steps = "dhfoDgvulfnTUtnIf"
6701 "#,
6702 )?;
6703
6704 jail.create_file(
6705 "foundry.toml",
6706 r#"
6707 [profile.default]
6708 extends = "base.toml"
6709 evm_version = "shanghai" # Override
6710 optimizer_runs = 1000 # Override
6711
6712 [profile.default.optimizer_details]
6713 inliner = true # Override
6714 # Rest inherited
6715 "#,
6716 )?;
6717
6718 let config = Config::load().unwrap();
6719
6720 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
6722 assert_eq!(config.evm_version, EvmVersion::Shanghai);
6723 assert_eq!(config.via_ir, false);
6724 assert_eq!(config.optimizer, Some(true));
6725 assert_eq!(config.optimizer_runs, Some(1000));
6726
6727 let details = config.optimizer_details.as_ref().unwrap();
6729 assert_eq!(details.peephole, Some(true));
6730 assert_eq!(details.inliner, Some(true));
6731 assert_eq!(details.jumpdest_remover, None);
6732 assert_eq!(details.order_literals, None);
6733 assert_eq!(details.deduplicate, Some(true));
6734 assert_eq!(details.cse, Some(true));
6735 assert_eq!(details.constant_optimizer, None);
6736 assert_eq!(details.yul, Some(true));
6737
6738 if let Some(yul_details) = details.yul_details.as_ref() {
6740 assert_eq!(yul_details.stack_allocation, Some(true));
6741 assert_eq!(yul_details.optimizer_steps, Some("dhfoDgvulfnTUtnIf".to_string()));
6742 }
6743
6744 Ok(())
6745 });
6746 }
6747
6748 #[test]
6749 fn test_inheritance_with_remappings() {
6750 figment::Jail::expect_with(|jail| {
6751 jail.create_file(
6752 "base.toml",
6753 r#"
6754 [profile.default]
6755 remappings = [
6756 "forge-std/=lib/forge-std/src/",
6757 "@openzeppelin/=lib/openzeppelin-contracts/",
6758 "ds-test/=lib/ds-test/src/"
6759 ]
6760 auto_detect_remappings = false
6761 "#,
6762 )?;
6763
6764 jail.create_file(
6765 "foundry.toml",
6766 r#"
6767 [profile.default]
6768 extends = "base.toml"
6769 remappings = [
6770 "@custom/=lib/custom/",
6771 "ds-test/=lib/forge-std/lib/ds-test/src/" # Note: This will be added alongside base remappings
6772 ]
6773 "#,
6774 )?;
6775
6776 let config = Config::load().unwrap();
6777
6778 assert!(config.remappings.iter().any(|r| r.to_string().contains("@custom/")));
6780 assert!(config.remappings.iter().any(|r| r.to_string().contains("ds-test/")));
6781 assert!(config.remappings.iter().any(|r| r.to_string().contains("forge-std/")));
6782 assert!(config.remappings.iter().any(|r| r.to_string().contains("@openzeppelin/")));
6783
6784 assert!(!config.auto_detect_remappings);
6786
6787 Ok(())
6788 });
6789 }
6790
6791 #[test]
6792 fn test_inheritance_with_multiple_profiles_and_single_file() {
6793 figment::Jail::expect_with(|jail| {
6794 jail.create_file(
6796 "base.toml",
6797 r#"
6798 [profile.prod]
6799 optimizer = true
6800 optimizer_runs = 10000
6801 via_ir = true
6802
6803 [profile.test]
6804 optimizer = false
6805
6806 [profile.test.fuzz]
6807 runs = 100
6808 "#,
6809 )?;
6810
6811 jail.create_file(
6813 "foundry.toml",
6814 r#"
6815 [profile.prod]
6816 extends = "base.toml"
6817 evm_version = "shanghai" # Additional setting
6818
6819 [profile.test]
6820 extends = "base.toml"
6821
6822 [profile.test.fuzz]
6823 runs = 500 # Override
6824 "#,
6825 )?;
6826
6827 jail.set_env("FOUNDRY_PROFILE", "prod");
6829 let config = Config::load().unwrap();
6830 assert_eq!(config.optimizer, Some(true));
6831 assert_eq!(config.optimizer_runs, Some(10000));
6832 assert_eq!(config.via_ir, true);
6833 assert_eq!(config.evm_version, EvmVersion::Shanghai);
6834
6835 jail.set_env("FOUNDRY_PROFILE", "test");
6837 let config = Config::load().unwrap();
6838 assert_eq!(config.optimizer, Some(false));
6839 assert_eq!(config.fuzz.runs, 500);
6840
6841 Ok(())
6842 });
6843 }
6844
6845 #[test]
6846 fn test_inheritance_with_multiple_profiles_and_files() {
6847 figment::Jail::expect_with(|jail| {
6848 jail.create_file(
6849 "prod.toml",
6850 r#"
6851 [profile.prod]
6852 optimizer = true
6853 optimizer_runs = 20000
6854 gas_limit = 50000000
6855 "#,
6856 )?;
6857 jail.create_file(
6858 "dev.toml",
6859 r#"
6860 [profile.dev]
6861 optimizer = true
6862 optimizer_runs = 333
6863 gas_limit = 555555
6864 "#,
6865 )?;
6866
6867 jail.create_file(
6869 "foundry.toml",
6870 r#"
6871 [profile.dev]
6872 extends = "dev.toml"
6873 sender = "0x0000000000000000000000000000000000000001"
6874
6875 [profile.prod]
6876 extends = "prod.toml"
6877 sender = "0x0000000000000000000000000000000000000002"
6878 "#,
6879 )?;
6880
6881 jail.set_env("FOUNDRY_PROFILE", "dev");
6883 let config = Config::load().unwrap();
6884 assert_eq!(config.optimizer, Some(true));
6885 assert_eq!(config.optimizer_runs, Some(333));
6886 assert_eq!(config.gas_limit, 555555.into());
6887 assert_eq!(
6888 config.sender,
6889 "0x0000000000000000000000000000000000000001"
6890 .parse::<alloy_primitives::Address>()
6891 .unwrap()
6892 );
6893
6894 jail.set_env("FOUNDRY_PROFILE", "prod");
6896 let config = Config::load().unwrap();
6897 assert_eq!(config.optimizer, Some(true));
6898 assert_eq!(config.optimizer_runs, Some(20000));
6899 assert_eq!(config.gas_limit, 50000000.into());
6900 assert_eq!(
6901 config.sender,
6902 "0x0000000000000000000000000000000000000002"
6903 .parse::<alloy_primitives::Address>()
6904 .unwrap()
6905 );
6906
6907 Ok(())
6908 });
6909 }
6910
6911 #[test]
6912 fn test_extends_strategy_extend_arrays() {
6913 figment::Jail::expect_with(|jail| {
6914 jail.create_file(
6916 "base.toml",
6917 r#"
6918 [profile.default]
6919 libs = ["lib", "node_modules"]
6920 ignored_error_codes = [5667, 1878]
6921 optimizer_runs = 200
6922 "#,
6923 )?;
6924
6925 jail.create_file(
6927 "foundry.toml",
6928 r#"
6929 [profile.default]
6930 extends = "base.toml"
6931 libs = ["mylib", "customlib"]
6932 ignored_error_codes = [1234]
6933 optimizer_runs = 500
6934 "#,
6935 )?;
6936
6937 let config = Config::load().unwrap();
6938
6939 assert_eq!(config.libs.len(), 4);
6941 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6942 assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6943 assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
6944 assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
6945
6946 assert_eq!(config.ignored_error_codes.len(), 3);
6947 assert!(
6948 config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
6949 ); assert!(
6951 config.ignored_error_codes.contains(&SolidityErrorCode::SpdxLicenseNotProvided)
6952 ); assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); assert_eq!(config.optimizer_runs, Some(500));
6957
6958 Ok(())
6959 });
6960 }
6961
6962 #[test]
6963 fn test_extends_strategy_replace_arrays() {
6964 figment::Jail::expect_with(|jail| {
6965 jail.create_file(
6967 "base.toml",
6968 r#"
6969 [profile.default]
6970 libs = ["lib", "node_modules"]
6971 ignored_error_codes = [5667, 1878]
6972 optimizer_runs = 200
6973 "#,
6974 )?;
6975
6976 jail.create_file(
6978 "foundry.toml",
6979 r#"
6980 [profile.default]
6981 extends = { path = "base.toml", strategy = "replace-arrays" }
6982 libs = ["mylib", "customlib"]
6983 ignored_error_codes = [1234]
6984 optimizer_runs = 500
6985 "#,
6986 )?;
6987
6988 let config = Config::load().unwrap();
6989
6990 assert_eq!(config.libs.len(), 2);
6992 assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
6993 assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
6994 assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
6995 assert!(!config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6996
6997 assert_eq!(config.ignored_error_codes.len(), 1);
6998 assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); assert!(
7000 !config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
7001 ); assert_eq!(config.optimizer_runs, Some(500));
7005
7006 Ok(())
7007 });
7008 }
7009
7010 #[test]
7011 fn test_extends_strategy_no_collision_success() {
7012 figment::Jail::expect_with(|jail| {
7013 jail.create_file(
7015 "base.toml",
7016 r#"
7017 [profile.default]
7018 optimizer = true
7019 optimizer_runs = 200
7020 src = "src"
7021 "#,
7022 )?;
7023
7024 jail.create_file(
7026 "foundry.toml",
7027 r#"
7028 [profile.default]
7029 extends = { path = "base.toml", strategy = "no-collision" }
7030 test = "tests"
7031 libs = ["lib"]
7032 "#,
7033 )?;
7034
7035 let config = Config::load().unwrap();
7036
7037 assert_eq!(config.optimizer, Some(true));
7039 assert_eq!(config.optimizer_runs, Some(200));
7040 assert_eq!(config.src, PathBuf::from("src"));
7041
7042 assert_eq!(config.test, PathBuf::from("tests"));
7044 assert_eq!(config.libs.len(), 1);
7045 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
7046
7047 Ok(())
7048 });
7049 }
7050
7051 #[test]
7052 fn test_extends_strategy_no_collision_error() {
7053 figment::Jail::expect_with(|jail| {
7054 jail.create_file(
7056 "base.toml",
7057 r#"
7058 [profile.default]
7059 optimizer = true
7060 optimizer_runs = 200
7061 libs = ["lib", "node_modules"]
7062 "#,
7063 )?;
7064
7065 jail.create_file(
7067 "foundry.toml",
7068 r#"
7069 [profile.default]
7070 extends = { path = "base.toml", strategy = "no-collision" }
7071 optimizer_runs = 500
7072 libs = ["mylib"]
7073 "#,
7074 )?;
7075
7076 let result = Config::load();
7078
7079 if let Ok(config) = result {
7080 panic!(
7081 "Expected error but got config with optimizer_runs: {:?}, libs: {:?}",
7082 config.optimizer_runs, config.libs
7083 );
7084 }
7085
7086 let err = result.unwrap_err();
7087 let err_str = err.to_string();
7088 assert!(
7089 err_str.contains("Key collision detected") || err_str.contains("collision"),
7090 "Error message doesn't mention collision: {err_str}"
7091 );
7092
7093 Ok(())
7094 });
7095 }
7096
7097 #[test]
7098 fn test_extends_both_syntaxes() {
7099 figment::Jail::expect_with(|jail| {
7100 jail.create_file(
7102 "base.toml",
7103 r#"
7104 [profile.default]
7105 libs = ["lib"]
7106 optimizer = true
7107 "#,
7108 )?;
7109
7110 jail.create_file(
7112 "foundry_string.toml",
7113 r#"
7114 [profile.default]
7115 extends = "base.toml"
7116 libs = ["custom"]
7117 "#,
7118 )?;
7119
7120 jail.create_file(
7122 "foundry_object.toml",
7123 r#"
7124 [profile.default]
7125 extends = { path = "base.toml", strategy = "replace-arrays" }
7126 libs = ["custom"]
7127 "#,
7128 )?;
7129
7130 jail.set_env("FOUNDRY_CONFIG", "foundry_string.toml");
7132 let config = Config::load().unwrap();
7133 assert_eq!(config.libs.len(), 2); assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
7135 assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
7136
7137 jail.set_env("FOUNDRY_CONFIG", "foundry_object.toml");
7139 let config = Config::load().unwrap();
7140 assert_eq!(config.libs.len(), 1); assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
7142 assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
7143
7144 Ok(())
7145 });
7146 }
7147
7148 #[test]
7149 fn test_extends_strategy_default_is_extend_arrays() {
7150 figment::Jail::expect_with(|jail| {
7151 jail.create_file(
7153 "base.toml",
7154 r#"
7155 [profile.default]
7156 libs = ["lib", "node_modules"]
7157 optimizer = true
7158 "#,
7159 )?;
7160
7161 jail.create_file(
7163 "foundry.toml",
7164 r#"
7165 [profile.default]
7166 extends = "base.toml"
7167 libs = ["custom"]
7168 optimizer = false
7169 "#,
7170 )?;
7171
7172 let config = Config::load().unwrap();
7174
7175 assert_eq!(config.libs.len(), 3);
7177 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
7178 assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
7179 assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
7180
7181 assert_eq!(config.optimizer, Some(false));
7183
7184 Ok(())
7185 });
7186 }
7187
7188 #[test]
7189 fn test_deprecated_deny_warnings_is_handled() {
7190 figment::Jail::expect_with(|jail| {
7191 jail.create_file(
7192 "foundry.toml",
7193 r#"
7194 [profile.default]
7195 deny_warnings = true
7196 "#,
7197 )?;
7198 let config = Config::load().unwrap();
7199
7200 assert_eq!(config.deny, DenyLevel::Warnings);
7202 Ok(())
7203 });
7204 }
7205
7206 #[test]
7207 fn warns_on_deprecated_keys_in_inactive_profiles() {
7208 figment::Jail::expect_with(|jail| {
7209 jail.create_file(
7210 "foundry.toml",
7211 r#"
7212 [profile.default]
7213 src = "src"
7214
7215 [profile.ci]
7216 deny_warnings = true
7217 "#,
7218 )?;
7219
7220 let cfg = Config::load().unwrap();
7221 assert!(
7222 cfg.warnings.iter().any(|w| matches!(
7223 w,
7224 crate::Warning::DeprecatedKey { old, new }
7225 if old == "deny_warnings" && new == "deny = warnings"
7226 )),
7227 "Expected deprecated key warning for inactive profile, got: {:?}",
7228 cfg.warnings
7229 );
7230 Ok(())
7231 });
7232 }
7233
7234 #[test]
7235 fn warns_on_deprecated_profile_names() {
7236 figment::Jail::expect_with(|jail| {
7237 jail.create_file(
7238 "foundry.toml",
7239 r#"
7240 [profile.cancun]
7241 "#,
7242 )?;
7243
7244 let cfg = Config::load().unwrap();
7245 assert!(
7246 cfg.warnings.iter().any(|w| matches!(
7247 w,
7248 crate::Warning::DeprecatedKey { old, new }
7249 if old == "cancun" && new == "evm_version = Cancun"
7250 )),
7251 "Expected deprecated profile-name warning, got: {:?}",
7252 cfg.warnings
7253 );
7254 Ok(())
7255 });
7256 }
7257
7258 #[test]
7259 fn warns_on_unknown_keys_in_profile() {
7260 figment::Jail::expect_with(|jail| {
7261 jail.create_file(
7262 "foundry.toml",
7263 r#"
7264 [profile.default]
7265 unknown_key_xyz = 123
7266 "#,
7267 )?;
7268
7269 let cfg = Config::load().unwrap();
7270 assert!(cfg.warnings.iter().any(
7271 |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "unknown_key_xyz")
7272 ));
7273 Ok(())
7274 });
7275 }
7276
7277 #[test]
7278 fn no_unknown_key_warning_for_network_field() {
7279 figment::Jail::expect_with(|jail| {
7282 jail.create_file(
7283 "foundry.toml",
7284 r#"
7285 [profile.default]
7286 network = "tempo"
7287 "#,
7288 )?;
7289
7290 let cfg = Config::load().unwrap();
7291 assert!(
7292 !cfg.warnings.iter().any(
7293 |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "network")
7294 ),
7295 "did not expect UnknownKey warning for `network`, got: {:?}",
7296 cfg.warnings
7297 );
7298 Ok(())
7299 });
7300 }
7301
7302 #[test]
7303 fn no_unknown_key_warning_for_legacy_tempo_alias() {
7304 figment::Jail::expect_with(|jail| {
7306 jail.create_file(
7307 "foundry.toml",
7308 r#"
7309 [profile.default]
7310 tempo = true
7311 "#,
7312 )?;
7313
7314 let cfg = Config::load().unwrap();
7315 assert!(
7316 !cfg.warnings
7317 .iter()
7318 .any(|w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "tempo")),
7319 "did not expect UnknownKey warning for `tempo`, got: {:?}",
7320 cfg.warnings
7321 );
7322 Ok(())
7323 });
7324 }
7325
7326 #[test]
7327 fn fails_on_ambiguous_version_in_compilation_restrictions() {
7328 figment::Jail::expect_with(|jail| {
7329 jail.create_file(
7330 "foundry.toml",
7331 r#"
7332 [profile.default]
7333 src = "src"
7334
7335 [[profile.default.compilation_restrictions]]
7336 paths = "src/*.sol"
7337 version = "0.8.11"
7338 "#,
7339 )?;
7340
7341 let err = Config::load().expect_err("expected bare version to fail");
7342 let err_msg = err.to_string();
7343 assert!(
7344 err_msg.contains("Invalid version format '0.8.11'")
7345 && err_msg.contains("Bare version numbers are ambiguous"),
7346 "Expected error about ambiguous version, got: {err_msg}"
7347 );
7348
7349 Ok(())
7350 });
7351 }
7352
7353 #[test]
7354 fn accepts_explicit_version_requirements() {
7355 figment::Jail::expect_with(|jail| {
7356 jail.create_file(
7357 "foundry.toml",
7358 r#"
7359 [profile.default]
7360 src = "src"
7361
7362 [[profile.default.compilation_restrictions]]
7363 paths = "src/*.sol"
7364 version = "=0.8.11"
7365
7366 [[profile.default.compilation_restrictions]]
7367 paths = "test/*.sol"
7368 version = ">=0.8.11"
7369 "#,
7370 )?;
7371
7372 let config = Config::load().expect("should accept explicit version requirements");
7373 assert_eq!(config.compilation_restrictions.len(), 2);
7374
7375 Ok(())
7376 });
7377 }
7378
7379 #[test]
7380 fn warns_on_unknown_keys_in_all_config_sections() {
7381 figment::Jail::expect_with(|jail| {
7382 jail.create_file(
7383 "foundry.toml",
7384 r#"
7385 [profile.default]
7386 src = "src"
7387 unknown_profile_key = "should_warn"
7388
7389 # Standalone sections with unknown keys
7390 [fmt]
7391 line_length = 120
7392 unknown_fmt_key = "should_warn"
7393
7394 [lint]
7395 severity = ["high"]
7396 unknown_lint_key = "should_warn"
7397
7398 [doc]
7399 out = "docs"
7400 unknown_doc_key = "should_warn"
7401
7402 [fuzz]
7403 runs = 256
7404 unknown_fuzz_key = "should_warn"
7405
7406 [invariant]
7407 runs = 256
7408 unknown_invariant_key = "should_warn"
7409
7410 [symbolic]
7411 enabled = true
7412 depth = 128
7413 unknown_symbolic_key = "should_warn"
7414
7415 [mutation]
7416 unknown_mutation_key = "should_warn"
7417
7418 [vyper]
7419 unknown_vyper_key = "should_warn"
7420
7421 [bind_json]
7422 out = "bindings.sol"
7423 unknown_bind_json_key = "should_warn"
7424
7425 # Nested profile sections with unknown keys
7426 [profile.default.fmt]
7427 line_length = 100
7428 unknown_nested_fmt_key = "should_warn"
7429
7430 [profile.default.lint]
7431 severity = ["low"]
7432 unknown_nested_lint_key = "should_warn"
7433
7434 [profile.default.doc]
7435 out = "documentation"
7436 unknown_nested_doc_key = "should_warn"
7437
7438 [profile.default.fuzz]
7439 runs = 512
7440 unknown_nested_fuzz_key = "should_warn"
7441
7442 [profile.default.invariant]
7443 runs = 512
7444 unknown_nested_invariant_key = "should_warn"
7445
7446 [profile.default.symbolic]
7447 max_paths = 512
7448 unknown_nested_symbolic_key = "should_warn"
7449
7450 [profile.default.mutation]
7451 unknown_nested_mutation_key = "should_warn"
7452
7453 [profile.default.vyper]
7454 unknown_nested_vyper_key = "should_warn"
7455
7456 [profile.default.bind_json]
7457 out = "nested_bindings.sol"
7458 unknown_nested_bind_json_key = "should_warn"
7459
7460 # Array sections with unknown keys
7461 [[profile.default.compilation_restrictions]]
7462 paths = "src/*.sol"
7463 unknown_compilation_key = "should_warn"
7464
7465 [[profile.default.additional_compiler_profiles]]
7466 name = "via-ir"
7467 via_ir = true
7468 unknown_compiler_profile_key = "should_warn"
7469 "#,
7470 )?;
7471
7472 let cfg = Config::load().unwrap();
7473
7474 assert!(
7476 cfg.warnings.iter().any(|w| matches!(
7477 w,
7478 crate::Warning::UnknownKey { key, .. } if key == "unknown_profile_key"
7479 )),
7480 "Expected warning for 'unknown_profile_key' in profile, got: {:?}",
7481 cfg.warnings
7482 );
7483
7484 let standalone_expected = [
7486 ("unknown_fmt_key", "fmt"),
7487 ("unknown_lint_key", "lint"),
7488 ("unknown_doc_key", "doc"),
7489 ("unknown_fuzz_key", "fuzz"),
7490 ("unknown_invariant_key", "invariant"),
7491 ("unknown_symbolic_key", "symbolic"),
7492 ("unknown_mutation_key", "mutation"),
7493 ("unknown_vyper_key", "vyper"),
7494 ("unknown_bind_json_key", "bind_json"),
7495 ];
7496
7497 for (expected_key, expected_section) in standalone_expected {
7498 assert!(
7499 cfg.warnings.iter().any(|w| matches!(
7500 w,
7501 crate::Warning::UnknownSectionKey { key, section, .. }
7502 if key == expected_key && section == expected_section
7503 )),
7504 "Expected warning for '{}' in standalone section '{}', got: {:?}",
7505 expected_key,
7506 expected_section,
7507 cfg.warnings
7508 );
7509 }
7510
7511 let nested_expected = [
7513 ("unknown_nested_fmt_key", "fmt"),
7514 ("unknown_nested_lint_key", "lint"),
7515 ("unknown_nested_doc_key", "doc"),
7516 ("unknown_nested_fuzz_key", "fuzz"),
7517 ("unknown_nested_invariant_key", "invariant"),
7518 ("unknown_nested_symbolic_key", "symbolic"),
7519 ("unknown_nested_mutation_key", "mutation"),
7520 ("unknown_nested_vyper_key", "vyper"),
7521 ("unknown_nested_bind_json_key", "bind_json"),
7522 ];
7523
7524 for (expected_key, expected_section) in nested_expected {
7525 assert!(
7526 cfg.warnings.iter().any(|w| matches!(
7527 w,
7528 crate::Warning::UnknownSectionKey { key, section, .. }
7529 if key == expected_key && section == expected_section
7530 )),
7531 "Expected warning for '{}' in nested section '{}', got: {:?}",
7532 expected_key,
7533 expected_section,
7534 cfg.warnings
7535 );
7536 }
7537
7538 let array_expected = [
7540 ("unknown_compilation_key", "compilation_restrictions"),
7541 ("unknown_compiler_profile_key", "additional_compiler_profiles"),
7542 ];
7543
7544 for (expected_key, expected_section) in array_expected {
7545 assert!(
7546 cfg.warnings.iter().any(|w| matches!(
7547 w,
7548 crate::Warning::UnknownSectionKey { key, section, .. }
7549 if key == expected_key && section == expected_section
7550 )),
7551 "Expected warning for '{}' in array section '{}', got: {:?}",
7552 expected_key,
7553 expected_section,
7554 cfg.warnings
7555 );
7556 }
7557
7558 let unknown_key_warnings: Vec<_> = cfg
7560 .warnings
7561 .iter()
7562 .filter(|w| {
7563 matches!(w, crate::Warning::UnknownKey { .. })
7564 || matches!(w, crate::Warning::UnknownSectionKey { .. })
7565 })
7566 .collect();
7567
7568 assert_eq!(
7570 unknown_key_warnings.len(),
7571 21,
7572 "Expected 21 unknown key warnings (1 profile + 9 standalone + 9 nested + 2 array), got {}: {:?}",
7573 unknown_key_warnings.len(),
7574 unknown_key_warnings
7575 );
7576
7577 Ok(())
7578 });
7579 }
7580
7581 #[test]
7582 fn warns_on_unknown_keys_in_extended_config() {
7583 figment::Jail::expect_with(|jail| {
7584 jail.create_file(
7586 "base.toml",
7587 r#"
7588 [profile.default]
7589 optimizer_runs = 800
7590 unknown_base_profile_key = "should_warn"
7591
7592 [lint]
7593 severity = ["high"]
7594 unknown_base_lint_key = "should_warn"
7595
7596 [fmt]
7597 line_length = 100
7598 unknown_base_fmt_key = "should_warn"
7599 "#,
7600 )?;
7601
7602 jail.create_file(
7604 "foundry.toml",
7605 r#"
7606 [profile.default]
7607 extends = "base.toml"
7608 src = "src"
7609 unknown_local_profile_key = "should_warn"
7610
7611 [lint]
7612 unknown_local_lint_key = "should_warn"
7613
7614 [fuzz]
7615 runs = 512
7616 unknown_local_fuzz_key = "should_warn"
7617
7618 [[profile.default.compilation_restrictions]]
7619 paths = "src/*.sol"
7620 unknown_local_restriction_key = "should_warn"
7621 "#,
7622 )?;
7623
7624 let cfg = Config::load().unwrap();
7625
7626 assert_eq!(cfg.optimizer_runs, Some(800));
7628
7629 let expected_unknown_keys = ["unknown_base_profile_key", "unknown_local_profile_key"];
7637 for expected_key in expected_unknown_keys {
7638 assert!(
7639 cfg.warnings.iter().any(|w| matches!(
7640 w,
7641 crate::Warning::UnknownKey { key, .. } if key == expected_key
7642 )),
7643 "Expected warning for '{}', got: {:?}",
7644 expected_key,
7645 cfg.warnings
7646 );
7647 }
7648
7649 let expected_section_keys = [
7650 ("unknown_base_lint_key", "lint"),
7651 ("unknown_base_fmt_key", "fmt"),
7652 ("unknown_local_lint_key", "lint"),
7653 ("unknown_local_fuzz_key", "fuzz"),
7654 ("unknown_local_restriction_key", "compilation_restrictions"),
7655 ];
7656 for (expected_key, expected_section) in expected_section_keys {
7657 assert!(
7658 cfg.warnings.iter().any(|w| matches!(
7659 w,
7660 crate::Warning::UnknownSectionKey { key, section, .. }
7661 if key == expected_key && section == expected_section
7662 )),
7663 "Expected warning for '{}' in section '{}', got: {:?}",
7664 expected_key,
7665 expected_section,
7666 cfg.warnings
7667 );
7668 }
7669
7670 let unknown_warnings: Vec<_> = cfg
7672 .warnings
7673 .iter()
7674 .filter(|w| {
7675 matches!(w, crate::Warning::UnknownKey { .. })
7676 || matches!(w, crate::Warning::UnknownSectionKey { .. })
7677 })
7678 .collect();
7679 assert_eq!(
7680 unknown_warnings.len(),
7681 7,
7682 "Expected 7 unknown key warnings, got {}: {:?}",
7683 unknown_warnings.len(),
7684 unknown_warnings
7685 );
7686
7687 Ok(())
7688 });
7689 }
7690
7691 #[test]
7693 fn warns_on_unknown_profile() {
7694 figment::Jail::expect_with(|jail| {
7695 jail.create_file(
7696 "foundry.toml",
7697 r#"
7698 [profile.default]
7699 src = "src"
7700 "#,
7701 )?;
7702
7703 jail.set_env("FOUNDRY_PROFILE", "nonexistent");
7704 let cfg = Config::load().expect("expected unknown profile to fall back to default");
7705 assert_eq!(cfg.profile, Config::DEFAULT_PROFILE);
7706 assert!(
7707 cfg.warnings.iter().any(|w| matches!(
7708 w,
7709 crate::Warning::UnknownProfile { profile } if profile == "nonexistent"
7710 )),
7711 "Expected UnknownProfile warning, got: {:?}",
7712 cfg.warnings
7713 );
7714
7715 Ok(())
7716 });
7717 }
7718
7719 #[test]
7721 fn no_false_warnings_for_vyper_config_keys() {
7722 figment::Jail::expect_with(|jail| {
7723 jail.create_file(
7724 "foundry.toml",
7725 r#"
7726 [profile.default]
7727 src = "src"
7728
7729 [vyper]
7730 optimize = "O1"
7731 path = "/usr/bin/vyper"
7732 experimental_codegen = true
7733 venom_experimental = false
7734 debug = true
7735 enable_decimals = true
7736
7737 [vyper.venom]
7738 disable_cse = true
7739 inline_threshold = 15
7740 "#,
7741 )?;
7742
7743 let cfg = Config::load().unwrap();
7744 let vyper_warnings: Vec<_> = cfg
7746 .warnings
7747 .iter()
7748 .filter(|w| {
7749 matches!(
7750 w,
7751 crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
7752 )
7753 })
7754 .collect();
7755
7756 assert!(
7757 vyper_warnings.is_empty(),
7758 "Valid vyper keys should not trigger warnings, got: {vyper_warnings:?}"
7759 );
7760
7761 Ok(())
7762 });
7763 }
7764
7765 #[test]
7767 fn no_false_warnings_for_nested_vyper_config_keys() {
7768 figment::Jail::expect_with(|jail| {
7769 jail.create_file(
7770 "foundry.toml",
7771 r#"
7772 [profile.default]
7773 src = "src"
7774
7775 [profile.default.vyper]
7776 opt_level = "s"
7777 path = "/opt/vyper/bin/vyper"
7778 experimental_codegen = false
7779 debug = true
7780 enable_decimals = true
7781 venom = { disable_sccp = true, disable_mem2var = false }
7782 "#,
7783 )?;
7784
7785 let cfg = Config::load().unwrap();
7786 let vyper_warnings: Vec<_> = cfg
7788 .warnings
7789 .iter()
7790 .filter(|w| {
7791 matches!(
7792 w,
7793 crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
7794 )
7795 })
7796 .collect();
7797
7798 assert!(
7799 vyper_warnings.is_empty(),
7800 "Valid nested vyper keys should not trigger warnings, got: {vyper_warnings:?}"
7801 );
7802
7803 Ok(())
7804 });
7805 }
7806
7807 #[test]
7810 fn no_false_warnings_for_inline_vyper_config() {
7811 figment::Jail::expect_with(|jail| {
7812 jail.create_file(
7813 "foundry.toml",
7814 r#"
7815 [profile.default]
7816 src = "src"
7817 vyper = { optimize = "gas", debug = true }
7818
7819 [profile.default-venom]
7820 vyper = { opt_level = "O2", experimental_codegen = true, venom = { disable_cse = true } }
7821
7822 [profile.ci-venom]
7823 vyper = { venom_experimental = true, enable_decimals = true }
7824 "#,
7825 )?;
7826
7827 let cfg = Config::load().unwrap();
7828 let vyper_warnings: Vec<_> = cfg
7829 .warnings
7830 .iter()
7831 .filter(|w| {
7832 matches!(
7833 w,
7834 crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
7835 )
7836 })
7837 .collect();
7838
7839 assert!(
7840 vyper_warnings.is_empty(),
7841 "Valid inline vyper config should not trigger warnings, got: {vyper_warnings:?}"
7842 );
7843
7844 Ok(())
7845 });
7846 }
7847
7848 #[test]
7850 fn warns_on_unknown_vyper_keys() {
7851 figment::Jail::expect_with(|jail| {
7852 jail.create_file(
7853 "foundry.toml",
7854 r#"
7855 [profile.default]
7856 src = "src"
7857
7858 [vyper]
7859 optimize = "gas"
7860 unknown_vyper_option = true
7861 "#,
7862 )?;
7863
7864 let cfg = Config::load().unwrap();
7865 assert!(
7866 cfg.warnings.iter().any(|w| matches!(
7867 w,
7868 crate::Warning::UnknownSectionKey { key, section, .. }
7869 if key == "unknown_vyper_option" && section == "vyper"
7870 )),
7871 "Unknown vyper key should trigger warning, got: {:?}",
7872 cfg.warnings
7873 );
7874
7875 Ok(())
7876 });
7877 }
7878
7879 #[test]
7881 fn succeeds_on_known_profile() {
7882 figment::Jail::expect_with(|jail| {
7883 jail.create_file(
7884 "foundry.toml",
7885 r#"
7886 [profile.default]
7887 src = "src"
7888
7889 [profile.ci]
7890 src = "src"
7891 fuzz = { runs = 10000 }
7892 "#,
7893 )?;
7894
7895 jail.set_env("FOUNDRY_PROFILE", "ci");
7896 let config = Config::load().expect("known profile should work");
7897 assert_eq!(config.profile.as_str(), "ci");
7898 assert_eq!(config.fuzz.runs, 10000);
7899
7900 Ok(())
7901 });
7902 }
7903
7904 #[test]
7907 fn nested_lib_config_falls_back_to_default_profile() {
7908 figment::Jail::expect_with(|jail| {
7909 let lib_path = jail.directory().join("lib/mylib");
7911 std::fs::create_dir_all(&lib_path).unwrap();
7912 jail.create_file(
7913 "lib/mylib/foundry.toml",
7914 r#"
7915 [profile.default]
7916 src = "contracts"
7917 "#,
7918 )?;
7919
7920 jail.set_env("FOUNDRY_PROFILE", "ci");
7922
7923 let config = Config::load_with_root_and_fallback(&lib_path)
7925 .expect("lib config should load with fallback");
7926 assert_eq!(config.profile, Config::DEFAULT_PROFILE);
7927 assert_eq!(config.src.as_os_str(), "contracts");
7928
7929 Ok(())
7930 });
7931 }
7932
7933 #[test]
7935 fn nested_lib_config_uses_profile_if_exists() {
7936 figment::Jail::expect_with(|jail| {
7937 let lib_path = jail.directory().join("lib/mylib");
7939 std::fs::create_dir_all(&lib_path).unwrap();
7940 jail.create_file(
7941 "lib/mylib/foundry.toml",
7942 r#"
7943 [profile.default]
7944 src = "contracts"
7945
7946 [profile.ci]
7947 src = "contracts"
7948 fuzz = { runs = 5000 }
7949 "#,
7950 )?;
7951
7952 jail.set_env("FOUNDRY_PROFILE", "ci");
7954
7955 let config = Config::load_with_root_and_fallback(&lib_path)
7957 .expect("lib config should load with profile");
7958 assert_eq!(config.profile.as_str(), "ci");
7959 assert_eq!(config.fuzz.runs, 5000);
7960
7961 Ok(())
7962 });
7963 }
7964
7965 #[test]
7967 fn succeeds_on_hyphenated_profile_name() {
7968 figment::Jail::expect_with(|jail| {
7969 jail.create_file(
7970 "foundry.toml",
7971 r#"
7972 [profile.default]
7973 src = "src"
7974
7975 [profile.ci-venom]
7976 src = "src"
7977 fuzz = { runs = 7500 }
7978
7979 [profile.default-venom]
7980 src = "src"
7981 fuzz = { runs = 8000 }
7982 "#,
7983 )?;
7984
7985 jail.set_env("FOUNDRY_PROFILE", "ci-venom");
7987 let config = Config::load().expect("hyphenated profile should work");
7988 assert_eq!(config.profile.as_str(), "ci-venom");
7989 assert_eq!(config.fuzz.runs, 7500);
7990
7991 jail.set_env("FOUNDRY_PROFILE", "default-venom");
7993 let config = Config::load().expect("hyphenated profile should work");
7994 assert_eq!(config.profile.as_str(), "default-venom");
7995 assert_eq!(config.fuzz.runs, 8000);
7996
7997 assert!(
7999 config.profiles.iter().any(|p| p.as_str() == "ci-venom"),
8000 "profiles should contain 'ci-venom', got: {:?}",
8001 config.profiles
8002 );
8003 assert!(
8004 config.profiles.iter().any(|p| p.as_str() == "default-venom"),
8005 "profiles should contain 'default-venom', got: {:?}",
8006 config.profiles
8007 );
8008
8009 Ok(())
8010 });
8011 }
8012
8013 #[test]
8014 fn standalone_section_name_can_be_used_as_profile_name() {
8015 figment::Jail::expect_with(|jail| {
8016 jail.create_file(
8017 "foundry.toml",
8018 r#"
8019 [profile.symbolic]
8020 eth-rpc-url = "https://example.com/"
8021 "#,
8022 )?;
8023 jail.set_env("FOUNDRY_PROFILE", "symbolic");
8024
8025 let config = Config::load().unwrap();
8026 assert_eq!(config.profile.as_str(), "symbolic");
8027 assert_eq!(config.eth_rpc_url.as_deref(), Some("https://example.com/"));
8028
8029 Ok(())
8030 });
8031 }
8032
8033 #[test]
8035 fn hyphenated_profile_with_nested_sections() {
8036 figment::Jail::expect_with(|jail| {
8037 jail.create_file(
8038 "foundry.toml",
8039 r#"
8040 [profile.default]
8041 src = "src"
8042
8043 [profile.ci-venom]
8044 src = "src"
8045 optimizer_runs = 500
8046
8047 [profile.ci-venom.fuzz]
8048 runs = 10000
8049 max_test_rejects = 350000
8050
8051 [profile.ci-venom.invariant]
8052 runs = 375
8053 depth = 500
8054 "#,
8055 )?;
8056
8057 jail.set_env("FOUNDRY_PROFILE", "ci-venom");
8058 let config =
8059 Config::load().expect("hyphenated profile with nested sections should work");
8060 assert_eq!(config.profile.as_str(), "ci-venom");
8061 assert_eq!(config.optimizer_runs, Some(500));
8062 assert_eq!(config.fuzz.runs, 10000);
8063 assert_eq!(config.fuzz.max_test_rejects, 350000);
8064 assert_eq!(config.invariant.runs, 375);
8065 assert_eq!(config.invariant.depth, 500);
8066
8067 Ok(())
8068 });
8069 }
8070
8071 #[test]
8072 fn coverage_section_in_profile() {
8073 figment::Jail::expect_with(|jail| {
8074 jail.create_file(
8075 "foundry.toml",
8076 r#"
8077 [profile.default.coverage]
8078 report = ["summary", "lcov"]
8079 lcov_version = "2.2.0"
8080 ir_minimum = true
8081 report_file = "out/lcov.info"
8082 include_libs = true
8083 exclude_tests = true
8084 skip_files = ["test/**", "src/mocks/**"]
8085 "#,
8086 )?;
8087 let config = Config::load_with_root(jail.directory()).unwrap();
8088 assert_eq!(
8089 config.coverage.report,
8090 vec![CoverageReportKind::Summary, CoverageReportKind::Lcov]
8091 );
8092 assert_eq!(config.coverage.lcov_version, semver::Version::new(2, 2, 0));
8093 assert!(config.coverage.ir_minimum);
8094 assert_eq!(
8095 config.coverage.report_file.as_deref(),
8096 Some(std::path::Path::new("out/lcov.info"))
8097 );
8098 assert!(config.coverage.include_libs);
8099 assert!(config.coverage.exclude_tests);
8100 assert_eq!(
8101 config.coverage.skip_files,
8102 vec!["test/**".to_string(), "src/mocks/**".to_string()]
8103 );
8104 Ok(())
8105 });
8106 }
8107
8108 #[test]
8109 fn coverage_standalone_section_falls_back_to_default_profile() {
8110 figment::Jail::expect_with(|jail| {
8111 jail.create_file(
8114 "foundry.toml",
8115 r#"
8116 [coverage]
8117 skip_files = ["script/**"]
8118 exclude_tests = true
8119 "#,
8120 )?;
8121 let config = Config::load_with_root(jail.directory()).unwrap();
8122 assert_eq!(config.coverage.skip_files, vec!["script/**".to_string()]);
8123 assert!(config.coverage.exclude_tests);
8124 assert_eq!(config.coverage.report, vec![CoverageReportKind::Summary]);
8126 assert_eq!(config.coverage.lcov_version, semver::Version::new(1, 0, 0));
8127 Ok(())
8128 });
8129 }
8130
8131 #[test]
8132 fn coverage_per_profile_overrides_default() {
8133 figment::Jail::expect_with(|jail| {
8134 jail.create_file(
8135 "foundry.toml",
8136 r#"
8137 [profile.default.coverage]
8138 skip_files = ["script/**"]
8139
8140 [profile.ci.coverage]
8141 skip_files = ["test/**", "lib/**"]
8142 exclude_tests = true
8143 "#,
8144 )?;
8145
8146 let config = Config::load_with_root(jail.directory()).unwrap();
8148 assert_eq!(config.coverage.skip_files, vec!["script/**".to_string()]);
8149 assert!(!config.coverage.exclude_tests);
8150
8151 jail.set_env("FOUNDRY_PROFILE", "ci");
8153 let config = Config::load_with_root(jail.directory()).unwrap();
8154 assert_eq!(
8155 config.coverage.skip_files,
8156 vec!["test/**".to_string(), "lib/**".to_string()]
8157 );
8158 assert!(config.coverage.exclude_tests);
8159 Ok(())
8160 });
8161 }
8162}