What's the difference between upgradable and non upgradable Initializable contracts? Aren't all proxies meant to be support upgradable implementation contract?

What's the difference between upgradable and non upgradable proxies contracts?

@openzeppelin/contracts/proxy/utils/Initializable.sol

vs

@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol

Why would anyone use proxies that do not support upgradable contracts? Instead, just create contract without a proxy, doesn't that save the trouble?

// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";

contract Test is Initializable {
    address public owner;

    constructor() public {
        owner = msg.sender;
    }
    function initialize()
        public
        initializer
    {
    // logic
    }
}

Aren't all proxies meant to support upgradable implementation contracts??

What happens if I use @openzeppelin/contracts/proxy/utils/Initializable.sol instead of @openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol

Another question is since it uses a non-upgradable contract, is it still susceptible to the constructor caveat? Openzeppelin Docs

Hey @eeshenggoh,

Nothing. It's reexported for convenience since there are use cases for it in both versions of the library.

Aren't all proxies meant to support upgradable implementation contracts??

No. A proxy is only meant to delegate all calls to an implementation. The reasons for doing so can range from upgradeable to clones.

Another question is since it uses a non-upgradable contract, is it still susceptible to the constructor caveat? Openzeppelin Docs

Seems like you're asking this question assuming there's a difference with the Initializer, so there's no answer. However, in the case of using Clones, each clone needs to be initialized separately so the constructor caveat applies to them as well.

Hi, thanks for the clarification!
I have another question.
Let's say that you were to create an upgradable contract.

  1. Is this susceptible to the constructor caveat? What if I create a getter method for the variables count and immutable_number is it still susceptible?

  2. If a user were to call through the proxy, are both count and immutable_number able to return the value set in constructor? Must they be set in the initialize function?

    address public owner;
    unit256 public count;
    uint256 internal immutable immutable_number;

   constructor(
        uint256 _immutable_number,
        unit256 _count;
    ) {
        immutable_number= _immutable_number;
        count= _count; 
        initialize(msg.sender);
    }

    function initialize(address _owner)
        public
        initializer
    {
     address owner  = _owner;
    }

Every contract will run its constructor at construction.

When you deploy an implementation contract, it will run the constructor only once in its execution context. Every proxy pointing to it will need to execute the initialize function.

I suggest you to study the differences between CALL and DELEGATECALL and it should be clear.

  1. Is this susceptible to the constructor caveat? What if I create a getter method for the variables count and immutable_number is it still susceptible?

The getters will be both at the implementation and proxy contracts. On the former, the values will be those you set up in construction, while on the latter, the values will be 0 because the execution context of the proxy is different. In such context, these values haven't been initialized.

In other words, the Proxy will delegate the logic to the implementation but it has no way to run its construction (constructor code is removed after deployment, the deployed bytecode doesn't even include it). In summary, every proxy needs to run an initializer, and there's no way to use the implementation constructor.

If a user were to call through the proxy, are both count and immutable_number able to return the value set in constructor? Must they be set in the initialize function?

They must be set at initialization

1 Like

If user were to get count directly or via proxy, will it be the same value set during construction?

Since the it is not stored in proxy, what if I was to set it via proxy or directly via implementation contract?

address public owner;
    unit256 public count;
    uint256 internal immutable immutable_number;

   constructor(
        uint256 _immutable_number,
        unit256 _count;
    ) {
        immutable_number= _immutable_number;
        count= _count; 
        initialize(msg.sender);
    }

    function initialize(address _owner)
        public
        initializer
    {
     address owner  = _owner;
    }
   function set(unit _count) external { //here
    unit count = _count;
    }

Each contract has its own execution context:

  • Implementation will run its constructor on its own context (changing its own variables). It can't do anything to the proxy.
  • Proxy constructor will run it its own context too, meaning that each proxy will run a constructor if it has one, but initializers are recommended. A constructor in the proxy is an antipattern imo. Every variable set (even using the initializer) will apply only to the constructor storage. It can't do anything to the implementation.