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 evm_opts: EvmOpts,
47 pub labels: AddressHashMap<String>,
49 pub available_artifacts: Option<ContractsByArtifact>,
53 pub running_artifact: Option<ArtifactId>,
55 pub assertions_revert: bool,
57 pub seed: Option<U256>,
59 pub internal_expect_revert: bool,
61 pub fee_token: Option<Address>,
63}
64
65impl CheatsConfig {
66 pub fn new(
68 config: &Config,
69 evm_opts: EvmOpts,
70 available_artifacts: Option<ContractsByArtifact>,
71 running_artifact: Option<ArtifactId>,
72 fee_token: Option<Address>,
73 batch_rewrite_creates: bool,
74 ) -> Self {
75 let rpc_endpoints = config.rpc_endpoints.clone().resolved();
76 trace!(?rpc_endpoints, "using resolved rpc endpoints");
77
78 let available_artifacts =
80 if config.unchecked_cheatcode_artifacts { None } else { available_artifacts };
81
82 Self {
83 ffi: evm_opts.ffi,
84 always_use_create_2_factory: evm_opts.always_use_create_2_factory,
85 batch_rewrite_creates,
86 prompt_timeout: Duration::from_secs(config.prompt_timeout),
87 rpc_storage_caching: config.rpc_storage_caching.clone(),
88 no_storage_caching: config.no_storage_caching,
89 rpc_endpoints,
90 paths: config.project_paths(),
91 bind_json_path: config.bind_json.out.clone(),
92 fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()),
93 root: config.root.clone(),
94 broadcast: config.root.clone().join(&config.broadcast),
95 evm_opts,
96 labels: config.labels.clone(),
97 available_artifacts,
98 running_artifact,
99 assertions_revert: config.assertions_revert,
100 seed: config.fuzz.seed,
101 internal_expect_revert: config.allow_internal_expect_revert,
102 fee_token,
103 }
104 }
105
106 pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self {
108 Self::new(
109 config,
110 evm_opts,
111 self.available_artifacts.clone(),
112 self.running_artifact.clone(),
113 self.fee_token,
114 self.batch_rewrite_creates,
115 )
116 }
117
118 pub fn normalized_path(&self, path: impl AsRef<Path>) -> PathBuf {
122 let path = self.root.join(path);
123 canonicalize(&path).unwrap_or_else(|_| normalize_path(&path))
124 }
125
126 pub fn is_path_allowed(&self, path: impl AsRef<Path>, kind: FsAccessKind) -> bool {
133 self.is_normalized_path_allowed(&self.normalized_path(path), kind)
134 }
135
136 fn is_normalized_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
137 self.fs_permissions.is_path_allowed(path, kind)
138 }
139
140 pub fn ensure_path_allowed(
144 &self,
145 path: impl AsRef<Path>,
146 kind: FsAccessKind,
147 ) -> Result<PathBuf> {
148 let path = path.as_ref();
149 let normalized = self.normalized_path(path);
150 ensure!(
151 self.is_normalized_path_allowed(&normalized, kind),
152 "the path {} is not allowed to be accessed for {kind} operations",
153 normalized.strip_prefix(&self.root).unwrap_or(path).display()
154 );
155 Ok(normalized)
156 }
157
158 pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
162 let foundry_toml = self.root.join(Config::FILE_NAME);
167 Path::new(&foundry_toml.to_string_lossy().to_lowercase())
168 .starts_with(Path::new(&path.as_ref().to_string_lossy().to_lowercase()))
169 }
170
171 pub fn ensure_not_foundry_toml(&self, path: impl AsRef<Path>) -> Result<()> {
174 ensure!(!self.is_foundry_toml(path), "access to `foundry.toml` is not allowed");
175 Ok(())
176 }
177
178 pub fn rpc_endpoint(&self, url_or_alias: &str) -> Result<ResolvedRpcEndpoint> {
192 if let Some(endpoint) = self.rpc_endpoints.get(url_or_alias) {
193 Ok(endpoint.clone().try_resolve())
194 } else if let Some(builtin_url) = foundry_config::builtin_rpc_url(url_or_alias) {
195 let url = RpcEndpointUrl::Url(builtin_url.to_string());
196 Ok(RpcEndpoint::new(url).resolve())
197 } else {
198 if url_or_alias.starts_with("http") ||
200 url_or_alias.starts_with("ws") ||
201 Path::new(url_or_alias).exists()
203 {
204 let url = RpcEndpointUrl::Env(url_or_alias.to_string());
205 Ok(RpcEndpoint::new(url).resolve())
206 } else {
207 Err(fmt_err!("invalid rpc url: {url_or_alias}"))
208 }
209 }
210 }
211 pub fn rpc_urls(&self) -> Result<Vec<Rpc>> {
213 let mut urls = Vec::with_capacity(self.rpc_endpoints.len());
214 for alias in self.rpc_endpoints.keys() {
215 let url = self.rpc_endpoint(alias)?.url()?;
216 urls.push(Rpc { key: alias.clone(), url });
217 }
218 Ok(urls)
219 }
220}
221
222impl Default for CheatsConfig {
223 fn default() -> Self {
224 Self {
225 ffi: false,
226 always_use_create_2_factory: false,
227 batch_rewrite_creates: false,
228 prompt_timeout: Duration::from_secs(120),
229 rpc_storage_caching: Default::default(),
230 no_storage_caching: false,
231 rpc_endpoints: Default::default(),
232 paths: ProjectPathsConfig::builder().build_with_root("./"),
233 fs_permissions: Default::default(),
234 root: Default::default(),
235 bind_json_path: PathBuf::default().join("utils").join("jsonBindings.sol"),
236 broadcast: Default::default(),
237 evm_opts: Default::default(),
238 labels: Default::default(),
239 available_artifacts: Default::default(),
240 running_artifact: Default::default(),
241 assertions_revert: true,
242 seed: None,
243 internal_expect_revert: false,
244 fee_token: None,
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use foundry_config::fs_permissions::PathPermission;
253
254 fn config(root: &str, fs_permissions: FsPermissions) -> CheatsConfig {
255 CheatsConfig::new(
256 &Config { root: root.into(), fs_permissions, ..Default::default() },
257 Default::default(),
258 None,
259 None,
260 None,
261 false,
262 )
263 }
264
265 #[test]
266 fn test_allowed_paths() {
267 let root = "/my/project/root/";
268 let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
269
270 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Read).is_ok());
271 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Write).is_ok());
272 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Read).is_ok());
273 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Write).is_ok());
274 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Read).is_err());
275 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Write).is_err());
276 }
277
278 #[test]
279 fn test_batch_rewrite_creates_flag_plumbing() {
280 assert!(!CheatsConfig::default().batch_rewrite_creates);
281
282 let on = CheatsConfig::new(&Config::default(), Default::default(), None, None, None, true);
283 assert!(on.batch_rewrite_creates);
284
285 let cloned = on.clone_with(&Config::default(), Default::default());
286 assert!(cloned.batch_rewrite_creates);
287 }
288
289 #[test]
290 fn test_is_foundry_toml() {
291 let root = "/my/project/root/";
292 let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
293
294 let f = format!("{root}foundry.toml");
295 assert!(config.is_foundry_toml(f));
296
297 let f = format!("{root}Foundry.toml");
298 assert!(config.is_foundry_toml(f));
299
300 let f = format!("{root}lib/other/foundry.toml");
301 assert!(!config.is_foundry_toml(f));
302 }
303}