OZ recommends that for upgradeable contracts, one must specify initializer
on the initialize function and for every upgrade, we add initializeV2
, initializeV3
, and so on. This means that if the user wants to install the very latest version, he must call these in order:
- first call
initialize
- then call
initializeV2
- then call
initializeV3
.
This is not doable for me due to some problems it raises. So I thought what if I always have 2 functions only. initialize
and initializeFrom(uint8 prevVersion, bytes memory data)
and depending on what prevVersion
is, I will decode data
accordingly and call appropriate internal functions.
Going with this way, i can not use initializer
anymore on initialize
function, because if I do so, then initializeFrom
that will have reinitializer(x)
will also be callable. So I thought to always add reinitializer
with the same number to both initialize
and initializeFrom
functions. The only downside is that one can even call initialize
on the upgrade as well. So I thought to do:
modifier InitializationOnlyOnce() {
if(_getInitializedVersion() != 0) {
revert AlreadyInitialized();
}
_;
}
function initialize() external InitializationOnlyOnce reinitializer(x) {}
function initializeFrom(
uint16 _fromBuild,
bytes calldata _initData
) external reinitializer(x) {}
Note that x
are the same always. I think this solves everything and was wondering if OZ sees any attack scenario here. I'd appreciate your inputs.
It looks like you are trying to prevent initializing from the wrong version (in the initialize
case, from any version). However, the initializeFrom
function would still have this issue, for example if you incorrectly called initializeFrom(0, 0)
during an upgrade on a proxy which was already initialized.
Perhaps you could simplify this by always checking the current version from _getInitializedVersion()
and call appropriate internal functions based on that. In this case, you may not even need the uint16 _fromBuild
parameter if you can detect the current version internally. I haven't tested this out though, and you should consider and fully test whether this works for your use case.
This is actually an amazing feedback. Thanks so much.
One caveat is that your modifier reinitializer
is not virtual and problem i have is i need to do something like:
uint8 fromVersion = _getInitializedVersion() - 1;
I need to know from which version it's upgrading from, but with the above, caveat is that what if in the first contract, reinitializer was set to (2) and in the next, it was set to 10. Then, fromVersion would be as 9 and my checks for sure wouldn't account for 9
.
Just wondering how to get the actual/100% correct prevVersion it's upgrading from. But I believe, with upgradeable contracts, we follow so many rules, so one extra rule where reinitializer(x
) (x values must always increase by 1) can also be followed/obeyed.
If you call _getInitializedVersion()
(for example in a separate modifier) before the initializer
or reinitializer
modifier, then that should be the previous version it's upgrading from. But I don't see a way to get that value into the function body without using an extra storage variable.
1 Like