How to implement a non-upgradeable proxy with constructor/initializer calls that can have varying signatures?

I have an upgradeable proxy contract that looks like the below:

pragma solidity 0.8.15;

// SPDX-License-Identifier: MIT
import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";

contract MyContract is TransparentUpgradeableProxy {
  constructor(address _logic, address admin, bytes memory data) TransparentUpgradeableProxy(_logic, admin, data) {}
}

I want to remove the upgradeability feature because it is not needed.

I want to retain the functionality of being able to create the proxy contracts with different implementations and different signatures for the initialize() function that gets called in the proxy implementation.

I can see that a non-upgradeable minimal clone is available in "@openzeppelin/contracts/proxy/Clones.sol", but that doesn't have the same feature of calling an initialize function with (_logic, admin, data) like the TransparentUpgradeableProxy does, so I'd have to code that all myself.

Perhaps it will be easiest to retain the upgradeable contracts just as they are, and I just make sure nobody can call .upgradeToAndCall(implementation, data) any time after the first deployment of the proxy (which is very easy to do).

Many thanks for any thoughts.

1 Like

Hey @NethDote, if I'm understanding correctly, you're trying to make clones of a single implementation.

This pattern is usually called a "Factory" and can be accomplished by using the Clones library with an upgradeable contract although it's not required to be upgradeable. The reason for using an upgradeable contract behind for clones is that it allows initializing the contract in the way you're trying to make.

The code for creating a token factory that uses Clones and leverages ERC20Upgradeable's initializer:

// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import "@openzeppelin/contracts/proxy/Clones.sol";
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";

contract Token is ERC20Upgradeable {
    constructor() {
        _disableInitializers();
    }

    function initialize(
        string memory name_,
        string memory symbol_
    ) public initializer {
        __ERC20_init(name_, symbol_);
    }
}

contract Factory {
    Token private _token;

    event TokenCreated(address indexed token, string name, string symbol);

    constructor() {
        _token = new Token();
    }

    function create(
        string memory name,
        string memory symbol
    ) external returns (Token) {
        Token token = Token(Clones.clone(address(_token)));
        token.initialize(name, symbol);
        emit TokenCreated(address(token), _token.name(), _token.symbol());
        return token;
    }
}

And a Foundry test:

// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import "forge-std/Test.sol";
import "../src/Factory.sol";

contract FactoryTest is Test {
    Factory private _factory;

    function setUp() public {
        _factory = new Factory();
    }

    function testMultipleClones() public {
        // Clone A
        Token a = _factory.create("A", "a");

        // Clone B
        Token b = _factory.create("B", "b");

        // Assertions
        assertNotEq(address(a), address(b));
        assertEq(a.name(), "A");
        assertEq(b.symbol(), "b");
        assertEq(a.name(), "A");
        assertEq(b.symbol(), "b");
    }
}

In case you want to make the clones actually upgradeable as well, you may want to take a look to the BeaconProxy pattern where a single upgradeable implementation lives in an UpgradeableBeacon that allows upgrading all dependant clones.

I can see that a non-upgradeable minimal clone is available in "@openzeppelin/contracts/proxy/Clones.sol", but that doesn't have the same feature of calling an initialize function with (_logic, admin, data) like the TransparentUpgradeableProxy does, so I'd have to code that all myself.

Would you mind clarifying this? There's no reason for having an initialize function in the proxy, perhaps you're referring to the constructor but I don't see how that'd be an issue

Perhaps it will be easiest to retain the upgradeable contracts just as they are, and I just make sure nobody can call .upgradeToAndCall(implementation, data) any time after the first deployment of the proxy (which is very easy to do).

Every upgradeable pattern we support has its own way of ensuring nobody can call the upgradeToAndCall function arbitrarily.

  • TransparentUpgradeableProxy: It requires a ProxyAdmin that (after oz contracts 5.0) will be deployed with each transparent proxy, that instance is the only address able to upgrade and it's an ownable contract
  • BeaconProxy: It requires an UpgradeableBeacon that's also ownable and follows a similar pattern to the ProxyAdmin.
  • UUPS: This is the most flexible pattern since the upgrade functions live in the implementation and you need to make sure to keep permissions on each upgrade.

My recommendation is to take a look to our upgrades plugins, where you can find documentation and tools to deploy upgradeable contracts securely without much hassle. Also you can find examples of how to use with a Beacon proxy.

Hi Ernesto,

Thanks, that's really helpful. Some clarifications:

No, the goal is to be able to make clones of different implementations, including implementations not written yet that may have different constructor/initialization requirements.

In my design, the caller of the factory is responsible for passing into the factory an implementation address and a "data" field containing encoded data for a function call that will "initialize(params)" on the implementation, with all the correct parameters there.

So the factory needs to clone the given implementation, then basically does a .delegatecall(data) to do the initialization, trusting that the caller has passed a correct "data" field.

I've got this all working fine, using the OZ TransparentUpgradeableProxy contract. This works and gives me the functionality I'm after - the ability to clone any implementation and then call an initialize function on it.

The constraint is that I actively don't want upgradeability.

This is what leads to my concern. When I send this factory contract off to the auditors, they will say "why are you spending gas deploying upgradeable functionality when you're not using it".

No, the goal is to be able to make clones of different implementations, including implementations not written yet that may have different constructor/initialization requirements.

Then I'd say the use case is the same but the code is even more simple, look:

contract CloneFactory {
    event CloneCreated(address indexed clone);
    
    /// @notice This function assumes `implementation` is trusted. Do not
    /// copy and paste if the implementation can reenter the factory or any
    /// other contract in your system in a malicious way
    function create(
        address implementation,
        bytes calldata initializationData
    ) external returns (address) {
        address clone = Clones.clone(implementation);
        Address.functionCall(clone, initializationData);
        emit CloneCreated(clone);
        return clone;
    }
}

I think your hunch is correct, and you shouldn't be used the upgradeable proxy if your contract is not upgradeable, however, it requires to be initializable.

Remember all upgradeable contracts are initializable but not all initializable contracts are upgradables. The clones are the perfect example for this distinction.

1 Like

Awesome, thanks Ernesto.

And just so I am clear then: in the implementations, I can now safely use "Pausable.sol" and "ERC20.sol". I can stop using "PausableUpgradeable.sol" and "ERC20Upgradeable.sol" because they are only necessary when upgrades happen.

You're welcome!

I think you do need to use the Upgradeable versions because those are the ones that are initializable. In reality, the upgradeability needs to be enabled by using a proxy (or manually adding the upgradeability functions with UUPS), so by using ERC20Upgradeable as an implementation for Clones, each clone wouldn't be upgradeable simply because there's no mechanism to do so.

What you can skip is the Proxy, but I'm afraid you can't skip using the Upgradeable versions unless you manually change the code of the nonupgradeable contracts, but we don't recommend doing so for security reasons.

1 Like

Hey Ernesto,

Thanks for all your help so far, one more quick question.

I have executed this factory code:

  address clone = Clones.clone(implementation);
  Address.functionCall(clone, initializationData);
  emit CloneCreated(clone);

Then I go into Remix, and I paste in the source code of the implementation that was used. Then I compile it. Then I go to the Remix "Deploy and run transactions" tab, and I paste in the address of the CloneCreated next to the Remix "At Address" button.

If I do that, then I should be able to interact with my clone contract in Remix, yes? That's what I think should be working, but it isn't, I get "transaction reverted" every time I try to interact with the clone.

Ignore above, it does work exactly as I described in Remix.

(I was pasting in the wrong address)

1 Like

Hi, I need some help with my question, in regards to purpose of using non-upgradeable vs upgradeable proxies? Why would anyone wants to use non-upgradeable proxies?

Hey @eeshenggoh, perhaps the term "non upgradeable" proxy is not the best here, so let's refer to them as just Clones.

The Clones is a special type of Proxy standardized in EIP-1167 and offered in OpenZeppelin Contracts. As any other proxy, its purpose is to forward all requests to an implementation using delegatecall but it does so in a way in which deployment is cheaper.

The common architecture is to have a single upgradeable initializable contract deployed as the "master" (and only) implementation, and then you deploy clones pointing to this implementation. Each Clone is initialized individually.

Why would anyone wants to use non-upgradeable proxies?

If you want to deploy the same contract multiple times for different users, using a Clone (or Minimal Proxy) is perhaps the best way to do it. Etherscan also supports them so any deployed Clone will show the implementation code if verified.

1 Like