From 943a663a31021db3cb4894c5617bdf53bb5bde3d Mon Sep 17 00:00:00 2001 From: Andres Bachfischer <38696580+andresbach@users.noreply.github.com> Date: Tue, 11 Aug 2020 16:42:51 -0300 Subject: [PATCH 01/20] Updated ERC1155 tests (#2107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alan Lu Co-authored-by: Nicolás Venturo Co-authored-by: Francisco Giordano --- test/token/ERC1155/ERC1155.behavior.js | 72 +++++++++++++++++++++++- test/token/ERC1155/ERC1155.test.js | 27 ++++++++- test/token/ERC1155/ERC1155Holder.test.js | 51 ++++++++++------- 3 files changed, 127 insertions(+), 23 deletions(-) diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index fb25d175c..9401cdfa9 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -92,6 +92,14 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder, ), 'ERC1155: accounts and ids length mismatch' ); + + await expectRevert( + this.token.balanceOfBatch( + [firstTokenHolder, secondTokenHolder], + [firstTokenId, secondTokenId, unknownTokenId] + ), + 'ERC1155: accounts and ids length mismatch' + ); }); it('reverts when one of the addresses is the zero address', async function () { @@ -143,6 +151,18 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder, expect(result[1]).to.be.a.bignumber.equal(firstAmount); expect(result[2]).to.be.a.bignumber.equal('0'); }); + + it('returns multiple times the balance of the same address when asked', async function () { + const result = await this.token.balanceOfBatch( + [firstTokenHolder, secondTokenHolder, firstTokenHolder], + [firstTokenId, secondTokenId, firstTokenId] + ); + expect(result).to.be.an('array'); + expect(result[0]).to.be.a.bignumber.equal(result[2]); + expect(result[0]).to.be.a.bignumber.equal(firstAmount); + expect(result[1]).to.be.a.bignumber.equal(secondAmount); + expect(result[2]).to.be.a.bignumber.equal(firstAmount); + }); }); }); @@ -298,8 +318,11 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder, }); it('preserves operator\'s balances not involved in the transfer', async function () { - const balance = await this.token.balanceOf(proxy, firstTokenId); - expect(balance).to.be.a.bignumber.equal('0'); + const balance1 = await this.token.balanceOf(proxy, firstTokenId); + expect(balance1).to.be.a.bignumber.equal('0'); + + const balance2 = await this.token.balanceOf(proxy, secondTokenId); + expect(balance2).to.be.a.bignumber.equal('0'); }); }); }); @@ -464,6 +487,16 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder, ), 'ERC1155: ids and amounts length mismatch' ); + + await expectRevert( + this.token.safeBatchTransferFrom( + multiTokenHolder, recipient, + [firstTokenId, secondTokenId], + [firstAmount], + '0x', { from: multiTokenHolder } + ), + 'ERC1155: ids and amounts length mismatch' + ); }); it('reverts when transferring to zero address', async function () { @@ -684,6 +717,41 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder, }); }); + context('to a receiver contract that reverts only on single transfers', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, true, + RECEIVER_BATCH_MAGIC_VALUE, false, + ); + + this.toWhom = this.receiver.address; + this.transferReceipt = await this.token.safeBatchTransferFrom( + multiTokenHolder, this.receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', { from: multiTokenHolder }, + ); + ({ logs: this.transferLogs } = this.transferReceipt); + }); + + batchTransferWasSuccessful.call(this, { + operator: multiTokenHolder, + from: multiTokenHolder, + ids: [firstTokenId, secondTokenId], + values: [firstAmount, secondAmount], + }); + + it('should call onERC1155BatchReceived', async function () { + await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', { + operator: multiTokenHolder, + from: multiTokenHolder, + // ids: [firstTokenId, secondTokenId], + // values: [firstAmount, secondAmount], + data: null, + }); + }); + }); + context('to a contract that does not implement the required function', function () { it('reverts', async function () { const invalidReceiver = this.token; diff --git a/test/token/ERC1155/ERC1155.test.js b/test/token/ERC1155/ERC1155.test.js index a9248830e..97818c3af 100644 --- a/test/token/ERC1155/ERC1155.test.js +++ b/test/token/ERC1155/ERC1155.test.js @@ -28,7 +28,7 @@ describe('ERC1155', function () { const mintAmounts = [new BN(5000), new BN(10000), new BN(42195)]; const burnAmounts = [new BN(5000), new BN(9001), new BN(195)]; - const data = '0xcafebabe'; + const data = '0x12345678'; describe('_mint', function () { it('reverts with a zero destination address', async function () { @@ -72,6 +72,11 @@ describe('ERC1155', function () { this.token.mintBatch(tokenBatchHolder, tokenBatchIds, mintAmounts.slice(1), data), 'ERC1155: ids and amounts length mismatch' ); + + await expectRevert( + this.token.mintBatch(tokenBatchHolder, tokenBatchIds.slice(1), mintAmounts, data), + 'ERC1155: ids and amounts length mismatch' + ); }); context('with minted batch of tokens', function () { @@ -121,6 +126,21 @@ describe('ERC1155', function () { ); }); + it('reverts when burning more than available tokens', async function () { + await this.token.mint( + tokenHolder, + tokenId, + mintAmount, + data, + { from: operator } + ); + + await expectRevert( + this.token.burn(tokenHolder, tokenId, mintAmount.addn(1)), + 'ERC1155: burn amount exceeds balance' + ); + }); + context('with minted-then-burnt tokens', function () { beforeEach(async function () { await this.token.mint(tokenHolder, tokenId, mintAmount, data); @@ -164,6 +184,11 @@ describe('ERC1155', function () { this.token.burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts.slice(1)), 'ERC1155: ids and amounts length mismatch' ); + + await expectRevert( + this.token.burnBatch(tokenBatchHolder, tokenBatchIds.slice(1), burnAmounts), + 'ERC1155: ids and amounts length mismatch' + ); }); it('reverts when burning a non-existent token id', async function () { diff --git a/test/token/ERC1155/ERC1155Holder.test.js b/test/token/ERC1155/ERC1155Holder.test.js index 912abcc1b..80044a3cd 100644 --- a/test/token/ERC1155/ERC1155Holder.test.js +++ b/test/token/ERC1155/ERC1155Holder.test.js @@ -8,41 +8,52 @@ const { expect } = require('chai'); describe('ERC1155Holder', function () { const [creator] = accounts; + const uri = 'https://token-cdn-domain/{id}.json'; + const multiTokenIds = [new BN(1), new BN(2), new BN(3)]; + const multiTokenAmounts = [new BN(1000), new BN(2000), new BN(3000)]; + const transferData = '0x12345678'; - it('receives ERC1155 tokens', async function () { - const uri = 'https://token-cdn-domain/{id}.json'; + beforeEach(async function () { + this.multiToken = await ERC1155Mock.new(uri, { from: creator }); + this.holder = await ERC1155Holder.new(); + await this.multiToken.mintBatch(creator, multiTokenIds, multiTokenAmounts, '0x', { from: creator }); + }); - const multiToken = await ERC1155Mock.new(uri, { from: creator }); - const multiTokenIds = [new BN(1), new BN(2), new BN(3)]; - const multiTokenAmounts = [new BN(1000), new BN(2000), new BN(3000)]; - await multiToken.mintBatch(creator, multiTokenIds, multiTokenAmounts, '0x', { from: creator }); - - const transferData = '0xf00dbabe'; - - const holder = await ERC1155Holder.new(); - - await multiToken.safeTransferFrom( + it('receives ERC1155 tokens from a single ID', async function () { + await this.multiToken.safeTransferFrom( creator, - holder.address, + this.holder.address, multiTokenIds[0], multiTokenAmounts[0], transferData, { from: creator }, ); - expect(await multiToken.balanceOf(holder.address, multiTokenIds[0])).to.be.bignumber.equal(multiTokenAmounts[0]); + expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[0])) + .to.be.bignumber.equal(multiTokenAmounts[0]); - await multiToken.safeBatchTransferFrom( + for (let i = 1; i < multiTokenIds.length; i++) { + expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[i])).to.be.bignumber.equal(new BN(0)); + } + }); + + it('receives ERC1155 tokens from a multiple IDs', async function () { + for (let i = 0; i < multiTokenIds.length; i++) { + expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[i])).to.be.bignumber.equal(new BN(0)); + }; + + await this.multiToken.safeBatchTransferFrom( creator, - holder.address, - multiTokenIds.slice(1), - multiTokenAmounts.slice(1), + this.holder.address, + multiTokenIds, + multiTokenAmounts, transferData, { from: creator }, ); - for (let i = 1; i < multiTokenIds.length; i++) { - expect(await multiToken.balanceOf(holder.address, multiTokenIds[i])).to.be.bignumber.equal(multiTokenAmounts[i]); + for (let i = 0; i < multiTokenIds.length; i++) { + expect(await this.multiToken.balanceOf(this.holder.address, multiTokenIds[i])) + .to.be.bignumber.equal(multiTokenAmounts[i]); } }); }); From 722879b32d930780a4d3d496d0737b7b3df865a6 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Tue, 11 Aug 2020 16:45:44 -0300 Subject: [PATCH 02/20] increase mocha timeout --- .mocharc.js | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .mocharc.js diff --git a/.mocharc.js b/.mocharc.js new file mode 100644 index 000000000..fac5fdfe6 --- /dev/null +++ b/.mocharc.js @@ -0,0 +1,3 @@ +module.exports = { + timeout: 4000, +}; From 9700e6b4bdcd3dace1405c17cd4f048d455d4014 Mon Sep 17 00:00:00 2001 From: "Julian M. Rodriguez" <56316686+julianmrodri@users.noreply.github.com> Date: Tue, 11 Aug 2020 16:51:58 -0300 Subject: [PATCH 03/20] Use beforeTokenTransfer hook in ERC20Snapshot (#2312) --- contracts/token/ERC20/ERC20Snapshot.sol | 33 ++++++++++--------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/contracts/token/ERC20/ERC20Snapshot.sol b/contracts/token/ERC20/ERC20Snapshot.sol index 54d91c057..cc25d618e 100644 --- a/contracts/token/ERC20/ERC20Snapshot.sol +++ b/contracts/token/ERC20/ERC20Snapshot.sol @@ -104,28 +104,21 @@ abstract contract ERC20Snapshot is ERC20 { return snapshotted ? value : totalSupply(); } - // _transfer, _mint and _burn are the only functions where the balances are modified, so it is there that the - // snapshots are updated. Note that the update happens _before_ the balance change, with the pre-modified value. - // The same is true for the total supply and _mint and _burn. - function _transfer(address from, address to, uint256 value) internal virtual override { + + // Update balance and/or total supply snapshots before the values are modified. This is implemented + // in the _beforeTokenTransfer hook, which is executed for _mint, _burn, and _transfer operations. + function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override { + super._beforeTokenTransfer(from, to, amount); + + if (from == address(0) || to == address(0)) { + // mint or burn + _updateAccountSnapshot(from); + _updateTotalSupplySnapshot(); + } else { + // transfer _updateAccountSnapshot(from); _updateAccountSnapshot(to); - - super._transfer(from, to, value); - } - - function _mint(address account, uint256 value) internal virtual override { - _updateAccountSnapshot(account); - _updateTotalSupplySnapshot(); - - super._mint(account, value); - } - - function _burn(address account, uint256 value) internal virtual override { - _updateAccountSnapshot(account); - _updateTotalSupplySnapshot(); - - super._burn(account, value); + } } function _valueAt(uint256 snapshotId, Snapshots storage snapshots) From d1f336d8fddc14598a59a7dd054a34a9726dc3b3 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Wed, 12 Aug 2020 20:47:30 -0300 Subject: [PATCH 04/20] use svg logo for better scaling --- README.md | 2 +- logo.png | Bin 126219 -> 0 bytes logo.svg | 15 +++++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) delete mode 100644 logo.png create mode 100644 logo.svg diff --git a/README.md b/README.md index 0c993c135..c731a4097 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# OpenZeppelin +# OpenZeppelin [![Docs](https://img.shields.io/badge/docs-%F0%9F%93%84-blue)](https://docs.openzeppelin.com/contracts) [![NPM Package](https://img.shields.io/npm/v/@openzeppelin/contracts.svg)](https://www.npmjs.org/package/@openzeppelin/contracts) diff --git a/logo.png b/logo.png deleted file mode 100644 index fd65cdb70a4671e8425069281f42df0ceb380c28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 126219 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 + + + + + + + + + + + + + + From 6d5a73815de843267c75f9134c4c11243fa895d8 Mon Sep 17 00:00:00 2001 From: sunhurts <66348679+sunhurts@users.noreply.github.com> Date: Fri, 14 Aug 2020 20:29:20 +0530 Subject: [PATCH 05/20] Fix ERC20Snapshot#_beforeTokenTransfer (#2328) --- contracts/token/ERC20/ERC20Snapshot.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/contracts/token/ERC20/ERC20Snapshot.sol b/contracts/token/ERC20/ERC20Snapshot.sol index cc25d618e..0f8e7b6f5 100644 --- a/contracts/token/ERC20/ERC20Snapshot.sol +++ b/contracts/token/ERC20/ERC20Snapshot.sol @@ -110,8 +110,12 @@ abstract contract ERC20Snapshot is ERC20 { function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual override { super._beforeTokenTransfer(from, to, amount); - if (from == address(0) || to == address(0)) { - // mint or burn + if (from == address(0)) { + // mint + _updateAccountSnapshot(to); + _updateTotalSupplySnapshot(); + } else if (to == address(0)) { + // burn _updateAccountSnapshot(from); _updateTotalSupplySnapshot(); } else { From 7d48d79b532f9f79a9984924ff94c0bb97da5980 Mon Sep 17 00:00:00 2001 From: Alejo Salles Date: Tue, 18 Aug 2020 12:50:35 -0300 Subject: [PATCH 06/20] Fixed sidebar reference in README.md (#2329) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c731a4097..ad52447f5 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ To keep your system secure, you should **always** use the installed code as-is, ## Learn More -The guides in the sidebar will teach about different concepts, and how to use the related contracts that OpenZeppelin Contracts provides: +The guides in the [docs site](https://docs.openzeppelin.com/contracts) will teach about different concepts, and how to use the related contracts that OpenZeppelin Contracts provides: * [Access Control](https://docs.openzeppelin.com/contracts/access-control): decide who can perform each of the actions on your system. * [Tokens](https://docs.openzeppelin.com/contracts/tokens): create tradeable assets or collectives, and distribute them via [Crowdsales](https://docs.openzeppelin.com/contracts/crowdsales). From b1ea59e8147ea084d395ee15d66691d1eff52ecb Mon Sep 17 00:00:00 2001 From: "Julian M. Rodriguez" <56316686+julianmrodri@users.noreply.github.com> Date: Tue, 18 Aug 2020 20:40:51 -0300 Subject: [PATCH 07/20] Improve testing for ERC20Snapshot (#2331) --- test/token/ERC20/ERC20Snapshot.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/token/ERC20/ERC20Snapshot.test.js b/test/token/ERC20/ERC20Snapshot.test.js index c8aac9f7f..a00738193 100644 --- a/test/token/ERC20/ERC20Snapshot.test.js +++ b/test/token/ERC20/ERC20Snapshot.test.js @@ -134,7 +134,7 @@ describe('ERC20Snapshot', function () { context('with balance changes after the snapshot', function () { beforeEach(async function () { await this.token.transfer(recipient, new BN('10'), { from: initialHolder }); - await this.token.mint(recipient, new BN('50')); + await this.token.mint(other, new BN('50')); await this.token.burn(initialHolder, new BN('20')); }); From 0fc9578fe6984f30fd745bcb51ad82bc632f3af2 Mon Sep 17 00:00:00 2001 From: Alejo Salles Date: Tue, 18 Aug 2020 20:57:49 -0300 Subject: [PATCH 08/20] Merge CODE_STYLE.md into GUIDELINES.md (#2330) --- CODE_STYLE.md | 69 --------------------------------------------- GUIDELINES.md | 77 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 59 insertions(+), 87 deletions(-) delete mode 100644 CODE_STYLE.md diff --git a/CODE_STYLE.md b/CODE_STYLE.md deleted file mode 100644 index a5beeb971..000000000 --- a/CODE_STYLE.md +++ /dev/null @@ -1,69 +0,0 @@ -# Code Style - -We value clean code and consistency, and those are prerequisites for us to -include new code in the repository. Before proposing a change, please read this -document and take some time to familiarize yourself with the style of the -existing codebase. - -## Solidity code - -In order to be consistent with all the other Solidity projects, we follow the -[official recommendations documented in the Solidity style guide](http://solidity.readthedocs.io/en/latest/style-guide.html). - -Any exception or additions specific to our project are documented below. - -### Naming - -* Try to avoid acronyms and abbreviations. - -* All state variables should be private. - -* Private state variables should have an underscore prefix. - - ``` - contract TestContract { - uint256 private _privateVar; - uint256 internal _internalVar; - } - ``` - -* Parameters must not be prefixed with an underscore. - - ``` - function test(uint256 testParameter1, uint256 testParameter2) { - ... - } - ``` - -* Internal and private functions should have an underscore prefix. - - ``` - function _testInternal() internal { - ... - } - ``` - - ``` - function _testPrivate() private { - ... - } - ``` - -* Events should be emitted immediately after the state change that they - represent, and consequently they should be named in past tense. - - ``` - function _burn(address _who, uint256 _value) internal { - super._burn(_who, _value); - emit TokensBurned(_who, _value); - } - ``` - - Some standards (e.g. ERC20) use present tense, and in those cases the - standard specification prevails. - -* Interface names should have a capital I prefix. - - ``` - interface IERC777 { - ``` diff --git a/GUIDELINES.md b/GUIDELINES.md index c88559517..2766fec3b 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -28,37 +28,78 @@ Consistency on the way classes are used is paramount to an easier understanding #### D6 - Regular Audits Following good programming practices is a way to reduce the risk of vulnerabilities, but professional code audits are still needed. We will perform regular code audits on major releases, and hire security professionals to provide independent review. -## Style Guidelines +# Style Guidelines -The design guidelines have quite a high abstraction level. These style guidelines are more concrete and easier to apply, and also more opinionated. +The design guidelines have quite a high abstraction level. These style guidelines are more concrete and easier to apply, and also more opinionated. We value clean code and consistency, and those are prerequisites for us to include new code in the repository. Before proposing a change, please read these guidelines and take some time to familiarize yourself with the style of the existing codebase. -### General +## Solidity code -#### G0 - Default to Solidity's official style guide. +In order to be consistent with all the other Solidity projects, we follow the +[official recommendations documented in the Solidity style guide](http://solidity.readthedocs.io/en/latest/style-guide.html). -Follow the official Solidity style guide: https://solidity.readthedocs.io/en/latest/style-guide.html +Any exception or additions specific to our project are documented below. -#### G1 - No Magic Constants +* Try to avoid acronyms and abbreviations. -Avoid constants in the code as much as possible. Magic strings are also magic constants. +* All state variables should be private. -#### G2 - Code that Fails Early +* Private state variables should have an underscore prefix. -We ask our code to fail as soon as possible when an unexpected input was provided or unexpected state was found. + ``` + contract TestContract { + uint256 private _privateVar; + uint256 internal _internalVar; + } + ``` -#### G3 - Internal Amounts Must be Signed Integers and Represent the Smallest Units. +* Parameters must not be prefixed with an underscore. -Avoid representation errors by always dealing with weis when handling ether. GUIs can convert to more human-friendly representations. Use Signed Integers (int) to prevent underflow problems. + ``` + function test(uint256 testParameter1, uint256 testParameter2) { + ... + } + ``` + +* Internal and private functions should have an underscore prefix. + + ``` + function _testInternal() internal { + ... + } + ``` + + ``` + function _testPrivate() private { + ... + } + ``` + +* Events should be emitted immediately after the state change that they + represent, and consequently they should be named in past tense. + + ``` + function _burn(address _who, uint256 _value) internal { + super._burn(_who, _value); + emit TokensBurned(_who, _value); + } + ``` + + Some standards (e.g. ERC20) use present tense, and in those cases the + standard specification prevails. + +* Interface names should have a capital I prefix. + + ``` + interface IERC777 { + ``` -### Testing +## Tests -#### T1 - Tests Must be Written Elegantly +* Tests Must be Written Elegantly -Style guidelines are not relaxed for tests. Tests are a good way to show how to use the library, and maintaining them is extremely necessary. + Tests are a good way to show how to use the library, and maintaining them is extremely necessary. Don't write long tests, write helper functions to make them be as short and concise as possible (they should take just a few lines each), and use good variable names. -Don't write long tests, write helper functions to make them be as short and concise as possible (they should take just a few lines each), and use good variable names. +* Tests Must not be Random -#### T2 - Tests Must not be Random - -Inputs for tests should not be generated randomly. Accounts used to create test contracts are an exception, those can be random. Also, the type and structure of outputs should be checked. + Inputs for tests should not be generated randomly. Accounts used to create test contracts are an exception, those can be random. Also, the type and structure of outputs should be checked. From e5da0986bbc3217c4c82a3a7ca6a9a312599c74c Mon Sep 17 00:00:00 2001 From: "Julian M. Rodriguez" <56316686+julianmrodri@users.noreply.github.com> Date: Wed, 19 Aug 2020 11:23:36 -0300 Subject: [PATCH 09/20] Fix code style parameters in Event (#2324) * chore: fix code style parameters in Event * chore: update code style for events --- GUIDELINES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GUIDELINES.md b/GUIDELINES.md index 2766fec3b..5ea5566e8 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -78,9 +78,9 @@ Any exception or additions specific to our project are documented below. represent, and consequently they should be named in past tense. ``` - function _burn(address _who, uint256 _value) internal { - super._burn(_who, _value); - emit TokensBurned(_who, _value); + function _burn(address who, uint256 value) internal { + super._burn(who, value); + emit TokensBurned(who, value); } ``` From 1f06fd7e66d9cf4c0d568e5e23093fc158a6f011 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2020 18:00:34 -0300 Subject: [PATCH 10/20] Update all non-major dependencies (#2317) * Update all non-major dependencies * disable solhint reason-string rule Co-authored-by: Renovate Bot Co-authored-by: Francisco Giordano --- .solhint.json | 3 +- package-lock.json | 601 ++++++++++++++++++++++++++++------------------ 2 files changed, 365 insertions(+), 239 deletions(-) diff --git a/.solhint.json b/.solhint.json index 82f1dbede..44741b02f 100644 --- a/.solhint.json +++ b/.solhint.json @@ -5,6 +5,7 @@ "mark-callable-contracts": "off", "no-empty-blocks": "off", "compiler-version": ["error", "^0.6.0"], - "private-vars-leading-underscore": "error" + "private-vars-leading-underscore": "error", + "reason-string": "off" } } diff --git a/package-lock.json b/package-lock.json index 49cfec1e4..427bd830f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1465,9 +1465,9 @@ "dev": true }, "@openzeppelin/gsn-helpers": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@openzeppelin/gsn-helpers/-/gsn-helpers-0.2.3.tgz", - "integrity": "sha512-NRPFy6rbMfQWgvHW6jlJ0zg5L3wHrCZ9wKO2CmjszUZBwR3K1n8OfKNAXWEYiUArz0c7tP3qZuI7ifGb6dZvJg==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@openzeppelin/gsn-helpers/-/gsn-helpers-0.2.4.tgz", + "integrity": "sha512-gxAXoJvmtBiiJgC1yjX5s+e08WuytH2yhvp7C8tA9Gsac3P9grdF0xJaw7wfZnhwtby4A7J7+LHKbaZhYichvw==", "dev": true, "requires": { "axios": "^0.19.0", @@ -1496,11 +1496,20 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", "dev": true }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "tmp": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", @@ -1508,25 +1517,14 @@ "dev": true, "requires": { "rimraf": "^2.6.3" - }, - "dependencies": { - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } } } } }, "@openzeppelin/gsn-provider": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/@openzeppelin/gsn-provider/-/gsn-provider-0.1.10.tgz", - "integrity": "sha512-DKuU5zVE1I+cbPWFb9kSTtIZ2OTnPfCedseBIbGMgaZM8HhW1O4iSlrqXWk+lQprQn/DF4DcxPgMPjDau2zp5w==", + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@openzeppelin/gsn-provider/-/gsn-provider-0.1.11.tgz", + "integrity": "sha512-UeAsBj1ICk883A3CQNdZcoeowh01WE6lqOXJbMoXLFtZJmtnAzXNocup38aXi75W03L11cua5tr4PWh1nwMhDg==", "dev": true, "requires": { "abi-decoder": "^2.1.0", @@ -1548,50 +1546,35 @@ "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==", "dev": true }, + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, "ethereumjs-util": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.0.tgz", - "integrity": "sha512-vb0XN9J2QGdZGIEKG2vXM+kUdEivUfU6Wmi5y0cg+LRhDYKnXIZ/Lz7XjFbHRR9VIKq2lVGLzGBkA++y2nOdOQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-6.2.1.tgz", + "integrity": "sha512-W2Ktez4L01Vexijrm5EB6w7dg4n/TgpoYU4avuT5T3Vmnw/eCRtiBrJfQYS/DCSvDIOLn2k57GcHdeBcgVxAqw==", "dev": true, "requires": { "@types/bn.js": "^4.11.3", "bn.js": "^4.11.0", "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", "ethjs-util": "0.1.6", - "keccak": "^2.0.0", - "rlp": "^2.2.3", - "secp256k1": "^3.0.1" + "rlp": "^2.2.3" } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "keccak": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-2.1.0.tgz", - "integrity": "sha512-m1wbJRTo+gWbctZWay9i26v5fFnYkOn7D5PCxJ3fZUGUEb49dE1Pm4BREUYCt/aoO6di7jeoGmhvqN9Nzylm3Q==", - "dev": true, - "requires": { - "bindings": "^1.5.0", - "inherits": "^2.0.4", - "nan": "^2.14.0", - "safe-buffer": "^5.2.0" - } - }, - "nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", - "dev": true - }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "dev": true } } }, @@ -4313,9 +4296,9 @@ } }, "@types/pbkdf2": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.0.0.tgz", - "integrity": "sha512-6J6MHaAlBJC/eVMy9jOwj9oHaprfutukfW/Dyt0NEnpQ/6HN6YQrpvLwzWdWDeWZIdenjGHlbYDzyEODO5Z+2Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz", + "integrity": "sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ==", "dev": true, "requires": { "@types/node": "*" @@ -4825,9 +4808,9 @@ "dev": true }, "is-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", - "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "dev": true, "requires": { "has-symbols": "^1.0.1" @@ -4931,39 +4914,12 @@ "dev": true }, "axios": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", - "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.2.tgz", + "integrity": "sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==", "dev": true, "requires": { - "follow-redirects": "1.5.10", - "is-buffer": "^2.0.2" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "follow-redirects": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", - "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", - "dev": true, - "requires": { - "debug": "=3.1.0" - } - }, - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", - "dev": true - } + "follow-redirects": "1.5.10" } }, "babel-runtime": { @@ -6682,9 +6638,9 @@ "dev": true }, "is-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", - "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "dev": true, "requires": { "has-symbols": "^1.0.1" @@ -7223,9 +7179,9 @@ "dev": true }, "eth-crypto": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/eth-crypto/-/eth-crypto-1.5.2.tgz", - "integrity": "sha512-xhu7rt3CdNKSQ5VcqiFwfp50YVtc1+VPiqYEfMa8Omx9rGn5QHdIjImSoEnlej3VbfHv25Kvpu6A5oPoSI6iUA==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/eth-crypto/-/eth-crypto-1.6.0.tgz", + "integrity": "sha512-BRnSxQ/DyaI1YvBWk7FiA2HSO+q7WqxZrAm/ErgIZxG4MCBsuiCXpxiBfwatOhlbkf6h76bEZq8tzTfbfEibEg==", "dev": true, "requires": { "@types/bn.js": "4.11.6", @@ -7234,8 +7190,8 @@ "eth-lib": "0.2.8", "ethereumjs-tx": "2.1.2", "ethereumjs-util": "6.2.0", - "ethers": "4.0.44", - "secp256k1": "3.8.0" + "ethers": "4.0.47", + "secp256k1": "4.0.1" }, "dependencies": { "@types/bn.js": { @@ -7259,9 +7215,9 @@ } }, "ethereumjs-common": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/ethereumjs-common/-/ethereumjs-common-1.5.0.tgz", - "integrity": "sha512-SZOjgK1356hIY7MRj3/ma5qtfr/4B5BL+G4rP/XSMYr2z1H5el4RX5GReYCKmQmYI/nSBmRnwrZ17IfHuG0viQ==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/ethereumjs-common/-/ethereumjs-common-1.5.2.tgz", + "integrity": "sha512-hTfZjwGX52GS2jcVO6E2sx4YuFnf0Fhp5ylo4pEPhEffNln7vS59Hr5sLnp3/QCazFLluuBZ+FZ6J5HTp0EqCA==", "dev": true }, "ethereumjs-tx": { @@ -7287,12 +7243,45 @@ "keccak": "^2.0.0", "rlp": "^2.2.3", "secp256k1": "^3.0.1" + }, + "dependencies": { + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, + "secp256k1": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz", + "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==", + "dev": true, + "requires": { + "bindings": "^1.5.0", + "bip66": "^1.1.5", + "bn.js": "^4.11.8", + "create-hash": "^1.2.0", + "drbg.js": "^1.0.1", + "elliptic": "^6.5.2", + "nan": "^2.14.0", + "safe-buffer": "^5.1.2" + } + } } }, "ethers": { - "version": "4.0.44", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.44.tgz", - "integrity": "sha512-kCkMPkpYjBkxzqjcuYUfDY7VHDbf5EXnfRPUOazdqdf59SvXaT+w5lgauxLlk1UjxnAiNfeNS87rkIXnsTaM7Q==", + "version": "4.0.47", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.47.tgz", + "integrity": "sha512-hssRYhngV4hiDNeZmVU/k5/E8xmLG8UpcNUzg6mb7lqhgpFPH/t7nuv20RjRrEf0gblzvi2XwR5Te+V3ZFc9pQ==", "dev": true, "requires": { "aes-js": "3.0.0", @@ -7329,58 +7318,33 @@ "integrity": "sha1-DU/9gALVMzqrr0oj7tL2N0yfKOc=", "dev": true }, - "keccak": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-2.1.0.tgz", - "integrity": "sha512-m1wbJRTo+gWbctZWay9i26v5fFnYkOn7D5PCxJ3fZUGUEb49dE1Pm4BREUYCt/aoO6di7jeoGmhvqN9Nzylm3Q==", - "dev": true, - "requires": { - "bindings": "^1.5.0", - "inherits": "^2.0.4", - "nan": "^2.14.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - } - } - }, "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", "dev": true }, "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "dev": true }, "secp256k1": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz", - "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.1.tgz", + "integrity": "sha512-iGRjbGAKfXMqhtdkkuNxsgJQfJO8Oo78Rm7DAvsG3XKngq+nJIOGqrCSXcQqIVsmCj0wFanE5uTKFxV3T9j2wg==", "dev": true, "requires": { - "bindings": "^1.5.0", - "bip66": "^1.1.5", - "bn.js": "^4.11.8", - "create-hash": "^1.2.0", - "drbg.js": "^1.0.1", "elliptic": "^6.5.2", - "nan": "^2.14.0", - "safe-buffer": "^5.1.2" + "node-addon-api": "^2.0.0", + "node-gyp-build": "^4.2.0" }, "dependencies": { "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "dev": true, "requires": { "bn.js": "^4.4.0", @@ -7450,18 +7414,35 @@ }, "dependencies": { "ethereumjs-util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", - "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", + "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", "dev": true, "requires": { "bn.js": "^4.11.0", "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", "ethjs-util": "^0.1.3", - "keccak": "^1.0.2", "rlp": "^2.0.0", - "safe-buffer": "^5.1.1", - "secp256k1": "^3.0.1" + "safe-buffer": "^5.1.1" + }, + "dependencies": { + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + } } }, "tweetnacl": { @@ -7544,9 +7525,9 @@ } }, "keccak": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.0.tgz", - "integrity": "sha512-/4h4FIfFEpTEuySXi/nVFM5rqSKPnnhI7cL4K3MFSwoI3VyM7AhPSq3SsysARtnEBEeIKMBUWD8cTh9nHE8AkA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", + "integrity": "sha512-epq90L9jlFWCW7+pQa6JOnKn2Xgl2mtI664seYR6MHskvI9agt7AnDqmAlp9TqU4/caMYbA08Hi5DMZAl5zdkA==", "dev": true, "requires": { "node-addon-api": "^2.0.0", @@ -7572,9 +7553,9 @@ "dev": true }, "secp256k1": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.1.tgz", - "integrity": "sha512-iGRjbGAKfXMqhtdkkuNxsgJQfJO8Oo78Rm7DAvsG3XKngq+nJIOGqrCSXcQqIVsmCj0wFanE5uTKFxV3T9j2wg==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.2.tgz", + "integrity": "sha512-UDar4sKvWAksIlfX3xIaQReADn+WFnHvbVujpcbr+9Sf/69odMwy2MUsz5CKLQgX9nsIyrjuxL2imVyoNHa3fg==", "dev": true, "requires": { "elliptic": "^6.5.2", @@ -7622,17 +7603,32 @@ "ethereumjs-util": "^4.3.0" }, "dependencies": { + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, "ethereumjs-util": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.0.tgz", - "integrity": "sha1-PpQosxfuvaPXJg2FT93alUsfG8Y=", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-4.5.1.tgz", + "integrity": "sha512-WrckOZ7uBnei4+AKimpuF1B3Fv25OmoRgmYCpGsP7u8PFxXAmAgiJSYT2kRWnt6fVIlKaQlZvuwXp7PIrmn3/w==", "dev": true, "requires": { "bn.js": "^4.8.0", "create-hash": "^1.1.2", - "keccakjs": "^0.2.0", - "rlp": "^2.0.0", - "secp256k1": "^3.0.1" + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", + "rlp": "^2.0.0" } } } @@ -7662,27 +7658,42 @@ "ethereumjs-util": "^5.0.0" }, "dependencies": { + "elliptic": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", + "dev": true, + "requires": { + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" + } + }, "ethereumjs-util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.0.tgz", - "integrity": "sha512-CJAKdI0wgMbQFLlLRtZKGcy/L6pzVRgelIZqRqNbuVFM3K9VEnyfbcvz0ncWMRNCe4kaHWjwRYQcYMucmwsnWA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-5.2.1.tgz", + "integrity": "sha512-v3kT+7zdyCm1HIqWlLNrHGqHGLpGYIhjeHxQjnDXjLT2FyGJDsd3LWMYUo7pAFRrk86CR3nUJfhC81CCoJNNGQ==", "dev": true, "requires": { "bn.js": "^4.11.0", "create-hash": "^1.1.2", + "elliptic": "^6.5.2", + "ethereum-cryptography": "^0.1.3", "ethjs-util": "^0.1.3", - "keccak": "^1.0.2", "rlp": "^2.0.0", - "safe-buffer": "^5.1.1", - "secp256k1": "^3.0.1" + "safe-buffer": "^5.1.1" } } } }, "ethereumjs-util": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.0.3.tgz", - "integrity": "sha512-uLQsGPOwsRxe50WV1Dybh5N8zXDz4ev7wP49LKX9kr28I5TmcDILPgpKK/BFe5zYSfRGEeo+hPT7W3tjghYLuA==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/ethereumjs-util/-/ethereumjs-util-7.0.4.tgz", + "integrity": "sha512-isldtbCn9fdnhBPxedMNbFkNWVZ8ZdQvKRDSrdflame/AycAPKMer+vEpndpBxYIB3qxN6bd3Gh1YCQW9LDkCQ==", "dev": true, "requires": { "@types/bn.js": "^4.11.3", @@ -7694,15 +7705,15 @@ }, "dependencies": { "bn.js": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.2.tgz", - "integrity": "sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", + "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", "dev": true }, "rlp": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.5.tgz", - "integrity": "sha512-y1QxTQOp0OZnjn19FxBmped4p+BSKPHwGndaqrESseyd2xXZtcgR3yuTIosh8CaMaOii9SKIYerBXnV/CpJ3qw==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/rlp/-/rlp-2.2.6.tgz", + "integrity": "sha512-HAfAmL6SDYNWPUOJNrM500x4Thn4PZsEy5pijPh40U9WfNk0z15hUYzO9xVIMAdIHdFtD8CBDHd75Td1g36Mjg==", "dev": true, "requires": { "bn.js": "^4.11.1" @@ -30441,15 +30452,35 @@ } }, "keccak": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/keccak/-/keccak-1.4.0.tgz", - "integrity": "sha512-eZVaCpblK5formjPjeTBik7TAg+pqnDrMHIffSvi9Lh7PQgM1+hSzakUeZFCk9DVVG0dacZJuaz2ntwlzZUIBw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/keccak/-/keccak-2.1.0.tgz", + "integrity": "sha512-m1wbJRTo+gWbctZWay9i26v5fFnYkOn7D5PCxJ3fZUGUEb49dE1Pm4BREUYCt/aoO6di7jeoGmhvqN9Nzylm3Q==", "dev": true, "requires": { - "bindings": "^1.2.1", - "inherits": "^2.0.3", - "nan": "^2.2.1", - "safe-buffer": "^5.1.0" + "bindings": "^1.5.0", + "inherits": "^2.0.4", + "nan": "^2.14.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } } }, "keccakjs": { @@ -31694,9 +31725,9 @@ } }, "mocha": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.0.1.tgz", - "integrity": "sha512-vefaXfdYI8+Yo8nPZQQi0QO2o+5q9UIMX1jZ1XMmK3+4+CQjc7+B0hPdUeglXiTlr8IHMVRo63IhO9Mzt6fxOg==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.1.tgz", + "integrity": "sha512-p7FuGlYH8t7gaiodlFreseLxEmxTgvyG9RgPHODFPySNhwUehu8NIb0vdSt3WFckSneswZ0Un5typYcWElk7HQ==", "dev": true, "requires": { "ansi-colors": "4.1.1", @@ -31715,7 +31746,7 @@ "ms": "2.1.2", "object.assign": "4.1.0", "promise.allsettled": "1.0.2", - "serialize-javascript": "3.0.0", + "serialize-javascript": "4.0.0", "strip-json-comments": "3.0.1", "supports-color": "7.1.0", "which": "2.0.2", @@ -31723,7 +31754,7 @@ "workerpool": "6.0.0", "yargs": "13.3.2", "yargs-parser": "13.1.2", - "yargs-unparser": "1.6.0" + "yargs-unparser": "1.6.1" }, "dependencies": { "chokidar": { @@ -31792,12 +31823,6 @@ "picomatch": "^2.0.7" } }, - "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", - "dev": true - }, "supports-color": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", @@ -31927,9 +31952,9 @@ } }, "node-gyp-build": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.2.tgz", - "integrity": "sha512-Lqh7mrByWCM8Cf9UPqpeoVBBo5Ugx+RKu885GAzmLBVYjeywScxHXPGLa4JfYNZmcNGwzR0Glu5/9GaQZMFqyA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", + "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", "dev": true }, "nofilter": { @@ -32744,9 +32769,9 @@ "dev": true }, "is-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", - "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "dev": true, "requires": { "has-symbols": "^1.0.1" @@ -33428,10 +33453,13 @@ } }, "serialize-javascript": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-3.0.0.tgz", - "integrity": "sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw==", - "dev": true + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } }, "serve-index": { "version": "1.9.1", @@ -33928,12 +33956,12 @@ } }, "solhint": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.0.0.tgz", - "integrity": "sha512-z6JBNrtWZ51g/tkuZGc0ywQVskRqIGjXppR4g30bjPgOrxSBWJp3qX2pcI9+FbSI3uu1RqqbZ89CeURkZUF+RA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.2.0.tgz", + "integrity": "sha512-BGp7JnnoLzknGC/arcH33oN/LjOz0hKgdauOcBOO5jNjhjnPQ3cAacSMH64fWYShAg5+HYQaSRubInpSKSvzLg==", "dev": true, "requires": { - "@solidity-parser/parser": "^0.6.0", + "@solidity-parser/parser": "^0.7.0", "ajv": "^6.6.1", "antlr4": "4.7.1", "ast-parents": "0.0.1", @@ -33951,9 +33979,9 @@ }, "dependencies": { "@solidity-parser/parser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.6.0.tgz", - "integrity": "sha512-RiJXfS22frulogcfQCFhbKrd5ATu6P4tYUv/daChiIh6VHyKQ1kkVZVfX6aP7c2YGU/Bf9RwGNKdwLjfpaoqYQ==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.7.0.tgz", + "integrity": "sha512-YJ333ezgd9slnwCpFQVfsBcYsTcLWZRpVswlKgS82YDZPzzNtVnkEs5DX5+jMsu8PNnVxwZuxC6ucukima9x6w==", "dev": true }, "acorn": { @@ -33963,9 +33991,9 @@ "dev": true }, "ajv": { - "version": "6.12.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", - "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", + "version": "6.12.4", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", + "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -34069,9 +34097,9 @@ } }, "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "glob": { @@ -34124,6 +34152,12 @@ "requires": { "ansi-regex": "^3.0.0" } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true } } }, @@ -34824,9 +34858,9 @@ "dev": true }, "is-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", - "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "dev": true, "requires": { "has-symbols": "^1.0.1" @@ -34938,9 +34972,9 @@ "dev": true }, "is-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", - "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", "dev": true, "requires": { "has-symbols": "^1.0.1" @@ -34991,9 +35025,9 @@ } }, "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", "dev": true }, "supports-color": { @@ -37413,21 +37447,112 @@ } }, "yargs-unparser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz", - "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.1.tgz", + "integrity": "sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA==", "dev": true, "requires": { + "camelcase": "^5.3.1", + "decamelize": "^1.2.0", "flat": "^4.1.0", - "lodash": "^4.17.15", - "yargs": "^13.3.0" + "is-plain-obj": "^1.1.0", + "yargs": "^14.2.3" }, "dependencies": { - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "yargs": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", + "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" + } + }, + "yargs-parser": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz", + "integrity": "sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } } } }, From 0b489f4d79544ab9b870abe121077043feb971b8 Mon Sep 17 00:00:00 2001 From: dibi91 <34476725+dibi91@users.noreply.github.com> Date: Tue, 25 Aug 2020 19:58:45 +0200 Subject: [PATCH 11/20] Improve test descriptions #1157 (#2334) Co-authored-by: Paolo Dibitonto Co-authored-by: Francisco Giordano --- test/access/Ownable.test.js | 62 ++++---- test/cryptography/MerkleProof.test.js | 6 +- .../SupportsInterface.behavior.js | 6 +- test/payment/PaymentSplitter.test.js | 45 +++--- test/payment/PullPayment.test.js | 54 +++---- test/token/ERC1155/ERC1155.behavior.js | 10 +- .../ERC20/behaviors/ERC20Capped.behavior.js | 8 +- test/token/ERC721/ERC721.test.js | 16 +-- test/token/ERC721/ERC721Pausable.test.js | 2 +- test/utils/Address.test.js | 4 +- test/utils/Arrays.test.js | 136 +++++++++--------- test/utils/Counters.test.js | 56 ++++---- test/utils/Create2.test.js | 107 +++++++------- test/utils/EnumerableMap.test.js | 134 ++++++++--------- test/utils/EnumerableSet.behavior.js | 124 ++++++++-------- test/utils/ReentrancyGuard.test.js | 7 +- 16 files changed, 402 insertions(+), 375 deletions(-) diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index dce0756e7..b36cd4998 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -13,42 +13,46 @@ describe('Ownable', function () { this.ownable = await Ownable.new({ from: owner }); }); - it('should have an owner', async function () { + it('has an owner', async function () { expect(await this.ownable.owner()).to.equal(owner); }); - it('changes owner after transfer', async function () { - const receipt = await this.ownable.transferOwnership(other, { from: owner }); - expectEvent(receipt, 'OwnershipTransferred'); + describe('transfer ownership', function () { + it('changes owner after transfer', async function () { + const receipt = await this.ownable.transferOwnership(other, { from: owner }); + expectEvent(receipt, 'OwnershipTransferred'); - expect(await this.ownable.owner()).to.equal(other); + expect(await this.ownable.owner()).to.equal(other); + }); + + it('prevents non-owners from transferring', async function () { + await expectRevert( + this.ownable.transferOwnership(other, { from: other }), + 'Ownable: caller is not the owner' + ); + }); + + it('guards ownership against stuck state', async function () { + await expectRevert( + this.ownable.transferOwnership(ZERO_ADDRESS, { from: owner }), + 'Ownable: new owner is the zero address' + ); + }); }); - it('should prevent non-owners from transferring', async function () { - await expectRevert( - this.ownable.transferOwnership(other, { from: other }), - 'Ownable: caller is not the owner' - ); - }); + describe('renounce ownership', function () { + it('loses owner after renouncement', async function () { + const receipt = await this.ownable.renounceOwnership({ from: owner }); + expectEvent(receipt, 'OwnershipTransferred'); - it('should guard ownership against stuck state', async function () { - await expectRevert( - this.ownable.transferOwnership(ZERO_ADDRESS, { from: owner }), - 'Ownable: new owner is the zero address' - ); - }); + expect(await this.ownable.owner()).to.equal(ZERO_ADDRESS); + }); - it('loses owner after renouncement', async function () { - const receipt = await this.ownable.renounceOwnership({ from: owner }); - expectEvent(receipt, 'OwnershipTransferred'); - - expect(await this.ownable.owner()).to.equal(ZERO_ADDRESS); - }); - - it('should prevent non-owners from renouncement', async function () { - await expectRevert( - this.ownable.renounceOwnership({ from: other }), - 'Ownable: caller is not the owner' - ); + it('prevents non-owners from renouncement', async function () { + await expectRevert( + this.ownable.renounceOwnership({ from: other }), + 'Ownable: caller is not the owner' + ); + }); }); }); diff --git a/test/cryptography/MerkleProof.test.js b/test/cryptography/MerkleProof.test.js index cd1595a71..724864672 100644 --- a/test/cryptography/MerkleProof.test.js +++ b/test/cryptography/MerkleProof.test.js @@ -15,7 +15,7 @@ describe('MerkleProof', function () { }); describe('verify', function () { - it('should return true for a valid Merkle proof', async function () { + it('returns true for a valid Merkle proof', async function () { const elements = ['a', 'b', 'c', 'd']; const merkleTree = new MerkleTree(elements); @@ -28,7 +28,7 @@ describe('MerkleProof', function () { expect(await this.merkleProof.verify(proof, root, leaf)).to.equal(true); }); - it('should return false for an invalid Merkle proof', async function () { + it('returns false for an invalid Merkle proof', async function () { const correctElements = ['a', 'b', 'c']; const correctMerkleTree = new MerkleTree(correctElements); @@ -44,7 +44,7 @@ describe('MerkleProof', function () { expect(await this.merkleProof.verify(badProof, correctRoot, correctLeaf)).to.equal(false); }); - it('should return false for a Merkle proof of invalid length', async function () { + it('returns false for a Merkle proof of invalid length', async function () { const elements = ['a', 'b', 'c']; const merkleTree = new MerkleTree(elements); diff --git a/test/introspection/SupportsInterface.behavior.js b/test/introspection/SupportsInterface.behavior.js index 83b1511b0..a43ff2280 100644 --- a/test/introspection/SupportsInterface.behavior.js +++ b/test/introspection/SupportsInterface.behavior.js @@ -57,11 +57,11 @@ function shouldSupportInterfaces (interfaces = []) { const interfaceId = INTERFACE_IDS[k]; describe(k, function () { describe('ERC165\'s supportsInterface(bytes4)', function () { - it('should use less than 30k gas', async function () { + it('uses less than 30k gas', async function () { expect(await this.contractUnderTest.supportsInterface.estimateGas(interfaceId)).to.be.lte(30000); }); - it('should claim support', async function () { + it('claims support', async function () { expect(await this.contractUnderTest.supportsInterface(interfaceId)).to.equal(true); }); }); @@ -69,7 +69,7 @@ function shouldSupportInterfaces (interfaces = []) { for (const fnName of INTERFACES[k]) { const fnSig = FN_SIGNATURES[fnName]; describe(fnName, function () { - it('should be implemented', function () { + it('has to be implemented', function () { expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal(1); }); }); diff --git a/test/payment/PaymentSplitter.test.js b/test/payment/PaymentSplitter.test.js index da5111133..fca2a5393 100644 --- a/test/payment/PaymentSplitter.test.js +++ b/test/payment/PaymentSplitter.test.js @@ -54,45 +54,48 @@ describe('PaymentSplitter', function () { this.contract = await PaymentSplitter.new(this.payees, this.shares); }); - it('should have total shares', async function () { + it('has total shares', async function () { expect(await this.contract.totalShares()).to.be.bignumber.equal('100'); }); - it('should have payees', async function () { + it('has payees', async function () { await Promise.all(this.payees.map(async (payee, index) => { expect(await this.contract.payee(index)).to.equal(payee); expect(await this.contract.released(payee)).to.be.bignumber.equal('0'); })); }); - it('should accept payments', async function () { + it('accepts payments', async function () { await send.ether(owner, this.contract.address, amount); expect(await balance.current(this.contract.address)).to.be.bignumber.equal(amount); }); - it('should store shares if address is payee', async function () { - expect(await this.contract.shares(payee1)).to.be.bignumber.not.equal('0'); + describe('shares', async function () { + it('stores shares if address is payee', async function () { + expect(await this.contract.shares(payee1)).to.be.bignumber.not.equal('0'); + }); + + it('does not store shares if address is not payee', async function () { + expect(await this.contract.shares(nonpayee1)).to.be.bignumber.equal('0'); + }); }); - it('should not store shares if address is not payee', async function () { - expect(await this.contract.shares(nonpayee1)).to.be.bignumber.equal('0'); + describe('release', async function () { + it('reverts if no funds to claim', async function () { + await expectRevert(this.contract.release(payee1), + 'PaymentSplitter: account is not due payment' + ); + }); + it('reverts if non-payee want to claim', async function () { + await send.ether(payer1, this.contract.address, amount); + await expectRevert(this.contract.release(nonpayee1), + 'PaymentSplitter: account has no shares' + ); + }); }); - it('should throw if no funds to claim', async function () { - await expectRevert(this.contract.release(payee1), - 'PaymentSplitter: account is not due payment' - ); - }); - - it('should throw if non-payee want to claim', async function () { - await send.ether(payer1, this.contract.address, amount); - await expectRevert(this.contract.release(nonpayee1), - 'PaymentSplitter: account has no shares' - ); - }); - - it('should distribute funds to payees', async function () { + it('distributes funds to payees', async function () { await send.ether(payer1, this.contract.address, amount); // receive funds diff --git a/test/payment/PullPayment.test.js b/test/payment/PullPayment.test.js index 3c2634b18..f312d290d 100644 --- a/test/payment/PullPayment.test.js +++ b/test/payment/PullPayment.test.js @@ -15,35 +15,39 @@ describe('PullPayment', function () { this.contract = await PullPaymentMock.new({ value: amount }); }); - it('can record an async payment correctly', async function () { - await this.contract.callTransfer(payee1, 100, { from: payer }); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('100'); + describe('payments', function () { + it('can record an async payment correctly', async function () { + await this.contract.callTransfer(payee1, 100, { from: payer }); + expect(await this.contract.payments(payee1)).to.be.bignumber.equal('100'); + }); + + it('can add multiple balances on one account', async function () { + await this.contract.callTransfer(payee1, 200, { from: payer }); + await this.contract.callTransfer(payee1, 300, { from: payer }); + expect(await this.contract.payments(payee1)).to.be.bignumber.equal('500'); + }); + + it('can add balances on multiple accounts', async function () { + await this.contract.callTransfer(payee1, 200, { from: payer }); + await this.contract.callTransfer(payee2, 300, { from: payer }); + + expect(await this.contract.payments(payee1)).to.be.bignumber.equal('200'); + + expect(await this.contract.payments(payee2)).to.be.bignumber.equal('300'); + }); }); - it('can add multiple balances on one account', async function () { - await this.contract.callTransfer(payee1, 200, { from: payer }); - await this.contract.callTransfer(payee1, 300, { from: payer }); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('500'); - }); + describe('withdrawPayments', function () { + it('can withdraw payment', async function () { + const balanceTracker = await balance.tracker(payee1); - it('can add balances on multiple accounts', async function () { - await this.contract.callTransfer(payee1, 200, { from: payer }); - await this.contract.callTransfer(payee2, 300, { from: payer }); + await this.contract.callTransfer(payee1, amount, { from: payer }); + expect(await this.contract.payments(payee1)).to.be.bignumber.equal(amount); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('200'); + await this.contract.withdrawPayments(payee1); - expect(await this.contract.payments(payee2)).to.be.bignumber.equal('300'); - }); - - it('can withdraw payment', async function () { - const balanceTracker = await balance.tracker(payee1); - - await this.contract.callTransfer(payee1, amount, { from: payer }); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal(amount); - - await this.contract.withdrawPayments(payee1); - - expect(await balanceTracker.delta()).to.be.bignumber.equal(amount); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('0'); + expect(await balanceTracker.delta()).to.be.bignumber.equal(amount); + expect(await this.contract.payments(payee1)).to.be.bignumber.equal('0'); + }); }); }); diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index 9401cdfa9..fd855bf27 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -356,7 +356,7 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder, value: firstAmount, }); - it('should call onERC1155Received', async function () { + it('calls onERC1155Received', async function () { await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'Received', { operator: multiTokenHolder, from: multiTokenHolder, @@ -389,7 +389,7 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder, value: firstAmount, }); - it('should call onERC1155Received', async function () { + it('calls onERC1155Received', async function () { await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'Received', { operator: multiTokenHolder, from: multiTokenHolder, @@ -632,7 +632,7 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder, values: [firstAmount, secondAmount], }); - it('should call onERC1155BatchReceived', async function () { + it('calls onERC1155BatchReceived', async function () { await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', { operator: multiTokenHolder, from: multiTokenHolder, @@ -663,7 +663,7 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder, values: [firstAmount, secondAmount], }); - it('should call onERC1155Received', async function () { + it('calls onERC1155Received', async function () { await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', { operator: multiTokenHolder, from: multiTokenHolder, @@ -741,7 +741,7 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder, values: [firstAmount, secondAmount], }); - it('should call onERC1155BatchReceived', async function () { + it('calls onERC1155BatchReceived', async function () { await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', { operator: multiTokenHolder, from: multiTokenHolder, diff --git a/test/token/ERC20/behaviors/ERC20Capped.behavior.js b/test/token/ERC20/behaviors/ERC20Capped.behavior.js index 29a0a390f..4692f997f 100644 --- a/test/token/ERC20/behaviors/ERC20Capped.behavior.js +++ b/test/token/ERC20/behaviors/ERC20Capped.behavior.js @@ -6,21 +6,21 @@ function shouldBehaveLikeERC20Capped (minter, [other], cap) { describe('capped token', function () { const from = minter; - it('should start with the correct cap', async function () { + it('starts with the correct cap', async function () { expect(await this.token.cap()).to.be.bignumber.equal(cap); }); - it('should mint when amount is less than cap', async function () { + it('mints when amount is less than cap', async function () { await this.token.mint(other, cap.subn(1), { from }); expect(await this.token.totalSupply()).to.be.bignumber.equal(cap.subn(1)); }); - it('should fail to mint if the amount exceeds the cap', async function () { + it('fails to mint if the amount exceeds the cap', async function () { await this.token.mint(other, cap.subn(1), { from }); await expectRevert(this.token.mint(other, 2, { from }), 'ERC20Capped: cap exceeded'); }); - it('should fail to mint after cap is reached', async function () { + it('fails to mint after cap is reached', async function () { await this.token.mint(other, cap, { from }); await expectRevert(this.token.mint(other, 1, { from }), 'ERC20Capped: cap exceeded'); }); diff --git a/test/token/ERC721/ERC721.test.js b/test/token/ERC721/ERC721.test.js index 102853226..90827264b 100644 --- a/test/token/ERC721/ERC721.test.js +++ b/test/token/ERC721/ERC721.test.js @@ -332,7 +332,7 @@ describe('ERC721', function () { shouldTransferTokensByUsers(transferFun); - it('should call onERC721Received', async function () { + it('calls onERC721Received', async function () { const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: owner }); await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { @@ -343,7 +343,7 @@ describe('ERC721', function () { }); }); - it('should call onERC721Received from approved', async function () { + it('calls onERC721Received from approved', async function () { const receipt = await transferFun.call(this, owner, this.receiver.address, tokenId, { from: approved }); await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { @@ -417,7 +417,7 @@ describe('ERC721', function () { const data = '0x42'; describe('via safeMint', function () { // regular minting is tested in ERC721Mintable.test.js and others - it('should call onERC721Received — with data', async function () { + it('calls onERC721Received — with data', async function () { this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, false); const receipt = await this.token.safeMint(this.receiver.address, tokenId, data); @@ -428,7 +428,7 @@ describe('ERC721', function () { }); }); - it('should call onERC721Received — without data', async function () { + it('calls onERC721Received — without data', async function () { this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, false); const receipt = await this.token.safeMint(this.receiver.address, tokenId); @@ -691,7 +691,7 @@ describe('ERC721', function () { await this.token.approve(approved, firstTokenId, { from: owner }); }); - it('should return approved account', async function () { + it('returns approved account', async function () { expect(await this.token.getApproved(firstTokenId)).to.be.equal(approved); }); }); @@ -752,7 +752,7 @@ describe('ERC721', function () { }); describe('tokenByIndex', function () { - it('should return all tokens', async function () { + it('returns all tokens', async function () { const tokensListed = await Promise.all( [0, 1].map(i => this.token.tokenByIndex(i)) ); @@ -760,14 +760,14 @@ describe('ERC721', function () { secondTokenId.toNumber()]); }); - it('should revert if index is greater than supply', async function () { + it('reverts if index is greater than supply', async function () { await expectRevert( this.token.tokenByIndex(2), 'EnumerableMap: index out of bounds' ); }); [firstTokenId, secondTokenId].forEach(function (tokenId) { - it(`should return all tokens after burning token ${tokenId} and minting new tokens`, async function () { + it(`returns all tokens after burning token ${tokenId} and minting new tokens`, async function () { const newTokenId = new BN(300); const anotherNewTokenId = new BN(400); diff --git a/test/token/ERC721/ERC721Pausable.test.js b/test/token/ERC721/ERC721Pausable.test.js index 659757ee7..b544ca37b 100644 --- a/test/token/ERC721/ERC721Pausable.test.js +++ b/test/token/ERC721/ERC721Pausable.test.js @@ -86,7 +86,7 @@ describe('ERC721Pausable', function () { }); describe('exists', function () { - it('should return token existence', async function () { + it('returns token existence', async function () { expect(await this.token.exists(firstTokenId)).to.equal(true); }); }); diff --git a/test/utils/Address.test.js b/test/utils/Address.test.js index 8c0844287..60e83c74d 100644 --- a/test/utils/Address.test.js +++ b/test/utils/Address.test.js @@ -15,11 +15,11 @@ describe('Address', function () { }); describe('isContract', function () { - it('should return false for account address', async function () { + it('returns false for account address', async function () { expect(await this.mock.isContract(other)).to.equal(false); }); - it('should return true for contract address', async function () { + it('returns true for contract address', async function () { const contract = await AddressImpl.new(); expect(await this.mock.isContract(contract.address)).to.equal(true); }); diff --git a/test/utils/Arrays.test.js b/test/utils/Arrays.test.js index 92c970d8c..42693aeb8 100644 --- a/test/utils/Arrays.test.js +++ b/test/utils/Arrays.test.js @@ -6,81 +6,83 @@ const { expect } = require('chai'); const ArraysImpl = contract.fromArtifact('ArraysImpl'); describe('Arrays', function () { - context('Even number of elements', function () { - const EVEN_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; + describe('findUpperBound', function () { + context('Even number of elements', function () { + const EVEN_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]; - beforeEach(async function () { - this.arrays = await ArraysImpl.new(EVEN_ELEMENTS_ARRAY); + beforeEach(async function () { + this.arrays = await ArraysImpl.new(EVEN_ELEMENTS_ARRAY); + }); + + it('returns correct index for the basic case', async function () { + expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5'); + }); + + it('returns 0 for the first element', async function () { + expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0'); + }); + + it('returns index of the last element', async function () { + expect(await this.arrays.findUpperBound(20)).to.be.bignumber.equal('9'); + }); + + it('returns first index after last element if searched value is over the upper boundary', async function () { + expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('10'); + }); + + it('returns 0 for the element under the lower boundary', async function () { + expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0'); + }); }); - it('should return correct index for the basic case', async function () { - expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5'); + context('Odd number of elements', function () { + const ODD_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]; + + beforeEach(async function () { + this.arrays = await ArraysImpl.new(ODD_ELEMENTS_ARRAY); + }); + + it('returns correct index for the basic case', async function () { + expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5'); + }); + + it('returns 0 for the first element', async function () { + expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0'); + }); + + it('returns index of the last element', async function () { + expect(await this.arrays.findUpperBound(21)).to.be.bignumber.equal('10'); + }); + + it('returns first index after last element if searched value is over the upper boundary', async function () { + expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('11'); + }); + + it('returns 0 for the element under the lower boundary', async function () { + expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0'); + }); }); - it('should return 0 for the first element', async function () { - expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0'); + context('Array with gap', function () { + const WITH_GAP_ARRAY = [11, 12, 13, 14, 15, 20, 21, 22, 23, 24]; + + beforeEach(async function () { + this.arrays = await ArraysImpl.new(WITH_GAP_ARRAY); + }); + + it('returns index of first element in next filled range', async function () { + expect(await this.arrays.findUpperBound(17)).to.be.bignumber.equal('5'); + }); }); - it('should return index of the last element', async function () { - expect(await this.arrays.findUpperBound(20)).to.be.bignumber.equal('9'); - }); + context('Empty array', function () { + beforeEach(async function () { + this.arrays = await ArraysImpl.new([]); + }); - it('should return first index after last element if searched value is over the upper boundary', async function () { - expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('10'); - }); - - it('should return 0 for the element under the lower boundary', async function () { - expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0'); - }); - }); - - context('Odd number of elements', function () { - const ODD_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]; - - beforeEach(async function () { - this.arrays = await ArraysImpl.new(ODD_ELEMENTS_ARRAY); - }); - - it('should return correct index for the basic case', async function () { - expect(await this.arrays.findUpperBound(16)).to.be.bignumber.equal('5'); - }); - - it('should return 0 for the first element', async function () { - expect(await this.arrays.findUpperBound(11)).to.be.bignumber.equal('0'); - }); - - it('should return index of the last element', async function () { - expect(await this.arrays.findUpperBound(21)).to.be.bignumber.equal('10'); - }); - - it('should return first index after last element if searched value is over the upper boundary', async function () { - expect(await this.arrays.findUpperBound(32)).to.be.bignumber.equal('11'); - }); - - it('should return 0 for the element under the lower boundary', async function () { - expect(await this.arrays.findUpperBound(2)).to.be.bignumber.equal('0'); - }); - }); - - context('Array with gap', function () { - const WITH_GAP_ARRAY = [11, 12, 13, 14, 15, 20, 21, 22, 23, 24]; - - beforeEach(async function () { - this.arrays = await ArraysImpl.new(WITH_GAP_ARRAY); - }); - - it('should return index of first element in next filled range', async function () { - expect(await this.arrays.findUpperBound(17)).to.be.bignumber.equal('5'); - }); - }); - - context('Empty array', function () { - beforeEach(async function () { - this.arrays = await ArraysImpl.new([]); - }); - - it('should always return 0 for empty array', async function () { - expect(await this.arrays.findUpperBound(10)).to.be.bignumber.equal('0'); + it('always returns 0 for empty array', async function () { + expect(await this.arrays.findUpperBound(10)).to.be.bignumber.equal('0'); + }); }); }); }); diff --git a/test/utils/Counters.test.js b/test/utils/Counters.test.js index cbfadc2d7..5650bab87 100644 --- a/test/utils/Counters.test.js +++ b/test/utils/Counters.test.js @@ -15,17 +15,19 @@ describe('Counters', function () { }); describe('increment', function () { - it('increments the current value by one', async function () { - await this.counter.increment(); - expect(await this.counter.current()).to.be.bignumber.equal('1'); - }); + context('starting from 0', function () { + it('increments the current value by one', async function () { + await this.counter.increment(); + expect(await this.counter.current()).to.be.bignumber.equal('1'); + }); - it('can be called multiple times', async function () { - await this.counter.increment(); - await this.counter.increment(); - await this.counter.increment(); + it('can be called multiple times', async function () { + await this.counter.increment(); + await this.counter.increment(); + await this.counter.increment(); - expect(await this.counter.current()).to.be.bignumber.equal('3'); + expect(await this.counter.current()).to.be.bignumber.equal('3'); + }); }); }); @@ -34,28 +36,30 @@ describe('Counters', function () { await this.counter.increment(); expect(await this.counter.current()).to.be.bignumber.equal('1'); }); + context('starting from 1', function () { + it('decrements the current value by one', async function () { + await this.counter.decrement(); + expect(await this.counter.current()).to.be.bignumber.equal('0'); + }); - it('decrements the current value by one', async function () { - await this.counter.decrement(); - expect(await this.counter.current()).to.be.bignumber.equal('0'); + it('reverts if the current value is 0', async function () { + await this.counter.decrement(); + await expectRevert(this.counter.decrement(), 'SafeMath: subtraction overflow'); + }); }); + context('after incremented to 3', function () { + it('can be called multiple times', async function () { + await this.counter.increment(); + await this.counter.increment(); - it('reverts if the current value is 0', async function () { - await this.counter.decrement(); - await expectRevert(this.counter.decrement(), 'SafeMath: subtraction overflow'); - }); + expect(await this.counter.current()).to.be.bignumber.equal('3'); - it('can be called multiple times', async function () { - await this.counter.increment(); - await this.counter.increment(); + await this.counter.decrement(); + await this.counter.decrement(); + await this.counter.decrement(); - expect(await this.counter.current()).to.be.bignumber.equal('3'); - - await this.counter.decrement(); - await this.counter.decrement(); - await this.counter.decrement(); - - expect(await this.counter.current()).to.be.bignumber.equal('0'); + expect(await this.counter.current()).to.be.bignumber.equal('0'); + }); }); }); }); diff --git a/test/utils/Create2.test.js b/test/utils/Create2.test.js index 347ad5e54..54b9b9938 100644 --- a/test/utils/Create2.test.js +++ b/test/utils/Create2.test.js @@ -23,71 +23,72 @@ describe('Create2', function () { beforeEach(async function () { this.factory = await Create2Impl.new(); }); + describe('computeAddress', function () { + it('computes the correct contract address', async function () { + const onChainComputed = await this.factory + .computeAddress(saltHex, web3.utils.keccak256(constructorByteCode)); + const offChainComputed = + computeCreate2Address(saltHex, constructorByteCode, this.factory.address); + expect(onChainComputed).to.equal(offChainComputed); + }); - it('should compute the correct contract address', async function () { - const onChainComputed = await this.factory - .computeAddress(saltHex, web3.utils.keccak256(constructorByteCode)); - const offChainComputed = - computeCreate2Address(saltHex, constructorByteCode, this.factory.address); - expect(onChainComputed).to.equal(offChainComputed); + it('computes the correct contract address with deployer', async function () { + const onChainComputed = await this.factory + .computeAddressWithDeployer(saltHex, web3.utils.keccak256(constructorByteCode), deployerAccount); + const offChainComputed = + computeCreate2Address(saltHex, constructorByteCode, deployerAccount); + expect(onChainComputed).to.equal(offChainComputed); + }); }); - it('should compute the correct contract address with deployer', async function () { - const onChainComputed = await this.factory - .computeAddressWithDeployer(saltHex, web3.utils.keccak256(constructorByteCode), deployerAccount); - const offChainComputed = - computeCreate2Address(saltHex, constructorByteCode, deployerAccount); - expect(onChainComputed).to.equal(offChainComputed); - }); + describe('deploy', function () { + it('deploys a ERC1820Implementer from inline assembly code', async function () { + const offChainComputed = + computeCreate2Address(saltHex, ERC1820Implementer.bytecode, this.factory.address); + await this.factory.deployERC1820Implementer(0, saltHex); + expect(ERC1820Implementer.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2)); + }); - it('should deploy a ERC1820Implementer from inline assembly code', async function () { - const offChainComputed = - computeCreate2Address(saltHex, ERC1820Implementer.bytecode, this.factory.address); + it('deploys a ERC20Mock with correct balances', async function () { + const offChainComputed = computeCreate2Address(saltHex, constructorByteCode, this.factory.address); - await this.factory.deployERC1820Implementer(0, saltHex); + await this.factory.deploy(0, saltHex, constructorByteCode); - expect(ERC1820Implementer.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2)); - }); + const erc20 = await ERC20Mock.at(offChainComputed); + expect(await erc20.balanceOf(deployerAccount)).to.be.bignumber.equal(new BN(100)); + }); - it('should deploy a ERC20Mock with correct balances', async function () { - const offChainComputed = computeCreate2Address(saltHex, constructorByteCode, this.factory.address); + it('deploys a contract with funds deposited in the factory', async function () { + const deposit = ether('2'); + await send.ether(deployerAccount, this.factory.address, deposit); + expect(await balance.current(this.factory.address)).to.be.bignumber.equal(deposit); - await this.factory.deploy(0, saltHex, constructorByteCode); + const onChainComputed = await this.factory + .computeAddressWithDeployer(saltHex, web3.utils.keccak256(constructorByteCode), this.factory.address); - const erc20 = await ERC20Mock.at(offChainComputed); - expect(await erc20.balanceOf(deployerAccount)).to.be.bignumber.equal(new BN(100)); - }); + await this.factory.deploy(deposit, saltHex, constructorByteCode); + expect(await balance.current(onChainComputed)).to.be.bignumber.equal(deposit); + }); - it('should deploy a contract with funds deposited in the factory', async function () { - const deposit = ether('2'); - await send.ether(deployerAccount, this.factory.address, deposit); - expect(await balance.current(this.factory.address)).to.be.bignumber.equal(deposit); + it('fails deploying a contract in an existent address', async function () { + await this.factory.deploy(0, saltHex, constructorByteCode, { from: deployerAccount }); + await expectRevert( + this.factory.deploy(0, saltHex, constructorByteCode, { from: deployerAccount }), 'Create2: Failed on deploy' + ); + }); - const onChainComputed = await this.factory - .computeAddressWithDeployer(saltHex, web3.utils.keccak256(constructorByteCode), this.factory.address); + it('fails deploying a contract if the bytecode length is zero', async function () { + await expectRevert( + this.factory.deploy(0, saltHex, '0x', { from: deployerAccount }), 'Create2: bytecode length is zero' + ); + }); - await this.factory.deploy(deposit, saltHex, constructorByteCode); - expect(await balance.current(onChainComputed)).to.be.bignumber.equal(deposit); - }); - - it('should failed deploying a contract in an existent address', async function () { - await this.factory.deploy(0, saltHex, constructorByteCode, { from: deployerAccount }); - await expectRevert( - this.factory.deploy(0, saltHex, constructorByteCode, { from: deployerAccount }), 'Create2: Failed on deploy' - ); - }); - - it('should fail deploying a contract if the bytecode length is zero', async function () { - await expectRevert( - this.factory.deploy(0, saltHex, '0x', { from: deployerAccount }), 'Create2: bytecode length is zero' - ); - }); - - it('should fail deploying a contract if factory contract does not have sufficient balance', async function () { - await expectRevert( - this.factory.deploy(1, saltHex, constructorByteCode, { from: deployerAccount }), - 'Create2: insufficient balance' - ); + it('fails deploying a contract if factory contract does not have sufficient balance', async function () { + await expectRevert( + this.factory.deploy(1, saltHex, constructorByteCode, { from: deployerAccount }), + 'Create2: insufficient balance' + ); + }); }); }); diff --git a/test/utils/EnumerableMap.test.js b/test/utils/EnumerableMap.test.js index 19e24c887..219fc6d5f 100644 --- a/test/utils/EnumerableMap.test.js +++ b/test/utils/EnumerableMap.test.js @@ -46,94 +46,98 @@ describe('EnumerableMap', function () { await expectMembersMatch(this.map, [], []); }); - it('adds a key', async function () { - const receipt = await this.map.set(keyA, accountA); - expectEvent(receipt, 'OperationResult', { result: true }); + describe('set', function () { + it('adds a key', async function () { + const receipt = await this.map.set(keyA, accountA); + expectEvent(receipt, 'OperationResult', { result: true }); - await expectMembersMatch(this.map, [keyA], [accountA]); + await expectMembersMatch(this.map, [keyA], [accountA]); + }); + + it('adds several keys', async function () { + await this.map.set(keyA, accountA); + await this.map.set(keyB, accountB); + + await expectMembersMatch(this.map, [keyA, keyB], [accountA, accountB]); + expect(await this.map.contains(keyC)).to.equal(false); + }); + + it('returns false when adding keys already in the set', async function () { + await this.map.set(keyA, accountA); + + const receipt = (await this.map.set(keyA, accountA)); + expectEvent(receipt, 'OperationResult', { result: false }); + + await expectMembersMatch(this.map, [keyA], [accountA]); + }); + + it('updates values for keys already in the set', async function () { + await this.map.set(keyA, accountA); + + await this.map.set(keyA, accountB); + + await expectMembersMatch(this.map, [keyA], [accountB]); + }); }); - it('adds several keys', async function () { - await this.map.set(keyA, accountA); - await this.map.set(keyB, accountB); + describe('remove', function () { + it('removes added keys', async function () { + await this.map.set(keyA, accountA); - await expectMembersMatch(this.map, [keyA, keyB], [accountA, accountB]); - expect(await this.map.contains(keyC)).to.equal(false); - }); + const receipt = await this.map.remove(keyA); + expectEvent(receipt, 'OperationResult', { result: true }); - it('returns false when adding keys already in the set', async function () { - await this.map.set(keyA, accountA); + expect(await this.map.contains(keyA)).to.equal(false); + await expectMembersMatch(this.map, [], []); + }); - const receipt = (await this.map.set(keyA, accountA)); - expectEvent(receipt, 'OperationResult', { result: false }); + it('returns false when removing keys not in the set', async function () { + const receipt = await this.map.remove(keyA); + expectEvent(receipt, 'OperationResult', { result: false }); - await expectMembersMatch(this.map, [keyA], [accountA]); - }); + expect(await this.map.contains(keyA)).to.equal(false); + }); - it('updates values for keys already in the set', async function () { - await this.map.set(keyA, accountA); + it('adds and removes multiple keys', async function () { + // [] - await this.map.set(keyA, accountB); + await this.map.set(keyA, accountA); + await this.map.set(keyC, accountC); - await expectMembersMatch(this.map, [keyA], [accountB]); - }); + // [A, C] - it('removes added keys', async function () { - await this.map.set(keyA, accountA); + await this.map.remove(keyA); + await this.map.remove(keyB); - const receipt = await this.map.remove(keyA); - expectEvent(receipt, 'OperationResult', { result: true }); + // [C] - expect(await this.map.contains(keyA)).to.equal(false); - await expectMembersMatch(this.map, [], []); - }); + await this.map.set(keyB, accountB); - it('returns false when removing keys not in the set', async function () { - const receipt = await this.map.remove(keyA); - expectEvent(receipt, 'OperationResult', { result: false }); + // [C, B] - expect(await this.map.contains(keyA)).to.equal(false); - }); + await this.map.set(keyA, accountA); + await this.map.remove(keyC); - it('adds and removes multiple keys', async function () { - // [] + // [A, B] - await this.map.set(keyA, accountA); - await this.map.set(keyC, accountC); + await this.map.set(keyA, accountA); + await this.map.set(keyB, accountB); - // [A, C] + // [A, B] - await this.map.remove(keyA); - await this.map.remove(keyB); + await this.map.set(keyC, accountC); + await this.map.remove(keyA); - // [C] + // [B, C] - await this.map.set(keyB, accountB); + await this.map.set(keyA, accountA); + await this.map.remove(keyB); - // [C, B] + // [A, C] - await this.map.set(keyA, accountA); - await this.map.remove(keyC); + await expectMembersMatch(this.map, [keyA, keyC], [accountA, accountC]); - // [A, B] - - await this.map.set(keyA, accountA); - await this.map.set(keyB, accountB); - - // [A, B] - - await this.map.set(keyC, accountC); - await this.map.remove(keyA); - - // [B, C] - - await this.map.set(keyA, accountA); - await this.map.remove(keyB); - - // [A, C] - - await expectMembersMatch(this.map, [keyA, keyC], [accountA, accountC]); - - expect(await this.map.contains(keyB)).to.equal(false); + expect(await this.map.contains(keyB)).to.equal(false); + }); }); }); diff --git a/test/utils/EnumerableSet.behavior.js b/test/utils/EnumerableSet.behavior.js index 7c4711c4e..747344046 100644 --- a/test/utils/EnumerableSet.behavior.js +++ b/test/utils/EnumerableSet.behavior.js @@ -23,91 +23,97 @@ function shouldBehaveLikeSet (valueA, valueB, valueC) { await expectMembersMatch(this.set, []); }); - it('adds a value', async function () { - const receipt = await this.set.add(valueA); - expectEvent(receipt, 'OperationResult', { result: true }); + describe('add', function () { + it('adds a value', async function () { + const receipt = await this.set.add(valueA); + expectEvent(receipt, 'OperationResult', { result: true }); - await expectMembersMatch(this.set, [valueA]); + await expectMembersMatch(this.set, [valueA]); + }); + + it('adds several values', async function () { + await this.set.add(valueA); + await this.set.add(valueB); + + await expectMembersMatch(this.set, [valueA, valueB]); + expect(await this.set.contains(valueC)).to.equal(false); + }); + + it('returns false when adding values already in the set', async function () { + await this.set.add(valueA); + + const receipt = (await this.set.add(valueA)); + expectEvent(receipt, 'OperationResult', { result: false }); + + await expectMembersMatch(this.set, [valueA]); + }); }); - it('adds several values', async function () { - await this.set.add(valueA); - await this.set.add(valueB); - - await expectMembersMatch(this.set, [valueA, valueB]); - expect(await this.set.contains(valueC)).to.equal(false); + describe('at', function () { + it('reverts when retrieving non-existent elements', async function () { + await expectRevert(this.set.at(0), 'EnumerableSet: index out of bounds'); + }); }); - it('returns false when adding values already in the set', async function () { - await this.set.add(valueA); + describe('remove', function () { + it('removes added values', async function () { + await this.set.add(valueA); - const receipt = (await this.set.add(valueA)); - expectEvent(receipt, 'OperationResult', { result: false }); + const receipt = await this.set.remove(valueA); + expectEvent(receipt, 'OperationResult', { result: true }); - await expectMembersMatch(this.set, [valueA]); - }); + expect(await this.set.contains(valueA)).to.equal(false); + await expectMembersMatch(this.set, []); + }); - it('reverts when retrieving non-existent elements', async function () { - await expectRevert(this.set.at(0), 'EnumerableSet: index out of bounds'); - }); + it('returns false when removing values not in the set', async function () { + const receipt = await this.set.remove(valueA); + expectEvent(receipt, 'OperationResult', { result: false }); - it('removes added values', async function () { - await this.set.add(valueA); + expect(await this.set.contains(valueA)).to.equal(false); + }); - const receipt = await this.set.remove(valueA); - expectEvent(receipt, 'OperationResult', { result: true }); + it('adds and removes multiple values', async function () { + // [] - expect(await this.set.contains(valueA)).to.equal(false); - await expectMembersMatch(this.set, []); - }); + await this.set.add(valueA); + await this.set.add(valueC); - it('returns false when removing values not in the set', async function () { - const receipt = await this.set.remove(valueA); - expectEvent(receipt, 'OperationResult', { result: false }); + // [A, C] - expect(await this.set.contains(valueA)).to.equal(false); - }); + await this.set.remove(valueA); + await this.set.remove(valueB); - it('adds and removes multiple values', async function () { - // [] + // [C] - await this.set.add(valueA); - await this.set.add(valueC); + await this.set.add(valueB); - // [A, C] + // [C, B] - await this.set.remove(valueA); - await this.set.remove(valueB); + await this.set.add(valueA); + await this.set.remove(valueC); - // [C] + // [A, B] - await this.set.add(valueB); + await this.set.add(valueA); + await this.set.add(valueB); - // [C, B] + // [A, B] - await this.set.add(valueA); - await this.set.remove(valueC); + await this.set.add(valueC); + await this.set.remove(valueA); - // [A, B] + // [B, C] - await this.set.add(valueA); - await this.set.add(valueB); + await this.set.add(valueA); + await this.set.remove(valueB); - // [A, B] + // [A, C] - await this.set.add(valueC); - await this.set.remove(valueA); + await expectMembersMatch(this.set, [valueA, valueC]); - // [B, C] - - await this.set.add(valueA); - await this.set.remove(valueB); - - // [A, C] - - await expectMembersMatch(this.set, [valueA, valueC]); - - expect(await this.set.contains(valueB)).to.equal(false); + expect(await this.set.contains(valueB)).to.equal(false); + }); }); } diff --git a/test/utils/ReentrancyGuard.test.js b/test/utils/ReentrancyGuard.test.js index b40d29a2c..7ba5e8663 100644 --- a/test/utils/ReentrancyGuard.test.js +++ b/test/utils/ReentrancyGuard.test.js @@ -12,7 +12,7 @@ describe('ReentrancyGuard', function () { expect(await this.reentrancyMock.counter()).to.be.bignumber.equal('0'); }); - it('should not allow remote callback', async function () { + it('does not allow remote callback', async function () { const attacker = await ReentrancyAttack.new(); await expectRevert( this.reentrancyMock.countAndCall(attacker.address), 'ReentrancyAttack: failed call'); @@ -21,14 +21,13 @@ describe('ReentrancyGuard', function () { // The following are more side-effects than intended behavior: // I put them here as documentation, and to monitor any changes // in the side-effects. - - it('should not allow local recursion', async function () { + it('does not allow local recursion', async function () { await expectRevert( this.reentrancyMock.countLocalRecursive(10), 'ReentrancyGuard: reentrant call' ); }); - it('should not allow indirect local recursion', async function () { + it('does not allow indirect local recursion', async function () { await expectRevert( this.reentrancyMock.countThisRecursive(10), 'ReentrancyMock: failed call' ); From cb791a1b219c72e285ee49bda7b5b9e289fef1ee Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 27 Aug 2020 21:02:42 -0300 Subject: [PATCH 12/20] Add Proxies from OpenZeppelin SDK (#2335) --- contracts/mocks/ClashingImplementation.sol | 20 + contracts/mocks/DummyImplementation.sol | 57 +++ contracts/mocks/InitializableMock.sol | 36 ++ .../MultipleInheritanceInitializableMocks.sol | 76 +++ contracts/mocks/RegressionImplementation.sol | 66 +++ .../SingleInheritanceInitializableMocks.sol | 49 ++ contracts/proxy/Initializable.sol | 62 +++ contracts/proxy/Proxy.sol | 78 ++++ contracts/proxy/ProxyAdmin.sol | 70 +++ .../proxy/TransparentUpgradeableProxy.sol | 139 ++++++ contracts/proxy/UpgradeableProxy.sol | 81 ++++ test/proxy/Initializable.test.js | 81 ++++ test/proxy/ProxyAdmin.test.js | 119 +++++ .../TransparentUpgradeableProxy.behaviour.js | 436 ++++++++++++++++++ .../proxy/TransparentUpgradeableProxy.test.js | 17 + test/proxy/UpgradeableProxy.behaviour.js | 216 +++++++++ test/proxy/UpgradeableProxy.test.js | 15 + 17 files changed, 1618 insertions(+) create mode 100644 contracts/mocks/ClashingImplementation.sol create mode 100644 contracts/mocks/DummyImplementation.sol create mode 100644 contracts/mocks/InitializableMock.sol create mode 100644 contracts/mocks/MultipleInheritanceInitializableMocks.sol create mode 100644 contracts/mocks/RegressionImplementation.sol create mode 100644 contracts/mocks/SingleInheritanceInitializableMocks.sol create mode 100644 contracts/proxy/Initializable.sol create mode 100644 contracts/proxy/Proxy.sol create mode 100644 contracts/proxy/ProxyAdmin.sol create mode 100644 contracts/proxy/TransparentUpgradeableProxy.sol create mode 100644 contracts/proxy/UpgradeableProxy.sol create mode 100644 test/proxy/Initializable.test.js create mode 100644 test/proxy/ProxyAdmin.test.js create mode 100644 test/proxy/TransparentUpgradeableProxy.behaviour.js create mode 100644 test/proxy/TransparentUpgradeableProxy.test.js create mode 100644 test/proxy/UpgradeableProxy.behaviour.js create mode 100644 test/proxy/UpgradeableProxy.test.js diff --git a/contracts/mocks/ClashingImplementation.sol b/contracts/mocks/ClashingImplementation.sol new file mode 100644 index 000000000..b75621351 --- /dev/null +++ b/contracts/mocks/ClashingImplementation.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + + +/** + * @dev Implementation contract with an admin() function made to clash with + * @dev TransparentUpgradeableProxy's to test correct functioning of the + * @dev Transparent Proxy feature. + */ +contract ClashingImplementation { + + function admin() external pure returns (address) { + return 0x0000000000000000000000000000000011111142; + } + + function delegatedFunction() external pure returns (bool) { + return true; + } +} diff --git a/contracts/mocks/DummyImplementation.sol b/contracts/mocks/DummyImplementation.sol new file mode 100644 index 000000000..e18692034 --- /dev/null +++ b/contracts/mocks/DummyImplementation.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +abstract contract Impl { + function version() public pure virtual returns (string memory); +} + +contract DummyImplementation { + uint256 public value; + string public text; + uint256[] public values; + + function initializeNonPayable() public { + value = 10; + } + + function initializePayable() payable public { + value = 100; + } + + function initializeNonPayable(uint256 _value) public { + value = _value; + } + + function initializePayable(uint256 _value) payable public { + value = _value; + } + + function initialize(uint256 _value, string memory _text, uint256[] memory _values) public { + value = _value; + text = _text; + values = _values; + } + + function get() public pure returns (bool) { + return true; + } + + function version() public pure virtual returns (string memory) { + return "V1"; + } + + function reverts() public pure { + require(false); + } +} + +contract DummyImplementationV2 is DummyImplementation { + function migrate(uint256 newVal) payable public { + value = newVal; + } + + function version() public pure override returns (string memory) { + return "V2"; + } +} diff --git a/contracts/mocks/InitializableMock.sol b/contracts/mocks/InitializableMock.sol new file mode 100644 index 000000000..a3f53687c --- /dev/null +++ b/contracts/mocks/InitializableMock.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../proxy/Initializable.sol"; + +/** + * @title InitializableMock + * @dev This contract is a mock to test initializable functionality + */ +contract InitializableMock is Initializable { + + bool public initializerRan; + uint256 public x; + + function initialize() public initializer { + initializerRan = true; + } + + function initializeNested() public initializer { + initialize(); + } + + function initializeWithX(uint256 _x) public payable initializer { + x = _x; + } + + function nonInitializable(uint256 _x) public payable { + x = _x; + } + + function fail() public pure { + require(false, "InitializableMock forced failure"); + } + +} diff --git a/contracts/mocks/MultipleInheritanceInitializableMocks.sol b/contracts/mocks/MultipleInheritanceInitializableMocks.sol new file mode 100644 index 000000000..e7e82c57f --- /dev/null +++ b/contracts/mocks/MultipleInheritanceInitializableMocks.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../proxy/Initializable.sol"; + +// Sample contracts showing upgradeability with multiple inheritance. +// Child contract inherits from Father and Mother contracts, and Father extends from Gramps. +// +// Human +// / \ +// | Gramps +// | | +// Mother Father +// | | +// -- Child -- + +/** + * Sample base intializable contract that is a human + */ +contract SampleHuman is Initializable { + bool public isHuman; + + function initialize() public initializer { + isHuman = true; + } +} + +/** + * Sample base intializable contract that defines a field mother + */ +contract SampleMother is Initializable, SampleHuman { + uint256 public mother; + + function initialize(uint256 value) public initializer virtual { + SampleHuman.initialize(); + mother = value; + } +} + +/** + * Sample base intializable contract that defines a field gramps + */ +contract SampleGramps is Initializable, SampleHuman { + string public gramps; + + function initialize(string memory value) public initializer virtual { + SampleHuman.initialize(); + gramps = value; + } +} + +/** + * Sample base intializable contract that defines a field father and extends from gramps + */ +contract SampleFather is Initializable, SampleGramps { + uint256 public father; + + function initialize(string memory _gramps, uint256 _father) public initializer { + SampleGramps.initialize(_gramps); + father = _father; + } +} + +/** + * Child extends from mother, father (gramps) + */ +contract SampleChild is Initializable, SampleMother, SampleFather { + uint256 public child; + + function initialize(uint256 _mother, string memory _gramps, uint256 _father, uint256 _child) public initializer { + SampleMother.initialize(_mother); + SampleFather.initialize(_gramps, _father); + child = _child; + } +} diff --git a/contracts/mocks/RegressionImplementation.sol b/contracts/mocks/RegressionImplementation.sol new file mode 100644 index 000000000..1db26ac96 --- /dev/null +++ b/contracts/mocks/RegressionImplementation.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../proxy/Initializable.sol"; + +contract Implementation1 is Initializable { + uint internal _value; + + function initialize() public initializer { + } + + function setValue(uint _number) public { + _value = _number; + } +} + +contract Implementation2 is Initializable { + uint internal _value; + + function initialize() public initializer { + } + + function setValue(uint _number) public { + _value = _number; + } + + function getValue() public view returns (uint) { + return _value; + } +} + +contract Implementation3 is Initializable { + uint internal _value; + + function initialize() public initializer { + } + + function setValue(uint _number) public { + _value = _number; + } + + function getValue(uint _number) public view returns (uint) { + return _value + _number; + } +} + +contract Implementation4 is Initializable { + uint internal _value; + + function initialize() public initializer { + } + + function setValue(uint _number) public { + _value = _number; + } + + function getValue() public view returns (uint) { + return _value; + } + + // solhint-disable-next-line payable-fallback + fallback() external { + _value = 1; + } +} diff --git a/contracts/mocks/SingleInheritanceInitializableMocks.sol b/contracts/mocks/SingleInheritanceInitializableMocks.sol new file mode 100644 index 000000000..a9fcbce2d --- /dev/null +++ b/contracts/mocks/SingleInheritanceInitializableMocks.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../proxy/Initializable.sol"; + +/** + * @title MigratableMockV1 + * @dev This contract is a mock to test initializable functionality through migrations + */ +contract MigratableMockV1 is Initializable { + uint256 public x; + + function initialize(uint256 value) public payable initializer { + x = value; + } +} + +/** + * @title MigratableMockV2 + * @dev This contract is a mock to test migratable functionality with params + */ +contract MigratableMockV2 is MigratableMockV1 { + bool internal _migratedV2; + uint256 public y; + + function migrate(uint256 value, uint256 anotherValue) public payable { + require(!_migratedV2); + x = value; + y = anotherValue; + _migratedV2 = true; + } +} + +/** + * @title MigratableMockV3 + * @dev This contract is a mock to test migratable functionality without params + */ +contract MigratableMockV3 is MigratableMockV2 { + bool internal _migratedV3; + + function migrate() public payable { + require(!_migratedV3); + uint256 oldX = x; + x = y; + y = oldX; + _migratedV3 = true; + } +} diff --git a/contracts/proxy/Initializable.sol b/contracts/proxy/Initializable.sol new file mode 100644 index 000000000..449a62540 --- /dev/null +++ b/contracts/proxy/Initializable.sol @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: MIT + +pragma solidity >=0.4.24 <0.7.0; + + +/** + * @title Initializable + * + * @dev Helper contract to support initializer functions. To use it, replace + * the constructor with a function that has the `initializer` modifier. + * WARNING: Unlike constructors, initializer functions must be manually + * invoked. This applies both to deploying an Initializable contract, as well + * as extending an Initializable contract via inheritance. + * WARNING: When used with inheritance, manual care must be taken to not invoke + * a parent initializer twice, or ensure that all initializers are idempotent, + * because this is not dealt with automatically as with constructors. + */ +contract Initializable { + + /** + * @dev Indicates that the contract has been initialized. + */ + bool private _initialized; + + /** + * @dev Indicates that the contract is in the process of being initialized. + */ + bool private _initializing; + + /** + * @dev Modifier to use in the initializer function of a contract. + */ + modifier initializer() { + require(_initializing || _isConstructor() || !_initialized, "Initializable: contract is already initialized"); + + bool isTopLevelCall = !_initializing; + if (isTopLevelCall) { + _initializing = true; + _initialized = true; + } + + _; + + if (isTopLevelCall) { + _initializing = false; + } + } + + /// @dev Returns true if and only if the function is running in the constructor + function _isConstructor() private view returns (bool) { + // extcodesize checks the size of the code stored in an address, and + // address returns the current address. Since the code is still not + // deployed when running a constructor, any checks on its code size will + // yield zero, making it an effective way to detect if a contract is + // under construction or not. + address self = address(this); + uint256 cs; + // solhint-disable-next-line no-inline-assembly + assembly { cs := extcodesize(self) } + return cs == 0; + } +} diff --git a/contracts/proxy/Proxy.sol b/contracts/proxy/Proxy.sol new file mode 100644 index 000000000..e68622f6d --- /dev/null +++ b/contracts/proxy/Proxy.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +/** + * @title Proxy + * @dev Implements delegation of calls to other contracts, with proper + * forwarding of return values and bubbling of failures. + * It defines a fallback function that delegates all calls to the address + * returned by the abstract _implementation() internal function. + */ +abstract contract Proxy { + /** + * @dev Fallback function. + * Implemented entirely in `_fallback`. + */ + fallback () payable external { + _fallback(); + } + + /** + * @dev Receive function. + * Implemented entirely in `_fallback`. + */ + receive () payable external { + _fallback(); + } + + /** + * @return The Address of the implementation. + */ + function _implementation() internal virtual view returns (address); + + /** + * @dev Delegates execution to an implementation contract. + * This is a low level function that doesn't return to its internal call site. + * It will return to the external caller whatever the implementation returns. + * @param implementation Address to delegate. + */ + function _delegate(address implementation) internal { + // solhint-disable-next-line no-inline-assembly + assembly { + // Copy msg.data. We take full control of memory in this inline assembly + // block because it will not return to Solidity code. We overwrite the + // Solidity scratch pad at memory position 0. + calldatacopy(0, 0, calldatasize()) + + // Call the implementation. + // out and outsize are 0 because we don't know the size yet. + let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0) + + // Copy the returned data. + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecall returns 0 on error. + case 0 { revert(0, returndatasize()) } + default { return(0, returndatasize()) } + } + } + + /** + * @dev Function that is run as the first thing in the fallback function. + * Can be redefined in derived contracts to add functionality. + * Redefinitions must call super._willFallback(). + */ + function _willFallback() internal virtual { + } + + /** + * @dev fallback implementation. + * Extracted to enable manual triggering. + */ + function _fallback() internal { + _willFallback(); + _delegate(_implementation()); + } +} diff --git a/contracts/proxy/ProxyAdmin.sol b/contracts/proxy/ProxyAdmin.sol new file mode 100644 index 000000000..35ce8298e --- /dev/null +++ b/contracts/proxy/ProxyAdmin.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "../access/Ownable.sol"; +import "./TransparentUpgradeableProxy.sol"; + +/** + * @title ProxyAdmin + * @dev This contract is the admin of a proxy, and is in charge + * of upgrading it as well as transferring it to another admin. + */ +contract ProxyAdmin is Ownable { + + /** + * @dev Returns the current implementation of a proxy. + * This is needed because only the proxy admin can query it. + * @return The address of the current implementation of the proxy. + */ + function getProxyImplementation(TransparentUpgradeableProxy proxy) public view returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("implementation()")) == 0x5c60da1b + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"5c60da1b"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Returns the admin of a proxy. Only the admin can query it. + * @return The address of the current admin of the proxy. + */ + function getProxyAdmin(TransparentUpgradeableProxy proxy) public view returns (address) { + // We need to manually run the static call since the getter cannot be flagged as view + // bytes4(keccak256("admin()")) == 0xf851a440 + (bool success, bytes memory returndata) = address(proxy).staticcall(hex"f851a440"); + require(success); + return abi.decode(returndata, (address)); + } + + /** + * @dev Changes the admin of a proxy. + * @param proxy Proxy to change admin. + * @param newAdmin Address to transfer proxy administration to. + */ + function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public onlyOwner { + proxy.changeAdmin(newAdmin); + } + + /** + * @dev Upgrades a proxy to the newest implementation of a contract. + * @param proxy Proxy to be upgraded. + * @param implementation the address of the Implementation. + */ + function upgrade(TransparentUpgradeableProxy proxy, address implementation) public onlyOwner { + proxy.upgradeTo(implementation); + } + + /** + * @dev Upgrades a proxy to the newest implementation of a contract and forwards a function call to it. + * This is useful to initialize the proxied contract. + * @param proxy Proxy to be upgraded. + * @param implementation Address of the Implementation. + * @param data Data to send as msg.data in the low level call. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + */ + function upgradeAndCall(TransparentUpgradeableProxy proxy, address implementation, bytes memory data) public payable onlyOwner { + proxy.upgradeToAndCall{value: msg.value}(implementation, data); + } +} diff --git a/contracts/proxy/TransparentUpgradeableProxy.sol b/contracts/proxy/TransparentUpgradeableProxy.sol new file mode 100644 index 000000000..fbd431f69 --- /dev/null +++ b/contracts/proxy/TransparentUpgradeableProxy.sol @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./UpgradeableProxy.sol"; + +/** + * @title TransparentUpgradeableProxy + * @dev This contract combines an upgradeability proxy with an authorization + * mechanism for administrative tasks. + * All external functions in this contract must be guarded by the + * `ifAdmin` modifier. See ethereum/solidity#3864 for a Solidity + * feature proposal that would enable this to be done automatically. + */ +contract TransparentUpgradeableProxy is UpgradeableProxy { + /** + * Contract constructor. + * @param _logic address of the initial implementation. + * @param _admin Address of the proxy administrator. + * @param _data Data to send as msg.data to the implementation to initialize the proxied contract. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + * This parameter is optional, if no data is given the initialization call to proxied contract will be skipped. + */ + constructor(address _logic, address _admin, bytes memory _data) public payable UpgradeableProxy(_logic, _data) { + assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)); + _setAdmin(_admin); + } + + /** + * @dev Emitted when the administration has been transferred. + * @param previousAdmin Address of the previous admin. + * @param newAdmin Address of the new admin. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is + * validated in the constructor. + */ + + bytes32 private constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Modifier to check whether the `msg.sender` is the admin. + * If it is, it will run the function. Otherwise, it will delegate the call + * to the implementation. + */ + modifier ifAdmin() { + if (msg.sender == _admin()) { + _; + } else { + _fallback(); + } + } + + /** + * @return The address of the proxy admin. + */ + function admin() external ifAdmin returns (address) { + return _admin(); + } + + /** + * @return The address of the implementation. + */ + function implementation() external ifAdmin returns (address) { + return _implementation(); + } + + /** + * @dev Changes the admin of the proxy. + * Only the current admin can call this function. + * @param newAdmin Address to transfer proxy administration to. + */ + function changeAdmin(address newAdmin) external ifAdmin { + require(newAdmin != address(0), "TransparentUpgradeableProxy: new admin is the zero address"); + emit AdminChanged(_admin(), newAdmin); + _setAdmin(newAdmin); + } + + /** + * @dev Upgrade the backing implementation of the proxy. + * Only the admin can call this function. + * @param newImplementation Address of the new implementation. + */ + function upgradeTo(address newImplementation) external ifAdmin { + _upgradeTo(newImplementation); + } + + /** + * @dev Upgrade the backing implementation of the proxy and call a function + * on the new implementation. + * This is useful to initialize the proxied contract. + * @param newImplementation Address of the new implementation. + * @param data Data to send as msg.data in the low level call. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + */ + function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin { + _upgradeTo(newImplementation); + // solhint-disable-next-line avoid-low-level-calls + (bool success,) = newImplementation.delegatecall(data); + require(success); + } + + /** + * @return adm The admin slot. + */ + function _admin() internal view returns (address adm) { + bytes32 slot = _ADMIN_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + adm := sload(slot) + } + } + + /** + * @dev Sets the address of the proxy admin. + * @param newAdmin Address of the new proxy admin. + */ + function _setAdmin(address newAdmin) internal { + bytes32 slot = _ADMIN_SLOT; + + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, newAdmin) + } + } + + /** + * @dev Only fallback when the sender is not the admin. + */ + function _willFallback() internal override virtual { + require(msg.sender != _admin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); + super._willFallback(); + } +} diff --git a/contracts/proxy/UpgradeableProxy.sol b/contracts/proxy/UpgradeableProxy.sol new file mode 100644 index 000000000..8b120c069 --- /dev/null +++ b/contracts/proxy/UpgradeableProxy.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.6.0; + +import "./Proxy.sol"; +import "../utils/Address.sol"; + +/** + * @title UpgradeableProxy + * @dev This contract implements a proxy that allows to change the + * implementation address to which it will delegate. + * Such a change is called an implementation upgrade. + */ +contract UpgradeableProxy is Proxy { + /** + * @dev Contract constructor. + * @param _logic Address of the initial implementation. + * @param _data Data to send as msg.data to the implementation to initialize the proxied contract. + * It should include the signature and the parameters of the function to be called, as described in + * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + * This parameter is optional, if no data is given the initialization call to proxied contract will be skipped. + */ + constructor(address _logic, bytes memory _data) public payable { + assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); + _setImplementation(_logic); + if(_data.length > 0) { + // solhint-disable-next-line avoid-low-level-calls + (bool success,) = _logic.delegatecall(_data); + require(success); + } + } + + /** + * @dev Emitted when the implementation is upgraded. + * @param implementation Address of the new implementation. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + * validated in the constructor. + */ + bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev Returns the current implementation. + * @return impl Address of the current implementation + */ + function _implementation() internal override view returns (address impl) { + bytes32 slot = _IMPLEMENTATION_SLOT; + // solhint-disable-next-line no-inline-assembly + assembly { + impl := sload(slot) + } + } + + /** + * @dev Upgrades the proxy to a new implementation. + * @param newImplementation Address of the new implementation. + */ + function _upgradeTo(address newImplementation) internal { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /** + * @dev Sets the implementation address of the proxy. + * @param newImplementation Address of the new implementation. + */ + function _setImplementation(address newImplementation) internal { + require(Address.isContract(newImplementation), "UpgradeableProxy: new implementation is not a contract"); + + bytes32 slot = _IMPLEMENTATION_SLOT; + + // solhint-disable-next-line no-inline-assembly + assembly { + sstore(slot, newImplementation) + } + } +} diff --git a/test/proxy/Initializable.test.js b/test/proxy/Initializable.test.js new file mode 100644 index 000000000..c20356c4d --- /dev/null +++ b/test/proxy/Initializable.test.js @@ -0,0 +1,81 @@ +const { contract } = require('@openzeppelin/test-environment'); + +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const { assert } = require('chai'); + +const InitializableMock = contract.fromArtifact('InitializableMock'); +const SampleChild = contract.fromArtifact('SampleChild'); + +describe('Initializable', function () { + describe('basic testing without inheritance', function () { + beforeEach('deploying', async function () { + this.contract = await InitializableMock.new(); + }); + + context('before initialize', function () { + it('initializer has not run', async function () { + assert.isFalse(await this.contract.initializerRan()); + }); + }); + + context('after initialize', function () { + beforeEach('initializing', async function () { + await this.contract.initialize(); + }); + + it('initializer has run', async function () { + assert.isTrue(await this.contract.initializerRan()); + }); + + it('initializer does not run again', async function () { + await expectRevert(this.contract.initialize(), 'Initializable: contract is already initialized'); + }); + }); + + context('after nested initialize', function () { + beforeEach('initializing', async function () { + await this.contract.initializeNested(); + }); + + it('initializer has run', async function () { + assert.isTrue(await this.contract.initializerRan()); + }); + }); + }); + + describe('complex testing with inheritance', function () { + const mother = 12; + const gramps = '56'; + const father = 34; + const child = 78; + + beforeEach('deploying', async function () { + this.contract = await SampleChild.new(); + }); + + beforeEach('initializing', async function () { + await this.contract.initialize(mother, gramps, father, child); + }); + + it('initializes human', async function () { + assert.equal(await this.contract.isHuman(), true); + }); + + it('initializes mother', async function () { + assert.equal(await this.contract.mother(), mother); + }); + + it('initializes gramps', async function () { + assert.equal(await this.contract.gramps(), gramps); + }); + + it('initializes father', async function () { + assert.equal(await this.contract.father(), father); + }); + + it('initializes child', async function () { + assert.equal(await this.contract.child(), child); + }); + }); +}); diff --git a/test/proxy/ProxyAdmin.test.js b/test/proxy/ProxyAdmin.test.js new file mode 100644 index 000000000..991c9f653 --- /dev/null +++ b/test/proxy/ProxyAdmin.test.js @@ -0,0 +1,119 @@ +const { accounts, contract } = require('@openzeppelin/test-environment'); + +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const ImplV1 = contract.fromArtifact('DummyImplementation'); +const ImplV2 = contract.fromArtifact('DummyImplementationV2'); +const ProxyAdmin = contract.fromArtifact('ProxyAdmin'); +const TransparentUpgradeableProxy = contract.fromArtifact('TransparentUpgradeableProxy'); + +describe('ProxyAdmin', function () { + const [proxyAdminOwner, newAdmin, anotherAccount] = accounts; + + before('set implementations', async function () { + this.implementationV1 = await ImplV1.new(); + this.implementationV2 = await ImplV2.new(); + }); + + beforeEach(async function () { + const initializeData = Buffer.from(''); + this.proxyAdmin = await ProxyAdmin.new({ from: proxyAdminOwner }); + this.proxy = await TransparentUpgradeableProxy.new( + this.implementationV1.address, + this.proxyAdmin.address, + initializeData, + { from: proxyAdminOwner }, + ); + }); + + it('has an owner', async function () { + expect(await this.proxyAdmin.owner()).to.equal(proxyAdminOwner); + }); + + describe('#getProxyAdmin', function () { + it('returns proxyAdmin as admin of the proxy', async function () { + const admin = await this.proxyAdmin.getProxyAdmin(this.proxy.address); + expect(admin).to.be.equal(this.proxyAdmin.address); + }); + }); + + describe('#changeProxyAdmin', function () { + it('fails to change proxy admin if its not the proxy owner', async function () { + await expectRevert( + this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: anotherAccount }), + 'caller is not the owner', + ); + }); + + it('changes proxy admin', async function () { + await this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: proxyAdminOwner }); + expect(await this.proxy.admin.call({ from: newAdmin })).to.eq(newAdmin); + }); + }); + + describe('#getProxyImplementation', function () { + it('returns proxy implementation address', async function () { + const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address); + expect(implementationAddress).to.be.equal(this.implementationV1.address); + }); + }); + + describe('#upgrade', function () { + context('with unauthorized account', function () { + it('fails to upgrade', async function () { + await expectRevert( + this.proxyAdmin.upgrade(this.proxy.address, this.implementationV2.address, { from: anotherAccount }), + 'caller is not the owner', + ); + }); + }); + + context('with authorized account', function () { + it('upgrades implementation', async function () { + await this.proxyAdmin.upgrade(this.proxy.address, this.implementationV2.address, { from: proxyAdminOwner }); + const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address); + expect(implementationAddress).to.be.equal(this.implementationV2.address); + }); + }); + }); + + describe('#upgradeAndCall', function () { + context('with unauthorized account', function () { + it('fails to upgrade', async function () { + const callData = new ImplV1('').contract.methods['initializeNonPayable(uint256)'](1337).encodeABI(); + await expectRevert( + this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, + { from: anotherAccount } + ), + 'caller is not the owner', + ); + }); + }); + + context('with authorized account', function () { + context('with invalid callData', function () { + it('fails to upgrade', async function () { + const callData = '0x12345678'; + await expectRevert.unspecified( + this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, + { from: proxyAdminOwner } + ), + ); + }); + }); + + context('with valid callData', function () { + it('upgrades implementation', async function () { + const callData = new ImplV1('').contract.methods['initializeNonPayable(uint256)'](1337).encodeABI(); + await this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, + { from: proxyAdminOwner } + ); + const implementationAddress = await this.proxyAdmin.getProxyImplementation(this.proxy.address); + expect(implementationAddress).to.be.equal(this.implementationV2.address); + }); + }); + }); + }); +}); diff --git a/test/proxy/TransparentUpgradeableProxy.behaviour.js b/test/proxy/TransparentUpgradeableProxy.behaviour.js new file mode 100644 index 000000000..0107680a2 --- /dev/null +++ b/test/proxy/TransparentUpgradeableProxy.behaviour.js @@ -0,0 +1,436 @@ +const { contract, web3 } = require('@openzeppelin/test-environment'); + +const { BN, expectRevert, expectEvent, constants } = require('@openzeppelin/test-helpers'); +const { ZERO_ADDRESS } = constants; +const { toChecksumAddress, keccak256 } = require('ethereumjs-util'); + +const { expect } = require('chai'); + +const Proxy = contract.fromArtifact('Proxy'); +const Implementation1 = contract.fromArtifact('Implementation1'); +const Implementation2 = contract.fromArtifact('Implementation2'); +const Implementation3 = contract.fromArtifact('Implementation3'); +const Implementation4 = contract.fromArtifact('Implementation4'); +const MigratableMockV1 = contract.fromArtifact('MigratableMockV1'); +const MigratableMockV2 = contract.fromArtifact('MigratableMockV2'); +const MigratableMockV3 = contract.fromArtifact('MigratableMockV3'); +const InitializableMock = contract.fromArtifact('InitializableMock'); +const DummyImplementation = contract.fromArtifact('DummyImplementation'); +const ClashingImplementation = contract.fromArtifact('ClashingImplementation'); + +const IMPLEMENTATION_LABEL = 'eip1967.proxy.implementation'; +const ADMIN_LABEL = 'eip1967.proxy.admin'; + +module.exports = function shouldBehaveLikeTransparentUpgradeableProxy (createProxy, accounts) { + const [proxyAdminAddress, proxyAdminOwner, anotherAccount] = accounts; + + before(async function () { + this.implementationV0 = (await DummyImplementation.new()).address; + this.implementationV1 = (await DummyImplementation.new()).address; + }); + + beforeEach(async function () { + const initializeData = Buffer.from(''); + this.proxy = await createProxy(this.implementationV0, proxyAdminAddress, initializeData, { + from: proxyAdminOwner, + }); + this.proxyAddress = this.proxy.address; + }); + + describe('implementation', function () { + it('returns the current implementation address', async function () { + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + + expect(implementation).to.be.equal(this.implementationV0); + }); + + it('delegates to the implementation', async function () { + const dummy = new DummyImplementation(this.proxyAddress); + const value = await dummy.get(); + + expect(value).to.equal(true); + }); + }); + + describe('upgradeTo', function () { + describe('when the sender is the admin', function () { + const from = proxyAdminAddress; + + describe('when the given implementation is different from the current one', function () { + it('upgrades to the requested implementation', async function () { + await this.proxy.upgradeTo(this.implementationV1, { from }); + + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + expect(implementation).to.be.equal(this.implementationV1); + }); + + it('emits an event', async function () { + expectEvent( + await this.proxy.upgradeTo(this.implementationV1, { from }), + 'Upgraded', { + implementation: this.implementationV1, + }, + ); + }); + }); + + describe('when the given implementation is the zero address', function () { + it('reverts', async function () { + await expectRevert( + this.proxy.upgradeTo(ZERO_ADDRESS, { from }), + 'UpgradeableProxy: new implementation is not a contract', + ); + }); + }); + }); + + describe('when the sender is not the admin', function () { + const from = anotherAccount; + + it('reverts', async function () { + await expectRevert.unspecified( + this.proxy.upgradeTo(this.implementationV1, { from }) + ); + }); + }); + }); + + describe('upgradeToAndCall', function () { + describe('without migrations', function () { + beforeEach(async function () { + this.behavior = await InitializableMock.new(); + }); + + describe('when the call does not fail', function () { + const initializeData = new InitializableMock('').contract.methods['initializeWithX(uint256)'](42).encodeABI(); + + describe('when the sender is the admin', function () { + const from = proxyAdminAddress; + const value = 1e5; + + beforeEach(async function () { + this.receipt = await this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from, value }); + }); + + it('upgrades to the requested implementation', async function () { + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + expect(implementation).to.be.equal(this.behavior.address); + }); + + it('emits an event', function () { + expectEvent(this.receipt, 'Upgraded', { implementation: this.behavior.address }); + }); + + it('calls the initializer function', async function () { + const migratable = new InitializableMock(this.proxyAddress); + const x = await migratable.x(); + expect(x).to.be.bignumber.equal('42'); + }); + + it('sends given value to the proxy', async function () { + const balance = await web3.eth.getBalance(this.proxyAddress); + expect(balance.toString()).to.be.bignumber.equal(value.toString()); + }); + + it.skip('uses the storage of the proxy', async function () { + // storage layout should look as follows: + // - 0: Initializable storage + // - 1-50: Initailizable reserved storage (50 slots) + // - 51: initializerRan + // - 52: x + const storedValue = await Proxy.at(this.proxyAddress).getStorageAt(52); + expect(parseInt(storedValue)).to.eq(42); + }); + }); + + describe('when the sender is not the admin', function () { + it('reverts', async function () { + await expectRevert.unspecified( + this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: anotherAccount }), + ); + }); + }); + }); + + describe('when the call does fail', function () { + const initializeData = new InitializableMock('').contract.methods.fail().encodeABI(); + + it('reverts', async function () { + await expectRevert.unspecified( + this.proxy.upgradeToAndCall(this.behavior.address, initializeData, { from: proxyAdminAddress }), + ); + }); + }); + }); + + describe('with migrations', function () { + describe('when the sender is the admin', function () { + const from = proxyAdminAddress; + const value = 1e5; + + describe('when upgrading to V1', function () { + const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI(); + + beforeEach(async function () { + this.behaviorV1 = await MigratableMockV1.new(); + this.balancePreviousV1 = new BN(await web3.eth.getBalance(this.proxyAddress)); + this.receipt = await this.proxy.upgradeToAndCall(this.behaviorV1.address, v1MigrationData, { from, value }); + }); + + it('upgrades to the requested version and emits an event', async function () { + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + expect(implementation).to.be.equal(this.behaviorV1.address); + expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV1.address }); + }); + + it('calls the \'initialize\' function and sends given value to the proxy', async function () { + const migratable = new MigratableMockV1(this.proxyAddress); + + const x = await migratable.x(); + expect(x).to.be.bignumber.equal('42'); + + const balance = await web3.eth.getBalance(this.proxyAddress); + expect(new BN(balance)).to.be.bignumber.equal(this.balancePreviousV1.addn(value)); + }); + + describe('when upgrading to V2', function () { + const v2MigrationData = new MigratableMockV2('').contract.methods.migrate(10, 42).encodeABI(); + + beforeEach(async function () { + this.behaviorV2 = await MigratableMockV2.new(); + this.balancePreviousV2 = new BN(await web3.eth.getBalance(this.proxyAddress)); + this.receipt = + await this.proxy.upgradeToAndCall(this.behaviorV2.address, v2MigrationData, { from, value }); + }); + + it('upgrades to the requested version and emits an event', async function () { + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + expect(implementation).to.be.equal(this.behaviorV2.address); + expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV2.address }); + }); + + it('calls the \'migrate\' function and sends given value to the proxy', async function () { + const migratable = new MigratableMockV2(this.proxyAddress); + + const x = await migratable.x(); + expect(x).to.be.bignumber.equal('10'); + + const y = await migratable.y(); + expect(y).to.be.bignumber.equal('42'); + + const balance = new BN(await web3.eth.getBalance(this.proxyAddress)); + expect(balance).to.be.bignumber.equal(this.balancePreviousV2.addn(value)); + }); + + describe('when upgrading to V3', function () { + const v3MigrationData = new MigratableMockV3('').contract.methods['migrate()']().encodeABI(); + + beforeEach(async function () { + this.behaviorV3 = await MigratableMockV3.new(); + this.balancePreviousV3 = new BN(await web3.eth.getBalance(this.proxyAddress)); + this.receipt = + await this.proxy.upgradeToAndCall(this.behaviorV3.address, v3MigrationData, { from, value }); + }); + + it('upgrades to the requested version and emits an event', async function () { + const implementation = await this.proxy.implementation.call({ from: proxyAdminAddress }); + expect(implementation).to.be.equal(this.behaviorV3.address); + expectEvent(this.receipt, 'Upgraded', { implementation: this.behaviorV3.address }); + }); + + it('calls the \'migrate\' function and sends given value to the proxy', async function () { + const migratable = new MigratableMockV3(this.proxyAddress); + + const x = await migratable.x(); + expect(x).to.be.bignumber.equal('42'); + + const y = await migratable.y(); + expect(y).to.be.bignumber.equal('10'); + + const balance = new BN(await web3.eth.getBalance(this.proxyAddress)); + expect(balance).to.be.bignumber.equal(this.balancePreviousV3.addn(value)); + }); + }); + }); + }); + }); + + describe('when the sender is not the admin', function () { + const from = anotherAccount; + + it('reverts', async function () { + const behaviorV1 = await MigratableMockV1.new(); + const v1MigrationData = new MigratableMockV1('').contract.methods.initialize(42).encodeABI(); + await expectRevert.unspecified( + this.proxy.upgradeToAndCall(behaviorV1.address, v1MigrationData, { from }), + ); + }); + }); + }); + }); + + describe('changeAdmin', function () { + describe('when the new proposed admin is not the zero address', function () { + const newAdmin = anotherAccount; + + describe('when the sender is the admin', function () { + beforeEach('transferring', async function () { + this.receipt = await this.proxy.changeAdmin(newAdmin, { from: proxyAdminAddress }); + }); + + it('assigns new proxy admin', async function () { + const newProxyAdmin = await this.proxy.admin.call({ from: newAdmin }); + expect(newProxyAdmin).to.be.equal(anotherAccount); + }); + + it('emits an event', function () { + expectEvent(this.receipt, 'AdminChanged', { + previousAdmin: proxyAdminAddress, + newAdmin: newAdmin, + }); + }); + }); + + describe('when the sender is not the admin', function () { + it('reverts', async function () { + await expectRevert.unspecified(this.proxy.changeAdmin(newAdmin, { from: anotherAccount })); + }); + }); + }); + + describe('when the new proposed admin is the zero address', function () { + it('reverts', async function () { + await expectRevert( + this.proxy.changeAdmin(ZERO_ADDRESS, { from: proxyAdminAddress }), + 'TransparentUpgradeableProxy: new admin is the zero address', + ); + }); + }); + }); + + describe('storage', function () { + it('should store the implementation address in specified location', async function () { + const slot = '0x' + new BN(keccak256(Buffer.from(IMPLEMENTATION_LABEL))).subn(1).toString(16); + const implementation = toChecksumAddress(await web3.eth.getStorageAt(this.proxyAddress, slot)); + expect(implementation).to.be.equal(this.implementationV0); + }); + + it('should store the admin proxy in specified location', async function () { + const slot = '0x' + new BN(keccak256(Buffer.from(ADMIN_LABEL))).subn(1).toString(16); + const proxyAdmin = toChecksumAddress(await web3.eth.getStorageAt(this.proxyAddress, slot)); + expect(proxyAdmin).to.be.equal(proxyAdminAddress); + }); + }); + + describe('transparent proxy', function () { + beforeEach('creating proxy', async function () { + const initializeData = Buffer.from(''); + this.impl = await ClashingImplementation.new(); + this.proxy = await createProxy(this.impl.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + this.clashing = new ClashingImplementation(this.proxy.address); + }); + + it('proxy admin cannot call delegated functions', async function () { + await expectRevert( + this.clashing.delegatedFunction({ from: proxyAdminAddress }), + 'TransparentUpgradeableProxy: admin cannot fallback to proxy target', + ); + }); + + context('when function names clash', function () { + it('when sender is proxy admin should run the proxy function', async function () { + const value = await this.proxy.admin.call({ from: proxyAdminAddress }); + expect(value).to.be.equal(proxyAdminAddress); + }); + + it('when sender is other should delegate to implementation', async function () { + const value = await this.proxy.admin.call({ from: anotherAccount }); + expect(value).to.be.equal('0x0000000000000000000000000000000011111142'); + }); + }); + }); + + describe('regression', () => { + const initializeData = Buffer.from(''); + + it('should add new function', async () => { + const instance1 = await Implementation1.new(); + const proxy = await createProxy(instance1.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + const proxyInstance1 = new Implementation1(proxy.address); + await proxyInstance1.setValue(42); + + const instance2 = await Implementation2.new(); + await proxy.upgradeTo(instance2.address, { from: proxyAdminAddress }); + + const proxyInstance2 = new Implementation2(proxy.address); + const res = await proxyInstance2.getValue(); + expect(res.toString()).to.eq('42'); + }); + + it('should remove function', async () => { + const instance2 = await Implementation2.new(); + const proxy = await createProxy(instance2.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + const proxyInstance2 = new Implementation2(proxy.address); + await proxyInstance2.setValue(42); + const res = await proxyInstance2.getValue(); + expect(res.toString()).to.eq('42'); + + const instance1 = await Implementation1.new(); + await proxy.upgradeTo(instance1.address, { from: proxyAdminAddress }); + + const proxyInstance1 = new Implementation2(proxy.address); + await expectRevert.unspecified(proxyInstance1.getValue()); + }); + + it('should change function signature', async () => { + const instance1 = await Implementation1.new(); + const proxy = await createProxy(instance1.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + const proxyInstance1 = new Implementation1(proxy.address); + await proxyInstance1.setValue(42); + + const instance3 = await Implementation3.new(); + await proxy.upgradeTo(instance3.address, { from: proxyAdminAddress }); + const proxyInstance3 = new Implementation3(proxy.address); + + const res = await proxyInstance3.getValue(8); + expect(res.toString()).to.eq('50'); + }); + + it('should add fallback function', async () => { + const initializeData = Buffer.from(''); + const instance1 = await Implementation1.new(); + const proxy = await createProxy(instance1.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + const instance4 = await Implementation4.new(); + await proxy.upgradeTo(instance4.address, { from: proxyAdminAddress }); + const proxyInstance4 = new Implementation4(proxy.address); + + const data = ''; + await web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data }); + + const res = await proxyInstance4.getValue(); + expect(res.toString()).to.eq('1'); + }); + + it('should remove fallback function', async () => { + const instance4 = await Implementation4.new(); + const proxy = await createProxy(instance4.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); + + const instance2 = await Implementation2.new(); + await proxy.upgradeTo(instance2.address, { from: proxyAdminAddress }); + + const data = ''; + await expectRevert.unspecified( + web3.eth.sendTransaction({ to: proxy.address, from: anotherAccount, data }), + ); + + const proxyInstance2 = new Implementation2(proxy.address); + const res = await proxyInstance2.getValue(); + expect(res.toString()).to.eq('0'); + }); + }); +}; diff --git a/test/proxy/TransparentUpgradeableProxy.test.js b/test/proxy/TransparentUpgradeableProxy.test.js new file mode 100644 index 000000000..ea7613bbc --- /dev/null +++ b/test/proxy/TransparentUpgradeableProxy.test.js @@ -0,0 +1,17 @@ +const { accounts, contract } = require('@openzeppelin/test-environment'); + +const shouldBehaveLikeUpgradeableProxy = require('./UpgradeableProxy.behaviour'); +const shouldBehaveLikeTransparentUpgradeableProxy = require('./TransparentUpgradeableProxy.behaviour'); + +const TransparentUpgradeableProxy = contract.fromArtifact('TransparentUpgradeableProxy'); + +describe('TransparentUpgradeableProxy', function () { + const [proxyAdminAddress, proxyAdminOwner] = accounts; + + const createProxy = async function (logic, admin, initData, opts) { + return TransparentUpgradeableProxy.new(logic, admin, initData, opts); + }; + + shouldBehaveLikeUpgradeableProxy(createProxy, proxyAdminAddress, proxyAdminOwner); + shouldBehaveLikeTransparentUpgradeableProxy(createProxy, accounts); +}); diff --git a/test/proxy/UpgradeableProxy.behaviour.js b/test/proxy/UpgradeableProxy.behaviour.js new file mode 100644 index 000000000..72e5c258a --- /dev/null +++ b/test/proxy/UpgradeableProxy.behaviour.js @@ -0,0 +1,216 @@ +const { contract, web3 } = require('@openzeppelin/test-environment'); + +const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { toChecksumAddress, keccak256 } = require('ethereumjs-util'); + +const { expect } = require('chai'); + +const DummyImplementation = contract.fromArtifact('DummyImplementation'); + +const IMPLEMENTATION_LABEL = 'eip1967.proxy.implementation'; + +module.exports = function shouldBehaveLikeUpgradeableProxy (createProxy, proxyAdminAddress, proxyCreator) { + it('cannot be initialized with a non-contract address', async function () { + const nonContractAddress = proxyCreator; + const initializeData = Buffer.from(''); + await expectRevert.unspecified( + createProxy(nonContractAddress, proxyAdminAddress, initializeData, { + from: proxyCreator, + }), + ); + }); + + before('deploy implementation', async function () { + this.implementation = web3.utils.toChecksumAddress((await DummyImplementation.new()).address); + }); + + const assertProxyInitialization = function ({ value, balance }) { + it('sets the implementation address', async function () { + const slot = '0x' + new BN(keccak256(Buffer.from(IMPLEMENTATION_LABEL))).subn(1).toString(16); + const implementation = toChecksumAddress(await web3.eth.getStorageAt(this.proxy, slot)); + expect(implementation).to.be.equal(this.implementation); + }); + + it('initializes the proxy', async function () { + const dummy = new DummyImplementation(this.proxy); + expect(await dummy.value()).to.be.bignumber.equal(value.toString()); + }); + + it('has expected balance', async function () { + expect(await web3.eth.getBalance(this.proxy)).to.be.bignumber.equal(balance.toString()); + }); + }; + + describe('without initialization', function () { + const initializeData = Buffer.from(''); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + }) + ).address; + }); + + assertProxyInitialization({ value: 0, balance: 0 }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + value, + }) + ).address; + }); + + assertProxyInitialization({ value: 0, balance: value }); + }); + }); + + describe('initialization without parameters', function () { + describe('non payable', function () { + const expectedInitializedValue = 10; + const initializeData = new DummyImplementation('').contract.methods['initializeNonPayable()']().encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + it('reverts', async function () { + await expectRevert.unspecified( + createProxy(this.implementation, proxyAdminAddress, initializeData, { from: proxyCreator, value }), + ); + }); + }); + }); + + describe('payable', function () { + const expectedInitializedValue = 100; + const initializeData = new DummyImplementation('').contract.methods['initializePayable()']().encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + value, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: value, + }); + }); + }); + }); + + describe('initialization with parameters', function () { + describe('non payable', function () { + const expectedInitializedValue = 10; + const initializeData = new DummyImplementation('').contract + .methods['initializeNonPayable(uint256)'](expectedInitializedValue).encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + it('reverts', async function () { + await expectRevert.unspecified( + createProxy(this.implementation, proxyAdminAddress, initializeData, { from: proxyCreator, value }), + ); + }); + }); + }); + + describe('payable', function () { + const expectedInitializedValue = 42; + const initializeData = new DummyImplementation('').contract + .methods['initializePayable(uint256)'](expectedInitializedValue).encodeABI(); + + describe('when not sending balance', function () { + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: 0, + }); + }); + + describe('when sending some balance', function () { + const value = 10e5; + + beforeEach('creating proxy', async function () { + this.proxy = ( + await createProxy(this.implementation, proxyAdminAddress, initializeData, { + from: proxyCreator, + value, + }) + ).address; + }); + + assertProxyInitialization({ + value: expectedInitializedValue, + balance: value, + }); + }); + }); + }); +}; diff --git a/test/proxy/UpgradeableProxy.test.js b/test/proxy/UpgradeableProxy.test.js new file mode 100644 index 000000000..e9d70bdf4 --- /dev/null +++ b/test/proxy/UpgradeableProxy.test.js @@ -0,0 +1,15 @@ +const { accounts, contract } = require('@openzeppelin/test-environment'); + +const shouldBehaveLikeUpgradeableProxy = require('./UpgradeableProxy.behaviour'); + +const UpgradeableProxy = contract.fromArtifact('UpgradeableProxy'); + +describe('UpgradeableProxy', function () { + const [proxyAdminOwner] = accounts; + + const createProxy = async function (implementation, _admin, initData, opts) { + return UpgradeableProxy.new(implementation, initData, opts); + }; + + shouldBehaveLikeUpgradeableProxy(createProxy, undefined, proxyAdminOwner); +}); From aaa5ef81cf75454d1c337dc3de03d12480849ad1 Mon Sep 17 00:00:00 2001 From: tincho Date: Tue, 1 Sep 2020 14:57:40 -0300 Subject: [PATCH 13/20] Fix typos (#2343) --- DOCUMENTATION.md | 2 +- contracts/GSN/GSNRecipient.sol | 4 ++-- contracts/GSN/IRelayRecipient.sol | 2 +- contracts/token/ERC1155/ERC1155.sol | 8 ++++---- contracts/token/ERC1155/IERC1155.sol | 2 +- contracts/token/ERC721/ERC721.sol | 2 +- contracts/token/ERC721/IERC721.sol | 2 +- contracts/utils/README.adoc | 2 +- test/helpers/sign.js | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 50c83fa29..ca39e51db 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -10,7 +10,7 @@ program that extracts the API Reference from source code. The [`docs.openzeppelin.com`](https://github.com/OpenZeppelin/docs.openzeppelin.com) repository hosts the configuration for the entire site, which includes -documetation for all of the OpenZeppelin projects. +documentation for all of the OpenZeppelin projects. To run the docs locally you should run `npm run docs:watch` on this repository. diff --git a/contracts/GSN/GSNRecipient.sol b/contracts/GSN/GSNRecipient.sol index a719ab87f..c69d53bbc 100644 --- a/contracts/GSN/GSNRecipient.sol +++ b/contracts/GSN/GSNRecipient.sol @@ -115,7 +115,7 @@ abstract contract GSNRecipient is IRelayRecipient, Context { /** * @dev See `IRelayRecipient.preRelayedCall`. * - * This function should not be overriden directly, use `_preRelayedCall` instead. + * This function should not be overridden directly, use `_preRelayedCall` instead. * * * Requirements: * @@ -138,7 +138,7 @@ abstract contract GSNRecipient is IRelayRecipient, Context { /** * @dev See `IRelayRecipient.postRelayedCall`. * - * This function should not be overriden directly, use `_postRelayedCall` instead. + * This function should not be overridden directly, use `_postRelayedCall` instead. * * * Requirements: * diff --git a/contracts/GSN/IRelayRecipient.sol b/contracts/GSN/IRelayRecipient.sol index be0ffe56f..4ecaf1e25 100644 --- a/contracts/GSN/IRelayRecipient.sol +++ b/contracts/GSN/IRelayRecipient.sol @@ -53,7 +53,7 @@ interface IRelayRecipient { * * Returns a value to be passed to {postRelayedCall}. * - * {preRelayedCall} is called with 100k gas: if it runs out during exection or otherwise reverts, the relayed call + * {preRelayedCall} is called with 100k gas: if it runs out during execution or otherwise reverts, the relayed call * will not be executed, but the recipient will still be charged for the transaction's cost. */ function preRelayedCall(bytes calldata context) external returns (bytes32); diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index 2e39cbb42..6cb5ca978 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -28,7 +28,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { // Mapping from account to operator approvals mapping (address => mapping(address => bool)) private _operatorApprovals; - // Used as the URI for all token types by relying on ID substition, e.g. https://token-cdn-domain/{id}.json + // Used as the URI for all token types by relying on ID substitution, e.g. https://token-cdn-domain/{id}.json string private _uri; /* @@ -66,7 +66,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * @dev See {IERC1155MetadataURI-uri}. * * This implementation returns the same URI for *all* token types. It relies - * on the token type ID substituion mechanism + * on the token type ID substitution mechanism * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. * * Clients calling this function must replace the `\{id\}` substring with the @@ -208,10 +208,10 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { /** * @dev Sets a new URI for all token types, by relying on the token type ID - * substituion mechanism + * substitution mechanism * https://eips.ethereum.org/EIPS/eip-1155#metadata[defined in the EIP]. * - * By this mechanism, any occurence of the `\{id\}` substring in either the + * By this mechanism, any occurrence of the `\{id\}` substring in either the * URI or any of the amounts in the JSON file at said URI will be replaced by * clients with the token type ID. * diff --git a/contracts/token/ERC1155/IERC1155.sol b/contracts/token/ERC1155/IERC1155.sol index be606beeb..5262661eb 100644 --- a/contracts/token/ERC1155/IERC1155.sol +++ b/contracts/token/ERC1155/IERC1155.sol @@ -12,7 +12,7 @@ import "../../introspection/IERC165.sol"; */ interface IERC1155 is IERC165 { /** - * @dev Emitted when `value` tokens of token type `id` are transfered from `from` to `to` by `operator`. + * @dev Emitted when `value` tokens of token type `id` are transferred from `from` to `to` by `operator`. */ event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 6094c1531..fc04f1dde 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -254,7 +254,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Enumerable * `_data` is additional data, it has no specified format and it is sent in call to `to`. * * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g. - * implement alternative mecanisms to perform token transfer, such as signature-based. + * implement alternative mechanisms to perform token transfer, such as signature-based. * * Requirements: * diff --git a/contracts/token/ERC721/IERC721.sol b/contracts/token/ERC721/IERC721.sol index 16f7aca87..5e7d20e35 100644 --- a/contracts/token/ERC721/IERC721.sol +++ b/contracts/token/ERC721/IERC721.sol @@ -9,7 +9,7 @@ import "../../introspection/IERC165.sol"; */ interface IERC721 is IERC165 { /** - * @dev Emitted when `tokenId` token is transfered from `from` to `to`. + * @dev Emitted when `tokenId` token is transferred from `from` to `to`. */ event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 8995a1858..3773c411f 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -7,7 +7,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t Security tools include: - * {Pausable}: provides a simple way to halt activity in your contracts (often in reponse to an external threat). + * {Pausable}: provides a simple way to halt activity in your contracts (often in response to an external threat). * {ReentrancyGuard}: protects you from https://blog.openzeppelin.com/reentrancy-after-istanbul/[reentrant calls]. The {Address}, {Arrays} and {Strings} libraries provide more operations related to these native data types, while {SafeCast} adds ways to safely convert between the different signed and unsigned numeric types. diff --git a/test/helpers/sign.js b/test/helpers/sign.js index 6cb57f4eb..b4a9710c3 100644 --- a/test/helpers/sign.js +++ b/test/helpers/sign.js @@ -38,7 +38,7 @@ const getSignFor = (contract, signer) => (redeemer, methodName, methodArgs = []) redeemer, ]; - const REAL_SIGNATURE_SIZE = 2 * 65; // 65 bytes in hexadecimal string legnth + const REAL_SIGNATURE_SIZE = 2 * 65; // 65 bytes in hexadecimal string length const PADDED_SIGNATURE_SIZE = 2 * 96; // 96 bytes in hexadecimal string length const DUMMY_SIGNATURE = `0x${web3.utils.padLeft('', REAL_SIGNATURE_SIZE)}`; From 0f08b1d0995e506b12da5bd43884f3fc218f6ed5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2020 15:10:54 -0300 Subject: [PATCH 14/20] Update dependency mocha to v8.1.3 (#2340) Co-authored-by: Renovate Bot --- package-lock.json | 165 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 141 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 427bd830f..7def0c566 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31464,12 +31464,64 @@ "dev": true }, "log-symbols": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", - "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz", + "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==", "dev": true, "requires": { - "chalk": "^2.4.2" + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "long": { @@ -31725,23 +31777,23 @@ } }, "mocha": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.1.tgz", - "integrity": "sha512-p7FuGlYH8t7gaiodlFreseLxEmxTgvyG9RgPHODFPySNhwUehu8NIb0vdSt3WFckSneswZ0Un5typYcWElk7HQ==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.1.3.tgz", + "integrity": "sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw==", "dev": true, "requires": { "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "chokidar": "3.3.1", - "debug": "3.2.6", + "chokidar": "3.4.2", + "debug": "4.1.1", "diff": "4.0.2", - "escape-string-regexp": "1.0.5", - "find-up": "4.1.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", "glob": "7.1.6", "growl": "1.10.5", "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "3.0.0", + "js-yaml": "3.14.0", + "log-symbols": "4.0.0", "minimatch": "3.0.4", "ms": "2.1.2", "object.assign": "4.1.0", @@ -31758,9 +31810,9 @@ }, "dependencies": { "chokidar": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.1.tgz", - "integrity": "sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.2.tgz", + "integrity": "sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A==", "dev": true, "requires": { "anymatch": "~3.1.1", @@ -31770,18 +31822,40 @@ "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", - "readdirp": "~3.3.0" + "readdirp": "~3.4.0" } }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { "ms": "^2.1.1" } }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -31802,12 +31876,55 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "js-yaml": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", + "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, + "p-limit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.0.2.tgz", + "integrity": "sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", @@ -31815,12 +31932,12 @@ "dev": true }, "readdirp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.3.0.tgz", - "integrity": "sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.4.0.tgz", + "integrity": "sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ==", "dev": true, "requires": { - "picomatch": "^2.0.7" + "picomatch": "^2.2.1" } }, "supports-color": { From ded2b0a55c9c13731963ab7b85a70c8e73504bab Mon Sep 17 00:00:00 2001 From: Andrew B Coathup <28278242+abcoathup@users.noreply.github.com> Date: Wed, 2 Sep 2020 04:19:17 +1000 Subject: [PATCH 15/20] Fix minor typos and grammar in docs (#2338) * Fix typos and formatting * Add Solidity release dates: releases-stability --- contracts/GSN/IRelayHub.sol | 4 ++-- contracts/math/SignedSafeMath.sol | 2 +- contracts/presets/ERC721PresetMinterPauserAutoId.sol | 2 +- contracts/token/ERC20/ERC20.sol | 4 ++-- contracts/token/ERC20/README.adoc | 2 +- contracts/token/ERC721/README.adoc | 2 +- contracts/token/ERC777/README.adoc | 2 +- docs/modules/ROOT/pages/access-control.adoc | 2 +- docs/modules/ROOT/pages/index.adoc | 2 +- docs/modules/ROOT/pages/releases-stability.adoc | 4 ++-- 10 files changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/GSN/IRelayHub.sol b/contracts/GSN/IRelayHub.sol index 30b0ab951..7e4be7cfb 100644 --- a/contracts/GSN/IRelayHub.sol +++ b/contracts/GSN/IRelayHub.sol @@ -41,7 +41,7 @@ interface IRelayHub { function registerRelay(uint256 transactionFee, string calldata url) external; /** - * @dev Emitted when a relay is registered or re-registerd. Looking at these events (and filtering out + * @dev Emitted when a relay is registered or re-registered. Looking at these events (and filtering out * {RelayRemoved} events) lets a client discover the list of available relays. */ event RelayAdded(address indexed relay, address indexed owner, uint256 transactionFee, uint256 stake, uint256 unstakeDelay, string url); @@ -105,7 +105,7 @@ interface IRelayHub { event Deposited(address indexed recipient, address indexed from, uint256 amount); /** - * @dev Returns an account's deposits. These can be either a contracts's funds, or a relay owner's revenue. + * @dev Returns an account's deposits. These can be either a contract's funds, or a relay owner's revenue. */ function balanceOf(address target) external view returns (uint256); diff --git a/contracts/math/SignedSafeMath.sol b/contracts/math/SignedSafeMath.sol index a8f51dc3d..ab87c27c9 100644 --- a/contracts/math/SignedSafeMath.sol +++ b/contracts/math/SignedSafeMath.sol @@ -9,7 +9,7 @@ pragma solidity ^0.6.0; library SignedSafeMath { int256 constant private _INT256_MIN = -2**255; - /** + /** * @dev Returns the multiplication of two signed integers, reverting on * overflow. * diff --git a/contracts/presets/ERC721PresetMinterPauserAutoId.sol b/contracts/presets/ERC721PresetMinterPauserAutoId.sol index 9a9ce0dee..18605c7b0 100644 --- a/contracts/presets/ERC721PresetMinterPauserAutoId.sol +++ b/contracts/presets/ERC721PresetMinterPauserAutoId.sol @@ -62,7 +62,7 @@ contract ERC721PresetMinterPauserAutoId is Context, AccessControl, ERC721Burnabl function mint(address to) public virtual { require(hasRole(MINTER_ROLE, _msgSender()), "ERC721PresetMinterPauserAutoId: must have minter role to mint"); - // We can just use balanceOf to create the new tokenId because tokens + // We cannot just use balanceOf to create the new tokenId because tokens // can be burned (destroyed), so we need a separate counter. _mint(to, _tokenIdTracker.current()); _tokenIdTracker.increment(); diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index a8ab97f27..dce6f4593 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -258,9 +258,9 @@ contract ERC20 is Context, IERC20 { } /** - * @dev Sets `amount` as the allowance of `spender` over the `owner`s tokens. + * @dev Sets `amount` as the allowance of `spender` over the `owner` s tokens. * - * This is internal function is equivalent to `approve`, and can be used to + * This internal function is equivalent to `approve`, and can be used to * e.g. set automatic allowances for certain subsystems, etc. * * Emits an {Approval} event. diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index e12e57621..f47f94b94 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -5,7 +5,7 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-20[ERC20 Token Standard]. -TIP: For an overview of ERC20 tokens and a walkthrough on how to create a token contract read our xref:ROOT:erc20.adoc[ERC20 guide]. +TIP: For an overview of ERC20 tokens and a walk through on how to create a token contract read our xref:ROOT:erc20.adoc[ERC20 guide]. There a few core contracts that implement the behavior specified in the EIP: diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index 69191ab30..5f584eee1 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -5,7 +5,7 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-721[ERC721 Non-Fungible Token Standard]. -TIP: For a walkthrough on how to create an ERC721 token read our xref:ROOT:erc721.adoc[ERC721 guide]. +TIP: For a walk through on how to create an ERC721 token read our xref:ROOT:erc721.adoc[ERC721 guide]. The EIP consists of three interfaces, found here as {IERC721}, {IERC721Metadata}, and {IERC721Enumerable}. Only the first one is required in a contract to be ERC721 compliant. However, all three are implemented in {ERC721}. diff --git a/contracts/token/ERC777/README.adoc b/contracts/token/ERC777/README.adoc index e078f86ec..4dee8a661 100644 --- a/contracts/token/ERC777/README.adoc +++ b/contracts/token/ERC777/README.adoc @@ -5,7 +5,7 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ This set of interfaces and contracts are all related to the [ERC777 token standard](https://eips.ethereum.org/EIPS/eip-777). -TIP: For an overview of ERC777 tokens and a walkthrough on how to create a token contract read our xref:ROOT:erc777.adoc[ERC777 guide]. +TIP: For an overview of ERC777 tokens and a walk through on how to create a token contract read our xref:ROOT:erc777.adoc[ERC777 guide]. The token behavior itself is implemented in the core contracts: {IERC777}, {ERC777}. diff --git a/docs/modules/ROOT/pages/access-control.adoc b/docs/modules/ROOT/pages/access-control.adoc index 20ac4cf03..acb0b6765 100644 --- a/docs/modules/ROOT/pages/access-control.adoc +++ b/docs/modules/ROOT/pages/access-control.adoc @@ -131,7 +131,7 @@ By default, **accounts with a role cannot grant it or revoke it from other accou Every role has an associated admin role, which grants permission to call the `grantRole` and `revokeRole` functions. A role can be granted or revoked by using these if the calling account has the corresponding admin role. Multiple roles may have the same admin role to make management easier. A role's admin can even be the same role itself, which would cause accounts with that role to be able to also grant and revoke it. -This mechanism can be used to create complex permissioning structures resembling organizational charts, but it also provides an easy way to manage simpler applications. `AccessControl` includes a special role, called `DEFAULT_ADMIN_ROLE`, which acts as the **default admin role for all roles**. An account with this role will be able to manage any other role, unless `\_setRoleAdmin` is used to select a new admin role. +This mechanism can be used to create complex permissioning structures resembling organizational charts, but it also provides an easy way to manage simpler applications. `AccessControl` includes a special role, called `DEFAULT_ADMIN_ROLE`, which acts as the **default admin role for all roles**. An account with this role will be able to manage any other role, unless `_setRoleAdmin` is used to select a new admin role. Let's take a look at the ERC20 token example, this time taking advantage of the default admin role: diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index b43cbe086..8c76b298e 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -52,7 +52,7 @@ The guides in the sidebar will teach about different concepts, and how to use th * xref:gsn.adoc[Gas Station Network]: let your users interact with your contracts without having to pay for gas themselves. * xref:utilities.adoc[Utilities]: generic useful tools, including non-overflowing math, signature verification, and trustless paying systems. -The xref:api:token/ERC20.adoc[full API] is also thoroughly documented, and serves as a great reference when developing your smart contract application. You can also ask for help or follow Contracts's development in the https://forum.openzeppelin.com[community forum]. +The xref:api:token/ERC20.adoc[full API] is also thoroughly documented, and serves as a great reference when developing your smart contract application. You can also ask for help or follow Contracts' development in the https://forum.openzeppelin.com[community forum]. Finally, you may want to take a look at the https://blog.openzeppelin.com/guides/[guides on our blog], which cover several common use cases and good practices.. The following articles provide great background reading, though please note, some of the referenced tools have changed as the tooling in the ecosystem continues to rapidly evolve. diff --git a/docs/modules/ROOT/pages/releases-stability.adoc b/docs/modules/ROOT/pages/releases-stability.adoc index b12cdb974..5708c193f 100644 --- a/docs/modules/ROOT/pages/releases-stability.adoc +++ b/docs/modules/ROOT/pages/releases-stability.adoc @@ -69,12 +69,12 @@ While attempts will generally be made to lower the gas costs of working with Ope [[bugfixes]] === Bug Fixes -The API stability guarantees may need to be broken in order to fix a bug, and we will do so. This decision won't be made lightly however, and all options will be explored to make the change as non-disruptive as possible. When sufficient, contracts or functions which may result in unsafe behaviour will be deprecated instead of removed (e.g. https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1543[#1543] and https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1550[#1550]). +The API stability guarantees may need to be broken in order to fix a bug, and we will do so. This decision won't be made lightly however, and all options will be explored to make the change as non-disruptive as possible. When sufficient, contracts or functions which may result in unsafe behavior will be deprecated instead of removed (e.g. https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1543[#1543] and https://github.com/OpenZeppelin/openzeppelin-contracts/pull/1550[#1550]). [[solidity-compiler-version]] === Solidity Compiler Version -Starting on version 0.5.0, the Solidity team switched to a faster release cycle, with minor releases every few weeks (v0.5.0 was released on November 2018, and v0.5.5 on March 2019), and major, breaking-change releases every couple months (with v0.6.0 scheduled for late March 2019). Including the compiler version in OpenZeppelin Contract's stability guarantees would therefore force the library to either stick to old compilers, or release frequent major updates simply to keep up with newer Solidity releases. +Starting on version 0.5.0, the Solidity team switched to a faster release cycle, with minor releases every few weeks (v0.5.0 was released on November 2018, and v0.5.5 on March 2019), and major, breaking-change releases every couple of months (with v0.6.0 released on December 2019 and v0.7.0 on July 2020). Including the compiler version in OpenZeppelin Contract's stability guarantees would therefore force the library to either stick to old compilers, or release frequent major updates simply to keep up with newer Solidity releases. Because of this, *the minimum required Solidity compiler version is not part of the stability guarantees*, and users may be required to upgrade their compiler when using newer versions of Contracts. Bug fixes will still be backported to older library releases so that all versions currently in use receive these updates. From 885b76f66f8660bed46930011d606c1a98921c94 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Tue, 1 Sep 2020 18:48:43 -0300 Subject: [PATCH 16/20] Fix AsciiDoc missing attribute references --- docs/modules/ROOT/pages/erc1155.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/ROOT/pages/erc1155.adoc b/docs/modules/ROOT/pages/erc1155.adoc index f00dd499b..414237b07 100644 --- a/docs/modules/ROOT/pages/erc1155.adoc +++ b/docs/modules/ROOT/pages/erc1155.adoc @@ -94,9 +94,9 @@ The metadata uri can be obtained: "https://game.example/api/item/{id}.json" ---- -The `uri` can include the string `{id}` which clients must replace with the actual token ID, in lowercase hexadecimal (with no 0x prefix) and leading zero padded to 64 hex characters. +The `uri` can include the string `++{id}++` which clients must replace with the actual token ID, in lowercase hexadecimal (with no 0x prefix) and leading zero padded to 64 hex characters. -For token ID `2` and uri `https://game.example/api/item/{id}.json` clients would replace `{id}` with `0000000000000000000000000000000000000000000000000000000000000002` to retrieve JSON at `https://game.example/api/item/0000000000000000000000000000000000000000000000000000000000000002.json`. +For token ID `2` and uri `++https://game.example/api/item/{id}.json++` clients would replace `++{id}++` with `0000000000000000000000000000000000000000000000000000000000000002` to retrieve JSON at `https://game.example/api/item/0000000000000000000000000000000000000000000000000000000000000002.json`. The JSON document for token ID 2 might look something like: From 6bc2ae3731b7130dfd8a87e7e6de8bc985e88531 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Wed, 2 Sep 2020 20:19:54 -0300 Subject: [PATCH 17/20] Add documentation for proxies (#2344) --- .editorconfig | 5 +- contracts/proxy/Initializable.sol | 22 ++--- contracts/proxy/Proxy.sol | 79 ++++++++-------- contracts/proxy/ProxyAdmin.sol | 49 +++++----- contracts/proxy/README.adoc | 26 ++++++ .../proxy/TransparentUpgradeableProxy.sol | 90 +++++++++++-------- contracts/proxy/UpgradeableProxy.sol | 33 ++++--- 7 files changed, 179 insertions(+), 125 deletions(-) create mode 100644 contracts/proxy/README.adoc diff --git a/.editorconfig b/.editorconfig index 4a1b689c3..9e0ee91b0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,7 +8,7 @@ charset = utf-8 end_of_line = lf indent_style = space insert_final_newline = true -trim_trailing_whitespace = true +trim_trailing_whitespace = false max_line_length = 120 [*.sol] @@ -16,3 +16,6 @@ indent_size = 4 [*.js] indent_size = 2 + +[*.adoc] +max_line_length = 0 diff --git a/contracts/proxy/Initializable.sol b/contracts/proxy/Initializable.sol index 449a62540..6956ac278 100644 --- a/contracts/proxy/Initializable.sol +++ b/contracts/proxy/Initializable.sol @@ -4,16 +4,16 @@ pragma solidity >=0.4.24 <0.7.0; /** - * @title Initializable - * - * @dev Helper contract to support initializer functions. To use it, replace - * the constructor with a function that has the `initializer` modifier. - * WARNING: Unlike constructors, initializer functions must be manually - * invoked. This applies both to deploying an Initializable contract, as well - * as extending an Initializable contract via inheritance. - * WARNING: When used with inheritance, manual care must be taken to not invoke - * a parent initializer twice, or ensure that all initializers are idempotent, - * because this is not dealt with automatically as with constructors. + * @dev This is a base contract to aid in writing upgradeable contracts, or any kind of contract that will be deployed + * behind a proxy. Since a proxied contract can't have a constructor, it's common to move constructor logic to an + * external initializer function, usually called `initialize`. It then becomes necessary to protect this initializer + * function so it can only be called once. The {initializer} modifier provided by this contract will have this effect. + * + * TIP: To avoid leaving the proxy in an uninitialized state, the initializer function should be called as early as + * possible by providing the encoded function call as the `_data` argument to {UpgradeableProxy-constructor}. + * + * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure + * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. */ contract Initializable { @@ -28,7 +28,7 @@ contract Initializable { bool private _initializing; /** - * @dev Modifier to use in the initializer function of a contract. + * @dev Modifier to protect an initializer function from being invoked twice. */ modifier initializer() { require(_initializing || _isConstructor() || !_initialized, "Initializable: contract is already initialized"); diff --git a/contracts/proxy/Proxy.sol b/contracts/proxy/Proxy.sol index e68622f6d..69230a5b6 100644 --- a/contracts/proxy/Proxy.sol +++ b/contracts/proxy/Proxy.sol @@ -3,39 +3,20 @@ pragma solidity ^0.6.0; /** - * @title Proxy - * @dev Implements delegation of calls to other contracts, with proper - * forwarding of return values and bubbling of failures. - * It defines a fallback function that delegates all calls to the address - * returned by the abstract _implementation() internal function. + * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM + * instruction `delegatecall`. We refer to the second contract as the _implementation_ behind the proxy, and it has to + * be specified by overriding the virtual {_implementation} function. + * + * Additionally, delegation to the implementation can be triggered manually through the {_fallback} function, or to a + * different contract through the {_delegate} function. + * + * The success and return data of the delegated call will be returned back to the caller of the proxy. */ abstract contract Proxy { /** - * @dev Fallback function. - * Implemented entirely in `_fallback`. - */ - fallback () payable external { - _fallback(); - } - - /** - * @dev Receive function. - * Implemented entirely in `_fallback`. - */ - receive () payable external { - _fallback(); - } - - /** - * @return The Address of the implementation. - */ - function _implementation() internal virtual view returns (address); - - /** - * @dev Delegates execution to an implementation contract. - * This is a low level function that doesn't return to its internal call site. - * It will return to the external caller whatever the implementation returns. - * @param implementation Address to delegate. + * @dev Delegates the current call to `implementation`. + * + * This function does not return to its internall call site, it will return directly to the external caller. */ function _delegate(address implementation) internal { // solhint-disable-next-line no-inline-assembly @@ -60,19 +41,43 @@ abstract contract Proxy { } /** - * @dev Function that is run as the first thing in the fallback function. - * Can be redefined in derived contracts to add functionality. - * Redefinitions must call super._willFallback(). + * @dev This is a virtual function that should be overriden so it returns the address to which the fallback function + * and {_fallback} should delegate. */ - function _willFallback() internal virtual { - } + function _implementation() internal virtual view returns (address); /** - * @dev fallback implementation. - * Extracted to enable manual triggering. + * @dev Delegates the current call to the address returned by `_implementation()`. + * + * This function does not return to its internall call site, it will return directly to the external caller. */ function _fallback() internal { _willFallback(); _delegate(_implementation()); } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if no other + * function in the contract matches the call data. + */ + fallback () payable external { + _fallback(); + } + + /** + * @dev Fallback function that delegates calls to the address returned by `_implementation()`. Will run if call data + * is empty. + */ + receive () payable external { + _fallback(); + } + + /** + * @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback` + * call, or as part of the Solidity `fallback` or `receive` functions. + * + * If overriden should call `super._willFallback()`. + */ + function _willFallback() internal virtual { + } } diff --git a/contracts/proxy/ProxyAdmin.sol b/contracts/proxy/ProxyAdmin.sol index 35ce8298e..26af407f4 100644 --- a/contracts/proxy/ProxyAdmin.sol +++ b/contracts/proxy/ProxyAdmin.sol @@ -6,16 +6,17 @@ import "../access/Ownable.sol"; import "./TransparentUpgradeableProxy.sol"; /** - * @title ProxyAdmin - * @dev This contract is the admin of a proxy, and is in charge - * of upgrading it as well as transferring it to another admin. + * @dev This is an auxiliary contract meant to be assigned as the admin of a {TransparentUpgradeableProxy}. For an + * explanation of why you would want to use this see the documentation for {TransparentUpgradeableProxy}. */ contract ProxyAdmin is Ownable { /** - * @dev Returns the current implementation of a proxy. - * This is needed because only the proxy admin can query it. - * @return The address of the current implementation of the proxy. + * @dev Returns the current implementation of `proxy`. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. */ function getProxyImplementation(TransparentUpgradeableProxy proxy) public view returns (address) { // We need to manually run the static call since the getter cannot be flagged as view @@ -26,8 +27,11 @@ contract ProxyAdmin is Ownable { } /** - * @dev Returns the admin of a proxy. Only the admin can query it. - * @return The address of the current admin of the proxy. + * @dev Returns the current admin of `proxy`. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. */ function getProxyAdmin(TransparentUpgradeableProxy proxy) public view returns (address) { // We need to manually run the static call since the getter cannot be flagged as view @@ -38,31 +42,34 @@ contract ProxyAdmin is Ownable { } /** - * @dev Changes the admin of a proxy. - * @param proxy Proxy to change admin. - * @param newAdmin Address to transfer proxy administration to. + * @dev Changes the admin of `proxy` to `newAdmin`. + * + * Requirements: + * + * - This contract must be the current admin of `proxy`. */ function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public onlyOwner { proxy.changeAdmin(newAdmin); } /** - * @dev Upgrades a proxy to the newest implementation of a contract. - * @param proxy Proxy to be upgraded. - * @param implementation the address of the Implementation. + * @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. */ function upgrade(TransparentUpgradeableProxy proxy, address implementation) public onlyOwner { proxy.upgradeTo(implementation); } /** - * @dev Upgrades a proxy to the newest implementation of a contract and forwards a function call to it. - * This is useful to initialize the proxied contract. - * @param proxy Proxy to be upgraded. - * @param implementation Address of the Implementation. - * @param data Data to send as msg.data in the low level call. - * It should include the signature and the parameters of the function to be called, as described in - * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + * @dev Upgrades `proxy` to `implementation` and calls a function on the new implementation. See + * {TransparentUpgradeableProxy-upgradeToAndCall}. + * + * Requirements: + * + * - This contract must be the admin of `proxy`. */ function upgradeAndCall(TransparentUpgradeableProxy proxy, address implementation, bytes memory data) public payable onlyOwner { proxy.upgradeToAndCall{value: msg.value}(implementation, data); diff --git a/contracts/proxy/README.adoc b/contracts/proxy/README.adoc new file mode 100644 index 000000000..68d55854e --- /dev/null +++ b/contracts/proxy/README.adoc @@ -0,0 +1,26 @@ += Proxies + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/proxy + +This is a low-level set of contracts implementing the proxy pattern for upgradeability. For an in-depth overview of this pattern check out the xref:upgrades-plugins::proxies.adoc[Proxy Upgrade Pattern] page. + +The abstract {Proxy} contract implements the core delegation functionality. If the concrete proxies that we provide below are not suitable, we encourage building on top of this base contract since it contains an assembly block that may be hard to get right. + +Upgradeability is implemented in the {UpgradeableProxy} contract, although it provides only an internal upgrade interface. For an upgrade interface exposed externally to an admin, we provide {TransparentUpgradeableProxy}. Both of these contracts use the storage slots specified in https://eips.ethereum.org/EIPS/eip-1967[EIP1967] to avoid clashes with the storage of the implementation contract behind the proxy. + +CAUTION: Using upgradeable proxies correctly and securely is a difficult task that requires deep knowledge of the proxy pattern, Solidity, and the EVM. Unless you want a lot of low level control, we recommend using the xref:upgrades-plugins::index.adoc[OpenZeppelin Upgrades Plugins] for Truffle and Buidler. + +== Core + +{{Proxy}} + +{{UpgradeableProxy}} + +{{TransparentUpgradeableProxy}} + +== Utilities + +{{Initializable}} + +{{ProxyAdmin}} diff --git a/contracts/proxy/TransparentUpgradeableProxy.sol b/contracts/proxy/TransparentUpgradeableProxy.sol index fbd431f69..55c1d9263 100644 --- a/contracts/proxy/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/TransparentUpgradeableProxy.sol @@ -5,22 +5,30 @@ pragma solidity ^0.6.0; import "./UpgradeableProxy.sol"; /** - * @title TransparentUpgradeableProxy - * @dev This contract combines an upgradeability proxy with an authorization - * mechanism for administrative tasks. - * All external functions in this contract must be guarded by the - * `ifAdmin` modifier. See ethereum/solidity#3864 for a Solidity - * feature proposal that would enable this to be done automatically. + * @dev This contract implements a proxy that is upgradeable by an admin. + * + * To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector + * clashing], which can potentially be used in an attack, this contract uses the + * https://blog.openzeppelin.com/the-transparent-proxy-pattern/[transparent proxy pattern]. This pattern implies two + * things that go hand in hand: + * + * 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if + * that call matches one of the admin functions exposed by the proxy itself. + * 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the + * implementation. If the admin tries to call a function on the implementation it will fail with an error that says + * "admin cannot fallback to proxy target". + * + * These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing + * the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due + * to sudden errors when trying to call a function from the proxy implementation. + * + * Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way, + * you should think of the `ProxyAdmin` instance as the real administrative inerface of your proxy. */ contract TransparentUpgradeableProxy is UpgradeableProxy { /** - * Contract constructor. - * @param _logic address of the initial implementation. - * @param _admin Address of the proxy administrator. - * @param _data Data to send as msg.data to the implementation to initialize the proxied contract. - * It should include the signature and the parameters of the function to be called, as described in - * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. - * This parameter is optional, if no data is given the initialization call to proxied contract will be skipped. + * @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and + * optionally initialized with `_data` as explained in {UpgradeableProxy-constructor}. */ constructor(address _logic, address _admin, bytes memory _data) public payable UpgradeableProxy(_logic, _data) { assert(_ADMIN_SLOT == bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)); @@ -28,9 +36,7 @@ contract TransparentUpgradeableProxy is UpgradeableProxy { } /** - * @dev Emitted when the administration has been transferred. - * @param previousAdmin Address of the previous admin. - * @param newAdmin Address of the new admin. + * @dev Emitted when the admin account has changed. */ event AdminChanged(address previousAdmin, address newAdmin); @@ -39,13 +45,10 @@ contract TransparentUpgradeableProxy is UpgradeableProxy { * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is * validated in the constructor. */ - bytes32 private constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; /** - * @dev Modifier to check whether the `msg.sender` is the admin. - * If it is, it will run the function. Otherwise, it will delegate the call - * to the implementation. + * @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin. */ modifier ifAdmin() { if (msg.sender == _admin()) { @@ -56,14 +59,26 @@ contract TransparentUpgradeableProxy is UpgradeableProxy { } /** - * @return The address of the proxy admin. + * @dev Returns the current admin. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyAdmin}. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` */ function admin() external ifAdmin returns (address) { return _admin(); } /** - * @return The address of the implementation. + * @dev Returns the current implementation. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-getProxyImplementation}. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` */ function implementation() external ifAdmin returns (address) { return _implementation(); @@ -71,8 +86,10 @@ contract TransparentUpgradeableProxy is UpgradeableProxy { /** * @dev Changes the admin of the proxy. - * Only the current admin can call this function. - * @param newAdmin Address to transfer proxy administration to. + * + * Emits an {AdminChanged} event. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}. */ function changeAdmin(address newAdmin) external ifAdmin { require(newAdmin != address(0), "TransparentUpgradeableProxy: new admin is the zero address"); @@ -81,22 +98,20 @@ contract TransparentUpgradeableProxy is UpgradeableProxy { } /** - * @dev Upgrade the backing implementation of the proxy. - * Only the admin can call this function. - * @param newImplementation Address of the new implementation. + * @dev Upgrade the implementation of the proxy. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}. */ function upgradeTo(address newImplementation) external ifAdmin { _upgradeTo(newImplementation); } /** - * @dev Upgrade the backing implementation of the proxy and call a function - * on the new implementation. - * This is useful to initialize the proxied contract. - * @param newImplementation Address of the new implementation. - * @param data Data to send as msg.data in the low level call. - * It should include the signature and the parameters of the function to be called, as described in - * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. + * @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified + * by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the + * proxied contract. + * + * NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}. */ function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin { _upgradeTo(newImplementation); @@ -106,7 +121,7 @@ contract TransparentUpgradeableProxy is UpgradeableProxy { } /** - * @return adm The admin slot. + * @dev Returns the current admin. */ function _admin() internal view returns (address adm) { bytes32 slot = _ADMIN_SLOT; @@ -117,8 +132,7 @@ contract TransparentUpgradeableProxy is UpgradeableProxy { } /** - * @dev Sets the address of the proxy admin. - * @param newAdmin Address of the new proxy admin. + * @dev Stores a new address in the EIP1967 admin slot. */ function _setAdmin(address newAdmin) internal { bytes32 slot = _ADMIN_SLOT; @@ -130,7 +144,7 @@ contract TransparentUpgradeableProxy is UpgradeableProxy { } /** - * @dev Only fallback when the sender is not the admin. + * @dev Makes sure the admin cannot access the fallback function. See {Proxy-_willFallback}. */ function _willFallback() internal override virtual { require(msg.sender != _admin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); diff --git a/contracts/proxy/UpgradeableProxy.sol b/contracts/proxy/UpgradeableProxy.sol index 8b120c069..2ee8faf7f 100644 --- a/contracts/proxy/UpgradeableProxy.sol +++ b/contracts/proxy/UpgradeableProxy.sol @@ -6,19 +6,20 @@ import "./Proxy.sol"; import "../utils/Address.sol"; /** - * @title UpgradeableProxy - * @dev This contract implements a proxy that allows to change the - * implementation address to which it will delegate. - * Such a change is called an implementation upgrade. + * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an + * implementation address that can be changed. This address is stored in storage in the location specified by + * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the + * implementation behind the proxy. + * + * Upgradeability is only provided internally through {_upgradeTo}. For an externally upgradeable proxy see + * {TransparentUpgradeableProxy}. */ contract UpgradeableProxy is Proxy { /** - * @dev Contract constructor. - * @param _logic Address of the initial implementation. - * @param _data Data to send as msg.data to the implementation to initialize the proxied contract. - * It should include the signature and the parameters of the function to be called, as described in - * https://solidity.readthedocs.io/en/v0.4.24/abi-spec.html#function-selector-and-argument-encoding. - * This parameter is optional, if no data is given the initialization call to proxied contract will be skipped. + * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`. + * + * If `_data` is nonempty, it's used as data in a delegate call to `_logic`. This will typically be an encoded + * function call, and allows initializating the storage of the proxy like a Solidity constructor. */ constructor(address _logic, bytes memory _data) public payable { assert(_IMPLEMENTATION_SLOT == bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)); @@ -28,11 +29,10 @@ contract UpgradeableProxy is Proxy { (bool success,) = _logic.delegatecall(_data); require(success); } - } + } /** * @dev Emitted when the implementation is upgraded. - * @param implementation Address of the new implementation. */ event Upgraded(address indexed implementation); @@ -44,8 +44,7 @@ contract UpgradeableProxy is Proxy { bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; /** - * @dev Returns the current implementation. - * @return impl Address of the current implementation + * @dev Returns the current implementation address. */ function _implementation() internal override view returns (address impl) { bytes32 slot = _IMPLEMENTATION_SLOT; @@ -57,7 +56,8 @@ contract UpgradeableProxy is Proxy { /** * @dev Upgrades the proxy to a new implementation. - * @param newImplementation Address of the new implementation. + * + * Emits an {Upgraded} event. */ function _upgradeTo(address newImplementation) internal { _setImplementation(newImplementation); @@ -65,8 +65,7 @@ contract UpgradeableProxy is Proxy { } /** - * @dev Sets the implementation address of the proxy. - * @param newImplementation Address of the new implementation. + * @dev Stores a new address in the EIP1967 implementation slot. */ function _setImplementation(address newImplementation) internal { require(Address.isContract(newImplementation), "UpgradeableProxy: new implementation is not a contract"); From 91f16a7e4751c1dadee93e1212e09baa868fa190 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 3 Sep 2020 13:49:47 -0300 Subject: [PATCH 18/20] Adapt proxies to Contracts conventions (#2345) --- contracts/proxy/Initializable.sol | 2 +- contracts/proxy/Proxy.sol | 6 +++--- contracts/proxy/TransparentUpgradeableProxy.sol | 8 ++++---- contracts/proxy/UpgradeableProxy.sol | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contracts/proxy/Initializable.sol b/contracts/proxy/Initializable.sol index 6956ac278..75a28a1f1 100644 --- a/contracts/proxy/Initializable.sol +++ b/contracts/proxy/Initializable.sol @@ -15,7 +15,7 @@ pragma solidity >=0.4.24 <0.7.0; * CAUTION: When used with inheritance, manual care must be taken to not invoke a parent initializer twice, or to ensure * that all initializers are idempotent. This is not verified automatically as constructors are by Solidity. */ -contract Initializable { +abstract contract Initializable { /** * @dev Indicates that the contract has been initialized. diff --git a/contracts/proxy/Proxy.sol b/contracts/proxy/Proxy.sol index 69230a5b6..6b618af79 100644 --- a/contracts/proxy/Proxy.sol +++ b/contracts/proxy/Proxy.sol @@ -52,7 +52,7 @@ abstract contract Proxy { * This function does not return to its internall call site, it will return directly to the external caller. */ function _fallback() internal { - _willFallback(); + _beforeFallback(); _delegate(_implementation()); } @@ -76,8 +76,8 @@ abstract contract Proxy { * @dev Hook that is called before falling back to the implementation. Can happen as part of a manual `_fallback` * call, or as part of the Solidity `fallback` or `receive` functions. * - * If overriden should call `super._willFallback()`. + * If overriden should call `super._beforeFallback()`. */ - function _willFallback() internal virtual { + function _beforeFallback() internal virtual { } } diff --git a/contracts/proxy/TransparentUpgradeableProxy.sol b/contracts/proxy/TransparentUpgradeableProxy.sol index 55c1d9263..363c2586d 100644 --- a/contracts/proxy/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/TransparentUpgradeableProxy.sol @@ -134,7 +134,7 @@ contract TransparentUpgradeableProxy is UpgradeableProxy { /** * @dev Stores a new address in the EIP1967 admin slot. */ - function _setAdmin(address newAdmin) internal { + function _setAdmin(address newAdmin) private { bytes32 slot = _ADMIN_SLOT; // solhint-disable-next-line no-inline-assembly @@ -144,10 +144,10 @@ contract TransparentUpgradeableProxy is UpgradeableProxy { } /** - * @dev Makes sure the admin cannot access the fallback function. See {Proxy-_willFallback}. + * @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}. */ - function _willFallback() internal override virtual { + function _beforeFallback() internal override virtual { require(msg.sender != _admin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); - super._willFallback(); + super._beforeFallback(); } } diff --git a/contracts/proxy/UpgradeableProxy.sol b/contracts/proxy/UpgradeableProxy.sol index 2ee8faf7f..e031d4121 100644 --- a/contracts/proxy/UpgradeableProxy.sol +++ b/contracts/proxy/UpgradeableProxy.sol @@ -67,7 +67,7 @@ contract UpgradeableProxy is Proxy { /** * @dev Stores a new address in the EIP1967 implementation slot. */ - function _setImplementation(address newImplementation) internal { + function _setImplementation(address newImplementation) private { require(Address.isContract(newImplementation), "UpgradeableProxy: new implementation is not a contract"); bytes32 slot = _IMPLEMENTATION_SLOT; From 9f900f6dbad9f254819453438fb2628fda4e9072 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 3 Sep 2020 14:13:11 -0300 Subject: [PATCH 19/20] Add changelog entries for proxies and ERC20Snapshot --- CHANGELOG.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d479eb28e..a5068de3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,19 @@ # Changelog -## 3.1.1 (Unreleased) +## 3.2.0 (unreleased) + +### New features + * Proxies: added the proxy contracts from OpenZeppelin SDK. ([#2335](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2335)) ### Improvements * `Address.isContract`: switched from `extcodehash` to `extcodesize` for less gas usage. ([#2311](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2311)) +### Breaking changes + * `ERC20Snapshot`: switched to using `_beforeTokenTransfer` hook instead of overriding ERC20 operations. ([#2312](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/2312)) + +This small change in the way we implemented `ERC20Snapshot` may affect users who are combining this contract with +other ERC20 flavors, since it no longer overrides `_transfer`, `_mint`, and `_burn`. This can result in having to remove Solidity `override(...)` specifiers in derived contracts for these functions, and to instead have to add it for `_beforeTokenTransfer`. See [Using Hooks](https://docs.openzeppelin.com/contracts/3.x/extending-contracts#using-hooks) in the documentation. + ## 3.1.0 (2020-06-23) ### New features From f2fb8cf23b5f2b868f986ad91369e5004842c274 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Thu, 3 Sep 2020 16:26:55 -0300 Subject: [PATCH 20/20] 3.2.0-rc.0 --- contracts/package.json | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/package.json b/contracts/package.json index 0592d6a2d..e50a5eb2f 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@openzeppelin/contracts", - "version": "3.1.0", + "version": "3.2.0-rc.0", "description": "Secure Smart Contract library for Solidity", "files": [ "**/*.sol", diff --git a/package-lock.json b/package-lock.json index 7def0c566..a9c4f9454 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "openzeppelin-solidity", - "version": "3.1.0", + "version": "3.2.0-rc.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 03981ff5e..24743980e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openzeppelin-solidity", - "version": "3.1.0", + "version": "3.2.0-rc.0", "description": "Secure Smart Contract library for Solidity", "files": [ "/contracts/**/*.sol",