Browse Source

Merge branch 'OpenZeppelin:master' into certora/erc1155ext

teryanarmen 3 years ago
parent
commit
be18334b69
47 changed files with 2044 additions and 581 deletions
  1. 20 0
      .github/actions/setup/action.yml
  2. 54 0
      .github/workflows/checks.yml
  3. 2 11
      .github/workflows/docs.yml
  4. 0 28
      .github/workflows/slither.yml
  5. 0 57
      .github/workflows/test.yml
  6. 11 0
      CHANGELOG.md
  7. 2 2
      contracts/crosschain/README.adoc
  8. 19 4
      contracts/finance/PaymentSplitter.sol
  9. 239 0
      contracts/interfaces/IERC4626.sol
  10. 4 0
      contracts/mocks/CheckpointsImpl.sol
  11. 22 0
      contracts/mocks/ERC20TokenizedVaultMock.sol
  12. 20 0
      contracts/mocks/InitializableMock.sol
  13. 9 0
      contracts/mocks/MathMock.sol
  14. 20 8
      contracts/mocks/MerkleProofWrapper.sol
  15. 15 30
      contracts/proxy/utils/Initializable.sol
  16. 2 2
      contracts/token/ERC1155/ERC1155.sol
  17. 2 2
      contracts/token/ERC1155/extensions/ERC1155Burnable.sol
  18. 3 0
      contracts/token/ERC20/README.adoc
  19. 217 0
      contracts/token/ERC20/extensions/ERC20TokenizedVault.sol
  20. 13 7
      contracts/token/ERC721/ERC721.sol
  21. 1 1
      contracts/token/ERC721/extensions/ERC721Burnable.sol
  22. 1 1
      contracts/token/ERC721/extensions/ERC721URIStorage.sol
  23. 53 24
      contracts/utils/cryptography/MerkleProof.sol
  24. 110 1
      contracts/utils/math/Math.sol
  25. 299 294
      package-lock.json
  26. 2 2
      package.json
  27. 4 0
      scripts/checks/inheritance-ordering.js
  28. 10 0
      scripts/prepare.sh
  29. 5 0
      test/crosschain/CrossChainEnabled.test.js
  30. 27 6
      test/finance/PaymentSplitter.test.js
  31. 10 0
      test/governance/compatibility/GovernorCompatibilityBravo.test.js
  32. 5 0
      test/helpers/enums.js
  33. 40 0
      test/helpers/txpool.js
  34. 16 0
      test/proxy/utils/Initializable.test.js
  35. 2 2
      test/token/ERC1155/ERC1155.behavior.js
  36. 2 2
      test/token/ERC1155/extensions/ERC1155Burnable.test.js
  37. 612 0
      test/token/ERC20/extensions/ERC20TokenizedVault.test.js
  38. 1 34
      test/token/ERC20/extensions/ERC20Votes.test.js
  39. 1 34
      test/token/ERC20/extensions/ERC20VotesComp.test.js
  40. 13 13
      test/token/ERC721/ERC721.behavior.js
  41. 3 3
      test/token/ERC721/extensions/ERC721Burnable.test.js
  42. 3 3
      test/token/ERC721/extensions/ERC721URIStorage.test.js
  43. 1 1
      test/token/common/ERC2981.behavior.js
  44. 4 0
      test/utils/Base64.test.js
  45. 15 0
      test/utils/Checkpoints.test.js
  46. 32 8
      test/utils/cryptography/MerkleProof.test.js
  47. 98 1
      test/utils/math/Math.test.js

+ 20 - 0
.github/actions/setup/action.yml

@@ -0,0 +1,20 @@
+name: Setup
+
+runs:
+  using: composite
+  steps:
+    - uses: actions/setup-node@v3
+      with:
+        node-version: 14.x
+        cache: npm
+    - uses: actions/cache@v3
+      id: cache
+      with:
+        path: '**/node_modules'
+        key: npm-v3-${{ hashFiles('**/package-lock.json') }}
+    - name: Install dependencies
+      run: npm ci --prefer-offline
+      shell: bash
+      if: steps.cache.outputs.cache-hit != 'true'
+      env:
+        SKIP_COMPILE: true

+ 54 - 0
.github/workflows/checks.yml

@@ -0,0 +1,54 @@
+name: checks
+
+on:
+  push:
+    branches:
+      - master
+      - release-v*
+  pull_request: {}
+  workflow_dispatch: {}
+
+concurrency:
+  group: checks-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Set up environment
+        uses: ./.github/actions/setup
+      - run: npm run lint
+
+  tests:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Set up environment
+        uses: ./.github/actions/setup
+      - run: npm run test
+        env:
+          FORCE_COLOR: 1
+          ENABLE_GAS_REPORT: true
+      - run: npm run test:inheritance
+      - run: npm run test:generation
+
+  coverage:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Set up environment
+        uses: ./.github/actions/setup
+      - run: npm run coverage
+        env:
+          NODE_OPTIONS: --max_old_space_size=4096
+      - uses: codecov/codecov-action@v3
+
+  slither:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - name: Set up environment
+        uses: ./.github/actions/setup
+      - uses: crytic/slither-action@v0.1.1

+ 2 - 11
.github/workflows/docs.yml

@@ -9,17 +9,8 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
-      - uses: actions/setup-node@v3
-        with:
-          node-version: 12.x
-      - uses: actions/cache@v3
-        id: cache
-        with:
-          path: '**/node_modules'
-          key: npm-v2-${{ hashFiles('**/package-lock.json') }}
-          restore-keys: npm-v2-
-      - run: npm ci
-        if: steps.cache.outputs.cache-hit != 'true'
+      - name: Set up environment
+        uses: ./.github/actions/setup
       - run: bash scripts/git-user-config.sh
       - run: node scripts/update-docs-branch.js
       - run: git push --all origin 

+ 0 - 28
.github/workflows/slither.yml

@@ -1,28 +0,0 @@
-name: Slither Analysis
-on:
-  push:
-    branches:
-      - master
-      - release-v*
-  pull_request: {}
-  workflow_dispatch: {}
-
-jobs:
-  analyze:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-node@v3
-        with:
-          node-version: 12.x
-      - uses: actions/cache@v3
-        id: cache
-        with:
-          path: '**/node_modules'
-          key: npm-v2-${{ hashFiles('**/package-lock.json') }}
-          restore-keys: npm-v2-
-      - run: npm ci
-        if: steps.cache.outputs.cache-hit != 'true'
-      - name: Clean project         
-        run: npm run clean
-      - uses: crytic/slither-action@v0.1.1

+ 0 - 57
.github/workflows/test.yml

@@ -1,57 +0,0 @@
-name: Test
-
-on:
-  push:
-    branches:
-      - master
-      - release-v*
-  pull_request: {}
-  workflow_dispatch: {}
-
-jobs:
-  test:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-      - uses: actions/setup-node@v3
-        with:
-          node-version: 12.x
-      - uses: actions/cache@v3
-        id: cache
-        with:
-          path: '**/node_modules'
-          key: npm-v2-${{ hashFiles('**/package-lock.json') }}
-          restore-keys: npm-v2-
-      - run: npm ci
-        if: steps.cache.outputs.cache-hit != 'true'
-      - run: npm run lint
-      - run: npm run test
-        env:
-          FORCE_COLOR: 1
-          ENABLE_GAS_REPORT: true
-      - run: npm run test:inheritance
-      - run: npm run test:generation
-      - name: Print gas report
-        run: cat gas-report.txt
-
-  coverage:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/checkout@v3
-        with:
-          fetch-depth: 2
-      - uses: actions/setup-node@v3
-        with:
-          node-version: 12.x
-      - uses: actions/cache@v3
-        id: cache
-        with:
-          path: '**/node_modules'
-          key: npm-v2-${{ hashFiles('**/package-lock.json') }}
-          restore-keys: npm-v2-
-      - run: npm ci
-        if: steps.cache.outputs.cache-hit != 'true'
-      - run: npm run coverage
-        env:
-          NODE_OPTIONS: --max_old_space_size=4096
-      - uses: codecov/codecov-action@v3

+ 11 - 0
CHANGELOG.md

