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