Skip to main content

foundry_cheatcodes/
config.rs

1use super::Result;
2use crate::{CheatcodesStrategy, 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/// 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    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
25    pub always_use_create_2_factory: bool,
26    /// Sets a timeout for vm.prompt cheatcodes
27    pub prompt_timeout: Duration,
28    /// RPC storage caching settings determines what chains and endpoints to cache
29    pub rpc_storage_caching: StorageCachingConfig,
30    /// Disables storage caching entirely.
31    pub no_storage_caching: bool,
32    /// All known endpoints and their aliases
33    pub rpc_endpoints: ResolvedRpcEndpoints,
34    /// Project's paths as configured
35    pub paths: ProjectPathsConfig,
36    /// Path to the directory that contains the bindings generated by `forge bind-json`.
37    pub bind_json_path: PathBuf,
38    /// Filesystem permissions for cheatcodes like `writeFile`, `readFile`
39    pub fs_permissions: FsPermissions,
40    /// Project root
41    pub root: PathBuf,
42    /// Absolute Path to broadcast dir i.e project_root/broadcast
43    pub broadcast: PathBuf,
44    /// Paths (directories) where file reading/writing is allowed
45    pub allowed_paths: Vec<PathBuf>,
46    /// How the evm was configured by the user
47    pub evm_opts: EvmOpts,
48    /// Address labels from config
49    pub labels: AddressHashMap<String>,
50    /// Artifacts which are guaranteed to be fresh (either recompiled or cached).
51    /// If Some, `vm.getDeployedCode` invocations are validated to be in scope of this list.
52    /// If None, no validation is performed.
53    pub available_artifacts: Option<ContractsByArtifact>,
54    /// Currently running artifact.
55    pub running_artifact: Option<ArtifactId>,
56    /// Whether to enable legacy (non-reverting) assertions.
57    pub assertions_revert: bool,
58    /// Optional seed for the RNG algorithm.
59    pub seed: Option<U256>,
60    /// Whether to allow `expectRevert` to work for internal calls.
61    pub internal_expect_revert: bool,
62    /// Mapping of chain aliases to chain data
63    pub chains: HashMap<String, ChainData>,
64    /// Mapping of chain IDs to their aliases
65    pub chain_id_to_alias: HashMap<u64, String>,
66    /// Cheatcode inspector behavior.
67    pub strategy: CheatcodesStrategy,
68    /// Whether to use PVM mode instead of EVM
69    pub pvm_enabled: bool,
70}
71
72/// Chain data for getChain cheatcodes
73#[derive(Clone, Debug)]
74pub struct ChainData {
75    pub name: String,
76    pub chain_id: u64,
77    pub default_rpc_url: String, // Store default RPC URL
78}
79
80impl CheatsConfig {
81    /// Extracts the necessary settings from the Config
82    pub fn new(
83        strategy: CheatcodesStrategy,
84        config: &Config,
85        evm_opts: EvmOpts,
86        available_artifacts: Option<ContractsByArtifact>,
87        running_artifact: Option<ArtifactId>,
88    ) -> Self {
89        let mut allowed_paths = vec![config.root.clone()];
90        allowed_paths.extend(config.libs.iter().cloned());
91        allowed_paths.extend(config.allow_paths.iter().cloned());
92
93        let rpc_endpoints = config.rpc_endpoints.clone().resolved();
94        trace!(?rpc_endpoints, "using resolved rpc endpoints");
95
96        // If user explicitly disabled safety checks, do not set available_artifacts
97        let available_artifacts =
98            if config.unchecked_cheatcode_artifacts { None } else { available_artifacts };
99
100        Self {
101            ffi: evm_opts.ffi,
102            always_use_create_2_factory: evm_opts.always_use_create_2_factory,
103            prompt_timeout: Duration::from_secs(config.prompt_timeout),
104            rpc_storage_caching: config.rpc_storage_caching.clone(),
105            no_storage_caching: config.no_storage_caching,
106            rpc_endpoints,
107            paths: config.project_paths(),
108            bind_json_path: config.bind_json.out.clone(),
109            fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()),
110            root: config.root.clone(),
111            broadcast: config.root.clone().join(&config.broadcast),
112            allowed_paths,
113            evm_opts,
114            labels: config.labels.clone(),
115            available_artifacts,
116            running_artifact,
117            assertions_revert: config.assertions_revert,
118            seed: config.fuzz.seed,
119            internal_expect_revert: config.allow_internal_expect_revert,
120            chains: HashMap::new(),
121            chain_id_to_alias: HashMap::new(),
122            strategy,
123            pvm_enabled: false,
124        }
125    }
126
127    /// Returns a new `CheatsConfig` configured with the given `Config` and `EvmOpts`.
128    pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self {
129        let mut new_config = Self::new(
130            self.strategy.clone(),
131            config,
132            evm_opts,
133            self.available_artifacts.clone(),
134            self.running_artifact.clone(),
135        );
136        new_config.pvm_enabled = self.pvm_enabled;
137        new_config
138    }
139
140    /// Attempts to canonicalize (see [std::fs::canonicalize]) the path.
141    ///
142    /// Canonicalization fails for non-existing paths, in which case we just normalize the path.
143    pub fn normalized_path(&self, path: impl AsRef<Path>) -> PathBuf {
144        let path = self.root.join(path);
145        canonicalize(&path).unwrap_or_else(|_| normalize_path(&path))
146    }
147
148    /// Returns true if the given path is allowed, if any path `allowed_paths` is an ancestor of the
149    /// path
150    ///
151    /// We only allow paths that are inside  allowed paths. To prevent path traversal
152    /// ("../../etc/passwd") we canonicalize/normalize the path first. We always join with the
153    /// configured root directory.
154    pub fn is_path_allowed(&self, path: impl AsRef<Path>, kind: FsAccessKind) -> bool {
155        self.is_normalized_path_allowed(&self.normalized_path(path), kind)
156    }
157
158    fn is_normalized_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
159        self.fs_permissions.is_path_allowed(path, kind)
160    }
161
162    /// Returns an error if no access is granted to access `path`, See also [Self::is_path_allowed]
163    ///
164    /// Returns the normalized version of `path`, see [`CheatsConfig::normalized_path`]
165    pub fn ensure_path_allowed(
166        &self,
167        path: impl AsRef<Path>,
168        kind: FsAccessKind,
169    ) -> Result<PathBuf> {
170        let path = path.as_ref();
171        let normalized = self.normalized_path(path);
172        ensure!(
173            self.is_normalized_path_allowed(&normalized, kind),
174            "the path {} is not allowed to be accessed for {kind} operations",
175            normalized.strip_prefix(&self.root).unwrap_or(path).display()
176        );
177        Ok(normalized)
178    }
179
180    /// Returns true if the given `path` is the project's foundry.toml file
181    ///
182    /// Note: this should be called with normalized path
183    pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
184        // path methods that do not access the filesystem are such as [`Path::starts_with`], are
185        // case-sensitive no matter the platform or filesystem. to make this case-sensitive
186        // we convert the underlying `OssStr` to lowercase checking that `path` and
187        // `foundry.toml` are the same file by comparing the FD, because it may not exist
188        let foundry_toml = self.root.join(Config::FILE_NAME);
189        Path::new(&foundry_toml.to_string_lossy().to_lowercase())
190            .starts_with(Path::new(&path.as_ref().to_string_lossy().to_lowercase()))
191    }
192
193    /// Same as [`Self::is_foundry_toml`] but returns an `Err` if [`Self::is_foundry_toml`] returns
194    /// true
195    pub fn ensure_not_foundry_toml(&self, path: impl AsRef<Path>) -> Result<()> {
196        ensure!(!self.is_foundry_toml(path), "access to `foundry.toml` is not allowed");
197        Ok(())
198    }
199
200    /// Returns the RPC to use
201    ///
202    /// If `url_or_alias` is a known alias in the `ResolvedRpcEndpoints` then it returns the
203    /// corresponding URL of that alias. otherwise this assumes `url_or_alias` is itself a URL
204    /// if it starts with a `http` or `ws` scheme.
205    ///
206    /// If the url is a path to an existing file, it is also considered a valid RPC URL, IPC path.
207    ///
208    /// # Errors
209    ///
210    ///  - Returns an error if `url_or_alias` is a known alias but references an unresolved env var.
211    ///  - Returns an error if `url_or_alias` is not an alias but does not start with a `http` or
212    ///    `ws` `scheme` and is not a path to an existing file
213    pub fn rpc_endpoint(&self, url_or_alias: &str) -> Result<ResolvedRpcEndpoint> {
214        if let Some(endpoint) = self.rpc_endpoints.get(url_or_alias) {
215            Ok(endpoint.clone().try_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    /// Initialize default chain data (similar to initializeStdChains in Solidity)
241    pub fn initialize_chain_data(&mut self) {
242        if !self.chains.is_empty() {
243            return; // Already initialized
244        }
245
246        // Use the same function to create chains
247        let chains = create_default_chains();
248
249        // Add all chains to the config
250        for (alias, data) in chains {
251            self.set_chain_with_default_rpc_url(&alias, data);
252        }
253    }
254
255    /// Set chain with default RPC URL (similar to setChainWithDefaultRpcUrl in Solidity)
256    pub fn set_chain_with_default_rpc_url(&mut self, alias: &str, data: ChainData) {
257        // Store the default RPC URL is already stored in the data
258        // No need to clone it separately
259
260        // Add chain data
261        self.set_chain_data(alias, data);
262    }
263
264    /// Set chain data for a specific alias
265    pub fn set_chain_data(&mut self, alias: &str, data: ChainData) {
266        // Remove old chain ID mapping if it exists
267        if let Some(old_data) = self.chains.get(alias) {
268            self.chain_id_to_alias.remove(&old_data.chain_id);
269        }
270
271        // Add new mappings
272        self.chain_id_to_alias.insert(data.chain_id, alias.to_string());
273        self.chains.insert(alias.to_string(), data);
274    }
275
276    /// Get chain data by alias
277    pub fn get_chain_data_by_alias_non_mut(&self, alias: &str) -> Result<ChainData> {
278        // Initialize chains if not already done
279        if self.chains.is_empty() {
280            // Create a temporary copy with initialized chains
281            // This is inefficient but handles the edge case
282            let temp_chains = create_default_chains();
283
284            if let Some(data) = temp_chains.get(alias) {
285                return Ok(data.clone());
286            }
287        } else {
288            // Normal path - chains are initialized
289            if let Some(data) = self.chains.get(alias) {
290                return Ok(data.clone());
291            }
292        }
293
294        // Chain not found in either case
295        Err(fmt_err!("vm.getChain: Chain with alias \"{}\" not found", alias))
296    }
297
298    /// Get RPC URL for an alias
299    pub fn get_rpc_url_non_mut(&self, alias: &str) -> Result<String> {
300        // Try to get from config first
301        match self.rpc_endpoint(alias) {
302            Ok(endpoint) => Ok(endpoint.url()?),
303            Err(_) => {
304                // If not in config, try to get default URL
305                let chain_data = self.get_chain_data_by_alias_non_mut(alias)?;
306                Ok(chain_data.default_rpc_url)
307            }
308        }
309    }
310}
311
312impl Default for CheatsConfig {
313    fn default() -> Self {
314        Self {
315            ffi: false,
316            always_use_create_2_factory: false,
317            prompt_timeout: Duration::from_secs(120),
318            rpc_storage_caching: Default::default(),
319            no_storage_caching: false,
320            rpc_endpoints: Default::default(),
321            paths: ProjectPathsConfig::builder().build_with_root("./"),
322            fs_permissions: Default::default(),
323            root: Default::default(),
324            bind_json_path: PathBuf::default().join("utils").join("jsonBindings.sol"),
325            broadcast: Default::default(),
326            allowed_paths: vec![],
327            evm_opts: Default::default(),
328            labels: Default::default(),
329            available_artifacts: Default::default(),
330            running_artifact: Default::default(),
331            assertions_revert: true,
332            seed: None,
333            internal_expect_revert: false,
334            chains: HashMap::new(),
335            chain_id_to_alias: HashMap::new(),
336            strategy: CheatcodesStrategy::new_evm(),
337            pvm_enabled: false,
338        }
339    }
340}
341
342// Helper function to set default chains
343fn create_default_chains() -> HashMap<String, ChainData> {
344    let mut chains = HashMap::new();
345
346    // Define all chains in one place
347    chains.insert(
348        "anvil".to_string(),
349        ChainData {
350            name: "Anvil".to_string(),
351            chain_id: 31337,
352            default_rpc_url: "http://127.0.0.1:8545".to_string(),
353        },
354    );
355
356    chains.insert(
357        "mainnet".to_string(),
358        ChainData {
359            name: "Mainnet".to_string(),
360            chain_id: 1,
361            default_rpc_url: "https://eth.llamarpc.com".to_string(),
362        },
363    );
364
365    chains.insert(
366        "sepolia".to_string(),
367        ChainData {
368            name: "Sepolia".to_string(),
369            chain_id: 11155111,
370            default_rpc_url: "https://sepolia.infura.io/v3/b9794ad1ddf84dfb8c34d6bb5dca2001"
371                .to_string(),
372        },
373    );
374
375    chains.insert(
376        "holesky".to_string(),
377        ChainData {
378            name: "Holesky".to_string(),
379            chain_id: 17000,
380            default_rpc_url: "https://rpc.holesky.ethpandaops.io".to_string(),
381        },
382    );
383
384    chains.insert(
385        "optimism".to_string(),
386        ChainData {
387            name: "Optimism".to_string(),
388            chain_id: 10,
389            default_rpc_url: "https://mainnet.optimism.io".to_string(),
390        },
391    );
392
393    chains.insert(
394        "optimism_sepolia".to_string(),
395        ChainData {
396            name: "Optimism Sepolia".to_string(),
397            chain_id: 11155420,
398            default_rpc_url: "https://sepolia.optimism.io".to_string(),
399        },
400    );
401
402    chains.insert(
403        "arbitrum_one".to_string(),
404        ChainData {
405            name: "Arbitrum One".to_string(),
406            chain_id: 42161,
407            default_rpc_url: "https://arb1.arbitrum.io/rpc".to_string(),
408        },
409    );
410
411    chains.insert(
412        "arbitrum_one_sepolia".to_string(),
413        ChainData {
414            name: "Arbitrum One Sepolia".to_string(),
415            chain_id: 421614,
416            default_rpc_url: "https://sepolia-rollup.arbitrum.io/rpc".to_string(),
417        },
418    );
419
420    chains.insert(
421        "arbitrum_nova".to_string(),
422        ChainData {
423            name: "Arbitrum Nova".to_string(),
424            chain_id: 42170,
425            default_rpc_url: "https://nova.arbitrum.io/rpc".to_string(),
426        },
427    );
428
429    chains.insert(
430        "polygon".to_string(),
431        ChainData {
432            name: "Polygon".to_string(),
433            chain_id: 137,
434            default_rpc_url: "https://polygon-rpc.com".to_string(),
435        },
436    );
437
438    chains.insert(
439        "polygon_amoy".to_string(),
440        ChainData {
441            name: "Polygon Amoy".to_string(),
442            chain_id: 80002,
443            default_rpc_url: "https://rpc-amoy.polygon.technology".to_string(),
444        },
445    );
446
447    chains.insert(
448        "avalanche".to_string(),
449        ChainData {
450            name: "Avalanche".to_string(),
451            chain_id: 43114,
452            default_rpc_url: "https://api.avax.network/ext/bc/C/rpc".to_string(),
453        },
454    );
455
456    chains.insert(
457        "avalanche_fuji".to_string(),
458        ChainData {
459            name: "Avalanche Fuji".to_string(),
460            chain_id: 43113,
461            default_rpc_url: "https://api.avax-test.network/ext/bc/C/rpc".to_string(),
462        },
463    );
464
465    chains.insert(
466        "bnb_smart_chain".to_string(),
467        ChainData {
468            name: "BNB Smart Chain".to_string(),
469            chain_id: 56,
470            default_rpc_url: "https://bsc-dataseed1.binance.org".to_string(),
471        },
472    );
473
474    chains.insert(
475        "bnb_smart_chain_testnet".to_string(),
476        ChainData {
477            name: "BNB Smart Chain Testnet".to_string(),
478            chain_id: 97,
479            default_rpc_url: "https://rpc.ankr.com/bsc_testnet_chapel".to_string(),
480        },
481    );
482
483    chains.insert(
484        "gnosis_chain".to_string(),
485        ChainData {
486            name: "Gnosis Chain".to_string(),
487            chain_id: 100,
488            default_rpc_url: "https://rpc.gnosischain.com".to_string(),
489        },
490    );
491
492    chains.insert(
493        "moonbeam".to_string(),
494        ChainData {
495            name: "Moonbeam".to_string(),
496            chain_id: 1284,
497            default_rpc_url: "https://rpc.api.moonbeam.network".to_string(),
498        },
499    );
500
501    chains.insert(
502        "moonriver".to_string(),
503        ChainData {
504            name: "Moonriver".to_string(),
505            chain_id: 1285,
506            default_rpc_url: "https://rpc.api.moonriver.moonbeam.network".to_string(),
507        },
508    );
509
510    chains.insert(
511        "moonbase".to_string(),
512        ChainData {
513            name: "Moonbase".to_string(),
514            chain_id: 1287,
515            default_rpc_url: "https://rpc.testnet.moonbeam.network".to_string(),
516        },
517    );
518
519    chains.insert(
520        "base_sepolia".to_string(),
521        ChainData {
522            name: "Base Sepolia".to_string(),
523            chain_id: 84532,
524            default_rpc_url: "https://sepolia.base.org".to_string(),
525        },
526    );
527
528    chains.insert(
529        "base".to_string(),
530        ChainData {
531            name: "Base".to_string(),
532            chain_id: 8453,
533            default_rpc_url: "https://mainnet.base.org".to_string(),
534        },
535    );
536
537    chains.insert(
538        "blast_sepolia".to_string(),
539        ChainData {
540            name: "Blast Sepolia".to_string(),
541            chain_id: 168587773,
542            default_rpc_url: "https://sepolia.blast.io".to_string(),
543        },
544    );
545
546    chains.insert(
547        "blast".to_string(),
548        ChainData {
549            name: "Blast".to_string(),
550            chain_id: 81457,
551            default_rpc_url: "https://rpc.blast.io".to_string(),
552        },
553    );
554
555    chains.insert(
556        "fantom_opera".to_string(),
557        ChainData {
558            name: "Fantom Opera".to_string(),
559            chain_id: 250,
560            default_rpc_url: "https://rpc.ankr.com/fantom/".to_string(),
561        },
562    );
563
564    chains.insert(
565        "fantom_opera_testnet".to_string(),
566        ChainData {
567            name: "Fantom Opera Testnet".to_string(),
568            chain_id: 4002,
569            default_rpc_url: "https://rpc.ankr.com/fantom_testnet/".to_string(),
570        },
571    );
572
573    chains.insert(
574        "fraxtal".to_string(),
575        ChainData {
576            name: "Fraxtal".to_string(),
577            chain_id: 252,
578            default_rpc_url: "https://rpc.frax.com".to_string(),
579        },
580    );
581
582    chains.insert(
583        "fraxtal_testnet".to_string(),
584        ChainData {
585            name: "Fraxtal Testnet".to_string(),
586            chain_id: 2522,
587            default_rpc_url: "https://rpc.testnet.frax.com".to_string(),
588        },
589    );
590
591    chains.insert(
592        "berachain_bartio_testnet".to_string(),
593        ChainData {
594            name: "Berachain bArtio Testnet".to_string(),
595            chain_id: 80084,
596            default_rpc_url: "https://bartio.rpc.berachain.com".to_string(),
597        },
598    );
599
600    chains.insert(
601        "flare".to_string(),
602        ChainData {
603            name: "Flare".to_string(),
604            chain_id: 14,
605            default_rpc_url: "https://flare-api.flare.network/ext/C/rpc".to_string(),
606        },
607    );
608
609    chains.insert(
610        "flare_coston2".to_string(),
611        ChainData {
612            name: "Flare Coston2".to_string(),
613            chain_id: 114,
614            default_rpc_url: "https://coston2-api.flare.network/ext/C/rpc".to_string(),
615        },
616    );
617
618    chains.insert(
619        "mode".to_string(),
620        ChainData {
621            name: "Mode".to_string(),
622            chain_id: 34443,
623            default_rpc_url: "https://mode.drpc.org".to_string(),
624        },
625    );
626
627    chains.insert(
628        "mode_sepolia".to_string(),
629        ChainData {
630            name: "Mode Sepolia".to_string(),
631            chain_id: 919,
632            default_rpc_url: "https://sepolia.mode.network".to_string(),
633        },
634    );
635
636    chains.insert(
637        "zora".to_string(),
638        ChainData {
639            name: "Zora".to_string(),
640            chain_id: 7777777,
641            default_rpc_url: "https://zora.drpc.org".to_string(),
642        },
643    );
644
645    chains.insert(
646        "zora_sepolia".to_string(),
647        ChainData {
648            name: "Zora Sepolia".to_string(),
649            chain_id: 999999999,
650            default_rpc_url: "https://sepolia.rpc.zora.energy".to_string(),
651        },
652    );
653
654    chains.insert(
655        "race".to_string(),
656        ChainData {
657            name: "Race".to_string(),
658            chain_id: 6805,
659            default_rpc_url: "https://racemainnet.io".to_string(),
660        },
661    );
662
663    chains.insert(
664        "race_sepolia".to_string(),
665        ChainData {
666            name: "Race Sepolia".to_string(),
667            chain_id: 6806,
668            default_rpc_url: "https://racemainnet.io".to_string(),
669        },
670    );
671
672    chains.insert(
673        "metal".to_string(),
674        ChainData {
675            name: "Metal".to_string(),
676            chain_id: 1750,
677            default_rpc_url: "https://metall2.drpc.org".to_string(),
678        },
679    );
680
681    chains.insert(
682        "metal_sepolia".to_string(),
683        ChainData {
684            name: "Metal Sepolia".to_string(),
685            chain_id: 1740,
686            default_rpc_url: "https://testnet.rpc.metall2.com".to_string(),
687        },
688    );
689
690    chains.insert(
691        "binary".to_string(),
692        ChainData {
693            name: "Binary".to_string(),
694            chain_id: 624,
695            default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string(),
696        },
697    );
698
699    chains.insert(
700        "binary_sepolia".to_string(),
701        ChainData {
702            name: "Binary Sepolia".to_string(),
703            chain_id: 625,
704            default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string(),
705        },
706    );
707
708    chains.insert(
709        "orderly".to_string(),
710        ChainData {
711            name: "Orderly".to_string(),
712            chain_id: 291,
713            default_rpc_url: "https://rpc.orderly.network".to_string(),
714        },
715    );
716
717    chains.insert(
718        "orderly_sepolia".to_string(),
719        ChainData {
720            name: "Orderly Sepolia".to_string(),
721            chain_id: 4460,
722            default_rpc_url: "https://testnet-rpc.orderly.org".to_string(),
723        },
724    );
725
726    chains
727}
728
729#[cfg(test)]
730mod tests {
731    use super::*;
732    use foundry_config::fs_permissions::PathPermission;
733
734    fn config(root: &str, fs_permissions: FsPermissions) -> CheatsConfig {
735        CheatsConfig::new(
736            CheatcodesStrategy::new_evm(),
737            &Config { root: root.into(), fs_permissions, ..Default::default() },
738            Default::default(),
739            None,
740            None,
741        )
742    }
743
744    #[test]
745    fn test_allowed_paths() {
746        let root = "/my/project/root/";
747        let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
748
749        assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Read).is_ok());
750        assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Write).is_ok());
751        assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Read).is_ok());
752        assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Write).is_ok());
753        assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Read).is_err());
754        assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Write).is_err());
755    }
756
757    #[test]
758    fn test_is_foundry_toml() {
759        let root = "/my/project/root/";
760        let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
761
762        let f = format!("{root}foundry.toml");
763        assert!(config.is_foundry_toml(f));
764
765        let f = format!("{root}Foundry.toml");
766        assert!(config.is_foundry_toml(f));
767
768        let f = format!("{root}lib/other/foundry.toml");
769        assert!(!config.is_foundry_toml(f));
770    }
771}