diff --git a/Cargo.lock b/Cargo.lock index d3510d645ff34..e93fb59313253 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1032,7 +1032,7 @@ dependencies = [ [[package]] name = "anvil" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -1098,7 +1098,7 @@ dependencies = [ [[package]] name = "anvil-core" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -1122,7 +1122,7 @@ dependencies = [ [[package]] name = "anvil-rpc" -version = "1.3.0" +version = "1.3.1" dependencies = [ "serde", "serde_json", @@ -1130,7 +1130,7 @@ dependencies = [ [[package]] name = "anvil-server" -version = "1.3.0" +version = "1.3.1" dependencies = [ "anvil-rpc", "async-trait", @@ -2357,7 +2357,7 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cast" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -2458,7 +2458,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chisel" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -3789,7 +3789,7 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "forge" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-chains", "alloy-dyn-abi", @@ -3873,7 +3873,7 @@ dependencies = [ [[package]] name = "forge-doc" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-primitives", "derive_more 2.0.1", @@ -3896,7 +3896,7 @@ dependencies = [ [[package]] name = "forge-fmt" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-primitives", "ariadne", @@ -3912,7 +3912,7 @@ dependencies = [ [[package]] name = "forge-lint" -version = "1.3.0" +version = "1.3.1" dependencies = [ "foundry-common", "foundry-compilers", @@ -3929,7 +3929,7 @@ dependencies = [ [[package]] name = "forge-script" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -3974,7 +3974,7 @@ dependencies = [ [[package]] name = "forge-script-sequence" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-network", "alloy-primitives", @@ -3990,7 +3990,7 @@ dependencies = [ [[package]] name = "forge-sol-macro-gen" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-sol-macro-expander", "alloy-sol-macro-input", @@ -4006,7 +4006,7 @@ dependencies = [ [[package]] name = "forge-verify" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -4088,7 +4088,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4138,7 +4138,7 @@ dependencies = [ [[package]] name = "foundry-cheatcodes-spec" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-sol-types", "foundry-macros", @@ -4149,7 +4149,7 @@ dependencies = [ [[package]] name = "foundry-cli" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-chains", "alloy-dyn-abi", @@ -4197,7 +4197,7 @@ dependencies = [ [[package]] name = "foundry-common" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -4254,7 +4254,7 @@ dependencies = [ [[package]] name = "foundry-common-fmt" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -4273,9 +4273,9 @@ dependencies = [ [[package]] name = "foundry-compilers" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f163b01cdad921f139776084391db2f1f5fb206ce2395f1847f0c1e992a89a4f" +checksum = "4e953daf389ec3f8d82566cfcdadae58ca01b1bf0a7c467b2b03d447971589df" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -4310,9 +4310,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2676d70082ed23680fe2d08c0b750d5f7f2438c6d946f1cb140a76c5e5e0392" +checksum = "f88824bfa6a5c2d7ecd076ba232cf37532af611c764fc27bb74371a91f662c36" dependencies = [ "foundry-compilers-artifacts-solc", "foundry-compilers-artifacts-vyper", @@ -4320,9 +4320,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-solc" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ada94dc5946334bb08df574855ba345ab03ba8c6f233560c72c8d61fa9db80" +checksum = "55273eea0135b2d6e80d8475f660c87c855100999ce6f3e8c065e2fc55b95d7c" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -4343,9 +4343,9 @@ dependencies = [ [[package]] name = "foundry-compilers-artifacts-vyper" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372052af72652e375a6e7eed22179bd8935114e25e1c5a8cca7f00e8f20bd94c" +checksum = "19d0254354ec10a7fc3b5a38ea0e6d9c841c35159f9b052e0bd87769e28c00da" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -4358,9 +4358,9 @@ dependencies = [ [[package]] name = "foundry-compilers-core" -version = "0.18.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0962c46855979300f6526ed57f987ccf6a025c2b92ce574b281d9cb2ef666b" +checksum = "0773827e62d4eba29b04dff112211c1cadb614352da1cc2d951c33ca446415be" dependencies = [ "alloy-primitives", "cfg-if", @@ -4381,7 +4381,7 @@ dependencies = [ [[package]] name = "foundry-config" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-chains", "alloy-primitives", @@ -4420,7 +4420,7 @@ dependencies = [ [[package]] name = "foundry-debugger" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-primitives", "crossterm", @@ -4438,7 +4438,7 @@ dependencies = [ [[package]] name = "foundry-evm" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-dyn-abi", "alloy-evm", @@ -4469,7 +4469,7 @@ dependencies = [ [[package]] name = "foundry-evm-abi" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-primitives", "alloy-sol-types", @@ -4481,7 +4481,7 @@ dependencies = [ [[package]] name = "foundry-evm-core" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-chains", "alloy-consensus", @@ -4520,7 +4520,7 @@ dependencies = [ [[package]] name = "foundry-evm-coverage" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-primitives", "eyre", @@ -4535,7 +4535,7 @@ dependencies = [ [[package]] name = "foundry-evm-fuzz" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -4559,7 +4559,7 @@ dependencies = [ [[package]] name = "foundry-evm-traces" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-dyn-abi", "alloy-json-abi", @@ -4611,7 +4611,7 @@ dependencies = [ [[package]] name = "foundry-linking" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-primitives", "foundry-compilers", @@ -4621,7 +4621,7 @@ dependencies = [ [[package]] name = "foundry-macros" -version = "1.3.0" +version = "1.3.1" dependencies = [ "proc-macro-error2", "proc-macro2", @@ -4645,7 +4645,7 @@ dependencies = [ [[package]] name = "foundry-test-utils" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-primitives", "alloy-provider", @@ -4670,7 +4670,7 @@ dependencies = [ [[package]] name = "foundry-wallets" -version = "1.3.0" +version = "1.3.1" dependencies = [ "alloy-consensus", "alloy-dyn-abi", diff --git a/Cargo.toml b/Cargo.toml index 2aa5c2a3ff5e7..cadc9af1ebb8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ members = [ resolver = "2" [workspace.package] -version = "1.3.0" +version = "1.3.1" edition = "2024" # Remember to update clippy.toml as well rust-version = "1.88" @@ -205,7 +205,7 @@ foundry-linking = { path = "crates/linking" } # solc & compilation utilities foundry-block-explorers = { version = "0.20.0", default-features = false } -foundry-compilers = { version = "0.18.0", default-features = false } +foundry-compilers = { version = "0.18.2", default-features = false } foundry-fork-db = "0.16" solang-parser = { version = "=0.3.9", package = "foundry-solang-parser" } solar-ast = { version = "=0.1.5", default-features = false } diff --git a/crates/anvil/core/src/eth/transaction/mod.rs b/crates/anvil/core/src/eth/transaction/mod.rs index a85d08e38c373..25ead884739f1 100644 --- a/crates/anvil/core/src/eth/transaction/mod.rs +++ b/crates/anvil/core/src/eth/transaction/mod.rs @@ -81,7 +81,7 @@ pub fn transaction_request_to_typed( to: to?.into_to()?, chain_id: 0, access_list: access_list.unwrap_or_default(), - authorization_list: authorization_list.unwrap(), + authorization_list: authorization_list.unwrap_or_default(), })); } diff --git a/crates/cast/src/cmd/call.rs b/crates/cast/src/cmd/call.rs index 7948d22b9eb83..6c487dffb2b75 100644 --- a/crates/cast/src/cmd/call.rs +++ b/crates/cast/src/cmd/call.rs @@ -21,7 +21,7 @@ use foundry_compilers::artifacts::EvmVersion; use foundry_config::{ Config, figment::{ - self, Figment, Metadata, Profile, + self, Metadata, Profile, value::{Dict, Map}, }, }; @@ -191,7 +191,7 @@ pub enum CallSubcommands { impl CallArgs { pub async fn run(self) -> Result<()> { - let figment = Into::::into(&self.eth).merge(&self); + let figment = self.eth.rpc.clone().into_figment(self.with_local_artifacts).merge(&self); let evm_opts = figment.extract::()?; let mut config = Config::from_provider(figment)?.sanitized(); let state_overrides = self.get_state_overrides()?; diff --git a/crates/cast/src/cmd/mktx.rs b/crates/cast/src/cmd/mktx.rs index df71528ddecdd..344c7b93b1083 100644 --- a/crates/cast/src/cmd/mktx.rs +++ b/crates/cast/src/cmd/mktx.rs @@ -1,11 +1,11 @@ use crate::tx::{self, CastTxBuilder}; use alloy_ens::NameOrAddress; use alloy_network::{EthereumWallet, TransactionBuilder, eip2718::Encodable2718}; -use alloy_primitives::hex; +use alloy_primitives::{Address, hex}; use alloy_provider::Provider; use alloy_signer::Signer; use clap::Parser; -use eyre::{OptionExt, Result}; +use eyre::Result; use foundry_cli::{ opts::{EthereumOpts, TransactionOpts}, utils::{LoadConfig, get_provider}, @@ -49,7 +49,7 @@ pub struct MakeTxArgs { /// Generate a raw RLP-encoded unsigned transaction. /// /// Relaxes the wallet requirement. - #[arg(long, requires = "from")] + #[arg(long)] raw_unsigned: bool, /// Call `eth_signTransaction` using the `--from` argument or $ETH_FROM as sender @@ -96,7 +96,7 @@ impl MakeTxArgs { let provider = get_provider(&config)?; - let tx_builder = CastTxBuilder::new(&provider, tx, &config) + let tx_builder = CastTxBuilder::new(&provider, tx.clone(), &config) .await? .with_to(to) .await? @@ -106,7 +106,17 @@ impl MakeTxArgs { if raw_unsigned { // Build unsigned raw tx - let from = eth.wallet.from.ok_or_eyre("missing `--from` address")?; + // Check if nonce is provided when --from is not specified + // See: + if eth.wallet.from.is_none() && tx.nonce.is_none() { + eyre::bail!( + "Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce" + ); + } + + // Use zero address as placeholder for unsigned transactions + let from = eth.wallet.from.unwrap_or(Address::ZERO); + let raw_tx = tx_builder.build_unsigned_raw(from).await?; sh_println!("{raw_tx}")?; diff --git a/crates/cast/src/cmd/run.rs b/crates/cast/src/cmd/run.rs index 412dd970a89f1..770b718536aa1 100644 --- a/crates/cast/src/cmd/run.rs +++ b/crates/cast/src/cmd/run.rs @@ -17,7 +17,7 @@ use foundry_compilers::artifacts::EvmVersion; use foundry_config::{ Config, figment::{ - self, Figment, Metadata, Profile, + self, Metadata, Profile, value::{Dict, Map}, }, }; @@ -114,10 +114,15 @@ impl RunArgs { /// /// Note: This executes the transaction(s) as is: Cheatcodes are disabled pub async fn run(self) -> Result<()> { - let figment = Into::::into(&self.rpc).merge(&self); + let figment = self.rpc.clone().into_figment(self.with_local_artifacts).merge(&self); let evm_opts = figment.extract::()?; let mut config = Config::from_provider(figment)?.sanitized(); + let label = self.label; + let with_local_artifacts = self.with_local_artifacts; + let debug = self.debug; + let decode_internal = self.decode_internal; + let disable_labels = self.disable_labels; let compute_units_per_second = if self.no_rate_limit { Some(u64::MAX) } else { self.compute_units_per_second }; @@ -291,11 +296,11 @@ impl RunArgs { &config, chain, &contracts_bytecode, - self.label, - self.with_local_artifacts, - self.debug, - self.decode_internal, - self.disable_labels, + label, + with_local_artifacts, + debug, + decode_internal, + disable_labels, ) .await?; diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 82c6bfa614d0f..4b0df9b59b120 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -1603,6 +1603,73 @@ casttest!(mktx_raw_unsigned, |_prj, cmd| { ]]); }); +casttest!(mktx_raw_unsigned_no_from_missing_chain, async |_prj, cmd| { + // As chain is not provided, a query is made to the provider to get the chain id, before the tx + // is built. Anvil is configured to use chain id 1 so that the produced tx will be the same + // as in the `mktx_raw_unsigned` test. + let (_, handle) = anvil::spawn(NodeConfig::test().with_chain_id(Some(1u64))).await; + cmd.args([ + "mktx", + "--nonce", + "0", + "--gas-limit", + "21000", + "--gas-price", + "10000000000", + "--priority-gas-price", + "1000000000", + "0x0000000000000000000000000000000000000001", + "--raw-unsigned", + "--rpc-url", + &handle.http_endpoint(), + ]) + .assert_success() + .stdout_eq(str![[ + r#"0x02e80180843b9aca008502540be4008252089400000000000000000000000000000000000000018080c0 + +"# + ]]); +}); + +casttest!(mktx_raw_unsigned_no_from_missing_gas_pricing, async |_prj, cmd| { + let (_, handle) = anvil::spawn(NodeConfig::test()).await; + cmd.args([ + "mktx", + "--nonce", + "0", + "0x0000000000000000000000000000000000000001", + "--raw-unsigned", + "--rpc-url", + &handle.http_endpoint(), + ]) + .assert_success() + .stdout_eq(str![[ + r#"0x02e5827a69800184773594018252089400000000000000000000000000000000000000018080c0 + +"# + ]]); +}); + +casttest!(mktx_raw_unsigned_no_from_missing_nonce, |_prj, cmd| { + cmd.args([ + "mktx", + "--chain", + "1", + "--gas-limit", + "21000", + "--gas-price", + "20000000000", + "0x742d35Cc6634C0532925a3b8D6Ac6F67C9c2b7FD", + "--raw-unsigned", + ]) + .assert_failure() + .stderr_eq(str![[ + r#"Error: Missing required parameters for raw unsigned transaction. When --from is not provided, you must specify: --nonce + +"# + ]]); +}); + casttest!(mktx_ethsign, async |_prj, cmd| { let (_api, handle) = anvil::spawn(NodeConfig::test()).await; let rpc = handle.http_endpoint(); diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index a3822d55c6ff4..80740acf857c7 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -4177,7 +4177,7 @@ { "func": { "id": "deriveKey_0", - "description": "Derive a private key from a provided mnenomic string (or mnenomic file path)\nat the derivation path `m/44'/60'/0'/0/{index}`.", + "description": "Derive a private key from a provided mnemonic string (or mnemonic file path)\nat the derivation path `m/44'/60'/0'/0/{index}`.", "declaration": "function deriveKey(string calldata mnemonic, uint32 index) external pure returns (uint256 privateKey);", "visibility": "external", "mutability": "pure", @@ -4197,7 +4197,7 @@ { "func": { "id": "deriveKey_1", - "description": "Derive a private key from a provided mnenomic string (or mnenomic file path)\nat `{derivationPath}{index}`.", + "description": "Derive a private key from a provided mnemonic string (or mnemonic file path)\nat `{derivationPath}{index}`.", "declaration": "function deriveKey(string calldata mnemonic, string calldata derivationPath, uint32 index) external pure returns (uint256 privateKey);", "visibility": "external", "mutability": "pure", @@ -4217,7 +4217,7 @@ { "func": { "id": "deriveKey_2", - "description": "Derive a private key from a provided mnenomic string (or mnenomic file path) in the specified language\nat the derivation path `m/44'/60'/0'/0/{index}`.", + "description": "Derive a private key from a provided mnemonic string (or mnemonic file path) in the specified language\nat the derivation path `m/44'/60'/0'/0/{index}`.", "declaration": "function deriveKey(string calldata mnemonic, uint32 index, string calldata language) external pure returns (uint256 privateKey);", "visibility": "external", "mutability": "pure", @@ -4237,7 +4237,7 @@ { "func": { "id": "deriveKey_3", - "description": "Derive a private key from a provided mnenomic string (or mnenomic file path) in the specified language\nat `{derivationPath}{index}`.", + "description": "Derive a private key from a provided mnemonic string (or mnemonic file path) in the specified language\nat `{derivationPath}{index}`.", "declaration": "function deriveKey(string calldata mnemonic, string calldata derivationPath, uint32 index, string calldata language) external pure returns (uint256 privateKey);", "visibility": "external", "mutability": "pure", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 009c63f6c5059..2aac5d22c7e61 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -2728,25 +2728,25 @@ interface Vm { #[cheatcode(group = Crypto)] function publicKeyP256(uint256 privateKey) external pure returns (uint256 publicKeyX, uint256 publicKeyY); - /// Derive a private key from a provided mnenomic string (or mnenomic file path) + /// Derive a private key from a provided mnemonic string (or mnemonic file path) /// at the derivation path `m/44'/60'/0'/0/{index}`. #[cheatcode(group = Crypto)] function deriveKey(string calldata mnemonic, uint32 index) external pure returns (uint256 privateKey); - /// Derive a private key from a provided mnenomic string (or mnenomic file path) + /// Derive a private key from a provided mnemonic string (or mnemonic file path) /// at `{derivationPath}{index}`. #[cheatcode(group = Crypto)] function deriveKey(string calldata mnemonic, string calldata derivationPath, uint32 index) external pure returns (uint256 privateKey); - /// Derive a private key from a provided mnenomic string (or mnenomic file path) in the specified language + /// Derive a private key from a provided mnemonic string (or mnemonic file path) in the specified language /// at the derivation path `m/44'/60'/0'/0/{index}`. #[cheatcode(group = Crypto)] function deriveKey(string calldata mnemonic, uint32 index, string calldata language) external pure returns (uint256 privateKey); - /// Derive a private key from a provided mnenomic string (or mnenomic file path) in the specified language + /// Derive a private key from a provided mnemonic string (or mnemonic file path) in the specified language /// at `{derivationPath}{index}`. #[cheatcode(group = Crypto)] function deriveKey(string calldata mnemonic, string calldata derivationPath, uint32 index, string calldata language) diff --git a/crates/cli/src/opts/rpc.rs b/crates/cli/src/opts/rpc.rs index f9369385859f6..e8b2f03d87437 100644 --- a/crates/cli/src/opts/rpc.rs +++ b/crates/cli/src/opts/rpc.rs @@ -4,12 +4,12 @@ use clap::Parser; use eyre::Result; use foundry_block_explorers::EtherscanApiVersion; use foundry_config::{ - Chain, Config, + Chain, Config, FigmentProviders, figment::{ - self, Metadata, Profile, + self, Figment, Metadata, Profile, value::{Dict, Map}, }, - impl_figment_convert_cast, + find_project_root, impl_figment_convert_cast, }; use foundry_wallets::WalletOpts; use serde::Serialize; @@ -116,6 +116,13 @@ impl RpcOpts { } dict } + + pub fn into_figment(self, all: bool) -> Figment { + let root = find_project_root(None).expect("could not determine project root"); + Config::with_root(&root) + .to_figment(if all { FigmentProviders::All } else { FigmentProviders::Cast }) + .merge(self) + } } #[derive(Clone, Debug, Default, Serialize, Parser)] diff --git a/crates/config/src/etherscan.rs b/crates/config/src/etherscan.rs index d8aee66eb2bc2..4c75ffc12cfad 100644 --- a/crates/config/src/etherscan.rs +++ b/crates/config/src/etherscan.rs @@ -53,7 +53,11 @@ pub enum EtherscanConfigError { #[error(transparent)] Unresolved(#[from] UnresolvedEnvVarError), - #[error("No known Etherscan API URL for config{0} with chain `{1}`. Please specify a `url`")] + #[error( + "No known Etherscan API URL for chain `{1}`. To fix this, please:\n\ + 1. Specify a `url` {0}\n\ + 2. Verify the chain `{1}` is correct" + )] UnknownChain(String, Chain), #[error("At least one of `url` or `chain` must be present{0}")] @@ -233,7 +237,7 @@ impl EtherscanConfig { }), (Some(chain), None) => ResolvedEtherscanConfig::create(key, chain, api_version) .ok_or_else(|| { - let msg = alias.map(|a| format!(" `{a}`")).unwrap_or_default(); + let msg = alias.map(|a| format!("for `{a}`")).unwrap_or_default(); EtherscanConfigError::UnknownChain(msg, chain) }), (None, Some(api_url)) => Ok(ResolvedEtherscanConfig { diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 9d265867a11d3..2aa4cc7b61a41 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1429,13 +1429,20 @@ impl Config { // etherscan fallback via API key if let Some(key) = self.etherscan_api_key.as_ref() { - return Ok(ResolvedEtherscanConfig::create( + match ResolvedEtherscanConfig::create( key, chain.or(self.chain).unwrap_or_default(), default_api_version, - )); + ) { + Some(config) => return Ok(Some(config)), + None => { + return Err(EtherscanConfigError::UnknownChain( + String::new(), + chain.unwrap_or_default(), + )); + } + } } - Ok(None) } @@ -5081,4 +5088,45 @@ mod tests { .unwrap(); assert_eq!(endpoint.url, "https://rpc.sophon.xyz"); } + + #[test] + fn test_get_etherscan_config_with_unknown_chain() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [etherscan] + mainnet = { chain = 3658348, key = "api-key"} + "#, + )?; + let config = Config::load().unwrap(); + let unknown_chain = Chain::from_id(3658348); + let result = config.get_etherscan_config_with_chain(Some(unknown_chain)); + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!(error_msg.contains("No known Etherscan API URL for chain `3658348`")); + assert!(error_msg.contains("Specify a `url`")); + assert!(error_msg.contains("Verify the chain `3658348` is correct")); + + Ok(()) + }); + } + + #[test] + fn test_get_etherscan_config_with_existing_chain_and_url() { + figment::Jail::expect_with(|jail| { + jail.create_file( + "foundry.toml", + r#" + [etherscan] + mainnet = { chain = 1, key = "api-key" } + "#, + )?; + let config = Config::load().unwrap(); + let unknown_chain = Chain::from_id(1); + let result = config.get_etherscan_config_with_chain(Some(unknown_chain)); + assert!(result.is_ok()); + Ok(()) + }); + } } diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index abe2448523c6e..fce8fbde30bed 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -1133,6 +1133,8 @@ impl DatabaseExt for Backend { // selected. This ensures that there are no gaps in depth which would // otherwise cause issues with the tracer fork.journaled_state.depth = active_journaled_state.depth; + // Set proper journal of state changes into the fork. + fork.journaled_state.journal = active_journaled_state.journal.clone(); // another edge case where a fork is created and selected during setup with not // necessarily the same caller as for the test, however we must always @@ -1200,6 +1202,8 @@ impl DatabaseExt for Backend { active.journaled_state = self.fork_init_journaled_state.clone(); active.journaled_state.depth = journaled_state.depth; + // Set proper journal of state changes into the fork. + active.journaled_state.journal = journaled_state.journal.clone(); for addr in persistent_addrs { merge_journaled_state_data(addr, journaled_state, &mut active.journaled_state); } diff --git a/crates/evm/coverage/src/analysis.rs b/crates/evm/coverage/src/analysis.rs index 4709a49cfbacd..926913f58cc1a 100644 --- a/crates/evm/coverage/src/analysis.rs +++ b/crates/evm/coverage/src/analysis.rs @@ -10,7 +10,7 @@ use std::sync::Arc; /// A visitor that walks the AST of a single contract and finds coverage items. #[derive(Clone, Debug)] -pub struct ContractVisitor<'a> { +struct ContractVisitor<'a> { /// The source ID of the contract. source_id: u32, /// The source code that contains the AST being walked. @@ -25,11 +25,11 @@ pub struct ContractVisitor<'a> { last_line: u32, /// Coverage items - pub items: Vec, + items: Vec, } impl<'a> ContractVisitor<'a> { - pub fn new(source_id: usize, source: &'a str, contract_name: &'a Arc) -> Self { + fn new(source_id: usize, source: &'a str, contract_name: &'a Arc) -> Self { Self { source_id: source_id.try_into().expect("too many sources"), source, @@ -40,7 +40,46 @@ impl<'a> ContractVisitor<'a> { } } - pub fn visit_contract(&mut self, node: &Node) -> eyre::Result<()> { + /// Filter out all items if the contract has any test functions. + fn clear_if_test(&mut self) { + let has_tests = self.items.iter().any(|item| { + if let CoverageItemKind::Function { name } = &item.kind { + name.is_any_test() + } else { + false + } + }); + if has_tests { + self.items = Vec::new(); + } + } + + /// Disambiguate functions with the same name in the same contract. + fn disambiguate_functions(&mut self) { + if self.items.is_empty() { + return; + } + + let mut dups = HashMap::<_, Vec>::default(); + for (i, item) in self.items.iter().enumerate() { + if let CoverageItemKind::Function { name } = &item.kind { + dups.entry(name.clone()).or_default().push(i); + } + } + for dups in dups.values() { + if dups.len() > 1 { + for (i, &dup) in dups.iter().enumerate() { + let item = &mut self.items[dup]; + if let CoverageItemKind::Function { name } = &item.kind { + item.kind = + CoverageItemKind::Function { name: format!("{name}.{i}").into() }; + } + } + } + } + } + + fn visit_contract(&mut self, node: &Node) -> eyre::Result<()> { // Find all functions and walk their AST for node in &node.nodes { match node.node_type { @@ -59,14 +98,14 @@ impl<'a> ContractVisitor<'a> { fn visit_function_definition(&mut self, node: &Node) -> eyre::Result<()> { let Some(body) = &node.body else { return Ok(()) }; - let name: String = + let name: Box = node.attribute("name").ok_or_else(|| eyre::eyre!("Function has no name"))?; - let kind: String = + let kind: Box = node.attribute("kind").ok_or_else(|| eyre::eyre!("Function has no kind"))?; // TODO: We currently can only detect empty bodies in normal functions, not any of the other // kinds: https://github.com/foundry-rs/foundry/issues/9458 - if kind != "function" && !has_statements(body) { + if &*kind != "function" && !has_statements(body) { return Ok(()); } @@ -79,16 +118,12 @@ impl<'a> ContractVisitor<'a> { } fn visit_modifier_or_yul_fn_definition(&mut self, node: &Node) -> eyre::Result<()> { - let name: String = - node.attribute("name").ok_or_else(|| eyre::eyre!("Modifier has no name"))?; + let Some(body) = &node.body else { return Ok(()) }; - match &node.body { - Some(body) => { - self.push_item_kind(CoverageItemKind::Function { name }, &node.src); - self.visit_block(body) - } - _ => Ok(()), - } + let name: Box = + node.attribute("name").ok_or_else(|| eyre::eyre!("Modifier has no name"))?; + self.push_item_kind(CoverageItemKind::Function { name }, &node.src); + self.visit_block(body) } fn visit_block(&mut self, node: &Node) -> eyre::Result<()> { @@ -571,6 +606,7 @@ impl SourceAnalysis { /// Note: Source IDs are only unique per compilation job; that is, a code base compiled with /// two different solc versions will produce overlapping source IDs if the compiler version is /// not taken into account. + #[instrument(name = "SourceAnalysis::new", skip_all)] pub fn new(data: &SourceFiles<'_>) -> eyre::Result { let mut sourced_items = data .sources @@ -592,23 +628,12 @@ impl SourceAnalysis { let name = node .attribute("name") .ok_or_else(|| eyre::eyre!("Contract has no name"))?; - + let _guard = debug_span!("visit_contract", %name).entered(); let mut visitor = ContractVisitor::new(source_id, &source.content, &name); visitor.visit_contract(node)?; - let mut items = visitor.items; - - let is_test = items.iter().any(|item| { - if let CoverageItemKind::Function { name } = &item.kind { - name.is_any_test() - } else { - false - } - }); - if is_test { - items.clear(); - } - - Ok(items) + visitor.clear_if_test(); + visitor.disambiguate_functions(); + Ok(visitor.items) }); items.map(move |items| items.map(|items| (source_id, items))) }) diff --git a/crates/evm/coverage/src/lib.rs b/crates/evm/coverage/src/lib.rs index 8e6f889f309cd..dfcea24549095 100644 --- a/crates/evm/coverage/src/lib.rs +++ b/crates/evm/coverage/src/lib.rs @@ -326,7 +326,7 @@ pub enum CoverageItemKind { /// A function in the code. Function { /// The name of the function. - name: String, + name: Box, }, } diff --git a/crates/forge/tests/cli/build.rs b/crates/forge/tests/cli/build.rs index c0dd0517551d8..76783ca7301c8 100644 --- a/crates/forge/tests/cli/build.rs +++ b/crates/forge/tests/cli/build.rs @@ -329,3 +329,46 @@ contract ValidContract {} cmd.args(["build"]).assert_success(); }); + +// +forgetest_init!(test_consistent_build_output, |prj, cmd| { + prj.add_source( + "AContract.sol", + r#" +import {B} from "/badpath/B.sol"; + +contract A is B {} + "#, + ) + .unwrap(); + + prj.add_source( + "CContract.sol", + r#" +import {B} from "badpath/B.sol"; + +contract C is B {} + "#, + ) + .unwrap(); + + cmd.args(["build", "src/AContract.sol"]).assert_failure().stdout_eq(str![[r#" +... +Unable to resolve imports: + "/badpath/B.sol" in "[..]" +with remappings: + forge-std/=[..] +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] + +"#]]); + cmd.forge_fuse().args(["build", "src/CContract.sol"]).assert_failure().stdout_eq(str![[r#" +Unable to resolve imports: + "badpath/B.sol" in "[..]" +with remappings: + forge-std/=[..] +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] + +"#]]); +}); diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index fcf10023cdf6e..0be5e77956fc5 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -2677,7 +2677,7 @@ contract GasReportFallbackTest is Test { +========================================================================================================+ | Deployment Cost | Deployment Size | | | | | |---------------------------------------------------+-----------------+-------+--------+-------+---------| -| 117171 | 471 | | | | | +| 117159 | 471 | | | | | |---------------------------------------------------+-----------------+-------+--------+-------+---------| | | | | | | | |---------------------------------------------------+-----------------+-------+--------+-------+---------| @@ -2716,7 +2716,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) { "contract": "test/DelegateProxyTest.sol:DelegateProxy", "deployment": { - "gas": 117171, + "gas": 117159, "size": 471 }, "functions": { @@ -2928,7 +2928,7 @@ contract NestedDeploy is Test { +============================================================================================+ | Deployment Cost | Deployment Size | | | | | |-------------------------------------------+-----------------+-----+--------+-----+---------| -| 328961 | 1163 | | | | | +| 328949 | 1163 | | | | | |-------------------------------------------+-----------------+-----+--------+-----+---------| | | | | | | | |-------------------------------------------+-----------------+-----+--------+-----+---------| @@ -2983,7 +2983,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) { "contract": "test/NestedDeployTest.sol:Parent", "deployment": { - "gas": 328961, + "gas": 328949, "size": 1163 }, "functions": { @@ -3670,7 +3670,7 @@ forgetest_init!(gas_report_include_tests, |prj, cmd| { +================================================================================================+ | Deployment Cost | Deployment Size | | | | | |-----------------------------------------+-----------------+--------+--------+--------+---------| -| 1545498 | 7578 | | | | | +| 1544498 | 7573 | | | | | |-----------------------------------------+-----------------+--------+--------+--------+---------| | | | | | | | |-----------------------------------------+-----------------+--------+--------+--------+---------| @@ -3678,7 +3678,7 @@ forgetest_init!(gas_report_include_tests, |prj, cmd| { |-----------------------------------------+-----------------+--------+--------+--------+---------| | setUp | 218902 | 218902 | 218902 | 218902 | 1 | |-----------------------------------------+-----------------+--------+--------+--------+---------| -| test_Increment | 54915 | 54915 | 54915 | 54915 | 1 | +| test_Increment | 51847 | 51847 | 51847 | 51847 | 1 | ╰-----------------------------------------+-----------------+--------+--------+--------+---------╯ @@ -3725,8 +3725,8 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) { "contract": "test/Counter.t.sol:CounterTest", "deployment": { - "gas": 1545498, - "size": 7578 + "gas": 1544498, + "size": 7573 }, "functions": { "setUp()": { @@ -3738,10 +3738,10 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) }, "test_Increment()": { "calls": 1, - "min": 54915, - "mean": 54915, - "median": 54915, - "max": 54915 + "min": 51847, + "mean": 51847, + "median": 51847, + "max": 51847 } } } diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index aa27ba775495c..dd99de3fd281f 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -663,6 +663,8 @@ forgetest_init!(can_prioritise_closer_lib_remappings, |prj, cmd| { // remapping. // See // Test that +// - single file remapping is properly added, see +// and // - project defined `@openzeppelin/contracts` remapping is added // - library defined `@openzeppelin/contracts-upgradeable` remapping is added // - library defined `@openzeppelin/contracts/upgradeable` remapping is not added as it conflicts @@ -672,6 +674,7 @@ forgetest_init!(can_prioritise_project_remappings, |prj, cmd| { let mut config = cmd.config(); // Add `@utils/` remapping in project config. config.remappings = vec![ + Remapping::from_str("@utils/libraries/Contract.sol=src/Contract.sol").unwrap().into(), Remapping::from_str("@utils/=src/").unwrap().into(), Remapping::from_str("@openzeppelin/contracts=lib/openzeppelin-contracts/").unwrap().into(), ]; @@ -699,6 +702,7 @@ forgetest_init!(can_prioritise_project_remappings, |prj, cmd| { cmd.args(["remappings", "--pretty"]).assert_success().stdout_eq(str![[r#" Global: +- @utils/libraries/Contract.sol=src/Contract.sol - @utils/=src/ - @openzeppelin/contracts/=lib/openzeppelin-contracts/ - @openzeppelin/contracts-upgradeable/=lib/dep1/lib/openzeppelin-upgradeable/ diff --git a/crates/forge/tests/cli/coverage.rs b/crates/forge/tests/cli/coverage.rs index d1e6c9f4751d2..c74e0a6f43485 100644 --- a/crates/forge/tests/cli/coverage.rs +++ b/crates/forge/tests/cli/coverage.rs @@ -5,6 +5,11 @@ use foundry_test_utils::{ }; use std::path::Path; +#[track_caller] +fn assert_lcov(cmd: &mut TestCommand, data: impl IntoData) { + cmd.args(["--report=lcov", "--report-file"]).assert_file(data.into_data()); +} + fn basic_base(prj: TestProject, mut cmd: TestCommand) { cmd.args(["coverage", "--report=lcov", "--report=summary"]).assert_success().stdout_eq(str![[ r#" @@ -1911,7 +1916,68 @@ end_of_record ); }); -#[track_caller] -fn assert_lcov(cmd: &mut TestCommand, data: impl IntoData) { - cmd.args(["--report=lcov", "--report-file"]).assert_file(data.into_data()); +// +// Test that overridden functions are disambiguated in the LCOV report. +forgetest!(disambiguate_functions, |prj, cmd| { + prj.insert_ds_test(); + prj.add_source( + "Counter.sol", + r#" +contract Counter { + uint256 public number; + + function increment() public { + number++; + } + function increment(uint256 amount) public { + number += amount; + } +} + "#, + ) + .unwrap(); + + prj.add_source( + "Counter.t.sol", + r#" +import "./test.sol"; +import "./Counter.sol"; + +contract CounterTest is DSTest { + function test_overridden() public { + Counter counter = new Counter(); + counter.increment(); + counter.increment(1); + counter.increment(2); + counter.increment(3); + assertEq(counter.number(), 7); + } } + "#, + ) + .unwrap(); + + assert_lcov( + cmd.arg("coverage"), + str![[r#" +TN: +SF:src/Counter.sol +DA:7,1 +FN:7,Counter.increment.0 +FNDA:1,Counter.increment.0 +DA:8,1 +DA:10,3 +FN:10,Counter.increment.1 +FNDA:3,Counter.increment.1 +DA:11,3 +FNF:2 +FNH:2 +LF:4 +LH:4 +BRF:0 +BRH:0 +end_of_record + +"#]], + ); +}); diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index f11450bcd63f1..112a800cbbf85 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -37,6 +37,17 @@ const OTHER_CONTRACT: &str = r#" } "#; +const ONLY_IMPORTS: &str = r#" + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.0; + + // forge-lint: disable-next-line + import { ContractWithLints } from "./ContractWithLints.sol"; + + import { _PascalCaseInfo } from "./ContractWithLints.sol"; + import "./ContractWithLints.sol"; + "#; + forgetest!(can_use_config, |prj, cmd| { prj.wipe_contracts(); prj.add_source("ContractWithLints", CONTRACT).unwrap(); @@ -359,6 +370,26 @@ forgetest!(can_process_inline_config_regardless_of_input_order, |prj, cmd| { cmd.arg("lint").assert_success(); }); +// +forgetest!(can_use_only_lint_with_multilint_passes, |prj, cmd| { + prj.wipe_contracts(); + prj.add_source("ContractWithLints", CONTRACT).unwrap(); + prj.add_source("OnlyImports", ONLY_IMPORTS).unwrap(); + cmd.arg("lint").args(["--only-lint", "unused-import"]).assert_success().stderr_eq(str![[r#" +note[unused-import]: unused imports should be removed + [FILE]:8:14 + | +8 | import { _PascalCaseInfo } from "./ContractWithLints.sol"; + | --------------- + | + = help: https://book.getfoundry.sh/reference/forge/forge-lint#unused-import + + +"#]]); +}); + +// ------------------------------------------------------------------------------------------------ + #[tokio::test] async fn ensure_lint_rule_docs() { const FOUNDRY_BOOK_LINT_PAGE_URL: &str = diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs index 9db8736d6237f..091f341fe988c 100644 --- a/crates/forge/tests/cli/test_cmd.rs +++ b/crates/forge/tests/cli/test_cmd.rs @@ -2912,15 +2912,13 @@ Traces: │ └─ ← [Stop] └─ ← [Stop] - [31851] CounterTest::test_Increment() + [28783] CounterTest::test_Increment() ├─ [22418] Counter::increment() │ ├─ storage changes: │ │ @ 0: 0 → 1 │ └─ ← [Stop] ├─ [424] Counter::number() [staticcall] │ └─ ← [Return] 1 - ├─ [0] VM::assertEq(1, 1) [staticcall] - │ └─ ← [Return] └─ ← [Stop] Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] @@ -3056,7 +3054,7 @@ Traces: │ └─ ← [Stop] └─ ← [Stop] - [35178] SuppressTracesTest::test_increment_failure() + [35200] SuppressTracesTest::test_increment_failure() ├─ [0] console::log("test increment failure") [staticcall] │ └─ ← [Stop] ├─ [22418] Counter::increment() @@ -3101,7 +3099,7 @@ Traces: │ └─ ← [Stop] └─ ← [Stop] - [35178] SuppressTracesTest::test_increment_failure() + [35200] SuppressTracesTest::test_increment_failure() ├─ [0] console::log("test increment failure") [staticcall] │ └─ ← [Stop] ├─ [22418] Counter::increment() @@ -3117,15 +3115,13 @@ Logs: test increment success Traces: - [35229] SuppressTracesTest::test_increment_success() + [32164] SuppressTracesTest::test_increment_success() ├─ [0] console::log("test increment success") [staticcall] │ └─ ← [Stop] ├─ [22418] Counter::increment() │ └─ ← [Stop] ├─ [424] Counter::number() [staticcall] │ └─ ← [Return] 1 - ├─ [0] VM::assertEq(1, 1) [staticcall] - │ └─ ← [Return] └─ ← [Stop] Suite result: FAILED. 1 passed; 1 failed; 0 skipped; [ELAPSED] @@ -3932,8 +3928,6 @@ Traces: │ └─ ← [Stop] ├─ [..] Counter::number() [staticcall] │ └─ ← [Return] 1 - ├─ [0] VM::assertEq(1, 1) [staticcall] - │ └─ ← [Return] ├─ storage changes: │ @ 31: 0x00000000000000000000006cdbd1b486b8fbd4140e8cd6daaed05be13ed91401 → 0x0000000000000000000000c4b957cd61beb9b9afd76204b30683edaaab51ec01 └─ ← [Stop] @@ -3944,3 +3938,88 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) "#]]); }); + +// tests proper reverts in fork mode for contracts with non-existent linked libraries. +// +forgetest_init!(can_fork_test_with_non_existent_linked_library, |prj, cmd| { + prj.update_config(|config| { + config.libraries = + vec!["src/Counter.sol:LibCounter:0x530008d2b058137d9c475b1b7d83984f1fcf1dd0".into()]; + }); + prj.add_source( + "Counter.sol", + r" +library LibCounter { + function dummy() external pure returns (uint) { + return 1; + } +} + +contract Counter { + uint256 public number; + + constructor() { + LibCounter.dummy(); + } + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } + + function dummy() external pure returns (uint) { + return LibCounter.dummy(); + } +} + ", + ) + .unwrap(); + + let endpoint = rpc::next_http_archive_rpc_url(); + + prj.add_test( + "Counter.t.sol", + &r#" +import "forge-std/Test.sol"; +import "src/Counter.sol"; + +contract CounterTest is Test { + function test_select_fork() public { + vm.createSelectFork(""); + new Counter(); + } + + function test_roll_fork() public { + vm.rollFork(block.number - 100); + new Counter(); + } +} + "# + .replace("", &endpoint), + ) + .unwrap(); + + cmd.args(["test", "--fork-url", &endpoint]).assert_failure().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 2 tests for test/Counter.t.sol:CounterTest +[FAIL: EvmError: Revert] test_roll_fork() ([GAS]) +[FAIL: Contract 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f does not exist and is not marked as persistent, see `vm.makePersistent()`] test_select_fork() ([GAS]) +Suite result: FAILED. 0 passed; 2 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 0 tests passed, 2 failed, 0 skipped (2 total tests) + +Failing tests: +Encountered 2 failing tests in test/Counter.t.sol:CounterTest +[FAIL: EvmError: Revert] test_roll_fork() ([GAS]) +[FAIL: Contract 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f does not exist and is not marked as persistent, see `vm.makePersistent()`] test_select_fork() ([GAS]) + +Encountered a total of 2 failing tests, 0 tests succeeded + +"#]]); +}); diff --git a/crates/forge/tests/cli/test_optimizer.rs b/crates/forge/tests/cli/test_optimizer.rs index de0f57022a96b..c1987a033a481 100644 --- a/crates/forge/tests/cli/test_optimizer.rs +++ b/crates/forge/tests/cli/test_optimizer.rs @@ -1401,8 +1401,6 @@ Traces: ├─ [..] Counter::number() [staticcall] │ └─ ← [Return] 1 ├─ [..] StdAssertions::assertEq(1, 1) - │ ├─ [0] VM::assertEq(1, 1) [staticcall] - │ │ └─ ← [Return] │ └─ ← └─ ← [Stop] diff --git a/crates/forge/tests/fixtures/colored_traces.svg b/crates/forge/tests/fixtures/colored_traces.svg index e4181676bcc86..af49528caa136 100644 --- a/crates/forge/tests/fixtures/colored_traces.svg +++ b/crates/forge/tests/fixtures/colored_traces.svg @@ -1,8 +1,7 @@ - +