Skip to main content

foundry_cheatcodes/
config.rs

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 foundry_evm_traces::identifier::ExternalIdentifierConfig;
12use std::{
13    path::{Path, PathBuf},
14    time::Duration,
15};
16
17/// Additional, configurable context the `Cheatcodes` inspector has access to
18///
19/// This is essentially a subset of various `Config` settings `Cheatcodes` needs to know.
20#[derive(Clone, Debug)]
21pub struct CheatsConfig {
22    /// Whether the FFI cheatcode is enabled.
23    pub ffi: bool,
24    /// Cheatcode selectors rejected before dispatch for restricted executions.
25    pub blocked_cheatcodes: Vec<[u8; 4]>,
26    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
27    pub always_use_create_2_factory: bool,
28    /// Rewrite plain CREATE to CREATE2 for `forge script --batch`.
29    pub batch_rewrite_creates: bool,
30    /// Sets a timeout for vm.prompt cheatcodes
31    pub prompt_timeout: Duration,
32    /// RPC storage caching settings determines what chains and endpoints to cache
33    pub rpc_storage_caching: StorageCachingConfig,
34    /// Disables storage caching entirely.
35    pub no_storage_caching: bool,
36    /// All known endpoints and their aliases
37    pub rpc_endpoints: ResolvedRpcEndpoints,
38    /// Project's paths as configured
39    pub paths: ProjectPathsConfig,
40    /// Path to the directory that contains the bindings generated by `forge bind-json`.
41    pub bind_json_path: PathBuf,
42    /// Filesystem permissions for cheatcodes like `writeFile`, `readFile`
43    pub fs_permissions: FsPermissions,
44    /// Project root
45    pub root: PathBuf,
46    /// Absolute Path to broadcast dir i.e project_root/broadcast
47    pub broadcast: PathBuf,
48    /// Whether isolated test execution is enabled.
49    pub isolate: bool,
50    /// How the evm was configured by the user
51    pub evm_opts: EvmOpts,
52    /// Address labels from config
53    pub labels: AddressHashMap<String>,
54    /// Artifacts which are guaranteed to be fresh (either recompiled or cached).
55    /// If Some, `vm.getDeployedCode` invocations are validated to be in scope of this list.
56    /// If None, no validation is performed.
57    pub available_artifacts: Option<ContractsByArtifact>,
58    /// Artifacts used to resolve cheatcode artifact references.
59    /// Unlike `available_artifacts`, this is retained when artifact safety checks are disabled.
60    pub artifact_lookup: Option<ContractsByArtifact>,
61    /// Whether to decode the storage layouts of contracts outside the local project in state
62    /// diffs, by fetching their verified source code from a block explorer.
63    pub decode_external_storage: bool,
64    /// Settings for looking contracts up on block explorers, resolved lazily against the chain a
65    /// test is running on: a `vm.createSelectFork` can change it after this config was built.
66    pub external_sources: ExternalIdentifierConfig,
67    /// Currently running artifact.
68    pub running_artifact: Option<ArtifactId>,
69    /// Whether to enable legacy (non-reverting) assertions.
70    pub assertions_revert: bool,
71    /// Optional seed for the RNG algorithm.
72    pub seed: Option<U256>,
73    /// Whether to allow `expectRevert` to work for internal calls.
74    pub internal_expect_revert: bool,
75}
76
77impl CheatsConfig {
78    /// Extracts the necessary settings from the Config
79    pub fn new(
80        config: &Config,
81        evm_opts: EvmOpts,
82        available_artifacts: Option<ContractsByArtifact>,
83        running_artifact: Option<ArtifactId>,
84        batch_rewrite_creates: bool,
85    ) -> Self {
86        let rpc_endpoints = config.rpc_endpoints.clone().resolved();
87        trace!(?rpc_endpoints, "using resolved rpc endpoints");
88
89        let artifact_lookup = available_artifacts.clone();
90        // If user explicitly disabled safety checks, do not set available_artifacts.
91        let available_artifacts =
92            if config.unchecked_cheatcode_artifacts { None } else { available_artifacts };
93        let mut labels = config.labels.clone();
94        labels.extend(config.tracing.labels.clone());
95
96        Self {
97            ffi: evm_opts.ffi,
98            blocked_cheatcodes: Vec::new(),
99            always_use_create_2_factory: evm_opts.always_use_create_2_factory,
100            batch_rewrite_creates,
101            prompt_timeout: Duration::from_secs(config.prompt_timeout),
102            rpc_storage_caching: config.rpc_storage_caching.clone(),
103            no_storage_caching: config.no_storage_caching,
104            rpc_endpoints,
105            paths: config.project_paths(),
106            bind_json_path: config.bind_json.out.clone(),
107            fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()),
108            root: config.root.clone(),
109            broadcast: config.root.clone().join(&config.broadcast),
110            isolate: config.isolate,
111            evm_opts,
112            labels,
113            available_artifacts,
114            artifact_lookup,
115            decode_external_storage: config.decode_external_storage,
116            external_sources: ExternalIdentifierConfig::new(config),
117            running_artifact,
118            assertions_revert: config.assertions_revert,
119            seed: config.fuzz.seed,
120            internal_expect_revert: config.allow_internal_expect_revert,
121        }
122    }
123
124    /// Returns a new `CheatsConfig` configured with the given `Config` and `EvmOpts`.
125    pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self {
126        let mut cloned = Self::new(
127            config,
128            evm_opts,
129            self.artifact_lookup.clone().or_else(|| self.available_artifacts.clone()),
130            self.running_artifact.clone(),
131            self.batch_rewrite_creates,
132        );
133        cloned.blocked_cheatcodes.clone_from(&self.blocked_cheatcodes);
134        cloned
135    }
136
137    /// Attempts to canonicalize (see [std::fs::canonicalize]) the path.
138    ///
139    /// Canonicalization fails for non-existing paths, in which case we just normalize the path.
140    pub fn normalized_path(&self, path: impl AsRef<Path>) -> PathBuf {
141        let path = self.root.join(path);
142        canonicalize(&path).unwrap_or_else(|_| canonicalize_existing_ancestor(&path))
143    }
144
145    /// Returns true if the given path is allowed, if any path `allowed_paths` is an ancestor of the
146    /// path
147    ///
148    /// We only allow paths that are inside  allowed paths. To prevent path traversal
149    /// ("../../etc/passwd") we canonicalize/normalize the path first. We always join with the
150    /// configured root directory.
151    pub fn is_path_allowed(&self, path: impl AsRef<Path>, kind: FsAccessKind) -> bool {
152        self.is_normalized_path_allowed(&self.normalized_path(path), kind)
153    }
154
155    fn is_normalized_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
156        self.fs_permissions.is_path_allowed(path, kind)
157    }
158
159    /// Returns an error if no access is granted to access `path`, See also [Self::is_path_allowed]
160    ///
161    /// Returns the normalized version of `path`, see [`CheatsConfig::normalized_path`]
162    pub fn ensure_path_allowed(
163        &self,
164        path: impl AsRef<Path>,
165        kind: FsAccessKind,
166    ) -> Result<PathBuf> {
167        let path = path.as_ref();
168        let normalized = self.normalized_path(path);
169        ensure!(
170            self.is_normalized_path_allowed(&normalized, kind),
171            "the path {} is not allowed to be accessed for {kind} operations",
172            normalized.strip_prefix(&self.root).unwrap_or(path).display()
173        );
174        Ok(normalized)
175    }
176
177    /// Returns true if the given `path` is the project's foundry.toml file
178    ///
179    /// Note: this should be called with normalized path
180    pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
181        // path methods that do not access the filesystem are such as [`Path::starts_with`], are
182        // case-sensitive no matter the platform or filesystem. to make this case-sensitive
183        // we convert the underlying `OssStr` to lowercase checking that `path` and
184        // `foundry.toml` are the same file by comparing the FD, because it may not exist
185        let foundry_toml = self.root.join(Config::FILE_NAME);
186        Path::new(&foundry_toml.to_string_lossy().to_lowercase())
187            .starts_with(Path::new(&path.as_ref().to_string_lossy().to_lowercase()))
188    }
189
190    /// Same as [`Self::is_foundry_toml`] but returns an `Err` if [`Self::is_foundry_toml`] returns
191    /// true
192    pub fn ensure_not_foundry_toml(&self, path: impl AsRef<Path>) -> Result<()> {
193        ensure!(!self.is_foundry_toml(path), "access to `foundry.toml` is not allowed");
194        Ok(())
195    }
196
197    /// Returns the RPC to use
198    ///
199    /// If `url_or_alias` is a known alias in the `ResolvedRpcEndpoints` then it returns the
200    /// corresponding URL of that alias. otherwise this assumes `url_or_alias` is itself a URL
201    /// if it starts with a `http` or `ws` scheme.
202    ///
203    /// If the url is a path to an existing file, it is also considered a valid RPC URL, IPC path.
204    ///
205    /// # Errors
206    ///
207    ///  - Returns an error if `url_or_alias` is a known alias but references an unresolved env var.
208    ///  - Returns an error if `url_or_alias` is not an alias but does not start with a `http` or
209    ///    `ws` `scheme` and is not a path to an existing file
210    pub fn rpc_endpoint(&self, url_or_alias: &str) -> Result<ResolvedRpcEndpoint> {
211        if let Some(endpoint) = self.rpc_endpoints.get(url_or_alias) {
212            Ok(endpoint.clone().try_resolve())
213        } else if let Some(builtin_url) = foundry_config::builtin_rpc_url(url_or_alias) {
214            let url = RpcEndpointUrl::Url(builtin_url.to_string());
215            Ok(RpcEndpoint::new(url).resolve())
216        } else {
217            // check if it's a URL or a path to an existing file to an ipc socket
218            if url_or_alias.starts_with("http") ||
219                url_or_alias.starts_with("ws") ||
220                // check for existing ipc file
221                Path::new(url_or_alias).exists()
222            {
223                let url = RpcEndpointUrl::Env(url_or_alias.to_string());
224                Ok(RpcEndpoint::new(url).resolve())
225            } else {
226                Err(fmt_err!("invalid rpc url: {url_or_alias}"))
227            }
228        }
229    }
230    /// Returns all the RPC urls and their alias.
231    pub fn rpc_urls(&self) -> Result<Vec<Rpc>> {
232        let mut urls = Vec::with_capacity(self.rpc_endpoints.len());
233        for alias in self.rpc_endpoints.keys() {
234            let url = self.rpc_endpoint(alias)?.url()?;
235            urls.push(Rpc { key: alias.clone(), url });
236        }
237        Ok(urls)
238    }
239}
240
241impl Default for CheatsConfig {
242    fn default() -> Self {
243        Self {
244            ffi: false,
245            blocked_cheatcodes: Vec::new(),
246            always_use_create_2_factory: false,
247            batch_rewrite_creates: false,
248            prompt_timeout: Duration::from_secs(120),
249            rpc_storage_caching: Default::default(),
250            no_storage_caching: false,
251            rpc_endpoints: Default::default(),
252            paths: ProjectPathsConfig::builder().build_with_root("./"),
253            fs_permissions: Default::default(),
254            root: Default::default(),
255            bind_json_path: PathBuf::default().join("utils").join("jsonBindings.sol"),
256            broadcast: Default::default(),
257            isolate: Config::default().isolate,
258            evm_opts: Default::default(),
259            labels: Default::default(),
260            available_artifacts: Default::default(),
261            artifact_lookup: Default::default(),
262            decode_external_storage: false,
263            external_sources: Default::default(),
264            running_artifact: Default::default(),
265            assertions_revert: true,
266            seed: None,
267            internal_expect_revert: false,
268        }
269    }
270}
271
272fn canonicalize_existing_ancestor(path: &Path) -> PathBuf {
273    let normalized = normalize_path(path);
274    let mut missing = Vec::new();
275    let mut ancestor = normalized.as_path();
276    while !ancestor.exists() {
277        let Some(name) = ancestor.file_name() else { return normalized };
278        missing.push(name.to_owned());
279        let Some(parent) = ancestor.parent() else { return normalized };
280        ancestor = parent;
281    }
282
283    let mut path = canonicalize(ancestor).unwrap_or_else(|_| ancestor.to_path_buf());
284    for component in missing.iter().rev() {
285        path.push(component);
286    }
287    path
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use alloy_primitives::address;
294    use foundry_config::fs_permissions::PathPermission;
295    use tempfile::TempDir;
296
297    fn config(root: &Path, fs_permissions: FsPermissions) -> CheatsConfig {
298        CheatsConfig::new(
299            &Config { root: root.into(), fs_permissions, ..Default::default() },
300            Default::default(),
301            None,
302            None,
303            false,
304        )
305    }
306
307    #[test]
308    fn test_allowed_paths() {
309        let temp = TempDir::new().unwrap();
310        let root = temp.path().join("my/project/root");
311        std::fs::create_dir_all(&root).unwrap();
312        let config = config(&root, FsPermissions::new(vec![PathPermission::read_write("./")]));
313
314        assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Read).is_ok());
315        assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Write).is_ok());
316        assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Read).is_ok());
317        assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Write).is_ok());
318        assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Read).is_err());
319        assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Write).is_err());
320    }
321
322    #[test]
323    fn test_batch_rewrite_creates_flag_plumbing() {
324        assert!(!CheatsConfig::default().batch_rewrite_creates);
325
326        let on = CheatsConfig::new(&Config::default(), Default::default(), None, None, true);
327        assert!(on.batch_rewrite_creates);
328
329        let cloned = on.clone_with(&Config::default(), Default::default());
330        assert!(cloned.batch_rewrite_creates);
331    }
332
333    #[test]
334    fn unchecked_artifacts_retain_lookup_without_validation() {
335        let config = Config { unchecked_cheatcode_artifacts: true, ..Default::default() };
336        let cheats = CheatsConfig::new(
337            &config,
338            Default::default(),
339            Some(ContractsByArtifact::default()),
340            None,
341            false,
342        );
343
344        assert!(cheats.available_artifacts.is_none());
345        assert!(cheats.artifact_lookup.is_some());
346
347        let cloned = cheats.clone_with(&config, Default::default());
348        assert!(cloned.available_artifacts.is_none());
349        assert!(cloned.artifact_lookup.is_some());
350    }
351
352    #[test]
353    fn clone_with_preserves_available_artifacts_without_lookup() {
354        let cheats = CheatsConfig {
355            available_artifacts: Some(ContractsByArtifact::default()),
356            ..Default::default()
357        };
358
359        let cloned = cheats.clone_with(&Config::default(), Default::default());
360        assert!(cloned.available_artifacts.is_some());
361        assert!(cloned.artifact_lookup.is_some());
362    }
363
364    #[test]
365    fn tracing_labels_override_legacy_labels() {
366        let address = address!("0x0000000000000000000000000000000000000001");
367        let mut config = Config::default();
368        config.labels.insert(address, "legacy".to_string());
369        config.tracing.labels.insert(address, "canonical".to_string());
370
371        let config = CheatsConfig::new(&config, Default::default(), None, None, false);
372
373        assert_eq!(config.labels.get(&address).map(String::as_str), Some("canonical"));
374    }
375
376    #[test]
377    fn test_is_foundry_toml() {
378        let root = Path::new("/my/project/root/");
379        let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
380
381        let f = root.join("foundry.toml");
382        assert!(config.is_foundry_toml(f));
383
384        let f = root.join("Foundry.toml");
385        assert!(config.is_foundry_toml(f));
386
387        let f = root.join("lib/other/foundry.toml");
388        assert!(!config.is_foundry_toml(f));
389    }
390}