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