From 8bcba2f8ff0e57a2ca336e03181cebd95b5f3c2a Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 14 Jul 2023 15:18:41 -0600 Subject: [PATCH 01/12] Allow reinitializers versions greater than 256 --- contracts/mocks/InitializableMock.sol | 8 +-- contracts/proxy/utils/Initializable.sol | 84 +++++++++++++++++-------- test/proxy/utils/Initializable.test.js | 2 +- 3 files changed, 63 insertions(+), 31 deletions(-) diff --git a/contracts/mocks/InitializableMock.sol b/contracts/mocks/InitializableMock.sol index 155aaefb799..456d2d1dfe5 100644 --- a/contracts/mocks/InitializableMock.sol +++ b/contracts/mocks/InitializableMock.sol @@ -79,7 +79,7 @@ contract ChildConstructorInitializableMock is ConstructorInitializableMock { contract ReinitializerMock is Initializable { uint256 public counter; - function getInitializedVersion() public view returns (uint8) { + function getInitializedVersion() public view returns (uint16) { return _getInitializedVersion(); } @@ -87,15 +87,15 @@ contract ReinitializerMock is Initializable { doStuff(); } - function reinitialize(uint8 i) public reinitializer(i) { + function reinitialize(uint16 i) public reinitializer(i) { doStuff(); } - function nestedReinitialize(uint8 i, uint8 j) public reinitializer(i) { + function nestedReinitialize(uint16 i, uint16 j) public reinitializer(i) { reinitialize(j); } - function chainReinitialize(uint8 i, uint8 j) public { + function chainReinitialize(uint16 i, uint16 j) public { reinitialize(i); reinitialize(j); } diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index 08171015d3c..8a99aca5917 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/Initializable.sol) -pragma solidity ^0.8.19; +pragma solidity ^0.8.20; /** * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed @@ -55,14 +55,27 @@ pragma solidity ^0.8.19; */ abstract contract Initializable { /** - * @dev Indicates that the contract has been initialized. + * @dev Storage of the initializable contract. + * + * It's implemented on a custom ERC-7201 namespace to reduce the risk of storage collisions + * when using with upgradeable contracts. + * + * @custom:storage-location erc7201:openzeppelin.storage.Initializable */ - uint8 private _initialized; + struct InitializableStorage { + /** + * @dev Indicates that the contract has been initialized. + */ + uint16 _initialized; + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool _initializing; + } - /** - * @dev Indicates that the contract is in the process of being initialized. - */ - bool private _initializing; + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Initializable")) - 1)) + bytes32 private constant _INITIALIZABLE_STORAGE = + 0xf0c57e16840df040f15088dc2f81fe391c3923bec73e23a9662efc9c229c6a0e; /** * @dev The contract is already initialized. @@ -77,7 +90,7 @@ abstract contract Initializable { /** * @dev Triggered when the contract has been initialized or reinitialized. */ - event Initialized(uint8 version); + event Initialized(uint16 version); /** * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, @@ -89,17 +102,20 @@ abstract contract Initializable { * Emits an {Initialized} event. */ modifier initializer() { - bool isTopLevelCall = !_initializing; - if (!(isTopLevelCall && _initialized < 1) && !(address(this).code.length == 0 && _initialized == 1)) { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + bool isTopLevelCall = !$._initializing; + if (!(isTopLevelCall && $._initialized < 1) && !(address(this).code.length == 0 && $._initialized == 1)) { revert AlreadyInitialized(); } - _initialized = 1; + $._initialized = 1; if (isTopLevelCall) { - _initializing = true; + $._initializing = true; } _; if (isTopLevelCall) { - _initializing = false; + $._initializing = false; emit Initialized(1); } } @@ -122,14 +138,17 @@ abstract contract Initializable { * * Emits an {Initialized} event. */ - modifier reinitializer(uint8 version) { - if (_initializing || _initialized >= version) { + modifier reinitializer(uint16 version) { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + if ($._initializing || $._initialized >= version) { revert AlreadyInitialized(); } - _initialized = version; - _initializing = true; + $._initialized = version; + $._initializing = true; _; - _initializing = false; + $._initializing = false; emit Initialized(version); } @@ -138,7 +157,7 @@ abstract contract Initializable { * {initializer} and {reinitializer} modifiers, directly or indirectly. */ modifier onlyInitializing() { - if (!_initializing) { + if (!_isInitializing()) { revert NotInitializing(); } _; @@ -153,26 +172,39 @@ abstract contract Initializable { * Emits an {Initialized} event the first time it is successfully executed. */ function _disableInitializers() internal virtual { - if (_initializing) { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + if ($._initializing) { revert AlreadyInitialized(); } - if (_initialized != type(uint8).max) { - _initialized = type(uint8).max; - emit Initialized(type(uint8).max); + if ($._initialized != type(uint16).max) { + $._initialized = type(uint16).max; + emit Initialized(type(uint16).max); } } /** * @dev Returns the highest version that has been initialized. See {reinitializer}. */ - function _getInitializedVersion() internal view returns (uint8) { - return _initialized; + function _getInitializedVersion() internal view returns (uint16) { + return _getInitializableStorage()._initialized; } /** * @dev Returns `true` if the contract is currently initializing. See {onlyInitializing}. */ function _isInitializing() internal view returns (bool) { - return _initializing; + return _getInitializableStorage()._initializing; + } + + /** + * @dev Returns a pointer to the storage namespace. + */ + // solhint-disable-next-line var-name-mixedcase + function _getInitializableStorage() private pure returns (InitializableStorage storage $) { + assembly { + $.slot := _INITIALIZABLE_STORAGE + } } } diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index e3e0fc02f77..fa8a0387b02 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -213,7 +213,7 @@ contract('Initializable', function () { it('old and new patterns in good sequence', async function () { const ok = await DisableOk.new(); await expectEvent.inConstruction(ok, 'Initialized', { version: '1' }); - await expectEvent.inConstruction(ok, 'Initialized', { version: '255' }); + await expectEvent.inConstruction(ok, 'Initialized', { version: '65535' }); }); }); }); From 6df8c25cfeb8ad645c6ac98234251d1cf095fb8e Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 14 Jul 2023 15:20:44 -0600 Subject: [PATCH 02/12] Use uint64 instead --- contracts/mocks/InitializableMock.sol | 8 ++++---- contracts/proxy/utils/Initializable.sol | 14 +++++++------- test/proxy/utils/Initializable.test.js | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/mocks/InitializableMock.sol b/contracts/mocks/InitializableMock.sol index 456d2d1dfe5..ab5ea031132 100644 --- a/contracts/mocks/InitializableMock.sol +++ b/contracts/mocks/InitializableMock.sol @@ -79,7 +79,7 @@ contract ChildConstructorInitializableMock is ConstructorInitializableMock { contract ReinitializerMock is Initializable { uint256 public counter; - function getInitializedVersion() public view returns (uint16) { + function getInitializedVersion() public view returns (uint64) { return _getInitializedVersion(); } @@ -87,15 +87,15 @@ contract ReinitializerMock is Initializable { doStuff(); } - function reinitialize(uint16 i) public reinitializer(i) { + function reinitialize(uint64 i) public reinitializer(i) { doStuff(); } - function nestedReinitialize(uint16 i, uint16 j) public reinitializer(i) { + function nestedReinitialize(uint64 i, uint64 j) public reinitializer(i) { reinitialize(j); } - function chainReinitialize(uint16 i, uint16 j) public { + function chainReinitialize(uint64 i, uint64 j) public { reinitialize(i); reinitialize(j); } diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index 8a99aca5917..897e9eff87f 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -66,7 +66,7 @@ abstract contract Initializable { /** * @dev Indicates that the contract has been initialized. */ - uint16 _initialized; + uint64 _initialized; /** * @dev Indicates that the contract is in the process of being initialized. */ @@ -90,7 +90,7 @@ abstract contract Initializable { /** * @dev Triggered when the contract has been initialized or reinitialized. */ - event Initialized(uint16 version); + event Initialized(uint64 version); /** * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, @@ -138,7 +138,7 @@ abstract contract Initializable { * * Emits an {Initialized} event. */ - modifier reinitializer(uint16 version) { + modifier reinitializer(uint64 version) { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); @@ -178,16 +178,16 @@ abstract contract Initializable { if ($._initializing) { revert AlreadyInitialized(); } - if ($._initialized != type(uint16).max) { - $._initialized = type(uint16).max; - emit Initialized(type(uint16).max); + if ($._initialized != type(uint64).max) { + $._initialized = type(uint64).max; + emit Initialized(type(uint64).max); } } /** * @dev Returns the highest version that has been initialized. See {reinitializer}. */ - function _getInitializedVersion() internal view returns (uint16) { + function _getInitializedVersion() internal view returns (uint64) { return _getInitializableStorage()._initialized; } diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index fa8a0387b02..cb45995ea44 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -213,7 +213,7 @@ contract('Initializable', function () { it('old and new patterns in good sequence', async function () { const ok = await DisableOk.new(); await expectEvent.inConstruction(ok, 'Initialized', { version: '1' }); - await expectEvent.inConstruction(ok, 'Initialized', { version: '65535' }); + await expectEvent.inConstruction(ok, 'Initialized', { version: '18446744073709551615' }); // MAX_UINT64 }); }); }); From 2a6e2f67368749c079cf2f8566551714f87e42ae Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 14 Jul 2023 15:23:46 -0600 Subject: [PATCH 03/12] Add changeset --- .changeset/thick-pumpkins-exercise.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/thick-pumpkins-exercise.md diff --git a/.changeset/thick-pumpkins-exercise.md b/.changeset/thick-pumpkins-exercise.md new file mode 100644 index 00000000000..52966efe8ad --- /dev/null +++ b/.changeset/thick-pumpkins-exercise.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`Initializable`: Allow versions greater than 256. From 54395dadcf45269a386a2d7ee9024a6573c40ede Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 15 Jul 2023 01:39:58 -0600 Subject: [PATCH 04/12] Simplify initializer --- contracts/mocks/InitializableMock.sol | 28 +---------- contracts/proxy/utils/Initializable.sol | 64 +++++++++++++++---------- test/proxy/utils/Initializable.test.js | 27 +++-------- 3 files changed, 47 insertions(+), 72 deletions(-) diff --git a/contracts/mocks/InitializableMock.sol b/contracts/mocks/InitializableMock.sol index ab5ea031132..29fa56ebc8c 100644 --- a/contracts/mocks/InitializableMock.sol +++ b/contracts/mocks/InitializableMock.sol @@ -25,10 +25,6 @@ contract InitializableMock is Initializable { onlyInitializingRan = true; } - function initializerNested() public initializer { - initialize(); - } - function onlyInitializingNested() public initializer { initializeOnlyInitializing(); } @@ -64,18 +60,6 @@ contract ConstructorInitializableMock is Initializable { } } -contract ChildConstructorInitializableMock is ConstructorInitializableMock { - bool public childInitializerRan; - - constructor() initializer { - childInitialize(); - } - - function childInitialize() public initializer { - childInitializerRan = true; - } -} - contract ReinitializerMock is Initializable { uint256 public counter; @@ -109,22 +93,14 @@ contract ReinitializerMock is Initializable { } } -contract DisableNew is Initializable { +contract DisableOk is Initializable { constructor() { _disableInitializers(); } } -contract DisableOld is Initializable { - constructor() initializer {} -} - -contract DisableBad1 is DisableNew, DisableOld {} - -contract DisableBad2 is Initializable { +contract DisableBad is Initializable { constructor() initializer { _disableInitializers(); } } - -contract DisableOk is DisableOld, DisableNew {} diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index 897e9eff87f..f5f9f81b9cd 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -102,22 +102,9 @@ abstract contract Initializable { * Emits an {Initialized} event. */ modifier initializer() { - // solhint-disable-next-line var-name-mixedcase - InitializableStorage storage $ = _getInitializableStorage(); - - bool isTopLevelCall = !$._initializing; - if (!(isTopLevelCall && $._initialized < 1) && !(address(this).code.length == 0 && $._initialized == 1)) { - revert AlreadyInitialized(); - } - $._initialized = 1; - if (isTopLevelCall) { - $._initializing = true; - } + bool isTopLevelCall = _beforeInitialize(1); _; - if (isTopLevelCall) { - $._initializing = false; - emit Initialized(1); - } + _afterInitialize(isTopLevelCall, 1); } /** @@ -139,17 +126,9 @@ abstract contract Initializable { * Emits an {Initialized} event. */ modifier reinitializer(uint64 version) { - // solhint-disable-next-line var-name-mixedcase - InitializableStorage storage $ = _getInitializableStorage(); - - if ($._initializing || $._initialized >= version) { - revert AlreadyInitialized(); - } - $._initialized = version; - $._initializing = true; + bool isTopLevelCall = _beforeInitialize(version); _; - $._initializing = false; - emit Initialized(version); + _afterInitialize(isTopLevelCall, version); } /** @@ -207,4 +186,39 @@ abstract contract Initializable { $.slot := _INITIALIZABLE_STORAGE } } + + function _setInitializedVersion(uint64 version) private returns (bool) { + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + // Cache values + bool initializing = $._initializing; + uint64 initialized = $._initialized; + + bool reinitializing = version > 1; + bool increasing = version > initialized; + + if (initializing ? reinitializing : !increasing) { + revert AlreadyInitialized(); + } + + $._initialized = version; + + return !initializing; + } + + function _beforeInitialize(uint64 version) private returns (bool) { + bool isTopLevelCall = _setInitializedVersion(version); + if (isTopLevelCall) { + _getInitializableStorage()._initializing = true; + } + return isTopLevelCall; + } + + function _afterInitialize(bool isTopLevelCall, uint64 version) private { + if (isTopLevelCall) { + _getInitializableStorage()._initializing = false; + emit Initialized(version); + } + } } diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index cb45995ea44..3e50111a42a 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -4,11 +4,9 @@ const { expectRevertCustomError } = require('../../helpers/customError'); const InitializableMock = artifacts.require('InitializableMock'); const ConstructorInitializableMock = artifacts.require('ConstructorInitializableMock'); -const ChildConstructorInitializableMock = artifacts.require('ChildConstructorInitializableMock'); const ReinitializerMock = artifacts.require('ReinitializerMock'); const SampleChild = artifacts.require('SampleChild'); -const DisableBad1 = artifacts.require('DisableBad1'); -const DisableBad2 = artifacts.require('DisableBad2'); +const DisableBad = artifacts.require('DisableBad'); const DisableOk = artifacts.require('DisableOk'); contract('Initializable', function () { @@ -46,10 +44,6 @@ contract('Initializable', function () { }); describe('nested under an initializer', function () { - it('initializer modifier reverts', async function () { - await expectRevertCustomError(this.contract.initializerNested(), 'AlreadyInitialized', []); - }); - it('onlyInitializing modifier succeeds', async function () { await this.contract.onlyInitializingNested(); expect(await this.contract.onlyInitializingRan()).to.equal(true); @@ -67,13 +61,6 @@ contract('Initializable', function () { expect(await contract2.onlyInitializingRan()).to.equal(true); }); - it('multiple constructor levels can be initializers', async function () { - const contract2 = await ChildConstructorInitializableMock.new(); - expect(await contract2.initializerRan()).to.equal(true); - expect(await contract2.childInitializerRan()).to.equal(true); - expect(await contract2.onlyInitializingRan()).to.equal(true); - }); - describe('reinitialization', function () { beforeEach('deploying', async function () { this.contract = await ReinitializerMock.new(); @@ -205,15 +192,13 @@ contract('Initializable', function () { }); describe('disabling initialization', function () { - it('old and new patterns in bad sequence', async function () { - await expectRevertCustomError(DisableBad1.new(), 'AlreadyInitialized', []); - await expectRevertCustomError(DisableBad2.new(), 'AlreadyInitialized', []); - }); - - it('old and new patterns in good sequence', async function () { + it('disables initializers', async function () { const ok = await DisableOk.new(); - await expectEvent.inConstruction(ok, 'Initialized', { version: '1' }); await expectEvent.inConstruction(ok, 'Initialized', { version: '18446744073709551615' }); // MAX_UINT64 }); + + it('reverts disabling initializers and using the initializer modifier', async function () { + await expectRevertCustomError(DisableBad.new(), 'AlreadyInitialized', []); + }); }); }); From eabc9c00e7a35d46d04b1532134b3e32bb398d3f Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 17 Jul 2023 21:08:22 -0600 Subject: [PATCH 05/12] Improve gas (?) --- contracts/proxy/utils/Initializable.sol | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index f5f9f81b9cd..87a2701a80d 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -191,14 +191,11 @@ abstract contract Initializable { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); - // Cache values bool initializing = $._initializing; - uint64 initialized = $._initialized; - bool reinitializing = version > 1; - bool increasing = version > initialized; - - if (initializing ? reinitializing : !increasing) { + // When it's initializing check if it's a reinitializer, otherwise check if + // the intended version is not already set. + if (initializing ? version > 1 : version <= $._initialized) { revert AlreadyInitialized(); } From 9777b62972df08dfc1515cea53ecfeb66a53029e Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 20 Jul 2023 15:31:55 -0600 Subject: [PATCH 06/12] Improved function docs --- contracts/proxy/utils/Initializable.sol | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index 87a2701a80d..9c4297c0157 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -187,6 +187,14 @@ abstract contract Initializable { } } + /** + * @dev Sets the initialized version. + * + * Requirements: + * + * - If the contract is initializing the version set must not be that of an reinitializer. + * - If the contract is not initializing the version must not be already set. + */ function _setInitializedVersion(uint64 version) private returns (bool) { // solhint-disable-next-line var-name-mixedcase InitializableStorage storage $ = _getInitializableStorage(); @@ -204,6 +212,10 @@ abstract contract Initializable { return !initializing; } + /** + * @dev Runs before initialization. + * It sets the initialized version and sets the initializing flag to true. + */ function _beforeInitialize(uint64 version) private returns (bool) { bool isTopLevelCall = _setInitializedVersion(version); if (isTopLevelCall) { @@ -212,6 +224,10 @@ abstract contract Initializable { return isTopLevelCall; } + /** + * @dev Runs after initialization. + * It clears the initializing flag and emits an {Initialized} event if it is a top level call. + */ function _afterInitialize(bool isTopLevelCall, uint64 version) private { if (isTopLevelCall) { _getInitializableStorage()._initializing = false; From aa0bb6f3024098958d7850cd415e51fd17ddb972 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 27 Jul 2023 15:53:54 -0600 Subject: [PATCH 07/12] Revert #3950 fix --- contracts/mocks/InitializableMock.sol | 28 ++++++++- contracts/proxy/utils/Initializable.sol | 77 ++++++++----------------- test/proxy/utils/Initializable.test.js | 27 +++++++-- 3 files changed, 72 insertions(+), 60 deletions(-) diff --git a/contracts/mocks/InitializableMock.sol b/contracts/mocks/InitializableMock.sol index 29fa56ebc8c..ab5ea031132 100644 --- a/contracts/mocks/InitializableMock.sol +++ b/contracts/mocks/InitializableMock.sol @@ -25,6 +25,10 @@ contract InitializableMock is Initializable { onlyInitializingRan = true; } + function initializerNested() public initializer { + initialize(); + } + function onlyInitializingNested() public initializer { initializeOnlyInitializing(); } @@ -60,6 +64,18 @@ contract ConstructorInitializableMock is Initializable { } } +contract ChildConstructorInitializableMock is ConstructorInitializableMock { + bool public childInitializerRan; + + constructor() initializer { + childInitialize(); + } + + function childInitialize() public initializer { + childInitializerRan = true; + } +} + contract ReinitializerMock is Initializable { uint256 public counter; @@ -93,14 +109,22 @@ contract ReinitializerMock is Initializable { } } -contract DisableOk is Initializable { +contract DisableNew is Initializable { constructor() { _disableInitializers(); } } -contract DisableBad is Initializable { +contract DisableOld is Initializable { + constructor() initializer {} +} + +contract DisableBad1 is DisableNew, DisableOld {} + +contract DisableBad2 is Initializable { constructor() initializer { _disableInitializers(); } } + +contract DisableOk is DisableOld, DisableNew {} diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index 9c4297c0157..897e9eff87f 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -102,9 +102,22 @@ abstract contract Initializable { * Emits an {Initialized} event. */ modifier initializer() { - bool isTopLevelCall = _beforeInitialize(1); + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + bool isTopLevelCall = !$._initializing; + if (!(isTopLevelCall && $._initialized < 1) && !(address(this).code.length == 0 && $._initialized == 1)) { + revert AlreadyInitialized(); + } + $._initialized = 1; + if (isTopLevelCall) { + $._initializing = true; + } _; - _afterInitialize(isTopLevelCall, 1); + if (isTopLevelCall) { + $._initializing = false; + emit Initialized(1); + } } /** @@ -126,9 +139,17 @@ abstract contract Initializable { * Emits an {Initialized} event. */ modifier reinitializer(uint64 version) { - bool isTopLevelCall = _beforeInitialize(version); + // solhint-disable-next-line var-name-mixedcase + InitializableStorage storage $ = _getInitializableStorage(); + + if ($._initializing || $._initialized >= version) { + revert AlreadyInitialized(); + } + $._initialized = version; + $._initializing = true; _; - _afterInitialize(isTopLevelCall, version); + $._initializing = false; + emit Initialized(version); } /** @@ -186,52 +207,4 @@ abstract contract Initializable { $.slot := _INITIALIZABLE_STORAGE } } - - /** - * @dev Sets the initialized version. - * - * Requirements: - * - * - If the contract is initializing the version set must not be that of an reinitializer. - * - If the contract is not initializing the version must not be already set. - */ - function _setInitializedVersion(uint64 version) private returns (bool) { - // solhint-disable-next-line var-name-mixedcase - InitializableStorage storage $ = _getInitializableStorage(); - - bool initializing = $._initializing; - - // When it's initializing check if it's a reinitializer, otherwise check if - // the intended version is not already set. - if (initializing ? version > 1 : version <= $._initialized) { - revert AlreadyInitialized(); - } - - $._initialized = version; - - return !initializing; - } - - /** - * @dev Runs before initialization. - * It sets the initialized version and sets the initializing flag to true. - */ - function _beforeInitialize(uint64 version) private returns (bool) { - bool isTopLevelCall = _setInitializedVersion(version); - if (isTopLevelCall) { - _getInitializableStorage()._initializing = true; - } - return isTopLevelCall; - } - - /** - * @dev Runs after initialization. - * It clears the initializing flag and emits an {Initialized} event if it is a top level call. - */ - function _afterInitialize(bool isTopLevelCall, uint64 version) private { - if (isTopLevelCall) { - _getInitializableStorage()._initializing = false; - emit Initialized(version); - } - } } diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index 3e50111a42a..cb45995ea44 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -4,9 +4,11 @@ const { expectRevertCustomError } = require('../../helpers/customError'); const InitializableMock = artifacts.require('InitializableMock'); const ConstructorInitializableMock = artifacts.require('ConstructorInitializableMock'); +const ChildConstructorInitializableMock = artifacts.require('ChildConstructorInitializableMock'); const ReinitializerMock = artifacts.require('ReinitializerMock'); const SampleChild = artifacts.require('SampleChild'); -const DisableBad = artifacts.require('DisableBad'); +const DisableBad1 = artifacts.require('DisableBad1'); +const DisableBad2 = artifacts.require('DisableBad2'); const DisableOk = artifacts.require('DisableOk'); contract('Initializable', function () { @@ -44,6 +46,10 @@ contract('Initializable', function () { }); describe('nested under an initializer', function () { + it('initializer modifier reverts', async function () { + await expectRevertCustomError(this.contract.initializerNested(), 'AlreadyInitialized', []); + }); + it('onlyInitializing modifier succeeds', async function () { await this.contract.onlyInitializingNested(); expect(await this.contract.onlyInitializingRan()).to.equal(true); @@ -61,6 +67,13 @@ contract('Initializable', function () { expect(await contract2.onlyInitializingRan()).to.equal(true); }); + it('multiple constructor levels can be initializers', async function () { + const contract2 = await ChildConstructorInitializableMock.new(); + expect(await contract2.initializerRan()).to.equal(true); + expect(await contract2.childInitializerRan()).to.equal(true); + expect(await contract2.onlyInitializingRan()).to.equal(true); + }); + describe('reinitialization', function () { beforeEach('deploying', async function () { this.contract = await ReinitializerMock.new(); @@ -192,13 +205,15 @@ contract('Initializable', function () { }); describe('disabling initialization', function () { - it('disables initializers', async function () { - const ok = await DisableOk.new(); - await expectEvent.inConstruction(ok, 'Initialized', { version: '18446744073709551615' }); // MAX_UINT64 + it('old and new patterns in bad sequence', async function () { + await expectRevertCustomError(DisableBad1.new(), 'AlreadyInitialized', []); + await expectRevertCustomError(DisableBad2.new(), 'AlreadyInitialized', []); }); - it('reverts disabling initializers and using the initializer modifier', async function () { - await expectRevertCustomError(DisableBad.new(), 'AlreadyInitialized', []); + it('old and new patterns in good sequence', async function () { + const ok = await DisableOk.new(); + await expectEvent.inConstruction(ok, 'Initialized', { version: '1' }); + await expectEvent.inConstruction(ok, 'Initialized', { version: '18446744073709551615' }); // MAX_UINT64 }); }); }); From 1b27b496ab8bc58050d63f13c45fd8648924ee4f Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 27 Jul 2023 18:51:34 -0600 Subject: [PATCH 08/12] Fix conflict --- contracts/proxy/utils/Initializable.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index 4aa7fe4f52d..7f33a6267c0 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -165,7 +165,9 @@ abstract contract Initializable { * @dev Reverts if the contract is not in an initializing state. See {onlyInitializing}. */ function _checkInitializing() internal view virtual { - if (!_initializing) revert NotInitializing(); + if (!_isInitializing()) { + revert NotInitializing(); + } } /** From f106f034bf515c59276a3bdb436ce5dc6df2fbf2 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sat, 29 Jul 2023 19:23:02 -0600 Subject: [PATCH 09/12] Use 0xFFFFFFFFFFFFFFFF in tests --- test/proxy/utils/Initializable.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index cb45995ea44..e42850c07b5 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -213,7 +213,7 @@ contract('Initializable', function () { it('old and new patterns in good sequence', async function () { const ok = await DisableOk.new(); await expectEvent.inConstruction(ok, 'Initialized', { version: '1' }); - await expectEvent.inConstruction(ok, 'Initialized', { version: '18446744073709551615' }); // MAX_UINT64 + await expectEvent.inConstruction(ok, 'Initialized', { version: BigInt('0xFFFFFFFFFFFFFFFF').toString() }); // MAX_UINT64 }); }); }); From eb7699133cebbd22c8d6edf7ea701e187db4bad5 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 30 Jul 2023 18:26:49 -0600 Subject: [PATCH 10/12] Use 2n ** 64n - 1n in tests --- test/proxy/utils/Initializable.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index e42850c07b5..52d3031da01 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -213,7 +213,7 @@ contract('Initializable', function () { it('old and new patterns in good sequence', async function () { const ok = await DisableOk.new(); await expectEvent.inConstruction(ok, 'Initialized', { version: '1' }); - await expectEvent.inConstruction(ok, 'Initialized', { version: BigInt('0xFFFFFFFFFFFFFFFF').toString() }); // MAX_UINT64 + await expectEvent.inConstruction(ok, 'Initialized', { version: (2n ** 64n - 1n).toString() }); // MAX_UINT64 }); }); }); From 4d6d20505ed032217ecf57a0b31443434f6fbcf3 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 2 Aug 2023 14:54:53 -0600 Subject: [PATCH 11/12] Used web3.utils.toBN for tests --- test/helpers/constants.js | 7 +++++++ test/metatx/ERC2771Context.test.js | 3 +-- test/proxy/utils/Initializable.test.js | 3 ++- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 test/helpers/constants.js diff --git a/test/helpers/constants.js b/test/helpers/constants.js new file mode 100644 index 00000000000..0f4d028cfce --- /dev/null +++ b/test/helpers/constants.js @@ -0,0 +1,7 @@ +const MAX_UINT48 = web3.utils.toBN(1).shln(48).subn(1).toString(); +const MAX_UINT64 = web3.utils.toBN(1).shln(64).subn(1).toString(); + +module.exports = { + MAX_UINT48, + MAX_UINT64, +}; diff --git a/test/metatx/ERC2771Context.test.js b/test/metatx/ERC2771Context.test.js index 0ec8d98dde6..678deb5addb 100644 --- a/test/metatx/ERC2771Context.test.js +++ b/test/metatx/ERC2771Context.test.js @@ -1,6 +1,7 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const { getDomain, domainType } = require('../helpers/eip712'); +const { MAX_UINT48 } = require('../helpers/constants'); const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); @@ -14,8 +15,6 @@ const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior'); contract('ERC2771Context', function (accounts) { const [, trustedForwarder] = accounts; - const MAX_UINT48 = web3.utils.toBN(1).shln(48).subn(1).toString(); - beforeEach(async function () { this.forwarder = await ERC2771Forwarder.new('ERC2771Forwarder'); this.recipient = await ERC2771ContextMock.new(this.forwarder.address); diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index 52d3031da01..98ed19d36a2 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -1,6 +1,7 @@ const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { expectRevertCustomError } = require('../../helpers/customError'); +const { MAX_UINT64 } = require('../../helpers/constants'); const InitializableMock = artifacts.require('InitializableMock'); const ConstructorInitializableMock = artifacts.require('ConstructorInitializableMock'); @@ -213,7 +214,7 @@ contract('Initializable', function () { it('old and new patterns in good sequence', async function () { const ok = await DisableOk.new(); await expectEvent.inConstruction(ok, 'Initialized', { version: '1' }); - await expectEvent.inConstruction(ok, 'Initialized', { version: (2n ** 64n - 1n).toString() }); // MAX_UINT64 + await expectEvent.inConstruction(ok, 'Initialized', { version: MAX_UINT64 }); }); }); }); From 593509064f3730883671d234879eb88c78345be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 2 Aug 2023 21:30:04 -0600 Subject: [PATCH 12/12] Update .changeset/thick-pumpkins-exercise.md Co-authored-by: Francisco --- .changeset/thick-pumpkins-exercise.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/thick-pumpkins-exercise.md b/.changeset/thick-pumpkins-exercise.md index 52966efe8ad..8df8b51cc47 100644 --- a/.changeset/thick-pumpkins-exercise.md +++ b/.changeset/thick-pumpkins-exercise.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': major --- -`Initializable`: Allow versions greater than 256. +`Initializable`: Use the namespaced storage pattern to avoid putting critical variables in slot 0. Allow reinitializer versions greater than 256.