foundry_cheatcodes/
config.rs

1use super::Result;
2use crate::Vm::Rpc;
3use alloy_primitives::{map::AddressHashMap, U256};
4use foundry_common::{fs::normalize_path, ContractsByArtifact};
5use foundry_compilers::{utils::canonicalize, ArtifactId, ProjectPathsConfig};
6use foundry_config::{
7    cache::StorageCachingConfig, fs_permissions::FsAccessKind, Config, FsPermissions,
8    ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl,
9};
10use foundry_evm_core::opts::EvmOpts;
11use std::{
12    path::{Path, PathBuf},
13    time::Duration,
14};
15
16/// Additional, configurable context the `Cheatcodes` inspector has access to
17///
18/// This is essentially a subset of various `Config` settings `Cheatcodes` needs to know.
19#[derive(Clone, Debug)]
20pub struct CheatsConfig {
21    /// Whether the FFI cheatcode is enabled.
22    pub ffi: bool,
23    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
24    pub always_use_create_2_factory: bool,
25    /// Sets a timeout for vm.prompt cheatcodes
26    pub prompt_timeout: Duration,
27    /// RPC storage caching settings determines what chains and endpoints to cache
28    pub rpc_storage_caching: StorageCachingConfig,
29    /// Disables storage caching entirely.
30    pub no_storage_caching: bool,
31    /// All known endpoints and their aliases
32    pub rpc_endpoints: ResolvedRpcEndpoints,
33    /// Project's paths as configured
34    pub paths: ProjectPathsConfig,
35    /// Filesystem permissions for cheatcodes like `writeFile`, `readFile`
36    pub fs_permissions: FsPermissions,
37    /// Project root
38    pub root: PathBuf,
39    /// Absolute Path to broadcast dir i.e project_root/broadcast
40    pub broadcast: PathBuf,
41    /// Paths (directories) where file reading/writing is allowed
42    pub allowed_paths: Vec<PathBuf>,
43    /// How the evm was configured by the user
44    pub evm_opts: EvmOpts,
45    /// Address labels from config
46    pub labels: AddressHashMap<String>,
47    /// Artifacts which are guaranteed to be fresh (either recompiled or cached).
48    /// If Some, `vm.getDeployedCode` invocations are validated to be in scope of this list.
49    /// If None, no validation is performed.
50    pub available_artifacts: Option<ContractsByArtifact>,
51    /// Currently running artifact.
52    pub running_artifact: Option<ArtifactId>,
53    /// Whether to enable legacy (non-reverting) assertions.
54    pub assertions_revert: bool,
55    /// Optional seed for the RNG algorithm.
56    pub seed: Option<U256>,
57    /// Whether to allow `expectRevert` to work for internal calls.
58    pub internal_expect_revert: bool,
59}
60
61impl CheatsConfig {
62    /// Extracts the necessary settings from the Config
63    pub fn new(
64        config: &Config,
65        evm_opts: EvmOpts,
66        available_artifacts: Option<ContractsByArtifact>,
67        running_artifact: Option<ArtifactId>,
68    ) -> Self {
69        let mut allowed_paths = vec![config.root.clone()];
70        allowed_paths.extend(config.libs.iter().cloned());
71        allowed_paths.extend(config.allow_paths.iter().cloned());
72
73        let rpc_endpoints = config.rpc_endpoints.clone().resolved();
74        trace!(?rpc_endpoints, "using resolved rpc endpoints");
75
76        // If user explicitly disabled safety checks, do not set available_artifacts
77        let available_artifacts =
78            if config.unchecked_cheatcode_artifacts { None } else { available_artifacts };
79
80        Self {
81            ffi: evm_opts.ffi,
82            always_use_create_2_factory: evm_opts.always_use_create_2_factory,
83            prompt_timeout: Duration::from_secs(config.prompt_timeout),
84            rpc_storage_caching: config.rpc_storage_caching.clone(),
85            no_storage_caching: config.no_storage_caching,
86            rpc_endpoints,
87            paths: config.project_paths(),
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            allowed_paths,
92            evm_opts,
93            labels: config.labels.clone(),
94            available_artifacts,
95            running_artifact,
96            assertions_revert: config.assertions_revert,
97            seed: config.fuzz.seed,
98            internal_expect_revert: config.allow_internal_expect_revert,
99        }
100    }
101
102    /// Returns a new `CheatsConfig` configured with the given `Config` and `EvmOpts`.
103    pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self {
104        Self::new(config, evm_opts, self.available_artifacts.clone(), self.running_artifact.clone())
105    }
106
107    /// Attempts to canonicalize (see [std::fs::canonicalize]) the path.
108    ///
109    /// Canonicalization fails for non-existing paths, in which case we just normalize the path.
110    pub fn normalized_path(&self, path: impl AsRef<Path>) -> PathBuf {
111        let path = self.root.join(path);
112        canonicalize(&path).unwrap_or_else(|_| normalize_path(&path))
113    }
114
115    /// Returns true if the given path is allowed, if any path `allowed_paths` is an ancestor of the
116    /// path
117    ///
118    /// We only allow paths that are inside  allowed paths. To prevent path traversal
119    /// ("../../etc/passwd") we canonicalize/normalize the path first. We always join with the
120    /// configured root directory.
121    pub fn is_path_allowed(&self, path: impl AsRef<Path>, kind: FsAccessKind) -> bool {
122        self.is_normalized_path_allowed(&self.normalized_path(path), kind)
123    }
124
125    fn is_normalized_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
126        self.fs_permissions.is_path_allowed(path, kind)
127    }
128
129    /// Returns an error if no access is granted to access `path`, See also [Self::is_path_allowed]
130    ///
131    /// Returns the normalized version of `path`, see [`CheatsConfig::normalized_path`]
132    pub fn ensure_path_allowed(
133        &self,
134        path: impl AsRef<Path>,
135        kind: FsAccessKind,
136    ) -> Result<PathBuf> {
137        let path = path.as_ref();
138        let normalized = self.normalized_path(path);
139        ensure!(
140            self.is_normalized_path_allowed(&normalized, kind),
141            "the path {} is not allowed to be accessed for {kind} operations",
142            normalized.strip_prefix(&self.root).unwrap_or(path).display()
143        );
144        Ok(normalized)
145    }
146
147    /// Returns true if the given `path` is the project's foundry.toml file
148    ///
149    /// Note: this should be called with normalized path
150    pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
151        // path methods that do not access the filesystem are such as [`Path::starts_with`], are
152        // case-sensitive no matter the platform or filesystem. to make this case-sensitive
153        // we convert the underlying `OssStr` to lowercase checking that `path` and
154        // `foundry.toml` are the same file by comparing the FD, because it may not exist
155        let foundry_toml = self.root.join(Config::FILE_NAME);
156        Path::new(&foundry_toml.to_string_lossy().to_lowercase())
157            .starts_with(Path::new(&path.as_ref().to_string_lossy().to_lowercase()))
158    }
159
160    /// Same as [`Self::is_foundry_toml`] but returns an `Err` if [`Self::is_foundry_toml`] returns
161    /// true
162    pub fn ensure_not_foundry_toml(&self, path: impl AsRef<Path>) -> Result<()> {
163        ensure!(!self.is_foundry_toml(path), "access to `foundry.toml` is not allowed");
164        Ok(())
165    }
166
167    /// Returns the RPC to use
168    ///
169    /// If `url_or_alias` is a known alias in the `ResolvedRpcEndpoints` then it returns the
170    /// corresponding URL of that alias. otherwise this assumes `url_or_alias` is itself a URL
171    /// if it starts with a `http` or `ws` scheme.
172    ///
173    /// If the url is a path to an existing file, it is also considered a valid RPC URL, IPC path.
174    ///
175    /// # Errors
176    ///
177    ///  - Returns an error if `url_or_alias` is a known alias but references an unresolved env var.
178    ///  - Returns an error if `url_or_alias` is not an alias but does not start with a `http` or
179    ///    `ws` `scheme` and is not a path to an existing file
180    pub fn rpc_endpoint(&self, url_or_alias: &str) -> Result<ResolvedRpcEndpoint> {
181        if let Some(endpoint) = self.rpc_endpoints.get(url_or_alias) {
182            Ok(endpoint.clone().try_resolve())
183        } else {
184            // check if it's a URL or a path to an existing file to an ipc socket
185            if url_or_alias.starts_with("http") ||
186                url_or_alias.starts_with("ws") ||
187                // check for existing ipc file
188                Path::new(url_or_alias).exists()
189            {
190                let url = RpcEndpointUrl::Env(url_or_alias.to_string());
191                Ok(RpcEndpoint::new(url).resolve())
192            } else {
193                Err(fmt_err!("invalid rpc url: {url_or_alias}"))
194            }
195        }
196    }
197    /// Returns all the RPC urls and their alias.
198    pub fn rpc_urls(&self) -> Result<Vec<Rpc>> {
199        let mut urls = Vec::with_capacity(self.rpc_endpoints.len());
200        for alias in self.rpc_endpoints.keys() {
201            let url = self.rpc_endpoint(alias)?.url()?;
202            urls.push(Rpc { key: alias.clone(), url });
203        }
204        Ok(urls)
205    }
206}
207
208impl Default for CheatsConfig {
209    fn default() -> Self {
210        Self {
211            ffi: false,
212            always_use_create_2_factory: false,
213            prompt_timeout: Duration::from_secs(120),
214            rpc_storage_caching: Default::default(),
215            no_storage_caching: false,
216            rpc_endpoints: Default::default(),
217            paths: ProjectPathsConfig::builder().build_with_root("./"),
218            fs_permissions: Default::default(),
219            root: Default::default(),
220            broadcast: Default::default(),
221            allowed_paths: vec![],
222            evm_opts: Default::default(),
223            labels: Default::default(),
224            available_artifacts: Default::default(),
225            running_artifact: Default::default(),
226            assertions_revert: true,
227            seed: None,
228            internal_expect_revert: false,
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use foundry_config::fs_permissions::PathPermission;
237
238    fn config(root: &str, fs_permissions: FsPermissions) -> CheatsConfig {
239        CheatsConfig::new(
240            &Config { root: root.into(), fs_permissions, ..Default::default() },
241            Default::default(),
242            None,
243            None,
244        )
245    }
246
247    #[test]
248    fn test_allowed_paths() {
249        let root = "/my/project/root/";
250        let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
251
252        assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Read).is_ok());
253        assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Write).is_ok());
254        assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Read).is_ok());
255        assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Write).is_ok());
256        assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Read).is_err());
257        assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Write).is_err());
258    }
259
260    #[test]
261    fn test_is_foundry_toml() {
262        let root = "/my/project/root/";
263        let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
264
265        let f = format!("{root}foundry.toml");
266        assert!(config.is_foundry_toml(f));
267
268        let f = format!("{root}Foundry.toml");
269        assert!(config.is_foundry_toml(f));
270
271        let f = format!("{root}lib/other/foundry.toml");
272        assert!(!config.is_foundry_toml(f));
273    }
274}