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 foundry_compilers::{
20 artifacts::{
21 output_selection::{ContractOutputSelection, OutputSelection},
22 serde_helpers, BytecodeHash, DebuggingSettings, EvmVersion, Libraries,
23 ModelCheckerSettings, ModelCheckerTarget, Optimizer, OptimizerDetails, RevertStrings,
24 Settings, SettingsMetadata, Severity,
25 },
26 cache::SOLIDITY_FILES_CACHE_FILENAME,
27 compilers::{
28 multi::{MultiCompiler, MultiCompilerSettings},
29 solc::{Solc, SolcCompiler},
30 vyper::{Vyper, VyperSettings},
31 Compiler,
32 },
33 error::SolcError,
34 ConfigurableArtifacts, Project, ProjectPathsConfig,
35};
36
37pub use foundry_compilers::artifacts::remappings::{RelativeRemapping, Remapping};
39
40use inflector::Inflector;
41use regex::Regex;
42use revm_primitives::{FixedBytes, SpecId};
43use semver::Version;
44use serde::{Deserialize, Deserializer, Serialize, Serializer};
45use std::{
46 borrow::Cow,
47 collections::HashMap,
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::{ResolvedRpcEndpoints, RpcEndpoint, RpcEndpoints};
60
61mod etherscan;
62use etherscan::{
63 EtherscanConfigError, EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig,
64};
65
66mod resolve;
67pub use resolve::UnresolvedEnvVarError;
68
69pub mod cache;
70use cache::{Cache, ChainCache};
71
72pub mod fmt;
73pub use fmt::FormatterConfig;
74
75pub mod fs_permissions;
76pub use fs_permissions::FsPermissions;
77use fs_permissions::PathPermission;
78
79pub mod error;
80use error::ExtractConfigError;
81pub use error::SolidityErrorCode;
82
83pub mod doc;
84pub use doc::DocConfig;
85
86pub mod filter;
87pub use filter::SkipBuildFilters;
88
89mod warning;
90pub use warning::*;
91
92pub mod fix;
93
94pub use alloy_chains::{Chain, NamedChain};
96pub use figment;
97
98pub mod providers;
99use providers::{remappings::RemappingsProvider, FallbackProfileProvider, WarningsProvider};
100
101mod fuzz;
102pub use fuzz::{FuzzConfig, FuzzDictionaryConfig};
103
104mod invariant;
105pub use invariant::InvariantConfig;
106
107mod inline;
108pub use inline::{validate_profiles, InlineConfig, InlineConfigError, InlineConfigParser, NatSpec};
109
110pub mod soldeer;
111use soldeer::SoldeerConfig;
112
113mod vyper;
114use vyper::VyperConfig;
115
116#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
148pub struct Config {
149 #[serde(skip)]
156 pub profile: Profile,
157 pub src: PathBuf,
159 pub test: PathBuf,
161 pub script: PathBuf,
163 pub out: PathBuf,
165 pub libs: Vec<PathBuf>,
167 pub remappings: Vec<RelativeRemapping>,
169 pub auto_detect_remappings: bool,
171 pub libraries: Vec<String>,
173 pub cache: bool,
175 pub cache_path: PathBuf,
177 pub broadcast: PathBuf,
179 pub allow_paths: Vec<PathBuf>,
181 pub include_paths: Vec<PathBuf>,
183 #[serde(with = "from_vec_glob")]
185 pub skip: Vec<globset::Glob>,
186 pub force: bool,
188 #[serde(with = "from_str_lowercase")]
190 pub evm_version: EvmVersion,
191 pub gas_reports: Vec<String>,
193 pub gas_reports_ignore: Vec<String>,
195 pub solc: Option<SolcReq>,
203 pub auto_detect_solc: bool,
205 pub offline: bool,
212 pub optimizer: bool,
214 pub optimizer_runs: usize,
216 pub optimizer_details: Option<OptimizerDetails>,
220 pub model_checker: Option<ModelCheckerSettings>,
222 pub verbosity: u8,
224 pub eth_rpc_url: Option<String>,
226 pub eth_rpc_jwt: Option<String>,
228 pub etherscan_api_key: Option<String>,
230 #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
232 pub etherscan: EtherscanConfigs,
233 pub ignored_error_codes: Vec<SolidityErrorCode>,
235 #[serde(rename = "ignored_warnings_from")]
237 pub ignored_file_paths: Vec<PathBuf>,
238 pub deny_warnings: bool,
240 #[serde(rename = "match_test")]
242 pub test_pattern: Option<RegexWrapper>,
243 #[serde(rename = "no_match_test")]
245 pub test_pattern_inverse: Option<RegexWrapper>,
246 #[serde(rename = "match_contract")]
248 pub contract_pattern: Option<RegexWrapper>,
249 #[serde(rename = "no_match_contract")]
251 pub contract_pattern_inverse: Option<RegexWrapper>,
252 #[serde(rename = "match_path", with = "from_opt_glob")]
254 pub path_pattern: Option<globset::Glob>,
255 #[serde(rename = "no_match_path", with = "from_opt_glob")]
257 pub path_pattern_inverse: Option<globset::Glob>,
258 pub fuzz: FuzzConfig,
260 pub invariant: InvariantConfig,
262 pub ffi: bool,
264 pub always_use_create_2_factory: bool,
266 pub prompt_timeout: u64,
268 pub sender: Address,
270 pub tx_origin: Address,
272 pub initial_balance: U256,
274 pub block_number: u64,
276 pub fork_block_number: Option<u64>,
278 #[serde(rename = "chain_id", alias = "chain")]
280 pub chain: Option<Chain>,
281 pub gas_limit: GasLimit,
283 pub code_size_limit: Option<usize>,
285 pub gas_price: Option<u64>,
290 pub block_base_fee_per_gas: u64,
292 pub block_coinbase: Address,
294 pub block_timestamp: u64,
296 pub block_difficulty: u64,
298 pub block_prevrandao: B256,
300 pub block_gas_limit: Option<GasLimit>,
302 pub memory_limit: u64,
307 #[serde(default)]
324 pub extra_output: Vec<ContractOutputSelection>,
325 #[serde(default)]
336 pub extra_output_files: Vec<ContractOutputSelection>,
337 pub names: bool,
339 pub sizes: bool,
341 pub via_ir: bool,
344 pub ast: bool,
346 pub rpc_storage_caching: StorageCachingConfig,
348 pub no_storage_caching: bool,
351 pub no_rpc_rate_limit: bool,
354 #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
356 pub rpc_endpoints: RpcEndpoints,
357 pub use_literal_content: bool,
359 #[serde(with = "from_str_lowercase")]
363 pub bytecode_hash: BytecodeHash,
364 pub cbor_metadata: bool,
369 #[serde(with = "serde_helpers::display_from_str_opt")]
371 pub revert_strings: Option<RevertStrings>,
372 pub sparse_mode: bool,
377 pub build_info: bool,
380 pub build_info_path: Option<PathBuf>,
382 pub fmt: FormatterConfig,
384 pub doc: DocConfig,
386 pub fs_permissions: FsPermissions,
390
391 pub prague: bool,
395
396 pub isolate: bool,
400
401 pub disable_block_gas_limit: bool,
403
404 pub labels: HashMap<Address, String>,
406
407 pub unchecked_cheatcode_artifacts: bool,
410
411 pub create2_library_salt: B256,
413
414 pub vyper: VyperConfig,
416
417 pub dependencies: Option<SoldeerConfig>,
419
420 #[serde(default, skip_serializing)]
425 pub root: RootPath,
426
427 #[serde(rename = "__warnings", default, skip_serializing)]
429 pub warnings: Vec<Warning>,
430
431 #[doc(hidden)]
440 #[serde(skip)]
441 pub _non_exhaustive: (),
442}
443
444pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
446
447pub const DEPRECATIONS: &[(&str, &str)] = &[("cancun", "evm_version = Cancun")];
451
452impl Config {
453 pub const DEFAULT_PROFILE: Profile = Profile::const_new("default");
455
456 pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
458
459 pub const PROFILE_SECTION: &'static str = "profile";
461
462 pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
464 "rpc_endpoints",
465 "etherscan",
466 "fmt",
467 "doc",
468 "fuzz",
469 "invariant",
470 "labels",
471 "dependencies",
472 "vyper",
473 ];
474
475 pub const FILE_NAME: &'static str = "foundry.toml";
477
478 pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
480
481 pub const DEFAULT_SENDER: Address = address!("1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
485
486 pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
488
489 #[track_caller]
493 pub fn load() -> Self {
494 Self::from_provider(Self::figment())
495 }
496
497 #[track_caller]
501 pub fn load_with_providers(providers: FigmentProviders) -> Self {
502 Self::default().to_figment(providers).extract().unwrap()
503 }
504
505 #[track_caller]
509 pub fn load_with_root(root: impl Into<PathBuf>) -> Self {
510 Self::from_provider(Self::figment_with_root(root))
511 }
512
513 #[track_caller]
533 pub fn from_provider<T: Provider>(provider: T) -> Self {
534 trace!("load config with provider: {:?}", provider.metadata());
535 Self::try_from(provider).unwrap_or_else(|err| panic!("{}", err))
536 }
537
538 pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
553 let figment = Figment::from(provider);
554 let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
555 config.profile = figment.profile().clone();
556 Ok(config)
557 }
558
559 pub fn to_figment(self, providers: FigmentProviders) -> Figment {
563 let mut c = self;
564 let profile = Self::selected_profile();
565 let mut figment = Figment::default().merge(DappHardhatDirProvider(&c.root.0));
566
567 if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
569 figment = Self::merge_toml_provider(
570 figment,
571 TomlFileProvider::new(None, global_toml).cached(),
572 profile.clone(),
573 );
574 }
575 figment = Self::merge_toml_provider(
577 figment,
578 TomlFileProvider::new(Some("FOUNDRY_CONFIG"), c.root.0.join(Self::FILE_NAME)).cached(),
579 profile.clone(),
580 );
581
582 figment = figment
584 .merge(
585 Env::prefixed("DAPP_")
586 .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
587 .global(),
588 )
589 .merge(
590 Env::prefixed("DAPP_TEST_")
591 .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
592 .global(),
593 )
594 .merge(DappEnvCompatProvider)
595 .merge(EtherscanEnvProvider::default())
596 .merge(
597 Env::prefixed("FOUNDRY_")
598 .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
599 .map(|key| {
600 let key = key.as_str();
601 if Self::STANDALONE_SECTIONS.iter().any(|section| {
602 key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
603 }) {
604 key.replacen('_', ".", 1).into()
605 } else {
606 key.into()
607 }
608 })
609 .global(),
610 )
611 .select(profile.clone());
612
613 if providers.is_all() {
615 let remappings = RemappingsProvider {
619 auto_detect_remappings: figment
620 .extract_inner::<bool>("auto_detect_remappings")
621 .unwrap_or(true),
622 lib_paths: figment
623 .extract_inner::<Vec<PathBuf>>("libs")
624 .map(Cow::Owned)
625 .unwrap_or_else(|_| Cow::Borrowed(&c.libs)),
626 root: &c.root.0,
627 remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
628 };
629 figment = figment.merge(remappings);
630 }
631
632 figment = c.normalize_defaults(figment);
634
635 Figment::from(c).merge(figment).select(profile)
636 }
637
638 #[must_use]
643 pub fn canonic(self) -> Self {
644 let root = self.root.0.clone();
645 self.canonic_at(root)
646 }
647
648 #[must_use]
666 pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
667 let root = canonic(root);
668
669 fn p(root: &Path, rem: &Path) -> PathBuf {
670 canonic(root.join(rem))
671 }
672
673 self.src = p(&root, &self.src);
674 self.test = p(&root, &self.test);
675 self.script = p(&root, &self.script);
676 self.out = p(&root, &self.out);
677 self.broadcast = p(&root, &self.broadcast);
678 self.cache_path = p(&root, &self.cache_path);
679
680 if let Some(build_info_path) = self.build_info_path {
681 self.build_info_path = Some(p(&root, &build_info_path));
682 }
683
684 self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
685
686 self.remappings =
687 self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
688
689 self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
690
691 self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
692
693 self.fs_permissions.join_all(&root);
694
695 if let Some(ref mut model_checker) = self.model_checker {
696 model_checker.contracts = std::mem::take(&mut model_checker.contracts)
697 .into_iter()
698 .map(|(path, contracts)| {
699 (format!("{}", p(&root, path.as_ref()).display()), contracts)
700 })
701 .collect();
702 }
703
704 self
705 }
706
707 pub fn normalized_evm_version(mut self) -> Self {
709 self.normalize_evm_version();
710 self
711 }
712
713 pub fn normalize_evm_version(&mut self) {
715 self.evm_version = self.get_normalized_evm_version();
716 }
717
718 pub fn get_normalized_evm_version(&self) -> EvmVersion {
723 if let Some(version) = self.solc.as_ref().and_then(|solc| solc.try_version().ok()) {
724 if let Some(evm_version) = self.evm_version.normalize_version_solc(&version) {
725 return evm_version;
726 }
727 }
728 self.evm_version
729 }
730
731 #[must_use]
736 pub fn sanitized(self) -> Self {
737 let mut config = self.canonic();
738
739 config.sanitize_remappings();
740
741 config.libs.sort_unstable();
742 config.libs.dedup();
743
744 config
745 }
746
747 pub fn sanitize_remappings(&mut self) {
751 #[cfg(target_os = "windows")]
752 {
753 use path_slash::PathBufExt;
755 self.remappings.iter_mut().for_each(|r| {
756 r.path.path = r.path.path.to_slash_lossy().into_owned().into();
757 });
758 }
759 }
760
761 pub fn install_lib_dir(&self) -> &Path {
765 self.libs
766 .iter()
767 .find(|p| !p.ends_with("node_modules"))
768 .map(|p| p.as_path())
769 .unwrap_or_else(|| Path::new("lib"))
770 }
771
772 pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
786 self.create_project(self.cache, false)
787 }
788
789 pub fn ephemeral_no_artifacts_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
792 self.create_project(false, true)
793 }
794
795 pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
797 let mut builder = Project::builder()
798 .artifacts(self.configured_artifacts_handler())
799 .paths(self.project_paths())
800 .settings(self.compiler_settings()?)
801 .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
802 .ignore_paths(self.ignored_file_paths.clone())
803 .set_compiler_severity_filter(if self.deny_warnings {
804 Severity::Warning
805 } else {
806 Severity::Error
807 })
808 .set_offline(self.offline)
809 .set_cached(cached)
810 .set_build_info(!no_artifacts && self.build_info)
811 .set_no_artifacts(no_artifacts);
812
813 if !self.skip.is_empty() {
814 let filter = SkipBuildFilters::new(self.skip.clone(), self.root.0.clone());
815 builder = builder.sparse_output(filter);
816 }
817
818 let project = builder.build(self.compiler()?)?;
819
820 if self.force {
821 self.cleanup(&project)?;
822 }
823
824 Ok(project)
825 }
826
827 pub fn cleanup<C: Compiler>(&self, project: &Project<C>) -> Result<(), SolcError> {
829 project.cleanup()?;
830
831 let remove_test_dir = |test_dir: &Option<PathBuf>| {
833 if let Some(test_dir) = test_dir {
834 let path = project.root().join(test_dir);
835 if path.exists() {
836 let _ = fs::remove_dir_all(&path);
837 }
838 }
839 };
840 remove_test_dir(&self.fuzz.failure_persist_dir);
841 remove_test_dir(&self.invariant.failure_persist_dir);
842
843 Ok(())
844 }
845
846 fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
853 if let Some(ref solc) = self.solc {
854 let solc = match solc {
855 SolcReq::Version(version) => {
856 if let Some(solc) = Solc::find_svm_installed_version(version)? {
857 solc
858 } else {
859 if self.offline {
860 return Err(SolcError::msg(format!(
861 "can't install missing solc {version} in offline mode"
862 )))
863 }
864 Solc::blocking_install(version)?
865 }
866 }
867 SolcReq::Local(solc) => {
868 if !solc.is_file() {
869 return Err(SolcError::msg(format!(
870 "`solc` {} does not exist",
871 solc.display()
872 )))
873 }
874 Solc::new(solc)?
875 }
876 };
877 return Ok(Some(solc))
878 }
879
880 Ok(None)
881 }
882
883 #[inline]
885 pub fn evm_spec_id(&self) -> SpecId {
886 if self.prague {
887 return SpecId::PRAGUE
888 }
889 evm_spec_id(&self.evm_version)
890 }
891
892 pub fn is_auto_detect(&self) -> bool {
897 if self.solc.is_some() {
898 return false
899 }
900 self.auto_detect_solc
901 }
902
903 pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
905 !self.no_storage_caching &&
906 self.rpc_storage_caching.enable_for_chain_id(chain_id.into()) &&
907 self.rpc_storage_caching.enable_for_endpoint(endpoint)
908 }
909
910 pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
924 let mut builder = ProjectPathsConfig::builder()
925 .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
926 .sources(&self.src)
927 .tests(&self.test)
928 .scripts(&self.script)
929 .artifacts(&self.out)
930 .libs(self.libs.iter())
931 .remappings(self.get_all_remappings())
932 .allowed_path(&self.root.0)
933 .allowed_paths(&self.libs)
934 .allowed_paths(&self.allow_paths)
935 .include_paths(&self.include_paths);
936
937 if let Some(build_info_path) = &self.build_info_path {
938 builder = builder.build_infos(build_info_path);
939 }
940
941 builder.build_with_root(&self.root.0)
942 }
943
944 pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
946 if let Some(solc) = self.ensure_solc()? {
947 Ok(SolcCompiler::Specific(solc))
948 } else {
949 Ok(SolcCompiler::AutoDetect)
950 }
951 }
952
953 pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
955 let vyper = if let Some(path) = &self.vyper.path {
956 Some(Vyper::new(path)?)
957 } else {
958 Vyper::new("vyper").ok()
959 };
960
961 Ok(vyper)
962 }
963
964 pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
966 Ok(MultiCompiler { solc: self.solc_compiler()?, vyper: self.vyper_compiler()? })
967 }
968
969 pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
971 Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
972 }
973
974 pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
993 self.remappings.iter().map(|m| m.clone().into())
994 }
995
996 pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1011 Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1012 }
1013
1014 pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1030 let maybe_alias = self.eth_rpc_url.as_ref().or(self.etherscan_api_key.as_ref())?;
1031 if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1032 Some(alias)
1033 } else {
1034 Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1035 }
1036 }
1037
1038 pub fn get_rpc_url_with_alias(
1054 &self,
1055 maybe_alias: &str,
1056 ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1057 let mut endpoints = self.rpc_endpoints.clone().resolved();
1058 Some(endpoints.remove(maybe_alias)?.map(Cow::Owned))
1059 }
1060
1061 pub fn get_rpc_url_or<'a>(
1073 &'a self,
1074 fallback: impl Into<Cow<'a, str>>,
1075 ) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1076 if let Some(url) = self.get_rpc_url() {
1077 url
1078 } else {
1079 Ok(fallback.into())
1080 }
1081 }
1082
1083 pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1095 self.get_rpc_url_or("http://localhost:8545")
1096 }
1097
1098 pub fn get_etherscan_config(
1118 &self,
1119 ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1120 self.get_etherscan_config_with_chain(None).transpose()
1121 }
1122
1123 pub fn get_etherscan_config_with_chain(
1130 &self,
1131 chain: Option<Chain>,
1132 ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1133 if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref()) {
1134 if self.etherscan.contains_key(maybe_alias) {
1135 return self.etherscan.clone().resolved().remove(maybe_alias).transpose()
1136 }
1137 }
1138
1139 if let Some(res) = chain
1141 .or(self.chain)
1142 .and_then(|chain| self.etherscan.clone().resolved().find_chain(chain))
1143 {
1144 match (res, self.etherscan_api_key.as_ref()) {
1145 (Ok(mut config), Some(key)) => {
1146 config.key.clone_from(key);
1149 return Ok(Some(config))
1150 }
1151 (Ok(config), None) => return Ok(Some(config)),
1152 (Err(err), None) => return Err(err),
1153 (Err(_), Some(_)) => {
1154 }
1156 }
1157 }
1158
1159 if let Some(key) = self.etherscan_api_key.as_ref() {
1161 let chain = chain.or(self.chain).unwrap_or_default();
1162 return Ok(ResolvedEtherscanConfig::create(key, chain))
1163 }
1164
1165 Ok(None)
1166 }
1167
1168 pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1174 self.get_etherscan_config_with_chain(chain).ok().flatten().map(|c| c.key)
1175 }
1176
1177 pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1184 get_dir_remapping(&self.src)
1185 }
1186
1187 pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1189 if self.root.0.join(&self.test).exists() {
1190 get_dir_remapping(&self.test)
1191 } else {
1192 None
1193 }
1194 }
1195
1196 pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1198 if self.root.0.join(&self.script).exists() {
1199 get_dir_remapping(&self.script)
1200 } else {
1201 None
1202 }
1203 }
1204
1205 pub fn optimizer(&self) -> Optimizer {
1211 Optimizer {
1212 enabled: Some(self.optimizer),
1213 runs: Some(self.optimizer_runs),
1214 details: self.optimizer_details.clone(),
1217 }
1218 }
1219
1220 pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1223 let mut extra_output = self.extra_output.clone();
1224
1225 if !extra_output.contains(&ContractOutputSelection::Metadata) {
1231 extra_output.push(ContractOutputSelection::Metadata);
1232 }
1233
1234 ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().cloned())
1235 }
1236
1237 pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1240 Libraries::parse(&self.libraries)
1241 }
1242
1243 pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1245 let paths: ProjectPathsConfig = self.project_paths();
1246 Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1247 }
1248
1249 pub fn solc_settings(&self) -> Result<Settings, SolcError> {
1254 let mut model_checker = self.model_checker.clone();
1258 if let Some(model_checker_settings) = &mut model_checker {
1259 if model_checker_settings.targets.is_none() {
1260 model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1261 }
1262 }
1263
1264 let mut settings = Settings {
1265 libraries: self.libraries_with_remappings()?,
1266 optimizer: self.optimizer(),
1267 evm_version: Some(self.evm_version),
1268 metadata: Some(SettingsMetadata {
1269 use_literal_content: Some(self.use_literal_content),
1270 bytecode_hash: Some(self.bytecode_hash),
1271 cbor_metadata: Some(self.cbor_metadata),
1272 }),
1273 debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1274 revert_strings: Some(revert_strings),
1275 debug_info: Vec::new(),
1277 }),
1278 model_checker,
1279 via_ir: Some(self.via_ir),
1280 stop_after: None,
1282 remappings: Vec::new(),
1284 output_selection: Default::default(),
1286 }
1287 .with_extra_output(self.configured_artifacts_handler().output_selection());
1288
1289 if self.ast || self.build_info {
1291 settings = settings.with_ast();
1292 }
1293
1294 Ok(settings)
1295 }
1296
1297 pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
1300 Ok(VyperSettings {
1301 evm_version: Some(self.evm_version),
1302 optimize: self.vyper.optimize,
1303 bytecode_metadata: None,
1304 output_selection: OutputSelection::common_output_selection([
1307 "abi".to_string(),
1308 "evm.bytecode".to_string(),
1309 "evm.deployedBytecode".to_string(),
1310 ]),
1311 })
1312 }
1313
1314 pub fn figment() -> Figment {
1335 Self::default().into()
1336 }
1337
1338 pub fn figment_with_root(root: impl Into<PathBuf>) -> Figment {
1350 Self::with_root(root).into()
1351 }
1352
1353 pub fn with_root(root: impl Into<PathBuf>) -> Self {
1362 let root = root.into();
1364 let paths = ProjectPathsConfig::builder().build_with_root::<()>(&root);
1365 let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
1366 Self {
1367 root: paths.root.into(),
1368 src: paths.sources.file_name().unwrap().into(),
1369 out: artifacts.clone(),
1370 libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
1371 remappings: paths
1372 .remappings
1373 .into_iter()
1374 .map(|r| RelativeRemapping::new(r, &root))
1375 .collect(),
1376 fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
1377 ..Self::default()
1378 }
1379 }
1380
1381 pub fn hardhat() -> Self {
1383 Self {
1384 src: "contracts".into(),
1385 out: "artifacts".into(),
1386 libs: vec!["node_modules".into()],
1387 ..Self::default()
1388 }
1389 }
1390
1391 pub fn dapptools() -> Self {
1393 Self {
1394 chain: Some(Chain::from_id(99)),
1395 block_timestamp: 0,
1396 block_number: 0,
1397 ..Self::default()
1398 }
1399 }
1400
1401 pub fn into_basic(self) -> BasicConfig {
1410 BasicConfig {
1411 profile: self.profile,
1412 src: self.src,
1413 out: self.out,
1414 libs: self.libs,
1415 remappings: self.remappings,
1416 }
1417 }
1418
1419 pub fn update_at<F>(root: impl Into<PathBuf>, f: F) -> eyre::Result<()>
1424 where
1425 F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
1426 {
1427 let config = Self::load_with_root(root).sanitized();
1428 config.update(|doc| f(&config, doc))
1429 }
1430
1431 pub fn update<F>(&self, f: F) -> eyre::Result<()>
1436 where
1437 F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
1438 {
1439 let file_path = self.get_config_path();
1440 if !file_path.exists() {
1441 return Ok(())
1442 }
1443 let contents = fs::read_to_string(&file_path)?;
1444 let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
1445 if f(&mut doc) {
1446 fs::write(file_path, doc.to_string())?;
1447 }
1448 Ok(())
1449 }
1450
1451 pub fn update_libs(&self) -> eyre::Result<()> {
1457 self.update(|doc| {
1458 let profile = self.profile.as_str().as_str();
1459 let root = &self.root.0;
1460 let libs: toml_edit::Value = self
1461 .libs
1462 .iter()
1463 .map(|path| {
1464 let path =
1465 if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
1466 toml_edit::Value::from(&*path.to_string_lossy())
1467 })
1468 .collect();
1469 let libs = toml_edit::value(libs);
1470 doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
1471 true
1472 })
1473 }
1474
1475 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
1487 let mut value = toml::Value::try_from(self)?;
1489 let value_table = value.as_table_mut().unwrap();
1491 let standalone_sections = Self::STANDALONE_SECTIONS
1493 .iter()
1494 .filter_map(|section| {
1495 let section = section.to_string();
1496 value_table.remove(§ion).map(|value| (section, value))
1497 })
1498 .collect::<Vec<_>>();
1499 let mut wrapping_table = [(
1501 Self::PROFILE_SECTION.into(),
1502 toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
1503 )]
1504 .into_iter()
1505 .collect::<toml::map::Map<_, _>>();
1506 for (section, value) in standalone_sections {
1508 wrapping_table.insert(section, value);
1509 }
1510 toml::to_string_pretty(&toml::Value::Table(wrapping_table))
1512 }
1513
1514 pub fn get_config_path(&self) -> PathBuf {
1516 self.root.0.join(Self::FILE_NAME)
1517 }
1518
1519 pub fn selected_profile() -> Profile {
1523 Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
1524 }
1525
1526 pub fn foundry_dir_toml() -> Option<PathBuf> {
1528 Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
1529 }
1530
1531 pub fn foundry_dir() -> Option<PathBuf> {
1533 dirs_next::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
1534 }
1535
1536 pub fn foundry_cache_dir() -> Option<PathBuf> {
1538 Self::foundry_dir().map(|p| p.join("cache"))
1539 }
1540
1541 pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
1543 Some(Self::foundry_cache_dir()?.join("rpc"))
1544 }
1545 pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1547 Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
1548 }
1549
1550 pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
1552 Some(Self::foundry_cache_dir()?.join("etherscan"))
1553 }
1554
1555 pub fn foundry_keystores_dir() -> Option<PathBuf> {
1557 Some(Self::foundry_dir()?.join("keystores"))
1558 }
1559
1560 pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1563 Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
1564 }
1565
1566 pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1569 Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
1570 }
1571
1572 pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1575 Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
1576 }
1577
1578 pub fn data_dir() -> eyre::Result<PathBuf> {
1586 let path = dirs_next::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
1587 std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
1588 Ok(path)
1589 }
1590
1591 pub fn find_config_file() -> Option<PathBuf> {
1598 fn find(path: &Path) -> Option<PathBuf> {
1599 if path.is_absolute() {
1600 return match path.is_file() {
1601 true => Some(path.to_path_buf()),
1602 false => None,
1603 }
1604 }
1605 let cwd = std::env::current_dir().ok()?;
1606 let mut cwd = cwd.as_path();
1607 loop {
1608 let file_path = cwd.join(path);
1609 if file_path.is_file() {
1610 return Some(file_path)
1611 }
1612 cwd = cwd.parent()?;
1613 }
1614 }
1615 find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
1616 .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
1617 }
1618
1619 pub fn clean_foundry_cache() -> eyre::Result<()> {
1621 if let Some(cache_dir) = Self::foundry_cache_dir() {
1622 let path = cache_dir.as_path();
1623 let _ = fs::remove_dir_all(path);
1624 } else {
1625 eyre::bail!("failed to get foundry_cache_dir");
1626 }
1627
1628 Ok(())
1629 }
1630
1631 pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<()> {
1633 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
1634 let path = cache_dir.as_path();
1635 let _ = fs::remove_dir_all(path);
1636 } else {
1637 eyre::bail!("failed to get foundry_chain_cache_dir");
1638 }
1639
1640 Ok(())
1641 }
1642
1643 pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<()> {
1645 if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
1646 let path = cache_dir.as_path();
1647 let _ = fs::remove_dir_all(path);
1648 } else {
1649 eyre::bail!("failed to get foundry_block_cache_dir");
1650 }
1651
1652 Ok(())
1653 }
1654
1655 pub fn clean_foundry_etherscan_cache() -> eyre::Result<()> {
1657 if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
1658 let path = cache_dir.as_path();
1659 let _ = fs::remove_dir_all(path);
1660 } else {
1661 eyre::bail!("failed to get foundry_etherscan_cache_dir");
1662 }
1663
1664 Ok(())
1665 }
1666
1667 pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<()> {
1669 if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
1670 let path = cache_dir.as_path();
1671 let _ = fs::remove_dir_all(path);
1672 } else {
1673 eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
1674 }
1675
1676 Ok(())
1677 }
1678
1679 pub fn list_foundry_cache() -> eyre::Result<Cache> {
1681 if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
1682 let mut cache = Cache { chains: vec![] };
1683 if !cache_dir.exists() {
1684 return Ok(cache)
1685 }
1686 if let Ok(entries) = cache_dir.as_path().read_dir() {
1687 for entry in entries.flatten().filter(|x| x.path().is_dir()) {
1688 match Chain::from_str(&entry.file_name().to_string_lossy()) {
1689 Ok(chain) => cache.chains.push(Self::list_foundry_chain_cache(chain)?),
1690 Err(_) => continue,
1691 }
1692 }
1693 Ok(cache)
1694 } else {
1695 eyre::bail!("failed to access foundry_cache_dir");
1696 }
1697 } else {
1698 eyre::bail!("failed to get foundry_cache_dir");
1699 }
1700 }
1701
1702 pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
1704 let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
1705 Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
1706 None => {
1707 warn!("failed to access foundry_etherscan_chain_cache_dir");
1708 0
1709 }
1710 };
1711
1712 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
1713 let blocks = Self::get_cached_blocks(&cache_dir)?;
1714 Ok(ChainCache {
1715 name: chain.to_string(),
1716 blocks,
1717 block_explorer: block_explorer_data_size,
1718 })
1719 } else {
1720 eyre::bail!("failed to get foundry_chain_cache_dir");
1721 }
1722 }
1723
1724 fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
1726 let mut blocks = vec![];
1727 if !chain_path.exists() {
1728 return Ok(blocks)
1729 }
1730 for block in chain_path.read_dir()?.flatten() {
1731 let file_type = block.file_type()?;
1732 let file_name = block.file_name();
1733 let filepath = if file_type.is_dir() {
1734 block.path().join("storage.json")
1735 } else if file_type.is_file() &&
1736 file_name.to_string_lossy().chars().all(char::is_numeric)
1737 {
1738 block.path()
1739 } else {
1740 continue
1741 };
1742 blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
1743 }
1744 Ok(blocks)
1745 }
1746
1747 fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
1749 if !chain_path.exists() {
1750 return Ok(0)
1751 }
1752
1753 fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
1754 dir.try_fold(0, |acc, file| {
1755 let file = file?;
1756 let size = match file.metadata()? {
1757 data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
1758 data => data.len(),
1759 };
1760 Ok(acc + size)
1761 })
1762 }
1763
1764 dir_size_recursive(fs::read_dir(chain_path)?)
1765 }
1766
1767 fn merge_toml_provider(
1768 mut figment: Figment,
1769 toml_provider: impl Provider,
1770 profile: Profile,
1771 ) -> Figment {
1772 figment = figment.select(profile.clone());
1773
1774 figment = {
1776 let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
1777 figment.merge(warnings)
1778 };
1779
1780 let mut profiles = vec![Self::DEFAULT_PROFILE];
1782 if profile != Self::DEFAULT_PROFILE {
1783 profiles.push(profile.clone());
1784 }
1785 let provider = toml_provider.strict_select(profiles);
1786
1787 let provider = BackwardsCompatTomlProvider(ForcedSnakeCaseData(provider));
1789
1790 if profile != Self::DEFAULT_PROFILE {
1792 figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
1793 }
1794 for standalone_key in Self::STANDALONE_SECTIONS {
1796 if let Some((_, fallback)) =
1797 STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
1798 {
1799 figment = figment.merge(
1800 provider
1801 .fallback(standalone_key, fallback)
1802 .wrap(profile.clone(), standalone_key),
1803 );
1804 } else {
1805 figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
1806 }
1807 }
1808 figment = figment.merge(provider);
1810 figment
1811 }
1812
1813 fn normalize_defaults(&mut self, figment: Figment) -> Figment {
1819 if let Ok(solc) = figment.extract_inner::<SolcReq>("solc") {
1820 if figment.find_value("evm_version").is_err() {
1823 if let Some(version) = solc
1824 .try_version()
1825 .ok()
1826 .and_then(|version| self.evm_version.normalize_version_solc(&version))
1827 {
1828 self.evm_version = version;
1830 }
1831 }
1832 }
1833
1834 figment
1835 }
1836}
1837
1838impl From<Config> for Figment {
1839 fn from(c: Config) -> Self {
1840 c.to_figment(FigmentProviders::All)
1841 }
1842}
1843
1844#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1846pub enum FigmentProviders {
1847 #[default]
1849 All,
1850 Cast,
1854 Anvil,
1858}
1859
1860impl FigmentProviders {
1861 pub const fn is_all(&self) -> bool {
1863 matches!(self, Self::All)
1864 }
1865
1866 pub const fn is_cast(&self) -> bool {
1868 matches!(self, Self::Cast)
1869 }
1870}
1871
1872#[derive(Clone, Debug, Serialize, Deserialize)]
1874#[serde(transparent)]
1875pub struct RegexWrapper {
1876 #[serde(with = "serde_regex")]
1877 inner: regex::Regex,
1878}
1879
1880impl std::ops::Deref for RegexWrapper {
1881 type Target = regex::Regex;
1882
1883 fn deref(&self) -> &Self::Target {
1884 &self.inner
1885 }
1886}
1887
1888impl std::cmp::PartialEq for RegexWrapper {
1889 fn eq(&self, other: &Self) -> bool {
1890 self.as_str() == other.as_str()
1891 }
1892}
1893
1894impl From<RegexWrapper> for regex::Regex {
1895 fn from(wrapper: RegexWrapper) -> Self {
1896 wrapper.inner
1897 }
1898}
1899
1900impl From<regex::Regex> for RegexWrapper {
1901 fn from(re: Regex) -> Self {
1902 Self { inner: re }
1903 }
1904}
1905
1906pub(crate) mod from_opt_glob {
1908 use serde::{Deserialize, Deserializer, Serializer};
1909
1910 pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
1911 where
1912 S: Serializer,
1913 {
1914 match value {
1915 Some(glob) => serializer.serialize_str(glob.glob()),
1916 None => serializer.serialize_none(),
1917 }
1918 }
1919
1920 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
1921 where
1922 D: Deserializer<'de>,
1923 {
1924 let s: Option<String> = Option::deserialize(deserializer)?;
1925 if let Some(s) = s {
1926 return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?))
1927 }
1928 Ok(None)
1929 }
1930}
1931
1932pub(crate) mod from_vec_glob {
1934 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1935
1936 pub fn serialize<S>(value: &[globset::Glob], serializer: S) -> Result<S::Ok, S::Error>
1937 where
1938 S: Serializer,
1939 {
1940 let value = value.iter().map(|g| g.glob()).collect::<Vec<_>>();
1941 value.serialize(serializer)
1942 }
1943
1944 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<globset::Glob>, D::Error>
1945 where
1946 D: Deserializer<'de>,
1947 {
1948 let s: Vec<String> = Vec::deserialize(deserializer)?;
1949 s.into_iter()
1950 .map(|s| globset::Glob::new(&s))
1951 .collect::<Result<Vec<_>, _>>()
1952 .map_err(serde::de::Error::custom)
1953 }
1954}
1955
1956#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
1958#[serde(transparent)]
1959pub struct RootPath(pub PathBuf);
1960
1961impl Default for RootPath {
1962 fn default() -> Self {
1963 ".".into()
1964 }
1965}
1966
1967impl<P: Into<PathBuf>> From<P> for RootPath {
1968 fn from(p: P) -> Self {
1969 Self(p.into())
1970 }
1971}
1972
1973impl AsRef<Path> for RootPath {
1974 fn as_ref(&self) -> &Path {
1975 &self.0
1976 }
1977}
1978
1979pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
1990 s: &str,
1991) -> Result<Option<(Profile, T)>, Error> {
1992 let figment = Config::merge_toml_provider(
1993 Figment::new(),
1994 Toml::string(s).nested(),
1995 Config::DEFAULT_PROFILE,
1996 );
1997 if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
1998 Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
1999 } else {
2000 Ok(None)
2001 }
2002}
2003
2004impl Provider for Config {
2005 fn metadata(&self) -> Metadata {
2006 Metadata::named("Foundry Config")
2007 }
2008
2009 #[track_caller]
2010 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2011 let mut data = Serialized::defaults(self).data()?;
2012 if let Some(entry) = data.get_mut(&self.profile) {
2013 entry.insert("root".to_string(), Value::serialize(self.root.clone())?);
2014 }
2015 Ok(data)
2016 }
2017
2018 fn profile(&self) -> Option<Profile> {
2019 Some(self.profile.clone())
2020 }
2021}
2022
2023impl Default for Config {
2024 fn default() -> Self {
2025 Self {
2026 profile: Self::DEFAULT_PROFILE,
2027 fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2028 prague: false,
2029 #[cfg(not(feature = "isolate-by-default"))]
2030 isolate: false,
2031 #[cfg(feature = "isolate-by-default")]
2032 isolate: true,
2033 root: Default::default(),
2034 src: "src".into(),
2035 test: "test".into(),
2036 script: "script".into(),
2037 out: "out".into(),
2038 libs: vec!["lib".into()],
2039 cache: true,
2040 cache_path: "cache".into(),
2041 broadcast: "broadcast".into(),
2042 allow_paths: vec![],
2043 include_paths: vec![],
2044 force: false,
2045 evm_version: EvmVersion::Paris,
2046 gas_reports: vec!["*".to_string()],
2047 gas_reports_ignore: vec![],
2048 solc: None,
2049 vyper: Default::default(),
2050 auto_detect_solc: true,
2051 offline: false,
2052 optimizer: true,
2053 optimizer_runs: 200,
2054 optimizer_details: None,
2055 model_checker: None,
2056 extra_output: Default::default(),
2057 extra_output_files: Default::default(),
2058 names: false,
2059 sizes: false,
2060 test_pattern: None,
2061 test_pattern_inverse: None,
2062 contract_pattern: None,
2063 contract_pattern_inverse: None,
2064 path_pattern: None,
2065 path_pattern_inverse: None,
2066 fuzz: FuzzConfig::new("cache/fuzz".into()),
2067 invariant: InvariantConfig::new("cache/invariant".into()),
2068 always_use_create_2_factory: false,
2069 ffi: false,
2070 prompt_timeout: 120,
2071 sender: Self::DEFAULT_SENDER,
2072 tx_origin: Self::DEFAULT_SENDER,
2073 initial_balance: U256::from(0xffffffffffffffffffffffffu128),
2074 block_number: 1,
2075 fork_block_number: None,
2076 chain: None,
2077 gas_limit: i64::MAX.into(),
2078 code_size_limit: None,
2079 gas_price: None,
2080 block_base_fee_per_gas: 0,
2081 block_coinbase: Address::ZERO,
2082 block_timestamp: 1,
2083 block_difficulty: 0,
2084 block_prevrandao: Default::default(),
2085 block_gas_limit: None,
2086 disable_block_gas_limit: false,
2087 memory_limit: 1 << 27, eth_rpc_url: None,
2089 eth_rpc_jwt: None,
2090 etherscan_api_key: None,
2091 verbosity: 0,
2092 remappings: vec![],
2093 auto_detect_remappings: true,
2094 libraries: vec![],
2095 ignored_error_codes: vec![
2096 SolidityErrorCode::SpdxLicenseNotProvided,
2097 SolidityErrorCode::ContractExceeds24576Bytes,
2098 SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2099 SolidityErrorCode::TransientStorageUsed,
2100 ],
2101 ignored_file_paths: vec![],
2102 deny_warnings: false,
2103 via_ir: false,
2104 ast: false,
2105 rpc_storage_caching: Default::default(),
2106 rpc_endpoints: Default::default(),
2107 etherscan: Default::default(),
2108 no_storage_caching: false,
2109 no_rpc_rate_limit: false,
2110 use_literal_content: false,
2111 bytecode_hash: BytecodeHash::Ipfs,
2112 cbor_metadata: true,
2113 revert_strings: None,
2114 sparse_mode: false,
2115 build_info: false,
2116 build_info_path: None,
2117 fmt: Default::default(),
2118 doc: Default::default(),
2119 labels: Default::default(),
2120 unchecked_cheatcode_artifacts: false,
2121 create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
2122 skip: vec![],
2123 dependencies: Default::default(),
2124 warnings: vec![],
2125 _non_exhaustive: (),
2126 }
2127 }
2128}
2129
2130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2135pub struct GasLimit(pub u64);
2136
2137impl From<u64> for GasLimit {
2138 fn from(gas: u64) -> Self {
2139 Self(gas)
2140 }
2141}
2142impl From<i64> for GasLimit {
2143 fn from(gas: i64) -> Self {
2144 Self(gas as u64)
2145 }
2146}
2147impl From<i32> for GasLimit {
2148 fn from(gas: i32) -> Self {
2149 Self(gas as u64)
2150 }
2151}
2152impl From<u32> for GasLimit {
2153 fn from(gas: u32) -> Self {
2154 Self(gas as u64)
2155 }
2156}
2157
2158impl From<GasLimit> for u64 {
2159 fn from(gas: GasLimit) -> Self {
2160 gas.0
2161 }
2162}
2163
2164impl Serialize for GasLimit {
2165 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2166 where
2167 S: Serializer,
2168 {
2169 if self.0 > i64::MAX as u64 {
2170 serializer.serialize_str(&self.0.to_string())
2171 } else {
2172 serializer.serialize_u64(self.0)
2173 }
2174 }
2175}
2176
2177impl<'de> Deserialize<'de> for GasLimit {
2178 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2179 where
2180 D: Deserializer<'de>,
2181 {
2182 use serde::de::Error;
2183
2184 #[derive(Deserialize)]
2185 #[serde(untagged)]
2186 enum Gas {
2187 Number(u64),
2188 Text(String),
2189 }
2190
2191 let gas = match Gas::deserialize(deserializer)? {
2192 Gas::Number(num) => Self(num),
2193 Gas::Text(s) => match s.as_str() {
2194 "max" | "MAX" | "Max" | "u64::MAX" | "u64::Max" => Self(u64::MAX),
2195 s => Self(s.parse().map_err(D::Error::custom)?),
2196 },
2197 };
2198
2199 Ok(gas)
2200 }
2201}
2202
2203#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2205#[serde(untagged)]
2206pub enum SolcReq {
2207 Version(Version),
2210 Local(PathBuf),
2212}
2213
2214impl SolcReq {
2215 fn try_version(&self) -> Result<Version, SolcError> {
2220 match self {
2221 Self::Version(version) => Ok(version.clone()),
2222 Self::Local(path) => Solc::new(path).map(|solc| solc.version),
2223 }
2224 }
2225}
2226
2227impl<T: AsRef<str>> From<T> for SolcReq {
2228 fn from(s: T) -> Self {
2229 let s = s.as_ref();
2230 if let Ok(v) = Version::from_str(s) {
2231 Self::Version(v)
2232 } else {
2233 Self::Local(s.into())
2234 }
2235 }
2236}
2237
2238struct TomlFileProvider {
2241 pub env_var: Option<&'static str>,
2242 pub default: PathBuf,
2243 pub cache: Option<Result<Map<Profile, Dict>, Error>>,
2244}
2245
2246impl TomlFileProvider {
2247 fn new(env_var: Option<&'static str>, default: impl Into<PathBuf>) -> Self {
2248 Self { env_var, default: default.into(), cache: None }
2249 }
2250
2251 fn env_val(&self) -> Option<String> {
2252 self.env_var.and_then(Env::var)
2253 }
2254
2255 fn file(&self) -> PathBuf {
2256 self.env_val().map(PathBuf::from).unwrap_or_else(|| self.default.clone())
2257 }
2258
2259 fn is_missing(&self) -> bool {
2260 if let Some(file) = self.env_val() {
2261 let path = Path::new(&file);
2262 if !path.exists() {
2263 return true
2264 }
2265 }
2266 false
2267 }
2268
2269 pub fn cached(mut self) -> Self {
2270 self.cache = Some(self.read());
2271 self
2272 }
2273
2274 fn read(&self) -> Result<Map<Profile, Dict>, Error> {
2275 use serde::de::Error as _;
2276 if let Some(file) = self.env_val() {
2277 let path = Path::new(&file);
2278 if !path.exists() {
2279 return Err(Error::custom(format!(
2280 "Config file `{}` set in env var `{}` does not exist",
2281 file,
2282 self.env_var.unwrap()
2283 )))
2284 }
2285 Toml::file(file)
2286 } else {
2287 Toml::file(&self.default)
2288 }
2289 .nested()
2290 .data()
2291 }
2292}
2293
2294impl Provider for TomlFileProvider {
2295 fn metadata(&self) -> Metadata {
2296 if self.is_missing() {
2297 Metadata::named("TOML file provider")
2298 } else {
2299 Toml::file(self.file()).nested().metadata()
2300 }
2301 }
2302
2303 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2304 if let Some(cache) = self.cache.as_ref() {
2305 cache.clone()
2306 } else {
2307 self.read()
2308 }
2309 }
2310}
2311
2312struct ForcedSnakeCaseData<P>(P);
2315
2316impl<P: Provider> Provider for ForcedSnakeCaseData<P> {
2317 fn metadata(&self) -> Metadata {
2318 self.0.metadata()
2319 }
2320
2321 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2322 let mut map = Map::new();
2323 for (profile, dict) in self.0.data()? {
2324 if Config::STANDALONE_SECTIONS.contains(&profile.as_ref()) {
2325 map.insert(profile, dict);
2327 continue
2328 }
2329 map.insert(profile, dict.into_iter().map(|(k, v)| (k.to_snake_case(), v)).collect());
2330 }
2331 Ok(map)
2332 }
2333}
2334
2335struct BackwardsCompatTomlProvider<P>(P);
2337
2338impl<P: Provider> Provider for BackwardsCompatTomlProvider<P> {
2339 fn metadata(&self) -> Metadata {
2340 self.0.metadata()
2341 }
2342
2343 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2344 let mut map = Map::new();
2345 let solc_env = std::env::var("FOUNDRY_SOLC_VERSION")
2346 .or_else(|_| std::env::var("DAPP_SOLC_VERSION"))
2347 .map(Value::from)
2348 .ok();
2349 for (profile, mut dict) in self.0.data()? {
2350 if let Some(v) = solc_env.clone() {
2351 dict.insert("solc".to_string(), v);
2353 } else if let Some(v) = dict.remove("solc_version") {
2354 if !dict.contains_key("solc") {
2356 dict.insert("solc".to_string(), v);
2357 }
2358 }
2359 map.insert(profile, dict);
2360 }
2361 Ok(map)
2362 }
2363}
2364
2365struct DappHardhatDirProvider<'a>(&'a Path);
2367
2368impl<'a> Provider for DappHardhatDirProvider<'a> {
2369 fn metadata(&self) -> Metadata {
2370 Metadata::named("Dapp Hardhat dir compat")
2371 }
2372
2373 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2374 let mut dict = Dict::new();
2375 dict.insert(
2376 "src".to_string(),
2377 ProjectPathsConfig::find_source_dir(self.0)
2378 .file_name()
2379 .unwrap()
2380 .to_string_lossy()
2381 .to_string()
2382 .into(),
2383 );
2384 dict.insert(
2385 "out".to_string(),
2386 ProjectPathsConfig::find_artifacts_dir(self.0)
2387 .file_name()
2388 .unwrap()
2389 .to_string_lossy()
2390 .to_string()
2391 .into(),
2392 );
2393
2394 let mut libs = vec![];
2399 let node_modules = self.0.join("node_modules");
2400 let lib = self.0.join("lib");
2401 if node_modules.exists() {
2402 if lib.exists() {
2403 libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
2404 }
2405 libs.push(node_modules.file_name().unwrap().to_string_lossy().to_string());
2406 } else {
2407 libs.push(lib.file_name().unwrap().to_string_lossy().to_string());
2408 }
2409
2410 dict.insert("libs".to_string(), libs.into());
2411
2412 Ok(Map::from([(Config::selected_profile(), dict)]))
2413 }
2414}
2415
2416struct DappEnvCompatProvider;
2418
2419impl Provider for DappEnvCompatProvider {
2420 fn metadata(&self) -> Metadata {
2421 Metadata::named("Dapp env compat")
2422 }
2423
2424 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2425 use serde::de::Error as _;
2426 use std::env;
2427
2428 let mut dict = Dict::new();
2429 if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
2430 dict.insert(
2431 "block_number".to_string(),
2432 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
2433 );
2434 }
2435 if let Ok(val) = env::var("DAPP_TEST_ADDRESS") {
2436 dict.insert("sender".to_string(), val.into());
2437 }
2438 if let Ok(val) = env::var("DAPP_FORK_BLOCK") {
2439 dict.insert(
2440 "fork_block_number".to_string(),
2441 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
2442 );
2443 } else if let Ok(val) = env::var("DAPP_TEST_NUMBER") {
2444 dict.insert(
2445 "fork_block_number".to_string(),
2446 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
2447 );
2448 }
2449 if let Ok(val) = env::var("DAPP_TEST_TIMESTAMP") {
2450 dict.insert(
2451 "block_timestamp".to_string(),
2452 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
2453 );
2454 }
2455 if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE_RUNS") {
2456 dict.insert(
2457 "optimizer_runs".to_string(),
2458 val.parse::<u64>().map_err(figment::Error::custom)?.into(),
2459 );
2460 }
2461 if let Ok(val) = env::var("DAPP_BUILD_OPTIMIZE") {
2462 let val = val.parse::<u8>().map_err(figment::Error::custom)?;
2464 if val > 1 {
2465 return Err(
2466 format!("Invalid $DAPP_BUILD_OPTIMIZE value `{val}`, expected 0 or 1").into()
2467 )
2468 }
2469 dict.insert("optimizer".to_string(), (val == 1).into());
2470 }
2471
2472 if let Ok(val) = env::var("DAPP_LIBRARIES").or_else(|_| env::var("FOUNDRY_LIBRARIES")) {
2474 dict.insert("libraries".to_string(), utils::to_array_value(&val)?);
2475 }
2476
2477 let mut fuzz_dict = Dict::new();
2478 if let Ok(val) = env::var("DAPP_TEST_FUZZ_RUNS") {
2479 fuzz_dict.insert(
2480 "runs".to_string(),
2481 val.parse::<u32>().map_err(figment::Error::custom)?.into(),
2482 );
2483 }
2484 dict.insert("fuzz".to_string(), fuzz_dict.into());
2485
2486 let mut invariant_dict = Dict::new();
2487 if let Ok(val) = env::var("DAPP_TEST_DEPTH") {
2488 invariant_dict.insert(
2489 "depth".to_string(),
2490 val.parse::<u32>().map_err(figment::Error::custom)?.into(),
2491 );
2492 }
2493 dict.insert("invariant".to_string(), invariant_dict.into());
2494
2495 Ok(Map::from([(Config::selected_profile(), dict)]))
2496 }
2497}
2498
2499struct RenameProfileProvider<P> {
2515 provider: P,
2516 from: Profile,
2517 to: Profile,
2518}
2519
2520impl<P> RenameProfileProvider<P> {
2521 pub fn new(provider: P, from: impl Into<Profile>, to: impl Into<Profile>) -> Self {
2522 Self { provider, from: from.into(), to: to.into() }
2523 }
2524}
2525
2526impl<P: Provider> Provider for RenameProfileProvider<P> {
2527 fn metadata(&self) -> Metadata {
2528 self.provider.metadata()
2529 }
2530 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2531 let mut data = self.provider.data()?;
2532 if let Some(data) = data.remove(&self.from) {
2533 return Ok(Map::from([(self.to.clone(), data)]))
2534 }
2535 Ok(Default::default())
2536 }
2537 fn profile(&self) -> Option<Profile> {
2538 Some(self.to.clone())
2539 }
2540}
2541
2542struct UnwrapProfileProvider<P> {
2558 provider: P,
2559 wrapping_key: Profile,
2560 profile: Profile,
2561}
2562
2563impl<P> UnwrapProfileProvider<P> {
2564 pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
2565 Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
2566 }
2567}
2568
2569impl<P: Provider> Provider for UnwrapProfileProvider<P> {
2570 fn metadata(&self) -> Metadata {
2571 self.provider.metadata()
2572 }
2573 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2574 self.provider.data().and_then(|mut data| {
2575 if let Some(profiles) = data.remove(&self.wrapping_key) {
2576 for (profile_str, profile_val) in profiles {
2577 let profile = Profile::new(&profile_str);
2578 if profile != self.profile {
2579 continue
2580 }
2581 match profile_val {
2582 Value::Dict(_, dict) => return Ok(profile.collect(dict)),
2583 bad_val => {
2584 let mut err = Error::from(figment::error::Kind::InvalidType(
2585 bad_val.to_actual(),
2586 "dict".into(),
2587 ));
2588 err.metadata = Some(self.provider.metadata());
2589 err.profile = Some(self.profile.clone());
2590 return Err(err)
2591 }
2592 }
2593 }
2594 }
2595 Ok(Default::default())
2596 })
2597 }
2598 fn profile(&self) -> Option<Profile> {
2599 Some(self.profile.clone())
2600 }
2601}
2602
2603struct WrapProfileProvider<P> {
2619 provider: P,
2620 wrapping_key: Profile,
2621 profile: Profile,
2622}
2623
2624impl<P> WrapProfileProvider<P> {
2625 pub fn new(provider: P, wrapping_key: impl Into<Profile>, profile: impl Into<Profile>) -> Self {
2626 Self { provider, wrapping_key: wrapping_key.into(), profile: profile.into() }
2627 }
2628}
2629
2630impl<P: Provider> Provider for WrapProfileProvider<P> {
2631 fn metadata(&self) -> Metadata {
2632 self.provider.metadata()
2633 }
2634 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2635 if let Some(inner) = self.provider.data()?.remove(&self.profile) {
2636 let value = Value::from(inner);
2637 let dict = [(self.profile.to_string().to_snake_case(), value)].into_iter().collect();
2638 Ok(self.wrapping_key.collect(dict))
2639 } else {
2640 Ok(Default::default())
2641 }
2642 }
2643 fn profile(&self) -> Option<Profile> {
2644 Some(self.profile.clone())
2645 }
2646}
2647
2648struct OptionalStrictProfileProvider<P> {
2671 provider: P,
2672 profiles: Vec<Profile>,
2673}
2674
2675impl<P> OptionalStrictProfileProvider<P> {
2676 pub const PROFILE_PROFILE: Profile = Profile::const_new("profile");
2677
2678 pub fn new(provider: P, profiles: impl IntoIterator<Item = impl Into<Profile>>) -> Self {
2679 Self { provider, profiles: profiles.into_iter().map(|profile| profile.into()).collect() }
2680 }
2681}
2682
2683impl<P: Provider> Provider for OptionalStrictProfileProvider<P> {
2684 fn metadata(&self) -> Metadata {
2685 self.provider.metadata()
2686 }
2687 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
2688 let mut figment = Figment::from(&self.provider);
2689 for profile in &self.profiles {
2690 figment = figment.merge(UnwrapProfileProvider::new(
2691 &self.provider,
2692 Self::PROFILE_PROFILE,
2693 profile.clone(),
2694 ));
2695 }
2696 figment.data().map_err(|err| {
2697 if let Err(root_err) = self.provider.data() {
2702 return root_err
2703 }
2704 err
2705 })
2706 }
2707 fn profile(&self) -> Option<Profile> {
2708 self.profiles.last().cloned()
2709 }
2710}
2711
2712trait ProviderExt: Provider {
2713 fn rename(
2714 &self,
2715 from: impl Into<Profile>,
2716 to: impl Into<Profile>,
2717 ) -> RenameProfileProvider<&Self> {
2718 RenameProfileProvider::new(self, from, to)
2719 }
2720
2721 fn wrap(
2722 &self,
2723 wrapping_key: impl Into<Profile>,
2724 profile: impl Into<Profile>,
2725 ) -> WrapProfileProvider<&Self> {
2726 WrapProfileProvider::new(self, wrapping_key, profile)
2727 }
2728
2729 fn strict_select(
2730 &self,
2731 profiles: impl IntoIterator<Item = impl Into<Profile>>,
2732 ) -> OptionalStrictProfileProvider<&Self> {
2733 OptionalStrictProfileProvider::new(self, profiles)
2734 }
2735
2736 fn fallback(
2737 &self,
2738 profile: impl Into<Profile>,
2739 fallback: impl Into<Profile>,
2740 ) -> FallbackProfileProvider<&Self> {
2741 FallbackProfileProvider::new(self, profile, fallback)
2742 }
2743}
2744impl<P: Provider> ProviderExt for P {}
2745
2746#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2758pub struct BasicConfig {
2759 #[serde(skip)]
2761 pub profile: Profile,
2762 pub src: PathBuf,
2764 pub out: PathBuf,
2766 pub libs: Vec<PathBuf>,
2768 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2770 pub remappings: Vec<RelativeRemapping>,
2771}
2772
2773impl BasicConfig {
2774 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2778 let s = toml::to_string_pretty(self)?;
2779 Ok(format!(
2780 "\
2781[profile.{}]
2782{s}
2783# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n",
2784 self.profile
2785 ))
2786 }
2787}
2788
2789pub(crate) mod from_str_lowercase {
2790 use serde::{Deserialize, Deserializer, Serializer};
2791 use std::str::FromStr;
2792
2793 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
2794 where
2795 T: std::fmt::Display,
2796 S: Serializer,
2797 {
2798 serializer.collect_str(&value.to_string().to_lowercase())
2799 }
2800
2801 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
2802 where
2803 D: Deserializer<'de>,
2804 T: FromStr,
2805 T::Err: std::fmt::Display,
2806 {
2807 String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
2808 }
2809}
2810
2811fn canonic(path: impl Into<PathBuf>) -> PathBuf {
2812 let path = path.into();
2813 foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
2814}
2815
2816#[cfg(test)]
2817mod tests {
2818 use super::*;
2819 use crate::{
2820 cache::{CachedChains, CachedEndpoints},
2821 endpoints::{RpcEndpointConfig, RpcEndpointType},
2822 etherscan::ResolvedEtherscanConfigs,
2823 };
2824 use figment::error::Kind::InvalidType;
2825 use foundry_compilers::artifacts::{
2826 vyper::VyperOptimizationMode, ModelCheckerEngine, YulDetails,
2827 };
2828 use similar_asserts::assert_eq;
2829 use std::{collections::BTreeMap, fs::File, io::Write};
2830 use tempfile::tempdir;
2831 use NamedChain::Moonbeam;
2832
2833 fn clear_warning(config: &mut Config) {
2836 config.warnings = vec![];
2837 }
2838
2839 #[test]
2840 fn default_sender() {
2841 assert_eq!(
2842 Config::DEFAULT_SENDER,
2843 Address::from_str("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38").unwrap()
2844 );
2845 }
2846
2847 #[test]
2848 fn test_caching() {
2849 let mut config = Config::default();
2850 let chain_id = NamedChain::Mainnet;
2851 let url = "https://eth-mainnet.alchemyapi";
2852 assert!(config.enable_caching(url, chain_id));
2853
2854 config.no_storage_caching = true;
2855 assert!(!config.enable_caching(url, chain_id));
2856
2857 config.no_storage_caching = false;
2858 assert!(!config.enable_caching(url, NamedChain::Dev));
2859 }
2860
2861 #[test]
2862 fn test_install_dir() {
2863 figment::Jail::expect_with(|jail| {
2864 let config = Config::load();
2865 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2866 jail.create_file(
2867 "foundry.toml",
2868 r"
2869 [profile.default]
2870 libs = ['node_modules', 'lib']
2871 ",
2872 )?;
2873 let config = Config::load();
2874 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2875
2876 jail.create_file(
2877 "foundry.toml",
2878 r"
2879 [profile.default]
2880 libs = ['custom', 'node_modules', 'lib']
2881 ",
2882 )?;
2883 let config = Config::load();
2884 assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
2885
2886 Ok(())
2887 });
2888 }
2889
2890 #[test]
2891 fn test_figment_is_default() {
2892 figment::Jail::expect_with(|_| {
2893 let mut default: Config = Config::figment().extract().unwrap();
2894 default.profile = Config::default().profile;
2895 assert_eq!(default, Config::default());
2896 Ok(())
2897 });
2898 }
2899
2900 #[test]
2901 fn test_default_round_trip() {
2902 figment::Jail::expect_with(|_| {
2903 let original = Config::figment();
2904 let roundtrip = Figment::from(Config::from_provider(&original));
2905 for figment in &[original, roundtrip] {
2906 let config = Config::from_provider(figment);
2907 assert_eq!(config, Config::default());
2908 }
2909 Ok(())
2910 });
2911 }
2912
2913 #[test]
2914 fn ffi_env_disallowed() {
2915 figment::Jail::expect_with(|jail| {
2916 jail.set_env("FOUNDRY_FFI", "true");
2917 jail.set_env("FFI", "true");
2918 jail.set_env("DAPP_FFI", "true");
2919 let config = Config::load();
2920 assert!(!config.ffi);
2921
2922 Ok(())
2923 });
2924 }
2925
2926 #[test]
2927 fn test_profile_env() {
2928 figment::Jail::expect_with(|jail| {
2929 jail.set_env("FOUNDRY_PROFILE", "default");
2930 let figment = Config::figment();
2931 assert_eq!(figment.profile(), "default");
2932
2933 jail.set_env("FOUNDRY_PROFILE", "hardhat");
2934 let figment: Figment = Config::hardhat().into();
2935 assert_eq!(figment.profile(), "hardhat");
2936
2937 jail.create_file(
2938 "foundry.toml",
2939 r"
2940 [profile.default]
2941 libs = ['lib']
2942 [profile.local]
2943 libs = ['modules']
2944 ",
2945 )?;
2946 jail.set_env("FOUNDRY_PROFILE", "local");
2947 let config = Config::load();
2948 assert_eq!(config.libs, vec![PathBuf::from("modules")]);
2949
2950 Ok(())
2951 });
2952 }
2953
2954 #[test]
2955 fn test_default_test_path() {
2956 figment::Jail::expect_with(|_| {
2957 let config = Config::default();
2958 let paths_config = config.project_paths::<Solc>();
2959 assert_eq!(paths_config.tests, PathBuf::from(r"test"));
2960 Ok(())
2961 });
2962 }
2963
2964 #[test]
2965 fn test_default_libs() {
2966 figment::Jail::expect_with(|jail| {
2967 let config = Config::load();
2968 assert_eq!(config.libs, vec![PathBuf::from("lib")]);
2969
2970 fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
2971 let config = Config::load();
2972 assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
2973
2974 fs::create_dir_all(jail.directory().join("lib")).unwrap();
2975 let config = Config::load();
2976 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2977
2978 Ok(())
2979 });
2980 }
2981
2982 #[test]
2983 fn test_inheritance_from_default_test_path() {
2984 figment::Jail::expect_with(|jail| {
2985 jail.create_file(
2986 "foundry.toml",
2987 r#"
2988 [profile.default]
2989 test = "defaulttest"
2990 src = "defaultsrc"
2991 libs = ['lib', 'node_modules']
2992
2993 [profile.custom]
2994 src = "customsrc"
2995 "#,
2996 )?;
2997
2998 let config = Config::load();
2999 assert_eq!(config.src, PathBuf::from("defaultsrc"));
3000 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3001
3002 jail.set_env("FOUNDRY_PROFILE", "custom");
3003 let config = Config::load();
3004
3005 assert_eq!(config.src, PathBuf::from("customsrc"));
3006 assert_eq!(config.test, PathBuf::from("defaulttest"));
3007 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3008
3009 Ok(())
3010 });
3011 }
3012
3013 #[test]
3014 fn test_custom_test_path() {
3015 figment::Jail::expect_with(|jail| {
3016 jail.create_file(
3017 "foundry.toml",
3018 r#"
3019 [profile.default]
3020 test = "mytest"
3021 "#,
3022 )?;
3023
3024 let config = Config::load();
3025 let paths_config = config.project_paths::<Solc>();
3026 assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
3027 Ok(())
3028 });
3029 }
3030
3031 #[test]
3032 fn test_remappings() {
3033 figment::Jail::expect_with(|jail| {
3034 jail.create_file(
3035 "foundry.toml",
3036 r#"
3037 [profile.default]
3038 src = "some-source"
3039 out = "some-out"
3040 cache = true
3041 "#,
3042 )?;
3043 let config = Config::load();
3044 assert!(config.remappings.is_empty());
3045
3046 jail.create_file(
3047 "remappings.txt",
3048 r"
3049 file-ds-test/=lib/ds-test/
3050 file-other/=lib/other/
3051 ",
3052 )?;
3053
3054 let config = Config::load();
3055 assert_eq!(
3056 config.remappings,
3057 vec![
3058 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3059 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3060 ],
3061 );
3062
3063 jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
3064 let config = Config::load();
3065
3066 assert_eq!(
3067 config.remappings,
3068 vec![
3069 Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
3071 Remapping::from_str("other/=lib/other/").unwrap().into(),
3072 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3074 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3075 ],
3076 );
3077
3078 Ok(())
3079 });
3080 }
3081
3082 #[test]
3083 fn test_remappings_override() {
3084 figment::Jail::expect_with(|jail| {
3085 jail.create_file(
3086 "foundry.toml",
3087 r#"
3088 [profile.default]
3089 src = "some-source"
3090 out = "some-out"
3091 cache = true
3092 "#,
3093 )?;
3094 let config = Config::load();
3095 assert!(config.remappings.is_empty());
3096
3097 jail.create_file(
3098 "remappings.txt",
3099 r"
3100 ds-test/=lib/ds-test/
3101 other/=lib/other/
3102 ",
3103 )?;
3104
3105 let config = Config::load();
3106 assert_eq!(
3107 config.remappings,
3108 vec![
3109 Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
3110 Remapping::from_str("other/=lib/other/").unwrap().into(),
3111 ],
3112 );
3113
3114 jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
3115 let config = Config::load();
3116
3117 assert_eq!(
3122 config.remappings,
3123 vec![
3124 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
3125 Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
3126 Remapping::from_str("other/=lib/other/").unwrap().into(),
3127 ],
3128 );
3129
3130 assert_eq!(
3132 config.get_all_remappings().collect::<Vec<_>>(),
3133 vec![
3134 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
3135 Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
3136 Remapping::from_str("other/=lib/other/").unwrap(),
3137 ],
3138 );
3139
3140 Ok(())
3141 });
3142 }
3143
3144 #[test]
3145 fn test_can_update_libs() {
3146 figment::Jail::expect_with(|jail| {
3147 jail.create_file(
3148 "foundry.toml",
3149 r#"
3150 [profile.default]
3151 libs = ["node_modules"]
3152 "#,
3153 )?;
3154
3155 let mut config = Config::load();
3156 config.libs.push("libs".into());
3157 config.update_libs().unwrap();
3158
3159 let config = Config::load();
3160 assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
3161 Ok(())
3162 });
3163 }
3164
3165 #[test]
3166 fn test_large_gas_limit() {
3167 figment::Jail::expect_with(|jail| {
3168 let gas = u64::MAX;
3169 jail.create_file(
3170 "foundry.toml",
3171 &format!(
3172 r#"
3173 [profile.default]
3174 gas_limit = "{gas}"
3175 "#
3176 ),
3177 )?;
3178
3179 let config = Config::load();
3180 assert_eq!(config, Config { gas_limit: gas.into(), ..Config::default() });
3181
3182 Ok(())
3183 });
3184 }
3185
3186 #[test]
3187 #[should_panic]
3188 fn test_toml_file_parse_failure() {
3189 figment::Jail::expect_with(|jail| {
3190 jail.create_file(
3191 "foundry.toml",
3192 r#"
3193 [profile.default]
3194 eth_rpc_url = "https://example.com/
3195 "#,
3196 )?;
3197
3198 let _config = Config::load();
3199
3200 Ok(())
3201 });
3202 }
3203
3204 #[test]
3205 #[should_panic]
3206 fn test_toml_file_non_existing_config_var_failure() {
3207 figment::Jail::expect_with(|jail| {
3208 jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3209
3210 let _config = Config::load();
3211
3212 Ok(())
3213 });
3214 }
3215
3216 #[test]
3217 fn test_resolve_etherscan_with_chain() {
3218 figment::Jail::expect_with(|jail| {
3219 let env_key = "__BSC_ETHERSCAN_API_KEY";
3220 let env_value = "env value";
3221 jail.create_file(
3222 "foundry.toml",
3223 r#"
3224 [profile.default]
3225
3226 [etherscan]
3227 bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3228 "#,
3229 )?;
3230
3231 let config = Config::load();
3232 assert!(config
3233 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3234 .is_err());
3235
3236 std::env::set_var(env_key, env_value);
3237
3238 assert_eq!(
3239 config
3240 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3241 .unwrap()
3242 .unwrap()
3243 .key,
3244 env_value
3245 );
3246
3247 let mut with_key = config;
3248 with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3249
3250 assert_eq!(
3251 with_key
3252 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3253 .unwrap()
3254 .unwrap()
3255 .key,
3256 "via etherscan_api_key"
3257 );
3258
3259 std::env::remove_var(env_key);
3260 Ok(())
3261 });
3262 }
3263
3264 #[test]
3265 fn test_resolve_etherscan() {
3266 figment::Jail::expect_with(|jail| {
3267 jail.create_file(
3268 "foundry.toml",
3269 r#"
3270 [profile.default]
3271
3272 [etherscan]
3273 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3274 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3275 "#,
3276 )?;
3277
3278 let config = Config::load();
3279
3280 assert!(config.etherscan.clone().resolved().has_unresolved());
3281
3282 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3283
3284 let configs = config.etherscan.resolved();
3285 assert!(!configs.has_unresolved());
3286
3287 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3288 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3289 assert_eq!(
3290 configs,
3291 ResolvedEtherscanConfigs::new([
3292 (
3293 "mainnet",
3294 ResolvedEtherscanConfig {
3295 api_url: mainnet_urls.0.to_string(),
3296 chain: Some(NamedChain::Mainnet.into()),
3297 browser_url: Some(mainnet_urls.1.to_string()),
3298 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3299 }
3300 ),
3301 (
3302 "moonbeam",
3303 ResolvedEtherscanConfig {
3304 api_url: mb_urls.0.to_string(),
3305 chain: Some(Moonbeam.into()),
3306 browser_url: Some(mb_urls.1.to_string()),
3307 key: "123456789".to_string(),
3308 }
3309 ),
3310 ])
3311 );
3312
3313 Ok(())
3314 });
3315 }
3316
3317 #[test]
3318 fn test_resolve_etherscan_chain_id() {
3319 figment::Jail::expect_with(|jail| {
3320 jail.create_file(
3321 "foundry.toml",
3322 r#"
3323 [profile.default]
3324 chain_id = "sepolia"
3325
3326 [etherscan]
3327 sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3328 "#,
3329 )?;
3330
3331 let config = Config::load();
3332 let etherscan = config.get_etherscan_config().unwrap().unwrap();
3333 assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3334 assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3335
3336 Ok(())
3337 });
3338 }
3339
3340 #[test]
3341 fn test_resolve_rpc_url() {
3342 figment::Jail::expect_with(|jail| {
3343 jail.create_file(
3344 "foundry.toml",
3345 r#"
3346 [profile.default]
3347 [rpc_endpoints]
3348 optimism = "https://example.com/"
3349 mainnet = "${_CONFIG_MAINNET}"
3350 "#,
3351 )?;
3352 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3353
3354 let mut config = Config::load();
3355 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3356
3357 config.eth_rpc_url = Some("mainnet".to_string());
3358 assert_eq!(
3359 "https://eth-mainnet.alchemyapi.io/v2/123455",
3360 config.get_rpc_url_or_localhost_http().unwrap()
3361 );
3362
3363 config.eth_rpc_url = Some("optimism".to_string());
3364 assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
3365
3366 Ok(())
3367 })
3368 }
3369
3370 #[test]
3371 fn test_resolve_rpc_url_if_etherscan_set() {
3372 figment::Jail::expect_with(|jail| {
3373 jail.create_file(
3374 "foundry.toml",
3375 r#"
3376 [profile.default]
3377 etherscan_api_key = "dummy"
3378 [rpc_endpoints]
3379 optimism = "https://example.com/"
3380 "#,
3381 )?;
3382
3383 let config = Config::load();
3384 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3385
3386 Ok(())
3387 })
3388 }
3389
3390 #[test]
3391 fn test_resolve_rpc_url_alias() {
3392 figment::Jail::expect_with(|jail| {
3393 jail.create_file(
3394 "foundry.toml",
3395 r#"
3396 [profile.default]
3397 [rpc_endpoints]
3398 polygonMumbai = "https://polygon-mumbai.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
3399 "#,
3400 )?;
3401 let mut config = Config::load();
3402 config.eth_rpc_url = Some("polygonMumbai".to_string());
3403 assert!(config.get_rpc_url().unwrap().is_err());
3404
3405 jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
3406
3407 let mut config = Config::load();
3408 config.eth_rpc_url = Some("polygonMumbai".to_string());
3409 assert_eq!(
3410 "https://polygon-mumbai.g.alchemy.com/v2/123455",
3411 config.get_rpc_url().unwrap().unwrap()
3412 );
3413
3414 Ok(())
3415 })
3416 }
3417
3418 #[test]
3419 fn test_resolve_rpc_aliases() {
3420 figment::Jail::expect_with(|jail| {
3421 jail.create_file(
3422 "foundry.toml",
3423 r#"
3424 [profile.default]
3425 [etherscan]
3426 arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
3427 [rpc_endpoints]
3428 arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
3429 "#,
3430 )?;
3431
3432 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
3433 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
3434
3435 let config = Config::load();
3436
3437 let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
3438 assert!(config.is_err());
3439 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`");
3440
3441 Ok(())
3442 });
3443 }
3444
3445 #[test]
3446 fn test_resolve_rpc_config() {
3447 figment::Jail::expect_with(|jail| {
3448 jail.create_file(
3449 "foundry.toml",
3450 r#"
3451 [rpc_endpoints]
3452 optimism = "https://example.com/"
3453 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
3454 "#,
3455 )?;
3456 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3457
3458 let config = Config::load();
3459 assert_eq!(
3460 RpcEndpoints::new([
3461 (
3462 "optimism",
3463 RpcEndpointType::String(RpcEndpoint::Url(
3464 "https://example.com/".to_string()
3465 ))
3466 ),
3467 (
3468 "mainnet",
3469 RpcEndpointType::Config(RpcEndpointConfig {
3470 endpoint: RpcEndpoint::Env("${_CONFIG_MAINNET}".to_string()),
3471 retries: Some(3),
3472 retry_backoff: Some(1000),
3473 compute_units_per_second: Some(1000),
3474 })
3475 ),
3476 ]),
3477 config.rpc_endpoints
3478 );
3479
3480 let resolved = config.rpc_endpoints.resolved();
3481 assert_eq!(
3482 RpcEndpoints::new([
3483 ("optimism", RpcEndpoint::Url("https://example.com/".to_string())),
3484 (
3485 "mainnet",
3486 RpcEndpoint::Url("https://eth-mainnet.alchemyapi.io/v2/123455".to_string())
3487 ),
3488 ])
3489 .resolved(),
3490 resolved
3491 );
3492 Ok(())
3493 })
3494 }
3495
3496 #[test]
3497 fn test_resolve_endpoints() {
3498 figment::Jail::expect_with(|jail| {
3499 jail.create_file(
3500 "foundry.toml",
3501 r#"
3502 [profile.default]
3503 eth_rpc_url = "optimism"
3504 [rpc_endpoints]
3505 optimism = "https://example.com/"
3506 mainnet = "${_CONFIG_MAINNET}"
3507 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
3508 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
3509 "#,
3510 )?;
3511
3512 let config = Config::load();
3513
3514 assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
3515
3516 assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
3517
3518 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3519 jail.set_env("_CONFIG_API_KEY1", "123456");
3520 jail.set_env("_CONFIG_API_KEY2", "98765");
3521
3522 let endpoints = config.rpc_endpoints.resolved();
3523
3524 assert!(!endpoints.has_unresolved());
3525
3526 assert_eq!(
3527 endpoints,
3528 RpcEndpoints::new([
3529 ("optimism", RpcEndpoint::Url("https://example.com/".to_string())),
3530 (
3531 "mainnet",
3532 RpcEndpoint::Url("https://eth-mainnet.alchemyapi.io/v2/123455".to_string())
3533 ),
3534 (
3535 "mainnet_2",
3536 RpcEndpoint::Url("https://eth-mainnet.alchemyapi.io/v2/123456".to_string())
3537 ),
3538 (
3539 "mainnet_3",
3540 RpcEndpoint::Url(
3541 "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
3542 )
3543 ),
3544 ])
3545 .resolved()
3546 );
3547
3548 Ok(())
3549 });
3550 }
3551
3552 #[test]
3553 fn test_extract_etherscan_config() {
3554 figment::Jail::expect_with(|jail| {
3555 jail.create_file(
3556 "foundry.toml",
3557 r#"
3558 [profile.default]
3559 etherscan_api_key = "optimism"
3560
3561 [etherscan]
3562 optimism = { key = "https://etherscan-optimism.com/" }
3563 mumbai = { key = "https://etherscan-mumbai.com/" }
3564 "#,
3565 )?;
3566
3567 let mut config = Config::load();
3568
3569 let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
3570 assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
3571
3572 config.etherscan_api_key = Some("mumbai".to_string());
3573
3574 let mumbai = config.get_etherscan_api_key(Some(NamedChain::PolygonMumbai.into()));
3575 assert_eq!(mumbai, Some("https://etherscan-mumbai.com/".to_string()));
3576
3577 Ok(())
3578 });
3579 }
3580
3581 #[test]
3582 fn test_extract_etherscan_config_by_chain() {
3583 figment::Jail::expect_with(|jail| {
3584 jail.create_file(
3585 "foundry.toml",
3586 r#"
3587 [profile.default]
3588
3589 [etherscan]
3590 mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 }
3591 "#,
3592 )?;
3593
3594 let config = Config::load();
3595
3596 let mumbai = config
3597 .get_etherscan_config_with_chain(Some(NamedChain::PolygonMumbai.into()))
3598 .unwrap()
3599 .unwrap();
3600 assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3601
3602 Ok(())
3603 });
3604 }
3605
3606 #[test]
3607 fn test_extract_etherscan_config_by_chain_with_url() {
3608 figment::Jail::expect_with(|jail| {
3609 jail.create_file(
3610 "foundry.toml",
3611 r#"
3612 [profile.default]
3613
3614 [etherscan]
3615 mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 , url = "https://verifier-url.com/"}
3616 "#,
3617 )?;
3618
3619 let config = Config::load();
3620
3621 let mumbai = config
3622 .get_etherscan_config_with_chain(Some(NamedChain::PolygonMumbai.into()))
3623 .unwrap()
3624 .unwrap();
3625 assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3626 assert_eq!(mumbai.api_url, "https://verifier-url.com/".to_string());
3627
3628 Ok(())
3629 });
3630 }
3631
3632 #[test]
3633 fn test_extract_etherscan_config_by_chain_and_alias() {
3634 figment::Jail::expect_with(|jail| {
3635 jail.create_file(
3636 "foundry.toml",
3637 r#"
3638 [profile.default]
3639 eth_rpc_url = "mumbai"
3640
3641 [etherscan]
3642 mumbai = { key = "https://etherscan-mumbai.com/" }
3643
3644 [rpc_endpoints]
3645 mumbai = "https://polygon-mumbai.g.alchemy.com/v2/mumbai"
3646 "#,
3647 )?;
3648
3649 let config = Config::load();
3650
3651 let mumbai = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
3652 assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3653
3654 let mumbai_rpc = config.get_rpc_url().unwrap().unwrap();
3655 assert_eq!(mumbai_rpc, "https://polygon-mumbai.g.alchemy.com/v2/mumbai");
3656 Ok(())
3657 });
3658 }
3659
3660 #[test]
3661 fn test_toml_file() {
3662 figment::Jail::expect_with(|jail| {
3663 jail.create_file(
3664 "foundry.toml",
3665 r#"
3666 [profile.default]
3667 src = "some-source"
3668 out = "some-out"
3669 cache = true
3670 eth_rpc_url = "https://example.com/"
3671 verbosity = 3
3672 remappings = ["ds-test=lib/ds-test/"]
3673 via_ir = true
3674 rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
3675 use_literal_content = false
3676 bytecode_hash = "ipfs"
3677 cbor_metadata = true
3678 revert_strings = "strip"
3679 allow_paths = ["allow", "paths"]
3680 build_info_path = "build-info"
3681 always_use_create_2_factory = true
3682
3683 [rpc_endpoints]
3684 optimism = "https://example.com/"
3685 mainnet = "${RPC_MAINNET}"
3686 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3687 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3688 "#,
3689 )?;
3690
3691 let config = Config::load();
3692 assert_eq!(
3693 config,
3694 Config {
3695 src: "some-source".into(),
3696 out: "some-out".into(),
3697 cache: true,
3698 eth_rpc_url: Some("https://example.com/".to_string()),
3699 remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
3700 verbosity: 3,
3701 via_ir: true,
3702 rpc_storage_caching: StorageCachingConfig {
3703 chains: CachedChains::Chains(vec![
3704 Chain::mainnet(),
3705 Chain::optimism_mainnet(),
3706 Chain::from_id(999999)
3707 ]),
3708 endpoints: CachedEndpoints::All,
3709 },
3710 use_literal_content: false,
3711 bytecode_hash: BytecodeHash::Ipfs,
3712 cbor_metadata: true,
3713 revert_strings: Some(RevertStrings::Strip),
3714 allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
3715 rpc_endpoints: RpcEndpoints::new([
3716 ("optimism", RpcEndpoint::Url("https://example.com/".to_string())),
3717 ("mainnet", RpcEndpoint::Env("${RPC_MAINNET}".to_string())),
3718 (
3719 "mainnet_2",
3720 RpcEndpoint::Env(
3721 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3722 )
3723 ),
3724 (
3725 "mainnet_3",
3726 RpcEndpoint::Env(
3727 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3728 .to_string()
3729 )
3730 ),
3731 ]),
3732 build_info_path: Some("build-info".into()),
3733 always_use_create_2_factory: true,
3734 ..Config::default()
3735 }
3736 );
3737
3738 Ok(())
3739 });
3740 }
3741
3742 #[test]
3743 fn test_load_remappings() {
3744 figment::Jail::expect_with(|jail| {
3745 jail.create_file(
3746 "foundry.toml",
3747 r"
3748 [profile.default]
3749 remappings = ['nested/=lib/nested/']
3750 ",
3751 )?;
3752
3753 let config = Config::load_with_root(jail.directory());
3754 assert_eq!(
3755 config.remappings,
3756 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3757 );
3758
3759 Ok(())
3760 });
3761 }
3762
3763 #[test]
3764 fn test_load_full_toml() {
3765 figment::Jail::expect_with(|jail| {
3766 jail.create_file(
3767 "foundry.toml",
3768 r#"
3769 [profile.default]
3770 auto_detect_solc = true
3771 block_base_fee_per_gas = 0
3772 block_coinbase = '0x0000000000000000000000000000000000000000'
3773 block_difficulty = 0
3774 block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
3775 block_number = 1
3776 block_timestamp = 1
3777 use_literal_content = false
3778 bytecode_hash = 'ipfs'
3779 cbor_metadata = true
3780 cache = true
3781 cache_path = 'cache'
3782 evm_version = 'london'
3783 extra_output = []
3784 extra_output_files = []
3785 always_use_create_2_factory = false
3786 ffi = false
3787 force = false
3788 gas_limit = 9223372036854775807
3789 gas_price = 0
3790 gas_reports = ['*']
3791 ignored_error_codes = [1878]
3792 ignored_warnings_from = ["something"]
3793 deny_warnings = false
3794 initial_balance = '0xffffffffffffffffffffffff'
3795 libraries = []
3796 libs = ['lib']
3797 memory_limit = 134217728
3798 names = false
3799 no_storage_caching = false
3800 no_rpc_rate_limit = false
3801 offline = false
3802 optimizer = true
3803 optimizer_runs = 200
3804 out = 'out'
3805 remappings = ['nested/=lib/nested/']
3806 sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3807 sizes = false
3808 sparse_mode = false
3809 src = 'src'
3810 test = 'test'
3811 tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3812 verbosity = 0
3813 via_ir = false
3814
3815 [profile.default.rpc_storage_caching]
3816 chains = 'all'
3817 endpoints = 'all'
3818
3819 [rpc_endpoints]
3820 optimism = "https://example.com/"
3821 mainnet = "${RPC_MAINNET}"
3822 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3823
3824 [fuzz]
3825 runs = 256
3826 seed = '0x3e8'
3827 max_test_rejects = 65536
3828
3829 [invariant]
3830 runs = 256
3831 depth = 500
3832 fail_on_revert = false
3833 call_override = false
3834 shrink_run_limit = 5000
3835 "#,
3836 )?;
3837
3838 let config = Config::load_with_root(jail.directory());
3839
3840 assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
3841 assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
3842 assert_eq!(
3843 config.remappings,
3844 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3845 );
3846
3847 assert_eq!(
3848 config.rpc_endpoints,
3849 RpcEndpoints::new([
3850 ("optimism", RpcEndpoint::Url("https://example.com/".to_string())),
3851 ("mainnet", RpcEndpoint::Env("${RPC_MAINNET}".to_string())),
3852 (
3853 "mainnet_2",
3854 RpcEndpoint::Env(
3855 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3856 )
3857 ),
3858 ]),
3859 );
3860
3861 Ok(())
3862 });
3863 }
3864
3865 #[test]
3866 fn test_solc_req() {
3867 figment::Jail::expect_with(|jail| {
3868 jail.create_file(
3869 "foundry.toml",
3870 r#"
3871 [profile.default]
3872 solc_version = "0.8.12"
3873 "#,
3874 )?;
3875
3876 let config = Config::load();
3877 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3878
3879 jail.create_file(
3880 "foundry.toml",
3881 r#"
3882 [profile.default]
3883 solc = "0.8.12"
3884 "#,
3885 )?;
3886
3887 let config = Config::load();
3888 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3889
3890 jail.create_file(
3891 "foundry.toml",
3892 r#"
3893 [profile.default]
3894 solc = "path/to/local/solc"
3895 "#,
3896 )?;
3897
3898 let config = Config::load();
3899 assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
3900
3901 jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
3902 let config = Config::load();
3903 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
3904 Ok(())
3905 });
3906 }
3907
3908 #[test]
3910 fn test_backwards_solc_version() {
3911 figment::Jail::expect_with(|jail| {
3912 jail.create_file(
3913 "foundry.toml",
3914 r#"
3915 [default]
3916 solc = "0.8.12"
3917 solc_version = "0.8.20"
3918 "#,
3919 )?;
3920
3921 let config = Config::load();
3922 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3923
3924 Ok(())
3925 });
3926
3927 figment::Jail::expect_with(|jail| {
3928 jail.create_file(
3929 "foundry.toml",
3930 r#"
3931 [default]
3932 solc_version = "0.8.20"
3933 "#,
3934 )?;
3935
3936 let config = Config::load();
3937 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
3938
3939 Ok(())
3940 });
3941 }
3942
3943 #[test]
3944 fn test_toml_casing_file() {
3945 figment::Jail::expect_with(|jail| {
3946 jail.create_file(
3947 "foundry.toml",
3948 r#"
3949 [profile.default]
3950 src = "some-source"
3951 out = "some-out"
3952 cache = true
3953 eth-rpc-url = "https://example.com/"
3954 evm-version = "berlin"
3955 auto-detect-solc = false
3956 "#,
3957 )?;
3958
3959 let config = Config::load();
3960 assert_eq!(
3961 config,
3962 Config {
3963 src: "some-source".into(),
3964 out: "some-out".into(),
3965 cache: true,
3966 eth_rpc_url: Some("https://example.com/".to_string()),
3967 auto_detect_solc: false,
3968 evm_version: EvmVersion::Berlin,
3969 ..Config::default()
3970 }
3971 );
3972
3973 Ok(())
3974 });
3975 }
3976
3977 #[test]
3978 fn test_output_selection() {
3979 figment::Jail::expect_with(|jail| {
3980 jail.create_file(
3981 "foundry.toml",
3982 r#"
3983 [profile.default]
3984 extra_output = ["metadata", "ir-optimized"]
3985 extra_output_files = ["metadata"]
3986 "#,
3987 )?;
3988
3989 let config = Config::load();
3990
3991 assert_eq!(
3992 config.extra_output,
3993 vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
3994 );
3995 assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
3996
3997 Ok(())
3998 });
3999 }
4000
4001 #[test]
4002 fn test_precedence() {
4003 figment::Jail::expect_with(|jail| {
4004 jail.create_file(
4005 "foundry.toml",
4006 r#"
4007 [profile.default]
4008 src = "mysrc"
4009 out = "myout"
4010 verbosity = 3
4011 "#,
4012 )?;
4013
4014 let config = Config::load();
4015 assert_eq!(
4016 config,
4017 Config {
4018 src: "mysrc".into(),
4019 out: "myout".into(),
4020 verbosity: 3,
4021 ..Config::default()
4022 }
4023 );
4024
4025 jail.set_env("FOUNDRY_SRC", r"other-src");
4026 let config = Config::load();
4027 assert_eq!(
4028 config,
4029 Config {
4030 src: "other-src".into(),
4031 out: "myout".into(),
4032 verbosity: 3,
4033 ..Config::default()
4034 }
4035 );
4036
4037 jail.set_env("FOUNDRY_PROFILE", "foo");
4038 let val: Result<String, _> = Config::figment().extract_inner("profile");
4039 assert!(val.is_err());
4040
4041 Ok(())
4042 });
4043 }
4044
4045 #[test]
4046 fn test_extract_basic() {
4047 figment::Jail::expect_with(|jail| {
4048 jail.create_file(
4049 "foundry.toml",
4050 r#"
4051 [profile.default]
4052 src = "mysrc"
4053 out = "myout"
4054 verbosity = 3
4055 evm_version = 'berlin'
4056
4057 [profile.other]
4058 src = "other-src"
4059 "#,
4060 )?;
4061 let loaded = Config::load();
4062 assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4063 let base = loaded.into_basic();
4064 let default = Config::default();
4065 assert_eq!(
4066 base,
4067 BasicConfig {
4068 profile: Config::DEFAULT_PROFILE,
4069 src: "mysrc".into(),
4070 out: "myout".into(),
4071 libs: default.libs.clone(),
4072 remappings: default.remappings.clone(),
4073 }
4074 );
4075 jail.set_env("FOUNDRY_PROFILE", r"other");
4076 let base = Config::figment().extract::<BasicConfig>().unwrap();
4077 assert_eq!(
4078 base,
4079 BasicConfig {
4080 profile: Config::DEFAULT_PROFILE,
4081 src: "other-src".into(),
4082 out: "myout".into(),
4083 libs: default.libs.clone(),
4084 remappings: default.remappings,
4085 }
4086 );
4087 Ok(())
4088 });
4089 }
4090
4091 #[test]
4092 #[should_panic]
4093 fn test_parse_invalid_fuzz_weight() {
4094 figment::Jail::expect_with(|jail| {
4095 jail.create_file(
4096 "foundry.toml",
4097 r"
4098 [fuzz]
4099 dictionary_weight = 101
4100 ",
4101 )?;
4102 let _config = Config::load();
4103 Ok(())
4104 });
4105 }
4106
4107 #[test]
4108 fn test_fallback_provider() {
4109 figment::Jail::expect_with(|jail| {
4110 jail.create_file(
4111 "foundry.toml",
4112 r"
4113 [fuzz]
4114 runs = 1
4115 include_storage = false
4116 dictionary_weight = 99
4117
4118 [invariant]
4119 runs = 420
4120
4121 [profile.ci.fuzz]
4122 dictionary_weight = 5
4123
4124 [profile.ci.invariant]
4125 runs = 400
4126 ",
4127 )?;
4128
4129 let invariant_default = InvariantConfig::default();
4130 let config = Config::load();
4131
4132 assert_ne!(config.invariant.runs, config.fuzz.runs);
4133 assert_eq!(config.invariant.runs, 420);
4134
4135 assert_ne!(
4136 config.fuzz.dictionary.include_storage,
4137 invariant_default.dictionary.include_storage
4138 );
4139 assert_eq!(
4140 config.invariant.dictionary.include_storage,
4141 config.fuzz.dictionary.include_storage
4142 );
4143
4144 assert_ne!(
4145 config.fuzz.dictionary.dictionary_weight,
4146 invariant_default.dictionary.dictionary_weight
4147 );
4148 assert_eq!(
4149 config.invariant.dictionary.dictionary_weight,
4150 config.fuzz.dictionary.dictionary_weight
4151 );
4152
4153 jail.set_env("FOUNDRY_PROFILE", "ci");
4154 let ci_config = Config::load();
4155 assert_eq!(ci_config.fuzz.runs, 1);
4156 assert_eq!(ci_config.invariant.runs, 400);
4157 assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4158 assert_eq!(
4159 ci_config.invariant.dictionary.dictionary_weight,
4160 config.fuzz.dictionary.dictionary_weight
4161 );
4162
4163 Ok(())
4164 })
4165 }
4166
4167 #[test]
4168 fn test_standalone_profile_sections() {
4169 figment::Jail::expect_with(|jail| {
4170 jail.create_file(
4171 "foundry.toml",
4172 r"
4173 [fuzz]
4174 runs = 100
4175
4176 [invariant]
4177 runs = 120
4178
4179 [profile.ci.fuzz]
4180 runs = 420
4181
4182 [profile.ci.invariant]
4183 runs = 500
4184 ",
4185 )?;
4186
4187 let config = Config::load();
4188 assert_eq!(config.fuzz.runs, 100);
4189 assert_eq!(config.invariant.runs, 120);
4190
4191 jail.set_env("FOUNDRY_PROFILE", "ci");
4192 let config = Config::load();
4193 assert_eq!(config.fuzz.runs, 420);
4194 assert_eq!(config.invariant.runs, 500);
4195
4196 Ok(())
4197 });
4198 }
4199
4200 #[test]
4201 fn can_handle_deviating_dapp_aliases() {
4202 figment::Jail::expect_with(|jail| {
4203 let addr = Address::ZERO;
4204 jail.set_env("DAPP_TEST_NUMBER", 1337);
4205 jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4206 jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4207 jail.set_env("DAPP_TEST_DEPTH", 20);
4208 jail.set_env("DAPP_FORK_BLOCK", 100);
4209 jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4210 jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4211
4212 let config = Config::load();
4213
4214 assert_eq!(config.block_number, 1337);
4215 assert_eq!(config.sender, addr);
4216 assert_eq!(config.fuzz.runs, 420);
4217 assert_eq!(config.invariant.depth, 20);
4218 assert_eq!(config.fork_block_number, Some(100));
4219 assert_eq!(config.optimizer_runs, 999);
4220 assert!(!config.optimizer);
4221
4222 Ok(())
4223 });
4224 }
4225
4226 #[test]
4227 fn can_parse_libraries() {
4228 figment::Jail::expect_with(|jail| {
4229 jail.set_env(
4230 "DAPP_LIBRARIES",
4231 "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
4232 );
4233 let config = Config::load();
4234 assert_eq!(
4235 config.libraries,
4236 vec!["src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4237 .to_string()]
4238 );
4239
4240 jail.set_env(
4241 "DAPP_LIBRARIES",
4242 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4243 );
4244 let config = Config::load();
4245 assert_eq!(
4246 config.libraries,
4247 vec!["src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4248 .to_string(),]
4249 );
4250
4251 jail.set_env(
4252 "DAPP_LIBRARIES",
4253 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4254 );
4255 let config = Config::load();
4256 assert_eq!(
4257 config.libraries,
4258 vec![
4259 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4260 .to_string(),
4261 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4262 .to_string()
4263 ]
4264 );
4265
4266 Ok(())
4267 });
4268 }
4269
4270 #[test]
4271 fn test_parse_many_libraries() {
4272 figment::Jail::expect_with(|jail| {
4273 jail.create_file(
4274 "foundry.toml",
4275 r"
4276 [profile.default]
4277 libraries= [
4278 './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4279 './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4280 './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4281 './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4282 './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4283 ]
4284 ",
4285 )?;
4286 let config = Config::load();
4287
4288 let libs = config.parsed_libraries().unwrap().libs;
4289
4290 similar_asserts::assert_eq!(
4291 libs,
4292 BTreeMap::from([
4293 (
4294 PathBuf::from("./src/SizeAuctionDiscount.sol"),
4295 BTreeMap::from([
4296 (
4297 "Chainlink".to_string(),
4298 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4299 ),
4300 (
4301 "Math".to_string(),
4302 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4303 )
4304 ])
4305 ),
4306 (
4307 PathBuf::from("./src/SizeAuction.sol"),
4308 BTreeMap::from([
4309 (
4310 "ChainlinkTWAP".to_string(),
4311 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4312 ),
4313 (
4314 "Math".to_string(),
4315 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4316 )
4317 ])
4318 ),
4319 (
4320 PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
4321 BTreeMap::from([(
4322 "ChainlinkTWAP".to_string(),
4323 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4324 )])
4325 ),
4326 ])
4327 );
4328
4329 Ok(())
4330 });
4331 }
4332
4333 #[test]
4334 fn config_roundtrip() {
4335 figment::Jail::expect_with(|jail| {
4336 let default = Config::default();
4337 let basic = default.clone().into_basic();
4338 jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
4339
4340 let mut other = Config::load();
4341 clear_warning(&mut other);
4342 assert_eq!(default, other);
4343
4344 let other = other.into_basic();
4345 assert_eq!(basic, other);
4346
4347 jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
4348 let mut other = Config::load();
4349 clear_warning(&mut other);
4350 assert_eq!(default, other);
4351
4352 Ok(())
4353 });
4354 }
4355
4356 #[test]
4357 fn test_fs_permissions() {
4358 figment::Jail::expect_with(|jail| {
4359 jail.create_file(
4360 "foundry.toml",
4361 r#"
4362 [profile.default]
4363 fs_permissions = [{ access = "read-write", path = "./"}]
4364 "#,
4365 )?;
4366 let loaded = Config::load();
4367
4368 assert_eq!(
4369 loaded.fs_permissions,
4370 FsPermissions::new(vec![PathPermission::read_write("./")])
4371 );
4372
4373 jail.create_file(
4374 "foundry.toml",
4375 r#"
4376 [profile.default]
4377 fs_permissions = [{ access = "none", path = "./"}]
4378 "#,
4379 )?;
4380 let loaded = Config::load();
4381 assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
4382
4383 Ok(())
4384 });
4385 }
4386
4387 #[test]
4388 fn test_optimizer_settings_basic() {
4389 figment::Jail::expect_with(|jail| {
4390 jail.create_file(
4391 "foundry.toml",
4392 r"
4393 [profile.default]
4394 optimizer = true
4395
4396 [profile.default.optimizer_details]
4397 yul = false
4398
4399 [profile.default.optimizer_details.yulDetails]
4400 stackAllocation = true
4401 ",
4402 )?;
4403 let mut loaded = Config::load();
4404 clear_warning(&mut loaded);
4405 assert_eq!(
4406 loaded.optimizer_details,
4407 Some(OptimizerDetails {
4408 yul: Some(false),
4409 yul_details: Some(YulDetails {
4410 stack_allocation: Some(true),
4411 ..Default::default()
4412 }),
4413 ..Default::default()
4414 })
4415 );
4416
4417 let s = loaded.to_string_pretty().unwrap();
4418 jail.create_file("foundry.toml", &s)?;
4419
4420 let mut reloaded = Config::load();
4421 clear_warning(&mut reloaded);
4422 assert_eq!(loaded, reloaded);
4423
4424 Ok(())
4425 });
4426 }
4427
4428 #[test]
4429 fn test_model_checker_settings_basic() {
4430 figment::Jail::expect_with(|jail| {
4431 jail.create_file(
4432 "foundry.toml",
4433 r"
4434 [profile.default]
4435
4436 [profile.default.model_checker]
4437 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4438 engine = 'chc'
4439 targets = [ 'assert', 'outOfBounds' ]
4440 timeout = 10000
4441 ",
4442 )?;
4443 let mut loaded = Config::load();
4444 clear_warning(&mut loaded);
4445 assert_eq!(
4446 loaded.model_checker,
4447 Some(ModelCheckerSettings {
4448 contracts: BTreeMap::from([
4449 ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
4450 ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
4451 ]),
4452 engine: Some(ModelCheckerEngine::CHC),
4453 targets: Some(vec![
4454 ModelCheckerTarget::Assert,
4455 ModelCheckerTarget::OutOfBounds
4456 ]),
4457 timeout: Some(10000),
4458 invariants: None,
4459 show_unproved: None,
4460 div_mod_with_slacks: None,
4461 solvers: None,
4462 show_unsupported: None,
4463 show_proved_safe: None,
4464 })
4465 );
4466
4467 let s = loaded.to_string_pretty().unwrap();
4468 jail.create_file("foundry.toml", &s)?;
4469
4470 let mut reloaded = Config::load();
4471 clear_warning(&mut reloaded);
4472 assert_eq!(loaded, reloaded);
4473
4474 Ok(())
4475 });
4476 }
4477
4478 #[test]
4479 fn test_model_checker_settings_relative_paths() {
4480 figment::Jail::expect_with(|jail| {
4481 jail.create_file(
4482 "foundry.toml",
4483 r"
4484 [profile.default]
4485
4486 [profile.default.model_checker]
4487 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4488 engine = 'chc'
4489 targets = [ 'assert', 'outOfBounds' ]
4490 timeout = 10000
4491 ",
4492 )?;
4493 let loaded = Config::load().sanitized();
4494
4495 let dir = foundry_compilers::utils::canonicalize(jail.directory())
4500 .expect("Could not canonicalize jail path");
4501 assert_eq!(
4502 loaded.model_checker,
4503 Some(ModelCheckerSettings {
4504 contracts: BTreeMap::from([
4505 (
4506 format!("{}", dir.join("a.sol").display()),
4507 vec!["A1".to_string(), "A2".to_string()]
4508 ),
4509 (
4510 format!("{}", dir.join("b.sol").display()),
4511 vec!["B1".to_string(), "B2".to_string()]
4512 ),
4513 ]),
4514 engine: Some(ModelCheckerEngine::CHC),
4515 targets: Some(vec![
4516 ModelCheckerTarget::Assert,
4517 ModelCheckerTarget::OutOfBounds
4518 ]),
4519 timeout: Some(10000),
4520 invariants: None,
4521 show_unproved: None,
4522 div_mod_with_slacks: None,
4523 solvers: None,
4524 show_unsupported: None,
4525 show_proved_safe: None,
4526 })
4527 );
4528
4529 Ok(())
4530 });
4531 }
4532
4533 #[test]
4534 fn test_fmt_config() {
4535 figment::Jail::expect_with(|jail| {
4536 jail.create_file(
4537 "foundry.toml",
4538 r"
4539 [fmt]
4540 line_length = 100
4541 tab_width = 2
4542 bracket_spacing = true
4543 ",
4544 )?;
4545 let loaded = Config::load().sanitized();
4546 assert_eq!(
4547 loaded.fmt,
4548 FormatterConfig {
4549 line_length: 100,
4550 tab_width: 2,
4551 bracket_spacing: true,
4552 ..Default::default()
4553 }
4554 );
4555
4556 Ok(())
4557 });
4558 }
4559
4560 #[test]
4561 fn test_invariant_config() {
4562 figment::Jail::expect_with(|jail| {
4563 jail.create_file(
4564 "foundry.toml",
4565 r"
4566 [invariant]
4567 runs = 512
4568 depth = 10
4569 ",
4570 )?;
4571
4572 let loaded = Config::load().sanitized();
4573 assert_eq!(
4574 loaded.invariant,
4575 InvariantConfig {
4576 runs: 512,
4577 depth: 10,
4578 failure_persist_dir: Some(PathBuf::from("cache/invariant")),
4579 ..Default::default()
4580 }
4581 );
4582
4583 Ok(())
4584 });
4585 }
4586
4587 #[test]
4588 fn test_standalone_sections_env() {
4589 figment::Jail::expect_with(|jail| {
4590 jail.create_file(
4591 "foundry.toml",
4592 r"
4593 [fuzz]
4594 runs = 100
4595
4596 [invariant]
4597 depth = 1
4598 ",
4599 )?;
4600
4601 jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
4602 jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
4603 jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
4604
4605 let config = Config::load();
4606 assert_eq!(config.fmt.line_length, 95);
4607 assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
4608 assert_eq!(config.invariant.depth, 5);
4609
4610 Ok(())
4611 });
4612 }
4613
4614 #[test]
4615 fn test_parse_with_profile() {
4616 let foundry_str = r"
4617 [profile.default]
4618 src = 'src'
4619 out = 'out'
4620 libs = ['lib']
4621
4622 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
4623 ";
4624 assert_eq!(
4625 parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
4626 (
4627 Config::DEFAULT_PROFILE,
4628 BasicConfig {
4629 profile: Config::DEFAULT_PROFILE,
4630 src: "src".into(),
4631 out: "out".into(),
4632 libs: vec!["lib".into()],
4633 remappings: vec![]
4634 }
4635 )
4636 );
4637 }
4638
4639 #[test]
4640 fn test_implicit_profile_loads() {
4641 figment::Jail::expect_with(|jail| {
4642 jail.create_file(
4643 "foundry.toml",
4644 r"
4645 [default]
4646 src = 'my-src'
4647 out = 'my-out'
4648 ",
4649 )?;
4650 let loaded = Config::load().sanitized();
4651 assert_eq!(loaded.src.file_name().unwrap(), "my-src");
4652 assert_eq!(loaded.out.file_name().unwrap(), "my-out");
4653 assert_eq!(
4654 loaded.warnings,
4655 vec![Warning::UnknownSection {
4656 unknown_section: Profile::new("default"),
4657 source: Some("foundry.toml".into())
4658 }]
4659 );
4660
4661 Ok(())
4662 });
4663 }
4664
4665 #[test]
4666 fn test_etherscan_api_key() {
4667 figment::Jail::expect_with(|jail| {
4668 jail.create_file(
4669 "foundry.toml",
4670 r"
4671 [default]
4672 ",
4673 )?;
4674 jail.set_env("ETHERSCAN_API_KEY", "");
4675 let loaded = Config::load().sanitized();
4676 assert!(loaded.etherscan_api_key.is_none());
4677
4678 jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
4679 let loaded = Config::load().sanitized();
4680 assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
4681
4682 Ok(())
4683 });
4684 }
4685
4686 #[test]
4687 fn test_etherscan_api_key_figment() {
4688 figment::Jail::expect_with(|jail| {
4689 jail.create_file(
4690 "foundry.toml",
4691 r"
4692 [default]
4693 etherscan_api_key = 'DUMMY'
4694 ",
4695 )?;
4696 jail.set_env("ETHERSCAN_API_KEY", "ETHER");
4697
4698 let figment = Config::figment_with_root(jail.directory())
4699 .merge(("etherscan_api_key", "USER_KEY"));
4700
4701 let loaded = Config::from_provider(figment);
4702 assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
4703
4704 Ok(())
4705 });
4706 }
4707
4708 #[test]
4709 fn test_normalize_defaults() {
4710 figment::Jail::expect_with(|jail| {
4711 jail.create_file(
4712 "foundry.toml",
4713 r"
4714 [default]
4715 solc = '0.8.13'
4716 ",
4717 )?;
4718
4719 let loaded = Config::load().sanitized();
4720 assert_eq!(loaded.evm_version, EvmVersion::London);
4721 Ok(())
4722 });
4723 }
4724
4725 #[test]
4727 #[ignore]
4728 fn print_config() {
4729 let config = Config {
4730 optimizer_details: Some(OptimizerDetails {
4731 peephole: None,
4732 inliner: None,
4733 jumpdest_remover: None,
4734 order_literals: None,
4735 deduplicate: None,
4736 cse: None,
4737 constant_optimizer: Some(true),
4738 yul: Some(true),
4739 yul_details: Some(YulDetails {
4740 stack_allocation: None,
4741 optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
4742 }),
4743 simple_counter_for_loop_unchecked_increment: None,
4744 }),
4745 ..Default::default()
4746 };
4747 println!("{}", config.to_string_pretty().unwrap());
4748 }
4749
4750 #[test]
4751 #[allow(unknown_lints, non_local_definitions)]
4752 fn can_use_impl_figment_macro() {
4753 #[derive(Default, Serialize)]
4754 struct MyArgs {
4755 #[serde(skip_serializing_if = "Option::is_none")]
4756 root: Option<PathBuf>,
4757 }
4758 impl_figment_convert!(MyArgs);
4759
4760 impl Provider for MyArgs {
4761 fn metadata(&self) -> Metadata {
4762 Metadata::default()
4763 }
4764
4765 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
4766 let value = Value::serialize(self)?;
4767 let error = InvalidType(value.to_actual(), "map".into());
4768 let dict = value.into_dict().ok_or(error)?;
4769 Ok(Map::from([(Config::selected_profile(), dict)]))
4770 }
4771 }
4772
4773 let _figment: Figment = From::from(&MyArgs::default());
4774 let _config: Config = From::from(&MyArgs::default());
4775
4776 #[derive(Default)]
4777 struct Outer {
4778 start: MyArgs,
4779 other: MyArgs,
4780 another: MyArgs,
4781 }
4782 impl_figment_convert!(Outer, start, other, another);
4783
4784 let _figment: Figment = From::from(&Outer::default());
4785 let _config: Config = From::from(&Outer::default());
4786 }
4787
4788 #[test]
4789 fn list_cached_blocks() -> eyre::Result<()> {
4790 fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
4791 let block_path = chain_path.join(block_number);
4792 fs::create_dir(block_path.as_path()).unwrap();
4793 let file_path = block_path.join("storage.json");
4794 let mut file = File::create(file_path).unwrap();
4795 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4796 }
4797
4798 fn fake_block_cache_block_path_as_file(
4799 chain_path: &Path,
4800 block_number: &str,
4801 size_bytes: usize,
4802 ) {
4803 let block_path = chain_path.join(block_number);
4804 let mut file = File::create(block_path).unwrap();
4805 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4806 }
4807
4808 let chain_dir = tempdir()?;
4809
4810 fake_block_cache(chain_dir.path(), "1", 100);
4811 fake_block_cache(chain_dir.path(), "2", 500);
4812 fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
4813 let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
4815 writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
4816
4817 let result = Config::get_cached_blocks(chain_dir.path())?;
4818
4819 assert_eq!(result.len(), 3);
4820 let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
4821 let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
4822 let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
4823
4824 assert_eq!(block1.0, "1");
4825 assert_eq!(block1.1, 100);
4826 assert_eq!(block2.0, "2");
4827 assert_eq!(block2.1, 500);
4828 assert_eq!(block3.0, "3");
4829 assert_eq!(block3.1, 900);
4830
4831 chain_dir.close()?;
4832 Ok(())
4833 }
4834
4835 #[test]
4836 fn list_etherscan_cache() -> eyre::Result<()> {
4837 fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
4838 let metadata_path = chain_path.join("sources");
4839 let abi_path = chain_path.join("abi");
4840 let _ = fs::create_dir(metadata_path.as_path());
4841 let _ = fs::create_dir(abi_path.as_path());
4842
4843 let metadata_file_path = metadata_path.join(address);
4844 let mut metadata_file = File::create(metadata_file_path).unwrap();
4845 writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4846 .unwrap();
4847
4848 let abi_file_path = abi_path.join(address);
4849 let mut abi_file = File::create(abi_file_path).unwrap();
4850 writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4851 .unwrap();
4852 }
4853
4854 let chain_dir = tempdir()?;
4855
4856 fake_etherscan_cache(chain_dir.path(), "1", 100);
4857 fake_etherscan_cache(chain_dir.path(), "2", 500);
4858
4859 let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
4860
4861 assert_eq!(result, 600);
4862
4863 chain_dir.close()?;
4864 Ok(())
4865 }
4866
4867 #[test]
4868 fn test_parse_error_codes() {
4869 figment::Jail::expect_with(|jail| {
4870 jail.create_file(
4871 "foundry.toml",
4872 r#"
4873 [default]
4874 ignored_error_codes = ["license", "unreachable", 1337]
4875 "#,
4876 )?;
4877
4878 let config = Config::load();
4879 assert_eq!(
4880 config.ignored_error_codes,
4881 vec![
4882 SolidityErrorCode::SpdxLicenseNotProvided,
4883 SolidityErrorCode::Unreachable,
4884 SolidityErrorCode::Other(1337)
4885 ]
4886 );
4887
4888 Ok(())
4889 });
4890 }
4891
4892 #[test]
4893 fn test_parse_file_paths() {
4894 figment::Jail::expect_with(|jail| {
4895 jail.create_file(
4896 "foundry.toml",
4897 r#"
4898 [default]
4899 ignored_warnings_from = ["something"]
4900 "#,
4901 )?;
4902
4903 let config = Config::load();
4904 assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
4905
4906 Ok(())
4907 });
4908 }
4909
4910 #[test]
4911 fn test_parse_optimizer_settings() {
4912 figment::Jail::expect_with(|jail| {
4913 jail.create_file(
4914 "foundry.toml",
4915 r"
4916 [default]
4917 [profile.default.optimizer_details]
4918 ",
4919 )?;
4920
4921 let config = Config::load();
4922 assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
4923
4924 Ok(())
4925 });
4926 }
4927
4928 #[test]
4929 fn test_parse_labels() {
4930 figment::Jail::expect_with(|jail| {
4931 jail.create_file(
4932 "foundry.toml",
4933 r#"
4934 [labels]
4935 0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
4936 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
4937 "#,
4938 )?;
4939
4940 let config = Config::load();
4941 assert_eq!(
4942 config.labels,
4943 HashMap::from_iter(vec![
4944 (
4945 Address::from_str("0x1F98431c8aD98523631AE4a59f267346ea31F984").unwrap(),
4946 "Uniswap V3: Factory".to_string()
4947 ),
4948 (
4949 Address::from_str("0xC36442b4a4522E871399CD717aBDD847Ab11FE88").unwrap(),
4950 "Uniswap V3: Positions NFT".to_string()
4951 ),
4952 ])
4953 );
4954
4955 Ok(())
4956 });
4957 }
4958
4959 #[test]
4960 fn test_parse_vyper() {
4961 figment::Jail::expect_with(|jail| {
4962 jail.create_file(
4963 "foundry.toml",
4964 r#"
4965 [vyper]
4966 optimize = "codesize"
4967 path = "/path/to/vyper"
4968 "#,
4969 )?;
4970
4971 let config = Config::load();
4972 assert_eq!(
4973 config.vyper,
4974 VyperConfig {
4975 optimize: Some(VyperOptimizationMode::Codesize),
4976 path: Some("/path/to/vyper".into())
4977 }
4978 );
4979
4980 Ok(())
4981 });
4982 }
4983}