Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f96402d
wip
soul022 Jun 26, 2025
c01f6b2
[issue 130] - clean up
soul022 Jul 6, 2025
b6eb1e3
[issue 130] - fix fmt issues
soul022 Jul 6, 2025
c3be5c8
[issue 130] - fix fmt issues
soul022 Jul 6, 2025
f25cf41
[issue - 130] - fix clippy issues
soul022 Jul 6, 2025
869de53
Merge branch 'master' into soul022/factoryContracts
soul022 Jul 6, 2025
56bd04b
[issue - 130] - fix fmt issues
soul022 Jul 6, 2025
850206f
[issue - 130] - fix fmt issues
soul022 Jul 6, 2025
0efe3a4
[issue - 130] - fix clippy issues
soul022 Jul 6, 2025
4543076
[issue - 130] - updated code to get bytecode contract from compiler o…
soul022 Jul 7, 2025
b6328da
[issue - 130] - rustfmt fixes
soul022 Jul 7, 2025
8b65934
[issue - 130] - added comments to upload_child_contract_alloy
soul022 Jul 7, 2025
cccbe25
[issue - 130] - rustfmt fixes
soul022 Jul 7, 2025
bb249c9
[issue - 130] - remove internal libraries used as child contracts
soul022 Jul 8, 2025
0552ce5
[issue - 130] - clippy fix
soul022 Jul 8, 2025
64fa04d
[issue - 130] - reading factory contract from compilers
soul022 Jul 13, 2025
3411a24
[issue - 130] - remove unused code
soul022 Jul 13, 2025
af8036c
[issue - 130] - fix fmt issues
soul022 Jul 13, 2025
053f1e4
[issue - 130] - fix fmt issues
soul022 Jul 13, 2025
145f6da
[issue - 130] - fix fmt issues
soul022 Jul 13, 2025
ff4e4d1
[issue-130] - factory dependency changed to btree
soul022 Jul 14, 2025
90d9554
[issue - 130] - extensions changes
soul022 Jul 16, 2025
1c8fe0a
[issue - 130] - extensions fixes
soul022 Jul 17, 2025
4c53953
[issue - 130] - fmt fixes
soul022 Jul 17, 2025
f06ccf9
Merge branch 'master' into soul022/factoryContracts
soul022 Jul 17, 2025
05fbbcc
Merge remote-tracking branch 'origin/master' into soul022/factoryCont…
smiasojed Sep 16, 2025
2dc0615
Fmt
smiasojed Sep 16, 2025
c36e11c
use factory deps when revive build is enabled
smiasojed Sep 17, 2025
c58e525
Add forge create tests
smiasojed Sep 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 48 additions & 1 deletion crates/cli/src/utils/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -73,6 +73,53 @@ 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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In ResolcContract we have
pub factory_dependencies: Option<BTreeMap<String, String>>,
Is there any reason why you do not use it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no ResolcContract in workspace.
Also, Since I am already getting child contracts in pre-existing output object from compiler in
"let output: foundry_compilers::ProjectCompileOutput =
compile::compile_target(&target_path, &project, shell::is_json())?;"
I used it in our usecase

I have just looping through the artifacts from the compiled output to get the child contracts. Attaching below sample artifact array that we get from output, it has both child and parent contract details, get_child_contracts just extracts the child contracts from existing output -
https://gist.github.com/soul022/8f8dc0ef4c0811b05d382d7668f07e40

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will foundry behave when you compile the following code:

// 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";
    }
}

Will foundry upload the Assert library as a child contract?

What about the case where deploy time linking is being used - will your solution still work?

Copy link
Author

@soul022 soul022 Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is internal library, the compiler simply embeds its code directly into your contract. There’s no separate library contract on-chain and nothing extra to deploy or link at runtime.

My code to get child contracts was treating it as a contract and uploading it, I have modified code in get_child_contracts and added a check to filter the same, so it does not gets uploaded

Copy link
Author

