Possible approaches for new version of implementation contracts

Hello,

I have a upgradable contract UUPS pattern using the OpenZeppelin libraries. What would be the recommended approach to implement the new implementation version of my contract.

  • Duplicate code and override method e.g. ExampleV2X1 ?
  • Duplicate code, override method but remove Initializable because the proxy is already Initialized. e.g. ExampleV2X2?
  • Use inheritance and override e.g. ExampleV2X3 ?

:1234: Code to reproduce

ExampleV1 Contract

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v5.0.0/contracts/proxy/utils/Initializable.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v5.0.0/contracts/proxy/utils/UUPSUpgradeable.sol";

contract ExampleV1 is Initializable, UUPSUpgradeable {
    uint256 public value;

    constructor() { _disableInitializers();  }

    function initialize(uint256 _value) public initializer {
        __UUPSUpgradeable_init();
        value = _value;
    }

    function _authorizeUpgrade( address newContractAddress) internal view override  { /** Anyone */  }
    function setValue(uint256 _value) public { value = _value;  }
    function getValue() public view returns (uint256) { return value; }
}

ExampleV2X1

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v5.0.0/contracts/proxy/utils/Initializable.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v5.0.0/contracts/proxy/utils/UUPSUpgradeable.sol";

contract ExampleV2X1 is Initializable, UUPSUpgradeable {
    uint256 public value;

    constructor() { _disableInitializers(); }

    // Initializer function (replaces constructor)
    function initialize(uint256 _value) public initializer {
        __UUPSUpgradeable_init();
        value = _value;
    }

    function _authorizeUpgrade(address newContractAddress) internal view override  { /** Anyone */  }
    // Modified setValue method
    function setValue(uint256 _newValue) public { value = _newValue + 10; }
    // New increment method
    function increment() public { value = value + 1; }
    function getValue() public view returns (uint256) { return value; }
}

ExampleV2X2:

/ SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/v5.0.0/contracts/proxy/utils/UUPSUpgradeable.sol";

contract ExampleV2X2 is UUPSUpgradeable {
    uint256 public value;

    function _authorizeUpgrade(  address newContractAddress) internal view override  { /** Anyone */  }
    // Modified setValue method
    function setValue(uint256 _newValue) public { value = _newValue + 10; }
    // New increment method
    function increment() public { value = value + 1; }
    function getValue() public view returns (uint256) { return value;  }
}

ExampleV2X3:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.20;
import "./ExampleV1.sol"; // Import the base MyContract

contract ExampleV2X3 is ExampleV1 {
    // Override setValue method
    function setValue(uint256 _newValue) public override { value = _newValue + 10; }
    // New increment method
    function increment() public { value = value + 1; }
}

:computer: Environment

Any Environment.

Thank you

Each of those approaches seem reasonable to me, but have tradeoffs:

  • ExampleV2X1 is the most straightforward, and allows you to add, change, or even delete functions that are no longer used.
  • ExampleV2X2 is similar but removes the original initializer. Note that this approach should not be used to deploy a new proxy directly using this new implementation, otherwise the new proxy would be in an unexpected state because of the missing initializer function (which leaves a risk that someone else may find a way to take over the new proxy). Regardless, it is good practice to keep the import on Initializable, and still include the following to prevent the implementation from being initialized:
    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }
  • ExampleV2X3 has less code duplication, but the drawback is it won't let you delete functions or modify functions that were in V1 (unless the V1 function had virtual).
1 Like

thank you very much for the clarification. This means developers should use what they find reasonable for their use case and future development.

Sorry I have a related question. In ExampleV2X2 when Initializable is deleted, the INITIALIZABLE_STORAGE will also be deleted. This is okay because it is not used anymore and its slot is selected at a specific place only used for Initializable. Is this correct?

Thank you

It would be better practice to keep the import on Initializable and also keep the constructor to disable initializers on the implementation itself. Our upgrades plugins would give an error if it detects a deleted namespaced storage struct. I've updated my previous comment to clarify.

1 Like

Thank you again. I will follow the recommendation and I will add the Initializable, function initialize and the constructor with _disableInitializers. Just a note: The plugin didn't throw an error when i used ExampleV2X2 I was able to call upgrades.prepareUpgrade

