To Inherit Version1 to Version2, Or to Copy Code + Inheritance Order from Version1 to Version2?

I'm game planning for upgrades of implementation contracts using UUPSUpgradable contracts, and had some questions about how to carry the logic and inherited base contracts over SAFELY over from Version 1 to Version 2.

I've come across two patterns:

1: Inheritance Version 1 inherits Version2 like Version1 is Version0, CoolNewBaseContract, and expands from there with new base contracts, methods, etc.

This would look like:

Version0 is OwnableUpgradable {
    function myMethod() public view returns (uint256) {
        return 1;
    }
}

`Version1 is Version0, CoolNewBaseContract {
    // Gets OwnableUpgradable via inheritance from Version0
    // Inherits CoolNewBaseContract AFTER Version0 to preserve storage layout

    // Cannot override myMethod, so has to declare a new method
    function newMethod() public view returns (uint256) {
        return 2;
    }
}

2: Copying Code Across Versions, Not Inheriting Old Implementation Copying code from V1 into V2, inheriting the same contracts in the same order on the new V1 to preserve storage layout, and then updating/adding/deleting any methods as needed.

This would look like:

Version0 is OwnableUpgradable {
    function myMethod() public view returns (uint256) {
        return 1;
    }
}

`Version1 is OwnableUpgradable, CoolNewBaseContract {
    // Inherits in correct order, AND adds a cool new base contract with storage variables and methods

    // Same method signature, new logic
    function myMethod() public view returns (uint256) {
        return 2;
    }
}

ACTUAL EXAMPLE USING INHERITANCE

The original version 0 inherits all kinds of contracts-upgradable contracts from Open Zeppelin: https://github.com/OpenQDev/OpenQ-Contracts/blob/production/contracts/OpenQ/Implementations/OpenQV0.sol#L19

I was planning on upgrading like this in order to inherit the former version: https://github.com/OpenQDev/OpenQ-Contracts/blob/development/contracts/OpenQ/Implementations/OpenQVersionUpgrades.sol#L48

COMPOUND: ACTUAL EXAMPLE USING CODE COPY

I noticed Compound has deployed 7 implementation contracts, all of which inherit from one changing storage base contract. Besides updating their inherited Storage, they basically copy in the old contract and modify as needed: https://github.com/compound-finance/compound-protocol/tree/9e93e858dbad48ae4e0c59b13bb4c70856c596d9/contracts

BUT COMPOUND UPDATES NEVER INHERIT ANY NEW BASE CONTRACTS! And therein lies a secondary question about C3 linearized resolution order of storage layout...

SUGGESTIONS + DANGERS OF EACH APPROACH?

So which would you suggest?

  1. Inheriting the older versions in a chain
  • One downside is you can't override functions from the old version :grimacing:. Would end up with many methodName1, methodName2
  1. Making a copy of V1 in V2, with the same inheritance order to preserve storage layout?
  • What about the fact that storage layout results from the C3 linearization of the inherited base contracts? Could inheriting from a new 3rd party contract mess this up, EVEN IF we preserve the order of inheritance?

From the Solidity docs:

For contracts that use inheritance, the ordering of state variables is determined by the C3-linearized order of contracts starting with the most base-ward contract. If allowed by the above rules, state variables from different contracts do share the same storage slot.

I am leaning towards the Compound approach of not inheriting older versions since it allows for preserving method signatures.

I just want to make sure it will be safe to inherit from new base contracts from 3rd parties and such without knocking storage layouts due to funky C3 linearization resolution.

Hi @flaco_jones, that looks like a good summary of the tradeoffs, since both approaches have their pros and cons.

For the code copy approach, you are right that inheriting new parent contracts could cause conflicts with storage slots.

  • If your implementation contract itself does not have any state variables, then this is fine since you can add new parent contracts to the end of the inheritance list e.g. contract V1 is A, B -> contract V2 is A, B, C.
  • If your implementation contract itself does have state variables, then you cannot do the above unless you have storage gaps in your implementation, and then reduce those gaps as you use more storage slots in later versions (either in your contract itself or due to inheriting more parent contracts).

That's correct, my plan was to not have any storage variables in the implementation itself. ALL storage would live in the StorageV1 style contracts. That way there we don't have to think about __gaps. Leave that to the geniuses at OpenZeppelin :-).

I think I found a possible solution to this which leans mainly on the Compound code-copy approach and abstract contracts to keep inheritance ordered.

// Inherit all things onto an abstract contract, and add whatever storage vars you need after that
abstract contract OpenQStorageV0 is
    OwnableUpgradeable,
    UUPSUpgradeable,
    ReentrancyGuardUpgradeable,
    Oraclize
{
    BountyFactory public bountyFactory;
    OpenQTokenWhitelist public openQTokenWhitelist;
}

// Inherit the abstract contract onto your main implementation
// This works, as OpenQV0 overrides any necessary methods from the base contracts
contract OpenQV0 is OpenQStorageV0 {...}

// ----- MANY YEARS AND HACKS LATER -----

// Later, there's a New Base Contract you want to inherit
contract NewBaseContract {
    uint256 public foo;

    function setFoo(uint256 _foo) public {
        foo = _foo;
    }
}

// We Inherit the old storage contract, THEN we inherit from NewBaseContract, THEN we add any new custom storage variables
// I believe this maintains the storage order from before, and adds new ones after them.
// UNLESS there are dependency interactions between NewBaseContract and the old ones resulting from C3 linearization?
abstract contract OpenQStorageV1 is OpenQStorageV0, NewBaseContract {
    uint256 public newStorageVar = 456;
}

// Inherit the newer abstract contract onto your main implementation
contract OpenQV1 is OpenQStorageV1 {...}

I just tried this upgrade approach using code copy + abstract contracts for storage, and it worked without messing up storage layout :cowboy_hat_face:. I could update foo and newStorageVar.

So after this sequence of inheritance (NEVER putting storage vars directly in the implementation contract), I believe the storage layout of OpenQV1 would look something like this after upgrade:

[OpenZeppelin upgradable inherited contracts]
[2 OpenQStorageV0 storage variables]

after upgrade append....

[1 foo from NewBaseContract]
[1 newStorageVar from OpenQStorageV1]

Then I'd continue this chaining into the bright and storied future of the dApp...

Thoughts? Is this awful? What gotchas am I missing, or will this let me safely add both NEW CUSTOM STORAGE VARIABLES and NEW 3rd PARTY BASE CONTRACTS?

That looks like a reasonable approach! An alternative could be to define the inheritance in the implementation itself rather than the abstract storage contract, but that may be just a matter of preference. For example:

abstract contract V1Storage {
  (V1 state variables only)
}
contract V1 is ParentA, V1Storage {
  (functions only)
}

abstract contract V2Storage {
  (V2 state variables only)
}
contract V2 is ParentA, V1Storage, ParentB, V2Storage {
  (functions only)
}

This overall pattern that you described is indeed used, for example The Graph uses storage contracts as well.

1 Like

That's great!

I arrived at the abstract contract approach by first considering the contract V2 is ParentA, V1Storage, ParentB, V2Storage approach you mentioned.

I opted for the abstract contract approach so that the inheritance can be the single responsibility of the storage contracts.

So instead of contract V2 is ParentA, V1Storage, ParentB, V2Storage I can just do:

contract V2 is V2Storage

which resolves the same inheritance graph since

V2Storage is ParentA, V1Storage, ParentB

Thank you for the Graph link! We love and use them a lot.

1 Like

I do wonder if perhaps there are some 3rd party contracts that need to be inherited by contract rather than abstract contract. E.g., some contract which requires its direct parent to do implement some function.

If so that would seriously hamstring my approach.

Do such things exist?

If so, I suppose I could just implement any necessary methods in the abstract storage contract.

If you mean a contract that requires an override, then you should be able to override it in the implementation rather than the abstract contract (either place would work, although doing it in the abstract contract may prevent you from upgrading its logic with your example).

1 Like