1use super::Result;
2use crate::Vm::Rpc;
3use alloy_primitives::{Address, 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 std::{
12 path::{Path, PathBuf},
13 time::Duration,
14};
15
16#[derive(Clone, Debug)]
20pub struct CheatsConfig {
21 pub ffi: bool,
23 pub always_use_create_2_factory: bool,
25 pub batch_rewrite_creates: bool,
27 pub prompt_timeout: Duration,
29 pub rpc_storage_caching: StorageCachingConfig,
31 pub no_storage_caching: bool,
33 pub rpc_endpoints: ResolvedRpcEndpoints,
35 pub paths: ProjectPathsConfig,
37 pub bind_json_path: PathBuf,
39 pub fs_permissions: FsPermissions,
41 pub root: PathBuf,
43 pub broadcast: PathBuf,
45 pub isolate: bool,
47 pub evm_opts: EvmOpts,
49 pub labels: AddressHashMap<String>,
51 pub available_artifacts: Option<ContractsByArtifact>,
55 pub running_artifact: Option<ArtifactId>,
57 pub assertions_revert: bool,
59 pub seed: Option<U256>,
61 pub internal_expect_revert: bool,
63 pub fee_token: Option<Address>,
65}
66
67impl CheatsConfig {
68 pub fn new(
70 config: &Config,
71 evm_opts: EvmOpts,
72 available_artifacts: Option<ContractsByArtifact>,
73 running_artifact: Option<ArtifactId>,
74 fee_token: Option<Address>,
75 batch_rewrite_creates: bool,
76 ) -> Self {
77 let rpc_endpoints = config.rpc_endpoints.clone().resolved();
78 trace!(?rpc_endpoints, "using resolved rpc endpoints");
79
80 let available_artifacts =
82 if config.unchecked_cheatcode_artifacts { None } else { available_artifacts };
83 let mut labels = config.labels.clone();
84 labels.extend(config.tracing.labels.clone());
85
86 Self {
87 ffi: evm_opts.ffi,
88 always_use_create_2_factory: evm_opts.always_use_create_2_factory,
89 batch_rewrite_creates,
90 prompt_timeout: Duration::from_secs(config.prompt_timeout),
91 rpc_storage_caching: config.rpc_storage_caching.clone(),
92 no_storage_caching: config.no_storage_caching,
93 rpc_endpoints,
94 paths: config.project_paths(),
95 bind_json_path: config.bind_json.out.clone(),
96 fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()),
97 root: config.root.clone(),
98 broadcast: config.root.clone().join(&config.broadcast),
99 isolate: config.isolate,
100 evm_opts,
101 labels,
102 available_artifacts,
103 running_artifact,
104 assertions_revert: config.assertions_revert,
105 seed: config.fuzz.seed,
106 internal_expect_revert: config.allow_internal_expect_revert,
107 fee_token,
108 }
109 }
110
111 pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self {
113 Self::new(
114 config,
115 evm_opts,
116 self.available_artifacts.clone(),
117 self.running_artifact.clone(),
118 self.fee_token,
119 self.batch_rewrite_creates,
120 )
121 }
122
123 pub fn normalized_path(&self, path: impl AsRef<Path>) -> PathBuf {
127 let path = self.root.join(path);
128 canonicalize(&path).unwrap_or_else(|_| canonicalize_existing_ancestor(&path))
129 }
130
131 pub fn is_path_allowed(&self, path: impl AsRef<Path>, kind: FsAccessKind) -> bool {
138 self.is_normalized_path_allowed(&self.normalized_path(path), kind)
139 }
140
141 fn is_normalized_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
142 self.fs_permissions.is_path_allowed(path, kind)
143 }
144
145 pub fn ensure_path_allowed(
149 &self,
150 path: impl AsRef<Path>,
151 kind: FsAccessKind,
152 ) -> Result<PathBuf> {
153 let path = path.as_ref();
154 let normalized = self.normalized_path(path);
155 ensure!(
156 self.is_normalized_path_allowed(&normalized, kind),
157 "the path {} is not allowed to be accessed for {kind} operations",
158 normalized.strip_prefix(&self.root).unwrap_or(path).display()
159 );
160 Ok(normalized)
161 }
162
163 pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
167 let foundry_toml = self.root.join(Config::FILE_NAME);
172 Path::new(&foundry_toml.to_string_lossy().to_lowercase())
173 .starts_with(Path::new(&path.as_ref().to_string_lossy().to_lowercase()))
174 }
175
176 pub fn ensure_not_foundry_toml(&self, path: impl AsRef<Path>) -> Result<()> {
179 ensure!(!self.is_foundry_toml(path), "access to `foundry.toml` is not allowed");
180 Ok(())
181 }
182
183 pub fn rpc_endpoint(&self, url_or_alias: &str) -> Result<ResolvedRpcEndpoint> {
197 if let Some(endpoint) = self.rpc_endpoints.get(url_or_alias) {
198 Ok(endpoint.clone().try_resolve())
199 } else if let Some(builtin_url) = foundry_config::builtin_rpc_url(url_or_alias) {
200 let url = RpcEndpointUrl::Url(builtin_url.to_string());
201 Ok(RpcEndpoint::new(url).resolve())
202 } else {
203 if url_or_alias.starts_with("http") ||
205 url_or_alias.starts_with("ws") ||
206 Path::new(url_or_alias).exists()
208 {
209 let url = RpcEndpointUrl::Env(url_or_alias.to_string());
210 Ok(RpcEndpoint::new(url).resolve())
211 } else {
212 Err(fmt_err!("invalid rpc url: {url_or_alias}"))
213 }
214 }
215 }
216 pub fn rpc_urls(&self) -> Result<Vec<Rpc>> {
218 let mut urls = Vec::with_capacity(self.rpc_endpoints.len());
219 for alias in self.rpc_endpoints.keys() {
220 let url = self.rpc_endpoint(alias)?.url()?;
221 urls.push(Rpc { key: alias.clone(), url });
222 }
223 Ok(urls)
224 }
225}
226
227impl Default for CheatsConfig {
228 fn default() -> Self {
229 Self {
230 ffi: false,
231 always_use_create_2_factory: false,
232 batch_rewrite_creates: false,
233 prompt_timeout: Duration::from_secs(120),
234 rpc_storage_caching: Default::default(),
235 no_storage_caching: false,
236 rpc_endpoints: Default::default(),
237 paths: ProjectPathsConfig::builder().build_with_root("./"),
238 fs_permissions: Default::default(),
239 root: Default::default(),
240 bind_json_path: PathBuf::default().join("utils").join("jsonBindings.sol"),
241 broadcast: Default::default(),
242 isolate: Config::default().isolate,
243 evm_opts: Default::default(),
244 labels: Default::default(),
245 available_artifacts: Default::default(),
246 running_artifact: Default::default(),
247 assertions_revert: true,
248 seed: None,
249 internal_expect_revert: false,
250 fee_token: None,
251 }
252 }
253}
254
255fn canonicalize_existing_ancestor(path: &Path) -> PathBuf {
256 let normalized = normalize_path(path);
257 let mut missing = Vec::new();
258 let mut ancestor = normalized.as_path();
259 while !ancestor.exists() {
260 let Some(name) = ancestor.file_name() else { return normalized };
261 missing.push(name.to_owned());
262 let Some(parent) = ancestor.parent() else { return normalized };
263 ancestor = parent;
264 }
265
266 let mut path = canonicalize(ancestor).unwrap_or_else(|_| ancestor.to_path_buf());
267 for component in missing.iter().rev() {
268 path.push(component);
269 }
270 path
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use alloy_primitives::address;
277 use foundry_config::fs_permissions::PathPermission;
278 use tempfile::TempDir;
279
280 fn config(root: &Path, fs_permissions: FsPermissions) -> CheatsConfig {
281 CheatsConfig::new(
282 &Config { root: root.into(), fs_permissions, ..Default::default() },
283 Default::default(),
284 None,
285 None,
286 None,
287 false,
288 )
289 }
290
291 #[test]
292 fn test_allowed_paths() {
293 let temp = TempDir::new().unwrap();
294 let root = temp.path().join("my/project/root");
295 std::fs::create_dir_all(&root).unwrap();
296 let config = config(&root, FsPermissions::new(vec![PathPermission::read_write("./")]));
297
298 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Read).is_ok());
299 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Write).is_ok());
300 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Read).is_ok());
301 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Write).is_ok());
302 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Read).is_err());
303 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Write).is_err());
304 }
305
306 #[test]
307 fn test_batch_rewrite_creates_flag_plumbing() {
308 assert!(!CheatsConfig::default().batch_rewrite_creates);
309
310 let on = CheatsConfig::new(&Config::default(), Default::default(), None, None, None, true);
311 assert!(on.batch_rewrite_creates);
312
313 let cloned = on.clone_with(&Config::default(), Default::default());
314 assert!(cloned.batch_rewrite_creates);
315 }
316
317 #[test]
318 fn tracing_labels_override_legacy_labels() {
319 let address = address!("0x0000000000000000000000000000000000000001");
320 let mut config = Config::default();
321 config.labels.insert(address, "legacy".to_string());
322 config.tracing.labels.insert(address, "canonical".to_string());
323
324 let config = CheatsConfig::new(&config, Default::default(), None, None, None, false);
325
326 assert_eq!(config.labels.get(&address).map(String::as_str), Some("canonical"));
327 }
328
329 #[test]
330 fn test_is_foundry_toml() {
331 let root = Path::new("/my/project/root/");
332 let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
333
334 let f = root.join("foundry.toml");
335 assert!(config.is_foundry_toml(f));
336
337 let f = root.join("Foundry.toml");
338 assert!(config.is_foundry_toml(f));
339
340 let f = root.join("lib/other/foundry.toml");
341 assert!(!config.is_foundry_toml(f));
342 }
343}