import { ethers, upgrades } from "hardhat"
async function main() {
  // Set the address of the proxy contrat
  const proxyAddress = "<placeholder>"
  const ContractV2 = await ethers.getContractFactory("ContractV2")
  console.log("Preparing upgrade to ContractV2")
  const contractV2Address = await upgrades.prepareUpgrade(proxyAddress, ContractV2)
  console.log("ContractV2 implementation contract address " + contractV2Address)
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})

Can you share the exact contracts and scripts that you used? And the versions of the packages you have installed (see Required information when requesting support for Upgrades Plugins)

Thank you. This is an example of my contracts @ericglau

I have a hardhat project. both contracts V1 and V2 are under ./contracts . The script is under ./scripts. And I'm using ganache app with seed phrase

1. Debug logs

me@me-MacBook-Pro example % npx hardhat run scripts/deploy_and_upgrade.ts --network ganache
Compiled 29 Solidity files successfully (evm target: paris).
Deploy Example V1 Contract to network ganache
  @openzeppelin:upgrades:core manifest file: .openzeppelin/unknown-1337.json fallback file: .openzeppelin/unknown-1337.json +0ms
  @openzeppelin:upgrades:core manifest file: .openzeppelin/unknown-1337.json fallback file: .openzeppelin/unknown-1337.json +8ms
  @openzeppelin:upgrades:core fetching deployment of implementation d5edb5b0853a77ace3eb2cd2d1e49b9e29d6290d742707bbde85752d60948679 +0ms
  @openzeppelin:upgrades:core initiated deployment transaction hash: 0xe13cb86c4265facc86f4db97df43563dc82c48df61c284600450daa0dee297b6 merge: false +43ms
  @openzeppelin:upgrades:core polling timeout 60000 polling interval 5000 +0ms
  @openzeppelin:upgrades:core verifying deployment tx mined 0xe13cb86c4265facc86f4db97df43563dc82c48df61c284600450daa0dee297b6 +1ms
  @openzeppelin:upgrades:core succeeded verifying deployment tx mined 0xe13cb86c4265facc86f4db97df43563dc82c48df61c284600450daa0dee297b6 +1ms
  @openzeppelin:upgrades:core verifying code in target address 0x93bf5a364BcCc6e5c6F0273cf3Cc219c467e80fe +0ms
  @openzeppelin:upgrades:core code in target address found 0x93bf5a364BcCc6e5c6F0273cf3Cc219c467e80fe +1ms
The proxy of ExampleV1 Contract deployed to address 0xf8eeE0E663E0dc20072C97BB48d65fd5564bCC77
Preparing upgrade to ExampleV2
  @openzeppelin:upgrades:core manifest file: .openzeppelin/unknown-1337.json fallback file: .openzeppelin/unknown-1337.json +56ms
  @openzeppelin:upgrades:core manifest file: .openzeppelin/unknown-1337.json fallback file: .openzeppelin/unknown-1337.json +2ms
  @openzeppelin:upgrades:core manifest file: .openzeppelin/unknown-1337.json fallback file: .openzeppelin/unknown-1337.json +4ms
  @openzeppelin:upgrades:core fetching deployment of implementation 50cb387b9d03711341dd3f4777c8a775185df05eb1688e96a75239792a07937e +0ms
  @openzeppelin:upgrades:core initiated deployment transaction hash: 0x359c7330967e40da82b4d02ef06e400396a425608be711df091fc0a39573872c merge: false +23ms
  @openzeppelin:upgrades:core polling timeout 60000 polling interval 5000 +1ms
  @openzeppelin:upgrades:core verifying deployment tx mined 0x359c7330967e40da82b4d02ef06e400396a425608be711df091fc0a39573872c +0ms
  @openzeppelin:upgrades:core succeeded verifying deployment tx mined 0x359c7330967e40da82b4d02ef06e400396a425608be711df091fc0a39573872c +1ms
  @openzeppelin:upgrades:core verifying code in target address 0x9BE75FB171c494D2B57BB61387B448631f0a3953 +0ms
  @openzeppelin:upgrades:core code in target address found 0x9BE75FB171c494D2B57BB61387B448631f0a3953 +2ms
