From bb3db437a0be49bb3b58858c8c84d294a4b471d1 Mon Sep 17 00:00:00 2001 From: 0xcharlie Date: Tue, 1 Apr 2025 21:50:51 +0200 Subject: [PATCH 1/7] add Chain struct --- crates/cheatcodes/spec/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/cheatcodes/spec/src/lib.rs b/crates/cheatcodes/spec/src/lib.rs index b7c44f3284b47..7d1dbe33ac9e6 100644 --- a/crates/cheatcodes/spec/src/lib.rs +++ b/crates/cheatcodes/spec/src/lib.rs @@ -83,6 +83,7 @@ impl Cheatcodes<'static> { Vm::Wallet::STRUCT.clone(), Vm::FfiResult::STRUCT.clone(), Vm::ChainInfo::STRUCT.clone(), + Vm::Chain::STRUCT.clone(), Vm::AccountAccess::STRUCT.clone(), Vm::StorageAccess::STRUCT.clone(), Vm::Gas::STRUCT.clone(), From 099c5d5c1fa976208518092caf1bf2e694b53967 Mon Sep 17 00:00:00 2001 From: 0xcharlie Date: Tue, 1 Apr 2025 21:51:23 +0200 Subject: [PATCH 2/7] generate interface --- crates/cheatcodes/assets/cheatcodes.json | 46 ++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 82a8fcf0df9af..099edbe2b06c8 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -369,6 +369,32 @@ } ] }, + { + "name": "Chain", + "description": "Information about a blockchain.", + "fields": [ + { + "name": "name", + "ty": "string", + "description": "The chain name." + }, + { + "name": "chainId", + "ty": "uint256", + "description": "The chain's Chain ID." + }, + { + "name": "chainAlias", + "ty": "string", + "description": "The chain's alias. (i.e. what gets specified in `foundry.toml`)." + }, + { + "name": "rpcUrl", + "ty": "string", + "description": "A default RPC endpoint for this chain." + } + ] + }, { "name": "AccountAccess", "description": "The result of a `stopAndReturnStateDiff` call.", @@ -5928,6 +5954,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "getChain", + "description": "Returns a Chain struct for specific alias", + "declaration": "function getChain(string calldata chainAlias) external view returns (Chain memory chain);", + "visibility": "external", + "mutability": "view", + "signature": "getChain(string)", + "selector": "0x4cc1c2bb", + "selectorBytes": [ + 76, + 193, + 194, + 187 + ] + }, + "group": "testing", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "getCode", From 7df0cbce20ee87a1a7581eb18bb7628dba009753 Mon Sep 17 00:00:00 2001 From: 0xcharlie Date: Tue, 1 Apr 2025 21:52:01 +0200 Subject: [PATCH 3/7] define getChain cheatcode --- crates/cheatcodes/spec/src/vm.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 55ee7c93b8428..86d7309cd2c38 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -207,6 +207,18 @@ interface Vm { uint256 chainId; } + /// Information about a blockchain. + struct Chain { + /// The chain name. + string name; + /// The chain's Chain ID. + uint256 chainId; + /// The chain's alias. (i.e. what gets specified in `foundry.toml`). + string chainAlias; + /// A default RPC endpoint for this chain. + string rpcUrl; + } + /// The storage accessed during an `AccountAccess`. struct StorageAccess { /// The account whose storage was accessed. @@ -967,6 +979,10 @@ interface Vm { #[cheatcode(group = Testing, safety = Safe)] function rpcUrlStructs() external view returns (Rpc[] memory urls); + /// Returns a Chain struct for specific alias + #[cheatcode(group = Testing, safety = Safe)] + function getChain(string calldata chainAlias) external view returns (Chain memory chain); + /// Suspends execution of the main thread for `duration` milliseconds. #[cheatcode(group = Testing, safety = Safe)] function sleep(uint256 duration) external; From 3c6f529e7d239eb075128c9730a7639f46e08b4e Mon Sep 17 00:00:00 2001 From: 0xcharlie Date: Tue, 1 Apr 2025 21:52:36 +0200 Subject: [PATCH 4/7] add getChain(alias) implementation --- crates/cheatcodes/src/config.rs | 349 ++++++++++++++++++++++++++++++++ crates/cheatcodes/src/test.rs | 33 ++- 2 files changed, 381 insertions(+), 1 deletion(-) diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index 3ab70f6e053fb..6a67d56b3a331 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -11,6 +11,7 @@ use foundry_evm_core::opts::EvmOpts; use std::{ path::{Path, PathBuf}, time::Duration, + collections::HashMap, }; /// Additional, configurable context the `Cheatcodes` inspector has access to @@ -56,6 +57,18 @@ pub struct CheatsConfig { pub seed: Option, /// Whether to allow `expectRevert` to work for internal calls. pub internal_expect_revert: bool, + /// Mapping of chain aliases to chain data + pub chains: HashMap, + /// Mapping of chain IDs to their aliases + pub chain_id_to_alias: HashMap, +} + +/// Chain data for getChain cheatcodes +#[derive(Clone, Debug)] +pub struct ChainData { + pub name: String, + pub chain_id: u64, + pub default_rpc_url: String, // Store default RPC URL } impl CheatsConfig { @@ -96,6 +109,8 @@ impl CheatsConfig { assertions_revert: config.assertions_revert, seed: config.fuzz.seed, internal_expect_revert: config.allow_internal_expect_revert, + chains: HashMap::new(), + chain_id_to_alias: HashMap::new(), } } @@ -203,6 +218,78 @@ impl CheatsConfig { } Ok(urls) } + + /// Initialize default chain data (similar to initializeStdChains in Solidity) + pub fn initialize_chain_data(&mut self) { + if !self.chains.is_empty() { + return; // Already initialized + } + + // Use the same function to create chains + let chains = create_default_chains(); + + // Add all chains to the config + for (alias, data) in chains { + self.set_chain_with_default_rpc_url(&alias, data); + } + } + + /// Set chain with default RPC URL (similar to setChainWithDefaultRpcUrl in Solidity) + pub fn set_chain_with_default_rpc_url(&mut self, alias: &str, data: ChainData) { + // Store the default RPC URL is already stored in the data + // No need to clone it separately + + // Add chain data + self.set_chain_data(alias, data); + } + + /// Set chain data for a specific alias + pub fn set_chain_data(&mut self, alias: &str, data: ChainData) { + // Remove old chain ID mapping if it exists + if let Some(old_data) = self.chains.get(alias) { + self.chain_id_to_alias.remove(&old_data.chain_id); + } + + // Add new mappings + self.chain_id_to_alias.insert(data.chain_id, alias.to_string()); + self.chains.insert(alias.to_string(), data); + } + + /// Get chain data by alias + pub fn get_chain_data_by_alias_non_mut(&self, alias: &str) -> Result { + // Initialize chains if not already done + if self.chains.is_empty() { + + // Create a temporary copy with initialized chains + // This is inefficient but handles the edge case + let temp_chains = create_default_chains(); + + if let Some(data) = temp_chains.get(alias) { + return Ok(data.clone()); + } + } else { + // Normal path - chains are initialized + if let Some(data) = self.chains.get(alias) { + return Ok(data.clone()); + } + } + + // Chain not found in either case + Err(fmt_err!("vm.getChain: Chain with alias \"{}\" not found", alias)) + } + + /// Get RPC URL for an alias + pub fn get_rpc_url_non_mut(&self, alias: &str) -> Result { + // Try to get from config first + match self.rpc_endpoint(alias) { + Ok(endpoint) => Ok(endpoint.url()?), + Err(_) => { + // If not in config, try to get default URL + let chain_data = self.get_chain_data_by_alias_non_mut(alias)?; + Ok(chain_data.default_rpc_url) + } + } + } } impl Default for CheatsConfig { @@ -226,6 +313,8 @@ impl Default for CheatsConfig { assertions_revert: true, seed: None, internal_expect_revert: false, + chains: HashMap::new(), + chain_id_to_alias: HashMap::new(), } } } @@ -272,3 +361,263 @@ mod tests { assert!(!config.is_foundry_toml(f)); } } + +// Helper function to set default chains +fn create_default_chains() -> HashMap { + let mut chains = HashMap::new(); + + // Define all chains in one place + chains.insert("anvil".to_string(), ChainData { + name: "Anvil".to_string(), + chain_id: 31337, + default_rpc_url: "http://127.0.0.1:8545".to_string() + }); + + chains.insert("mainnet".to_string(), ChainData { + name: "Mainnet".to_string(), + chain_id: 1, + default_rpc_url: "https://eth.llamarpc.com".to_string() + }); + + chains.insert("sepolia".to_string(), ChainData { + name: "Sepolia".to_string(), + chain_id: 11155111, + default_rpc_url: "https://sepolia.infura.io/v3/b9794ad1ddf84dfb8c34d6bb5dca2001".to_string() + }); + + chains.insert("holesky".to_string(), ChainData { + name: "Holesky".to_string(), + chain_id: 17000, + default_rpc_url: "https://rpc.holesky.ethpandaops.io".to_string() + }); + + chains.insert("optimism".to_string(), ChainData { + name: "Optimism".to_string(), + chain_id: 10, + default_rpc_url: "https://mainnet.optimism.io".to_string() + }); + + chains.insert("optimism_sepolia".to_string(), ChainData { + name: "Optimism Sepolia".to_string(), + chain_id: 11155420, + default_rpc_url: "https://sepolia.optimism.io".to_string() + }); + + chains.insert("arbitrum_one".to_string(), ChainData { + name: "Arbitrum One".to_string(), + chain_id: 42161, + default_rpc_url: "https://arb1.arbitrum.io/rpc".to_string() + }); + + chains.insert("arbitrum_one_sepolia".to_string(), ChainData { + name: "Arbitrum One Sepolia".to_string(), + chain_id: 421614, + default_rpc_url: "https://sepolia-rollup.arbitrum.io/rpc".to_string() + }); + + chains.insert("arbitrum_nova".to_string(), ChainData { + name: "Arbitrum Nova".to_string(), + chain_id: 42170, + default_rpc_url: "https://nova.arbitrum.io/rpc".to_string() + }); + + chains.insert("polygon".to_string(), ChainData { + name: "Polygon".to_string(), + chain_id: 137, + default_rpc_url: "https://polygon-rpc.com".to_string() + }); + + chains.insert("polygon_amoy".to_string(), ChainData { + name: "Polygon Amoy".to_string(), + chain_id: 80002, + default_rpc_url: "https://rpc-amoy.polygon.technology".to_string() + }); + + chains.insert("avalanche".to_string(), ChainData { + name: "Avalanche".to_string(), + chain_id: 43114, + default_rpc_url: "https://api.avax.network/ext/bc/C/rpc".to_string() + }); + + chains.insert("avalanche_fuji".to_string(), ChainData { + name: "Avalanche Fuji".to_string(), + chain_id: 43113, + default_rpc_url: "https://api.avax-test.network/ext/bc/C/rpc".to_string() + }); + + chains.insert("bnb_smart_chain".to_string(), ChainData { + name: "BNB Smart Chain".to_string(), + chain_id: 56, + default_rpc_url: "https://bsc-dataseed1.binance.org".to_string() + }); + + chains.insert("bnb_smart_chain_testnet".to_string(), ChainData { + name: "BNB Smart Chain Testnet".to_string(), + chain_id: 97, + default_rpc_url: "https://rpc.ankr.com/bsc_testnet_chapel".to_string() + }); + + chains.insert("gnosis_chain".to_string(), ChainData { + name: "Gnosis Chain".to_string(), + chain_id: 100, + default_rpc_url: "https://rpc.gnosischain.com".to_string() + }); + + chains.insert("moonbeam".to_string(), ChainData { + name: "Moonbeam".to_string(), + chain_id: 1284, + default_rpc_url: "https://rpc.api.moonbeam.network".to_string() + }); + + chains.insert("moonriver".to_string(), ChainData { + name: "Moonriver".to_string(), + chain_id: 1285, + default_rpc_url: "https://rpc.api.moonriver.moonbeam.network".to_string() + }); + + chains.insert("moonbase".to_string(), ChainData { + name: "Moonbase".to_string(), + chain_id: 1287, + default_rpc_url: "https://rpc.testnet.moonbeam.network".to_string() + }); + + chains.insert("base_sepolia".to_string(), ChainData { + name: "Base Sepolia".to_string(), + chain_id: 84532, + default_rpc_url: "https://sepolia.base.org".to_string() + }); + + chains.insert("base".to_string(), ChainData { + name: "Base".to_string(), + chain_id: 8453, + default_rpc_url: "https://mainnet.base.org".to_string() + }); + + chains.insert("blast_sepolia".to_string(), ChainData { + name: "Blast Sepolia".to_string(), + chain_id: 168587773, + default_rpc_url: "https://sepolia.blast.io".to_string() + }); + + chains.insert("blast".to_string(), ChainData { + name: "Blast".to_string(), + chain_id: 81457, + default_rpc_url: "https://rpc.blast.io".to_string() + }); + + chains.insert("fantom_opera".to_string(), ChainData { + name: "Fantom Opera".to_string(), + chain_id: 250, + default_rpc_url: "https://rpc.ankr.com/fantom/".to_string() + }); + + chains.insert("fantom_opera_testnet".to_string(), ChainData { + name: "Fantom Opera Testnet".to_string(), + chain_id: 4002, + default_rpc_url: "https://rpc.ankr.com/fantom_testnet/".to_string() + }); + + chains.insert("fraxtal".to_string(), ChainData { + name: "Fraxtal".to_string(), + chain_id: 252, + default_rpc_url: "https://rpc.frax.com".to_string() + }); + + chains.insert("fraxtal_testnet".to_string(), ChainData { + name: "Fraxtal Testnet".to_string(), + chain_id: 2522, + default_rpc_url: "https://rpc.testnet.frax.com".to_string() + }); + + chains.insert("berachain_bartio_testnet".to_string(), ChainData { + name: "Berachain bArtio Testnet".to_string(), + chain_id: 80084, + default_rpc_url: "https://bartio.rpc.berachain.com".to_string() + }); + + chains.insert("flare".to_string(), ChainData { + name: "Flare".to_string(), + chain_id: 14, + default_rpc_url: "https://flare-api.flare.network/ext/C/rpc".to_string() + }); + + chains.insert("flare_coston2".to_string(), ChainData { + name: "Flare Coston2".to_string(), + chain_id: 114, + default_rpc_url: "https://coston2-api.flare.network/ext/C/rpc".to_string() + }); + + chains.insert("mode".to_string(), ChainData { + name: "Mode".to_string(), + chain_id: 34443, + default_rpc_url: "https://mode.drpc.org".to_string() + }); + + chains.insert("mode_sepolia".to_string(), ChainData { + name: "Mode Sepolia".to_string(), + chain_id: 919, + default_rpc_url: "https://sepolia.mode.network".to_string() + }); + + chains.insert("zora".to_string(), ChainData { + name: "Zora".to_string(), + chain_id: 7777777, + default_rpc_url: "https://zora.drpc.org".to_string() + }); + + chains.insert("zora_sepolia".to_string(), ChainData { + name: "Zora Sepolia".to_string(), + chain_id: 999999999, + default_rpc_url: "https://sepolia.rpc.zora.energy".to_string() + }); + + chains.insert("race".to_string(), ChainData { + name: "Race".to_string(), + chain_id: 6805, + default_rpc_url: "https://racemainnet.io".to_string() + }); + + chains.insert("race_sepolia".to_string(), ChainData { + name: "Race Sepolia".to_string(), + chain_id: 6806, + default_rpc_url: "https://racemainnet.io".to_string() + }); + + chains.insert("metal".to_string(), ChainData { + name: "Metal".to_string(), + chain_id: 1750, + default_rpc_url: "https://metall2.drpc.org".to_string() + }); + + chains.insert("metal_sepolia".to_string(), ChainData { + name: "Metal Sepolia".to_string(), + chain_id: 1740, + default_rpc_url: "https://testnet.rpc.metall2.com".to_string() + }); + + chains.insert("binary".to_string(), ChainData { + name: "Binary".to_string(), + chain_id: 624, + default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string() + }); + + chains.insert("binary_sepolia".to_string(), ChainData { + name: "Binary Sepolia".to_string(), + chain_id: 625, + default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string() + }); + + chains.insert("orderly".to_string(), ChainData { + name: "Orderly".to_string(), + chain_id: 291, + default_rpc_url: "https://rpc.orderly.network".to_string() + }); + + chains.insert("orderly_sepolia".to_string(), ChainData { + name: "Orderly Sepolia".to_string(), + chain_id: 4460, + default_rpc_url: "https://testnet-rpc.orderly.org".to_string() + }); + + chains +} diff --git a/crates/cheatcodes/src/test.rs b/crates/cheatcodes/src/test.rs index c12f4609bdef2..29f5472773721 100644 --- a/crates/cheatcodes/src/test.rs +++ b/crates/cheatcodes/src/test.rs @@ -1,7 +1,7 @@ //! Implementations of [`Testing`](spec::Group::Testing) cheatcodes. use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*}; -use alloy_primitives::Address; +use alloy_primitives::{Address, U256}; use alloy_sol_types::SolValue; use foundry_common::version::SEMVER_VERSION; use foundry_evm_core::constants::MAGIC_SKIP; @@ -32,6 +32,14 @@ impl Cheatcode for getFoundryVersionCall { } } +impl Cheatcode for getChainCall { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { chainAlias } = self; + let chain = get_chain_data(state, chainAlias)?; + Ok(chain.abi_encode()) + } +} + impl Cheatcode for rpcUrlCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { rpcAlias } = self; @@ -100,3 +108,26 @@ fn breakpoint(state: &mut Cheatcodes, caller: &Address, s: &str, add: bool) -> R Ok(Default::default()) } + +/// Get chain data and create a Chain struct +fn get_chain_data(state: &Cheatcodes, alias: &str) -> Result { + // Ensure the alias is not empty + if alias.is_empty() { + bail!("Chain alias cannot be empty"); + } + + // Get chain data from alias + let chain_data = state.config.get_chain_data_by_alias_non_mut(alias)?; + + // Get RPC URL + let rpc_url = state.config.get_rpc_url_non_mut(alias)?; + + // Create the Chain struct + Ok(Chain { + name: chain_data.name, + chainId: U256::from(chain_data.chain_id), + chainAlias: alias.to_string(), + rpcUrl: rpc_url, + }) +} + From 04c2f5a21514f39dd6293c2b2888fcf39ce0dbda Mon Sep 17 00:00:00 2001 From: 0xcharlie Date: Tue, 1 Apr 2025 21:53:57 +0200 Subject: [PATCH 5/7] add GetChain test --- testdata/cheats/Vm.sol | 2 + testdata/default/cheats/GetChain.t.sol | 54 ++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 testdata/default/cheats/GetChain.t.sol diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 20d7cc29d407d..3e589624a4b73 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -18,6 +18,7 @@ interface Vm { struct Wallet { address addr; uint256 publicKeyX; uint256 publicKeyY; uint256 privateKey; } struct FfiResult { int32 exitCode; bytes stdout; bytes stderr; } struct ChainInfo { uint256 forkId; uint256 chainId; } + struct Chain { string name; uint256 chainId; string chainAlias; string rpcUrl; } struct AccountAccess { ChainInfo chainInfo; AccountAccessKind kind; address account; address accessor; bool initialized; uint256 oldBalance; uint256 newBalance; bytes deployedCode; uint256 value; bytes data; bool reverted; StorageAccess[] storageAccesses; uint64 depth; } struct StorageAccess { address account; bytes32 slot; bool isWrite; bytes32 previousValue; bytes32 newValue; bool reverted; } struct Gas { uint64 gasLimit; uint64 gasTotalUsed; uint64 gasMemoryUsed; int64 gasRefunded; uint64 gasRemaining; } @@ -290,6 +291,7 @@ interface Vm { function getBroadcast(string calldata contractName, uint64 chainId, BroadcastTxType txType) external view returns (BroadcastTxSummary memory); function getBroadcasts(string calldata contractName, uint64 chainId, BroadcastTxType txType) external view returns (BroadcastTxSummary[] memory); function getBroadcasts(string calldata contractName, uint64 chainId) external view returns (BroadcastTxSummary[] memory); + function getChain(string calldata chainAlias) external view returns (Chain memory chain); function getCode(string calldata artifactPath) external view returns (bytes memory creationBytecode); function getDeployedCode(string calldata artifactPath) external view returns (bytes memory runtimeBytecode); function getDeployment(string calldata contractName) external view returns (address deployedAddress); diff --git a/testdata/default/cheats/GetChain.t.sol b/testdata/default/cheats/GetChain.t.sol new file mode 100644 index 0000000000000..a331e6421c6ce --- /dev/null +++ b/testdata/default/cheats/GetChain.t.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +contract GetChainTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function testGetChainByAlias() public { + // Test mainnet + Vm.Chain memory mainnet = vm.getChain("mainnet"); + assertEq(mainnet.name, "Mainnet"); + assertEq(mainnet.chainId, 1); + assertEq(mainnet.chainAlias, "mainnet"); + assertTrue(bytes(mainnet.rpcUrl).length > 0); + + // Test sepolia + Vm.Chain memory sepolia = vm.getChain("sepolia"); + assertEq(sepolia.name, "Sepolia"); + assertEq(sepolia.chainId, 11155111); + assertEq(sepolia.chainAlias, "sepolia"); + assertTrue(bytes(sepolia.rpcUrl).length > 0); + + // Test Anvil/Local chain + Vm.Chain memory anvil = vm.getChain("anvil"); + assertEq(anvil.name, "Anvil"); + assertEq(anvil.chainId, 31337); + assertEq(anvil.chainAlias, "anvil"); + assertTrue(bytes(anvil.rpcUrl).length > 0); + } + + function testGetChainInvalidAlias() public { + // Test with invalid alias - should revert + vm._expectCheatcodeRevert("vm.getChain: Chain with alias \"invalid_chain\" not found"); + vm.getChain("invalid_chain"); + } + + function testGetChainEmptyAlias() public { + vm._expectCheatcodeRevert("Chain alias cannot be empty"); + vm.getChain(""); + } + + + function testGetChainRpcUrlPriority() public { + // This test assumes running with default config where no custom RPC URLs are set + // for mainnet. So it should use the default RPC URL. + Vm.Chain memory mainnet = vm.getChain("mainnet"); + assertTrue(bytes(mainnet.rpcUrl).length > 0); + + // You can print the URL for manual verification + emit log_string(mainnet.rpcUrl); + } +} From 2a8a438ff4370f237bf1b2523c9f54e1107e61c2 Mon Sep 17 00:00:00 2001 From: 0xcharlie Date: Tue, 1 Apr 2025 23:48:46 +0200 Subject: [PATCH 6/7] run fmt --- crates/cheatcodes/spec/src/vm.rs | 2 +- crates/cheatcodes/src/config.rs | 670 ++++++++++++++++++------------- crates/cheatcodes/src/test.rs | 4 - 3 files changed, 399 insertions(+), 277 deletions(-) diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 86d7309cd2c38..b9dc720e8cd3c 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -979,7 +979,7 @@ interface Vm { #[cheatcode(group = Testing, safety = Safe)] function rpcUrlStructs() external view returns (Rpc[] memory urls); - /// Returns a Chain struct for specific alias + /// Returns a Chain struct for specific alias #[cheatcode(group = Testing, safety = Safe)] function getChain(string calldata chainAlias) external view returns (Chain memory chain); diff --git a/crates/cheatcodes/src/config.rs b/crates/cheatcodes/src/config.rs index 6a67d56b3a331..210c76553cba7 100644 --- a/crates/cheatcodes/src/config.rs +++ b/crates/cheatcodes/src/config.rs @@ -9,9 +9,9 @@ use foundry_config::{ }; use foundry_evm_core::opts::EvmOpts; use std::{ + collections::HashMap, path::{Path, PathBuf}, time::Duration, - collections::HashMap, }; /// Additional, configurable context the `Cheatcodes` inspector has access to @@ -224,46 +224,45 @@ impl CheatsConfig { if !self.chains.is_empty() { return; // Already initialized } - + // Use the same function to create chains let chains = create_default_chains(); - + // Add all chains to the config for (alias, data) in chains { self.set_chain_with_default_rpc_url(&alias, data); } } - + /// Set chain with default RPC URL (similar to setChainWithDefaultRpcUrl in Solidity) pub fn set_chain_with_default_rpc_url(&mut self, alias: &str, data: ChainData) { // Store the default RPC URL is already stored in the data // No need to clone it separately - + // Add chain data self.set_chain_data(alias, data); } - + /// Set chain data for a specific alias pub fn set_chain_data(&mut self, alias: &str, data: ChainData) { // Remove old chain ID mapping if it exists if let Some(old_data) = self.chains.get(alias) { self.chain_id_to_alias.remove(&old_data.chain_id); } - + // Add new mappings self.chain_id_to_alias.insert(data.chain_id, alias.to_string()); self.chains.insert(alias.to_string(), data); } - + /// Get chain data by alias pub fn get_chain_data_by_alias_non_mut(&self, alias: &str) -> Result { // Initialize chains if not already done if self.chains.is_empty() { - // Create a temporary copy with initialized chains // This is inefficient but handles the edge case let temp_chains = create_default_chains(); - + if let Some(data) = temp_chains.get(alias) { return Ok(data.clone()); } @@ -273,12 +272,12 @@ impl CheatsConfig { return Ok(data.clone()); } } - + // Chain not found in either case Err(fmt_err!("vm.getChain: Chain with alias \"{}\" not found", alias)) } - /// Get RPC URL for an alias + /// Get RPC URL for an alias pub fn get_rpc_url_non_mut(&self, alias: &str) -> Result { // Try to get from config first match self.rpc_endpoint(alias) { @@ -319,6 +318,393 @@ impl Default for CheatsConfig { } } +// Helper function to set default chains +fn create_default_chains() -> HashMap { + let mut chains = HashMap::new(); + + // Define all chains in one place + chains.insert( + "anvil".to_string(), + ChainData { + name: "Anvil".to_string(), + chain_id: 31337, + default_rpc_url: "http://127.0.0.1:8545".to_string(), + }, + ); + + chains.insert( + "mainnet".to_string(), + ChainData { + name: "Mainnet".to_string(), + chain_id: 1, + default_rpc_url: "https://eth.llamarpc.com".to_string(), + }, + ); + + chains.insert( + "sepolia".to_string(), + ChainData { + name: "Sepolia".to_string(), + chain_id: 11155111, + default_rpc_url: "https://sepolia.infura.io/v3/b9794ad1ddf84dfb8c34d6bb5dca2001" + .to_string(), + }, + ); + + chains.insert( + "holesky".to_string(), + ChainData { + name: "Holesky".to_string(), + chain_id: 17000, + default_rpc_url: "https://rpc.holesky.ethpandaops.io".to_string(), + }, + ); + + chains.insert( + "optimism".to_string(), + ChainData { + name: "Optimism".to_string(), + chain_id: 10, + default_rpc_url: "https://mainnet.optimism.io".to_string(), + }, + ); + + chains.insert( + "optimism_sepolia".to_string(), + ChainData { + name: "Optimism Sepolia".to_string(), + chain_id: 11155420, + default_rpc_url: "https://sepolia.optimism.io".to_string(), + }, + ); + + chains.insert( + "arbitrum_one".to_string(), + ChainData { + name: "Arbitrum One".to_string(), + chain_id: 42161, + default_rpc_url: "https://arb1.arbitrum.io/rpc".to_string(), + }, + ); + + chains.insert( + "arbitrum_one_sepolia".to_string(), + ChainData { + name: "Arbitrum One Sepolia".to_string(), + chain_id: 421614, + default_rpc_url: "https://sepolia-rollup.arbitrum.io/rpc".to_string(), + }, + ); + + chains.insert( + "arbitrum_nova".to_string(), + ChainData { + name: "Arbitrum Nova".to_string(), + chain_id: 42170, + default_rpc_url: "https://nova.arbitrum.io/rpc".to_string(), + }, + ); + + chains.insert( + "polygon".to_string(), + ChainData { + name: "Polygon".to_string(), + chain_id: 137, + default_rpc_url: "https://polygon-rpc.com".to_string(), + }, + ); + + chains.insert( + "polygon_amoy".to_string(), + ChainData { + name: "Polygon Amoy".to_string(), + chain_id: 80002, + default_rpc_url: "https://rpc-amoy.polygon.technology".to_string(), + }, + ); + + chains.insert( + "avalanche".to_string(), + ChainData { + name: "Avalanche".to_string(), + chain_id: 43114, + default_rpc_url: "https://api.avax.network/ext/bc/C/rpc".to_string(), + }, + ); + + chains.insert( + "avalanche_fuji".to_string(), + ChainData { + name: "Avalanche Fuji".to_string(), + chain_id: 43113, + default_rpc_url: "https://api.avax-test.network/ext/bc/C/rpc".to_string(), + }, + ); + + chains.insert( + "bnb_smart_chain".to_string(), + ChainData { + name: "BNB Smart Chain".to_string(), + chain_id: 56, + default_rpc_url: "https://bsc-dataseed1.binance.org".to_string(), + }, + ); + + chains.insert( + "bnb_smart_chain_testnet".to_string(), + ChainData { + name: "BNB Smart Chain Testnet".to_string(), + chain_id: 97, + default_rpc_url: "https://rpc.ankr.com/bsc_testnet_chapel".to_string(), + }, + ); + + chains.insert( + "gnosis_chain".to_string(), + ChainData { + name: "Gnosis Chain".to_string(), + chain_id: 100, + default_rpc_url: "https://rpc.gnosischain.com".to_string(), + }, + ); + + chains.insert( + "moonbeam".to_string(), + ChainData { + name: "Moonbeam".to_string(), + chain_id: 1284, + default_rpc_url: "https://rpc.api.moonbeam.network".to_string(), + }, + ); + + chains.insert( + "moonriver".to_string(), + ChainData { + name: "Moonriver".to_string(), + chain_id: 1285, + default_rpc_url: "https://rpc.api.moonriver.moonbeam.network".to_string(), + }, + ); + + chains.insert( + "moonbase".to_string(), + ChainData { + name: "Moonbase".to_string(), + chain_id: 1287, + default_rpc_url: "https://rpc.testnet.moonbeam.network".to_string(), + }, + ); + + chains.insert( + "base_sepolia".to_string(), + ChainData { + name: "Base Sepolia".to_string(), + chain_id: 84532, + default_rpc_url: "https://sepolia.base.org".to_string(), + }, + ); + + chains.insert( + "base".to_string(), + ChainData { + name: "Base".to_string(), + chain_id: 8453, + default_rpc_url: "https://mainnet.base.org".to_string(), + }, + ); + + chains.insert( + "blast_sepolia".to_string(), + ChainData { + name: "Blast Sepolia".to_string(), + chain_id: 168587773, + default_rpc_url: "https://sepolia.blast.io".to_string(), + }, + ); + + chains.insert( + "blast".to_string(), + ChainData { + name: "Blast".to_string(), + chain_id: 81457, + default_rpc_url: "https://rpc.blast.io".to_string(), + }, + ); + + chains.insert( + "fantom_opera".to_string(), + ChainData { + name: "Fantom Opera".to_string(), + chain_id: 250, + default_rpc_url: "https://rpc.ankr.com/fantom/".to_string(), + }, + ); + + chains.insert( + "fantom_opera_testnet".to_string(), + ChainData { + name: "Fantom Opera Testnet".to_string(), + chain_id: 4002, + default_rpc_url: "https://rpc.ankr.com/fantom_testnet/".to_string(), + }, + ); + + chains.insert( + "fraxtal".to_string(), + ChainData { + name: "Fraxtal".to_string(), + chain_id: 252, + default_rpc_url: "https://rpc.frax.com".to_string(), + }, + ); + + chains.insert( + "fraxtal_testnet".to_string(), + ChainData { + name: "Fraxtal Testnet".to_string(), + chain_id: 2522, + default_rpc_url: "https://rpc.testnet.frax.com".to_string(), + }, + ); + + chains.insert( + "berachain_bartio_testnet".to_string(), + ChainData { + name: "Berachain bArtio Testnet".to_string(), + chain_id: 80084, + default_rpc_url: "https://bartio.rpc.berachain.com".to_string(), + }, + ); + + chains.insert( + "flare".to_string(), + ChainData { + name: "Flare".to_string(), + chain_id: 14, + default_rpc_url: "https://flare-api.flare.network/ext/C/rpc".to_string(), + }, + ); + + chains.insert( + "flare_coston2".to_string(), + ChainData { + name: "Flare Coston2".to_string(), + chain_id: 114, + default_rpc_url: "https://coston2-api.flare.network/ext/C/rpc".to_string(), + }, + ); + + chains.insert( + "mode".to_string(), + ChainData { + name: "Mode".to_string(), + chain_id: 34443, + default_rpc_url: "https://mode.drpc.org".to_string(), + }, + ); + + chains.insert( + "mode_sepolia".to_string(), + ChainData { + name: "Mode Sepolia".to_string(), + chain_id: 919, + default_rpc_url: "https://sepolia.mode.network".to_string(), + }, + ); + + chains.insert( + "zora".to_string(), + ChainData { + name: "Zora".to_string(), + chain_id: 7777777, + default_rpc_url: "https://zora.drpc.org".to_string(), + }, + ); + + chains.insert( + "zora_sepolia".to_string(), + ChainData { + name: "Zora Sepolia".to_string(), + chain_id: 999999999, + default_rpc_url: "https://sepolia.rpc.zora.energy".to_string(), + }, + ); + + chains.insert( + "race".to_string(), + ChainData { + name: "Race".to_string(), + chain_id: 6805, + default_rpc_url: "https://racemainnet.io".to_string(), + }, + ); + + chains.insert( + "race_sepolia".to_string(), + ChainData { + name: "Race Sepolia".to_string(), + chain_id: 6806, + default_rpc_url: "https://racemainnet.io".to_string(), + }, + ); + + chains.insert( + "metal".to_string(), + ChainData { + name: "Metal".to_string(), + chain_id: 1750, + default_rpc_url: "https://metall2.drpc.org".to_string(), + }, + ); + + chains.insert( + "metal_sepolia".to_string(), + ChainData { + name: "Metal Sepolia".to_string(), + chain_id: 1740, + default_rpc_url: "https://testnet.rpc.metall2.com".to_string(), + }, + ); + + chains.insert( + "binary".to_string(), + ChainData { + name: "Binary".to_string(), + chain_id: 624, + default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string(), + }, + ); + + chains.insert( + "binary_sepolia".to_string(), + ChainData { + name: "Binary Sepolia".to_string(), + chain_id: 625, + default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string(), + }, + ); + + chains.insert( + "orderly".to_string(), + ChainData { + name: "Orderly".to_string(), + chain_id: 291, + default_rpc_url: "https://rpc.orderly.network".to_string(), + }, + ); + + chains.insert( + "orderly_sepolia".to_string(), + ChainData { + name: "Orderly Sepolia".to_string(), + chain_id: 4460, + default_rpc_url: "https://testnet-rpc.orderly.org".to_string(), + }, + ); + + chains +} + #[cfg(test)] mod tests { use super::*; @@ -361,263 +747,3 @@ mod tests { assert!(!config.is_foundry_toml(f)); } } - -// Helper function to set default chains -fn create_default_chains() -> HashMap { - let mut chains = HashMap::new(); - - // Define all chains in one place - chains.insert("anvil".to_string(), ChainData { - name: "Anvil".to_string(), - chain_id: 31337, - default_rpc_url: "http://127.0.0.1:8545".to_string() - }); - - chains.insert("mainnet".to_string(), ChainData { - name: "Mainnet".to_string(), - chain_id: 1, - default_rpc_url: "https://eth.llamarpc.com".to_string() - }); - - chains.insert("sepolia".to_string(), ChainData { - name: "Sepolia".to_string(), - chain_id: 11155111, - default_rpc_url: "https://sepolia.infura.io/v3/b9794ad1ddf84dfb8c34d6bb5dca2001".to_string() - }); - - chains.insert("holesky".to_string(), ChainData { - name: "Holesky".to_string(), - chain_id: 17000, - default_rpc_url: "https://rpc.holesky.ethpandaops.io".to_string() - }); - - chains.insert("optimism".to_string(), ChainData { - name: "Optimism".to_string(), - chain_id: 10, - default_rpc_url: "https://mainnet.optimism.io".to_string() - }); - - chains.insert("optimism_sepolia".to_string(), ChainData { - name: "Optimism Sepolia".to_string(), - chain_id: 11155420, - default_rpc_url: "https://sepolia.optimism.io".to_string() - }); - - chains.insert("arbitrum_one".to_string(), ChainData { - name: "Arbitrum One".to_string(), - chain_id: 42161, - default_rpc_url: "https://arb1.arbitrum.io/rpc".to_string() - }); - - chains.insert("arbitrum_one_sepolia".to_string(), ChainData { - name: "Arbitrum One Sepolia".to_string(), - chain_id: 421614, - default_rpc_url: "https://sepolia-rollup.arbitrum.io/rpc".to_string() - }); - - chains.insert("arbitrum_nova".to_string(), ChainData { - name: "Arbitrum Nova".to_string(), - chain_id: 42170, - default_rpc_url: "https://nova.arbitrum.io/rpc".to_string() - }); - - chains.insert("polygon".to_string(), ChainData { - name: "Polygon".to_string(), - chain_id: 137, - default_rpc_url: "https://polygon-rpc.com".to_string() - }); - - chains.insert("polygon_amoy".to_string(), ChainData { - name: "Polygon Amoy".to_string(), - chain_id: 80002, - default_rpc_url: "https://rpc-amoy.polygon.technology".to_string() - }); - - chains.insert("avalanche".to_string(), ChainData { - name: "Avalanche".to_string(), - chain_id: 43114, - default_rpc_url: "https://api.avax.network/ext/bc/C/rpc".to_string() - }); - - chains.insert("avalanche_fuji".to_string(), ChainData { - name: "Avalanche Fuji".to_string(), - chain_id: 43113, - default_rpc_url: "https://api.avax-test.network/ext/bc/C/rpc".to_string() - }); - - chains.insert("bnb_smart_chain".to_string(), ChainData { - name: "BNB Smart Chain".to_string(), - chain_id: 56, - default_rpc_url: "https://bsc-dataseed1.binance.org".to_string() - }); - - chains.insert("bnb_smart_chain_testnet".to_string(), ChainData { - name: "BNB Smart Chain Testnet".to_string(), - chain_id: 97, - default_rpc_url: "https://rpc.ankr.com/bsc_testnet_chapel".to_string() - }); - - chains.insert("gnosis_chain".to_string(), ChainData { - name: "Gnosis Chain".to_string(), - chain_id: 100, - default_rpc_url: "https://rpc.gnosischain.com".to_string() - }); - - chains.insert("moonbeam".to_string(), ChainData { - name: "Moonbeam".to_string(), - chain_id: 1284, - default_rpc_url: "https://rpc.api.moonbeam.network".to_string() - }); - - chains.insert("moonriver".to_string(), ChainData { - name: "Moonriver".to_string(), - chain_id: 1285, - default_rpc_url: "https://rpc.api.moonriver.moonbeam.network".to_string() - }); - - chains.insert("moonbase".to_string(), ChainData { - name: "Moonbase".to_string(), - chain_id: 1287, - default_rpc_url: "https://rpc.testnet.moonbeam.network".to_string() - }); - - chains.insert("base_sepolia".to_string(), ChainData { - name: "Base Sepolia".to_string(), - chain_id: 84532, - default_rpc_url: "https://sepolia.base.org".to_string() - }); - - chains.insert("base".to_string(), ChainData { - name: "Base".to_string(), - chain_id: 8453, - default_rpc_url: "https://mainnet.base.org".to_string() - }); - - chains.insert("blast_sepolia".to_string(), ChainData { - name: "Blast Sepolia".to_string(), - chain_id: 168587773, - default_rpc_url: "https://sepolia.blast.io".to_string() - }); - - chains.insert("blast".to_string(), ChainData { - name: "Blast".to_string(), - chain_id: 81457, - default_rpc_url: "https://rpc.blast.io".to_string() - }); - - chains.insert("fantom_opera".to_string(), ChainData { - name: "Fantom Opera".to_string(), - chain_id: 250, - default_rpc_url: "https://rpc.ankr.com/fantom/".to_string() - }); - - chains.insert("fantom_opera_testnet".to_string(), ChainData { - name: "Fantom Opera Testnet".to_string(), - chain_id: 4002, - default_rpc_url: "https://rpc.ankr.com/fantom_testnet/".to_string() - }); - - chains.insert("fraxtal".to_string(), ChainData { - name: "Fraxtal".to_string(), - chain_id: 252, - default_rpc_url: "https://rpc.frax.com".to_string() - }); - - chains.insert("fraxtal_testnet".to_string(), ChainData { - name: "Fraxtal Testnet".to_string(), - chain_id: 2522, - default_rpc_url: "https://rpc.testnet.frax.com".to_string() - }); - - chains.insert("berachain_bartio_testnet".to_string(), ChainData { - name: "Berachain bArtio Testnet".to_string(), - chain_id: 80084, - default_rpc_url: "https://bartio.rpc.berachain.com".to_string() - }); - - chains.insert("flare".to_string(), ChainData { - name: "Flare".to_string(), - chain_id: 14, - default_rpc_url: "https://flare-api.flare.network/ext/C/rpc".to_string() - }); - - chains.insert("flare_coston2".to_string(), ChainData { - name: "Flare Coston2".to_string(), - chain_id: 114, - default_rpc_url: "https://coston2-api.flare.network/ext/C/rpc".to_string() - }); - - chains.insert("mode".to_string(), ChainData { - name: "Mode".to_string(), - chain_id: 34443, - default_rpc_url: "https://mode.drpc.org".to_string() - }); - - chains.insert("mode_sepolia".to_string(), ChainData { - name: "Mode Sepolia".to_string(), - chain_id: 919, - default_rpc_url: "https://sepolia.mode.network".to_string() - }); - - chains.insert("zora".to_string(), ChainData { - name: "Zora".to_string(), - chain_id: 7777777, - default_rpc_url: "https://zora.drpc.org".to_string() - }); - - chains.insert("zora_sepolia".to_string(), ChainData { - name: "Zora Sepolia".to_string(), - chain_id: 999999999, - default_rpc_url: "https://sepolia.rpc.zora.energy".to_string() - }); - - chains.insert("race".to_string(), ChainData { - name: "Race".to_string(), - chain_id: 6805, - default_rpc_url: "https://racemainnet.io".to_string() - }); - - chains.insert("race_sepolia".to_string(), ChainData { - name: "Race Sepolia".to_string(), - chain_id: 6806, - default_rpc_url: "https://racemainnet.io".to_string() - }); - - chains.insert("metal".to_string(), ChainData { - name: "Metal".to_string(), - chain_id: 1750, - default_rpc_url: "https://metall2.drpc.org".to_string() - }); - - chains.insert("metal_sepolia".to_string(), ChainData { - name: "Metal Sepolia".to_string(), - chain_id: 1740, - default_rpc_url: "https://testnet.rpc.metall2.com".to_string() - }); - - chains.insert("binary".to_string(), ChainData { - name: "Binary".to_string(), - chain_id: 624, - default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string() - }); - - chains.insert("binary_sepolia".to_string(), ChainData { - name: "Binary Sepolia".to_string(), - chain_id: 625, - default_rpc_url: "https://rpc.zero.thebinaryholdings.com".to_string() - }); - - chains.insert("orderly".to_string(), ChainData { - name: "Orderly".to_string(), - chain_id: 291, - default_rpc_url: "https://rpc.orderly.network".to_string() - }); - - chains.insert("orderly_sepolia".to_string(), ChainData { - name: "Orderly Sepolia".to_string(), - chain_id: 4460, - default_rpc_url: "https://testnet-rpc.orderly.org".to_string() - }); - - chains -} diff --git a/crates/cheatcodes/src/test.rs b/crates/cheatcodes/src/test.rs index 29f5472773721..0bcc4c6bb85f3 100644 --- a/crates/cheatcodes/src/test.rs +++ b/crates/cheatcodes/src/test.rs @@ -115,13 +115,10 @@ fn get_chain_data(state: &Cheatcodes, alias: &str) -> Result { if alias.is_empty() { bail!("Chain alias cannot be empty"); } - // Get chain data from alias let chain_data = state.config.get_chain_data_by_alias_non_mut(alias)?; - // Get RPC URL let rpc_url = state.config.get_rpc_url_non_mut(alias)?; - // Create the Chain struct Ok(Chain { name: chain_data.name, @@ -130,4 +127,3 @@ fn get_chain_data(state: &Cheatcodes, alias: &str) -> Result { rpcUrl: rpc_url, }) } - From bb556e2e0f5aa25fe94f31a5e9a3ac0ad6358b79 Mon Sep 17 00:00:00 2001 From: 0xcharlie Date: Mon, 14 Apr 2025 20:11:45 +0200 Subject: [PATCH 7/7] fix: add alloy_chain for check chain validity --- Cargo.lock | 1 + crates/cheatcodes/Cargo.toml | 1 + crates/cheatcodes/assets/cheatcodes.json | 22 ++++- crates/cheatcodes/spec/src/vm.rs | 4 + crates/cheatcodes/src/test.rs | 66 ++++++++++----- testdata/cheats/Vm.sol | 1 + testdata/default/cheats/GetChain.t.sol | 103 +++++++++++++++-------- 7 files changed, 141 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d08cb6913c987..50a8562ca439d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3705,6 +3705,7 @@ dependencies = [ name = "foundry-cheatcodes" version = "1.0.0" dependencies = [ + "alloy-chains", "alloy-consensus", "alloy-dyn-abi", "alloy-genesis", diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 6965ebe0b646c..645426ea8bf29 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -40,6 +40,7 @@ parking_lot.workspace = true alloy-consensus = { workspace = true, features = ["k256"] } alloy-network.workspace = true alloy-rlp.workspace = true +alloy-chains.workspace = true base64.workspace = true dialoguer = "0.11" diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 099edbe2b06c8..b0428a2d90351 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -5956,7 +5956,7 @@ }, { "func": { - "id": "getChain", + "id": "getChain_0", "description": "Returns a Chain struct for specific alias", "declaration": "function getChain(string calldata chainAlias) external view returns (Chain memory chain);", "visibility": "external", @@ -5974,6 +5974,26 @@ "status": "stable", "safety": "safe" }, + { + "func": { + "id": "getChain_1", + "description": "Returns a Chain struct for specific chainId", + "declaration": "function getChain(uint256 chainId) external view returns (Chain memory chain);", + "visibility": "external", + "mutability": "view", + "signature": "getChain(uint256)", + "selector": "0xb6791ad4", + "selectorBytes": [ + 182, + 121, + 26, + 212 + ] + }, + "group": "testing", + "status": "stable", + "safety": "safe" + }, { "func": { "id": "getCode", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index b9dc720e8cd3c..dca29402ca47e 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -983,6 +983,10 @@ interface Vm { #[cheatcode(group = Testing, safety = Safe)] function getChain(string calldata chainAlias) external view returns (Chain memory chain); + /// Returns a Chain struct for specific chainId + #[cheatcode(group = Testing, safety = Safe)] + function getChain(uint256 chainId) external view returns (Chain memory chain); + /// Suspends execution of the main thread for `duration` milliseconds. #[cheatcode(group = Testing, safety = Safe)] function sleep(uint256 duration) external; diff --git a/crates/cheatcodes/src/test.rs b/crates/cheatcodes/src/test.rs index 0bcc4c6bb85f3..9a59430a9264c 100644 --- a/crates/cheatcodes/src/test.rs +++ b/crates/cheatcodes/src/test.rs @@ -1,10 +1,12 @@ //! Implementations of [`Testing`](spec::Group::Testing) cheatcodes. use crate::{Cheatcode, Cheatcodes, CheatsCtxt, Result, Vm::*}; +use alloy_chains::Chain as AlloyChain; use alloy_primitives::{Address, U256}; use alloy_sol_types::SolValue; use foundry_common::version::SEMVER_VERSION; use foundry_evm_core::constants::MAGIC_SKIP; +use std::str::FromStr; pub(crate) mod assert; pub(crate) mod assume; @@ -32,14 +34,6 @@ impl Cheatcode for getFoundryVersionCall { } } -impl Cheatcode for getChainCall { - fn apply(&self, state: &mut Cheatcodes) -> Result { - let Self { chainAlias } = self; - let chain = get_chain_data(state, chainAlias)?; - Ok(chain.abi_encode()) - } -} - impl Cheatcode for rpcUrlCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { rpcAlias } = self; @@ -92,6 +86,22 @@ impl Cheatcode for skip_1Call { } } +impl Cheatcode for getChain_0Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { chainAlias } = self; + get_chain(state, chainAlias) + } +} + +impl Cheatcode for getChain_1Call { + fn apply(&self, state: &mut Cheatcodes) -> Result { + let Self { chainId } = self; + // Convert the chainId to a string and use the existing get_chain function + let chain_id_str = chainId.to_string(); + get_chain(state, &chain_id_str) + } +} + /// Adds or removes the given breakpoint to the state. fn breakpoint(state: &mut Cheatcodes, caller: &Address, s: &str, add: bool) -> Result { let mut chars = s.chars(); @@ -109,21 +119,31 @@ fn breakpoint(state: &mut Cheatcodes, caller: &Address, s: &str, add: bool) -> R Ok(Default::default()) } -/// Get chain data and create a Chain struct -fn get_chain_data(state: &Cheatcodes, alias: &str) -> Result { - // Ensure the alias is not empty - if alias.is_empty() { - bail!("Chain alias cannot be empty"); +/// Gets chain information for the given alias. +fn get_chain(state: &mut Cheatcodes, chain_alias: &str) -> Result { + // Parse the chain alias - works for both chain names and IDs + let alloy_chain = AlloyChain::from_str(chain_alias) + .map_err(|_| fmt_err!("invalid chain alias: {chain_alias}"))?; + + // Check if this is an unknown chain ID by comparing the name to the chain ID + // When a numeric ID is passed for an unknown chain, alloy_chain.to_string() will return the ID + // So if they match, it's likely an unknown chain ID + if alloy_chain.to_string() == alloy_chain.id().to_string() { + return Err(fmt_err!("invalid chain alias: {chain_alias}")); } - // Get chain data from alias - let chain_data = state.config.get_chain_data_by_alias_non_mut(alias)?; - // Get RPC URL - let rpc_url = state.config.get_rpc_url_non_mut(alias)?; - // Create the Chain struct - Ok(Chain { - name: chain_data.name, - chainId: U256::from(chain_data.chain_id), - chainAlias: alias.to_string(), + + // First, try to get RPC URL from the user's config in foundry.toml + let rpc_url = state.config.rpc_endpoint(chain_alias).ok().and_then(|e| e.url().ok()); + + // If we couldn't get a URL from config, return an empty string + let rpc_url = rpc_url.unwrap_or_default(); + + let chain_struct = Chain { + name: alloy_chain.to_string(), + chainId: U256::from(alloy_chain.id()), + chainAlias: chain_alias.to_string(), rpcUrl: rpc_url, - }) + }; + + Ok(chain_struct.abi_encode()) } diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index 3e589624a4b73..69d156adcf632 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -292,6 +292,7 @@ interface Vm { function getBroadcasts(string calldata contractName, uint64 chainId, BroadcastTxType txType) external view returns (BroadcastTxSummary[] memory); function getBroadcasts(string calldata contractName, uint64 chainId) external view returns (BroadcastTxSummary[] memory); function getChain(string calldata chainAlias) external view returns (Chain memory chain); + function getChain(uint256 chainId) external view returns (Chain memory chain); function getCode(string calldata artifactPath) external view returns (bytes memory creationBytecode); function getDeployedCode(string calldata artifactPath) external view returns (bytes memory runtimeBytecode); function getDeployment(string calldata contractName) external view returns (address deployedAddress); diff --git a/testdata/default/cheats/GetChain.t.sol b/testdata/default/cheats/GetChain.t.sol index a331e6421c6ce..856a6b8a75712 100644 --- a/testdata/default/cheats/GetChain.t.sol +++ b/testdata/default/cheats/GetChain.t.sol @@ -7,48 +7,85 @@ import "cheats/Vm.sol"; contract GetChainTest is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); - function testGetChainByAlias() public { + function testGetMainnet() public { // Test mainnet Vm.Chain memory mainnet = vm.getChain("mainnet"); - assertEq(mainnet.name, "Mainnet"); + assertEq(mainnet.name, "mainnet"); assertEq(mainnet.chainId, 1); assertEq(mainnet.chainAlias, "mainnet"); - assertTrue(bytes(mainnet.rpcUrl).length > 0); - - // Test sepolia + } + + function testGetSepolia() public { + // Test Sepolia Vm.Chain memory sepolia = vm.getChain("sepolia"); - assertEq(sepolia.name, "Sepolia"); + assertEq(sepolia.name, "sepolia"); assertEq(sepolia.chainId, 11155111); assertEq(sepolia.chainAlias, "sepolia"); - assertTrue(bytes(sepolia.rpcUrl).length > 0); - - // Test Anvil/Local chain - Vm.Chain memory anvil = vm.getChain("anvil"); - assertEq(anvil.name, "Anvil"); - assertEq(anvil.chainId, 31337); - assertEq(anvil.chainAlias, "anvil"); - assertTrue(bytes(anvil.rpcUrl).length > 0); - } - - function testGetChainInvalidAlias() public { - // Test with invalid alias - should revert - vm._expectCheatcodeRevert("vm.getChain: Chain with alias \"invalid_chain\" not found"); - vm.getChain("invalid_chain"); - } - - function testGetChainEmptyAlias() public { - vm._expectCheatcodeRevert("Chain alias cannot be empty"); + } + + function testGetOptimism() public { + // Test Optimism + Vm.Chain memory optimism = vm.getChain("optimism"); + assertEq(optimism.name, "optimism"); + assertEq(optimism.chainId, 10); + assertEq(optimism.chainAlias, "optimism"); + } + + function testGetByChainId() public { + // Test getting a chain by its ID + vm._expectCheatcodeRevert("invalid chain alias:"); + Vm.Chain memory arbitrum = vm.getChain("42161222"); + } + + function testEmptyAlias() public { + // Test empty string + vm._expectCheatcodeRevert("invalid chain alias:"); vm.getChain(""); } - - function testGetChainRpcUrlPriority() public { - // This test assumes running with default config where no custom RPC URLs are set - // for mainnet. So it should use the default RPC URL. - Vm.Chain memory mainnet = vm.getChain("mainnet"); - assertTrue(bytes(mainnet.rpcUrl).length > 0); - - // You can print the URL for manual verification - emit log_string(mainnet.rpcUrl); + function testInvalidAlias() public { + // Test invalid alias + vm._expectCheatcodeRevert("invalid chain alias: nonexistent_chain"); + vm.getChain("nonexistent_chain"); + } + + // Tests for the numeric chainId version of getChain + + function testGetMainnetById() public { + // Test mainnet using chain ID + Vm.Chain memory mainnet = vm.getChain(1); + assertEq(mainnet.name, "mainnet"); + assertEq(mainnet.chainId, 1); + assertEq(mainnet.chainAlias, "1"); + } + + function testGetSepoliaById() public { + // Test Sepolia using chain ID + Vm.Chain memory sepolia = vm.getChain(11155111); + assertEq(sepolia.name, "sepolia"); + assertEq(sepolia.chainId, 11155111); + assertEq(sepolia.chainAlias, "11155111"); + } + + function testGetOptimismById() public { + // Test Optimism using chain ID + Vm.Chain memory optimism = vm.getChain(10); + assertEq(optimism.name, "optimism"); + assertEq(optimism.chainId, 10); + assertEq(optimism.chainAlias, "10"); + } + + function testGetArbitrumById() public { + // Test Arbitrum using chain ID + Vm.Chain memory arbitrum = vm.getChain(42161); + assertEq(arbitrum.name, "arbitrum"); + assertEq(arbitrum.chainId, 42161); + assertEq(arbitrum.chainAlias, "42161"); + } + + function testInvalidChainId() public { + // Test invalid chain ID (using a value that's unlikely to be a valid chain) + vm._expectCheatcodeRevert("invalid chain alias: 12345678"); + vm.getChain(12345678); } }