1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use crate::cache::StorageCachingConfig;
12use alloy_primitives::{Address, B256, FixedBytes, U256, address, map::AddressHashMap};
13use eyre::{ContextCompat, WrapErr};
14use figment::{
15 Error, Figment, Metadata, Profile, Provider,
16 providers::{Env, Format, Serialized, Toml},
17 value::{Dict, Map, Value},
18};
19use filter::GlobMatcher;
20use foundry_compilers::{
21 ArtifactOutput, ConfigurableArtifacts, Graph, Project, ProjectPathsConfig,
22 RestrictionsWithVersion, VyperLanguage,
23 artifacts::{
24 BytecodeHash, DebuggingSettings, EvmVersion, Libraries, ModelCheckerSettings,
25 ModelCheckerTarget, Optimizer, OptimizerDetails, RevertStrings, Settings, SettingsMetadata,
26 Severity,
27 output_selection::{ContractOutputSelection, OutputSelection},
28 remappings::{RelativeRemapping, Remapping},
29 serde_helpers,
30 },
31 cache::SOLIDITY_FILES_CACHE_FILENAME,
32 compilers::{
33 Compiler,
34 multi::{MultiCompiler, MultiCompilerSettings},
35 solc::{Solc, SolcCompiler},
36 vyper::{Vyper, VyperSettings},
37 },
38 error::SolcError,
39 multi::{MultiCompilerParsedSource, MultiCompilerRestrictions},
40 solc::{CliSettings, SolcSettings},
41};
42use regex::Regex;
43use revm::primitives::hardfork::SpecId;
44use semver::Version;
45use serde::{Deserialize, Serialize, Serializer};
46use std::{
47 borrow::Cow,
48 collections::BTreeMap,
49 fs,
50 path::{Path, PathBuf},
51 str::FromStr,
52};
53
54mod macros;
55
56pub mod utils;
57pub use utils::*;
58
59mod endpoints;
60pub use endpoints::{
61 ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
62};
63
64mod etherscan;
65use etherscan::{
66 EtherscanConfigError, EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig,
67};
68
69mod resolve;
70pub use resolve::UnresolvedEnvVarError;
71
72pub mod cache;
73use cache::{Cache, ChainCache};
74
75pub mod fmt;
76pub use fmt::FormatterConfig;
77
78pub mod lint;
79pub use lint::{LinterConfig, Severity as LintSeverity};
80
81pub mod fs_permissions;
82pub use fs_permissions::FsPermissions;
83use fs_permissions::PathPermission;
84
85pub mod error;
86use error::ExtractConfigError;
87pub use error::SolidityErrorCode;
88
89pub mod doc;
90pub use doc::DocConfig;
91
92pub mod filter;
93pub use filter::SkipBuildFilters;
94
95mod warning;
96pub use warning::*;
97
98pub mod fix;
99
100pub use alloy_chains::{Chain, NamedChain};
102pub use figment;
103use foundry_block_explorers::EtherscanApiVersion;
104
105pub mod providers;
106pub use providers::Remappings;
107use providers::*;
108
109mod fuzz;
110pub use fuzz::{FuzzConfig, FuzzDictionaryConfig};
111
112mod invariant;
113pub use invariant::InvariantConfig;
114
115mod inline;
116pub use inline::{InlineConfig, InlineConfigError, NatSpec};
117
118pub mod soldeer;
119use soldeer::{SoldeerConfig, SoldeerDependencyConfig};
120
121mod vyper;
122pub use vyper::VyperConfig;
123
124mod bind_json;
125use bind_json::BindJsonConfig;
126
127mod compilation;
128pub use compilation::{CompilationRestrictions, SettingsOverrides};
129
130#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
162pub struct Config {
163 #[serde(skip)]
170 pub profile: Profile,
171 #[serde(skip)]
175 pub profiles: Vec<Profile>,
176
177 #[serde(default = "root_default", skip_serializing)]
182 pub root: PathBuf,
183
184 pub src: PathBuf,
186 pub test: PathBuf,
188 pub script: PathBuf,
190 pub out: PathBuf,
192 pub libs: Vec<PathBuf>,
194 pub remappings: Vec<RelativeRemapping>,
196 pub auto_detect_remappings: bool,
198 pub libraries: Vec<String>,
200 pub cache: bool,
202 pub dynamic_test_linking: bool,
204 pub cache_path: PathBuf,
206 pub snapshots: PathBuf,
208 pub gas_snapshot_check: bool,
210 pub gas_snapshot_emit: bool,
212 pub broadcast: PathBuf,
214 pub allow_paths: Vec<PathBuf>,
216 pub include_paths: Vec<PathBuf>,
218 pub skip: Vec<GlobMatcher>,
220 pub force: bool,
222 #[serde(with = "from_str_lowercase")]
224 pub evm_version: EvmVersion,
225 pub gas_reports: Vec<String>,
227 pub gas_reports_ignore: Vec<String>,
229 pub gas_reports_include_tests: bool,
231 #[doc(hidden)]
241 pub solc: Option<SolcReq>,
242 pub auto_detect_solc: bool,
244 pub offline: bool,
251 pub optimizer: Option<bool>,
253 pub optimizer_runs: Option<usize>,
264 pub optimizer_details: Option<OptimizerDetails>,
268 pub model_checker: Option<ModelCheckerSettings>,
270 pub verbosity: u8,
272 pub eth_rpc_url: Option<String>,
274 pub eth_rpc_accept_invalid_certs: bool,
276 pub eth_rpc_jwt: Option<String>,
278 pub eth_rpc_timeout: Option<u64>,
280 pub eth_rpc_headers: Option<Vec<String>>,
289 pub etherscan_api_key: Option<String>,
291 pub etherscan_api_version: Option<EtherscanApiVersion>,
293 #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
295 pub etherscan: EtherscanConfigs,
296 pub ignored_error_codes: Vec<SolidityErrorCode>,
298 #[serde(rename = "ignored_warnings_from")]
300 pub ignored_file_paths: Vec<PathBuf>,
301 pub deny_warnings: bool,
303 #[serde(rename = "match_test")]
305 pub test_pattern: Option<RegexWrapper>,
306 #[serde(rename = "no_match_test")]
308 pub test_pattern_inverse: Option<RegexWrapper>,
309 #[serde(rename = "match_contract")]
311 pub contract_pattern: Option<RegexWrapper>,
312 #[serde(rename = "no_match_contract")]
314 pub contract_pattern_inverse: Option<RegexWrapper>,
315 #[serde(rename = "match_path", with = "from_opt_glob")]
317 pub path_pattern: Option<globset::Glob>,
318 #[serde(rename = "no_match_path", with = "from_opt_glob")]
320 pub path_pattern_inverse: Option<globset::Glob>,
321 #[serde(rename = "no_match_coverage")]
323 pub coverage_pattern_inverse: Option<RegexWrapper>,
324 pub test_failures_file: PathBuf,
326 pub threads: Option<usize>,
328 pub show_progress: bool,
330 pub fuzz: FuzzConfig,
332 pub invariant: InvariantConfig,
334 pub ffi: bool,
336 pub allow_internal_expect_revert: bool,
338 pub always_use_create_2_factory: bool,
340 pub prompt_timeout: u64,
342 pub sender: Address,
344 pub tx_origin: Address,
346 pub initial_balance: U256,
348 #[serde(
350 deserialize_with = "crate::deserialize_u64_to_u256",
351 serialize_with = "crate::serialize_u64_or_u256"
352 )]
353 pub block_number: U256,
354 pub fork_block_number: Option<u64>,
356 #[serde(rename = "chain_id", alias = "chain")]
358 pub chain: Option<Chain>,
359 pub gas_limit: GasLimit,
361 pub code_size_limit: Option<usize>,
363 pub gas_price: Option<u64>,
368 pub block_base_fee_per_gas: u64,
370 pub block_coinbase: Address,
372 #[serde(
374 deserialize_with = "crate::deserialize_u64_to_u256",
375 serialize_with = "crate::serialize_u64_or_u256"
376 )]
377 pub block_timestamp: U256,
378 pub block_difficulty: u64,
380 pub block_prevrandao: B256,
382 pub block_gas_limit: Option<GasLimit>,
384 pub memory_limit: u64,
389 #[serde(default)]
406 pub extra_output: Vec<ContractOutputSelection>,
407 #[serde(default)]
418 pub extra_output_files: Vec<ContractOutputSelection>,
419 pub names: bool,
421 pub sizes: bool,
423 pub via_ir: bool,
426 pub ast: bool,
428 pub rpc_storage_caching: StorageCachingConfig,
430 pub no_storage_caching: bool,
433 pub no_rpc_rate_limit: bool,
436 #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
438 pub rpc_endpoints: RpcEndpoints,
439 pub use_literal_content: bool,
441 #[serde(with = "from_str_lowercase")]
445 pub bytecode_hash: BytecodeHash,
446 pub cbor_metadata: bool,
451 #[serde(with = "serde_helpers::display_from_str_opt")]
453 pub revert_strings: Option<RevertStrings>,
454 pub sparse_mode: bool,
459 pub build_info: bool,
462 pub build_info_path: Option<PathBuf>,
464 pub fmt: FormatterConfig,
466 pub lint: LinterConfig,
468 pub doc: DocConfig,
470 pub bind_json: BindJsonConfig,
472 pub fs_permissions: FsPermissions,
476
477 pub isolate: bool,
481
482 pub disable_block_gas_limit: bool,
484
485 pub labels: AddressHashMap<String>,
487
488 pub unchecked_cheatcode_artifacts: bool,
491
492 pub create2_library_salt: B256,
494
495 pub create2_deployer: Address,
497
498 pub vyper: VyperConfig,
500
501 pub dependencies: Option<SoldeerDependencyConfig>,
503
504 pub soldeer: Option<SoldeerConfig>,
506
507 pub assertions_revert: bool,
511
512 pub legacy_assertions: bool,
514
515 #[serde(default, skip_serializing_if = "Vec::is_empty")]
517 pub extra_args: Vec<String>,
518
519 #[serde(alias = "alphanet")]
521 pub odyssey: bool,
522
523 pub transaction_timeout: u64,
525
526 #[serde(rename = "__warnings", default, skip_serializing)]
528 pub warnings: Vec<Warning>,
529
530 #[serde(default)]
532 pub additional_compiler_profiles: Vec<SettingsOverrides>,
533
534 #[serde(default)]
536 pub compilation_restrictions: Vec<CompilationRestrictions>,
537
538 pub script_execution_protection: bool,
540
541 #[doc(hidden)]
550 #[serde(skip)]
551 pub _non_exhaustive: (),
552}
553
554pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
556
557pub const DEPRECATIONS: &[(&str, &str)] = &[("cancun", "evm_version = Cancun")];
561
562impl Config {
563 pub const DEFAULT_PROFILE: Profile = Profile::Default;
565
566 pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
568
569 pub const PROFILE_SECTION: &'static str = "profile";
571
572 pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
574 "rpc_endpoints",
575 "etherscan",
576 "fmt",
577 "lint",
578 "doc",
579 "fuzz",
580 "invariant",
581 "labels",
582 "dependencies",
583 "soldeer",
584 "vyper",
585 "bind_json",
586 ];
587
588 pub const FILE_NAME: &'static str = "foundry.toml";
590
591 pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
593
594 pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
598
599 pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
601
602 pub const DEFAULT_CREATE2_DEPLOYER: Address =
604 address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");
605
606 pub fn load() -> Result<Self, ExtractConfigError> {
610 Self::from_provider(Self::figment())
611 }
612
613 pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
617 Self::from_provider(Self::default().to_figment(providers))
618 }
619
620 #[track_caller]
624 pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
625 Self::from_provider(Self::figment_with_root(root.as_ref()))
626 }
627
628 #[doc(alias = "try_from")]
643 pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
644 trace!("load config with provider: {:?}", provider.metadata());
645 Self::from_figment(Figment::from(provider))
646 }
647
648 #[doc(hidden)]
649 #[deprecated(note = "use `Config::from_provider` instead")]
650 pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
651 Self::from_provider(provider)
652 }
653
654 fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
655 let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
656 config.profile = figment.profile().clone();
657
658 let mut add_profile = |profile: &Profile| {
660 if !config.profiles.contains(profile) {
661 config.profiles.push(profile.clone());
662 }
663 };
664 let figment = figment.select(Self::PROFILE_SECTION);
665 if let Ok(data) = figment.data()
666 && let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION))
667 {
668 for profile in profiles.keys() {
669 add_profile(&Profile::new(profile));
670 }
671 }
672 add_profile(&Self::DEFAULT_PROFILE);
673 add_profile(&config.profile);
674
675 config.normalize_optimizer_settings();
676
677 Ok(config)
678 }
679
680 pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
685 if providers.is_none() {
688 return Figment::from(self);
689 }
690
691 let root = self.root.as_path();
692 let profile = Self::selected_profile();
693 let mut figment = Figment::default().merge(DappHardhatDirProvider(root));
694
695 if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
697 figment = Self::merge_toml_provider(
698 figment,
699 TomlFileProvider::new(None, global_toml).cached(),
700 profile.clone(),
701 );
702 }
703 figment = Self::merge_toml_provider(
705 figment,
706 TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)).cached(),
707 profile.clone(),
708 );
709
710 figment = figment
712 .merge(
713 Env::prefixed("DAPP_")
714 .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
715 .global(),
716 )
717 .merge(
718 Env::prefixed("DAPP_TEST_")
719 .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
720 .global(),
721 )
722 .merge(DappEnvCompatProvider)
723 .merge(EtherscanEnvProvider::default())
724 .merge(
725 Env::prefixed("FOUNDRY_")
726 .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
727 .map(|key| {
728 let key = key.as_str();
729 if Self::STANDALONE_SECTIONS.iter().any(|section| {
730 key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
731 }) {
732 key.replacen('_', ".", 1).into()
733 } else {
734 key.into()
735 }
736 })
737 .global(),
738 )
739 .select(profile.clone());
740
741 if providers.is_all() {
743 let remappings = RemappingsProvider {
747 auto_detect_remappings: figment
748 .extract_inner::<bool>("auto_detect_remappings")
749 .unwrap_or(true),
750 lib_paths: figment
751 .extract_inner::<Vec<PathBuf>>("libs")
752 .map(Cow::Owned)
753 .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
754 root,
755 remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
756 };
757 figment = figment.merge(remappings);
758 }
759
760 figment = self.normalize_defaults(figment);
762
763 Figment::from(self).merge(figment).select(profile)
764 }
765
766 #[must_use]
771 pub fn canonic(self) -> Self {
772 let root = self.root.clone();
773 self.canonic_at(root)
774 }
775
776 #[must_use]
794 pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
795 let root = canonic(root);
796
797 fn p(root: &Path, rem: &Path) -> PathBuf {
798 canonic(root.join(rem))
799 }
800
801 self.src = p(&root, &self.src);
802 self.test = p(&root, &self.test);
803 self.script = p(&root, &self.script);
804 self.out = p(&root, &self.out);
805 self.broadcast = p(&root, &self.broadcast);
806 self.cache_path = p(&root, &self.cache_path);
807 self.snapshots = p(&root, &self.snapshots);
808
809 if let Some(build_info_path) = self.build_info_path {
810 self.build_info_path = Some(p(&root, &build_info_path));
811 }
812
813 self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
814
815 self.remappings =
816 self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
817
818 self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
819
820 self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
821
822 self.fs_permissions.join_all(&root);
823
824 if let Some(model_checker) = &mut self.model_checker {
825 model_checker.contracts = std::mem::take(&mut model_checker.contracts)
826 .into_iter()
827 .map(|(path, contracts)| {
828 (format!("{}", p(&root, path.as_ref()).display()), contracts)
829 })
830 .collect();
831 }
832
833 self
834 }
835
836 pub fn normalized_evm_version(mut self) -> Self {
838 self.normalize_evm_version();
839 self
840 }
841
842 pub fn normalized_optimizer_settings(mut self) -> Self {
845 self.normalize_optimizer_settings();
846 self
847 }
848
849 pub fn normalize_evm_version(&mut self) {
851 self.evm_version = self.get_normalized_evm_version();
852 }
853
854 pub fn normalize_optimizer_settings(&mut self) {
859 match (self.optimizer, self.optimizer_runs) {
860 (None, None) => {
862 self.optimizer = Some(false);
863 self.optimizer_runs = Some(200);
864 }
865 (Some(_), None) => self.optimizer_runs = Some(200),
867 (None, Some(runs)) => self.optimizer = Some(runs > 0),
869 _ => {}
870 }
871 }
872
873 pub fn get_normalized_evm_version(&self) -> EvmVersion {
875 if let Some(version) = self.solc_version()
876 && let Some(evm_version) = self.evm_version.normalize_version_solc(&version)
877 {
878 return evm_version;
879 }
880 self.evm_version
881 }
882
883 #[must_use]
888 pub fn sanitized(self) -> Self {
889 let mut config = self.canonic();
890
891 config.sanitize_remappings();
892
893 config.libs.sort_unstable();
894 config.libs.dedup();
895
896 config
897 }
898
899 pub fn sanitize_remappings(&mut self) {
903 #[cfg(target_os = "windows")]
904 {
905 use path_slash::PathBufExt;
907 self.remappings.iter_mut().for_each(|r| {
908 r.path.path = r.path.path.to_slash_lossy().into_owned().into();
909 });
910 }
911 }
912
913 pub fn install_lib_dir(&self) -> &Path {
917 self.libs
918 .iter()
919 .find(|p| !p.ends_with("node_modules"))
920 .map(|p| p.as_path())
921 .unwrap_or_else(|| Path::new("lib"))
922 }
923
924 pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
939 self.create_project(self.cache, false)
940 }
941
942 pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
945 self.create_project(false, true)
946 }
947
948 fn additional_settings(
950 &self,
951 base: &MultiCompilerSettings,
952 ) -> BTreeMap<String, MultiCompilerSettings> {
953 let mut map = BTreeMap::new();
954
955 for profile in &self.additional_compiler_profiles {
956 let mut settings = base.clone();
957 profile.apply(&mut settings);
958 map.insert(profile.name.clone(), settings);
959 }
960
961 map
962 }
963
964 #[expect(clippy::disallowed_macros)]
966 fn restrictions(
967 &self,
968 paths: &ProjectPathsConfig,
969 ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
970 {
971 let mut map = BTreeMap::new();
972 if self.compilation_restrictions.is_empty() {
973 return Ok(BTreeMap::new());
974 }
975
976 let graph = Graph::<MultiCompilerParsedSource>::resolve(paths)?;
977 let (sources, _) = graph.into_sources();
978
979 for res in &self.compilation_restrictions {
980 for source in sources.keys().filter(|path| {
981 if res.paths.is_match(path) {
982 true
983 } else if let Ok(path) = path.strip_prefix(&paths.root) {
984 res.paths.is_match(path)
985 } else {
986 false
987 }
988 }) {
989 let res: RestrictionsWithVersion<_> =
990 res.clone().try_into().map_err(SolcError::msg)?;
991 if !map.contains_key(source) {
992 map.insert(source.clone(), res);
993 } else {
994 let value = map.remove(source.as_path()).unwrap();
995 if let Some(merged) = value.clone().merge(res) {
996 map.insert(source.clone(), merged);
997 } else {
998 eprintln!(
1000 "{}",
1001 yansi::Paint::yellow(&format!(
1002 "Failed to merge compilation restrictions for {}",
1003 source.display()
1004 ))
1005 );
1006 map.insert(source.clone(), value);
1007 }
1008 }
1009 }
1010 }
1011
1012 Ok(map)
1013 }
1014
1015 pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
1019 let settings = self.compiler_settings()?;
1020 let paths = self.project_paths();
1021 let mut builder = Project::builder()
1022 .artifacts(self.configured_artifacts_handler())
1023 .additional_settings(self.additional_settings(&settings))
1024 .restrictions(self.restrictions(&paths)?)
1025 .settings(settings)
1026 .paths(paths)
1027 .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
1028 .ignore_paths(self.ignored_file_paths.clone())
1029 .set_compiler_severity_filter(if self.deny_warnings {
1030 Severity::Warning
1031 } else {
1032 Severity::Error
1033 })
1034 .set_offline(self.offline)
1035 .set_cached(cached)
1036 .set_build_info(!no_artifacts && self.build_info)
1037 .set_no_artifacts(no_artifacts);
1038
1039 if !self.skip.is_empty() {
1040 let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
1041 builder = builder.sparse_output(filter);
1042 }
1043
1044 let project = builder.build(self.compiler()?)?;
1045
1046 if self.force {
1047 self.cleanup(&project)?;
1048 }
1049
1050 Ok(project)
1051 }
1052
1053 pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
1055 &self,
1056 project: &Project<C, T>,
1057 ) -> Result<(), SolcError> {
1058 project.cleanup()?;
1059
1060 let _ = fs::remove_file(&self.test_failures_file);
1062
1063 let remove_test_dir = |test_dir: &Option<PathBuf>| {
1065 if let Some(test_dir) = test_dir {
1066 let path = project.root().join(test_dir);
1067 if path.exists() {
1068 let _ = fs::remove_dir_all(&path);
1069 }
1070 }
1071 };
1072 remove_test_dir(&self.fuzz.failure_persist_dir);
1073 remove_test_dir(&self.invariant.corpus_dir);
1074 remove_test_dir(&self.invariant.failure_persist_dir);
1075
1076 Ok(())
1077 }
1078
1079 fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
1086 if let Some(solc) = &self.solc {
1087 let solc = match solc {
1088 SolcReq::Version(version) => {
1089 if let Some(solc) = Solc::find_svm_installed_version(version)? {
1090 solc
1091 } else {
1092 if self.offline {
1093 return Err(SolcError::msg(format!(
1094 "can't install missing solc {version} in offline mode"
1095 )));
1096 }
1097 Solc::blocking_install(version)?
1098 }
1099 }
1100 SolcReq::Local(solc) => {
1101 if !solc.is_file() {
1102 return Err(SolcError::msg(format!(
1103 "`solc` {} does not exist",
1104 solc.display()
1105 )));
1106 }
1107 Solc::new(solc)?
1108 }
1109 };
1110 return Ok(Some(solc));
1111 }
1112
1113 Ok(None)
1114 }
1115
1116 #[inline]
1118 pub fn evm_spec_id(&self) -> SpecId {
1119 evm_spec_id(self.evm_version, self.odyssey)
1120 }
1121
1122 pub fn is_auto_detect(&self) -> bool {
1127 if self.solc.is_some() {
1128 return false;
1129 }
1130 self.auto_detect_solc
1131 }
1132
1133 pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
1135 !self.no_storage_caching
1136 && self.rpc_storage_caching.enable_for_chain_id(chain_id.into())
1137 && self.rpc_storage_caching.enable_for_endpoint(endpoint)
1138 }
1139
1140 pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
1155 let mut builder = ProjectPathsConfig::builder()
1156 .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
1157 .sources(&self.src)
1158 .tests(&self.test)
1159 .scripts(&self.script)
1160 .artifacts(&self.out)
1161 .libs(self.libs.iter())
1162 .remappings(self.get_all_remappings())
1163 .allowed_path(&self.root)
1164 .allowed_paths(&self.libs)
1165 .allowed_paths(&self.allow_paths)
1166 .include_paths(&self.include_paths);
1167
1168 if let Some(build_info_path) = &self.build_info_path {
1169 builder = builder.build_infos(build_info_path);
1170 }
1171
1172 builder.build_with_root(&self.root)
1173 }
1174
1175 pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
1177 if let Some(solc) = self.ensure_solc()? {
1178 Ok(SolcCompiler::Specific(solc))
1179 } else {
1180 Ok(SolcCompiler::AutoDetect)
1181 }
1182 }
1183
1184 pub fn solc_version(&self) -> Option<Version> {
1186 self.solc.as_ref().and_then(|solc| solc.try_version().ok())
1187 }
1188
1189 pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
1191 if !self.project_paths::<VyperLanguage>().has_input_files() {
1193 return Ok(None);
1194 }
1195 let vyper = if let Some(path) = &self.vyper.path {
1196 Some(Vyper::new(path)?)
1197 } else {
1198 Vyper::new("vyper").ok()
1199 };
1200 Ok(vyper)
1201 }
1202
1203 pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
1205 Ok(MultiCompiler { solc: Some(self.solc_compiler()?), vyper: self.vyper_compiler()? })
1206 }
1207
1208 pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
1210 Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
1211 }
1212
1213 pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
1215 self.remappings.iter().map(|m| m.clone().into())
1216 }
1217
1218 pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1233 Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1234 }
1235
1236 pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1252 let maybe_alias = self.eth_rpc_url.as_ref().or(self.etherscan_api_key.as_ref())?;
1253 if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1254 Some(alias)
1255 } else {
1256 Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1257 }
1258 }
1259
1260 pub fn get_rpc_url_with_alias(
1285 &self,
1286 maybe_alias: &str,
1287 ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1288 let mut endpoints = self.rpc_endpoints.clone().resolved();
1289 if let Some(endpoint) = endpoints.remove(maybe_alias) {
1290 return Some(endpoint.url().map(Cow::Owned));
1291 }
1292
1293 if let Some(mesc_url) = self.get_rpc_url_from_mesc(maybe_alias) {
1294 return Some(Ok(Cow::Owned(mesc_url)));
1295 }
1296
1297 None
1298 }
1299
1300 pub fn get_rpc_url_from_mesc(&self, maybe_alias: &str) -> Option<String> {
1302 let mesc_config = mesc::load::load_config_data()
1305 .inspect_err(|err| debug!(%err, "failed to load mesc config"))
1306 .ok()?;
1307
1308 if let Ok(Some(endpoint)) =
1309 mesc::query::get_endpoint_by_query(&mesc_config, maybe_alias, Some("foundry"))
1310 {
1311 return Some(endpoint.url);
1312 }
1313
1314 if maybe_alias.chars().all(|c| c.is_numeric()) {
1315 if let Ok(Some(endpoint)) =
1321 mesc::query::get_endpoint_by_network(&mesc_config, maybe_alias, Some("foundry"))
1322 {
1323 return Some(endpoint.url);
1324 }
1325 }
1326
1327 None
1328 }
1329
1330 pub fn get_rpc_url_or<'a>(
1342 &'a self,
1343 fallback: impl Into<Cow<'a, str>>,
1344 ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
1345 if let Some(url) = self.get_rpc_url() { url } else { Ok(fallback.into()) }
1346 }
1347
1348 pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1360 self.get_rpc_url_or("http://localhost:8545")
1361 }
1362
1363 pub fn get_etherscan_config(
1383 &self,
1384 ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1385 self.get_etherscan_config_with_chain(None).transpose()
1386 }
1387
1388 pub fn get_etherscan_config_with_chain(
1395 &self,
1396 chain: Option<Chain>,
1397 ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1398 let default_api_version = self.etherscan_api_version.unwrap_or_default();
1399
1400 if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref())
1401 && self.etherscan.contains_key(maybe_alias)
1402 {
1403 return self
1404 .etherscan
1405 .clone()
1406 .resolved(default_api_version)
1407 .remove(maybe_alias)
1408 .transpose();
1409 }
1410
1411 if let Some(res) = chain.or(self.chain).and_then(|chain| {
1413 self.etherscan.clone().resolved(default_api_version).find_chain(chain)
1414 }) {
1415 match (res, self.etherscan_api_key.as_ref()) {
1416 (Ok(mut config), Some(key)) => {
1417 config.key.clone_from(key);
1420 return Ok(Some(config));
1421 }
1422 (Ok(config), None) => return Ok(Some(config)),
1423 (Err(err), None) => return Err(err),
1424 (Err(_), Some(_)) => {
1425 }
1427 }
1428 }
1429
1430 if let Some(key) = self.etherscan_api_key.as_ref() {
1432 return Ok(ResolvedEtherscanConfig::create(
1433 key,
1434 chain.or(self.chain).unwrap_or_default(),
1435 default_api_version,
1436 ));
1437 }
1438
1439 Ok(None)
1440 }
1441
1442 pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1448 self.get_etherscan_config_with_chain(chain).ok().flatten().map(|c| c.key)
1449 }
1450
1451 pub fn get_etherscan_api_version(&self, chain: Option<Chain>) -> EtherscanApiVersion {
1455 self.get_etherscan_config_with_chain(chain)
1456 .ok()
1457 .flatten()
1458 .map(|c| c.api_version)
1459 .unwrap_or_default()
1460 }
1461
1462 pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1469 get_dir_remapping(&self.src)
1470 }
1471
1472 pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1474 if self.root.join(&self.test).exists() { get_dir_remapping(&self.test) } else { None }
1475 }
1476
1477 pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1479 if self.root.join(&self.script).exists() { get_dir_remapping(&self.script) } else { None }
1480 }
1481
1482 pub fn optimizer(&self) -> Optimizer {
1488 Optimizer {
1489 enabled: self.optimizer,
1490 runs: self.optimizer_runs,
1491 details: self.optimizer_details.clone(),
1494 }
1495 }
1496
1497 pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1500 let mut extra_output = self.extra_output.clone();
1501
1502 if !extra_output.contains(&ContractOutputSelection::Metadata) {
1508 extra_output.push(ContractOutputSelection::Metadata);
1509 }
1510
1511 ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().copied())
1512 }
1513
1514 pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1517 Libraries::parse(&self.libraries)
1518 }
1519
1520 pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1522 let paths: ProjectPathsConfig = self.project_paths();
1523 Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1524 }
1525
1526 pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
1531 let mut model_checker = self.model_checker.clone();
1535 if let Some(model_checker_settings) = &mut model_checker
1536 && model_checker_settings.targets.is_none()
1537 {
1538 model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1539 }
1540
1541 let mut settings = Settings {
1542 libraries: self.libraries_with_remappings()?,
1543 optimizer: self.optimizer(),
1544 evm_version: Some(self.evm_version),
1545 metadata: Some(SettingsMetadata {
1546 use_literal_content: Some(self.use_literal_content),
1547 bytecode_hash: Some(self.bytecode_hash),
1548 cbor_metadata: Some(self.cbor_metadata),
1549 }),
1550 debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1551 revert_strings: Some(revert_strings),
1552 debug_info: Vec::new(),
1554 }),
1555 model_checker,
1556 via_ir: Some(self.via_ir),
1557 stop_after: None,
1559 remappings: Vec::new(),
1561 output_selection: Default::default(),
1563 }
1564 .with_extra_output(self.configured_artifacts_handler().output_selection());
1565
1566 if self.ast || self.build_info {
1568 settings = settings.with_ast();
1569 }
1570
1571 let cli_settings =
1572 CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };
1573
1574 Ok(SolcSettings { settings, cli_settings })
1575 }
1576
1577 pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
1580 Ok(VyperSettings {
1581 evm_version: Some(self.evm_version),
1582 optimize: self.vyper.optimize,
1583 bytecode_metadata: None,
1584 output_selection: OutputSelection::common_output_selection([
1587 "abi".to_string(),
1588 "evm.bytecode".to_string(),
1589 "evm.deployedBytecode".to_string(),
1590 ]),
1591 search_paths: None,
1592 experimental_codegen: self.vyper.experimental_codegen,
1593 })
1594 }
1595
1596 pub fn figment() -> Figment {
1617 Self::default().into()
1618 }
1619
1620 pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
1632 Self::with_root(root.as_ref()).into()
1633 }
1634
1635 #[doc(hidden)]
1636 #[track_caller]
1637 pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
1638 let root = match root {
1639 Some(root) => root,
1640 None => &find_project_root(None).expect("could not determine project root"),
1641 };
1642 Self::figment_with_root(root)
1643 }
1644
1645 pub fn with_root(root: impl AsRef<Path>) -> Self {
1654 Self::_with_root(root.as_ref())
1655 }
1656
1657 fn _with_root(root: &Path) -> Self {
1658 let paths = ProjectPathsConfig::builder().build_with_root::<()>(root);
1660 let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
1661 Self {
1662 root: paths.root,
1663 src: paths.sources.file_name().unwrap().into(),
1664 out: artifacts.clone(),
1665 libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
1666 remappings: paths
1667 .remappings
1668 .into_iter()
1669 .map(|r| RelativeRemapping::new(r, root))
1670 .collect(),
1671 fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
1672 ..Self::default()
1673 }
1674 }
1675
1676 pub fn hardhat() -> Self {
1678 Self {
1679 src: "contracts".into(),
1680 out: "artifacts".into(),
1681 libs: vec!["node_modules".into()],
1682 ..Self::default()
1683 }
1684 }
1685
1686 pub fn dapptools() -> Self {
1688 Self {
1689 chain: Some(Chain::from_id(99)),
1690 block_timestamp: U256::ZERO,
1691 block_number: U256::ZERO,
1692 ..Self::default()
1693 }
1694 }
1695
1696 pub fn into_basic(self) -> BasicConfig {
1705 BasicConfig {
1706 profile: self.profile,
1707 src: self.src,
1708 out: self.out,
1709 libs: self.libs,
1710 remappings: self.remappings,
1711 }
1712 }
1713
1714 pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
1719 where
1720 F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
1721 {
1722 let config = Self::load_with_root(root)?.sanitized();
1723 config.update(|doc| f(&config, doc))
1724 }
1725
1726 pub fn update<F>(&self, f: F) -> eyre::Result<()>
1731 where
1732 F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
1733 {
1734 let file_path = self.get_config_path();
1735 if !file_path.exists() {
1736 return Ok(());
1737 }
1738 let contents = fs::read_to_string(&file_path)?;
1739 let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
1740 if f(&mut doc) {
1741 fs::write(file_path, doc.to_string())?;
1742 }
1743 Ok(())
1744 }
1745
1746 pub fn update_libs(&self) -> eyre::Result<()> {
1752 self.update(|doc| {
1753 let profile = self.profile.as_str().as_str();
1754 let root = &self.root;
1755 let libs: toml_edit::Value = self
1756 .libs
1757 .iter()
1758 .map(|path| {
1759 let path =
1760 if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
1761 toml_edit::Value::from(&*path.to_string_lossy())
1762 })
1763 .collect();
1764 let libs = toml_edit::value(libs);
1765 doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
1766 true
1767 })
1768 }
1769
1770 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
1782 let mut value = toml::Value::try_from(self)?;
1784 let value_table = value.as_table_mut().unwrap();
1786 let standalone_sections = Self::STANDALONE_SECTIONS
1788 .iter()
1789 .filter_map(|section| {
1790 let section = section.to_string();
1791 value_table.remove(§ion).map(|value| (section, value))
1792 })
1793 .collect::<Vec<_>>();
1794 let mut wrapping_table = [(
1796 Self::PROFILE_SECTION.into(),
1797 toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
1798 )]
1799 .into_iter()
1800 .collect::<toml::map::Map<_, _>>();
1801 for (section, value) in standalone_sections {
1803 wrapping_table.insert(section, value);
1804 }
1805 toml::to_string_pretty(&toml::Value::Table(wrapping_table))
1807 }
1808
1809 pub fn get_config_path(&self) -> PathBuf {
1811 self.root.join(Self::FILE_NAME)
1812 }
1813
1814 pub fn selected_profile() -> Profile {
1818 #[cfg(test)]
1820 {
1821 Self::force_selected_profile()
1822 }
1823 #[cfg(not(test))]
1824 {
1825 static CACHE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();
1826 CACHE.get_or_init(Self::force_selected_profile).clone()
1827 }
1828 }
1829
1830 fn force_selected_profile() -> Profile {
1831 Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
1832 }
1833
1834 pub fn foundry_dir_toml() -> Option<PathBuf> {
1836 Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
1837 }
1838
1839 pub fn foundry_dir() -> Option<PathBuf> {
1841 dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
1842 }
1843
1844 pub fn foundry_cache_dir() -> Option<PathBuf> {
1846 Self::foundry_dir().map(|p| p.join("cache"))
1847 }
1848
1849 pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
1851 Some(Self::foundry_cache_dir()?.join("rpc"))
1852 }
1853 pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1855 Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
1856 }
1857
1858 pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
1860 Some(Self::foundry_cache_dir()?.join("etherscan"))
1861 }
1862
1863 pub fn foundry_keystores_dir() -> Option<PathBuf> {
1865 Some(Self::foundry_dir()?.join("keystores"))
1866 }
1867
1868 pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1871 Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
1872 }
1873
1874 pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1877 Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
1878 }
1879
1880 pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1883 Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
1884 }
1885
1886 pub fn data_dir() -> eyre::Result<PathBuf> {
1894 let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
1895 std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
1896 Ok(path)
1897 }
1898
1899 pub fn find_config_file() -> Option<PathBuf> {
1906 fn find(path: &Path) -> Option<PathBuf> {
1907 if path.is_absolute() {
1908 return match path.is_file() {
1909 true => Some(path.to_path_buf()),
1910 false => None,
1911 };
1912 }
1913 let cwd = std::env::current_dir().ok()?;
1914 let mut cwd = cwd.as_path();
1915 loop {
1916 let file_path = cwd.join(path);
1917 if file_path.is_file() {
1918 return Some(file_path);
1919 }
1920 cwd = cwd.parent()?;
1921 }
1922 }
1923 find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
1924 .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
1925 }
1926
1927 pub fn clean_foundry_cache() -> eyre::Result<()> {
1929 if let Some(cache_dir) = Self::foundry_cache_dir() {
1930 let path = cache_dir.as_path();
1931 let _ = fs::remove_dir_all(path);
1932 } else {
1933 eyre::bail!("failed to get foundry_cache_dir");
1934 }
1935
1936 Ok(())
1937 }
1938
1939 pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<()> {
1941 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
1942 let path = cache_dir.as_path();
1943 let _ = fs::remove_dir_all(path);
1944 } else {
1945 eyre::bail!("failed to get foundry_chain_cache_dir");
1946 }
1947
1948 Ok(())
1949 }
1950
1951 pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<()> {
1953 if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
1954 let path = cache_dir.as_path();
1955 let _ = fs::remove_dir_all(path);
1956 } else {
1957 eyre::bail!("failed to get foundry_block_cache_dir");
1958 }
1959
1960 Ok(())
1961 }
1962
1963 pub fn clean_foundry_etherscan_cache() -> eyre::Result<()> {
1965 if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
1966 let path = cache_dir.as_path();
1967 let _ = fs::remove_dir_all(path);
1968 } else {
1969 eyre::bail!("failed to get foundry_etherscan_cache_dir");
1970 }
1971
1972 Ok(())
1973 }
1974
1975 pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<()> {
1977 if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
1978 let path = cache_dir.as_path();
1979 let _ = fs::remove_dir_all(path);
1980 } else {
1981 eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
1982 }
1983
1984 Ok(())
1985 }
1986
1987 pub fn list_foundry_cache() -> eyre::Result<Cache> {
1989 if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
1990 let mut cache = Cache { chains: vec![] };
1991 if !cache_dir.exists() {
1992 return Ok(cache);
1993 }
1994 if let Ok(entries) = cache_dir.as_path().read_dir() {
1995 for entry in entries.flatten().filter(|x| x.path().is_dir()) {
1996 match Chain::from_str(&entry.file_name().to_string_lossy()) {
1997 Ok(chain) => cache.chains.push(Self::list_foundry_chain_cache(chain)?),
1998 Err(_) => continue,
1999 }
2000 }
2001 Ok(cache)
2002 } else {
2003 eyre::bail!("failed to access foundry_cache_dir");
2004 }
2005 } else {
2006 eyre::bail!("failed to get foundry_cache_dir");
2007 }
2008 }
2009
2010 pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
2012 let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
2013 Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
2014 None => {
2015 warn!("failed to access foundry_etherscan_chain_cache_dir");
2016 0
2017 }
2018 };
2019
2020 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2021 let blocks = Self::get_cached_blocks(&cache_dir)?;
2022 Ok(ChainCache {
2023 name: chain.to_string(),
2024 blocks,
2025 block_explorer: block_explorer_data_size,
2026 })
2027 } else {
2028 eyre::bail!("failed to get foundry_chain_cache_dir");
2029 }
2030 }
2031
2032 fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
2034 let mut blocks = vec![];
2035 if !chain_path.exists() {
2036 return Ok(blocks);
2037 }
2038 for block in chain_path.read_dir()?.flatten() {
2039 let file_type = block.file_type()?;
2040 let file_name = block.file_name();
2041 let filepath = if file_type.is_dir() {
2042 block.path().join("storage.json")
2043 } else if file_type.is_file()
2044 && file_name.to_string_lossy().chars().all(char::is_numeric)
2045 {
2046 block.path()
2047 } else {
2048 continue;
2049 };
2050 blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
2051 }
2052 Ok(blocks)
2053 }
2054
2055 fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
2057 if !chain_path.exists() {
2058 return Ok(0);
2059 }
2060
2061 fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
2062 dir.try_fold(0, |acc, file| {
2063 let file = file?;
2064 let size = match file.metadata()? {
2065 data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
2066 data => data.len(),
2067 };
2068 Ok(acc + size)
2069 })
2070 }
2071
2072 dir_size_recursive(fs::read_dir(chain_path)?)
2073 }
2074
2075 fn merge_toml_provider(
2076 mut figment: Figment,
2077 toml_provider: impl Provider,
2078 profile: Profile,
2079 ) -> Figment {
2080 figment = figment.select(profile.clone());
2081
2082 figment = {
2084 let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
2085 figment.merge(warnings)
2086 };
2087
2088 let mut profiles = vec![Self::DEFAULT_PROFILE];
2090 if profile != Self::DEFAULT_PROFILE {
2091 profiles.push(profile.clone());
2092 }
2093 let provider = toml_provider.strict_select(profiles);
2094
2095 let provider = &BackwardsCompatTomlProvider(ForcedSnakeCaseData(provider));
2097
2098 if profile != Self::DEFAULT_PROFILE {
2100 figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
2101 }
2102 for standalone_key in Self::STANDALONE_SECTIONS {
2104 if let Some((_, fallback)) =
2105 STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
2106 {
2107 figment = figment.merge(
2108 provider
2109 .fallback(standalone_key, fallback)
2110 .wrap(profile.clone(), standalone_key),
2111 );
2112 } else {
2113 figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
2114 }
2115 }
2116 figment = figment.merge(provider);
2118 figment
2119 }
2120
2121 fn normalize_defaults(&self, mut figment: Figment) -> Figment {
2127 if figment.contains("evm_version") {
2129 return figment;
2130 }
2131
2132 if let Ok(solc) = figment.extract_inner::<SolcReq>("solc")
2134 && let Some(version) = solc
2135 .try_version()
2136 .ok()
2137 .and_then(|version| self.evm_version.normalize_version_solc(&version))
2138 {
2139 figment = figment.merge(("evm_version", version));
2140 }
2141
2142 figment
2143 }
2144}
2145
2146impl From<Config> for Figment {
2147 fn from(c: Config) -> Self {
2148 (&c).into()
2149 }
2150}
2151impl From<&Config> for Figment {
2152 fn from(c: &Config) -> Self {
2153 c.to_figment(FigmentProviders::All)
2154 }
2155}
2156
2157#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2159pub enum FigmentProviders {
2160 #[default]
2162 All,
2163 Cast,
2167 Anvil,
2171 None,
2173}
2174
2175impl FigmentProviders {
2176 pub const fn is_all(&self) -> bool {
2178 matches!(self, Self::All)
2179 }
2180
2181 pub const fn is_cast(&self) -> bool {
2183 matches!(self, Self::Cast)
2184 }
2185
2186 pub const fn is_anvil(&self) -> bool {
2188 matches!(self, Self::Anvil)
2189 }
2190
2191 pub const fn is_none(&self) -> bool {
2193 matches!(self, Self::None)
2194 }
2195}
2196
2197#[derive(Clone, Debug, Serialize, Deserialize)]
2199#[serde(transparent)]
2200pub struct RegexWrapper {
2201 #[serde(with = "serde_regex")]
2202 inner: regex::Regex,
2203}
2204
2205impl std::ops::Deref for RegexWrapper {
2206 type Target = regex::Regex;
2207
2208 fn deref(&self) -> &Self::Target {
2209 &self.inner
2210 }
2211}
2212
2213impl std::cmp::PartialEq for RegexWrapper {
2214 fn eq(&self, other: &Self) -> bool {
2215 self.as_str() == other.as_str()
2216 }
2217}
2218
2219impl Eq for RegexWrapper {}
2220
2221impl From<RegexWrapper> for regex::Regex {
2222 fn from(wrapper: RegexWrapper) -> Self {
2223 wrapper.inner
2224 }
2225}
2226
2227impl From<regex::Regex> for RegexWrapper {
2228 fn from(re: Regex) -> Self {
2229 Self { inner: re }
2230 }
2231}
2232
2233mod serde_regex {
2234 use regex::Regex;
2235 use serde::{Deserialize, Deserializer, Serializer};
2236
2237 pub(crate) fn serialize<S>(value: &Regex, serializer: S) -> Result<S::Ok, S::Error>
2238 where
2239 S: Serializer,
2240 {
2241 serializer.serialize_str(value.as_str())
2242 }
2243
2244 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
2245 where
2246 D: Deserializer<'de>,
2247 {
2248 let s = String::deserialize(deserializer)?;
2249 Regex::new(&s).map_err(serde::de::Error::custom)
2250 }
2251}
2252
2253pub(crate) mod from_opt_glob {
2255 use serde::{Deserialize, Deserializer, Serializer};
2256
2257 pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
2258 where
2259 S: Serializer,
2260 {
2261 match value {
2262 Some(glob) => serializer.serialize_str(glob.glob()),
2263 None => serializer.serialize_none(),
2264 }
2265 }
2266
2267 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
2268 where
2269 D: Deserializer<'de>,
2270 {
2271 let s: Option<String> = Option::deserialize(deserializer)?;
2272 if let Some(s) = s {
2273 return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
2274 }
2275 Ok(None)
2276 }
2277}
2278
2279pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
2290 s: &str,
2291) -> Result<Option<(Profile, T)>, Error> {
2292 let figment = Config::merge_toml_provider(
2293 Figment::new(),
2294 Toml::string(s).nested(),
2295 Config::DEFAULT_PROFILE,
2296 );
2297 if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
2298 Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
2299 } else {
2300 Ok(None)
2301 }
2302}
2303
2304impl Provider for Config {
2305 fn metadata(&self) -> Metadata {
2306 Metadata::named("Foundry Config")
2307 }
2308
2309 #[track_caller]
2310 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2311 let mut data = Serialized::defaults(self).data()?;
2312 if let Some(entry) = data.get_mut(&self.profile) {
2313 entry.insert("root".to_string(), Value::serialize(self.root.clone())?);
2314 }
2315 Ok(data)
2316 }
2317
2318 fn profile(&self) -> Option<Profile> {
2319 Some(self.profile.clone())
2320 }
2321}
2322
2323impl Default for Config {
2324 fn default() -> Self {
2325 Self {
2326 profile: Self::DEFAULT_PROFILE,
2327 profiles: vec![Self::DEFAULT_PROFILE],
2328 fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2329 isolate: cfg!(feature = "isolate-by-default"),
2330 root: root_default(),
2331 src: "src".into(),
2332 test: "test".into(),
2333 script: "script".into(),
2334 out: "out".into(),
2335 libs: vec!["lib".into()],
2336 cache: true,
2337 dynamic_test_linking: false,
2338 cache_path: "cache".into(),
2339 broadcast: "broadcast".into(),
2340 snapshots: "snapshots".into(),
2341 gas_snapshot_check: false,
2342 gas_snapshot_emit: true,
2343 allow_paths: vec![],
2344 include_paths: vec![],
2345 force: false,
2346 evm_version: EvmVersion::Prague,
2347 gas_reports: vec!["*".to_string()],
2348 gas_reports_ignore: vec![],
2349 gas_reports_include_tests: false,
2350 solc: None,
2351 vyper: Default::default(),
2352 auto_detect_solc: true,
2353 offline: false,
2354 optimizer: None,
2355 optimizer_runs: None,
2356 optimizer_details: None,
2357 model_checker: None,
2358 extra_output: Default::default(),
2359 extra_output_files: Default::default(),
2360 names: false,
2361 sizes: false,
2362 test_pattern: None,
2363 test_pattern_inverse: None,
2364 contract_pattern: None,
2365 contract_pattern_inverse: None,
2366 path_pattern: None,
2367 path_pattern_inverse: None,
2368 coverage_pattern_inverse: None,
2369 test_failures_file: "cache/test-failures".into(),
2370 threads: None,
2371 show_progress: false,
2372 fuzz: FuzzConfig::new("cache/fuzz".into()),
2373 invariant: InvariantConfig::new("cache/invariant".into()),
2374 always_use_create_2_factory: false,
2375 ffi: false,
2376 allow_internal_expect_revert: false,
2377 prompt_timeout: 120,
2378 sender: Self::DEFAULT_SENDER,
2379 tx_origin: Self::DEFAULT_SENDER,
2380 initial_balance: U256::from((1u128 << 96) - 1),
2381 block_number: U256::from(1),
2382 fork_block_number: None,
2383 chain: None,
2384 gas_limit: (1u64 << 30).into(), code_size_limit: None,
2386 gas_price: None,
2387 block_base_fee_per_gas: 0,
2388 block_coinbase: Address::ZERO,
2389 block_timestamp: U256::from(1),
2390 block_difficulty: 0,
2391 block_prevrandao: Default::default(),
2392 block_gas_limit: None,
2393 disable_block_gas_limit: false,
2394 memory_limit: 1 << 27, eth_rpc_url: None,
2396 eth_rpc_accept_invalid_certs: false,
2397 eth_rpc_jwt: None,
2398 eth_rpc_timeout: None,
2399 eth_rpc_headers: None,
2400 etherscan_api_key: None,
2401 etherscan_api_version: None,
2402 verbosity: 0,
2403 remappings: vec![],
2404 auto_detect_remappings: true,
2405 libraries: vec![],
2406 ignored_error_codes: vec![
2407 SolidityErrorCode::SpdxLicenseNotProvided,
2408 SolidityErrorCode::ContractExceeds24576Bytes,
2409 SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2410 SolidityErrorCode::TransientStorageUsed,
2411 ],
2412 ignored_file_paths: vec![],
2413 deny_warnings: false,
2414 via_ir: false,
2415 ast: false,
2416 rpc_storage_caching: Default::default(),
2417 rpc_endpoints: Default::default(),
2418 etherscan: Default::default(),
2419 no_storage_caching: false,
2420 no_rpc_rate_limit: false,
2421 use_literal_content: false,
2422 bytecode_hash: BytecodeHash::Ipfs,
2423 cbor_metadata: true,
2424 revert_strings: None,
2425 sparse_mode: false,
2426 build_info: false,
2427 build_info_path: None,
2428 fmt: Default::default(),
2429 lint: Default::default(),
2430 doc: Default::default(),
2431 bind_json: Default::default(),
2432 labels: Default::default(),
2433 unchecked_cheatcode_artifacts: false,
2434 create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
2435 create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
2436 skip: vec![],
2437 dependencies: Default::default(),
2438 soldeer: Default::default(),
2439 assertions_revert: true,
2440 legacy_assertions: false,
2441 warnings: vec![],
2442 extra_args: vec![],
2443 odyssey: false,
2444 transaction_timeout: 120,
2445 additional_compiler_profiles: Default::default(),
2446 compilation_restrictions: Default::default(),
2447 script_execution_protection: true,
2448 _non_exhaustive: (),
2449 }
2450 }
2451}
2452
2453#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
2459pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);
2460
2461impl From<u64> for GasLimit {
2462 fn from(gas: u64) -> Self {
2463 Self(gas)
2464 }
2465}
2466
2467impl From<GasLimit> for u64 {
2468 fn from(gas: GasLimit) -> Self {
2469 gas.0
2470 }
2471}
2472
2473impl Serialize for GasLimit {
2474 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2475 where
2476 S: Serializer,
2477 {
2478 if self.0 == u64::MAX {
2479 serializer.serialize_str("max")
2480 } else if self.0 > i64::MAX as u64 {
2481 serializer.serialize_str(&self.0.to_string())
2482 } else {
2483 serializer.serialize_u64(self.0)
2484 }
2485 }
2486}
2487
2488#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2490#[serde(untagged)]
2491pub enum SolcReq {
2492 Version(Version),
2495 Local(PathBuf),
2497}
2498
2499impl SolcReq {
2500 fn try_version(&self) -> Result<Version, SolcError> {
2505 match self {
2506 Self::Version(version) => Ok(version.clone()),
2507 Self::Local(path) => Solc::new(path).map(|solc| solc.version),
2508 }
2509 }
2510}
2511
2512impl<T: AsRef<str>> From<T> for SolcReq {
2513 fn from(s: T) -> Self {
2514 let s = s.as_ref();
2515 if let Ok(v) = Version::from_str(s) { Self::Version(v) } else { Self::Local(s.into()) }
2516 }
2517}
2518
2519#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2531pub struct BasicConfig {
2532 #[serde(skip)]
2534 pub profile: Profile,
2535 pub src: PathBuf,
2537 pub out: PathBuf,
2539 pub libs: Vec<PathBuf>,
2541 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2543 pub remappings: Vec<RelativeRemapping>,
2544}
2545
2546impl BasicConfig {
2547 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2551 let s = toml::to_string_pretty(self)?;
2552 Ok(format!(
2553 "\
2554[profile.{}]
2555{s}
2556# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n",
2557 self.profile
2558 ))
2559 }
2560}
2561
2562pub(crate) mod from_str_lowercase {
2563 use serde::{Deserialize, Deserializer, Serializer};
2564 use std::str::FromStr;
2565
2566 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
2567 where
2568 T: std::fmt::Display,
2569 S: Serializer,
2570 {
2571 serializer.collect_str(&value.to_string().to_lowercase())
2572 }
2573
2574 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
2575 where
2576 D: Deserializer<'de>,
2577 T: FromStr,
2578 T::Err: std::fmt::Display,
2579 {
2580 String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
2581 }
2582}
2583
2584fn canonic(path: impl Into<PathBuf>) -> PathBuf {
2585 let path = path.into();
2586 foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
2587}
2588
2589fn root_default() -> PathBuf {
2590 ".".into()
2591}
2592
2593#[cfg(test)]
2594mod tests {
2595 use super::*;
2596 use crate::{
2597 cache::{CachedChains, CachedEndpoints},
2598 endpoints::RpcEndpointType,
2599 etherscan::ResolvedEtherscanConfigs,
2600 fmt::IndentStyle,
2601 };
2602 use NamedChain::Moonbeam;
2603 use endpoints::{RpcAuth, RpcEndpointConfig};
2604 use figment::error::Kind::InvalidType;
2605 use foundry_compilers::artifacts::{
2606 ModelCheckerEngine, YulDetails, vyper::VyperOptimizationMode,
2607 };
2608 use similar_asserts::assert_eq;
2609 use soldeer_core::remappings::RemappingsLocation;
2610 use std::{fs::File, io::Write};
2611 use tempfile::tempdir;
2612
2613 fn clear_warning(config: &mut Config) {
2616 config.warnings = vec![];
2617 }
2618
2619 #[test]
2620 fn default_sender() {
2621 assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
2622 }
2623
2624 #[test]
2625 fn test_caching() {
2626 let mut config = Config::default();
2627 let chain_id = NamedChain::Mainnet;
2628 let url = "https://eth-mainnet.alchemyapi";
2629 assert!(config.enable_caching(url, chain_id));
2630
2631 config.no_storage_caching = true;
2632 assert!(!config.enable_caching(url, chain_id));
2633
2634 config.no_storage_caching = false;
2635 assert!(!config.enable_caching(url, NamedChain::Dev));
2636 }
2637
2638 #[test]
2639 fn test_install_dir() {
2640 figment::Jail::expect_with(|jail| {
2641 let config = Config::load().unwrap();
2642 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2643 jail.create_file(
2644 "foundry.toml",
2645 r"
2646 [profile.default]
2647 libs = ['node_modules', 'lib']
2648 ",
2649 )?;
2650 let config = Config::load().unwrap();
2651 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2652
2653 jail.create_file(
2654 "foundry.toml",
2655 r"
2656 [profile.default]
2657 libs = ['custom', 'node_modules', 'lib']
2658 ",
2659 )?;
2660 let config = Config::load().unwrap();
2661 assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
2662
2663 Ok(())
2664 });
2665 }
2666
2667 #[test]
2668 fn test_figment_is_default() {
2669 figment::Jail::expect_with(|_| {
2670 let mut default: Config = Config::figment().extract()?;
2671 let default2 = Config::default();
2672 default.profile = default2.profile.clone();
2673 default.profiles = default2.profiles.clone();
2674 assert_eq!(default, default2);
2675 Ok(())
2676 });
2677 }
2678
2679 #[test]
2680 fn figment_profiles() {
2681 figment::Jail::expect_with(|jail| {
2682 jail.create_file(
2683 "foundry.toml",
2684 r"
2685 [foo.baz]
2686 libs = ['node_modules', 'lib']
2687
2688 [profile.default]
2689 libs = ['node_modules', 'lib']
2690
2691 [profile.ci]
2692 libs = ['node_modules', 'lib']
2693
2694 [profile.local]
2695 libs = ['node_modules', 'lib']
2696 ",
2697 )?;
2698
2699 let config = crate::Config::load().unwrap();
2700 let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
2701 assert_eq!(config.profiles, expected);
2702
2703 Ok(())
2704 });
2705 }
2706
2707 #[test]
2708 fn test_default_round_trip() {
2709 figment::Jail::expect_with(|_| {
2710 let original = Config::figment();
2711 let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
2712 for figment in &[original, roundtrip] {
2713 let config = Config::from_provider(figment).unwrap();
2714 assert_eq!(config, Config::default().normalized_optimizer_settings());
2715 }
2716 Ok(())
2717 });
2718 }
2719
2720 #[test]
2721 fn ffi_env_disallowed() {
2722 figment::Jail::expect_with(|jail| {
2723 jail.set_env("FOUNDRY_FFI", "true");
2724 jail.set_env("FFI", "true");
2725 jail.set_env("DAPP_FFI", "true");
2726 let config = Config::load().unwrap();
2727 assert!(!config.ffi);
2728
2729 Ok(())
2730 });
2731 }
2732
2733 #[test]
2734 fn test_profile_env() {
2735 figment::Jail::expect_with(|jail| {
2736 jail.set_env("FOUNDRY_PROFILE", "default");
2737 let figment = Config::figment();
2738 assert_eq!(figment.profile(), "default");
2739
2740 jail.set_env("FOUNDRY_PROFILE", "hardhat");
2741 let figment: Figment = Config::hardhat().into();
2742 assert_eq!(figment.profile(), "hardhat");
2743
2744 jail.create_file(
2745 "foundry.toml",
2746 r"
2747 [profile.default]
2748 libs = ['lib']
2749 [profile.local]
2750 libs = ['modules']
2751 ",
2752 )?;
2753 jail.set_env("FOUNDRY_PROFILE", "local");
2754 let config = Config::load().unwrap();
2755 assert_eq!(config.libs, vec![PathBuf::from("modules")]);
2756
2757 Ok(())
2758 });
2759 }
2760
2761 #[test]
2762 fn test_default_test_path() {
2763 figment::Jail::expect_with(|_| {
2764 let config = Config::default();
2765 let paths_config = config.project_paths::<Solc>();
2766 assert_eq!(paths_config.tests, PathBuf::from(r"test"));
2767 Ok(())
2768 });
2769 }
2770
2771 #[test]
2772 fn test_default_libs() {
2773 figment::Jail::expect_with(|jail| {
2774 let config = Config::load().unwrap();
2775 assert_eq!(config.libs, vec![PathBuf::from("lib")]);
2776
2777 fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
2778 let config = Config::load().unwrap();
2779 assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
2780
2781 fs::create_dir_all(jail.directory().join("lib")).unwrap();
2782 let config = Config::load().unwrap();
2783 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2784
2785 Ok(())
2786 });
2787 }
2788
2789 #[test]
2790 fn test_inheritance_from_default_test_path() {
2791 figment::Jail::expect_with(|jail| {
2792 jail.create_file(
2793 "foundry.toml",
2794 r#"
2795 [profile.default]
2796 test = "defaulttest"
2797 src = "defaultsrc"
2798 libs = ['lib', 'node_modules']
2799
2800 [profile.custom]
2801 src = "customsrc"
2802 "#,
2803 )?;
2804
2805 let config = Config::load().unwrap();
2806 assert_eq!(config.src, PathBuf::from("defaultsrc"));
2807 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2808
2809 jail.set_env("FOUNDRY_PROFILE", "custom");
2810 let config = Config::load().unwrap();
2811 assert_eq!(config.src, PathBuf::from("customsrc"));
2812 assert_eq!(config.test, PathBuf::from("defaulttest"));
2813 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2814
2815 Ok(())
2816 });
2817 }
2818
2819 #[test]
2820 fn test_custom_test_path() {
2821 figment::Jail::expect_with(|jail| {
2822 jail.create_file(
2823 "foundry.toml",
2824 r#"
2825 [profile.default]
2826 test = "mytest"
2827 "#,
2828 )?;
2829
2830 let config = Config::load().unwrap();
2831 let paths_config = config.project_paths::<Solc>();
2832 assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
2833 Ok(())
2834 });
2835 }
2836
2837 #[test]
2838 fn test_remappings() {
2839 figment::Jail::expect_with(|jail| {
2840 jail.create_file(
2841 "foundry.toml",
2842 r#"
2843 [profile.default]
2844 src = "some-source"
2845 out = "some-out"
2846 cache = true
2847 "#,
2848 )?;
2849 let config = Config::load().unwrap();
2850 assert!(config.remappings.is_empty());
2851
2852 jail.create_file(
2853 "remappings.txt",
2854 r"
2855 file-ds-test/=lib/ds-test/
2856 file-other/=lib/other/
2857 ",
2858 )?;
2859
2860 let config = Config::load().unwrap();
2861 assert_eq!(
2862 config.remappings,
2863 vec![
2864 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
2865 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
2866 ],
2867 );
2868
2869 jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
2870 let config = Config::load().unwrap();
2871
2872 assert_eq!(
2873 config.remappings,
2874 vec![
2875 Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
2877 Remapping::from_str("other/=lib/other/").unwrap().into(),
2878 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
2880 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
2881 ],
2882 );
2883
2884 Ok(())
2885 });
2886 }
2887
2888 #[test]
2889 fn test_remappings_override() {
2890 figment::Jail::expect_with(|jail| {
2891 jail.create_file(
2892 "foundry.toml",
2893 r#"
2894 [profile.default]
2895 src = "some-source"
2896 out = "some-out"
2897 cache = true
2898 "#,
2899 )?;
2900 let config = Config::load().unwrap();
2901 assert!(config.remappings.is_empty());
2902
2903 jail.create_file(
2904 "remappings.txt",
2905 r"
2906 ds-test/=lib/ds-test/
2907 other/=lib/other/
2908 ",
2909 )?;
2910
2911 let config = Config::load().unwrap();
2912 assert_eq!(
2913 config.remappings,
2914 vec![
2915 Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
2916 Remapping::from_str("other/=lib/other/").unwrap().into(),
2917 ],
2918 );
2919
2920 jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
2921 let config = Config::load().unwrap();
2922
2923 assert_eq!(
2928 config.remappings,
2929 vec![
2930 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
2931 Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
2932 Remapping::from_str("other/=lib/other/").unwrap().into(),
2933 ],
2934 );
2935
2936 assert_eq!(
2938 config.get_all_remappings().collect::<Vec<_>>(),
2939 vec![
2940 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
2941 Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
2942 Remapping::from_str("other/=lib/other/").unwrap(),
2943 ],
2944 );
2945
2946 Ok(())
2947 });
2948 }
2949
2950 #[test]
2951 fn test_can_update_libs() {
2952 figment::Jail::expect_with(|jail| {
2953 jail.create_file(
2954 "foundry.toml",
2955 r#"
2956 [profile.default]
2957 libs = ["node_modules"]
2958 "#,
2959 )?;
2960
2961 let mut config = Config::load().unwrap();
2962 config.libs.push("libs".into());
2963 config.update_libs().unwrap();
2964
2965 let config = Config::load().unwrap();
2966 assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
2967 Ok(())
2968 });
2969 }
2970
2971 #[test]
2972 fn test_large_gas_limit() {
2973 figment::Jail::expect_with(|jail| {
2974 let gas = u64::MAX;
2975 jail.create_file(
2976 "foundry.toml",
2977 &format!(
2978 r#"
2979 [profile.default]
2980 gas_limit = "{gas}"
2981 "#
2982 ),
2983 )?;
2984
2985 let config = Config::load().unwrap();
2986 assert_eq!(
2987 config,
2988 Config {
2989 gas_limit: gas.into(),
2990 ..Config::default().normalized_optimizer_settings()
2991 }
2992 );
2993
2994 Ok(())
2995 });
2996 }
2997
2998 #[test]
2999 #[should_panic]
3000 fn test_toml_file_parse_failure() {
3001 figment::Jail::expect_with(|jail| {
3002 jail.create_file(
3003 "foundry.toml",
3004 r#"
3005 [profile.default]
3006 eth_rpc_url = "https://example.com/
3007 "#,
3008 )?;
3009
3010 let _config = Config::load().unwrap();
3011
3012 Ok(())
3013 });
3014 }
3015
3016 #[test]
3017 #[should_panic]
3018 fn test_toml_file_non_existing_config_var_failure() {
3019 figment::Jail::expect_with(|jail| {
3020 jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3021
3022 let _config = Config::load().unwrap();
3023
3024 Ok(())
3025 });
3026 }
3027
3028 #[test]
3029 fn test_resolve_etherscan_with_chain() {
3030 figment::Jail::expect_with(|jail| {
3031 let env_key = "__BSC_ETHERSCAN_API_KEY";
3032 let env_value = "env value";
3033 jail.create_file(
3034 "foundry.toml",
3035 r#"
3036 [profile.default]
3037
3038 [etherscan]
3039 bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3040 "#,
3041 )?;
3042
3043 let config = Config::load().unwrap();
3044 assert!(
3045 config
3046 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3047 .is_err()
3048 );
3049
3050 unsafe {
3051 std::env::set_var(env_key, env_value);
3052 }
3053
3054 assert_eq!(
3055 config
3056 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3057 .unwrap()
3058 .unwrap()
3059 .key,
3060 env_value
3061 );
3062
3063 let mut with_key = config;
3064 with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3065
3066 assert_eq!(
3067 with_key
3068 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3069 .unwrap()
3070 .unwrap()
3071 .key,
3072 "via etherscan_api_key"
3073 );
3074
3075 unsafe {
3076 std::env::remove_var(env_key);
3077 }
3078 Ok(())
3079 });
3080 }
3081
3082 #[test]
3083 fn test_resolve_etherscan() {
3084 figment::Jail::expect_with(|jail| {
3085 jail.create_file(
3086 "foundry.toml",
3087 r#"
3088 [profile.default]
3089
3090 [etherscan]
3091 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3092 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3093 "#,
3094 )?;
3095
3096 let config = Config::load().unwrap();
3097
3098 assert!(config.etherscan.clone().resolved(EtherscanApiVersion::V2).has_unresolved());
3099
3100 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3101
3102 let configs = config.etherscan.resolved(EtherscanApiVersion::V2);
3103 assert!(!configs.has_unresolved());
3104
3105 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3106 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3107 assert_eq!(
3108 configs,
3109 ResolvedEtherscanConfigs::new([
3110 (
3111 "mainnet",
3112 ResolvedEtherscanConfig {
3113 api_url: mainnet_urls.0.to_string(),
3114 chain: Some(NamedChain::Mainnet.into()),
3115 browser_url: Some(mainnet_urls.1.to_string()),
3116 api_version: EtherscanApiVersion::V2,
3117 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3118 }
3119 ),
3120 (
3121 "moonbeam",
3122 ResolvedEtherscanConfig {
3123 api_url: mb_urls.0.to_string(),
3124 chain: Some(Moonbeam.into()),
3125 browser_url: Some(mb_urls.1.to_string()),
3126 api_version: EtherscanApiVersion::V2,
3127 key: "123456789".to_string(),
3128 }
3129 ),
3130 ])
3131 );
3132
3133 Ok(())
3134 });
3135 }
3136
3137 #[test]
3138 fn test_resolve_etherscan_with_versions() {
3139 figment::Jail::expect_with(|jail| {
3140 jail.create_file(
3141 "foundry.toml",
3142 r#"
3143 [profile.default]
3144
3145 [etherscan]
3146 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
3147 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
3148 "#,
3149 )?;
3150
3151 let config = Config::load().unwrap();
3152
3153 assert!(config.etherscan.clone().resolved(EtherscanApiVersion::V2).has_unresolved());
3154
3155 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3156
3157 let configs = config.etherscan.resolved(EtherscanApiVersion::V2);
3158 assert!(!configs.has_unresolved());
3159
3160 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3161 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3162 assert_eq!(
3163 configs,
3164 ResolvedEtherscanConfigs::new([
3165 (
3166 "mainnet",
3167 ResolvedEtherscanConfig {
3168 api_url: mainnet_urls.0.to_string(),
3169 chain: Some(NamedChain::Mainnet.into()),
3170 browser_url: Some(mainnet_urls.1.to_string()),
3171 api_version: EtherscanApiVersion::V2,
3172 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3173 }
3174 ),
3175 (
3176 "moonbeam",
3177 ResolvedEtherscanConfig {
3178 api_url: mb_urls.0.to_string(),
3179 chain: Some(Moonbeam.into()),
3180 browser_url: Some(mb_urls.1.to_string()),
3181 api_version: EtherscanApiVersion::V1,
3182 key: "123456789".to_string(),
3183 }
3184 ),
3185 ])
3186 );
3187
3188 Ok(())
3189 });
3190 }
3191
3192 #[test]
3193 fn test_resolve_etherscan_chain_id() {
3194 figment::Jail::expect_with(|jail| {
3195 jail.create_file(
3196 "foundry.toml",
3197 r#"
3198 [profile.default]
3199 chain_id = "sepolia"
3200
3201 [etherscan]
3202 sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3203 "#,
3204 )?;
3205
3206 let config = Config::load().unwrap();
3207 let etherscan = config.get_etherscan_config().unwrap().unwrap();
3208 assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3209 assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3210
3211 Ok(())
3212 });
3213 }
3214
3215 #[test]
3216 fn test_resolve_rpc_url() {
3217 figment::Jail::expect_with(|jail| {
3218 jail.create_file(
3219 "foundry.toml",
3220 r#"
3221 [profile.default]
3222 [rpc_endpoints]
3223 optimism = "https://example.com/"
3224 mainnet = "${_CONFIG_MAINNET}"
3225 "#,
3226 )?;
3227 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3228
3229 let mut config = Config::load().unwrap();
3230 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3231
3232 config.eth_rpc_url = Some("mainnet".to_string());
3233 assert_eq!(
3234 "https://eth-mainnet.alchemyapi.io/v2/123455",
3235 config.get_rpc_url_or_localhost_http().unwrap()
3236 );
3237
3238 config.eth_rpc_url = Some("optimism".to_string());
3239 assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
3240
3241 Ok(())
3242 })
3243 }
3244
3245 #[test]
3246 fn test_resolve_rpc_url_if_etherscan_set() {
3247 figment::Jail::expect_with(|jail| {
3248 jail.create_file(
3249 "foundry.toml",
3250 r#"
3251 [profile.default]
3252 etherscan_api_key = "dummy"
3253 [rpc_endpoints]
3254 optimism = "https://example.com/"
3255 "#,
3256 )?;
3257
3258 let config = Config::load().unwrap();
3259 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3260
3261 Ok(())
3262 })
3263 }
3264
3265 #[test]
3266 fn test_resolve_rpc_url_alias() {
3267 figment::Jail::expect_with(|jail| {
3268 jail.create_file(
3269 "foundry.toml",
3270 r#"
3271 [profile.default]
3272 [rpc_endpoints]
3273 polygonMumbai = "https://polygon-mumbai.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
3274 "#,
3275 )?;
3276 let mut config = Config::load().unwrap();
3277 config.eth_rpc_url = Some("polygonMumbai".to_string());
3278 assert!(config.get_rpc_url().unwrap().is_err());
3279
3280 jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
3281
3282 let mut config = Config::load().unwrap();
3283 config.eth_rpc_url = Some("polygonMumbai".to_string());
3284 assert_eq!(
3285 "https://polygon-mumbai.g.alchemy.com/v2/123455",
3286 config.get_rpc_url().unwrap().unwrap()
3287 );
3288
3289 Ok(())
3290 })
3291 }
3292
3293 #[test]
3294 fn test_resolve_rpc_aliases() {
3295 figment::Jail::expect_with(|jail| {
3296 jail.create_file(
3297 "foundry.toml",
3298 r#"
3299 [profile.default]
3300 [etherscan]
3301 arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
3302 [rpc_endpoints]
3303 arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
3304 "#,
3305 )?;
3306
3307 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
3308 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
3309
3310 let config = Config::load().unwrap();
3311
3312 let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
3313 assert!(config.is_err());
3314 assert_eq!(
3315 config.unwrap_err().to_string(),
3316 "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`"
3317 );
3318
3319 Ok(())
3320 });
3321 }
3322
3323 #[test]
3324 fn test_resolve_rpc_config() {
3325 figment::Jail::expect_with(|jail| {
3326 jail.create_file(
3327 "foundry.toml",
3328 r#"
3329 [rpc_endpoints]
3330 optimism = "https://example.com/"
3331 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
3332 "#,
3333 )?;
3334 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3335
3336 let config = Config::load().unwrap();
3337 assert_eq!(
3338 RpcEndpoints::new([
3339 (
3340 "optimism",
3341 RpcEndpointType::String(RpcEndpointUrl::Url(
3342 "https://example.com/".to_string()
3343 ))
3344 ),
3345 (
3346 "mainnet",
3347 RpcEndpointType::Config(RpcEndpoint {
3348 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3349 config: RpcEndpointConfig {
3350 retries: Some(3),
3351 retry_backoff: Some(1000),
3352 compute_units_per_second: Some(1000),
3353 },
3354 auth: None,
3355 })
3356 ),
3357 ]),
3358 config.rpc_endpoints
3359 );
3360
3361 let resolved = config.rpc_endpoints.resolved();
3362 assert_eq!(
3363 RpcEndpoints::new([
3364 (
3365 "optimism",
3366 RpcEndpointType::String(RpcEndpointUrl::Url(
3367 "https://example.com/".to_string()
3368 ))
3369 ),
3370 (
3371 "mainnet",
3372 RpcEndpointType::Config(RpcEndpoint {
3373 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3374 config: RpcEndpointConfig {
3375 retries: Some(3),
3376 retry_backoff: Some(1000),
3377 compute_units_per_second: Some(1000),
3378 },
3379 auth: None,
3380 })
3381 ),
3382 ])
3383 .resolved(),
3384 resolved
3385 );
3386 Ok(())
3387 })
3388 }
3389
3390 #[test]
3391 fn test_resolve_auth() {
3392 figment::Jail::expect_with(|jail| {
3393 jail.create_file(
3394 "foundry.toml",
3395 r#"
3396 [profile.default]
3397 eth_rpc_url = "optimism"
3398 [rpc_endpoints]
3399 optimism = "https://example.com/"
3400 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
3401 "#,
3402 )?;
3403
3404 let config = Config::load().unwrap();
3405
3406 jail.set_env("_CONFIG_AUTH", "123456");
3407 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3408
3409 assert_eq!(
3410 RpcEndpoints::new([
3411 (
3412 "optimism",
3413 RpcEndpointType::String(RpcEndpointUrl::Url(
3414 "https://example.com/".to_string()
3415 ))
3416 ),
3417 (
3418 "mainnet",
3419 RpcEndpointType::Config(RpcEndpoint {
3420 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3421 config: RpcEndpointConfig {
3422 retries: Some(3),
3423 retry_backoff: Some(1000),
3424 compute_units_per_second: Some(1000)
3425 },
3426 auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
3427 })
3428 ),
3429 ]),
3430 config.rpc_endpoints
3431 );
3432 let resolved = config.rpc_endpoints.resolved();
3433 assert_eq!(
3434 RpcEndpoints::new([
3435 (
3436 "optimism",
3437 RpcEndpointType::String(RpcEndpointUrl::Url(
3438 "https://example.com/".to_string()
3439 ))
3440 ),
3441 (
3442 "mainnet",
3443 RpcEndpointType::Config(RpcEndpoint {
3444 endpoint: RpcEndpointUrl::Url(
3445 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3446 ),
3447 config: RpcEndpointConfig {
3448 retries: Some(3),
3449 retry_backoff: Some(1000),
3450 compute_units_per_second: Some(1000)
3451 },
3452 auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
3453 })
3454 ),
3455 ])
3456 .resolved(),
3457 resolved
3458 );
3459
3460 Ok(())
3461 });
3462 }
3463
3464 #[test]
3465 fn test_resolve_endpoints() {
3466 figment::Jail::expect_with(|jail| {
3467 jail.create_file(
3468 "foundry.toml",
3469 r#"
3470 [profile.default]
3471 eth_rpc_url = "optimism"
3472 [rpc_endpoints]
3473 optimism = "https://example.com/"
3474 mainnet = "${_CONFIG_MAINNET}"
3475 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
3476 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
3477 "#,
3478 )?;
3479
3480 let config = Config::load().unwrap();
3481
3482 assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
3483
3484 assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
3485
3486 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3487 jail.set_env("_CONFIG_API_KEY1", "123456");
3488 jail.set_env("_CONFIG_API_KEY2", "98765");
3489
3490 let endpoints = config.rpc_endpoints.resolved();
3491
3492 assert!(!endpoints.has_unresolved());
3493
3494 assert_eq!(
3495 endpoints,
3496 RpcEndpoints::new([
3497 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3498 (
3499 "mainnet",
3500 RpcEndpointUrl::Url(
3501 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3502 )
3503 ),
3504 (
3505 "mainnet_2",
3506 RpcEndpointUrl::Url(
3507 "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
3508 )
3509 ),
3510 (
3511 "mainnet_3",
3512 RpcEndpointUrl::Url(
3513 "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
3514 )
3515 ),
3516 ])
3517 .resolved()
3518 );
3519
3520 Ok(())
3521 });
3522 }
3523
3524 #[test]
3525 fn test_extract_etherscan_config() {
3526 figment::Jail::expect_with(|jail| {
3527 jail.create_file(
3528 "foundry.toml",
3529 r#"
3530 [profile.default]
3531 etherscan_api_key = "optimism"
3532
3533 [etherscan]
3534 optimism = { key = "https://etherscan-optimism.com/" }
3535 mumbai = { key = "https://etherscan-mumbai.com/" }
3536 "#,
3537 )?;
3538
3539 let mut config = Config::load().unwrap();
3540
3541 let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
3542 assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
3543
3544 config.etherscan_api_key = Some("mumbai".to_string());
3545
3546 let mumbai = config.get_etherscan_api_key(Some(NamedChain::PolygonMumbai.into()));
3547 assert_eq!(mumbai, Some("https://etherscan-mumbai.com/".to_string()));
3548
3549 Ok(())
3550 });
3551 }
3552
3553 #[test]
3554 fn test_extract_etherscan_config_by_chain() {
3555 figment::Jail::expect_with(|jail| {
3556 jail.create_file(
3557 "foundry.toml",
3558 r#"
3559 [profile.default]
3560
3561 [etherscan]
3562 mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 }
3563 "#,
3564 )?;
3565
3566 let config = Config::load().unwrap();
3567
3568 let mumbai = config
3569 .get_etherscan_config_with_chain(Some(NamedChain::PolygonMumbai.into()))
3570 .unwrap()
3571 .unwrap();
3572 assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3573
3574 Ok(())
3575 });
3576 }
3577
3578 #[test]
3579 fn test_extract_etherscan_config_by_chain_with_url() {
3580 figment::Jail::expect_with(|jail| {
3581 jail.create_file(
3582 "foundry.toml",
3583 r#"
3584 [profile.default]
3585
3586 [etherscan]
3587 mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 , url = "https://verifier-url.com/"}
3588 "#,
3589 )?;
3590
3591 let config = Config::load().unwrap();
3592
3593 let mumbai = config
3594 .get_etherscan_config_with_chain(Some(NamedChain::PolygonMumbai.into()))
3595 .unwrap()
3596 .unwrap();
3597 assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3598 assert_eq!(mumbai.api_url, "https://verifier-url.com/".to_string());
3599
3600 Ok(())
3601 });
3602 }
3603
3604 #[test]
3605 fn test_extract_etherscan_config_by_chain_and_alias() {
3606 figment::Jail::expect_with(|jail| {
3607 jail.create_file(
3608 "foundry.toml",
3609 r#"
3610 [profile.default]
3611 eth_rpc_url = "mumbai"
3612
3613 [etherscan]
3614 mumbai = { key = "https://etherscan-mumbai.com/" }
3615
3616 [rpc_endpoints]
3617 mumbai = "https://polygon-mumbai.g.alchemy.com/v2/mumbai"
3618 "#,
3619 )?;
3620
3621 let config = Config::load().unwrap();
3622
3623 let mumbai = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
3624 assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3625
3626 let mumbai_rpc = config.get_rpc_url().unwrap().unwrap();
3627 assert_eq!(mumbai_rpc, "https://polygon-mumbai.g.alchemy.com/v2/mumbai");
3628 Ok(())
3629 });
3630 }
3631
3632 #[test]
3633 fn test_toml_file() {
3634 figment::Jail::expect_with(|jail| {
3635 jail.create_file(
3636 "foundry.toml",
3637 r#"
3638 [profile.default]
3639 src = "some-source"
3640 out = "some-out"
3641 cache = true
3642 eth_rpc_url = "https://example.com/"
3643 verbosity = 3
3644 remappings = ["ds-test=lib/ds-test/"]
3645 via_ir = true
3646 rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
3647 use_literal_content = false
3648 bytecode_hash = "ipfs"
3649 cbor_metadata = true
3650 revert_strings = "strip"
3651 allow_paths = ["allow", "paths"]
3652 build_info_path = "build-info"
3653 always_use_create_2_factory = true
3654
3655 [rpc_endpoints]
3656 optimism = "https://example.com/"
3657 mainnet = "${RPC_MAINNET}"
3658 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3659 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3660 "#,
3661 )?;
3662
3663 let config = Config::load().unwrap();
3664 assert_eq!(
3665 config,
3666 Config {
3667 src: "some-source".into(),
3668 out: "some-out".into(),
3669 cache: true,
3670 eth_rpc_url: Some("https://example.com/".to_string()),
3671 remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
3672 verbosity: 3,
3673 via_ir: true,
3674 rpc_storage_caching: StorageCachingConfig {
3675 chains: CachedChains::Chains(vec![
3676 Chain::mainnet(),
3677 Chain::optimism_mainnet(),
3678 Chain::from_id(999999)
3679 ]),
3680 endpoints: CachedEndpoints::All,
3681 },
3682 use_literal_content: false,
3683 bytecode_hash: BytecodeHash::Ipfs,
3684 cbor_metadata: true,
3685 revert_strings: Some(RevertStrings::Strip),
3686 allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
3687 rpc_endpoints: RpcEndpoints::new([
3688 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3689 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3690 (
3691 "mainnet_2",
3692 RpcEndpointUrl::Env(
3693 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3694 )
3695 ),
3696 (
3697 "mainnet_3",
3698 RpcEndpointUrl::Env(
3699 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3700 .to_string()
3701 )
3702 ),
3703 ]),
3704 build_info_path: Some("build-info".into()),
3705 always_use_create_2_factory: true,
3706 ..Config::default().normalized_optimizer_settings()
3707 }
3708 );
3709
3710 Ok(())
3711 });
3712 }
3713
3714 #[test]
3715 fn test_load_remappings() {
3716 figment::Jail::expect_with(|jail| {
3717 jail.create_file(
3718 "foundry.toml",
3719 r"
3720 [profile.default]
3721 remappings = ['nested/=lib/nested/']
3722 ",
3723 )?;
3724
3725 let config = Config::load_with_root(jail.directory()).unwrap();
3726 assert_eq!(
3727 config.remappings,
3728 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3729 );
3730
3731 Ok(())
3732 });
3733 }
3734
3735 #[test]
3736 fn test_load_full_toml() {
3737 figment::Jail::expect_with(|jail| {
3738 jail.create_file(
3739 "foundry.toml",
3740 r#"
3741 [profile.default]
3742 auto_detect_solc = true
3743 block_base_fee_per_gas = 0
3744 block_coinbase = '0x0000000000000000000000000000000000000000'
3745 block_difficulty = 0
3746 block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
3747 block_number = 1
3748 block_timestamp = 1
3749 use_literal_content = false
3750 bytecode_hash = 'ipfs'
3751 cbor_metadata = true
3752 cache = true
3753 cache_path = 'cache'
3754 evm_version = 'london'
3755 extra_output = []
3756 extra_output_files = []
3757 always_use_create_2_factory = false
3758 ffi = false
3759 force = false
3760 gas_limit = 9223372036854775807
3761 gas_price = 0
3762 gas_reports = ['*']
3763 ignored_error_codes = [1878]
3764 ignored_warnings_from = ["something"]
3765 deny_warnings = false
3766 initial_balance = '0xffffffffffffffffffffffff'
3767 libraries = []
3768 libs = ['lib']
3769 memory_limit = 134217728
3770 names = false
3771 no_storage_caching = false
3772 no_rpc_rate_limit = false
3773 offline = false
3774 optimizer = true
3775 optimizer_runs = 200
3776 out = 'out'
3777 remappings = ['nested/=lib/nested/']
3778 sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3779 sizes = false
3780 sparse_mode = false
3781 src = 'src'
3782 test = 'test'
3783 tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3784 verbosity = 0
3785 via_ir = false
3786
3787 [profile.default.rpc_storage_caching]
3788 chains = 'all'
3789 endpoints = 'all'
3790
3791 [rpc_endpoints]
3792 optimism = "https://example.com/"
3793 mainnet = "${RPC_MAINNET}"
3794 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3795
3796 [fuzz]
3797 runs = 256
3798 seed = '0x3e8'
3799 max_test_rejects = 65536
3800
3801 [invariant]
3802 runs = 256
3803 depth = 500
3804 fail_on_revert = false
3805 call_override = false
3806 shrink_run_limit = 5000
3807 "#,
3808 )?;
3809
3810 let config = Config::load_with_root(jail.directory()).unwrap();
3811
3812 assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
3813 assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
3814 assert_eq!(
3815 config.remappings,
3816 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3817 );
3818
3819 assert_eq!(
3820 config.rpc_endpoints,
3821 RpcEndpoints::new([
3822 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3823 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3824 (
3825 "mainnet_2",
3826 RpcEndpointUrl::Env(
3827 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3828 )
3829 ),
3830 ]),
3831 );
3832
3833 Ok(())
3834 });
3835 }
3836
3837 #[test]
3838 fn test_solc_req() {
3839 figment::Jail::expect_with(|jail| {
3840 jail.create_file(
3841 "foundry.toml",
3842 r#"
3843 [profile.default]
3844 solc_version = "0.8.12"
3845 "#,
3846 )?;
3847
3848 let config = Config::load().unwrap();
3849 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3850
3851 jail.create_file(
3852 "foundry.toml",
3853 r#"
3854 [profile.default]
3855 solc = "0.8.12"
3856 "#,
3857 )?;
3858
3859 let config = Config::load().unwrap();
3860 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3861
3862 jail.create_file(
3863 "foundry.toml",
3864 r#"
3865 [profile.default]
3866 solc = "path/to/local/solc"
3867 "#,
3868 )?;
3869
3870 let config = Config::load().unwrap();
3871 assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
3872
3873 jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
3874 let config = Config::load().unwrap();
3875 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
3876 Ok(())
3877 });
3878 }
3879
3880 #[test]
3882 fn test_backwards_solc_version() {
3883 figment::Jail::expect_with(|jail| {
3884 jail.create_file(
3885 "foundry.toml",
3886 r#"
3887 [default]
3888 solc = "0.8.12"
3889 solc_version = "0.8.20"
3890 "#,
3891 )?;
3892
3893 let config = Config::load().unwrap();
3894 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3895
3896 Ok(())
3897 });
3898
3899 figment::Jail::expect_with(|jail| {
3900 jail.create_file(
3901 "foundry.toml",
3902 r#"
3903 [default]
3904 solc_version = "0.8.20"
3905 "#,
3906 )?;
3907
3908 let config = Config::load().unwrap();
3909 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
3910
3911 Ok(())
3912 });
3913 }
3914
3915 #[test]
3916 fn test_toml_casing_file() {
3917 figment::Jail::expect_with(|jail| {
3918 jail.create_file(
3919 "foundry.toml",
3920 r#"
3921 [profile.default]
3922 src = "some-source"
3923 out = "some-out"
3924 cache = true
3925 eth-rpc-url = "https://example.com/"
3926 evm-version = "berlin"
3927 auto-detect-solc = false
3928 "#,
3929 )?;
3930
3931 let config = Config::load().unwrap();
3932 assert_eq!(
3933 config,
3934 Config {
3935 src: "some-source".into(),
3936 out: "some-out".into(),
3937 cache: true,
3938 eth_rpc_url: Some("https://example.com/".to_string()),
3939 auto_detect_solc: false,
3940 evm_version: EvmVersion::Berlin,
3941 ..Config::default().normalized_optimizer_settings()
3942 }
3943 );
3944
3945 Ok(())
3946 });
3947 }
3948
3949 #[test]
3950 fn test_output_selection() {
3951 figment::Jail::expect_with(|jail| {
3952 jail.create_file(
3953 "foundry.toml",
3954 r#"
3955 [profile.default]
3956 extra_output = ["metadata", "ir-optimized"]
3957 extra_output_files = ["metadata"]
3958 "#,
3959 )?;
3960
3961 let config = Config::load().unwrap();
3962
3963 assert_eq!(
3964 config.extra_output,
3965 vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
3966 );
3967 assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
3968
3969 Ok(())
3970 });
3971 }
3972
3973 #[test]
3974 fn test_precedence() {
3975 figment::Jail::expect_with(|jail| {
3976 jail.create_file(
3977 "foundry.toml",
3978 r#"
3979 [profile.default]
3980 src = "mysrc"
3981 out = "myout"
3982 verbosity = 3
3983 "#,
3984 )?;
3985
3986 let config = Config::load().unwrap();
3987 assert_eq!(
3988 config,
3989 Config {
3990 src: "mysrc".into(),
3991 out: "myout".into(),
3992 verbosity: 3,
3993 ..Config::default().normalized_optimizer_settings()
3994 }
3995 );
3996
3997 jail.set_env("FOUNDRY_SRC", r"other-src");
3998 let config = Config::load().unwrap();
3999 assert_eq!(
4000 config,
4001 Config {
4002 src: "other-src".into(),
4003 out: "myout".into(),
4004 verbosity: 3,
4005 ..Config::default().normalized_optimizer_settings()
4006 }
4007 );
4008
4009 jail.set_env("FOUNDRY_PROFILE", "foo");
4010 let val: Result<String, _> = Config::figment().extract_inner("profile");
4011 assert!(val.is_err());
4012
4013 Ok(())
4014 });
4015 }
4016
4017 #[test]
4018 fn test_extract_basic() {
4019 figment::Jail::expect_with(|jail| {
4020 jail.create_file(
4021 "foundry.toml",
4022 r#"
4023 [profile.default]
4024 src = "mysrc"
4025 out = "myout"
4026 verbosity = 3
4027 evm_version = 'berlin'
4028
4029 [profile.other]
4030 src = "other-src"
4031 "#,
4032 )?;
4033 let loaded = Config::load().unwrap();
4034 assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4035 let base = loaded.into_basic();
4036 let default = Config::default();
4037 assert_eq!(
4038 base,
4039 BasicConfig {
4040 profile: Config::DEFAULT_PROFILE,
4041 src: "mysrc".into(),
4042 out: "myout".into(),
4043 libs: default.libs.clone(),
4044 remappings: default.remappings.clone(),
4045 }
4046 );
4047 jail.set_env("FOUNDRY_PROFILE", r"other");
4048 let base = Config::figment().extract::<BasicConfig>().unwrap();
4049 assert_eq!(
4050 base,
4051 BasicConfig {
4052 profile: Config::DEFAULT_PROFILE,
4053 src: "other-src".into(),
4054 out: "myout".into(),
4055 libs: default.libs.clone(),
4056 remappings: default.remappings,
4057 }
4058 );
4059 Ok(())
4060 });
4061 }
4062
4063 #[test]
4064 #[should_panic]
4065 fn test_parse_invalid_fuzz_weight() {
4066 figment::Jail::expect_with(|jail| {
4067 jail.create_file(
4068 "foundry.toml",
4069 r"
4070 [fuzz]
4071 dictionary_weight = 101
4072 ",
4073 )?;
4074 let _config = Config::load().unwrap();
4075 Ok(())
4076 });
4077 }
4078
4079 #[test]
4080 fn test_fallback_provider() {
4081 figment::Jail::expect_with(|jail| {
4082 jail.create_file(
4083 "foundry.toml",
4084 r"
4085 [fuzz]
4086 runs = 1
4087 include_storage = false
4088 dictionary_weight = 99
4089
4090 [invariant]
4091 runs = 420
4092
4093 [profile.ci.fuzz]
4094 dictionary_weight = 5
4095
4096 [profile.ci.invariant]
4097 runs = 400
4098 ",
4099 )?;
4100
4101 let invariant_default = InvariantConfig::default();
4102 let config = Config::load().unwrap();
4103
4104 assert_ne!(config.invariant.runs, config.fuzz.runs);
4105 assert_eq!(config.invariant.runs, 420);
4106
4107 assert_ne!(
4108 config.fuzz.dictionary.include_storage,
4109 invariant_default.dictionary.include_storage
4110 );
4111 assert_eq!(
4112 config.invariant.dictionary.include_storage,
4113 config.fuzz.dictionary.include_storage
4114 );
4115
4116 assert_ne!(
4117 config.fuzz.dictionary.dictionary_weight,
4118 invariant_default.dictionary.dictionary_weight
4119 );
4120 assert_eq!(
4121 config.invariant.dictionary.dictionary_weight,
4122 config.fuzz.dictionary.dictionary_weight
4123 );
4124
4125 jail.set_env("FOUNDRY_PROFILE", "ci");
4126 let ci_config = Config::load().unwrap();
4127 assert_eq!(ci_config.fuzz.runs, 1);
4128 assert_eq!(ci_config.invariant.runs, 400);
4129 assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4130 assert_eq!(
4131 ci_config.invariant.dictionary.dictionary_weight,
4132 config.fuzz.dictionary.dictionary_weight
4133 );
4134
4135 Ok(())
4136 })
4137 }
4138
4139 #[test]
4140 fn test_standalone_profile_sections() {
4141 figment::Jail::expect_with(|jail| {
4142 jail.create_file(
4143 "foundry.toml",
4144 r"
4145 [fuzz]
4146 runs = 100
4147
4148 [invariant]
4149 runs = 120
4150
4151 [profile.ci.fuzz]
4152 runs = 420
4153
4154 [profile.ci.invariant]
4155 runs = 500
4156 ",
4157 )?;
4158
4159 let config = Config::load().unwrap();
4160 assert_eq!(config.fuzz.runs, 100);
4161 assert_eq!(config.invariant.runs, 120);
4162
4163 jail.set_env("FOUNDRY_PROFILE", "ci");
4164 let config = Config::load().unwrap();
4165 assert_eq!(config.fuzz.runs, 420);
4166 assert_eq!(config.invariant.runs, 500);
4167
4168 Ok(())
4169 });
4170 }
4171
4172 #[test]
4173 fn can_handle_deviating_dapp_aliases() {
4174 figment::Jail::expect_with(|jail| {
4175 let addr = Address::ZERO;
4176 jail.set_env("DAPP_TEST_NUMBER", 1337);
4177 jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4178 jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4179 jail.set_env("DAPP_TEST_DEPTH", 20);
4180 jail.set_env("DAPP_FORK_BLOCK", 100);
4181 jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4182 jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4183
4184 let config = Config::load().unwrap();
4185
4186 assert_eq!(config.block_number, U256::from(1337));
4187 assert_eq!(config.sender, addr);
4188 assert_eq!(config.fuzz.runs, 420);
4189 assert_eq!(config.invariant.depth, 20);
4190 assert_eq!(config.fork_block_number, Some(100));
4191 assert_eq!(config.optimizer_runs, Some(999));
4192 assert!(!config.optimizer.unwrap());
4193
4194 Ok(())
4195 });
4196 }
4197
4198 #[test]
4199 fn can_parse_libraries() {
4200 figment::Jail::expect_with(|jail| {
4201 jail.set_env(
4202 "DAPP_LIBRARIES",
4203 "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
4204 );
4205 let config = Config::load().unwrap();
4206 assert_eq!(
4207 config.libraries,
4208 vec![
4209 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4210 .to_string()
4211 ]
4212 );
4213
4214 jail.set_env(
4215 "DAPP_LIBRARIES",
4216 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4217 );
4218 let config = Config::load().unwrap();
4219 assert_eq!(
4220 config.libraries,
4221 vec![
4222 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4223 .to_string(),
4224 ]
4225 );
4226
4227 jail.set_env(
4228 "DAPP_LIBRARIES",
4229 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4230 );
4231 let config = Config::load().unwrap();
4232 assert_eq!(
4233 config.libraries,
4234 vec![
4235 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4236 .to_string(),
4237 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4238 .to_string()
4239 ]
4240 );
4241
4242 Ok(())
4243 });
4244 }
4245
4246 #[test]
4247 fn test_parse_many_libraries() {
4248 figment::Jail::expect_with(|jail| {
4249 jail.create_file(
4250 "foundry.toml",
4251 r"
4252 [profile.default]
4253 libraries= [
4254 './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4255 './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4256 './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4257 './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4258 './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4259 ]
4260 ",
4261 )?;
4262 let config = Config::load().unwrap();
4263
4264 let libs = config.parsed_libraries().unwrap().libs;
4265
4266 similar_asserts::assert_eq!(
4267 libs,
4268 BTreeMap::from([
4269 (
4270 PathBuf::from("./src/SizeAuctionDiscount.sol"),
4271 BTreeMap::from([
4272 (
4273 "Chainlink".to_string(),
4274 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4275 ),
4276 (
4277 "Math".to_string(),
4278 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4279 )
4280 ])
4281 ),
4282 (
4283 PathBuf::from("./src/SizeAuction.sol"),
4284 BTreeMap::from([
4285 (
4286 "ChainlinkTWAP".to_string(),
4287 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4288 ),
4289 (
4290 "Math".to_string(),
4291 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4292 )
4293 ])
4294 ),
4295 (
4296 PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
4297 BTreeMap::from([(
4298 "ChainlinkTWAP".to_string(),
4299 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4300 )])
4301 ),
4302 ])
4303 );
4304
4305 Ok(())
4306 });
4307 }
4308
4309 #[test]
4310 fn config_roundtrip() {
4311 figment::Jail::expect_with(|jail| {
4312 let default = Config::default().normalized_optimizer_settings();
4313 let basic = default.clone().into_basic();
4314 jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
4315
4316 let mut other = Config::load().unwrap();
4317 clear_warning(&mut other);
4318 assert_eq!(default, other);
4319
4320 let other = other.into_basic();
4321 assert_eq!(basic, other);
4322
4323 jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
4324 let mut other = Config::load().unwrap();
4325 clear_warning(&mut other);
4326 assert_eq!(default, other);
4327
4328 Ok(())
4329 });
4330 }
4331
4332 #[test]
4333 fn test_fs_permissions() {
4334 figment::Jail::expect_with(|jail| {
4335 jail.create_file(
4336 "foundry.toml",
4337 r#"
4338 [profile.default]
4339 fs_permissions = [{ access = "read-write", path = "./"}]
4340 "#,
4341 )?;
4342 let loaded = Config::load().unwrap();
4343
4344 assert_eq!(
4345 loaded.fs_permissions,
4346 FsPermissions::new(vec![PathPermission::read_write("./")])
4347 );
4348
4349 jail.create_file(
4350 "foundry.toml",
4351 r#"
4352 [profile.default]
4353 fs_permissions = [{ access = "none", path = "./"}]
4354 "#,
4355 )?;
4356 let loaded = Config::load().unwrap();
4357 assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
4358
4359 Ok(())
4360 });
4361 }
4362
4363 #[test]
4364 fn test_optimizer_settings_basic() {
4365 figment::Jail::expect_with(|jail| {
4366 jail.create_file(
4367 "foundry.toml",
4368 r"
4369 [profile.default]
4370 optimizer = true
4371
4372 [profile.default.optimizer_details]
4373 yul = false
4374
4375 [profile.default.optimizer_details.yulDetails]
4376 stackAllocation = true
4377 ",
4378 )?;
4379 let mut loaded = Config::load().unwrap();
4380 clear_warning(&mut loaded);
4381 assert_eq!(
4382 loaded.optimizer_details,
4383 Some(OptimizerDetails {
4384 yul: Some(false),
4385 yul_details: Some(YulDetails {
4386 stack_allocation: Some(true),
4387 ..Default::default()
4388 }),
4389 ..Default::default()
4390 })
4391 );
4392
4393 let s = loaded.to_string_pretty().unwrap();
4394 jail.create_file("foundry.toml", &s)?;
4395
4396 let mut reloaded = Config::load().unwrap();
4397 clear_warning(&mut reloaded);
4398 assert_eq!(loaded, reloaded);
4399
4400 Ok(())
4401 });
4402 }
4403
4404 #[test]
4405 fn test_model_checker_settings_basic() {
4406 figment::Jail::expect_with(|jail| {
4407 jail.create_file(
4408 "foundry.toml",
4409 r"
4410 [profile.default]
4411
4412 [profile.default.model_checker]
4413 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4414 engine = 'chc'
4415 targets = [ 'assert', 'outOfBounds' ]
4416 timeout = 10000
4417 ",
4418 )?;
4419 let mut loaded = Config::load().unwrap();
4420 clear_warning(&mut loaded);
4421 assert_eq!(
4422 loaded.model_checker,
4423 Some(ModelCheckerSettings {
4424 contracts: BTreeMap::from([
4425 ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
4426 ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
4427 ]),
4428 engine: Some(ModelCheckerEngine::CHC),
4429 targets: Some(vec![
4430 ModelCheckerTarget::Assert,
4431 ModelCheckerTarget::OutOfBounds
4432 ]),
4433 timeout: Some(10000),
4434 invariants: None,
4435 show_unproved: None,
4436 div_mod_with_slacks: None,
4437 solvers: None,
4438 show_unsupported: None,
4439 show_proved_safe: None,
4440 })
4441 );
4442
4443 let s = loaded.to_string_pretty().unwrap();
4444 jail.create_file("foundry.toml", &s)?;
4445
4446 let mut reloaded = Config::load().unwrap();
4447 clear_warning(&mut reloaded);
4448 assert_eq!(loaded, reloaded);
4449
4450 Ok(())
4451 });
4452 }
4453
4454 #[test]
4455 fn test_model_checker_settings_relative_paths() {
4456 figment::Jail::expect_with(|jail| {
4457 jail.create_file(
4458 "foundry.toml",
4459 r"
4460 [profile.default]
4461
4462 [profile.default.model_checker]
4463 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4464 engine = 'chc'
4465 targets = [ 'assert', 'outOfBounds' ]
4466 timeout = 10000
4467 ",
4468 )?;
4469 let loaded = Config::load().unwrap().sanitized();
4470
4471 let dir = foundry_compilers::utils::canonicalize(jail.directory())
4476 .expect("Could not canonicalize jail path");
4477 assert_eq!(
4478 loaded.model_checker,
4479 Some(ModelCheckerSettings {
4480 contracts: BTreeMap::from([
4481 (
4482 format!("{}", dir.join("a.sol").display()),
4483 vec!["A1".to_string(), "A2".to_string()]
4484 ),
4485 (
4486 format!("{}", dir.join("b.sol").display()),
4487 vec!["B1".to_string(), "B2".to_string()]
4488 ),
4489 ]),
4490 engine: Some(ModelCheckerEngine::CHC),
4491 targets: Some(vec![
4492 ModelCheckerTarget::Assert,
4493 ModelCheckerTarget::OutOfBounds
4494 ]),
4495 timeout: Some(10000),
4496 invariants: None,
4497 show_unproved: None,
4498 div_mod_with_slacks: None,
4499 solvers: None,
4500 show_unsupported: None,
4501 show_proved_safe: None,
4502 })
4503 );
4504
4505 Ok(())
4506 });
4507 }
4508
4509 #[test]
4510 fn test_fmt_config() {
4511 figment::Jail::expect_with(|jail| {
4512 jail.create_file(
4513 "foundry.toml",
4514 r#"
4515 [fmt]
4516 line_length = 100
4517 tab_width = 2
4518 bracket_spacing = true
4519 style = "space"
4520 "#,
4521 )?;
4522 let loaded = Config::load().unwrap().sanitized();
4523 assert_eq!(
4524 loaded.fmt,
4525 FormatterConfig {
4526 line_length: 100,
4527 tab_width: 2,
4528 bracket_spacing: true,
4529 style: IndentStyle::Space,
4530 ..Default::default()
4531 }
4532 );
4533
4534 Ok(())
4535 });
4536 }
4537
4538 #[test]
4539 fn test_lint_config() {
4540 figment::Jail::expect_with(|jail| {
4541 jail.create_file(
4542 "foundry.toml",
4543 r"
4544 [lint]
4545 severity = ['high', 'medium']
4546 exclude_lints = ['incorrect-shift']
4547 ",
4548 )?;
4549 let loaded = Config::load().unwrap().sanitized();
4550 assert_eq!(
4551 loaded.lint,
4552 LinterConfig {
4553 severity: vec![LintSeverity::High, LintSeverity::Med],
4554 exclude_lints: vec!["incorrect-shift".into()],
4555 ..Default::default()
4556 }
4557 );
4558
4559 Ok(())
4560 });
4561 }
4562
4563 #[test]
4564 fn test_invariant_config() {
4565 figment::Jail::expect_with(|jail| {
4566 jail.create_file(
4567 "foundry.toml",
4568 r"
4569 [invariant]
4570 runs = 512
4571 depth = 10
4572 ",
4573 )?;
4574
4575 let loaded = Config::load().unwrap().sanitized();
4576 assert_eq!(
4577 loaded.invariant,
4578 InvariantConfig {
4579 runs: 512,
4580 depth: 10,
4581 failure_persist_dir: Some(PathBuf::from("cache/invariant")),
4582 corpus_dir: None,
4583 ..Default::default()
4584 }
4585 );
4586
4587 Ok(())
4588 });
4589 }
4590
4591 #[test]
4592 fn test_standalone_sections_env() {
4593 figment::Jail::expect_with(|jail| {
4594 jail.create_file(
4595 "foundry.toml",
4596 r"
4597 [fuzz]
4598 runs = 100
4599
4600 [invariant]
4601 depth = 1
4602 ",
4603 )?;
4604
4605 jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
4606 jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
4607 jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
4608
4609 let config = Config::load().unwrap();
4610 assert_eq!(config.fmt.line_length, 95);
4611 assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
4612 assert_eq!(config.invariant.depth, 5);
4613
4614 Ok(())
4615 });
4616 }
4617
4618 #[test]
4619 fn test_parse_with_profile() {
4620 let foundry_str = r"
4621 [profile.default]
4622 src = 'src'
4623 out = 'out'
4624 libs = ['lib']
4625
4626 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
4627 ";
4628 assert_eq!(
4629 parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
4630 (
4631 Config::DEFAULT_PROFILE,
4632 BasicConfig {
4633 profile: Config::DEFAULT_PROFILE,
4634 src: "src".into(),
4635 out: "out".into(),
4636 libs: vec!["lib".into()],
4637 remappings: vec![]
4638 }
4639 )
4640 );
4641 }
4642
4643 #[test]
4644 fn test_implicit_profile_loads() {
4645 figment::Jail::expect_with(|jail| {
4646 jail.create_file(
4647 "foundry.toml",
4648 r"
4649 [default]
4650 src = 'my-src'
4651 out = 'my-out'
4652 ",
4653 )?;
4654 let loaded = Config::load().unwrap().sanitized();
4655 assert_eq!(loaded.src.file_name().unwrap(), "my-src");
4656 assert_eq!(loaded.out.file_name().unwrap(), "my-out");
4657 assert_eq!(
4658 loaded.warnings,
4659 vec![Warning::UnknownSection {
4660 unknown_section: Profile::new("default"),
4661 source: Some("foundry.toml".into())
4662 }]
4663 );
4664
4665 Ok(())
4666 });
4667 }
4668
4669 #[test]
4670 fn test_etherscan_api_key() {
4671 figment::Jail::expect_with(|jail| {
4672 jail.create_file(
4673 "foundry.toml",
4674 r"
4675 [default]
4676 ",
4677 )?;
4678 jail.set_env("ETHERSCAN_API_KEY", "");
4679 let loaded = Config::load().unwrap().sanitized();
4680 assert!(loaded.etherscan_api_key.is_none());
4681
4682 jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
4683 let loaded = Config::load().unwrap().sanitized();
4684 assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
4685
4686 Ok(())
4687 });
4688 }
4689
4690 #[test]
4691 fn test_etherscan_api_key_figment() {
4692 figment::Jail::expect_with(|jail| {
4693 jail.create_file(
4694 "foundry.toml",
4695 r"
4696 [default]
4697 etherscan_api_key = 'DUMMY'
4698 ",
4699 )?;
4700 jail.set_env("ETHERSCAN_API_KEY", "ETHER");
4701
4702 let figment = Config::figment_with_root(jail.directory())
4703 .merge(("etherscan_api_key", "USER_KEY"));
4704
4705 let loaded = Config::from_provider(figment).unwrap();
4706 assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
4707
4708 Ok(())
4709 });
4710 }
4711
4712 #[test]
4713 fn test_normalize_defaults() {
4714 figment::Jail::expect_with(|jail| {
4715 jail.create_file(
4716 "foundry.toml",
4717 r"
4718 [default]
4719 solc = '0.8.13'
4720 ",
4721 )?;
4722
4723 let loaded = Config::load().unwrap().sanitized();
4724 assert_eq!(loaded.evm_version, EvmVersion::London);
4725 Ok(())
4726 });
4727 }
4728
4729 #[expect(clippy::disallowed_macros)]
4731 #[test]
4732 #[ignore]
4733 fn print_config() {
4734 let config = Config {
4735 optimizer_details: Some(OptimizerDetails {
4736 peephole: None,
4737 inliner: None,
4738 jumpdest_remover: None,
4739 order_literals: None,
4740 deduplicate: None,
4741 cse: None,
4742 constant_optimizer: Some(true),
4743 yul: Some(true),
4744 yul_details: Some(YulDetails {
4745 stack_allocation: None,
4746 optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
4747 }),
4748 simple_counter_for_loop_unchecked_increment: None,
4749 }),
4750 ..Default::default()
4751 };
4752 println!("{}", config.to_string_pretty().unwrap());
4753 }
4754
4755 #[test]
4756 fn can_use_impl_figment_macro() {
4757 #[derive(Default, Serialize)]
4758 struct MyArgs {
4759 #[serde(skip_serializing_if = "Option::is_none")]
4760 root: Option<PathBuf>,
4761 }
4762 impl_figment_convert!(MyArgs);
4763
4764 impl Provider for MyArgs {
4765 fn metadata(&self) -> Metadata {
4766 Metadata::default()
4767 }
4768
4769 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
4770 let value = Value::serialize(self)?;
4771 let error = InvalidType(value.to_actual(), "map".into());
4772 let dict = value.into_dict().ok_or(error)?;
4773 Ok(Map::from([(Config::selected_profile(), dict)]))
4774 }
4775 }
4776
4777 let _figment: Figment = From::from(&MyArgs::default());
4778
4779 #[derive(Default)]
4780 struct Outer {
4781 start: MyArgs,
4782 other: MyArgs,
4783 another: MyArgs,
4784 }
4785 impl_figment_convert!(Outer, start, other, another);
4786
4787 let _figment: Figment = From::from(&Outer::default());
4788 }
4789
4790 #[test]
4791 fn list_cached_blocks() -> eyre::Result<()> {
4792 fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
4793 let block_path = chain_path.join(block_number);
4794 fs::create_dir(block_path.as_path()).unwrap();
4795 let file_path = block_path.join("storage.json");
4796 let mut file = File::create(file_path).unwrap();
4797 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4798 }
4799
4800 fn fake_block_cache_block_path_as_file(
4801 chain_path: &Path,
4802 block_number: &str,
4803 size_bytes: usize,
4804 ) {
4805 let block_path = chain_path.join(block_number);
4806 let mut file = File::create(block_path).unwrap();
4807 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4808 }
4809
4810 let chain_dir = tempdir()?;
4811
4812 fake_block_cache(chain_dir.path(), "1", 100);
4813 fake_block_cache(chain_dir.path(), "2", 500);
4814 fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
4815 let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
4817 writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
4818
4819 let result = Config::get_cached_blocks(chain_dir.path())?;
4820
4821 assert_eq!(result.len(), 3);
4822 let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
4823 let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
4824 let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
4825
4826 assert_eq!(block1.0, "1");
4827 assert_eq!(block1.1, 100);
4828 assert_eq!(block2.0, "2");
4829 assert_eq!(block2.1, 500);
4830 assert_eq!(block3.0, "3");
4831 assert_eq!(block3.1, 900);
4832
4833 chain_dir.close()?;
4834 Ok(())
4835 }
4836
4837 #[test]
4838 fn list_etherscan_cache() -> eyre::Result<()> {
4839 fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
4840 let metadata_path = chain_path.join("sources");
4841 let abi_path = chain_path.join("abi");
4842 let _ = fs::create_dir(metadata_path.as_path());
4843 let _ = fs::create_dir(abi_path.as_path());
4844
4845 let metadata_file_path = metadata_path.join(address);
4846 let mut metadata_file = File::create(metadata_file_path).unwrap();
4847 writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4848 .unwrap();
4849
4850 let abi_file_path = abi_path.join(address);
4851 let mut abi_file = File::create(abi_file_path).unwrap();
4852 writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4853 .unwrap();
4854 }
4855
4856 let chain_dir = tempdir()?;
4857
4858 fake_etherscan_cache(chain_dir.path(), "1", 100);
4859 fake_etherscan_cache(chain_dir.path(), "2", 500);
4860
4861 let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
4862
4863 assert_eq!(result, 600);
4864
4865 chain_dir.close()?;
4866 Ok(())
4867 }
4868
4869 #[test]
4870 fn test_parse_error_codes() {
4871 figment::Jail::expect_with(|jail| {
4872 jail.create_file(
4873 "foundry.toml",
4874 r#"
4875 [default]
4876 ignored_error_codes = ["license", "unreachable", 1337]
4877 "#,
4878 )?;
4879
4880 let config = Config::load().unwrap();
4881 assert_eq!(
4882 config.ignored_error_codes,
4883 vec![
4884 SolidityErrorCode::SpdxLicenseNotProvided,
4885 SolidityErrorCode::Unreachable,
4886 SolidityErrorCode::Other(1337)
4887 ]
4888 );
4889
4890 Ok(())
4891 });
4892 }
4893
4894 #[test]
4895 fn test_parse_file_paths() {
4896 figment::Jail::expect_with(|jail| {
4897 jail.create_file(
4898 "foundry.toml",
4899 r#"
4900 [default]
4901 ignored_warnings_from = ["something"]
4902 "#,
4903 )?;
4904
4905 let config = Config::load().unwrap();
4906 assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
4907
4908 Ok(())
4909 });
4910 }
4911
4912 #[test]
4913 fn test_parse_optimizer_settings() {
4914 figment::Jail::expect_with(|jail| {
4915 jail.create_file(
4916 "foundry.toml",
4917 r"
4918 [default]
4919 [profile.default.optimizer_details]
4920 ",
4921 )?;
4922
4923 let config = Config::load().unwrap();
4924 assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
4925
4926 Ok(())
4927 });
4928 }
4929
4930 #[test]
4931 fn test_parse_labels() {
4932 figment::Jail::expect_with(|jail| {
4933 jail.create_file(
4934 "foundry.toml",
4935 r#"
4936 [labels]
4937 0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
4938 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
4939 "#,
4940 )?;
4941
4942 let config = Config::load().unwrap();
4943 assert_eq!(
4944 config.labels,
4945 AddressHashMap::from_iter(vec![
4946 (
4947 address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
4948 "Uniswap V3: Factory".to_string()
4949 ),
4950 (
4951 address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
4952 "Uniswap V3: Positions NFT".to_string()
4953 ),
4954 ])
4955 );
4956
4957 Ok(())
4958 });
4959 }
4960
4961 #[test]
4962 fn test_parse_vyper() {
4963 figment::Jail::expect_with(|jail| {
4964 jail.create_file(
4965 "foundry.toml",
4966 r#"
4967 [vyper]
4968 optimize = "codesize"
4969 path = "/path/to/vyper"
4970 experimental_codegen = true
4971 "#,
4972 )?;
4973
4974 let config = Config::load().unwrap();
4975 assert_eq!(
4976 config.vyper,
4977 VyperConfig {
4978 optimize: Some(VyperOptimizationMode::Codesize),
4979 path: Some("/path/to/vyper".into()),
4980 experimental_codegen: Some(true),
4981 }
4982 );
4983
4984 Ok(())
4985 });
4986 }
4987
4988 #[test]
4989 fn test_parse_soldeer() {
4990 figment::Jail::expect_with(|jail| {
4991 jail.create_file(
4992 "foundry.toml",
4993 r#"
4994 [soldeer]
4995 remappings_generate = true
4996 remappings_regenerate = false
4997 remappings_version = true
4998 remappings_prefix = "@"
4999 remappings_location = "txt"
5000 recursive_deps = true
5001 "#,
5002 )?;
5003
5004 let config = Config::load().unwrap();
5005
5006 assert_eq!(
5007 config.soldeer,
5008 Some(SoldeerConfig {
5009 remappings_generate: true,
5010 remappings_regenerate: false,
5011 remappings_version: true,
5012 remappings_prefix: "@".to_string(),
5013 remappings_location: RemappingsLocation::Txt,
5014 recursive_deps: true,
5015 })
5016 );
5017
5018 Ok(())
5019 });
5020 }
5021
5022 #[test]
5024 fn test_resolve_mesc_by_chain_id() {
5025 let s = r#"{
5026 "mesc_version": "0.2.1",
5027 "default_endpoint": null,
5028 "endpoints": {
5029 "sophon_50104": {
5030 "name": "sophon_50104",
5031 "url": "https://rpc.sophon.xyz",
5032 "chain_id": "50104",
5033 "endpoint_metadata": {}
5034 }
5035 },
5036 "network_defaults": {
5037 },
5038 "network_names": {},
5039 "profiles": {
5040 "foundry": {
5041 "name": "foundry",
5042 "default_endpoint": "local_ethereum",
5043 "network_defaults": {
5044 "50104": "sophon_50104"
5045 },
5046 "profile_metadata": {},
5047 "use_mesc": true
5048 }
5049 },
5050 "global_metadata": {}
5051}"#;
5052
5053 let config = serde_json::from_str(s).unwrap();
5054 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5055 .unwrap()
5056 .unwrap();
5057 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5058
5059 let s = r#"{
5060 "mesc_version": "0.2.1",
5061 "default_endpoint": null,
5062 "endpoints": {
5063 "sophon_50104": {
5064 "name": "sophon_50104",
5065 "url": "https://rpc.sophon.xyz",
5066 "chain_id": "50104",
5067 "endpoint_metadata": {}
5068 }
5069 },
5070 "network_defaults": {
5071 "50104": "sophon_50104"
5072 },
5073 "network_names": {},
5074 "profiles": {},
5075 "global_metadata": {}
5076}"#;
5077
5078 let config = serde_json::from_str(s).unwrap();
5079 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5080 .unwrap()
5081 .unwrap();
5082 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5083 }
5084}