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 std::{
12 collections::HashMap,
13 path::{Path, PathBuf},
14 time::Duration,
15};
16
17#[derive(Clone, Debug)]
21pub struct CheatsConfig {
22 pub ffi: bool,
24 pub always_use_create_2_factory: bool,
26 pub prompt_timeout: Duration,
28 pub rpc_storage_caching: StorageCachingConfig,
30 pub no_storage_caching: bool,
32 pub rpc_endpoints: ResolvedRpcEndpoints,
34 pub paths: ProjectPathsConfig,
36 pub bind_json_path: PathBuf,
38 pub fs_permissions: FsPermissions,
40 pub root: PathBuf,
42 pub broadcast: PathBuf,
44 pub allowed_paths: Vec<PathBuf>,
46 pub evm_opts: EvmOpts,
48 pub labels: AddressHashMap<String>,
50 pub available_artifacts: Option<ContractsByArtifact>,
54 pub running_artifact: Option<ArtifactId>,
56 pub assertions_revert: bool,
58 pub seed: Option<U256>,
60 pub internal_expect_revert: bool,
62 pub chains: HashMap<String, ChainData>,
64 pub chain_id_to_alias: HashMap<u64, String>,
66}
67
68#[derive(Clone, Debug)]
70pub struct ChainData {
71 pub name: String,
72 pub chain_id: u64,
73 pub default_rpc_url: String, }
75
76impl CheatsConfig {
77 pub fn new(
79 config: &Config,
80 evm_opts: EvmOpts,
81 available_artifacts: Option<ContractsByArtifact>,
82 running_artifact: Option<ArtifactId>,
83 ) -> Self {
84 let mut allowed_paths = vec![config.root.clone()];
85 allowed_paths.extend(config.libs.iter().cloned());
86 allowed_paths.extend(config.allow_paths.iter().cloned());
87
88 let rpc_endpoints = config.rpc_endpoints.clone().resolved();
89 trace!(?rpc_endpoints, "using resolved rpc endpoints");
90
91 let available_artifacts =
93 if config.unchecked_cheatcode_artifacts { None } else { available_artifacts };
94
95 Self {
96 ffi: evm_opts.ffi,
97 always_use_create_2_factory: evm_opts.always_use_create_2_factory,
98 prompt_timeout: Duration::from_secs(config.prompt_timeout),
99 rpc_storage_caching: config.rpc_storage_caching.clone(),
100 no_storage_caching: config.no_storage_caching,
101 rpc_endpoints,
102 paths: config.project_paths(),
103 bind_json_path: config.bind_json.out.clone(),
104 fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()),
105 root: config.root.clone(),
106 broadcast: config.root.clone().join(&config.broadcast),
107 allowed_paths,
108 evm_opts,
109 labels: config.labels.clone(),
110 available_artifacts,
111 running_artifact,
112 assertions_revert: config.assertions_revert,
113 seed: config.fuzz.seed,
114 internal_expect_revert: config.allow_internal_expect_revert,
115 chains: HashMap::new(),
116 chain_id_to_alias: HashMap::new(),
117 }
118 }
119
120 pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self {
122 Self::new(config, evm_opts, self.available_artifacts.clone(), self.running_artifact.clone())
123 }
124
125 pub fn normalized_path(&self, path: impl AsRef<Path>) -> PathBuf {
129 let path = self.root.join(path);
130 canonicalize(&path).unwrap_or_else(|_| normalize_path(&path))
131 }
132
133 pub fn is_path_allowed(&self, path: impl AsRef<Path>, kind: FsAccessKind) -> bool {
140 self.is_normalized_path_allowed(&self.normalized_path(path), kind)
141 }
142
143 fn is_normalized_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
144 self.fs_permissions.is_path_allowed(path, kind)
145 }
146
147 pub fn ensure_path_allowed(
151 &self,
152 path: impl AsRef<Path>,
153 kind: FsAccessKind,
154 ) -> Result<PathBuf> {
155 let path = path.as_ref();
156 let normalized = self.normalized_path(path);
157 ensure!(
158 self.is_normalized_path_allowed(&normalized, kind),
159 "the path {} is not allowed to be accessed for {kind} operations",
160 normalized.strip_prefix(&self.root).unwrap_or(path).display()
161 );
162 Ok(normalized)
163 }
164
165 pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
169 let foundry_toml = self.root.join(Config::FILE_NAME);
174 Path::new(&foundry_toml.to_string_lossy().to_lowercase())
175 .starts_with(Path::new(&path.as_ref().to_string_lossy().to_lowercase()))
176 }
177
178 pub fn ensure_not_foundry_toml(&self, path: impl AsRef<Path>) -> Result<()> {
181 ensure!(!self.is_foundry_toml(path), "access to `foundry.toml` is not allowed");
182 Ok(())
183 }
184
185 pub fn rpc_endpoint(&self, url_or_alias: &str) -> Result<ResolvedRpcEndpoint> {
199 if let Some(endpoint) = self.rpc_endpoints.get(url_or_alias) {
200 Ok(endpoint.clone().try_resolve())
201 } else {
202 if url_or_alias.starts_with("http") ||
204 url_or_alias.starts_with("ws") ||
205 Path::new(url_or_alias).exists()
207 {
208 let url = RpcEndpointUrl::Env(url_or_alias.to_string());
209 Ok(RpcEndpoint::new(url).resolve())
210 } else {
211 Err(fmt_err!("invalid rpc url: {url_or_alias}"))
212 }
213 }
214 }
215 pub fn rpc_urls(&self) -> Result<Vec<Rpc>> {
217 let mut urls = Vec::with_capacity(self.rpc_endpoints.len());
218 for alias in self.rpc_endpoints.keys() {
219 let url = self.rpc_endpoint(alias)?.url()?;
220 urls.push(Rpc { key: alias.clone(), url });
221 }
222 Ok(urls)
223 }
224}
225
226impl Default for CheatsConfig {
227 fn default() -> Self {
228 Self {
229 ffi: false,
230 always_use_create_2_factory: false,
231 prompt_timeout: Duration::from_secs(120),
232 rpc_storage_caching: Default::default(),
233 no_storage_caching: false,
234 rpc_endpoints: Default::default(),
235 paths: ProjectPathsConfig::builder().build_with_root("./"),
236 fs_permissions: Default::default(),
237 root: Default::default(),
238 bind_json_path: PathBuf::default().join("utils").join("jsonBindings.sol"),
239 broadcast: Default::default(),
240 allowed_paths: vec![],
241 evm_opts: Default::default(),
242 labels: Default::default(),
243 available_artifacts: Default::default(),
244 running_artifact: Default::default(),
245 assertions_revert: true,
246 seed: None,
247 internal_expect_revert: false,
248 chains: HashMap::new(),
249 chain_id_to_alias: HashMap::new(),
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use foundry_config::fs_permissions::PathPermission;
258
259 fn config(root: &str, fs_permissions: FsPermissions) -> CheatsConfig {
260 CheatsConfig::new(
261 &Config { root: root.into(), fs_permissions, ..Default::default() },
262 Default::default(),
263 None,
264 None,
265 )
266 }
267
268 #[test]
269 fn test_allowed_paths() {
270 let root = "/my/project/root/";
271 let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
272
273 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Read).is_ok());
274 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Write).is_ok());
275 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Read).is_ok());
276 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Write).is_ok());
277 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Read).is_err());
278 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Write).is_err());
279 }
280
281 #[test]
282 fn test_is_foundry_toml() {
283 let root = "/my/project/root/";
284 let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
285
286 let f = format!("{root}foundry.toml");
287 assert!(config.is_foundry_toml(f));
288
289 let f = format!("{root}Foundry.toml");
290 assert!(config.is_foundry_toml(f));
291
292 let f = format!("{root}lib/other/foundry.toml");
293 assert!(!config.is_foundry_toml(f));
294 }
295}