From f96402d81dd95354869a2638f46844d7ac9a16fd Mon Sep 17 00:00:00 2001 From: soul022 Date: Thu, 26 Jun 2025 20:40:30 +0530 Subject: [PATCH 01/26] wip --- Cargo.lock | 18 +- crates/forge/Cargo.toml | 4 + crates/forge/src/cmd/create.rs | 340 ++++++++++++++++++++++++++++++++- 3 files changed, 352 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 32b3384e7831e..9af27a0c1762b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3918,12 +3918,14 @@ dependencies = [ "foundry-wallets", "futures", "globset", + "hex", "indicatif", "inferno", "itertools 0.14.0", "lazy_static", "mockall", "opener", + "parity-scale-codec", "parking_lot", "paste", "path-slash", @@ -7109,9 +7111,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.7.4" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9fde3d0718baf5bc92f577d652001da0f8d54cd03a7974e118d04fc888dc23d" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" dependencies = [ "arrayvec 0.7.6", "bitvec", @@ -7125,9 +7127,9 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.7.4" +version = "3.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581c837bb6b9541ce7faa9377c20616e4fb7650f6b0f68bc93c827ee504fb7b3" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -10400,9 +10402,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.44.2" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -10963,9 +10965,9 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index bbf4beddb272d..6d399ffe5ff89 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -56,6 +56,7 @@ alloy-rpc-types.workspace = true alloy-serde.workspace = true alloy-signer.workspace = true alloy-transport.workspace = true +alloy-signer-local.workspace = true clap = { version = "4", features = ["derive", "env", "unicode", "wrap_help"] } clap_complete = "4" @@ -84,6 +85,9 @@ watchexec-signals = "4.0" clearscreen = "4.0" evm-disassembler.workspace = true +hex = "0.4.3" +codec = { package = "parity-scale-codec", version = "3.7.5", default-features = false, features = ["derive"] } + # doc server axum = { workspace = true, features = ["ws"] } tower-http = { workspace = true, features = ["fs"] } diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 744c38d71d977..681da6553428a 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -10,6 +10,8 @@ use alloy_serde::WithOtherFields; use alloy_signer::Signer; use alloy_transport::TransportError; use clap::{Parser, ValueHint}; + +use codec::{Compact, Encode}; use eyre::{Context, Result}; use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::{ @@ -33,6 +35,7 @@ use foundry_config::{ merge_impl_figment_convert, Config, }; use serde_json::json; +use std::collections::HashSet; use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; merge_impl_figment_convert!(CreateArgs, build, eth); @@ -100,11 +103,300 @@ pub struct CreateArgs { retry: RetryArgs, } +/// Finds all contracts being initialized in the target contract along with their file paths +/// +/// # Arguments +/// * `target_path` - Path to the contract file being analyzed +/// * `output` - The compilation output containing all contract information +/// +/// # Returns +/// * `Result>` - Vector of (contract_name, file_path) tuples +pub fn find_initialized_contracts_with_paths( + target_path: &PathBuf, + output: &foundry_compilers::ProjectCompileOutput, +) -> Result> { + let mut initialized_contracts = Vec::new(); + + // Read the source file content + let source_content = std::fs::read_to_string(target_path) + .wrap_err_with(|| format!("Failed to read target file: {}", target_path.display()))?; + + // Parse the Solidity source code + let (parse_tree, _diagnostics) = match solang_parser::parse(&source_content, 0) { + Ok((tree, diag)) => (tree, diag), + Err(_diagnostics) => { + // If parsing fails, return empty list + return Ok(vec![]); + } + }; + + // Find all contract names being initialized + let mut initialized_names = HashSet::new(); + find_contract_initializations(&parse_tree.0, &mut initialized_names); + + // Map contract names to their file paths using compilation output + let contracts = &output.output().contracts; + for contract_name in initialized_names { + // Search through all files to find where this contract is defined + for (file_path, file_contracts) in contracts.iter() { + if file_contracts.contains_key(&contract_name) { + initialized_contracts.push((contract_name.clone(), file_path.clone())); + break; + } + } + } + + initialized_contracts.sort_by(|a, b| a.0.cmp(&b.0)); + Ok(initialized_contracts) +} + +/// Recursively searches AST for contract initializations (new ContractName() expressions) +fn find_contract_initializations( + source_unit_parts: &[solang_parser::pt::SourceUnitPart], + initialized_names: &mut HashSet, +) { + use solang_parser::pt::*; + + for part in source_unit_parts.iter() { + match part { + SourceUnitPart::ContractDefinition(contract) => { + // Look for initializations within contract functions + for contract_part in contract.parts.iter() { + match contract_part { + ContractPart::FunctionDefinition(func) => { + if let Some(Statement::Block { statements, .. }) = &func.body { + find_initializations_in_statements(statements, initialized_names); + } + } + ContractPart::VariableDefinition(var_def) => { + // Check for contract initializations in variable definitions + if let Some(expr) = &var_def.initializer { + find_initializations_in_expression(expr, initialized_names); + } + } + _ => {} + } + } + } + _ => {} + } + } +} + +/// Recursively searches statements for contract initializations +fn find_initializations_in_statements( + statements: &[solang_parser::pt::Statement], + initialized_names: &mut HashSet, +) { + use solang_parser::pt::*; + + for stmt in statements.iter() { + match stmt { + Statement::Expression(_, expr) => { + find_initializations_in_expression(expr, initialized_names); + } + Statement::VariableDefinition(_, _var_decl, init_expr) => { + if let Some(expr) = init_expr { + find_initializations_in_expression(expr, initialized_names); + } + } + Statement::Block { statements, .. } => { + find_initializations_in_statements(statements, initialized_names); + } + _ => {} + } + } +} + +/// Searches expressions for contract initializations (new ContractName() calls) +fn find_initializations_in_expression( + expr: &solang_parser::pt::Expression, + initialized_names: &mut HashSet, +) { + use solang_parser::pt::*; + + match expr { + Expression::New(_, type_expr) => { + // Extract contract name from the type expression + let contract_name = extract_contract_name_from_expression(type_expr); + if !contract_name.is_empty() { + initialized_names.insert(contract_name); + } + } + Expression::FunctionCall(_, func_expr, args) => { + // Recursively check function call expressions and arguments + find_initializations_in_expression(func_expr, initialized_names); + for arg in args.iter() { + find_initializations_in_expression(arg, initialized_names); + } + } + Expression::Assign(_, left_expr, right_expr) => { + find_initializations_in_expression(left_expr, initialized_names); + find_initializations_in_expression(right_expr, initialized_names); + } + Expression::MemberAccess(_, base_expr, _member) => { + find_initializations_in_expression(base_expr, initialized_names); + } + _ => {} + } +} + +/// Extracts contract name from an Expression (for new Contract() patterns) +fn extract_contract_name_from_expression(expr: &solang_parser::pt::Expression) -> String { + use solang_parser::pt::*; + + match expr { + Expression::Variable(identifier) => identifier.name.clone(), + Expression::MemberAccess(_, _, identifier) => identifier.name.clone(), + Expression::FunctionCall(_, func_expr, _args) => { + // Handle cases like ContractName() where the contract name is in the function expression + extract_contract_name_from_expression(func_expr) + } + Expression::Type(_, ty) => extract_contract_name_from_type(ty), + _ => String::new(), + } +} + +/// Extracts contract name from a Type +fn extract_contract_name_from_type(ty: &solang_parser::pt::Type) -> String { + use solang_parser::pt::*; + + match ty { + Type::Address + | Type::AddressPayable + | Type::Bool + | Type::String + | Type::Int(_) + | Type::Uint(_) + | Type::Bytes(_) + | Type::DynamicBytes + | Type::Mapping { .. } + | Type::Function { .. } => String::new(), + _ => { + // For custom types (likely contracts), try to extract the identifier + let type_str = format!("{:?}", ty); + + // Look for identifier patterns in the debug output + if let Some(start) = type_str.find("name: \"") { + let start = start + 7; // Skip 'name: "' + if let Some(end) = type_str[start..].find('"') { + let name = &type_str[start..start + end]; + return name.to_string(); + } + } + + // Fallback: look for capitalized identifiers + type_str + .split_whitespace() + .find(|s| s.chars().next().map_or(false, |c| c.is_ascii_uppercase()) && s.len() > 1) + .unwrap_or("") + .trim_matches(',') + .trim_matches(')') + .to_string() + } + } +} + +async fn upload_child_contract_alloy( + rpc_url: Option, + private_key: Option, + encoded_bytes: String, +) -> Result { + use alloy_primitives::{Address, U256}; + use alloy_provider::Provider; + use alloy_rpc_types::TransactionRequest; + use alloy_signer_local::PrivateKeySigner; + use alloy_serde::WithOtherFields; + use foundry_common::provider::ProviderBuilder; + use std::str::FromStr; + + // Use provided RPC URL or fallback to default + let rpc_url = rpc_url.unwrap_or_else(|| "https://testnet-passet-hub-eth-rpc.polkadot.io/".to_string()); + + // Use provided private key or fallback to default + let private_key = private_key.unwrap_or_else(|| { + "5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133".to_string() + }); + + // 1. Create wallet from private key + let wallet = PrivateKeySigner::from_str(&private_key)?; + + // 2. Create provider with wallet using the proper Foundry pattern + let provider = ProviderBuilder::new(&rpc_url) + .build_with_wallet(EthereumWallet::new(wallet))?; + + // 3. Build transaction + let magic_address: Address = "0x6d6f646c70792f70616464720000000000000000".parse()?; + + // Convert hex string to bytes for input + let input_bytes = hex::decode(encoded_bytes.trim_start_matches("0x"))?; + + let tx = TransactionRequest::default() + .to(magic_address) + .input(input_bytes.into()) + .value(U256::from(0u64)); + + // 4. Sign and send transaction + let wrapped_tx = WithOtherFields::new(tx); + let pending_tx = provider.send_transaction(wrapped_tx).await?; + let receipt = pending_tx.get_receipt().await?; + + println!("Transaction sent! Hash: {:?}", receipt.transaction_hash); + Ok(receipt.transaction_hash.to_string()) +} + +async fn upload_child_contract( + rpc_url: Option, + private_key: Option, + code: String +) -> Result { + use std::process::Command; + + // Use provided RPC URL or fallback to default + let rpc_url = + rpc_url.unwrap_or_else(|| "https://testnet-passet-hub-eth-rpc.polkadot.io/".to_string()); + + // Use provided private key or fallback to default + let private_key = private_key.unwrap_or_else(|| { + "5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133".to_string() + }); + + // Build the cast send command + let mut cmd = Command::new("cast"); + cmd.arg("send"); + cmd.arg("0x6d6f646c70792f70616464720000000000000000"); + cmd.arg("--rpc-url").arg(&rpc_url); + cmd.arg("--private-key").arg(&private_key); + cmd.arg(&code); + + println!("Executing command: {:?}", cmd); + + // Execute the command + let output = cmd.output().map_err(|e| eyre::eyre!("Failed to execute cast send: {}", e))?; + + if output.status.success() { + let tx_hash = String::from_utf8(output.stdout) + .map_err(|e| eyre::eyre!("Failed to parse cast output: {}", e))? + .trim() + .to_string(); + println!("Transaction successful: {}", tx_hash); + Ok(tx_hash) + } else { + let error = + String::from_utf8(output.stderr).unwrap_or_else(|_| "Unknown error".to_string()); + println!("Command failed with error: {}", error); + Err(eyre::eyre!("Cast send failed: {}", error)) + } +} + impl CreateArgs { /// Executes the command to create a contract pub async fn run(mut self) -> Result<()> { let mut config = self.load_config()?; - + println!("=== User Inputs ==="); + println!("RPC URL: {:?}", self.eth.rpc.url); + println!("Private Key: {:?}", self.eth.wallet.raw.private_key); + println!("=================="); // Install missing dependencies. if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings { // need to re-configure here to also catch additional remappings @@ -120,7 +412,51 @@ impl CreateArgs { project.find_contract_path(&self.contract.name)? }; - let output = compile::compile_target(&target_path, &project, shell::is_json())?; + let output: foundry_compilers::ProjectCompileOutput = + compile::compile_target(&target_path, &project, shell::is_json())?; + + // Find all contracts being initialized in the target contract along with their paths + let initialized_contracts = find_initialized_contracts_with_paths(&target_path, &output)?; + if !initialized_contracts.is_empty() { + println!("Contracts being initialized in {}:", target_path.display()); + for (contract_name, contract_path) in &initialized_contracts { + println!(" - {} (defined in: {})", contract_name, contract_path.display()); + + // Try to get bytecode information for this contract + if let Ok((_, bin, _)) = + remove_contract(output.clone(), contract_path, contract_name) + { + match &bin.object { + BytecodeObject::Bytecode(bytes) => { + println!(" Bytecode: Available ({} bytes)", bytes.len()); + println!(" Bytecode: 0x{}", hex::encode(bytes)); + let scaled_encoded_bytes = bytes.encode(); + + let storage_deposit_limit = Compact(5378900000u128); + let encoded_storage_deposit_limit = storage_deposit_limit.encode(); + let combined_hex = "0x3c04".to_string() + + &hex::encode(&scaled_encoded_bytes) + + &hex::encode(&encoded_storage_deposit_limit); + println!( + " SCALE Encoded Bytecode: {}", + &combined_hex + ); + // Pass RPC URL and private key to upload_child_contract + let rpc_url = self.eth.rpc.url.clone(); + let private_key = self.eth.wallet.raw.private_key.clone(); + //upload_child_contract(rpc_url, private_key, combined_hex).await; + upload_child_contract(rpc_url, private_key, combined_hex).await?; + } + BytecodeObject::Unlinked(_) => { + println!(" Bytecode: Available (unlinked)"); + } + } + } else { + println!(" Bytecode: Not available or compilation error"); + } + //println!(); + } + } let (abi, bin, id) = remove_contract(output, &target_path, &self.contract.name)?; From c01f6b2865c757df913652980da2244b2eea1385 Mon Sep 17 00:00:00 2001 From: soul022 Date: Sun, 6 Jul 2025 21:00:39 +0530 Subject: [PATCH 02/26] [issue 130] - clean up --- crates/forge/src/cmd/create.rs | 88 +++++----------------------------- 1 file changed, 13 insertions(+), 75 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 681da6553428a..d2d7671cab22d 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -12,7 +12,7 @@ use alloy_transport::TransportError; use clap::{Parser, ValueHint}; use codec::{Compact, Encode}; -use eyre::{Context, Result}; +use eyre::{Context, OptionExt, Result}; use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::{ opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts}, @@ -298,8 +298,8 @@ fn extract_contract_name_from_type(ty: &solang_parser::pt::Type) -> String { } async fn upload_child_contract_alloy( - rpc_url: Option, - private_key: Option, + rpc_url: &str, + private_key: String, encoded_bytes: String, ) -> Result { use alloy_primitives::{Address, U256}; @@ -310,14 +310,6 @@ async fn upload_child_contract_alloy( use foundry_common::provider::ProviderBuilder; use std::str::FromStr; - // Use provided RPC URL or fallback to default - let rpc_url = rpc_url.unwrap_or_else(|| "https://testnet-passet-hub-eth-rpc.polkadot.io/".to_string()); - - // Use provided private key or fallback to default - let private_key = private_key.unwrap_or_else(|| { - "5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133".to_string() - }); - // 1. Create wallet from private key let wallet = PrivateKeySigner::from_str(&private_key)?; @@ -341,62 +333,13 @@ async fn upload_child_contract_alloy( let pending_tx = provider.send_transaction(wrapped_tx).await?; let receipt = pending_tx.get_receipt().await?; - println!("Transaction sent! Hash: {:?}", receipt.transaction_hash); Ok(receipt.transaction_hash.to_string()) } -async fn upload_child_contract( - rpc_url: Option, - private_key: Option, - code: String -) -> Result { - use std::process::Command; - - // Use provided RPC URL or fallback to default - let rpc_url = - rpc_url.unwrap_or_else(|| "https://testnet-passet-hub-eth-rpc.polkadot.io/".to_string()); - - // Use provided private key or fallback to default - let private_key = private_key.unwrap_or_else(|| { - "5fb92d6e98884f76de468fa3f6278f8807c48bebc13595d45af5bdc4da702133".to_string() - }); - - // Build the cast send command - let mut cmd = Command::new("cast"); - cmd.arg("send"); - cmd.arg("0x6d6f646c70792f70616464720000000000000000"); - cmd.arg("--rpc-url").arg(&rpc_url); - cmd.arg("--private-key").arg(&private_key); - cmd.arg(&code); - - println!("Executing command: {:?}", cmd); - - // Execute the command - let output = cmd.output().map_err(|e| eyre::eyre!("Failed to execute cast send: {}", e))?; - - if output.status.success() { - let tx_hash = String::from_utf8(output.stdout) - .map_err(|e| eyre::eyre!("Failed to parse cast output: {}", e))? - .trim() - .to_string(); - println!("Transaction successful: {}", tx_hash); - Ok(tx_hash) - } else { - let error = - String::from_utf8(output.stderr).unwrap_or_else(|_| "Unknown error".to_string()); - println!("Command failed with error: {}", error); - Err(eyre::eyre!("Cast send failed: {}", error)) - } -} - impl CreateArgs { /// Executes the command to create a contract pub async fn run(mut self) -> Result<()> { let mut config = self.load_config()?; - println!("=== User Inputs ==="); - println!("RPC URL: {:?}", self.eth.rpc.url); - println!("Private Key: {:?}", self.eth.wallet.raw.private_key); - println!("=================="); // Install missing dependencies. if install::install_missing_dependencies(&mut config) && config.auto_detect_remappings { // need to re-configure here to also catch additional remappings @@ -428,33 +371,28 @@ impl CreateArgs { { match &bin.object { BytecodeObject::Bytecode(bytes) => { - println!(" Bytecode: Available ({} bytes)", bytes.len()); - println!(" Bytecode: 0x{}", hex::encode(bytes)); let scaled_encoded_bytes = bytes.encode(); - - let storage_deposit_limit = Compact(5378900000u128); + let storage_deposit_limit = Compact(10000000000u128); let encoded_storage_deposit_limit = storage_deposit_limit.encode(); let combined_hex = "0x3c04".to_string() + &hex::encode(&scaled_encoded_bytes) + &hex::encode(&encoded_storage_deposit_limit); - println!( - " SCALE Encoded Bytecode: {}", - &combined_hex - ); + // Pass RPC URL and private key to upload_child_contract - let rpc_url = self.eth.rpc.url.clone(); - let private_key = self.eth.wallet.raw.private_key.clone(); - //upload_child_contract(rpc_url, private_key, combined_hex).await; - upload_child_contract(rpc_url, private_key, combined_hex).await?; + let rpc_url = config.get_rpc_url_or_localhost_http()?; + let private_key = self.eth.wallet.raw.private_key.clone() + .ok_or_eyre("Private key not provided")?; + + let tx_hash =upload_child_contract_alloy(rpc_url.as_ref(), private_key, combined_hex).await?; + println!("Transaction sent! Hash: {:?} for child contract {:?}", tx_hash, contract_name); } BytecodeObject::Unlinked(_) => { - println!(" Bytecode: Available (unlinked)"); + println!("Bytecode: Available (unlinked)"); } } } else { - println!(" Bytecode: Not available or compilation error"); + println!("Bytecode: Not available or compilation error"); } - //println!(); } } From b6eb1e396a8cb8dbb029ed748054eaf1ef114e5e Mon Sep 17 00:00:00 2001 From: soul022 Date: Sun, 6 Jul 2025 21:39:42 +0530 Subject: [PATCH 03/26] [issue 130] - fix fmt issues --- crates/forge/src/cmd/create.rs | 68 +++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index d2d7671cab22d..265caff488b48 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -35,9 +35,10 @@ use foundry_config::{ merge_impl_figment_convert, Config, }; use serde_json::json; -use std::collections::HashSet; -use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; - +use std::{ + borrow::Borrow, collections::HashSet, marker::PhantomData, path::PathBuf, sync::Arc, + time::Duration, +}; merge_impl_figment_convert!(CreateArgs, build, eth); /// CLI arguments for `forge create`. @@ -249,7 +250,6 @@ fn extract_contract_name_from_expression(expr: &solang_parser::pt::Expression) - Expression::Variable(identifier) => identifier.name.clone(), Expression::MemberAccess(_, _, identifier) => identifier.name.clone(), Expression::FunctionCall(_, func_expr, _args) => { - // Handle cases like ContractName() where the contract name is in the function expression extract_contract_name_from_expression(func_expr) } Expression::Type(_, ty) => extract_contract_name_from_type(ty), @@ -262,16 +262,16 @@ fn extract_contract_name_from_type(ty: &solang_parser::pt::Type) -> String { use solang_parser::pt::*; match ty { - Type::Address - | Type::AddressPayable - | Type::Bool - | Type::String - | Type::Int(_) - | Type::Uint(_) - | Type::Bytes(_) - | Type::DynamicBytes - | Type::Mapping { .. } - | Type::Function { .. } => String::new(), + Type::Address | + Type::AddressPayable | + Type::Bool | + Type::String | + Type::Int(_) | + Type::Uint(_) | + Type::Bytes(_) | + Type::DynamicBytes | + Type::Mapping { .. } | + Type::Function { .. } => String::new(), _ => { // For custom types (likely contracts), try to extract the identifier let type_str = format!("{:?}", ty); @@ -305,8 +305,8 @@ async fn upload_child_contract_alloy( use alloy_primitives::{Address, U256}; use alloy_provider::Provider; use alloy_rpc_types::TransactionRequest; - use alloy_signer_local::PrivateKeySigner; use alloy_serde::WithOtherFields; + use alloy_signer_local::PrivateKeySigner; use foundry_common::provider::ProviderBuilder; use std::str::FromStr; @@ -314,25 +314,20 @@ async fn upload_child_contract_alloy( let wallet = PrivateKeySigner::from_str(&private_key)?; // 2. Create provider with wallet using the proper Foundry pattern - let provider = ProviderBuilder::new(&rpc_url) - .build_with_wallet(EthereumWallet::new(wallet))?; + let provider = ProviderBuilder::new(&rpc_url).build_with_wallet(EthereumWallet::new(wallet))?; // 3. Build transaction let magic_address: Address = "0x6d6f646c70792f70616464720000000000000000".parse()?; - // Convert hex string to bytes for input let input_bytes = hex::decode(encoded_bytes.trim_start_matches("0x"))?; - let tx = TransactionRequest::default() .to(magic_address) .input(input_bytes.into()) .value(U256::from(0u64)); - // 4. Sign and send transaction let wrapped_tx = WithOtherFields::new(tx); let pending_tx = provider.send_transaction(wrapped_tx).await?; let receipt = pending_tx.get_receipt().await?; - Ok(receipt.transaction_hash.to_string()) } @@ -374,20 +369,33 @@ impl CreateArgs { let scaled_encoded_bytes = bytes.encode(); let storage_deposit_limit = Compact(10000000000u128); let encoded_storage_deposit_limit = storage_deposit_limit.encode(); - let combined_hex = "0x3c04".to_string() - + &hex::encode(&scaled_encoded_bytes) - + &hex::encode(&encoded_storage_deposit_limit); + let combined_hex = "0x3c04".to_string() + + &hex::encode(&scaled_encoded_bytes) + + &hex::encode(&encoded_storage_deposit_limit); // Pass RPC URL and private key to upload_child_contract let rpc_url = config.get_rpc_url_or_localhost_http()?; - let private_key = self.eth.wallet.raw.private_key.clone() - .ok_or_eyre("Private key not provided")?; - - let tx_hash =upload_child_contract_alloy(rpc_url.as_ref(), private_key, combined_hex).await?; - println!("Transaction sent! Hash: {:?} for child contract {:?}", tx_hash, contract_name); + let private_key = self + .eth + .wallet + .raw + .private_key + .clone() + .ok_or_eyre("Private key not provided")?; + + let tx_hash = upload_child_contract_alloy( + rpc_url.as_ref(), + private_key, + combined_hex, + ) + .await?; + println!( + "Transaction sent! Hash: {:?} for child contract {:?}", + tx_hash, contract_name + ); } BytecodeObject::Unlinked(_) => { - println!("Bytecode: Available (unlinked)"); + println!("Bytecode: Available (unlinked) for child contract) {:?}", contract_name); } } } else { From c3be5c8351cecd4d3bb32071f225fa9707cb1e5b Mon Sep 17 00:00:00 2001 From: soul022 Date: Sun, 6 Jul 2025 21:41:31 +0530 Subject: [PATCH 04/26] [issue 130] - fix fmt issues --- crates/forge/src/cmd/create.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 265caff488b48..ae2d6921d9d72 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -395,7 +395,10 @@ impl CreateArgs { ); } BytecodeObject::Unlinked(_) => { - println!("Bytecode: Available (unlinked) for child contract) {:?}", contract_name); + println!( + "Bytecode: Available (unlinked) for child contract) {:?}", + contract_name + ); } } } else { From f25cf4183c7f33569285c95359a67c31b4a0d366 Mon Sep 17 00:00:00 2001 From: soul022 Date: Sun, 6 Jul 2025 22:04:03 +0530 Subject: [PATCH 05/26] [issue - 130] - fix clippy issues --- crates/forge/src/cmd/create.rs | 52 ++++++++++++++++------------------ 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index ae2d6921d9d72..cc66579660259 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -35,6 +35,7 @@ use foundry_config::{ merge_impl_figment_convert, Config, }; use serde_json::json; +use tracing::debug; use std::{ borrow::Borrow, collections::HashSet, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration, @@ -158,28 +159,25 @@ fn find_contract_initializations( ) { use solang_parser::pt::*; - for part in source_unit_parts.iter() { - match part { - SourceUnitPart::ContractDefinition(contract) => { - // Look for initializations within contract functions - for contract_part in contract.parts.iter() { - match contract_part { - ContractPart::FunctionDefinition(func) => { - if let Some(Statement::Block { statements, .. }) = &func.body { - find_initializations_in_statements(statements, initialized_names); - } + for part in source_unit_parts { + if let SourceUnitPart::ContractDefinition(contract) = part { + // Look for initializations within contract functions + for contract_part in &contract.parts { + match contract_part { + ContractPart::FunctionDefinition(func) => { + if let Some(Statement::Block { statements, .. }) = &func.body { + find_initializations_in_statements(statements, initialized_names); } - ContractPart::VariableDefinition(var_def) => { - // Check for contract initializations in variable definitions - if let Some(expr) = &var_def.initializer { - find_initializations_in_expression(expr, initialized_names); - } + } + ContractPart::VariableDefinition(var_def) => { + // Check for contract initializations in variable definitions + if let Some(expr) = &var_def.initializer { + find_initializations_in_expression(expr, initialized_names); } - _ => {} } + _ => {} } } - _ => {} } } } @@ -191,7 +189,7 @@ fn find_initializations_in_statements( ) { use solang_parser::pt::*; - for stmt in statements.iter() { + for stmt in statements { match stmt { Statement::Expression(_, expr) => { find_initializations_in_expression(expr, initialized_names); @@ -227,7 +225,7 @@ fn find_initializations_in_expression( Expression::FunctionCall(_, func_expr, args) => { // Recursively check function call expressions and arguments find_initializations_in_expression(func_expr, initialized_names); - for arg in args.iter() { + for arg in args { find_initializations_in_expression(arg, initialized_names); } } @@ -274,7 +272,7 @@ fn extract_contract_name_from_type(ty: &solang_parser::pt::Type) -> String { Type::Function { .. } => String::new(), _ => { // For custom types (likely contracts), try to extract the identifier - let type_str = format!("{:?}", ty); + let type_str = format!("{ty:?}"); // Look for identifier patterns in the debug output if let Some(start) = type_str.find("name: \"") { @@ -288,7 +286,7 @@ fn extract_contract_name_from_type(ty: &solang_parser::pt::Type) -> String { // Fallback: look for capitalized identifiers type_str .split_whitespace() - .find(|s| s.chars().next().map_or(false, |c| c.is_ascii_uppercase()) && s.len() > 1) + .find(|s| s.chars().next().is_some_and(|c| c.is_ascii_uppercase()) && s.len() > 1) .unwrap_or("") .trim_matches(',') .trim_matches(')') @@ -314,7 +312,7 @@ async fn upload_child_contract_alloy( let wallet = PrivateKeySigner::from_str(&private_key)?; // 2. Create provider with wallet using the proper Foundry pattern - let provider = ProviderBuilder::new(&rpc_url).build_with_wallet(EthereumWallet::new(wallet))?; + let provider = ProviderBuilder::new(rpc_url).build_with_wallet(EthereumWallet::new(wallet))?; // 3. Build transaction let magic_address: Address = "0x6d6f646c70792f70616464720000000000000000".parse()?; @@ -356,9 +354,9 @@ impl CreateArgs { // Find all contracts being initialized in the target contract along with their paths let initialized_contracts = find_initialized_contracts_with_paths(&target_path, &output)?; if !initialized_contracts.is_empty() { - println!("Contracts being initialized in {}:", target_path.display()); + debug!("Contracts being initialized in {}:", target_path.display()); for (contract_name, contract_path) in &initialized_contracts { - println!(" - {} (defined in: {})", contract_name, contract_path.display()); + debug!(" - {} (defined in: {})", contract_name, contract_path.display()); // Try to get bytecode information for this contract if let Ok((_, bin, _)) = @@ -389,20 +387,20 @@ impl CreateArgs { combined_hex, ) .await?; - println!( + debug!( "Transaction sent! Hash: {:?} for child contract {:?}", tx_hash, contract_name ); } BytecodeObject::Unlinked(_) => { - println!( + debug!( "Bytecode: Available (unlinked) for child contract) {:?}", contract_name ); } } } else { - println!("Bytecode: Not available or compilation error"); + debug!("Bytecode: Not available or compilation error"); } } } From 56bd04bb281ec792f01c3ae5f83a0ef4f43819a3 Mon Sep 17 00:00:00 2001 From: soul022 Date: Sun, 6 Jul 2025 22:10:23 +0530 Subject: [PATCH 06/26] [issue - 130] - fix fmt issues --- crates/forge/src/cmd/create.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index cc66579660259..347531d60d934 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -35,11 +35,11 @@ use foundry_config::{ merge_impl_figment_convert, Config, }; use serde_json::json; -use tracing::debug; use std::{ borrow::Borrow, collections::HashSet, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration, }; +use tracing::debug; merge_impl_figment_convert!(CreateArgs, build, eth); /// CLI arguments for `forge create`. From 850206faf18237d02d2cc6647f42d553c08d55eb Mon Sep 17 00:00:00 2001 From: soul022 Date: Sun, 6 Jul 2025 22:17:39 +0530 Subject: [PATCH 07/26] [issue - 130] - fix fmt issues --- crates/forge/src/cmd/create.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 347531d60d934..c3250f5787c56 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -194,7 +194,7 @@ fn find_initializations_in_statements( Statement::Expression(_, expr) => { find_initializations_in_expression(expr, initialized_names); } - Statement::VariableDefinition(_, _var_decl, init_expr) => { + Statement::VariableDefinition(_, var_decl, init_expr) => { if let Some(expr) = init_expr { find_initializations_in_expression(expr, initialized_names); } From 0efe3a44bd1c640ff33c642171efd0475c5c89ad Mon Sep 17 00:00:00 2001 From: soul022 Date: Sun, 6 Jul 2025 22:25:22 +0530 Subject: [PATCH 08/26] [issue - 130] - fix clippy issues --- crates/forge/src/cmd/create.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index c3250f5787c56..43794e9bdacf4 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -194,10 +194,8 @@ fn find_initializations_in_statements( Statement::Expression(_, expr) => { find_initializations_in_expression(expr, initialized_names); } - Statement::VariableDefinition(_, var_decl, init_expr) => { - if let Some(expr) = init_expr { - find_initializations_in_expression(expr, initialized_names); - } + Statement::VariableDefinition(_, _var_decl, Some(expr)) => { + find_initializations_in_expression(expr, initialized_names); } Statement::Block { statements, .. } => { find_initializations_in_statements(statements, initialized_names); From 4543076e422d56092dfd246d6847133a599af3d5 Mon Sep 17 00:00:00 2001 From: soul022 Date: Mon, 7 Jul 2025 22:43:28 +0530 Subject: [PATCH 09/26] [issue - 130] - updated code to get bytecode contract from compiler output --- crates/cli/src/utils/cmd.rs | 36 +++- crates/forge/src/cmd/create.rs | 300 +++++++-------------------------- 2 files changed, 92 insertions(+), 244 deletions(-) diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index a28b0780e450b..8ad6b7bef7602 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -6,7 +6,7 @@ use foundry_common::{ TestFunctionExt, }; use foundry_compilers::{ - artifacts::{CompactBytecode, Settings}, + artifacts::{BytecodeObject, CompactBytecode, ConfigurableContractArtifact, Settings}, cache::{CacheEntry, CompilerCache}, utils::read_json_file, Artifact, ArtifactId, ProjectCompileOutput, @@ -73,6 +73,40 @@ pub fn remove_contract( Ok((abi, bin, id)) } +#[derive(Debug, Clone)] +pub struct ChildContract { + pub name: String, + pub bytecode: BytecodeObject, +} + +/// Get child contracts from compiled output +pub fn get_child_contracts( + output: ProjectCompileOutput, + parent_contract_name: &str, +) -> Result> { + let artifacts: Vec<(ArtifactId, ConfigurableContractArtifact)> = + output.into_artifacts().collect(); + let mut child_contracts = Vec::new(); + + // Find all contracts that are not the parent contract + for (artifact_id, artifact) in artifacts { + let contract_name = &artifact_id.name; + + if contract_name != parent_contract_name { + // This is a child contract (different name from parent) + if let Some(bytecode) = &artifact.bytecode { + let child_contract = ChildContract { + name: contract_name.clone(), + bytecode: bytecode.object.clone(), + }; + child_contracts.push(child_contract); + } + } + } + + Ok(child_contracts) +} + /// Helper function for finding a contract by ContractName // TODO: Is there a better / more ergonomic way to get the artifacts given a project and a // contract name? diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 43794e9bdacf4..d1fce40ba9e3c 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -16,7 +16,7 @@ use eyre::{Context, OptionExt, Result}; use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::{ opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts}, - utils::{self, read_constructor_args_file, remove_contract, LoadConfig}, + utils::{self, read_constructor_args_file, remove_contract, get_child_contracts, LoadConfig}, }; use foundry_common::{ compile::{self}, @@ -24,7 +24,10 @@ use foundry_common::{ shell, }; use foundry_compilers::{ - artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, ArtifactId, + artifacts::BytecodeObject, + info::ContractInfo, + utils::canonicalize, + ArtifactId, }; use foundry_config::{ figment::{ @@ -35,10 +38,7 @@ use foundry_config::{ merge_impl_figment_convert, Config, }; use serde_json::json; -use std::{ - borrow::Borrow, collections::HashSet, marker::PhantomData, path::PathBuf, sync::Arc, - time::Duration, -}; +use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; use tracing::debug; merge_impl_figment_convert!(CreateArgs, build, eth); @@ -105,194 +105,6 @@ pub struct CreateArgs { retry: RetryArgs, } -/// Finds all contracts being initialized in the target contract along with their file paths -/// -/// # Arguments -/// * `target_path` - Path to the contract file being analyzed -/// * `output` - The compilation output containing all contract information -/// -/// # Returns -/// * `Result>` - Vector of (contract_name, file_path) tuples -pub fn find_initialized_contracts_with_paths( - target_path: &PathBuf, - output: &foundry_compilers::ProjectCompileOutput, -) -> Result> { - let mut initialized_contracts = Vec::new(); - - // Read the source file content - let source_content = std::fs::read_to_string(target_path) - .wrap_err_with(|| format!("Failed to read target file: {}", target_path.display()))?; - - // Parse the Solidity source code - let (parse_tree, _diagnostics) = match solang_parser::parse(&source_content, 0) { - Ok((tree, diag)) => (tree, diag), - Err(_diagnostics) => { - // If parsing fails, return empty list - return Ok(vec![]); - } - }; - - // Find all contract names being initialized - let mut initialized_names = HashSet::new(); - find_contract_initializations(&parse_tree.0, &mut initialized_names); - - // Map contract names to their file paths using compilation output - let contracts = &output.output().contracts; - for contract_name in initialized_names { - // Search through all files to find where this contract is defined - for (file_path, file_contracts) in contracts.iter() { - if file_contracts.contains_key(&contract_name) { - initialized_contracts.push((contract_name.clone(), file_path.clone())); - break; - } - } - } - - initialized_contracts.sort_by(|a, b| a.0.cmp(&b.0)); - Ok(initialized_contracts) -} - -/// Recursively searches AST for contract initializations (new ContractName() expressions) -fn find_contract_initializations( - source_unit_parts: &[solang_parser::pt::SourceUnitPart], - initialized_names: &mut HashSet, -) { - use solang_parser::pt::*; - - for part in source_unit_parts { - if let SourceUnitPart::ContractDefinition(contract) = part { - // Look for initializations within contract functions - for contract_part in &contract.parts { - match contract_part { - ContractPart::FunctionDefinition(func) => { - if let Some(Statement::Block { statements, .. }) = &func.body { - find_initializations_in_statements(statements, initialized_names); - } - } - ContractPart::VariableDefinition(var_def) => { - // Check for contract initializations in variable definitions - if let Some(expr) = &var_def.initializer { - find_initializations_in_expression(expr, initialized_names); - } - } - _ => {} - } - } - } - } -} - -/// Recursively searches statements for contract initializations -fn find_initializations_in_statements( - statements: &[solang_parser::pt::Statement], - initialized_names: &mut HashSet, -) { - use solang_parser::pt::*; - - for stmt in statements { - match stmt { - Statement::Expression(_, expr) => { - find_initializations_in_expression(expr, initialized_names); - } - Statement::VariableDefinition(_, _var_decl, Some(expr)) => { - find_initializations_in_expression(expr, initialized_names); - } - Statement::Block { statements, .. } => { - find_initializations_in_statements(statements, initialized_names); - } - _ => {} - } - } -} - -/// Searches expressions for contract initializations (new ContractName() calls) -fn find_initializations_in_expression( - expr: &solang_parser::pt::Expression, - initialized_names: &mut HashSet, -) { - use solang_parser::pt::*; - - match expr { - Expression::New(_, type_expr) => { - // Extract contract name from the type expression - let contract_name = extract_contract_name_from_expression(type_expr); - if !contract_name.is_empty() { - initialized_names.insert(contract_name); - } - } - Expression::FunctionCall(_, func_expr, args) => { - // Recursively check function call expressions and arguments - find_initializations_in_expression(func_expr, initialized_names); - for arg in args { - find_initializations_in_expression(arg, initialized_names); - } - } - Expression::Assign(_, left_expr, right_expr) => { - find_initializations_in_expression(left_expr, initialized_names); - find_initializations_in_expression(right_expr, initialized_names); - } - Expression::MemberAccess(_, base_expr, _member) => { - find_initializations_in_expression(base_expr, initialized_names); - } - _ => {} - } -} - -/// Extracts contract name from an Expression (for new Contract() patterns) -fn extract_contract_name_from_expression(expr: &solang_parser::pt::Expression) -> String { - use solang_parser::pt::*; - - match expr { - Expression::Variable(identifier) => identifier.name.clone(), - Expression::MemberAccess(_, _, identifier) => identifier.name.clone(), - Expression::FunctionCall(_, func_expr, _args) => { - extract_contract_name_from_expression(func_expr) - } - Expression::Type(_, ty) => extract_contract_name_from_type(ty), - _ => String::new(), - } -} - -/// Extracts contract name from a Type -fn extract_contract_name_from_type(ty: &solang_parser::pt::Type) -> String { - use solang_parser::pt::*; - - match ty { - Type::Address | - Type::AddressPayable | - Type::Bool | - Type::String | - Type::Int(_) | - Type::Uint(_) | - Type::Bytes(_) | - Type::DynamicBytes | - Type::Mapping { .. } | - Type::Function { .. } => String::new(), - _ => { - // For custom types (likely contracts), try to extract the identifier - let type_str = format!("{ty:?}"); - - // Look for identifier patterns in the debug output - if let Some(start) = type_str.find("name: \"") { - let start = start + 7; // Skip 'name: "' - if let Some(end) = type_str[start..].find('"') { - let name = &type_str[start..start + end]; - return name.to_string(); - } - } - - // Fallback: look for capitalized identifiers - type_str - .split_whitespace() - .find(|s| s.chars().next().is_some_and(|c| c.is_ascii_uppercase()) && s.len() > 1) - .unwrap_or("") - .trim_matches(',') - .trim_matches(')') - .to_string() - } - } -} - async fn upload_child_contract_alloy( rpc_url: &str, private_key: String, @@ -349,58 +161,60 @@ impl CreateArgs { let output: foundry_compilers::ProjectCompileOutput = compile::compile_target(&target_path, &project, shell::is_json())?; - // Find all contracts being initialized in the target contract along with their paths - let initialized_contracts = find_initialized_contracts_with_paths(&target_path, &output)?; - if !initialized_contracts.is_empty() { - debug!("Contracts being initialized in {}:", target_path.display()); - for (contract_name, contract_path) in &initialized_contracts { - debug!(" - {} (defined in: {})", contract_name, contract_path.display()); - - // Try to get bytecode information for this contract - if let Ok((_, bin, _)) = - remove_contract(output.clone(), contract_path, contract_name) - { - match &bin.object { - BytecodeObject::Bytecode(bytes) => { - let scaled_encoded_bytes = bytes.encode(); - let storage_deposit_limit = Compact(10000000000u128); - let encoded_storage_deposit_limit = storage_deposit_limit.encode(); - let combined_hex = "0x3c04".to_string() + - &hex::encode(&scaled_encoded_bytes) + - &hex::encode(&encoded_storage_deposit_limit); - - // Pass RPC URL and private key to upload_child_contract - let rpc_url = config.get_rpc_url_or_localhost_http()?; - let private_key = self - .eth - .wallet - .raw - .private_key - .clone() - .ok_or_eyre("Private key not provided")?; - - let tx_hash = upload_child_contract_alloy( - rpc_url.as_ref(), - private_key, - combined_hex, - ) - .await?; - debug!( - "Transaction sent! Hash: {:?} for child contract {:?}", - tx_hash, contract_name - ); - } - BytecodeObject::Unlinked(_) => { - debug!( - "Bytecode: Available (unlinked) for child contract) {:?}", - contract_name - ); - } - } + match get_child_contracts(output.clone(), &self.contract.name) { + Ok(child_contracts) => { + if child_contracts.is_empty() { + debug!("No child contracts found for '{}'", self.contract.name); } else { - debug!("Bytecode: Not available or compilation error"); + debug!( + "Found {} child contract(s) for '{}':", + child_contracts.len(), + self.contract.name + ); + for child in &child_contracts { + match &child.bytecode { + BytecodeObject::Bytecode(bytes) => { + let scaled_encoded_bytes = bytes.encode(); + let storage_deposit_limit = Compact(10000000000u128); + let encoded_storage_deposit_limit = storage_deposit_limit.encode(); + let combined_hex = "0x3c04".to_string() + + &hex::encode(&scaled_encoded_bytes) + + &hex::encode(&encoded_storage_deposit_limit); + + // Pass RPC URL and private key to upload_child_contract + let rpc_url = config.get_rpc_url_or_localhost_http()?; + let private_key = self + .eth + .wallet + .raw + .private_key + .clone() + .ok_or_eyre("Private key not provided")?; + + let tx_hash = upload_child_contract_alloy( + rpc_url.as_ref(), + private_key, + combined_hex, + ) + .await?; + debug!( + "Transaction sent! Hash: {:?} for child contract {:?}", + tx_hash, self.contract.name + ); + } + BytecodeObject::Unlinked(_) => { + debug!( + "Bytecode: Available (unlinked) for child contract) {:?}", + self.contract.name + ); + } + }; + } } } + Err(e) => { + debug!("Error getting child contracts: {}", e); + } } let (abi, bin, id) = remove_contract(output, &target_path, &self.contract.name)?; From b6328da778daa9c126b052505f07183750a74fbd Mon Sep 17 00:00:00 2001 From: soul022 Date: Mon, 7 Jul 2025 23:01:54 +0530 Subject: [PATCH 10/26] [issue - 130] - rustfmt fixes --- crates/forge/src/cmd/create.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index d1fce40ba9e3c..97c31f3100e71 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -16,7 +16,7 @@ use eyre::{Context, OptionExt, Result}; use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::{ opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts}, - utils::{self, read_constructor_args_file, remove_contract, get_child_contracts, LoadConfig}, + utils::{self, get_child_contracts, read_constructor_args_file, remove_contract, LoadConfig}, }; use foundry_common::{ compile::{self}, @@ -24,10 +24,7 @@ use foundry_common::{ shell, }; use foundry_compilers::{ - artifacts::BytecodeObject, - info::ContractInfo, - utils::canonicalize, - ArtifactId, + artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, ArtifactId, }; use foundry_config::{ figment::{ From 8b65934414700787c9f22a0c942a308352753463 Mon Sep 17 00:00:00 2001 From: soul022 Date: Mon, 7 Jul 2025 23:08:25 +0530 Subject: [PATCH 11/26] [issue - 130] - added comments to upload_child_contract_alloy --- crates/forge/src/cmd/create.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 97c31f3100e71..d582595a6706c 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -102,6 +102,7 @@ pub struct CreateArgs { retry: RetryArgs, } +/// Uploads a child contract to a blockchain network using the Alloy framework. async fn upload_child_contract_alloy( rpc_url: &str, private_key: String, @@ -115,24 +116,33 @@ async fn upload_child_contract_alloy( use foundry_common::provider::ProviderBuilder; use std::str::FromStr; - // 1. Create wallet from private key + // This wallet will be used to sign the deployment transaction let wallet = PrivateKeySigner::from_str(&private_key)?; - // 2. Create provider with wallet using the proper Foundry pattern + // This establishes the connection to the target network and prepares for transaction signing let provider = ProviderBuilder::new(rpc_url).build_with_wallet(EthereumWallet::new(wallet))?; - // 3. Build transaction + // Use the special "magic address" for child contract deployment let magic_address: Address = "0x6d6f646c70792f70616464720000000000000000".parse()?; - // Convert hex string to bytes for input + + // Convert the hex-encoded bytecode string to actual bytes for the transaction input + // Remove "0x" prefix if present before decoding let input_bytes = hex::decode(encoded_bytes.trim_start_matches("0x"))?; + + // Construct the transaction request let tx = TransactionRequest::default() .to(magic_address) .input(input_bytes.into()) .value(U256::from(0u64)); - // 4. Sign and send transaction + + // Wrap the transaction in WithOtherFields for proper serialization let wrapped_tx = WithOtherFields::new(tx); + + // Send the transaction to the network and wait for it to be included in a block let pending_tx = provider.send_transaction(wrapped_tx).await?; let receipt = pending_tx.get_receipt().await?; + + // Return the transaction hash as a string for tracking and verification Ok(receipt.transaction_hash.to_string()) } From cccbe25e8f20b9a25e14dba43ffc752d1d2e4d88 Mon Sep 17 00:00:00 2001 From: soul022 Date: Mon, 7 Jul 2025 23:12:45 +0530 Subject: [PATCH 12/26] [issue - 130] - rustfmt fixes --- crates/forge/src/cmd/create.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index d582595a6706c..06f201ce12106 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -124,11 +124,11 @@ async fn upload_child_contract_alloy( // Use the special "magic address" for child contract deployment let magic_address: Address = "0x6d6f646c70792f70616464720000000000000000".parse()?; - + // Convert the hex-encoded bytecode string to actual bytes for the transaction input // Remove "0x" prefix if present before decoding let input_bytes = hex::decode(encoded_bytes.trim_start_matches("0x"))?; - + // Construct the transaction request let tx = TransactionRequest::default() .to(magic_address) @@ -137,11 +137,11 @@ async fn upload_child_contract_alloy( // Wrap the transaction in WithOtherFields for proper serialization let wrapped_tx = WithOtherFields::new(tx); - + // Send the transaction to the network and wait for it to be included in a block let pending_tx = provider.send_transaction(wrapped_tx).await?; let receipt = pending_tx.get_receipt().await?; - + // Return the transaction hash as a string for tracking and verification Ok(receipt.transaction_hash.to_string()) } From bb249c97e7c2b02c647526960714f154c3ba0a35 Mon Sep 17 00:00:00 2001 From: soul022 Date: Tue, 8 Jul 2025 23:30:56 +0530 Subject: [PATCH 13/26] [issue - 130] - remove internal libraries used as child contracts --- crates/cli/src/utils/cmd.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 8ad6b7bef7602..6ee21de2b72a3 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -80,6 +80,7 @@ pub struct ChildContract { } /// Get child contracts from compiled output +/// Excludes internal libraries since they are not meant to be deployed separately pub fn get_child_contracts( output: ProjectCompileOutput, parent_contract_name: &str, @@ -88,18 +89,30 @@ pub fn get_child_contracts( output.into_artifacts().collect(); let mut child_contracts = Vec::new(); - // Find all contracts that are not the parent contract + // Find all contracts that are not the parent contract and are not libraries for (artifact_id, artifact) in artifacts { let contract_name = &artifact_id.name; if contract_name != parent_contract_name { - // This is a child contract (different name from parent) - if let Some(bytecode) = &artifact.bytecode { - let child_contract = ChildContract { - name: contract_name.clone(), - bytecode: bytecode.object.clone(), - }; - child_contracts.push(child_contract); + // Check if this is a library by looking at the ABI + let is_library = if let Some(abi) = &artifact.abi { + abi.functions().count() == 0 + } else { + // No ABI indicates a library + true + }; + + // Only include non-library contracts as child contracts + if !is_library { + if let Some(bytecode) = &artifact.bytecode { + let child_contract = ChildContract { + name: contract_name.clone(), + bytecode: bytecode.object.clone(), + }; + child_contracts.push(child_contract); + } + } else { + println!("Skipping library: '{}' (not a deployable child contract)", contract_name); } } } From 0552ce58eb76d085cb6f6f1f14cccb84dc8d8022 Mon Sep 17 00:00:00 2001 From: soul022 Date: Tue, 8 Jul 2025 23:45:12 +0530 Subject: [PATCH 14/26] [issue - 130] - clippy fix --- crates/cli/src/utils/cmd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 6ee21de2b72a3..e9f52cb3d5899 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -112,7 +112,7 @@ pub fn get_child_contracts( child_contracts.push(child_contract); } } else { - println!("Skipping library: '{}' (not a deployable child contract)", contract_name); + tracing::debug!("Skipping library: '{contract_name}' (not a deployable child contract)"); } } } From 64fa04d1197cf0593fa6453cbf0ad655bdaafe61 Mon Sep 17 00:00:00 2001 From: soul022 Date: Sun, 13 Jul 2025 23:41:26 +0530 Subject: [PATCH 15/26] [issue - 130] - reading factory contract from compilers --- crates/forge/src/cmd/create.rs | 170 +++++++++++++++++++++------------ 1 file changed, 109 insertions(+), 61 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 06f201ce12106..7159d8def183d 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -16,7 +16,7 @@ use eyre::{Context, OptionExt, Result}; use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::{ opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts}, - utils::{self, get_child_contracts, read_constructor_args_file, remove_contract, LoadConfig}, + utils::{self, read_constructor_args_file, remove_contract, LoadConfig}, }; use foundry_common::{ compile::{self}, @@ -24,7 +24,7 @@ use foundry_common::{ shell, }; use foundry_compilers::{ - artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, ArtifactId, + artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, ArtifactId, ProjectCompileOutput, Artifact, }; use foundry_config::{ figment::{ @@ -35,10 +35,102 @@ use foundry_config::{ merge_impl_figment_convert, Config, }; use serde_json::json; -use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; -use tracing::debug; +use std::{borrow::Borrow, collections::BTreeMap, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; merge_impl_figment_convert!(CreateArgs, build, eth); +/// Finds a contract in the artifacts by its bytecode hash +fn find_contract_by_hash( + output: &ProjectCompileOutput, + target_hash: &str, +) -> Option { + for (_contract_name, artifact) in output.artifacts() { + if let Some(bytecode) = artifact.get_bytecode_bytes() { + let bytecode_bytes = bytecode.into_owned(); + if !bytecode_bytes.is_empty() { + // Calculate keccak256 hash of the bytecode + use alloy_primitives::keccak256; + let calculated_hash = hex::encode(keccak256(&bytecode_bytes)); + + // Normalize both hashes by removing 0x prefix for comparison + let normalized_target = target_hash.trim_start_matches("0x"); + let normalized_calculated = calculated_hash.trim_start_matches("0x"); + + if normalized_calculated == normalized_target { + return Some(bytecode_bytes.into()); + } + } + } + } + None +} + +/// Handles factory dependencies for a contract deployment +async fn handle_factory_dependencies( + output: &ProjectCompileOutput, + config: &Config, + private_key: &str, +) -> Result<()> { + let artifacts: Vec<_> = output.artifacts().collect(); + + if artifacts.is_empty() { + return Ok(()); + } + + // Collect all factory dependencies from all contracts + let mut all_dependencies = BTreeMap::new(); + + for artifact in artifacts.into_iter().map(|(_, artifact)| artifact) { + if let Some(factory_deps) = &artifact.factory_dependencies { + for (dep_hash, dep_name) in factory_deps { + all_dependencies.insert(dep_hash.clone(), dep_name.clone()); + } + } + } + + if all_dependencies.is_empty() { + return Ok(()); + } + + // Get RPC URL for factory dependency uploads + let rpc_url = config.get_rpc_url_or_localhost_http()?; + + // Upload each factory dependency + for (hash, name) in all_dependencies { + // Try to find the contract by hash directly (skip name-based lookup) + let bytecode = find_contract_by_hash(output, &hash); + + if let Some(bytecode) = bytecode { + // Skip child contracts (those with 0x3c04 prefix) + let bytecode_slice = bytecode.as_ref(); + if bytecode_slice.len() >= 2 && bytecode_slice[0] == 0x3c && bytecode_slice[1] == 0x04 { + continue; + } + + // Upload factory dependency using upload_child_contract_alloy + let scaled_encoded_bytes = bytecode.encode(); + let storage_deposit_limit = Compact(10000000000u128); + let encoded_storage_deposit_limit = storage_deposit_limit.encode(); + let combined_hex = "0x3c04".to_string() + + &hex::encode(&scaled_encoded_bytes) + + &hex::encode(&encoded_storage_deposit_limit); + + let _tx_hash = upload_child_contract_alloy( + rpc_url.as_ref(), + private_key.to_string(), + combined_hex, + ) + .await?; + } else { + return Err(eyre::eyre!( + "Could not find contract '{}' (hash: {}) in artifacts", + name, hash + )); + } + } + + Ok(()) +} + /// CLI arguments for `forge create`. #[derive(Clone, Debug, Parser)] pub struct CreateArgs { @@ -168,63 +260,7 @@ impl CreateArgs { let output: foundry_compilers::ProjectCompileOutput = compile::compile_target(&target_path, &project, shell::is_json())?; - match get_child_contracts(output.clone(), &self.contract.name) { - Ok(child_contracts) => { - if child_contracts.is_empty() { - debug!("No child contracts found for '{}'", self.contract.name); - } else { - debug!( - "Found {} child contract(s) for '{}':", - child_contracts.len(), - self.contract.name - ); - for child in &child_contracts { - match &child.bytecode { - BytecodeObject::Bytecode(bytes) => { - let scaled_encoded_bytes = bytes.encode(); - let storage_deposit_limit = Compact(10000000000u128); - let encoded_storage_deposit_limit = storage_deposit_limit.encode(); - let combined_hex = "0x3c04".to_string() + - &hex::encode(&scaled_encoded_bytes) + - &hex::encode(&encoded_storage_deposit_limit); - - // Pass RPC URL and private key to upload_child_contract - let rpc_url = config.get_rpc_url_or_localhost_http()?; - let private_key = self - .eth - .wallet - .raw - .private_key - .clone() - .ok_or_eyre("Private key not provided")?; - - let tx_hash = upload_child_contract_alloy( - rpc_url.as_ref(), - private_key, - combined_hex, - ) - .await?; - debug!( - "Transaction sent! Hash: {:?} for child contract {:?}", - tx_hash, self.contract.name - ); - } - BytecodeObject::Unlinked(_) => { - debug!( - "Bytecode: Available (unlinked) for child contract) {:?}", - self.contract.name - ); - } - }; - } - } - } - Err(e) => { - debug!("Error getting child contracts: {}", e); - } - } - - let (abi, bin, id) = remove_contract(output, &target_path, &self.contract.name)?; + let (abi, bin, id) = remove_contract(output.clone(), &target_path, &self.contract.name)?; let bin = match bin.object { BytecodeObject::Bytecode(_) => bin.object, @@ -255,6 +291,18 @@ impl CreateArgs { let provider = utils::get_provider(&config)?; + // Handle factory dependencies before deploying the main contract + if self.broadcast { + let private_key = self + .eth + .wallet + .raw + .private_key + .clone() + .ok_or_eyre("Private key not provided")?; + handle_factory_dependencies(&output, &config, &private_key).await?; + } + // respect chain, if set explicitly via cmd args let chain_id = if let Some(chain_id) = self.chain_id() { chain_id From 3411a24b0a71642344a79552c92e180f70cdb958 Mon Sep 17 00:00:00 2001 From: soul022 Date: Sun, 13 Jul 2025 23:47:55 +0530 Subject: [PATCH 16/26] [issue - 130] - remove unused code --- crates/cli/src/utils/cmd.rs | 49 +------------------------------------ 1 file changed, 1 insertion(+), 48 deletions(-) diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index e9f52cb3d5899..a28b0780e450b 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -6,7 +6,7 @@ use foundry_common::{ TestFunctionExt, }; use foundry_compilers::{ - artifacts::{BytecodeObject, CompactBytecode, ConfigurableContractArtifact, Settings}, + artifacts::{CompactBytecode, Settings}, cache::{CacheEntry, CompilerCache}, utils::read_json_file, Artifact, ArtifactId, ProjectCompileOutput, @@ -73,53 +73,6 @@ pub fn remove_contract( Ok((abi, bin, id)) } -#[derive(Debug, Clone)] -pub struct ChildContract { - pub name: String, - pub bytecode: BytecodeObject, -} - -/// Get child contracts from compiled output -/// Excludes internal libraries since they are not meant to be deployed separately -pub fn get_child_contracts( - output: ProjectCompileOutput, - parent_contract_name: &str, -) -> Result> { - let artifacts: Vec<(ArtifactId, ConfigurableContractArtifact)> = - output.into_artifacts().collect(); - let mut child_contracts = Vec::new(); - - // Find all contracts that are not the parent contract and are not libraries - for (artifact_id, artifact) in artifacts { - let contract_name = &artifact_id.name; - - if contract_name != parent_contract_name { - // Check if this is a library by looking at the ABI - let is_library = if let Some(abi) = &artifact.abi { - abi.functions().count() == 0 - } else { - // No ABI indicates a library - true - }; - - // Only include non-library contracts as child contracts - if !is_library { - if let Some(bytecode) = &artifact.bytecode { - let child_contract = ChildContract { - name: contract_name.clone(), - bytecode: bytecode.object.clone(), - }; - child_contracts.push(child_contract); - } - } else { - tracing::debug!("Skipping library: '{contract_name}' (not a deployable child contract)"); - } - } - } - - Ok(child_contracts) -} - /// Helper function for finding a contract by ContractName // TODO: Is there a better / more ergonomic way to get the artifacts given a project and a // contract name? From af8036c20d889c330fd373b2f80327e215fba3d1 Mon Sep 17 00:00:00 2001 From: soul022 Date: Sun, 13 Jul 2025 23:51:34 +0530 Subject: [PATCH 17/26] [issue - 130] - fix fmt issues --- crates/forge/src/cmd/create.rs | 53 ++++++++++++++++------------------ 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 7159d8def183d..e5715519bf3ec 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -24,7 +24,8 @@ use foundry_common::{ shell, }; use foundry_compilers::{ - artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, ArtifactId, ProjectCompileOutput, Artifact, + artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, Artifact, ArtifactId, + ProjectCompileOutput, }; use foundry_config::{ figment::{ @@ -35,14 +36,14 @@ use foundry_config::{ merge_impl_figment_convert, Config, }; use serde_json::json; -use std::{borrow::Borrow, collections::BTreeMap, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration}; +use std::{ + borrow::Borrow, collections::BTreeMap, marker::PhantomData, path::PathBuf, sync::Arc, + time::Duration, +}; merge_impl_figment_convert!(CreateArgs, build, eth); /// Finds a contract in the artifacts by its bytecode hash -fn find_contract_by_hash( - output: &ProjectCompileOutput, - target_hash: &str, -) -> Option { +fn find_contract_by_hash(output: &ProjectCompileOutput, target_hash: &str) -> Option { for (_contract_name, artifact) in output.artifacts() { if let Some(bytecode) = artifact.get_bytecode_bytes() { let bytecode_bytes = bytecode.into_owned(); @@ -50,11 +51,11 @@ fn find_contract_by_hash( // Calculate keccak256 hash of the bytecode use alloy_primitives::keccak256; let calculated_hash = hex::encode(keccak256(&bytecode_bytes)); - + // Normalize both hashes by removing 0x prefix for comparison let normalized_target = target_hash.trim_start_matches("0x"); let normalized_calculated = calculated_hash.trim_start_matches("0x"); - + if normalized_calculated == normalized_target { return Some(bytecode_bytes.into()); } @@ -71,14 +72,14 @@ async fn handle_factory_dependencies( private_key: &str, ) -> Result<()> { let artifacts: Vec<_> = output.artifacts().collect(); - + if artifacts.is_empty() { return Ok(()); } - + // Collect all factory dependencies from all contracts let mut all_dependencies = BTreeMap::new(); - + for artifact in artifacts.into_iter().map(|(_, artifact)| artifact) { if let Some(factory_deps) = &artifact.factory_dependencies { for (dep_hash, dep_name) in factory_deps { @@ -86,33 +87,33 @@ async fn handle_factory_dependencies( } } } - + if all_dependencies.is_empty() { return Ok(()); } - + // Get RPC URL for factory dependency uploads let rpc_url = config.get_rpc_url_or_localhost_http()?; - + // Upload each factory dependency for (hash, name) in all_dependencies { // Try to find the contract by hash directly (skip name-based lookup) let bytecode = find_contract_by_hash(output, &hash); - + if let Some(bytecode) = bytecode { // Skip child contracts (those with 0x3c04 prefix) let bytecode_slice = bytecode.as_ref(); if bytecode_slice.len() >= 2 && bytecode_slice[0] == 0x3c && bytecode_slice[1] == 0x04 { continue; } - + // Upload factory dependency using upload_child_contract_alloy let scaled_encoded_bytes = bytecode.encode(); let storage_deposit_limit = Compact(10000000000u128); let encoded_storage_deposit_limit = storage_deposit_limit.encode(); - let combined_hex = "0x3c04".to_string() + - &hex::encode(&scaled_encoded_bytes) + - &hex::encode(&encoded_storage_deposit_limit); + let combined_hex = "0x3c04".to_string() + + &hex::encode(&scaled_encoded_bytes) + + &hex::encode(&encoded_storage_deposit_limit); let _tx_hash = upload_child_contract_alloy( rpc_url.as_ref(), @@ -123,11 +124,12 @@ async fn handle_factory_dependencies( } else { return Err(eyre::eyre!( "Could not find contract '{}' (hash: {}) in artifacts", - name, hash + name, + hash )); } } - + Ok(()) } @@ -293,13 +295,8 @@ impl CreateArgs { // Handle factory dependencies before deploying the main contract if self.broadcast { - let private_key = self - .eth - .wallet - .raw - .private_key - .clone() - .ok_or_eyre("Private key not provided")?; + let private_key = + self.eth.wallet.raw.private_key.clone().ok_or_eyre("Private key not provided")?; handle_factory_dependencies(&output, &config, &private_key).await?; } From 053f1e4313a7c0c1fe38ee73d0bd25432e787cb4 Mon Sep 17 00:00:00 2001 From: soul022 Date: Sun, 13 Jul 2025 23:58:24 +0530 Subject: [PATCH 18/26] [issue - 130] - fix fmt issues --- crates/forge/src/cmd/create.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index e5715519bf3ec..78801d554ca2c 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -81,9 +81,15 @@ async fn handle_factory_dependencies( let mut all_dependencies = BTreeMap::new(); for artifact in artifacts.into_iter().map(|(_, artifact)| artifact) { - if let Some(factory_deps) = &artifact.factory_dependencies { - for (dep_hash, dep_name) in factory_deps { - all_dependencies.insert(dep_hash.clone(), dep_name.clone()); + // Convert artifact to JSON to access factoryDependencies + let artifact_json = serde_json::to_value(&artifact)?; + if let Some(factory_deps) = artifact_json.get("factoryDependencies") { + if let Some(deps_map) = factory_deps.as_object() { + for (dep_hash, dep_name) in deps_map { + if let Some(name) = dep_name.as_str() { + all_dependencies.insert(dep_hash.clone(), name.to_string()); + } + } } } } @@ -111,9 +117,9 @@ async fn handle_factory_dependencies( let scaled_encoded_bytes = bytecode.encode(); let storage_deposit_limit = Compact(10000000000u128); let encoded_storage_deposit_limit = storage_deposit_limit.encode(); - let combined_hex = "0x3c04".to_string() - + &hex::encode(&scaled_encoded_bytes) - + &hex::encode(&encoded_storage_deposit_limit); + let combined_hex = "0x3c04".to_string() + + &hex::encode(&scaled_encoded_bytes) + + &hex::encode(&encoded_storage_deposit_limit); let _tx_hash = upload_child_contract_alloy( rpc_url.as_ref(), From 145f6da384e8c428478d85e652c46040f36e37df Mon Sep 17 00:00:00 2001 From: soul022 Date: Mon, 14 Jul 2025 00:13:06 +0530 Subject: [PATCH 19/26] [issue - 130] - fix fmt issues --- crates/anvil/src/eth/backend/mem/mod.rs | 2 +- crates/forge/src/cmd/create.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/anvil/src/eth/backend/mem/mod.rs b/crates/anvil/src/eth/backend/mem/mod.rs index 2f295ed3311b9..d482687955972 100644 --- a/crates/anvil/src/eth/backend/mem/mod.rs +++ b/crates/anvil/src/eth/backend/mem/mod.rs @@ -1755,7 +1755,7 @@ impl Backend { GethDebugBuiltInTracerType::CallTracer => { let call_config = tracer_config .into_call_config() - .map_err(|e| (RpcError::invalid_params(e.to_string())))?; + .map_err(|e| RpcError::invalid_params(e.to_string()))?; let mut inspector = self.build_inspector().with_tracing_config( TracingInspectorConfig::from_geth_call_config(&call_config), diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 78801d554ca2c..bbb3350609310 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -57,7 +57,7 @@ fn find_contract_by_hash(output: &ProjectCompileOutput, target_hash: &str) -> Op let normalized_calculated = calculated_hash.trim_start_matches("0x"); if normalized_calculated == normalized_target { - return Some(bytecode_bytes.into()); + return Some(bytecode_bytes); } } } @@ -82,7 +82,7 @@ async fn handle_factory_dependencies( for artifact in artifacts.into_iter().map(|(_, artifact)| artifact) { // Convert artifact to JSON to access factoryDependencies - let artifact_json = serde_json::to_value(&artifact)?; + let artifact_json = serde_json::to_value(artifact)?; if let Some(factory_deps) = artifact_json.get("factoryDependencies") { if let Some(deps_map) = factory_deps.as_object() { for (dep_hash, dep_name) in deps_map { From ff4e4d17c9a96da35d7abccd4fd02504bb42a278 Mon Sep 17 00:00:00 2001 From: soul022 Date: Mon, 14 Jul 2025 23:39:23 +0530 Subject: [PATCH 20/26] [issue-130] - factory dependency changed to btree --- crates/forge/src/cmd/create.rs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index bbb3350609310..1190099bca3bf 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -80,17 +80,9 @@ async fn handle_factory_dependencies( // Collect all factory dependencies from all contracts let mut all_dependencies = BTreeMap::new(); - for artifact in artifacts.into_iter().map(|(_, artifact)| artifact) { - // Convert artifact to JSON to access factoryDependencies - let artifact_json = serde_json::to_value(artifact)?; - if let Some(factory_deps) = artifact_json.get("factoryDependencies") { - if let Some(deps_map) = factory_deps.as_object() { - for (dep_hash, dep_name) in deps_map { - if let Some(name) = dep_name.as_str() { - all_dependencies.insert(dep_hash.clone(), name.to_string()); - } - } - } + for artifact in artifacts.clone().into_iter().map(|(_, artifact)| artifact) { + for (dep_hash, dep_name) in &artifact.factory_dependencies { + all_dependencies.insert(dep_hash.clone(), dep_name.clone()); } } From 90d955412852c98b6dde62c214a99e8d6d7fce3b Mon Sep 17 00:00:00 2001 From: soul022 Date: Wed, 16 Jul 2025 21:59:04 +0530 Subject: [PATCH 21/26] [issue - 130] - extensions changes --- crates/config/src/lib.rs | 1 + crates/linking/src/lib.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 9d3d9b83bfbce..d7541ebedcf41 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1263,6 +1263,7 @@ impl Config { SolidityCompiler::Solc(self.solc_compiler()?) }, vyper: self.vyper_compiler()?, + extensions: Default::default(), }) } diff --git a/crates/linking/src/lib.rs b/crates/linking/src/lib.rs index 69080344e4459..e33e564097248 100644 --- a/crates/linking/src/lib.rs +++ b/crates/linking/src/lib.rs @@ -315,6 +315,7 @@ mod tests { .build(MultiCompiler { solidity: SolidityCompiler::Solc(SolcCompiler::Specific(solc)), vyper: None, + extensions: Default::default(), }) .unwrap(); From 1c8fe0a24b5c37c442af306a1b5f5ba934acac25 Mon Sep 17 00:00:00 2001 From: soul022 Date: Thu, 17 Jul 2025 23:51:13 +0530 Subject: [PATCH 22/26] [issue - 130] - extensions fixes --- crates/config/src/lib.rs | 1 - crates/forge/src/cmd/create.rs | 21 +++++++++++---------- crates/linking/src/lib.rs | 1 - 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index d7541ebedcf41..9d3d9b83bfbce 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -1263,7 +1263,6 @@ impl Config { SolidityCompiler::Solc(self.solc_compiler()?) }, vyper: self.vyper_compiler()?, - extensions: Default::default(), }) } diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 1190099bca3bf..8ddb3dbf298bc 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -24,9 +24,14 @@ use foundry_common::{ shell, }; use foundry_compilers::{ - artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, Artifact, ArtifactId, + artifacts::BytecodeObject, + info::ContractInfo, + utils::canonicalize, + Artifact, + ArtifactId, ProjectCompileOutput, }; +use foundry_compilers::artifacts::solc::Extensions; use foundry_config::{ figment::{ self, @@ -71,18 +76,14 @@ async fn handle_factory_dependencies( config: &Config, private_key: &str, ) -> Result<()> { - let artifacts: Vec<_> = output.artifacts().collect(); - - if artifacts.is_empty() { - return Ok(()); - } - // Collect all factory dependencies from all contracts let mut all_dependencies = BTreeMap::new(); - for artifact in artifacts.clone().into_iter().map(|(_, artifact)| artifact) { - for (dep_hash, dep_name) in &artifact.factory_dependencies { - all_dependencies.insert(dep_hash.clone(), dep_name.clone()); + for (_id, contract) in output.artifact_ids() { + if let Extensions::Resolc(extras) = &contract.extensions { + for (bytecode_hash, contract_name) in &extras.factory_dependencies { + all_dependencies.insert(bytecode_hash.clone(), contract_name.clone()); + } } } diff --git a/crates/linking/src/lib.rs b/crates/linking/src/lib.rs index e33e564097248..69080344e4459 100644 --- a/crates/linking/src/lib.rs +++ b/crates/linking/src/lib.rs @@ -315,7 +315,6 @@ mod tests { .build(MultiCompiler { solidity: SolidityCompiler::Solc(SolcCompiler::Specific(solc)), vyper: None, - extensions: Default::default(), }) .unwrap(); From 4c539534ebf7db123929ac8a58aa88ca00863464 Mon Sep 17 00:00:00 2001 From: soul022 Date: Thu, 17 Jul 2025 23:54:20 +0530 Subject: [PATCH 23/26] [issue - 130] - fmt fixes --- crates/forge/src/cmd/create.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 8ddb3dbf298bc..798a0d1bc2eff 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -23,15 +23,11 @@ use foundry_common::{ fmt::parse_tokens, shell, }; +use foundry_compilers::artifacts::solc::Extensions; use foundry_compilers::{ - artifacts::BytecodeObject, - info::ContractInfo, - utils::canonicalize, - Artifact, - ArtifactId, + artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, Artifact, ArtifactId, ProjectCompileOutput, }; -use foundry_compilers::artifacts::solc::Extensions; use foundry_config::{ figment::{ self, @@ -110,9 +106,9 @@ async fn handle_factory_dependencies( let scaled_encoded_bytes = bytecode.encode(); let storage_deposit_limit = Compact(10000000000u128); let encoded_storage_deposit_limit = storage_deposit_limit.encode(); - let combined_hex = "0x3c04".to_string() + - &hex::encode(&scaled_encoded_bytes) + - &hex::encode(&encoded_storage_deposit_limit); + let combined_hex = "0x3c04".to_string() + + &hex::encode(&scaled_encoded_bytes) + + &hex::encode(&encoded_storage_deposit_limit); let _tx_hash = upload_child_contract_alloy( rpc_url.as_ref(), From 2dc06155f20cfc81f1f0e578c5b82cc29744f488 Mon Sep 17 00:00:00 2001 From: Sebastian Miasojed Date: Tue, 16 Sep 2025 15:22:20 +0200 Subject: [PATCH 24/26] Fmt --- crates/forge/src/cmd/create.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 308b2acddbfd6..6de235b73f3ca 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -24,8 +24,10 @@ use foundry_common::{ shell, }; use foundry_compilers::{ - artifacts::{ArtifactExtras, BytecodeObject}, info::ContractInfo, utils::canonicalize, Artifact, ArtifactId, - ProjectCompileOutput, + artifacts::{ArtifactExtras, BytecodeObject}, + info::ContractInfo, + utils::canonicalize, + Artifact, ArtifactId, ProjectCompileOutput, }; use foundry_config::{ figment::{ @@ -107,9 +109,9 @@ async fn handle_factory_dependencies( let scaled_encoded_bytes = bytecode.encode(); let storage_deposit_limit = Compact(10000000000u128); let encoded_storage_deposit_limit = storage_deposit_limit.encode(); - let combined_hex = "0x3c04".to_string() - + &hex::encode(&scaled_encoded_bytes) - + &hex::encode(&encoded_storage_deposit_limit); + let combined_hex = "0x3c04".to_string() + + &hex::encode(&scaled_encoded_bytes) + + &hex::encode(&encoded_storage_deposit_limit); let _tx_hash = upload_child_contract_alloy( rpc_url.as_ref(), From c36e11c1750f356ac4b955a3f84aded0229fa943 Mon Sep 17 00:00:00 2001 From: Sebastian Miasojed Date: Wed, 17 Sep 2025 15:27:20 +0200 Subject: [PATCH 25/26] use factory deps when revive build is enabled --- crates/forge/src/cmd/create.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/forge/src/cmd/create.rs b/crates/forge/src/cmd/create.rs index 6de235b73f3ca..e50fd206e2805 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -68,7 +68,7 @@ fn find_contract_by_hash(output: &ProjectCompileOutput, target_hash: &str) -> Op } /// Handles factory dependencies for a contract deployment -async fn handle_factory_dependencies( +async fn upload_factory_dependencies( output: &ProjectCompileOutput, config: &Config, private_key: &str, @@ -292,10 +292,10 @@ impl CreateArgs { let provider = utils::get_provider(&config)?; // Handle factory dependencies before deploying the main contract - if self.broadcast { + if self.broadcast && self.build.compiler.resolc_opts.resolc_compile.unwrap_or_default() { let private_key = self.eth.wallet.raw.private_key.clone().ok_or_eyre("Private key not provided")?; - handle_factory_dependencies(&output, &config, &private_key).await?; + upload_factory_dependencies(&output, &config, &private_key).await?; } // respect chain, if set explicitly via cmd args From c58e5257f57ebe89ea787305710355e473964d46 Mon Sep 17 00:00:00 2001 From: Sebastian Miasojed Date: Fri, 19 Sep 2025 13:00:05 +0200 Subject: [PATCH 26/26] Add forge create tests --- crates/forge/tests/cli/revive_create.rs | 88 +++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/crates/forge/tests/cli/revive_create.rs b/crates/forge/tests/cli/revive_create.rs index 8c127cc3fc419..dc4c769f9677c 100644 --- a/crates/forge/tests/cli/revive_create.rs +++ b/crates/forge/tests/cli/revive_create.rs @@ -115,6 +115,68 @@ constructor(Point[] memory _points) {} "src/TupleArrayConstructorContract.sol:TupleArrayConstructorContract".to_string() } +fn setup_with_factory_pattern(prj: &TestProject) -> String { + prj.add_source( + "Child.sol", + r#" +pragma solidity ^0.8.20; + +contract Child { + uint256 public x; + + constructor() { + x = 1; + } +} +"#, + ) + .unwrap(); + prj.add_source( + "Factory.sol", + r#" +pragma solidity ^0.8.20; + +import "./Child.sol"; + +contract Factory { + constructor() { + new Child(); + } +} +"#, + ) + .unwrap(); + + "src/Factory.sol:Factory".to_string() +} + +fn setup_with_library(prj: &TestProject) -> String { + prj.add_source( + "Library.sol", + r#" +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity >=0.7.0 <0.9.0; + +library Assert { + function equal(uint256 a, uint256 b) internal pure returns (bool result) { + result = (a == b); + } +} + +contract TestAssert { + function checkEquality(uint256 a, uint256 b) public pure returns (string memory) { + Assert.equal(a, b); + return "Values are equal"; + } +} +"#, + ) + .unwrap(); + + "src/Library.sol:TestAssert".to_string() +} + /// configures the `TestProject` with the given closure and calls the `forge create` command fn create_on_chain( network_args: Option>, @@ -246,3 +308,29 @@ forgetest_serial!(can_create_with_constructor_args_on_polkadot_localnode, |prj, ); } }); + +forgetest_serial!(can_create_with_factory_deps_on_polkadot_localnode, |prj, cmd| { + if let Ok(_node) = tokio::runtime::Runtime::new().unwrap().block_on(PolkadotNode::start()) { + create_on_chain( + localnode_args(), + None, + prj, + cmd, + setup_with_factory_pattern, + CREATE_RESPONSE_PATTERN, + ); + } +}); + +forgetest_serial!(can_create_with_library_deps_on_polkadot_localnode, |prj, cmd| { + if let Ok(_node) = tokio::runtime::Runtime::new().unwrap().block_on(PolkadotNode::start()) { + create_on_chain( + localnode_args(), + None, + prj, + cmd, + setup_with_library, + CREATE_RESPONSE_PATTERN, + ); + } +});