From 48cfac59c6515d138319fb57431b1975fbcaa037 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Wed, 6 Aug 2025 07:10:39 -0700 Subject: [PATCH 01/15] setup script config env vars --- crates/cheatcodes/assets/cheatcodes.json | 200 +++++++++++++++++++++++ crates/cheatcodes/spec/src/vm.rs | 32 ++++ crates/cheatcodes/src/config.rs | 6 +- crates/cheatcodes/src/script.rs | 33 ++++ crates/config/src/lib.rs | 20 ++- crates/forge/tests/cli/config.rs | 1 + crates/forge/tests/cli/script.rs | 91 +++++++++-- testdata/cheats/Vm.sol | 10 ++ 8 files changed, 381 insertions(+), 12 deletions(-) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index deda608395ae5..c8473b2e8c458 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -6414,6 +6414,206 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "getScriptChainAddress", + "description": "", + "declaration": "function getScriptChainAddress(string memory chain, string memory key) external view returns (address);", + "visibility": "external", + "mutability": "view", + "signature": "getScriptChainAddress(string,string)", + "selector": "0x0397a972", + "selectorBytes": [ + 3, + 151, + 169, + 114 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getScriptChainBool", + "description": "", + "declaration": "function getScriptChainBool(string memory chain, string memory key) external view returns (bool);", + "visibility": "external", + "mutability": "view", + "signature": "getScriptChainBool(string,string)", + "selector": "0x5a9036e7", + "selectorBytes": [ + 90, + 144, + 54, + 231 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getScriptChainBytes", + "description": "", + "declaration": "function getScriptChainBytes(string memory chain, string memory key) external view returns (bytes memory);", + "visibility": "external", + "mutability": "view", + "signature": "getScriptChainBytes(string,string)", + "selector": "0xb22b866c", + "selectorBytes": [ + 178, + 43, + 134, + 108 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getScriptChainBytes32", + "description": "", + "declaration": "function getScriptChainBytes32(string memory chain, string memory key) external view returns (bytes32);", + "visibility": "external", + "mutability": "view", + "signature": "getScriptChainBytes32(string,string)", + "selector": "0x06e5e401", + "selectorBytes": [ + 6, + 229, + 228, + 1 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getScriptChainId", + "description": "", + "declaration": "function getScriptChainId(string memory chain) external view returns (uint256);", + "visibility": "external", + "mutability": "view", + "signature": "getScriptChainId(string)", + "selector": "0x1ebbc380", + "selectorBytes": [ + 30, + 187, + 195, + 128 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getScriptChainInt", + "description": "", + "declaration": "function getScriptChainInt(string memory chain, string memory key) external view returns (int256);", + "visibility": "external", + "mutability": "view", + "signature": "getScriptChainInt(string,string)", + "selector": "0x6d8f7a80", + "selectorBytes": [ + 109, + 143, + 122, + 128 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getScriptChainRpcUrl", + "description": "", + "declaration": "function getScriptChainRpcUrl(string memory chain) external view returns (string memory);", + "visibility": "external", + "mutability": "view", + "signature": "getScriptChainRpcUrl(string)", + "selector": "0x8a767a2d", + "selectorBytes": [ + 138, + 118, + 122, + 45 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getScriptChainString", + "description": "", + "declaration": "function getScriptChainString(string memory chain, string memory key) external view returns (string memory);", + "visibility": "external", + "mutability": "view", + "signature": "getScriptChainString(string,string)", + "selector": "0xd88b9d50", + "selectorBytes": [ + 216, + 139, + 157, + 80 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getScriptChainUint", + "description": "", + "declaration": "function getScriptChainUint(string memory chain, string memory key) external view returns (uint256);", + "visibility": "external", + "mutability": "view", + "signature": "getScriptChainUint(string,string)", + "selector": "0x9285c4a6", + "selectorBytes": [ + 146, + 133, + 196, + 166 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, + { + "func": { + "id": "getScriptChains", + "description": "", + "declaration": "function getScriptChains() external view returns (string[] memory);", + "visibility": "external", + "mutability": "view", + "signature": "getScriptChains()", + "selector": "0x1f280520", + "selectorBytes": [ + 31, + 40, + 5, + 32 + ] + }, + "group": "scripting", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "getStateDiff", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index c3722aae73ab7..80009d964158c 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -2177,6 +2177,38 @@ interface Vm { // ======== Scripts ======== + // -------- Loading Config Vars (from foundry.toml) -------- + + #[cheatcode(group = Scripting)] + function getScriptChains() external view returns (string[] memory); + + #[cheatcode(group = Scripting)] + function getScriptChainId(string memory chain) external view returns (uint256); + + #[cheatcode(group = Scripting)] + function getScriptChainRpcUrl(string memory chain) external view returns (string memory); + + #[cheatcode(group = Scripting)] + function getScriptChainBool(string memory chain, string memory key) external view returns (bool); + + #[cheatcode(group = Scripting)] + function getScriptChainInt(string memory chain, string memory key) external view returns (int256); + + #[cheatcode(group = Scripting)] + function getScriptChainUint(string memory chain, string memory key) external view returns (uint256); + + #[cheatcode(group = Scripting)] + function getScriptChainAddress(string memory chain, string memory key) external view returns (address); + + #[cheatcode(group = Scripting)] + function getScriptChainBytes32(string memory chain, string memory key) external view returns (bytes32); + + #[cheatcode(group = Scripting)] + function getScriptChainString(string memory chain, string memory key) external view returns (string memory); + + #[cheatcode(group = Scripting)] + function getScriptChainBytes(string memory chain, string memory key) external view returns (bytes memory); + // -------- 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..29b973bca97a1 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -5,7 +5,7 @@ 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, + ScriptConfig, 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, + /// Script configuration + pub script_config: ScriptConfig, } /// 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(), + script_config: config.script_config.clone(), } } @@ -318,6 +321,7 @@ impl Default for CheatsConfig { internal_expect_revert: false, chains: HashMap::new(), chain_id_to_alias: HashMap::new(), + script_config: Default::default(), } } } diff --git a/crates/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 209bc4d02f435..8a0f9909325e2 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -17,6 +17,39 @@ use revm::{ }; use std::sync::Arc; +impl Cheatcode for getScriptChainsCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self {} = self; + Ok(state.config.script_config.chains.keys().collect::>().abi_encode()) + } +} + +impl Cheatcode for getScriptChainIdCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self { chain } = self; + + // Get the chain ID from the chain_configs + if let Some(chain_config) = state.config.script_config.chains.get(chain) { + Ok(chain_config.id.abi_encode()) + } else { + bail!("'{chain}' subsection not found in [script] section of 'foundry.toml'"); + } + } +} + +impl Cheatcode for getScriptChainRpcUrlCall { + fn apply(&self, state: &mut crate::Cheatcodes) -> Result { + let Self { chain } = self; + + // Get the chain ID from the chain_configs + if let Some(config) = state.config.script_config.chains.get(chain) { + Ok(config.rpc_url.abi_encode()) + } else { + bail!("'{chain}' subsection not found in [script] section of 'foundry.toml'"); + } + } +} + impl Cheatcode for broadcast_0Call { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 2aa4cc7b61a41..f261f44d3445a 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, @@ -538,6 +538,9 @@ pub struct Config { /// Whether to enable script execution protection. pub script_execution_protection: bool, + /// Script (variables) configuration + pub script_config: ScriptConfig, + /// PRIVATE: This structure may grow, As such, constructing this structure should /// _always_ be done using a public constructor or update syntax: /// @@ -551,6 +554,20 @@ pub struct Config { pub _non_exhaustive: (), } +/// Foundry Script Config: Chains +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)] +pub struct ScriptConfig { + pub chains: HashMap, +} + +/// Foundry Script - Chain Config: Variables +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ChainConfig { + pub id: u64, + pub rpc_url: String, + pub vars: HashMap, +} + /// Mapping of fallback standalone sections. See [`FallbackProfileProvider`]. pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")]; @@ -2453,6 +2470,7 @@ impl Default for Config { compilation_restrictions: Default::default(), script_execution_protection: true, _non_exhaustive: (), + script_config: Default::default(), } } } diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index dd99de3fd281f..9532d6b82dd82 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, + script_config: Default::default(), _non_exhaustive: (), }; prj.write_config(input.clone()); diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 272ee21019349..5586cb8dacf52 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::ChainConfig; use foundry_test_utils::{ ScriptOutcome, ScriptTester, rpc::{self, next_http_archive_rpc_url}, @@ -13,7 +14,7 @@ use foundry_test_utils::{ }; use regex::Regex; use serde_json::Value; -use std::{env, fs, path::PathBuf}; +use std::{collections::HashMap, env, fs, path::PathBuf}; // Tests that fork cheat codes can be used in script forgetest_init!( @@ -2578,13 +2579,13 @@ Chain 31337 accessList [] chainId 31337 gasLimit [..] -gasPrice +gasPrice input [..] -maxFeePerBlobGas -maxFeePerGas -maxPriorityFeePerGas +maxFeePerBlobGas +maxFeePerGas +maxPriorityFeePerGas nonce 0 -to +to type 0 value 0 @@ -2593,11 +2594,11 @@ value 0 accessList [] chainId 31337 gasLimit 93856 -gasPrice +gasPrice input 0x7357f5d2000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000001c8 -maxFeePerBlobGas -maxFeePerGas -maxPriorityFeePerGas +maxFeePerBlobGas +maxFeePerGas +maxPriorityFeePerGas nonce 1 to 0x5FbDB2315678afecb367f032d93F642f64180aa3 type 0 @@ -3101,3 +3102,73 @@ contract FactoryScript is Script { .expect("no Counter contract"); assert_eq!(counter_contract.contract_name, Some("Counter".to_string())); }); + +// Tests that script can get chain ids from `foundry.toml` +forgetest_init!(can_access_chain_ids, |prj, cmd| { + prj.update_config(|config| { + config.script_config.chains = vec![ + ( + "mainnet".into(), + ChainConfig { id: 1, rpc_url: "mainnet-rpc".to_string(), vars: HashMap::new() }, + ), + ( + "optimism".into(), + ChainConfig { id: 10, rpc_url: "optimism-rpc".to_string(), vars: HashMap::new() }, + ), + ] + .into_iter() + .collect(); + }); + + prj.add_script( + "ScriptWithConfig.s.sol", + r#" + import {console} from "forge-std/Script.sol"; + + interface Vm { + function getScriptChains() external view returns (string[] memory); + function getScriptChainId(string memory chain) external view returns (uint256); + function getScriptChainRpcUrl(string memory chain) external view returns (string memory); + } + + contract ScriptWithConfig { + address internal constant HEVM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + Vm constant vm = Vm(HEVM_ADDRESS); + + function run() public view { + string[2] memory chains = ["mainnet", "optimism"]; + string[] memory cheatChains = vm.getScriptChains(); + for (uint256 i = 0; i < chains.length; i++) { + assert(eqString(chains[i], cheatChains[0]) || eqString(chains[i], cheatChains[1])); + console.log("chain:", chains[i]); + console.log(" > id:", vm.getScriptChainId(chains[i])); + console.log(" > rpc:", vm.getScriptChainRpcUrl(chains[i])); + } + } + + function eqString(string memory s1, string memory s2) public pure returns(bool) { + return keccak256(bytes(s1)) == keccak256(bytes(s2)); + } + } + "#, + ) + .unwrap(); + + cmd.arg("script").arg("ScriptWithConfig").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Script ran successfully. +[GAS] + +== Logs == + chain: mainnet + > id: 1 + > rpc: mainnet-rpc + chain: optimism + > id: 10 + > rpc: optimism-rpc + chain: base + +"#]]); +}); diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 5959572375115..4c68b19f41467 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -314,6 +314,16 @@ interface Vm { function getNonce(Wallet calldata wallet) external returns (uint64 nonce); function getRawBlockHeader(uint256 blockNumber) external view returns (bytes memory rlpHeader); function getRecordedLogs() external returns (Log[] memory logs); + function getScriptChainAddress(string memory chain, string memory key) external view returns (address); + function getScriptChainBool(string memory chain, string memory key) external view returns (bool); + function getScriptChainBytes(string memory chain, string memory key) external view returns (bytes memory); + function getScriptChainBytes32(string memory chain, string memory key) external view returns (bytes32); + function getScriptChainId(string memory chain) external view returns (uint256); + function getScriptChainInt(string memory chain, string memory key) external view returns (int256); + function getScriptChainRpcUrl(string memory chain) external view returns (string memory); + function getScriptChainString(string memory chain, string memory key) external view returns (string memory); + function getScriptChainUint(string memory chain, string memory key) external view returns (uint256); + function getScriptChains() external view returns (string[] memory); function getStateDiff() external view returns (string memory diff); function getStateDiffJson() external view returns (string memory diff); function getWallets() external returns (address[] memory wallets); From d9e5c8c5b94c0e2e4309fb6aa491554c0e296100 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 7 Aug 2025 11:01:35 -0700 Subject: [PATCH 02/15] feat(cheats): new fork group --- crates/cheatcodes/assets/cheatcodes.json | 600 ++++++++++++------ .../cheatcodes/assets/cheatcodes.schema.json | 5 + crates/cheatcodes/spec/src/cheatcode.rs | 8 + crates/cheatcodes/spec/src/vm.rs | 122 +++- crates/cheatcodes/src/config.rs | 10 +- crates/cheatcodes/src/fork.rs | 297 +++++++++ crates/cheatcodes/src/lib.rs | 2 + crates/cheatcodes/src/script.rs | 33 - crates/config/src/lib.rs | 80 ++- crates/forge/tests/cli/config.rs | 2 +- crates/forge/tests/cli/script.rs | 272 +++++++- testdata/cheats/Vm.sol | 30 +- 12 files changed, 1146 insertions(+), 315 deletions(-) create mode 100644 crates/cheatcodes/src/fork.rs diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index c8473b2e8c458..273b46b50c846 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -5854,6 +5854,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[]);", + "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", @@ -6414,206 +6814,6 @@ "status": "stable", "safety": "safe" }, - { - "func": { - "id": "getScriptChainAddress", - "description": "", - "declaration": "function getScriptChainAddress(string memory chain, string memory key) external view returns (address);", - "visibility": "external", - "mutability": "view", - "signature": "getScriptChainAddress(string,string)", - "selector": "0x0397a972", - "selectorBytes": [ - 3, - 151, - 169, - 114 - ] - }, - "group": "scripting", - "status": "stable", - "safety": "safe" - }, - { - "func": { - "id": "getScriptChainBool", - "description": "", - "declaration": "function getScriptChainBool(string memory chain, string memory key) external view returns (bool);", - "visibility": "external", - "mutability": "view", - "signature": "getScriptChainBool(string,string)", - "selector": "0x5a9036e7", - "selectorBytes": [ - 90, - 144, - 54, - 231 - ] - }, - "group": "scripting", - "status": "stable", - "safety": "safe" - }, - { - "func": { - "id": "getScriptChainBytes", - "description": "", - "declaration": "function getScriptChainBytes(string memory chain, string memory key) external view returns (bytes memory);", - "visibility": "external", - "mutability": "view", - "signature": "getScriptChainBytes(string,string)", - "selector": "0xb22b866c", - "selectorBytes": [ - 178, - 43, - 134, - 108 - ] - }, - "group": "scripting", - "status": "stable", - "safety": "safe" - }, - { - "func": { - "id": "getScriptChainBytes32", - "description": "", - "declaration": "function getScriptChainBytes32(string memory chain, string memory key) external view returns (bytes32);", - "visibility": "external", - "mutability": "view", - "signature": "getScriptChainBytes32(string,string)", - "selector": "0x06e5e401", - "selectorBytes": [ - 6, - 229, - 228, - 1 - ] - }, - "group": "scripting", - "status": "stable", - "safety": "safe" - }, - { - "func": { - "id": "getScriptChainId", - "description": "", - "declaration": "function getScriptChainId(string memory chain) external view returns (uint256);", - "visibility": "external", - "mutability": "view", - "signature": "getScriptChainId(string)", - "selector": "0x1ebbc380", - "selectorBytes": [ - 30, - 187, - 195, - 128 - ] - }, - "group": "scripting", - "status": "stable", - "safety": "safe" - }, - { - "func": { - "id": "getScriptChainInt", - "description": "", - "declaration": "function getScriptChainInt(string memory chain, string memory key) external view returns (int256);", - "visibility": "external", - "mutability": "view", - "signature": "getScriptChainInt(string,string)", - "selector": "0x6d8f7a80", - "selectorBytes": [ - 109, - 143, - 122, - 128 - ] - }, - "group": "scripting", - "status": "stable", - "safety": "safe" - }, - { - "func": { - "id": "getScriptChainRpcUrl", - "description": "", - "declaration": "function getScriptChainRpcUrl(string memory chain) external view returns (string memory);", - "visibility": "external", - "mutability": "view", - "signature": "getScriptChainRpcUrl(string)", - "selector": "0x8a767a2d", - "selectorBytes": [ - 138, - 118, - 122, - 45 - ] - }, - "group": "scripting", - "status": "stable", - "safety": "safe" - }, - { - "func": { - "id": "getScriptChainString", - "description": "", - "declaration": "function getScriptChainString(string memory chain, string memory key) external view returns (string memory);", - "visibility": "external", - "mutability": "view", - "signature": "getScriptChainString(string,string)", - "selector": "0xd88b9d50", - "selectorBytes": [ - 216, - 139, - 157, - 80 - ] - }, - "group": "scripting", - "status": "stable", - "safety": "safe" - }, - { - "func": { - "id": "getScriptChainUint", - "description": "", - "declaration": "function getScriptChainUint(string memory chain, string memory key) external view returns (uint256);", - "visibility": "external", - "mutability": "view", - "signature": "getScriptChainUint(string,string)", - "selector": "0x9285c4a6", - "selectorBytes": [ - 146, - 133, - 196, - 166 - ] - }, - "group": "scripting", - "status": "stable", - "safety": "safe" - }, - { - "func": { - "id": "getScriptChains", - "description": "", - "declaration": "function getScriptChains() external view returns (string[] memory);", - "visibility": "external", - "mutability": "view", - "signature": "getScriptChains()", - "selector": "0x1f280520", - "selectorBytes": [ - 31, - 40, - 5, - 32 - ] - }, - "group": "scripting", - "status": "stable", - "safety": "safe" - }, { "func": { "id": "getStateDiff", 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 80009d964158c..256851e221ae3 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -2179,35 +2179,109 @@ interface Vm { // -------- Loading Config Vars (from foundry.toml) -------- - #[cheatcode(group = Scripting)] - function getScriptChains() external view returns (string[] memory); - - #[cheatcode(group = Scripting)] - function getScriptChainId(string memory chain) external view returns (uint256); - - #[cheatcode(group = Scripting)] - function getScriptChainRpcUrl(string memory chain) external view returns (string memory); - - #[cheatcode(group = Scripting)] - function getScriptChainBool(string memory chain, string memory key) external view returns (bool); - - #[cheatcode(group = Scripting)] - function getScriptChainInt(string memory chain, string memory key) external view returns (int256); + /// 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); - #[cheatcode(group = Scripting)] - function getScriptChainUint(string memory chain, string memory key) external view returns (uint256); + /// 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[]); - #[cheatcode(group = Scripting)] - function getScriptChainAddress(string memory chain, string memory key) external view returns (address); + /// Returns the chain name of the currently selected fork. + #[cheatcode(group = Forking)] + function forkChain() external view returns (string memory); - #[cheatcode(group = Scripting)] - function getScriptChainBytes32(string memory chain, string memory key) external view returns (bytes32); + /// Returns the chain id of the currently selected fork. + #[cheatcode(group = Forking)] + function forkChainId() external view returns (uint256); - #[cheatcode(group = Scripting)] - function getScriptChainString(string memory chain, string memory key) external view returns (string memory); + /// 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); - #[cheatcode(group = Scripting)] - function getScriptChainBytes(string memory chain, string memory key) external view returns (bytes 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); // -------- Broadcasting Transactions -------- diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index 29b973bca97a1..a99cef1ca6c07 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, - ScriptConfig, 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::{ @@ -64,7 +64,7 @@ pub struct CheatsConfig { /// Mapping of chain IDs to their aliases pub chain_id_to_alias: HashMap, /// Script configuration - pub script_config: ScriptConfig, + pub forks: HashMap, } /// Chain data for getChain cheatcodes @@ -116,7 +116,7 @@ impl CheatsConfig { internal_expect_revert: config.allow_internal_expect_revert, chains: HashMap::new(), chain_id_to_alias: HashMap::new(), - script_config: config.script_config.clone(), + forks: config.forks.clone(), } } @@ -321,7 +321,7 @@ impl Default for CheatsConfig { internal_expect_revert: false, chains: HashMap::new(), chain_id_to_alias: HashMap::new(), - script_config: Default::default(), + forks: Default::default(), } } } diff --git a/crates/cheatcodes/src/fork.rs b/crates/cheatcodes/src/fork.rs new file mode 100644 index 0000000000000..a67ef50e3e245 --- /dev/null +++ b/crates/cheatcodes/src/fork.rs @@ -0,0 +1,297 @@ +//! 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_url { + Some(ref url) => url.clone().resolve(), + None => state.config.rpc_endpoint(name).map_err(|e| Error::from(e))?, + }; + + return Ok(rpc.url().map_err(|e| Error::from(e))?.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("unkown 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<'evm>(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) + } +} + +fn get_bool(chain: u64, key: &str, state: &crate::Cheatcodes) -> Result { + let name = get_chain_name(chain)?; + if let Some(config) = state.config.forks.get(name) { + if let Some(value) = config.vars.get(key) { + 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"); + } + } else { + bail!("Variable '{key}' not found in [fork.{name}] configuration"); + } + } else { + bail!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'"); + } +} + +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) + } +} + +fn get_int256(chain: u64, key: &str, state: &crate::Cheatcodes) -> Result { + let name = get_chain_name(chain)?; + if let Some(config) = state.config.forks.get(name) { + if let Some(value) = config.vars.get(key) { + 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"); + } + } else { + bail!("Variable '{key}' not found in [fork.{name}] configuration"); + } + } else { + bail!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'"); + } +} + +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) + } +} + +fn get_uint256(chain: u64, key: &str, state: &crate::Cheatcodes) -> Result { + let name = get_chain_name(chain)?; + if let Some(config) = state.config.forks.get(name) { + if let Some(value) = config.vars.get(key) { + 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"); + } + } else { + bail!("Variable '{key}' not found in [fork.{name}] configuration"); + } + } else { + bail!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'"); + } +} + +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_type_from_str_input( + chain: u64, + key: &str, + ty: &DynSolType, + state: &crate::Cheatcodes, +) -> Result { + let name = get_chain_name(chain)?; + if let Some(config) = state.config.forks.get(name) { + if let Some(value) = config.vars.get(key) { + if let Some(val) = value.as_str() { + cast_string(key, val, ty) + } else { + bail!("Variable '{key}' in [fork.{name}] must be a string"); + } + } else { + bail!("Variable '{key}' not found in [fork.{name}] configuration"); + } + } else { + bail!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'"); + } +} 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/cheatcodes/src/script.rs b/crates/cheatcodes/src/script.rs index 8a0f9909325e2..209bc4d02f435 100644 --- a/crates/cheatcodes/src/script.rs +++ b/crates/cheatcodes/src/script.rs @@ -17,39 +17,6 @@ use revm::{ }; use std::sync::Arc; -impl Cheatcode for getScriptChainsCall { - fn apply(&self, state: &mut crate::Cheatcodes) -> Result { - let Self {} = self; - Ok(state.config.script_config.chains.keys().collect::>().abi_encode()) - } -} - -impl Cheatcode for getScriptChainIdCall { - fn apply(&self, state: &mut crate::Cheatcodes) -> Result { - let Self { chain } = self; - - // Get the chain ID from the chain_configs - if let Some(chain_config) = state.config.script_config.chains.get(chain) { - Ok(chain_config.id.abi_encode()) - } else { - bail!("'{chain}' subsection not found in [script] section of 'foundry.toml'"); - } - } -} - -impl Cheatcode for getScriptChainRpcUrlCall { - fn apply(&self, state: &mut crate::Cheatcodes) -> Result { - let Self { chain } = self; - - // Get the chain ID from the chain_configs - if let Some(config) = state.config.script_config.chains.get(chain) { - Ok(config.rpc_url.abi_encode()) - } else { - bail!("'{chain}' subsection not found in [script] section of 'foundry.toml'"); - } - } -} - impl Cheatcode for broadcast_0Call { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { let Self {} = self; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index f261f44d3445a..b997154ceffb0 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -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 (variables) 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. @@ -538,9 +540,6 @@ pub struct Config { /// Whether to enable script execution protection. pub script_execution_protection: bool, - /// Script (variables) configuration - pub script_config: ScriptConfig, - /// PRIVATE: This structure may grow, As such, constructing this structure should /// _always_ be done using a public constructor or update syntax: /// @@ -554,17 +553,15 @@ pub struct Config { pub _non_exhaustive: (), } -/// Foundry Script Config: Chains -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)] -pub struct ScriptConfig { - pub chains: HashMap, -} - -/// Foundry Script - Chain Config: Variables +/// Fork-scoped config for tests and scripts. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ChainConfig { - pub id: u64, - pub rpc_url: String, +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_url: Option, + // Any arbitrary key-value pair of variables. pub vars: HashMap, } @@ -600,6 +597,7 @@ impl Config { "soldeer", "vyper", "bind_json", + "forks", ]; /// File name of config toml file @@ -2470,7 +2468,7 @@ impl Default for Config { compilation_restrictions: Default::default(), script_execution_protection: true, _non_exhaustive: (), - script_config: Default::default(), + forks: Default::default(), } } } @@ -2630,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}; @@ -5147,4 +5146,57 @@ mod tests { Ok(()) }); } + + #[test] + fn test_get_script_config() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [forks] + + [forks.mainnet] + rpc_url = "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_url: 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_url, mainnet.rpc_url); + for (k, v) in expected_mainnet.vars.iter() { + 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 9532d6b82dd82..c632e8e41b3ae 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -172,7 +172,7 @@ forgetest!(can_extract_config_values, |prj, cmd| { additional_compiler_profiles: Default::default(), compilation_restrictions: Default::default(), script_execution_protection: true, - script_config: Default::default(), + forks: Default::default(), _non_exhaustive: (), }; prj.write_config(input.clone()); diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 5586cb8dacf52..8b97ded822eb8 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -5,7 +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::ChainConfig; +use foundry_config::{ForkConfig, RpcEndpoint, RpcEndpointUrl, RpcEndpoints}; use foundry_test_utils::{ ScriptOutcome, ScriptTester, rpc::{self, next_http_archive_rpc_url}, @@ -14,7 +14,7 @@ use foundry_test_utils::{ }; use regex::Regex; use serde_json::Value; -use std::{collections::HashMap, env, fs, path::PathBuf}; +use std::{env, fs, path::PathBuf}; // Tests that fork cheat codes can be used in script forgetest_init!( @@ -3103,46 +3103,116 @@ contract FactoryScript is Script { assert_eq!(counter_contract.contract_name, Some("Counter".to_string())); }); -// Tests that script can get chain ids from `foundry.toml` -forgetest_init!(can_access_chain_ids, |prj, cmd| { +// Tests that can access the fork config for each chain from `foundry.toml` +forgetest_init!(can_access_fork_config_chain_ids, |prj, cmd| { prj.update_config(|config| { - config.script_config.chains = vec![ + config.forks = vec![ ( - "mainnet".into(), - ChainConfig { id: 1, rpc_url: "mainnet-rpc".to_string(), vars: HashMap::new() }, + "mainnet".to_string(), + ForkConfig { + rpc_url: 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".into(), - ChainConfig { id: 10, rpc_url: "optimism-rpc".to_string(), vars: HashMap::new() }, + "optimism".to_string(), + ForkConfig { + rpc_url: 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_script( - "ScriptWithConfig.s.sol", + "ForkScript.s.sol", r#" import {console} from "forge-std/Script.sol"; interface Vm { - function getScriptChains() external view returns (string[] memory); - function getScriptChainId(string memory chain) external view returns (uint256); - function getScriptChainRpcUrl(string memory chain) external view returns (string memory); + function forkChains() external view returns (string[] memory); + function forkChainIds() external view returns (uint256[] memory); + function forkChainId(uint256 chain) external view returns (uint256); + function forkChainRpcUrl(uint256 chain) external view returns (string memory); + function forkChainInt(uint256 chain, string memory key) external view returns (int256); + function forkChainUint(uint256 chain, string memory key) external view returns (uint256); + function forkChainBool(uint256 chain, string memory key) external view returns (bool); + function forkChainAddress(uint256 chain, string memory key) external view returns (address); + function forkChainBytes32(uint256 chain, string memory key) external view returns (bytes32); + function forkChainString(uint256 chain, string memory key) external view returns (string memory); + function forkChainBytes(uint256 chain, string memory key) external view returns (bytes memory); } - contract ScriptWithConfig { + contract ForkScript { address internal constant HEVM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); Vm constant vm = Vm(HEVM_ADDRESS); function run() public view { - string[2] memory chains = ["mainnet", "optimism"]; - string[] memory cheatChains = vm.getScriptChains(); + (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:", vm.getScriptChainId(chains[i])); - console.log(" > rpc:", vm.getScriptChainRpcUrl(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 + ); } } @@ -3154,21 +3224,167 @@ forgetest_init!(can_access_chain_ids, |prj, cmd| { ) .unwrap(); - cmd.arg("script").arg("ScriptWithConfig").assert_success().stdout_eq(str![[r#" -[COMPILING_FILES] with [SOLC_VERSION] -[SOLC_VERSION] [ELAPSED] -Compiler run successful! -Script ran successfully. -[GAS] - + cmd.arg("script").arg("ForkScript").assert_success().stdout_eq(str![[r#" +... == Logs == chain: mainnet - > id: 1 + id: 1 > rpc: mainnet-rpc + > vars: + > i256: -1234 + > u256: 1234 + > bool: true + > addr: 0xdEADBEeF00000000000000000000000000000000 + > string: bar chain: optimism - > id: 10 + id: 10 > rpc: optimism-rpc - chain: base + > 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| { + let mainnet_endpoint = rpc::next_http_rpc_endpoint(); + + prj.update_config(|config| { + config.forks = vec![ + ( + "mainnet".to_string(), + ForkConfig { + rpc_url: 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_url: 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_test( + "ForkTest.t.sol", + &r#" +import {console} from "forge-std/Test.sol"; + +interface Vm { + function createSelectFork(string memory) external view; + function forkChain() external view returns (string memory); + function forkChainId() external view returns (uint256); + function forkRpcUrl() external view returns (string memory); + function forkInt(string memory key) external view returns (int256); + function forkUint(string memory key) external view returns (uint256); + function forkBool(string memory key) external view returns (bool); + function forkAddress(string memory key) external view returns (address); + function forkBytes32(string memory key) external view returns (bytes32); + function forkString(string memory key) external view returns (string memory); + function forkBytes(string memory key) external view returns (bytes memory); +} +contract ForkTest { + address internal constant HEVM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); + Vm constant 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 4c68b19f41467..2408c50e5618a 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[]); + 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); @@ -314,16 +334,6 @@ interface Vm { function getNonce(Wallet calldata wallet) external returns (uint64 nonce); function getRawBlockHeader(uint256 blockNumber) external view returns (bytes memory rlpHeader); function getRecordedLogs() external returns (Log[] memory logs); - function getScriptChainAddress(string memory chain, string memory key) external view returns (address); - function getScriptChainBool(string memory chain, string memory key) external view returns (bool); - function getScriptChainBytes(string memory chain, string memory key) external view returns (bytes memory); - function getScriptChainBytes32(string memory chain, string memory key) external view returns (bytes32); - function getScriptChainId(string memory chain) external view returns (uint256); - function getScriptChainInt(string memory chain, string memory key) external view returns (int256); - function getScriptChainRpcUrl(string memory chain) external view returns (string memory); - function getScriptChainString(string memory chain, string memory key) external view returns (string memory); - function getScriptChainUint(string memory chain, string memory key) external view returns (uint256); - function getScriptChains() external view returns (string[] memory); function getStateDiff() external view returns (string memory diff); function getStateDiffJson() external view returns (string memory diff); function getWallets() external returns (address[] memory wallets); From d2e2cf9b675ae95daba283e1f4a1d33f293fd626 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 7 Aug 2025 11:58:20 -0700 Subject: [PATCH 03/15] style: nits --- crates/cheatcodes/src/config.rs | 2 +- crates/cheatcodes/src/fork.rs | 2 +- crates/config/src/lib.rs | 12 +++++++----- crates/forge/tests/cli/script.rs | 12 ++++++++---- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index a99cef1ca6c07..361c1a9fe59da 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -63,7 +63,7 @@ pub struct CheatsConfig { pub chains: HashMap, /// Mapping of chain IDs to their aliases pub chain_id_to_alias: HashMap, - /// Script configuration + /// Fork configuration pub forks: HashMap, } diff --git a/crates/cheatcodes/src/fork.rs b/crates/cheatcodes/src/fork.rs index a67ef50e3e245..678c3531301c7 100644 --- a/crates/cheatcodes/src/fork.rs +++ b/crates/cheatcodes/src/fork.rs @@ -41,7 +41,7 @@ impl Cheatcode for forkChainIdCall { 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_url { + let rpc = match config.rpc_endpoint { Some(ref url) => url.clone().resolve(), None => state.config.rpc_endpoint(name).map_err(|e| Error::from(e))?, }; diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index b997154ceffb0..bda3e278969ec 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -436,7 +436,7 @@ pub struct Config { /// Multiple rpc endpoints and their aliases #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")] pub rpc_endpoints: RpcEndpoints, - /// Fork (variables) configuration + /// Fork configuration pub forks: HashMap, /// Whether to store the referenced sources in the metadata as literal data. pub use_literal_content: bool, @@ -560,7 +560,7 @@ pub struct ForkConfig { // // If uninformed, it will attempt to load one from `[rpc_endpoints]` with a matching alias // for the name of the forked chain. - pub rpc_url: Option, + pub rpc_endpoint: Option, // Any arbitrary key-value pair of variables. pub vars: HashMap, } @@ -5156,7 +5156,7 @@ mod tests { [forks] [forks.mainnet] - rpc_url = "mainnet-rpc" + rpc_endpoint = "mainnet-rpc" [forks.mainnet.vars] weth = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" @@ -5171,7 +5171,9 @@ mod tests { let expected: HashMap = vec![( "mainnet".to_string(), ForkConfig { - rpc_url: Some(RpcEndpoint::new(RpcEndpointUrl::Url("mainnet-rpc".to_string()))), + rpc_endpoint: Some(RpcEndpoint::new(RpcEndpointUrl::Url( + "mainnet-rpc".to_string(), + ))), vars: vec![ ("weth".into(), "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2".into()), ("usdc".into(), "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48".into()), @@ -5192,7 +5194,7 @@ mod tests { let expected_mainnet = expected.get("mainnet").unwrap(); let mainnet = config.forks.get("mainnet").unwrap(); - assert_eq!(expected_mainnet.rpc_url, mainnet.rpc_url); + assert_eq!(expected_mainnet.rpc_endpoint, mainnet.rpc_endpoint); for (k, v) in expected_mainnet.vars.iter() { assert_eq!(v, mainnet.vars.get(k).unwrap()); } diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 8b97ded822eb8..71887058703a4 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -3110,7 +3110,9 @@ forgetest_init!(can_access_fork_config_chain_ids, |prj, cmd| { ( "mainnet".to_string(), ForkConfig { - rpc_url: Some(RpcEndpoint::new(RpcEndpointUrl::Url("mainnet-rpc".to_string()))), + rpc_endpoint: Some(RpcEndpoint::new(RpcEndpointUrl::Url( + "mainnet-rpc".to_string(), + ))), vars: vec![ ("i256".into(), "-1234".into()), ("u256".into(), 1234.into()), @@ -3131,7 +3133,7 @@ forgetest_init!(can_access_fork_config_chain_ids, |prj, cmd| { ( "optimism".to_string(), ForkConfig { - rpc_url: None, + rpc_endpoint: None, vars: vec![ ("i256".into(), "-4321".into()), ("u256".into(), 4321.into()), @@ -3258,7 +3260,9 @@ forgetest_init!(can_derive_chain_id_access_fork_config, |prj, cmd| { ( "mainnet".to_string(), ForkConfig { - rpc_url: Some(RpcEndpoint::new(RpcEndpointUrl::Url(mainnet_endpoint.clone()))), + rpc_endpoint: Some(RpcEndpoint::new(RpcEndpointUrl::Url( + mainnet_endpoint.clone(), + ))), vars: vec![ ("i256".into(), "-1234".into()), ("u256".into(), 1234.into()), @@ -3279,7 +3283,7 @@ forgetest_init!(can_derive_chain_id_access_fork_config, |prj, cmd| { ( "optimism".to_string(), ForkConfig { - rpc_url: None, + rpc_endpoint: None, vars: vec![ ("i256".into(), "-4321".into()), ("u256".into(), 4321.into()), From e934d4080d105d25017befd95e708ccd29f163ce Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 7 Aug 2025 12:02:54 -0700 Subject: [PATCH 04/15] style: comments --- crates/cheatcodes/spec/src/vm.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 256851e221ae3..b72ab077fa021 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -2175,9 +2175,7 @@ interface Vm { #[cheatcode(group = Environment)] function isContext(ForgeContext context) external view returns (bool result); - // ======== Scripts ======== - - // -------- Loading Config Vars (from foundry.toml) -------- + // ======== Forks ======== /// Returns an array with the name of all the configured fork chains. /// @@ -2283,6 +2281,7 @@ interface Vm { #[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. From e25cae7c417cbfd83e2b3b4c070b8c4a76a14325 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 7 Aug 2025 12:16:09 -0700 Subject: [PATCH 05/15] style: clippy --- crates/cheatcodes/src/fork.rs | 8 ++++---- crates/config/src/lib.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/cheatcodes/src/fork.rs b/crates/cheatcodes/src/fork.rs index 678c3531301c7..c489068443aa7 100644 --- a/crates/cheatcodes/src/fork.rs +++ b/crates/cheatcodes/src/fork.rs @@ -43,10 +43,10 @@ fn resolve_rpc_url(name: &'static str, state: &mut crate::Cheatcodes) -> Result 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).map_err(|e| Error::from(e))?, + None => state.config.rpc_endpoint(name)?, }; - return Ok(rpc.url().map_err(|e| Error::from(e))?.abi_encode()); + return Ok(rpc.url()?.abi_encode()); } bail!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'") @@ -68,7 +68,7 @@ impl Cheatcode for forkRpcUrlCall { } fn cast_string(key: &str, val: &str, ty: &DynSolType) -> Result { - string::parse(val, ty).map_err(map_env_err(key, &val)) + 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 @@ -96,7 +96,7 @@ fn get_chain_name(id: u64) -> Result<&'static str> { } /// Gets the chain id of the active fork. Panics if no fork is selected. -fn get_active_fork_chain_id<'evm>(ccx: &mut CheatsCtxt) -> Result { +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"); diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index bda3e278969ec..332fadf0d6662 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -5195,7 +5195,7 @@ mod tests { 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.iter() { + for (k, v) in &expected_mainnet.vars { assert_eq!(v, mainnet.vars.get(k).unwrap()); } Ok(()) From a7df748557676103410ca99ff7fa6fb4a49968c6 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 7 Aug 2025 13:31:15 -0700 Subject: [PATCH 06/15] fix: typo --- crates/cheatcodes/src/fork.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cheatcodes/src/fork.rs b/crates/cheatcodes/src/fork.rs index c489068443aa7..936df93503533 100644 --- a/crates/cheatcodes/src/fork.rs +++ b/crates/cheatcodes/src/fork.rs @@ -90,7 +90,7 @@ fn map_env_err<'a>(key: &'a str, value: &'a str) -> impl FnOnce(Error) -> Error fn get_chain_name(id: u64) -> Result<&'static str> { let chain = alloy_chains::Chain::from_id(id) .named() - .ok_or_eyre("unkown name for active forked chain")?; + .ok_or_eyre("unknown name for active forked chain")?; Ok(chain.as_str()) } From 51e04dd3deb76b0049d257d8f81eec3093a336bf Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Thu, 7 Aug 2025 13:53:34 -0700 Subject: [PATCH 07/15] fix: failing tests --- crates/cheatcodes/assets/cheatcodes.json | 2 +- crates/cheatcodes/spec/src/vm.rs | 2 +- crates/forge/tests/cli/config.rs | 3 +++ testdata/cheats/Vm.sol | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 273b46b50c846..9a9b2ac88aa99 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -6058,7 +6058,7 @@ "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[]);", + "declaration": "function forkChainIds() external view returns (uint256[] memory);", "visibility": "external", "mutability": "view", "signature": "forkChainIds()", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index b72ab077fa021..2ad9125eea0d4 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -2187,7 +2187,7 @@ interface Vm { /// /// Note that the configured fork chains are subsections of the `[fork]` section of 'foundry.toml'. #[cheatcode(group = Forking)] - function forkChainIds() external view returns (uint256[]); + function forkChainIds() external view returns (uint256[] memory); /// Returns the chain name of the currently selected fork. #[cheatcode(group = Forking)] diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 29cc67cbc7dbd..2eac161ade76c 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -1137,6 +1137,8 @@ out = "utils/JsonBindings.sol" include = [] exclude = [] +[forks] + "#]]); @@ -1275,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/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 2408c50e5618a..6cf08877116ad 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -296,7 +296,7 @@ interface Vm { 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[]); + 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); From 02cb27bb0f76577124dcbad05e813e7ad87978b4 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Thu, 7 Aug 2025 14:58:07 -0700 Subject: [PATCH 08/15] fix: spacing --- crates/forge/tests/cli/script.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 71887058703a4..195aeef5d83a0 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -2579,13 +2579,13 @@ Chain 31337 accessList [] chainId 31337 gasLimit [..] -gasPrice +gasPrice input [..] -maxFeePerBlobGas -maxFeePerGas +maxFeePerBlobGas +maxFeePerGas maxPriorityFeePerGas nonce 0 -to +to type 0 value 0 @@ -2594,10 +2594,10 @@ value 0 accessList [] chainId 31337 gasLimit 93856 -gasPrice +gasPrice input 0x7357f5d2000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000001c8 -maxFeePerBlobGas -maxFeePerGas +maxFeePerBlobGas +maxFeePerGas maxPriorityFeePerGas nonce 1 to 0x5FbDB2315678afecb367f032d93F642f64180aa3 From 457d10cb3df6dfbd8e437a7b72215dab9711ddfc Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Thu, 7 Aug 2025 14:59:35 -0700 Subject: [PATCH 09/15] fix: spacing --- crates/forge/tests/cli/script.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 195aeef5d83a0..5d6ebac452460 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -2583,7 +2583,7 @@ gasPrice input [..] maxFeePerBlobGas maxFeePerGas -maxPriorityFeePerGas +maxPriorityFeePerGas nonce 0 to type 0 @@ -2598,7 +2598,7 @@ gasPrice input 0x7357f5d2000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000001c8 maxFeePerBlobGas maxFeePerGas -maxPriorityFeePerGas +maxPriorityFeePerGas nonce 1 to 0x5FbDB2315678afecb367f032d93F642f64180aa3 type 0 From 8f03e33440d72433d15f7d228ca9e9aa805b02e6 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Tue, 12 Aug 2025 09:49:58 +0200 Subject: [PATCH 10/15] fix: logs spacing --- crates/forge/tests/cli/script.rs | 71 ++++++++++++++++---------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 4e4824d8cec23..cd4039384d68e 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -3357,28 +3357,27 @@ forgetest_init!(can_access_fork_config_chain_ids, |prj, cmd| { .unwrap(); cmd.arg("script").arg("ForkScript").assert_success().stdout_eq(str![[r#" - ... - == Logs == - 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 - - "#]]); +... + 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` @@ -3506,19 +3505,19 @@ forgetest_init!(can_derive_chain_id_access_fork_config, |prj, cmd| { .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]) - ... +... +[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]) +... "#]]); }); From e87217cf01f6e2cb5c104967fadf342a3ab00deb Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Tue, 12 Aug 2025 10:04:21 +0200 Subject: [PATCH 11/15] style: simplify helper fns --- crates/cheatcodes/src/fork.rs | 141 ++++++++++++++++------------------ 1 file changed, 67 insertions(+), 74 deletions(-) diff --git a/crates/cheatcodes/src/fork.rs b/crates/cheatcodes/src/fork.rs index 936df93503533..437eac611407b 100644 --- a/crates/cheatcodes/src/fork.rs +++ b/crates/cheatcodes/src/fork.rs @@ -123,27 +123,6 @@ impl Cheatcode for forkBoolCall { } } -fn get_bool(chain: u64, key: &str, state: &crate::Cheatcodes) -> Result { - let name = get_chain_name(chain)?; - if let Some(config) = state.config.forks.get(name) { - if let Some(value) = config.vars.get(key) { - 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"); - } - } else { - bail!("Variable '{key}' not found in [fork.{name}] configuration"); - } - } else { - bail!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'"); - } -} - impl Cheatcode for forkChainIntCall { fn apply(&self, state: &mut crate::Cheatcodes) -> Result { let Self { chain, key } = self; @@ -158,25 +137,6 @@ impl Cheatcode for forkIntCall { } } -fn get_int256(chain: u64, key: &str, state: &crate::Cheatcodes) -> Result { - let name = get_chain_name(chain)?; - if let Some(config) = state.config.forks.get(name) { - if let Some(value) = config.vars.get(key) { - 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"); - } - } else { - bail!("Variable '{key}' not found in [fork.{name}] configuration"); - } - } else { - bail!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'"); - } -} - impl Cheatcode for forkChainUintCall { fn apply(&self, state: &mut crate::Cheatcodes) -> Result { let Self { chain, key } = self; @@ -191,29 +151,6 @@ impl Cheatcode for forkUintCall { } } -fn get_uint256(chain: u64, key: &str, state: &crate::Cheatcodes) -> Result { - let name = get_chain_name(chain)?; - if let Some(config) = state.config.forks.get(name) { - if let Some(value) = config.vars.get(key) { - 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"); - } - } else { - bail!("Variable '{key}' not found in [fork.{name}] configuration"); - } - } else { - bail!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'"); - } -} - impl Cheatcode for forkChainAddressCall { fn apply(&self, state: &mut crate::Cheatcodes) -> Result { let Self { chain, key } = self; @@ -274,6 +211,68 @@ impl Cheatcode for forkStringCall { } } +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_eyre("[fork.{name}] subsection not found in [fork] of 'foundry.toml'")?; + let value = config + .vars + .get(key) + .ok_or_eyre("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, @@ -281,17 +280,11 @@ fn get_type_from_str_input( state: &crate::Cheatcodes, ) -> Result { let name = get_chain_name(chain)?; - if let Some(config) = state.config.forks.get(name) { - if let Some(value) = config.vars.get(key) { - if let Some(val) = value.as_str() { - cast_string(key, val, ty) - } else { - bail!("Variable '{key}' in [fork.{name}] must be a string"); - } - } else { - bail!("Variable '{key}' not found in [fork.{name}] configuration"); - } + let value = get_toml_value(name, key, state)?; + + if let Some(val) = value.as_str() { + cast_string(key, val, ty) } else { - bail!("[fork.{name}] subsection not found in [fork] of 'foundry.toml'"); + bail!("Variable '{key}' in [fork.{name}] must be a string"); } } From 70481047765f0b9ee069e132db33a775bddc5998 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Tue, 12 Aug 2025 10:28:23 +0200 Subject: [PATCH 12/15] fix: fmt_error + insert_vm --- crates/cheatcodes/src/fork.rs | 10 +- crates/forge/tests/cli/script.rs | 207 ++++++++++++++----------------- 2 files changed, 98 insertions(+), 119 deletions(-) diff --git a/crates/cheatcodes/src/fork.rs b/crates/cheatcodes/src/fork.rs index 437eac611407b..8c15218e3424d 100644 --- a/crates/cheatcodes/src/fork.rs +++ b/crates/cheatcodes/src/fork.rs @@ -216,15 +216,13 @@ fn get_toml_value<'a>( key: &'a str, state: &'a crate::Cheatcodes, ) -> Result<&'a toml::Value> { - let config = state - .config - .forks - .get(name) - .ok_or_eyre("[fork.{name}] subsection not found in [fork] of 'foundry.toml'")?; + 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_eyre("Variable '{key}' not found in [fork.{name}] configuration")?; + .ok_or_else(|| fmt_err!("Variable '{key}' not found in [fork.{name}] configuration"))?; Ok(value) } diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index cd4039384d68e..a0163faadb40e 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -3235,6 +3235,10 @@ 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![ ( @@ -3291,67 +3295,54 @@ forgetest_init!(can_access_fork_config_chain_ids, |prj, cmd| { )]); }); - prj.add_script( + prj.add_source( "ForkScript.s.sol", r#" - import {console} from "forge-std/Script.sol"; - - interface Vm { - function forkChains() external view returns (string[] memory); - function forkChainIds() external view returns (uint256[] memory); - function forkChainId(uint256 chain) external view returns (uint256); - function forkChainRpcUrl(uint256 chain) external view returns (string memory); - function forkChainInt(uint256 chain, string memory key) external view returns (int256); - function forkChainUint(uint256 chain, string memory key) external view returns (uint256); - function forkChainBool(uint256 chain, string memory key) external view returns (bool); - function forkChainAddress(uint256 chain, string memory key) external view returns (address); - function forkChainBytes32(uint256 chain, string memory key) external view returns (bytes32); - function forkChainString(uint256 chain, string memory key) external view returns (string memory); - function forkChainBytes(uint256 chain, string memory key) external view returns (bytes memory); - } +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); - contract ForkScript { - address internal constant HEVM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - Vm constant 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)); - } + 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(); @@ -3382,6 +3373,9 @@ forgetest_init!(can_access_fork_config_chain_ids, |prj, cmd| { // 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| { @@ -3440,69 +3434,56 @@ forgetest_init!(can_derive_chain_id_access_fork_config, |prj, cmd| { )]); }); - prj.add_test( - "ForkTest.t.sol", - &r#" - import {console} from "forge-std/Test.sol"; - - interface Vm { - function createSelectFork(string memory) external view; - function forkChain() external view returns (string memory); - function forkChainId() external view returns (uint256); - function forkRpcUrl() external view returns (string memory); - function forkInt(string memory key) external view returns (int256); - function forkUint(string memory key) external view returns (uint256); - function forkBool(string memory key) external view returns (bool); - function forkAddress(string memory key) external view returns (address); - function forkBytes32(string memory key) external view returns (bytes32); - function forkString(string memory key) external view returns (string memory); - function forkBytes(string memory key) external view returns (bytes memory); - } - - contract ForkTest { - address internal constant HEVM_ADDRESS = address(uint160(uint256(keccak256("hevm cheat code")))); - Vm constant vm = Vm(HEVM_ADDRESS); - - function test_panicsWhithoutSelectedFork() public { - vm.forkChain(); - } - - function test_forkVars() public { - vm.createSelectFork(""); + prj.add_source( + "ForkTest.t.sol", + &r#" +import {Vm} from "./Vm.sol"; +import {DSTest} from "./test.sol"; +import {console} from "./Console.sol"; - console.log("chain:", vm.forkChain()); - console.log("id:", vm.forkChainId()); - assert(eqString(vm.forkRpcUrl(), "")); +contract ForkTest is DSTest { + Vm vm = Vm(HEVM_ADDRESS); - 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"); + function test_panicsWhithoutSelectedFork() public { + vm.forkChain(); + } - console.log(" > vars:"); - console.log(" > i256:", i256); - console.log(" > u256:", u256); - console.log(" > bool:", boolean); - console.log(" > addr:", addr); - console.log(" > string:", str); + function test_forkVars() public { + vm.createSelectFork(""); - assert( - b256 == 0xdeadbeaf00000000000000000000000000000000000000000000000000000000 - || b256 == 0x000000000000000000000000000000000000000000000000000000deadc0ffee - ); - } + 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)); - } + function eqString(string memory s1, string memory s2) public pure returns(bool) { + return keccak256(bytes(s1)) == keccak256(bytes(s2)); } +} "# - .replace("", &mainnet_endpoint), - ) - .unwrap(); + .replace("", &mainnet_endpoint), + ) + .unwrap(); cmd.args(["test", "-vvv", "ForkTest"]).assert_failure().stdout_eq(str![[r#" ... From 1839b7d00cfa21e8c401be3feb448af78e0ab232 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Tue, 12 Aug 2025 10:46:37 +0200 Subject: [PATCH 13/15] fix: spacing --- crates/forge/tests/cli/script.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index a0163faadb40e..e68c6eef18b16 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -2614,13 +2614,13 @@ Chain 31337 accessList [] chainId 31337 gasLimit [..] -gasPrice +gasPrice input [..] -maxFeePerBlobGas -maxFeePerGas -maxPriorityFeePerGas +maxFeePerBlobGas +maxFeePerGas +maxPriorityFeePerGas nonce 0 -to +to type 0 value 0 @@ -2629,11 +2629,11 @@ value 0 accessList [] chainId 31337 gasLimit 93856 -gasPrice +gasPrice input 0x7357f5d2000000000000000000000000000000000000000000000000000000000000007b00000000000000000000000000000000000000000000000000000000000001c8 -maxFeePerBlobGas -maxFeePerGas -maxPriorityFeePerGas +maxFeePerBlobGas +maxFeePerGas +maxPriorityFeePerGas nonce 1 to 0x5FbDB2315678afecb367f032d93F642f64180aa3 type 0 From 6e6f8c03c83d09cc02a985dfbfc67992e8fe0996 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Tue, 12 Aug 2025 11:23:07 +0200 Subject: [PATCH 14/15] fix: file name case --- crates/forge/tests/cli/script.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index e68c6eef18b16..904e4ecf7fbb1 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -3439,7 +3439,7 @@ forgetest_init!(can_derive_chain_id_access_fork_config, |prj, cmd| { &r#" import {Vm} from "./Vm.sol"; import {DSTest} from "./test.sol"; -import {console} from "./Console.sol"; +import {console} from "./console.sol"; contract ForkTest is DSTest { Vm vm = Vm(HEVM_ADDRESS); From c5787b0eef2702d20eeafbc89cc3f4724c8fb88b Mon Sep 17 00:00:00 2001 From: 0xrusowsky <0xrusowsky@proton.me> Date: Tue, 12 Aug 2025 11:32:32 +0200 Subject: [PATCH 15/15] fix: missing name case --- crates/forge/tests/cli/script.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 904e4ecf7fbb1..29f274b5190eb 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -3300,7 +3300,7 @@ forgetest_init!(can_access_fork_config_chain_ids, |prj, cmd| { r#" import {Vm} from "./Vm.sol"; import {DSTest} from "./test.sol"; -import {console} from "./Console.sol"; +import {console} from "./console.sol"; contract ForkScript is DSTest { Vm vm = Vm(HEVM_ADDRESS);