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#[derive(Clone, Debug)]
21pub struct CheatsConfig {
22 pub ffi: bool,
24 pub always_use_create_2_factory: bool,
26 pub prompt_timeout: Duration,
28 pub rpc_storage_caching: StorageCachingConfig,
30 pub no_storage_caching: bool,
32 pub rpc_endpoints: ResolvedRpcEndpoints,
34 pub paths: ProjectPathsConfig,
36 pub bind_json_path: PathBuf,
38 pub fs_permissions: FsPermissions,
40 pub root: PathBuf,
42 pub broadcast: PathBuf,
44 pub allowed_paths: Vec<PathBuf>,
46 pub evm_opts: EvmOpts,
48 pub labels: AddressHashMap<String>,
50 pub available_artifacts: Option<ContractsByArtifact>,
54 pub running_artifact: Option<ArtifactId>,
56 pub assertions_revert: bool,
58 pub seed: Option<U256>,
60 pub internal_expect_revert: bool,
62 pub chains: HashMap<String, ChainData>,
64 pub chain_id_to_alias: HashMap<u64, String>,
66 pub strategy: CheatcodesStrategy,
68 pub pvm_enabled: bool,
70}
71
72#[derive(Clone, Debug)]
74pub struct ChainData {
75 pub name: String,
76 pub chain_id: u64,
77 pub default_rpc_url: String, }
79
80impl CheatsConfig {
81 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 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 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 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 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 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 pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
184 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 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 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 if url_or_alias.starts_with("http") ||
219 url_or_alias.starts_with("ws") ||
220 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 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 pub fn initialize_chain_data(&mut self) {
242 if !self.chains.is_empty() {
243 return; }
245
246 let chains = create_default_chains();
248
249 for (alias, data) in chains {
251 self.set_chain_with_default_rpc_url(&alias, data);
252 }
253 }
254
255 pub fn set_chain_with_default_rpc_url(&mut self, alias: &str, data: ChainData) {
257 self.set_chain_data(alias, data);
262 }
263
264 pub fn set_chain_data(&mut self, alias: &str, data: ChainData) {
266 if let Some(old_data) = self.chains.get(alias) {
268 self.chain_id_to_alias.remove(&old_data.chain_id);
269 }
270
271 self.chain_id_to_alias.insert(data.chain_id, alias.to_string());
273 self.chains.insert(alias.to_string(), data);
274 }
275
276 pub fn get_chain_data_by_alias_non_mut(&self, alias: &str) -> Result<ChainData> {
278 if self.chains.is_empty() {
280 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 if let Some(data) = self.chains.get(alias) {
290 return Ok(data.clone());
291 }
292 }
293
294 Err(fmt_err!("vm.getChain: Chain with alias \"{}\" not found", alias))
296 }
297
298 pub fn get_rpc_url_non_mut(&self, alias: &str) -> Result<String> {
300 match self.rpc_endpoint(alias) {
302 Ok(endpoint) => Ok(endpoint.url()?),
303 Err(_) => {
304 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
342fn create_default_chains() -> HashMap<String, ChainData> {
344 let mut chains = HashMap::new();
345
346 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}