Immutable variables and Upgradable contracts

I’m trying to understand something about upgradable contracts that I can’t quite wrap my head around. I found an upgradable contract using a transparent proxy pattern, where the implementation contract uses immutable variables set in the constructor.

From what I understand, after an upgrade, the proxy wouldn't be able to read the new implementation’s immutable variables if they have changed. However, I’m not sure which part of the process would be causing the issue.

Here’s a minimal example


pragma solidity ^0.8.25;

import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";

contract CustomProxy is ERC1967Proxy {
    address private immutable _admin;

    error ErrorProxy();

    constructor(address logic, bytes memory data) ERC1967Proxy(logic, data) {
        _admin = msg.sender;
        ERC1967Utils.changeAdmin(_proxyAdmin());
    }

    function upgradeToAndCall(address newImplementation, bytes calldata data) external {
        if (msg.sender != _proxyAdmin()) {
            revert ErrorProxy();
        }

        ERC1967Utils.upgradeToAndCall(newImplementation, data);
    }

    function _proxyAdmin() internal view returns (address) {
        return _admin;
    }
}

Implementation 1 :

pragma solidity ^0.8.25;

abstract contract StorageModuleV1 {
    uint256 public immutable VAR_ONE;
    uint256 public immutable VAR_TWO;

    constructor(uint256 varOne, uint256 varTwo) {
        VAR_ONE = varOne;
        VAR_TWO = varTwo;
    }
}

contract CustomLogicV1 is StorageModuleV1 {
    constructor(uint256 varOne, uint256 varTwo)
        StorageModuleV1(varOne, varTwo)
    {}

    function getVars() external view returns (uint256, uint256) {
        return (VAR_ONE, VAR_TWO);
    }
}

Implementation 2:

pragma solidity ^0.8.25;

abstract contract StorageModuleV2 {
    uint256 public immutable NEW_VAR_ONE;
    uint256 public immutable NEW_VAR_TWO;

    constructor(uint256 newVarOne, uint256 newVarTwo) {
        NEW_VAR_ONE = newVarOne;
        NEW_VAR_TWO = newVarTwo;
    }
}

contract CustomLogicV2 is StorageModuleV2 {
    constructor(uint256 newVarOne, uint256 newVarTwo)
        StorageModuleV2(newVarOne, newVarTwo)
    {}

    function getNewVars() external view returns (uint256, uint256) {
        return (NEW_VAR_ONE, NEW_VAR_TWO);
    }
}

From what I understand, the proxy wouldn’t be able to read the new implementation’s immutable variables if they are changed after an upgrade. But I don't know why..

  1. Either the proxy has access to the bytecode and immutable variables of CustomLogic version 1, but not CustomLogic version 2, for some reason?
  2. Or the constructor of CustomLogic version 2 cannot be called for some reason, meaning the immutable variables in the new implementation are never set during the upgrade?
  3. Or, the proxy somehow stores the immutable variables from CustomLogic version 1 in its state, making it impossible to change them after the upgrade?

I’m not sure which one it is, or if it’s something else entirely. Could anyone clarify how the proxy interacts with the immutable variables during an upgrade and why the new implementation’s immutable variables might be inaccessible?

1 Like

Hi, welcome to the community! :wave:

This is a good question!

I think immutable variables defined in the constructor are known at deployment, but not part of the deployed contract's runtime bytecode, and can not be modified afterward. So if you want to upgrade these values, you should move initialization logic from the constructor to an initializer function that can be called after upgrades, for more details, I think you can have a look at here:

Hi Skyge, immutable variables become part of the bytecode

The way they work is that at construction time, when you make an assignment to the immutable the value is replaced at the cote at every place it appears, so they become hardcoded

They are similar to constants but they value is only know at construction Time instead of compile time

1 Like

Given that proxies that upgrade their implementation can have new values because the value is stored at the code not in storage

2 Likes

Yes, you are right, immutable variables are stored directly in the runtime code, that is they are not stored in the storage.

2 Likes

@RenanSouza2

Exactly, thanks both.

I tested it later, and yes, the proxy has access to them on read, and they are "updated", as the variables will be hardcoded in the new implementations bytecode.

3 Likes

So the immutable variable is actually hardcoded inside the proxy contract bytecode?

1 Like

Inside the implementations contract bytecode*