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