1use super::Result;
2use crate::Vm::Rpc;
3use alloy_primitives::{U256, map::AddressHashMap};
4use foundry_common::{ContractsByArtifact, fs::normalize_path};
5use foundry_compilers::{ArtifactId, ProjectPathsConfig, utils::canonicalize};
6use foundry_config::{
7 Config, FsPermissions, ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl,
8 cache::StorageCachingConfig, fs_permissions::FsAccessKind,
9};
10use foundry_evm_core::opts::EvmOpts;
11use foundry_evm_traces::identifier::ExternalIdentifierConfig;
12use std::{
13 path::{Path, PathBuf},
14 time::Duration,
15};
16
17#[derive(Clone, Debug)]
21pub struct CheatsConfig {
22 pub ffi: bool,
24 pub blocked_cheatcodes: Vec<[u8; 4]>,
26 pub always_use_create_2_factory: bool,
28 pub batch_rewrite_creates: bool,
30 pub prompt_timeout: Duration,
32 pub rpc_storage_caching: StorageCachingConfig,
34 pub no_storage_caching: bool,
36 pub rpc_endpoints: ResolvedRpcEndpoints,
38 pub paths: ProjectPathsConfig,
40 pub bind_json_path: PathBuf,
42 pub fs_permissions: FsPermissions,
44 pub root: PathBuf,
46 pub broadcast: PathBuf,
48 pub isolate: bool,
50 pub evm_opts: EvmOpts,
52 pub labels: AddressHashMap<String>,
54 pub available_artifacts: Option<ContractsByArtifact>,
58 pub artifact_lookup: Option<ContractsByArtifact>,
61 pub decode_external_storage: bool,
64 pub external_sources: ExternalIdentifierConfig,
67 pub running_artifact: Option<ArtifactId>,
69 pub assertions_revert: bool,
71 pub seed: Option<U256>,
73 pub internal_expect_revert: bool,
75}
76
77impl CheatsConfig {
78 pub fn new(
80 config: &Config,
81 evm_opts: EvmOpts,
82 available_artifacts: Option<ContractsByArtifact>,
83 running_artifact: Option<ArtifactId>,
84 batch_rewrite_creates: bool,
85 ) -> Self {
86 let rpc_endpoints = config.rpc_endpoints.clone().resolved();
87 trace!(?rpc_endpoints, "using resolved rpc endpoints");
88
89 let artifact_lookup = available_artifacts.clone();
90 let available_artifacts =
92 if config.unchecked_cheatcode_artifacts { None } else { available_artifacts };
93 let mut labels = config.labels.clone();
94 labels.extend(config.tracing.labels.clone());
95
96 Self {
97 ffi: evm_opts.ffi,
98 blocked_cheatcodes: Vec::new(),
99 always_use_create_2_factory: evm_opts.always_use_create_2_factory,
100 batch_rewrite_creates,
101 prompt_timeout: Duration::from_secs(config.prompt_timeout),
102 rpc_storage_caching: config.rpc_storage_caching.clone(),
103 no_storage_caching: config.no_storage_caching,
104 rpc_endpoints,
105 paths: config.project_paths(),
106 bind_json_path: config.bind_json.out.clone(),
107 fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()),
108 root: config.root.clone(),
109 broadcast: config.root.clone().join(&config.broadcast),
110 isolate: config.isolate,
111 evm_opts,
112 labels,
113 available_artifacts,
114 artifact_lookup,
115 decode_external_storage: config.decode_external_storage,
116 external_sources: ExternalIdentifierConfig::new(config),
117 running_artifact,
118 assertions_revert: config.assertions_revert,
119 seed: config.fuzz.seed,
120 internal_expect_revert: config.allow_internal_expect_revert,
121 }
122 }
123
124 pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self {
126 let mut cloned = Self::new(
127 config,
128 evm_opts,
129 self.artifact_lookup.clone().or_else(|| self.available_artifacts.clone()),
130 self.running_artifact.clone(),
131 self.batch_rewrite_creates,
132 );
133 cloned.blocked_cheatcodes.clone_from(&self.blocked_cheatcodes);
134 cloned
135 }
136
137 pub fn normalized_path(&self, path: impl AsRef<Path>) -> PathBuf {
141 let path = self.root.join(path);
142 canonicalize(&path).unwrap_or_else(|_| canonicalize_existing_ancestor(&path))
143 }
144
145 pub fn is_path_allowed(&self, path: impl AsRef<Path>, kind: FsAccessKind) -> bool {
152 self.is_normalized_path_allowed(&self.normalized_path(path), kind)
153 }
154
155 fn is_normalized_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
156 self.fs_permissions.is_path_allowed(path, kind)
157 }
158
159 pub fn ensure_path_allowed(
163 &self,
164 path: impl AsRef<Path>,
165 kind: FsAccessKind,
166 ) -> Result<PathBuf> {
167 let path = path.as_ref();
168 let normalized = self.normalized_path(path);
169 ensure!(
170 self.is_normalized_path_allowed(&normalized, kind),
171 "the path {} is not allowed to be accessed for {kind} operations",
172 normalized.strip_prefix(&self.root).unwrap_or(path).display()
173 );
174 Ok(normalized)
175 }
176
177 pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
181 let foundry_toml = self.root.join(Config::FILE_NAME);
186 Path::new(&foundry_toml.to_string_lossy().to_lowercase())
187 .starts_with(Path::new(&path.as_ref().to_string_lossy().to_lowercase()))
188 }
189
190 pub fn ensure_not_foundry_toml(&self, path: impl AsRef<Path>) -> Result<()> {
193 ensure!(!self.is_foundry_toml(path), "access to `foundry.toml` is not allowed");
194 Ok(())
195 }
196
197 pub fn rpc_endpoint(&self, url_or_alias: &str) -> Result<ResolvedRpcEndpoint> {
212 if self.evm_opts.no_network {
214 return Err(fmt_err!("network access is disabled"));
215 }
216 if let Some(endpoint) = self.rpc_endpoints.get(url_or_alias) {
217 Ok(endpoint.clone().try_resolve())
218 } else if let Some(builtin_url) = foundry_config::builtin_rpc_url(url_or_alias) {
219 let url = RpcEndpointUrl::Url(builtin_url.to_string());
220 Ok(RpcEndpoint::new(url).resolve())
221 } else {
222 if url_or_alias.starts_with("http") ||
224 url_or_alias.starts_with("ws") ||
225 Path::new(url_or_alias).exists()
227 {
228 let url = RpcEndpointUrl::Env(url_or_alias.to_string());
229 Ok(RpcEndpoint::new(url).resolve())
230 } else {
231 Err(fmt_err!("invalid rpc url: {url_or_alias}"))
232 }
233 }
234 }
235 pub fn rpc_urls(&self) -> Result<Vec<Rpc>> {
237 let mut urls = Vec::with_capacity(self.rpc_endpoints.len());
238 for alias in self.rpc_endpoints.keys() {
239 let url = self.rpc_endpoint(alias)?.url()?;
240 urls.push(Rpc { key: alias.clone(), url });
241 }
242 Ok(urls)
243 }
244}
245
246impl Default for CheatsConfig {
247 fn default() -> Self {
248 Self {
249 ffi: false,
250 blocked_cheatcodes: Vec::new(),
251 always_use_create_2_factory: false,
252 batch_rewrite_creates: false,
253 prompt_timeout: Duration::from_secs(120),
254 rpc_storage_caching: Default::default(),
255 no_storage_caching: false,
256 rpc_endpoints: Default::default(),
257 paths: ProjectPathsConfig::builder().build_with_root("./"),
258 fs_permissions: Default::default(),
259 root: Default::default(),
260 bind_json_path: PathBuf::default().join("utils").join("jsonBindings.sol"),
261 broadcast: Default::default(),
262 isolate: Config::default().isolate,
263 evm_opts: Default::default(),
264 labels: Default::default(),
265 available_artifacts: Default::default(),
266 artifact_lookup: Default::default(),
267 decode_external_storage: false,
268 external_sources: Default::default(),
269 running_artifact: Default::default(),
270 assertions_revert: true,
271 seed: None,
272 internal_expect_revert: false,
273 }
274 }
275}
276
277fn canonicalize_existing_ancestor(path: &Path) -> PathBuf {
278 let normalized = normalize_path(path);
279 let mut missing = Vec::new();
280 let mut ancestor = normalized.as_path();
281 while !ancestor.exists() {
282 let Some(name) = ancestor.file_name() else { return normalized };
283 missing.push(name.to_owned());
284 let Some(parent) = ancestor.parent() else { return normalized };
285 ancestor = parent;
286 }
287
288 let mut path = canonicalize(ancestor).unwrap_or_else(|_| ancestor.to_path_buf());
289 for component in missing.iter().rev() {
290 path.push(component);
291 }
292 path
293}
294
295#[cfg(test)]
296mod tests {
297
298 use super::*;
299 use alloy_primitives::Address;
300 use foundry_config::fs_permissions::PathPermission;
301 use tempfile::TempDir;
302
303 fn config(root: &Path, fs_permissions: FsPermissions) -> CheatsConfig {
304 CheatsConfig::new(
305 &Config { root: root.into(), fs_permissions, ..Default::default() },
306 Default::default(),
307 None,
308 None,
309 false,
310 )
311 }
312
313 #[test]
314 fn test_allowed_paths() {
315 let temp = TempDir::new().unwrap();
316 let root = temp.path().join("my/project/root");
317 std::fs::create_dir_all(&root).unwrap();
318 let config = config(&root, FsPermissions::new(vec![PathPermission::read_write("./")]));
319
320 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Read).is_ok());
321 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Write).is_ok());
322 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Read).is_ok());
323 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Write).is_ok());
324 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Read).is_err());
325 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Write).is_err());
326 }
327
328 #[test]
329 fn test_batch_rewrite_creates_flag_plumbing() {
330 assert!(!CheatsConfig::default().batch_rewrite_creates);
331
332 let on = CheatsConfig::new(&Config::default(), Default::default(), None, None, true);
333 assert!(on.batch_rewrite_creates);
334
335 let cloned = on.clone_with(&Config::default(), Default::default());
336 assert!(cloned.batch_rewrite_creates);
337 }
338
339 #[test]
340 fn unchecked_artifacts_retain_lookup_without_validation() {
341 let config = Config { unchecked_cheatcode_artifacts: true, ..Default::default() };
342 let cheats = CheatsConfig::new(
343 &config,
344 Default::default(),
345 Some(ContractsByArtifact::default()),
346 None,
347 false,
348 );
349
350 assert!(cheats.available_artifacts.is_none());
351 assert!(cheats.artifact_lookup.is_some());
352
353 let cloned = cheats.clone_with(&config, Default::default());
354 assert!(cloned.available_artifacts.is_none());
355 assert!(cloned.artifact_lookup.is_some());
356 }
357
358 #[test]
359 fn clone_with_preserves_available_artifacts_without_lookup() {
360 let cheats = CheatsConfig {
361 available_artifacts: Some(ContractsByArtifact::default()),
362 ..Default::default()
363 };
364
365 let cloned = cheats.clone_with(&Config::default(), Default::default());
366 assert!(cloned.available_artifacts.is_some());
367 assert!(cloned.artifact_lookup.is_some());
368 }
369
370 #[test]
371 fn tracing_labels_override_legacy_labels() {
372 let address = Address::with_last_byte(1);
373 let mut config = Config::default();
374 config.labels.insert(address, "legacy".to_string());
375 config.tracing.labels.insert(address, "canonical".to_string());
376
377 let config = CheatsConfig::new(&config, Default::default(), None, None, false);
378
379 assert_eq!(config.labels.get(&address).map(String::as_str), Some("canonical"));
380 }
381
382 #[test]
383 fn test_is_foundry_toml() {
384 let root = Path::new("/my/project/root/");
385 let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
386
387 let f = root.join("foundry.toml");
388 assert!(config.is_foundry_toml(f));
389
390 let f = root.join("Foundry.toml");
391 assert!(config.is_foundry_toml(f));
392
393 let f = root.join("lib/other/foundry.toml");
394 assert!(!config.is_foundry_toml(f));
395 }
396}