Skip to content

Commit 553c8fd

Browse files
committed
Update initializer modifier to prevent reentrancy during initialization (#3006)
Co-authored-by: Francisco Giordano <[email protected]> (cherry picked from commit 08840b9)
1 parent 4961a51 commit 553c8fd

File tree

5 files changed

+144
-13
lines changed

5 files changed

+144
-13
lines changed

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
## 4.4.1 (2021-12-10)
4+
5+
* `Initializable`: change the existing `initializer` modifier and add a new `onlyInitializing` modifier to prevent reentrancy risk. ([#3006](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3006))
6+
7+
### Breaking change
8+
9+
It is no longer possible to call an `initializer`-protected function from within another `initializer` function outside the context of a constructor. Projects using OpenZeppelin upgradeable proxies should continue to work as is, since in the common case the initializer is invoked in the constructor directly. If this is not the case for you, the suggested change is to use the new `onlyInitializing` modifier in the following way:
10+
11+
```diff
12+
contract A {
13+
- function initialize() public initializer { ... }
14+
+ function initialize() internal onlyInitializing { ... }
15+
}
16+
contract B is A {
17+
function initialize() public initializer {
18+
A.initialize();
19+
}
20+
}
21+
```
22+
323
## 4.4.0 (2021-11-25)
424

525
* `Ownable`: add an internal `_transferOwnership(address)`. ([#2568](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2568))

contracts/mocks/InitializableMock.sol

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,25 @@ import "../proxy/utils/Initializable.sol";
1010
*/
1111
contract InitializableMock is Initializable {
1212
bool public initializerRan;
13+
bool public onlyInitializingRan;
1314
uint256 public x;
1415

1516
function initialize() public initializer {
1617
initializerRan = true;
1718
}
1819

19-
function initializeNested() public initializer {
20+
function initializeOnlyInitializing() public onlyInitializing {
21+
onlyInitializingRan = true;
22+
}
23+
24+
function initializerNested() public initializer {
2025
initialize();
2126
}
2227

28+
function onlyInitializingNested() public initializer {
29+
initializeOnlyInitializing();
30+
}
31+
2332
function initializeWithX(uint256 _x) public payable initializer {
2433
x = _x;
2534
}
@@ -32,3 +41,21 @@ contract InitializableMock is Initializable {
3241
require(false, "InitializableMock forced failure");
3342
}
3443
}
44+
45+
contract ConstructorInitializableMock is Initializable {
46+
bool public initializerRan;
47+
bool public onlyInitializingRan;
48+
49+
constructor() initializer {
50+
initialize();
51+
initializeOnlyInitializing();
52+
}
53+
54+
function initialize() public initializer {
55+
initializerRan = true;
56+
}
57+
58+
function initializeOnlyInitializing() public onlyInitializing {
59+
onlyInitializingRan = true;
60+
}
61+
}

contracts/mocks/MultipleInheritanceInitializableMocks.sol

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,16 @@ contract SampleHuman is Initializable {
2222
bool public isHuman;
2323

2424
function initialize() public initializer {
25+
__SampleHuman_init();
26+
}
27+
28+
// solhint-disable-next-line func-name-mixedcase
29+
function __SampleHuman_init() internal onlyInitializing {
30+
__SampleHuman_init_unchained();
31+
}
32+
33+
// solhint-disable-next-line func-name-mixedcase
34+
function __SampleHuman_init_unchained() internal onlyInitializing {
2535
isHuman = true;
2636
}
2737
}
@@ -33,7 +43,17 @@ contract SampleMother is Initializable, SampleHuman {
3343
uint256 public mother;
3444

3545
function initialize(uint256 value) public virtual initializer {
36-
SampleHuman.initialize();
46+
__SampleMother_init(value);
47+
}
48+
49+
// solhint-disable-next-line func-name-mixedcase
50+
function __SampleMother_init(uint256 value) internal onlyInitializing {
51+
__SampleHuman_init();
52+
__SampleMother_init_unchained(value);
53+
}
54+
55+
// solhint-disable-next-line func-name-mixedcase
56+
function __SampleMother_init_unchained(uint256 value) internal onlyInitializing {
3757
mother = value;
3858
}
3959
}
@@ -45,7 +65,17 @@ contract SampleGramps is Initializable, SampleHuman {
4565
string public gramps;
4666

4767
function initialize(string memory value) public virtual initializer {
48-
SampleHuman.initialize();
68+
__SampleGramps_init(value);
69+
}
70+
71+
// solhint-disable-next-line func-name-mixedcase
72+
function __SampleGramps_init(string memory value) internal onlyInitializing {
73+
__SampleHuman_init();
74+
__SampleGramps_init_unchained(value);
75+
}
76+
77+
// solhint-disable-next-line func-name-mixedcase
78+
function __SampleGramps_init_unchained(string memory value) internal onlyInitializing {
4979
gramps = value;
5080
}
5181
}
@@ -57,7 +87,17 @@ contract SampleFather is Initializable, SampleGramps {
5787
uint256 public father;
5888

5989
function initialize(string memory _gramps, uint256 _father) public initializer {
60-
SampleGramps.initialize(_gramps);
90+
__SampleFather_init(_gramps, _father);
91+
}
92+
93+
// solhint-disable-next-line func-name-mixedcase
94+
function __SampleFather_init(string memory _gramps, uint256 _father) internal onlyInitializing {
95+
__SampleGramps_init(_gramps);
96+
__SampleFather_init_unchained(_father);
97+
}
98+
99+
// solhint-disable-next-line func-name-mixedcase
100+
function __SampleFather_init_unchained(uint256 _father) internal onlyInitializing {
61101
father = _father;
62102
}
63103
}
@@ -74,8 +114,23 @@ contract SampleChild is Initializable, SampleMother, SampleFather {
74114
uint256 _father,
75115
uint256 _child
76116
) public initializer {
77-
SampleMother.initialize(_mother);
78-
SampleFather.initialize(_gramps, _father);
117+
__SampleChild_init(_mother, _gramps, _father, _child);
118+
}
119+
120+
// solhint-disable-next-line func-name-mixedcase
121+
function __SampleChild_init(
122+
uint256 _mother,
123+
string memory _gramps,
124+
uint256 _father,
125+
uint256 _child
126+
) internal onlyInitializing {
127+
__SampleMother_init(_mother);
128+
__SampleFather_init(_gramps, _father);
129+
__SampleChild_init_unchained(_child);
130+
}
131+
132+
// solhint-disable-next-line func-name-mixedcase
133+
function __SampleChild_init_unchained(uint256 _child) internal onlyInitializing {
79134
child = _child;
80135
}
81136
}

contracts/proxy/utils/Initializable.sol

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
pragma solidity ^0.8.0;
55

6+
import "../../utils/Address.sol";
7+
68
/**
79
* @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed
810
* behind a proxy. Since a proxied contract can't have a constructor, it's common to move constructor logic to an
@@ -45,7 +47,10 @@ abstract contract Initializable {
4547
* @dev Modifier to protect an initializer function from being invoked twice.
4648
*/
4749
modifier initializer() {
48-
require(_initializing || !_initialized, "Initializable: contract is already initialized");
50+
// If the contract is initializing we ignore whether _initialized is set in order to support multiple
51+
// inheritance patterns, but we only do this in the context of a constructor, because in other contexts the
52+
// contract may have been reentered.
53+
require(_initializing ? _isConstructor() : !_initialized, "Initializable: contract is already initialized");
4954

5055
bool isTopLevelCall = !_initializing;
5156
if (isTopLevelCall) {
@@ -59,4 +64,17 @@ abstract contract Initializable {
5964
_initializing = false;
6065
}
6166
}
67+
68+
/**
69+
* @dev Modifier to protect an initialization function so that it can only be invoked by functions with the
70+
* {initializer} modifier, directly or indirectly.
71+
*/
72+
modifier onlyInitializing() {
73+
require(_initializing, "Initializable: contract is not initializing");
74+
_;
75+
}
76+
77+
function _isConstructor() private view returns (bool) {
78+
return !Address.isContract(address(this));
79+
}
6280
}

test/proxy/utils/Initializable.test.js

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
const { expectRevert } = require('@openzeppelin/test-helpers');
2-
32
const { assert } = require('chai');
43

54
const InitializableMock = artifacts.require('InitializableMock');
5+
const ConstructorInitializableMock = artifacts.require('ConstructorInitializableMock');
66
const SampleChild = artifacts.require('SampleChild');
77

88
contract('Initializable', function (accounts) {
@@ -31,15 +31,26 @@ contract('Initializable', function (accounts) {
3131
});
3232
});
3333

34-
context('after nested initialize', function () {
35-
beforeEach('initializing', async function () {
36-
await this.contract.initializeNested();
34+
context('nested under an initializer', function () {
35+
it('initializer modifier reverts', async function () {
36+
await expectRevert(this.contract.initializerNested(), 'Initializable: contract is already initialized');
3737
});
3838

39-
it('initializer has run', async function () {
40-
assert.isTrue(await this.contract.initializerRan());
39+
it('onlyInitializing modifier succeeds', async function () {
40+
await this.contract.onlyInitializingNested();
41+
assert.isTrue(await this.contract.onlyInitializingRan());
4142
});
4243
});
44+
45+
it('cannot call onlyInitializable function outside the scope of an initializable function', async function () {
46+
await expectRevert(this.contract.initializeOnlyInitializing(), 'Initializable: contract is not initializing');
47+
});
48+
});
49+
50+
it('nested initializer can run during construction', async function () {
51+
const contract2 = await ConstructorInitializableMock.new();
52+
assert.isTrue(await contract2.initializerRan());
53+
assert.isTrue(await contract2.onlyInitializingRan());
4354
});
4455

4556
describe('complex testing with inheritance', function () {

0 commit comments

Comments
 (0)