use super::Result;
use crate::Vm::Rpc;
use alloy_primitives::{map::AddressHashMap, U256};
use foundry_common::{fs::normalize_path, ContractsByArtifact};
use foundry_compilers::{utils::canonicalize, ProjectPathsConfig};
use foundry_config::{
cache::StorageCachingConfig, fs_permissions::FsAccessKind, Config, FsPermissions,
ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl,
};
use foundry_evm_core::opts::EvmOpts;
use semver::Version;
use std::{
path::{Path, PathBuf},
time::Duration,
};
#[derive(Clone, Debug)]
pub struct CheatsConfig {
pub ffi: bool,
pub always_use_create_2_factory: bool,
pub prompt_timeout: Duration,
pub rpc_storage_caching: StorageCachingConfig,
pub no_storage_caching: bool,
pub rpc_endpoints: ResolvedRpcEndpoints,
pub paths: ProjectPathsConfig,
pub fs_permissions: FsPermissions,
pub root: PathBuf,
pub broadcast: PathBuf,
pub allowed_paths: Vec<PathBuf>,
pub evm_opts: EvmOpts,
pub labels: AddressHashMap<String>,
pub available_artifacts: Option<ContractsByArtifact>,
pub running_contract: Option<String>,
pub running_version: Option<Version>,
pub assertions_revert: bool,
pub seed: Option<U256>,
}
impl CheatsConfig {
pub fn new(
config: &Config,
evm_opts: EvmOpts,
available_artifacts: Option<ContractsByArtifact>,
running_contract: Option<String>,
running_version: Option<Version>,
) -> Self {
let mut allowed_paths = vec![config.root.clone()];
allowed_paths.extend(config.libs.iter().cloned());
allowed_paths.extend(config.allow_paths.iter().cloned());
let rpc_endpoints = config.rpc_endpoints.clone().resolved();
trace!(?rpc_endpoints, "using resolved rpc endpoints");
let available_artifacts =
if config.unchecked_cheatcode_artifacts { None } else { available_artifacts };
Self {
ffi: evm_opts.ffi,
always_use_create_2_factory: evm_opts.always_use_create_2_factory,
prompt_timeout: Duration::from_secs(config.prompt_timeout),
rpc_storage_caching: config.rpc_storage_caching.clone(),
no_storage_caching: config.no_storage_caching,
rpc_endpoints,
paths: config.project_paths(),
fs_permissions: config.fs_permissions.clone().joined(config.root.as_ref()),
root: config.root.clone(),
broadcast: config.root.clone().join(&config.broadcast),
allowed_paths,
evm_opts,
labels: config.labels.clone(),
available_artifacts,
running_contract,
running_version,
assertions_revert: config.assertions_revert,
seed: config.fuzz.seed,
}
}
pub fn clone_with(&self, config: &Config, evm_opts: EvmOpts) -> Self {
Self::new(
config,
evm_opts,
self.available_artifacts.clone(),
self.running_contract.clone(),
self.running_version.clone(),
)
}
pub fn normalized_path(&self, path: impl AsRef<Path>) -> PathBuf {
let path = self.root.join(path);
canonicalize(&path).unwrap_or_else(|_| normalize_path(&path))
}
pub fn is_path_allowed(&self, path: impl AsRef<Path>, kind: FsAccessKind) -> bool {
self.is_normalized_path_allowed(&self.normalized_path(path), kind)
}
fn is_normalized_path_allowed(&self, path: &Path, kind: FsAccessKind) -> bool {
self.fs_permissions.is_path_allowed(path, kind)
}
pub fn ensure_path_allowed(
&self,
path: impl AsRef<Path>,
kind: FsAccessKind,
) -> Result<PathBuf> {
let path = path.as_ref();
let normalized = self.normalized_path(path);
ensure!(
self.is_normalized_path_allowed(&normalized, kind),
"the path {} is not allowed to be accessed for {kind} operations",
normalized.strip_prefix(&self.root).unwrap_or(path).display()
);
Ok(normalized)
}
pub fn is_foundry_toml(&self, path: impl AsRef<Path>) -> bool {
let foundry_toml = self.root.join(Config::FILE_NAME);
Path::new(&foundry_toml.to_string_lossy().to_lowercase())
.starts_with(Path::new(&path.as_ref().to_string_lossy().to_lowercase()))
}
pub fn ensure_not_foundry_toml(&self, path: impl AsRef<Path>) -> Result<()> {
ensure!(!self.is_foundry_toml(path), "access to `foundry.toml` is not allowed");
Ok(())
}
pub fn rpc_endpoint(&self, url_or_alias: &str) -> Result<ResolvedRpcEndpoint> {
if let Some(endpoint) = self.rpc_endpoints.get(url_or_alias) {
Ok(endpoint.clone().try_resolve())
} else {
if url_or_alias.starts_with("http") ||
url_or_alias.starts_with("ws") ||
Path::new(url_or_alias).exists()
{
let url = RpcEndpointUrl::Env(url_or_alias.to_string());
Ok(RpcEndpoint::new(url).resolve())
} else {
Err(fmt_err!("invalid rpc url: {url_or_alias}"))
}
}
}
pub fn rpc_urls(&self) -> Result<Vec<Rpc>> {
let mut urls = Vec::with_capacity(self.rpc_endpoints.len());
for alias in self.rpc_endpoints.keys() {
let url = self.rpc_endpoint(alias)?.url()?;
urls.push(Rpc { key: alias.clone(), url });
}
Ok(urls)
}
}
impl Default for CheatsConfig {
fn default() -> Self {
Self {
ffi: false,
always_use_create_2_factory: false,
prompt_timeout: Duration::from_secs(120),
rpc_storage_caching: Default::default(),
no_storage_caching: false,
rpc_endpoints: Default::default(),
paths: ProjectPathsConfig::builder().build_with_root("./"),
fs_permissions: Default::default(),
root: Default::default(),
broadcast: Default::default(),
allowed_paths: vec![],
evm_opts: Default::default(),
labels: Default::default(),
available_artifacts: Default::default(),
running_contract: Default::default(),
running_version: Default::default(),
assertions_revert: true,
seed: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use foundry_config::fs_permissions::PathPermission;
fn config(root: &str, fs_permissions: FsPermissions) -> CheatsConfig {
CheatsConfig::new(
&Config { root: root.into(), fs_permissions, ..Default::default() },
Default::default(),
None,
None,
None,
)
}
#[test]
fn test_allowed_paths() {
let root = "/my/project/root/";
let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Read).is_ok());
assert!(config.ensure_path_allowed("./t.txt", FsAccessKind::Write).is_ok());
assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Read).is_ok());
assert!(config.ensure_path_allowed("../root/t.txt", FsAccessKind::Write).is_ok());
assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Read).is_err());
assert!(config.ensure_path_allowed("../../root/t.txt", FsAccessKind::Write).is_err());
}
#[test]
fn test_is_foundry_toml() {
let root = "/my/project/root/";
let config = config(root, FsPermissions::new(vec![PathPermission::read_write("./")]));
let f = format!("{root}foundry.toml");
assert!(config.is_foundry_toml(f));
let f = format!("{root}Foundry.toml");
assert!(config.is_foundry_toml(f));
let f = format!("{root}lib/other/foundry.toml");
assert!(!config.is_foundry_toml(f));
}
}