diff --git a/Cargo.lock b/Cargo.lock index 8bed973b9cfcb..efef26c7faed4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6225,12 +6225,14 @@ dependencies = [ "foundry-wallets", "futures", "globset", + "hex", "indicatif", "inferno", "itertools 0.14.0", "lazy_static", "mockall", "opener", + "parity-scale-codec", "parking_lot 0.12.3", "paste", "path-slash", diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index 9f67e49927bf3..47a548639c384 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -57,6 +57,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" @@ -85,6 +86,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 5cffad23b9141..e50fd206e2805 100644 --- a/crates/forge/src/cmd/create.rs +++ b/crates/forge/src/cmd/create.rs @@ -10,7 +10,9 @@ use alloy_serde::WithOtherFields; use alloy_signer::Signer; use alloy_transport::TransportError; use clap::{Parser, ValueHint}; -use eyre::{Context, Result}; + +use codec::{Compact, Encode}; +use eyre::{Context, OptionExt, Result}; use forge_verify::{RetryArgs, VerifierArgs, VerifyArgs}; use foundry_cli::{ opts::{BuildOpts, EthereumOpts, EtherscanOpts, TransactionOpts}, @@ -22,7 +24,10 @@ use foundry_common::{ shell, }; use foundry_compilers::{ - artifacts::BytecodeObject, info::ContractInfo, utils::canonicalize, ArtifactId, + artifacts::{ArtifactExtras, BytecodeObject}, + info::ContractInfo, + utils::canonicalize, + Artifact, ArtifactId, ProjectCompileOutput, }; use foundry_config::{ figment::{ @@ -33,10 +38,99 @@ 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 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); + } + } + } + } + None +} + +/// Handles factory dependencies for a contract deployment +async fn upload_factory_dependencies( + output: &ProjectCompileOutput, + config: &Config, + private_key: &str, +) -> Result<()> { + // Collect all factory dependencies from all contracts + let mut all_dependencies = BTreeMap::new(); + + for (_id, contract) in output.artifact_ids() { + if let ArtifactExtras::Resolc(extras) = &contract.extensions { + if let Some(factory_dependencies) = &extras.factory_dependencies { + for (bytecode_hash, contract_name) in factory_dependencies { + all_dependencies.insert(bytecode_hash, contract_name); + } + } + } + } + + 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 { @@ -100,11 +194,54 @@ 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, + encoded_bytes: String, +) -> Result { + use alloy_primitives::{Address, U256}; + use alloy_provider::Provider; + use alloy_rpc_types::TransactionRequest; + use alloy_serde::WithOtherFields; + use alloy_signer_local::PrivateKeySigner; + use foundry_common::provider::ProviderBuilder; + use std::str::FromStr; + + // This wallet will be used to sign the deployment transaction + let wallet = PrivateKeySigner::from_str(&private_key)?; + + // 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))?; + + // 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) + .input(input_bytes.into()) + .value(U256::from(0u64)); + + // 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()) +} + impl CreateArgs { /// Executes the command to create a contract pub async fn run(mut self) -> Result<()> { let mut config = self.load_config()?; - // 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,9 +257,10 @@ 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())?; - 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, @@ -153,6 +291,13 @@ impl CreateArgs { let provider = utils::get_provider(&config)?; + // Handle factory dependencies before deploying the main contract + 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")?; + upload_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 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, + ); + } +});