You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There is currently no standard or well documented best practices for a use case where upgradeable contracts require chaining initializers as the contract develops. When a contract gets to it's third of fourth initializers it requires multiple calls to each upgrade step as well as being difficult to mark a method to require the latest version.
This proposal aims to provide an interface that can clearly signal when a new major version is deployed as well as center the upgrade path into a single upgrade() function, that will initialize the contract to the latest version wherever it is down the chain.
This pattern becomes increasingly frustrating when using BeaconProxies for user-deployed content, as it would have you require multiple transactions for the user's logic to be brought up-to-date.
📝 Details
The proposed contract should:
Provide an interface that, when followed, ensures the best upgradability practices.
Have text-editor and compile-time hints to requiring a new override to the upgrade function
Contain a modifier that prevents a certain function from executing when it is below the required version, even when the underlying implementation contract has been upgraded
Be able to upgrade to it's latest version with a single transaction
What I came up with is the following contract, with given rules:
Contract
contractBaseVersionisInitializable {
function _version() internalviewvirtualreturns (uint8) {
return1;
}
function version() publicviewreturns (uint8) {
return_version();
}
function upgrade() publicvirtual {
_upgradeFrom(_getInitializedVersion());
}
function _upgradeFrom(uint8version_) internalvirtual {
// Silencing warning
version_;
revert("BaseVersion: No upgrade function defined");
}
modifier requireVersion(uint8_minimum) {
require(
_getInitializedVersion() >= _minimum,
string(
abi.encodePacked(
"BeaconVersion: Your contract is out of date - needed: ",
_minimum,
" currently: ",
_getInitializedVersion()
)
)
);
_;
}
}
contractMajorVersionisBaseVersion {
function _upgradeFrom(uint8version_) internalvirtualoverride {
// Silencing warningsuper._upgradeFrom(version_);
}
}
At first sight the MajorVersion contract might seem redundant and pointless, but it's only there because it was the way I managed to get this compile-time behaviour when declaring new MajorVersions
By following this pattern of declaring versions, the compiler will instruct you correctly.
Here's an extended example, using initializer and reinitializer to define a way to handle upgrades in the proposed way
Implementation
// SPDX-License-Identifier: MITimport"@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
pragma solidity^0.8.0;
contractBaseVersionisInitializable {
function _version() internalviewvirtualreturns (uint8) {
return1;
}
function version() publicviewreturns (uint8) {
return_version();
}
function upgrade() publicvirtual {
_upgradeFrom(_getInitializedVersion());
}
function _upgradeFrom(uint8version_) internalvirtual {
// Silencing warning
version_;
revert("BaseVersion: No upgrade function defined");
}
modifier requireVersion(uint8_minimum) {
require(
_getInitializedVersion() >= _minimum,e
string(
abi.encodePacked(
"BasVersion: Your contract is out of date - needed: ",
_minimum,
" currently: ",
_getInitializedVersion()
)
)
);
_;
}
}
contractMajorVersionisBaseVersion {
function _upgradeFrom(uint8version_) internalvirtualoverride {
// Silencing warningsuper._upgradeFrom(version_);
}
}
// Is base version so doesnt need to implement upgrade methods, should correclty revertcontractContractV1isBaseVersion {
stringprivate someString;
function __ContractV1_initialize() internal onlyInitializing {
someString ="Some value";
}
function initialize() publicvirtual initializer {
__ContractV1_initialize();
}
function echoString() publicviewvirtualreturns (stringmemory) {
return someString;
}
}
contractContractV2isMajorVersion, ContractV1 {
uint8private constant v2 =2;
function _version() internalviewvirtualoverridereturns (uint8) {
return v2;
}
function _upgradeFrom(
uint8version_
) internalvirtualoverride(BaseVersion, MajorVersion) {
// If is more out-of-date than previous version// would never happen in this case cause it would be version 1// but placed it here for consistencyif (version_ < v2 -1) {
super._upgradeFrom(version_);
}
initializeV2();
}
uint256private someInt;
function __ContractV2_initialize_unchained() internal onlyInitializing {
someInt =11;
}
function __ContractV2_initialize() internal onlyInitializing {
super.__ContractV1_initialize();
__ContractV2_initialize_unchained();
}
function initialize() publicvirtualoverridereinitializer(2) {
__ContractV2_initialize();
}
function initializeV2() publicreinitializer(v2) {
__ContractV2_initialize_unchained();
}
function echoInt() publicviewrequireVersion(v2) returns (uint256) {
return someInt;
}
}
contractContractV3isMajorVersion, ContractV2 {
uint8private constant v3 =3;
function _version()
internalviewvirtualoverride(BaseVersion, ContractV2)
returns (uint8)
{
return v3;
}
function _upgradeFrom(
uint8version_
) internalvirtualoverride(MajorVersion, ContractV2) {
// If is more out-of-date than previous versionif (version_ < v3 -1) {
ContractV2._upgradeFrom(version_);
}
initializeV3();
}
stringprivate v3string;
function __ContractV3_initialize_unchained() internal onlyInitializing {
v3string ="v3 is here";
}
function __ContractV3_initialize() internal onlyInitializing {
__ContractV2_initialize();
__ContractV3_initialize_unchained();
}
function initialize() publicoverridereinitializer(v3) {
__ContractV3_initialize();
}
function initializeV3() publicreinitializer(v3) {
__ContractV3_initialize_unchained();
}
function echoString()
publicviewvirtualoverriderequireVersion(v3)
returns (stringmemory)
{
return v3string;
}
}
I've implemented this with three contract version and uploaded them to this repo for reference. It includes unit tests that deploy and upgrades a few BeaconProxies
Conclusion
I'm not attached to the name or implementation, just really wanted to raise this discussion and see the community's thoughts. Would love to hear what everyone thinks.
I don't quite like the amount of boilerplate required, including having to use a constant version declaration on every new contract implementation to avoid bad comparisons on the modifier and upgrade functions
The text was updated successfully, but these errors were encountered:
🧐 Motivation
There is currently no standard or well documented best practices for a use case where upgradeable contracts require chaining initializers as the contract develops. When a contract gets to it's third of fourth initializers it requires multiple calls to each upgrade step as well as being difficult to mark a method to require the latest version.
This proposal aims to provide an interface that can clearly signal when a new major version is deployed as well as center the upgrade path into a single
upgrade()
function, that will initialize the contract to the latest version wherever it is down the chain.This pattern becomes increasingly frustrating when using BeaconProxies for user-deployed content, as it would have you require multiple transactions for the user's logic to be brought up-to-date.
📝 Details
The proposed contract should:
What I came up with is the following contract, with given rules:
Contract
At first sight the MajorVersion contract might seem redundant and pointless, but it's only there because it was the way I managed to get this compile-time behaviour when declaring new MajorVersions
By following this pattern of declaring versions, the compiler will instruct you correctly.
Here's an extended example, using
initializer
andreinitializer
to define a way to handle upgrades in the proposed wayImplementation
I've implemented this with three contract version and uploaded them to this repo for reference. It includes unit tests that deploy and upgrades a few BeaconProxies
Conclusion
I'm not attached to the name or implementation, just really wanted to raise this discussion and see the community's thoughts. Would love to hear what everyone thinks.
I don't quite like the amount of boilerplate required, including having to use a
constant
version declaration on every new contract implementation to avoid bad comparisons on the modifier and upgrade functionsThe text was updated successfully, but these errors were encountered: