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 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}
67
68#[derive(Clone, Debug)]
70pub struct ChainData {
71 pub name: String,
72 pub chain_id: u64,
73 pub default_rpc_url: String, }
75
76impl CheatsConfig {
77 pub fn new(
79 config: &Config,
80 evm_opts: EvmOpts,
81 available_artifacts: Option<ContractsByArtifact>,
82 running_artifact: Option<ArtifactId>,
83 ) -> Self {
84 let mut allowed_paths = vec![config.root.clone()];
85 allowed_paths.extend(config.libs.iter().cloned());
86 allowed_paths.extend(config.allow_paths.iter().cloned());
87
88 let rpc_endpoints = config.rpc_endpoints.clone().resolved();
89 trace!(?rpc_endpoints, "using resolved rpc endpoints");
90
91 let available_artifacts =
93 if config.unchecked_cheatcode_artifacts { None } else { available_artifacts };
94
95 Self {
96 ffi: evm_opts.ffi,
97 always_use_create_2_factory: evm_opts.always_use_create_2_factory,
98 prompt_timeout: Duration::from_secs(config.prompt_timeout),
99 rpc_storage_caching: config.rpc_storage_caching.clone(),
100 no_storage_caching: config.no_storage_caching,
101 rpc_endpoints,
102 paths: config.project_paths(),
103 bind_json_path: config.bind_json.out.clone(),
104 fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()),
105 root: config.root.clone(),
106 broadcast: config.root.clone().join(&config.broadcast),
107 allowed_paths,
108 evm_opts,
109 labels: config.labels.clone(),
110 available_artifacts,
111 running_artifact,
112 assertions_revert: config.assertions_revert,
113 seed: config.fuzz.seed,
114 internal_expect_revert: config.allow_internal_expect_revert,
115 chains: HashMap::new(),
116 chain_id_to_alias: HashMap::new(),
117 }
118 }
119
120 pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self {
122 Self::new(config, evm_opts, self.available_artifacts.clone(), self.running_artifact.clone())
123 }
124
125 pub fn normalized_path(&self, path: impl AsRef<Path>) -> PathBuf {
129 let path = self.root.join(path);
130 canonicalize(&path).unwrap_or_else(|_| normalize_path(&path))
131 }
132
133 pub fn is_path_allowed(&self, path: impl AsRef<Path>, kind: FsAccessKind) -> bool {
140 self.is_normalized_path_allowed(&self.normalized_path(path), kind)
141 }
142
143 fn is_normalized_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
144 self.fs_permissions.is_path_allowed(path, kind)
145 }
146
147 pub fn ensure_path_allowed(
151 &self,
152 path: impl AsRef<Path>,
153 kind: FsAccessKind,
154 ) -> Result<PathBuf> {
155 let path = path.as_ref();
156 let normalized = self.normalized_path(path);
157 ensure!(
158 self.is_normalized_path_allowed(&normalized, kind),
159 "the path {} is not allowed to be accessed for {kind} operations",
160 normalized.strip_prefix(&self.root).unwrap_or(path).display()
161 );
162 Ok(normalized)
163 }
164
165 pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
169 let foundry_toml = self.root.join(Config::FILE_NAME);
174 Path::new(&foundry_toml.to_string_lossy().to_lowercase())
175 .starts_with(Path::new(&path.as_ref().to_string_lossy().to_lowercase()))
176 }
177
178 pub fn ensure_not_foundry_toml(&self, path: impl AsRef<Path>) -> Result<()> {
181 ensure!(!self.is_foundry_toml(path), "access to `foundry.toml` is not allowed");
182 Ok(())
183 }
184
185 pub fn rpc_endpoint(&self, url_or_alias: &str) -> Result<ResolvedRpcEndpoint> {
199 if let Some(endpoint) = self.rpc_endpoints.get(url_or_alias) {
200 Ok(endpoint.clone().try_resolve())
201 } else {
202 if url_or_alias.starts_with("http") ||
204 url_or_alias.starts_with("ws") ||
205 Path::new(url_or_alias).exists()
207 {
208 let url = RpcEndpointUrl::Env(url_or_alias.to_string());
209 Ok(RpcEndpoint::new(url).resolve())
210 } else {
211 Err(fmt_err!("invalid rpc url: {url_or_alias}"))
212 }
213 }
214 }
215 pub fn rpc_urls(&self) -> Result<Vec<Rpc>> {
217 let mut urls = Vec::with_capacity(self.rpc_endpoints.len());
218 for alias in self.rpc_endpoints.keys() {
219 let url = self.rpc_endpoint(alias)?.url()?;
220 urls.push(Rpc { key: alias.clone(), url });
221 }
222 Ok(urls)
223 }
224
225 pub fn initialize_chain_data(&mut self) {
227 if !self.chains.is_empty() {
228 return; }
230
231 let chains = create_default_chains();
233
234 for (alias, data) in chains {
236 self.set_chain_with_default_rpc_url(&alias, data);
237 }
238 }
239
240 pub fn set_chain_with_default_rpc_url(&mut self, alias: &str, data: ChainData) {
242 self.set_chain_data(alias, data);
247 }
248
249 pub fn set_chain_data(&mut self, alias: &str, data: ChainData) {
251 if let Some(old_data) = self.chains.get(alias) {
253 self.chain_id_to_alias.remove(&old_data.chain_id);
254 }
255
256 self.chain_id_to_alias.insert(data.chain_id, alias.to_string());
258 self.chains.insert(alias.to_string(), data);
259 }
260
261 pub fn get_chain_data_by_alias_non_mut(&self, alias: &str) -> Result<ChainData> {
263 if self.chains.is_empty() {
265 let temp_chains = create_default_chains();
268
269 if let Some(data) = temp_chains.get(alias) {
270 return Ok(data.clone());
271 }
272 } else {
273 if let Some(data) = self.chains.get(alias) {
275 return Ok(data.clone());
276 }
277 }
278
279 Err(fmt_err!("vm.getChain: Chain with alias \"{}\" not found", alias))
281 }
282
283 pub fn get_rpc_url_non_mut(&self, alias: &str) -> Result<String> {
285 match self.rpc_endpoint(alias) {
287 Ok(endpoint) => Ok(endpoint.url()?),
288 Err(_) => {
289 let chain_data = self.get_chain_data_by_alias_non_mut(alias)?;
291 Ok(chain_data.default_rpc_url)
292 }
293 }
294 }
295}
296
297impl Default for CheatsConfig {
298 fn default() -> Self {
299 Self {
300 ffi: false,
301 always_use_create_2_factory: false,
302 prompt_timeout: Duration::from_secs(120),
303 rpc_storage_caching: Default::default(),
304 no_storage_caching: false,
305 rpc_endpoints: Default::default(),
306 paths: ProjectPathsConfig::builder().build_with_root("./"),
307 fs_permissions: Default::default(),
308 root: Default::default(),
309 bind_json_path: PathBuf::default().join("utils").join("jsonBindings.sol"),
310 broadcast: Default::default(),
311 allowed_paths: vec![],
312 evm_opts: Default::default(),
313 labels: Default::default(),
314 available_artifacts: Default::default(),
315 running_artifact: Default::default(),
316 assertions_revert: true,
317 seed: None,
318 internal_expect_revert: false,
319 chains: HashMap::new(),
320 chain_id_to_alias: HashMap::new(),
321 }
322 }
323}
324
325fn create_default_chains() -> HashMap<String, ChainData> {
327 let mut chains = HashMap::new();
328
329 chains.insert(
331 "anvil".to_string(),
332 ChainData {
333 name: "Anvil".to_string(),
334 chain_id: 31337,
335 default_rpc_url: "http://127.0.0.1:8545".to_string(),
336 },
337 );
338
339 chains.insert(
340 "mainnet".to_string(),
341 ChainData {
342 name: "Mainnet".to_string(),
343 chain_id: 1,
344 default_rpc_url: "https://eth.llamarpc.com".to_string(),
345 },
346 );
347
348 chains.insert(
349 "sepolia".to_string(),
350 ChainData {
351 name: "Sepolia".to_string(),
352 chain_id: 11155111,
353 default_rpc_url: "https://sepolia.infura.io/v3/b9794ad1ddf84dfb8c34d6bb5dca2001"
354 .to_string(),
355 },
356 );
357
358 chains.insert(
359 "holesky".to_string(),
360 ChainData {
361 name: "Holesky".to_string(),
362 chain_id: 17000,
363 default_rpc_url: "https://rpc.holesky.ethpandaops.io".to_string(),
364 },
365 );
366
367 chains.insert(
368 "optimism".to_string(),
369 ChainData {
370 name: "Optimism".to_string(),
371 chain_id: 10,
372 default_rpc_url: "https://mainnet.optimism.io".to_string(),
373 },
374 );
375
376 chains.insert(
377 "optimism_sepolia".to_string(),
378 ChainData {
379 name: "Optimism Sepolia".to_string(),
380 chain_id: 11155420,
381 default_rpc_url: "https://sepolia.optimism.io".to_string(),
382 },
383 );
384
385 chains.insert(
386 "arbitrum_one".to_string(),
387 ChainData {
388 name: "Arbitrum One".to_string(),
389 chain_id: 42161,
390 default_rpc_url: "https://arb1.arbitrum.io/rpc".to_string(),
391 },
392 );
393
394 chains.insert(
395 "arbitrum_one_sepolia".to_string(),
396 ChainData {
397 name: "Arbitrum One Sepolia".to_string(),
398 chain_id: 421614,
399 default_rpc_url: "https://sepolia-rollup.arbitrum.io/rpc".to_string(),
400 },
401 );
402
403 chains.insert(
404 "arbitrum_nova".to_string(),
405 ChainData {
406 name: "Arbitrum Nova".to_string(),
407 chain_id: 42170,
408 default_rpc_url: "https://nova.arbitrum.io/rpc".to_string(),
409 },
410 );
411
412 chains.insert(
413 "polygon".to_string(),
414 ChainData {
415 name: "Polygon".to_string(),
416 chain_id: 137,
417 default_rpc_url: "https://polygon-rpc.com".to_string(),
418 },
419 );
420
421 chains.insert(
422 "polygon_amoy".to_string(),
423 ChainData {
424 name: "Polygon Amoy".to_string(),
425 chain_id: 80002,
426 default_rpc_url: "https://rpc-amoy.polygon.technology".to_string(),
427 },
428 );
429
430 chains.insert(
431 "avalanche".to_string(),
432 ChainData {
433 name: "Avalanche".to_string(),
434 chain_id: 43114,
435 default_rpc_url: "https://api.avax.network/ext/bc/C/rpc".to_string(),
436 },
437 );
438
439 chains.insert(
440 "avalanche_fuji".to_string(),
441 ChainData {
442 name: "Avalanche Fuji".to_string(),
443 chain_id: 43113,
444 default_rpc_url: "https://api.avax-test.network/ext/bc/C/rpc".to_string(),
445 },
446 );
447
448 chains.insert(
449 "bnb_smart_chain".to_string(),
450 ChainData {
451 name: "BNB Smart Chain".to_string(),
452 chain_id: 56,
453 default_rpc_url: "https://bsc-dataseed1.binance.org".to_string(),
454 },
455 );
456
457 chains.insert(
458 "bnb_smart_chain_testnet".to_string(),
459 ChainData {
460 name: "BNB Smart Chain Testnet".to_string(),
461 chain_id: 97,
462 default_rpc_url: "https://rpc.ankr.com/bsc_testnet_chapel".to_string(),
463 },
464 );
465
466 chains.insert(
467 "gnosis_chain".to_string(),
468 ChainData {
469 name: "Gnosis Chain".to_string(),
470 chain_id: 100,
471 default_rpc_url: "https://rpc.gnosischain.com".to_string(),
472 },
473 );
474
475 chains.insert(
476 "moonbeam".to_string(),
477 ChainData {
478 name: "Moonbeam".to_string(),
479 chain_id: 1284,
480 default_rpc_url: "https://rpc.api.moonbeam.network".to_string(),
481 },
482 );
483
484 chains.insert(
485 "moonriver".to_string(),
486 ChainData {
487 name: "Moonriver".to_string(),
488 chain_id: 1285,
489 default_rpc_url: "https://rpc.api.moonriver.moonbeam.network".to_string(),
490 },
491 );
492
493 chains.insert(
494 "moonbase".to_string(),
495 ChainData {
496 name: "Moonbase".to_string(),
497 chain_id: 1287,
498 default_rpc_url: "https://rpc.testnet.moonbeam.network".to_string(),
499 },
500 );
501
502 chains.insert(
503 "base_sepolia".to_string(),
504 ChainData {
505 name: "Base Sepolia".to_string(),
506 chain_id: 84532,
507 default_rpc_url: "https://sepolia.base.org".to_string(),
508 },
509 );
510
511 chains.insert(
512 "base".to_string(),
513 ChainData {
514 name: "Base".to_string(),
515 chain_id: 8453,
516 default_rpc_url: "https://mainnet.base.org".to_string(),
517 },
518 );
519
520 chains.insert(
521 "blast_sepolia".to_string(),
522 ChainData {
523 name: "Blast Sepolia".to_string(),
524 chain_id: 168587773,
525 default_rpc_url: "https://sepolia.blast.io".to_string(),
526 },
527 );
528
529 chains.insert(
530 "blast".to_string(),
531 ChainData {
532 name: "Blast".to_string(),
533 chain_id: 81457,
534 default_rpc_url: "https://rpc.blast.io".to_string(),
535 },
536 );
537
538 chains.insert(
539 "fantom_opera".to_string(),
540 ChainData {
541 name: "Fantom Opera".to_string(),
542 chain_id: 250,
543 default_rpc_url: "https://rpc.ankr.com/fantom/".to_string(),
544 },
545 );
546
547 chains.insert(
548 "fantom_opera_testnet".to_string(),
549 ChainData {
550 name: "Fantom Opera Testnet".to_string(),
551 chain_id: 4002,
552 default_rpc_url: "https://rpc.ankr.com/fantom_testnet/".to_string(),
553 },
554 );
555
556 chains.insert(
557 "fraxtal".to_string(),
558 ChainData {
559 name: "Fraxtal".to_string(),
560 chain_id: 252,
561 default_rpc_url: "https://rpc.frax.com".to_string(),
562 },
563 );
564
565 chains.insert(
566 "fraxtal_testnet".to_string(),
567 ChainData {
568 name: "Fraxtal Testnet".to_string(),
569 chain_id: 2522,
570 default_rpc_url: "https://rpc.testnet.frax.com".to_string(),
571 },
572 );
573
574 chains.insert(
575 "berachain_bartio_testnet".to_string(),
576 ChainData {
577 name: "Berachain bArtio Testnet".to_string(),
578 chain_id: 80084,
579 default_rpc_url: "https://bartio.rpc.berachain.com".to_string(),
580 },
581 );
582
583 chains.insert(
584 "flare".to_string(),
585 ChainData {
586 name: "Flare".to_string(),
587 chain_id: 14,
588 default_rpc_url: "https://flare-api.flare.network/ext/C/rpc".to_string(),
589 },
590 );
591
592 chains.insert(
593 "flare_coston2".to_string(),
594 ChainData {
595 name: "Flare Coston2".to_string(),
596 chain_id: 114,
597 default_rpc_url: "https://coston2-api.flare.network/ext/C/rpc".to_string(),
598 },
599 );
600
601 chains.insert(
602 "mode".to_string(),
603 ChainData {
604 name: "Mode".to_string(),
605 chain_id: 34443,
606 default_rpc_url: "https://mode.drpc.org".to_string(),
607 },
608 );
609
610 chains.insert(
611 "mode_sepolia".to_string(),
612 ChainData {
613 name: "Mode Sepolia".to_string(),
614 chain_id: 919,
615 default_rpc_url: "https://sepolia.mode.network".to_string(),
616 },
617 );
618
619 chains.insert(
620 "zora".to_string(),
621 ChainData {
622 name: "Zora".to_string(),
623 chain_id: 7777777,
624 default_rpc_url: "https://zora.drpc.org".to_string(),
625 },
626 );
627
628 chains.insert(
629 "zora_sepolia".to_string(),
630 ChainData {
631 name: "Zora Sepolia".to_string(),
632 chain_id: 999999999,
633 default_rpc_url: "https://sepolia.rpc.zora.energy".to_string(),
634 },
635 );
636
637 chains.insert(
638 "race".to_string(),
639 ChainData {
640 name: "Race".to_string(),
641 chain_id: 6805,
642 default_rpc_url: "https://racemainnet.io".to_string(),
643 },
644 );
645
646 chains.insert(
647 "race_sepolia".to_string(),
648 ChainData {
649 name: "Race Sepolia".to_string(),
650 chain_id: 6806,
651 default_rpc_url: "https://racemainnet.io".to_string(),
652 },
653 );
654
655 chains.insert(
656 "metal".to_string(),
657 ChainData {
658 name: "Metal".to_string(),
659 chain_id: 1750,
660 default_rpc_url: "https://metall2.drpc.org".to_string(),
661 },
662 );
663
664 chains.insert(
665 "metal_sepolia".to_string(),
666 ChainData {
667 name: "Metal Sepolia".to_string(),
668 chain_id: 1740,
669 default_rpc_url: "https://testnet.rpc.metall2.com".to_string(),
670 },
671 );
672
673 chains.insert(
674 "binary".to_string(),
675 ChainData {
676 name: "Binary".to_string(),
677 chain_id: 624,
678 default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string(),
679 },
680 );
681
682 chains.insert(
683 "binary_sepolia".to_string(),
684 ChainData {
685 name: "Binary Sepolia".to_string(),
686 chain_id: 625,
687 default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string(),
688 },
689 );
690
691 chains.insert(
692 "orderly".to_string(),
693 ChainData {
694 name: "Orderly".to_string(),
695 chain_id: 291,
696 default_rpc_url: "https://rpc.orderly.network".to_string(),
697 },
698 );
699
700 chains.insert(
701 "orderly_sepolia".to_string(),
702 ChainData {
703 name: "Orderly Sepolia".to_string(),
704 chain_id: 4460,
705 default_rpc_url: "https://testnet-rpc.orderly.org".to_string(),
706 },
707 );
708
709 chains
710}
711
712#[cfg(test)]
713mod tests {
714 use super::*;
715 use foundry_config::fs_permissions::PathPermission;
716
717 fn config(root: &str, fs_permissions: FsPermissions) -> CheatsConfig {
718 CheatsConfig::new(
719 &Config { root: root.into(), fs_permissions, ..Default::default() },
720 Default::default(),
721 None,
722 None,
723 )
724 }
725
726 #[test]
727 fn test_allowed_paths() {
728 let root = "/my/project/root/";
729 let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
730
731 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Read).is_ok());
732 assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Write).is_ok());
733 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Read).is_ok());
734 assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Write).is_ok());
735 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Read).is_err());
736 assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Write).is_err());
737 }
738
739 #[test]
740 fn test_is_foundry_toml() {
741 let root = "/my/project/root/";
742 let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
743
744 let f = format!("{root}foundry.toml");
745 assert!(config.is_foundry_toml(f));
746
747 let f = format!("{root}Foundry.toml");
748 assert!(config.is_foundry_toml(f));
749
750 let f = format!("{root}lib/other/foundry.toml");
751 assert!(!config.is_foundry_toml(f));
752 }
753}