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 prompt_timeout: Duration,
27 pub rpc_storage_caching: StorageCachingConfig,
29 pub no_storage_caching: bool,
31 pub rpc_endpoints: ResolvedRpcEndpoints,
33 pub paths: ProjectPathsConfig,
35 pub bind_json_path: PathBuf,
37 pub fs_permissions: FsPermissions,
39 pub root: PathBuf,
41 pub broadcast: PathBuf,
43 pub evm_opts: EvmOpts,
45 pub labels: AddressHashMap<String>,
47 pub available_artifacts: Option<ContractsByArtifact>,
51 pub running_artifact: Option<ArtifactId>,
53 pub assertions_revert: bool,
55 pub seed: Option<U256>,
57 pub internal_expect_revert: bool,
59 pub fee_token: Option<Address>,
61}
62
63impl CheatsConfig {
64 pub fn new(
66 config: &Config,
67 evm_opts: EvmOpts,
68 available_artifacts: Option<ContractsByArtifact>,
69 running_artifact: Option<ArtifactId>,
70 fee_token: Option<Address>,
71 ) -> Self {
72 let rpc_endpoints = config.rpc_endpoints.clone().resolved();
73 trace!(?rpc_endpoints, "using resolved rpc endpoints");
74
75 let available_artifacts =
77 if config.unchecked_cheatcode_artifacts { None } else { available_artifacts };
78
79 Self {
80 ffi: evm_opts.ffi,
81 always_use_create_2_factory: evm_opts.always_use_create_2_factory,
82 prompt_timeout: Duration::from_secs(config.prompt_timeout),
83 rpc_storage_caching: config.rpc_storage_caching.clone(),
84 no_storage_caching: config.no_storage_caching,
85 rpc_endpoints,
86 paths: config.project_paths(),
87 bind_json_path: config.bind_json.out.clone(),
88 fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()),
89 root: config.root.clone(),
90 broadcast: config.root.clone().join(&config.broadcast),
91 evm_opts,
92 labels: config.labels.clone(),
93 available_artifacts,
94 running_artifact,
95 assertions_revert: config.assertions_revert,
96 seed: config.fuzz.seed,
97 internal_expect_revert: config.allow_internal_expect_revert,
98 fee_token,
99 }
100 }
101
102 pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self {
104 Self::new(
105 config,
106 evm_opts,
107 self.available_artifacts.clone(),
108 self.running_artifact.clone(),
109 self.fee_token,
110 )
111 }
112
113 pub fn normalized_path(&self, path: impl AsRef<Path>) -> PathBuf {
117 let path = self.root.join(path);
118 canonicalize(&path).unwrap_or_else(|_| normalize_path(&path))
119 }
120
121 pub fn is_path_allowed(&self, path: impl AsRef<Path>, kind: FsAccessKind) -> bool {
128 self.is_normalized_path_allowed(&self.normalized_path(path), kind)
129 }
130
131 fn is_normalized_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
132 self.fs_permissions.is_path_allowed(path, kind)
133 }
134
135 pub fn ensure_path_allowed(
139 &self,
140 path: impl AsRef<Path>,
141 kind: FsAccessKind,
142 ) -> Result<PathBuf> {
143 let path = path.as_ref();
144 let normalized = self.normalized_path(path);
145 ensure!(
146 self.is_normalized_path_allowed(&normalized, kind),
147 "the path {} is not allowed to be accessed for {kind} operations",
148 normalized.strip_prefix(&self.root).unwrap_or(path).display()
149 );
150 Ok(normalized)
151 }
152
153 pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
157 let foundry_toml = self.root.join(Config::FILE_NAME);
162 Path::new(&foundry_toml.to_string_lossy().to_lowercase())
163 .starts_with(Path::new(&path.as_ref().to_string_lossy().to_lowercase()))
164 }
165
166 pub fn ensure_not_foundry_toml(&self, path: impl AsRef<Path>) -> Result<()> {
169 ensure!(!self.is_foundry_toml(path), "access to `foundry.toml` is not allowed");
170 Ok(())
171 }
172
173 pub fn rpc_endpoint(&self, url_or_alias: &str) -> Result<ResolvedRpcEndpoint> {
187 if let Some(endpoint) = self.rpc_endpoints.get(url_or_alias) {
188 Ok(endpoint.clone().try_resolve())
189 } else if let Some(builtin_url) = foundry_config::builtin_rpc_url(url_or_alias) {
190 let url = RpcEndpointUrl::Url(builtin_url.to_string());
191 Ok(RpcEndpoint::new(url).resolve())
192 } else {
193 if url_or_alias.starts_with("http") ||
195 url_or_alias.starts_with("ws") ||
196 Path::new(url_or_alias).exists()
198 {
199 let url = RpcEndpointUrl::Env(url_or_alias.to_string());
200 Ok(RpcEndpoint::new(url).resolve())
201 } else {
202 Err(fmt_err!("invalid rpc url: {url_or_alias}"))
203 }
204 }
205 }
206 pub fn rpc_urls(&self) -> Result<Vec<Rpc>> {
208 let mut urls = Vec::with_capacity(self.rpc_endpoints.len());
209 for alias in self.rpc_endpoints.keys() {
210 let url = self.rpc_endpoint(alias)?.url()?;
211 urls.push(Rpc { key: alias.clone(), url });
212 }
213 Ok(urls)
214 }
215}
216
217impl Default for CheatsConfig {
218 fn default() -> Self {
219 Self {
220 ffi: false,
221 always_use_create_2_factory: false,
222 prompt_timeout: Duration::from_secs(120),
223 rpc_storage_caching: Default::default(),
224 no_storage_caching: false,
225 rpc_endpoints: Default::default(),
226 paths: ProjectPathsConfig::builder().build_with_root("./"),
227 fs_permissions: Default::default(),
228 root: Default::default(),
229 bind_json_path: PathBuf::default().join("utils").join("jsonBindings.sol"),
230 broadcast: Default::default(),
231 evm_opts: Default::default(),
232 labels: Default::default(),
233 available_artifacts: Default::default(),
234 running_artifact: Default::default(),
235 assertions_revert: true,
236 seed: None,
237 internal_expect_revert: false,
238 fee_token: None,
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use foundry_config::fs_permissions::PathPermission;
247
248 fn config(root: &str, fs_permissions: FsPermissions) -> CheatsConfig {
249 CheatsConfig::new(
250 &Config { root: root.into(), fs_permissions, ..Default::default() },
251 Default::default(),
252 None,
253 None,
254 None,
255 )
256 }
257
258 #[test]
259 fn test_allowed_paths() {
260 let root = "/my/project/root/";
261 let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
262
263 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Read).is_ok());
264 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Write).is_ok());
265 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Read).is_ok());
266 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Write).is_ok());
267 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Read).is_err());
268 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Write).is_err());
269 }
270
271 #[test]
272 fn test_is_foundry_toml() {
273 let root = "/my/project/root/";
274 let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
275
276 let f = format!("{root}foundry.toml");
277 assert!(config.is_foundry_toml(f));
278
279 let f = format!("{root}Foundry.toml");
280 assert!(config.is_foundry_toml(f));
281
282 let f = format!("{root}lib/other/foundry.toml");
283 assert!(!config.is_foundry_toml(f));
284 }
285}