@soul022 soul022 Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for deploy time linking i.e external library, it should work as is, As per foundry book -we need to deploy external library first and use linker options (https://paritytech.github.io/foundry-book-polkadot/reference/forge/forge-create.html#linker-options) to create the contract using that external library.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried testing linker option for forge create for below contract, but it failed. seems like linker option is not working in foundry-polkadot.

contract - https://gist.github.com/soul022/9ef77e6ac0cf10365b8949fd9bc310fa
logs - https://gist.github.com/soul022/eccb3fc929953e207adafcfd62cba1c9

Copy link
Collaborator

@smiasojed smiasojed Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is that the logic you're implementing already exists in the Revive compiler. I think we should find a way to make factory_dependencies available.
In the near future, we’ll probably also need to expose unlinked_dependencies for deploy time linking. WDYT?

@pkhry do you have any opinion on this? You have been looking into the linking already.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just add a field to solc contract artifact in foundry-compilers. factory-deps are already present, you just need to forward them. unlinked deps should probably forwarded the same way as a field on solc::Contract.

something like this below a la how metadata is currently forwarded:

struct Contract { 
... 
#[serde(flatten)] 
auxiliary: Option<BTreeMap<String, serde_json::Value>>,

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also yeah, not a fan of doing something manually if it's already done by the compiler.

output: ProjectCompileOutput,
parent_contract_name: &str,
) -> Result<Vec<ChildContract>> {
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?
Expand Down
4 changes: 4 additions & 0 deletions crates/forge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"] }
Expand Down
112 changes: 107 additions & 5 deletions crates/forge/src/cmd/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ 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},
utils::{self, read_constructor_args_file, remove_contract, LoadConfig},
utils::{self, get_child_contracts, read_constructor_args_file, remove_contract, LoadConfig},
};
use foundry_common::{
compile::{self},
Expand All @@ -34,7 +36,7 @@ use foundry_config::{
};
use serde_json::json;
use std::{borrow::Borrow, marker::PhantomData, path::PathBuf, sync::Arc, time::Duration};

use tracing::debug;
merge_impl_figment_convert!(CreateArgs, build, eth);

/// CLI arguments for `forge create`.
Expand Down Expand Up @@ -100,11 +102,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<String> {
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
Expand All @@ -120,7 +165,64 @@ 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())?;

match get_child_contracts(output.clone(), &self.contract.name) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this introduces new codepath even for solidity contracts, ie should break compat between resolc and non resolc versions.

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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we first check if the contract is already present on-chain?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only possible way that I am aware to check if contract is uploaded/exists is to check if storage deposit limit value is 0 using revive api dynamically, there is no possible way to do it right now as it needs substrate node url.

Copy link
Author

@soul022 soul022 Jul 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the process is already optimized, it won't take up any funds if we try to upload already existing/uploaded contract

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgherveou Do you plan to allow for such a check, or is the current approach good enough in your opinion?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should, you would still pay for a transaction that does nothing otherwise
let me look into it

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry didn't look at this, I think you could check that the code exist using eth_getCode first, that should fix it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pgherveou , seems like when uploading via magic address, it is not returning contract address, without it we cannot call eth_getCode.

{
"status": "0x1",
"cumulativeGasUsed": "0x0",
"logs": [],
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"type": "0x2",
"transactionHash": "0x2889879e6939bc6a7fde7eb2392329961a9681feb4b60b0ccc3a358f01315bf8",
"transactionIndex": "0x2",
"blockHash": "0xa0f9fd2baca909f5839587037a11203b9831b5401dcb59fdadb61fb406178904",
"blockNumber": "0xa03df",
"gasUsed": "0x57c8d1df958a",
"effectiveGasPrice": "0x4b0",
"from": "0xf24ff3a9cf04c71dbc94d0b566f7a27b94566cac",
"to": "0x6d6f646c70792f70616464720000000000000000",
"contractAddress": null
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my bad I thought you had an address already, yeah this just upload the code without instantiating it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smiasojed @pgherveou, based on the last discussion, to avoid extra transactions for already uploaded bytecode, we probably need some precompile/check, which would determine if the given bytecode is already present on-chain. For now, @pgherveou suggested to move ahead and this can be picked sooner.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly this would be a blocker to merge it, as it incurs extra cost for user

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)?;

Expand Down
Loading