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