@@ -6,11 +6,22 @@
  * `TimelockController`: Migrate `_call` to `_execute` and allow inheritance and overriding similar to `Governor`. ([#3317](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3317))
  * `CrossChainEnabledPolygonChild`: replace the `require` statement with the custom error `NotCrossChainCall`. ([#3380](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3380))
  * `ERC20FlashMint`: Add customizable flash fee receiver. ([#3327](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3327))
+ * `ERC20TokenizedVault`: add an extension of `ERC20` that implements the ERC4626 Tokenized Vault Standard. ([#3171](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3171))
+ * `Math`: add a `mulDiv` function that can round the result either up or down. ([#3171](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3171))
  * `Strings`: add a new overloaded function `toHexString` that converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation. ([#3403](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3403))
  * `EnumerableMap`: add new `UintToUintMap` map type. ([#3338](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3338))
  * `EnumerableMap`: add new `Bytes32ToUintMap` map type. ([#3416](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3416))
  * `SafeCast`: add support for many more types, using procedural code generation. ([#3245](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3245))
  * `MerkleProof`: add `multiProofVerify` to prove multiple values are part of a Merkle tree. ([#3276](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3276))
+ * `MerkleProof`: add calldata versions of the functions to avoid copying input arrays to memory and save gas. ([#3200](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3200))
+ * `ERC721`, `ERC1155`: simplified revert reasons. ([#3254](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3254), ([#3438](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3438)))
+ * `ERC721`: removed redundant require statement. ([#3434](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3434))
+ * `PaymentSplitter`: add `releasable` getters. ([#3350](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3350))
+ * `Initializable`: refactored implementation of modifiers for easier understanding. ([#3450](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3450))
+
+### Breaking changes
+
+ * `Initializable`: functions decorated with the modifier `reinitializer(1)` may no longer invoke each other.
 
 ## 4.6.0 (2022-04-26)
 

+ 2 - 2
contracts/crosschain/README.adoc

@@ -9,7 +9,7 @@ This directory provides building blocks to improve cross-chain awareness of smar
 
 == CrossChainEnabled specializations
 
-The following specializations of {CrossChainEnabled} provide implementations of the {CrossChainEnabled} abstraction for specific bridges. This can be used to complexe cross-chain aware components such as {AccessControlCrossChain}.
+The following specializations of {CrossChainEnabled} provide implementations of the {CrossChainEnabled} abstraction for specific bridges. This can be used to complex cross-chain aware components such as {AccessControlCrossChain}.
 
 {{CrossChainEnabledAMB}}
 
@@ -23,7 +23,7 @@ The following specializations of {CrossChainEnabled} provide implementations of
 
 == Libraries for cross-chain
 
-In addition to the {CrossChainEnable} abstraction, cross-chain awareness is also available through libraries. These libraries can be used to build complex designs such as contracts with the ability to interact with multiple bridges.
+In addition to the {CrossChainEnabled} abstraction, cross-chain awareness is also available through libraries. These libraries can be used to build complex designs such as contracts with the ability to interact with multiple bridges.
 
 {{LibAMB}}
 

+ 19 - 4
contracts/finance/PaymentSplitter.sol

@@ -120,6 +120,23 @@ contract PaymentSplitter is Context {
         return _payees[index];
     }
 
+    /**
+     * @dev Getter for the amount of payee's releasable Ether.
+     */
+    function releasable(address account) public view returns (uint256) {
+        uint256 totalReceived = address(this).balance + totalReleased();
+        return _pendingPayment(account, totalReceived, released(account));
+    }
+
+    /**
+     * @dev Getter for the amount of payee's releasable `token` tokens. `token` should be the address of an
+     * IERC20 contract.
+     */
+    function releasable(IERC20 token, address account) public view returns (uint256) {
+        uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token);
+        return _pendingPayment(account, totalReceived, released(token, account));
+    }
+
     /**
      * @dev Triggers a transfer to `account` of the amount of Ether they are owed, according to their percentage of the
      * total shares and their previous withdrawals.
@@ -127,8 +144,7 @@ contract PaymentSplitter is Context {
     function release(address payable account) public virtual {
         require(_shares[account] > 0, "PaymentSplitter: account has no shares");
 
-        uint256 totalReceived = address(this).balance + totalReleased();
-        uint256 payment = _pendingPayment(account, totalReceived, released(account));
+        uint256 payment = releasable(account);
 
         require(payment != 0, "PaymentSplitter: account is not due payment");
 
@@ -147,8 +163,7 @@ contract PaymentSplitter is Context {
     function release(IERC20 token, address account) public virtual {
         require(_shares[account] > 0, "PaymentSplitter: account has no shares");
 
-        uint256 totalReceived = token.balanceOf(address(this)) + totalReleased(token);
-        uint256 payment = _pendingPayment(account, totalReceived, released(token, account));
+        uint256 payment = releasable(token, account);
 
         require(payment != 0, "PaymentSplitter: account is not due payment");
 

+ 239 - 0
contracts/interfaces/IERC4626.sol

@@ -0,0 +1,239 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../token/ERC20/IERC20.sol";
+import "../token/ERC20/extensions/IERC20Metadata.sol";
+
+/**
+ * @dev Interface of the ERC4626 "Tokenized Vault Standard", as defined in
+ * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626].
+ *
+ * _Available since v4.7._
+ */
+interface IERC4626 is IERC20, IERC20Metadata {
+    event Deposit(address indexed caller, address indexed owner, uint256 assets, uint256 shares);
+
+    event Withdraw(
+        address indexed caller,
+        address indexed receiver,
+        address indexed owner,
+        uint256 assets,
+        uint256 shares
+    );
+
+    /**
+     * @dev Returns the address of the underlying token used for the Vault for accounting, depositing, and withdrawing.
+     *
+     * - MUST be an ERC-20 token contract.
+     * - MUST NOT revert.
+     */
+    function asset() external view returns (address assetTokenAddress);
+
+    /**
+     * @dev Returns the total amount of the underlying asset that is “managed” by Vault.
+     *
+     * - SHOULD include any compounding that occurs from yield.
+     * - MUST be inclusive of any fees that are charged against assets in the Vault.
+     * - MUST NOT revert.
+     */
+    function totalAssets() external view returns (uint256 totalManagedAssets);
+
+    /**
+     * @dev Returns the amount of shares that the Vault would exchange for the amount of assets provided, in an ideal
+     * scenario where all the conditions are met.
+     *
+     * - MUST NOT be inclusive of any fees that are charged against assets in the Vault.
+     * - MUST NOT show any variations depending on the caller.
+     * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.
+     * - MUST NOT revert.
+     *
+     * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the
+     * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and
+     * from.
+     */
+    function convertToShares(uint256 assets) external view returns (uint256 shares);
+
+    /**
+     * @dev Returns the amount of assets that the Vault would exchange for the amount of shares provided, in an ideal
+     * scenario where all the conditions are met.
+     *
+     * - MUST NOT be inclusive of any fees that are charged against assets in the Vault.
+     * - MUST NOT show any variations depending on the caller.
+     * - MUST NOT reflect slippage or other on-chain conditions, when performing the actual exchange.
+     * - MUST NOT revert.
+     *
+     * NOTE: This calculation MAY NOT reflect the “per-user” price-per-share, and instead should reflect the
+     * “average-user’s” price-per-share, meaning what the average user should expect to see when exchanging to and
+     * from.
+     */
+    function convertToAssets(uint256 shares) external view returns (uint256 assets);
+
+    /**
+     * @dev Returns the maximum amount of the underlying asset that can be deposited into the Vault for the receiver,
+     * through a deposit call.
+     *
+     * - MUST return a limited value if receiver is subject to some deposit limit.
+     * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of assets that may be deposited.
+     * - MUST NOT revert.
+     */
+    function maxDeposit(address receiver) external view returns (uint256 maxAssets);
+
+    /**
+     * @dev Allows an on-chain or off-chain user to simulate the effects of their deposit at the current block, given
+     * current on-chain conditions.
+     *
+     * - MUST return as close to and no more than the exact amount of Vault shares that would be minted in a deposit
+     *   call in the same transaction. I.e. deposit should return the same or more shares as previewDeposit if called
+     *   in the same transaction.
+     * - MUST NOT account for deposit limits like those returned from maxDeposit and should always act as though the
+     *   deposit would be accepted, regardless if the user has enough tokens approved, etc.
+     * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.
+     * - MUST NOT revert.
+     *
+     * NOTE: any unfavorable discrepancy between convertToShares and previewDeposit SHOULD be considered slippage in
+     * share price or some other type of condition, meaning the depositor will lose assets by depositing.
+     */
+    function previewDeposit(uint256 assets) external view returns (uint256 shares);
+
+    /**
+     * @dev Mints shares Vault shares to receiver by depositing exactly amount of underlying tokens.
+     *
+     * - MUST emit the Deposit event.
+     * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the
+     *   deposit execution, and are accounted for during deposit.
+     * - MUST revert if all of assets cannot be deposited (due to deposit limit being reached, slippage, the user not
+     *   approving enough underlying tokens to the Vault contract, etc).
+     *
+     * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token.
+     */
+    function deposit(uint256 assets, address receiver) external returns (uint256 shares);
+
+    /**
+     * @dev Returns the maximum amount of the Vault shares that can be minted for the receiver, through a mint call.
+     * - MUST return a limited value if receiver is subject to some mint limit.
+     * - MUST return 2 ** 256 - 1 if there is no limit on the maximum amount of shares that may be minted.
+     * - MUST NOT revert.
+     */
+    function maxMint(address receiver) external view returns (uint256 maxShares);
+
+    /**
+     * @dev Allows an on-chain or off-chain user to simulate the effects of their mint at the current block, given
+     * current on-chain conditions.
+     *
+     * - MUST return as close to and no fewer than the exact amount of assets that would be deposited in a mint call
+     *   in the same transaction. I.e. mint should return the same or fewer assets as previewMint if called in the
+     *   same transaction.
+     * - MUST NOT account for mint limits like those returned from maxMint and should always act as though the mint
+     *   would be accepted, regardless if the user has enough tokens approved, etc.
+     * - MUST be inclusive of deposit fees. Integrators should be aware of the existence of deposit fees.
+     * - MUST NOT revert.
+     *
+     * NOTE: any unfavorable discrepancy between convertToAssets and previewMint SHOULD be considered slippage in
+     * share price or some other type of condition, meaning the depositor will lose assets by minting.
+     */
+    function previewMint(uint256 shares) external view returns (uint256 assets);
+
+    /**
+     * @dev Mints exactly shares Vault shares to receiver by depositing amount of underlying tokens.
+     *
+     * - MUST emit the Deposit event.
+     * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the mint
+     *   execution, and are accounted for during mint.
+     * - MUST revert if all of shares cannot be minted (due to deposit limit being reached, slippage, the user not
+     *   approving enough underlying tokens to the Vault contract, etc).
+     *
+     * NOTE: most implementations will require pre-approval of the Vault with the Vault’s underlying asset token.
+     */
+    function mint(uint256 shares, address receiver) external returns (uint256 assets);
+
+    /**
+     * @dev Returns the maximum amount of the underlying asset that can be withdrawn from the owner balance in the
+     * Vault, through a withdraw call.
+     *
+     * - MUST return a limited value if owner is subject to some withdrawal limit or timelock.
+     * - MUST NOT revert.
+     */
+    function maxWithdraw(address owner) external view returns (uint256 maxAssets);
+
+    /**
+     * @dev Allows an on-chain or off-chain user to simulate the effects of their withdrawal at the current block,
+     * given current on-chain conditions.
+     *
+     * - MUST return as close to and no fewer than the exact amount of Vault shares that would be burned in a withdraw
+     *   call in the same transaction. I.e. withdraw should return the same or fewer shares as previewWithdraw if
+     *   called
+     *   in the same transaction.
+     * - MUST NOT account for withdrawal limits like those returned from maxWithdraw and should always act as though
+     *   the withdrawal would be accepted, regardless if the user has enough shares, etc.
+     * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.
+     * - MUST NOT revert.
+     *
+     * NOTE: any unfavorable discrepancy between convertToShares and previewWithdraw SHOULD be considered slippage in
+     * share price or some other type of condition, meaning the depositor will lose assets by depositing.
+     */
+    function previewWithdraw(uint256 assets) external view returns (uint256 shares);
+
+    /**
+     * @dev Burns shares from owner and sends exactly assets of underlying tokens to receiver.
+     *
+     * - MUST emit the Withdraw event.
+     * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the
+     *   withdraw execution, and are accounted for during withdraw.
+     * - MUST revert if all of assets cannot be withdrawn (due to withdrawal limit being reached, slippage, the owner
+     *   not having enough shares, etc).
+     *
+     * Note that some implementations will require pre-requesting to the Vault before a withdrawal may be performed.
+     * Those methods should be performed separately.
+     */
+    function withdraw(
+        uint256 assets,
+        address receiver,
+        address owner
+    ) external returns (uint256 shares);
+
+    /**
+     * @dev Returns the maximum amount of Vault shares that can be redeemed from the owner balance in the Vault,
+     * through a redeem call.
+     *
+     * - MUST return a limited value if owner is subject to some withdrawal limit or timelock.
+     * - MUST return balanceOf(owner) if owner is not subject to any withdrawal limit or timelock.
+     * - MUST NOT revert.
+     */
+    function maxRedeem(address owner) external view returns (uint256 maxShares);
+
+    /**
+     * @dev Allows an on-chain or off-chain user to simulate the effects of their redeemption at the current block,
+     * given current on-chain conditions.
+     *
+     * - MUST return as close to and no more than the exact amount of assets that would be withdrawn in a redeem call
+     *   in the same transaction. I.e. redeem should return the same or more assets as previewRedeem if called in the
+     *   same transaction.
+     * - MUST NOT account for redemption limits like those returned from maxRedeem and should always act as though the
+     *   redemption would be accepted, regardless if the user has enough shares, etc.
+     * - MUST be inclusive of withdrawal fees. Integrators should be aware of the existence of withdrawal fees.
+     * - MUST NOT revert.
+     *
+     * NOTE: any unfavorable discrepancy between convertToAssets and previewRedeem SHOULD be considered slippage in
+     * share price or some other type of condition, meaning the depositor will lose assets by redeeming.
+     */
+    function previewRedeem(uint256 shares) external view returns (uint256 assets);
+
+    /**
+     * @dev Burns exactly shares from owner and sends assets of underlying tokens to receiver.
+     *
+     * - MUST emit the Withdraw event.
+     * - MAY support an additional flow in which the underlying tokens are owned by the Vault contract before the
+     *   redeem execution, and are accounted for during redeem.
+     * - MUST revert if all of shares cannot be redeemed (due to withdrawal limit being reached, slippage, the owner
+     *   not having enough shares, etc).
+     *
+     * NOTE: some implementations will require pre-requesting to the Vault before a withdrawal may be performed.
+     * Those methods should be performed separately.
+     */
+    function redeem(
+        uint256 shares,
+        address receiver,
+        address owner
+    ) external returns (uint256 assets);
+}

+ 4 - 0
contracts/mocks/CheckpointsImpl.sol

@@ -20,4 +20,8 @@ contract CheckpointsImpl {
     function push(uint256 value) public returns (uint256, uint256) {
         return _totalCheckpoints.push(value);
     }
+
+    function length() public view returns (uint256) {
+        return _totalCheckpoints._checkpoints.length;
+    }
 }

+ 22 - 0
contracts/mocks/ERC20TokenizedVaultMock.sol

@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../token/ERC20/extensions/ERC20TokenizedVault.sol";
+
+// mock class using ERC20
+contract ERC20TokenizedVaultMock is ERC20TokenizedVault {
+    constructor(
+        IERC20Metadata asset,
+        string memory name,
+        string memory symbol
+    ) ERC20(name, symbol) ERC20TokenizedVault(asset) {}
+
+    function mockMint(address account, uint256 amount) public {
+        _mint(account, amount);
+    }
+
+    function mockBurn(address account, uint256 amount) public {
+        _burn(account, amount);
+    }
+}

+ 20 - 0
contracts/mocks/InitializableMock.sol

@@ -100,3 +100,23 @@ contract ReinitializerMock is Initializable {
         counter++;
     }
 }
+
+contract DisableNew is Initializable {
+    constructor() {
+        _disableInitializers();
+    }
+}
+
+contract DisableOld is Initializable {
+    constructor() initializer {}
+}
+
+contract DisableBad1 is DisableNew, DisableOld {}
+
+contract DisableBad2 is Initializable {
+    constructor() initializer {
+        _disableInitializers();
+    }
+}
+
+contract DisableOk is DisableOld, DisableNew {}

+ 9 - 0
contracts/mocks/MathMock.sol

@@ -20,4 +20,13 @@ contract MathMock {
     function ceilDiv(uint256 a, uint256 b) public pure returns (uint256) {
         return Math.ceilDiv(a, b);
     }
+
+    function mulDiv(
+        uint256 a,
+        uint256 b,
+        uint256 denominator,
+        Math.Rounding direction
+    ) public pure returns (uint256) {
+        return Math.mulDiv(a, b, denominator, direction);
+    }
 }

+ 20 - 8
contracts/mocks/MerkleProofWrapper.sol

@@ -13,24 +13,36 @@ contract MerkleProofWrapper {
         return MerkleProof.verify(proof, root, leaf);
     }
 
+    function verifyCalldata(
+        bytes32[] calldata proof,
+        bytes32 root,
+        bytes32 leaf
+    ) public pure returns (bool) {
+        return MerkleProof.verifyCalldata(proof, root, leaf);
+    }
+
     function processProof(bytes32[] memory proof, bytes32 leaf) public pure returns (bytes32) {
         return MerkleProof.processProof(proof, leaf);
     }
 
+    function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) public pure returns (bytes32) {
+        return MerkleProof.processProofCalldata(proof, leaf);
+    }
+
     function multiProofVerify(
+        bytes32[] calldata proofs,
+        bool[] calldata proofFlag,
         bytes32 root,
-        bytes32[] memory leafs,
-        bytes32[] memory proofs,
-        bool[] memory proofFlag
+        bytes32[] calldata leaves
     ) public pure returns (bool) {
-        return MerkleProof.multiProofVerify(root, leafs, proofs, proofFlag);
+        return MerkleProof.multiProofVerify(proofs, proofFlag, root, leaves);
     }
 
     function processMultiProof(
-        bytes32[] memory leafs,
-        bytes32[] memory proofs,
-        bool[] memory proofFlag
+        bytes32[] calldata proofs,
+        bool[] calldata proofFlag,
+        bytes32[] calldata leaves
     ) public pure returns (bytes32) {
-        return MerkleProof.processMultiProof(leafs, proofs, proofFlag);
+        return MerkleProof.processMultiProof(proofs, proofFlag, leaves);
     }
 }

+ 15 - 30
contracts/proxy/utils/Initializable.sol

@@ -76,7 +76,12 @@ abstract contract Initializable {
      * `onlyInitializing` functions can be used to initialize parent contracts. Equivalent to `reinitializer(1)`.
      */
     modifier initializer() {
-        bool isTopLevelCall = _setInitializedVersion(1);
+        bool isTopLevelCall = !_initializing;
+        require(
+            (isTopLevelCall && _initialized < 1) || (!Address.isContract(address(this)) && _initialized == 1),
+            "Initializable: contract is already initialized"
+        );
+        _initialized = 1;
         if (isTopLevelCall) {
             _initializing = true;
         }
@@ -100,15 +105,12 @@ abstract contract Initializable {
      * a contract, executing them in the right order is up to the developer or operator.
      */
     modifier reinitializer(uint8 version) {
-        bool isTopLevelCall = _setInitializedVersion(version);
-        if (isTopLevelCall) {
-            _initializing = true;
-        }
+        require(!_initializing && _initialized < version, "Initializable: contract is already initialized");
+        _initialized = version;
+        _initializing = true;
         _;
-        if (isTopLevelCall) {
-            _initializing = false;
-            emit Initialized(version);
-        }
+        _initializing = false;
+        emit Initialized(version);
     }
 
     /**
@@ -127,27 +129,10 @@ abstract contract Initializable {
      * through proxies.
      */
     function _disableInitializers() internal virtual {
-        _setInitializedVersion(type(uint8).max);
-    }
-
-    function _setInitializedVersion(uint8 version) private returns (bool) {
-        // If the contract is initializing we ignore whether _initialized is set in order to support multiple
-        // inheritance patterns, but we only do this in the context of a constructor, and for the lowest level
-        // of initializers, because in other contexts the contract may have been reentered.
-
-        bool isTopLevelCall = !_initializing; // cache sload
-        uint8 currentVersion = _initialized; // cache sload
-
-        require(
-            (isTopLevelCall && version > currentVersion) || // not nested with increasing version or
-                (!Address.isContract(address(this)) && (version == 1 || version == type(uint8).max)), // contract being constructed
-            "Initializable: contract is already initialized"
-        );
-
-        if (isTopLevelCall) {
-            _initialized = version;
+        require(!_initializing, "Initializable: contract is initializing");
+        if (_initialized < type(uint8).max) {
+            _initialized = type(uint8).max;
+            emit Initialized(type(uint8).max);
         }
-
-        return isTopLevelCall;
     }
 }

+ 2 - 2
contracts/token/ERC1155/ERC1155.sol

@@ -123,7 +123,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI {
     ) public virtual override {
         require(
             from == _msgSender() || isApprovedForAll(from, _msgSender()),
-            "ERC1155: caller is not owner nor approved"
+            "ERC1155: caller is not token owner nor approved"
         );
         _safeTransferFrom(from, to, id, amount, data);
     }
@@ -140,7 +140,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI {
     ) public virtual override {
         require(
             from == _msgSender() || isApprovedForAll(from, _msgSender()),
-            "ERC1155: transfer caller is not owner nor approved"
+            "ERC1155: caller is not token owner nor approved"
         );
         _safeBatchTransferFrom(from, to, ids, amounts, data);
     }

+ 2 - 2
contracts/token/ERC1155/extensions/ERC1155Burnable.sol

@@ -19,7 +19,7 @@ abstract contract ERC1155Burnable is ERC1155 {
     ) public virtual {
         require(
             account == _msgSender() || isApprovedForAll(account, _msgSender()),
-            "ERC1155: caller is not owner nor approved"
+            "ERC1155: caller is not token owner nor approved"
         );
 
         _burn(account, id, value);
@@ -32,7 +32,7 @@ abstract contract ERC1155Burnable is ERC1155 {
     ) public virtual {
         require(
             account == _msgSender() || isApprovedForAll(account, _msgSender()),
-            "ERC1155: caller is not owner nor approved"
+            "ERC1155: caller is not token owner nor approved"
         );
 
         _burnBatch(account, ids, values);

+ 3 - 0
contracts/token/ERC20/README.adoc

@@ -24,6 +24,7 @@ Additionally there are multiple custom extensions, including:
 * {ERC20Votes}: support for voting and vote delegation.
 * {ERC20VotesComp}: support for voting and vote delegation (compatible with Compound's token, with uint96 restrictions).
 * {ERC20Wrapper}: wrapper to create an ERC20 backed by another ERC20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}.
+* {ERC20TokenizedVault}: tokenized vault that manages shares (represented as ERC20) that are backed by assets (another ERC20).
 
 Finally, there are some utilities to interact with ERC20 contracts in various ways.
 
@@ -62,6 +63,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
 
 {{ERC20FlashMint}}
 
+{{ERC20TokenizedVault}}
+
 == Draft EIPs
 
 The following EIPs are still in Draft status. Due to their nature as drafts, the details of these contracts may change and we cannot guarantee their xref:ROOT:releases-stability.adoc[stability]. Minor releases of OpenZeppelin Contracts may contain breaking changes for the contracts in this directory, which will be duly announced in the https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CHANGELOG.md[changelog]. The EIPs included here are used by projects in production and this may make them less likely to change significantly.

+ 217 - 0
contracts/token/ERC20/extensions/ERC20TokenizedVault.sol

@@ -0,0 +1,217 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../ERC20.sol";
+import "../utils/SafeERC20.sol";
+import "../../../interfaces/IERC4626.sol";
+import "../../../utils/math/Math.sol";
+
+/**
+ * @dev Implementation of the ERC4626 "Tokenized Vault Standard" as defined in
+ * https://eips.ethereum.org/EIPS/eip-4626[EIP-4626].
+ *
+ * This extension allows the minting and burning of "shares" (represented using the ERC20 inheritance) in exchange for
+ * underlying "assets" through standardized {deposit}, {mint}, {redeem} and {burn} workflows. This contract extends
+ * the ERC20 standard. Any additional extensions included along it would affect the "shares" token represented by this
+ * contract and not the "assets" token which is an independent contract.
+ *
+ * _Available since v4.7._
+ */
+abstract contract ERC20TokenizedVault is ERC20, IERC4626 {
+    using Math for uint256;
+
+    IERC20Metadata private immutable _asset;
+
+    /**
+     * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777).
+     */
+    constructor(IERC20Metadata asset_) {
+        _asset = asset_;
+    }
+
+    /** @dev See {IERC4262-asset} */
+    function asset() public view virtual override returns (address) {
+        return address(_asset);
+    }
+
+    /** @dev See {IERC4262-totalAssets} */
+    function totalAssets() public view virtual override returns (uint256) {
+        return _asset.balanceOf(address(this));
+    }
+
+    /** @dev See {IERC4262-convertToShares} */
+    function convertToShares(uint256 assets) public view virtual override returns (uint256 shares) {
+        return _convertToShares(assets, Math.Rounding.Down);
+    }
+
+    /** @dev See {IERC4262-convertToAssets} */
+    function convertToAssets(uint256 shares) public view virtual override returns (uint256 assets) {
+        return _convertToAssets(shares, Math.Rounding.Down);
+    }
+
+    /** @dev See {IERC4262-maxDeposit} */
+    function maxDeposit(address) public view virtual override returns (uint256) {
+        return _isVaultCollateralized() ? type(uint256).max : 0;
+    }
+
+    /** @dev See {IERC4262-maxMint} */
+    function maxMint(address) public view virtual override returns (uint256) {
+        return type(uint256).max;
+    }
+
+    /** @dev See {IERC4262-maxWithdraw} */
+    function maxWithdraw(address owner) public view virtual override returns (uint256) {
+        return _convertToAssets(balanceOf(owner), Math.Rounding.Down);
+    }
+
+    /** @dev See {IERC4262-maxRedeem} */
+    function maxRedeem(address owner) public view virtual override returns (uint256) {
+        return balanceOf(owner);
+    }
+
+    /** @dev See {IERC4262-previewDeposit} */
+    function previewDeposit(uint256 assets) public view virtual override returns (uint256) {
+        return _convertToShares(assets, Math.Rounding.Down);
+    }
+
+    /** @dev See {IERC4262-previewMint} */
+    function previewMint(uint256 shares) public view virtual override returns (uint256) {
+        return _convertToAssets(shares, Math.Rounding.Up);
+    }
+
+    /** @dev See {IERC4262-previewWithdraw} */
+    function previewWithdraw(uint256 assets) public view virtual override returns (uint256) {
+        return _convertToShares(assets, Math.Rounding.Up);
+    }
+
+    /** @dev See {IERC4262-previewRedeem} */
+    function previewRedeem(uint256 shares) public view virtual override returns (uint256) {
+        return _convertToAssets(shares, Math.Rounding.Down);
+    }
+
+    /** @dev See {IERC4262-deposit} */
+    function deposit(uint256 assets, address receiver) public virtual override returns (uint256) {
+        require(assets <= maxDeposit(receiver), "ERC20TokenizedVault: deposit more than max");
+
+        uint256 shares = previewDeposit(assets);
+        _deposit(_msgSender(), receiver, assets, shares);
+
+        return shares;
+    }
+
+    /** @dev See {IERC4262-mint} */
+    function mint(uint256 shares, address receiver) public virtual override returns (uint256) {
+        require(shares <= maxMint(receiver), "ERC20TokenizedVault: mint more than max");
+
+        uint256 assets = previewMint(shares);
+        _deposit(_msgSender(), receiver, assets, shares);
+
+        return assets;
+    }
+
+    /** @dev See {IERC4262-withdraw} */
+    function withdraw(
+        uint256 assets,
+        address receiver,
+        address owner
+    ) public virtual override returns (uint256) {
+        require(assets <= maxWithdraw(owner), "ERC20TokenizedVault: withdraw more than max");
+
+        uint256 shares = previewWithdraw(assets);
+        _withdraw(_msgSender(), receiver, owner, assets, shares);
+
+        return shares;
+    }
+
+    /** @dev See {IERC4262-redeem} */
+    function redeem(
+        uint256 shares,
+        address receiver,
+        address owner
+    ) public virtual override returns (uint256) {
+        require(shares <= maxRedeem(owner), "ERC20TokenizedVault: redeem more than max");
+
+        uint256 assets = previewRedeem(shares);
+        _withdraw(_msgSender(), receiver, owner, assets, shares);
+
+        return assets;
+    }
+
+    /**
+     * @dev Internal convertion function (from assets to shares) with support for rounding direction
+     *
+     * Will revert if assets > 0, totalSupply > 0 and totalAssets = 0. That corresponds to a case where any asset
+     * would represent an infinite amout of shares.
+     */
+    function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256 shares) {
+        uint256 supply = totalSupply();
+        return
+            (assets == 0 || supply == 0)
+                ? assets.mulDiv(10**decimals(), 10**_asset.decimals(), rounding)
+                : assets.mulDiv(supply, totalAssets(), rounding);
+    }
+
+    /**
+     * @dev Internal convertion function (from shares to assets) with support for rounding direction
+     */
+    function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256 assets) {
+        uint256 supply = totalSupply();
+        return
+            (supply == 0)
+                ? shares.mulDiv(10**_asset.decimals(), 10**decimals(), rounding)
+                : shares.mulDiv(totalAssets(), supply, rounding);
+    }
+
+    /**
+     * @dev Deposit/mint common workflow
+     */
+    function _deposit(
+        address caller,
+        address receiver,
+        uint256 assets,
+        uint256 shares
+    ) private {
+        // If _asset is ERC777, `transferFrom` can trigger a reenterancy BEFORE the transfer happens through the
+        // `tokensToSend` hook. On the other hand, the `tokenReceived` hook, that is triggered after the transfer,
+        // calls the vault, which is assumed not malicious.
+        //
+        // Conclusion: we need to do the transfer before we mint so that any reentrancy would happen before the
+        // assets are transfered and before the shares are minted, which is a valid state.
+        // slither-disable-next-line reentrancy-no-eth
+        SafeERC20.safeTransferFrom(_asset, caller, address(this), assets);
+        _mint(receiver, shares);
+
+        emit Deposit(caller, receiver, assets, shares);
+    }
+
+    /**
+     * @dev Withdraw/redeem common workflow
+     */
+    function _withdraw(
+        address caller,
+        address receiver,
+        address owner,
+        uint256 assets,
+        uint256 shares
+    ) private {
+        if (caller != owner) {
+            _spendAllowance(owner, caller, shares);
+        }
+
+        // If _asset is ERC777, `transfer` can trigger trigger a reentrancy AFTER the transfer happens through the
+        // `tokensReceived` hook. On the other hand, the `tokensToSend` hook, that is triggered before the transfer,
+        // calls the vault, which is assumed not malicious.
+        //
+        // Conclusion: we need to do the transfer after the burn so that any reentrancy would happen after the
+        // shares are burned and after the assets are transfered, which is a valid state.
+        _burn(owner, shares);
+        SafeERC20.safeTransfer(_asset, receiver, assets);
+
+        emit Withdraw(caller, receiver, owner, assets, shares);
+    }
+
+    function _isVaultCollateralized() private view returns (bool) {
+        return totalAssets() > 0 || totalSupply() == 0;
+    }
+}

+ 13 - 7
contracts/token/ERC721/ERC721.sol

@@ -69,7 +69,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
      */
     function ownerOf(uint256 tokenId) public view virtual override returns (address) {
         address owner = _owners[tokenId];
-        require(owner != address(0), "ERC721: owner query for nonexistent token");
+        require(owner != address(0), "ERC721: invalid token ID");
         return owner;
     }
 
@@ -91,7 +91,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
      * @dev See {IERC721Metadata-tokenURI}.
      */
     function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
-        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
+        _requireMinted(tokenId);
 
         string memory baseURI = _baseURI();
         return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
@@ -115,7 +115,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
 
         require(
             _msgSender() == owner || isApprovedForAll(owner, _msgSender()),
-            "ERC721: approve caller is not owner nor approved for all"
+            "ERC721: approve caller is not token owner nor approved for all"
         );
 
         _approve(to, tokenId);
@@ -125,7 +125,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
      * @dev See {IERC721-getApproved}.
      */
     function getApproved(uint256 tokenId) public view virtual override returns (address) {
-        require(_exists(tokenId), "ERC721: approved query for nonexistent token");
+        _requireMinted(tokenId);
 
         return _tokenApprovals[tokenId];
     }
@@ -153,7 +153,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
         uint256 tokenId
     ) public virtual override {
         //solhint-disable-next-line max-line-length
-        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
+        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");
 
         _transfer(from, to, tokenId);
     }
@@ -178,7 +178,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
         uint256 tokenId,
         bytes memory data
     ) public virtual override {
-        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: transfer caller is not owner nor approved");
+        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");
         _safeTransfer(from, to, tokenId, data);
     }
 
@@ -230,7 +230,6 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
      * - `tokenId` must exist.
      */
     function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
-        require(_exists(tokenId), "ERC721: operator query for nonexistent token");
         address owner = ERC721.ownerOf(tokenId);
         return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
     }
@@ -375,6 +374,13 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
         emit ApprovalForAll(owner, operator, approved);
     }
 
+    /**
+     * @dev Reverts if the `tokenId` has not been minted yet.
+     */
+    function _requireMinted(uint256 tokenId) internal view virtual {
+        require(_exists(tokenId), "ERC721: invalid token ID");
+    }
+
     /**
      * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address.
      * The call is not executed if the target address is not a contract.

+ 1 - 1
contracts/token/ERC721/extensions/ERC721Burnable.sol

@@ -20,7 +20,7 @@ abstract contract ERC721Burnable is Context, ERC721 {
      */
     function burn(uint256 tokenId) public virtual {
         //solhint-disable-next-line max-line-length
-        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721Burnable: caller is not owner nor approved");
+        require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner nor approved");
         _burn(tokenId);
     }
 }

+ 1 - 1
contracts/token/ERC721/extensions/ERC721URIStorage.sol

@@ -18,7 +18,7 @@ abstract contract ERC721URIStorage is ERC721 {
      * @dev See {IERC721Metadata-tokenURI}.
      */
     function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
-        require(_exists(tokenId), "ERC721URIStorage: URI query for nonexistent token");
+        _requireMinted(tokenId);
 
         string memory _tokenURI = _tokenURIs[tokenId];
         string memory base = _baseURI();

+ 53 - 24
contracts/utils/cryptography/MerkleProof.sol

@@ -4,7 +4,7 @@
 pragma solidity ^0.8.0;
 
 /**
- * @dev These functions deal with verification of Merkle Trees proofs.
+ * @dev These functions deal with verification of Merkle Tree proofs.
  *
  * The proofs can be generated using the JavaScript library
  * https://github.com/miguelmota/merkletreejs[merkletreejs].
@@ -32,6 +32,19 @@ library MerkleProof {
         return processProof(proof, leaf) == root;
     }
 
+    /**
+     * @dev Calldata version of {verify}
+     *
+     * _Available since v4.7._
+     */
+    function verifyCalldata(
+        bytes32[] calldata proof,
+        bytes32 root,
+        bytes32 leaf
+    ) internal pure returns (bool) {
+        return processProofCalldata(proof, leaf) == root;
+    }
+
     /**
      * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
      * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
@@ -49,44 +62,54 @@ library MerkleProof {
     }
 
     /**
-     * @dev Returns true if a `leafs` can be proved to be a part of a Merkle tree
-     * defined by `root`. For this, `proofs` for each leaf must be provided, containing
-     * sibling hashes on the branch from the leaf to the root of the tree. Then
-     * 'proofFlag' designates the nodes needed for the multi proof.
+     * @dev Calldata version of {processProof}
+     *
+     * _Available since v4.7._
+     */
+    function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) {
+        bytes32 computedHash = leaf;
+        for (uint256 i = 0; i < proof.length; i++) {
+            computedHash = _hashPair(computedHash, proof[i]);
+        }
+        return computedHash;
+    }
+
+    /**
+     * @dev Returns true if the `leaves` can be proved to be a part of a Merkle tree defined by
+     * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
      *
      * _Available since v4.7._
      */
     function multiProofVerify(
+        bytes32[] calldata proof,
+        bool[] calldata proofFlags,
         bytes32 root,
-        bytes32[] memory leafs,
-        bytes32[] memory proofs,
-        bool[] memory proofFlag
+        bytes32[] calldata leaves
     ) internal pure returns (bool) {
-        return processMultiProof(leafs, proofs, proofFlag) == root;
+        return processMultiProof(proof, proofFlags, leaves) == root;
     }
 
     /**
-     * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
-     * from `leaf` using the multi proof as `proofFlag`. A multi proof is
-     * valid if the final hash matches the root of the tree.
+     * @dev Returns the root of a tree reconstructed from `leaves` and the sibling nodes in `proof`,
+     * consuming from one or the other at each step according to the instructions given by
+     * `proofFlags`.
      *
      * _Available since v4.7._
      */
     function processMultiProof(
-        bytes32[] memory leafs,
-        bytes32[] memory proofs,
-        bool[] memory proofFlag
+        bytes32[] calldata proof,
+        bool[] calldata proofFlags,
+        bytes32[] calldata leaves
     ) internal pure returns (bytes32 merkleRoot) {
         // This function rebuild the root hash by traversing the tree up from the leaves. The root is rebuilt by
-        // consuming and producing values on a queue. The queue starts with the `leafs` array, then goes onto the
+        // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
         // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
         // the merkle tree.
-        uint256 leafsLen = leafs.length;
-        uint256 proofsLen = proofs.length;
-        uint256 totalHashes = proofFlag.length;
+        uint256 leavesLen = leaves.length;
+        uint256 totalHashes = proofFlags.length;
 
         // Check proof validity.
-        require(leafsLen + proofsLen - 1 == totalHashes, "MerkleProof: invalid multiproof");
+        require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof");
 
         // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
         // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
@@ -98,14 +121,20 @@ library MerkleProof {
         // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
         //   get the next hash.
         // - depending on the flag, either another value for the "main queue" (merging branches) or an element from the
-        //   `proofs` array.
+        //   `proof` array.
         for (uint256 i = 0; i < totalHashes; i++) {
-            bytes32 a = leafPos < leafsLen ? leafs[leafPos++] : hashes[hashPos++];
-            bytes32 b = proofFlag[i] ? leafPos < leafsLen ? leafs[leafPos++] : hashes[hashPos++] : proofs[proofPos++];
+            bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
+            bytes32 b = proofFlags[i] ? leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++] : proof[proofPos++];
             hashes[i] = _hashPair(a, b);
         }
 
-        return hashes[totalHashes - 1];
+        if (totalHashes > 0) {
+            return hashes[totalHashes - 1];
+        } else if (leavesLen > 0) {
+            return leaves[0];
+        } else {
+            return proof[0];
+        }
     }
 
     function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {

+ 110 - 1
contracts/utils/math/Math.sol

@@ -7,6 +7,12 @@ pragma solidity ^0.8.0;
  * @dev Standard math utilities missing in the Solidity language.
  */
 library Math {
+    enum Rounding {
+        Down, // Toward negative infinity
+        Up, // Toward infinity
+        Zero // Toward zero
+    }
+
     /**
      * @dev Returns the largest of two numbers.
      */
@@ -38,6 +44,109 @@ library Math {
      */
     function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
         // (a + b - 1) / b can overflow on addition, so we distribute.
-        return a / b + (a % b == 0 ? 0 : 1);
+        return a == 0 ? 0 : (a - 1) / b + 1;
+    }
+
+    /**
+     * @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
+     * @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv)
+     * with further edits by Uniswap Labs also under MIT license.
+     */
+    function mulDiv(
+        uint256 x,
+        uint256 y,
+        uint256 denominator
+    ) internal pure returns (uint256 result) {
+        unchecked {
+            // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use
+            // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
+            // variables such that product = prod1 * 2^256 + prod0.
+            uint256 prod0; // Least significant 256 bits of the product
+            uint256 prod1; // Most significant 256 bits of the product
+            assembly {
+                let mm := mulmod(x, y, not(0))
+                prod0 := mul(x, y)
+                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
+            }
+
+            // Handle non-overflow cases, 256 by 256 division.
+            if (prod1 == 0) {
+                return prod0 / denominator;
+            }
+
+            // Make sure the result is less than 2^256. Also prevents denominator == 0.
+            require(denominator > prod1);
+
+            ///////////////////////////////////////////////
+            // 512 by 256 division.
+            ///////////////////////////////////////////////
+
+            // Make division exact by subtracting the remainder from [prod1 prod0].
+            uint256 remainder;
+            assembly {
+                // Compute remainder using mulmod.
+                remainder := mulmod(x, y, denominator)
+
+                // Subtract 256 bit number from 512 bit number.
+                prod1 := sub(prod1, gt(remainder, prod0))
+                prod0 := sub(prod0, remainder)
+            }
+
+            // Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1.
+            // See https://cs.stackexchange.com/q/138556/92363.
+
+            // Does not overflow because the denominator cannot be zero at this stage in the function.
+            uint256 twos = denominator & (~denominator + 1);
+            assembly {
+                // Divide denominator by twos.
+                denominator := div(denominator, twos)
+
+                // Divide [prod1 prod0] by twos.
+                prod0 := div(prod0, twos)
+
+                // Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.
+                twos := add(div(sub(0, twos), twos), 1)
+            }
+
+            // Shift in bits from prod1 into prod0.
+            prod0 |= prod1 * twos;
+
+            // Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such
+            // that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for
+            // four bits. That is, denominator * inv = 1 mod 2^4.
+            uint256 inverse = (3 * denominator) ^ 2;
+
+            // Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works
+            // in modular arithmetic, doubling the correct bits in each step.
+            inverse *= 2 - denominator * inverse; // inverse mod 2^8
+            inverse *= 2 - denominator * inverse; // inverse mod 2^16
+            inverse *= 2 - denominator * inverse; // inverse mod 2^32
+            inverse *= 2 - denominator * inverse; // inverse mod 2^64
+            inverse *= 2 - denominator * inverse; // inverse mod 2^128
+            inverse *= 2 - denominator * inverse; // inverse mod 2^256
+
+            // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
+            // This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is
+            // less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1
+            // is no longer required.
+            result = prod0 * inverse;
+            return result;
+        }
+    }
+
+    /**
+     * @notice Calculates x * y / denominator with full precision, following the selected rounding direction.
+     */
+    function mulDiv(
+        uint256 x,
+        uint256 y,
+        uint256 denominator,
+        Rounding rounding
+    ) internal pure returns (uint256) {
+        uint256 result = mulDiv(x, y, denominator);
+        if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) {
+            result += 1;
+        }
+        return result;
     }
 }

File diff suppressed because it is too large
+ 299 - 294
package-lock.json


+ 2 - 2
package.json

@@ -23,13 +23,13 @@
     "lint:sol": "solhint 'contracts/**/*.sol' && prettier -c 'contracts/**/*.sol'",
     "lint:sol:fix": "prettier --write \"contracts/**/*.sol\"",
     "clean": "hardhat clean && rimraf build contracts/build",
-    "prepare": "npm run clean && env COMPILE_MODE=production npm run compile",
+    "prepare": "scripts/prepare.sh",
     "prepack": "scripts/prepack.sh",
     "generate": "scripts/generate/run.js",
     "release": "scripts/release/release.sh",
     "version": "scripts/release/version.sh",
     "test": "hardhat test",
-    "test:inheritance": "scripts/checks/inheritanceOrdering.js artifacts/build-info/*",
+    "test:inheritance": "scripts/checks/inheritance-ordering.js artifacts/build-info/*",
     "test:generation": "scripts/checks/generation.sh",
     "gas-report": "env ENABLE_GAS_REPORT=true npm run test",
     "slither": "npm run clean && slither . --detect reentrancy-eth,reentrancy-no-eth,reentrancy-unlimited-gas"

+ 4 - 0
scripts/checks/inheritanceOrdering.js → scripts/checks/inheritance-ordering.js

@@ -13,6 +13,10 @@ for (const artifact of artifacts) {
   const linearized = [];
 
   for (const source in solcOutput.contracts) {
+    if (source.includes('/mocks/')) {
+      continue;
+    }
+
     for (const contractDef of findAll('ContractDefinition', solcOutput.sources[source].ast)) {
       names[contractDef.id] = contractDef.name;
       linearized.push(contractDef.linearizedBaseContracts);

+ 10 - 0
scripts/prepare.sh

@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+if [ "${SKIP_COMPILE:-}" == true ]; then
+  exit
+fi
+
+npm run clean
+env COMPILE_MODE=production npm run compile

+ 5 - 0
test/crosschain/CrossChainEnabled.test.js

@@ -17,6 +17,11 @@ function shouldBehaveLikeReceiver (sender = randomAddress()) {
       this.receiver.crossChainRestricted(),
       'NotCrossChainCall()',
     );
+
+    await expectRevertCustomError(
+      this.receiver.crossChainOwnerRestricted(),
+      'NotCrossChainCall()',
+    );
   });
 
   it('should restrict to cross-chain call from a invalid sender', async function () {

+ 27 - 6
test/finance/PaymentSplitter.test.js

@@ -62,10 +62,11 @@ contract('PaymentSplitter', function (accounts) {
       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');
+        expect(await this.contract.releasable(payee)).to.be.bignumber.equal('0');
       }));
     });
 
-    describe('accepts payments', async function () {
+    describe('accepts payments', function () {
       it('Ether', async function () {
         await send.ether(owner, this.contract.address, amount);
 
@@ -79,7 +80,7 @@ contract('PaymentSplitter', function (accounts) {
       });
     });
 
-    describe('shares', async function () {
+    describe('shares', function () {
       it('stores shares if address is payee', async function () {
         expect(await this.contract.shares(payee1)).to.be.bignumber.not.equal('0');
       });
@@ -89,8 +90,8 @@ contract('PaymentSplitter', function (accounts) {
       });
     });
 
-    describe('release', async function () {
-      describe('Ether', async function () {
+    describe('release', function () {
+      describe('Ether', function () {
         it('reverts if no funds to claim', async function () {
           await expectRevert(this.contract.release(payee1),
             'PaymentSplitter: account is not due payment',
@@ -104,7 +105,7 @@ contract('PaymentSplitter', function (accounts) {
         });
       });
 
-      describe('Token', async function () {
+      describe('Token', function () {
         it('reverts if no funds to claim', async function () {
           await expectRevert(this.contract.release(this.token.address, payee1),
             'PaymentSplitter: account is not due payment',
@@ -119,7 +120,27 @@ contract('PaymentSplitter', function (accounts) {
       });
     });
 
-    describe('distributes funds to payees', async function () {
+    describe('tracks releasable and released', function () {
+      it('Ether', async function () {
+        await send.ether(payer1, this.contract.address, amount);
+        const payment = amount.divn(10);
+        expect(await this.contract.releasable(payee2)).to.be.bignumber.equal(payment);
+        await this.contract.release(payee2);
+        expect(await this.contract.releasable(payee2)).to.be.bignumber.equal('0');
+        expect(await this.contract.released(payee2)).to.be.bignumber.equal(payment);
+      });
+
+      it('Token', async function () {
+        await this.token.transfer(this.contract.address, amount, { from: owner });
+        const payment = amount.divn(10);
+        expect(await this.contract.releasable(this.token.address, payee2, {})).to.be.bignumber.equal(payment);
+        await this.contract.release(this.token.address, payee2);
+        expect(await this.contract.releasable(this.token.address, payee2, {})).to.be.bignumber.equal('0');
+        expect(await this.contract.released(this.token.address, payee2)).to.be.bignumber.equal(payment);
+      });
+    });
+
+    describe('distributes funds to payees', function () {
       it('Ether', async function () {
         await send.ether(payer1, this.contract.address, amount);
 

+ 10 - 0
test/governance/compatibility/GovernorCompatibilityBravo.test.js

@@ -168,6 +168,16 @@ contract('GovernorCompatibilityBravo', function (accounts) {
     );
   });
 
+  it('double voting is forbiden', async function () {
+    await this.helper.propose({ from: proposer });
+    await this.helper.waitForSnapshot();
+    await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 });
+    await expectRevert(
+      this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }),
+      'GovernorCompatibilityBravo: vote already cast',
+    );
+  });
+
   it('with function selector and arguments', async function () {
     const target = this.receiver.address;
     this.helper.setProposal([

+ 5 - 0
test/helpers/enums.js

@@ -21,4 +21,9 @@ module.exports = {
     'For',
     'Abstain',
   ),
+  Rounding: Enum(
+    'Down',
+    'Up',
+    'Zero',
+  ),
 };

+ 40 - 0
test/helpers/txpool.js

@@ -0,0 +1,40 @@
+const { network } = require('hardhat');
+const { promisify } = require('util');
+
+const queue = promisify(setImmediate);
+
+async function countPendingTransactions () {
+  return parseInt(
+    await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending']),
+  );
+}
+
+async function batchInBlock (txs) {
+  try {
+    // disable auto-mining
+    await network.provider.send('evm_setAutomine', [false]);
+    // send all transactions
+    const promises = txs.map(fn => fn());
+    // wait for node to have all pending transactions
+    while (txs.length > await countPendingTransactions()) {
+      await queue();
+    }
+    // mine one block
+    await network.provider.send('evm_mine');
+    // fetch receipts
+    const receipts = await Promise.all(promises);
+    // Sanity check, all tx should be in the same block
+    const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber));
+    expect(minedBlocks.size).to.equal(1);
+
+    return receipts;
+  } finally {
+    // enable auto-mining
+    await network.provider.send('evm_setAutomine', [true]);
+  }
+}
+
+module.exports = {
+  countPendingTransactions,
+  batchInBlock,
+};

+ 16 - 0
test/proxy/utils/Initializable.test.js

@@ -6,6 +6,9 @@ const ConstructorInitializableMock = artifacts.require('ConstructorInitializable
 const ChildConstructorInitializableMock = artifacts.require('ChildConstructorInitializableMock');
 const ReinitializerMock = artifacts.require('ReinitializerMock');
 const SampleChild = artifacts.require('SampleChild');
+const DisableBad1 = artifacts.require('DisableBad1');
+const DisableBad2 = artifacts.require('DisableBad2');
+const DisableOk = artifacts.require('DisableOk');
 
 contract('Initializable', function (accounts) {
   describe('basic testing without inheritance', function () {
@@ -184,4 +187,17 @@ contract('Initializable', function (accounts) {
       expect(await this.contract.child()).to.be.bignumber.equal(child);
     });
   });
+
+  describe('disabling initialization', function () {
+    it('old and new patterns in bad sequence', async function () {
+      await expectRevert(DisableBad1.new(), 'Initializable: contract is already initialized');
+      await expectRevert(DisableBad2.new(), 'Initializable: contract is initializing');
+    });
+
+    it('old and new patterns in good sequence', async function () {
+      const ok = await DisableOk.new();
+      await expectEvent.inConstruction(ok, 'Initialized', { version: '1' });
+      await expectEvent.inConstruction(ok, 'Initialized', { version: '255' });
+    });
+  });
 });

+ 2 - 2
test/token/ERC1155/ERC1155.behavior.js

@@ -293,7 +293,7 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder,
               this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount, '0x', {
                 from: proxy,
               }),
-              'ERC1155: caller is not owner nor approved',
+              'ERC1155: caller is not token owner nor approved',
             );
           });
         });
@@ -569,7 +569,7 @@ function shouldBehaveLikeERC1155 ([minter, firstTokenHolder, secondTokenHolder,
                 [firstAmount, secondAmount],
                 '0x', { from: proxy },
               ),
-              'ERC1155: transfer caller is not owner nor approved',
+              'ERC1155: caller is not token owner nor approved',
             );
           });
         });

+ 2 - 2
test/token/ERC1155/extensions/ERC1155Burnable.test.js

@@ -36,7 +36,7 @@ contract('ERC1155Burnable', function (accounts) {
     it('unapproved accounts cannot burn the holder\'s tokens', async function () {
       await expectRevert(
         this.token.burn(holder, tokenIds[0], amounts[0].subn(1), { from: other }),
-        'ERC1155: caller is not owner nor approved',
+        'ERC1155: caller is not token owner nor approved',
       );
     });
   });
@@ -60,7 +60,7 @@ contract('ERC1155Burnable', function (accounts) {
     it('unapproved accounts cannot burn the holder\'s tokens', async function () {
       await expectRevert(
         this.token.burnBatch(holder, tokenIds, [ amounts[0].subn(1), amounts[1].subn(2) ], { from: other }),
-        'ERC1155: caller is not owner nor approved',
+        'ERC1155: caller is not token owner nor approved',
       );
     });
   });

+ 612 - 0
test/token/ERC20/extensions/ERC20TokenizedVault.test.js

@@ -0,0 +1,612 @@
+const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
+const { expect } = require('chai');
+
+const ERC20DecimalsMock = artifacts.require('ERC20DecimalsMock');
+const ERC20TokenizedVaultMock = artifacts.require('ERC20TokenizedVaultMock');
+
+const parseToken = (token) => (new BN(token)).mul(new BN('1000000000000'));
+const parseShare = (share) => (new BN(share)).mul(new BN('1000000000000000000'));
+
+contract('ERC20TokenizedVault', function (accounts) {
+  const [ holder, recipient, spender, other, user1, user2 ] = accounts;
+
+  const name = 'My Token';
+  const symbol = 'MTKN';
+
+  beforeEach(async function () {
+    this.token = await ERC20DecimalsMock.new(name, symbol, 12);
+    this.vault = await ERC20TokenizedVaultMock.new(this.token.address, name + ' Vault', symbol + 'V');
+
+    await this.token.mint(holder, web3.utils.toWei('100'));
+    await this.token.approve(this.vault.address, constants.MAX_UINT256, { from: holder });
+    await this.vault.approve(spender, constants.MAX_UINT256, { from: holder });
+  });
+
+  it('metadata', async function () {
+    expect(await this.vault.name()).to.be.equal(name + ' Vault');
+    expect(await this.vault.symbol()).to.be.equal(symbol + 'V');
+    expect(await this.vault.asset()).to.be.equal(this.token.address);
+  });
+
+  describe('empty vault: no assets & no shares', function () {
+    it('status', async function () {
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal('0');
+    });
+
+    it('deposit', async function () {
+      expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
+      expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(1));
+
+      const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: holder,
+        to: this.vault.address,
+        value: parseToken(1),
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: recipient,
+        value: parseShare(1),
+      });
+    });
+
+    it('mint', async function () {
+      expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
+      expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1));
+
+      const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: holder,
+        to: this.vault.address,
+        value: parseToken(1),
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: recipient,
+        value: parseShare(1),
+      });
+    });
+
+    it('withdraw', async function () {
+      expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0');
+      expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0');
+
+      const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: recipient,
+        value: '0',
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: holder,
+        to: constants.ZERO_ADDRESS,
+        value: '0',
+      });
+    });
+
+    it('redeem', async function () {
+      expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0');
+      expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0');
+
+      const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: recipient,
+        value: '0',
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: holder,
+        to: constants.ZERO_ADDRESS,
+        value: '0',
+      });
+    });
+  });
+
+  describe('partially empty vault: assets & no shares', function () {
+    beforeEach(async function () {
+      await this.token.mint(this.vault.address, parseToken(1)); // 1 token
+    });
+
+    it('status', async function () {
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1));
+    });
+
+    it('deposit', async function () {
+      expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
+      expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(1));
+
+      const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: holder,
+        to: this.vault.address,
+        value: parseToken(1),
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: recipient,
+        value: parseShare(1),
+      });
+    });
+
+    it('mint', async function () {
+      expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
+      expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1));
+
+      const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: holder,
+        to: this.vault.address,
+        value: parseToken(1),
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: recipient,
+        value: parseShare(1),
+      });
+    });
+
+    it('withdraw', async function () {
+      expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0');
+      expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0');
+
+      const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: recipient,
+        value: '0',
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: holder,
+        to: constants.ZERO_ADDRESS,
+        value: '0',
+      });
+    });
+
+    it('redeem', async function () {
+      expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal('0');
+      expect(await this.vault.previewRedeem('0')).to.be.bignumber.equal('0');
+
+      const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: recipient,
+        value: '0',
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: holder,
+        to: constants.ZERO_ADDRESS,
+        value: '0',
+      });
+    });
+  });
+
+  describe('partially empty vault: shares & no assets', function () {
+    beforeEach(async function () {
+      await this.vault.mockMint(holder, parseShare(1)); // 1 share
+    });
+
+    it('status', async function () {
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal('0');
+    });
+
+    it('deposit', async function () {
+      expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal('0');
+
+      // Can deposit 0 (max deposit)
+      const { tx } = await this.vault.deposit(0, recipient, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: holder,
+        to: this.vault.address,
+        value: 0,
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: recipient,
+        value: 0,
+      });
+
+      // Cannot deposit more than 0
+      await expectRevert.unspecified(this.vault.previewDeposit(parseToken(1)));
+      await expectRevert(
+        this.vault.deposit(parseToken(1), recipient, { from: holder }),
+        'ERC20TokenizedVault: deposit more than max',
+      );
+    });
+
+    it('mint', async function () {
+      expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
+      expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal('0');
+
+      const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: holder,
+        to: this.vault.address,
+        value: '0',
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: recipient,
+        value: parseShare(1),
+      });
+    });
+
+    it('withdraw', async function () {
+      expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal('0');
+      expect(await this.vault.previewWithdraw('0')).to.be.bignumber.equal('0');
+      await expectRevert.unspecified(this.vault.previewWithdraw('1'));
+
+      const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: recipient,
+        value: '0',
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: holder,
+        to: constants.ZERO_ADDRESS,
+        value: '0',
+      });
+    });
+
+    it('redeem', async function () {
+      expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(parseShare(1));
+      expect(await this.vault.previewRedeem(parseShare(1))).to.be.bignumber.equal('0');
+
+      const { tx } = await this.vault.redeem(parseShare(1), recipient, holder, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: recipient,
+        value: '0',
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: holder,
+        to: constants.ZERO_ADDRESS,
+        value: parseShare(1),
+      });
+    });
+  });
+
+  describe('full vault: assets & shares', function () {
+    beforeEach(async function () {
+      await this.token.mint(this.vault.address, parseToken(1)); // 1 tokens
+      await this.vault.mockMint(holder, parseShare(100)); // 100 share
+    });
+
+    it('status', async function () {
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1));
+    });
+
+    it('deposit', async function () {
+      expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
+      expect(await this.vault.previewDeposit(parseToken(1))).to.be.bignumber.equal(parseShare(100));
+
+      const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: holder,
+        to: this.vault.address,
+        value: parseToken(1),
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: recipient,
+        value: parseShare(100),
+      });
+    });
+
+    it('mint', async function () {
+      expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256);
+      expect(await this.vault.previewMint(parseShare(1))).to.be.bignumber.equal(parseToken(1).divn(100));
+
+      const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: holder,
+        to: this.vault.address,
+        value: parseToken(1).divn(100),
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: recipient,
+        value: parseShare(1),
+      });
+    });
+
+    it('withdraw', async function () {
+      expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal(parseToken(1));
+      expect(await this.vault.previewWithdraw(parseToken(1))).to.be.bignumber.equal(parseShare(100));
+
+      const { tx } = await this.vault.withdraw(parseToken(1), recipient, holder, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: recipient,
+        value: parseToken(1),
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: holder,
+        to: constants.ZERO_ADDRESS,
+        value: parseShare(100),
+      });
+    });
+
+    it('withdraw with approval', async function () {
+      await expectRevert(
+        this.vault.withdraw(parseToken(1), recipient, holder, { from: other }),
+        'ERC20: insufficient allowance',
+      );
+
+      await this.vault.withdraw(parseToken(1), recipient, holder, { from: spender });
+    });
+
+    it('redeem', async function () {
+      expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(parseShare(100));
+      expect(await this.vault.previewRedeem(parseShare(100))).to.be.bignumber.equal(parseToken(1));
+
+      const { tx } = await this.vault.redeem(parseShare(100), recipient, holder, { from: holder });
+
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: recipient,
+        value: parseToken(1),
+      });
+
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: holder,
+        to: constants.ZERO_ADDRESS,
+        value: parseShare(100),
+      });
+    });
+
+    it('redeem with approval', async function () {
+      await expectRevert(
+        this.vault.redeem(parseShare(100), recipient, holder, { from: other }),
+        'ERC20: insufficient allowance',
+      );
+
+      await this.vault.redeem(parseShare(100), recipient, holder, { from: spender });
+    });
+  });
+
+  /// Scenario inspired by solmate ERC4626 tests:
+  /// https://github.com/Rari-Capital/solmate/blob/main/src/test/ERC4626.t.sol
+  it('multiple mint, deposit, redeem & withdrawal', async function () {
+    // test designed with both asset using similar decimals
+    this.token = await ERC20DecimalsMock.new(name, symbol, 18);
+    this.vault = await ERC20TokenizedVaultMock.new(this.token.address, name + ' Vault', symbol + 'V');
+
+    await this.token.mint(user1, 4000);
+    await this.token.mint(user2, 7001);
+    await this.token.approve(this.vault.address, 4000, { from: user1 });
+    await this.token.approve(this.vault.address, 7001, { from: user2 });
+
+    // 1. Alice mints 2000 shares (costs 2000 tokens)
+    {
+      const { tx } = await this.vault.mint(2000, user1, { from: user1 });
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: user1,
+        to: this.vault.address,
+        value: '2000',
+      });
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: user1,
+        value: '2000',
+      });
+
+      expect(await this.vault.previewDeposit(2000)).to.be.bignumber.equal('2000');
+      expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
+      expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
+      expect(await this.vault.totalSupply()).to.be.bignumber.equal('2000');
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal('2000');
+    }
+
+    // 2. Bob deposits 4000 tokens (mints 4000 shares)
+    {
+      const { tx } = await this.vault.mint(4000, user2, { from: user2 });
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: user2,
+        to: this.vault.address,
+        value: '4000',
+      });
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: user2,
+        value: '4000',
+      });
+
+      expect(await this.vault.previewDeposit(4000)).to.be.bignumber.equal('4000');
+      expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
+      expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2000');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('4000');
+      expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal('6000');
+    }
+
+    // 3. Vault mutates by +3000 tokens (simulated yield returned from strategy)
+    await this.token.mint(this.vault.address, 3000);
+
+    expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
+    expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
+    expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3000');
+    expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('6000');
+    expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000');
+    expect(await this.vault.totalAssets()).to.be.bignumber.equal('9000');
+
+    // 4. Alice deposits 2000 tokens (mints 1333 shares)
+    {
+      const { tx } = await this.vault.deposit(2000, user1, { from: user1 });
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: user1,
+        to: this.vault.address,
+        value: '2000',
+      });
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: user1,
+        value: '1333',
+      });
+
+      expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
+      expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4000');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('4999');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('6000');
+      expect(await this.vault.totalSupply()).to.be.bignumber.equal('7333');
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal('11000');
+    }
+
+    // 5. Bob mints 2000 shares (costs 3001 assets)
+    // NOTE: Bob's assets spent got rounded up
+    // NOTE: Alices's vault assets got rounded up
+    {
+      const { tx } = await this.vault.mint(2000, user2, { from: user2 });
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: user2,
+        to: this.vault.address,
+        value: '3001',
+      });
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: constants.ZERO_ADDRESS,
+        to: user2,
+        value: '2000',
+      });
+
+      expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
+      expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('5000');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('9000');
+      expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal('14001');
+    }
+
+    // 6. Vault mutates by +3000 tokens
+    // NOTE: Vault holds 17001 tokens, but sum of assetsOf() is 17000.
+    await this.token.mint(this.vault.address, 3000);
+
+    expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('3333');
+    expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
+    expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('6071');
+    expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10929');
+    expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333');
+    expect(await this.vault.totalAssets()).to.be.bignumber.equal('17001');
+
+    // 7. Alice redeem 1333 shares (2428 assets)
+    {
+      const { tx } = await this.vault.redeem(1333, user1, user1, { from: user1 });
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: user1,
+        to: constants.ZERO_ADDRESS,
+        value: '1333',
+      });
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: user1,
+        value: '2428',
+      });
+
+      expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
+      expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('6000');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10929');
+      expect(await this.vault.totalSupply()).to.be.bignumber.equal('8000');
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal('14573');
+    }
+
+    // 8. Bob withdraws 2929 assets (1608 shares)
+    {
+      const { tx } = await this.vault.withdraw(2929, user2, user2, { from: user2 });
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: user2,
+        to: constants.ZERO_ADDRESS,
+        value: '1608',
+      });
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: user2,
+        value: '2929',
+      });
+
+      expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('2000');
+      expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('3643');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000');
+      expect(await this.vault.totalSupply()).to.be.bignumber.equal('6392');
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal('11644');
+    }
+
+    // 9. Alice withdraws 3643 assets (2000 shares)
+    // NOTE: Bob's assets have been rounded back up
+    {
+      const { tx } = await this.vault.withdraw(3643, user1, user1, { from: user1 });
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: user1,
+        to: constants.ZERO_ADDRESS,
+        value: '2000',
+      });
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: user1,
+        value: '3643',
+      });
+
+      expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0');
+      expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('4392');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8001');
+      expect(await this.vault.totalSupply()).to.be.bignumber.equal('4392');
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal('8001');
+    }
+
+    // 10. Bob redeem 4392 shares (8001 tokens)
+    {
+      const { tx } = await this.vault.redeem(4392, user2, user2, { from: user2 });
+      expectEvent.inTransaction(tx, this.vault, 'Transfer', {
+        from: user2,
+        to: constants.ZERO_ADDRESS,
+        value: '4392',
+      });
+      expectEvent.inTransaction(tx, this.token, 'Transfer', {
+        from: this.vault.address,
+        to: user2,
+        value: '8001',
+      });
+
+      expect(await this.vault.balanceOf(user1)).to.be.bignumber.equal('0');
+      expect(await this.vault.balanceOf(user2)).to.be.bignumber.equal('0');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('0');
+      expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('0');
+      expect(await this.vault.totalSupply()).to.be.bignumber.equal('0');
+      expect(await this.vault.totalAssets()).to.be.bignumber.equal('0');
+    }
+  });
+});

