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