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, Address, B256, U256};
13use eyre::{ContextCompat, WrapErr};
14use figment::{
15 providers::{Env, Format, Serialized, Toml},
16 value::{Dict, Map, Value},
17 Error, Figment, Metadata, Profile, Provider,
18};
19use filter::GlobMatcher;
20use foundry_compilers::{
21 artifacts::{
22 output_selection::{ContractOutputSelection, OutputSelection},
23 remappings::{RelativeRemapping, Remapping},
24 serde_helpers, BytecodeHash, DebuggingSettings, EofVersion, EvmVersion, Libraries,
25 ModelCheckerSettings, ModelCheckerTarget, Optimizer, OptimizerDetails, RevertStrings,
26 Settings, SettingsMetadata, Severity,
27 },
28 cache::SOLIDITY_FILES_CACHE_FILENAME,
29 compilers::{
30 multi::{MultiCompiler, MultiCompilerSettings},
31 solc::{Solc, SolcCompiler},
32 vyper::{Vyper, VyperSettings},
33 Compiler,
34 },
35 error::SolcError,
36 multi::{MultiCompilerParsedSource, MultiCompilerRestrictions},
37 solc::{CliSettings, SolcSettings},
38 ArtifactOutput, ConfigurableArtifacts, Graph, Project, ProjectPathsConfig,
39 RestrictionsWithVersion, VyperLanguage,
40};
41use regex::Regex;
42use revm_primitives::{map::AddressHashMap, FixedBytes, SpecId};
43use semver::Version;
44use serde::{Deserialize, Serialize, Serializer};
45use std::{
46 borrow::Cow,
47 collections::BTreeMap,
48 fs,
49 path::{Path, PathBuf},
50 str::FromStr,
51};
52
53mod macros;
54
55pub mod utils;
56pub use utils::*;
57
58mod endpoints;
59pub use endpoints::{
60 ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
61};
62
63mod etherscan;
64use etherscan::{
65 EtherscanConfigError, EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig,
66};
67
68mod resolve;
69pub use resolve::UnresolvedEnvVarError;
70
71pub mod cache;
72use cache::{Cache, ChainCache};
73
74pub mod fmt;
75pub use fmt::FormatterConfig;
76
77pub mod fs_permissions;
78pub use fs_permissions::FsPermissions;
79use fs_permissions::PathPermission;
80
81pub mod error;
82use error::ExtractConfigError;
83pub use error::SolidityErrorCode;
84
85pub mod doc;
86pub use doc::DocConfig;
87
88pub mod filter;
89pub use filter::SkipBuildFilters;
90
91mod warning;
92pub use warning::*;
93
94pub mod fix;
95
96pub use alloy_chains::{Chain, NamedChain};
98pub use figment;
99
100pub mod providers;
101pub use providers::Remappings;
102use providers::*;
103
104mod fuzz;
105pub use fuzz::{FuzzConfig, FuzzDictionaryConfig};
106
107mod invariant;
108pub use invariant::InvariantConfig;
109
110mod inline;
111pub use inline::{InlineConfig, InlineConfigError, NatSpec};
112
113pub mod soldeer;
114use soldeer::{SoldeerConfig, SoldeerDependencyConfig};
115
116mod vyper;
117use vyper::VyperConfig;
118
119mod bind_json;
120use bind_json::BindJsonConfig;
121
122mod compilation;
123pub use compilation::{CompilationRestrictions, SettingsOverrides};
124
125#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
157pub struct Config {
158 #[serde(skip)]
165 pub profile: Profile,
166 #[serde(skip)]
170 pub profiles: Vec<Profile>,
171
172 #[serde(default = "root_default", skip_serializing)]
177 pub root: PathBuf,
178
179 pub src: PathBuf,
181 pub test: PathBuf,
183 pub script: PathBuf,
185 pub out: PathBuf,
187 pub libs: Vec<PathBuf>,
189 pub remappings: Vec<RelativeRemapping>,
191 pub auto_detect_remappings: bool,
193 pub libraries: Vec<String>,
195 pub cache: bool,
197 pub cache_path: PathBuf,
199 pub snapshots: PathBuf,
201 pub gas_snapshot_check: bool,
203 pub gas_snapshot_emit: bool,
205 pub broadcast: PathBuf,
207 pub allow_paths: Vec<PathBuf>,
209 pub include_paths: Vec<PathBuf>,
211 pub skip: Vec<GlobMatcher>,
213 pub force: bool,
215 #[serde(with = "from_str_lowercase")]
217 pub evm_version: EvmVersion,
218 pub gas_reports: Vec<String>,
220 pub gas_reports_ignore: Vec<String>,
222 pub gas_reports_include_tests: bool,
224 #[doc(hidden)]
234 pub solc: Option<SolcReq>,
235 pub auto_detect_solc: bool,
237 pub offline: bool,
244 pub optimizer: Option<bool>,
246 pub optimizer_runs: Option<usize>,
257 pub optimizer_details: Option<OptimizerDetails>,
261 pub model_checker: Option<ModelCheckerSettings>,
263 pub verbosity: u8,
265 pub eth_rpc_url: Option<String>,
267 pub eth_rpc_jwt: Option<String>,
269 pub eth_rpc_timeout: Option<u64>,
271 pub eth_rpc_headers: Option<Vec<String>>,
280 pub etherscan_api_key: Option<String>,
282 #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
284 pub etherscan: EtherscanConfigs,
285 pub ignored_error_codes: Vec<SolidityErrorCode>,
287 #[serde(rename = "ignored_warnings_from")]
289 pub ignored_file_paths: Vec<PathBuf>,
290 pub deny_warnings: bool,
292 #[serde(rename = "match_test")]
294 pub test_pattern: Option<RegexWrapper>,
295 #[serde(rename = "no_match_test")]
297 pub test_pattern_inverse: Option<RegexWrapper>,
298 #[serde(rename = "match_contract")]
300 pub contract_pattern: Option<RegexWrapper>,
301 #[serde(rename = "no_match_contract")]
303 pub contract_pattern_inverse: Option<RegexWrapper>,
304 #[serde(rename = "match_path", with = "from_opt_glob")]
306 pub path_pattern: Option<globset::Glob>,
307 #[serde(rename = "no_match_path", with = "from_opt_glob")]
309 pub path_pattern_inverse: Option<globset::Glob>,
310 #[serde(rename = "no_match_coverage")]
312 pub coverage_pattern_inverse: Option<RegexWrapper>,
313 pub test_failures_file: PathBuf,
315 pub threads: Option<usize>,
317 pub show_progress: bool,
319 pub fuzz: FuzzConfig,
321 pub invariant: InvariantConfig,
323 pub ffi: bool,
325 pub allow_internal_expect_revert: bool,
327 pub always_use_create_2_factory: bool,
329 pub prompt_timeout: u64,
331 pub sender: Address,
333 pub tx_origin: Address,
335 pub initial_balance: U256,
337 pub block_number: u64,
339 pub fork_block_number: Option<u64>,
341 #[serde(rename = "chain_id", alias = "chain")]
343 pub chain: Option<Chain>,
344 pub gas_limit: GasLimit,
346 pub code_size_limit: Option<usize>,
348 pub gas_price: Option<u64>,
353 pub block_base_fee_per_gas: u64,
355 pub block_coinbase: Address,
357 pub block_timestamp: u64,
359 pub block_difficulty: u64,
361 pub block_prevrandao: B256,
363 pub block_gas_limit: Option<GasLimit>,
365 pub memory_limit: u64,
370 #[serde(default)]
387 pub extra_output: Vec<ContractOutputSelection>,
388 #[serde(default)]
399 pub extra_output_files: Vec<ContractOutputSelection>,
400 pub names: bool,
402 pub sizes: bool,
404 pub via_ir: bool,
407 pub ast: bool,
409 pub rpc_storage_caching: StorageCachingConfig,
411 pub no_storage_caching: bool,
414 pub no_rpc_rate_limit: bool,
417 #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
419 pub rpc_endpoints: RpcEndpoints,
420 pub use_literal_content: bool,
422 #[serde(with = "from_str_lowercase")]
426 pub bytecode_hash: BytecodeHash,
427 pub cbor_metadata: bool,
432 #[serde(with = "serde_helpers::display_from_str_opt")]
434 pub revert_strings: Option<RevertStrings>,
435 pub sparse_mode: bool,
440 pub build_info: bool,
443 pub build_info_path: Option<PathBuf>,
445 pub fmt: FormatterConfig,
447 pub doc: DocConfig,
449 pub bind_json: BindJsonConfig,
451 pub fs_permissions: FsPermissions,
455
456 pub isolate: bool,
460
461 pub disable_block_gas_limit: bool,
463
464 pub labels: AddressHashMap<String>,
466
467 pub unchecked_cheatcode_artifacts: bool,
470
471 pub create2_library_salt: B256,
473
474 pub create2_deployer: Address,
476
477 pub vyper: VyperConfig,
479
480 pub dependencies: Option<SoldeerDependencyConfig>,
482
483 pub soldeer: Option<SoldeerConfig>,
485
486 pub assertions_revert: bool,
490
491 pub legacy_assertions: bool,
493
494 #[serde(default, skip_serializing_if = "Vec::is_empty")]
496 pub extra_args: Vec<String>,
497
498 #[serde(default, skip_serializing_if = "Option::is_none")]
500 pub eof_version: Option<EofVersion>,
501
502 #[serde(alias = "alphanet")]
504 pub odyssey: bool,
505
506 pub transaction_timeout: u64,
508
509 pub eof: bool,
511
512 #[serde(rename = "__warnings", default, skip_serializing)]
514 pub warnings: Vec<Warning>,
515
516 #[serde(default)]
518 pub additional_compiler_profiles: Vec<SettingsOverrides>,
519
520 #[serde(default)]
522 pub compilation_restrictions: Vec<CompilationRestrictions>,
523
524 #[doc(hidden)]
533 #[serde(skip)]
534 pub _non_exhaustive: (),
535}
536
537pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
539
540pub const DEPRECATIONS: &[(&str, &str)] = &[("cancun", "evm_version = Cancun")];
544
545impl Config {
546 pub const DEFAULT_PROFILE: Profile = Profile::Default;
548
549 pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
551
552 pub const PROFILE_SECTION: &'static str = "profile";
554
555 pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
557 "rpc_endpoints",
558 "etherscan",
559 "fmt",
560 "doc",
561 "fuzz",
562 "invariant",
563 "labels",
564 "dependencies",
565 "soldeer",
566 "vyper",
567 "bind_json",
568 ];
569
570 pub const FILE_NAME: &'static str = "foundry.toml";
572
573 pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
575
576 pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
580
581 pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
583
584 pub const DEFAULT_CREATE2_DEPLOYER: Address =
586 address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");
587
588 pub fn load() -> Result<Self, ExtractConfigError> {
592 Self::from_provider(Self::figment())
593 }
594
595 pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
599 Self::from_provider(Self::default().to_figment(providers))
600 }
601
602 #[track_caller]
606 pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
607 Self::from_provider(Self::figment_with_root(root.as_ref()))
608 }
609
610 #[doc(alias = "try_from")]
625 pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
626 trace!("load config with provider: {:?}", provider.metadata());
627 Self::from_figment(Figment::from(provider))
628 }
629
630 #[doc(hidden)]
631 #[deprecated(note = "use `Config::from_provider` instead")]
632 pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
633 Self::from_provider(provider)
634 }
635
636 fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
637 let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
638 config.profile = figment.profile().clone();
639
640 let mut add_profile = |profile: &Profile| {
642 if !config.profiles.contains(profile) {
643 config.profiles.push(profile.clone());
644 }
645 };
646 let figment = figment.select(Self::PROFILE_SECTION);
647 if let Ok(data) = figment.data() {
648 if let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION)) {
649 for profile in profiles.keys() {
650 add_profile(&Profile::new(profile));
651 }
652 }
653 }
654 add_profile(&Self::DEFAULT_PROFILE);
655 add_profile(&config.profile);
656
657 config.normalize_optimizer_settings();
658
659 Ok(config)
660 }
661
662 pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
667 if providers.is_none() {
670 return Figment::from(self);
671 }
672
673 let root = self.root.as_path();
674 let profile = Self::selected_profile();
675 let mut figment = Figment::default().merge(DappHardhatDirProvider(root));
676
677 if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
679 figment = Self::merge_toml_provider(
680 figment,
681 TomlFileProvider::new(None, global_toml).cached(),
682 profile.clone(),
683 );
684 }
685 figment = Self::merge_toml_provider(
687 figment,
688 TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)).cached(),
689 profile.clone(),
690 );
691
692 figment = figment
694 .merge(
695 Env::prefixed("DAPP_")
696 .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
697 .global(),
698 )
699 .merge(
700 Env::prefixed("DAPP_TEST_")
701 .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
702 .global(),
703 )
704 .merge(DappEnvCompatProvider)
705 .merge(EtherscanEnvProvider::default())
706 .merge(
707 Env::prefixed("FOUNDRY_")
708 .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
709 .map(|key| {
710 let key = key.as_str();
711 if Self::STANDALONE_SECTIONS.iter().any(|section| {
712 key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
713 }) {
714 key.replacen('_', ".", 1).into()
715 } else {
716 key.into()
717 }
718 })
719 .global(),
720 )
721 .select(profile.clone());
722
723 if providers.is_all() {
725 let remappings = RemappingsProvider {
729 auto_detect_remappings: figment
730 .extract_inner::<bool>("auto_detect_remappings")
731 .unwrap_or(true),
732 lib_paths: figment
733 .extract_inner::<Vec<PathBuf>>("libs")
734 .map(Cow::Owned)
735 .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
736 root,
737 remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
738 };
739 figment = figment.merge(remappings);
740 }
741
742 figment = self.normalize_defaults(figment);
744
745 Figment::from(self).merge(figment).select(profile)
746 }
747
748 #[must_use]
753 pub fn canonic(self) -> Self {
754 let root = self.root.clone();
755 self.canonic_at(root)
756 }
757
758 #[must_use]
776 pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
777 let root = canonic(root);
778
779 fn p(root: &Path, rem: &Path) -> PathBuf {
780 canonic(root.join(rem))
781 }
782
783 self.src = p(&root, &self.src);
784 self.test = p(&root, &self.test);
785 self.script = p(&root, &self.script);
786 self.out = p(&root, &self.out);
787 self.broadcast = p(&root, &self.broadcast);
788 self.cache_path = p(&root, &self.cache_path);
789 self.snapshots = p(&root, &self.snapshots);
790
791 if let Some(build_info_path) = self.build_info_path {
792 self.build_info_path = Some(p(&root, &build_info_path));
793 }
794
795 self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
796
797 self.remappings =
798 self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
799
800 self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
801
802 self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
803
804 self.fs_permissions.join_all(&root);
805
806 if let Some(model_checker) = &mut self.model_checker {
807 model_checker.contracts = std::mem::take(&mut model_checker.contracts)
808 .into_iter()
809 .map(|(path, contracts)| {
810 (format!("{}", p(&root, path.as_ref()).display()), contracts)
811 })
812 .collect();
813 }
814
815 self
816 }
817
818 pub fn normalized_evm_version(mut self) -> Self {
820 self.normalize_evm_version();
821 self
822 }
823
824 pub fn normalized_optimizer_settings(mut self) -> Self {
827 self.normalize_optimizer_settings();
828 self
829 }
830
831 pub fn normalize_evm_version(&mut self) {
833 self.evm_version = self.get_normalized_evm_version();
834 }
835
836 pub fn normalize_optimizer_settings(&mut self) {
841 match (self.optimizer, self.optimizer_runs) {
842 (None, None) => {
844 self.optimizer = Some(false);
845 self.optimizer_runs = Some(200);
846 }
847 (Some(_), None) => self.optimizer_runs = Some(200),
849 (None, Some(runs)) => self.optimizer = Some(runs > 0),
851 _ => {}
852 }
853 }
854
855 pub fn get_normalized_evm_version(&self) -> EvmVersion {
857 if let Some(version) = self.solc_version() {
858 if let Some(evm_version) = self.evm_version.normalize_version_solc(&version) {
859 return evm_version;
860 }
861 }
862 self.evm_version
863 }
864
865 #[must_use]
870 pub fn sanitized(self) -> Self {
871 let mut config = self.canonic();
872
873 config.sanitize_remappings();
874
875 config.libs.sort_unstable();
876 config.libs.dedup();
877
878 config.sanitize_eof_settings();
879
880 config
881 }
882
883 pub fn sanitize_remappings(&mut self) {
887 #[cfg(target_os = "windows")]
888 {
889 use path_slash::PathBufExt;
891 self.remappings.iter_mut().for_each(|r| {
892 r.path.path = r.path.path.to_slash_lossy().into_owned().into();
893 });
894 }
895 }
896
897 pub fn sanitize_eof_settings(&mut self) {
902 if self.eof {
903 self.optimizer = Some(true);
904 self.normalize_optimizer_settings();
905
906 if self.eof_version.is_none() {
907 self.eof_version = Some(EofVersion::V1);
908 }
909
910 self.via_ir = true;
911 if self.evm_version < EvmVersion::Osaka {
912 self.evm_version = EvmVersion::Osaka;
913 }
914 }
915 }
916
917 pub fn install_lib_dir(&self) -> &Path {
921 self.libs
922 .iter()
923 .find(|p| !p.ends_with("node_modules"))
924 .map(|p| p.as_path())
925 .unwrap_or_else(|| Path::new("lib"))
926 }
927
928 pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
943 self.create_project(self.cache, false)
944 }
945
946 pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
949 self.create_project(false, true)
950 }
951
952 fn additional_settings(
954 &self,
955 base: &MultiCompilerSettings,
956 ) -> BTreeMap<String, MultiCompilerSettings> {
957 let mut map = BTreeMap::new();
958
959 for profile in &self.additional_compiler_profiles {
960 let mut settings = base.clone();
961 profile.apply(&mut settings);
962 map.insert(profile.name.clone(), settings);
963 }
964
965 map
966 }
967
968 #[expect(clippy::disallowed_macros)]
970 fn restrictions(
971 &self,
972 paths: &ProjectPathsConfig,
973 ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
974 {
975 let mut map = BTreeMap::new();
976 if self.compilation_restrictions.is_empty() {
977 return Ok(BTreeMap::new());
978 }
979
980 let graph = Graph::<MultiCompilerParsedSource>::resolve(paths)?;
981 let (sources, _) = graph.into_sources();
982
983 for res in &self.compilation_restrictions {
984 for source in sources.keys().filter(|path| {
985 if res.paths.is_match(path) {
986 true
987 } else if let Ok(path) = path.strip_prefix(&paths.root) {
988 res.paths.is_match(path)
989 } else {
990 false
991 }
992 }) {
993 let res: RestrictionsWithVersion<_> =
994 res.clone().try_into().map_err(SolcError::msg)?;
995 if !map.contains_key(source) {
996 map.insert(source.clone(), res);
997 } else {
998 let value = map.remove(source.as_path()).unwrap();
999 if let Some(merged) = value.clone().merge(res) {
1000 map.insert(source.clone(), merged);
1001 } else {
1002 eprintln!(
1004 "{}",
1005 yansi::Paint::yellow(&format!(
1006 "Failed to merge compilation restrictions for {}",
1007 source.display()
1008 ))
1009 );
1010 map.insert(source.clone(), value);
1011 }
1012 }
1013 }
1014 }
1015
1016 Ok(map)
1017 }
1018
1019 pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
1023 let settings = self.compiler_settings()?;
1024 let paths = self.project_paths();
1025 let mut builder = Project::builder()
1026 .artifacts(self.configured_artifacts_handler())
1027 .additional_settings(self.additional_settings(&settings))
1028 .restrictions(self.restrictions(&paths)?)
1029 .settings(settings)
1030 .paths(paths)
1031 .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
1032 .ignore_paths(self.ignored_file_paths.clone())
1033 .set_compiler_severity_filter(if self.deny_warnings {
1034 Severity::Warning
1035 } else {
1036 Severity::Error
1037 })
1038 .set_offline(self.offline)
1039 .set_cached(cached)
1040 .set_build_info(!no_artifacts && self.build_info)
1041 .set_no_artifacts(no_artifacts);
1042
1043 if !self.skip.is_empty() {
1044 let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
1045 builder = builder.sparse_output(filter);
1046 }
1047
1048 let project = builder.build(self.compiler()?)?;
1049
1050 if self.force {
1051 self.cleanup(&project)?;
1052 }
1053
1054 Ok(project)
1055 }
1056
1057 pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
1059 &self,
1060 project: &Project<C, T>,
1061 ) -> Result<(), SolcError> {
1062 project.cleanup()?;
1063
1064 let _ = fs::remove_file(&self.test_failures_file);
1066
1067 let remove_test_dir = |test_dir: &Option<PathBuf>| {
1069 if let Some(test_dir) = test_dir {
1070 let path = project.root().join(test_dir);
1071 if path.exists() {
1072 let _ = fs::remove_dir_all(&path);
1073 }
1074 }
1075 };
1076 remove_test_dir(&self.fuzz.failure_persist_dir);
1077 remove_test_dir(&self.invariant.failure_persist_dir);
1078
1079 Ok(())
1080 }
1081
1082 fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
1089 if let Some(solc) = &self.solc {
1090 let solc = match solc {
1091 SolcReq::Version(version) => {
1092 if let Some(solc) = Solc::find_svm_installed_version(version)? {
1093 solc
1094 } else {
1095 if self.offline {
1096 return Err(SolcError::msg(format!(
1097 "can't install missing solc {version} in offline mode"
1098 )));
1099 }
1100 Solc::blocking_install(version)?
1101 }
1102 }
1103 SolcReq::Local(solc) => {
1104 if !solc.is_file() {
1105 return Err(SolcError::msg(format!(
1106 "`solc` {} does not exist",
1107 solc.display()
1108 )));
1109 }
1110 Solc::new(solc)?
1111 }
1112 };
1113 return Ok(Some(solc));
1114 }
1115
1116 Ok(None)
1117 }
1118
1119 #[inline]
1121 pub fn evm_spec_id(&self) -> SpecId {
1122 evm_spec_id(self.evm_version, self.odyssey)
1123 }
1124
1125 pub fn is_auto_detect(&self) -> bool {
1130 if self.solc.is_some() {
1131 return false;
1132 }
1133 self.auto_detect_solc
1134 }
1135
1136 pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
1138 !self.no_storage_caching &&
1139 self.rpc_storage_caching.enable_for_chain_id(chain_id.into()) &&
1140 self.rpc_storage_caching.enable_for_endpoint(endpoint)
1141 }
1142
1143 pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
1158 let mut builder = ProjectPathsConfig::builder()
1159 .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
1160 .sources(&self.src)
1161 .tests(&self.test)
1162 .scripts(&self.script)
1163 .artifacts(&self.out)
1164 .libs(self.libs.iter())
1165 .remappings(self.get_all_remappings())
1166 .allowed_path(&self.root)
1167 .allowed_paths(&self.libs)
1168 .allowed_paths(&self.allow_paths)
1169 .include_paths(&self.include_paths);
1170
1171 if let Some(build_info_path) = &self.build_info_path {
1172 builder = builder.build_infos(build_info_path);
1173 }
1174
1175 builder.build_with_root(&self.root)
1176 }
1177
1178 pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
1180 if let Some(solc) = self.ensure_solc()? {
1181 Ok(SolcCompiler::Specific(solc))
1182 } else {
1183 Ok(SolcCompiler::AutoDetect)
1184 }
1185 }
1186
1187 pub fn solc_version(&self) -> Option<Version> {
1189 self.solc.as_ref().and_then(|solc| solc.try_version().ok())
1190 }
1191
1192 pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
1194 if self.project_paths::<VyperLanguage>().input_files_iter().next().is_none() {
1196 return Ok(None);
1197 }
1198 let vyper = if let Some(path) = &self.vyper.path {
1199 Some(Vyper::new(path)?)
1200 } else {
1201 Vyper::new("vyper").ok()
1202 };
1203
1204 Ok(vyper)
1205 }
1206
1207 pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
1209 Ok(MultiCompiler { solc: Some(self.solc_compiler()?), vyper: self.vyper_compiler()? })
1210 }
1211
1212 pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
1214 Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
1215 }
1216
1217 pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
1219 self.remappings.iter().map(|m| m.clone().into())
1220 }
1221
1222 pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1237 Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1238 }
1239
1240 pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1256 let maybe_alias = self.eth_rpc_url.as_ref().or(self.etherscan_api_key.as_ref())?;
1257 if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1258 Some(alias)
1259 } else {
1260 Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1261 }
1262 }
1263
1264 pub fn get_rpc_url_with_alias(
1289 &self,
1290 maybe_alias: &str,
1291 ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1292 let mut endpoints = self.rpc_endpoints.clone().resolved();
1293 if let Some(endpoint) = endpoints.remove(maybe_alias) {
1294 return Some(endpoint.url().map(Cow::Owned));
1295 }
1296
1297 if let Ok(Some(endpoint)) = mesc::get_endpoint_by_query(maybe_alias, Some("foundry")) {
1298 return Some(Ok(Cow::Owned(endpoint.url)));
1299 }
1300
1301 None
1302 }
1303
1304 pub fn get_rpc_url_or<'a>(
1316 &'a self,
1317 fallback: impl Into<Cow<'a, str>>,
1318 ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
1319 if let Some(url) = self.get_rpc_url() {
1320 url
1321 } else {
1322 Ok(fallback.into())
1323 }
1324 }
1325
1326 pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1338 self.get_rpc_url_or("http://localhost:8545")
1339 }
1340
1341 pub fn get_etherscan_config(
1361 &self,
1362 ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1363 self.get_etherscan_config_with_chain(None).transpose()
1364 }
1365
1366 pub fn get_etherscan_config_with_chain(
1373 &self,
1374 chain: Option<Chain>,
1375 ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1376 if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref()) {
1377 if self.etherscan.contains_key(maybe_alias) {
1378 return self.etherscan.clone().resolved().remove(maybe_alias).transpose();
1379 }
1380 }
1381
1382 if let Some(res) = chain
1384 .or(self.chain)
1385 .and_then(|chain| self.etherscan.clone().resolved().find_chain(chain))
1386 {
1387 match (res, self.etherscan_api_key.as_ref()) {
1388 (Ok(mut config), Some(key)) => {
1389 config.key.clone_from(key);
1392 return Ok(Some(config));
1393 }
1394 (Ok(config), None) => return Ok(Some(config)),
1395 (Err(err), None) => return Err(err),
1396 (Err(_), Some(_)) => {
1397 }
1399 }
1400 }
1401
1402 if let Some(key) = self.etherscan_api_key.as_ref() {
1404 let chain = chain.or(self.chain).unwrap_or_default();
1405 return Ok(ResolvedEtherscanConfig::create(key, chain));
1406 }
1407
1408 Ok(None)
1409 }
1410
1411 pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1417 self.get_etherscan_config_with_chain(chain).ok().flatten().map(|c| c.key)
1418 }
1419
1420 pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1427 get_dir_remapping(&self.src)
1428 }
1429
1430 pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1432 if self.root.join(&self.test).exists() {
1433 get_dir_remapping(&self.test)
1434 } else {
1435 None
1436 }
1437 }
1438
1439 pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1441 if self.root.join(&self.script).exists() {
1442 get_dir_remapping(&self.script)
1443 } else {
1444 None
1445 }
1446 }
1447
1448 pub fn optimizer(&self) -> Optimizer {
1454 Optimizer {
1455 enabled: self.optimizer,
1456 runs: self.optimizer_runs,
1457 details: self.optimizer_details.clone(),
1460 }
1461 }
1462
1463 pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1466 let mut extra_output = self.extra_output.clone();
1467
1468 if !extra_output.contains(&ContractOutputSelection::Metadata) {
1474 extra_output.push(ContractOutputSelection::Metadata);
1475 }
1476
1477 ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().cloned())
1478 }
1479
1480 pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1483 Libraries::parse(&self.libraries)
1484 }
1485
1486 pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1488 let paths: ProjectPathsConfig = self.project_paths();
1489 Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1490 }
1491
1492 pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
1497 let mut model_checker = self.model_checker.clone();
1501 if let Some(model_checker_settings) = &mut model_checker {
1502 if model_checker_settings.targets.is_none() {
1503 model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1504 }
1505 }
1506
1507 let mut settings = Settings {
1508 libraries: self.libraries_with_remappings()?,
1509 optimizer: self.optimizer(),
1510 evm_version: Some(self.evm_version),
1511 metadata: Some(SettingsMetadata {
1512 use_literal_content: Some(self.use_literal_content),
1513 bytecode_hash: Some(self.bytecode_hash),
1514 cbor_metadata: Some(self.cbor_metadata),
1515 }),
1516 debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1517 revert_strings: Some(revert_strings),
1518 debug_info: Vec::new(),
1520 }),
1521 model_checker,
1522 via_ir: Some(self.via_ir),
1523 stop_after: None,
1525 remappings: Vec::new(),
1527 output_selection: Default::default(),
1529 eof_version: self.eof_version,
1530 }
1531 .with_extra_output(self.configured_artifacts_handler().output_selection());
1532
1533 if self.ast || self.build_info {
1535 settings = settings.with_ast();
1536 }
1537
1538 let cli_settings =
1539 CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };
1540
1541 Ok(SolcSettings { settings, cli_settings })
1542 }
1543
1544 pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
1547 Ok(VyperSettings {
1548 evm_version: Some(self.evm_version),
1549 optimize: self.vyper.optimize,
1550 bytecode_metadata: None,
1551 output_selection: OutputSelection::common_output_selection([
1554 "abi".to_string(),
1555 "evm.bytecode".to_string(),
1556 "evm.deployedBytecode".to_string(),
1557 ]),
1558 search_paths: None,
1559 experimental_codegen: self.vyper.experimental_codegen,
1560 })
1561 }
1562
1563 pub fn figment() -> Figment {
1584 Self::default().into()
1585 }
1586
1587 pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
1599 Self::with_root(root.as_ref()).into()
1600 }
1601
1602 #[doc(hidden)]
1603 #[track_caller]
1604 pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
1605 let root = match root {
1606 Some(root) => root,
1607 None => &find_project_root(None).expect("could not determine project root"),
1608 };
1609 Self::figment_with_root(root)
1610 }
1611
1612 pub fn with_root(root: impl AsRef<Path>) -> Self {
1621 Self::_with_root(root.as_ref())
1622 }
1623
1624 fn _with_root(root: &Path) -> Self {
1625 let paths = ProjectPathsConfig::builder().build_with_root::<()>(root);
1627 let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
1628 Self {
1629 root: paths.root,
1630 src: paths.sources.file_name().unwrap().into(),
1631 out: artifacts.clone(),
1632 libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
1633 remappings: paths
1634 .remappings
1635 .into_iter()
1636 .map(|r| RelativeRemapping::new(r, root))
1637 .collect(),
1638 fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
1639 ..Self::default()
1640 }
1641 }
1642
1643 pub fn hardhat() -> Self {
1645 Self {
1646 src: "contracts".into(),
1647 out: "artifacts".into(),
1648 libs: vec!["node_modules".into()],
1649 ..Self::default()
1650 }
1651 }
1652
1653 pub fn dapptools() -> Self {
1655 Self {
1656 chain: Some(Chain::from_id(99)),
1657 block_timestamp: 0,
1658 block_number: 0,
1659 ..Self::default()
1660 }
1661 }
1662
1663 pub fn into_basic(self) -> BasicConfig {
1672 BasicConfig {
1673 profile: self.profile,
1674 src: self.src,
1675 out: self.out,
1676 libs: self.libs,
1677 remappings: self.remappings,
1678 }
1679 }
1680
1681 pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
1686 where
1687 F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
1688 {
1689 let config = Self::load_with_root(root)?.sanitized();
1690 config.update(|doc| f(&config, doc))
1691 }
1692
1693 pub fn update<F>(&self, f: F) -> eyre::Result<()>
1698 where
1699 F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
1700 {
1701 let file_path = self.get_config_path();
1702 if !file_path.exists() {
1703 return Ok(());
1704 }
1705 let contents = fs::read_to_string(&file_path)?;
1706 let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
1707 if f(&mut doc) {
1708 fs::write(file_path, doc.to_string())?;
1709 }
1710 Ok(())
1711 }
1712
1713 pub fn update_libs(&self) -> eyre::Result<()> {
1719 self.update(|doc| {
1720 let profile = self.profile.as_str().as_str();
1721 let root = &self.root;
1722 let libs: toml_edit::Value = self
1723 .libs
1724 .iter()
1725 .map(|path| {
1726 let path =
1727 if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
1728 toml_edit::Value::from(&*path.to_string_lossy())
1729 })
1730 .collect();
1731 let libs = toml_edit::value(libs);
1732 doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
1733 true
1734 })
1735 }
1736
1737 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
1749 let mut value = toml::Value::try_from(self)?;
1751 let value_table = value.as_table_mut().unwrap();
1753 let standalone_sections = Self::STANDALONE_SECTIONS
1755 .iter()
1756 .filter_map(|section| {
1757 let section = section.to_string();
1758 value_table.remove(§ion).map(|value| (section, value))
1759 })
1760 .collect::<Vec<_>>();
1761 let mut wrapping_table = [(
1763 Self::PROFILE_SECTION.into(),
1764 toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
1765 )]
1766 .into_iter()
1767 .collect::<toml::map::Map<_, _>>();
1768 for (section, value) in standalone_sections {
1770 wrapping_table.insert(section, value);
1771 }
1772 toml::to_string_pretty(&toml::Value::Table(wrapping_table))
1774 }
1775
1776 pub fn get_config_path(&self) -> PathBuf {
1778 self.root.join(Self::FILE_NAME)
1779 }
1780
1781 pub fn selected_profile() -> Profile {
1785 #[cfg(test)]
1787 {
1788 Self::force_selected_profile()
1789 }
1790 #[cfg(not(test))]
1791 {
1792 static CACHE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();
1793 CACHE.get_or_init(Self::force_selected_profile).clone()
1794 }
1795 }
1796
1797 fn force_selected_profile() -> Profile {
1798 Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
1799 }
1800
1801 pub fn foundry_dir_toml() -> Option<PathBuf> {
1803 Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
1804 }
1805
1806 pub fn foundry_dir() -> Option<PathBuf> {
1808 dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
1809 }
1810
1811 pub fn foundry_cache_dir() -> Option<PathBuf> {
1813 Self::foundry_dir().map(|p| p.join("cache"))
1814 }
1815
1816 pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
1818 Some(Self::foundry_cache_dir()?.join("rpc"))
1819 }
1820 pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1822 Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
1823 }
1824
1825 pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
1827 Some(Self::foundry_cache_dir()?.join("etherscan"))
1828 }
1829
1830 pub fn foundry_keystores_dir() -> Option<PathBuf> {
1832 Some(Self::foundry_dir()?.join("keystores"))
1833 }
1834
1835 pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1838 Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
1839 }
1840
1841 pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1844 Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
1845 }
1846
1847 pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1850 Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
1851 }
1852
1853 pub fn data_dir() -> eyre::Result<PathBuf> {
1861 let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
1862 std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
1863 Ok(path)
1864 }
1865
1866 pub fn find_config_file() -> Option<PathBuf> {
1873 fn find(path: &Path) -> Option<PathBuf> {
1874 if path.is_absolute() {
1875 return match path.is_file() {
1876 true => Some(path.to_path_buf()),
1877 false => None,
1878 };
1879 }
1880 let cwd = std::env::current_dir().ok()?;
1881 let mut cwd = cwd.as_path();
1882 loop {
1883 let file_path = cwd.join(path);
1884 if file_path.is_file() {
1885 return Some(file_path);
1886 }
1887 cwd = cwd.parent()?;
1888 }
1889 }
1890 find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
1891 .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
1892 }
1893
1894 pub fn clean_foundry_cache() -> eyre::Result<()> {
1896 if let Some(cache_dir) = Self::foundry_cache_dir() {
1897 let path = cache_dir.as_path();
1898 let _ = fs::remove_dir_all(path);
1899 } else {
1900 eyre::bail!("failed to get foundry_cache_dir");
1901 }
1902
1903 Ok(())
1904 }
1905
1906 pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<()> {
1908 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
1909 let path = cache_dir.as_path();
1910 let _ = fs::remove_dir_all(path);
1911 } else {
1912 eyre::bail!("failed to get foundry_chain_cache_dir");
1913 }
1914
1915 Ok(())
1916 }
1917
1918 pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<()> {
1920 if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
1921 let path = cache_dir.as_path();
1922 let _ = fs::remove_dir_all(path);
1923 } else {
1924 eyre::bail!("failed to get foundry_block_cache_dir");
1925 }
1926
1927 Ok(())
1928 }
1929
1930 pub fn clean_foundry_etherscan_cache() -> eyre::Result<()> {
1932 if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
1933 let path = cache_dir.as_path();
1934 let _ = fs::remove_dir_all(path);
1935 } else {
1936 eyre::bail!("failed to get foundry_etherscan_cache_dir");
1937 }
1938
1939 Ok(())
1940 }
1941
1942 pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<()> {
1944 if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
1945 let path = cache_dir.as_path();
1946 let _ = fs::remove_dir_all(path);
1947 } else {
1948 eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
1949 }
1950
1951 Ok(())
1952 }
1953
1954 pub fn list_foundry_cache() -> eyre::Result<Cache> {
1956 if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
1957 let mut cache = Cache { chains: vec![] };
1958 if !cache_dir.exists() {
1959 return Ok(cache);
1960 }
1961 if let Ok(entries) = cache_dir.as_path().read_dir() {
1962 for entry in entries.flatten().filter(|x| x.path().is_dir()) {
1963 match Chain::from_str(&entry.file_name().to_string_lossy()) {
1964 Ok(chain) => cache.chains.push(Self::list_foundry_chain_cache(chain)?),
1965 Err(_) => continue,
1966 }
1967 }
1968 Ok(cache)
1969 } else {
1970 eyre::bail!("failed to access foundry_cache_dir");
1971 }
1972 } else {
1973 eyre::bail!("failed to get foundry_cache_dir");
1974 }
1975 }
1976
1977 pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
1979 let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
1980 Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
1981 None => {
1982 warn!("failed to access foundry_etherscan_chain_cache_dir");
1983 0
1984 }
1985 };
1986
1987 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
1988 let blocks = Self::get_cached_blocks(&cache_dir)?;
1989 Ok(ChainCache {
1990 name: chain.to_string(),
1991 blocks,
1992 block_explorer: block_explorer_data_size,
1993 })
1994 } else {
1995 eyre::bail!("failed to get foundry_chain_cache_dir");
1996 }
1997 }
1998
1999 fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
2001 let mut blocks = vec![];
2002 if !chain_path.exists() {
2003 return Ok(blocks);
2004 }
2005 for block in chain_path.read_dir()?.flatten() {
2006 let file_type = block.file_type()?;
2007 let file_name = block.file_name();
2008 let filepath = if file_type.is_dir() {
2009 block.path().join("storage.json")
2010 } else if file_type.is_file() &&
2011 file_name.to_string_lossy().chars().all(char::is_numeric)
2012 {
2013 block.path()
2014 } else {
2015 continue;
2016 };
2017 blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
2018 }
2019 Ok(blocks)
2020 }
2021
2022 fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
2024 if !chain_path.exists() {
2025 return Ok(0);
2026 }
2027
2028 fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
2029 dir.try_fold(0, |acc, file| {
2030 let file = file?;
2031 let size = match file.metadata()? {
2032 data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
2033 data => data.len(),
2034 };
2035 Ok(acc + size)
2036 })
2037 }
2038
2039 dir_size_recursive(fs::read_dir(chain_path)?)
2040 }
2041
2042 fn merge_toml_provider(
2043 mut figment: Figment,
2044 toml_provider: impl Provider,
2045 profile: Profile,
2046 ) -> Figment {
2047 figment = figment.select(profile.clone());
2048
2049 figment = {
2051 let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
2052 figment.merge(warnings)
2053 };
2054
2055 let mut profiles = vec![Self::DEFAULT_PROFILE];
2057 if profile != Self::DEFAULT_PROFILE {
2058 profiles.push(profile.clone());
2059 }
2060 let provider = toml_provider.strict_select(profiles);
2061
2062 let provider = &BackwardsCompatTomlProvider(ForcedSnakeCaseData(provider));
2064
2065 if profile != Self::DEFAULT_PROFILE {
2067 figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
2068 }
2069 for standalone_key in Self::STANDALONE_SECTIONS {
2071 if let Some((_, fallback)) =
2072 STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
2073 {
2074 figment = figment.merge(
2075 provider
2076 .fallback(standalone_key, fallback)
2077 .wrap(profile.clone(), standalone_key),
2078 );
2079 } else {
2080 figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
2081 }
2082 }
2083 figment = figment.merge(provider);
2085 figment
2086 }
2087
2088 fn normalize_defaults(&self, mut figment: Figment) -> Figment {
2094 if figment.contains("evm_version") {
2096 return figment;
2097 }
2098
2099 if let Ok(solc) = figment.extract_inner::<SolcReq>("solc") {
2101 if let Some(version) = solc
2102 .try_version()
2103 .ok()
2104 .and_then(|version| self.evm_version.normalize_version_solc(&version))
2105 {
2106 figment = figment.merge(("evm_version", version));
2107 }
2108 }
2109
2110 figment
2111 }
2112}
2113
2114impl From<Config> for Figment {
2115 fn from(c: Config) -> Self {
2116 (&c).into()
2117 }
2118}
2119impl From<&Config> for Figment {
2120 fn from(c: &Config) -> Self {
2121 c.to_figment(FigmentProviders::All)
2122 }
2123}
2124
2125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2127pub enum FigmentProviders {
2128 #[default]
2130 All,
2131 Cast,
2135 Anvil,
2139 None,
2141}
2142
2143impl FigmentProviders {
2144 pub const fn is_all(&self) -> bool {
2146 matches!(self, Self::All)
2147 }
2148
2149 pub const fn is_cast(&self) -> bool {
2151 matches!(self, Self::Cast)
2152 }
2153
2154 pub const fn is_anvil(&self) -> bool {
2156 matches!(self, Self::Anvil)
2157 }
2158
2159 pub const fn is_none(&self) -> bool {
2161 matches!(self, Self::None)
2162 }
2163}
2164
2165#[derive(Clone, Debug, Serialize, Deserialize)]
2167#[serde(transparent)]
2168pub struct RegexWrapper {
2169 #[serde(with = "serde_regex")]
2170 inner: regex::Regex,
2171}
2172
2173impl std::ops::Deref for RegexWrapper {
2174 type Target = regex::Regex;
2175
2176 fn deref(&self) -> &Self::Target {
2177 &self.inner
2178 }
2179}
2180
2181impl std::cmp::PartialEq for RegexWrapper {
2182 fn eq(&self, other: &Self) -> bool {
2183 self.as_str() == other.as_str()
2184 }
2185}
2186
2187impl From<RegexWrapper> for regex::Regex {
2188 fn from(wrapper: RegexWrapper) -> Self {
2189 wrapper.inner
2190 }
2191}
2192
2193impl From<regex::Regex> for RegexWrapper {
2194 fn from(re: Regex) -> Self {
2195 Self { inner: re }
2196 }
2197}
2198
2199pub(crate) mod from_opt_glob {
2201 use serde::{Deserialize, Deserializer, Serializer};
2202
2203 pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
2204 where
2205 S: Serializer,
2206 {
2207 match value {
2208 Some(glob) => serializer.serialize_str(glob.glob()),
2209 None => serializer.serialize_none(),
2210 }
2211 }
2212
2213 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
2214 where
2215 D: Deserializer<'de>,
2216 {
2217 let s: Option<String> = Option::deserialize(deserializer)?;
2218 if let Some(s) = s {
2219 return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
2220 }
2221 Ok(None)
2222 }
2223}
2224
2225pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
2236 s: &str,
2237) -> Result<Option<(Profile, T)>, Error> {
2238 let figment = Config::merge_toml_provider(
2239 Figment::new(),
2240 Toml::string(s).nested(),
2241 Config::DEFAULT_PROFILE,
2242 );
2243 if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
2244 Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
2245 } else {
2246 Ok(None)
2247 }
2248}
2249
2250impl Provider for Config {
2251 fn metadata(&self) -> Metadata {
2252 Metadata::named("Foundry Config")
2253 }
2254
2255 #[track_caller]
2256 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2257 let mut data = Serialized::defaults(self).data()?;
2258 if let Some(entry) = data.get_mut(&self.profile) {
2259 entry.insert("root".to_string(), Value::serialize(self.root.clone())?);
2260 }
2261 Ok(data)
2262 }
2263
2264 fn profile(&self) -> Option<Profile> {
2265 Some(self.profile.clone())
2266 }
2267}
2268
2269impl Default for Config {
2270 fn default() -> Self {
2271 Self {
2272 profile: Self::DEFAULT_PROFILE,
2273 profiles: vec![Self::DEFAULT_PROFILE],
2274 fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2275 isolate: cfg!(feature = "isolate-by-default"),
2276 root: root_default(),
2277 src: "src".into(),
2278 test: "test".into(),
2279 script: "script".into(),
2280 out: "out".into(),
2281 libs: vec!["lib".into()],
2282 cache: true,
2283 cache_path: "cache".into(),
2284 broadcast: "broadcast".into(),
2285 snapshots: "snapshots".into(),
2286 gas_snapshot_check: false,
2287 gas_snapshot_emit: true,
2288 allow_paths: vec![],
2289 include_paths: vec![],
2290 force: false,
2291 evm_version: EvmVersion::Cancun,
2292 gas_reports: vec!["*".to_string()],
2293 gas_reports_ignore: vec![],
2294 gas_reports_include_tests: false,
2295 solc: None,
2296 vyper: Default::default(),
2297 auto_detect_solc: true,
2298 offline: false,
2299 optimizer: None,
2300 optimizer_runs: None,
2301 optimizer_details: None,
2302 model_checker: None,
2303 extra_output: Default::default(),
2304 extra_output_files: Default::default(),
2305 names: false,
2306 sizes: false,
2307 test_pattern: None,
2308 test_pattern_inverse: None,
2309 contract_pattern: None,
2310 contract_pattern_inverse: None,
2311 path_pattern: None,
2312 path_pattern_inverse: None,
2313 coverage_pattern_inverse: None,
2314 test_failures_file: "cache/test-failures".into(),
2315 threads: None,
2316 show_progress: false,
2317 fuzz: FuzzConfig::new("cache/fuzz".into()),
2318 invariant: InvariantConfig::new("cache/invariant".into()),
2319 always_use_create_2_factory: false,
2320 ffi: false,
2321 allow_internal_expect_revert: false,
2322 prompt_timeout: 120,
2323 sender: Self::DEFAULT_SENDER,
2324 tx_origin: Self::DEFAULT_SENDER,
2325 initial_balance: U256::from((1u128 << 96) - 1),
2326 block_number: 1,
2327 fork_block_number: None,
2328 chain: None,
2329 gas_limit: (1u64 << 30).into(), code_size_limit: None,
2331 gas_price: None,
2332 block_base_fee_per_gas: 0,
2333 block_coinbase: Address::ZERO,
2334 block_timestamp: 1,
2335 block_difficulty: 0,
2336 block_prevrandao: Default::default(),
2337 block_gas_limit: None,
2338 disable_block_gas_limit: false,
2339 memory_limit: 1 << 27, eth_rpc_url: None,
2341 eth_rpc_jwt: None,
2342 eth_rpc_timeout: None,
2343 eth_rpc_headers: None,
2344 etherscan_api_key: None,
2345 verbosity: 0,
2346 remappings: vec![],
2347 auto_detect_remappings: true,
2348 libraries: vec![],
2349 ignored_error_codes: vec![
2350 SolidityErrorCode::SpdxLicenseNotProvided,
2351 SolidityErrorCode::ContractExceeds24576Bytes,
2352 SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2353 SolidityErrorCode::TransientStorageUsed,
2354 ],
2355 ignored_file_paths: vec![],
2356 deny_warnings: false,
2357 via_ir: false,
2358 ast: false,
2359 rpc_storage_caching: Default::default(),
2360 rpc_endpoints: Default::default(),
2361 etherscan: Default::default(),
2362 no_storage_caching: false,
2363 no_rpc_rate_limit: false,
2364 use_literal_content: false,
2365 bytecode_hash: BytecodeHash::Ipfs,
2366 cbor_metadata: true,
2367 revert_strings: None,
2368 sparse_mode: false,
2369 build_info: false,
2370 build_info_path: None,
2371 fmt: Default::default(),
2372 doc: Default::default(),
2373 bind_json: Default::default(),
2374 labels: Default::default(),
2375 unchecked_cheatcode_artifacts: false,
2376 create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
2377 create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
2378 skip: vec![],
2379 dependencies: Default::default(),
2380 soldeer: Default::default(),
2381 assertions_revert: true,
2382 legacy_assertions: false,
2383 warnings: vec![],
2384 extra_args: vec![],
2385 eof_version: None,
2386 odyssey: false,
2387 transaction_timeout: 120,
2388 additional_compiler_profiles: Default::default(),
2389 compilation_restrictions: Default::default(),
2390 eof: false,
2391 _non_exhaustive: (),
2392 }
2393 }
2394}
2395
2396#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
2402pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);
2403
2404impl From<u64> for GasLimit {
2405 fn from(gas: u64) -> Self {
2406 Self(gas)
2407 }
2408}
2409
2410impl From<GasLimit> for u64 {
2411 fn from(gas: GasLimit) -> Self {
2412 gas.0
2413 }
2414}
2415
2416impl Serialize for GasLimit {
2417 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2418 where
2419 S: Serializer,
2420 {
2421 if self.0 == u64::MAX {
2422 serializer.serialize_str("max")
2423 } else if self.0 > i64::MAX as u64 {
2424 serializer.serialize_str(&self.0.to_string())
2425 } else {
2426 serializer.serialize_u64(self.0)
2427 }
2428 }
2429}
2430
2431#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2433#[serde(untagged)]
2434pub enum SolcReq {
2435 Version(Version),
2438 Local(PathBuf),
2440}
2441
2442impl SolcReq {
2443 fn try_version(&self) -> Result<Version, SolcError> {
2448 match self {
2449 Self::Version(version) => Ok(version.clone()),
2450 Self::Local(path) => Solc::new(path).map(|solc| solc.version),
2451 }
2452 }
2453}
2454
2455impl<T: AsRef<str>> From<T> for SolcReq {
2456 fn from(s: T) -> Self {
2457 let s = s.as_ref();
2458 if let Ok(v) = Version::from_str(s) {
2459 Self::Version(v)
2460 } else {
2461 Self::Local(s.into())
2462 }
2463 }
2464}
2465
2466#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2478pub struct BasicConfig {
2479 #[serde(skip)]
2481 pub profile: Profile,
2482 pub src: PathBuf,
2484 pub out: PathBuf,
2486 pub libs: Vec<PathBuf>,
2488 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2490 pub remappings: Vec<RelativeRemapping>,
2491}
2492
2493impl BasicConfig {
2494 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2498 let s = toml::to_string_pretty(self)?;
2499 Ok(format!(
2500 "\
2501[profile.{}]
2502{s}
2503# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n",
2504 self.profile
2505 ))
2506 }
2507}
2508
2509pub(crate) mod from_str_lowercase {
2510 use serde::{Deserialize, Deserializer, Serializer};
2511 use std::str::FromStr;
2512
2513 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
2514 where
2515 T: std::fmt::Display,
2516 S: Serializer,
2517 {
2518 serializer.collect_str(&value.to_string().to_lowercase())
2519 }
2520
2521 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
2522 where
2523 D: Deserializer<'de>,
2524 T: FromStr,
2525 T::Err: std::fmt::Display,
2526 {
2527 String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
2528 }
2529}
2530
2531fn canonic(path: impl Into<PathBuf>) -> PathBuf {
2532 let path = path.into();
2533 foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
2534}
2535
2536fn root_default() -> PathBuf {
2537 ".".into()
2538}
2539
2540#[cfg(test)]
2541mod tests {
2542 use super::*;
2543 use crate::{
2544 cache::{CachedChains, CachedEndpoints},
2545 endpoints::{RpcEndpoint, RpcEndpointType},
2546 etherscan::ResolvedEtherscanConfigs,
2547 };
2548 use endpoints::{RpcAuth, RpcEndpointConfig};
2549 use figment::error::Kind::InvalidType;
2550 use foundry_compilers::artifacts::{
2551 vyper::VyperOptimizationMode, ModelCheckerEngine, YulDetails,
2552 };
2553 use similar_asserts::assert_eq;
2554 use soldeer::RemappingsLocation;
2555 use std::{collections::BTreeMap, fs::File, io::Write};
2556 use tempfile::tempdir;
2557 use NamedChain::Moonbeam;
2558
2559 fn clear_warning(config: &mut Config) {
2562 config.warnings = vec![];
2563 }
2564
2565 #[test]
2566 fn default_sender() {
2567 assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
2568 }
2569
2570 #[test]
2571 fn test_caching() {
2572 let mut config = Config::default();
2573 let chain_id = NamedChain::Mainnet;
2574 let url = "https://eth-mainnet.alchemyapi";
2575 assert!(config.enable_caching(url, chain_id));
2576
2577 config.no_storage_caching = true;
2578 assert!(!config.enable_caching(url, chain_id));
2579
2580 config.no_storage_caching = false;
2581 assert!(!config.enable_caching(url, NamedChain::Dev));
2582 }
2583
2584 #[test]
2585 fn test_install_dir() {
2586 figment::Jail::expect_with(|jail| {
2587 let config = Config::load().unwrap();
2588 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2589 jail.create_file(
2590 "foundry.toml",
2591 r"
2592 [profile.default]
2593 libs = ['node_modules', 'lib']
2594 ",
2595 )?;
2596 let config = Config::load().unwrap();
2597 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2598
2599 jail.create_file(
2600 "foundry.toml",
2601 r"
2602 [profile.default]
2603 libs = ['custom', 'node_modules', 'lib']
2604 ",
2605 )?;
2606 let config = Config::load().unwrap();
2607 assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
2608
2609 Ok(())
2610 });
2611 }
2612
2613 #[test]
2614 fn test_figment_is_default() {
2615 figment::Jail::expect_with(|_| {
2616 let mut default: Config = Config::figment().extract()?;
2617 let default2 = Config::default();
2618 default.profile = default2.profile.clone();
2619 default.profiles = default2.profiles.clone();
2620 assert_eq!(default, default2);
2621 Ok(())
2622 });
2623 }
2624
2625 #[test]
2626 fn figment_profiles() {
2627 figment::Jail::expect_with(|jail| {
2628 jail.create_file(
2629 "foundry.toml",
2630 r"
2631 [foo.baz]
2632 libs = ['node_modules', 'lib']
2633
2634 [profile.default]
2635 libs = ['node_modules', 'lib']
2636
2637 [profile.ci]
2638 libs = ['node_modules', 'lib']
2639
2640 [profile.local]
2641 libs = ['node_modules', 'lib']
2642 ",
2643 )?;
2644
2645 let config = crate::Config::load().unwrap();
2646 let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
2647 assert_eq!(config.profiles, expected);
2648
2649 Ok(())
2650 });
2651 }
2652
2653 #[test]
2654 fn test_default_round_trip() {
2655 figment::Jail::expect_with(|_| {
2656 let original = Config::figment();
2657 let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
2658 for figment in &[original, roundtrip] {
2659 let config = Config::from_provider(figment).unwrap();
2660 assert_eq!(config, Config::default().normalized_optimizer_settings());
2661 }
2662 Ok(())
2663 });
2664 }
2665
2666 #[test]
2667 fn ffi_env_disallowed() {
2668 figment::Jail::expect_with(|jail| {
2669 jail.set_env("FOUNDRY_FFI", "true");
2670 jail.set_env("FFI", "true");
2671 jail.set_env("DAPP_FFI", "true");
2672 let config = Config::load().unwrap();
2673 assert!(!config.ffi);
2674
2675 Ok(())
2676 });
2677 }
2678
2679 #[test]
2680 fn test_profile_env() {
2681 figment::Jail::expect_with(|jail| {
2682 jail.set_env("FOUNDRY_PROFILE", "default");
2683 let figment = Config::figment();
2684 assert_eq!(figment.profile(), "default");
2685
2686 jail.set_env("FOUNDRY_PROFILE", "hardhat");
2687 let figment: Figment = Config::hardhat().into();
2688 assert_eq!(figment.profile(), "hardhat");
2689
2690 jail.create_file(
2691 "foundry.toml",
2692 r"
2693 [profile.default]
2694 libs = ['lib']
2695 [profile.local]
2696 libs = ['modules']
2697 ",
2698 )?;
2699 jail.set_env("FOUNDRY_PROFILE", "local");
2700 let config = Config::load().unwrap();
2701 assert_eq!(config.libs, vec![PathBuf::from("modules")]);
2702
2703 Ok(())
2704 });
2705 }
2706
2707 #[test]
2708 fn test_default_test_path() {
2709 figment::Jail::expect_with(|_| {
2710 let config = Config::default();
2711 let paths_config = config.project_paths::<Solc>();
2712 assert_eq!(paths_config.tests, PathBuf::from(r"test"));
2713 Ok(())
2714 });
2715 }
2716
2717 #[test]
2718 fn test_default_libs() {
2719 figment::Jail::expect_with(|jail| {
2720 let config = Config::load().unwrap();
2721 assert_eq!(config.libs, vec![PathBuf::from("lib")]);
2722
2723 fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
2724 let config = Config::load().unwrap();
2725 assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
2726
2727 fs::create_dir_all(jail.directory().join("lib")).unwrap();
2728 let config = Config::load().unwrap();
2729 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2730
2731 Ok(())
2732 });
2733 }
2734
2735 #[test]
2736 fn test_inheritance_from_default_test_path() {
2737 figment::Jail::expect_with(|jail| {
2738 jail.create_file(
2739 "foundry.toml",
2740 r#"
2741 [profile.default]
2742 test = "defaulttest"
2743 src = "defaultsrc"
2744 libs = ['lib', 'node_modules']
2745
2746 [profile.custom]
2747 src = "customsrc"
2748 "#,
2749 )?;
2750
2751 let config = Config::load().unwrap();
2752 assert_eq!(config.src, PathBuf::from("defaultsrc"));
2753 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2754
2755 jail.set_env("FOUNDRY_PROFILE", "custom");
2756 let config = Config::load().unwrap();
2757 assert_eq!(config.src, PathBuf::from("customsrc"));
2758 assert_eq!(config.test, PathBuf::from("defaulttest"));
2759 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2760
2761 Ok(())
2762 });
2763 }
2764
2765 #[test]
2766 fn test_custom_test_path() {
2767 figment::Jail::expect_with(|jail| {
2768 jail.create_file(
2769 "foundry.toml",
2770 r#"
2771 [profile.default]
2772 test = "mytest"
2773 "#,
2774 )?;
2775
2776 let config = Config::load().unwrap();
2777 let paths_config = config.project_paths::<Solc>();
2778 assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
2779 Ok(())
2780 });
2781 }
2782
2783 #[test]
2784 fn test_remappings() {
2785 figment::Jail::expect_with(|jail| {
2786 jail.create_file(
2787 "foundry.toml",
2788 r#"
2789 [profile.default]
2790 src = "some-source"
2791 out = "some-out"
2792 cache = true
2793 "#,
2794 )?;
2795 let config = Config::load().unwrap();
2796 assert!(config.remappings.is_empty());
2797
2798 jail.create_file(
2799 "remappings.txt",
2800 r"
2801 file-ds-test/=lib/ds-test/
2802 file-other/=lib/other/
2803 ",
2804 )?;
2805
2806 let config = Config::load().unwrap();
2807 assert_eq!(
2808 config.remappings,
2809 vec![
2810 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
2811 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
2812 ],
2813 );
2814
2815 jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
2816 let config = Config::load().unwrap();
2817
2818 assert_eq!(
2819 config.remappings,
2820 vec![
2821 Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
2823 Remapping::from_str("other/=lib/other/").unwrap().into(),
2824 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
2826 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
2827 ],
2828 );
2829
2830 Ok(())
2831 });
2832 }
2833
2834 #[test]
2835 fn test_remappings_override() {
2836 figment::Jail::expect_with(|jail| {
2837 jail.create_file(
2838 "foundry.toml",
2839 r#"
2840 [profile.default]
2841 src = "some-source"
2842 out = "some-out"
2843 cache = true
2844 "#,
2845 )?;
2846 let config = Config::load().unwrap();
2847 assert!(config.remappings.is_empty());
2848
2849 jail.create_file(
2850 "remappings.txt",
2851 r"
2852 ds-test/=lib/ds-test/
2853 other/=lib/other/
2854 ",
2855 )?;
2856
2857 let config = Config::load().unwrap();
2858 assert_eq!(
2859 config.remappings,
2860 vec![
2861 Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
2862 Remapping::from_str("other/=lib/other/").unwrap().into(),
2863 ],
2864 );
2865
2866 jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
2867 let config = Config::load().unwrap();
2868
2869 assert_eq!(
2874 config.remappings,
2875 vec![
2876 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
2877 Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
2878 Remapping::from_str("other/=lib/other/").unwrap().into(),
2879 ],
2880 );
2881
2882 assert_eq!(
2884 config.get_all_remappings().collect::<Vec<_>>(),
2885 vec![
2886 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
2887 Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
2888 Remapping::from_str("other/=lib/other/").unwrap(),
2889 ],
2890 );
2891
2892 Ok(())
2893 });
2894 }
2895
2896 #[test]
2897 fn test_can_update_libs() {
2898 figment::Jail::expect_with(|jail| {
2899 jail.create_file(
2900 "foundry.toml",
2901 r#"
2902 [profile.default]
2903 libs = ["node_modules"]
2904 "#,
2905 )?;
2906
2907 let mut config = Config::load().unwrap();
2908 config.libs.push("libs".into());
2909 config.update_libs().unwrap();
2910
2911 let config = Config::load().unwrap();
2912 assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
2913 Ok(())
2914 });
2915 }
2916
2917 #[test]
2918 fn test_large_gas_limit() {
2919 figment::Jail::expect_with(|jail| {
2920 let gas = u64::MAX;
2921 jail.create_file(
2922 "foundry.toml",
2923 &format!(
2924 r#"
2925 [profile.default]
2926 gas_limit = "{gas}"
2927 "#
2928 ),
2929 )?;
2930
2931 let config = Config::load().unwrap();
2932 assert_eq!(
2933 config,
2934 Config {
2935 gas_limit: gas.into(),
2936 ..Config::default().normalized_optimizer_settings()
2937 }
2938 );
2939
2940 Ok(())
2941 });
2942 }
2943
2944 #[test]
2945 #[should_panic]
2946 fn test_toml_file_parse_failure() {
2947 figment::Jail::expect_with(|jail| {
2948 jail.create_file(
2949 "foundry.toml",
2950 r#"
2951 [profile.default]
2952 eth_rpc_url = "https://example.com/
2953 "#,
2954 )?;
2955
2956 let _config = Config::load().unwrap();
2957
2958 Ok(())
2959 });
2960 }
2961
2962 #[test]
2963 #[should_panic]
2964 fn test_toml_file_non_existing_config_var_failure() {
2965 figment::Jail::expect_with(|jail| {
2966 jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
2967
2968 let _config = Config::load().unwrap();
2969
2970 Ok(())
2971 });
2972 }
2973
2974 #[test]
2975 fn test_resolve_etherscan_with_chain() {
2976 figment::Jail::expect_with(|jail| {
2977 let env_key = "__BSC_ETHERSCAN_API_KEY";
2978 let env_value = "env value";
2979 jail.create_file(
2980 "foundry.toml",
2981 r#"
2982 [profile.default]
2983
2984 [etherscan]
2985 bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
2986 "#,
2987 )?;
2988
2989 let config = Config::load().unwrap();
2990 assert!(config
2991 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
2992 .is_err());
2993
2994 std::env::set_var(env_key, env_value);
2995
2996 assert_eq!(
2997 config
2998 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
2999 .unwrap()
3000 .unwrap()
3001 .key,
3002 env_value
3003 );
3004
3005 let mut with_key = config;
3006 with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3007
3008 assert_eq!(
3009 with_key
3010 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3011 .unwrap()
3012 .unwrap()
3013 .key,
3014 "via etherscan_api_key"
3015 );
3016
3017 std::env::remove_var(env_key);
3018 Ok(())
3019 });
3020 }
3021
3022 #[test]
3023 fn test_resolve_etherscan() {
3024 figment::Jail::expect_with(|jail| {
3025 jail.create_file(
3026 "foundry.toml",
3027 r#"
3028 [profile.default]
3029
3030 [etherscan]
3031 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3032 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3033 "#,
3034 )?;
3035
3036 let config = Config::load().unwrap();
3037
3038 assert!(config.etherscan.clone().resolved().has_unresolved());
3039
3040 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3041
3042 let configs = config.etherscan.resolved();
3043 assert!(!configs.has_unresolved());
3044
3045 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3046 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3047 assert_eq!(
3048 configs,
3049 ResolvedEtherscanConfigs::new([
3050 (
3051 "mainnet",
3052 ResolvedEtherscanConfig {
3053 api_url: mainnet_urls.0.to_string(),
3054 chain: Some(NamedChain::Mainnet.into()),
3055 browser_url: Some(mainnet_urls.1.to_string()),
3056 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3057 }
3058 ),
3059 (
3060 "moonbeam",
3061 ResolvedEtherscanConfig {
3062 api_url: mb_urls.0.to_string(),
3063 chain: Some(Moonbeam.into()),
3064 browser_url: Some(mb_urls.1.to_string()),
3065 key: "123456789".to_string(),
3066 }
3067 ),
3068 ])
3069 );
3070
3071 Ok(())
3072 });
3073 }
3074
3075 #[test]
3076 fn test_resolve_etherscan_chain_id() {
3077 figment::Jail::expect_with(|jail| {
3078 jail.create_file(
3079 "foundry.toml",
3080 r#"
3081 [profile.default]
3082 chain_id = "sepolia"
3083
3084 [etherscan]
3085 sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3086 "#,
3087 )?;
3088
3089 let config = Config::load().unwrap();
3090 let etherscan = config.get_etherscan_config().unwrap().unwrap();
3091 assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3092 assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3093
3094 Ok(())
3095 });
3096 }
3097
3098 #[test]
3099 fn test_resolve_rpc_url() {
3100 figment::Jail::expect_with(|jail| {
3101 jail.create_file(
3102 "foundry.toml",
3103 r#"
3104 [profile.default]
3105 [rpc_endpoints]
3106 optimism = "https://example.com/"
3107 mainnet = "${_CONFIG_MAINNET}"
3108 "#,
3109 )?;
3110 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3111
3112 let mut config = Config::load().unwrap();
3113 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3114
3115 config.eth_rpc_url = Some("mainnet".to_string());
3116 assert_eq!(
3117 "https://eth-mainnet.alchemyapi.io/v2/123455",
3118 config.get_rpc_url_or_localhost_http().unwrap()
3119 );
3120
3121 config.eth_rpc_url = Some("optimism".to_string());
3122 assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
3123
3124 Ok(())
3125 })
3126 }
3127
3128 #[test]
3129 fn test_resolve_rpc_url_if_etherscan_set() {
3130 figment::Jail::expect_with(|jail| {
3131 jail.create_file(
3132 "foundry.toml",
3133 r#"
3134 [profile.default]
3135 etherscan_api_key = "dummy"
3136 [rpc_endpoints]
3137 optimism = "https://example.com/"
3138 "#,
3139 )?;
3140
3141 let config = Config::load().unwrap();
3142 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3143
3144 Ok(())
3145 })
3146 }
3147
3148 #[test]
3149 fn test_resolve_rpc_url_alias() {
3150 figment::Jail::expect_with(|jail| {
3151 jail.create_file(
3152 "foundry.toml",
3153 r#"
3154 [profile.default]
3155 [rpc_endpoints]
3156 polygonMumbai = "https://polygon-mumbai.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
3157 "#,
3158 )?;
3159 let mut config = Config::load().unwrap();
3160 config.eth_rpc_url = Some("polygonMumbai".to_string());
3161 assert!(config.get_rpc_url().unwrap().is_err());
3162
3163 jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
3164
3165 let mut config = Config::load().unwrap();
3166 config.eth_rpc_url = Some("polygonMumbai".to_string());
3167 assert_eq!(
3168 "https://polygon-mumbai.g.alchemy.com/v2/123455",
3169 config.get_rpc_url().unwrap().unwrap()
3170 );
3171
3172 Ok(())
3173 })
3174 }
3175
3176 #[test]
3177 fn test_resolve_rpc_aliases() {
3178 figment::Jail::expect_with(|jail| {
3179 jail.create_file(
3180 "foundry.toml",
3181 r#"
3182 [profile.default]
3183 [etherscan]
3184 arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
3185 [rpc_endpoints]
3186 arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
3187 "#,
3188 )?;
3189
3190 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
3191 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
3192
3193 let config = Config::load().unwrap();
3194
3195 let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
3196 assert!(config.is_err());
3197 assert_eq!(config.unwrap_err().to_string(), "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`");
3198
3199 Ok(())
3200 });
3201 }
3202
3203 #[test]
3204 fn test_resolve_rpc_config() {
3205 figment::Jail::expect_with(|jail| {
3206 jail.create_file(
3207 "foundry.toml",
3208 r#"
3209 [rpc_endpoints]
3210 optimism = "https://example.com/"
3211 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
3212 "#,
3213 )?;
3214 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3215
3216 let config = Config::load().unwrap();
3217 assert_eq!(
3218 RpcEndpoints::new([
3219 (
3220 "optimism",
3221 RpcEndpointType::String(RpcEndpointUrl::Url(
3222 "https://example.com/".to_string()
3223 ))
3224 ),
3225 (
3226 "mainnet",
3227 RpcEndpointType::Config(RpcEndpoint {
3228 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3229 config: RpcEndpointConfig {
3230 retries: Some(3),
3231 retry_backoff: Some(1000),
3232 compute_units_per_second: Some(1000),
3233 },
3234 auth: None,
3235 })
3236 ),
3237 ]),
3238 config.rpc_endpoints
3239 );
3240
3241 let resolved = config.rpc_endpoints.resolved();
3242 assert_eq!(
3243 RpcEndpoints::new([
3244 (
3245 "optimism",
3246 RpcEndpointType::String(RpcEndpointUrl::Url(
3247 "https://example.com/".to_string()
3248 ))
3249 ),
3250 (
3251 "mainnet",
3252 RpcEndpointType::Config(RpcEndpoint {
3253 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3254 config: RpcEndpointConfig {
3255 retries: Some(3),
3256 retry_backoff: Some(1000),
3257 compute_units_per_second: Some(1000),
3258 },
3259 auth: None,
3260 })
3261 ),
3262 ])
3263 .resolved(),
3264 resolved
3265 );
3266 Ok(())
3267 })
3268 }
3269
3270 #[test]
3271 fn test_resolve_auth() {
3272 figment::Jail::expect_with(|jail| {
3273 jail.create_file(
3274 "foundry.toml",
3275 r#"
3276 [profile.default]
3277 eth_rpc_url = "optimism"
3278 [rpc_endpoints]
3279 optimism = "https://example.com/"
3280 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
3281 "#,
3282 )?;
3283
3284 let config = Config::load().unwrap();
3285
3286 jail.set_env("_CONFIG_AUTH", "123456");
3287 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3288
3289 assert_eq!(
3290 RpcEndpoints::new([
3291 (
3292 "optimism",
3293 RpcEndpointType::String(RpcEndpointUrl::Url(
3294 "https://example.com/".to_string()
3295 ))
3296 ),
3297 (
3298 "mainnet",
3299 RpcEndpointType::Config(RpcEndpoint {
3300 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3301 config: RpcEndpointConfig {
3302 retries: Some(3),
3303 retry_backoff: Some(1000),
3304 compute_units_per_second: Some(1000)
3305 },
3306 auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
3307 })
3308 ),
3309 ]),
3310 config.rpc_endpoints
3311 );
3312 let resolved = config.rpc_endpoints.resolved();
3313 assert_eq!(
3314 RpcEndpoints::new([
3315 (
3316 "optimism",
3317 RpcEndpointType::String(RpcEndpointUrl::Url(
3318 "https://example.com/".to_string()
3319 ))
3320 ),
3321 (
3322 "mainnet",
3323 RpcEndpointType::Config(RpcEndpoint {
3324 endpoint: RpcEndpointUrl::Url(
3325 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3326 ),
3327 config: RpcEndpointConfig {
3328 retries: Some(3),
3329 retry_backoff: Some(1000),
3330 compute_units_per_second: Some(1000)
3331 },
3332 auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
3333 })
3334 ),
3335 ])
3336 .resolved(),
3337 resolved
3338 );
3339
3340 Ok(())
3341 });
3342 }
3343
3344 #[test]
3345 fn test_resolve_endpoints() {
3346 figment::Jail::expect_with(|jail| {
3347 jail.create_file(
3348 "foundry.toml",
3349 r#"
3350 [profile.default]
3351 eth_rpc_url = "optimism"
3352 [rpc_endpoints]
3353 optimism = "https://example.com/"
3354 mainnet = "${_CONFIG_MAINNET}"
3355 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
3356 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
3357 "#,
3358 )?;
3359
3360 let config = Config::load().unwrap();
3361
3362 assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
3363
3364 assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
3365
3366 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3367 jail.set_env("_CONFIG_API_KEY1", "123456");
3368 jail.set_env("_CONFIG_API_KEY2", "98765");
3369
3370 let endpoints = config.rpc_endpoints.resolved();
3371
3372 assert!(!endpoints.has_unresolved());
3373
3374 assert_eq!(
3375 endpoints,
3376 RpcEndpoints::new([
3377 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3378 (
3379 "mainnet",
3380 RpcEndpointUrl::Url(
3381 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3382 )
3383 ),
3384 (
3385 "mainnet_2",
3386 RpcEndpointUrl::Url(
3387 "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
3388 )
3389 ),
3390 (
3391 "mainnet_3",
3392 RpcEndpointUrl::Url(
3393 "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
3394 )
3395 ),
3396 ])
3397 .resolved()
3398 );
3399
3400 Ok(())
3401 });
3402 }
3403
3404 #[test]
3405 fn test_extract_etherscan_config() {
3406 figment::Jail::expect_with(|jail| {
3407 jail.create_file(
3408 "foundry.toml",
3409 r#"
3410 [profile.default]
3411 etherscan_api_key = "optimism"
3412
3413 [etherscan]
3414 optimism = { key = "https://etherscan-optimism.com/" }
3415 mumbai = { key = "https://etherscan-mumbai.com/" }
3416 "#,
3417 )?;
3418
3419 let mut config = Config::load().unwrap();
3420
3421 let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
3422 assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
3423
3424 config.etherscan_api_key = Some("mumbai".to_string());
3425
3426 let mumbai = config.get_etherscan_api_key(Some(NamedChain::PolygonMumbai.into()));
3427 assert_eq!(mumbai, Some("https://etherscan-mumbai.com/".to_string()));
3428
3429 Ok(())
3430 });
3431 }
3432
3433 #[test]
3434 fn test_extract_etherscan_config_by_chain() {
3435 figment::Jail::expect_with(|jail| {
3436 jail.create_file(
3437 "foundry.toml",
3438 r#"
3439 [profile.default]
3440
3441 [etherscan]
3442 mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 }
3443 "#,
3444 )?;
3445
3446 let config = Config::load().unwrap();
3447
3448 let mumbai = config
3449 .get_etherscan_config_with_chain(Some(NamedChain::PolygonMumbai.into()))
3450 .unwrap()
3451 .unwrap();
3452 assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3453
3454 Ok(())
3455 });
3456 }
3457
3458 #[test]
3459 fn test_extract_etherscan_config_by_chain_with_url() {
3460 figment::Jail::expect_with(|jail| {
3461 jail.create_file(
3462 "foundry.toml",
3463 r#"
3464 [profile.default]
3465
3466 [etherscan]
3467 mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 , url = "https://verifier-url.com/"}
3468 "#,
3469 )?;
3470
3471 let config = Config::load().unwrap();
3472
3473 let mumbai = config
3474 .get_etherscan_config_with_chain(Some(NamedChain::PolygonMumbai.into()))
3475 .unwrap()
3476 .unwrap();
3477 assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3478 assert_eq!(mumbai.api_url, "https://verifier-url.com/".to_string());
3479
3480 Ok(())
3481 });
3482 }
3483
3484 #[test]
3485 fn test_extract_etherscan_config_by_chain_and_alias() {
3486 figment::Jail::expect_with(|jail| {
3487 jail.create_file(
3488 "foundry.toml",
3489 r#"
3490 [profile.default]
3491 eth_rpc_url = "mumbai"
3492
3493 [etherscan]
3494 mumbai = { key = "https://etherscan-mumbai.com/" }
3495
3496 [rpc_endpoints]
3497 mumbai = "https://polygon-mumbai.g.alchemy.com/v2/mumbai"
3498 "#,
3499 )?;
3500
3501 let config = Config::load().unwrap();
3502
3503 let mumbai = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
3504 assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3505
3506 let mumbai_rpc = config.get_rpc_url().unwrap().unwrap();
3507 assert_eq!(mumbai_rpc, "https://polygon-mumbai.g.alchemy.com/v2/mumbai");
3508 Ok(())
3509 });
3510 }
3511
3512 #[test]
3513 fn test_toml_file() {
3514 figment::Jail::expect_with(|jail| {
3515 jail.create_file(
3516 "foundry.toml",
3517 r#"
3518 [profile.default]
3519 src = "some-source"
3520 out = "some-out"
3521 cache = true
3522 eth_rpc_url = "https://example.com/"
3523 verbosity = 3
3524 remappings = ["ds-test=lib/ds-test/"]
3525 via_ir = true
3526 rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
3527 use_literal_content = false
3528 bytecode_hash = "ipfs"
3529 cbor_metadata = true
3530 revert_strings = "strip"
3531 allow_paths = ["allow", "paths"]
3532 build_info_path = "build-info"
3533 always_use_create_2_factory = true
3534
3535 [rpc_endpoints]
3536 optimism = "https://example.com/"
3537 mainnet = "${RPC_MAINNET}"
3538 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3539 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3540 "#,
3541 )?;
3542
3543 let config = Config::load().unwrap();
3544 assert_eq!(
3545 config,
3546 Config {
3547 src: "some-source".into(),
3548 out: "some-out".into(),
3549 cache: true,
3550 eth_rpc_url: Some("https://example.com/".to_string()),
3551 remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
3552 verbosity: 3,
3553 via_ir: true,
3554 rpc_storage_caching: StorageCachingConfig {
3555 chains: CachedChains::Chains(vec![
3556 Chain::mainnet(),
3557 Chain::optimism_mainnet(),
3558 Chain::from_id(999999)
3559 ]),
3560 endpoints: CachedEndpoints::All,
3561 },
3562 use_literal_content: false,
3563 bytecode_hash: BytecodeHash::Ipfs,
3564 cbor_metadata: true,
3565 revert_strings: Some(RevertStrings::Strip),
3566 allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
3567 rpc_endpoints: RpcEndpoints::new([
3568 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3569 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3570 (
3571 "mainnet_2",
3572 RpcEndpointUrl::Env(
3573 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3574 )
3575 ),
3576 (
3577 "mainnet_3",
3578 RpcEndpointUrl::Env(
3579 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3580 .to_string()
3581 )
3582 ),
3583 ]),
3584 build_info_path: Some("build-info".into()),
3585 always_use_create_2_factory: true,
3586 ..Config::default().normalized_optimizer_settings()
3587 }
3588 );
3589
3590 Ok(())
3591 });
3592 }
3593
3594 #[test]
3595 fn test_load_remappings() {
3596 figment::Jail::expect_with(|jail| {
3597 jail.create_file(
3598 "foundry.toml",
3599 r"
3600 [profile.default]
3601 remappings = ['nested/=lib/nested/']
3602 ",
3603 )?;
3604
3605 let config = Config::load_with_root(jail.directory()).unwrap();
3606 assert_eq!(
3607 config.remappings,
3608 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3609 );
3610
3611 Ok(())
3612 });
3613 }
3614
3615 #[test]
3616 fn test_load_full_toml() {
3617 figment::Jail::expect_with(|jail| {
3618 jail.create_file(
3619 "foundry.toml",
3620 r#"
3621 [profile.default]
3622 auto_detect_solc = true
3623 block_base_fee_per_gas = 0
3624 block_coinbase = '0x0000000000000000000000000000000000000000'
3625 block_difficulty = 0
3626 block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
3627 block_number = 1
3628 block_timestamp = 1
3629 use_literal_content = false
3630 bytecode_hash = 'ipfs'
3631 cbor_metadata = true
3632 cache = true
3633 cache_path = 'cache'
3634 evm_version = 'london'
3635 extra_output = []
3636 extra_output_files = []
3637 always_use_create_2_factory = false
3638 ffi = false
3639 force = false
3640 gas_limit = 9223372036854775807
3641 gas_price = 0
3642 gas_reports = ['*']
3643 ignored_error_codes = [1878]
3644 ignored_warnings_from = ["something"]
3645 deny_warnings = false
3646 initial_balance = '0xffffffffffffffffffffffff'
3647 libraries = []
3648 libs = ['lib']
3649 memory_limit = 134217728
3650 names = false
3651 no_storage_caching = false
3652 no_rpc_rate_limit = false
3653 offline = false
3654 optimizer = true
3655 optimizer_runs = 200
3656 out = 'out'
3657 remappings = ['nested/=lib/nested/']
3658 sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3659 sizes = false
3660 sparse_mode = false
3661 src = 'src'
3662 test = 'test'
3663 tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3664 verbosity = 0
3665 via_ir = false
3666
3667 [profile.default.rpc_storage_caching]
3668 chains = 'all'
3669 endpoints = 'all'
3670
3671 [rpc_endpoints]
3672 optimism = "https://example.com/"
3673 mainnet = "${RPC_MAINNET}"
3674 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3675
3676 [fuzz]
3677 runs = 256
3678 seed = '0x3e8'
3679 max_test_rejects = 65536
3680
3681 [invariant]
3682 runs = 256
3683 depth = 500
3684 fail_on_revert = false
3685 call_override = false
3686 shrink_run_limit = 5000
3687 "#,
3688 )?;
3689
3690 let config = Config::load_with_root(jail.directory()).unwrap();
3691
3692 assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
3693 assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
3694 assert_eq!(
3695 config.remappings,
3696 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3697 );
3698
3699 assert_eq!(
3700 config.rpc_endpoints,
3701 RpcEndpoints::new([
3702 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3703 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3704 (
3705 "mainnet_2",
3706 RpcEndpointUrl::Env(
3707 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3708 )
3709 ),
3710 ]),
3711 );
3712
3713 Ok(())
3714 });
3715 }
3716
3717 #[test]
3718 fn test_solc_req() {
3719 figment::Jail::expect_with(|jail| {
3720 jail.create_file(
3721 "foundry.toml",
3722 r#"
3723 [profile.default]
3724 solc_version = "0.8.12"
3725 "#,
3726 )?;
3727
3728 let config = Config::load().unwrap();
3729 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3730
3731 jail.create_file(
3732 "foundry.toml",
3733 r#"
3734 [profile.default]
3735 solc = "0.8.12"
3736 "#,
3737 )?;
3738
3739 let config = Config::load().unwrap();
3740 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3741
3742 jail.create_file(
3743 "foundry.toml",
3744 r#"
3745 [profile.default]
3746 solc = "path/to/local/solc"
3747 "#,
3748 )?;
3749
3750 let config = Config::load().unwrap();
3751 assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
3752
3753 jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
3754 let config = Config::load().unwrap();
3755 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
3756 Ok(())
3757 });
3758 }
3759
3760 #[test]
3762 fn test_backwards_solc_version() {
3763 figment::Jail::expect_with(|jail| {
3764 jail.create_file(
3765 "foundry.toml",
3766 r#"
3767 [default]
3768 solc = "0.8.12"
3769 solc_version = "0.8.20"
3770 "#,
3771 )?;
3772
3773 let config = Config::load().unwrap();
3774 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3775
3776 Ok(())
3777 });
3778
3779 figment::Jail::expect_with(|jail| {
3780 jail.create_file(
3781 "foundry.toml",
3782 r#"
3783 [default]
3784 solc_version = "0.8.20"
3785 "#,
3786 )?;
3787
3788 let config = Config::load().unwrap();
3789 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
3790
3791 Ok(())
3792 });
3793 }
3794
3795 #[test]
3796 fn test_toml_casing_file() {
3797 figment::Jail::expect_with(|jail| {
3798 jail.create_file(
3799 "foundry.toml",
3800 r#"
3801 [profile.default]
3802 src = "some-source"
3803 out = "some-out"
3804 cache = true
3805 eth-rpc-url = "https://example.com/"
3806 evm-version = "berlin"
3807 auto-detect-solc = false
3808 "#,
3809 )?;
3810
3811 let config = Config::load().unwrap();
3812 assert_eq!(
3813 config,
3814 Config {
3815 src: "some-source".into(),
3816 out: "some-out".into(),
3817 cache: true,
3818 eth_rpc_url: Some("https://example.com/".to_string()),
3819 auto_detect_solc: false,
3820 evm_version: EvmVersion::Berlin,
3821 ..Config::default().normalized_optimizer_settings()
3822 }
3823 );
3824
3825 Ok(())
3826 });
3827 }
3828
3829 #[test]
3830 fn test_output_selection() {
3831 figment::Jail::expect_with(|jail| {
3832 jail.create_file(
3833 "foundry.toml",
3834 r#"
3835 [profile.default]
3836 extra_output = ["metadata", "ir-optimized"]
3837 extra_output_files = ["metadata"]
3838 "#,
3839 )?;
3840
3841 let config = Config::load().unwrap();
3842
3843 assert_eq!(
3844 config.extra_output,
3845 vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
3846 );
3847 assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
3848
3849 Ok(())
3850 });
3851 }
3852
3853 #[test]
3854 fn test_precedence() {
3855 figment::Jail::expect_with(|jail| {
3856 jail.create_file(
3857 "foundry.toml",
3858 r#"
3859 [profile.default]
3860 src = "mysrc"
3861 out = "myout"
3862 verbosity = 3
3863 "#,
3864 )?;
3865
3866 let config = Config::load().unwrap();
3867 assert_eq!(
3868 config,
3869 Config {
3870 src: "mysrc".into(),
3871 out: "myout".into(),
3872 verbosity: 3,
3873 ..Config::default().normalized_optimizer_settings()
3874 }
3875 );
3876
3877 jail.set_env("FOUNDRY_SRC", r"other-src");
3878 let config = Config::load().unwrap();
3879 assert_eq!(
3880 config,
3881 Config {
3882 src: "other-src".into(),
3883 out: "myout".into(),
3884 verbosity: 3,
3885 ..Config::default().normalized_optimizer_settings()
3886 }
3887 );
3888
3889 jail.set_env("FOUNDRY_PROFILE", "foo");
3890 let val: Result<String, _> = Config::figment().extract_inner("profile");
3891 assert!(val.is_err());
3892
3893 Ok(())
3894 });
3895 }
3896
3897 #[test]
3898 fn test_extract_basic() {
3899 figment::Jail::expect_with(|jail| {
3900 jail.create_file(
3901 "foundry.toml",
3902 r#"
3903 [profile.default]
3904 src = "mysrc"
3905 out = "myout"
3906 verbosity = 3
3907 evm_version = 'berlin'
3908
3909 [profile.other]
3910 src = "other-src"
3911 "#,
3912 )?;
3913 let loaded = Config::load().unwrap();
3914 assert_eq!(loaded.evm_version, EvmVersion::Berlin);
3915 let base = loaded.into_basic();
3916 let default = Config::default();
3917 assert_eq!(
3918 base,
3919 BasicConfig {
3920 profile: Config::DEFAULT_PROFILE,
3921 src: "mysrc".into(),
3922 out: "myout".into(),
3923 libs: default.libs.clone(),
3924 remappings: default.remappings.clone(),
3925 }
3926 );
3927 jail.set_env("FOUNDRY_PROFILE", r"other");
3928 let base = Config::figment().extract::<BasicConfig>().unwrap();
3929 assert_eq!(
3930 base,
3931 BasicConfig {
3932 profile: Config::DEFAULT_PROFILE,
3933 src: "other-src".into(),
3934 out: "myout".into(),
3935 libs: default.libs.clone(),
3936 remappings: default.remappings,
3937 }
3938 );
3939 Ok(())
3940 });
3941 }
3942
3943 #[test]
3944 #[should_panic]
3945 fn test_parse_invalid_fuzz_weight() {
3946 figment::Jail::expect_with(|jail| {
3947 jail.create_file(
3948 "foundry.toml",
3949 r"
3950 [fuzz]
3951 dictionary_weight = 101
3952 ",
3953 )?;
3954 let _config = Config::load().unwrap();
3955 Ok(())
3956 });
3957 }
3958
3959 #[test]
3960 fn test_fallback_provider() {
3961 figment::Jail::expect_with(|jail| {
3962 jail.create_file(
3963 "foundry.toml",
3964 r"
3965 [fuzz]
3966 runs = 1
3967 include_storage = false
3968 dictionary_weight = 99
3969
3970 [invariant]
3971 runs = 420
3972
3973 [profile.ci.fuzz]
3974 dictionary_weight = 5
3975
3976 [profile.ci.invariant]
3977 runs = 400
3978 ",
3979 )?;
3980
3981 let invariant_default = InvariantConfig::default();
3982 let config = Config::load().unwrap();
3983
3984 assert_ne!(config.invariant.runs, config.fuzz.runs);
3985 assert_eq!(config.invariant.runs, 420);
3986
3987 assert_ne!(
3988 config.fuzz.dictionary.include_storage,
3989 invariant_default.dictionary.include_storage
3990 );
3991 assert_eq!(
3992 config.invariant.dictionary.include_storage,
3993 config.fuzz.dictionary.include_storage
3994 );
3995
3996 assert_ne!(
3997 config.fuzz.dictionary.dictionary_weight,
3998 invariant_default.dictionary.dictionary_weight
3999 );
4000 assert_eq!(
4001 config.invariant.dictionary.dictionary_weight,
4002 config.fuzz.dictionary.dictionary_weight
4003 );
4004
4005 jail.set_env("FOUNDRY_PROFILE", "ci");
4006 let ci_config = Config::load().unwrap();
4007 assert_eq!(ci_config.fuzz.runs, 1);
4008 assert_eq!(ci_config.invariant.runs, 400);
4009 assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4010 assert_eq!(
4011 ci_config.invariant.dictionary.dictionary_weight,
4012 config.fuzz.dictionary.dictionary_weight
4013 );
4014
4015 Ok(())
4016 })
4017 }
4018
4019 #[test]
4020 fn test_standalone_profile_sections() {
4021 figment::Jail::expect_with(|jail| {
4022 jail.create_file(
4023 "foundry.toml",
4024 r"
4025 [fuzz]
4026 runs = 100
4027
4028 [invariant]
4029 runs = 120
4030
4031 [profile.ci.fuzz]
4032 runs = 420
4033
4034 [profile.ci.invariant]
4035 runs = 500
4036 ",
4037 )?;
4038
4039 let config = Config::load().unwrap();
4040 assert_eq!(config.fuzz.runs, 100);
4041 assert_eq!(config.invariant.runs, 120);
4042
4043 jail.set_env("FOUNDRY_PROFILE", "ci");
4044 let config = Config::load().unwrap();
4045 assert_eq!(config.fuzz.runs, 420);
4046 assert_eq!(config.invariant.runs, 500);
4047
4048 Ok(())
4049 });
4050 }
4051
4052 #[test]
4053 fn can_handle_deviating_dapp_aliases() {
4054 figment::Jail::expect_with(|jail| {
4055 let addr = Address::ZERO;
4056 jail.set_env("DAPP_TEST_NUMBER", 1337);
4057 jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4058 jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4059 jail.set_env("DAPP_TEST_DEPTH", 20);
4060 jail.set_env("DAPP_FORK_BLOCK", 100);
4061 jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4062 jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4063
4064 let config = Config::load().unwrap();
4065
4066 assert_eq!(config.block_number, 1337);
4067 assert_eq!(config.sender, addr);
4068 assert_eq!(config.fuzz.runs, 420);
4069 assert_eq!(config.invariant.depth, 20);
4070 assert_eq!(config.fork_block_number, Some(100));
4071 assert_eq!(config.optimizer_runs, Some(999));
4072 assert!(!config.optimizer.unwrap());
4073
4074 Ok(())
4075 });
4076 }
4077
4078 #[test]
4079 fn can_parse_libraries() {
4080 figment::Jail::expect_with(|jail| {
4081 jail.set_env(
4082 "DAPP_LIBRARIES",
4083 "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
4084 );
4085 let config = Config::load().unwrap();
4086 assert_eq!(
4087 config.libraries,
4088 vec!["src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4089 .to_string()]
4090 );
4091
4092 jail.set_env(
4093 "DAPP_LIBRARIES",
4094 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4095 );
4096 let config = Config::load().unwrap();
4097 assert_eq!(
4098 config.libraries,
4099 vec!["src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4100 .to_string(),]
4101 );
4102
4103 jail.set_env(
4104 "DAPP_LIBRARIES",
4105 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4106 );
4107 let config = Config::load().unwrap();
4108 assert_eq!(
4109 config.libraries,
4110 vec![
4111 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4112 .to_string(),
4113 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4114 .to_string()
4115 ]
4116 );
4117
4118 Ok(())
4119 });
4120 }
4121
4122 #[test]
4123 fn test_parse_many_libraries() {
4124 figment::Jail::expect_with(|jail| {
4125 jail.create_file(
4126 "foundry.toml",
4127 r"
4128 [profile.default]
4129 libraries= [
4130 './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4131 './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4132 './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4133 './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4134 './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4135 ]
4136 ",
4137 )?;
4138 let config = Config::load().unwrap();
4139
4140 let libs = config.parsed_libraries().unwrap().libs;
4141
4142 similar_asserts::assert_eq!(
4143 libs,
4144 BTreeMap::from([
4145 (
4146 PathBuf::from("./src/SizeAuctionDiscount.sol"),
4147 BTreeMap::from([
4148 (
4149 "Chainlink".to_string(),
4150 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4151 ),
4152 (
4153 "Math".to_string(),
4154 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4155 )
4156 ])
4157 ),
4158 (
4159 PathBuf::from("./src/SizeAuction.sol"),
4160 BTreeMap::from([
4161 (
4162 "ChainlinkTWAP".to_string(),
4163 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4164 ),
4165 (
4166 "Math".to_string(),
4167 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4168 )
4169 ])
4170 ),
4171 (
4172 PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
4173 BTreeMap::from([(
4174 "ChainlinkTWAP".to_string(),
4175 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4176 )])
4177 ),
4178 ])
4179 );
4180
4181 Ok(())
4182 });
4183 }
4184
4185 #[test]
4186 fn config_roundtrip() {
4187 figment::Jail::expect_with(|jail| {
4188 let default = Config::default().normalized_optimizer_settings();
4189 let basic = default.clone().into_basic();
4190 jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
4191
4192 let mut other = Config::load().unwrap();
4193 clear_warning(&mut other);
4194 assert_eq!(default, other);
4195
4196 let other = other.into_basic();
4197 assert_eq!(basic, other);
4198
4199 jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
4200 let mut other = Config::load().unwrap();
4201 clear_warning(&mut other);
4202 assert_eq!(default, other);
4203
4204 Ok(())
4205 });
4206 }
4207
4208 #[test]
4209 fn test_fs_permissions() {
4210 figment::Jail::expect_with(|jail| {
4211 jail.create_file(
4212 "foundry.toml",
4213 r#"
4214 [profile.default]
4215 fs_permissions = [{ access = "read-write", path = "./"}]
4216 "#,
4217 )?;
4218 let loaded = Config::load().unwrap();
4219
4220 assert_eq!(
4221 loaded.fs_permissions,
4222 FsPermissions::new(vec![PathPermission::read_write("./")])
4223 );
4224
4225 jail.create_file(
4226 "foundry.toml",
4227 r#"
4228 [profile.default]
4229 fs_permissions = [{ access = "none", path = "./"}]
4230 "#,
4231 )?;
4232 let loaded = Config::load().unwrap();
4233 assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
4234
4235 Ok(())
4236 });
4237 }
4238
4239 #[test]
4240 fn test_optimizer_settings_basic() {
4241 figment::Jail::expect_with(|jail| {
4242 jail.create_file(
4243 "foundry.toml",
4244 r"
4245 [profile.default]
4246 optimizer = true
4247
4248 [profile.default.optimizer_details]
4249 yul = false
4250
4251 [profile.default.optimizer_details.yulDetails]
4252 stackAllocation = true
4253 ",
4254 )?;
4255 let mut loaded = Config::load().unwrap();
4256 clear_warning(&mut loaded);
4257 assert_eq!(
4258 loaded.optimizer_details,
4259 Some(OptimizerDetails {
4260 yul: Some(false),
4261 yul_details: Some(YulDetails {
4262 stack_allocation: Some(true),
4263 ..Default::default()
4264 }),
4265 ..Default::default()
4266 })
4267 );
4268
4269 let s = loaded.to_string_pretty().unwrap();
4270 jail.create_file("foundry.toml", &s)?;
4271
4272 let mut reloaded = Config::load().unwrap();
4273 clear_warning(&mut reloaded);
4274 assert_eq!(loaded, reloaded);
4275
4276 Ok(())
4277 });
4278 }
4279
4280 #[test]
4281 fn test_model_checker_settings_basic() {
4282 figment::Jail::expect_with(|jail| {
4283 jail.create_file(
4284 "foundry.toml",
4285 r"
4286 [profile.default]
4287
4288 [profile.default.model_checker]
4289 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4290 engine = 'chc'
4291 targets = [ 'assert', 'outOfBounds' ]
4292 timeout = 10000
4293 ",
4294 )?;
4295 let mut loaded = Config::load().unwrap();
4296 clear_warning(&mut loaded);
4297 assert_eq!(
4298 loaded.model_checker,
4299 Some(ModelCheckerSettings {
4300 contracts: BTreeMap::from([
4301 ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
4302 ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
4303 ]),
4304 engine: Some(ModelCheckerEngine::CHC),
4305 targets: Some(vec![
4306 ModelCheckerTarget::Assert,
4307 ModelCheckerTarget::OutOfBounds
4308 ]),
4309 timeout: Some(10000),
4310 invariants: None,
4311 show_unproved: None,
4312 div_mod_with_slacks: None,
4313 solvers: None,
4314 show_unsupported: None,
4315 show_proved_safe: None,
4316 })
4317 );
4318
4319 let s = loaded.to_string_pretty().unwrap();
4320 jail.create_file("foundry.toml", &s)?;
4321
4322 let mut reloaded = Config::load().unwrap();
4323 clear_warning(&mut reloaded);
4324 assert_eq!(loaded, reloaded);
4325
4326 Ok(())
4327 });
4328 }
4329
4330 #[test]
4331 fn test_model_checker_settings_relative_paths() {
4332 figment::Jail::expect_with(|jail| {
4333 jail.create_file(
4334 "foundry.toml",
4335 r"
4336 [profile.default]
4337
4338 [profile.default.model_checker]
4339 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4340 engine = 'chc'
4341 targets = [ 'assert', 'outOfBounds' ]
4342 timeout = 10000
4343 ",
4344 )?;
4345 let loaded = Config::load().unwrap().sanitized();
4346
4347 let dir = foundry_compilers::utils::canonicalize(jail.directory())
4352 .expect("Could not canonicalize jail path");
4353 assert_eq!(
4354 loaded.model_checker,
4355 Some(ModelCheckerSettings {
4356 contracts: BTreeMap::from([
4357 (
4358 format!("{}", dir.join("a.sol").display()),
4359 vec!["A1".to_string(), "A2".to_string()]
4360 ),
4361 (
4362 format!("{}", dir.join("b.sol").display()),
4363 vec!["B1".to_string(), "B2".to_string()]
4364 ),
4365 ]),
4366 engine: Some(ModelCheckerEngine::CHC),
4367 targets: Some(vec![
4368 ModelCheckerTarget::Assert,
4369 ModelCheckerTarget::OutOfBounds
4370 ]),
4371 timeout: Some(10000),
4372 invariants: None,
4373 show_unproved: None,
4374 div_mod_with_slacks: None,
4375 solvers: None,
4376 show_unsupported: None,
4377 show_proved_safe: None,
4378 })
4379 );
4380
4381 Ok(())
4382 });
4383 }
4384
4385 #[test]
4386 fn test_fmt_config() {
4387 figment::Jail::expect_with(|jail| {
4388 jail.create_file(
4389 "foundry.toml",
4390 r"
4391 [fmt]
4392 line_length = 100
4393 tab_width = 2
4394 bracket_spacing = true
4395 ",
4396 )?;
4397 let loaded = Config::load().unwrap().sanitized();
4398 assert_eq!(
4399 loaded.fmt,
4400 FormatterConfig {
4401 line_length: 100,
4402 tab_width: 2,
4403 bracket_spacing: true,
4404 ..Default::default()
4405 }
4406 );
4407
4408 Ok(())
4409 });
4410 }
4411
4412 #[test]
4413 fn test_invariant_config() {
4414 figment::Jail::expect_with(|jail| {
4415 jail.create_file(
4416 "foundry.toml",
4417 r"
4418 [invariant]
4419 runs = 512
4420 depth = 10
4421 ",
4422 )?;
4423
4424 let loaded = Config::load().unwrap().sanitized();
4425 assert_eq!(
4426 loaded.invariant,
4427 InvariantConfig {
4428 runs: 512,
4429 depth: 10,
4430 failure_persist_dir: Some(PathBuf::from("cache/invariant")),
4431 ..Default::default()
4432 }
4433 );
4434
4435 Ok(())
4436 });
4437 }
4438
4439 #[test]
4440 fn test_standalone_sections_env() {
4441 figment::Jail::expect_with(|jail| {
4442 jail.create_file(
4443 "foundry.toml",
4444 r"
4445 [fuzz]
4446 runs = 100
4447
4448 [invariant]
4449 depth = 1
4450 ",
4451 )?;
4452
4453 jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
4454 jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
4455 jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
4456
4457 let config = Config::load().unwrap();
4458 assert_eq!(config.fmt.line_length, 95);
4459 assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
4460 assert_eq!(config.invariant.depth, 5);
4461
4462 Ok(())
4463 });
4464 }
4465
4466 #[test]
4467 fn test_parse_with_profile() {
4468 let foundry_str = r"
4469 [profile.default]
4470 src = 'src'
4471 out = 'out'
4472 libs = ['lib']
4473
4474 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
4475 ";
4476 assert_eq!(
4477 parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
4478 (
4479 Config::DEFAULT_PROFILE,
4480 BasicConfig {
4481 profile: Config::DEFAULT_PROFILE,
4482 src: "src".into(),
4483 out: "out".into(),
4484 libs: vec!["lib".into()],
4485 remappings: vec![]
4486 }
4487 )
4488 );
4489 }
4490
4491 #[test]
4492 fn test_implicit_profile_loads() {
4493 figment::Jail::expect_with(|jail| {
4494 jail.create_file(
4495 "foundry.toml",
4496 r"
4497 [default]
4498 src = 'my-src'
4499 out = 'my-out'
4500 ",
4501 )?;
4502 let loaded = Config::load().unwrap().sanitized();
4503 assert_eq!(loaded.src.file_name().unwrap(), "my-src");
4504 assert_eq!(loaded.out.file_name().unwrap(), "my-out");
4505 assert_eq!(
4506 loaded.warnings,
4507 vec![Warning::UnknownSection {
4508 unknown_section: Profile::new("default"),
4509 source: Some("foundry.toml".into())
4510 }]
4511 );
4512
4513 Ok(())
4514 });
4515 }
4516
4517 #[test]
4518 fn test_etherscan_api_key() {
4519 figment::Jail::expect_with(|jail| {
4520 jail.create_file(
4521 "foundry.toml",
4522 r"
4523 [default]
4524 ",
4525 )?;
4526 jail.set_env("ETHERSCAN_API_KEY", "");
4527 let loaded = Config::load().unwrap().sanitized();
4528 assert!(loaded.etherscan_api_key.is_none());
4529
4530 jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
4531 let loaded = Config::load().unwrap().sanitized();
4532 assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
4533
4534 Ok(())
4535 });
4536 }
4537
4538 #[test]
4539 fn test_etherscan_api_key_figment() {
4540 figment::Jail::expect_with(|jail| {
4541 jail.create_file(
4542 "foundry.toml",
4543 r"
4544 [default]
4545 etherscan_api_key = 'DUMMY'
4546 ",
4547 )?;
4548 jail.set_env("ETHERSCAN_API_KEY", "ETHER");
4549
4550 let figment = Config::figment_with_root(jail.directory())
4551 .merge(("etherscan_api_key", "USER_KEY"));
4552
4553 let loaded = Config::from_provider(figment).unwrap();
4554 assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
4555
4556 Ok(())
4557 });
4558 }
4559
4560 #[test]
4561 fn test_normalize_defaults() {
4562 figment::Jail::expect_with(|jail| {
4563 jail.create_file(
4564 "foundry.toml",
4565 r"
4566 [default]
4567 solc = '0.8.13'
4568 ",
4569 )?;
4570
4571 let loaded = Config::load().unwrap().sanitized();
4572 assert_eq!(loaded.evm_version, EvmVersion::London);
4573 Ok(())
4574 });
4575 }
4576
4577 #[expect(clippy::disallowed_macros)]
4579 #[test]
4580 #[ignore]
4581 fn print_config() {
4582 let config = Config {
4583 optimizer_details: Some(OptimizerDetails {
4584 peephole: None,
4585 inliner: None,
4586 jumpdest_remover: None,
4587 order_literals: None,
4588 deduplicate: None,
4589 cse: None,
4590 constant_optimizer: Some(true),
4591 yul: Some(true),
4592 yul_details: Some(YulDetails {
4593 stack_allocation: None,
4594 optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
4595 }),
4596 simple_counter_for_loop_unchecked_increment: None,
4597 }),
4598 ..Default::default()
4599 };
4600 println!("{}", config.to_string_pretty().unwrap());
4601 }
4602
4603 #[test]
4604 fn can_use_impl_figment_macro() {
4605 #[derive(Default, Serialize)]
4606 struct MyArgs {
4607 #[serde(skip_serializing_if = "Option::is_none")]
4608 root: Option<PathBuf>,
4609 }
4610 impl_figment_convert!(MyArgs);
4611
4612 impl Provider for MyArgs {
4613 fn metadata(&self) -> Metadata {
4614 Metadata::default()
4615 }
4616
4617 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
4618 let value = Value::serialize(self)?;
4619 let error = InvalidType(value.to_actual(), "map".into());
4620 let dict = value.into_dict().ok_or(error)?;
4621 Ok(Map::from([(Config::selected_profile(), dict)]))
4622 }
4623 }
4624
4625 let _figment: Figment = From::from(&MyArgs::default());
4626
4627 #[derive(Default)]
4628 struct Outer {
4629 start: MyArgs,
4630 other: MyArgs,
4631 another: MyArgs,
4632 }
4633 impl_figment_convert!(Outer, start, other, another);
4634
4635 let _figment: Figment = From::from(&Outer::default());
4636 }
4637
4638 #[test]
4639 fn list_cached_blocks() -> eyre::Result<()> {
4640 fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
4641 let block_path = chain_path.join(block_number);
4642 fs::create_dir(block_path.as_path()).unwrap();
4643 let file_path = block_path.join("storage.json");
4644 let mut file = File::create(file_path).unwrap();
4645 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4646 }
4647
4648 fn fake_block_cache_block_path_as_file(
4649 chain_path: &Path,
4650 block_number: &str,
4651 size_bytes: usize,
4652 ) {
4653 let block_path = chain_path.join(block_number);
4654 let mut file = File::create(block_path).unwrap();
4655 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4656 }
4657
4658 let chain_dir = tempdir()?;
4659
4660 fake_block_cache(chain_dir.path(), "1", 100);
4661 fake_block_cache(chain_dir.path(), "2", 500);
4662 fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
4663 let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
4665 writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
4666
4667 let result = Config::get_cached_blocks(chain_dir.path())?;
4668
4669 assert_eq!(result.len(), 3);
4670 let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
4671 let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
4672 let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
4673
4674 assert_eq!(block1.0, "1");
4675 assert_eq!(block1.1, 100);
4676 assert_eq!(block2.0, "2");
4677 assert_eq!(block2.1, 500);
4678 assert_eq!(block3.0, "3");
4679 assert_eq!(block3.1, 900);
4680
4681 chain_dir.close()?;
4682 Ok(())
4683 }
4684
4685 #[test]
4686 fn list_etherscan_cache() -> eyre::Result<()> {
4687 fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
4688 let metadata_path = chain_path.join("sources");
4689 let abi_path = chain_path.join("abi");
4690 let _ = fs::create_dir(metadata_path.as_path());
4691 let _ = fs::create_dir(abi_path.as_path());
4692
4693 let metadata_file_path = metadata_path.join(address);
4694 let mut metadata_file = File::create(metadata_file_path).unwrap();
4695 writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4696 .unwrap();
4697
4698 let abi_file_path = abi_path.join(address);
4699 let mut abi_file = File::create(abi_file_path).unwrap();
4700 writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4701 .unwrap();
4702 }
4703
4704 let chain_dir = tempdir()?;
4705
4706 fake_etherscan_cache(chain_dir.path(), "1", 100);
4707 fake_etherscan_cache(chain_dir.path(), "2", 500);
4708
4709 let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
4710
4711 assert_eq!(result, 600);
4712
4713 chain_dir.close()?;
4714 Ok(())
4715 }
4716
4717 #[test]
4718 fn test_parse_error_codes() {
4719 figment::Jail::expect_with(|jail| {
4720 jail.create_file(
4721 "foundry.toml",
4722 r#"
4723 [default]
4724 ignored_error_codes = ["license", "unreachable", 1337]
4725 "#,
4726 )?;
4727
4728 let config = Config::load().unwrap();
4729 assert_eq!(
4730 config.ignored_error_codes,
4731 vec![
4732 SolidityErrorCode::SpdxLicenseNotProvided,
4733 SolidityErrorCode::Unreachable,
4734 SolidityErrorCode::Other(1337)
4735 ]
4736 );
4737
4738 Ok(())
4739 });
4740 }
4741
4742 #[test]
4743 fn test_parse_file_paths() {
4744 figment::Jail::expect_with(|jail| {
4745 jail.create_file(
4746 "foundry.toml",
4747 r#"
4748 [default]
4749 ignored_warnings_from = ["something"]
4750 "#,
4751 )?;
4752
4753 let config = Config::load().unwrap();
4754 assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
4755
4756 Ok(())
4757 });
4758 }
4759
4760 #[test]
4761 fn test_parse_optimizer_settings() {
4762 figment::Jail::expect_with(|jail| {
4763 jail.create_file(
4764 "foundry.toml",
4765 r"
4766 [default]
4767 [profile.default.optimizer_details]
4768 ",
4769 )?;
4770
4771 let config = Config::load().unwrap();
4772 assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
4773
4774 Ok(())
4775 });
4776 }
4777
4778 #[test]
4779 fn test_parse_labels() {
4780 figment::Jail::expect_with(|jail| {
4781 jail.create_file(
4782 "foundry.toml",
4783 r#"
4784 [labels]
4785 0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
4786 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
4787 "#,
4788 )?;
4789
4790 let config = Config::load().unwrap();
4791 assert_eq!(
4792 config.labels,
4793 AddressHashMap::from_iter(vec![
4794 (
4795 address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
4796 "Uniswap V3: Factory".to_string()
4797 ),
4798 (
4799 address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
4800 "Uniswap V3: Positions NFT".to_string()
4801 ),
4802 ])
4803 );
4804
4805 Ok(())
4806 });
4807 }
4808
4809 #[test]
4810 fn test_parse_vyper() {
4811 figment::Jail::expect_with(|jail| {
4812 jail.create_file(
4813 "foundry.toml",
4814 r#"
4815 [vyper]
4816 optimize = "codesize"
4817 path = "/path/to/vyper"
4818 experimental_codegen = true
4819 "#,
4820 )?;
4821
4822 let config = Config::load().unwrap();
4823 assert_eq!(
4824 config.vyper,
4825 VyperConfig {
4826 optimize: Some(VyperOptimizationMode::Codesize),
4827 path: Some("/path/to/vyper".into()),
4828 experimental_codegen: Some(true),
4829 }
4830 );
4831
4832 Ok(())
4833 });
4834 }
4835
4836 #[test]
4837 fn test_parse_soldeer() {
4838 figment::Jail::expect_with(|jail| {
4839 jail.create_file(
4840 "foundry.toml",
4841 r#"
4842 [soldeer]
4843 remappings_generate = true
4844 remappings_regenerate = false
4845 remappings_version = true
4846 remappings_prefix = "@"
4847 remappings_location = "txt"
4848 recursive_deps = true
4849 "#,
4850 )?;
4851
4852 let config = Config::load().unwrap();
4853
4854 assert_eq!(
4855 config.soldeer,
4856 Some(SoldeerConfig {
4857 remappings_generate: true,
4858 remappings_regenerate: false,
4859 remappings_version: true,
4860 remappings_prefix: "@".to_string(),
4861 remappings_location: RemappingsLocation::Txt,
4862 recursive_deps: true,
4863 })
4864 );
4865
4866 Ok(())
4867 });
4868 }
4869}