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