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