Skip to content

Conversation

soul022
Copy link

@soul022 soul022 commented Jun 26, 2025

Motivation

Solution

This change enhances forge create on Substrate-based networks by automatically detecting, encoding, and uploading any factory-declared child contracts before instantiating the main contract:

New dependencies
Adds hex = "0.4.3" and parity-scale-codec (with derive) in crates/forge/Cargo.toml to support SCALE encoding and hex utilities.

find_contract_by_hash helper
Implements a utility in create.rs that scans compiled artifacts, computes each bytecode’s Keccak-256 hash, and returns the matching raw bytes to locate dependencies at runtime.

handle_factory_dependencies flow
Introduces an async routine in create.rs that:

  • Reads each artifact’s factoryDependencies map.
  • Filters out proxy “child” contracts.
  • ABI-encodes each dependency’s bytecode and a Compact storage limit, prefixes with the EIP-1167 marker, and uploads via the Alloy RPC.
  • Ensures all dependencies are on-chain before deploying the main contract.

Cleanup & lockfile update
Imports Compact, Encode, and BTreeMap, removes unused code, and bumps Cargo.lock for the new crates.

With this PR, users can simply declare child-contract dependencies in their artifacts and rely on forge create to handle the full upload and instantiation sequence automatically.

PR Checklist

  • Added Tests
  • Added Documentation
  • Breaking changes

Test Evidance

Contract Tested -
https://gist.github.com/soul022/268ff439b8381bb02839161121066b0a

Logs -
https://gist.github.com/soul022/63b440a61e442ff57081132afdd2da09

closes: #130

@soul022 soul022 changed the title [WIP] - Factory Dependecy fix Support for Factory Contract Dependency in forge create Jul 6, 2025
@soul022 soul022 linked an issue Jul 6, 2025 that may be closed by this pull request
@soul022 soul022 marked this pull request as ready for review July 6, 2025 16:59
@filip-parity
Copy link

High level comments:

  • The pull requests mentions that tests were added, but no test code is included in the diff...
  • The find_initialized_contracts_with_paths function has a good docstring, but other new functions (e.g., find_contract_initializations, upload_child_contract_alloy) lack documentation.

@smiasojed
Copy link
Collaborator

Thank you, @soul022, for this pull request.

Could you please link this PR to the relevant GitHub issue? You can do this by adding closes: #<issue_number> in the description.

Regarding the parsing of factory dependencies from Solidity code: The idea from the ticket description was that this information would be available directly in the resolc compilation output in factoryDependencies field. Could you clarify why parsing it from the Solidity code is necessary in this context?

@soul022
Copy link
Author

soul022 commented Jul 7, 2025

closes: #130

@soul022
Copy link
Author

soul022 commented Jul 7, 2025

Thank you, @soul022, for this pull request.

Could you please link this PR to the relevant GitHub issue? You can do this by adding closes: #<issue_number> in the description.

Regarding the parsing of factory dependencies from Solidity code: The idea from the ticket description was that this information would be available directly in the resolc compilation output in factoryDependencies field. Could you clarify why parsing it from the Solidity code is necessary in this context?

Linked it to issue.
Changed code to read bytecode from compiler output, instead of parsing it out, please check.

@soul022
Copy link
Author

soul022 commented Jul 7, 2025

  • upload_child_contract_alloy

added comments to upload_child_contract_alloy, removed find_contract_initializations based on new logic

}

/// Get child contracts from compiled output
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.

.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

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.

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

Choose a reason for hiding this comment

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

It should be based on node metadata. Pallet index may be different between runtimes

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

@smiasojed smiasojed Sep 19, 2025

Choose a reason for hiding this comment

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

Why do we need this check?

@smiasojed
Copy link
Collaborator

TODO:

  • use subxt with node metadata
  • get metadata through eth-proxy - API missing
  • add check if code is present on chain
  • get deposit limit instead hardcoding it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Factory contracts deployment support
5 participants