+ 1 - 34
test/token/ERC20/extensions/ERC20Votes.test.js

@@ -8,11 +8,9 @@ const { fromRpcSig } = require('ethereumjs-util');
 const ethSigUtil = require('eth-sig-util');
 const Wallet = require('ethereumjs-wallet').default;
 
-const { promisify } = require('util');
-const queue = promisify(setImmediate);
-
 const ERC20VotesMock = artifacts.require('ERC20VotesMock');
 
+const { batchInBlock } = require('../../../helpers/txpool');
 const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712');
 
 const Delegation = [
@@ -21,37 +19,6 @@ const Delegation = [
   { name: 'expiry', type: 'uint256' },
 ];
 
-async function countPendingTransactions() {
-  return parseInt(
-    await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending'])
-  );
-}
-
-async function batchInBlock (txs) {
-  try {
-    // disable auto-mining
-    await network.provider.send('evm_setAutomine', [false]);
-    // send all transactions
-    const promises = txs.map(fn => fn());
-    // wait for node to have all pending transactions
-    while (txs.length > await countPendingTransactions()) {
-      await queue();
-    }
-    // mine one block
-    await network.provider.send('evm_mine');
-    // fetch receipts
-    const receipts = await Promise.all(promises);
-    // Sanity check, all tx should be in the same block
-    const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber));
-    expect(minedBlocks.size).to.equal(1);
-
-    return receipts;
-  } finally {
-    // enable auto-mining
-    await network.provider.send('evm_setAutomine', [true]);
-  }
-}
-
 contract('ERC20Votes', function (accounts) {
   const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts;
 

+ 1 - 34
test/token/ERC20/extensions/ERC20VotesComp.test.js

@@ -8,11 +8,9 @@ const { fromRpcSig } = require('ethereumjs-util');
 const ethSigUtil = require('eth-sig-util');
 const Wallet = require('ethereumjs-wallet').default;
 
-const { promisify } = require('util');
-const queue = promisify(setImmediate);
-
 const ERC20VotesCompMock = artifacts.require('ERC20VotesCompMock');
 
+const { batchInBlock } = require('../../../helpers/txpool');
 const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712');
 
 const Delegation = [
@@ -21,37 +19,6 @@ const Delegation = [
   { name: 'expiry', type: 'uint256' },
 ];
 
-async function countPendingTransactions() {
-  return parseInt(
-    await network.provider.send('eth_getBlockTransactionCountByNumber', ['pending'])
-  );
-}
-
-async function batchInBlock (txs) {
-  try {
-    // disable auto-mining
-    await network.provider.send('evm_setAutomine', [false]);
-    // send all transactions
-    const promises = txs.map(fn => fn());
-    // wait for node to have all pending transactions
-    while (txs.length > await countPendingTransactions()) {
-      await queue();
-    }
-    // mine one block
-    await network.provider.send('evm_mine');
-    // fetch receipts
-    const receipts = await Promise.all(promises);
-    // Sanity check, all tx should be in the same block
-    const minedBlocks = new Set(receipts.map(({ receipt }) => receipt.blockNumber));
-    expect(minedBlocks.size).to.equal(1);
-
-    return receipts;
-  } finally {
-    // enable auto-mining
-    await network.provider.send('evm_setAutomine', [true]);
-  }
-}
-
 contract('ERC20VotesComp', function (accounts) {
   const [ holder, recipient, holderDelegatee, recipientDelegatee, other1, other2 ] = accounts;
 

+ 13 - 13
test/token/ERC721/ERC721.behavior.js

@@ -66,7 +66,7 @@ function shouldBehaveLikeERC721 (errorPrefix, owner, newOwner, approved, another
 
         it('reverts', async function () {
           await expectRevert(
-            this.token.ownerOf(tokenId), 'ERC721: owner query for nonexistent token',
+            this.token.ownerOf(tokenId), 'ERC721: invalid token ID',
           );
         });
       });
@@ -192,7 +192,7 @@ function shouldBehaveLikeERC721 (errorPrefix, owner, newOwner, approved, another
           it('reverts', async function () {
             await expectRevert(
               transferFunction.call(this, owner, other, tokenId, { from: other }),
-              'ERC721: transfer caller is not owner nor approved',
+              'ERC721: caller is not token owner nor approved',
             );
           });
         });
@@ -201,7 +201,7 @@ function shouldBehaveLikeERC721 (errorPrefix, owner, newOwner, approved, another
           it('reverts', async function () {
             await expectRevert(
               transferFunction.call(this, owner, other, nonExistentTokenId, { from: owner }),
-              'ERC721: operator query for nonexistent token',
+              'ERC721: invalid token ID',
             );
           });
         });
@@ -276,7 +276,7 @@ function shouldBehaveLikeERC721 (errorPrefix, owner, newOwner, approved, another
                     nonExistentTokenId,
                     { from: owner },
                   ),
-                  'ERC721: operator query for nonexistent token',
+                  'ERC721: invalid token ID',
                 );
               });
             });
