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