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?
- Inheriting the older versions in a chain
- One downside is you can't override functions from the old version . Would end up with many
methodName1
,methodName2
- 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.