0

As the title suggests, I'm not sure what is considered to be good practice when it comes to deploying smart contracts that interact with each-other.

My example: migrate contract A. Contract A creates new instance of Contract B in constructor. Contract B creates new instance of contract C in constructor. Contract C created new instance of contract D in constructor.

This chained relationship is important for my contracts as they rely on the ownership structure to execute my design pattern.

When attempting to migrate contract A over in truffle I am getting: "ran out of gas (using a value you set in your network config or deployment parameters.)

  • Block limit: 6721975 (0x6691b7)
  • Gas sent: 6721975 (0x6691b7)"

I guess my question is, is this pattern madness?

The block limit in the error scares me as I'm not sure if I will encounter this when migrating to mainnet?

Is this chaining of contracts adding to my gas costs in an exponential manner that could otherwise be avoided by using a different pattern?

1

It's not madness. On-chain deployment patterns are one way to establish trust between separate contracts. But, you are running into a classic problem.

When a contract A deploys another contract B, the bytecode for contract B gets rolled up into the bytecode for contract A. This is so contract A knows what to do.

Reducing the size of the deployed contract is one of the reasons one might want to split logic into separate modules, but the roll-up defeats the purpose. In a chain like you describe, the total bytecode of all the contracts will end up in contract A and it might be too expensive to deploy, i.e. > 25K bytes.

A solution to this problem is to use a factory pattern. Contract B deployed by BFactory, C by CFactory and so on. Deploy the factories separately and pass their addresses to the contract that will own the deployed contracts. Use an interface to describe the deployment functions so the whole bytecode doesn't wind up the contract that needs the factory output.

Here's a sketchy idea to get you thinking about how it might work in your case.

interface IFactoryB {
  function deployB() external;
}

interface IB {
  function doSomething() external;
}

// Inheritance helps catch developer errors at compile time 
// You want to catch any difference between implementation and interface description

contract B is IB {
  
  function doSomething() public {
    // ... heavy code
  }
}

contract BFactory is IFactoryB { 

  function deployB() public returns(IB) {
    B b = new B(); // this is what tells the compiler to compile and include B
    b.transferOwnership(msg.sender); // same result as if A deploys an Ownable contract
    return IB(address(b));

  }

contract A { // does not need B's bytecode, only IB which is much smaller

  constructor(address factoryB) {
    IB b = IFactoryB(factoryB).deployB(); // this tells the compiler to include IB and IFactoryB
    // can now interact with b's functions
    // there is no reason at all to include B's bytecode in A because 
    // calls to B are always external to A
  }
}

In summary, use interfaces to control bytecode size and use your judgment to organize the code into separate concerns.

Hope it helps

1
  • 1
    Thanks for your response Rob! The solution I ended up going with was A deploys B. C deploys D where I pass in the address of B into C's constructor to complete the chain. I had not thought about using Interfaces however! so this is definitely something I will look into. Thanks again – genzr Mar 1 at 8:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.