ExampleV2 implementation contract address 0x9BE75FB171c494D2B57BB61387B448631f0a3953

2. Network file unknown-1337.json

{
  "manifestVersion": "3.2",
  "proxies": [
    {
      "address": "0xf8eeE0E663E0dc20072C97BB48d65fd5564bCC77",
      "txHash": "0x135c666c252501483574aa704a8da274f891be4738af23eefb5cf1870777192a",
      "kind": "uups"
    }
  ],
  "impls": {
    "d5edb5b0853a77ace3eb2cd2d1e49b9e29d6290d742707bbde85752d60948679": {
      "address": "0x93bf5a364BcCc6e5c6F0273cf3Cc219c467e80fe",
      "txHash": "0xe13cb86c4265facc86f4db97df43563dc82c48df61c284600450daa0dee297b6",
      "layout": {
        "solcVersion": "0.8.20",
        "storage": [
          {
            "label": "value",
            "offset": 0,
            "slot": "0",
            "type": "t_uint256",
            "contract": "ExampleV1",
            "src": "contracts/ExampleV1.sol:9"
          }
        ],
        "types": {
          "t_address": {
            "label": "address",
            "numberOfBytes": "20"
          },
          "t_bool": {
            "label": "bool",
            "numberOfBytes": "1"
          },
          "t_bytes32": {
            "label": "bytes32",
            "numberOfBytes": "32"
          },
          "t_mapping(t_address,t_bool)": {
            "label": "mapping(address => bool)",
            "numberOfBytes": "32"
          },
          "t_mapping(t_bytes32,t_struct(RoleData)25_storage)": {
            "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)",
            "numberOfBytes": "32"
          },
          "t_struct(AccessControlStorage)32_storage": {
            "label": "struct AccessControlUpgradeable.AccessControlStorage",
            "members": [
              {
                "label": "_roles",
                "type": "t_mapping(t_bytes32,t_struct(RoleData)25_storage)",
                "offset": 0,
                "slot": "0"
              }
            ],
            "numberOfBytes": "32"
          },
          "t_struct(InitializableStorage)47_storage": {
            "label": "struct Initializable.InitializableStorage",
            "members": [
              {
                "label": "_initialized",
                "type": "t_uint64",
                "offset": 0,
                "slot": "0"
              },
              {
                "label": "_initializing",
                "type": "t_bool",
                "offset": 8,
                "slot": "0"
              }
            ],
            "numberOfBytes": "32"
          },
          "t_struct(RoleData)25_storage": {
            "label": "struct AccessControlUpgradeable.RoleData",
            "members": [
              {
                "label": "hasRole",
                "type": "t_mapping(t_address,t_bool)",
                "offset": 0,
                "slot": "0"
              },
              {
                "label": "adminRole",
                "type": "t_bytes32",
                "offset": 0,
                "slot": "1"
              }
            ],
            "numberOfBytes": "64"
          },
          "t_uint64": {
            "label": "uint64",
            "numberOfBytes": "8"
          },
          "t_uint256": {
            "label": "uint256",
            "numberOfBytes": "32"
          }
        },
        "namespaces": {
          "erc7201:openzeppelin.storage.AccessControl": [
            {
              "contract": "AccessControlUpgradeable",
              "label": "_roles",
              "type": "t_mapping(t_bytes32,t_struct(RoleData)25_storage)",
              "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61",
              "offset": 0,
              "slot": "0"
            }
          ],
          "erc7201:openzeppelin.storage.Initializable": [
            {
              "contract": "Initializable",
              "label": "_initialized",
              "type": "t_uint64",
              "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69",
              "offset": 0,
              "slot": "0"
            },
            {
              "contract": "Initializable",
              "label": "_initializing",
              "type": "t_bool",
              "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73",
              "offset": 8,
              "slot": "0"
            }
          ]
        }
      }
    },
    "50cb387b9d03711341dd3f4777c8a775185df05eb1688e96a75239792a07937e": {
      "address": "0x9BE75FB171c494D2B57BB61387B448631f0a3953",
      "txHash": "0x359c7330967e40da82b4d02ef06e400396a425608be711df091fc0a39573872c",
      "layout": {
        "solcVersion": "0.8.20",
        "storage": [
          {
            "label": "value",
            "offset": 0,
            "slot": "0",
            "type": "t_uint256",
            "contract": "ExampleV2",
            "src": "contracts/ExampleV2.sol:8"
          }
        ],
        "types": {
          "t_address": {
            "label": "address",
            "numberOfBytes": "20"
          },
          "t_bool": {
            "label": "bool",
            "numberOfBytes": "1"
          },
          "t_bytes32": {
            "label": "bytes32",
            "numberOfBytes": "32"
          },
          "t_mapping(t_address,t_bool)": {
            "label": "mapping(address => bool)",
            "numberOfBytes": "32"
          },
          "t_mapping(t_bytes32,t_struct(RoleData)25_storage)": {
            "label": "mapping(bytes32 => struct AccessControlUpgradeable.RoleData)",
            "numberOfBytes": "32"
          },
          "t_struct(AccessControlStorage)32_storage": {
            "label": "struct AccessControlUpgradeable.AccessControlStorage",
            "members": [
              {
                "label": "_roles",
                "type": "t_mapping(t_bytes32,t_struct(RoleData)25_storage)",
                "offset": 0,
                "slot": "0"
              }
            ],
            "numberOfBytes": "32"
          },
          "t_struct(InitializableStorage)47_storage": {
            "label": "struct Initializable.InitializableStorage",
            "members": [
              {
                "label": "_initialized",
                "type": "t_uint64",
                "offset": 0,
                "slot": "0"
              },
              {
                "label": "_initializing",
                "type": "t_bool",
                "offset": 8,
                "slot": "0"
              }
            ],
            "numberOfBytes": "32"
          },
          "t_struct(RoleData)25_storage": {
            "label": "struct AccessControlUpgradeable.RoleData",
            "members": [
              {
                "label": "hasRole",
                "type": "t_mapping(t_address,t_bool)",
                "offset": 0,
                "slot": "0"
              },
              {
                "label": "adminRole",
                "type": "t_bytes32",
                "offset": 0,
                "slot": "1"
              }
            ],
            "numberOfBytes": "64"
          },
          "t_uint64": {
            "label": "uint64",
            "numberOfBytes": "8"
          },
          "t_uint256": {
            "label": "uint256",
            "numberOfBytes": "32"
          }
        },
        "namespaces": {
          "erc7201:openzeppelin.storage.AccessControl": [
            {
              "contract": "AccessControlUpgradeable",
              "label": "_roles",
              "type": "t_mapping(t_bytes32,t_struct(RoleData)25_storage)",
              "src": "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol:61",
              "offset": 0,
              "slot": "0"
            }
          ],
          "erc7201:openzeppelin.storage.Initializable": [
            {
              "contract": "Initializable",
              "label": "_initialized",
              "type": "t_uint64",
              "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:69",
              "offset": 0,
              "slot": "0"
            },
            {
              "contract": "Initializable",
              "label": "_initializing",
              "type": "t_bool",
              "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:73",
              "offset": 8,
              "slot": "0"
            }
          ]
        }
      }
    }
  }
}

3. Hardhat config file

import "@nomicfoundation/hardhat-toolbox"
import "@openzeppelin/hardhat-upgrades"
import * as dotenv from "dotenv"
import { HardhatUserConfig } from "hardhat/config"
import "solidity-coverage"

dotenv.config()

const config: HardhatUserConfig = {
  defaultNetwork: "hardhat",
  networks: {
    hardhat: {
      blockGasLimit: 15000000,
      accounts: {
        mnemonic: "above north wish cable copper subject patch cage cause rent unfold twice",
        count: 20
      }
    },
    ganache: {
      url: "http://127.0.0.1:7545",
      blockGasLimit: 15000000,
      accounts: {
        mnemonic: "turn illness antique boost state tribe rebuild pave ivory mammal seat violin"
      }
    }
  },
  solidity: {
    version: "0.8.20",
    settings: {
      optimizer: {
        enabled: true,
        runs: 200
      }
    }
  },
  paths: {
    sources: "./contracts",
    tests: "./test",
    cache: "./cache",
    artifacts: "./artifacts"
  }
}

export default config

3. Script(s) deploy_and_upgrade.ts

const hre = require("hardhat")
import { ethers, upgrades } from "hardhat"

async function main() {
  console.log(`Deploy Example V1 Contract to network ${hre.network.name}`)

  const ExampleV1 = await ethers.getContractFactory("ExampleV1")
  const exampleV1Instance = await upgrades.deployProxy(
    ExampleV1, [ 2 ], { kind: "uups" }
  )
  await exampleV1Instance.waitForDeployment()

  console.log(
    `The proxy of ExampleV1 Contract deployed to address ${await exampleV1Instance.getAddress()}`
  )

  // Set the address of the proxy contrat
  const proxyAddress = await exampleV1Instance.getAddress()
  const ExampleV2 = await ethers.getContractFactory("ExampleV2")
  console.log("Preparing upgrade to ExampleV2")
  const exampleV2Address = await upgrades.prepareUpgrade(proxyAddress, ExampleV2)
  console.log("ExampleV2 implementation contract address " + exampleV2Address)
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})

