diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 0422c484bfc6f..68151f1466bb5 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -5864,6 +5864,406 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "forkAddress", + "description": "Gets the value for the key `key` from the currently active fork and parses it as `address`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkAddress(string memory key) external view returns (address);", + "visibility": "external", + "mutability": "view", + "signature": "forkAddress(string)", + "selector": "0x27b1efc0", + "selectorBytes": [ + 39, + 177, + 239, + 192 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkBool", + "description": "Gets the value for the key `key` from the currently active fork and parses it as `bool`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkBool(string memory key) external view returns (bool);", + "visibility": "external", + "mutability": "view", + "signature": "forkBool(string)", + "selector": "0xc06e85dd", + "selectorBytes": [ + 192, + 110, + 133, + 221 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkBytes", + "description": "Gets the value for the key `key` from the currently active fork and parses it as `bytes`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkBytes(string memory key) external view returns (bytes memory);", + "visibility": "external", + "mutability": "view", + "signature": "forkBytes(string)", + "selector": "0x7c055631", + "selectorBytes": [ + 124, + 5, + 86, + 49 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkBytes32", + "description": "Gets the value for the key `key` from the currently active fork and parses it as `bytes32`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkBytes32(string memory key) external view returns (bytes32);", + "visibility": "external", + "mutability": "view", + "signature": "forkBytes32(string)", + "selector": "0xa912a3f4", + "selectorBytes": [ + 169, + 18, + 163, + 244 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChain", + "description": "Returns the chain name of the currently selected fork.", + "declaration": "function forkChain() external view returns (string memory);", + "visibility": "external", + "mutability": "view", + "signature": "forkChain()", + "selector": "0xe6075d94", + "selectorBytes": [ + 230, + 7, + 93, + 148 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChainAddress", + "description": "Gets the value for the key `key` from the fork config for chain `chain` and parses it as `address`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkChainAddress(uint256 chain, string memory key) external view returns (address);", + "visibility": "external", + "mutability": "view", + "signature": "forkChainAddress(uint256,string)", + "selector": "0x5dc00a35", + "selectorBytes": [ + 93, + 192, + 10, + 53 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChainBool", + "description": "Gets the value for the key `key` from the fork config for chain `chain` and parses it as `bool`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkChainBool(uint256 chain, string memory key) external view returns (bool);", + "visibility": "external", + "mutability": "view", + "signature": "forkChainBool(uint256,string)", + "selector": "0x8f947e16", + "selectorBytes": [ + 143, + 148, + 126, + 22 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChainBytes", + "description": "Gets the value for the key `key` from the fork config for chain `chain` and parses it as `bytes`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkChainBytes(uint256 chain, string memory key) external view returns (bytes memory);", + "visibility": "external", + "mutability": "view", + "signature": "forkChainBytes(uint256,string)", + "selector": "0x0e73256d", + "selectorBytes": [ + 14, + 115, + 37, + 109 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChainBytes32", + "description": "Gets the value for the key `key` from the fork config for chain `chain` and parses it as `bytes32`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkChainBytes32(uint256 chain, string memory key) external view returns (bytes32);", + "visibility": "external", + "mutability": "view", + "signature": "forkChainBytes32(uint256,string)", + "selector": "0x05dd6d07", + "selectorBytes": [ + 5, + 221, + 109, + 7 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChainId", + "description": "Returns the chain id of the currently selected fork.", + "declaration": "function forkChainId() external view returns (uint256);", + "visibility": "external", + "mutability": "view", + "signature": "forkChainId()", + "selector": "0xe6b661e2", + "selectorBytes": [ + 230, + 182, + 97, + 226 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChainIds", + "description": "Returns an array with the ids of all the configured fork chains.\nNote that the configured fork chains are subsections of the `[fork]` section of 'foundry.toml'.", + "declaration": "function forkChainIds() external view returns (uint256[] memory);", + "visibility": "external", + "mutability": "view", + "signature": "forkChainIds()", + "selector": "0x04dc8feb", + "selectorBytes": [ + 4, + 220, + 143, + 235 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChainInt", + "description": "Gets the value for the key `key` from the fork config for chain `chain` and parses it as `int256`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkChainInt(uint256 chain, string memory key) external view returns (int256);", + "visibility": "external", + "mutability": "view", + "signature": "forkChainInt(uint256,string)", + "selector": "0xc1ff595f", + "selectorBytes": [ + 193, + 255, + 89, + 95 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChainRpcUrl", + "description": "Returns the rpc url of the corresponding chain id.\nBy default, the rpc url of each fork is derived from the `[rpc_endpoints]`, unless\nthe rpc config is specifically informed in the fork config for that specific chain.", + "declaration": "function forkChainRpcUrl(uint256 id) external view returns (string memory);", + "visibility": "external", + "mutability": "view", + "signature": "forkChainRpcUrl(uint256)", + "selector": "0xa2c51cca", + "selectorBytes": [ + 162, + 197, + 28, + 202 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChainString", + "description": "Gets the value for the key `key` from the fork config for chain `chain` and parses it as `string`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkChainString(uint256 chain, string memory key) external view returns (string memory);", + "visibility": "external", + "mutability": "view", + "signature": "forkChainString(uint256,string)", + "selector": "0x24062881", + "selectorBytes": [ + 36, + 6, + 40, + 129 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChainUint", + "description": "Gets the value for the key `key` from the fork config for chain `chain` and parses it as `uint256`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkChainUint(uint256 chain, string memory key) external view returns (uint256);", + "visibility": "external", + "mutability": "view", + "signature": "forkChainUint(uint256,string)", + "selector": "0x2bfffa5c", + "selectorBytes": [ + 43, + 255, + 250, + 92 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkChains", + "description": "Returns an array with the name of all the configured fork chains.\nNote that the configured fork chains are subsections of the `[fork]` section of 'foundry.toml'.", + "declaration": "function forkChains() external view returns (string[] memory);", + "visibility": "external", + "mutability": "view", + "signature": "forkChains()", + "selector": "0x081f64cd", + "selectorBytes": [ + 8, + 31, + 100, + 205 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkInt", + "description": "Gets the value for the key `key` from the currently active fork and parses it as `int256`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkInt(string memory key) external view returns (int256);", + "visibility": "external", + "mutability": "view", + "signature": "forkInt(string)", + "selector": "0x4b326af0", + "selectorBytes": [ + 75, + 50, + 106, + 240 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkRpcUrl", + "description": "Returns the rpc url of the currently selected fork.\nBy default, the rpc url of each fork is derived from the `[rpc_endpoints]`, unless\nthe rpc config is specifically informed in the fork config for that specific chain.", + "declaration": "function forkRpcUrl() external view returns (string memory);", + "visibility": "external", + "mutability": "view", + "signature": "forkRpcUrl()", + "selector": "0x66d48d21", + "selectorBytes": [ + 102, + 212, + 141, + 33 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkString", + "description": "Gets the value for the key `key` from the currently active fork and parses it as `string`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkString(string memory key) external view returns (string memory);", + "visibility": "external", + "mutability": "view", + "signature": "forkString(string)", + "selector": "0xd8525752", + "selectorBytes": [ + 216, + 82, + 87, + 82 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "forkUint", + "description": "Gets the value for the key `key` from the currently active fork and parses it as `uint256`.\nReverts if the key was not found or the value could not be parsed.", + "declaration": "function forkUint(string memory key) external view returns (uint256);", + "visibility": "external", + "mutability": "view", + "signature": "forkUint(string)", + "selector": "0xbe0fe4a8", + "selectorBytes": [ + 190, + 15, + 228, + 168 + ] + }, + "group": "forking", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "foundryVersionAtLeast", diff --git a/crates/cheatcodes/assets/cheatcodes.schema.json b/crates/cheatcodes/assets/cheatcodes.schema.json index c98cfb69357bd..ef0e061b22764 100644 --- a/crates/cheatcodes/assets/cheatcodes.schema.json +++ b/crates/cheatcodes/assets/cheatcodes.schema.json @@ -241,6 +241,11 @@ "type": "string", "const": "scripting" }, + { + "description": "Cheatcodes that interact with the program's forking configuration.\n\nExamples: `forkChains`, `forkChainRpcUrl`, `forkUint`.\n\nSafety: safe.", + "type": "string", + "const": "forking" + }, { "description": "Cheatcodes that interact with the OS or filesystem.\n\nExamples: `ffi`, `projectRoot`, `writeFile`.\n\nSafety: safe.", "type": "string", diff --git a/crates/cheatcodes/spec/src/cheatcode.rs b/crates/cheatcodes/spec/src/cheatcode.rs index 69fe97781c719..216adb4aa84b2 100644 --- a/crates/cheatcodes/spec/src/cheatcode.rs +++ b/crates/cheatcodes/spec/src/cheatcode.rs @@ -86,6 +86,12 @@ pub enum Group { /// /// Safety: safe. Scripting, + /// Cheatcodes that interact with the program's forking configuration. + /// + /// Examples: `forkChains`, `forkChainRpcUrl`, `forkUint`. + /// + /// Safety: safe. + Forking, /// Cheatcodes that interact with the OS or filesystem. /// /// Examples: `ffi`, `projectRoot`, `writeFile`. @@ -140,6 +146,7 @@ impl Group { match self { Self::Evm | Self::Testing => None, Self::Scripting + | Self::Forking | Self::Filesystem | Self::Environment | Self::String @@ -157,6 +164,7 @@ impl Group { Self::Evm => "evm", Self::Testing => "testing", Self::Scripting => "scripting", + Self::Forking => "forking", Self::Filesystem => "filesystem", Self::Environment => "environment", Self::String => "string", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 9f00928ef2418..a0eb2f59e04ee 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -2179,8 +2179,113 @@ interface Vm { #[cheatcode(group = Environment)] function isContext(ForgeContext context) external view returns (bool result); - // ======== Scripts ======== + // ======== Forks ======== + + /// Returns an array with the name of all the configured fork chains. + /// + /// Note that the configured fork chains are subsections of the `[fork]` section of 'foundry.toml'. + #[cheatcode(group = Forking)] + function forkChains() external view returns (string[] memory); + + /// Returns an array with the ids of all the configured fork chains. + /// + /// Note that the configured fork chains are subsections of the `[fork]` section of 'foundry.toml'. + #[cheatcode(group = Forking)] + function forkChainIds() external view returns (uint256[] memory); + + /// Returns the chain name of the currently selected fork. + #[cheatcode(group = Forking)] + function forkChain() external view returns (string memory); + /// Returns the chain id of the currently selected fork. + #[cheatcode(group = Forking)] + function forkChainId() external view returns (uint256); + + /// Returns the rpc url of the currently selected fork. + /// + /// By default, the rpc url of each fork is derived from the `[rpc_endpoints]`, unless + /// the rpc config is specifically informed in the fork config for that specific chain. + #[cheatcode(group = Forking)] + function forkRpcUrl() external view returns (string memory); + + /// Returns the rpc url of the corresponding chain id. + /// + /// By default, the rpc url of each fork is derived from the `[rpc_endpoints]`, unless + /// the rpc config is specifically informed in the fork config for that specific chain. + #[cheatcode(group = Forking)] + function forkChainRpcUrl(uint256 id) external view returns (string memory); + + /// Gets the value for the key `key` from the currently active fork and parses it as `bool`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkBool(string memory key) external view returns (bool); + + /// Gets the value for the key `key` from the fork config for chain `chain` and parses it as `bool`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkChainBool(uint256 chain, string memory key) external view returns (bool); + + /// Gets the value for the key `key` from the currently active fork and parses it as `int256`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkInt(string memory key) external view returns (int256); + + /// Gets the value for the key `key` from the fork config for chain `chain` and parses it as `int256`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkChainInt(uint256 chain, string memory key) external view returns (int256); + + /// Gets the value for the key `key` from the currently active fork and parses it as `uint256`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkUint(string memory key) external view returns (uint256); + + /// Gets the value for the key `key` from the fork config for chain `chain` and parses it as `uint256`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkChainUint(uint256 chain, string memory key) external view returns (uint256); + + /// Gets the value for the key `key` from the currently active fork and parses it as `address`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkAddress(string memory key) external view returns (address); + + /// Gets the value for the key `key` from the fork config for chain `chain` and parses it as `address`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkChainAddress(uint256 chain, string memory key) external view returns (address); + + /// Gets the value for the key `key` from the currently active fork and parses it as `bytes32`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkBytes32(string memory key) external view returns (bytes32); + + /// Gets the value for the key `key` from the fork config for chain `chain` and parses it as `bytes32`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkChainBytes32(uint256 chain, string memory key) external view returns (bytes32); + + /// Gets the value for the key `key` from the currently active fork and parses it as `string`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkString(string memory key) external view returns (string memory); + + /// Gets the value for the key `key` from the fork config for chain `chain` and parses it as `string`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkChainString(uint256 chain, string memory key) external view returns (string memory); + + /// Gets the value for the key `key` from the currently active fork and parses it as `bytes`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkBytes(string memory key) external view returns (bytes memory); + + /// Gets the value for the key `key` from the fork config for chain `chain` and parses it as `bytes`. + /// Reverts if the key was not found or the value could not be parsed. + #[cheatcode(group = Forking)] + function forkChainBytes(uint256 chain, string memory key) external view returns (bytes memory); + + // ======== Scripts ======== // -------- Broadcasting Transactions -------- /// Has the next call (at this call depth only) create transactions that can later be signed and sent onchain. diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index 4ea7f2db28800..361c1a9fe59da 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -4,8 +4,8 @@ use alloy_primitives::{U256, map::AddressHashMap}; use foundry_common::{ContractsByArtifact, fs::normalize_path}; use foundry_compilers::{ArtifactId, ProjectPathsConfig, utils::canonicalize}; use foundry_config::{ - Config, FsPermissions, ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, - cache::StorageCachingConfig, fs_permissions::FsAccessKind, + Config, ForkConfig, FsPermissions, ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, + RpcEndpointUrl, cache::StorageCachingConfig, fs_permissions::FsAccessKind, }; use foundry_evm_core::opts::EvmOpts; use std::{ @@ -63,6 +63,8 @@ pub struct CheatsConfig { pub chains: HashMap, /// Mapping of chain IDs to their aliases pub chain_id_to_alias: HashMap, + /// Fork configuration + pub forks: HashMap, } /// Chain data for getChain cheatcodes @@ -114,6 +116,7 @@ impl CheatsConfig { internal_expect_revert: config.allow_internal_expect_revert, chains: HashMap::new(), chain_id_to_alias: HashMap::new(), + forks: config.forks.clone(), } } @@ -318,6 +321,7 @@ impl Default for CheatsConfig { internal_expect_revert: false, chains: HashMap::new(), chain_id_to_alias: HashMap::new(), + forks: Default::default(), } } } diff --git a/crates/cheatcodes/src/fork.rs b/crates/cheatcodes/src/fork.rs new file mode 100644 index 0000000000000..8c15218e3424d --- /dev/null +++ b/crates/cheatcodes/src/fork.rs @@ -0,0 +1,288 @@ +//! Implementations of [`Forking`](spec::Group::Forking) cheatcodes. + +use crate::{Cheatcode, CheatsCtxt, DatabaseExt, Error, Result, Vm::*, string}; +use alloy_dyn_abi::DynSolType; +use alloy_sol_types::SolValue; +use eyre::OptionExt; +use foundry_evm_core::ContextExt; + +impl Cheatcode for forkChainIdsCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self {} = self; + Ok(state + .config + .forks + .keys() + .map(|name| alloy_chains::Chain::from_named(name.parse().unwrap()).id()) + .collect::>() + .abi_encode()) + } +} + +impl Cheatcode for forkChainsCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self {} = self; + Ok(state.config.forks.keys().collect::>().abi_encode()) + } +} + +impl Cheatcode for forkChainCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + Ok(get_active_fork_chain_name(ccx)?.abi_encode()) + } +} + +impl Cheatcode for forkChainIdCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + Ok(get_active_fork_chain_id(ccx)?.abi_encode()) + } +} + +fn resolve_rpc_url(name: &'static str, state: &mut crate::Cheatcodes) -> Result { + // Get the chain ID from the chain_configs + if let Some(config) = state.config.forks.get(name) { + let rpc = match config.rpc_endpoint { + Some(ref url) => url.clone().resolve(), + None => state.config.rpc_endpoint(name)?, + }; + + return Ok(rpc.url()?.abi_encode()); + } + + bail!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'") +} + +impl Cheatcode for forkChainRpcUrlCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self { id } = self; + let name = get_chain_name(id.to::())?; + resolve_rpc_url(name, state) + } +} + +impl Cheatcode for forkRpcUrlCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let name = get_active_fork_chain_name(ccx)?; + resolve_rpc_url(name, ccx.state) + } +} + +fn cast_string(key: &str, val: &str, ty: &DynSolType) -> Result { + string::parse(val, ty).map_err(map_env_err(key, val)) +} + +/// Converts the error message of a failed parsing attempt to a more user-friendly message that +/// doesn't leak the value. +fn map_env_err<'a>(key: &'a str, value: &'a str) -> impl FnOnce(Error) -> Error + 'a { + move |e| { + // failed parsing as type `uint256`: parser error: + // + // ^ + // expected at least one digit + let mut e = e.to_string(); + e = e.replacen(&format!("\"{value}\""), &format!("${key}"), 1); + e = e.replacen(&format!("\n{value}\n"), &format!("\n${key}\n"), 1); + Error::from(e) + } +} + +/// Gets the alloy chain name for a given chain id. +fn get_chain_name(id: u64) -> Result<&'static str> { + let chain = alloy_chains::Chain::from_id(id) + .named() + .ok_or_eyre("unknown name for active forked chain")?; + + Ok(chain.as_str()) +} + +/// Gets the chain id of the active fork. Panics if no fork is selected. +fn get_active_fork_chain_id(ccx: &mut CheatsCtxt) -> Result { + let (db, _, env) = ccx.as_db_env_and_journal(); + if !db.is_forked_mode() { + bail!("a fork must be selected"); + } + Ok(env.cfg.chain_id) +} + +/// Gets the alloy chain name for the active fork. Panics if no fork is selected. +fn get_active_fork_chain_name(ccx: &mut CheatsCtxt) -> Result<&'static str> { + get_chain_name(get_active_fork_chain_id(ccx)?) +} + +impl Cheatcode for forkChainBoolCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self { chain, key } = self; + get_bool(chain.to::(), key, state) + } +} + +impl Cheatcode for forkBoolCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { key } = self; + get_bool(get_active_fork_chain_id(ccx)?, key, ccx.state) + } +} + +impl Cheatcode for forkChainIntCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self { chain, key } = self; + get_int256(chain.to::(), key, state) + } +} + +impl Cheatcode for forkIntCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { key } = self; + get_int256(get_active_fork_chain_id(ccx)?, key, ccx.state) + } +} + +impl Cheatcode for forkChainUintCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self { chain, key } = self; + get_uint256(chain.to::(), key, state) + } +} + +impl Cheatcode for forkUintCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { key } = self; + get_uint256(get_active_fork_chain_id(ccx)?, key, ccx.state) + } +} + +impl Cheatcode for forkChainAddressCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self { chain, key } = self; + get_type_from_str_input(chain.to::(), key, &DynSolType::Address, state) + } +} + +impl Cheatcode for forkAddressCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { key } = self; + let chain = get_active_fork_chain_id(ccx)?; + get_type_from_str_input(chain, key, &DynSolType::Address, ccx.state) + } +} + +impl Cheatcode for forkChainBytes32Call { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self { chain, key } = self; + get_type_from_str_input(chain.to::(), key, &DynSolType::FixedBytes(32), state) + } +} + +impl Cheatcode for forkBytes32Call { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { key } = self; + let chain = get_active_fork_chain_id(ccx)?; + get_type_from_str_input(chain, key, &DynSolType::FixedBytes(32), ccx.state) + } +} + +impl Cheatcode for forkChainBytesCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self { chain, key } = self; + get_type_from_str_input(chain.to::(), key, &DynSolType::Bytes, state) + } +} + +impl Cheatcode for forkBytesCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { key } = self; + let chain = get_active_fork_chain_id(ccx)?; + get_type_from_str_input(chain, key, &DynSolType::Bytes, ccx.state) + } +} + +impl Cheatcode for forkChainStringCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self { chain, key } = self; + get_type_from_str_input(chain.to::(), key, &DynSolType::String, state) + } +} + +impl Cheatcode for forkStringCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { key } = self; + let chain = get_active_fork_chain_id(ccx)?; + get_type_from_str_input(chain, key, &DynSolType::String, ccx.state) + } +} + +fn get_toml_value<'a>( + name: &'a str, + key: &'a str, + state: &'a crate::Cheatcodes, +) -> Result<&'a toml::Value> { + let config = state.config.forks.get(name).ok_or_else(|| { + fmt_err!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'") + })?; + let value = config + .vars + .get(key) + .ok_or_else(|| fmt_err!("Variable '{key}' not found in [fork.{name}] configuration"))?; + + Ok(value) +} + +fn get_bool(chain: u64, key: &str, state: &crate::Cheatcodes) -> Result { + let name = get_chain_name(chain)?; + let value = get_toml_value(name, key, state)?; + + if let Some(b) = value.as_bool() { + Ok(b.abi_encode()) + } else if let Some(v) = value.as_integer() { + Ok((v == 0).abi_encode()) + } else if let Some(s) = value.as_str() { + cast_string(key, s, &DynSolType::Bool) + } else { + bail!("Variable '{key}' in [fork.{name}] must be a boolean or a string"); + } +} + +fn get_int256(chain: u64, key: &str, state: &crate::Cheatcodes) -> Result { + let name = get_chain_name(chain)?; + let value = get_toml_value(name, key, state)?; + if let Some(int_value) = value.as_integer() { + Ok(int_value.abi_encode()) + } else if let Some(s) = value.as_str() { + cast_string(key, s, &DynSolType::Int(256)) + } else { + bail!("Variable '{key}' in [fork.{name}] must be an integer or a string"); + } +} + +fn get_uint256(chain: u64, key: &str, state: &crate::Cheatcodes) -> Result { + let name = get_chain_name(chain)?; + let value = get_toml_value(name, key, state)?; + + if let Some(int_value) = value.as_integer() { + if int_value >= 0 { + Ok((int_value as u64).abi_encode()) + } else { + bail!("Variable '{key}' in [fork.{name}] is a negative integer"); + } + } else if let Some(s) = value.as_str() { + cast_string(key, s, &DynSolType::Uint(256)) + } else { + bail!("Variable '{key}' in [fork.{name}] must be an integer or a string"); + } +} + +fn get_type_from_str_input( + chain: u64, + key: &str, + ty: &DynSolType, + state: &crate::Cheatcodes, +) -> Result { + let name = get_chain_name(chain)?; + let value = get_toml_value(name, key, state)?; + + if let Some(val) = value.as_str() { + cast_string(key, val, ty) + } else { + bail!("Variable '{key}' in [fork.{name}] must be a string"); + } +} diff --git a/crates/cheatcodes/src/lib.rs b/crates/cheatcodes/src/lib.rs index 37e2afc1471ad..f793c97bc017c 100644 --- a/crates/cheatcodes/src/lib.rs +++ b/crates/cheatcodes/src/lib.rs @@ -42,6 +42,8 @@ mod version; mod env; pub use env::set_execution_context; +mod fork; + mod evm; mod fs; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 2aa4cc7b61a41..332fadf0d6662 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -45,7 +45,7 @@ use semver::Version; use serde::{Deserialize, Serialize, Serializer}; use std::{ borrow::Cow, - collections::BTreeMap, + collections::{BTreeMap, HashMap}, fs, path::{Path, PathBuf}, str::FromStr, @@ -436,6 +436,8 @@ pub struct Config { /// Multiple rpc endpoints and their aliases #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")] pub rpc_endpoints: RpcEndpoints, + /// Fork configuration + pub forks: HashMap, /// Whether to store the referenced sources in the metadata as literal data. pub use_literal_content: bool, /// Whether to include the metadata hash. @@ -551,6 +553,18 @@ pub struct Config { pub _non_exhaustive: (), } +/// Fork-scoped config for tests and scripts. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ForkConfig { + // Optional RPC endpoint for the fork. + // + // If uninformed, it will attempt to load one from `[rpc_endpoints]` with a matching alias + // for the name of the forked chain. + pub rpc_endpoint: Option, + // Any arbitrary key-value pair of variables. + pub vars: HashMap, +} + /// Mapping of fallback standalone sections. See [`FallbackProfileProvider`]. pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")]; @@ -583,6 +597,7 @@ impl Config { "soldeer", "vyper", "bind_json", + "forks", ]; /// File name of config toml file @@ -2453,6 +2468,7 @@ impl Default for Config { compilation_restrictions: Default::default(), script_execution_protection: true, _non_exhaustive: (), + forks: Default::default(), } } } @@ -2612,6 +2628,7 @@ mod tests { use foundry_compilers::artifacts::{ ModelCheckerEngine, YulDetails, vyper::VyperOptimizationMode, }; + use itertools::Itertools; use similar_asserts::assert_eq; use soldeer_core::remappings::RemappingsLocation; use std::{fs::File, io::Write}; @@ -5129,4 +5146,59 @@ mod tests { Ok(()) }); } + + #[test] + fn test_get_script_config() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [forks] + + [forks.mainnet] + rpc_endpoint = "mainnet-rpc" + + [forks.mainnet.vars] + weth = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + usdc = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" + pool_name = "USDC-ETH" + pool_fee = 3000 + max_slippage = 500 + "#, + )?; + let config = Config::load().unwrap(); + + let expected: HashMap = vec![( + "mainnet".to_string(), + ForkConfig { + rpc_endpoint: Some(RpcEndpoint::new(RpcEndpointUrl::Url( + "mainnet-rpc".to_string(), + ))), + vars: vec![ + ("weth".into(), "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".into()), + ("usdc".into(), "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".into()), + ("pool_name".into(), "USDC-ETH".into()), + ("pool_fee".into(), 3000.into()), + ("max_slippage".into(), 500.into()), + ] + .into_iter() + .collect(), + }, + )] + .into_iter() + .collect(); + assert_eq!( + expected.keys().sorted().collect::>(), + config.forks.keys().sorted().collect::>() + ); + + let expected_mainnet = expected.get("mainnet").unwrap(); + let mainnet = config.forks.get("mainnet").unwrap(); + assert_eq!(expected_mainnet.rpc_endpoint, mainnet.rpc_endpoint); + for (k, v) in &expected_mainnet.vars { + assert_eq!(v, mainnet.vars.get(k).unwrap()); + } + Ok(()) + }); + } } diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 734742e49b581..2eac161ade76c 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -172,6 +172,7 @@ forgetest!(can_extract_config_values, |prj, cmd| { additional_compiler_profiles: Default::default(), compilation_restrictions: Default::default(), script_execution_protection: true, + forks: Default::default(), _non_exhaustive: (), }; prj.write_config(input.clone()); @@ -1136,6 +1137,8 @@ out = "utils/JsonBindings.sol" include = [] exclude = [] +[forks] + "#]]); @@ -1274,6 +1277,7 @@ exclude = [] }, "no_storage_caching": false, "no_rpc_rate_limit": false, + "forks": {}, "use_literal_content": false, "bytecode_hash": "ipfs", "cbor_metadata": true, diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index a03ca88dba72c..29f274b5190eb 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -5,6 +5,7 @@ use alloy_hardforks::EthereumHardfork; use alloy_primitives::{Address, Bytes, address, hex}; use anvil::{NodeConfig, spawn}; use forge_script_sequence::ScriptSequence; +use foundry_config::{ForkConfig, RpcEndpoint, RpcEndpointUrl, RpcEndpoints}; use foundry_test_utils::{ ScriptOutcome, ScriptTester, rpc::{self, next_http_archive_rpc_url}, @@ -3231,3 +3232,273 @@ Traces: Error: script failed: call to non-contract address [..] "#]]); }); + +// Tests that can access the fork config for each chain from `foundry.toml` +forgetest_init!(can_access_fork_config_chain_ids, |prj, cmd| { + prj.insert_vm(); + prj.insert_console(); + prj.insert_ds_test(); + + prj.update_config(|config| { + config.forks = vec![ + ( + "mainnet".to_string(), + ForkConfig { + rpc_endpoint: Some(RpcEndpoint::new(RpcEndpointUrl::Url( + "mainnet-rpc".to_string(), + ))), + vars: vec![ + ("i256".into(), "-1234".into()), + ("u256".into(), 1234.into()), + ("bool".into(), true.into()), + ( + "b256".into(), + "0xdeadbeaf00000000000000000000000000000000000000000000000000000000" + .into(), + ), + ("addr".into(), "0xdeadbeef00000000000000000000000000000000".into()), + ("bytes".into(), "0x00000000000f00".into()), + ("str".into(), "bar".into()), + ] + .into_iter() + .collect(), + }, + ), + ( + "optimism".to_string(), + ForkConfig { + rpc_endpoint: None, + vars: vec![ + ("i256".into(), "-4321".into()), + ("u256".into(), 4321.into()), + ("bool".into(), "false".into()), + ( + "b256".into(), + "0x000000000000000000000000000000000000000000000000000000deadc0ffee" + .into(), + ), + ("addr".into(), "0x00000000000000000000000000000000deadbeef".into()), + ("bytes".into(), "0x00f00000000000".into()), + ("str".into(), "bazz".into()), + ] + .into_iter() + .collect(), + }, + ), + ] + .into_iter() + .collect(); + + config.rpc_endpoints = RpcEndpoints::new(vec![( + "optimism", + RpcEndpoint::new(RpcEndpointUrl::Url("optimism-rpc".to_string())), + )]); + }); + + prj.add_source( + "ForkScript.s.sol", + r#" +import {Vm} from "./Vm.sol"; +import {DSTest} from "./test.sol"; +import {console} from "./console.sol"; + +contract ForkScript is DSTest { + Vm vm = Vm(HEVM_ADDRESS); + + function run() public view { + (uint256[2] memory chainIds, string[2] memory chains) = ([uint256(1), uint256(10)], ["mainnet", "optimism"]); + (uint256[] memory cheatChainIds, string[] memory cheatChains) = (vm.forkChainIds(), vm.forkChains()); + + for (uint256 i = 0; i < chains.length; i++) { + assert(chainIds[i] == cheatChainIds[0] || chainIds[i] == cheatChainIds[1]); + assert(eqString(chains[i], cheatChains[0]) || eqString(chains[i], cheatChains[1])); + console.log("chain:", chains[i]); + console.log("id:", chainIds[i]); + + string memory rpc = vm.forkChainRpcUrl(chainIds[i]); + int256 i256 = vm.forkChainInt(chainIds[i], "i256"); + uint256 u256 = vm.forkChainUint(chainIds[i], "u256"); + bool boolean = vm.forkChainBool(chainIds[i], "bool"); + address addr = vm.forkChainAddress(chainIds[i], "addr"); + bytes32 b256 = vm.forkChainBytes32(chainIds[i], "b256"); + bytes memory byytes = vm.forkChainBytes(chainIds[i], "bytes"); + string memory str = vm.forkChainString(chainIds[i], "str"); + + console.log(" > rpc:", rpc); + console.log(" > vars:"); + console.log(" > i256:", i256); + console.log(" > u256:", u256); + console.log(" > bool:", boolean); + console.log(" > addr:", addr); + console.log(" > string:", str); + + assert( + b256 == 0xdeadbeaf00000000000000000000000000000000000000000000000000000000 + || b256 == 0x000000000000000000000000000000000000000000000000000000deadc0ffee + ); + } + } + + function eqString(string memory s1, string memory s2) public pure returns(bool) { + return keccak256(bytes(s1)) == keccak256(bytes(s2)); + } +} + "#, + ) + .unwrap(); + + cmd.arg("script").arg("ForkScript").assert_success().stdout_eq(str![[r#" +... + chain: mainnet + id: 1 + > rpc: mainnet-rpc + > vars: + > i256: -1234 + > u256: 1234 + > bool: true + > addr: 0xdEADBEeF00000000000000000000000000000000 + > string: bar + chain: optimism + id: 10 + > rpc: optimism-rpc + > vars: + > i256: -4321 + > u256: 4321 + > bool: false + > addr: 0x00000000000000000000000000000000DeaDBeef + > string: bazz + +"#]]); +}); + +// Tests that can derive chain id of the active fork + get the config from `foundry.toml` +forgetest_init!(can_derive_chain_id_access_fork_config, |prj, cmd| { + prj.insert_vm(); + prj.insert_console(); + prj.insert_ds_test(); + let mainnet_endpoint = rpc::next_http_rpc_endpoint(); + + prj.update_config(|config| { + config.forks = vec![ + ( + "mainnet".to_string(), + ForkConfig { + rpc_endpoint: Some(RpcEndpoint::new(RpcEndpointUrl::Url( + mainnet_endpoint.clone(), + ))), + vars: vec![ + ("i256".into(), "-1234".into()), + ("u256".into(), 1234.into()), + ("bool".into(), true.into()), + ( + "b256".into(), + "0xdeadbeaf00000000000000000000000000000000000000000000000000000000" + .into(), + ), + ("addr".into(), "0xdeadbeef00000000000000000000000000000000".into()), + ("bytes".into(), "0x00000000000f00".into()), + ("str".into(), "bar".into()), + ] + .into_iter() + .collect(), + }, + ), + ( + "optimism".to_string(), + ForkConfig { + rpc_endpoint: None, + vars: vec![ + ("i256".into(), "-4321".into()), + ("u256".into(), 4321.into()), + ("bool".into(), "false".into()), + ( + "b256".into(), + "0x000000000000000000000000000000000000000000000000000000deadc0ffee" + .into(), + ), + ("addr".into(), "0x00000000000000000000000000000000deadbeef".into()), + ("bytes".into(), "0x00f00000000000".into()), + ("str".into(), "bazz".into()), + ] + .into_iter() + .collect(), + }, + ), + ] + .into_iter() + .collect(); + + config.rpc_endpoints = RpcEndpoints::new(vec![( + "optimism", + RpcEndpoint::new(RpcEndpointUrl::Url("optimism-rpc".to_string())), + )]); + }); + + prj.add_source( + "ForkTest.t.sol", + &r#" +import {Vm} from "./Vm.sol"; +import {DSTest} from "./test.sol"; +import {console} from "./console.sol"; + +contract ForkTest is DSTest { + Vm vm = Vm(HEVM_ADDRESS); + + function test_panicsWhithoutSelectedFork() public { + vm.forkChain(); + } + + function test_forkVars() public { + vm.createSelectFork(""); + + console.log("chain:", vm.forkChain()); + console.log("id:", vm.forkChainId()); + assert(eqString(vm.forkRpcUrl(), "")); + + int256 i256 = vm.forkInt("i256"); + uint256 u256 = vm.forkUint("u256"); + bool boolean = vm.forkBool("bool"); + address addr = vm.forkAddress("addr"); + bytes32 b256 = vm.forkBytes32("b256"); + bytes memory byytes = vm.forkBytes("bytes"); + string memory str = vm.forkString("str"); + + console.log(" > vars:"); + console.log(" > i256:", i256); + console.log(" > u256:", u256); + console.log(" > bool:", boolean); + console.log(" > addr:", addr); + console.log(" > string:", str); + + assert( + b256 == 0xdeadbeaf00000000000000000000000000000000000000000000000000000000 + || b256 == 0x000000000000000000000000000000000000000000000000000000deadc0ffee + ); + } + + function eqString(string memory s1, string memory s2) public pure returns(bool) { + return keccak256(bytes(s1)) == keccak256(bytes(s2)); + } +} + "# + .replace("", &mainnet_endpoint), + ) + .unwrap(); + + cmd.args(["test", "-vvv", "ForkTest"]).assert_failure().stdout_eq(str![[r#" +... +[PASS] test_forkVars() ([GAS]) +Logs: + chain: mainnet + id: 1 + > vars: + > i256: -1234 + > u256: 1234 + > bool: true + > addr: 0xdEADBEeF00000000000000000000000000000000 + > string: bar + +[FAIL: vm.forkChain: a fork must be selected] test_panicsWhithoutSelectedFork() ([GAS]) +... +"#]]); +}); diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index dfbef7a7df762..c9247b2de9abd 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -286,6 +286,26 @@ interface Vm { function expectSafeMemoryCall(uint64 min, uint64 max) external; function fee(uint256 newBasefee) external; function ffi(string[] calldata commandInput) external returns (bytes memory result); + function forkAddress(string memory key) external view returns (address); + function forkBool(string memory key) external view returns (bool); + function forkBytes(string memory key) external view returns (bytes memory); + function forkBytes32(string memory key) external view returns (bytes32); + function forkChain() external view returns (string memory); + function forkChainAddress(uint256 chain, string memory key) external view returns (address); + function forkChainBool(uint256 chain, string memory key) external view returns (bool); + function forkChainBytes(uint256 chain, string memory key) external view returns (bytes memory); + function forkChainBytes32(uint256 chain, string memory key) external view returns (bytes32); + function forkChainId() external view returns (uint256); + function forkChainIds() external view returns (uint256[] memory); + function forkChainInt(uint256 chain, string memory key) external view returns (int256); + function forkChainRpcUrl(uint256 id) external view returns (string memory); + function forkChainString(uint256 chain, string memory key) external view returns (string memory); + function forkChainUint(uint256 chain, string memory key) external view returns (uint256); + function forkChains() external view returns (string[] memory); + function forkInt(string memory key) external view returns (int256); + function forkRpcUrl() external view returns (string memory); + function forkString(string memory key) external view returns (string memory); + function forkUint(string memory key) external view returns (uint256); function foundryVersionAtLeast(string calldata version) external view returns (bool); function foundryVersionCmp(string calldata version) external view returns (int256); function fsMetadata(string calldata path) external view returns (FsMetadata memory metadata);