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