diff --git a/crates/forge/tests/cli/verify_bytecode.rs b/crates/forge/tests/cli/verify_bytecode.rs index 398ecb52d353..14b2e6b7110b 100644 --- a/crates/forge/tests/cli/verify_bytecode.rs +++ b/crates/forge/tests/cli/verify_bytecode.rs @@ -72,6 +72,7 @@ fn test_verify_bytecode_with_ignore( verifier_url: &str, expected_matches: (&str, &str), ignore: &str, + ignore_immutables: bool, chain: &str, ) { let etherscan_key = next_mainnet_etherscan_api_key(); @@ -96,26 +97,27 @@ fn test_verify_bytecode_with_ignore( prj.add_source(contract_name, &source_code).unwrap(); prj.write_config(config); - let output = cmd - .forge_fuse() - .args([ - "verify-bytecode", - addr, - contract_name, - "--etherscan-api-key", - ðerscan_key, - "--verifier", - verifier, - "--verifier-url", - verifier_url, - "--rpc-url", - &rpc_url, - "--ignore", - ignore, - ]) - .assert_success() - .get_output() - .stdout_lossy(); + let mut args = vec![ + "verify-bytecode", + addr, + contract_name, + "--etherscan-api-key", + ðerscan_key, + "--verifier", + verifier, + "--verifier-url", + verifier_url, + "--rpc-url", + &rpc_url, + "--ignore", + ignore, + ]; + + if ignore_immutables { + args.push("--ignore-predeploy-immutables"); + } + + let output = cmd.forge_fuse().args(args).assert_success().get_output().stdout_lossy(); if ignore == "creation" { assert!(!output.contains( @@ -131,6 +133,9 @@ fn test_verify_bytecode_with_ignore( assert!(!output .contains(format!("Runtime code matched with status {}", expected_matches.1).as_str())); } else { + if ignore_immutables { + assert!(output.contains("Ignoring immutable references")); + } assert!(output .contains(format!("Runtime code matched with status {}", expected_matches.1).as_str())); } @@ -261,6 +266,7 @@ forgetest_async!(can_ignore_creation, |prj, cmd| { "https://api.etherscan.io/api", ("ignored", "partial"), "creation", + false, "1", ); }); @@ -283,31 +289,7 @@ forgetest_async!(can_ignore_runtime, |prj, cmd| { "https://api.etherscan.io/api", ("partial", "ignored"), "runtime", + false, "1", ); }); - -// Test predeploy contracts -// TODO: Add test utils for base such as basescan keys and alchemy keys. -// WETH9 Predeploy -// forgetest_async!(can_verify_predeploys, |prj, cmd| { -// test_verify_bytecode_with_ignore( -// prj, -// cmd, -// "0x4200000000000000000000000000000000000006", -// "WETH9", -// Config { -// evm_version: EvmVersion::default(), -// optimizer: true, -// optimizer_runs: 10000, -// cbor_metadata: true, -// bytecode_hash: BytecodeHash::Bzzr1, -// ..Default::default() -// }, -// "etherscan", -// "https://api.basescan.org/api", -// ("ignored", "partial"), -// "creation", -// "base", -// ); -// }); diff --git a/crates/verify/src/bytecode.rs b/crates/verify/src/bytecode.rs index 661eb5c8dcb3..0a15eec24b2d 100644 --- a/crates/verify/src/bytecode.rs +++ b/crates/verify/src/bytecode.rs @@ -89,6 +89,11 @@ pub struct VerifyBytecodeArgs { /// Ignore verification for creation or runtime bytecode. #[clap(long, value_name = "BYTECODE_TYPE")] pub ignore: Option, + + /// Ignore immutable references while verifying runtime bytecode for predeployed contracts. + /// Use this to avoid passing `--constructor-args`. + #[clap(long, default_value = "false")] + pub ignore_predeploy_immutables: bool, } impl figment::Provider for VerifyBytecodeArgs { @@ -138,6 +143,14 @@ impl VerifyBytecodeArgs { &config, )?; + // ignore flag setup + let ignore_predeploy_immutables = self.ignore_predeploy_immutables; + let mut ignore = self.ignore; + if ignore_predeploy_immutables && self.ignore.is_none() { + ignore = Some(BytecodeType::Creation); + } + + trace!(?ignore_predeploy_immutables); // Get the bytecode at the address, bailing if it doesn't exist. let code = provider.get_code_at(self.address).await?; if code.is_empty() { @@ -209,6 +222,8 @@ impl VerifyBytecodeArgs { check_explorer_args(source_code.clone())? }; + trace!(provided_constructor_args = ?constructor_args); + // This fails only when the contract expects constructor args but NONE were provided OR // retrieved from explorer (in case of predeploys). crate::utils::check_args_len(&artifact, &constructor_args)?; @@ -220,7 +235,10 @@ impl VerifyBytecodeArgs { format!("Attempting to verify predeployed contract at {:?}. Ignoring creation code verification.", self.address) .yellow() .bold() - ) + ); + if ignore_predeploy_immutables { + println!("{}", "Ignoring immutable references for predeploys.".yellow().bold()); + } } // Append constructor args to the local_bytecode. @@ -272,17 +290,38 @@ impl VerifyBytecodeArgs { crate::utils::deploy_contract(&mut executor, &env, config.evm_spec_id(), &gen_tx)?; // Compare runtime bytecode - let (deployed_bytecode, onchain_runtime_code) = crate::utils::get_runtime_codes( - &mut executor, - &provider, - self.address, - fork_address, - None, - ) - .await?; + let (mut deployed_bytecode, mut onchain_runtime_code) = + crate::utils::get_runtime_codes( + &mut executor, + &provider, + self.address, + fork_address, + None, + ) + .await?; + + if ignore_predeploy_immutables { + // Locate immutable refs using the offsets in the artifact + let immutable_refs = crate::utils::get_immutable_refs(&artifact); + + trace!(immutable_refs_found = immutable_refs.is_some()); + + if let Some(refs) = immutable_refs { + // TODO: Extract those sections from both `deployed_bytecode` and + // `onchain_runtime_code`. + + trace!("extracting refs from deployed bytecode"); + deployed_bytecode = + crate::utils::extract_immutables_refs(refs.clone(), deployed_bytecode); + + trace!("extracting refs from onchain runtime code"); + onchain_runtime_code = + crate::utils::extract_immutables_refs(refs, onchain_runtime_code); + } + } let match_type = crate::utils::match_bytecodes( - &deployed_bytecode.original_bytes(), + &deployed_bytecode, &onchain_runtime_code, &constructor_args, true, @@ -365,7 +404,7 @@ impl VerifyBytecodeArgs { trace!(ignore = ?self.ignore); // Check if `--ignore` is set to `creation`. - if !self.ignore.is_some_and(|b| b.is_creation()) { + if !ignore.is_some_and(|b| b.is_creation()) { // Compare creation code with locally built bytecode and `maybe_creation_code`. let match_type = crate::utils::match_bytecodes( local_bytecode_vec.as_slice(), @@ -401,7 +440,7 @@ impl VerifyBytecodeArgs { } } - if !self.ignore.is_some_and(|b| b.is_runtime()) { + if !ignore.is_some_and(|b| b.is_runtime()) { // Get contract creation block. let simulation_block = match self.block { Some(BlockId::Number(BlockNumberOrTag::Number(block))) => block, @@ -468,7 +507,7 @@ impl VerifyBytecodeArgs { &transaction, )?; - // State committed using deploy_with_env, now get the runtime bytecode from the db. + // State commited using deploy_with_env, now get the runtime bytecode from the db. let (fork_runtime_code, onchain_runtime_code) = crate::utils::get_runtime_codes( &mut executor, &provider, @@ -480,7 +519,7 @@ impl VerifyBytecodeArgs { // Compare the onchain runtime bytecode with the runtime code from the fork. let match_type = crate::utils::match_bytecodes( - &fork_runtime_code.original_bytes(), + &fork_runtime_code, &onchain_runtime_code, &constructor_args, true, diff --git a/crates/verify/src/utils.rs b/crates/verify/src/utils.rs index aaf8a0e016b7..e65ee1e7ddbb 100644 --- a/crates/verify/src/utils.rs +++ b/crates/verify/src/utils.rs @@ -10,17 +10,18 @@ use foundry_block_explorers::{ errors::EtherscanError, }; use foundry_common::{abi::encode_args, compile::ProjectCompiler, provider::RetryProvider}; -use foundry_compilers::artifacts::{BytecodeHash, CompactContractBytecode, EvmVersion}; +use foundry_compilers::artifacts::{BytecodeHash, CompactContractBytecode, EvmVersion, Offsets}; use foundry_config::Config; use foundry_evm::{constants::DEFAULT_CREATE2_DEPLOYER, executors::TracingExecutor, opts::EvmOpts}; use reqwest::Url; use revm_primitives::{ db::Database, env::{EnvWithHandlerCfg, HandlerCfg}, - Bytecode, Env, SpecId, + Env, SpecId, }; use semver::Version; use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; use yansi::Paint; /// Enum to represent the type of bytecode being verified @@ -136,6 +137,56 @@ pub fn build_using_cache( eyre::bail!("couldn't find cached artifact for contract {}", args.contract.name) } +pub fn get_immutable_refs( + artifact: &CompactContractBytecode, +) -> Option>> { + if artifact.deployed_bytecode.as_ref().is_some_and(|b| !b.immutable_references.is_empty()) { + let immutable_refs = + artifact.deployed_bytecode.as_ref().unwrap().immutable_references.clone(); + + return Some(immutable_refs); + } + + None +} + +pub fn extract_immutables_refs( + immutable_refs: BTreeMap>, + mut bytecode: Bytes, +) -> Bytes { + let mut total_len_extracted_expected = 0; + let init_length = bytecode.len(); + for (_key, offsets) in immutable_refs { + for offset in offsets { + let start = offset.start as usize; + let end = (offset.start + offset.length) as usize; + + total_len_extracted_expected += offset.length; + + // Remove this sections of bytes from the bytecode + let start_section = bytecode.slice(0..start); + let end_section = bytecode.slice(end..bytecode.len()); + + // Combine the start and end sections + let mut start_section_vec = start_section.to_vec(); + + start_section_vec.extend_from_slice(&end_section); + + bytecode = Bytes::from(start_section_vec); + } + } + let end_length = bytecode.len(); + + tracing::info!( + "Total length extracted (Expected): {}, Initial length: {}, Final length: {}", + total_len_extracted_expected, + init_length, + end_length + ); + + bytecode +} + pub fn print_result( args: &VerifyBytecodeArgs, res: Option, @@ -390,7 +441,7 @@ pub async fn get_runtime_codes( address: Address, fork_address: Address, block: Option, -) -> Result<(Bytecode, Bytes)> { +) -> Result<(Bytes, Bytes)> { let fork_runtime_code = executor .backend_mut() .basic(fork_address)? @@ -414,7 +465,7 @@ pub async fn get_runtime_codes( provider.get_code_at(address).await? }; - Ok((fork_runtime_code, onchain_runtime_code)) + Ok((fork_runtime_code.original_bytes(), onchain_runtime_code)) } /// Returns `true` if the URL only consists of host.