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