@@ -509,7 +509,7 @@ function shouldBehaveLikeERC721 (errorPrefix, owner, newOwner, approved, another
       context('when the sender does not own the given token ID', function () {
         it('reverts', async function () {
           await expectRevert(this.token.approve(approved, tokenId, { from: other }),
-            'ERC721: approve caller is not owner nor approved');
+            'ERC721: approve caller is not token owner nor approved');
         });
       });
 
@@ -517,7 +517,7 @@ function shouldBehaveLikeERC721 (errorPrefix, owner, newOwner, approved, another
         it('reverts', async function () {
           await this.token.approve(approved, tokenId, { from: owner });
           await expectRevert(this.token.approve(anotherApproved, tokenId, { from: approved }),
-            'ERC721: approve caller is not owner nor approved for all');
+            'ERC721: approve caller is not token owner nor approved for all');
         });
       });
 
@@ -534,7 +534,7 @@ function shouldBehaveLikeERC721 (errorPrefix, owner, newOwner, approved, another
       context('when the given token ID does not exist', function () {
         it('reverts', async function () {
           await expectRevert(this.token.approve(approved, nonExistentTokenId, { from: operator }),
-            'ERC721: owner query for nonexistent token');
+            'ERC721: invalid token ID');
         });
       });
     });
@@ -623,7 +623,7 @@ function shouldBehaveLikeERC721 (errorPrefix, owner, newOwner, approved, another
         it('reverts', async function () {
           await expectRevert(
             this.token.getApproved(nonExistentTokenId),
-            'ERC721: approved query for nonexistent token',
+            'ERC721: invalid token ID',
           );
         });
       });
