From bb5463c3dff9b6c2cc6c7ad11c1983a5c6c1962e Mon Sep 17 00:00:00 2001 From: Do Tu Date: Tue, 13 Aug 2024 16:24:43 +0700 Subject: [PATCH] feat(LibInitializeGuard): add guard for safely upgrade contract with reinitializer --- foundry.toml | 3 +- remappings.txt | 3 +- script/BaseMigration.s.sol | 14 + script/configs/NetworkConfig.sol | 6 + script/extensions/ScriptExtended.s.sol | 10 +- script/interfaces/configs/INetworkConfig.sol | 4 + script/libraries/LibErrorHandler.sol | 6 +- script/libraries/LibInitializeGuard.sol | 321 ++++++++++++++++++ .../sample/contracts/SampleProxyDeploy.s.sol | 3 + script/utils/Helpers.sol | 6 +- soldeer.lock | 6 + src/mocks/SampleProxy.sol | 16 +- 12 files changed, 386 insertions(+), 12 deletions(-) create mode 100644 script/libraries/LibInitializeGuard.sol diff --git a/foundry.toml b/foundry.toml index 0d1b661..e143394 100644 --- a/foundry.toml +++ b/foundry.toml @@ -27,4 +27,5 @@ localhost = "http://localhost:8545" [dependencies] "@openzeppelin" = { version = "4.9.3", url = "https://github.com/OpenZeppelin/openzeppelin-contracts/archive/refs/tags/v4.9.3.zip" } "@forge-std" = { version = "1.9.1", url = "https://github.com/foundry-rs/forge-std/archive/refs/tags/v1.9.1.zip" } -"@solady" = { version = "0.0.228", url = "https://github.com/Vectorized/solady/archive/refs/tags/v0.0.228.zip" } \ No newline at end of file +"@solady" = { version = "0.0.228", url = "https://github.com/Vectorized/solady/archive/refs/tags/v0.0.228.zip" } +"@openzeppelin-v5" = { version = "5.0.2", url = "https://github.com/OpenZeppelin/openzeppelin-contracts/archive/refs/tags/v5.0.2.zip" } diff --git a/remappings.txt b/remappings.txt index 4b814a5..2259aab 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,5 @@ @fdk/=script/ @forge-std-1.9.1=dependencies/@forge-std-1.9.1/src/ @openzeppelin-4.9.3=dependencies/@openzeppelin-4.9.3 -@solady-0.0.228=dependencies/@solady-0.0.228 \ No newline at end of file +@solady-0.0.228=dependencies/@solady-0.0.228 +@openzeppelin-v5-5.0.2=dependencies/@openzeppelin-v5-5.0.2 \ No newline at end of file diff --git a/script/BaseMigration.s.sol b/script/BaseMigration.s.sol index 1cc70d5..fcab641 100644 --- a/script/BaseMigration.s.sol +++ b/script/BaseMigration.s.sol @@ -7,10 +7,12 @@ import { TransparentProxyOZv4_9_5 } from "../src/TransparentProxyOZv4_9_5.sol"; import { LibString } from "../dependencies/@solady-0.0.228/src/utils/LibString.sol"; import { console } from "../dependencies/@forge-std-1.9.1/src/console.sol"; import { StdStyle } from "../dependencies/@forge-std-1.9.1/src/StdStyle.sol"; +import { Vm } from "../dependencies/@forge-std-1.9.1/src/Vm.sol"; import { ScriptExtended, IScriptExtended } from "./extensions/ScriptExtended.s.sol"; import { OnchainExecutor } from "./OnchainExecutor.s.sol"; // cheat to load artifact to parent `out` directory import { IMigrationScript } from "./interfaces/IMigrationScript.sol"; import { LibProxy } from "./libraries/LibProxy.sol"; +import { LibInitializeGuard } from "./libraries/LibInitializeGuard.sol"; import { DefaultContract } from "./utils/DefaultContract.sol"; import { ProxyInterface, LibDeploy, DeployInfo, UpgradeInfo } from "./libraries/LibDeploy.sol"; import { cheatBroadcast } from "./utils/Helpers.sol"; @@ -38,6 +40,18 @@ abstract contract BaseMigration is ScriptExtended { ProxyInterface /* proxyInterface */ ) internal virtual { } + function _beforeRunningScript() internal virtual override { + vm.recordLogs(); + vm.startStateDiffRecording(); + } + + function _afterRunningScript() internal virtual override { + Vm.Log[] memory recordedLogs = vm.getRecordedLogs(); + Vm.AccountAccess[] memory stateDiffs = vm.stopAndReturnStateDiff(); + + LibInitializeGuard.validate({ logs: recordedLogs, stateDiffs: stateDiffs }); + } + function _sharedArguments() internal virtual returns (bytes memory rawSharedArgs); function _injectDependencies() internal virtual { } diff --git a/script/configs/NetworkConfig.sol b/script/configs/NetworkConfig.sol index 67443bb..abbff0c 100644 --- a/script/configs/NetworkConfig.sol +++ b/script/configs/NetworkConfig.sol @@ -74,12 +74,18 @@ abstract contract NetworkConfig is INetworkConfig { function setForkMode(bool shouldEnable) public virtual { _isForkModeEnabled = shouldEnable; + emit ForkModeUpdated(shouldEnable); } function getNetworkData(TNetwork network) public view virtual returns (NetworkData memory) { return _networkDataMap[network]; } + function getNetworkTypeByForkId(uint256 forkId) public view virtual returns (TNetwork network) { + network = _forkId2Network[forkId]; + if (network == TNetwork.wrap(0x0)) network = DefaultNetwork.LocalHost.key(); + } + function getDeploymentDirectory(TNetwork network) public view virtual returns (string memory dirPath) { string memory dirName = network.dir(); require(bytes(dirName).length != 0, "NetworkConfig: Deployment directory not found"); diff --git a/script/extensions/ScriptExtended.s.sol b/script/extensions/ScriptExtended.s.sol index b7bc400..b3f7170 100644 --- a/script/extensions/ScriptExtended.s.sol +++ b/script/extensions/ScriptExtended.s.sol @@ -83,9 +83,13 @@ abstract contract ScriptExtended is BaseScriptExtended, Script, StdAssertions, I console.log("ScriptExtended:".blue(), "Prechecking completed in", vm.toString(end - start), "milliseconds.\n"); } + _beforeRunningScript(); + (bool success, bytes memory data) = address(this).delegatecall(callData); success.handleRevert(msg.sig, data); + _afterRunningScript(); + if (vme.getRuntimeConfig().disablePostcheck) { console.log("\nPostchecking is disabled.".yellow()); } else { @@ -99,6 +103,10 @@ abstract contract ScriptExtended is BaseScriptExtended, Script, StdAssertions, I } } + function _beforeRunningScript() internal virtual { } + + function _afterRunningScript() internal virtual { } + function _requireOn(TNetwork networkType) private view { require(network() == networkType, string.concat("ScriptExtended: Only allowed on ", vme.getAlias(networkType))); } @@ -137,7 +145,7 @@ abstract contract ScriptExtended is BaseScriptExtended, Script, StdAssertions, I } function prankOrBroadcast(address by) internal virtual { - if (vme.isPostChecking()) { + if (vme.isPostChecking() || vme.isPreChecking()) { vm.prank(by); } else { vm.broadcast(by); diff --git a/script/interfaces/configs/INetworkConfig.sol b/script/interfaces/configs/INetworkConfig.sol index 987b349..9287f28 100644 --- a/script/interfaces/configs/INetworkConfig.sol +++ b/script/interfaces/configs/INetworkConfig.sol @@ -13,6 +13,8 @@ interface INetworkConfig { string explorer; } + event ForkModeUpdated(bool enabled); + function setNetworkInfo(NetworkData memory networkData) external; function setForkMode(bool shouldEnable) external; @@ -25,6 +27,8 @@ interface INetworkConfig { function getNetworkData(TNetwork network) external view returns (NetworkData memory); + function getNetworkTypeByForkId(uint256 forkId) external view returns (TNetwork network); + function getForkId(TNetwork network) external view returns (uint256 forkId); function getForkId(TNetwork, uint256 forkBlockNumber) external view returns (uint256 forkId); diff --git a/script/libraries/LibErrorHandler.sol b/script/libraries/LibErrorHandler.sol index 9f9ccea..aad3c3d 100644 --- a/script/libraries/LibErrorHandler.sol +++ b/script/libraries/LibErrorHandler.sol @@ -1,9 +1,9 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.2 <0.9.0; +pragma experimental ABIEncoderV2; library LibErrorHandler { /// @dev Reserves error definition to upload to signature database. - error ExternalCallFailed(bytes4 msgSig, bytes4 callSig); /// @notice handle low level call revert if call failed, /// If external call return empty bytes, reverts with custom error. diff --git a/script/libraries/LibInitializeGuard.sol b/script/libraries/LibInitializeGuard.sol new file mode 100644 index 0000000..ad07963 --- /dev/null +++ b/script/libraries/LibInitializeGuard.sol @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.6.2 <0.9.0; +pragma experimental ABIEncoderV2; + +import { EnumerableSet } from "../../dependencies/@openzeppelin-4.9.3/contracts/utils/structs/EnumerableSet.sol"; +import { JSONParserLib } from "../../dependencies/@solady-0.0.228/src/utils/JSONParserLib.sol"; +import { LibString } from "../../dependencies/@solady-0.0.228/src/utils/LibString.sol"; +import { Vm, VmSafe } from "../../dependencies/@forge-std-1.9.1/src/Vm.sol"; +import { StdStyle } from "../../dependencies/@forge-std-1.9.1/src/StdStyle.sol"; +import { console, vm, vme } from "../utils/Helpers.sol"; +import { TNetwork } from "../types/TNetwork.sol"; +import { TContract } from "../types/TContract.sol"; + +interface InitializableOZV4 { + event Initialized(uint8); +} + +interface InitializableOZV5 { + event Initialized(uint64); +} + +interface IERC1967 { + event Upgraded(address indexed); +} + +/** + * @dev Library to guard the initialization of the proxies and logics. + * - Proxy: + * + The proxy MUST have `_initialized` slot. + * + `_initialized` value MUST increment by 1 after each `initialize` function call. + * + The last initialized version MUST equal to the number of `initialize` functions. + * - Logic: + * + The logic MUST disable the initialized version. + */ +library LibInitializeGuard { + using StdStyle for *; + using JSONParserLib for string; + using JSONParserLib for JSONParserLib.Item; + using LibString for string; + using EnumerableSet for EnumerableSet.AddressSet; + + struct InitializedSlot { + bool found; + bytes32 slot; + uint256 bitOffset; + uint256 nBit; + } + + struct Cache { + EnumerableSet.AddressSet _logics; + EnumerableSet.AddressSet _proxies; + mapping(address addr => uint256) _lastInitVer; + mapping(address addr => Vm.ChainInfo) _chainInfo; + mapping(address proxy => InitializedSlot) _initSlot; + mapping(address logic => address proxy) _logic2Proxy; + } + + /// @dev Number of bits used to store initialized version in `_initialized` slot of OpenZeppelin v4 + uint256 private constant N_BIT_INIT_V4 = 8; + /// @dev Maximum value of initialized version in `_initialized` slot of OpenZeppelin v4 + uint256 private constant MAX_VER_V4 = type(uint8).max; + /// @dev Number of bits used to store initialized version in `_initialized` slot of OpenZeppelin v5 + uint256 private constant N_BIT_INIT_V5 = 64; + /// @dev Maximum value of initialized version in `_initialized` slot of OpenZeppelin v5 + uint256 private constant MAX_VER_V5 = type(uint64).max; + + /// @dev See: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.2/contracts/proxy/utils/Initializable.sol#L77 + bytes32 private constant INITIALIZABLE_STORAGE_OZV5 = + 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a00; + + /// @dev Custom storage slot of the `Cache` struct + bytes32 private constant $$_CacheStorageLocation = keccak256("LibInitializeGuard.Cache.storage.slot"); + /// @dev Custom storage slot of the `StdStorage` struct + bytes32 private constant $$_StdStorageLocation = keccak256("LibInitializeGuard.StdStorage.storage.slot"); + + function validate(Vm.Log[] memory logs, Vm.AccountAccess[] memory stateDiffs) internal { + Cache storage $ = _getCacheStorage(); + + _recordUpgradesAndInitializations({ $cache: $, logs: logs }); + + for (uint256 i; i < stateDiffs.length; ++i) { + address addr = stateDiffs[i].account; + + if ($._proxies.contains(addr) && !$._initSlot[addr].found) { + // Record the chain info and initialized slot of the `addr`. + $._chainInfo[addr] = stateDiffs[i].chainInfo; + $._initSlot[addr] = _getInitializedSlot($, addr); + } + + if ($._logics.contains(addr) && stateDiffs[i].kind == VmSafe.AccountAccessKind.DelegateCall) { + address proxy = $._logic2Proxy[addr]; + Vm.StorageAccess[] memory accs = stateDiffs[i].storageAccesses; + + for (uint256 j; j < accs.length; ++j) { + // Skip if changes does not made changes to `initSlot` by `proxy` to `logic` + if (!(accs[j].isWrite && accs[j].account == proxy && accs[j].slot == $._initSlot[proxy].slot)) { + continue; + } + + bool shouldSkip = _validateInitChanges(accs[j], $._initSlot[proxy]); + if (shouldSkip) continue; + } + } + } + + _validateLogicsVersion({ $cache: $ }); + _validateProxiesVersion({ $cache: $ }); + } + + /** + * @dev Validate the initialized version of the logics. + * - Check if the logic disable initialized version. + */ + function _validateLogicsVersion(Cache storage $cache) private view { + address[] memory logics = $cache._logics.values(); + uint256 length = logics.length; + + for (uint256 i; i < length; ++i) { + uint256 lastInitVer = $cache._lastInitVer[logics[i]]; + address proxy = $cache._logic2Proxy[logics[i]]; + require( + (lastInitVer == MAX_VER_V4 && $cache._initSlot[proxy].nBit == N_BIT_INIT_V4) + || (lastInitVer == MAX_VER_V5 && $cache._initSlot[proxy].nBit == N_BIT_INIT_V5), + string.concat("LibInitializeGuard: Logic ", vm.getLabel(logics[i]), " does not disable initialized version!") + ); + } + } + + /** + * @dev Validate the initialized version of the proxies. + * - Check if `_initialized` slot is found. + * - Check if the last initialized version is equal to the number of `initialize` functions. + */ + function _validateProxiesVersion(Cache storage $cache) private { + address[] memory proxies = $cache._proxies.values(); + uint256 length = proxies.length; + + for (uint256 i; i < length; ++i) { + address proxy = proxies[i]; + InitializedSlot memory slot = $cache._initSlot[proxy]; + + require( + slot.found, + string.concat("LibInitializeGuard: Proxy ", vm.getLabel(proxies[i]), " does not have `_initialized` slot!") + ); + + uint256 lastInitVer = $cache._lastInitVer[proxy]; + + require( + lastInitVer != 0, string.concat("LibInitializeGuard: Proxy ", vm.getLabel(proxy), " does not initialize!") + ); + + if ( + (lastInitVer == MAX_VER_V4 && slot.nBit == N_BIT_INIT_V4) + || (lastInitVer == MAX_VER_V5 && slot.nBit == N_BIT_INIT_V5) + ) { + string memory ret = vm.prompt( + string.concat( + "[WARNING] ".yellow(), + vm.getLabel(proxy), + " disabled initialized version.".yellow(), + " Is it intentional?\n".yellow(), + "Press ", + "yes ".blue(), + "to continue..." + ) + ); + require(keccak256(bytes(vm.toLowercase(ret))) == keccak256("yes"), "LibInitializeGuard: User aborted!"); + + continue; + } + + uint256 initFnCount = _getInitializeFnCount($cache, proxy); + require( + lastInitVer == initFnCount, + string.concat( + "LibInitializeGuard: Invalid initialized version!", + " Expected: ", + vm.toString(initFnCount), + " Got: ", + vm.toString(lastInitVer) + ) + ); + } + } + + /** + * @dev Validate the intermediate changes of the `_initialized` slot of the given `access` storage. + * + * @param acc The storage access of data. + * @param slot The initialized slot of the proxy. + * @return shouldSkip Whether to skip the validation. + */ + function _validateInitChanges(Vm.StorageAccess memory acc, InitializedSlot memory slot) + private + view + returns (bool shouldSkip) + { + uint256 mask = (1 << slot.nBit) - 1; + + uint256 prvVer = (uint256(acc.previousValue) >> slot.bitOffset) & mask; + uint256 newVer = (uint256(acc.newValue) >> slot.bitOffset) & mask; + + // Skip if `_initialized` bytes location in `slot` does not change + // Assume other data in given slot is not related to initialized version + if (prvVer == newVer) return true; + // Skip if the proxy disable initialized version + if ((newVer == MAX_VER_V4 && slot.nBit == N_BIT_INIT_V4) || (newVer == MAX_VER_V5 && slot.nBit == N_BIT_INIT_V5)) { + console.log("[INIT] %s: Disabled initialized version", vm.getLabel(acc.account)); + return true; + } + + console.log(unicode"[INIT] %s: v%d → v%d", vm.getLabel(acc.account), prvVer, newVer); + + require(newVer == prvVer + 1, "LibInitializeGuard: Version does not correctly increment!"); + } + + /** + * @dev Record the upgrades and initializations of proxies and logics. + */ + function _recordUpgradesAndInitializations(Cache storage $cache, Vm.Log[] memory logs) private { + for (uint256 i; i < logs.length; ++i) { + address emitter = logs[i].emitter; + bytes32 eventSig = logs[i].topics[0]; + + if (eventSig == InitializableOZV4.Initialized.selector) { + $cache._lastInitVer[emitter] = abi.decode(logs[i].data, (uint8)); + } + + if (eventSig == InitializableOZV5.Initialized.selector) { + $cache._lastInitVer[emitter] = abi.decode(logs[i].data, (uint64)); + } + + if (eventSig == IERC1967.Upgraded.selector) { + address logic = address(uint160(uint256(logs[i].topics[1]))); + $cache._logics.add(logic); + $cache._proxies.add(emitter); + $cache._logic2Proxy[logic] = emitter; + } + } + } + + /** + * @dev Get the number of `initialize` functions of the given `proxy` by inspecting its storage layout using `forge inspect methodIdentifiers`. + */ + function _getInitializeFnCount(Cache storage $cache, address proxy) private returns (uint256 count) { + string[] memory inputs = new string[](4); + inputs[0] = "forge"; + inputs[1] = "inspect"; + inputs[2] = _getContractName($cache._chainInfo[proxy].forkId, proxy); + inputs[3] = "methodIdentifiers"; + + string memory ret = vm.toLowercase(string(vm.ffi(inputs))); + string[] memory allFns = vm.parseJsonKeys(ret, "."); + uint256 length = allFns.length; + + for (uint256 i; i < length; ++i) { + if (allFns[i].contains("initialize")) count++; + } + } + + /** + * @dev Get `_initialized` slot of the given `proxy` by inspecting its storage layout using `forge inspect storage`. + * If the slot is not found, infer it used OpenZeppelin v5 `Initializable` extension and see if the custom storage slot has value. + */ + function _getInitializedSlot(Cache storage $cache, address proxy) private returns (InitializedSlot memory initSlot) { + string[] memory inputs = new string[](4); + inputs[0] = "forge"; + inputs[1] = "inspect"; + inputs[2] = _getContractName($cache._chainInfo[proxy].forkId, proxy); + inputs[3] = "storage"; + + string memory ret = string(vm.ffi(inputs)); + JSONParserLib.Item memory layout = ret.parse().at('"storage"'); + uint256 layoutSize = layout.size(); + + for (uint256 i; i < layoutSize; ++i) { + JSONParserLib.Item memory storageSlot = layout.at(i); + + if (keccak256(bytes(storageSlot.at('"label"').value().decodeString())) == keccak256("_initialized")) { + initSlot.found = true; + initSlot.bitOffset = storageSlot.at('"offset"').value().parseUint() * 8; + initSlot.nBit = N_BIT_INIT_V4; + initSlot.slot = bytes32(vm.parseUint(storageSlot.at('"slot"').value().decodeString())); + + return initSlot; + } + } + + if ($cache._lastInitVer[proxy] != 0) { + // assume given proxy use `Initializable` from OpenZeppelin v5 + bytes32 slotValue = vm.load(proxy, INITIALIZABLE_STORAGE_OZV5); + if (slotValue != 0) { + initSlot.found = true; + initSlot.bitOffset = 0; + initSlot.nBit = N_BIT_INIT_V5; + initSlot.slot = INITIALIZABLE_STORAGE_OZV5; + } + } + } + + /** + * @dev Get the contract name by the given `addr` and `forkId`. + */ + function _getContractName(uint256 forkId, address addr) private view returns (string memory contractName) { + TNetwork networkType = vme.getNetworkTypeByForkId(forkId); + TContract contractType = vme.getContractTypeByRawData(networkType, addr); + contractName = vme.getContractName(contractType); + } + + /** + * @dev Get the storage slot of the `Cache` struct. + */ + function _getCacheStorage() private pure returns (Cache storage $) { + bytes32 slot = $$_CacheStorageLocation; + + assembly ("memory-safe") { + $.slot := slot + } + } +} diff --git a/script/sample/contracts/SampleProxyDeploy.s.sol b/script/sample/contracts/SampleProxyDeploy.s.sol index 51aeeda..9c22d71 100644 --- a/script/sample/contracts/SampleProxyDeploy.s.sol +++ b/script/sample/contracts/SampleProxyDeploy.s.sol @@ -15,5 +15,8 @@ contract SampleProxyDeploy is SampleMigration { function run() public virtual returns (SampleProxy instance) { instance = SampleProxy(_deployProxy(Contract.SampleProxy.key())); assertEq(instance.getMessage(), ISharedArgument(address(vme)).sharedArguments().proxyMessage); + + vm.prank(sender()); + instance.initializeV4(); } } diff --git a/script/utils/Helpers.sol b/script/utils/Helpers.sol index 1816e71..3ea7f8e 100644 --- a/script/utils/Helpers.sol +++ b/script/utils/Helpers.sol @@ -115,10 +115,6 @@ function decodeData(bytes memory data) returns (string memory decodedData) { decodedData = string(vm.ffi(commandInputs)); } -function loadContract(TContract contractType) view returns (address payable contractAddr) { - return loadContract({ contractType: contractType, shouldRevert: true }); -} - function loadContract(TContract contractType, bool shouldRevert) view returns (address payable contractAddr) { try vme.getAddressFromCurrentNetwork(contractType) returns (address payable res) { contractAddr = res; @@ -132,7 +128,7 @@ function loadContract(TContract contractType, bool shouldRevert) view returns (a } function prankOrBroadcast(address by) { - if (vme.isPostChecking()) { + if (vme.isPostChecking() || vme.isPreChecking()) { vm.prank(by); } else { vm.broadcast(by); diff --git a/soldeer.lock b/soldeer.lock index 2e813c3..9e138ef 100644 --- a/soldeer.lock +++ b/soldeer.lock @@ -16,3 +16,9 @@ name = "@solady" version = "0.0.228" source = "https://github.com/Vectorized/solady/archive/refs/tags/v0.0.228.zip" checksum = "90c6c562403b67338e3fa5fbad36a80b129df333d89db6759c5fd3ed15d4f0da" + +[[dependencies]] +name = "@openzeppelin-v5" +version = "5.0.2" +source = "https://github.com/OpenZeppelin/openzeppelin-contracts/archive/refs/tags/v5.0.2.zip" +checksum = "ca49e0776066328da0087977864dfaef0d5e54a0ea9859236a7cd9ad02abb9e5" diff --git a/src/mocks/SampleProxy.sol b/src/mocks/SampleProxy.sol index a90e9b9..11ea4e4 100644 --- a/src/mocks/SampleProxy.sol +++ b/src/mocks/SampleProxy.sol @@ -2,11 +2,15 @@ pragma solidity ^0.8.19; import { Initializable } from "../../dependencies/@openzeppelin-4.9.3/contracts/proxy/utils/Initializable.sol"; +import { Initializable as InitializableV5 } from + "../../dependencies/@openzeppelin-v5-5.0.2/contracts/proxy/utils/Initializable.sol"; +import { Ownable } from "../../dependencies/@openzeppelin-4.9.3/contracts/access/Ownable.sol"; -contract SampleProxy is Initializable { +contract SampleProxy is Ownable, InitializableV5 { uint256[50] private __gap; string internal _message; + address internal _addr; constructor() { _disableInitializers(); @@ -16,6 +20,16 @@ contract SampleProxy is Initializable { _message = message; } + function initializeV2() external reinitializer(2) { } + + function initializeV3(address a) external reinitializer(3) { + _addr = a; + } + + function initializeV4() external { + _disableInitializers(); + } + function setMessage(string memory message) public { _message = message; }