4. Command that was run

Ganache running in the background using this compromised seed phrase "turn illness antique boost state tribe rebuild pave ivory mammal seat violin"

npx hardhat run scripts/deploy_and_upgrade.ts --network ganache

6. Installed versions of packages

npm list
├── @nomicfoundation/hardhat-ethers@3.0.4
├── @nomicfoundation/hardhat-toolbox@3.0.0
├── @openzeppelin/contracts-upgradeable@5.0.0
├── @openzeppelin/contracts@5.0.0
├── @openzeppelin/hardhat-upgrades@2.4.1
├── dotenv@16.3.1
├── ethereumjs-util@7.1.5
├── ethers@6.8.1
├── hardhat-contract-sizer@2.10.0
├── hardhat-gas-reporter@1.0.9
├── hardhat-log-remover@2.0.2
├── hardhat-solpp@1.0.1
├── hardhat@2.19.0
├── node-gyp@9.4.0
├── prettier-plugin-solidity@1.1.3
├── prettier@3.1.0
├── solhint@3.6.2
├── solidity-coverage@0.8.5
└── winston@3.11.0
npm list @openzeppelin/upgrades-core
└─┬ @openzeppelin/hardhat-upgrades@2.4.1
  └── @openzeppelin/upgrades-core@1.31.1

The same result for OpenZeppelin version 4.9.0

7. Implementation contract(s)

ExampleV1

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

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";

contract ExampleV1 is Initializable, UUPSUpgradeable, AccessControlUpgradeable {
  uint256 public value;

  /// @custom:oz-upgrades-unsafe-allow constructor
  constructor() {
    _disableInitializers();
  }

  function initialize(uint256 _value) public initializer {
    __UUPSUpgradeable_init();
    __AccessControl_init();
    value = _value;
  }

  function _authorizeUpgrade(address newContractAddress) internal view override {
    /** Anyone */
  }

  function setValue(uint256 _value) public {
    value = _value;
  }

  function getValue() public view returns (uint256) {
    return value;
  }
}

ExampleV2 - the implementation updated contract with no Initializable

// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;
import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol";

contract ExampleV2 is UUPSUpgradeable, AccessControlUpgradeable {
  uint256 public value;

  function _authorizeUpgrade(address newContractAddress) internal view override {
    /** Anyone */
  }

  // Modified setValue method
  function setValue(uint256 _newValue) public {
    value = _newValue + 10;
  }

  // New increment method
  function increment() public {
    value = value + 1;
  }

  function getValue() public view returns (uint256) {
    return value;
  }
}

Thank you for the detailed information. This works because ExampleV2 is inheriting UUPSUpgradeable and AccessControlUpgradeable, and both of those inherit Initializable. So your contract is inheriting Initializable indirectly through its parents.

Because of this possibility for confusion, this is another reason to avoid deleting Initializable in V2.

1 Like

awesome. Thank you very much