Initializing inherited `private immutable` variables when cloning a contract?


I built and extended several smart contracts leveraging the wonderful openzeppelin contracts library whenever possible. Thank you for providing this great resource!

Now, I want to make deployments cheaper by creating clones instead of standalone contracts. Upgradeability is not needed. Initializing the clones is necessary though, which is why I created initializer functions. This works well, except for when the contracts I inherit from declare private or immutable variables, like the ERC2771Context's _trustedForwarder. As expected, I am unable to access or initialize this variable. My attempt fails with "DeclarationError: Undeclared identifier."

So my question is: what can I do here? Is there any way to still leverage inheritance from contracts declaring private or immutable variables when using clones or other proxies? Or do I have to rewrite all contracts I inherit from when I need clones?

Why not just pass the value (of the private immutable variable) upon the construction of your contract?

Hi @barakman, I totally do that when creating the logic contract and it works well. But when cloning a contract, the new instance is created without constructor execution. Therefore, an initializer function is needed to do the tasks that the constructor would usually do. And this where my troubles start, because:

  • immutable variables can only be set in the constructor
  • private variables can not be accessed in related contracts

Can you please share the exact details (i.e., a coding example)?

Sure, thanks for asking, @barakman. Sorry I didn't provide exact links: Forum rules limited my first post to 2 links.
Here is the clone factory I wrote.
Here I create a new clone.

When running the clone creation test, it fails because of the private declaration:

$ ETH_RPC_URL=$ETH_RPC_URL forge test --match-path test/DssVestClone.t.sol -vvvvv
[⠢] Compiling...
[⠑] Compiling 8 files with 0.8.17
[⠘] Solc 0.8.17 finished in 401.87ms
Compiler run failed
error[7576]: DeclarationError: Undeclared identifier. Did you mean "trustedForwarder"?
   --> src/DssVest.sol:133:9:
133 |         _trustedForwarder = trustedForwarder; 
    |         ^^^^^^^^^^^^^^^^^

Here is the initializer that works.
Here is the one that doesn't.

If all of your clone instances will share the same trusted forwarder, you can initialize the immutable variable in the logic contract and it will be automatically shared by the clones. For example, you might write the following:

    constructor (address trustedForwarder) ERC2771Context(trustedForwarder) {

     * todo: decide on how to treat initializer inheritance. This initializer is not protected by  "initializer" modifier, but should be
    function initialize(address _ward) public {
        wards[_ward] = 1;
        emit Rely(_ward);

You have both a constructor and an initializer, and the constructor is what initializes the immutable variable.

You have to be careful with this approach because not every contract in @openzeppelin/contracts can be used in this way, in fact most can't. In this particular case it's ok, and you would know this because when you deploy it using OpenZeppelin Upgrades Plugins you will not see an error.

If your clone instances will use different trusted forwarders it's a little tricky in this case. You can't use immutable variables, so our ERC2771Context will not work for you. You would have to "fork" the code, that is to copy-paste it in your project and remove the immutable keyword and change the constructor into an initializer.

Thank you, @frangio!
immutable variables are not really variables, instead their value is copied into the contract's bytecode. Like you said, this makes changing the forwarder impossible. Luckily in my case, I do not need to replace it - all instances should use the same forwarder anyway :slight_smile:
This solved my problem.