diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eef2cb91..086c58cc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ * `ERC20Wrapper`: the `decimals()` function now tries to fetch the value from the underlying token instance. If that calls revert, then the default value is used. ([#3259](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3259)) * `Governor`: Implement `IERC721Receiver` and `IERC1155Receiver` to improve token custody by governors. ([#3230](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3230)) * `TimelockController`: Implement `IERC721Receiver` and `IERC1155Receiver` to improve token custody by timelocks. ([#3230](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3230)) + * `Initializable`: add a reinitializer modifier that enables the initialization of new modules, added to already initialized contracts through upgradeability. ([#3232](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3232)) + * `Initializable`: add an Initialized event that tracks initialized version numbers. ([#3294](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3294)) ### Breaking changes diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index e6b2a4423..3ec66bb6e 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -66,6 +66,11 @@ abstract contract Initializable { */ bool private _initializing; + /** + * @dev Triggered when the contract has been initialized or reinitialized. + */ + event Initialized(uint8 version); + /** * @dev A modifier that defines a protected initializer function that can be invoked at most once. In its scope, * `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`. @@ -78,6 +83,7 @@ abstract contract Initializable { _; if (isTopLevelCall) { _initializing = false; + emit Initialized(1); } } @@ -101,6 +107,7 @@ abstract contract Initializable { _; if (isTopLevelCall) { _initializing = false; + emit Initialized(version); } } diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index 1efb72850..9879b4820 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -1,4 +1,4 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const InitializableMock = artifacts.require('InitializableMock'); @@ -12,13 +12,13 @@ contract('Initializable', function (accounts) { this.contract = await InitializableMock.new(); }); - context('before initialize', function () { + describe('before initialize', function () { it('initializer has not run', async function () { expect(await this.contract.initializerRan()).to.equal(false); }); }); - context('after initialize', function () { + describe('after initialize', function () { beforeEach('initializing', async function () { await this.contract.initialize(); }); @@ -32,7 +32,7 @@ contract('Initializable', function (accounts) { }); }); - context('nested under an initializer', function () { + describe('nested under an initializer', function () { it('initializer modifier reverts', async function () { await expectRevert(this.contract.initializerNested(), 'Initializable: contract is already initialized'); }); @@ -108,6 +108,39 @@ contract('Initializable', function (accounts) { }); }); + describe('events', function () { + it('constructor initialization emits event', async function () { + const contract = await ConstructorInitializableMock.new(); + + await expectEvent.inTransaction(contract.transactionHash, contract, 'Initialized', { version: '1' }); + }); + + it('initialization emits event', async function () { + const contract = await ReinitializerMock.new(); + + const { receipt } = await contract.initialize(); + expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(1); + expectEvent(receipt, 'Initialized', { version: '1' }); + }); + + it('reinitialization emits event', async function () { + const contract = await ReinitializerMock.new(); + + const { receipt } = await contract.reinitialize(128); + expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(1); + expectEvent(receipt, 'Initialized', { version: '128' }); + }); + + it('chained reinitialization emits multiple events', async function () { + const contract = await ReinitializerMock.new(); + + const { receipt } = await contract.chainReinitialize(2, 3); + expect(receipt.logs.filter(({ event }) => event === 'Initialized').length).to.be.equal(2); + expectEvent(receipt, 'Initialized', { version: '2' }); + expectEvent(receipt, 'Initialized', { version: '3' }); + }); + }); + describe('complex testing with inheritance', function () { const mother = '12'; const gramps = '56';