Writing modular upgradable contracts

I have the following contract:

import { UUPSUpgradeable } from "@openzeppelin-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { OwnableUpgradeable } from "@openzeppelin-upgradeable/access/OwnableUpgradeable.sol";

contract One is UUPSUpgradeable, OwnableUpgradeable {
    uint256 public var1;
    uint256 public var2;

  function initialize() public initializer {
    __Ownable_init();
    __UUPSUpgradeable_init();
  }

    function setVar1(uint256 _var1) public {
        var1 = _var1;
    }

    function setVar2(uint256 _var1) public {
        var2 = _var2;
    }

  function _authorizeUpgrade(address) internal virtual override onlyOwner {}

}

I deploy a proxy with hardhat-upgrades, then realize that setVar2() and var2 would be better placed in a separate file to make the contract more modular. So I create a Utils contract that I will inherit from.

My intuition is that I should be able to do this:

contract Utils{
    uint256 public var2;

    function setVar2(uint256 _var2) public {
        var2 = _var2;
    }
}

But then, wouldn't lose the upgradeability of everything in Utils? If I want to preserve the upgradeability of what I have going on in contract One would I need to do deploy another* proxy? (for Utils) resulting in the following code?

import { UUPSUpgradeable } from "@openzeppelin-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import { OwnableUpgradeable } from "@openzeppelin-upgradeable/access/OwnableUpgradeable.sol";

contract Utils is UUPSUpgradeable, OwnableUpgradeable {
    uint256 public var2;

  function initialize() public initializer {
    __Ownable_init();
    __UUPSUpgradeable_init();
  }

    function setVar2(uint256 _var2) public {
        var2 = _var2;
    }

  function _authorizeUpgrade(address) internal virtual override onlyOwner {}

}

then update One to have:

contract One is Utils {
    uint256 public var1;

    function setVar1(uint256 _var1) public {
        var1 = _var1;
    }

  function _authorizeUpgrade(address) internal virtual override onlyOwner {}

}

Questions:

  1. Do I need to add an initializer and inherit UUPSUpgradeable and OwnableUpgradeable to Utils to maintain upgradeability?
  2. In the latest version of contract One ... do I need _authorizeUpgrade? Since it's already inherited from Utils?
  3. Is it ok that in the latest version of contract One I got rid of the initializer?
  1. If Utils will not be deployed by itself, and will only be inherited by contract One, then no. Although consider marking it as abstract in this case. Note: it can have an initialization function if needed (to be called by One in One's initializer).
  2. It can be in either place. It does not need to be in both in this case.
  3. I don't suggest removing an initializer unless you are sure that you will never need it in the future. See Removing initializer on upgrades - #2 by ericglau

But a major problem with the above is that you CANNOT upgrade from your first One (with everything in it) to the bottom One as it is written -- this is because the storage layout will change due to the inheritance. In the bottom One, the inherited Utils's var2 occupies a storage slot before var1. A possible solution is to use a storage contract similar to this approach.

1 Like