Skip to main content

foundry_cheatcodes/
config.rs

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