@@ -678,7 +678,7 @@ function shouldBehaveLikeERC721 (errorPrefix, owner, newOwner, approved, another
   describe('_burn', function () {
     it('reverts when burning a non-existent token id', async function () {
       await expectRevert(
-        this.token.burn(nonExistentTokenId), 'ERC721: owner query for nonexistent token',
+        this.token.burn(nonExistentTokenId), 'ERC721: invalid token ID',
       );
     });
 
@@ -704,13 +704,13 @@ function shouldBehaveLikeERC721 (errorPrefix, owner, newOwner, approved, another
         it('deletes the token', async function () {
           expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1');
           await expectRevert(
-            this.token.ownerOf(firstTokenId), 'ERC721: owner query for nonexistent token',
+            this.token.ownerOf(firstTokenId), 'ERC721: invalid token ID',
           );
         });
 
         it('reverts when burning a token id that has been deleted', async function () {
           await expectRevert(
-            this.token.burn(firstTokenId), 'ERC721: owner query for nonexistent token',
+            this.token.burn(firstTokenId), 'ERC721: invalid token ID',
           );
         });
       });
@@ -846,7 +846,7 @@ function shouldBehaveLikeERC721Enumerable (errorPrefix, owner, newOwner, approve
   describe('_burn', function () {
     it('reverts when burning a non-existent token id', async function () {
       await expectRevert(
-        this.token.burn(firstTokenId), 'ERC721: owner query for nonexistent token',
+        this.token.burn(firstTokenId), 'ERC721: invalid token ID',
       );
     });
 
@@ -906,7 +906,7 @@ function shouldBehaveLikeERC721Metadata (errorPrefix, name, symbol, owner) {
 
       it('reverts when queried for non existent token id', async function () {
         await expectRevert(
-          this.token.tokenURI(nonExistentTokenId), 'ERC721Metadata: URI query for nonexistent token',
+          this.token.tokenURI(nonExistentTokenId), 'ERC721: invalid token ID',
         );
       });
 

+ 3 - 3
test/token/ERC721/extensions/ERC721Burnable.test.js

@@ -37,7 +37,7 @@ contract('ERC721Burnable', function (accounts) {
         it('burns the given token ID and adjusts the balance of the owner', async function () {
           await expectRevert(
             this.token.ownerOf(tokenId),
-            'ERC721: owner query for nonexistent token',
+            'ERC721: invalid token ID',
           );
           expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1');
         });
@@ -60,7 +60,7 @@ contract('ERC721Burnable', function (accounts) {
         context('getApproved', function () {
           it('reverts', async function () {
             await expectRevert(
-              this.token.getApproved(tokenId), 'ERC721: approved query for nonexistent token',
+              this.token.getApproved(tokenId), 'ERC721: invalid token ID',
             );
           });
         });
@@ -69,7 +69,7 @@ contract('ERC721Burnable', function (accounts) {
       describe('when the given token ID was not tracked by this contract', function () {
         it('reverts', async function () {
           await expectRevert(
-            this.token.burn(unknownTokenId, { from: owner }), 'ERC721: operator query for nonexistent token',
+            this.token.burn(unknownTokenId, { from: owner }), 'ERC721: invalid token ID',
           );
         });
       });

+ 3 - 3
test/token/ERC721/extensions/ERC721URIStorage.test.js

@@ -31,7 +31,7 @@ contract('ERC721URIStorage', function (accounts) {
 
     it('reverts when queried for non existent token id', async function () {
       await expectRevert(
-        this.token.tokenURI(nonExistentTokenId), 'ERC721URIStorage: URI query for nonexistent token',
+        this.token.tokenURI(nonExistentTokenId), 'ERC721: invalid token ID',
       );
     });
 
@@ -78,7 +78,7 @@ contract('ERC721URIStorage', function (accounts) {
 
       expect(await this.token.exists(firstTokenId)).to.equal(false);
       await expectRevert(
-        this.token.tokenURI(firstTokenId), 'ERC721URIStorage: URI query for nonexistent token',
+        this.token.tokenURI(firstTokenId), 'ERC721: invalid token ID',
       );
     });
 
@@ -89,7 +89,7 @@ contract('ERC721URIStorage', function (accounts) {
 
       expect(await this.token.exists(firstTokenId)).to.equal(false);
       await expectRevert(
-        this.token.tokenURI(firstTokenId), 'ERC721URIStorage: URI query for nonexistent token',
+        this.token.tokenURI(firstTokenId), 'ERC721: invalid token ID',
       );
     });
   });

+ 1 - 1
test/token/common/ERC2981.behavior.js

@@ -66,7 +66,7 @@ function shouldBehaveLikeERC2981 () {
       );
 
       await expectRevert(
-        this.token.setTokenRoyalty(this.tokenId1, this.account1, new BN('11000')),
+        this.token.setDefaultRoyalty(this.account1, new BN('11000')),
         'ERC2981: royalty fee will exceed salePrice',
       );
     });

+ 4 - 0
test/utils/Base64.test.js

@@ -25,5 +25,9 @@ contract('Strings', function () {
       const input = web3.utils.asciiToHex(TEST_MESSAGE);
       expect(await this.base64.encode(input)).to.equal('dGVzdDEy');
     });
+
+    it('empty bytes', async function () {
+      expect(await this.base64.encode([])).to.equal('');
+    });
   });
 });

+ 15 - 0
test/utils/Checkpoints.test.js

@@ -2,6 +2,8 @@ const { expectRevert, time } = require('@openzeppelin/test-helpers');
 
 const { expect } = require('chai');
 
+const { batchInBlock } = require('../helpers/txpool');
+
 const CheckpointsImpl = artifacts.require('CheckpointsImpl');
 
 contract('Checkpoints', function (accounts) {
@@ -55,5 +57,18 @@ contract('Checkpoints', function (accounts) {
         'Checkpoints: block not yet mined',
       );
     });
+
+    it('multiple checkpoints in the same block', async function () {
+      const lengthBefore = await this.checkpoint.length();
+      await batchInBlock([
+        () => this.checkpoint.push(8, { gas: 100000 }),
+        () => this.checkpoint.push(9, { gas: 100000 }),
+        () => this.checkpoint.push(10, { gas: 100000 }),
+      ]);
+      const lengthAfter = await this.checkpoint.length();
+
+      expect(lengthAfter.toNumber()).to.be.equal(lengthBefore.toNumber() + 1);
+      expect(await this.checkpoint.latest()).to.be.bignumber.equal('10');
+    });
   });
 });

+ 32 - 8
test/utils/cryptography/MerkleProof.test.js

@@ -25,12 +25,14 @@ contract('MerkleProof', function (accounts) {
       const proof = merkleTree.getHexProof(leaf);
 
       expect(await this.merkleProof.verify(proof, root, leaf)).to.equal(true);
+      expect(await this.merkleProof.verifyCalldata(proof, root, leaf)).to.equal(true);
 
       // For demonstration, it is also possible to create valid proofs for certain 64-byte values *not* in elements:
       const noSuchLeaf = keccak256(
         Buffer.concat([keccak256(elements[0]), keccak256(elements[1])].sort(Buffer.compare)),
       );
       expect(await this.merkleProof.verify(proof.slice(1), root, noSuchLeaf)).to.equal(true);
+      expect(await this.merkleProof.verifyCalldata(proof.slice(1), root, noSuchLeaf)).to.equal(true);
     });
 
     it('returns false for an invalid Merkle proof', async function () {
@@ -47,6 +49,7 @@ contract('MerkleProof', function (accounts) {
       const badProof = badMerkleTree.getHexProof(badElements[0]);
 
       expect(await this.merkleProof.verify(badProof, correctRoot, correctLeaf)).to.equal(false);
+      expect(await this.merkleProof.verifyCalldata(badProof, correctRoot, correctLeaf)).to.equal(false);
     });
 
     it('returns false for a Merkle proof of invalid length', async function () {
@@ -61,6 +64,7 @@ contract('MerkleProof', function (accounts) {
       const badProof = proof.slice(0, proof.length - 5);
 
       expect(await this.merkleProof.verify(badProof, root, leaf)).to.equal(false);
+      expect(await this.merkleProof.verifyCalldata(badProof, root, leaf)).to.equal(false);
     });
   });
 
@@ -74,7 +78,7 @@ contract('MerkleProof', function (accounts) {
       const proof = merkleTree.getMultiProof(proofLeaves);
       const proofFlags = merkleTree.getProofFlags(proofLeaves, proof);
 
-      expect(await this.merkleProof.multiProofVerify(root, proofLeaves, proof, proofFlags)).to.equal(true);
+      expect(await this.merkleProof.multiProofVerify(proof, proofFlags, root, proofLeaves)).to.equal(true);
     });
 
     it('returns false for an invalid Merkle multi proof', async function () {
@@ -87,23 +91,23 @@ contract('MerkleProof', function (accounts) {
       const badProof = badMerkleTree.getMultiProof(badProofLeaves);
       const badProofFlags = badMerkleTree.getProofFlags(badProofLeaves, badProof);
 
-      expect(await this.merkleProof.multiProofVerify(root, badProofLeaves, badProof, badProofFlags)).to.equal(false);
+      expect(await this.merkleProof.multiProofVerify(badProof, badProofFlags, root, badProofLeaves)).to.equal(false);
     });
 
     it('revert with invalid multi proof #1', async function () {
       const fill = Buffer.alloc(32); // This could be anything, we are reconstructing a fake branch
       const leaves = ['a', 'b', 'c', 'd'].map(keccak256).sort(Buffer.compare);
-      const badLeave = keccak256('e');
+      const badLeaf = keccak256('e');
       const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
 
       const root = merkleTree.getRoot();
 
       await expectRevert(
         this.merkleProof.multiProofVerify(
-          root,
-          [ leaves[0], badLeave ], // A, E
           [ leaves[1], fill, merkleTree.layers[1][1] ],
           [ false, false, false ],
+          root,
+          [ leaves[0], badLeaf ], // A, E
         ),
         'MerkleProof: invalid multiproof',
       );
@@ -112,20 +116,40 @@ contract('MerkleProof', function (accounts) {
     it('revert with invalid multi proof #2', async function () {
       const fill = Buffer.alloc(32); // This could be anything, we are reconstructing a fake branch
       const leaves = ['a', 'b', 'c', 'd'].map(keccak256).sort(Buffer.compare);
-      const badLeave = keccak256('e');
+      const badLeaf = keccak256('e');
       const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
 
       const root = merkleTree.getRoot();
 
       await expectRevert(
         this.merkleProof.multiProofVerify(
-          root,
-          [ badLeave, leaves[0] ], // A, E
           [ leaves[1], fill, merkleTree.layers[1][1] ],
           [ false, false, false, false ],
+          root,
+          [ badLeaf, leaves[0] ], // A, E
         ),
         'reverted with panic code 0x32',
       );
     });
+
+    it('limit case: works for tree containing a single leaf', async function () {
+      const leaves = ['a'].map(keccak256).sort(Buffer.compare);
+      const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
+
+      const root = merkleTree.getRoot();
+      const proofLeaves = ['a'].map(keccak256).sort(Buffer.compare);
+      const proof = merkleTree.getMultiProof(proofLeaves);
+      const proofFlags = merkleTree.getProofFlags(proofLeaves, proof);
+
+      expect(await this.merkleProof.multiProofVerify(proof, proofFlags, root, proofLeaves)).to.equal(true);
+    });
+
+    it('limit case: can prove empty leaves', async function () {
+      const leaves = ['a', 'b', 'c', 'd'].map(keccak256).sort(Buffer.compare);
+      const merkleTree = new MerkleTree(leaves, keccak256, { sort: true });
+
+      const root = merkleTree.getRoot();
+      expect(await this.merkleProof.multiProofVerify([ root ], [], root, [])).to.equal(true);
+    });
   });
 });

+ 98 - 1
test/utils/math/Math.test.js

@@ -1,12 +1,15 @@
-const { BN, constants } = require('@openzeppelin/test-helpers');
+const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers');
 const { expect } = require('chai');
 const { MAX_UINT256 } = constants;
+const { Rounding } = require('../../helpers/enums.js');
 
 const MathMock = artifacts.require('MathMock');
 
 contract('Math', function (accounts) {
   const min = new BN('1234');
   const max = new BN('5678');
+  const MAX_UINT256_SUB1 = MAX_UINT256.sub(new BN('1'));
+  const MAX_UINT256_SUB2 = MAX_UINT256.sub(new BN('2'));
 
   beforeEach(async function () {
     this.math = await MathMock.new();
@@ -85,4 +88,98 @@ contract('Math', function (accounts) {
       expect(await this.math.ceilDiv(MAX_UINT256, b)).to.be.bignumber.equal(MAX_UINT256);
     });
   });
+
+  describe('muldiv', function () {
+    it('divide by 0', async function () {
+      await expectRevert.unspecified(this.math.mulDiv(1, 1, 0, Rounding.Down));
+    });
+
+    describe('does round down', async function () {
+      it('small values', async function () {
+        expect(await this.math.mulDiv('3', '4', '5', Rounding.Down)).to.be.bignumber.equal('2');
+        expect(await this.math.mulDiv('3', '5', '5', Rounding.Down)).to.be.bignumber.equal('3');
+      });
+
+      it('large values', async function () {
+        expect(await this.math.mulDiv(
+          new BN('42'),
+          MAX_UINT256_SUB1,
+          MAX_UINT256,
+          Rounding.Down,
+        )).to.be.bignumber.equal(new BN('41'));
+
+        expect(await this.math.mulDiv(
+          new BN('17'),
+          MAX_UINT256,
+          MAX_UINT256,
+          Rounding.Down,
+        )).to.be.bignumber.equal(new BN('17'));
+
+        expect(await this.math.mulDiv(
+          MAX_UINT256_SUB1,
+          MAX_UINT256_SUB1,
+          MAX_UINT256,
+          Rounding.Down,
+        )).to.be.bignumber.equal(MAX_UINT256_SUB2);
+
+        expect(await this.math.mulDiv(
+          MAX_UINT256,
+          MAX_UINT256_SUB1,
+          MAX_UINT256,
+          Rounding.Down,
+        )).to.be.bignumber.equal(MAX_UINT256_SUB1);
+
+        expect(await this.math.mulDiv(
+          MAX_UINT256,
+          MAX_UINT256,
+          MAX_UINT256,
+          Rounding.Down,
+        )).to.be.bignumber.equal(MAX_UINT256);
+      });
+    });
+
+    describe('does round up', async function () {
+      it('small values', async function () {
+        expect(await this.math.mulDiv('3', '4', '5', Rounding.Up)).to.be.bignumber.equal('3');
+        expect(await this.math.mulDiv('3', '5', '5', Rounding.Up)).to.be.bignumber.equal('3');
+      });
+
+      it('large values', async function () {
+        expect(await this.math.mulDiv(
+          new BN('42'),
+          MAX_UINT256_SUB1,
+          MAX_UINT256,
+          Rounding.Up,
+        )).to.be.bignumber.equal(new BN('42'));
+
+        expect(await this.math.mulDiv(
+          new BN('17'),
+          MAX_UINT256,
+          MAX_UINT256,
+          Rounding.Up,
+        )).to.be.bignumber.equal(new BN('17'));
+
+        expect(await this.math.mulDiv(
+          MAX_UINT256_SUB1,
+          MAX_UINT256_SUB1,
+          MAX_UINT256,
+          Rounding.Up,
+        )).to.be.bignumber.equal(MAX_UINT256_SUB1);
+
+        expect(await this.math.mulDiv(
+          MAX_UINT256,
+          MAX_UINT256_SUB1,
+          MAX_UINT256,
+          Rounding.Up,
+        )).to.be.bignumber.equal(MAX_UINT256_SUB1);
+
+        expect(await this.math.mulDiv(
+          MAX_UINT256,
+          MAX_UINT256,
+          MAX_UINT256,
+          Rounding.Up,
+        )).to.be.bignumber.equal(MAX_UINT256);
+      });
+    });
+  });
 });

Some files were not shown because too many files changed in this diff