diff --git a/.changeset/beige-buses-drop.md b/.changeset/beige-buses-drop.md deleted file mode 100644 index 4566eccb0..000000000 --- a/.changeset/beige-buses-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Initializable`: optimize `_disableInitializers` by using `!=` instead of `<`. ([#3787](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3787)) diff --git a/.changeset/big-plums-cover.md b/.changeset/big-plums-cover.md new file mode 100644 index 000000000..411156253 --- /dev/null +++ b/.changeset/big-plums-cover.md @@ -0,0 +1,4 @@ +--- +'openzeppelin-solidity': major +--- +Use `abi.encodeCall` in place of `abi.encodeWithSelector` and `abi.encodeWithSignature` for improved type-checking of parameters diff --git a/.changeset/blue-scissors-design.md b/.changeset/blue-scissors-design.md new file mode 100644 index 000000000..c2f815aae --- /dev/null +++ b/.changeset/blue-scissors-design.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Math`: Make `ceilDiv` to revert on 0 division even if the numerator is 0 diff --git a/.changeset/clever-pumas-beg.md b/.changeset/clever-pumas-beg.md new file mode 100644 index 000000000..5f1f4b13b --- /dev/null +++ b/.changeset/clever-pumas-beg.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`Ownable`: Add an `initialOwner` parameter to the constructor, making the ownership initialization explicit. diff --git a/.changeset/curvy-shrimps-enjoy.md b/.changeset/curvy-shrimps-enjoy.md deleted file mode 100644 index 4bc410abf..000000000 --- a/.changeset/curvy-shrimps-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ReentrancyGuard`: Add a `_reentrancyGuardEntered` function to expose the guard status. ([#3714](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3714)) diff --git a/.changeset/curvy-suns-sort.md b/.changeset/curvy-suns-sort.md deleted file mode 100644 index 97b51fed7..000000000 --- a/.changeset/curvy-suns-sort.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Ownable2Step`: make `acceptOwnership` public virtual to enable usecases that require overriding it. ([#3960](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3960)) diff --git a/.changeset/dirty-mangos-sort.md b/.changeset/dirty-mangos-sort.md deleted file mode 100644 index 6981399c0..000000000 --- a/.changeset/dirty-mangos-sort.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -Removed presets in favor of [OpenZeppelin Contracts Wizard](https://wizard.openzeppelin.com/). diff --git a/.changeset/eighty-crabs-listen.md b/.changeset/eighty-crabs-listen.md new file mode 100644 index 000000000..7de904db8 --- /dev/null +++ b/.changeset/eighty-crabs-listen.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +Optimize `Strings.equal` diff --git a/.changeset/famous-rules-burn.md b/.changeset/famous-rules-burn.md deleted file mode 100644 index a746dc21d..000000000 --- a/.changeset/famous-rules-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`EnumerableMap`: add a `keys()` function that returns an array containing all the keys. ([#3920](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3920)) diff --git a/.changeset/flat-bottles-wonder.md b/.changeset/flat-bottles-wonder.md new file mode 100644 index 000000000..099ea8339 --- /dev/null +++ b/.changeset/flat-bottles-wonder.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +Replace some uses of `abi.encodePacked` with clearer alternatives (e.g. `bytes.concat`, `string.concat`). diff --git a/.changeset/four-adults-knock.md b/.changeset/four-adults-knock.md new file mode 100644 index 000000000..f6f566d7a --- /dev/null +++ b/.changeset/four-adults-knock.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ECDSA`: Use unchecked arithmetic for the `tryRecover` function that receives the `r` and `vs` short-signature fields separately. diff --git a/.changeset/fresh-birds-kiss.md b/.changeset/fresh-birds-kiss.md new file mode 100644 index 000000000..221f54cdf --- /dev/null +++ b/.changeset/fresh-birds-kiss.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`Checkpoints`: library moved from `utils` to `utils/structs` diff --git a/.changeset/funny-rockets-compete.md b/.changeset/funny-rockets-compete.md deleted file mode 100644 index a8c77c619..000000000 --- a/.changeset/funny-rockets-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -Reformatted codebase with latest version of Prettier Solidity. ([#3898](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3898)) diff --git a/.changeset/gold-chicken-clean.md b/.changeset/gold-chicken-clean.md deleted file mode 100644 index 0d64fde6d..000000000 --- a/.changeset/gold-chicken-clean.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Strings`: add `equal` method. ([#3774](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3774)) diff --git a/.changeset/grumpy-bulldogs-call.md b/.changeset/grumpy-bulldogs-call.md new file mode 100644 index 000000000..c034587f3 --- /dev/null +++ b/.changeset/grumpy-bulldogs-call.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`Governor`: Optimized use of storage for proposal data diff --git a/.changeset/grumpy-worms-tease.md b/.changeset/grumpy-worms-tease.md new file mode 100644 index 000000000..910b996c6 --- /dev/null +++ b/.changeset/grumpy-worms-tease.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`ERC1967Utils`: Refactor the `ERC1967Upgrade` abstract contract as a library. diff --git a/.changeset/happy-falcons-walk.md b/.changeset/happy-falcons-walk.md new file mode 100644 index 000000000..bba9642aa --- /dev/null +++ b/.changeset/happy-falcons-walk.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`TransparentUpgradeableProxy`: Admin is now stored in an immutable variable (set during construction) to avoid unnecessary storage reads on every proxy call. This removed the ability to ever change the admin. Transfer of the upgrade capability is exclusively handled through the ownership of the `ProxyAdmin`. diff --git a/.changeset/healthy-squids-stare.md b/.changeset/healthy-squids-stare.md deleted file mode 100644 index fad0872e2..000000000 --- a/.changeset/healthy-squids-stare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Math`: optimize `log256` rounding check. ([#3745](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3745)) diff --git a/.changeset/heavy-drinks-fail.md b/.changeset/heavy-drinks-fail.md new file mode 100644 index 000000000..bbe93ca90 --- /dev/null +++ b/.changeset/heavy-drinks-fail.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`ERC20`: Remove `Approval` event previously emitted in `transferFrom` to indicate that part of the allowance was consumed. With this change, allowances are no longer reconstructible from events. See the code for guidelines on how to re-enable this event if needed. diff --git a/.changeset/hip-beds-provide.md b/.changeset/hip-beds-provide.md new file mode 100644 index 000000000..c67283813 --- /dev/null +++ b/.changeset/hip-beds-provide.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Move the logic to validate ERC-1822 during an upgrade from `ERC1967Utils` to `UUPSUpgradeable`. diff --git a/.changeset/hot-coins-judge.md b/.changeset/hot-coins-judge.md new file mode 100644 index 000000000..e544af467 --- /dev/null +++ b/.changeset/hot-coins-judge.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Arrays`: Add `unsafeMemoryAccess` helpers to read from a memory array without checking the length. diff --git a/.changeset/hot-plums-approve.md b/.changeset/hot-plums-approve.md new file mode 100644 index 000000000..131559027 --- /dev/null +++ b/.changeset/hot-plums-approve.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`GovernorTimelockControl`: Clean up timelock id on execution for gas refund. diff --git a/.changeset/hungry-impalas-perform.md b/.changeset/hungry-impalas-perform.md deleted file mode 100644 index 24901ec86..000000000 --- a/.changeset/hungry-impalas-perform.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': major ---- - -`ERC165Storage`: Removed this contract in favor of inheritance based approach. ([#3880](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3880)) diff --git a/.changeset/lemon-dogs-kiss.md b/.changeset/lemon-dogs-kiss.md deleted file mode 100644 index 976949d2c..000000000 --- a/.changeset/lemon-dogs-kiss.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`ERC20Votes`: optimize by using unchecked arithmetic. ([#3748](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3748)) diff --git a/.changeset/little-falcons-build.md b/.changeset/little-falcons-build.md new file mode 100644 index 000000000..b310a8ae6 --- /dev/null +++ b/.changeset/little-falcons-build.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`EIP712`: Add internal getters for the name and version strings diff --git a/.changeset/little-kiwis-ring.md b/.changeset/little-kiwis-ring.md deleted file mode 100644 index a1cb7bb95..000000000 --- a/.changeset/little-kiwis-ring.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Multicall`: annotate `multicall` function as upgrade safe to not raise a flag for its delegatecall. ([#3961](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3961)) diff --git a/.changeset/lovely-geckos-hide.md b/.changeset/lovely-geckos-hide.md new file mode 100644 index 000000000..1fbcb2077 --- /dev/null +++ b/.changeset/lovely-geckos-hide.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Replace revert strings and require statements with custom errors. diff --git a/.changeset/mighty-donuts-smile.md b/.changeset/mighty-donuts-smile.md new file mode 100644 index 000000000..5885a7370 --- /dev/null +++ b/.changeset/mighty-donuts-smile.md @@ -0,0 +1,6 @@ +--- +'openzeppelin-solidity': patch +--- + +`Governor`: Add validation in ERC1155 and ERC721 receiver hooks to ensure Governor is the executor. + diff --git a/.changeset/pretty-hornets-play.md b/.changeset/pretty-hornets-play.md deleted file mode 100644 index 230a53bb2..000000000 --- a/.changeset/pretty-hornets-play.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Strings`: add `toString` method for signed integers. ([#3773](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3773)) diff --git a/.changeset/red-dots-fold.md b/.changeset/red-dots-fold.md new file mode 100644 index 000000000..08cc77843 --- /dev/null +++ b/.changeset/red-dots-fold.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Overrides are now used internally for a number of functions that were previously hardcoded to their default implementation in certain locations: `ERC1155Supply.totalSupply`, `ERC721.ownerOf`, `ERC721.balanceOf` and `ERC721.totalSupply` in `ERC721Enumerable`, `ERC20.totalSupply` in `ERC20FlashMint`, and `ERC1967._getImplementation` in `ERC1967Proxy`. diff --git a/.changeset/serious-books-lie.md b/.changeset/serious-books-lie.md new file mode 100644 index 000000000..6f0a0a732 --- /dev/null +++ b/.changeset/serious-books-lie.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC1155`: Optimize array allocation. diff --git a/.changeset/short-eels-enjoy.md b/.changeset/short-eels-enjoy.md new file mode 100644 index 000000000..b7d74c1a2 --- /dev/null +++ b/.changeset/short-eels-enjoy.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +Bump minimum compiler version required to 0.8.19 diff --git a/.changeset/silly-bees-beam.md b/.changeset/silly-bees-beam.md index 6be23768c..0f4f40507 100644 --- a/.changeset/silly-bees-beam.md +++ b/.changeset/silly-bees-beam.md @@ -2,4 +2,6 @@ 'openzeppelin-solidity': major --- -`ERC20Votes`: Changed internal vote accounting to reusable `Votes` module previously used by `ERC721Votes`. Removed implicit `ERC20Permit` inheritance. [#3816](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3816) +`ERC20Votes`: Changed internal vote accounting to reusable `Votes` module previously used by `ERC721Votes`. Removed implicit `ERC20Permit` inheritance. Note that the `DOMAIN_SEPARATOR` getter was previously guaranteed to be available for `ERC20Votes` contracts, but is no longer available unless `ERC20Permit` is explicitly used; ERC-5267 support is included in `ERC20Votes` with `EIP712` and is recommended as an alternative. + +pr: #3816 diff --git a/.changeset/smooth-books-wink.md b/.changeset/smooth-books-wink.md new file mode 100644 index 000000000..e5eb3fbeb --- /dev/null +++ b/.changeset/smooth-books-wink.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`ERC1155`: Remove check for address zero in `balanceOf`. diff --git a/.changeset/spotty-hotels-type.md b/.changeset/spotty-hotels-type.md new file mode 100644 index 000000000..866d8fc02 --- /dev/null +++ b/.changeset/spotty-hotels-type.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC721Consecutive`: Add a `_firstConsecutiveId` internal function that can be overridden to change the id of the first token minted through `_mintConsecutive`. diff --git a/.changeset/swift-bags-divide.md b/.changeset/swift-bags-divide.md new file mode 100644 index 000000000..9af63e98e --- /dev/null +++ b/.changeset/swift-bags-divide.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`Governor`: Add a mechanism to restrict the address of the proposer using a suffix in the description. diff --git a/.changeset/tame-ladybugs-sit.md b/.changeset/tame-ladybugs-sit.md deleted file mode 100644 index 8a1e416de..000000000 --- a/.changeset/tame-ladybugs-sit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`MerkleProof`: optimize by using unchecked arithmetic. ([#3745](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3745)) diff --git a/.changeset/tasty-tomatoes-turn.md b/.changeset/tasty-tomatoes-turn.md new file mode 100644 index 000000000..3fe46a9b1 --- /dev/null +++ b/.changeset/tasty-tomatoes-turn.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`Strings`: Rename `toString(int256)` to `toStringSigned(int256)`. diff --git a/.changeset/thin-camels-matter.md b/.changeset/thin-camels-matter.md new file mode 100644 index 000000000..c832b1163 --- /dev/null +++ b/.changeset/thin-camels-matter.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC1155`: Bubble errors triggered in the `onERC1155Received` and `onERC1155BatchReceived` hooks. diff --git a/.changeset/tough-drinks-hammer.md b/.changeset/tough-drinks-hammer.md new file mode 100644 index 000000000..51b3836e4 --- /dev/null +++ b/.changeset/tough-drinks-hammer.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC1155`: Optimize array accesses by skipping bounds checking when unnecessary. diff --git a/.changeset/unlucky-snakes-drive.md b/.changeset/unlucky-snakes-drive.md deleted file mode 100644 index d4c78dd50..000000000 --- a/.changeset/unlucky-snakes-drive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Address`: Removed `isContract` because of its ambiguous nature and potential for misuse. diff --git a/.changeset/violet-dancers-cough.md b/.changeset/violet-dancers-cough.md new file mode 100644 index 000000000..c6160d287 --- /dev/null +++ b/.changeset/violet-dancers-cough.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +Remove the `override` specifier from functions that only override a single interface function. diff --git a/.changeset/wild-windows-trade.md b/.changeset/wild-windows-trade.md new file mode 100644 index 000000000..f599d0fcb --- /dev/null +++ b/.changeset/wild-windows-trade.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': major +--- + +`SafeERC20`: Refactor `safeDecreaseAllowance` and `safeIncreaseAllowance` to support USDT-like tokens. diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 52031de51..000000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.sol linguist-language=Solidity diff --git a/.github/actions/storage-layout/action.yml b/.github/actions/storage-layout/action.yml new file mode 100644 index 000000000..fdfd5a25b --- /dev/null +++ b/.github/actions/storage-layout/action.yml @@ -0,0 +1,55 @@ +name: Compare storage layouts +inputs: + token: + description: github token + required: true + buildinfo: + description: compilation artifacts + required: false + default: artifacts/build-info/*.json + layout: + description: extracted storage layout + required: false + default: HEAD.layout.json + out_layout: + description: storage layout to upload + required: false + default: ${{ github.ref_name }}.layout.json + ref_layout: + description: storage layout for the reference branch + required: false + default: ${{ github.base_ref }}.layout.json + +runs: + using: composite + steps: + - name: Extract layout + run: | + node scripts/checks/extract-layout.js ${{ inputs.buildinfo }} > ${{ inputs.layout }} + shell: bash + - name: Download reference + if: github.event_name == 'pull_request' + run: | + RUN_ID=`gh run list --repo ${{ github.repository }} --branch ${{ github.base_ref }} --workflow ${{ github.workflow }} --limit 100 --json 'conclusion,databaseId,event' --jq 'map(select(.conclusion=="success" and .event!="pull_request"))[0].databaseId'` + gh run download ${RUN_ID} --repo ${{ github.repository }} -n layout + env: + GITHUB_TOKEN: ${{ inputs.token }} + shell: bash + continue-on-error: true + id: reference + - name: Compare layouts + if: steps.reference.outcome == 'success' && github.event_name == 'pull_request' + run: | + node scripts/checks/compare-layout.js --head ${{ inputs.layout }} --ref ${{ inputs.ref_layout }} + shell: bash + - name: Rename artifacts for upload + if: github.event_name != 'pull_request' + run: | + mv ${{ inputs.layout }} ${{ inputs.out_layout }} + shell: bash + - name: Save artifacts + if: github.event_name != 'pull_request' + uses: actions/upload-artifact@v3 + with: + name: layout + path: ${{ inputs.out_layout }} diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 615ecb36d..122d39564 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -13,9 +13,11 @@ concurrency: group: checks-${{ github.ref }} cancel-in-progress: true +env: + NODE_OPTIONS: --max_old_space_size=5120 + jobs: lint: - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -37,15 +39,34 @@ jobs: - name: Check linearisation of the inheritance graph run: npm run test:inheritance - name: Check proceduraly generated contracts are up-to-date - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' run: npm run test:generation - name: Compare gas costs uses: ./.github/actions/gas-compare with: token: ${{ github.token }} - foundry-tests: - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' + tests-upgradeable: + runs-on: ubuntu-latest + env: + FORCE_COLOR: 1 + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 # Include history so patch conflicts are resolved automatically + - name: Set up environment + uses: ./.github/actions/setup + - name: Transpile to upgradeable + run: bash scripts/upgradeable/transpile.sh + - name: Run tests + run: npm run test + - name: Check linearisation of the inheritance graph + run: npm run test:inheritance + - name: Check storage layout + uses: ./.github/actions/storage-layout + with: + token: ${{ github.token }} + + tests-foundry: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 @@ -59,34 +80,34 @@ jobs: run: forge test -vv coverage: - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' 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 + with: + token: ${{ secrets.CODECOV_TOKEN }} slither: - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up environment uses: ./.github/actions/setup - run: rm foundry.toml - - uses: crytic/slither-action@v0.2.0 + - uses: crytic/slither-action@v0.3.0 + with: + node-version: 18.15 codespell: - if: github.repository != 'OpenZeppelin/openzeppelin-contracts-upgradeable' runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Run CodeSpell - uses: codespell-project/actions-codespell@v1.0 + uses: codespell-project/actions-codespell@v2.0 with: + check_hidden: true check_filenames: true skip: package-lock.json,*.pdf diff --git a/.github/workflows/formal-verification.yml b/.github/workflows/formal-verification.yml new file mode 100644 index 000000000..ae5eba006 --- /dev/null +++ b/.github/workflows/formal-verification.yml @@ -0,0 +1,68 @@ +name: formal verification + +on: + pull_request: + types: + - opened + - reopened + - synchronize + - labeled + workflow_dispatch: {} + +env: + PIP_VERSION: '3.10' + JAVA_VERSION: '11' + SOLC_VERSION: '0.8.19' + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + apply-diff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Apply patches + run: make -C certora apply + + verify: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'formal-verification') + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up environment + uses: ./.github/actions/setup + - name: identify specs that need to be run + id: arguments + run: | + if [[ ${{ github.event_name }} = 'pull_request' ]]; + then + RESULT=$(git diff ${{ github.event.pull_request.head.sha }}..${{ github.event.pull_request.base.sha }} --name-only certora/specs/*.spec | while IFS= read -r file; do [[ -f $file ]] && basename "${file%.spec}"; done | tr "\n" " ") + else + RESULT='--all' + fi + echo "result=$RESULT" >> "$GITHUB_OUTPUT" + - name: Install python + uses: actions/setup-python@v4 + with: + python-version: ${{ env.PIP_VERSION }} + cache: 'pip' + - name: Install python packages + run: pip install -r requirements.txt + - name: Install java + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: ${{ env.JAVA_VERSION }} + - name: Install solc + run: | + wget https://github.com/ethereum/solidity/releases/download/v${{ env.SOLC_VERSION }}/solc-static-linux + sudo mv solc-static-linux /usr/local/bin/solc + chmod +x /usr/local/bin/solc + - name: Verify specification + run: | + make -C certora apply + node certora/run.js ${{ steps.arguments.outputs.result }} >> "$GITHUB_STEP_SUMMARY" + env: + CERTORAKEY: ${{ secrets.CERTORAKEY }} diff --git a/.github/workflows/release-cycle.yml b/.github/workflows/release-cycle.yml index 4fe09a4eb..9d35022dc 100644 --- a/.github/workflows/release-cycle.yml +++ b/.github/workflows/release-cycle.yml @@ -1,3 +1,15 @@ +# D: Manual Dispatch +# M: Merge release PR +# C: Commit +# ┌───────────┐ ┌─────────────┐ ┌────────────────┐ +# │Development├──D──►RC-Unreleased│ ┌──►Final-Unreleased│ +# └───────────┘ └─┬─────────▲─┘ │ └─┬────────────▲─┘ +# │ │ │ │ │ +# M C D M C +# │ │ │ │ │ +# ┌▼─────────┴┐ │ ┌▼────────────┴┐ +# │RC-Released├───┘ │Final-Released│ +# └───────────┘ └──────────────┘ name: Release Cycle on: @@ -130,6 +142,11 @@ jobs: run: bash scripts/release/workflow/pack.sh env: PRERELEASE: ${{ needs.state.outputs.is_prerelease }} + - name: Upload tarball artifact + uses: actions/upload-artifact@v3 + with: + name: ${{ github.ref_name }} + path: ${{ steps.pack.outputs.tarball }} - name: Tag run: npx changeset tag - name: Publish @@ -146,6 +163,26 @@ jobs: PRERELEASE: ${{ needs.state.outputs.is_prerelease }} with: script: await require('./scripts/release/workflow/github-release.js')({ github, context }) + outputs: + tarball_name: ${{ steps.pack.outputs.tarball_name }} + + integrity_check: + needs: publish + name: Tarball Integrity Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Download tarball artifact + id: artifact + # Replace with actions/upload-artifact@v3 when + # https://github.com/actions/download-artifact/pull/194 gets released + uses: actions/download-artifact@e9ef242655d12993efdcda9058dee2db83a2cb9b + with: + name: ${{ github.ref_name }} + - name: Check integrity + run: bash scripts/release/workflow/integrity-check.sh + env: + TARBALL: ${{ steps.artifact.outputs.download-path }}/${{ needs.publish.outputs.tarball_name }} merge: needs: state @@ -155,6 +192,8 @@ jobs: pull-requests: write if: needs.state.outputs.merge == 'true' runs-on: ubuntu-latest + env: + MERGE_BRANCH: merge/${{ github.ref_name }} steps: - uses: actions/checkout@v3 with: @@ -163,7 +202,9 @@ jobs: uses: ./.github/actions/setup - run: bash scripts/git-user-config.sh - name: Create branch to merge - run: bash scripts/release/workflow/prepare-release-merge.sh + run: | + git checkout -B "$MERGE_BRANCH" "$GITHUB_REF_NAME" + git push -f origin "$MERGE_BRANCH" - name: Create PR back to master uses: actions/github-script@v6 with: @@ -171,7 +212,7 @@ jobs: await github.rest.pulls.create({ owner: context.repo.owner, repo: context.repo.repo, - head: 'merge/${{ github.ref_name }}', + head: process.env.MERGE_BRANCH, base: 'master', title: '${{ format('Merge {0} branch', github.ref_name) }}' }); diff --git a/.github/workflows/upgradeable.yml b/.github/workflows/upgradeable.yml index a94f78c98..649596abb 100644 --- a/.github/workflows/upgradeable.yml +++ b/.github/workflows/upgradeable.yml @@ -1,4 +1,4 @@ -name: Upgradeable Trigger +name: transpile upgradeable on: push: @@ -7,17 +7,24 @@ on: - release-v* jobs: - trigger: + transpile: + environment: push-upgradeable runs-on: ubuntu-latest steps: - - id: app - uses: getsentry/action-github-app-token@v1 + - uses: actions/checkout@v3 with: - app_id: ${{ secrets.UPGRADEABLE_APP_ID }} - private_key: ${{ secrets.UPGRADEABLE_APP_PK }} - - run: | - curl -X POST \ - https://api.github.com/repos/OpenZeppelin/openzeppelin-contracts-upgradeable/dispatches \ - -H 'Accept: application/vnd.github.v3+json' \ - -H 'Authorization: token ${{ steps.app.outputs.token }}' \ - -d '{ "event_type": "Update", "client_payload": { "ref": "${{ github.ref }}" } }' + repository: OpenZeppelin/openzeppelin-contracts-upgradeable + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_UPGRADEABLE }} + - name: Fetch current non-upgradeable branch + run: | + git fetch "https://github.com/${{ github.repository }}.git" "$REF" + git checkout FETCH_HEAD + env: + REF: ${{ github.ref }} + - name: Set up environment + uses: ./.github/actions/setup + - run: bash scripts/git-user-config.sh + - name: Transpile to upgradeable + run: bash scripts/upgradeable/transpile-onto.sh ${{ github.ref_name }} origin/${{ github.ref_name }} + - run: git push origin ${{ github.ref_name }} diff --git a/.gitignore b/.gitignore index 9350b262e..2aac74485 100644 --- a/.gitignore +++ b/.gitignore @@ -68,3 +68,4 @@ contracts-exposed .certora* .last_confs certora_* +.zip-output-url.txt diff --git a/.gitmodules b/.gitmodules index aaa229f8d..08a09bbcf 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,5 @@ [submodule "lib/forge-std"] + branch = v1 path = lib/forge-std url = https://github.com/foundry-rs/forge-std [submodule "lib/erc4626-tests"] diff --git a/CHANGELOG.md b/CHANGELOG.md index 98ba5f938..2936877e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## Unreleased + +> **Warning** Version 5.0 is under active development and should not be used. Install the releases from npm or use the version tags in the repository. + +### Removals + +The following contracts, libraries and functions were removed: + +- `Address.isContract` (because of its ambiguous nature and potential for misuse) +- `Checkpoints.History` +- `Counters` +- `ERC20Snapshot` +- `ERC20VotesComp` +- `ERC165Storage` (in favor of inheritance based approach) +- `ERC777` +- `ERC1820Implementer` +- `GovernorVotesComp` +- `GovernorProposalThreshold` (deprecated since 4.4) +- `PaymentSplitter` +- `PullPayment` +- `SafeMath` +- `SignedSafeMath` +- `Timers` +- `TokenTimelock` (in favor of `VestingWallet`) +- All escrow contracts (`Escrow`, `ConditionalEscrow` and `RefundEscrow`) +- All cross-chain contracts, including `AccessControlCrossChain` and all the vendored bridge interfaces +- All presets in favor of [OpenZeppelin Contracts Wizard](https://wizard.openzeppelin.com/) + +These removals were implemented in the following PRs: [#3637](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3637), [#3880](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3880), [#3945](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3945), [#4258](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4258), [#4276](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4276), [#4289](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4289) + ### How to upgrade from 4.x #### ERC20, ERC721, and ERC1155 @@ -33,6 +63,77 @@ function supportsInterface(bytes4 interfaceId) public view virtual override retu } ``` +## 4.9.2 (2023-06-16) + +- `MerkleProof`: Fix a bug in `processMultiProof` and `processMultiProofCalldata` that allows proving arbitrary leaves if the tree contains a node with value 0 at depth 1. + +## 4.9.1 (2023-06-07) + +- `Governor`: Add a mechanism to restrict the address of the proposer using a suffix in the description. + +## 4.9.0 (2023-05-23) + +- `ReentrancyGuard`: Add a `_reentrancyGuardEntered` function to expose the guard status. ([#3714](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3714)) +- `ERC721Wrapper`: add a new extension of the `ERC721` token which wraps an underlying token. Deposit and withdraw guarantee that the ownership of each token is backed by a corresponding underlying token with the same identifier. ([#3863](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3863)) +- `EnumerableMap`: add a `keys()` function that returns an array containing all the keys. ([#3920](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3920)) +- `Governor`: add a public `cancel(uint256)` function. ([#3983](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3983)) +- `Governor`: Enable timestamp operation for blockchains without a stable block time. This is achieved by connecting a Governor's internal clock to match a voting token's EIP-6372 interface. ([#3934](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3934)) +- `Strings`: add `equal` method. ([#3774](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3774)) +- `IERC5313`: Add an interface for EIP-5313 that is now final. ([#4013](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4013)) +- `IERC4906`: Add an interface for ERC-4906 that is now Final. ([#4012](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4012)) +- `StorageSlot`: Add support for `string` and `bytes`. ([#4008](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4008)) +- `Votes`, `ERC20Votes`, `ERC721Votes`: support timestamp checkpointing using EIP-6372. ([#3934](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3934)) +- `ERC4626`: Add mitigation to the inflation attack through virtual shares and assets. ([#3979](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3979)) +- `Strings`: add `toString` method for signed integers. ([#3773](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3773)) +- `ERC20Wrapper`: Make the `underlying` variable private and add a public accessor. ([#4029](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4029)) +- `EIP712`: add EIP-5267 support for better domain discovery. ([#3969](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3969)) +- `AccessControlDefaultAdminRules`: Add an extension of `AccessControl` with additional security rules for the `DEFAULT_ADMIN_ROLE`. ([#4009](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4009)) +- `SignatureChecker`: Add `isValidERC1271SignatureNow` for checking a signature directly against a smart contract using ERC-1271. ([#3932](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3932)) +- `SafeERC20`: Add a `forceApprove` function to improve compatibility with tokens behaving like USDT. ([#4067](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4067)) +- `ERC1967Upgrade`: removed contract-wide `oz-upgrades-unsafe-allow delegatecall` annotation, replaced by granular annotation in `UUPSUpgradeable`. ([#3971](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3971)) +- `ERC20Wrapper`: self wrapping and deposit by the wrapper itself are now explicitly forbidden. ([#4100](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4100)) +- `ECDSA`: optimize bytes32 computation by using assembly instead of `abi.encodePacked`. ([#3853](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3853)) +- `ERC721URIStorage`: Emit ERC-4906 `MetadataUpdate` in `_setTokenURI`. ([#4012](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4012)) +- `ShortStrings`: Added a library for handling short strings in a gas efficient way, with fallback to storage for longer strings. ([#4023](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4023)) +- `SignatureChecker`: Allow return data length greater than 32 from EIP-1271 signers. ([#4038](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4038)) +- `UUPSUpgradeable`: added granular `oz-upgrades-unsafe-allow-reachable` annotation to improve upgrade safety checks on latest version of the Upgrades Plugins (starting with `@openzeppelin/upgrades-core@1.21.0`). ([#3971](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3971)) +- `Initializable`: optimize `_disableInitializers` by using `!=` instead of `<`. ([#3787](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3787)) +- `Ownable2Step`: make `acceptOwnership` public virtual to enable usecases that require overriding it. ([#3960](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3960)) +- `UUPSUpgradeable.sol`: Change visibility to the functions `upgradeTo ` and `upgradeToAndCall ` from `external` to `public`. ([#3959](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3959)) +- `TimelockController`: Add the `CallSalt` event to emit on operation schedule. ([#4001](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4001)) +- Reformatted codebase with latest version of Prettier Solidity. ([#3898](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3898)) +- `Math`: optimize `log256` rounding check. ([#3745](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3745)) +- `ERC20Votes`: optimize by using unchecked arithmetic. ([#3748](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3748)) +- `Multicall`: annotate `multicall` function as upgrade safe to not raise a flag for its delegatecall. ([#3961](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3961)) +- `ERC20Pausable`, `ERC721Pausable`, `ERC1155Pausable`: Add note regarding missing public pausing functionality ([#4007](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4007)) +- `ECDSA`: Add a function `toDataWithIntendedValidatorHash` that encodes data with version 0x00 following EIP-191. ([#4063](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4063)) +- `MerkleProof`: optimize by using unchecked arithmetic. ([#3745](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3745)) + +### Breaking changes + +- `EIP712`: Addition of ERC5267 support requires support for user defined value types, which was released in Solidity version 0.8.8. This requires a pragma change from `^0.8.0` to `^0.8.8`. +- `EIP712`: Optimization of the cache for the upgradeable version affects the way `name` and `version` are set. This is no longer done through an initializer, and is instead part of the implementation's constructor. As a consequence, all proxies using the same implementation will necessarily share the same `name` and `version`. Additionally, an implementation upgrade risks changing the EIP712 domain unless the same `name` and `version` are used when deploying the new implementation contract. + +### Deprecations + +- `ERC20Permit`: Added the file `IERC20Permit.sol` and `ERC20Permit.sol` and deprecated `draft-IERC20Permit.sol` and `draft-ERC20Permit.sol` since [EIP-2612](https://eips.ethereum.org/EIPS/eip-2612) is no longer a Draft. Developers are encouraged to update their imports. ([#3793](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3793)) +- `Timers`: The `Timers` library is now deprecated and will be removed in the next major release. ([#4062](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4062)) +- `ERC777`: The `ERC777` token standard is no longer supported by OpenZeppelin. Our implementation is now deprecated and will be removed in the next major release. The corresponding standard interfaces remain available. ([#4066](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4066)) +- `ERC1820Implementer`: The `ERC1820` pseudo-introspection mechanism is no longer supported by OpenZeppelin. Our implementation is now deprecated and will be removed in the next major release. The corresponding standard interfaces remain available. ([#4066](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4066)) + +## 4.8.3 (2023-04-13) + +- `GovernorCompatibilityBravo`: Fix encoding of proposal data when signatures are missing. +- `TransparentUpgradeableProxy`: Fix transparency in case of selector clash with non-decodable calldata or payable mutability. ([#4154](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4154)) + +## 4.8.2 (2023-03-02) + +- `ERC721Consecutive`: Fixed a bug when `_mintConsecutive` is used for batches of size 1 that could lead to balance overflow. Refer to the breaking changes section in the changelog for a note on the behavior of `ERC721._beforeTokenTransfer`. + +### Breaking changes + +- `ERC721`: The internal function `_beforeTokenTransfer` no longer updates balances, which it previously did when `batchSize` was greater than 1. This change has no consequence unless a custom ERC721 extension is explicitly invoking `_beforeTokenTransfer`. Balance updates in extensions must now be done explicitly using `__unsafe_increaseBalance`, with a name that indicates that there is an invariant that has to be manually verified. + ## 4.8.1 (2023-01-12) - `ERC4626`: Use staticcall instead of call when fetching underlying ERC-20 decimals. ([#3943](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3943)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ecbc2d400..1a44cc27d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,8 @@ Any non-trivial code contribution must be first discussed with the maintainers i Make sure to read and follow the [engineering guidelines](./GUIDELINES.md). Run linter and tests to make sure your pull request is good before submitting it. +Changelog entries should be added to each pull request by using [Changesets](https://github.com/changesets/changesets/). + When opening the pull request you will be presented with a template and a series of instructions. Read through it carefully and follow all the steps. Expect a review and feedback from the maintainers afterwards. If you're looking for a good place to start, look for issues labelled ["good first issue"](https://github.com/OpenZeppelin/openzeppelin-contracts/labels/good%20first%20issue)! diff --git a/GUIDELINES.md b/GUIDELINES.md index cdeddb90c..71f166405 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -62,6 +62,20 @@ Some other examples of automation are: - Looking for common security vulnerabilities or errors in our code (eg. reentrancy analysis). - Keeping dependencies up to date and monitoring for vulnerable dependencies. +## Pull requests + +Pull requests are squash-merged to keep the `master` branch history clean. The title of the pull request becomes the commit message, so it should be written in a consistent format: + +1) Begin with a capital letter. +2) Do not end with a period. +3) Write in the imperative: "Add feature X" and not "Adds feature X" or "Added feature X". + +This repository does not follow conventional commits, so do not prefix the title with "fix:" or "feat:". + +Work in progress pull requests should be submitted as Drafts and should not be prefixed with "WIP:". + +Branch names don't matter, and commit messages within a pull request mostly don't matter either, although they can help the review process. + # Solidity Conventions In addition to the official Solidity Style Guide we have a number of other conventions that must be followed. @@ -72,7 +86,7 @@ In addition to the official Solidity Style Guide we have a number of other conve * Internal or private state variables or functions should have an underscore prefix. - ``` + ```solidity contract TestContract { uint256 private _privateVar; uint256 internal _internalVar; @@ -84,7 +98,7 @@ In addition to the official Solidity Style Guide we have a number of other conve * Events should be emitted immediately after the state change that they represent, and should be named in the past tense. - ``` + ```solidity function _burn(address who, uint256 value) internal { super._burn(who, value); emit TokensBurned(who, value); @@ -96,8 +110,29 @@ In addition to the official Solidity Style Guide we have a number of other conve * Interface names should have a capital I prefix. - ``` + ```solidity interface IERC777 { ``` +* Contracts not intended to be used standalone should be marked abstract + so they are required to be inherited to other contracts. + + ```solidity + abstract contract AccessControl is ..., { + ``` + * Unchecked arithmetic blocks should contain comments explaining why overflow is guaranteed not to happen. If the reason is immediately apparent from the line above the unchecked block, the comment may be omitted. + +* Custom errors should be declared following the [EIP-6093](https://eips.ethereum.org/EIPS/eip-6093) rationale whenever reasonable. Also, consider the following: + + * The domain prefix should be picked in the following order: + 1. Use `ERC` if the error is a violation of an ERC specification. + 2. Use the name of the underlying component where it belongs (eg. `Governor`, `ECDSA`, or `Timelock`). + + * The location of custom errors should be decided in the following order: + 1. Take the errors from their underlying ERCs if they're already defined. + 2. Declare the errors in the underlying interface/library if the error makes sense in its context. + 3. Declare the error in the implementation if the underlying interface/library is not suitable to do so (eg. interface/library already specified in an ERC). + 4. Declare the error in an extension if the error only happens in such extension or child contracts. + + * Custom error names should not be declared twice along the library to avoid duplicated identifier declarations when inheriting from multiple contracts. diff --git a/README.md b/README.md index d58c29e7e..9d1c405b6 100644 --- a/README.md +++ b/README.md @@ -23,20 +23,30 @@ ### Installation +#### Hardhat, Truffle (npm) + ``` $ npm install @openzeppelin/contracts ``` OpenZeppelin Contracts features a [stable API](https://docs.openzeppelin.com/contracts/releases-stability#api-stability), which means that your contracts won't break unexpectedly when upgrading to a newer minor version. -An alternative to npm is to use the GitHub repository (`openzeppelin/openzeppelin-contracts`) to retrieve the contracts. When doing this, make sure to specify the tag for a release such as `v4.5.0`, instead of using the `master` branch. +#### Foundry (git) + +> **Warning** When installing via git, it is a common error to use the `master` branch. This is a development branch that should be avoided in favor of tagged releases. The release process involves security measures that the `master` branch does not guarantee. + +> **Warning** Foundry installs the latest version initially, but subsequent `forge update` commands will use the `master` branch. + +``` +$ forge install OpenZeppelin/openzeppelin-contracts +``` ### Usage Once installed, you can use the contracts in the library by importing them: ```solidity -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; @@ -70,13 +80,15 @@ Finally, you may want to take a look at the [guides on our blog](https://blog.op This project is maintained by [OpenZeppelin](https://openzeppelin.com) with the goal of providing a secure and reliable library of smart contract components for the ecosystem. We address security through risk management in various areas such as engineering and open source best practices, scoping and API design, multi-layered review processes, and incident response preparedness. -The security policy is detailed in [`SECURITY.md`](./SECURITY.md), and specifies how you can report security vulnerabilities, which versions will receive security patches, and how to stay informed about them. We run a [bug bounty program on Immunefi](https://immunefi.com/bounty/openzeppelin) to reward the responsible disclosure of vulnerabilities. +The [OpenZeppelin Contracts Security Center](https://contracts.openzeppelin.com/security) contains more details about the secure development process. + +The security policy is detailed in [`SECURITY.md`](./SECURITY.md) as well, and specifies how you can report security vulnerabilities, which versions will receive security patches, and how to stay informed about them. We run a [bug bounty program on Immunefi](https://immunefi.com/bounty/openzeppelin) to reward the responsible disclosure of vulnerabilities. The engineering guidelines we follow to promote project quality can be found in [`GUIDELINES.md`](./GUIDELINES.md). Past audits can be found in [`audits/`](./audits). -Smart contracts are a nascent techology and carry a high level of technical risk and uncertainty. Although OpenZeppelin is well known for its security audits, using OpenZeppelin Contracts is not a substitute for a security audit. +Smart contracts are a nascent technology and carry a high level of technical risk and uncertainty. Although OpenZeppelin is well known for its security audits, using OpenZeppelin Contracts is not a substitute for a security audit. OpenZeppelin Contracts is made available under the MIT License, which disclaims all warranties in relation to the project and which limits the liability of those that contribute and maintain the project, including OpenZeppelin. As set out further in the Terms, you acknowledge that you are solely responsible for any use of OpenZeppelin Contracts and you assume all risks associated with any such use. diff --git a/RELEASING.md b/RELEASING.md index f356ab2e4..0318f6338 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,36 +1,47 @@ # Releasing -> Visit the documentation for [details about release schedule]. +> Visit the documentation for [details about release schedule](https://docs.openzeppelin.com/contracts/releases-stability). -Start on an up-to-date `master` branch. +OpenZeppelin Contracts uses a fully automated release process that takes care of compiling, packaging, and publishing the library, all of which is carried out in a clean CI environment (GitHub Actions), implemented in the ([`release-cycle`](.github/workflows/release-cycle.yml)) workflow. This helps to reduce the potential for human error and inconsistencies, and ensures that the release process is ongoing and reliable. -Create the release branch with `npm run release start minor`. +## Changesets -Publish a release candidate with `npm run release rc`. +[Changesets](https://github.com/changesets/changesets/) is used as part of our release process for `CHANGELOG.md` management. Each change that is relevant for the codebase is expected to include a changeset. -Publish the final release with `npm run release final`. +## Branching model -Follow the general [OpenZeppelin Contracts release checklist]. +The release cycle happens on release branches called `release-vX.Y`. Each of these branches starts as a release candidate (rc) and is eventually promoted to final. -[details about release schedule]: https://docs.openzeppelin.com/contracts/releases-stability -[OpenZeppelin Contracts release checklist]: https://github.com/OpenZeppelin/code-style/blob/master/RELEASE_CHECKLIST.md +A release branch can be updated with cherry-picked patches from `master`, or may sometimes be committed to directly in the case of old releases. These commits will lead to a new release candidate or a patch increment depending on the state of the release branch. +```mermaid + %%{init: {'gitGraph': {'mainBranchName': 'master'}} }%% + gitGraph + commit id: "Feature A" + commit id: "Feature B" + branch release-vX.Y + commit id: "Start release" + commit id: "Release vX.Y.0-rc.0" -## Merging the release branch + checkout master + commit id: "Feature C" + commit id: "Fix A" -After the final release, the release branch should be merged back into `master`. This merge must not be squashed because it would lose the tagged release commit. Since the GitHub repo is set up to only allow squashed merges, the merge should be done locally and pushed. + checkout release-vX.Y + cherry-pick id: "Fix A" tag: "" + commit id: "Release vX.Y.0-rc.1" + commit id: "Release vX.Y.0" -Make sure to have the latest changes from `upstream` in your local release branch. + checkout master + merge release-vX.Y + commit id: "Feature D" + commit id: "Patch B" + checkout release-vX.Y + cherry-pick id: "Patch B" tag: "" + commit id: "Release vX.Y.1" + + checkout master + merge release-vX.Y + commit id: "Feature E" ``` -git checkout release-vX.Y.Z -git pull upstream -``` - -``` -git checkout master -git merge --no-ff release-vX.Y.Z -git push upstream master -``` - -The release branch can then be deleted on GitHub. diff --git a/audits/2023-05-v4.9.pdf b/audits/2023-05-v4.9.pdf new file mode 100644 index 000000000..21cd7f593 Binary files /dev/null and b/audits/2023-05-v4.9.pdf differ diff --git a/audits/README.md b/audits/README.md index 40098a44d..c629f51d3 100644 --- a/audits/README.md +++ b/audits/README.md @@ -2,6 +2,7 @@ | Date | Version | Commit | Auditor | Scope | Links | | ------------ | ------- | --------- | ------------ | -------------------- | ----------------------------------------------------------- | +| May 2023 | v4.9.0 | `91df66c` | OpenZeppelin | v4.9 Changes | [🔗](./2023-05-v4.9.pdf) | | October 2022 | v4.8.0 | `14f98db` | OpenZeppelin | ERC4626, Checkpoints | [🔗](./2022-10-ERC4626.pdf) [🔗](./2022-10-Checkpoints.pdf) | | October 2018 | v2.0.0 | `dac5bcc` | LevelK | Everything | [🔗](./2018-10.pdf) | | March 2017 | v1.0.4 | `9c5975a` | New Alchemy | Everything | [🔗](./2017-03.md) | diff --git a/certora/.gitignore b/certora/.gitignore new file mode 100644 index 000000000..893adcd31 --- /dev/null +++ b/certora/.gitignore @@ -0,0 +1 @@ +patched diff --git a/certora/Makefile b/certora/Makefile index bbbddbcab..50fd6da08 100644 --- a/certora/Makefile +++ b/certora/Makefile @@ -1,24 +1,54 @@ default: help -PATCH = applyHarness.patch -CONTRACTS_DIR = ../contracts -MUNGED_DIR = munged +SRC := ../contracts +DST := patched +DIFF := diff +SRCS := $(shell find $(SRC) -type f) +DSTS := $(shell find $(DST) -type f) +DIFFS := $(shell find $(DIFF) -type f) +############################################################################### +# Apply all patches in the $DIFF folder to the $DST folder +apply: $(DST) $(patsubst $(DIFF)/%.patch,$(DST)/%,$(subst _,/,$(DIFFS))) + +# Reset the $DST folder +$(DST): FORCE + @rm -rf $@ + @cp -r $(SRC) $@ + +# Update a solidity file in the $DST directory using the corresponding patch +$(DST)/%.sol: FORCE + @echo Applying patch to $@ + @patch -p0 -d $(DST) < $(patsubst $(DST)_%,$(DIFF)/%.patch,$(subst /,_,$@)) + +############################################################################### +# Record all difference between $SRC and $DST in patches +record: $(DIFF) $(patsubst %,$(DIFF)/%.patch,$(subst /,_,$(subst $(SRC)/,,$(SRCS)) $(subst $(DST)/,,$(DSTS)))) + +# Create the $DIFF folder +$(DIFF): FORCE + @rm -rf $@ + @mkdir $@ + +# Create the patch file by comparing the source and the destination +$(DIFF)/%.patch: FORCE + @echo Generating patch $@ + @diff -ruN \ + $(patsubst $(DIFF)/%.patch,$(SRC)/%,$(subst _,/,$@)) \ + $(patsubst $(DIFF)/%.patch,$(DST)/%,$(subst _,/,$@)) \ + | sed 's+$(SRC)/++g' \ + | sed 's+$(DST)/++g' \ + > $@ + @[ -s $@ ] || rm $@ + +############################################################################### help: @echo "usage:" + @echo " make apply: create $(DST) directory by applying the patches to $(SRC)" + @echo " make record: record the patches capturing the differences between $(SRC) and $(DST)" @echo " make clean: remove all generated files (those ignored by git)" - @echo " make $(MUNGED_DIR): create $(MUNGED_DIR) directory by applying the patch file to $(CONTRACTS_DIR)" - @echo " make record: record a new patch file capturing the differences between $(CONTRACTS_DIR) and $(MUNGED_DIR)" - -munged: $(wildcard $(CONTRACTS_DIR)/*.sol) $(PATCH) - rm -rf $@ - cp -r $(CONTRACTS_DIR) $@ - patch -p0 -d $@ < $(PATCH) - -record: - diff -ruN $(CONTRACTS_DIR) $(MUNGED_DIR) | sed 's+../contracts/++g' | sed 's+munged/++g' > $(PATCH) clean: git clean -fdX - touch $(PATCH) +FORCE: ; diff --git a/certora/README.md b/certora/README.md index 55f84d42f..cd85ba3d4 100644 --- a/certora/README.md +++ b/certora/README.md @@ -1,56 +1,60 @@ # Running the certora verification tool -These instructions detail the process for running CVT on the OpenZeppelin (Wizard/Governor) contracts. +These instructions detail the process for running Certora Verification Tool on OpenZeppelin Contracts. -Documentation for CVT and the specification language are available -[here](https://certora.atlassian.net/wiki/spaces/CPD/overview) +Documentation for CVT and the specification language are available [here](https://certora.atlassian.net/wiki/spaces/CPD/overview). + +## Prerequisites + +Follow the [Certora installation guide](https://docs.certora.com/en/latest/docs/user-guide/getting-started/install.html) in order to get the Certora Prover Package and the `solc` executable folder in your path. + +> **Note** +> An API Key is required for local testing. Although the prover will run on a Github Actions' CI environment on selected Pull Requests. ## Running the verification -The scripts in the `certora/scripts` directory are used to submit verification -jobs to the Certora verification service. After the job is complete, the results will be available on -[the Certora portal](https://vaas-stg.certora.com/). +The Certora Verification Tool proves specs for contracts, which are defined by the `./specs.json` file along with their pre-configured options. -These scripts should be run from the root directory; for example by running +The verification script `./run.js` is used to submit verification jobs to the Certora Verification service. -``` -sh certora/scripts/verifyAll.sh +You can run it from the root of the repository with the following command: + +```bash +node certora/run.js [[CONTRACT_NAME:]SPEC_NAME] [OPTIONS...] ``` -The most important of these is `verifyAll.sh`, which checks -all of the harnessed contracts (`certora/harness/Wizard*.sol`) against all of -the specifications (`certora/spec/*.spec`). +Where: -The other scripts run a subset of the specifications or the contracts. You can -verify different contracts or specifications by changing the `--verify` option, -and you can run a single rule or method with the `--rule` or `--method` option. +- `CONTRACT_NAME` matches the `contract` key in the `./spec.json` file and may be empty. It will run all matching contracts if not provided. +- `SPEC_NAME` refers to a `spec` key from the `./specs.json` file. It will run every spec if not provided. +- `OPTIONS` extend the [Certora Prover CLI options](https://docs.certora.com/en/latest/docs/prover/cli/options.html#certora-prover-cli-options) and will respect the preconfigured options in the `specs.json` file. -For example, to verify the `WizardFirstPriority` contract against the -`GovernorCountingSimple` specification, you could change the `--verify` line of -the `WizardControlFirstPriortity.sh` script to: +> **Note** +> A single spec may be configured to run for multiple contracts, whereas a single contract may run multiple specs. -``` ---verify WizardFirstPriority:certora/specs/GovernorCountingSimple.spec \ +Example usage: + +```bash +node certora/run.js AccessControl # Run the AccessControl spec against every contract implementing it ``` ## Adapting to changes in the contracts -Some of our rules require the code to be simplified in various ways. Our -primary tool for performing these simplifications is to run verification on a -contract that extends the original contracts and overrides some of the methods. -These "harness" contracts can be found in the `certora/harness` directory. +Some of our rules require the code to be simplified in various ways. Our primary tool for performing these simplifications is to run verification on a contract that extends the original contracts and overrides some of the methods. These "harness" contracts can be found in the `certora/harness` directory. -This pattern does require some modifications to the original code: some methods -need to be made virtual or public, for example. These changes are handled by -applying a patch to the code before verification. +This pattern does require some modifications to the original code: some methods need to be made virtual or public, for example. These changes are handled by applying a patch +to the code before verification by running: -When one of the `verify` scripts is executed, it first applies the patch file -`certora/applyHarness.patch` to the `contracts` directory, placing the output -in the `certora/munged` directory. We then verify the contracts in the -`certora/munged` directory. +```bash +make -C certora apply +``` -If the original contracts change, it is possible to create a conflict with the -patch. In this case, the verify scripts will report an error message and output -rejected changes in the `munged` directory. After merging the changes, run -`make record` in the `certora` directory; this will regenerate the patch file, -which can then be checked into git. +Before running the `certora/run.js` script, it's required to apply the corresponding patches to the `contracts` directory, placing the output in the `certora/patched` directory. Then, the contracts are verified by running the verification for the `certora/patched` directory. + +If the original contracts change, it is possible to create a conflict with the patch. In this case, the verify scripts will report an error message and output rejected changes in the `patched` directory. After merging the changes, run `make record` in the `certora` directory; this will regenerate the patch file, which can then be checked into git. + +For more information about the `make` scripts available, run: + +```bash +make -C certora help +``` diff --git a/certora/applyHarness.patch b/certora/applyHarness.patch deleted file mode 100644 index 0fbe9acad..000000000 --- a/certora/applyHarness.patch +++ /dev/null @@ -1,101 +0,0 @@ -diff -ruN .gitignore .gitignore ---- .gitignore 1969-12-31 19:00:00.000000000 -0500 -+++ .gitignore 2021-12-09 14:46:33.923637220 -0500 -@@ -0,0 +1,2 @@ -+* -+!.gitignore -diff -ruN governance/compatibility/GovernorCompatibilityBravo.sol governance/compatibility/GovernorCompatibilityBravo.sol ---- governance/compatibility/GovernorCompatibilityBravo.sol 2021-12-03 15:24:56.523654357 -0500 -+++ governance/compatibility/GovernorCompatibilityBravo.sol 2021-12-09 14:46:33.923637220 -0500 -@@ -245,7 +245,7 @@ - /** - * @dev See {Governor-_quorumReached}. In this module, only forVotes count toward the quorum. - */ -- function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) { -+ function _quorumReached(uint256 proposalId) public view virtual override returns (bool) { // HARNESS: changed to public from internal - ProposalDetails storage details = _proposalDetails[proposalId]; - return quorum(proposalSnapshot(proposalId)) <= details.forVotes; - } -@@ -253,7 +253,7 @@ - /** - * @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be scritly over the againstVotes. - */ -- function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) { -+ function _voteSucceeded(uint256 proposalId) public view virtual override returns (bool) { // HARNESS: changed to public from internal - ProposalDetails storage details = _proposalDetails[proposalId]; - return details.forVotes > details.againstVotes; - } -diff -ruN governance/extensions/GovernorCountingSimple.sol governance/extensions/GovernorCountingSimple.sol ---- governance/extensions/GovernorCountingSimple.sol 2021-12-03 15:24:56.523654357 -0500 -+++ governance/extensions/GovernorCountingSimple.sol 2021-12-09 14:46:33.923637220 -0500 -@@ -64,7 +64,7 @@ - /** - * @dev See {Governor-_quorumReached}. - */ -- function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) { -+ function _quorumReached(uint256 proposalId) public view virtual override returns (bool) { - ProposalVote storage proposalvote = _proposalVotes[proposalId]; - - return quorum(proposalSnapshot(proposalId)) <= proposalvote.forVotes + proposalvote.abstainVotes; -@@ -73,7 +73,7 @@ - /** - * @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be strictly over the againstVotes. - */ -- function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) { -+ function _voteSucceeded(uint256 proposalId) public view virtual override returns (bool) { - ProposalVote storage proposalvote = _proposalVotes[proposalId]; - - return proposalvote.forVotes > proposalvote.againstVotes; -diff -ruN governance/extensions/GovernorTimelockControl.sol governance/extensions/GovernorTimelockControl.sol ---- governance/extensions/GovernorTimelockControl.sol 2021-12-03 15:24:56.523654357 -0500 -+++ governance/extensions/GovernorTimelockControl.sol 2021-12-09 14:46:33.923637220 -0500 -@@ -111,7 +111,7 @@ - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal virtual override { -- _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash); -+ _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash); - } - - /** -diff -ruN governance/Governor.sol governance/Governor.sol ---- governance/Governor.sol 2021-12-03 15:24:56.523654357 -0500 -+++ governance/Governor.sol 2021-12-09 14:46:56.411503587 -0500 -@@ -38,8 +38,8 @@ - - string private _name; - -- mapping(uint256 => ProposalCore) private _proposals; -- -+ mapping(uint256 => ProposalCore) public _proposals; -+ - /** - * @dev Restrict access to governor executing address. Some module might override the _executor function to make - * sure this modifier is consistent with the execution model. -@@ -167,12 +167,12 @@ - /** - * @dev Amount of votes already cast passes the threshold limit. - */ -- function _quorumReached(uint256 proposalId) internal view virtual returns (bool); -+ function _quorumReached(uint256 proposalId) public view virtual returns (bool); // HARNESS: changed to public from internal - - /** - * @dev Is the proposal successful or not. - */ -- function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool); -+ function _voteSucceeded(uint256 proposalId) public view virtual returns (bool); // HARNESS: changed to public from internal - - /** - * @dev Register a vote with a given support and voting weight. -diff -ruN token/ERC20/extensions/ERC20Votes.sol token/ERC20/extensions/ERC20Votes.sol ---- token/ERC20/extensions/ERC20Votes.sol 2021-12-03 15:24:56.527654330 -0500 -+++ token/ERC20/extensions/ERC20Votes.sol 2021-12-09 14:46:33.927637196 -0500 -@@ -84,7 +84,7 @@ - * - * - `blockNumber` must have been already mined - */ -- function getPastVotes(address account, uint256 blockNumber) public view returns (uint256) { -+ function getPastVotes(address account, uint256 blockNumber) public view virtual returns (uint256) { - require(blockNumber < block.number, "ERC20Votes: block not yet mined"); - return _checkpointsLookup(_checkpoints[account], blockNumber); - } diff --git a/certora/diff/token_ERC721_ERC721.sol.patch b/certora/diff/token_ERC721_ERC721.sol.patch new file mode 100644 index 000000000..c3eae357a --- /dev/null +++ b/certora/diff/token_ERC721_ERC721.sol.patch @@ -0,0 +1,14 @@ +--- token/ERC721/ERC721.sol 2023-03-07 10:48:47.736822221 +0100 ++++ token/ERC721/ERC721.sol 2023-03-09 19:49:39.669338673 +0100 +@@ -199,6 +199,11 @@ + return _owners[tokenId]; + } + ++ // FV ++ function _getApproved(uint256 tokenId) internal view returns (address) { ++ return _tokenApprovals[tokenId]; ++ } ++ + /** + * @dev Returns whether `tokenId` exists. + * diff --git a/certora/harnesses/AccessControlDefaultAdminRulesHarness.sol b/certora/harnesses/AccessControlDefaultAdminRulesHarness.sol new file mode 100644 index 000000000..3483c558f --- /dev/null +++ b/certora/harnesses/AccessControlDefaultAdminRulesHarness.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/access/AccessControlDefaultAdminRules.sol"; + +contract AccessControlDefaultAdminRulesHarness is AccessControlDefaultAdminRules { + uint48 private _delayIncreaseWait; + + constructor( + uint48 initialDelay, + address initialDefaultAdmin, + uint48 delayIncreaseWait + ) AccessControlDefaultAdminRules(initialDelay, initialDefaultAdmin) { + _delayIncreaseWait = delayIncreaseWait; + } + + // FV + function pendingDefaultAdmin_() external view returns (address) { + (address newAdmin, ) = pendingDefaultAdmin(); + return newAdmin; + } + + function pendingDefaultAdminSchedule_() external view returns (uint48) { + (, uint48 schedule) = pendingDefaultAdmin(); + return schedule; + } + + function pendingDelay_() external view returns (uint48) { + (uint48 newDelay, ) = pendingDefaultAdminDelay(); + return newDelay; + } + + function pendingDelaySchedule_() external view returns (uint48) { + (, uint48 schedule) = pendingDefaultAdminDelay(); + return schedule; + } + + function delayChangeWait_(uint48 newDelay) external view returns (uint48) { + return _delayChangeWait(newDelay); + } + + // Overrides + function defaultAdminDelayIncreaseWait() public view override returns (uint48) { + return _delayIncreaseWait; + } +} diff --git a/certora/harnesses/AccessControlHarness.sol b/certora/harnesses/AccessControlHarness.sol new file mode 100644 index 000000000..3363f3590 --- /dev/null +++ b/certora/harnesses/AccessControlHarness.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/access/AccessControl.sol"; + +contract AccessControlHarness is AccessControl {} diff --git a/certora/harnesses/DoubleEndedQueueHarness.sol b/certora/harnesses/DoubleEndedQueueHarness.sol new file mode 100644 index 000000000..35dd4a54a --- /dev/null +++ b/certora/harnesses/DoubleEndedQueueHarness.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/utils/structs/DoubleEndedQueue.sol"; + +contract DoubleEndedQueueHarness { + using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; + + DoubleEndedQueue.Bytes32Deque private _deque; + + function pushFront(bytes32 value) external { + _deque.pushFront(value); + } + + function pushBack(bytes32 value) external { + _deque.pushBack(value); + } + + function popFront() external returns (bytes32 value) { + return _deque.popFront(); + } + + function popBack() external returns (bytes32 value) { + return _deque.popBack(); + } + + function clear() external { + _deque.clear(); + } + + function begin() external view returns (int128) { + return _deque._begin; + } + + function end() external view returns (int128) { + return _deque._end; + } + + function length() external view returns (uint256) { + return _deque.length(); + } + + function empty() external view returns (bool) { + return _deque.empty(); + } + + function front() external view returns (bytes32 value) { + return _deque.front(); + } + + function back() external view returns (bytes32 value) { + return _deque.back(); + } + + function at_(uint256 index) external view returns (bytes32 value) { + return _deque.at(index); + } +} diff --git a/certora/harnesses/ERC20FlashMintHarness.sol b/certora/harnesses/ERC20FlashMintHarness.sol new file mode 100644 index 000000000..3599d8494 --- /dev/null +++ b/certora/harnesses/ERC20FlashMintHarness.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/token/ERC20/ERC20.sol"; +import "../patched/token/ERC20/extensions/ERC20Permit.sol"; +import "../patched/token/ERC20/extensions/ERC20FlashMint.sol"; + +contract ERC20FlashMintHarness is ERC20, ERC20Permit, ERC20FlashMint { + uint256 someFee; + address someFeeReceiver; + + constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {} + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } + + // public accessor + function flashFeeReceiver() public view returns (address) { + return someFeeReceiver; + } + + // internal hook + function _flashFee(address, uint256) internal view override returns (uint256) { + return someFee; + } + + function _flashFeeReceiver() internal view override returns (address) { + return someFeeReceiver; + } +} diff --git a/certora/harnesses/ERC20PermitHarness.sol b/certora/harnesses/ERC20PermitHarness.sol new file mode 100644 index 000000000..dccc9cb91 --- /dev/null +++ b/certora/harnesses/ERC20PermitHarness.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/token/ERC20/extensions/ERC20Permit.sol"; + +contract ERC20PermitHarness is ERC20Permit { + constructor(string memory name, string memory symbol) ERC20(name, symbol) ERC20Permit(name) {} + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } +} diff --git a/certora/harnesses/ERC20VotesHarness.sol b/certora/harnesses/ERC20VotesHarness.sol deleted file mode 100644 index 5067ecfba..000000000 --- a/certora/harnesses/ERC20VotesHarness.sol +++ /dev/null @@ -1,28 +0,0 @@ -import "../munged/token/ERC20/extensions/ERC20Votes.sol"; - -contract ERC20VotesHarness is ERC20Votes { - constructor(string memory name, string memory symbol) ERC20Permit(name) ERC20(name, symbol) {} - - mapping(address => mapping(uint256 => uint256)) public _getPastVotes; - - function _afterTokenTransfer( - address from, - address to, - uint256 amount - ) internal virtual override { - super._afterTokenTransfer(from, to, amount); - _getPastVotes[from][block.number] -= amount; - _getPastVotes[to][block.number] += amount; - } - - /** - * @dev Change delegation for `delegator` to `delegatee`. - * - * Emits events {DelegateChanged} and {DelegateVotesChanged}. - */ - function _delegate(address delegator, address delegatee) internal virtual override{ - super._delegate(delegator, delegatee); - _getPastVotes[delegator][block.number] -= balanceOf(delegator); - _getPastVotes[delegatee][block.number] += balanceOf(delegator); - } -} diff --git a/certora/harnesses/ERC20WrapperHarness.sol b/certora/harnesses/ERC20WrapperHarness.sol new file mode 100644 index 000000000..191c46cc4 --- /dev/null +++ b/certora/harnesses/ERC20WrapperHarness.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/token/ERC20/extensions/ERC20Wrapper.sol"; + +contract ERC20WrapperHarness is ERC20Wrapper { + constructor(IERC20 _underlying, string memory _name, string memory _symbol) ERC20(_name, _symbol) ERC20Wrapper(_underlying) {} + + function underlyingTotalSupply() public view returns (uint256) { + return underlying().totalSupply(); + } + + function underlyingBalanceOf(address account) public view returns (uint256) { + return underlying().balanceOf(account); + } + + function underlyingAllowanceToThis(address account) public view returns (uint256) { + return underlying().allowance(account, address(this)); + } + + function recover(address account) public returns (uint256) { + return _recover(account); + } +} diff --git a/certora/harnesses/ERC3156FlashBorrowerHarness.sol b/certora/harnesses/ERC3156FlashBorrowerHarness.sol new file mode 100644 index 000000000..d07d12e47 --- /dev/null +++ b/certora/harnesses/ERC3156FlashBorrowerHarness.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +import "../patched/interfaces/IERC3156FlashBorrower.sol"; + +pragma solidity ^0.8.19; + +contract ERC3156FlashBorrowerHarness is IERC3156FlashBorrower { + bytes32 somethingToReturn; + + function onFlashLoan(address, address, uint256, uint256, bytes calldata) external view override returns (bytes32) { + return somethingToReturn; + } +} diff --git a/certora/harnesses/ERC721Harness.sol b/certora/harnesses/ERC721Harness.sol new file mode 100644 index 000000000..f22a6811d --- /dev/null +++ b/certora/harnesses/ERC721Harness.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/token/ERC721/ERC721.sol"; + +contract ERC721Harness is ERC721 { + constructor(string memory name, string memory symbol) ERC721(name, symbol) {} + + function mint(address account, uint256 tokenId) external { + _mint(account, tokenId); + } + + function safeMint(address to, uint256 tokenId) external { + _safeMint(to, tokenId); + } + + function safeMint(address to, uint256 tokenId, bytes memory data) external { + _safeMint(to, tokenId, data); + } + + function burn(uint256 tokenId) external { + _burn(tokenId); + } + + function tokenExists(uint256 tokenId) external view returns (bool) { + return _exists(tokenId); + } + + function unsafeOwnerOf(uint256 tokenId) external view returns (address) { + return _ownerOf(tokenId); + } + + function unsafeGetApproved(uint256 tokenId) external view returns (address) { + return _getApproved(tokenId); + } +} diff --git a/certora/harnesses/ERC721ReceiverHarness.sol b/certora/harnesses/ERC721ReceiverHarness.sol new file mode 100644 index 000000000..a0e9e247a --- /dev/null +++ b/certora/harnesses/ERC721ReceiverHarness.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/interfaces/IERC721Receiver.sol"; + +contract ERC721ReceiverHarness is IERC721Receiver { + function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { + return this.onERC721Received.selector; + } +} diff --git a/certora/harnesses/EnumerableMapHarness.sol b/certora/harnesses/EnumerableMapHarness.sol new file mode 100644 index 000000000..40b752487 --- /dev/null +++ b/certora/harnesses/EnumerableMapHarness.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/utils/structs/EnumerableMap.sol"; + +contract EnumerableMapHarness { + using EnumerableMap for EnumerableMap.Bytes32ToBytes32Map; + + EnumerableMap.Bytes32ToBytes32Map private _map; + + function set(bytes32 key, bytes32 value) public returns (bool) { + return _map.set(key, value); + } + + function remove(bytes32 key) public returns (bool) { + return _map.remove(key); + } + + function contains(bytes32 key) public view returns (bool) { + return _map.contains(key); + } + + function length() public view returns (uint256) { + return _map.length(); + } + + function key_at(uint256 index) public view returns (bytes32) { + (bytes32 key,) = _map.at(index); + return key; + } + + function value_at(uint256 index) public view returns (bytes32) { + (,bytes32 value) = _map.at(index); + return value; + } + + function tryGet_contains(bytes32 key) public view returns (bool) { + (bool contained,) = _map.tryGet(key); + return contained; + } + + function tryGet_value(bytes32 key) public view returns (bytes32) { + (,bytes32 value) = _map.tryGet(key); + return value; + } + + function get(bytes32 key) public view returns (bytes32) { + return _map.get(key); + } + + function _indexOf(bytes32 key) public view returns (uint256) { + return _map._keys._inner._indexes[key]; + } +} diff --git a/certora/harnesses/EnumerableSetHarness.sol b/certora/harnesses/EnumerableSetHarness.sol new file mode 100644 index 000000000..21c7cc7c4 --- /dev/null +++ b/certora/harnesses/EnumerableSetHarness.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/utils/structs/EnumerableSet.sol"; + +contract EnumerableSetHarness { + using EnumerableSet for EnumerableSet.Bytes32Set; + + EnumerableSet.Bytes32Set private _set; + + function add(bytes32 value) public returns (bool) { + return _set.add(value); + } + + function remove(bytes32 value) public returns (bool) { + return _set.remove(value); + } + + function contains(bytes32 value) public view returns (bool) { + return _set.contains(value); + } + + function length() public view returns (uint256) { + return _set.length(); + } + + function at_(uint256 index) public view returns (bytes32) { + return _set.at(index); + } + + function _indexOf(bytes32 value) public view returns (uint256) { + return _set._inner._indexes[value]; + } +} diff --git a/certora/harnesses/InitializableHarness.sol b/certora/harnesses/InitializableHarness.sol new file mode 100644 index 000000000..52b48b82d --- /dev/null +++ b/certora/harnesses/InitializableHarness.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../patched/proxy/utils/Initializable.sol"; + +contract InitializableHarness is Initializable { + function initialize() public initializer {} + function reinitialize(uint8 n) public reinitializer(n) {} + function disable() public { _disableInitializers(); } + + function nested_init_init() public initializer { initialize(); } + function nested_init_reinit(uint8 m) public initializer { reinitialize(m); } + function nested_reinit_init(uint8 n) public reinitializer(n) { initialize(); } + function nested_reinit_reinit(uint8 n, uint8 m) public reinitializer(n) { reinitialize(m); } + + function version() public view returns (uint8) { + return _getInitializedVersion(); + } + + function initializing() public view returns (bool) { + return _isInitializing(); + } +} diff --git a/certora/harnesses/Ownable2StepHarness.sol b/certora/harnesses/Ownable2StepHarness.sol new file mode 100644 index 000000000..1a9ed76e5 --- /dev/null +++ b/certora/harnesses/Ownable2StepHarness.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/access/Ownable2Step.sol"; + +contract Ownable2StepHarness is Ownable2Step { + function restricted() external onlyOwner {} +} diff --git a/certora/harnesses/OwnableHarness.sol b/certora/harnesses/OwnableHarness.sol new file mode 100644 index 000000000..3113fa1bb --- /dev/null +++ b/certora/harnesses/OwnableHarness.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/access/Ownable.sol"; + +contract OwnableHarness is Ownable { + function restricted() external onlyOwner {} +} diff --git a/certora/harnesses/PausableHarness.sol b/certora/harnesses/PausableHarness.sol new file mode 100644 index 000000000..34d6a82d8 --- /dev/null +++ b/certora/harnesses/PausableHarness.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../patched/security/Pausable.sol"; + +contract PausableHarness is Pausable { + function pause() external { + _pause(); + } + + function unpause() external { + _unpause(); + } + + function onlyWhenPaused() external whenPaused {} + + function onlyWhenNotPaused() external whenNotPaused {} +} diff --git a/certora/harnesses/TimelockControllerHarness.sol b/certora/harnesses/TimelockControllerHarness.sol new file mode 100644 index 000000000..f75dcfa45 --- /dev/null +++ b/certora/harnesses/TimelockControllerHarness.sol @@ -0,0 +1,12 @@ +pragma solidity ^0.8.19; + +import "../patched/governance/TimelockController.sol"; + +contract TimelockControllerHarness is TimelockController { + constructor( + uint256 minDelay, + address[] memory proposers, + address[] memory executors, + address admin + ) TimelockController(minDelay, proposers, executors, admin) {} +} diff --git a/certora/harnesses/WizardControlFirstPriority.sol b/certora/harnesses/WizardControlFirstPriority.sol deleted file mode 100644 index 5ae7fe066..000000000 --- a/certora/harnesses/WizardControlFirstPriority.sol +++ /dev/null @@ -1,150 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "../munged/governance/Governor.sol"; -import "../munged/governance/extensions/GovernorCountingSimple.sol"; -import "../munged/governance/extensions/GovernorVotes.sol"; -import "../munged/governance/extensions/GovernorVotesQuorumFraction.sol"; -import "../munged/governance/extensions/GovernorTimelockControl.sol"; -import "../munged/governance/extensions/GovernorProposalThreshold.sol"; - -/* -Wizard options: -ProposalThreshhold = 10 -ERC20Votes -TimelockController -*/ - -contract WizardControlFirstPriority is Governor, GovernorProposalThreshold, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl { - constructor(ERC20Votes _token, TimelockController _timelock, string memory name, uint256 quorumFraction) - Governor(name) - GovernorVotes(_token) - GovernorVotesQuorumFraction(quorumFraction) - GovernorTimelockControl(_timelock) - {} - - //HARNESS - - function isExecuted(uint256 proposalId) public view returns (bool) { - return _proposals[proposalId].executed; - } - - function isCanceled(uint256 proposalId) public view returns (bool) { - return _proposals[proposalId].canceled; - } - - uint256 _votingDelay; - - uint256 _votingPeriod; - - uint256 _proposalThreshold; - - mapping(uint256 => uint256) public ghost_sum_vote_power_by_id; - - function _castVote( - uint256 proposalId, - address account, - uint8 support, - string memory reason - ) internal override virtual returns (uint256) { - - uint256 deltaWeight = super._castVote(proposalId, account, support, reason); //HARNESS - ghost_sum_vote_power_by_id[proposalId] += deltaWeight; - - return deltaWeight; - } - - function snapshot(uint256 proposalId) public view returns (uint64) { - return _proposals[proposalId].voteStart._deadline; - } - - - function getExecutor() public view returns (address){ - return _executor(); - } - - // original code, harnessed - - function votingDelay() public view override returns (uint256) { // HARNESS: pure -> view - return _votingDelay; // HARNESS: parametric - } - - function votingPeriod() public view override returns (uint256) { // HARNESS: pure -> view - return _votingPeriod; // HARNESS: parametric - } - - function proposalThreshold() public view override returns (uint256) { // HARNESS: pure -> view - return _proposalThreshold; // HARNESS: parametric - } - - // original code, not harnessed - // The following functions are overrides required by Solidity. - - function quorum(uint256 blockNumber) - public - view - override(IGovernor, GovernorVotesQuorumFraction) - returns (uint256) - { - return super.quorum(blockNumber); - } - - function getVotes(address account, uint256 blockNumber) - public - view - override(IGovernor, GovernorVotes) - returns (uint256) - { - return super.getVotes(account, blockNumber); - } - - function state(uint256 proposalId) - public - view - override(Governor, GovernorTimelockControl) - returns (ProposalState) - { - return super.state(proposalId); - } - - function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) - public - override(Governor, GovernorProposalThreshold, IGovernor) - returns (uint256) - { - return super.propose(targets, values, calldatas, description); - } - - function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockControl) - { - super._execute(proposalId, targets, values, calldatas, descriptionHash); - } - - function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockControl) - returns (uint256) - { - return super._cancel(targets, values, calldatas, descriptionHash); - } - - function _executor() - internal - view - override(Governor, GovernorTimelockControl) - returns (address) - { - return super._executor(); - } - - function supportsInterface(bytes4 interfaceId) - public - view - override(Governor, GovernorTimelockControl) - returns (bool) - { - return super.supportsInterface(interfaceId); - } -} diff --git a/certora/harnesses/WizardFirstTry.sol b/certora/harnesses/WizardFirstTry.sol deleted file mode 100644 index 83fece04f..000000000 --- a/certora/harnesses/WizardFirstTry.sol +++ /dev/null @@ -1,141 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "../munged/governance/Governor.sol"; -import "../munged/governance/extensions/GovernorCountingSimple.sol"; -import "../munged/governance/extensions/GovernorVotes.sol"; -import "../munged/governance/extensions/GovernorVotesQuorumFraction.sol"; -import "../munged/governance/extensions/GovernorTimelockCompound.sol"; - -/* -Wizard options: -ERC20Votes -TimelockCompound -*/ - -contract WizardFirstTry is Governor, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockCompound { - constructor(ERC20Votes _token, ICompoundTimelock _timelock, string memory name, uint256 quorumFraction) - Governor(name) - GovernorVotes(_token) - GovernorVotesQuorumFraction(quorumFraction) - GovernorTimelockCompound(_timelock) - {} - - //HARNESS - - function isExecuted(uint256 proposalId) public view returns (bool) { - return _proposals[proposalId].executed; - } - - function isCanceled(uint256 proposalId) public view returns (bool) { - return _proposals[proposalId].canceled; - } - - function snapshot(uint256 proposalId) public view returns (uint64) { - return _proposals[proposalId].voteStart._deadline; - } - - function getExecutor() public view returns (address){ - return _executor(); - } - - uint256 _votingDelay; - - uint256 _votingPeriod; - - mapping(uint256 => uint256) public ghost_sum_vote_power_by_id; - - function _castVote( - uint256 proposalId, - address account, - uint8 support, - string memory reason - ) internal override virtual returns (uint256) { - - uint256 deltaWeight = super._castVote(proposalId, account, support, reason); //HARNESS - ghost_sum_vote_power_by_id[proposalId] += deltaWeight; - - return deltaWeight; - } - - // original code, harnessed - - function votingDelay() public view override virtual returns (uint256) { // HARNESS: pure -> view - return _votingDelay; // HARNESS: parametric - } - - function votingPeriod() public view override virtual returns (uint256) { // HARNESS: pure -> view - return _votingPeriod; // HARNESS: parametric - } - - // original code, not harnessed - // The following functions are overrides required by Solidity. - - function quorum(uint256 blockNumber) - public - view - override(IGovernor, GovernorVotesQuorumFraction) - returns (uint256) - { - return super.quorum(blockNumber); - } - - function getVotes(address account, uint256 blockNumber) - public - view - override(IGovernor, GovernorVotes) - returns (uint256) - { - return super.getVotes(account, blockNumber); - } - - function state(uint256 proposalId) - public - view - override(Governor, GovernorTimelockCompound) - returns (ProposalState) - { - return super.state(proposalId); - } - - function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) - public - override(Governor, IGovernor) - returns (uint256) - { - return super.propose(targets, values, calldatas, description); - } - - function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockCompound) - { - super._execute(proposalId, targets, values, calldatas, descriptionHash); - } - - function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockCompound) - returns (uint256) - { - return super._cancel(targets, values, calldatas, descriptionHash); - } - - function _executor() - internal - view - override(Governor, GovernorTimelockCompound) - returns (address) - { - return super._executor(); - } - - function supportsInterface(bytes4 interfaceId) - public - view - override(Governor, GovernorTimelockCompound) - returns (bool) - { - return super.supportsInterface(interfaceId); - } -} diff --git a/certora/munged/.gitignore b/certora/munged/.gitignore deleted file mode 100644 index d6b7ef32c..000000000 --- a/certora/munged/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/certora/run.js b/certora/run.js new file mode 100755 index 000000000..fdee42d2e --- /dev/null +++ b/certora/run.js @@ -0,0 +1,146 @@ +#!/usr/bin/env node + +// USAGE: +// node certora/run.js [[CONTRACT_NAME:]SPEC_NAME]* [--all] [--options OPTIONS...] [--specs PATH] +// EXAMPLES: +// node certora/run.js --all +// node certora/run.js AccessControl +// node certora/run.js AccessControlHarness:AccessControl + +const proc = require('child_process'); +const { PassThrough } = require('stream'); +const events = require('events'); + +const argv = require('yargs') + .env('') + .options({ + all: { + alias: 'a', + type: 'boolean', + }, + spec: { + alias: 's', + type: 'string', + default: __dirname + '/specs.json', + }, + parallel: { + alias: 'p', + type: 'number', + default: 4, + }, + options: { + alias: 'o', + type: 'array', + default: [], + }, + }).argv; + +function match(entry, request) { + const [reqSpec, reqContract] = request.split(':').reverse(); + return entry.spec == reqSpec && (!reqContract || entry.contract == reqContract); +} + +const specs = require(argv.spec).filter(s => argv.all || argv._.some(r => match(s, r))); +const limit = require('p-limit')(argv.parallel); + +if (argv._.length == 0 && !argv.all) { + console.error(`Warning: No specs requested. Did you forgot to toggle '--all'?`); +} + +for (const r of argv._) { + if (!specs.some(s => match(s, r))) { + console.error(`Error: Requested spec '${r}' not found in ${argv.spec}`); + process.exitCode = 1; + } +} + +if (process.exitCode) { + process.exit(process.exitCode); +} + +for (const { spec, contract, files, options = [] } of specs) { + limit(runCertora, spec, contract, files, [...options.flatMap(opt => opt.split(' ')), ...argv.options]); +} + +// Run certora, aggregate the output and print it at the end +async function runCertora(spec, contract, files, options = []) { + const args = [...files, '--verify', `${contract}:certora/specs/${spec}.spec`, ...options]; + const child = proc.spawn('certoraRun', args); + + const stream = new PassThrough(); + const output = collect(stream); + + child.stdout.pipe(stream, { end: false }); + child.stderr.pipe(stream, { end: false }); + + // as soon as we have a job id, print the output link + stream.on('data', function logStatusUrl(data) { + const { '-DjobId': jobId, '-DuserId': userId } = Object.fromEntries( + data + .toString('utf8') + .match(/-D\S+=\S+/g) + ?.map(s => s.split('=')) || [], + ); + + if (jobId && userId) { + console.error(`[${spec}] https://prover.certora.com/output/${userId}/${jobId}/`); + stream.off('data', logStatusUrl); + } + }); + + // wait for process end + const [code, signal] = await events.once(child, 'exit'); + + // error + if (code || signal) { + console.error(`[${spec}] Exited with code ${code || signal}`); + process.exitCode = 1; + } + + // get all output + stream.end(); + + // write results in markdown format + writeEntry(spec, contract, code || signal, (await output).match(/https:\/\/prover.certora.com\/output\/\S*/)?.[0]); + + // write all details + console.error(`+ certoraRun ${args.join(' ')}\n` + (await output)); +} + +// Collects stream data into a string +async function collect(stream) { + const buffers = []; + for await (const data of stream) { + const buf = Buffer.isBuffer(data) ? data : Buffer.from(data); + buffers.push(buf); + } + return Buffer.concat(buffers).toString('utf8'); +} + +// Formatting +let hasHeader = false; + +function formatRow(...array) { + return ['', ...array, ''].join(' | '); +} + +function writeHeader() { + console.log(formatRow('spec', 'contract', 'result', 'status', 'output')); + console.log(formatRow('-', '-', '-', '-', '-')); +} + +function writeEntry(spec, contract, success, url) { + if (!hasHeader) { + hasHeader = true; + writeHeader(); + } + console.log( + formatRow( + spec, + contract, + success ? ':x:' : ':heavy_check_mark:', + url ? `[link](${url?.replace('/output/', '/jobStatus/')})` : 'error', + url ? `[link](${url})` : 'error', + ), + ); +} diff --git a/certora/scripts/Governor.sh b/certora/scripts/Governor.sh deleted file mode 100755 index 53ade5060..000000000 --- a/certora/scripts/Governor.sh +++ /dev/null @@ -1,10 +0,0 @@ -make -C certora munged - -certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/GovernorHarness.sol \ - --verify GovernorHarness:certora/specs/GovernorBase.spec \ - --solc solc8.0 \ - --staging shelly/forSasha \ - --optimistic_loop \ - --settings -copyLoopUnroll=4 \ - --rule voteStartBeforeVoteEnd \ - --msg "$1" diff --git a/certora/scripts/GovernorCountingSimple-counting.sh b/certora/scripts/GovernorCountingSimple-counting.sh deleted file mode 100644 index 9ed8fe34c..000000000 --- a/certora/scripts/GovernorCountingSimple-counting.sh +++ /dev/null @@ -1,10 +0,0 @@ -make -C certora munged - -certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/GovernorBasicHarness.sol \ - --verify GovernorBasicHarness:certora/specs/GovernorCountingSimple.spec \ - --solc solc8.2 \ - --staging shelly/forSasha \ - --optimistic_loop \ - --settings -copyLoopUnroll=4 \ - --rule hasVotedCorrelation \ - --msg "$1" diff --git a/certora/scripts/WizardControlFirstPriority.sh b/certora/scripts/WizardControlFirstPriority.sh deleted file mode 100644 index b815986ee..000000000 --- a/certora/scripts/WizardControlFirstPriority.sh +++ /dev/null @@ -1,12 +0,0 @@ -make -C certora munged - -certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/WizardControlFirstPriority.sol \ - --link WizardControlFirstPriority:token=ERC20VotesHarness \ - --verify WizardControlFirstPriority:certora/specs/GovernorBase.spec \ - --solc solc8.2 \ - --disableLocalTypeChecking \ - --staging shelly/forSasha \ - --optimistic_loop \ - --settings -copyLoopUnroll=4 \ - --rule canVoteDuringVotingPeriod \ - --msg "$1" diff --git a/certora/scripts/WizardFirstTry.sh b/certora/scripts/WizardFirstTry.sh deleted file mode 100644 index fd5a32ab4..000000000 --- a/certora/scripts/WizardFirstTry.sh +++ /dev/null @@ -1,10 +0,0 @@ -make -C certora munged - -certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/WizardFirstTry.sol \ - --verify WizardFirstTry:certora/specs/GovernorBase.spec \ - --solc solc8.2 \ - --staging shelly/forSasha \ - --optimistic_loop \ - --disableLocalTypeChecking \ - --settings -copyLoopUnroll=4 \ - --msg "$1" diff --git a/certora/scripts/sanity.sh b/certora/scripts/sanity.sh deleted file mode 100644 index 1c42bf67b..000000000 --- a/certora/scripts/sanity.sh +++ /dev/null @@ -1,14 +0,0 @@ -make -C certora munged - -for f in certora/harnesses/Wizard*.sol -do - echo "Processing $f" - file="$(basename $f)" - echo ${file%.*} - certoraRun certora/harnesses/$file \ - --verify ${file%.*}:certora/specs/sanity.spec "$@" \ - --solc solc8.2 --staging shelly/forSasha \ - --optimistic_loop \ - --msg "checking sanity on ${file%.*}" - --settings -copyLoopUnroll=4 -done diff --git a/certora/scripts/verifyAll.sh b/certora/scripts/verifyAll.sh deleted file mode 100644 index 9b2f11c8c..000000000 --- a/certora/scripts/verifyAll.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -make -C certora munged - -for contract in certora/harnesses/Wizard*.sol; -do - for spec in certora/specs/*.spec; - do - contractFile="$(basename $contract)" - specFile="$(basename $spec)" - if [[ "${specFile%.*}" != "RulesInProgress" ]]; - then - echo "Processing ${contractFile%.*} with $specFile" - if [[ "${contractFile%.*}" = *"WizardControl"* ]]; - then - certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/$contractFile \ - --link ${contractFile%.*}:token=ERC20VotesHarness \ - --verify ${contractFile%.*}:certora/specs/$specFile "$@" \ - --solc solc8.2 \ - --staging shelly/forSasha \ - --disableLocalTypeChecking \ - --optimistic_loop \ - --settings -copyLoopUnroll=4 \ - --send_only \ - --msg "checking $specFile on ${contractFile%.*}" - else - certoraRun certora/harnesses/ERC20VotesHarness.sol certora/harnesses/$contractFile \ - --verify ${contractFile%.*}:certora/specs/$specFile "$@" \ - --solc solc8.2 \ - --staging shelly/forSasha \ - --disableLocalTypeChecking \ - --optimistic_loop \ - --settings -copyLoopUnroll=4 \ - --send_only \ - --msg "checking $specFile on ${contractFile%.*}" - fi - fi - done -done diff --git a/certora/specs.json b/certora/specs.json new file mode 100644 index 000000000..3e5acb568 --- /dev/null +++ b/certora/specs.json @@ -0,0 +1,86 @@ +[ + { + "spec": "Pausable", + "contract": "PausableHarness", + "files": ["certora/harnesses/PausableHarness.sol"] + }, + { + "spec": "AccessControl", + "contract": "AccessControlHarness", + "files": ["certora/harnesses/AccessControlHarness.sol"] + }, + { + "spec": "AccessControlDefaultAdminRules", + "contract": "AccessControlDefaultAdminRulesHarness", + "files": ["certora/harnesses/AccessControlDefaultAdminRulesHarness.sol"] + }, + { + "spec": "DoubleEndedQueue", + "contract": "DoubleEndedQueueHarness", + "files": ["certora/harnesses/DoubleEndedQueueHarness.sol"] + }, + { + "spec": "Ownable", + "contract": "OwnableHarness", + "files": ["certora/harnesses/OwnableHarness.sol"] + }, + { + "spec": "Ownable2Step", + "contract": "Ownable2StepHarness", + "files": ["certora/harnesses/Ownable2StepHarness.sol"] + }, + { + "spec": "ERC20", + "contract": "ERC20PermitHarness", + "files": ["certora/harnesses/ERC20PermitHarness.sol"], + "options": ["--optimistic_loop"] + }, + { + "spec": "ERC20FlashMint", + "contract": "ERC20FlashMintHarness", + "files": [ + "certora/harnesses/ERC20FlashMintHarness.sol", + "certora/harnesses/ERC3156FlashBorrowerHarness.sol" + ], + "options": ["--optimistic_loop"] + }, + { + "spec": "ERC20Wrapper", + "contract": "ERC20WrapperHarness", + "files": [ + "certora/harnesses/ERC20PermitHarness.sol", + "certora/harnesses/ERC20WrapperHarness.sol" + ], + "options": [ + "--link ERC20WrapperHarness:_underlying=ERC20PermitHarness", + "--optimistic_loop" + ] + }, + { + "spec": "ERC721", + "contract": "ERC721Harness", + "files": ["certora/harnesses/ERC721Harness.sol", "certora/harnesses/ERC721ReceiverHarness.sol"], + "options": ["--optimistic_loop"] + }, + { + "spec": "Initializable", + "contract": "InitializableHarness", + "files": ["certora/harnesses/InitializableHarness.sol"] + }, + { + "spec": "EnumerableSet", + "contract": "EnumerableSetHarness", + "files": ["certora/harnesses/EnumerableSetHarness.sol"] + }, + { + "spec": "EnumerableMap", + "contract": "EnumerableMapHarness", + "files": ["certora/harnesses/EnumerableMapHarness.sol"] + }, + { + "spec": "TimelockController", + "contract": "TimelockControllerHarness", + "files": ["certora/harnesses/TimelockControllerHarness.sol"], + "options": ["--optimistic_hashing", "--optimistic_loop"] + } +] diff --git a/certora/specs/AccessControl.spec b/certora/specs/AccessControl.spec new file mode 100644 index 000000000..cd5af2a99 --- /dev/null +++ b/certora/specs/AccessControl.spec @@ -0,0 +1,126 @@ +import "helpers/helpers.spec" +import "methods/IAccessControl.spec" + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Definitions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +definition DEFAULT_ADMIN_ROLE() returns bytes32 = 0x0000000000000000000000000000000000000000000000000000000000000000; + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Identify entrypoints: only grantRole, revokeRole and renounceRole can alter permissions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyGrantCanGrant(env e, method f, bytes32 role, address account) { + calldataarg args; + + bool hasRoleBefore = hasRole(role, account); + f(e, args); + bool hasRoleAfter = hasRole(role, account); + + assert ( + !hasRoleBefore && + hasRoleAfter + ) => ( + f.selector == grantRole(bytes32, address).selector + ); + + assert ( + hasRoleBefore && + !hasRoleAfter + ) => ( + f.selector == revokeRole(bytes32, address).selector || + f.selector == renounceRole(bytes32, address).selector + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: grantRole only affects the specified user/role combo │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule grantRoleEffect(env e, bytes32 role) { + require nonpayable(e); + + bytes32 otherRole; + address account; + address otherAccount; + + bool isCallerAdmin = hasRole(getRoleAdmin(role), e.msg.sender); + bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); + + grantRole@withrevert(e, role, account); + bool success = !lastReverted; + + bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); + + // liveness + assert success <=> isCallerAdmin; + + // effect + assert success => hasRole(role, account); + + // no side effect + assert hasOtherRoleBefore != hasOtherRoleAfter => (role == otherRole && account == otherAccount); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: revokeRole only affects the specified user/role combo │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule revokeRoleEffect(env e, bytes32 role) { + require nonpayable(e); + + bytes32 otherRole; + address account; + address otherAccount; + + bool isCallerAdmin = hasRole(getRoleAdmin(role), e.msg.sender); + bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); + + revokeRole@withrevert(e, role, account); + bool success = !lastReverted; + + bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); + + // liveness + assert success <=> isCallerAdmin; + + // effect + assert success => !hasRole(role, account); + + // no side effect + assert hasOtherRoleBefore != hasOtherRoleAfter => (role == otherRole && account == otherAccount); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: renounceRole only affects the specified user/role combo │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule renounceRoleEffect(env e, bytes32 role) { + require nonpayable(e); + + bytes32 otherRole; + address account; + address otherAccount; + + bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); + + renounceRole@withrevert(e, role, account); + bool success = !lastReverted; + + bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); + + // liveness + assert success <=> account == e.msg.sender; + + // effect + assert success => !hasRole(role, account); + + // no side effect + assert hasOtherRoleBefore != hasOtherRoleAfter => (role == otherRole && account == otherAccount); +} diff --git a/certora/specs/AccessControlDefaultAdminRules.spec b/certora/specs/AccessControlDefaultAdminRules.spec new file mode 100644 index 000000000..58b9d1202 --- /dev/null +++ b/certora/specs/AccessControlDefaultAdminRules.spec @@ -0,0 +1,464 @@ +import "helpers/helpers.spec" +import "methods/IAccessControlDefaultAdminRules.spec" +import "methods/IAccessControl.spec" +import "AccessControl.spec" + +use rule onlyGrantCanGrant filtered { + f -> f.selector != acceptDefaultAdminTransfer().selector +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +definition timeSanity(env e) returns bool = + e.block.timestamp > 0 && e.block.timestamp + defaultAdminDelay(e) < max_uint48(); + +definition delayChangeWaitSanity(env e, uint48 newDelay) returns bool = + e.block.timestamp + delayChangeWait_(e, newDelay) < max_uint48(); + +definition isSet(uint48 schedule) returns bool = + schedule != 0; + +definition hasPassed(env e, uint48 schedule) returns bool = + schedule < e.block.timestamp; + +definition increasingDelaySchedule(env e, uint48 newDelay) returns mathint = + e.block.timestamp + min(newDelay, defaultAdminDelayIncreaseWait()); + +definition decreasingDelaySchedule(env e, uint48 newDelay) returns mathint = + e.block.timestamp + defaultAdminDelay(e) - newDelay; + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: defaultAdmin holds the DEFAULT_ADMIN_ROLE │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant defaultAdminConsistency(address account) + (account == defaultAdmin() && account != 0) <=> hasRole(DEFAULT_ADMIN_ROLE(), account) + { + preserved with (env e) { + require nonzerosender(e); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: Only one account holds the DEFAULT_ADMIN_ROLE │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant singleDefaultAdmin(address account, address another) + hasRole(DEFAULT_ADMIN_ROLE(), account) && hasRole(DEFAULT_ADMIN_ROLE(), another) => another == account + { + preserved { + requireInvariant defaultAdminConsistency(account); + requireInvariant defaultAdminConsistency(another); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: DEFAULT_ADMIN_ROLE's admin is always DEFAULT_ADMIN_ROLE │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant defaultAdminRoleAdminConsistency() + getRoleAdmin(DEFAULT_ADMIN_ROLE()) == DEFAULT_ADMIN_ROLE() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: owner is the defaultAdmin │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant ownerConsistency() + defaultAdmin() == owner() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: revokeRole only affects the specified user/role combo │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule revokeRoleEffect(env e, bytes32 role) { + require nonpayable(e); + + bytes32 otherRole; + address account; + address otherAccount; + + bool isCallerAdmin = hasRole(getRoleAdmin(role), e.msg.sender); + bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); + + revokeRole@withrevert(e, role, account); + bool success = !lastReverted; + + bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); + + // liveness + assert success <=> isCallerAdmin && role != DEFAULT_ADMIN_ROLE(), + "roles can only be revoked by their owner except for the default admin role"; + + // effect + assert success => !hasRole(role, account), + "role is revoked"; + + // no side effect + assert hasOtherRoleBefore != hasOtherRoleAfter => (role == otherRole && account == otherAccount), + "no other role is affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: renounceRole only affects the specified user/role combo │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule renounceRoleEffect(env e, bytes32 role) { + require nonpayable(e); + + bytes32 otherRole; + address account; + address otherAccount; + + bool hasOtherRoleBefore = hasRole(otherRole, otherAccount); + address adminBefore = defaultAdmin(); + address pendingAdminBefore = pendingDefaultAdmin_(); + uint48 scheduleBefore = pendingDefaultAdminSchedule_(); + + renounceRole@withrevert(e, role, account); + bool success = !lastReverted; + + bool hasOtherRoleAfter = hasRole(otherRole, otherAccount); + address adminAfter = defaultAdmin(); + address pendingAdminAfter = pendingDefaultAdmin_(); + uint48 scheduleAfter = pendingDefaultAdminSchedule_(); + + // liveness + assert success <=> ( + account == e.msg.sender && + ( + role != DEFAULT_ADMIN_ROLE() || + account != adminBefore || + ( + pendingAdminBefore == 0 && + isSet(scheduleBefore) && + hasPassed(e, scheduleBefore) + ) + ) + ), + "an account only can renounce by itself with a delay for the default admin role"; + + // effect + assert success => !hasRole(role, account), + "role is renounced"; + + assert success => ( + ( + role == DEFAULT_ADMIN_ROLE() && + account == adminBefore + ) ? ( + adminAfter == 0 && + pendingAdminAfter == 0 && + scheduleAfter == 0 + ) : ( + adminAfter == adminBefore && + pendingAdminAfter == pendingAdminBefore && + scheduleAfter == scheduleBefore + ) + ), + "renouncing default admin role cleans state iff called by previous admin"; + + // no side effect + assert hasOtherRoleBefore != hasOtherRoleAfter => ( + role == otherRole && + account == otherAccount + ), + "no other role is affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: defaultAdmin is only affected by accepting an admin transfer or renoucing │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noDefaultAdminChange(env e, method f, calldataarg args) { + address adminBefore = defaultAdmin(); + f(e, args); + address adminAfter = defaultAdmin(); + + assert adminBefore != adminAfter => ( + f.selector == acceptDefaultAdminTransfer().selector || + f.selector == renounceRole(bytes32,address).selector + ), + "default admin is only affected by accepting an admin transfer or renoucing"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: pendingDefaultAdmin is only affected by beginning, completing (accept or renounce), or canceling an admin │ +│ transfer │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noPendingDefaultAdminChange(env e, method f, calldataarg args) { + address pendingAdminBefore = pendingDefaultAdmin_(); + address scheduleBefore = pendingDefaultAdminSchedule_(); + f(e, args); + address pendingAdminAfter = pendingDefaultAdmin_(); + address scheduleAfter = pendingDefaultAdminSchedule_(); + + assert ( + pendingAdminBefore != pendingAdminAfter || + scheduleBefore != scheduleAfter + ) => ( + f.selector == beginDefaultAdminTransfer(address).selector || + f.selector == acceptDefaultAdminTransfer().selector || + f.selector == cancelDefaultAdminTransfer().selector || + f.selector == renounceRole(bytes32,address).selector + ), + "pending admin and its schedule is only affected by beginning, completing, or cancelling an admin transfer"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: defaultAdminDelay can't be changed atomically by any function │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noDefaultAdminDelayChange(env e, method f, calldataarg args) { + uint48 delayBefore = defaultAdminDelay(e); + f(e, args); + uint48 delayAfter = defaultAdminDelay(e); + + assert delayBefore == delayAfter, + "delay can't be changed atomically by any function"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: pendingDefaultAdminDelay is only affected by changeDefaultAdminDelay or rollbackDefaultAdminDelay │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noPendingDefaultAdminDelayChange(env e, method f, calldataarg args) { + uint48 pendingDelayBefore = pendingDelay_(e); + f(e, args); + uint48 pendingDelayAfter = pendingDelay_(e); + + assert pendingDelayBefore != pendingDelayAfter => ( + f.selector == changeDefaultAdminDelay(uint48).selector || + f.selector == rollbackDefaultAdminDelay().selector + ), + "pending delay is only affected by changeDefaultAdminDelay or rollbackDefaultAdminDelay"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: defaultAdminDelayIncreaseWait can't be changed atomically by any function │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noDefaultAdminDelayIncreaseWaitChange(env e, method f, calldataarg args) { + uint48 delayIncreaseWaitBefore = defaultAdminDelayIncreaseWait(); + f(e, args); + uint48 delayIncreaseWaitAfter = defaultAdminDelayIncreaseWait(); + + assert delayIncreaseWaitBefore == delayIncreaseWaitAfter, + "delay increase wait can't be changed atomically by any function"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: beginDefaultAdminTransfer sets a pending default admin and its schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule beginDefaultAdminTransfer(env e, address newAdmin) { + require timeSanity(e); + require nonpayable(e); + require nonzerosender(e); + requireInvariant defaultAdminConsistency(e.msg.sender); + + beginDefaultAdminTransfer@withrevert(e, newAdmin); + bool success = !lastReverted; + + // liveness + assert success <=> e.msg.sender == defaultAdmin(), + "only the current default admin can begin a transfer"; + + // effect + assert success => pendingDefaultAdmin_() == newAdmin, + "pending default admin is set"; + assert success => pendingDefaultAdminSchedule_() == e.block.timestamp + defaultAdminDelay(e), + "pending default admin delay is set"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: A default admin can't change in less than the applied schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pendingDefaultAdminDelayEnforced(env e1, env e2, method f, calldataarg args, address newAdmin) { + require e1.block.timestamp <= e2.block.timestamp; + + uint48 delayBefore = defaultAdminDelay(e1); + address adminBefore = defaultAdmin(); + + // There might be a better way to generalize this without requiring `beginDefaultAdminTransfer`, but currently + // it's the only way in which we can attest that only `delayBefore` has passed before a change. + beginDefaultAdminTransfer(e1, newAdmin); + f(e2, args); + + address adminAfter = defaultAdmin(); + + // change can only happen towards the newAdmin, with the delay + assert adminAfter != adminBefore => ( + adminAfter == newAdmin && + e2.block.timestamp >= e1.block.timestamp + delayBefore + ), + "The admin can only change after the enforced delay and to the previously scheduled new admin"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: acceptDefaultAdminTransfer updates defaultAdmin resetting the pending admin and its schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule acceptDefaultAdminTransfer(env e) { + require nonpayable(e); + + address pendingAdminBefore = pendingDefaultAdmin_(); + uint48 scheduleBefore = pendingDefaultAdminSchedule_(); + + acceptDefaultAdminTransfer@withrevert(e); + bool success = !lastReverted; + + // liveness + assert success <=> ( + e.msg.sender == pendingAdminBefore && + isSet(scheduleBefore) && + hasPassed(e, scheduleBefore) + ), + "only the pending default admin can accept the role after the schedule has been set and passed"; + + // effect + assert success => defaultAdmin() == pendingAdminBefore, + "Default admin is set to the previous pending default admin"; + assert success => pendingDefaultAdmin_() == 0, + "Pending default admin is reset"; + assert success => pendingDefaultAdminSchedule_() == 0, + "Pending default admin delay is reset"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: cancelDefaultAdminTransfer resets pending default admin and its schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cancelDefaultAdminTransfer(env e) { + require nonpayable(e); + require nonzerosender(e); + requireInvariant defaultAdminConsistency(e.msg.sender); + + cancelDefaultAdminTransfer@withrevert(e); + bool success = !lastReverted; + + // liveness + assert success <=> e.msg.sender == defaultAdmin(), + "only the current default admin can cancel a transfer"; + + // effect + assert success => pendingDefaultAdmin_() == 0, + "Pending default admin is reset"; + assert success => pendingDefaultAdminSchedule_() == 0, + "Pending default admin delay is reset"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: changeDefaultAdminDelay sets a pending default admin delay and its schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule changeDefaultAdminDelay(env e, uint48 newDelay) { + require timeSanity(e); + require nonpayable(e); + require nonzerosender(e); + require delayChangeWaitSanity(e, newDelay); + requireInvariant defaultAdminConsistency(e.msg.sender); + + uint48 delayBefore = defaultAdminDelay(e); + + changeDefaultAdminDelay@withrevert(e, newDelay); + bool success = !lastReverted; + + // liveness + assert success <=> e.msg.sender == defaultAdmin(), + "only the current default admin can begin a delay change"; + + // effect + assert success => pendingDelay_(e) == newDelay, + "pending delay is set"; + + assert success => ( + pendingDelaySchedule_(e) > e.block.timestamp || + delayBefore == newDelay || // Interpreted as decreasing, x - x = 0 + defaultAdminDelayIncreaseWait() == 0 + ), + "pending delay schedule is set in the future unless accepted edge cases"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: A delay can't change in less than the applied schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pendingDelayWaitEnforced(env e1, env e2, method f, calldataarg args, uint48 newDelay) { + require e1.block.timestamp <= e2.block.timestamp; + + uint48 delayBefore = defaultAdminDelay(e1); + + changeDefaultAdminDelay(e1, newDelay); + f(e2, args); + + uint48 delayAfter = defaultAdminDelay(e2); + + mathint delayWait = newDelay > delayBefore ? increasingDelaySchedule(e1, newDelay) : decreasingDelaySchedule(e1, newDelay); + + assert delayAfter != delayBefore => ( + delayAfter == newDelay && + e2.block.timestamp >= delayWait + ), + "A delay can only change after the applied schedule"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: pending delay wait is set depending on increasing or decreasing the delay │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pendingDelayWait(env e, uint48 newDelay) { + uint48 oldDelay = defaultAdminDelay(e); + changeDefaultAdminDelay(e, newDelay); + + assert newDelay > oldDelay => pendingDelaySchedule_(e) == increasingDelaySchedule(e, newDelay), + "Delay wait is the minimum between the new delay and a threshold when the delay is increased"; + assert newDelay <= oldDelay => pendingDelaySchedule_(e) == decreasingDelaySchedule(e, newDelay), + "Delay wait is the difference between the current and the new delay when the delay is decreased"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: rollbackDefaultAdminDelay resets the delay and its schedule │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule rollbackDefaultAdminDelay(env e) { + require nonpayable(e); + require nonzerosender(e); + requireInvariant defaultAdminConsistency(e.msg.sender); + + rollbackDefaultAdminDelay@withrevert(e); + bool success = !lastReverted; + + // liveness + assert success <=> e.msg.sender == defaultAdmin(), + "only the current default admin can rollback a delay change"; + + // effect + assert success => pendingDelay_(e) == 0, + "Pending default admin is reset"; + assert success => pendingDelaySchedule_(e) == 0, + "Pending default admin delay is reset"; +} diff --git a/certora/specs/DoubleEndedQueue.spec b/certora/specs/DoubleEndedQueue.spec new file mode 100644 index 000000000..2a196772d --- /dev/null +++ b/certora/specs/DoubleEndedQueue.spec @@ -0,0 +1,366 @@ +import "helpers/helpers.spec" + +methods { + pushFront(bytes32) envfree + pushBack(bytes32) envfree + popFront() returns (bytes32) envfree + popBack() returns (bytes32) envfree + clear() envfree + + // exposed for FV + begin() returns (int128) envfree + end() returns (int128) envfree + + // view + length() returns (uint256) envfree + empty() returns (bool) envfree + front() returns (bytes32) envfree + back() returns (bytes32) envfree + at_(uint256) returns (bytes32) envfree // at is a reserved word +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ + +function min_int128() returns mathint { + return -(1 << 127); +} + +function max_int128() returns mathint { + return (1 << 127) - 1; +} + +// Could be broken in theory, but not in practice +function boundedQueue() returns bool { + return + max_int128() > to_mathint(end()) && + min_int128() < to_mathint(begin()); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: end is larger or equal than begin │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant boundariesConsistency() + end() >= begin() + filtered { f -> !f.isView } + { preserved { require boundedQueue(); } } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: length is end minus begin │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant lengthConsistency() + length() == to_mathint(end()) - to_mathint(begin()) + filtered { f -> !f.isView } + { preserved { require boundedQueue(); } } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: empty() is length 0 and no element exists │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant emptiness() + empty() <=> length() == 0 + filtered { f -> !f.isView } + { preserved { require boundedQueue(); } } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: front points to the first index and back points to the last one │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant queueEndings() + at_(length() - 1) == back() && at_(0) == front() + filtered { f -> !f.isView } + { + preserved { + requireInvariant boundariesConsistency(); + require boundedQueue(); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: pushFront adds an element at the beginning of the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pushFront(bytes32 value) { + require boundedQueue(); + + uint256 lengthBefore = length(); + + pushFront@withrevert(value); + + // liveness + assert !lastReverted, "never reverts"; + + // effect + assert front() == value, "front set to value"; + assert length() == lengthBefore + 1, "queue extended"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: pushFront preserves the previous values in the queue with a +1 offset │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pushFrontConsistency(uint256 index) { + require boundedQueue(); + + bytes32 beforeAt = at_(index); + + bytes32 value; + pushFront(value); + + // try to read value + bytes32 afterAt = at_@withrevert(index + 1); + + assert !lastReverted, "value still there"; + assert afterAt == beforeAt, "data is preserved"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: pushBack adds an element at the end of the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pushBack(bytes32 value) { + require boundedQueue(); + + uint256 lengthBefore = length(); + + pushBack@withrevert(value); + + // liveness + assert !lastReverted, "never reverts"; + + // effect + assert back() == value, "back set to value"; + assert length() == lengthBefore + 1, "queue increased"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: pushBack preserves the previous values in the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pushBackConsistency(uint256 index) { + require boundedQueue(); + + bytes32 beforeAt = at_(index); + + bytes32 value; + pushBack(value); + + // try to read value + bytes32 afterAt = at_@withrevert(index); + + assert !lastReverted, "value still there"; + assert afterAt == beforeAt, "data is preserved"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: popFront removes an element from the beginning of the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule popFront { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + uint256 lengthBefore = length(); + bytes32 frontBefore = front@withrevert(); + + bytes32 popped = popFront@withrevert(); + bool success = !lastReverted; + + // liveness + assert success <=> lengthBefore != 0, "never reverts if not previously empty"; + + // effect + assert success => frontBefore == popped, "previous front is returned"; + assert success => length() == lengthBefore - 1, "queue decreased"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: at(x) is preserved and offset to at(x - 1) after calling popFront | +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule popFrontConsistency(uint256 index) { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + // Read (any) value that is not the front (this asserts the value exists / the queue is long enough) + require index > 1; + bytes32 before = at_(index); + + popFront(); + + // try to read value + bytes32 after = at_@withrevert(index - 1); + + assert !lastReverted, "value still exists in the queue"; + assert before == after, "values are offset and not modified"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: popBack removes an element from the end of the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule popBack { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + uint256 lengthBefore = length(); + bytes32 backBefore = back@withrevert(); + + bytes32 popped = popBack@withrevert(); + bool success = !lastReverted; + + // liveness + assert success <=> lengthBefore != 0, "never reverts if not previously empty"; + + // effect + assert success => backBefore == popped, "previous back is returned"; + assert success => length() == lengthBefore - 1, "queue decreased"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: at(x) is preserved after calling popBack | +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule popBackConsistency(uint256 index) { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + // Read (any) value that is not the back (this asserts the value exists / the queue is long enough) + require index < length() - 1; + bytes32 before = at_(index); + + popBack(); + + // try to read value + bytes32 after = at_@withrevert(index); + + assert !lastReverted, "value still exists in the queue"; + assert before == after, "values are offset and not modified"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: clear sets length to 0 │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule clear { + clear@withrevert(); + + // liveness + assert !lastReverted, "never reverts"; + + // effect + assert length() == 0, "sets length to 0"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: front/back access reverts only if the queue is empty or querying out of bounds │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyEmptyRevert(env e) { + require nonpayable(e); + requireInvariant boundariesConsistency(); + require boundedQueue(); + + method f; + calldataarg args; + + bool emptyBefore = empty(); + + f@withrevert(e, args); + + assert lastReverted => ( + (f.selector == front().selector && emptyBefore) || + (f.selector == back().selector && emptyBefore) || + (f.selector == popFront().selector && emptyBefore) || + (f.selector == popBack().selector && emptyBefore) || + f.selector == at_(uint256).selector // revert conditions are verified in onlyOutOfBoundsRevert + ), "only revert if empty or out of bounds"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: at(index) only reverts if index is out of bounds | +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyOutOfBoundsRevert(uint256 index) { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + at_@withrevert(index); + + assert lastReverted <=> index >= length(), "only reverts if index is out of bounds"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: only clear/push/pop operations can change the length of the queue │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noLengthChange(env e) { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + method f; + calldataarg args; + + uint256 lengthBefore = length(); + f(e, args); + uint256 lengthAfter = length(); + + assert lengthAfter != lengthBefore => ( + (f.selector == pushFront(bytes32).selector && lengthAfter == lengthBefore + 1) || + (f.selector == pushBack(bytes32).selector && lengthAfter == lengthBefore + 1) || + (f.selector == popBack().selector && lengthAfter == lengthBefore - 1) || + (f.selector == popFront().selector && lengthAfter == lengthBefore - 1) || + (f.selector == clear().selector && lengthAfter == 0) + ), "length is only affected by clear/pop/push operations"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: only push/pop can change values bounded in the queue (outside values aren't cleared) │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noDataChange(env e) { + requireInvariant boundariesConsistency(); + require boundedQueue(); + + method f; + calldataarg args; + + uint256 index; + bytes32 atBefore = at_(index); + f(e, args); + bytes32 atAfter = at_@withrevert(index); + bool atAfterSuccess = !lastReverted; + + assert !atAfterSuccess <=> ( + f.selector == clear().selector || + (f.selector == popBack().selector && index == length()) || + (f.selector == popFront().selector && index == length()) + ), "indexes of the queue are only removed by clear or pop"; + + assert atAfterSuccess && atAfter != atBefore => ( + f.selector == popFront().selector || + f.selector == pushFront(bytes32).selector + ), "values of the queue are only changed by popFront or pushFront"; +} diff --git a/certora/specs/ERC20.spec b/certora/specs/ERC20.spec new file mode 100644 index 000000000..3bd2b38ba --- /dev/null +++ b/certora/specs/ERC20.spec @@ -0,0 +1,414 @@ +import "helpers/helpers.spec" +import "methods/IERC20.spec" +import "methods/IERC2612.spec" + +methods { + // non standard ERC20 functions + increaseAllowance(address,uint256) returns (bool) + decreaseAllowance(address,uint256) returns (bool) + + // exposed for FV + mint(address,uint256) + burn(address,uint256) +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Ghost & hooks: sum of all balances │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +ghost sumOfBalances() returns uint256 { + init_state axiom sumOfBalances() == 0; +} + +hook Sstore _balances[KEY address addr] uint256 newValue (uint256 oldValue) STORAGE { + havoc sumOfBalances assuming sumOfBalances@new() == sumOfBalances@old() + newValue - oldValue; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: totalSupply is the sum of all balances │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant totalSupplyIsSumOfBalances() + totalSupply() == sumOfBalances() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: balance of address(0) is 0 │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant zeroAddressNoBalance() + balanceOf(0) == 0 + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: only mint and burn can change total supply │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noChangeTotalSupply(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + + method f; + calldataarg args; + + uint256 totalSupplyBefore = totalSupply(); + f(e, args); + uint256 totalSupplyAfter = totalSupply(); + + assert totalSupplyAfter > totalSupplyBefore => f.selector == mint(address,uint256).selector; + assert totalSupplyAfter < totalSupplyBefore => f.selector == burn(address,uint256).selector; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: only the token holder or an approved third party can reduce an account's balance │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyAuthorizedCanTransfer(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + + method f; + calldataarg args; + address account; + + uint256 allowanceBefore = allowance(account, e.msg.sender); + uint256 balanceBefore = balanceOf(account); + f(e, args); + uint256 balanceAfter = balanceOf(account); + + assert ( + balanceAfter < balanceBefore + ) => ( + f.selector == burn(address,uint256).selector || + e.msg.sender == account || + balanceBefore - balanceAfter <= allowanceBefore + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: only the token holder (or a permit) can increase allowance. The spender can decrease it by using it │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyHolderOfSpenderCanChangeAllowance(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + + method f; + calldataarg args; + address holder; + address spender; + + uint256 allowanceBefore = allowance(holder, spender); + f(e, args); + uint256 allowanceAfter = allowance(holder, spender); + + assert ( + allowanceAfter > allowanceBefore + ) => ( + (f.selector == approve(address,uint256).selector && e.msg.sender == holder) || + (f.selector == increaseAllowance(address,uint256).selector && e.msg.sender == holder) || + (f.selector == permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector) + ); + + assert ( + allowanceAfter < allowanceBefore + ) => ( + (f.selector == transferFrom(address,address,uint256).selector && e.msg.sender == spender) || + (f.selector == approve(address,uint256).selector && e.msg.sender == holder ) || + (f.selector == decreaseAllowance(address,uint256).selector && e.msg.sender == holder ) || + (f.selector == permit(address,address,uint256,uint256,uint8,bytes32,bytes32).selector) + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: mint behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule mint(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + require nonpayable(e); + + address to; + address other; + uint256 amount; + + // cache state + uint256 toBalanceBefore = balanceOf(to); + uint256 otherBalanceBefore = balanceOf(other); + uint256 totalSupplyBefore = totalSupply(); + + // run transaction + mint@withrevert(e, to, amount); + + // check outcome + if (lastReverted) { + assert to == 0 || totalSupplyBefore + amount > max_uint256; + } else { + // updates balance and totalSupply + assert balanceOf(to) == toBalanceBefore + amount; + assert totalSupply() == totalSupplyBefore + amount; + + // no other balance is modified + assert balanceOf(other) != otherBalanceBefore => other == to; + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: burn behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule burn(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + require nonpayable(e); + + address from; + address other; + uint256 amount; + + // cache state + uint256 fromBalanceBefore = balanceOf(from); + uint256 otherBalanceBefore = balanceOf(other); + uint256 totalSupplyBefore = totalSupply(); + + // run transaction + burn@withrevert(e, from, amount); + + // check outcome + if (lastReverted) { + assert from == 0 || fromBalanceBefore < amount; + } else { + // updates balance and totalSupply + assert balanceOf(from) == fromBalanceBefore - amount; + assert totalSupply() == totalSupplyBefore - amount; + + // no other balance is modified + assert balanceOf(other) != otherBalanceBefore => other == from; + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: transfer behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule transfer(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + require nonpayable(e); + + address holder = e.msg.sender; + address recipient; + address other; + uint256 amount; + + // cache state + uint256 holderBalanceBefore = balanceOf(holder); + uint256 recipientBalanceBefore = balanceOf(recipient); + uint256 otherBalanceBefore = balanceOf(other); + + // run transaction + transfer@withrevert(e, recipient, amount); + + // check outcome + if (lastReverted) { + assert holder == 0 || recipient == 0 || amount > holderBalanceBefore; + } else { + // balances of holder and recipient are updated + assert balanceOf(holder) == holderBalanceBefore - (holder == recipient ? 0 : amount); + assert balanceOf(recipient) == recipientBalanceBefore + (holder == recipient ? 0 : amount); + + // no other balance is modified + assert balanceOf(other) != otherBalanceBefore => (other == holder || other == recipient); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: transferFrom behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule transferFrom(env e) { + requireInvariant totalSupplyIsSumOfBalances(); + require nonpayable(e); + + address spender = e.msg.sender; + address holder; + address recipient; + address other; + uint256 amount; + + // cache state + uint256 allowanceBefore = allowance(holder, spender); + uint256 holderBalanceBefore = balanceOf(holder); + uint256 recipientBalanceBefore = balanceOf(recipient); + uint256 otherBalanceBefore = balanceOf(other); + + // run transaction + transferFrom@withrevert(e, holder, recipient, amount); + + // check outcome + if (lastReverted) { + assert holder == 0 || recipient == 0 || spender == 0 || amount > holderBalanceBefore || amount > allowanceBefore; + } else { + // allowance is valid & updated + assert allowanceBefore >= amount; + assert allowance(holder, spender) == (allowanceBefore == max_uint256 ? to_uint256(max_uint256) : allowanceBefore - amount); + + // balances of holder and recipient are updated + assert balanceOf(holder) == holderBalanceBefore - (holder == recipient ? 0 : amount); + assert balanceOf(recipient) == recipientBalanceBefore + (holder == recipient ? 0 : amount); + + // no other balance is modified + assert balanceOf(other) != otherBalanceBefore => (other == holder || other == recipient); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: approve behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule approve(env e) { + require nonpayable(e); + + address holder = e.msg.sender; + address spender; + address otherHolder; + address otherSpender; + uint256 amount; + + // cache state + uint256 otherAllowanceBefore = allowance(otherHolder, otherSpender); + + // run transaction + approve@withrevert(e, spender, amount); + + // check outcome + if (lastReverted) { + assert holder == 0 || spender == 0; + } else { + // allowance is updated + assert allowance(holder, spender) == amount; + + // other allowances are untouched + assert allowance(otherHolder, otherSpender) != otherAllowanceBefore => (otherHolder == holder && otherSpender == spender); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: increaseAllowance behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule increaseAllowance(env e) { + require nonpayable(e); + + address holder = e.msg.sender; + address spender; + address otherHolder; + address otherSpender; + uint256 amount; + + // cache state + uint256 allowanceBefore = allowance(holder, spender); + uint256 otherAllowanceBefore = allowance(otherHolder, otherSpender); + + // run transaction + increaseAllowance@withrevert(e, spender, amount); + + // check outcome + if (lastReverted) { + assert holder == 0 || spender == 0 || allowanceBefore + amount > max_uint256; + } else { + // allowance is updated + assert allowance(holder, spender) == allowanceBefore + amount; + + // other allowances are untouched + assert allowance(otherHolder, otherSpender) != otherAllowanceBefore => (otherHolder == holder && otherSpender == spender); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: decreaseAllowance behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule decreaseAllowance(env e) { + require nonpayable(e); + + address holder = e.msg.sender; + address spender; + address otherHolder; + address otherSpender; + uint256 amount; + + // cache state + uint256 allowanceBefore = allowance(holder, spender); + uint256 otherAllowanceBefore = allowance(otherHolder, otherSpender); + + // run transaction + decreaseAllowance@withrevert(e, spender, amount); + + // check outcome + if (lastReverted) { + assert holder == 0 || spender == 0 || allowanceBefore < amount; + } else { + // allowance is updated + assert allowance(holder, spender) == allowanceBefore - amount; + + // other allowances are untouched + assert allowance(otherHolder, otherSpender) != otherAllowanceBefore => (otherHolder == holder && otherSpender == spender); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: permit behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule permit(env e) { + require nonpayable(e); + + address holder; + address spender; + uint256 amount; + uint256 deadline; + uint8 v; + bytes32 r; + bytes32 s; + + address account1; + address account2; + address account3; + + // cache state + uint256 nonceBefore = nonces(holder); + uint256 otherNonceBefore = nonces(account1); + uint256 otherAllowanceBefore = allowance(account2, account3); + + // sanity: nonce overflow, which possible in theory, is assumed to be impossible in practice + require nonceBefore < max_uint256; + require otherNonceBefore < max_uint256; + + // run transaction + permit@withrevert(e, holder, spender, amount, deadline, v, r, s); + + // check outcome + if (lastReverted) { + // Without formally checking the signature, we can't verify exactly the revert causes + assert true; + } else { + // allowance and nonce are updated + assert allowance(holder, spender) == amount; + assert nonces(holder) == nonceBefore + 1; + + // deadline was respected + assert deadline >= e.block.timestamp; + + // no other allowance or nonce is modified + assert nonces(account1) != otherNonceBefore => account1 == holder; + assert allowance(account2, account3) != otherAllowanceBefore => (account2 == holder && account3 == spender); + } +} diff --git a/certora/specs/ERC20FlashMint.spec b/certora/specs/ERC20FlashMint.spec new file mode 100644 index 000000000..70a7c0795 --- /dev/null +++ b/certora/specs/ERC20FlashMint.spec @@ -0,0 +1,48 @@ +import "helpers/helpers.spec" +import "methods/IERC20.spec" +import "methods/IERC3156.spec" + +methods { + // non standard ERC3156 functions + flashFeeReceiver() returns (address) envfree + + // function summaries below + _mint(address account, uint256 amount) => specMint(account, amount) + _burn(address account, uint256 amount) => specBurn(account, amount) + _transfer(address from, address to, uint256 amount) => specTransfer(from, to, amount) +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Ghost: track mint and burns in the CVL │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +ghost mapping(address => uint256) trackedMintAmount; +ghost mapping(address => uint256) trackedBurnAmount; +ghost mapping(address => mapping(address => uint256)) trackedTransferedAmount; + +function specMint(address account, uint256 amount) returns bool { trackedMintAmount[account] = amount; return true; } +function specBurn(address account, uint256 amount) returns bool { trackedBurnAmount[account] = amount; return true; } +function specTransfer(address from, address to, uint256 amount) returns bool { trackedTransferedAmount[from][to] = amount; return true; } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: When doing a flashLoan, "amount" is minted and burnt, additionally, the fee is either burnt │ +│ (if the fee recipient is 0) or transferred (if the fee recipient is not 0) │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule checkMintAndBurn(env e) { + address receiver; + address token; + uint256 amount; + bytes data; + + uint256 fees = flashFee(token, amount); + address recipient = flashFeeReceiver(); + + flashLoan(e, receiver, token, amount, data); + + assert trackedMintAmount[receiver] == amount; + assert trackedBurnAmount[receiver] == amount + (recipient == 0 ? fees : 0); + assert (fees > 0 && recipient != 0) => trackedTransferedAmount[receiver][recipient] == fees; +} diff --git a/certora/specs/ERC20Wrapper.spec b/certora/specs/ERC20Wrapper.spec new file mode 100644 index 000000000..badfa7a28 --- /dev/null +++ b/certora/specs/ERC20Wrapper.spec @@ -0,0 +1,198 @@ +import "helpers/helpers.spec" +import "ERC20.spec" + +methods { + underlying() returns(address) envfree + underlyingTotalSupply() returns(uint256) envfree + underlyingBalanceOf(address) returns(uint256) envfree + underlyingAllowanceToThis(address) returns(uint256) envfree + + depositFor(address, uint256) returns(bool) + withdrawTo(address, uint256) returns(bool) + recover(address) returns(uint256) +} + +use invariant totalSupplyIsSumOfBalances + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helper: consequence of `totalSupplyIsSumOfBalances` applied to underlying │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +function underlyingBalancesLowerThanUnderlyingSupply(address a) returns bool { + return underlyingBalanceOf(a) <= underlyingTotalSupply(); +} + +function sumOfUnderlyingBalancesLowerThanUnderlyingSupply(address a, address b) returns bool { + return a != b => underlyingBalanceOf(a) + underlyingBalanceOf(b) <= underlyingTotalSupply(); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: wrapped token can't be undercollateralized (solvency of the wrapper) │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant totalSupplyIsSmallerThanUnderlyingBalance() + totalSupply() <= underlyingBalanceOf(currentContract) && + underlyingBalanceOf(currentContract) <= underlyingTotalSupply() && + underlyingTotalSupply() <= max_uint256 + { + preserved { + requireInvariant totalSupplyIsSumOfBalances; + require underlyingBalancesLowerThanUnderlyingSupply(currentContract); + } + preserved depositFor(address account, uint256 amount) with (env e) { + require sumOfUnderlyingBalancesLowerThanUnderlyingSupply(e.msg.sender, currentContract); + } + } + +invariant noSelfWrap() + currentContract != underlying() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: depositFor liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule depositFor(env e) { + require nonpayable(e); + + address sender = e.msg.sender; + address receiver; + address other; + uint256 amount; + + // sanity + requireInvariant noSelfWrap; + requireInvariant totalSupplyIsSumOfBalances; + requireInvariant totalSupplyIsSmallerThanUnderlyingBalance; + require sumOfUnderlyingBalancesLowerThanUnderlyingSupply(currentContract, sender); + + uint256 balanceBefore = balanceOf(receiver); + uint256 supplyBefore = totalSupply(); + uint256 senderUnderlyingBalanceBefore = underlyingBalanceOf(sender); + uint256 senderUnderlyingAllowanceBefore = underlyingAllowanceToThis(sender); + uint256 wrapperUnderlyingBalanceBefore = underlyingBalanceOf(currentContract); + uint256 underlyingSupplyBefore = underlyingTotalSupply(); + + uint256 otherBalanceBefore = balanceOf(other); + uint256 otherUnderlyingBalanceBefore = underlyingBalanceOf(other); + + depositFor@withrevert(e, receiver, amount); + bool success = !lastReverted; + + // liveness + assert success <=> ( + sender != currentContract && // invalid sender + sender != 0 && // invalid sender + receiver != 0 && // invalid receiver + amount <= senderUnderlyingBalanceBefore && // deposit doesn't exceed balance + amount <= senderUnderlyingAllowanceBefore // deposit doesn't exceed allowance + ); + + // effects + assert success => ( + balanceOf(receiver) == balanceBefore + amount && + totalSupply() == supplyBefore + amount && + underlyingBalanceOf(currentContract) == wrapperUnderlyingBalanceBefore + amount && + underlyingBalanceOf(sender) == senderUnderlyingBalanceBefore - amount + ); + + // no side effect + assert underlyingTotalSupply() == underlyingSupplyBefore; + assert balanceOf(other) != otherBalanceBefore => other == receiver; + assert underlyingBalanceOf(other) != otherUnderlyingBalanceBefore => (other == sender || other == currentContract); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: withdrawTo liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule withdrawTo(env e) { + require nonpayable(e); + + address sender = e.msg.sender; + address receiver; + address other; + uint256 amount; + + // sanity + requireInvariant noSelfWrap; + requireInvariant totalSupplyIsSumOfBalances; + requireInvariant totalSupplyIsSmallerThanUnderlyingBalance; + require sumOfUnderlyingBalancesLowerThanUnderlyingSupply(currentContract, receiver); + + uint256 balanceBefore = balanceOf(sender); + uint256 supplyBefore = totalSupply(); + uint256 receiverUnderlyingBalanceBefore = underlyingBalanceOf(receiver); + uint256 wrapperUnderlyingBalanceBefore = underlyingBalanceOf(currentContract); + uint256 underlyingSupplyBefore = underlyingTotalSupply(); + + uint256 otherBalanceBefore = balanceOf(other); + uint256 otherUnderlyingBalanceBefore = underlyingBalanceOf(other); + + withdrawTo@withrevert(e, receiver, amount); + bool success = !lastReverted; + + // liveness + assert success <=> ( + sender != 0 && // invalid sender + receiver != 0 && // invalid receiver + amount <= balanceBefore // withdraw doesn't exceed balance + ); + + // effects + assert success => ( + balanceOf(sender) == balanceBefore - amount && + totalSupply() == supplyBefore - amount && + underlyingBalanceOf(currentContract) == wrapperUnderlyingBalanceBefore - (currentContract != receiver ? amount : 0) && + underlyingBalanceOf(receiver) == receiverUnderlyingBalanceBefore + (currentContract != receiver ? amount : 0) + ); + + // no side effect + assert underlyingTotalSupply() == underlyingSupplyBefore; + assert balanceOf(other) != otherBalanceBefore => other == sender; + assert underlyingBalanceOf(other) != otherUnderlyingBalanceBefore => (other == receiver || other == currentContract); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: recover liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule recover(env e) { + require nonpayable(e); + + address receiver; + address other; + + // sanity + requireInvariant noSelfWrap; + requireInvariant totalSupplyIsSumOfBalances; + requireInvariant totalSupplyIsSmallerThanUnderlyingBalance; + + uint256 value = underlyingBalanceOf(currentContract) - totalSupply(); + uint256 supplyBefore = totalSupply(); + uint256 balanceBefore = balanceOf(receiver); + + uint256 otherBalanceBefore = balanceOf(other); + uint256 otherUnderlyingBalanceBefore = underlyingBalanceOf(other); + + recover@withrevert(e, receiver); + bool success = !lastReverted; + + // liveness + assert success <=> receiver != 0; + + // effect + assert success => ( + balanceOf(receiver) == balanceBefore + value && + totalSupply() == supplyBefore + value && + totalSupply() == underlyingBalanceOf(currentContract) + ); + + // no side effect + assert underlyingBalanceOf(other) == otherUnderlyingBalanceBefore; + assert balanceOf(other) != otherBalanceBefore => other == receiver; +} diff --git a/certora/specs/ERC721.spec b/certora/specs/ERC721.spec new file mode 100644 index 000000000..9db13f45c --- /dev/null +++ b/certora/specs/ERC721.spec @@ -0,0 +1,589 @@ +import "helpers/helpers.spec" +import "methods/IERC721.spec" + +methods { + // exposed for FV + mint(address,uint256) + safeMint(address,uint256) + safeMint(address,uint256,bytes) + burn(uint256) + + tokenExists(uint256) returns (bool) envfree + unsafeOwnerOf(uint256) returns (address) envfree + unsafeGetApproved(uint256) returns (address) envfree +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ + +// Could be broken in theory, but not in practice +function balanceLimited(address account) returns bool { + return balanceOf(account) < max_uint256; +} + +function helperTransferWithRevert(env e, method f, address from, address to, uint256 tokenId) { + if (f.selector == transferFrom(address,address,uint256).selector) { + transferFrom@withrevert(e, from, to, tokenId); + } else if (f.selector == safeTransferFrom(address,address,uint256).selector) { + safeTransferFrom@withrevert(e, from, to, tokenId); + } else if (f.selector == safeTransferFrom(address,address,uint256,bytes).selector) { + bytes params; + require params.length < 0xffff; + safeTransferFrom@withrevert(e, from, to, tokenId, params); + } else { + calldataarg args; + f@withrevert(e, args); + } +} + +function helperMintWithRevert(env e, method f, address to, uint256 tokenId) { + if (f.selector == mint(address,uint256).selector) { + mint@withrevert(e, to, tokenId); + } else if (f.selector == safeMint(address,uint256).selector) { + safeMint@withrevert(e, to, tokenId); + } else if (f.selector == safeMint(address,uint256,bytes).selector) { + bytes params; + require params.length < 0xffff; + safeMint@withrevert(e, to, tokenId, params); + } else { + require false; + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Ghost & hooks: ownership count │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +ghost ownedTotal() returns uint256 { + init_state axiom ownedTotal() == 0; +} + +ghost mapping(address => uint256) ownedByUser { + init_state axiom forall address a. ownedByUser[a] == 0; +} + +hook Sstore _owners[KEY uint256 tokenId] address newOwner (address oldOwner) STORAGE { + ownedByUser[newOwner] = ownedByUser[newOwner] + to_uint256(newOwner != 0 ? 1 : 0); + ownedByUser[oldOwner] = ownedByUser[oldOwner] - to_uint256(oldOwner != 0 ? 1 : 0); + + havoc ownedTotal assuming ownedTotal@new() == ownedTotal@old() + + to_uint256(newOwner != 0 ? 1 : 0) + - to_uint256(oldOwner != 0 ? 1 : 0); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Ghost & hooks: sum of all balances │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +ghost sumOfBalances() returns uint256 { + init_state axiom sumOfBalances() == 0; +} + +hook Sstore _balances[KEY address addr] uint256 newValue (uint256 oldValue) STORAGE { + havoc sumOfBalances assuming sumOfBalances@new() == sumOfBalances@old() + newValue - oldValue; +} + +ghost mapping(address => uint256) ghostBalanceOf { + init_state axiom forall address a. ghostBalanceOf[a] == 0; +} + +hook Sload uint256 value _balances[KEY address user] STORAGE { + require ghostBalanceOf[user] == value; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: ownedTotal is the sum of all balances │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant ownedTotalIsSumOfBalances() + ownedTotal() == sumOfBalances() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: balanceOf is the number of tokens owned │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant balanceOfConsistency(address user) + balanceOf(user) == ownedByUser[user] && + balanceOf(user) == ghostBalanceOf[user] + { + preserved { + require balanceLimited(user); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: owner of a token must have some balance │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant ownerHasBalance(uint256 tokenId) + balanceOf(ownerOf(tokenId)) > 0 + { + preserved { + requireInvariant balanceOfConsistency(ownerOf(tokenId)); + require balanceLimited(ownerOf(tokenId)); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: tokens that do not exist are not owned and not approved │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant notMintedUnset(uint256 tokenId) + (!tokenExists(tokenId) <=> unsafeOwnerOf(tokenId) == 0) && + (!tokenExists(tokenId) => unsafeGetApproved(tokenId) == 0) + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: ownerOf and getApproved revert if token does not exist │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule notMintedRevert(uint256 tokenId) { + requireInvariant notMintedUnset(tokenId); + + bool e = tokenExists(tokenId); + + address owner = ownerOf@withrevert(tokenId); + assert e <=> !lastReverted; + assert e => owner == unsafeOwnerOf(tokenId); // notMintedUnset tells us this is non-zero + + address approved = getApproved@withrevert(tokenId); + assert e <=> !lastReverted; + assert e => approved == unsafeGetApproved(tokenId); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: unsafeOwnerOf and unsafeGetApproved don't revert │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule unsafeDontRevert(uint256 tokenId) { + unsafeOwnerOf@withrevert(tokenId); + assert !lastReverted; + + unsafeGetApproved@withrevert(tokenId); + assert !lastReverted; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: balance of address(0) is 0 │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule zeroAddressBalanceRevert() { + balanceOf@withrevert(0); + assert lastReverted; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: total supply can only change through mint and burn │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule supplyChange(env e) { + uint256 supplyBefore = ownedTotal(); + method f; calldataarg args; f(e, args); + uint256 supplyAfter = ownedTotal(); + + assert supplyAfter > supplyBefore => ( + supplyAfter == supplyBefore + 1 && + ( + f.selector == mint(address,uint256).selector || + f.selector == safeMint(address,uint256).selector || + f.selector == safeMint(address,uint256,bytes).selector + ) + ); + assert supplyAfter < supplyBefore => ( + supplyAfter == supplyBefore - 1 && + f.selector == burn(uint256).selector + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: balanceOf can only change through mint, burn or transfers. balanceOf cannot change by more than 1. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule balanceChange(env e, address account) { + requireInvariant balanceOfConsistency(account); + require balanceLimited(account); + + uint256 balanceBefore = balanceOf(account); + method f; calldataarg args; f(e, args); + uint256 balanceAfter = balanceOf(account); + + // balance can change by at most 1 + assert balanceBefore != balanceAfter => ( + balanceAfter == balanceBefore - 1 || + balanceAfter == balanceBefore + 1 + ); + + // only selected function can change balances + assert balanceBefore != balanceAfter => ( + f.selector == transferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256,bytes).selector || + f.selector == mint(address,uint256).selector || + f.selector == safeMint(address,uint256).selector || + f.selector == safeMint(address,uint256,bytes).selector || + f.selector == burn(uint256).selector + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: ownership can only change through mint, burn or transfers. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule ownershipChange(env e, uint256 tokenId) { + address ownerBefore = unsafeOwnerOf(tokenId); + method f; calldataarg args; f(e, args); + address ownerAfter = unsafeOwnerOf(tokenId); + + assert ownerBefore == 0 && ownerAfter != 0 => ( + f.selector == mint(address,uint256).selector || + f.selector == safeMint(address,uint256).selector || + f.selector == safeMint(address,uint256,bytes).selector + ); + + assert ownerBefore != 0 && ownerAfter == 0 => ( + f.selector == burn(uint256).selector + ); + + assert (ownerBefore != ownerAfter && ownerBefore != 0 && ownerAfter != 0) => ( + f.selector == transferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256,bytes).selector + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: token approval can only change through approve or transfers (implicitly). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule approvalChange(env e, uint256 tokenId) { + address approvalBefore = unsafeGetApproved(tokenId); + method f; calldataarg args; f(e, args); + address approvalAfter = unsafeGetApproved(tokenId); + + // approve can set any value, other functions reset + assert approvalBefore != approvalAfter => ( + f.selector == approve(address,uint256).selector || + ( + ( + f.selector == transferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256,bytes).selector || + f.selector == burn(uint256).selector + ) && approvalAfter == 0 + ) + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: approval for all tokens can only change through isApprovedForAll. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule approvedForAllChange(env e, address owner, address spender) { + bool approvedForAllBefore = isApprovedForAll(owner, spender); + method f; calldataarg args; f(e, args); + bool approvedForAllAfter = isApprovedForAll(owner, spender); + + assert approvedForAllBefore != approvedForAllAfter => f.selector == setApprovalForAll(address,bool).selector; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: transferFrom behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule transferFrom(env e, address from, address to, uint256 tokenId) { + require nonpayable(e); + + address operator = e.msg.sender; + uint256 otherTokenId; + address otherAccount; + + requireInvariant ownerHasBalance(tokenId); + require balanceLimited(to); + + uint256 balanceOfFromBefore = balanceOf(from); + uint256 balanceOfToBefore = balanceOf(to); + uint256 balanceOfOtherBefore = balanceOf(otherAccount); + address ownerBefore = unsafeOwnerOf(tokenId); + address otherOwnerBefore = unsafeOwnerOf(otherTokenId); + address approvalBefore = unsafeGetApproved(tokenId); + address otherApprovalBefore = unsafeGetApproved(otherTokenId); + + transferFrom@withrevert(e, from, to, tokenId); + bool success = !lastReverted; + + // liveness + assert success <=> ( + from == ownerBefore && + from != 0 && + to != 0 && + (operator == from || operator == approvalBefore || isApprovedForAll(ownerBefore, operator)) + ); + + // effect + assert success => ( + balanceOf(from) == balanceOfFromBefore - to_uint256(from != to ? 1 : 0) && + balanceOf(to) == balanceOfToBefore + to_uint256(from != to ? 1 : 0) && + unsafeOwnerOf(tokenId) == to && + unsafeGetApproved(tokenId) == 0 + ); + + // no side effect + assert balanceOf(otherAccount) != balanceOfOtherBefore => (otherAccount == from || otherAccount == to); + assert unsafeOwnerOf(otherTokenId) != otherOwnerBefore => otherTokenId == tokenId; + assert unsafeGetApproved(otherTokenId) != otherApprovalBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: safeTransferFrom behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule safeTransferFrom(env e, method f, address from, address to, uint256 tokenId) filtered { f -> + f.selector == safeTransferFrom(address,address,uint256).selector || + f.selector == safeTransferFrom(address,address,uint256,bytes).selector +} { + require nonpayable(e); + + address operator = e.msg.sender; + uint256 otherTokenId; + address otherAccount; + + requireInvariant ownerHasBalance(tokenId); + require balanceLimited(to); + + uint256 balanceOfFromBefore = balanceOf(from); + uint256 balanceOfToBefore = balanceOf(to); + uint256 balanceOfOtherBefore = balanceOf(otherAccount); + address ownerBefore = unsafeOwnerOf(tokenId); + address otherOwnerBefore = unsafeOwnerOf(otherTokenId); + address approvalBefore = unsafeGetApproved(tokenId); + address otherApprovalBefore = unsafeGetApproved(otherTokenId); + + helperTransferWithRevert(e, f, from, to, tokenId); + bool success = !lastReverted; + + assert success <=> ( + from == ownerBefore && + from != 0 && + to != 0 && + (operator == from || operator == approvalBefore || isApprovedForAll(ownerBefore, operator)) + ); + + // effect + assert success => ( + balanceOf(from) == balanceOfFromBefore - to_uint256(from != to ? 1: 0) && + balanceOf(to) == balanceOfToBefore + to_uint256(from != to ? 1: 0) && + unsafeOwnerOf(tokenId) == to && + unsafeGetApproved(tokenId) == 0 + ); + + // no side effect + assert balanceOf(otherAccount) != balanceOfOtherBefore => (otherAccount == from || otherAccount == to); + assert unsafeOwnerOf(otherTokenId) != otherOwnerBefore => otherTokenId == tokenId; + assert unsafeGetApproved(otherTokenId) != otherApprovalBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: mint behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule mint(env e, address to, uint256 tokenId) { + require nonpayable(e); + requireInvariant notMintedUnset(tokenId); + + uint256 otherTokenId; + address otherAccount; + + require balanceLimited(to); + + uint256 supplyBefore = ownedTotal(); + uint256 balanceOfToBefore = balanceOf(to); + uint256 balanceOfOtherBefore = balanceOf(otherAccount); + address ownerBefore = unsafeOwnerOf(tokenId); + address otherOwnerBefore = unsafeOwnerOf(otherTokenId); + + mint@withrevert(e, to, tokenId); + bool success = !lastReverted; + + // liveness + assert success <=> ( + ownerBefore == 0 && + to != 0 + ); + + // effect + assert success => ( + ownedTotal() == supplyBefore + 1 && + balanceOf(to) == balanceOfToBefore + 1 && + unsafeOwnerOf(tokenId) == to + ); + + // no side effect + assert balanceOf(otherAccount) != balanceOfOtherBefore => otherAccount == to; + assert unsafeOwnerOf(otherTokenId) != otherOwnerBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: safeMint behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule safeMint(env e, method f, address to, uint256 tokenId) filtered { f -> + f.selector == safeMint(address,uint256).selector || + f.selector == safeMint(address,uint256,bytes).selector +} { + require nonpayable(e); + requireInvariant notMintedUnset(tokenId); + + uint256 otherTokenId; + address otherAccount; + + require balanceLimited(to); + + uint256 supplyBefore = ownedTotal(); + uint256 balanceOfToBefore = balanceOf(to); + uint256 balanceOfOtherBefore = balanceOf(otherAccount); + address ownerBefore = unsafeOwnerOf(tokenId); + address otherOwnerBefore = unsafeOwnerOf(otherTokenId); + + helperMintWithRevert(e, f, to, tokenId); + bool success = !lastReverted; + + assert success <=> ( + ownerBefore == 0 && + to != 0 + ); + + // effect + assert success => ( + ownedTotal() == supplyBefore + 1 && + balanceOf(to) == balanceOfToBefore + 1 && + unsafeOwnerOf(tokenId) == to + ); + + // no side effect + assert balanceOf(otherAccount) != balanceOfOtherBefore => otherAccount == to; + assert unsafeOwnerOf(otherTokenId) != otherOwnerBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: burn behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule burn(env e, uint256 tokenId) { + require nonpayable(e); + + address from = unsafeOwnerOf(tokenId); + uint256 otherTokenId; + address otherAccount; + + requireInvariant ownerHasBalance(tokenId); + + uint256 supplyBefore = ownedTotal(); + uint256 balanceOfFromBefore = balanceOf(from); + uint256 balanceOfOtherBefore = balanceOf(otherAccount); + address ownerBefore = unsafeOwnerOf(tokenId); + address otherOwnerBefore = unsafeOwnerOf(otherTokenId); + address otherApprovalBefore = unsafeGetApproved(otherTokenId); + + burn@withrevert(e, tokenId); + bool success = !lastReverted; + + // liveness + assert success <=> ( + ownerBefore != 0 + ); + + // effect + assert success => ( + ownedTotal() == supplyBefore - 1 && + balanceOf(from) == balanceOfFromBefore - 1 && + unsafeOwnerOf(tokenId) == 0 && + unsafeGetApproved(tokenId) == 0 + ); + + // no side effect + assert balanceOf(otherAccount) != balanceOfOtherBefore => otherAccount == from; + assert unsafeOwnerOf(otherTokenId) != otherOwnerBefore => otherTokenId == tokenId; + assert unsafeGetApproved(otherTokenId) != otherApprovalBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: approve behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule approve(env e, address spender, uint256 tokenId) { + require nonpayable(e); + + address caller = e.msg.sender; + address owner = unsafeOwnerOf(tokenId); + uint256 otherTokenId; + + address otherApprovalBefore = unsafeGetApproved(otherTokenId); + + approve@withrevert(e, spender, tokenId); + bool success = !lastReverted; + + // liveness + assert success <=> ( + owner != 0 && + owner != spender && + (owner == caller || isApprovedForAll(owner, caller)) + ); + + // effect + assert success => unsafeGetApproved(tokenId) == spender; + + // no side effect + assert unsafeGetApproved(otherTokenId) != otherApprovalBefore => otherTokenId == tokenId; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: setApprovalForAll behavior and side effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule setApprovalForAll(env e, address operator, bool approved) { + require nonpayable(e); + + address owner = e.msg.sender; + address otherOwner; + address otherOperator; + + bool otherIsApprovedForAllBefore = isApprovedForAll(otherOwner, otherOperator); + + setApprovalForAll@withrevert(e, operator, approved); + bool success = !lastReverted; + + // liveness + assert success <=> owner != operator; + + // effect + assert success => isApprovedForAll(owner, operator) == approved; + + // no side effect + assert isApprovedForAll(otherOwner, otherOperator) != otherIsApprovedForAllBefore => ( + otherOwner == owner && + otherOperator == operator + ); +} diff --git a/certora/specs/EnumerableMap.spec b/certora/specs/EnumerableMap.spec new file mode 100644 index 000000000..dea5d85ec --- /dev/null +++ b/certora/specs/EnumerableMap.spec @@ -0,0 +1,334 @@ +import "helpers/helpers.spec" + +methods { + // library + set(bytes32,bytes32) returns (bool) envfree + remove(bytes32) returns (bool) envfree + contains(bytes32) returns (bool) envfree + length() returns (uint256) envfree + key_at(uint256) returns (bytes32) envfree + value_at(uint256) returns (bytes32) envfree + tryGet_contains(bytes32) returns (bool) envfree + tryGet_value(bytes32) returns (bytes32) envfree + get(bytes32) returns (bytes32) envfree + + // FV + _indexOf(bytes32) returns (uint256) envfree +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +function sanity() returns bool { + return length() < max_uint256; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: the value mapping is empty for keys that are not in the EnumerableMap. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant noValueIfNotContained(bytes32 key) + !contains(key) => tryGet_value(key) == 0 + { + preserved set(bytes32 otherKey, bytes32 someValue) { + require sanity(); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: All indexed keys are contained │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant indexedContained(uint256 index) + index < length() => contains(key_at(index)) + { + preserved { + requireInvariant consistencyIndex(index); + requireInvariant consistencyIndex(to_uint256(length() - 1)); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: A value can only be stored at a single location │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant atUniqueness(uint256 index1, uint256 index2) + index1 == index2 <=> key_at(index1) == key_at(index2) + { + preserved remove(bytes32 key) { + requireInvariant atUniqueness(index1, to_uint256(length() - 1)); + requireInvariant atUniqueness(index2, to_uint256(length() - 1)); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: index <> value relationship is consistent │ +│ │ +│ Note that the two consistencyXxx invariants, put together, prove that at_ and _indexOf are inverse of one another. │ +│ This proves that we have a bijection between indices (the enumerability part) and keys (the entries that are set │ +│ and removed from the EnumerableMap). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant consistencyIndex(uint256 index) + index < length() => _indexOf(key_at(index)) == index + 1 + { + preserved remove(bytes32 key) { + requireInvariant consistencyIndex(to_uint256(length() - 1)); + } + } + +invariant consistencyKey(bytes32 key) + contains(key) => ( + _indexOf(key) > 0 && + _indexOf(key) <= length() && + key_at(to_uint256(_indexOf(key) - 1)) == key + ) + { + preserved remove(bytes32 otherKey) { + requireInvariant consistencyKey(otherKey); + requireInvariant atUniqueness( + to_uint256(_indexOf(key) - 1), + to_uint256(_indexOf(otherKey) - 1) + ); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: state only changes by setting or removing elements │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule stateChange(env e, bytes32 key) { + require sanity(); + requireInvariant consistencyKey(key); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + bytes32 valueBefore = tryGet_value(key); + + method f; + calldataarg args; + f(e, args); + + uint256 lengthAfter = length(); + bool containsAfter = contains(key); + bytes32 valueAfter = tryGet_value(key); + + assert lengthBefore != lengthAfter => ( + (f.selector == set(bytes32,bytes32).selector && lengthAfter == lengthBefore + 1) || + (f.selector == remove(bytes32).selector && lengthAfter == lengthBefore - 1) + ); + + assert containsBefore != containsAfter => ( + (f.selector == set(bytes32,bytes32).selector && containsAfter) || + (f.selector == remove(bytes32).selector && !containsAfter) + ); + + assert valueBefore != valueAfter => ( + (f.selector == set(bytes32,bytes32).selector && containsAfter) || + (f.selector == remove(bytes32).selector && !containsAfter && valueAfter == 0) + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: check liveness of view functions. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule liveness_1(bytes32 key) { + requireInvariant consistencyKey(key); + + // contains never revert + bool contains = contains@withrevert(key); + assert !lastReverted; + + // tryGet never reverts (key) + tryGet_contains@withrevert(key); + assert !lastReverted; + + // tryGet never reverts (value) + tryGet_value@withrevert(key); + assert !lastReverted; + + // get reverts iff the key is not in the map + get@withrevert(key); + assert !lastReverted <=> contains; +} + +rule liveness_2(uint256 index) { + requireInvariant consistencyIndex(index); + + // length never revert + uint256 length = length@withrevert(); + assert !lastReverted; + + // key_at reverts iff the index is out of bound + key_at@withrevert(index); + assert !lastReverted <=> index < length; + + // value_at reverts iff the index is out of bound + value_at@withrevert(index); + assert !lastReverted <=> index < length; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: get and tryGet return the expected values. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule getAndTryGet(bytes32 key) { + requireInvariant noValueIfNotContained(key); + + bool contained = contains(key); + bool tryContained = tryGet_contains(key); + bytes32 tryValue = tryGet_value(key); + bytes32 value = get@withrevert(key); // revert is not contained + + assert contained == tryContained; + assert contained => tryValue == value; + assert !contained => tryValue == 0; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: set key-value in EnumerableMap │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule set(bytes32 key, bytes32 value, bytes32 otherKey) { + require sanity(); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + bool containsOtherBefore = contains(otherKey); + bytes32 otherValueBefore = tryGet_value(otherKey); + + bool added = set@withrevert(key, value); + bool success = !lastReverted; + + assert success && contains(key) && get(key) == value, + "liveness & immediate effect"; + + assert added <=> !containsBefore, + "return value: added iff not contained"; + + assert length() == lengthBefore + to_mathint(added ? 1 : 0), + "effect: length increases iff added"; + + assert added => (key_at(lengthBefore) == key && value_at(lengthBefore) == value), + "effect: add at the end"; + + assert containsOtherBefore != contains(otherKey) => (added && key == otherKey), + "side effect: other keys are not affected"; + + assert otherValueBefore != tryGet_value(otherKey) => key == otherKey, + "side effect: values attached to other keys are not affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: remove key from EnumerableMap │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule remove(bytes32 key, bytes32 otherKey) { + requireInvariant consistencyKey(key); + requireInvariant consistencyKey(otherKey); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + bool containsOtherBefore = contains(otherKey); + bytes32 otherValueBefore = tryGet_value(otherKey); + + bool removed = remove@withrevert(key); + bool success = !lastReverted; + + assert success && !contains(key), + "liveness & immediate effect"; + + assert removed <=> containsBefore, + "return value: removed iff contained"; + + assert length() == lengthBefore - to_mathint(removed ? 1 : 0), + "effect: length decreases iff removed"; + + assert containsOtherBefore != contains(otherKey) => (removed && key == otherKey), + "side effect: other keys are not affected"; + + assert otherValueBefore != tryGet_value(otherKey) => key == otherKey, + "side effect: values attached to other keys are not affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: when adding a new key, the other keys remain in set, at the same index. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule setEnumerability(bytes32 key, bytes32 value, uint256 index) { + require sanity(); + + bytes32 atKeyBefore = key_at(index); + bytes32 atValueBefore = value_at(index); + + set(key, value); + + bytes32 atKeyAfter = key_at@withrevert(index); + assert !lastReverted; + + bytes32 atValueAfter = value_at@withrevert(index); + assert !lastReverted; + + assert atKeyAfter == atKeyBefore; + assert atValueAfter != atValueBefore => ( + key == atKeyBefore && + value == atValueAfter + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: when removing a existing key, the other keys remain in set, at the same index (except for the last one). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule removeEnumerability(bytes32 key, uint256 index) { + uint256 last = length() - 1; + + requireInvariant consistencyKey(key); + requireInvariant consistencyIndex(index); + requireInvariant consistencyIndex(last); + + bytes32 atKeyBefore = key_at(index); + bytes32 atValueBefore = value_at(index); + bytes32 lastKeyBefore = key_at(last); + bytes32 lastValueBefore = value_at(last); + + remove(key); + + // can't read last value & keys (length decreased) + bytes32 atKeyAfter = key_at@withrevert(index); + assert lastReverted <=> index == last; + + bytes32 atValueAfter = value_at@withrevert(index); + assert lastReverted <=> index == last; + + // One value that is allowed to change is if previous value was removed, + // in that case the last value before took its place. + assert ( + index != last && + atKeyBefore != atKeyAfter + ) => ( + atKeyBefore == key && + atKeyAfter == lastKeyBefore + ); + + assert ( + index != last && + atValueBefore != atValueAfter + ) => ( + atValueAfter == lastValueBefore + ); +} diff --git a/certora/specs/EnumerableSet.spec b/certora/specs/EnumerableSet.spec new file mode 100644 index 000000000..d63c556aa --- /dev/null +++ b/certora/specs/EnumerableSet.spec @@ -0,0 +1,247 @@ +import "helpers/helpers.spec" + +methods { + // library + add(bytes32) returns (bool) envfree + remove(bytes32) returns (bool) envfree + contains(bytes32) returns (bool) envfree + length() returns (uint256) envfree + at_(uint256) returns (bytes32) envfree + + // FV + _indexOf(bytes32) returns (uint256) envfree +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +function sanity() returns bool { + return length() < max_uint256; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: All indexed keys are contained │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant indexedContained(uint256 index) + index < length() => contains(at_(index)) + { + preserved { + requireInvariant consistencyIndex(index); + requireInvariant consistencyIndex(to_uint256(length() - 1)); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: A value can only be stored at a single location │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant atUniqueness(uint256 index1, uint256 index2) + index1 == index2 <=> at_(index1) == at_(index2) + { + preserved remove(bytes32 key) { + requireInvariant atUniqueness(index1, to_uint256(length() - 1)); + requireInvariant atUniqueness(index2, to_uint256(length() - 1)); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: index <> key relationship is consistent │ +│ │ +│ Note that the two consistencyXxx invariants, put together, prove that at_ and _indexOf are inverse of one another. │ +│ This proves that we have a bijection between indices (the enumerability part) and keys (the entries that are added │ +│ and removed from the EnumerableSet). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant consistencyIndex(uint256 index) + index < length() => _indexOf(at_(index)) == index + 1 + { + preserved remove(bytes32 key) { + requireInvariant consistencyIndex(to_uint256(length() - 1)); + } + } + +invariant consistencyKey(bytes32 key) + contains(key) => ( + _indexOf(key) > 0 && + _indexOf(key) <= length() && + at_(to_uint256(_indexOf(key) - 1)) == key + ) + { + preserved remove(bytes32 otherKey) { + requireInvariant consistencyKey(otherKey); + requireInvariant atUniqueness( + to_uint256(_indexOf(key) - 1), + to_uint256(_indexOf(otherKey) - 1) + ); + } + } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: state only changes by adding or removing elements │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule stateChange(env e, bytes32 key) { + require sanity(); + requireInvariant consistencyKey(key); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + + method f; + calldataarg args; + f(e, args); + + uint256 lengthAfter = length(); + bool containsAfter = contains(key); + + assert lengthBefore != lengthAfter => ( + (f.selector == add(bytes32).selector && lengthAfter == lengthBefore + 1) || + (f.selector == remove(bytes32).selector && lengthAfter == lengthBefore - 1) + ); + + assert containsBefore != containsAfter => ( + (f.selector == add(bytes32).selector && containsAfter) || + (f.selector == remove(bytes32).selector && containsBefore) + ); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: check liveness of view functions. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule liveness_1(bytes32 key) { + requireInvariant consistencyKey(key); + + // contains never revert + contains@withrevert(key); + assert !lastReverted; +} + +rule liveness_2(uint256 index) { + requireInvariant consistencyIndex(index); + + // length never revert + uint256 length = length@withrevert(); + assert !lastReverted; + + // at reverts iff the index is out of bound + at_@withrevert(index); + assert !lastReverted <=> index < length; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: add key to EnumerableSet if not already contained │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule add(bytes32 key, bytes32 otherKey) { + require sanity(); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + bool containsOtherBefore = contains(otherKey); + + bool added = add@withrevert(key); + bool success = !lastReverted; + + assert success && contains(key), + "liveness & immediate effect"; + + assert added <=> !containsBefore, + "return value: added iff not contained"; + + assert length() == lengthBefore + to_mathint(added ? 1 : 0), + "effect: length increases iff added"; + + assert added => at_(lengthBefore) == key, + "effect: add at the end"; + + assert containsOtherBefore != contains(otherKey) => (added && key == otherKey), + "side effect: other keys are not affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: remove key from EnumerableSet if already contained │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule remove(bytes32 key, bytes32 otherKey) { + requireInvariant consistencyKey(key); + requireInvariant consistencyKey(otherKey); + + uint256 lengthBefore = length(); + bool containsBefore = contains(key); + bool containsOtherBefore = contains(otherKey); + + bool removed = remove@withrevert(key); + bool success = !lastReverted; + + assert success && !contains(key), + "liveness & immediate effect"; + + assert removed <=> containsBefore, + "return value: removed iff contained"; + + assert length() == lengthBefore - to_mathint(removed ? 1 : 0), + "effect: length decreases iff removed"; + + assert containsOtherBefore != contains(otherKey) => (removed && key == otherKey), + "side effect: other keys are not affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: when adding a new key, the other keys remain in set, at the same index. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule addEnumerability(bytes32 key, uint256 index) { + require sanity(); + + bytes32 atBefore = at_(index); + add(key); + bytes32 atAfter = at_@withrevert(index); + bool atAfterSuccess = !lastReverted; + + assert atAfterSuccess; + assert atBefore == atAfter; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: when removing a existing key, the other keys remain in set, at the same index (except for the last one). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule removeEnumerability(bytes32 key, uint256 index) { + uint256 last = length() - 1; + + requireInvariant consistencyKey(key); + requireInvariant consistencyIndex(index); + requireInvariant consistencyIndex(last); + + bytes32 atBefore = at_(index); + bytes32 lastBefore = at_(last); + + remove(key); + + // can't read last value (length decreased) + bytes32 atAfter = at_@withrevert(index); + assert lastReverted <=> index == last; + + // One value that is allowed to change is if previous value was removed, + // in that case the last value before took its place. + assert ( + index != last && + atBefore != atAfter + ) => ( + atBefore == key && + atAfter == lastBefore + ); +} diff --git a/certora/specs/GovernorBase.spec b/certora/specs/GovernorBase.spec deleted file mode 100644 index de728ddac..000000000 --- a/certora/specs/GovernorBase.spec +++ /dev/null @@ -1,333 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -///////////////////// Governor.sol base definitions ////////////////////////// -////////////////////////////////////////////////////////////////////////////// - -using ERC20VotesHarness as erc20votes - -methods { - proposalSnapshot(uint256) returns uint256 envfree // matches proposalVoteStart - proposalDeadline(uint256) returns uint256 envfree // matches proposalVoteEnd - hashProposal(address[],uint256[],bytes[],bytes32) returns uint256 envfree - isExecuted(uint256) returns bool envfree - isCanceled(uint256) returns bool envfree - execute(address[], uint256[], bytes[], bytes32) returns uint256 - hasVoted(uint256, address) returns bool - castVote(uint256, uint8) returns uint256 - updateQuorumNumerator(uint256) - queue(address[], uint256[], bytes[], bytes32) returns uint256 - - // internal functions made public in harness: - _quorumReached(uint256) returns bool - _voteSucceeded(uint256) returns bool envfree - - // function summarization - proposalThreshold() returns uint256 envfree - - getVotes(address, uint256) returns uint256 => DISPATCHER(true) - - getPastTotalSupply(uint256 t) returns uint256 => PER_CALLEE_CONSTANT - getPastVotes(address a, uint256 t) returns uint256 => PER_CALLEE_CONSTANT - - //scheduleBatch(address[],uint256[],bytes[],bytes32,bytes32,uint256) => DISPATCHER(true) - //executeBatch(address[], uint256[], bytes[], bytes32, bytes32) => DISPATCHER(true) -} - -////////////////////////////////////////////////////////////////////////////// -//////////////////////////////// Definitions ///////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -// proposal was created - relation proved in noStartBeforeCreation -definition proposalCreated(uint256 pId) returns bool = proposalSnapshot(pId) > 0; - - -////////////////////////////////////////////////////////////////////////////// -///////////////////////////// Helper Functions /////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - -function helperFunctionsWithRevert(uint256 proposalId, method f, env e) { - address[] targets; uint256[] values; bytes[] calldatas; string reason; bytes32 descriptionHash; - uint8 support; uint8 v; bytes32 r; bytes32 s; - if (f.selector == propose(address[], uint256[], bytes[], string).selector) { - uint256 result = propose@withrevert(e, targets, values, calldatas, reason); - require(result == proposalId); - } else if (f.selector == execute(address[], uint256[], bytes[], bytes32).selector) { - uint256 result = execute@withrevert(e, targets, values, calldatas, descriptionHash); - require(result == proposalId); - } else if (f.selector == castVote(uint256, uint8).selector) { - castVote@withrevert(e, proposalId, support); - } else if (f.selector == castVoteWithReason(uint256, uint8, string).selector) { - castVoteWithReason@withrevert(e, proposalId, support, reason); - } else if (f.selector == castVoteBySig(uint256, uint8,uint8, bytes32, bytes32).selector) { - castVoteBySig@withrevert(e, proposalId, support, v, r, s); - } else if (f.selector == queue(address[], uint256[], bytes[], bytes32).selector) { - queue@withrevert(e, targets, values, calldatas, descriptionHash); - } else { - calldataarg args; - f@withrevert(e, args); - } -} - -/* - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - ///////////////////////////////////////////////////// State Diagram ////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // // - // castVote(s)() // - // ------------- propose() ---------------------- time pass --------------- time passes ----------- // - // | No Proposal | --------> | Before Start (Delay) | --------> | Voting Period | ----------------------> | execute() | // - // ------------- ---------------------- --------------- -> Executed/Canceled ----------- // - // ------------------------------------------------------------|---------------|-------------------------|--------------> // - // t start end timelock // - // // - ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -*/ - - -/////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////// Global Valid States ///////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////// - - -/* - * Start and end date are either initialized (non zero) or uninitialized (zero) simultaneously - * This invariant assumes that the block number cannot be 0 at any stage of the contract cycle - * This is very safe assumption as usually the 0 block is genesis block which is uploaded with data - * by the developers and will not be valid to raise proposals (at the current way that block chain is functioning) - */ - // To use env with general preserved block disable type checking [--disableLocalTypeChecking] -invariant startAndEndDatesNonZero(uint256 pId) - proposalSnapshot(pId) != 0 <=> proposalDeadline(pId) != 0 - { preserved with (env e){ - require e.block.number > 0; - }} - - -/* - * If a proposal is canceled it must have a start and an end date - */ - // To use env with general preserved block disable type checking [--disableLocalTypeChecking] -invariant canceledImplyStartAndEndDateNonZero(uint pId) - isCanceled(pId) => proposalSnapshot(pId) != 0 - {preserved with (env e){ - require e.block.number > 0; - }} - - -/* - * If a proposal is executed it must have a start and an end date - */ - // To use env with general preserved block disable type checking [--disableLocalTypeChecking] -invariant executedImplyStartAndEndDateNonZero(uint pId) - isExecuted(pId) => proposalSnapshot(pId) != 0 - { preserved with (env e){ - requireInvariant startAndEndDatesNonZero(pId); - require e.block.number > 0; - }} - - -/* - * A proposal starting block number must be less or equal than the proposal end date - */ -invariant voteStartBeforeVoteEnd(uint256 pId) - // from < to <= because snapshot and deadline can be the same block number if delays are set to 0 - // This is possible before the integration of GovernorSettings.sol to the system. - // After integration of GovernorSettings.sol the invariant expression should be changed from <= to < - (proposalSnapshot(pId) > 0 => proposalSnapshot(pId) <= proposalDeadline(pId)) - // (proposalSnapshot(pId) > 0 => proposalSnapshot(pId) <= proposalDeadline(pId)) - { preserved { - requireInvariant startAndEndDatesNonZero(pId); - }} - - -/* - * A proposal cannot be both executed and canceled simultaneously. - */ -invariant noBothExecutedAndCanceled(uint256 pId) - !isExecuted(pId) || !isCanceled(pId) - - -/* - * A proposal could be executed only if quorum was reached and vote succeeded - */ -rule executionOnlyIfQuoromReachedAndVoteSucceeded(uint256 pId, env e, method f){ - bool isExecutedBefore = isExecuted(pId); - bool quorumReachedBefore = _quorumReached(e, pId); - bool voteSucceededBefore = _voteSucceeded(pId); - - calldataarg args; - f(e, args); - - bool isExecutedAfter = isExecuted(pId); - assert (!isExecutedBefore && isExecutedAfter) => (quorumReachedBefore && voteSucceededBefore), "quorum was changed"; -} - -/////////////////////////////////////////////////////////////////////////////////////// -////////////////////////////////// In-State Rules ///////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////// - -//========================================== -//------------- Voting Period -------------- -//========================================== - -/* - * A user cannot vote twice - */ - // Checked for castVote only. all 3 castVote functions call _castVote, so the completeness of the verification is counted on - // the fact that the 3 functions themselves makes no changes, but rather call an internal function to execute. - // That means that we do not check those 3 functions directly, however for castVote & castVoteWithReason it is quite trivial - // to understand why this is ok. For castVoteBySig we basically assume that the signature referendum is correct without checking it. - // We could check each function separately and pass the rule, but that would have uglyfied the code with no concrete - // benefit, as it is evident that nothing is happening in the first 2 functions (calling a view function), and we do not desire to check the signature verification. -rule doubleVoting(uint256 pId, uint8 sup, method f) { - env e; - address user = e.msg.sender; - bool votedCheck = hasVoted(e, pId, user); - - castVote@withrevert(e, pId, sup); - - assert votedCheck => lastReverted, "double voting occurred"; -} - - -/////////////////////////////////////////////////////////////////////////////////////// -//////////////////////////// State Transitions Rules ////////////////////////////////// -/////////////////////////////////////////////////////////////////////////////////////// - -//=========================================== -//-------- Propose() --> End of Time -------- -//=========================================== - - -/* - * Once a proposal is created, voteStart and voteEnd are immutable - */ -rule immutableFieldsAfterProposalCreation(uint256 pId, method f) { - uint256 _voteStart = proposalSnapshot(pId); - uint256 _voteEnd = proposalDeadline(pId); - - require proposalCreated(pId); // startDate > 0 - - env e; calldataarg arg; - f(e, arg); - - uint256 voteStart_ = proposalSnapshot(pId); - uint256 voteEnd_ = proposalDeadline(pId); - assert _voteStart == voteStart_, "Start date was changed"; - assert _voteEnd == voteEnd_, "End date was changed"; -} - - -/* - * Voting cannot start at a block number prior to proposal’s creation block number - */ -rule noStartBeforeCreation(uint256 pId) { - uint256 previousStart = proposalSnapshot(pId); - // This line makes sure that we see only cases where start date is changed from 0, i.e. creation of proposal - // We proved in immutableFieldsAfterProposalCreation that once dates set for proposal, it cannot be changed - require !proposalCreated(pId); // previousStart == 0; - - env e; calldataarg args; - propose(e, args); - - uint256 newStart = proposalSnapshot(pId); - // if created, start is after current block number (creation block) - assert(newStart != previousStart => newStart >= e.block.number); -} - - -//============================================ -//--- End of Voting Period --> End of Time --- -//============================================ - - -/* - * A proposal can neither be executed nor canceled before it ends - */ - // By induction it cannot be executed nor canceled before it starts, due to voteStartBeforeVoteEnd -rule noExecuteOrCancelBeforeDeadline(uint256 pId, method f){ - require !isExecuted(pId) && !isCanceled(pId); - - env e; calldataarg args; - f(e, args); - - assert e.block.number < proposalDeadline(pId) => (!isExecuted(pId) && !isCanceled(pId)), "executed/cancelled before deadline"; -} - -//////////////////////////////////////////////////////////////////////////////// -////////////////////// Integrity Of Functions (Unit Tests) ///////////////////// -//////////////////////////////////////////////////////////////////////////////// - - -//////////////////////////////////////////////////////////////////////////////// -////////////////////////////// High Level Rules //////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - - -//////////////////////////////////////////////////////////////////////////////// -///////////////////////////// Not Categorized Yet ////////////////////////////// -//////////////////////////////////////////////////////////////////////////////// - - -/* - * All proposal specific (non-view) functions should revert if proposal is executed - */ - // In this rule we show that if a function is executed, i.e. execute() was called on the proposal ID, - // non of the proposal specific functions can make changes again. In executedOnlyAfterExecuteFunc - // we connected the executed attribute to the execute() function, showing that only execute() can - // change it, and that it will always change it. -rule allFunctionsRevertIfExecuted(method f) filtered { f -> - !f.isView && !f.isFallback - && f.selector != updateTimelock(address).selector - && f.selector != updateQuorumNumerator(uint256).selector - && f.selector != queue(address[],uint256[],bytes[],bytes32).selector - && f.selector != relay(address,uint256,bytes).selector - && f.selector != 0xb9a61961 // __acceptAdmin() -} { - env e; calldataarg args; - uint256 pId; - require(isExecuted(pId)); - requireInvariant noBothExecutedAndCanceled(pId); - requireInvariant executedImplyStartAndEndDateNonZero(pId); - - helperFunctionsWithRevert(pId, f, e); - - assert(lastReverted, "Function was not reverted"); -} - -/* - * All proposal specific (non-view) functions should revert if proposal is canceled - */ -rule allFunctionsRevertIfCanceled(method f) filtered { - f -> !f.isView && !f.isFallback - && f.selector != updateTimelock(address).selector - && f.selector != updateQuorumNumerator(uint256).selector - && f.selector != queue(address[],uint256[],bytes[],bytes32).selector - && f.selector != relay(address,uint256,bytes).selector - && f.selector != 0xb9a61961 // __acceptAdmin() -} { - env e; calldataarg args; - uint256 pId; - require(isCanceled(pId)); - requireInvariant noBothExecutedAndCanceled(pId); - requireInvariant canceledImplyStartAndEndDateNonZero(pId); - - helperFunctionsWithRevert(pId, f, e); - - assert(lastReverted, "Function was not reverted"); -} - -/* - * Proposal can be switched to executed only via execute() function - */ -rule executedOnlyAfterExecuteFunc(address[] targets, uint256[] values, bytes[] calldatas, bytes32 descriptionHash, method f) { - env e; calldataarg args; - uint256 pId; - bool executedBefore = isExecuted(pId); - require(!executedBefore); - - helperFunctionsWithRevert(pId, f, e); - - bool executedAfter = isExecuted(pId); - assert(executedAfter != executedBefore => f.selector == execute(address[], uint256[], bytes[], bytes32).selector, "isExecuted only changes in the execute method"); -} diff --git a/certora/specs/GovernorCountingSimple.spec b/certora/specs/GovernorCountingSimple.spec deleted file mode 100644 index 7af73beb6..000000000 --- a/certora/specs/GovernorCountingSimple.spec +++ /dev/null @@ -1,221 +0,0 @@ -import "GovernorBase.spec" - -using ERC20VotesHarness as erc20votes - -methods { - ghost_sum_vote_power_by_id(uint256) returns uint256 envfree - - quorum(uint256) returns uint256 - proposalVotes(uint256) returns (uint256, uint256, uint256) envfree - - quorumNumerator() returns uint256 - _executor() returns address - - erc20votes._getPastVotes(address, uint256) returns uint256 - - getExecutor() returns address - - timelock() returns address -} - - -////////////////////////////////////////////////////////////////////////////// -///////////////////////////////// GHOSTS ///////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -//////////// ghosts to keep track of votes counting //////////// - -/* - * the sum of voting power of those who voted - */ -ghost sum_all_votes_power() returns uint256 { - init_state axiom sum_all_votes_power() == 0; -} - -hook Sstore ghost_sum_vote_power_by_id [KEY uint256 pId] uint256 current_power(uint256 old_power) STORAGE { - havoc sum_all_votes_power assuming sum_all_votes_power@new() == sum_all_votes_power@old() - old_power + current_power; -} - -/* - * sum of all votes casted per proposal - */ -ghost tracked_weight(uint256) returns uint256 { - init_state axiom forall uint256 p. tracked_weight(p) == 0; -} - -/* - * sum of all votes casted - */ -ghost sum_tracked_weight() returns uint256 { - init_state axiom sum_tracked_weight() == 0; -} - -/* - * getter for _proposalVotes.againstVotes - */ -ghost votesAgainst() returns uint256 { - init_state axiom votesAgainst() == 0; -} - -/* - * getter for _proposalVotes.forVotes - */ -ghost votesFor() returns uint256 { - init_state axiom votesFor() == 0; -} - -/* - * getter for _proposalVotes.abstainVotes - */ -ghost votesAbstain() returns uint256 { - init_state axiom votesAbstain() == 0; -} - -hook Sstore _proposalVotes [KEY uint256 pId].againstVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesAgainst assuming votesAgainst@new() == votesAgainst@old() - old_votes + votes; -} - -hook Sstore _proposalVotes [KEY uint256 pId].forVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesFor assuming votesFor@new() == votesFor@old() - old_votes + votes; -} - -hook Sstore _proposalVotes [KEY uint256 pId].abstainVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesAbstain assuming votesAbstain@new() == votesAbstain@old() - old_votes + votes; -} - - -////////////////////////////////////////////////////////////////////////////// -////////////////////////////// INVARIANTS //////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -/* - * sum of all votes casted is equal to the sum of voting power of those who voted, per each proposal - */ -invariant SumOfVotesCastEqualSumOfPowerOfVotedPerProposal(uint256 pId) - tracked_weight(pId) == ghost_sum_vote_power_by_id(pId) - - -/* - * sum of all votes casted is equal to the sum of voting power of those who voted - */ -invariant SumOfVotesCastEqualSumOfPowerOfVoted() - sum_tracked_weight() == sum_all_votes_power() - - -/* -* sum of all votes casted is greater or equal to the sum of voting power of those who voted at a specific proposal -*/ -invariant OneIsNotMoreThanAll(uint256 pId) - sum_all_votes_power() >= tracked_weight(pId) - - -////////////////////////////////////////////////////////////////////////////// -///////////////////////////////// RULES ////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -/* - * Only sender's voting status can be changed by execution of any cast vote function - */ -// Checked for castVote only. all 3 castVote functions call _castVote, so the completeness of the verification is counted on - // the fact that the 3 functions themselves makes no changes, but rather call an internal function to execute. - // That means that we do not check those 3 functions directly, however for castVote & castVoteWithReason it is quite trivial - // to understand why this is ok. For castVoteBySig we basically assume that the signature referendum is correct without checking it. - // We could check each function separately and pass the rule, but that would have uglyfied the code with no concrete - // benefit, as it is evident that nothing is happening in the first 2 functions (calling a view function), and we do not desire to check the signature verification. -rule noVoteForSomeoneElse(uint256 pId, uint8 sup, method f) { - env e; calldataarg args; - - address voter = e.msg.sender; - address user; - - bool hasVotedBefore_User = hasVoted(e, pId, user); - - castVote@withrevert(e, pId, sup); - require(!lastReverted); - - bool hasVotedAfter_User = hasVoted(e, pId, user); - - assert user != voter => hasVotedBefore_User == hasVotedAfter_User; -} - - -/* -* Total voting tally is monotonically non-decreasing in every operation -*/ -rule votingWeightMonotonicity(method f){ - uint256 votingWeightBefore = sum_tracked_weight(); - - env e; - calldataarg args; - f(e, args); - - uint256 votingWeightAfter = sum_tracked_weight(); - - assert votingWeightBefore <= votingWeightAfter, "Voting weight was decreased somehow"; -} - - -/* -* A change in hasVoted must be correlated with an non-decreasing of the vote supports (nondecrease because user can vote with weight 0) -*/ -rule hasVotedCorrelation(uint256 pId, method f, env e, uint256 bn) { - address acc = e.msg.sender; - - uint256 againstBefore = votesAgainst(); - uint256 forBefore = votesFor(); - uint256 abstainBefore = votesAbstain(); - - bool hasVotedBefore = hasVoted(e, pId, acc); - - helperFunctionsWithRevert(pId, f, e); - require(!lastReverted); - - uint256 againstAfter = votesAgainst(); - uint256 forAfter = votesFor(); - uint256 abstainAfter = votesAbstain(); - - bool hasVotedAfter = hasVoted(e, pId, acc); - - assert (!hasVotedBefore && hasVotedAfter) => againstBefore <= againstAfter || forBefore <= forAfter || abstainBefore <= abstainAfter, "no correlation"; -} - - -/* -* Only privileged users can execute privileged operations, e.g. change _quorumNumerator or _timelock -*/ -rule privilegedOnlyNumerator(method f, uint256 newQuorumNumerator){ - env e; - calldataarg arg; - uint256 quorumNumBefore = quorumNumerator(e); - - f(e, arg); - - uint256 quorumNumAfter = quorumNumerator(e); - address executorCheck = getExecutor(e); - - assert quorumNumBefore != quorumNumAfter => e.msg.sender == executorCheck, "non privileged user changed quorum numerator"; -} - -rule privilegedOnlyTimelock(method f, uint256 newQuorumNumerator){ - env e; - calldataarg arg; - uint256 timelockBefore = timelock(e); - - f(e, arg); - - uint256 timelockAfter = timelock(e); - - assert timelockBefore != timelockAfter => e.msg.sender == timelockBefore, "non privileged user changed timelock"; -} diff --git a/certora/specs/Initializable.spec b/certora/specs/Initializable.spec new file mode 100644 index 000000000..0e0b1b714 --- /dev/null +++ b/certora/specs/Initializable.spec @@ -0,0 +1,165 @@ +import "helpers/helpers.spec" + +methods { + // initialize, reinitialize, disable + initialize() envfree + reinitialize(uint8) envfree + disable() envfree + + nested_init_init() envfree + nested_init_reinit(uint8) envfree + nested_reinit_init(uint8) envfree + nested_reinit_reinit(uint8,uint8) envfree + + // view + version() returns uint8 envfree + initializing() returns bool envfree +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Definitions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +definition isUninitialized() returns bool = version() == 0; +definition isInitialized() returns bool = version() > 0; +definition isDisabled() returns bool = version() == 255; + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: A contract must only ever be in an initializing state while in the middle of a transaction execution. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant notInitializing() + !initializing() + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: The version cannot decrease & disable state is irrevocable. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule increasingVersion(env e) { + uint8 versionBefore = version(); + bool disabledBefore = isDisabled(); + + method f; calldataarg args; + f(e, args); + + assert versionBefore <= version(), "_initialized must only increase"; + assert disabledBefore => isDisabled(), "a disabled initializer must stay disabled"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Cannot initialize a contract that is already initialized. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cannotInitializeTwice() { + require isInitialized(); + + initialize@withrevert(); + + assert lastReverted, "contract must only be initialized once"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Cannot initialize once disabled. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cannotInitializeOnceDisabled() { + require isDisabled(); + + initialize@withrevert(); + + assert lastReverted, "contract is disabled"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Cannot reinitialize once disabled. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cannotReinitializeOnceDisabled() { + require isDisabled(); + + uint8 n; + reinitialize@withrevert(n); + + assert lastReverted, "contract is disabled"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Cannot nest initializers (after construction). │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cannotNestInitializers_init_init() { + nested_init_init@withrevert(); + assert lastReverted, "nested initializers"; +} + +rule cannotNestInitializers_init_reinit(uint8 m) { + nested_init_reinit@withrevert(m); + assert lastReverted, "nested initializers"; +} + +rule cannotNestInitializers_reinit_init(uint8 n) { + nested_reinit_init@withrevert(n); + assert lastReverted, "nested initializers"; +} + +rule cannotNestInitializers_reinit_reinit(uint8 n, uint8 m) { + nested_reinit_reinit@withrevert(n, m); + assert lastReverted, "nested initializers"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Initialize correctly sets the version. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule initializeEffects() { + requireInvariant notInitializing(); + + bool isUninitializedBefore = isUninitialized(); + + initialize@withrevert(); + bool success = !lastReverted; + + assert success <=> isUninitializedBefore, "can only initialize uninitialized contracts"; + assert success => version() == 1, "initialize must set version() to 1"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Reinitialize correctly sets the version. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule reinitializeEffects() { + requireInvariant notInitializing(); + + uint8 versionBefore = version(); + + uint8 n; + reinitialize@withrevert(n); + bool success = !lastReverted; + + assert success <=> versionBefore < n, "can only reinitialize to a latter versions"; + assert success => version() == n, "reinitialize must set version() to n"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: Can disable. │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule disableEffect() { + requireInvariant notInitializing(); + + disable@withrevert(); + bool success = !lastReverted; + + assert success, "call to _disableInitializers failed"; + assert isDisabled(), "disable state not set"; +} diff --git a/certora/specs/Ownable.spec b/certora/specs/Ownable.spec new file mode 100644 index 000000000..4bf9e3005 --- /dev/null +++ b/certora/specs/Ownable.spec @@ -0,0 +1,78 @@ +import "helpers/helpers.spec" +import "methods/IOwnable.spec" + +methods { + restricted() +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: transferOwnership changes ownership │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule transferOwnership(env e) { + require nonpayable(e); + + address newOwner; + address current = owner(); + + transferOwnership@withrevert(e, newOwner); + bool success = !lastReverted; + + assert success <=> (e.msg.sender == current && newOwner != 0), "unauthorized caller or invalid arg"; + assert success => owner() == newOwner, "current owner changed"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: renounceOwnership removes the owner │ + +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule renounceOwnership(env e) { + require nonpayable(e); + + address current = owner(); + + renounceOwnership@withrevert(e); + bool success = !lastReverted; + + assert success <=> e.msg.sender == current, "unauthorized caller"; + assert success => owner() == 0, "owner not cleared"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Access control: only current owner can call restricted functions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyCurrentOwnerCanCallOnlyOwner(env e) { + require nonpayable(e); + + address current = owner(); + + calldataarg args; + restricted@withrevert(e, args); + + assert !lastReverted <=> e.msg.sender == current, "access control failed"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: ownership can only change in specific ways │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyOwnerOrPendingOwnerCanChangeOwnership(env e) { + address oldCurrent = owner(); + + method f; calldataarg args; + f(e, args); + + address newCurrent = owner(); + + // If owner changes, must be either transferOwnership or renounceOwnership + assert oldCurrent != newCurrent => ( + (e.msg.sender == oldCurrent && newCurrent != 0 && f.selector == transferOwnership(address).selector) || + (e.msg.sender == oldCurrent && newCurrent == 0 && f.selector == renounceOwnership().selector) + ); +} diff --git a/certora/specs/Ownable2Step.spec b/certora/specs/Ownable2Step.spec new file mode 100644 index 000000000..47b1b8d75 --- /dev/null +++ b/certora/specs/Ownable2Step.spec @@ -0,0 +1,108 @@ +import "helpers/helpers.spec" +import "methods/IOwnable2Step.spec" + +methods { + restricted() +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: transferOwnership sets the pending owner │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule transferOwnership(env e) { + require nonpayable(e); + + address newOwner; + address current = owner(); + + transferOwnership@withrevert(e, newOwner); + bool success = !lastReverted; + + assert success <=> e.msg.sender == current, "unauthorized caller"; + assert success => pendingOwner() == newOwner, "pending owner not set"; + assert success => owner() == current, "current owner changed"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: renounceOwnership removes the owner and the pendingOwner │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule renounceOwnership(env e) { + require nonpayable(e); + + address current = owner(); + + renounceOwnership@withrevert(e); + bool success = !lastReverted; + + assert success <=> e.msg.sender == current, "unauthorized caller"; + assert success => pendingOwner() == 0, "pending owner not cleared"; + assert success => owner() == 0, "owner not cleared"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: acceptOwnership changes owner and reset pending owner │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule acceptOwnership(env e) { + + require nonpayable(e); + + address current = owner(); + address pending = pendingOwner(); + + acceptOwnership@withrevert(e); + bool success = !lastReverted; + + assert success <=> e.msg.sender == pending, "unauthorized caller"; + assert success => pendingOwner() == 0, "pending owner not cleared"; + assert success => owner() == pending, "owner not transferred"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Access control: only current owner can call restricted functions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule onlyCurrentOwnerCanCallOnlyOwner(env e) { + require nonpayable(e); + + address current = owner(); + + calldataarg args; + restricted@withrevert(e, args); + + assert !lastReverted <=> e.msg.sender == current, "access control failed"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: ownership and pending ownership can only change in specific ways │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule ownerOrPendingOwnerChange(env e, method f) { + address oldCurrent = owner(); + address oldPending = pendingOwner(); + + calldataarg args; + f(e, args); + + address newCurrent = owner(); + address newPending = pendingOwner(); + + // If owner changes, must be either acceptOwnership or renounceOwnership + assert oldCurrent != newCurrent => ( + (e.msg.sender == oldPending && newCurrent == oldPending && newPending == 0 && f.selector == acceptOwnership().selector) || + (e.msg.sender == oldCurrent && newCurrent == 0 && newPending == 0 && f.selector == renounceOwnership().selector) + ); + + // If pending changes, must be either acceptance or reset + assert oldPending != newPending => ( + (e.msg.sender == oldCurrent && newCurrent == oldCurrent && f.selector == transferOwnership(address).selector) || + (e.msg.sender == oldPending && newCurrent == oldPending && newPending == 0 && f.selector == acceptOwnership().selector) || + (e.msg.sender == oldCurrent && newCurrent == 0 && newPending == 0 && f.selector == renounceOwnership().selector) + ); +} diff --git a/certora/specs/Pausable.spec b/certora/specs/Pausable.spec new file mode 100644 index 000000000..aea38003f --- /dev/null +++ b/certora/specs/Pausable.spec @@ -0,0 +1,96 @@ +import "helpers/helpers.spec" + +methods { + paused() returns (bool) envfree + pause() + unpause() + onlyWhenPaused() + onlyWhenNotPaused() +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: _pause pauses the contract │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule pause(env e) { + require nonpayable(e); + + bool pausedBefore = paused(); + + pause@withrevert(e); + bool success = !lastReverted; + + bool pausedAfter = paused(); + + // liveness + assert success <=> !pausedBefore, "works if and only if the contract was not paused before"; + + // effect + assert success => pausedAfter, "contract must be paused after a successful call"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: _unpause unpauses the contract │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule unpause(env e) { + require nonpayable(e); + + bool pausedBefore = paused(); + + unpause@withrevert(e); + bool success = !lastReverted; + + bool pausedAfter = paused(); + + // liveness + assert success <=> pausedBefore, "works if and only if the contract was paused before"; + + // effect + assert success => !pausedAfter, "contract must be unpaused after a successful call"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: whenPaused modifier can only be called if the contract is paused │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule whenPaused(env e) { + require nonpayable(e); + + onlyWhenPaused@withrevert(e); + assert !lastReverted <=> paused(), "works if and only if the contract is paused"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Function correctness: whenNotPaused modifier can only be called if the contract is not paused │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule whenNotPaused(env e) { + require nonpayable(e); + + onlyWhenNotPaused@withrevert(e); + assert !lastReverted <=> !paused(), "works if and only if the contract is not paused"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rules: only _pause and _unpause can change paused status │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule noPauseChange(env e) { + method f; + calldataarg args; + + bool pausedBefore = paused(); + f(e, args); + bool pausedAfter = paused(); + + assert pausedBefore != pausedAfter => ( + (!pausedAfter && f.selector == unpause().selector) || + (pausedAfter && f.selector == pause().selector) + ), "contract's paused status can only be changed by _pause() or _unpause()"; +} diff --git a/certora/specs/RulesInProgress.spec b/certora/specs/RulesInProgress.spec deleted file mode 100644 index cbad3336e..000000000 --- a/certora/specs/RulesInProgress.spec +++ /dev/null @@ -1,139 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -////////////// THIS SPEC IS A RESERVE FOR NOT IN PROGRESS ////////////// -////////////////////////////////////////////////////////////////////////////// - -import "GovernorBase.spec" - -using ERC20VotesHarness as erc20votes - -methods { - ghost_sum_vote_power_by_id(uint256) returns uint256 envfree - - quorum(uint256) returns uint256 - proposalVotes(uint256) returns (uint256, uint256, uint256) envfree - - quorumNumerator() returns uint256 - _executor() returns address - - erc20votes._getPastVotes(address, uint256) returns uint256 - - getExecutor() returns address - - timelock() returns address -} - - -////////////////////////////////////////////////////////////////////////////// -///////////////////////////////// GHOSTS ///////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -//////////// ghosts to keep track of votes counting //////////// - -/* - * the sum of voting power of those who voted - */ -ghost sum_all_votes_power() returns uint256 { - init_state axiom sum_all_votes_power() == 0; -} - -hook Sstore ghost_sum_vote_power_by_id [KEY uint256 pId] uint256 current_power(uint256 old_power) STORAGE { - havoc sum_all_votes_power assuming sum_all_votes_power@new() == sum_all_votes_power@old() - old_power + current_power; -} - -/* - * sum of all votes casted per proposal - */ -ghost tracked_weight(uint256) returns uint256 { - init_state axiom forall uint256 p. tracked_weight(p) == 0; -} - -/* - * sum of all votes casted - */ -ghost sum_tracked_weight() returns uint256 { - init_state axiom sum_tracked_weight() == 0; -} - -/* - * getter for _proposalVotes.againstVotes - */ -ghost votesAgainst() returns uint256 { - init_state axiom votesAgainst() == 0; -} - -/* - * getter for _proposalVotes.forVotes - */ -ghost votesFor() returns uint256 { - init_state axiom votesFor() == 0; -} - -/* - * getter for _proposalVotes.abstainVotes - */ -ghost votesAbstain() returns uint256 { - init_state axiom votesAbstain() == 0; -} - -hook Sstore _proposalVotes [KEY uint256 pId].againstVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesAgainst assuming votesAgainst@new() == votesAgainst@old() - old_votes + votes; -} - -hook Sstore _proposalVotes [KEY uint256 pId].forVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesFor assuming votesFor@new() == votesFor@old() - old_votes + votes; -} - -hook Sstore _proposalVotes [KEY uint256 pId].abstainVotes uint256 votes(uint256 old_votes) STORAGE { - havoc tracked_weight assuming forall uint256 p.(p == pId => tracked_weight@new(p) == tracked_weight@old(p) - old_votes + votes) && - (p != pId => tracked_weight@new(p) == tracked_weight@old(p)); - havoc sum_tracked_weight assuming sum_tracked_weight@new() == sum_tracked_weight@old() - old_votes + votes; - havoc votesAbstain assuming votesAbstain@new() == votesAbstain@old() - old_votes + votes; -} - - -////////////////////////////////////////////////////////////////////////////// -////////////////////////////// INVARIANTS //////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - - -////////////////////////////////////////////////////////////////////////////// -///////////////////////////////// RULES ////////////////////////////////////// -////////////////////////////////////////////////////////////////////////////// - - -//NOT FINISHED -/* -* the sum of voting power of those who voted is less or equal to the maximum possible votes, per each proposal -*/ -rule possibleTotalVotes(uint256 pId, uint8 sup, env e, method f) { - - // add requireinvariant for all i, j. i = i - 1 && i < j => checkpointlookup[i] < checkpointlookup[j]; - require tracked_weight(pId) <= erc20votes.getPastTotalSupply(e, proposalSnapshot(pId)); - - uint256 againstB; - uint256 forB; - uint256 absatinB; - againstB, forB, absatinB = proposalVotes(pId); - - calldataarg args; - //f(e, args); - - castVote(e, pId, sup); - - uint256 against; - uint256 for; - uint256 absatin; - against, for, absatin = proposalVotes(pId); - - uint256 ps = proposalSnapshot(pId); - - assert tracked_weight(pId) <= erc20votes.getPastTotalSupply(e, proposalSnapshot(pId)), "bla bla bla"; -} \ No newline at end of file diff --git a/certora/specs/TimelockController.spec b/certora/specs/TimelockController.spec new file mode 100644 index 000000000..05ecb1340 --- /dev/null +++ b/certora/specs/TimelockController.spec @@ -0,0 +1,275 @@ +import "helpers/helpers.spec" +import "methods/IAccessControl.spec" + +methods { + TIMELOCK_ADMIN_ROLE() returns (bytes32) envfree + PROPOSER_ROLE() returns (bytes32) envfree + EXECUTOR_ROLE() returns (bytes32) envfree + CANCELLER_ROLE() returns (bytes32) envfree + isOperation(bytes32) returns (bool) envfree + isOperationPending(bytes32) returns (bool) envfree + isOperationReady(bytes32) returns (bool) + isOperationDone(bytes32) returns (bool) envfree + getTimestamp(bytes32) returns (uint256) envfree + getMinDelay() returns (uint256) envfree + + hashOperation(address, uint256, bytes, bytes32, bytes32) returns(bytes32) envfree + hashOperationBatch(address[], uint256[], bytes[], bytes32, bytes32) returns(bytes32) envfree + + schedule(address, uint256, bytes, bytes32, bytes32, uint256) + scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256) + execute(address, uint256, bytes, bytes32, bytes32) + executeBatch(address[], uint256[], bytes[], bytes32, bytes32) + cancel(bytes32) + + updateDelay(uint256) +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Helpers │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +// Uniformly handle scheduling of batched and non-batched operations. +function helperScheduleWithRevert(env e, method f, bytes32 id, uint256 delay) { + if (f.selector == schedule(address, uint256, bytes, bytes32, bytes32, uint256).selector) { + address target; uint256 value; bytes data; bytes32 predecessor; bytes32 salt; + require hashOperation(target, value, data, predecessor, salt) == id; // Correlation + schedule@withrevert(e, target, value, data, predecessor, salt, delay); + } else if (f.selector == scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256).selector) { + address[] targets; uint256[] values; bytes[] payloads; bytes32 predecessor; bytes32 salt; + require hashOperationBatch(targets, values, payloads, predecessor, salt) == id; // Correlation + scheduleBatch@withrevert(e, targets, values, payloads, predecessor, salt, delay); + } else { + calldataarg args; + f@withrevert(e, args); + } +} + +// Uniformly handle execution of batched and non-batched operations. +function helperExecuteWithRevert(env e, method f, bytes32 id, bytes32 predecessor) { + if (f.selector == execute(address, uint256, bytes, bytes32, bytes32).selector) { + address target; uint256 value; bytes data; bytes32 salt; + require hashOperation(target, value, data, predecessor, salt) == id; // Correlation + execute@withrevert(e, target, value, data, predecessor, salt); + } else if (f.selector == executeBatch(address[], uint256[], bytes[], bytes32, bytes32).selector) { + address[] targets; uint256[] values; bytes[] payloads; bytes32 salt; + require hashOperationBatch(targets, values, payloads, predecessor, salt) == id; // Correlation + executeBatch@withrevert(e, targets, values, payloads, predecessor, salt); + } else { + calldataarg args; + f@withrevert(e, args); + } +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Definitions │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +definition DONE_TIMESTAMP() returns uint256 = 1; +definition UNSET() returns uint8 = 0x1; +definition PENDING() returns uint8 = 0x2; +definition DONE() returns uint8 = 0x4; + +definition isUnset(bytes32 id) returns bool = !isOperation(id); +definition isPending(bytes32 id) returns bool = isOperationPending(id); +definition isDone(bytes32 id) returns bool = isOperationDone(id); +definition state(bytes32 id) returns uint8 = (isUnset(id) ? UNSET() : 0) | (isPending(id) ? PENDING() : 0) | (isDone(id) ? DONE() : 0); + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariants: consistency of accessors │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant isOperationCheck(bytes32 id) + isOperation(id) <=> getTimestamp(id) > 0 + filtered { f -> !f.isView } + +invariant isOperationPendingCheck(bytes32 id) + isOperationPending(id) <=> getTimestamp(id) > DONE_TIMESTAMP() + filtered { f -> !f.isView } + +invariant isOperationDoneCheck(bytes32 id) + isOperationDone(id) <=> getTimestamp(id) == DONE_TIMESTAMP() + filtered { f -> !f.isView } + +invariant isOperationReadyCheck(env e, bytes32 id) + isOperationReady(e, id) <=> (isOperationPending(id) && getTimestamp(id) <= e.block.timestamp) + filtered { f -> !f.isView } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Invariant: a proposal id is either unset, pending or done │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +invariant stateConsistency(bytes32 id, env e) + // Check states are mutually exclusive + (isUnset(id) <=> (!isPending(id) && !isDone(id) )) && + (isPending(id) <=> (!isUnset(id) && !isDone(id) )) && + (isDone(id) <=> (!isUnset(id) && !isPending(id))) && + // Check that the state helper behaves as expected: + (isUnset(id) <=> state(id) == UNSET() ) && + (isPending(id) <=> state(id) == PENDING() ) && + (isDone(id) <=> state(id) == DONE() ) && + // Check substate + isOperationReady(e, id) => isPending(id) + filtered { f -> !f.isView } + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: state transition rules │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule stateTransition(bytes32 id, env e, method f, calldataarg args) { + require e.block.timestamp > 1; // Sanity + + uint8 stateBefore = state(id); + f(e, args); + uint8 stateAfter = state(id); + + // Cannot jump from UNSET to DONE + assert stateBefore == UNSET() => stateAfter != DONE(); + + // UNSET → PENDING: schedule or scheduleBatch + assert stateBefore == UNSET() && stateAfter == PENDING() => ( + f.selector == schedule(address, uint256, bytes, bytes32, bytes32, uint256).selector || + f.selector == scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256).selector + ); + + // PENDING → UNSET: cancel + assert stateBefore == PENDING() && stateAfter == UNSET() => ( + f.selector == cancel(bytes32).selector + ); + + // PENDING → DONE: execute or executeBatch + assert stateBefore == PENDING() && stateAfter == DONE() => ( + f.selector == execute(address, uint256, bytes, bytes32, bytes32).selector || + f.selector == executeBatch(address[], uint256[], bytes[], bytes32, bytes32).selector + ); + + // DONE is final + assert stateBefore == DONE() => stateAfter == DONE(); +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: minimum delay can only be updated through a timelock execution │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule minDelayOnlyChange(env e) { + uint256 delayBefore = getMinDelay(); + + method f; calldataarg args; + f(e, args); + + assert delayBefore != getMinDelay() => (e.msg.sender == currentContract && f.selector == updateDelay(uint256).selector), "Unauthorized delay update"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: schedule liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule schedule(env e, method f, bytes32 id, uint256 delay) filtered { f -> + f.selector == schedule(address, uint256, bytes, bytes32, bytes32, uint256).selector || + f.selector == scheduleBatch(address[], uint256[], bytes[], bytes32, bytes32, uint256).selector +} { + require nonpayable(e); + + // Basic timestamp assumptions + require e.block.timestamp > 1; + require e.block.timestamp + delay < max_uint256; + require e.block.timestamp + getMinDelay() < max_uint256; + + bytes32 otherId; uint256 otherTimestamp = getTimestamp(otherId); + + uint8 stateBefore = state(id); + bool isDelaySufficient = delay >= getMinDelay(); + bool isProposerBefore = hasRole(PROPOSER_ROLE(), e.msg.sender); + + helperScheduleWithRevert(e, f, id, delay); + bool success = !lastReverted; + + // liveness + assert success <=> ( + stateBefore == UNSET() && + isDelaySufficient && + isProposerBefore + ); + + // effect + assert success => state(id) == PENDING(), "State transition violation"; + assert success => getTimestamp(id) == to_uint256(e.block.timestamp + delay), "Proposal timestamp not correctly set"; + + // no side effect + assert otherTimestamp != getTimestamp(otherId) => id == otherId, "Other proposal affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: execute liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule execute(env e, method f, bytes32 id, bytes32 predecessor) filtered { f -> + f.selector == execute(address, uint256, bytes, bytes32, bytes32).selector || + f.selector == executeBatch(address[], uint256[], bytes[], bytes32, bytes32).selector +} { + bytes32 otherId; uint256 otherTimestamp = getTimestamp(otherId); + + uint8 stateBefore = state(id); + bool isOperationReadyBefore = isOperationReady(e, id); + bool isExecutorOrOpen = hasRole(EXECUTOR_ROLE(), e.msg.sender) || hasRole(EXECUTOR_ROLE(), 0); + bool predecessorDependency = predecessor == 0 || isDone(predecessor); + + helperExecuteWithRevert(e, f, id, predecessor); + bool success = !lastReverted; + + // The underlying transaction can revert, and that would cause the execution to revert. We can check that all non + // reverting calls meet the requirements in terms of proposal readiness, access control and predecessor dependency. + // We can't however guarantee that these requirements being meet ensure liveness of the proposal, because the + // proposal can revert for reasons beyond our control. + + // liveness, should be `<=>` but can only check `=>` (see comment above) + assert success => ( + stateBefore == PENDING() && + isOperationReadyBefore && + predecessorDependency && + isExecutorOrOpen + ); + + // effect + assert success => state(id) == DONE(), "State transition violation"; + + // no side effect + assert otherTimestamp != getTimestamp(otherId) => id == otherId, "Other proposal affected"; +} + +/* +┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│ Rule: cancel liveness and effects │ +└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ +*/ +rule cancel(env e, bytes32 id) { + require nonpayable(e); + + bytes32 otherId; uint256 otherTimestamp = getTimestamp(otherId); + + uint8 stateBefore = state(id); + bool isCanceller = hasRole(CANCELLER_ROLE(), e.msg.sender); + + cancel@withrevert(e, id); + bool success = !lastReverted; + + // liveness + assert success <=> ( + stateBefore == PENDING() && + isCanceller + ); + + // effect + assert success => state(id) == UNSET(), "State transition violation"; + + // no side effect + assert otherTimestamp != getTimestamp(otherId) => id == otherId, "Other proposal affected"; +} diff --git a/certora/specs/helpers/helpers.spec b/certora/specs/helpers/helpers.spec new file mode 100644 index 000000000..04e76df94 --- /dev/null +++ b/certora/specs/helpers/helpers.spec @@ -0,0 +1,10 @@ +// environment +definition nonpayable(env e) returns bool = e.msg.value == 0; +definition nonzerosender(env e) returns bool = e.msg.sender != 0; + +// constants +definition max_uint48() returns mathint = (1 << 48) - 1; + +// math +definition min(mathint a, mathint b) returns mathint = a < b ? a : b; +definition max(mathint a, mathint b) returns mathint = a > b ? a : b; diff --git a/certora/specs/methods/IAccessControl.spec b/certora/specs/methods/IAccessControl.spec new file mode 100644 index 000000000..4d41ffda7 --- /dev/null +++ b/certora/specs/methods/IAccessControl.spec @@ -0,0 +1,7 @@ +methods { + hasRole(bytes32, address) returns(bool) envfree + getRoleAdmin(bytes32) returns(bytes32) envfree + grantRole(bytes32, address) + revokeRole(bytes32, address) + renounceRole(bytes32, address) +} diff --git a/certora/specs/methods/IAccessControlDefaultAdminRules.spec b/certora/specs/methods/IAccessControlDefaultAdminRules.spec new file mode 100644 index 000000000..a9dd08b7f --- /dev/null +++ b/certora/specs/methods/IAccessControlDefaultAdminRules.spec @@ -0,0 +1,36 @@ +import "./IERC5313.spec" + +methods { + // === View == + + // Default Admin + defaultAdmin() returns(address) envfree + pendingDefaultAdmin() returns(address, uint48) envfree + + // Default Admin Delay + defaultAdminDelay() returns(uint48) + pendingDefaultAdminDelay() returns(uint48, uint48) + defaultAdminDelayIncreaseWait() returns(uint48) envfree + + // === Mutations == + + // Default Admin + beginDefaultAdminTransfer(address) + cancelDefaultAdminTransfer() + acceptDefaultAdminTransfer() + + // Default Admin Delay + changeDefaultAdminDelay(uint48) + rollbackDefaultAdminDelay() + + // == FV == + + // Default Admin + pendingDefaultAdmin_() returns (address) envfree + pendingDefaultAdminSchedule_() returns (uint48) envfree + + // Default Admin Delay + pendingDelay_() returns (uint48) + pendingDelaySchedule_() returns (uint48) + delayChangeWait_(uint48) returns (uint48) +} diff --git a/certora/specs/methods/IERC20.spec b/certora/specs/methods/IERC20.spec new file mode 100644 index 000000000..cfa454e13 --- /dev/null +++ b/certora/specs/methods/IERC20.spec @@ -0,0 +1,11 @@ +methods { + name() returns (string) envfree => DISPATCHER(true) + symbol() returns (string) envfree => DISPATCHER(true) + decimals() returns (uint8) envfree => DISPATCHER(true) + totalSupply() returns (uint256) envfree => DISPATCHER(true) + balanceOf(address) returns (uint256) envfree => DISPATCHER(true) + allowance(address,address) returns (uint256) envfree => DISPATCHER(true) + approve(address,uint256) returns (bool) => DISPATCHER(true) + transfer(address,uint256) returns (bool) => DISPATCHER(true) + transferFrom(address,address,uint256) returns (bool) => DISPATCHER(true) +} diff --git a/certora/specs/methods/IERC2612.spec b/certora/specs/methods/IERC2612.spec new file mode 100644 index 000000000..0c1689da4 --- /dev/null +++ b/certora/specs/methods/IERC2612.spec @@ -0,0 +1,5 @@ +methods { + permit(address,address,uint256,uint256,uint8,bytes32,bytes32) => DISPATCHER(true) + nonces(address) returns (uint256) envfree => DISPATCHER(true) + DOMAIN_SEPARATOR() returns (bytes32) envfree => DISPATCHER(true) +} diff --git a/certora/specs/methods/IERC3156.spec b/certora/specs/methods/IERC3156.spec new file mode 100644 index 000000000..18c10c515 --- /dev/null +++ b/certora/specs/methods/IERC3156.spec @@ -0,0 +1,5 @@ +methods { + maxFlashLoan(address) returns (uint256) envfree => DISPATCHER(true) + flashFee(address,uint256) returns (uint256) envfree => DISPATCHER(true) + flashLoan(address,address,uint256,bytes) returns (bool) => DISPATCHER(true) +} diff --git a/certora/specs/methods/IERC5313.spec b/certora/specs/methods/IERC5313.spec new file mode 100644 index 000000000..d4c5a0412 --- /dev/null +++ b/certora/specs/methods/IERC5313.spec @@ -0,0 +1,3 @@ +methods { + owner() returns (address) envfree +} diff --git a/certora/specs/methods/IERC721.spec b/certora/specs/methods/IERC721.spec new file mode 100644 index 000000000..e6d4e1e04 --- /dev/null +++ b/certora/specs/methods/IERC721.spec @@ -0,0 +1,20 @@ +methods { + // IERC721 + balanceOf(address) returns (uint256) envfree => DISPATCHER(true) + ownerOf(uint256) returns (address) envfree => DISPATCHER(true) + getApproved(uint256) returns (address) envfree => DISPATCHER(true) + isApprovedForAll(address,address) returns (bool) envfree => DISPATCHER(true) + safeTransferFrom(address,address,uint256,bytes) => DISPATCHER(true) + safeTransferFrom(address,address,uint256) => DISPATCHER(true) + transferFrom(address,address,uint256) => DISPATCHER(true) + approve(address,uint256) => DISPATCHER(true) + setApprovalForAll(address,bool) => DISPATCHER(true) + + // IERC721Metadata + name() returns (string) => DISPATCHER(true) + symbol() returns (string) => DISPATCHER(true) + tokenURI(uint256) returns (string) => DISPATCHER(true) + + // IERC721Receiver + onERC721Received(address,address,uint256,bytes) returns (bytes4) => DISPATCHER(true) +} diff --git a/certora/specs/methods/IOwnable.spec b/certora/specs/methods/IOwnable.spec new file mode 100644 index 000000000..cfa15f95f --- /dev/null +++ b/certora/specs/methods/IOwnable.spec @@ -0,0 +1,5 @@ +methods { + owner() returns (address) envfree + transferOwnership(address) + renounceOwnership() +} diff --git a/certora/specs/methods/IOwnable2Step.spec b/certora/specs/methods/IOwnable2Step.spec new file mode 100644 index 000000000..c8e671d27 --- /dev/null +++ b/certora/specs/methods/IOwnable2Step.spec @@ -0,0 +1,7 @@ +methods { + owner() returns (address) envfree + pendingOwner() returns (address) envfree + transferOwnership(address) + acceptOwnership() + renounceOwnership() +} diff --git a/certora/specs/sanity.spec b/certora/specs/sanity.spec deleted file mode 100644 index e08f68845..000000000 --- a/certora/specs/sanity.spec +++ /dev/null @@ -1,14 +0,0 @@ -/* -This rule looks for a non-reverting execution path to each method, including those overridden in the harness. -A method has such an execution path if it violates this rule. -How it works: - - If there is a non-reverting execution path, we reach the false assertion, and the sanity fails. - - If all execution paths are reverting, we never call the assertion, and the method will pass this rule vacuously. -*/ - -rule sanity(method f) { - env e; - calldataarg arg; - f(e, arg); - assert false; -} \ No newline at end of file diff --git a/contracts/access/AccessControl.sol b/contracts/access/AccessControl.sol index 386b85c03..12dc770b3 100644 --- a/contracts/access/AccessControl.sol +++ b/contracts/access/AccessControl.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (access/AccessControl.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (access/AccessControl.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IAccessControl.sol"; import "../utils/Context.sol"; @@ -19,14 +19,14 @@ import "../utils/introspection/ERC165.sol"; * in the external API and be unique. The best way to achieve this is by * using `public constant` hash digests: * - * ``` + * ```solidity * bytes32 public constant MY_ROLE = keccak256("MY_ROLE"); * ``` * * Roles can be used to represent a set of permissions. To restrict access to a * function call, use {hasRole}: * - * ``` + * ```solidity * function foo() public { * require(hasRole(MY_ROLE, msg.sender)); * ... @@ -44,7 +44,8 @@ import "../utils/introspection/ERC165.sol"; * * WARNING: The `DEFAULT_ADMIN_ROLE` is also its own admin: it has permission to * grant and revoke this role. Extra precautions should be taken to secure - * accounts that have been granted it. + * accounts that have been granted it. We recommend using {AccessControlDefaultAdminRules} + * to enforce additional security measures for this role. */ abstract contract AccessControl is Context, IAccessControl, ERC165 { struct RoleData { @@ -81,7 +82,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { /** * @dev Returns `true` if `account` has been granted `role`. */ - function hasRole(bytes32 role, address account) public view virtual override returns (bool) { + function hasRole(bytes32 role, address account) public view virtual returns (bool) { return _roles[role].members[account]; } @@ -106,16 +107,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { */ function _checkRole(bytes32 role, address account) internal view virtual { if (!hasRole(role, account)) { - revert( - string( - abi.encodePacked( - "AccessControl: account ", - Strings.toHexString(account), - " is missing role ", - Strings.toHexString(uint256(role), 32) - ) - ) - ); + revert AccessControlUnauthorizedAccount(account, role); } } @@ -125,7 +117,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { * * To change a role's admin, use {_setRoleAdmin}. */ - function getRoleAdmin(bytes32 role) public view virtual override returns (bytes32) { + function getRoleAdmin(bytes32 role) public view virtual returns (bytes32) { return _roles[role].adminRole; } @@ -141,7 +133,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { * * May emit a {RoleGranted} event. */ - function grantRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { + function grantRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) { _grantRole(role, account); } @@ -156,7 +148,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { * * May emit a {RoleRevoked} event. */ - function revokeRole(bytes32 role, address account) public virtual override onlyRole(getRoleAdmin(role)) { + function revokeRole(bytes32 role, address account) public virtual onlyRole(getRoleAdmin(role)) { _revokeRole(role, account); } @@ -172,38 +164,16 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { * * Requirements: * - * - the caller must be `account`. + * - the caller must be `callerConfirmation`. * * May emit a {RoleRevoked} event. */ - function renounceRole(bytes32 role, address account) public virtual override { - require(account == _msgSender(), "AccessControl: can only renounce roles for self"); + function renounceRole(bytes32 role, address callerConfirmation) public virtual { + if (callerConfirmation != _msgSender()) { + revert AccessControlBadConfirmation(); + } - _revokeRole(role, account); - } - - /** - * @dev Grants `role` to `account`. - * - * If `account` had not been already granted `role`, emits a {RoleGranted} - * event. Note that unlike {grantRole}, this function doesn't perform any - * checks on the calling account. - * - * May emit a {RoleGranted} event. - * - * [WARNING] - * ==== - * This function should only be called from the constructor when setting - * up the initial roles for the system. - * - * Using this function in any other way is effectively circumventing the admin - * system imposed by {AccessControl}. - * ==== - * - * NOTE: This function is deprecated in favor of {_grantRole}. - */ - function _setupRole(bytes32 role, address account) internal virtual { - _grantRole(role, account); + _revokeRole(role, callerConfirmation); } /** diff --git a/contracts/access/AccessControlCrossChain.sol b/contracts/access/AccessControlCrossChain.sol deleted file mode 100644 index 95be5091c..000000000 --- a/contracts/access/AccessControlCrossChain.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (access/AccessControlCrossChain.sol) - -pragma solidity ^0.8.4; - -import "./AccessControl.sol"; -import "../crosschain/CrossChainEnabled.sol"; - -/** - * @dev An extension to {AccessControl} with support for cross-chain access management. - * For each role, is extension implements an equivalent "aliased" role that is used for - * restricting calls originating from other chains. - * - * For example, if a function `myFunction` is protected by `onlyRole(SOME_ROLE)`, and - * if an address `x` has role `SOME_ROLE`, it would be able to call `myFunction` directly. - * A wallet or contract at the same address on another chain would however not be able - * to call this function. In order to do so, it would require to have the role - * `_crossChainRoleAlias(SOME_ROLE)`. - * - * This aliasing is required to protect against multiple contracts living at the same - * address on different chains but controlled by conflicting entities. - * - * _Available since v4.6._ - */ -abstract contract AccessControlCrossChain is AccessControl, CrossChainEnabled { - bytes32 public constant CROSSCHAIN_ALIAS = keccak256("CROSSCHAIN_ALIAS"); - - /** - * @dev See {AccessControl-_checkRole}. - */ - function _checkRole(bytes32 role) internal view virtual override { - if (_isCrossChain()) { - _checkRole(_crossChainRoleAlias(role), _crossChainSender()); - } else { - super._checkRole(role); - } - } - - /** - * @dev Returns the aliased role corresponding to `role`. - */ - function _crossChainRoleAlias(bytes32 role) internal pure virtual returns (bytes32) { - return role ^ CROSSCHAIN_ALIAS; - } -} diff --git a/contracts/access/AccessControlDefaultAdminRules.sol b/contracts/access/AccessControlDefaultAdminRules.sol new file mode 100644 index 000000000..e27eaf3db --- /dev/null +++ b/contracts/access/AccessControlDefaultAdminRules.sol @@ -0,0 +1,397 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (access/AccessControlDefaultAdminRules.sol) + +pragma solidity ^0.8.19; + +import "./AccessControl.sol"; +import "./IAccessControlDefaultAdminRules.sol"; +import "../utils/math/SafeCast.sol"; +import "../interfaces/IERC5313.sol"; + +/** + * @dev Extension of {AccessControl} that allows specifying special rules to manage + * the `DEFAULT_ADMIN_ROLE` holder, which is a sensitive role with special permissions + * over other roles that may potentially have privileged rights in the system. + * + * If a specific role doesn't have an admin role assigned, the holder of the + * `DEFAULT_ADMIN_ROLE` will have the ability to grant it and revoke it. + * + * This contract implements the following risk mitigations on top of {AccessControl}: + * + * * Only one account holds the `DEFAULT_ADMIN_ROLE` since deployment until it's potentially renounced. + * * Enforces a 2-step process to transfer the `DEFAULT_ADMIN_ROLE` to another account. + * * Enforces a configurable delay between the two steps, with the ability to cancel before the transfer is accepted. + * * The delay can be changed by scheduling, see {changeDefaultAdminDelay}. + * * It is not possible to use another role to manage the `DEFAULT_ADMIN_ROLE`. + * + * Example usage: + * + * ```solidity + * contract MyToken is AccessControlDefaultAdminRules { + * constructor() AccessControlDefaultAdminRules( + * 3 days, + * msg.sender // Explicit initial `DEFAULT_ADMIN_ROLE` holder + * ) {} + * } + * ``` + * + * _Available since v4.9._ + */ +abstract contract AccessControlDefaultAdminRules is IAccessControlDefaultAdminRules, IERC5313, AccessControl { + // pending admin pair read/written together frequently + address private _pendingDefaultAdmin; + uint48 private _pendingDefaultAdminSchedule; // 0 == unset + + uint48 private _currentDelay; + address private _currentDefaultAdmin; + + // pending delay pair read/written together frequently + uint48 private _pendingDelay; + uint48 private _pendingDelaySchedule; // 0 == unset + + /** + * @dev Sets the initial values for {defaultAdminDelay} and {defaultAdmin} address. + */ + constructor(uint48 initialDelay, address initialDefaultAdmin) { + if (initialDefaultAdmin == address(0)) { + revert AccessControlInvalidDefaultAdmin(address(0)); + } + _currentDelay = initialDelay; + _grantRole(DEFAULT_ADMIN_ROLE, initialDefaultAdmin); + } + + /** + * @dev See {IERC165-supportsInterface}. + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return interfaceId == type(IAccessControlDefaultAdminRules).interfaceId || super.supportsInterface(interfaceId); + } + + /** + * @dev See {IERC5313-owner}. + */ + function owner() public view virtual returns (address) { + return defaultAdmin(); + } + + /// + /// Override AccessControl role management + /// + + /** + * @dev See {AccessControl-grantRole}. Reverts for `DEFAULT_ADMIN_ROLE`. + */ + function grantRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { + if (role == DEFAULT_ADMIN_ROLE) { + revert AccessControlEnforcedDefaultAdminRules(); + } + super.grantRole(role, account); + } + + /** + * @dev See {AccessControl-revokeRole}. Reverts for `DEFAULT_ADMIN_ROLE`. + */ + function revokeRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { + if (role == DEFAULT_ADMIN_ROLE) { + revert AccessControlEnforcedDefaultAdminRules(); + } + super.revokeRole(role, account); + } + + /** + * @dev See {AccessControl-renounceRole}. + * + * For the `DEFAULT_ADMIN_ROLE`, it only allows renouncing in two steps by first calling + * {beginDefaultAdminTransfer} to the `address(0)`, so it's required that the {pendingDefaultAdmin} schedule + * has also passed when calling this function. + * + * After its execution, it will not be possible to call `onlyRole(DEFAULT_ADMIN_ROLE)` functions. + * + * NOTE: Renouncing `DEFAULT_ADMIN_ROLE` will leave the contract without a {defaultAdmin}, + * thereby disabling any functionality that is only available for it, and the possibility of reassigning a + * non-administrated role. + */ + function renounceRole(bytes32 role, address account) public virtual override(AccessControl, IAccessControl) { + if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) { + (address newDefaultAdmin, uint48 schedule) = pendingDefaultAdmin(); + if (newDefaultAdmin != address(0) || !_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) { + revert AccessControlEnforcedDefaultAdminDelay(schedule); + } + delete _pendingDefaultAdminSchedule; + } + super.renounceRole(role, account); + } + + /** + * @dev See {AccessControl-_grantRole}. + * + * For `DEFAULT_ADMIN_ROLE`, it only allows granting if there isn't already a {defaultAdmin} or if the + * role has been previously renounced. + * + * NOTE: Exposing this function through another mechanism may make the `DEFAULT_ADMIN_ROLE` + * assignable again. Make sure to guarantee this is the expected behavior in your implementation. + */ + function _grantRole(bytes32 role, address account) internal virtual override { + if (role == DEFAULT_ADMIN_ROLE) { + if (defaultAdmin() != address(0)) { + revert AccessControlEnforcedDefaultAdminRules(); + } + _currentDefaultAdmin = account; + } + super._grantRole(role, account); + } + + /** + * @dev See {AccessControl-_revokeRole}. + */ + function _revokeRole(bytes32 role, address account) internal virtual override { + if (role == DEFAULT_ADMIN_ROLE && account == defaultAdmin()) { + delete _currentDefaultAdmin; + } + super._revokeRole(role, account); + } + + /** + * @dev See {AccessControl-_setRoleAdmin}. Reverts for `DEFAULT_ADMIN_ROLE`. + */ + function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual override { + if (role == DEFAULT_ADMIN_ROLE) { + revert AccessControlEnforcedDefaultAdminRules(); + } + super._setRoleAdmin(role, adminRole); + } + + /// + /// AccessControlDefaultAdminRules accessors + /// + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function defaultAdmin() public view virtual returns (address) { + return _currentDefaultAdmin; + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function pendingDefaultAdmin() public view virtual returns (address newAdmin, uint48 schedule) { + return (_pendingDefaultAdmin, _pendingDefaultAdminSchedule); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function defaultAdminDelay() public view virtual returns (uint48) { + uint48 schedule = _pendingDelaySchedule; + return (_isScheduleSet(schedule) && _hasSchedulePassed(schedule)) ? _pendingDelay : _currentDelay; + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function pendingDefaultAdminDelay() public view virtual returns (uint48 newDelay, uint48 schedule) { + schedule = _pendingDelaySchedule; + return (_isScheduleSet(schedule) && !_hasSchedulePassed(schedule)) ? (_pendingDelay, schedule) : (0, 0); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function defaultAdminDelayIncreaseWait() public view virtual returns (uint48) { + return 5 days; + } + + /// + /// AccessControlDefaultAdminRules public and internal setters for defaultAdmin/pendingDefaultAdmin + /// + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function beginDefaultAdminTransfer(address newAdmin) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _beginDefaultAdminTransfer(newAdmin); + } + + /** + * @dev See {beginDefaultAdminTransfer}. + * + * Internal function without access restriction. + */ + function _beginDefaultAdminTransfer(address newAdmin) internal virtual { + uint48 newSchedule = SafeCast.toUint48(block.timestamp) + defaultAdminDelay(); + _setPendingDefaultAdmin(newAdmin, newSchedule); + emit DefaultAdminTransferScheduled(newAdmin, newSchedule); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function cancelDefaultAdminTransfer() public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _cancelDefaultAdminTransfer(); + } + + /** + * @dev See {cancelDefaultAdminTransfer}. + * + * Internal function without access restriction. + */ + function _cancelDefaultAdminTransfer() internal virtual { + _setPendingDefaultAdmin(address(0), 0); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function acceptDefaultAdminTransfer() public virtual { + (address newDefaultAdmin, ) = pendingDefaultAdmin(); + if (_msgSender() != newDefaultAdmin) { + // Enforce newDefaultAdmin explicit acceptance. + revert AccessControlInvalidDefaultAdmin(_msgSender()); + } + _acceptDefaultAdminTransfer(); + } + + /** + * @dev See {acceptDefaultAdminTransfer}. + * + * Internal function without access restriction. + */ + function _acceptDefaultAdminTransfer() internal virtual { + (address newAdmin, uint48 schedule) = pendingDefaultAdmin(); + if (!_isScheduleSet(schedule) || !_hasSchedulePassed(schedule)) { + revert AccessControlEnforcedDefaultAdminDelay(schedule); + } + _revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin()); + _grantRole(DEFAULT_ADMIN_ROLE, newAdmin); + delete _pendingDefaultAdmin; + delete _pendingDefaultAdminSchedule; + } + + /// + /// AccessControlDefaultAdminRules public and internal setters for defaultAdminDelay/pendingDefaultAdminDelay + /// + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function changeDefaultAdminDelay(uint48 newDelay) public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _changeDefaultAdminDelay(newDelay); + } + + /** + * @dev See {changeDefaultAdminDelay}. + * + * Internal function without access restriction. + */ + function _changeDefaultAdminDelay(uint48 newDelay) internal virtual { + uint48 newSchedule = SafeCast.toUint48(block.timestamp) + _delayChangeWait(newDelay); + _setPendingDelay(newDelay, newSchedule); + emit DefaultAdminDelayChangeScheduled(newDelay, newSchedule); + } + + /** + * @inheritdoc IAccessControlDefaultAdminRules + */ + function rollbackDefaultAdminDelay() public virtual onlyRole(DEFAULT_ADMIN_ROLE) { + _rollbackDefaultAdminDelay(); + } + + /** + * @dev See {rollbackDefaultAdminDelay}. + * + * Internal function without access restriction. + */ + function _rollbackDefaultAdminDelay() internal virtual { + _setPendingDelay(0, 0); + } + + /** + * @dev Returns the amount of seconds to wait after the `newDelay` will + * become the new {defaultAdminDelay}. + * + * The value returned guarantees that if the delay is reduced, it will go into effect + * after a wait that honors the previously set delay. + * + * See {defaultAdminDelayIncreaseWait}. + */ + function _delayChangeWait(uint48 newDelay) internal view virtual returns (uint48) { + uint48 currentDelay = defaultAdminDelay(); + + // When increasing the delay, we schedule the delay change to occur after a period of "new delay" has passed, up + // to a maximum given by defaultAdminDelayIncreaseWait, by default 5 days. For example, if increasing from 1 day + // to 3 days, the new delay will come into effect after 3 days. If increasing from 1 day to 10 days, the new + // delay will come into effect after 5 days. The 5 day wait period is intended to be able to fix an error like + // using milliseconds instead of seconds. + // + // When decreasing the delay, we wait the difference between "current delay" and "new delay". This guarantees + // that an admin transfer cannot be made faster than "current delay" at the time the delay change is scheduled. + // For example, if decreasing from 10 days to 3 days, the new delay will come into effect after 7 days. + return + newDelay > currentDelay + ? uint48(Math.min(newDelay, defaultAdminDelayIncreaseWait())) // no need to safecast, both inputs are uint48 + : currentDelay - newDelay; + } + + /// + /// Private setters + /// + + /** + * @dev Setter of the tuple for pending admin and its schedule. + * + * May emit a DefaultAdminTransferCanceled event. + */ + function _setPendingDefaultAdmin(address newAdmin, uint48 newSchedule) private { + (, uint48 oldSchedule) = pendingDefaultAdmin(); + + _pendingDefaultAdmin = newAdmin; + _pendingDefaultAdminSchedule = newSchedule; + + // An `oldSchedule` from `pendingDefaultAdmin()` is only set if it hasn't been accepted. + if (_isScheduleSet(oldSchedule)) { + // Emit for implicit cancellations when another default admin was scheduled. + emit DefaultAdminTransferCanceled(); + } + } + + /** + * @dev Setter of the tuple for pending delay and its schedule. + * + * May emit a DefaultAdminDelayChangeCanceled event. + */ + function _setPendingDelay(uint48 newDelay, uint48 newSchedule) private { + uint48 oldSchedule = _pendingDelaySchedule; + + if (_isScheduleSet(oldSchedule)) { + if (_hasSchedulePassed(oldSchedule)) { + // Materialize a virtual delay + _currentDelay = _pendingDelay; + } else { + // Emit for implicit cancellations when another delay was scheduled. + emit DefaultAdminDelayChangeCanceled(); + } + } + + _pendingDelay = newDelay; + _pendingDelaySchedule = newSchedule; + } + + /// + /// Private helpers + /// + + /** + * @dev Defines if an `schedule` is considered set. For consistency purposes. + */ + function _isScheduleSet(uint48 schedule) private pure returns (bool) { + return schedule != 0; + } + + /** + * @dev Defines if an `schedule` is considered passed. For consistency purposes. + */ + function _hasSchedulePassed(uint48 schedule) private view returns (bool) { + return schedule < block.timestamp; + } +} diff --git a/contracts/access/AccessControlEnumerable.sol b/contracts/access/AccessControlEnumerable.sol index 354e1bed2..297d34536 100644 --- a/contracts/access/AccessControlEnumerable.sol +++ b/contracts/access/AccessControlEnumerable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (access/AccessControlEnumerable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IAccessControlEnumerable.sol"; import "./AccessControl.sol"; @@ -34,7 +34,7 @@ abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessCon * https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] * for more information. */ - function getRoleMember(bytes32 role, uint256 index) public view virtual override returns (address) { + function getRoleMember(bytes32 role, uint256 index) public view virtual returns (address) { return _roleMembers[role].at(index); } @@ -42,7 +42,7 @@ abstract contract AccessControlEnumerable is IAccessControlEnumerable, AccessCon * @dev Returns the number of accounts that have `role`. Can be used * together with {getRoleMember} to enumerate all bearers of a role. */ - function getRoleMemberCount(bytes32 role) public view virtual override returns (uint256) { + function getRoleMemberCount(bytes32 role) public view virtual returns (uint256) { return _roleMembers[role].length(); } diff --git a/contracts/access/IAccessControl.sol b/contracts/access/IAccessControl.sol index f773ecc63..9abc2b735 100644 --- a/contracts/access/IAccessControl.sol +++ b/contracts/access/IAccessControl.sol @@ -1,12 +1,24 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (access/IAccessControl.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev External interface of AccessControl declared to support ERC165 detection. */ interface IAccessControl { + /** + * @dev The `account` is missing a role. + */ + error AccessControlUnauthorizedAccount(address account, bytes32 neededRole); + + /** + * @dev The caller of a function is not the expected one. + * + * NOTE: Don't confuse with {AccessControlUnauthorizedAccount}. + */ + error AccessControlBadConfirmation(); + /** * @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole` * @@ -82,7 +94,7 @@ interface IAccessControl { * * Requirements: * - * - the caller must be `account`. + * - the caller must be `callerConfirmation`. */ - function renounceRole(bytes32 role, address account) external; + function renounceRole(bytes32 role, address callerConfirmation) external; } diff --git a/contracts/access/IAccessControlDefaultAdminRules.sol b/contracts/access/IAccessControlDefaultAdminRules.sol new file mode 100644 index 000000000..fbecfe120 --- /dev/null +++ b/contracts/access/IAccessControlDefaultAdminRules.sol @@ -0,0 +1,194 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (access/IAccessControlDefaultAdminRules.sol) + +pragma solidity ^0.8.19; + +import "./IAccessControl.sol"; + +/** + * @dev External interface of AccessControlDefaultAdminRules declared to support ERC165 detection. + * + * _Available since v4.9._ + */ +interface IAccessControlDefaultAdminRules is IAccessControl { + /** + * @dev The new default admin is not a valid default admin. + */ + error AccessControlInvalidDefaultAdmin(address defaultAdmin); + + /** + * @dev At least one of the following rules was violated: + * + * - The `DEFAULT_ADMIN_ROLE` must only be managed by itself. + * - The `DEFAULT_ADMIN_ROLE` must only be held by one account at the time. + * - Any `DEFAULT_ADMIN_ROLE` transfer must be in two delayed steps. + */ + error AccessControlEnforcedDefaultAdminRules(); + + /** + * @dev The delay for transferring the default admin delay is enforced and + * the operation must wait until `schedule`. + * + * NOTE: `schedule` can be 0 indicating there's no transfer scheduled. + */ + error AccessControlEnforcedDefaultAdminDelay(uint48 schedule); + + /** + * @dev Emitted when a {defaultAdmin} transfer is started, setting `newAdmin` as the next + * address to become the {defaultAdmin} by calling {acceptDefaultAdminTransfer} only after `acceptSchedule` + * passes. + */ + event DefaultAdminTransferScheduled(address indexed newAdmin, uint48 acceptSchedule); + + /** + * @dev Emitted when a {pendingDefaultAdmin} is reset if it was never accepted, regardless of its schedule. + */ + event DefaultAdminTransferCanceled(); + + /** + * @dev Emitted when a {defaultAdminDelay} change is started, setting `newDelay` as the next + * delay to be applied between default admin transfer after `effectSchedule` has passed. + */ + event DefaultAdminDelayChangeScheduled(uint48 newDelay, uint48 effectSchedule); + + /** + * @dev Emitted when a {pendingDefaultAdminDelay} is reset if its schedule didn't pass. + */ + event DefaultAdminDelayChangeCanceled(); + + /** + * @dev Returns the address of the current `DEFAULT_ADMIN_ROLE` holder. + */ + function defaultAdmin() external view returns (address); + + /** + * @dev Returns a tuple of a `newAdmin` and an accept schedule. + * + * After the `schedule` passes, the `newAdmin` will be able to accept the {defaultAdmin} role + * by calling {acceptDefaultAdminTransfer}, completing the role transfer. + * + * A zero value only in `acceptSchedule` indicates no pending admin transfer. + * + * NOTE: A zero address `newAdmin` means that {defaultAdmin} is being renounced. + */ + function pendingDefaultAdmin() external view returns (address newAdmin, uint48 acceptSchedule); + + /** + * @dev Returns the delay required to schedule the acceptance of a {defaultAdmin} transfer started. + * + * This delay will be added to the current timestamp when calling {beginDefaultAdminTransfer} to set + * the acceptance schedule. + * + * NOTE: If a delay change has been scheduled, it will take effect as soon as the schedule passes, making this + * function returns the new delay. See {changeDefaultAdminDelay}. + */ + function defaultAdminDelay() external view returns (uint48); + + /** + * @dev Returns a tuple of `newDelay` and an effect schedule. + * + * After the `schedule` passes, the `newDelay` will get into effect immediately for every + * new {defaultAdmin} transfer started with {beginDefaultAdminTransfer}. + * + * A zero value only in `effectSchedule` indicates no pending delay change. + * + * NOTE: A zero value only for `newDelay` means that the next {defaultAdminDelay} + * will be zero after the effect schedule. + */ + function pendingDefaultAdminDelay() external view returns (uint48 newDelay, uint48 effectSchedule); + + /** + * @dev Starts a {defaultAdmin} transfer by setting a {pendingDefaultAdmin} scheduled for acceptance + * after the current timestamp plus a {defaultAdminDelay}. + * + * Requirements: + * + * - Only can be called by the current {defaultAdmin}. + * + * Emits a DefaultAdminRoleChangeStarted event. + */ + function beginDefaultAdminTransfer(address newAdmin) external; + + /** + * @dev Cancels a {defaultAdmin} transfer previously started with {beginDefaultAdminTransfer}. + * + * A {pendingDefaultAdmin} not yet accepted can also be cancelled with this function. + * + * Requirements: + * + * - Only can be called by the current {defaultAdmin}. + * + * May emit a DefaultAdminTransferCanceled event. + */ + function cancelDefaultAdminTransfer() external; + + /** + * @dev Completes a {defaultAdmin} transfer previously started with {beginDefaultAdminTransfer}. + * + * After calling the function: + * + * - `DEFAULT_ADMIN_ROLE` should be granted to the caller. + * - `DEFAULT_ADMIN_ROLE` should be revoked from the previous holder. + * - {pendingDefaultAdmin} should be reset to zero values. + * + * Requirements: + * + * - Only can be called by the {pendingDefaultAdmin}'s `newAdmin`. + * - The {pendingDefaultAdmin}'s `acceptSchedule` should've passed. + */ + function acceptDefaultAdminTransfer() external; + + /** + * @dev Initiates a {defaultAdminDelay} update by setting a {pendingDefaultAdminDelay} scheduled for getting + * into effect after the current timestamp plus a {defaultAdminDelay}. + * + * This function guarantees that any call to {beginDefaultAdminTransfer} done between the timestamp this + * method is called and the {pendingDefaultAdminDelay} effect schedule will use the current {defaultAdminDelay} + * set before calling. + * + * The {pendingDefaultAdminDelay}'s effect schedule is defined in a way that waiting until the schedule and then + * calling {beginDefaultAdminTransfer} with the new delay will take at least the same as another {defaultAdmin} + * complete transfer (including acceptance). + * + * The schedule is designed for two scenarios: + * + * - When the delay is changed for a larger one the schedule is `block.timestamp + newDelay` capped by + * {defaultAdminDelayIncreaseWait}. + * - When the delay is changed for a shorter one, the schedule is `block.timestamp + (current delay - new delay)`. + * + * A {pendingDefaultAdminDelay} that never got into effect will be canceled in favor of a new scheduled change. + * + * Requirements: + * + * - Only can be called by the current {defaultAdmin}. + * + * Emits a DefaultAdminDelayChangeScheduled event and may emit a DefaultAdminDelayChangeCanceled event. + */ + function changeDefaultAdminDelay(uint48 newDelay) external; + + /** + * @dev Cancels a scheduled {defaultAdminDelay} change. + * + * Requirements: + * + * - Only can be called by the current {defaultAdmin}. + * + * May emit a DefaultAdminDelayChangeCanceled event. + */ + function rollbackDefaultAdminDelay() external; + + /** + * @dev Maximum time in seconds for an increase to {defaultAdminDelay} (that is scheduled using {changeDefaultAdminDelay}) + * to take effect. Default to 5 days. + * + * When the {defaultAdminDelay} is scheduled to be increased, it goes into effect after the new delay has passed with + * the purpose of giving enough time for reverting any accidental change (i.e. using milliseconds instead of seconds) + * that may lock the contract. However, to avoid excessive schedules, the wait is capped by this function and it can + * be overrode for a custom {defaultAdminDelay} increase scheduling. + * + * IMPORTANT: Make sure to add a reasonable amount of time while overriding this value, otherwise, + * there's a risk of setting a high new delay that goes into effect almost immediately without the + * possibility of human intervention in the case of an input error (eg. set milliseconds instead of seconds). + */ + function defaultAdminDelayIncreaseWait() external view returns (uint48); +} diff --git a/contracts/access/IAccessControlEnumerable.sol b/contracts/access/IAccessControlEnumerable.sol index 61aaf57aa..240c61157 100644 --- a/contracts/access/IAccessControlEnumerable.sol +++ b/contracts/access/IAccessControlEnumerable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (access/IAccessControlEnumerable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IAccessControl.sol"; diff --git a/contracts/access/Ownable.sol b/contracts/access/Ownable.sol index 6d4e866f4..627226cf7 100644 --- a/contracts/access/Ownable.sol +++ b/contracts/access/Ownable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/Context.sol"; @@ -10,8 +10,8 @@ import "../utils/Context.sol"; * there is an account (an owner) that can be granted exclusive access to * specific functions. * - * By default, the owner account will be the one that deploys the contract. This - * can later be changed with {transferOwnership}. + * The initial owner is set to the address provided by the deployer. This can + * later be changed with {transferOwnership}. * * This module is used through inheritance. It will make available the modifier * `onlyOwner`, which can be applied to your functions to restrict their use to @@ -20,13 +20,23 @@ import "../utils/Context.sol"; abstract contract Ownable is Context { address private _owner; + /** + * @dev The caller account is not authorized to perform an operation. + */ + error OwnableUnauthorizedAccount(address account); + + /** + * @dev The owner is not a valid owner account. (eg. `address(0)`) + */ + error OwnableInvalidOwner(address owner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); /** - * @dev Initializes the contract setting the deployer as the initial owner. + * @dev Initializes the contract setting the address provided by the deployer as the initial owner. */ - constructor() { - _transferOwnership(_msgSender()); + constructor(address initialOwner) { + _transferOwnership(initialOwner); } /** @@ -48,15 +58,17 @@ abstract contract Ownable is Context { * @dev Throws if the sender is not the owner. */ function _checkOwner() internal view virtual { - require(owner() == _msgSender(), "Ownable: caller is not the owner"); + if (owner() != _msgSender()) { + revert OwnableUnauthorizedAccount(_msgSender()); + } } /** * @dev Leaves the contract without owner. It will not be possible to call - * `onlyOwner` functions anymore. Can only be called by the current owner. + * `onlyOwner` functions. Can only be called by the current owner. * * NOTE: Renouncing ownership will leave the contract without an owner, - * thereby removing any functionality that is only available to the owner. + * thereby disabling any functionality that is only available to the owner. */ function renounceOwnership() public virtual onlyOwner { _transferOwnership(address(0)); @@ -67,7 +79,9 @@ abstract contract Ownable is Context { * Can only be called by the current owner. */ function transferOwnership(address newOwner) public virtual onlyOwner { - require(newOwner != address(0), "Ownable: new owner is the zero address"); + if (newOwner == address(0)) { + revert OwnableInvalidOwner(address(0)); + } _transferOwnership(newOwner); } diff --git a/contracts/access/Ownable2Step.sol b/contracts/access/Ownable2Step.sol index f5a3d8047..f76eb2bba 100644 --- a/contracts/access/Ownable2Step.sol +++ b/contracts/access/Ownable2Step.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (access/Ownable2Step.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (access/Ownable2Step.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./Ownable.sol"; @@ -10,7 +10,7 @@ import "./Ownable.sol"; * there is an account (an owner) that can be granted exclusive access to * specific functions. * - * By default, the owner account will be the one that deploys the contract. This + * The initial owner is specified at deployment time in the constructor for `Ownable`. This * can later be changed with {transferOwnership} and {acceptOwnership}. * * This module is used through inheritance. It will make available all functions @@ -51,7 +51,9 @@ abstract contract Ownable2Step is Ownable { */ function acceptOwnership() public virtual { address sender = _msgSender(); - require(pendingOwner() == sender, "Ownable2Step: caller is not the new owner"); + if (pendingOwner() != sender) { + revert OwnableUnauthorizedAccount(sender); + } _transferOwnership(sender); } } diff --git a/contracts/access/README.adoc b/contracts/access/README.adoc index 888d0e99e..117cd7c97 100644 --- a/contracts/access/README.adoc +++ b/contracts/access/README.adoc @@ -18,8 +18,8 @@ This directory provides ways to restrict who can access the functions of a contr {{AccessControl}} -{{AccessControlCrossChain}} - {{IAccessControlEnumerable}} {{AccessControlEnumerable}} + +{{AccessControlDefaultAdminRules}} diff --git a/contracts/crosschain/CrossChainEnabled.sol b/contracts/crosschain/CrossChainEnabled.sol deleted file mode 100644 index 4c9b9e5c5..000000000 --- a/contracts/crosschain/CrossChainEnabled.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (crosschain/CrossChainEnabled.sol) - -pragma solidity ^0.8.4; - -import "./errors.sol"; - -/** - * @dev Provides information for building cross-chain aware contracts. This - * abstract contract provides accessors and modifiers to control the execution - * flow when receiving cross-chain messages. - * - * Actual implementations of cross-chain aware contracts, which are based on - * this abstraction, will have to inherit from a bridge-specific - * specialization. Such specializations are provided under - * `crosschain//CrossChainEnabled.sol`. - * - * _Available since v4.6._ - */ -abstract contract CrossChainEnabled { - /** - * @dev Throws if the current function call is not the result of a - * cross-chain execution. - */ - modifier onlyCrossChain() { - if (!_isCrossChain()) revert NotCrossChainCall(); - _; - } - - /** - * @dev Throws if the current function call is not the result of a - * cross-chain execution initiated by `account`. - */ - modifier onlyCrossChainSender(address expected) { - address actual = _crossChainSender(); - if (expected != actual) revert InvalidCrossChainSender(actual, expected); - _; - } - - /** - * @dev Returns whether the current function call is the result of a - * cross-chain message. - */ - function _isCrossChain() internal view virtual returns (bool); - - /** - * @dev Returns the address of the sender of the cross-chain message that - * triggered the current function call. - * - * IMPORTANT: Should revert with `NotCrossChainCall` if the current function - * call is not the result of a cross-chain message. - */ - function _crossChainSender() internal view virtual returns (address); -} diff --git a/contracts/crosschain/README.adoc b/contracts/crosschain/README.adoc deleted file mode 100644 index 266b1530f..000000000 --- a/contracts/crosschain/README.adoc +++ /dev/null @@ -1,34 +0,0 @@ -= Cross Chain Awareness - -[.readme-notice] -NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/crosschain - -This directory provides building blocks to improve cross-chain awareness of smart contracts. - -- {CrossChainEnabled} is an abstraction that contains accessors and modifiers to control the execution flow when receiving cross-chain messages. - -== CrossChainEnabled specializations - -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}} - -{{CrossChainEnabledArbitrumL1}} - -{{CrossChainEnabledArbitrumL2}} - -{{CrossChainEnabledOptimism}} - -{{CrossChainEnabledPolygonChild}} - -== Libraries for cross-chain - -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}} - -{{LibArbitrumL1}} - -{{LibArbitrumL2}} - -{{LibOptimism}} diff --git a/contracts/crosschain/amb/CrossChainEnabledAMB.sol b/contracts/crosschain/amb/CrossChainEnabledAMB.sol deleted file mode 100644 index e69355d62..000000000 --- a/contracts/crosschain/amb/CrossChainEnabledAMB.sol +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/amb/CrossChainEnabledAMB.sol) - -pragma solidity ^0.8.4; - -import "../CrossChainEnabled.sol"; -import "./LibAMB.sol"; - -/** - * @dev https://docs.tokenbridge.net/amb-bridge/about-amb-bridge[AMB] - * specialization or the {CrossChainEnabled} abstraction. - * - * As of february 2020, AMB bridges are available between the following chains: - * - * - https://docs.tokenbridge.net/eth-xdai-amb-bridge/about-the-eth-xdai-amb[ETH ⇌ xDai] - * - https://docs.tokenbridge.net/eth-qdai-bridge/about-the-eth-qdai-amb[ETH ⇌ qDai] - * - https://docs.tokenbridge.net/eth-etc-amb-bridge/about-the-eth-etc-amb[ETH ⇌ ETC] - * - https://docs.tokenbridge.net/eth-bsc-amb/about-the-eth-bsc-amb[ETH ⇌ BSC] - * - https://docs.tokenbridge.net/eth-poa-amb-bridge/about-the-eth-poa-amb[ETH ⇌ POA] - * - https://docs.tokenbridge.net/bsc-xdai-amb/about-the-bsc-xdai-amb[BSC ⇌ xDai] - * - https://docs.tokenbridge.net/poa-xdai-amb/about-the-poa-xdai-amb[POA ⇌ xDai] - * - https://docs.tokenbridge.net/rinkeby-xdai-amb-bridge/about-the-rinkeby-xdai-amb[Rinkeby ⇌ xDai] - * - https://docs.tokenbridge.net/kovan-sokol-amb-bridge/about-the-kovan-sokol-amb[Kovan ⇌ Sokol] - * - * _Available since v4.6._ - */ -contract CrossChainEnabledAMB is CrossChainEnabled { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address private immutable _bridge; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) { - _bridge = bridge; - } - - /** - * @dev see {CrossChainEnabled-_isCrossChain} - */ - function _isCrossChain() internal view virtual override returns (bool) { - return LibAMB.isCrossChain(_bridge); - } - - /** - * @dev see {CrossChainEnabled-_crossChainSender} - */ - function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { - return LibAMB.crossChainSender(_bridge); - } -} diff --git a/contracts/crosschain/amb/LibAMB.sol b/contracts/crosschain/amb/LibAMB.sol deleted file mode 100644 index aef9c43ee..000000000 --- a/contracts/crosschain/amb/LibAMB.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/amb/LibAMB.sol) - -pragma solidity ^0.8.4; - -import {IAMB as AMB_Bridge} from "../../vendor/amb/IAMB.sol"; -import "../errors.sol"; - -/** - * @dev Primitives for cross-chain aware contracts using the - * https://docs.tokenbridge.net/amb-bridge/about-amb-bridge[AMB] - * family of bridges. - */ -library LibAMB { - /** - * @dev Returns whether the current function call is the result of a - * cross-chain message relayed by `bridge`. - */ - function isCrossChain(address bridge) internal view returns (bool) { - return msg.sender == bridge; - } - - /** - * @dev Returns the address of the sender that triggered the current - * cross-chain message through `bridge`. - * - * NOTE: {isCrossChain} should be checked before trying to recover the - * sender, as it will revert with `NotCrossChainCall` if the current - * function call is not the result of a cross-chain message. - */ - function crossChainSender(address bridge) internal view returns (address) { - if (!isCrossChain(bridge)) revert NotCrossChainCall(); - return AMB_Bridge(bridge).messageSender(); - } -} diff --git a/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol b/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol deleted file mode 100644 index 5068da395..000000000 --- a/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol) - -pragma solidity ^0.8.4; - -import "../CrossChainEnabled.sol"; -import "./LibArbitrumL1.sol"; - -/** - * @dev https://arbitrum.io/[Arbitrum] specialization or the - * {CrossChainEnabled} abstraction the L1 side (mainnet). - * - * This version should only be deployed on L1 to process cross-chain messages - * originating from L2. For the other side, use {CrossChainEnabledArbitrumL2}. - * - * The bridge contract is provided and maintained by the arbitrum team. You can - * find the address of this contract on the rinkeby testnet in - * https://developer.offchainlabs.com/docs/useful_addresses[Arbitrum's developer documentation]. - * - * _Available since v4.6._ - */ -abstract contract CrossChainEnabledArbitrumL1 is CrossChainEnabled { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address private immutable _bridge; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) { - _bridge = bridge; - } - - /** - * @dev see {CrossChainEnabled-_isCrossChain} - */ - function _isCrossChain() internal view virtual override returns (bool) { - return LibArbitrumL1.isCrossChain(_bridge); - } - - /** - * @dev see {CrossChainEnabled-_crossChainSender} - */ - function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { - return LibArbitrumL1.crossChainSender(_bridge); - } -} diff --git a/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol b/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol deleted file mode 100644 index e85993dbb..000000000 --- a/contracts/crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol) - -pragma solidity ^0.8.4; - -import "../CrossChainEnabled.sol"; -import "./LibArbitrumL2.sol"; - -/** - * @dev https://arbitrum.io/[Arbitrum] specialization or the - * {CrossChainEnabled} abstraction the L2 side (arbitrum). - * - * This version should only be deployed on L2 to process cross-chain messages - * originating from L1. For the other side, use {CrossChainEnabledArbitrumL1}. - * - * Arbitrum L2 includes the `ArbSys` contract at a fixed address. Therefore, - * this specialization of {CrossChainEnabled} does not include a constructor. - * - * _Available since v4.6._ - * - * WARNING: There is currently a bug in Arbitrum that causes this contract to - * fail to detect cross-chain calls when deployed behind a proxy. This will be - * fixed when the network is upgraded to Arbitrum Nitro, currently scheduled for - * August 31st 2022. - */ -abstract contract CrossChainEnabledArbitrumL2 is CrossChainEnabled { - /** - * @dev see {CrossChainEnabled-_isCrossChain} - */ - function _isCrossChain() internal view virtual override returns (bool) { - return LibArbitrumL2.isCrossChain(LibArbitrumL2.ARBSYS); - } - - /** - * @dev see {CrossChainEnabled-_crossChainSender} - */ - function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { - return LibArbitrumL2.crossChainSender(LibArbitrumL2.ARBSYS); - } -} diff --git a/contracts/crosschain/arbitrum/LibArbitrumL1.sol b/contracts/crosschain/arbitrum/LibArbitrumL1.sol deleted file mode 100644 index be7236b24..000000000 --- a/contracts/crosschain/arbitrum/LibArbitrumL1.sol +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (crosschain/arbitrum/LibArbitrumL1.sol) - -pragma solidity ^0.8.4; - -import {IBridge as ArbitrumL1_Bridge} from "../../vendor/arbitrum/IBridge.sol"; -import {IOutbox as ArbitrumL1_Outbox} from "../../vendor/arbitrum/IOutbox.sol"; -import "../errors.sol"; - -/** - * @dev Primitives for cross-chain aware contracts for - * https://arbitrum.io/[Arbitrum]. - * - * This version should only be used on L1 to process cross-chain messages - * originating from L2. For the other side, use {LibArbitrumL2}. - */ -library LibArbitrumL1 { - /** - * @dev Returns whether the current function call is the result of a - * cross-chain message relayed by the `bridge`. - */ - function isCrossChain(address bridge) internal view returns (bool) { - return msg.sender == bridge; - } - - /** - * @dev Returns the address of the sender that triggered the current - * cross-chain message through the `bridge`. - * - * NOTE: {isCrossChain} should be checked before trying to recover the - * sender, as it will revert with `NotCrossChainCall` if the current - * function call is not the result of a cross-chain message. - */ - function crossChainSender(address bridge) internal view returns (address) { - if (!isCrossChain(bridge)) revert NotCrossChainCall(); - - address sender = ArbitrumL1_Outbox(ArbitrumL1_Bridge(bridge).activeOutbox()).l2ToL1Sender(); - require(sender != address(0), "LibArbitrumL1: system messages without sender"); - - return sender; - } -} diff --git a/contracts/crosschain/arbitrum/LibArbitrumL2.sol b/contracts/crosschain/arbitrum/LibArbitrumL2.sol deleted file mode 100644 index 715a3878f..000000000 --- a/contracts/crosschain/arbitrum/LibArbitrumL2.sol +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (crosschain/arbitrum/LibArbitrumL2.sol) - -pragma solidity ^0.8.4; - -import {IArbSys as ArbitrumL2_Bridge} from "../../vendor/arbitrum/IArbSys.sol"; -import "../errors.sol"; - -/** - * @dev Primitives for cross-chain aware contracts for - * https://arbitrum.io/[Arbitrum]. - * - * This version should only be used on L2 to process cross-chain messages - * originating from L1. For the other side, use {LibArbitrumL1}. - * - * WARNING: There is currently a bug in Arbitrum that causes this contract to - * fail to detect cross-chain calls when deployed behind a proxy. This will be - * fixed when the network is upgraded to Arbitrum Nitro, currently scheduled for - * August 31st 2022. - */ -library LibArbitrumL2 { - /** - * @dev Returns whether the current function call is the result of a - * cross-chain message relayed by `arbsys`. - */ - address public constant ARBSYS = 0x0000000000000000000000000000000000000064; - - function isCrossChain(address arbsys) internal view returns (bool) { - return ArbitrumL2_Bridge(arbsys).wasMyCallersAddressAliased(); - } - - /** - * @dev Returns the address of the sender that triggered the current - * cross-chain message through `arbsys`. - * - * NOTE: {isCrossChain} should be checked before trying to recover the - * sender, as it will revert with `NotCrossChainCall` if the current - * function call is not the result of a cross-chain message. - */ - function crossChainSender(address arbsys) internal view returns (address) { - if (!isCrossChain(arbsys)) revert NotCrossChainCall(); - - return ArbitrumL2_Bridge(arbsys).myCallersAddressWithoutAliasing(); - } -} diff --git a/contracts/crosschain/errors.sol b/contracts/crosschain/errors.sol deleted file mode 100644 index 004460e97..000000000 --- a/contracts/crosschain/errors.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (crosschain/errors.sol) - -pragma solidity ^0.8.4; - -error NotCrossChainCall(); -error InvalidCrossChainSender(address actual, address expected); diff --git a/contracts/crosschain/optimism/CrossChainEnabledOptimism.sol b/contracts/crosschain/optimism/CrossChainEnabledOptimism.sol deleted file mode 100644 index 1005864ae..000000000 --- a/contracts/crosschain/optimism/CrossChainEnabledOptimism.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/optimism/CrossChainEnabledOptimism.sol) - -pragma solidity ^0.8.4; - -import "../CrossChainEnabled.sol"; -import "./LibOptimism.sol"; - -/** - * @dev https://www.optimism.io/[Optimism] specialization or the - * {CrossChainEnabled} abstraction. - * - * The messenger (`CrossDomainMessenger`) contract is provided and maintained by - * the optimism team. You can find the address of this contract on mainnet and - * kovan in the https://github.com/ethereum-optimism/optimism/tree/develop/packages/contracts/deployments[deployments section of Optimism monorepo]. - * - * _Available since v4.6._ - */ -abstract contract CrossChainEnabledOptimism is CrossChainEnabled { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address private immutable _messenger; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address messenger) { - _messenger = messenger; - } - - /** - * @dev see {CrossChainEnabled-_isCrossChain} - */ - function _isCrossChain() internal view virtual override returns (bool) { - return LibOptimism.isCrossChain(_messenger); - } - - /** - * @dev see {CrossChainEnabled-_crossChainSender} - */ - function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { - return LibOptimism.crossChainSender(_messenger); - } -} diff --git a/contracts/crosschain/optimism/LibOptimism.sol b/contracts/crosschain/optimism/LibOptimism.sol deleted file mode 100644 index d963ade2a..000000000 --- a/contracts/crosschain/optimism/LibOptimism.sol +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/optimism/LibOptimism.sol) - -pragma solidity ^0.8.4; - -import {ICrossDomainMessenger as Optimism_Bridge} from "../../vendor/optimism/ICrossDomainMessenger.sol"; -import "../errors.sol"; - -/** - * @dev Primitives for cross-chain aware contracts for https://www.optimism.io/[Optimism]. - * See the https://community.optimism.io/docs/developers/bridge/messaging/#accessing-msg-sender[documentation] - * for the functionality used here. - */ -library LibOptimism { - /** - * @dev Returns whether the current function call is the result of a - * cross-chain message relayed by `messenger`. - */ - function isCrossChain(address messenger) internal view returns (bool) { - return msg.sender == messenger; - } - - /** - * @dev Returns the address of the sender that triggered the current - * cross-chain message through `messenger`. - * - * NOTE: {isCrossChain} should be checked before trying to recover the - * sender, as it will revert with `NotCrossChainCall` if the current - * function call is not the result of a cross-chain message. - */ - function crossChainSender(address messenger) internal view returns (address) { - if (!isCrossChain(messenger)) revert NotCrossChainCall(); - - return Optimism_Bridge(messenger).xDomainMessageSender(); - } -} diff --git a/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol b/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol deleted file mode 100644 index fa0994834..000000000 --- a/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (crosschain/polygon/CrossChainEnabledPolygonChild.sol) - -pragma solidity ^0.8.4; - -import "../CrossChainEnabled.sol"; -import "../../security/ReentrancyGuard.sol"; -import "../../utils/Address.sol"; -import "../../vendor/polygon/IFxMessageProcessor.sol"; - -address constant DEFAULT_SENDER = 0x000000000000000000000000000000000000dEaD; - -/** - * @dev https://polygon.technology/[Polygon] specialization or the - * {CrossChainEnabled} abstraction the child side (polygon/mumbai). - * - * This version should only be deployed on child chain to process cross-chain - * messages originating from the parent chain. - * - * The fxChild contract is provided and maintained by the polygon team. You can - * find the address of this contract polygon and mumbai in - * https://docs.polygon.technology/docs/develop/l1-l2-communication/fx-portal/#contract-addresses[Polygon's Fx-Portal documentation]. - * - * _Available since v4.6._ - */ -abstract contract CrossChainEnabledPolygonChild is IFxMessageProcessor, CrossChainEnabled, ReentrancyGuard { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable - address private immutable _fxChild; - address private _sender = DEFAULT_SENDER; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address fxChild) { - _fxChild = fxChild; - } - - /** - * @dev see {CrossChainEnabled-_isCrossChain} - */ - function _isCrossChain() internal view virtual override returns (bool) { - return msg.sender == _fxChild; - } - - /** - * @dev see {CrossChainEnabled-_crossChainSender} - */ - function _crossChainSender() internal view virtual override onlyCrossChain returns (address) { - return _sender; - } - - /** - * @dev External entry point to receive and relay messages originating - * from the fxChild. - * - * Non-reentrancy is crucial to avoid a cross-chain call being able - * to impersonate anyone by just looping through this with user-defined - * arguments. - * - * Note: if _fxChild calls any other function that does a delegate-call, - * then security could be compromised. - */ - function processMessageFromRoot( - uint256 /* stateId */, - address rootMessageSender, - bytes calldata data - ) external override nonReentrant { - if (!_isCrossChain()) revert NotCrossChainCall(); - - _sender = rootMessageSender; - Address.functionDelegateCall(address(this), data, "cross-chain execution failed"); - _sender = DEFAULT_SENDER; - } -} diff --git a/contracts/finance/PaymentSplitter.sol b/contracts/finance/PaymentSplitter.sol deleted file mode 100644 index daa9090eb..000000000 --- a/contracts/finance/PaymentSplitter.sol +++ /dev/null @@ -1,214 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (finance/PaymentSplitter.sol) - -pragma solidity ^0.8.0; - -import "../token/ERC20/utils/SafeERC20.sol"; -import "../utils/Address.sol"; -import "../utils/Context.sol"; - -/** - * @title PaymentSplitter - * @dev This contract allows to split Ether payments among a group of accounts. The sender does not need to be aware - * that the Ether will be split in this way, since it is handled transparently by the contract. - * - * The split can be in equal parts or in any other arbitrary proportion. The way this is specified is by assigning each - * account to a number of shares. Of all the Ether that this contract receives, each account will then be able to claim - * an amount proportional to the percentage of total shares they were assigned. The distribution of shares is set at the - * time of contract deployment and can't be updated thereafter. - * - * `PaymentSplitter` follows a _pull payment_ model. This means that payments are not automatically forwarded to the - * accounts but kept in this contract, and the actual transfer is triggered as a separate step by calling the {release} - * function. - * - * NOTE: This contract assumes that ERC20 tokens will behave similarly to native tokens (Ether). Rebasing tokens, and - * tokens that apply fees during transfers, are likely to not be supported as expected. If in doubt, we encourage you - * to run tests before sending real value to this contract. - */ -contract PaymentSplitter is Context { - event PayeeAdded(address account, uint256 shares); - event PaymentReleased(address to, uint256 amount); - event ERC20PaymentReleased(IERC20 indexed token, address to, uint256 amount); - event PaymentReceived(address from, uint256 amount); - - uint256 private _totalShares; - uint256 private _totalReleased; - - mapping(address => uint256) private _shares; - mapping(address => uint256) private _released; - address[] private _payees; - - mapping(IERC20 => uint256) private _erc20TotalReleased; - mapping(IERC20 => mapping(address => uint256)) private _erc20Released; - - /** - * @dev Creates an instance of `PaymentSplitter` where each account in `payees` is assigned the number of shares at - * the matching position in the `shares` array. - * - * All addresses in `payees` must be non-zero. Both arrays must have the same non-zero length, and there must be no - * duplicates in `payees`. - */ - constructor(address[] memory payees, uint256[] memory shares_) payable { - require(payees.length == shares_.length, "PaymentSplitter: payees and shares length mismatch"); - require(payees.length > 0, "PaymentSplitter: no payees"); - - for (uint256 i = 0; i < payees.length; i++) { - _addPayee(payees[i], shares_[i]); - } - } - - /** - * @dev The Ether received will be logged with {PaymentReceived} events. Note that these events are not fully - * reliable: it's possible for a contract to receive Ether without triggering this function. This only affects the - * reliability of the events, and not the actual splitting of Ether. - * - * To learn more about this see the Solidity documentation for - * https://solidity.readthedocs.io/en/latest/contracts.html#fallback-function[fallback - * functions]. - */ - receive() external payable virtual { - emit PaymentReceived(_msgSender(), msg.value); - } - - /** - * @dev Getter for the total shares held by payees. - */ - function totalShares() public view returns (uint256) { - return _totalShares; - } - - /** - * @dev Getter for the total amount of Ether already released. - */ - function totalReleased() public view returns (uint256) { - return _totalReleased; - } - - /** - * @dev Getter for the total amount of `token` already released. `token` should be the address of an IERC20 - * contract. - */ - function totalReleased(IERC20 token) public view returns (uint256) { - return _erc20TotalReleased[token]; - } - - /** - * @dev Getter for the amount of shares held by an account. - */ - function shares(address account) public view returns (uint256) { - return _shares[account]; - } - - /** - * @dev Getter for the amount of Ether already released to a payee. - */ - function released(address account) public view returns (uint256) { - return _released[account]; - } - - /** - * @dev Getter for the amount of `token` tokens already released to a payee. `token` should be the address of an - * IERC20 contract. - */ - function released(IERC20 token, address account) public view returns (uint256) { - return _erc20Released[token][account]; - } - - /** - * @dev Getter for the address of the payee number `index`. - */ - function payee(uint256 index) public view returns (address) { - 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. - */ - function release(address payable account) public virtual { - require(_shares[account] > 0, "PaymentSplitter: account has no shares"); - - uint256 payment = releasable(account); - - require(payment != 0, "PaymentSplitter: account is not due payment"); - - // _totalReleased is the sum of all values in _released. - // If "_totalReleased += payment" does not overflow, then "_released[account] += payment" cannot overflow. - _totalReleased += payment; - unchecked { - _released[account] += payment; - } - - Address.sendValue(account, payment); - emit PaymentReleased(account, payment); - } - - /** - * @dev Triggers a transfer to `account` of the amount of `token` tokens they are owed, according to their - * percentage of the total shares and their previous withdrawals. `token` must be the address of an IERC20 - * contract. - */ - function release(IERC20 token, address account) public virtual { - require(_shares[account] > 0, "PaymentSplitter: account has no shares"); - - uint256 payment = releasable(token, account); - - require(payment != 0, "PaymentSplitter: account is not due payment"); - - // _erc20TotalReleased[token] is the sum of all values in _erc20Released[token]. - // If "_erc20TotalReleased[token] += payment" does not overflow, then "_erc20Released[token][account] += payment" - // cannot overflow. - _erc20TotalReleased[token] += payment; - unchecked { - _erc20Released[token][account] += payment; - } - - SafeERC20.safeTransfer(token, account, payment); - emit ERC20PaymentReleased(token, account, payment); - } - - /** - * @dev internal logic for computing the pending payment of an `account` given the token historical balances and - * already released amounts. - */ - function _pendingPayment( - address account, - uint256 totalReceived, - uint256 alreadyReleased - ) private view returns (uint256) { - return (totalReceived * _shares[account]) / _totalShares - alreadyReleased; - } - - /** - * @dev Add a new payee to the contract. - * @param account The address of the payee to add. - * @param shares_ The number of shares owned by the payee. - */ - function _addPayee(address account, uint256 shares_) private { - require(account != address(0), "PaymentSplitter: account is the zero address"); - require(shares_ > 0, "PaymentSplitter: shares are 0"); - require(_shares[account] == 0, "PaymentSplitter: account already has shares"); - - _payees.push(account); - _shares[account] = shares_; - _totalShares = _totalShares + shares_; - emit PayeeAdded(account, shares_); - } -} diff --git a/contracts/finance/README.adoc b/contracts/finance/README.adoc index b64af3125..ac7e4f015 100644 --- a/contracts/finance/README.adoc +++ b/contracts/finance/README.adoc @@ -5,16 +5,10 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ This directory includes primitives for financial systems: -- {PaymentSplitter} allows to split Ether and ERC20 payments among a group of accounts. The sender does not need to be - aware that the assets will be split in this way, since it is handled transparently by the contract. The split can be - in equal parts or in any other arbitrary proportion. - - {VestingWallet} handles the vesting of Ether and ERC20 tokens for a given beneficiary. Custody of multiple tokens can be given to this contract, which will release the token to the beneficiary following a given, customizable, vesting schedule. == Contracts -{{PaymentSplitter}} - {{VestingWallet}} diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index fe67eb54f..ebdf0a330 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (finance/VestingWallet.sol) -pragma solidity ^0.8.0; +// OpenZeppelin Contracts (last updated v4.9.0) (finance/VestingWallet.sol) +pragma solidity ^0.8.19; import "../token/ERC20/utils/SafeERC20.sol"; import "../utils/Address.sol"; @@ -15,11 +15,19 @@ import "../utils/Context.sol"; * Any token transferred to this contract will follow the vesting schedule as if they were locked from the beginning. * Consequently, if the vesting has already started, any amount of tokens sent to this contract will (at least partly) * be immediately releasable. + * + * By setting the duration to 0, one can configure this contract to behave like an asset timelock that hold tokens for + * a beneficiary until a specified time. */ contract VestingWallet is Context { event EtherReleased(uint256 amount); event ERC20Released(address indexed token, uint256 amount); + /** + * @dev The `beneficiary` is not a valid account. + */ + error VestingWalletInvalidBeneficiary(address beneficiary); + uint256 private _released; mapping(address => uint256) private _erc20Released; address private immutable _beneficiary; @@ -30,7 +38,9 @@ contract VestingWallet is Context { * @dev Set the beneficiary, start timestamp and vesting duration of the vesting wallet. */ constructor(address beneficiaryAddress, uint64 startTimestamp, uint64 durationSeconds) payable { - require(beneficiaryAddress != address(0), "VestingWallet: beneficiary is zero address"); + if (beneficiaryAddress == address(0)) { + revert VestingWalletInvalidBeneficiary(address(0)); + } _beneficiary = beneficiaryAddress; _start = startTimestamp; _duration = durationSeconds; @@ -62,6 +72,13 @@ contract VestingWallet is Context { return _duration; } + /** + * @dev Getter for the end timestamp. + */ + function end() public view virtual returns (uint256) { + return start() + duration(); + } + /** * @dev Amount of eth already released */ @@ -136,7 +153,7 @@ contract VestingWallet is Context { function _vestingSchedule(uint256 totalAllocation, uint64 timestamp) internal view virtual returns (uint256) { if (timestamp < start()) { return 0; - } else if (timestamp > start() + duration()) { + } else if (timestamp > end()) { return totalAllocation; } else { return (totalAllocation * (timestamp - start())) / duration(); diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 8235fb5ec..bae5d62e6 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/Governor.sol) +// OpenZeppelin Contracts (last updated v4.9.1) (governance/Governor.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC721/IERC721Receiver.sol"; import "../token/ERC1155/IERC1155Receiver.sol"; @@ -12,38 +12,40 @@ import "../utils/math/SafeCast.sol"; import "../utils/structs/DoubleEndedQueue.sol"; import "../utils/Address.sol"; import "../utils/Context.sol"; -import "../utils/Timers.sol"; import "./IGovernor.sol"; /** * @dev Core of the governance system, designed to be extended though various modules. * - * This contract is abstract and requires several function to be implemented in various modules: + * This contract is abstract and requires several functions to be implemented in various modules: * * - A counting module must implement {quorum}, {_quorumReached}, {_voteSucceeded} and {_countVote} * - A voting module must implement {_getVotes} - * - Additionally, the {votingPeriod} must also be implemented + * - Additionally, {votingPeriod} must also be implemented * * _Available since v4.3._ */ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receiver, IERC1155Receiver { using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque; - using SafeCast for uint256; - using Timers for Timers.BlockNumber; bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,uint8 support)"); bytes32 public constant EXTENDED_BALLOT_TYPEHASH = keccak256("ExtendedBallot(uint256 proposalId,uint8 support,string reason,bytes params)"); + // solhint-disable var-name-mixedcase struct ProposalCore { - Timers.BlockNumber voteStart; - Timers.BlockNumber voteEnd; + address proposer; + uint48 voteStart; + uint32 voteDuration; bool executed; bool canceled; } + // solhint-enable var-name-mixedcase + bytes32 private constant _ALL_PROPOSAL_STATES_BITMAP = bytes32((2 ** (uint8(type(ProposalState).max) + 1)) - 1); string private _name; + /// @custom:oz-retyped-from mapping(uint256 => Governor.ProposalCore) mapping(uint256 => ProposalCore) private _proposals; // This queue keeps track of the governor operating on itself. Calls to functions protected by the @@ -63,7 +65,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive * governance protocol (since v4.6). */ modifier onlyGovernance() { - require(_msgSender() == _executor(), "Governor: onlyGovernance"); + if (_executor() != _msgSender()) { + revert GovernorOnlyExecutor(_msgSender()); + } if (_executor() != address(this)) { bytes32 msgDataHash = keccak256(_msgData()); // loop until popping the expected operation - throw if deque is empty (operation not authorized) @@ -83,22 +87,36 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive * @dev Function to receive ETH that will be handled by the governor (disabled if executor is a third party contract) */ receive() external payable virtual { - require(_executor() == address(this)); + if (_executor() != address(this)) { + revert GovernorDisabledDeposit(); + } } /** * @dev See {IERC165-supportsInterface}. */ function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, ERC165) returns (bool) { - // In addition to the current interfaceId, also support previous version of the interfaceId that did not - // include the castVoteWithReasonAndParams() function as standard + bytes4 governorCancelId = this.cancel.selector ^ this.proposalProposer.selector; + + bytes4 governorParamsId = this.castVoteWithReasonAndParams.selector ^ + this.castVoteWithReasonAndParamsBySig.selector ^ + this.getVotesWithParams.selector; + + // The original interface id in v4.3. + bytes4 governor43Id = type(IGovernor).interfaceId ^ + type(IERC6372).interfaceId ^ + governorCancelId ^ + governorParamsId; + + // An updated interface id in v4.6, with params added. + bytes4 governor46Id = type(IGovernor).interfaceId ^ type(IERC6372).interfaceId ^ governorCancelId; + + // For the updated interface id in v4.9, we use governorCancelId directly. + return - interfaceId == - (type(IGovernor).interfaceId ^ - this.castVoteWithReasonAndParams.selector ^ - this.castVoteWithReasonAndParamsBySig.selector ^ - this.getVotesWithParams.selector) || - interfaceId == type(IGovernor).interfaceId || + interfaceId == governor43Id || + interfaceId == governor46Id || + interfaceId == governorCancelId || interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); } @@ -143,7 +161,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive * @dev See {IGovernor-state}. */ function state(uint256 proposalId) public view virtual override returns (ProposalState) { - ProposalCore storage proposal = _proposals[proposalId]; + // ProposalCore is just one slot. We can load it from storage to memory with a single sload and use memory + // object as a cache. This avoid duplicating expensive sloads. + ProposalCore memory proposal = _proposals[proposalId]; if (proposal.executed) { return ProposalState.Executed; @@ -156,16 +176,18 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive uint256 snapshot = proposalSnapshot(proposalId); if (snapshot == 0) { - revert("Governor: unknown proposal id"); + revert GovernorNonexistentProposal(proposalId); } - if (snapshot >= block.number) { + uint256 currentTimepoint = clock(); + + if (snapshot >= currentTimepoint) { return ProposalState.Pending; } uint256 deadline = proposalDeadline(proposalId); - if (deadline >= block.number) { + if (deadline >= currentTimepoint) { return ProposalState.Active; } @@ -176,25 +198,32 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive } } + /** + * @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_. + */ + function proposalThreshold() public view virtual returns (uint256) { + return 0; + } + /** * @dev See {IGovernor-proposalSnapshot}. */ function proposalSnapshot(uint256 proposalId) public view virtual override returns (uint256) { - return _proposals[proposalId].voteStart.getDeadline(); + return _proposals[proposalId].voteStart; } /** * @dev See {IGovernor-proposalDeadline}. */ function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) { - return _proposals[proposalId].voteEnd.getDeadline(); + return _proposals[proposalId].voteStart + _proposals[proposalId].voteDuration; } /** - * @dev Part of the Governor Bravo's interface: _"The number of votes required in order for a voter to become a proposer"_. + * @dev Returns the account that created a given proposal. */ - function proposalThreshold() public view virtual returns (uint256) { - return 0; + function proposalProposer(uint256 proposalId) public view virtual override returns (address) { + return _proposals[proposalId].proposer; } /** @@ -208,13 +237,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive function _voteSucceeded(uint256 proposalId) internal view virtual returns (bool); /** - * @dev Get the voting weight of `account` at a specific `blockNumber`, for a vote as described by `params`. + * @dev Get the voting weight of `account` at a specific `timepoint`, for a vote as described by `params`. */ - function _getVotes( - address account, - uint256 blockNumber, - bytes memory params - ) internal view virtual returns (uint256); + function _getVotes(address account, uint256 timepoint, bytes memory params) internal view virtual returns (uint256); /** * @dev Register a vote for `proposalId` by `account` with a given `support`, voting `weight` and voting `params`. @@ -240,7 +265,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive } /** - * @dev See {IGovernor-propose}. + * @dev See {IGovernor-propose}. This function has opt-in frontrunning protection, described in {_isValidDescriptionForProposer}. */ function propose( address[] memory targets, @@ -248,35 +273,49 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive bytes[] memory calldatas, string memory description ) public virtual override returns (uint256) { - require( - getVotes(_msgSender(), block.number - 1) >= proposalThreshold(), - "Governor: proposer votes below proposal threshold" - ); + address proposer = _msgSender(); + require(_isValidDescriptionForProposer(proposer, description), "Governor: proposer restricted"); + + uint256 currentTimepoint = clock(); + + // Avoid stack too deep + { + uint256 proposerVotes = getVotes(proposer, currentTimepoint - 1); + uint256 votesThreshold = proposalThreshold(); + if (proposerVotes < votesThreshold) { + revert GovernorInsufficientProposerVotes(proposer, proposerVotes, votesThreshold); + } + } uint256 proposalId = hashProposal(targets, values, calldatas, keccak256(bytes(description))); - require(targets.length == values.length, "Governor: invalid proposal length"); - require(targets.length == calldatas.length, "Governor: invalid proposal length"); - require(targets.length > 0, "Governor: empty proposal"); + if (targets.length != values.length || targets.length != calldatas.length || targets.length == 0) { + revert GovernorInvalidProposalLength(targets.length, calldatas.length, values.length); + } + if (_proposals[proposalId].voteStart != 0) { + revert GovernorUnexpectedProposalState(proposalId, state(proposalId), bytes32(0)); + } - ProposalCore storage proposal = _proposals[proposalId]; - require(proposal.voteStart.isUnset(), "Governor: proposal already exists"); + uint256 snapshot = currentTimepoint + votingDelay(); + uint256 duration = votingPeriod(); - uint64 snapshot = block.number.toUint64() + votingDelay().toUint64(); - uint64 deadline = snapshot + votingPeriod().toUint64(); - - proposal.voteStart.setDeadline(snapshot); - proposal.voteEnd.setDeadline(deadline); + _proposals[proposalId] = ProposalCore({ + proposer: proposer, + voteStart: SafeCast.toUint48(snapshot), + voteDuration: SafeCast.toUint32(duration), + executed: false, + canceled: false + }); emit ProposalCreated( proposalId, - _msgSender(), + proposer, targets, values, new string[](targets.length), calldatas, snapshot, - deadline, + snapshot + duration, description ); @@ -294,11 +333,14 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive ) public payable virtual override returns (uint256) { uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - ProposalState status = state(proposalId); - require( - status == ProposalState.Succeeded || status == ProposalState.Queued, - "Governor: proposal not successful" - ); + ProposalState currentState = state(proposalId); + if (currentState != ProposalState.Succeeded && currentState != ProposalState.Queued) { + revert GovernorUnexpectedProposalState( + proposalId, + currentState, + _encodeStateBitmap(ProposalState.Succeeded) | _encodeStateBitmap(ProposalState.Queued) + ); + } _proposals[proposalId].executed = true; emit ProposalExecuted(proposalId); @@ -310,6 +352,26 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive return proposalId; } + /** + * @dev See {IGovernor-cancel}. + */ + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public virtual override returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + ProposalState currentState = state(proposalId); + if (currentState != ProposalState.Pending) { + revert GovernorUnexpectedProposalState(proposalId, currentState, _encodeStateBitmap(ProposalState.Pending)); + } + if (_msgSender() != proposalProposer(proposalId)) { + revert GovernorOnlyProposer(_msgSender()); + } + return _cancel(targets, values, calldatas, descriptionHash); + } + /** * @dev Internal execution mechanism. Can be overridden to implement different execution mechanism */ @@ -320,10 +382,9 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive bytes[] memory calldatas, bytes32 /*descriptionHash*/ ) internal virtual { - string memory errorMessage = "Governor: call reverted without message"; for (uint256 i = 0; i < targets.length; ++i) { (bool success, bytes memory returndata) = targets[i].call{value: values[i]}(calldatas[i]); - Address.verifyCallResult(success, returndata, errorMessage); + Address.verifyCallResult(success, returndata); } } @@ -376,12 +437,19 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive bytes32 descriptionHash ) internal virtual returns (uint256) { uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - ProposalState status = state(proposalId); - require( - status != ProposalState.Canceled && status != ProposalState.Expired && status != ProposalState.Executed, - "Governor: proposal not active" - ); + ProposalState currentState = state(proposalId); + + bytes32 forbiddenStates = _encodeStateBitmap(ProposalState.Canceled) | + _encodeStateBitmap(ProposalState.Expired) | + _encodeStateBitmap(ProposalState.Executed); + if (forbiddenStates & _encodeStateBitmap(currentState) != 0) { + revert GovernorUnexpectedProposalState( + proposalId, + currentState, + _ALL_PROPOSAL_STATES_BITMAP ^ forbiddenStates + ); + } _proposals[proposalId].canceled = true; emit ProposalCanceled(proposalId); @@ -392,8 +460,8 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive /** * @dev See {IGovernor-getVotes}. */ - function getVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { - return _getVotes(account, blockNumber, _defaultParams()); + function getVotes(address account, uint256 timepoint) public view virtual override returns (uint256) { + return _getVotes(account, timepoint, _defaultParams()); } /** @@ -401,10 +469,10 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive */ function getVotesWithParams( address account, - uint256 blockNumber, + uint256 timepoint, bytes memory params ) public view virtual override returns (uint256) { - return _getVotes(account, blockNumber, params); + return _getVotes(account, timepoint, params); } /** @@ -519,10 +587,12 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive string memory reason, bytes memory params ) internal virtual returns (uint256) { - ProposalCore storage proposal = _proposals[proposalId]; - require(state(proposalId) == ProposalState.Active, "Governor: vote not currently active"); + ProposalState currentState = state(proposalId); + if (currentState != ProposalState.Active) { + revert GovernorUnexpectedProposalState(proposalId, currentState, _encodeStateBitmap(ProposalState.Active)); + } - uint256 weight = _getVotes(account, proposal.voteStart.getDeadline(), params); + uint256 weight = _getVotes(account, proposalSnapshot(proposalId), params); _countVote(proposalId, account, support, weight, params); if (params.length == 0) { @@ -542,7 +612,7 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive */ function relay(address target, uint256 value, bytes calldata data) external payable virtual onlyGovernance { (bool success, bytes memory returndata) = target.call{value: value}(data); - Address.verifyCallResult(success, returndata, "Governor: relay reverted without message"); + Address.verifyCallResult(success, returndata); } /** @@ -555,26 +625,29 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive /** * @dev See {IERC721Receiver-onERC721Received}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). */ - function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { + if (_executor() != address(this)) { + revert GovernorDisabledDeposit(); + } return this.onERC721Received.selector; } /** * @dev See {IERC1155Receiver-onERC1155Received}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). */ - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes memory - ) public virtual override returns (bytes4) { + function onERC1155Received(address, address, uint256, uint256, bytes memory) public virtual returns (bytes4) { + if (_executor() != address(this)) { + revert GovernorDisabledDeposit(); + } return this.onERC1155Received.selector; } /** * @dev See {IERC1155Receiver-onERC1155BatchReceived}. + * Receiving tokens is disabled if the governance executor is other than the governor itself (eg. when using with a timelock). */ function onERC1155BatchReceived( address, @@ -582,7 +655,111 @@ abstract contract Governor is Context, ERC165, EIP712, IGovernor, IERC721Receive uint256[] memory, uint256[] memory, bytes memory - ) public virtual override returns (bytes4) { + ) public virtual returns (bytes4) { + if (_executor() != address(this)) { + revert GovernorDisabledDeposit(); + } return this.onERC1155BatchReceived.selector; } + + /** + * @dev Encodes a `ProposalState` into a `bytes32` representation where each bit enabled corresponds to + * the underlying position in the `ProposalState` enum. For example: + * + * 0x000...10000 + * ^^^^^^------ ... + * ^----- Succeeded + * ^---- Defeated + * ^--- Canceled + * ^-- Active + * ^- Pending + */ + function _encodeStateBitmap(ProposalState proposalState) internal pure returns (bytes32) { + return bytes32(1 << uint8(proposalState)); + } + + /* + * @dev Check if the proposer is authorized to submit a proposal with the given description. + * + * If the proposal description ends with `#proposer=0x???`, where `0x???` is an address written as a hex string + * (case insensitive), then the submission of this proposal will only be authorized to said address. + * + * This is used for frontrunning protection. By adding this pattern at the end of their proposal, one can ensure + * that no other address can submit the same proposal. An attacker would have to either remove or change that part, + * which would result in a different proposal id. + * + * If the description does not match this pattern, it is unrestricted and anyone can submit it. This includes: + * - If the `0x???` part is not a valid hex string. + * - If the `0x???` part is a valid hex string, but does not contain exactly 40 hex digits. + * - If it ends with the expected suffix followed by newlines or other whitespace. + * - If it ends with some other similar suffix, e.g. `#other=abc`. + * - If it does not end with any such suffix. + */ + function _isValidDescriptionForProposer( + address proposer, + string memory description + ) internal view virtual returns (bool) { + uint256 len = bytes(description).length; + + // Length is too short to contain a valid proposer suffix + if (len < 52) { + return true; + } + + // Extract what would be the `#proposer=0x` marker beginning the suffix + bytes12 marker; + assembly { + // - Start of the string contents in memory = description + 32 + // - First character of the marker = len - 52 + // - Length of "#proposer=0x0000000000000000000000000000000000000000" = 52 + // - We read the memory word starting at the first character of the marker: + // - (description + 32) + (len - 52) = description + (len - 20) + // - Note: Solidity will ignore anything past the first 12 bytes + marker := mload(add(description, sub(len, 20))) + } + + // If the marker is not found, there is no proposer suffix to check + if (marker != bytes12("#proposer=0x")) { + return true; + } + + // Parse the 40 characters following the marker as uint160 + uint160 recovered = 0; + for (uint256 i = len - 40; i < len; ++i) { + (bool isHex, uint8 value) = _tryHexToUint(bytes(description)[i]); + // If any of the characters is not a hex digit, ignore the suffix entirely + if (!isHex) { + return true; + } + recovered = (recovered << 4) | value; + } + + return recovered == uint160(proposer); + } + + /** + * @dev Try to parse a character from a string as a hex value. Returns `(true, value)` if the char is in + * `[0-9a-fA-F]` and `(false, 0)` otherwise. Value is guaranteed to be in the range `0 <= value < 16` + */ + function _tryHexToUint(bytes1 char) private pure returns (bool, uint8) { + uint8 c = uint8(char); + unchecked { + // Case 0-9 + if (47 < c && c < 58) { + return (true, c - 48); + } + // Case A-F + else if (64 < c && c < 71) { + return (true, c - 55); + } + // Case a-f + else if (96 < c && c < 103) { + return (true, c - 87); + } + // Else: not a hex char + else { + return (false, 0); + } + } + } } diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index eb3d1fc05..0adde6795 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -1,16 +1,17 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/IGovernor.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/IGovernor.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import "../utils/introspection/ERC165.sol"; +import "../interfaces/IERC165.sol"; +import "../interfaces/IERC6372.sol"; /** * @dev Interface of the {Governor} core. * * _Available since v4.3._ */ -abstract contract IGovernor is IERC165 { +abstract contract IGovernor is IERC165, IERC6372 { enum ProposalState { Pending, Active, @@ -22,6 +23,63 @@ abstract contract IGovernor is IERC165 { Executed } + /** + * @dev Empty proposal or a mismatch between the parameters length for a proposal call. + */ + error GovernorInvalidProposalLength(uint256 targets, uint256 calldatas, uint256 values); + + /** + * @dev The vote was already cast. + */ + error GovernorAlreadyCastVote(address voter); + + /** + * @dev Token deposits are disabled in this contract. + */ + error GovernorDisabledDeposit(); + + /** + * @dev The `account` is not a proposer. + */ + error GovernorOnlyProposer(address account); + + /** + * @dev The `account` is not the governance executor. + */ + error GovernorOnlyExecutor(address account); + + /** + * @dev The `proposalId` doesn't exist. + */ + error GovernorNonexistentProposal(uint256 proposalId); + + /** + * @dev The current state of a proposal is not the required for performing an operation. + * The `expectedStates` is a bitmap with the bits enabled for each ProposalState enum position + * counting from right to left. + * + * NOTE: If `expectedState` is `bytes32(0)`, the proposal is expected to not be in any state (i.e. not exist). + * This is the case when a proposal that is expected to be unset is already initiated (the proposal is duplicated). + * + * See {Governor-_encodeStateBitmap}. + */ + error GovernorUnexpectedProposalState(uint256 proposalId, ProposalState current, bytes32 expectedStates); + + /** + * @dev The voting period set is not a valid period. + */ + error GovernorInvalidVotingPeriod(uint256 votingPeriod); + + /** + * @dev The `proposer` does not have the required votes to operate on a proposal. + */ + error GovernorInsufficientProposerVotes(address proposer, uint256 votes, uint256 threshold); + + /** + * @dev The vote type used is not valid for the corresponding counting module. + */ + error GovernorInvalidVoteType(); + /** * @dev Emitted when a proposal is created. */ @@ -32,8 +90,8 @@ abstract contract IGovernor is IERC165 { uint256[] values, string[] signatures, bytes[] calldatas, - uint256 startBlock, - uint256 endBlock, + uint256 voteStart, + uint256 voteEnd, string description ); @@ -58,7 +116,7 @@ abstract contract IGovernor is IERC165 { * @dev Emitted when a vote is cast with params. * * Note: `support` values should be seen as buckets. Their interpretation depends on the voting module used. - * `params` are additional encoded parameters. Their intepepretation also depends on the voting module used. + * `params` are additional encoded parameters. Their interpepretation also depends on the voting module used. */ event VoteCastWithParams( address indexed voter, @@ -81,6 +139,19 @@ abstract contract IGovernor is IERC165 { */ function version() public view virtual returns (string memory); + /** + * @notice module:core + * @dev See {IERC6372} + */ + function clock() public view virtual returns (uint48); + + /** + * @notice module:core + * @dev See EIP-6372. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual returns (string memory); + /** * @notice module:voting * @dev A description of the possible `support` values for {castVote} and the way these votes are counted, meant to @@ -104,7 +175,7 @@ abstract contract IGovernor is IERC165 { * JavaScript class. */ // solhint-disable-next-line func-name-mixedcase - function COUNTING_MODE() public pure virtual returns (string memory); + function COUNTING_MODE() public view virtual returns (string memory); /** * @notice module:core @@ -125,32 +196,49 @@ abstract contract IGovernor is IERC165 { /** * @notice module:core - * @dev Block number used to retrieve user's votes and quorum. As per Compound's Comp and OpenZeppelin's - * ERC20Votes, the snapshot is performed at the end of this block. Hence, voting for this proposal starts at the - * beginning of the following block. + * @dev Timepoint used to retrieve user's votes and quorum. If using block number (as per Compound's Comp), the + * snapshot is performed at the end of this block. Hence, voting for this proposal starts at the beginning of the + * following block. */ function proposalSnapshot(uint256 proposalId) public view virtual returns (uint256); /** * @notice module:core - * @dev Block number at which votes close. Votes close at the end of this block, so it is possible to cast a vote - * during this block. + * @dev Timepoint at which votes close. If using block number, votes close at the end of this block, so it is + * possible to cast a vote during this block. */ function proposalDeadline(uint256 proposalId) public view virtual returns (uint256); + /** + * @notice module:core + * @dev The account that created a proposal. + */ + function proposalProposer(uint256 proposalId) public view virtual returns (address); + /** * @notice module:user-config - * @dev Delay, in number of block, between the proposal is created and the vote starts. This can be increassed to - * leave time for users to buy voting power, or delegate it, before the voting of a proposal starts. + * @dev Delay, between the proposal is created and the vote starts. The unit this duration is expressed in depends + * on the clock (see EIP-6372) this contract uses. + * + * This can be increased to leave time for users to buy voting power, or delegate it, before the voting of a + * proposal starts. + * + * NOTE: While this interface returns a uint256, timepoints are stored as uint48 following the ERC-6372 clock type. + * Consequently this value must fit in a uint48 (when added to the current clock). See {IERC6372-clock}. */ function votingDelay() public view virtual returns (uint256); /** * @notice module:user-config - * @dev Delay, in number of blocks, between the vote start and vote ends. + * @dev Delay between the vote start and vote end. The unit this duration is expressed in depends on the clock + * (see EIP-6372) this contract uses. * * NOTE: The {votingDelay} can delay the start of the vote. This must be considered when setting the voting * duration compared to the voting delay. + * + * NOTE: This value is stored when the proposal is submitted so that possible changes to the value do not affect + * proposals that have already been submitted. The type used to save it is a uint32. Consequently, while this + * interface returns a uint256, the value it returns should fit in a uint32. */ function votingPeriod() public view virtual returns (uint256); @@ -158,27 +246,27 @@ abstract contract IGovernor is IERC165 { * @notice module:user-config * @dev Minimum number of cast voted required for a proposal to be successful. * - * Note: The `blockNumber` parameter corresponds to the snapshot used for counting vote. This allows to scale the - * quorum depending on values such as the totalSupply of a token at this block (see {ERC20Votes}). + * NOTE: The `timepoint` parameter corresponds to the snapshot used for counting vote. This allows to scale the + * quorum depending on values such as the totalSupply of a token at this timepoint (see {ERC20Votes}). */ - function quorum(uint256 blockNumber) public view virtual returns (uint256); + function quorum(uint256 timepoint) public view virtual returns (uint256); /** * @notice module:reputation - * @dev Voting power of an `account` at a specific `blockNumber`. + * @dev Voting power of an `account` at a specific `timepoint`. * * Note: this can be implemented in a number of ways, for example by reading the delegated balance from one (or * multiple), {ERC20Votes} tokens. */ - function getVotes(address account, uint256 blockNumber) public view virtual returns (uint256); + function getVotes(address account, uint256 timepoint) public view virtual returns (uint256); /** * @notice module:reputation - * @dev Voting power of an `account` at a specific `blockNumber` given additional encoded parameters. + * @dev Voting power of an `account` at a specific `timepoint` given additional encoded parameters. */ function getVotesWithParams( address account, - uint256 blockNumber, + uint256 timepoint, bytes memory params ) public view virtual returns (uint256); @@ -189,8 +277,8 @@ abstract contract IGovernor is IERC165 { function hasVoted(uint256 proposalId, address account) public view virtual returns (bool); /** - * @dev Create a new proposal. Vote start {IGovernor-votingDelay} blocks after the proposal is created and ends - * {IGovernor-votingPeriod} blocks after the voting starts. + * @dev Create a new proposal. Vote start after a delay specified by {IGovernor-votingDelay} and lasts for a + * duration specified by {IGovernor-votingPeriod}. * * Emits a {ProposalCreated} event. */ @@ -216,6 +304,19 @@ abstract contract IGovernor is IERC165 { bytes32 descriptionHash ) public payable virtual returns (uint256 proposalId); + /** + * @dev Cancel a proposal. A proposal is cancellable by the proposer, but only while it is Pending state, i.e. + * before the vote starts. + * + * Emits a {ProposalCanceled} event. + */ + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public virtual returns (uint256 proposalId); + /** * @dev Cast a vote * diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index a9f23860d..00edfe23d 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -22,8 +22,6 @@ Votes modules determine the source of voting power, and sometimes quorum number. * {GovernorVotes}: Extracts voting weight from an {ERC20Votes}, or since v4.5 an {ERC721Votes} token. -* {GovernorVotesComp}: Extracts voting weight from a COMP-like or {ERC20VotesComp} token. - * {GovernorVotesQuorumFraction}: Combines with `GovernorVotes` to set the quorum as a fraction of the total token supply. Counting modules determine valid voting options. @@ -46,9 +44,9 @@ Other extensions can customize the behavior or interface in multiple ways. In addition to modules and extensions, the core contract requires a few virtual functions to be implemented to your particular specifications: -* <>: Delay (in number of blocks) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes. -* <>: Delay (in number of blocks) since the proposal starts until voting ends. -* <>: Quorum required for a proposal to be successful. This function includes a `blockNumber` argument so the quorum can adapt through time, for example, to follow a token's `totalSupply`. +* <>: Delay (in EIP-6372 clock) since the proposal is submitted until voting power is fixed and voting starts. This can be used to enforce a delay after a proposal is published for users to buy tokens, or delegate their votes. +* <>: Delay (in EIP-6372 clock) since the proposal starts until voting ends. +* <>: Quorum required for a proposal to be successful. This function includes a `timepoint` argument (see EIP-6372) so the quorum can adapt through time, for example, to follow a token's `totalSupply`. NOTE: Functions of the `Governor` contract do not include access control. If you want to restrict access, you should add these checks by overloading the particular functions. Among these, {Governor-_cancel} is internal by default, and you will have to expose it (with the right access control mechanism) yourself if this function is needed. @@ -66,8 +64,6 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorVotesQuorumFraction}} -{{GovernorVotesComp}} - === Extensions {{GovernorTimelockControl}} @@ -80,10 +76,6 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorCompatibilityBravo}} -=== Deprecated - -{{GovernorProposalThreshold}} - == Utils {{Votes}} diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index 5073217e3..a25cd7b4a 100644 --- a/contracts/governance/TimelockController.sol +++ b/contracts/governance/TimelockController.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/TimelockController.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/TimelockController.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../access/AccessControl.sol"; -import "../token/ERC721/IERC721Receiver.sol"; -import "../token/ERC1155/IERC1155Receiver.sol"; +import "../token/ERC721/utils/ERC721Holder.sol"; +import "../token/ERC1155/utils/ERC1155Holder.sol"; import "../utils/Address.sol"; /** @@ -23,7 +23,7 @@ import "../utils/Address.sol"; * * _Available since v3.3._ */ -contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver { +contract TimelockController is AccessControl, ERC721Holder, ERC1155Holder { bytes32 public constant PROPOSER_ROLE = keccak256("PROPOSER_ROLE"); bytes32 public constant EXECUTOR_ROLE = keccak256("EXECUTOR_ROLE"); bytes32 public constant CANCELLER_ROLE = keccak256("CANCELLER_ROLE"); @@ -32,6 +32,38 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver mapping(bytes32 => uint256) private _timestamps; uint256 private _minDelay; + enum OperationState { + Unset, + Pending, + Ready, + Done + } + + /** + * @dev Mismatch between the parameters length for an operation call. + */ + error TimelockInvalidOperationLength(uint256 targets, uint256 payloads, uint256 values); + + /** + * @dev The schedule operation doesn't meet the minimum delay. + */ + error TimelockInsufficientDelay(uint256 delay, uint256 minDelay); + + /** + * @dev The current state of an operation is not as required. + */ + error TimelockUnexpectedOperationState(bytes32 operationId, OperationState expected); + + /** + * @dev The predecessor to an operation not yet done. + */ + error TimelockUnexecutedPredecessor(bytes32 predecessorId); + + /** + * @dev The caller account is not authorized. + */ + error TimelockUnauthorizedCaller(address caller); + /** * @dev Emitted when a call is scheduled as part of operation `id`. */ @@ -50,6 +82,11 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver */ event CallExecuted(bytes32 indexed id, uint256 indexed index, address target, uint256 value, bytes data); + /** + * @dev Emitted when new proposal is scheduled with non-zero salt. + */ + event CallSalt(bytes32 indexed id, bytes32 salt); + /** * @dev Emitted when operation `id` is cancelled. */ @@ -84,13 +121,13 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver // register proposers and cancellers for (uint256 i = 0; i < proposers.length; ++i) { - _setupRole(PROPOSER_ROLE, proposers[i]); - _setupRole(CANCELLER_ROLE, proposers[i]); + _grantRole(PROPOSER_ROLE, proposers[i]); + _grantRole(CANCELLER_ROLE, proposers[i]); } // register executors for (uint256 i = 0; i < executors.length; ++i) { - _setupRole(EXECUTOR_ROLE, executors[i]); + _grantRole(EXECUTOR_ROLE, executors[i]); } _minDelay = minDelay; @@ -118,29 +155,31 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver /** * @dev See {IERC165-supportsInterface}. */ - function supportsInterface(bytes4 interfaceId) public view virtual override(IERC165, AccessControl) returns (bool) { - return interfaceId == type(IERC1155Receiver).interfaceId || super.supportsInterface(interfaceId); + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(AccessControl, ERC1155Receiver) returns (bool) { + return super.supportsInterface(interfaceId); } /** * @dev Returns whether an id correspond to a registered operation. This * includes both Pending, Ready and Done operations. */ - function isOperation(bytes32 id) public view virtual returns (bool registered) { + function isOperation(bytes32 id) public view virtual returns (bool) { return getTimestamp(id) > 0; } /** - * @dev Returns whether an operation is pending or not. + * @dev Returns whether an operation is pending or not. Note that a "pending" operation may also be "ready". */ - function isOperationPending(bytes32 id) public view virtual returns (bool pending) { + function isOperationPending(bytes32 id) public view virtual returns (bool) { return getTimestamp(id) > _DONE_TIMESTAMP; } /** - * @dev Returns whether an operation is ready or not. + * @dev Returns whether an operation is ready for execution. Note that a "ready" operation is also "pending". */ - function isOperationReady(bytes32 id) public view virtual returns (bool ready) { + function isOperationReady(bytes32 id) public view virtual returns (bool) { uint256 timestamp = getTimestamp(id); return timestamp > _DONE_TIMESTAMP && timestamp <= block.timestamp; } @@ -148,7 +187,7 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver /** * @dev Returns whether an operation is done or not. */ - function isOperationDone(bytes32 id) public view virtual returns (bool done) { + function isOperationDone(bytes32 id) public view virtual returns (bool) { return getTimestamp(id) == _DONE_TIMESTAMP; } @@ -156,7 +195,7 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * @dev Returns the timestamp at which an operation becomes ready (0 for * unset operations, 1 for done operations). */ - function getTimestamp(bytes32 id) public view virtual returns (uint256 timestamp) { + function getTimestamp(bytes32 id) public view virtual returns (uint256) { return _timestamps[id]; } @@ -165,7 +204,7 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * * This value can be changed by executing an operation that calls `updateDelay`. */ - function getMinDelay() public view virtual returns (uint256 duration) { + function getMinDelay() public view virtual returns (uint256) { return _minDelay; } @@ -179,7 +218,7 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver bytes calldata data, bytes32 predecessor, bytes32 salt - ) public pure virtual returns (bytes32 hash) { + ) public pure virtual returns (bytes32) { return keccak256(abi.encode(target, value, data, predecessor, salt)); } @@ -193,14 +232,14 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver bytes[] calldata payloads, bytes32 predecessor, bytes32 salt - ) public pure virtual returns (bytes32 hash) { + ) public pure virtual returns (bytes32) { return keccak256(abi.encode(targets, values, payloads, predecessor, salt)); } /** * @dev Schedule an operation containing a single transaction. * - * Emits a {CallScheduled} event. + * Emits {CallSalt} if salt is nonzero, and {CallScheduled}. * * Requirements: * @@ -217,12 +256,15 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver bytes32 id = hashOperation(target, value, data, predecessor, salt); _schedule(id, delay); emit CallScheduled(id, 0, target, value, data, predecessor, delay); + if (salt != bytes32(0)) { + emit CallSalt(id, salt); + } } /** * @dev Schedule an operation containing a batch of transactions. * - * Emits one {CallScheduled} event per transaction in the batch. + * Emits {CallSalt} if salt is nonzero, and one {CallScheduled} event per transaction in the batch. * * Requirements: * @@ -236,22 +278,31 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver bytes32 salt, uint256 delay ) public virtual onlyRole(PROPOSER_ROLE) { - require(targets.length == values.length, "TimelockController: length mismatch"); - require(targets.length == payloads.length, "TimelockController: length mismatch"); + if (targets.length != values.length || targets.length != payloads.length) { + revert TimelockInvalidOperationLength(targets.length, payloads.length, values.length); + } bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt); _schedule(id, delay); for (uint256 i = 0; i < targets.length; ++i) { emit CallScheduled(id, i, targets[i], values[i], payloads[i], predecessor, delay); } + if (salt != bytes32(0)) { + emit CallSalt(id, salt); + } } /** * @dev Schedule an operation that is to become valid after a given delay. */ function _schedule(bytes32 id, uint256 delay) private { - require(!isOperation(id), "TimelockController: operation already scheduled"); - require(delay >= getMinDelay(), "TimelockController: insufficient delay"); + if (isOperation(id)) { + revert TimelockUnexpectedOperationState(id, OperationState.Unset); + } + uint256 minDelay = getMinDelay(); + if (delay < minDelay) { + revert TimelockInsufficientDelay(delay, minDelay); + } _timestamps[id] = block.timestamp + delay; } @@ -263,7 +314,9 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * - the caller must have the 'canceller' role. */ function cancel(bytes32 id) public virtual onlyRole(CANCELLER_ROLE) { - require(isOperationPending(id), "TimelockController: operation cannot be cancelled"); + if (!isOperationPending(id)) { + revert TimelockUnexpectedOperationState(id, OperationState.Pending); + } delete _timestamps[id]; emit Cancelled(id); @@ -315,8 +368,9 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver bytes32 predecessor, bytes32 salt ) public payable virtual onlyRoleOrOpenRole(EXECUTOR_ROLE) { - require(targets.length == values.length, "TimelockController: length mismatch"); - require(targets.length == payloads.length, "TimelockController: length mismatch"); + if (targets.length != values.length || targets.length != payloads.length) { + revert TimelockInvalidOperationLength(targets.length, payloads.length, values.length); + } bytes32 id = hashOperationBatch(targets, values, payloads, predecessor, salt); @@ -335,23 +389,29 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * @dev Execute an operation's call. */ function _execute(address target, uint256 value, bytes calldata data) internal virtual { - (bool success, ) = target.call{value: value}(data); - require(success, "TimelockController: underlying transaction reverted"); + (bool success, bytes memory returndata) = target.call{value: value}(data); + Address.verifyCallResult(success, returndata); } /** * @dev Checks before execution of an operation's calls. */ function _beforeCall(bytes32 id, bytes32 predecessor) private view { - require(isOperationReady(id), "TimelockController: operation is not ready"); - require(predecessor == bytes32(0) || isOperationDone(predecessor), "TimelockController: missing dependency"); + if (!isOperationReady(id)) { + revert TimelockUnexpectedOperationState(id, OperationState.Ready); + } + if (predecessor != bytes32(0) && !isOperationDone(predecessor)) { + revert TimelockUnexecutedPredecessor(predecessor); + } } /** * @dev Checks after execution of an operation's calls. */ function _afterCall(bytes32 id) private { - require(isOperationReady(id), "TimelockController: operation is not ready"); + if (!isOperationReady(id)) { + revert TimelockUnexpectedOperationState(id, OperationState.Ready); + } _timestamps[id] = _DONE_TIMESTAMP; } @@ -366,41 +426,10 @@ contract TimelockController is AccessControl, IERC721Receiver, IERC1155Receiver * an operation where the timelock is the target and the data is the ABI-encoded call to this function. */ function updateDelay(uint256 newDelay) external virtual { - require(msg.sender == address(this), "TimelockController: caller must be timelock"); + if (msg.sender != address(this)) { + revert TimelockUnauthorizedCaller(msg.sender); + } emit MinDelayChange(_minDelay, newDelay); _minDelay = newDelay; } - - /** - * @dev See {IERC721Receiver-onERC721Received}. - */ - function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { - return this.onERC721Received.selector; - } - - /** - * @dev See {IERC1155Receiver-onERC1155Received}. - */ - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes memory - ) public virtual override returns (bytes4) { - return this.onERC1155Received.selector; - } - - /** - * @dev See {IERC1155Receiver-onERC1155BatchReceived}. - */ - function onERC1155BatchReceived( - address, - address, - uint256[] memory, - uint256[] memory, - bytes memory - ) public virtual override returns (bytes4) { - return this.onERC1155BatchReceived.selector; - } } diff --git a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol index 8d74742c5..8d6d9ccb4 100644 --- a/contracts/governance/compatibility/GovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/GovernorCompatibilityBravo.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/compatibility/GovernorCompatibilityBravo.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/compatibility/GovernorCompatibilityBravo.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/math/SafeCast.sol"; import "../extensions/IGovernorTimelock.sol"; @@ -26,7 +26,6 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp } struct ProposalDetails { - address proposer; address[] targets; uint256[] values; string[] signatures; @@ -55,7 +54,8 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp bytes[] memory calldatas, string memory description ) public virtual override(IGovernor, Governor) returns (uint256) { - _storeProposal(_msgSender(), targets, values, new string[](calldatas.length), calldatas, description); + // Stores the proposal details (if not already present) and executes the propose logic from the core. + _storeProposal(targets, values, new string[](calldatas.length), calldatas, description); return super.propose(targets, values, calldatas, description); } @@ -69,7 +69,14 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp bytes[] memory calldatas, string memory description ) public virtual override returns (uint256) { - _storeProposal(_msgSender(), targets, values, signatures, calldatas, description); + if (signatures.length != calldatas.length) { + revert GovernorInvalidSignaturesLength(signatures.length, calldatas.length); + } + // Stores the full proposal and fallback to the public (possibly overridden) propose. The fallback is done + // after the full proposal is stored, so the store operation included in the fallback will be skipped. Here we + // call `propose` and not `super.propose` to make sure if a child contract override `propose`, whatever code + // is added there is also executed when calling this alternative interface. + _storeProposal(targets, values, signatures, calldatas, description); return propose(targets, values, _encodeCalldata(signatures, calldatas), description); } @@ -77,42 +84,64 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp * @dev See {IGovernorCompatibilityBravo-queue}. */ function queue(uint256 proposalId) public virtual override { - ProposalDetails storage details = _proposalDetails[proposalId]; - queue( - details.targets, - details.values, - _encodeCalldata(details.signatures, details.calldatas), - details.descriptionHash - ); + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) = _getProposalParameters(proposalId); + + queue(targets, values, calldatas, descriptionHash); } /** * @dev See {IGovernorCompatibilityBravo-execute}. */ function execute(uint256 proposalId) public payable virtual override { - ProposalDetails storage details = _proposalDetails[proposalId]; - execute( - details.targets, - details.values, - _encodeCalldata(details.signatures, details.calldatas), - details.descriptionHash - ); + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) = _getProposalParameters(proposalId); + + execute(targets, values, calldatas, descriptionHash); } + /** + * @dev Cancel a proposal with GovernorBravo logic. + */ function cancel(uint256 proposalId) public virtual override { - ProposalDetails storage details = _proposalDetails[proposalId]; + ( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) = _getProposalParameters(proposalId); - require( - _msgSender() == details.proposer || getVotes(details.proposer, block.number - 1) < proposalThreshold(), - "GovernorBravo: proposer above threshold" - ); + cancel(targets, values, calldatas, descriptionHash); + } - _cancel( - details.targets, - details.values, - _encodeCalldata(details.signatures, details.calldatas), - details.descriptionHash - ); + /** + * @dev Cancel a proposal with GovernorBravo logic. At any moment a proposal can be cancelled, either by the + * proposer, or by third parties if the proposer's voting power has dropped below the proposal threshold. + */ + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public virtual override(IGovernor, Governor) returns (uint256) { + uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); + address proposer = proposalProposer(proposalId); + + uint256 proposerVotes = getVotes(proposer, clock() - 1); + uint256 votesThreshold = proposalThreshold(); + if (_msgSender() != proposer && proposerVotes >= votesThreshold) { + revert GovernorInsufficientProposerVotes(proposer, proposerVotes, votesThreshold); + } + + return _cancel(targets, values, calldatas, descriptionHash); } /** @@ -123,21 +152,38 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp bytes[] memory calldatas ) private pure returns (bytes[] memory) { bytes[] memory fullcalldatas = new bytes[](calldatas.length); - - for (uint256 i = 0; i < signatures.length; ++i) { + for (uint256 i = 0; i < fullcalldatas.length; ++i) { fullcalldatas[i] = bytes(signatures[i]).length == 0 ? calldatas[i] - : abi.encodePacked(bytes4(keccak256(bytes(signatures[i]))), calldatas[i]); + : bytes.concat(abi.encodeWithSignature(signatures[i]), calldatas[i]); } return fullcalldatas; } /** - * @dev Store proposal metadata for later lookup + * @dev Retrieve proposal parameters by id, with fully encoded calldatas. + */ + function _getProposalParameters( + uint256 proposalId + ) + private + view + returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) + { + ProposalDetails storage details = _proposalDetails[proposalId]; + return ( + details.targets, + details.values, + _encodeCalldata(details.signatures, details.calldatas), + details.descriptionHash + ); + } + + /** + * @dev Store proposal metadata (if not already present) for later lookup. */ function _storeProposal( - address proposer, address[] memory targets, uint256[] memory values, string[] memory signatures, @@ -149,7 +195,6 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp ProposalDetails storage details = _proposalDetails[proposalId]; if (details.descriptionHash == bytes32(0)) { - details.proposer = proposer; details.targets = targets; details.values = values; details.signatures = signatures; @@ -183,19 +228,19 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp ) { id = proposalId; + proposer = proposalProposer(proposalId); eta = proposalEta(proposalId); startBlock = proposalSnapshot(proposalId); endBlock = proposalDeadline(proposalId); ProposalDetails storage details = _proposalDetails[proposalId]; - proposer = details.proposer; forVotes = details.forVotes; againstVotes = details.againstVotes; abstainVotes = details.abstainVotes; - ProposalState status = state(proposalId); - canceled = status == ProposalState.Canceled; - executed = status == ProposalState.Executed; + ProposalState currentState = state(proposalId); + canceled = currentState == ProposalState.Canceled; + executed = currentState == ProposalState.Executed; } /** @@ -230,7 +275,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp * @dev See {IGovernorCompatibilityBravo-quorumVotes}. */ function quorumVotes() public view virtual override returns (uint256) { - return quorum(block.number - 1); + return quorum(clock() - 1); } // ==================================================== Voting ==================================================== @@ -270,7 +315,9 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp ProposalDetails storage details = _proposalDetails[proposalId]; Receipt storage receipt = details.receipts[account]; - require(!receipt.hasVoted, "GovernorCompatibilityBravo: vote already cast"); + if (receipt.hasVoted) { + revert GovernorAlreadyCastVote(account); + } receipt.hasVoted = true; receipt.support = support; receipt.votes = SafeCast.toUint96(weight); @@ -282,7 +329,7 @@ abstract contract GovernorCompatibilityBravo is IGovernorTimelock, IGovernorComp } else if (support == uint8(VoteType.Abstain)) { details.abstainVotes += weight; } else { - revert("GovernorCompatibilityBravo: invalid vote type"); + revert GovernorInvalidVoteType(); } } } diff --git a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol index d1ec0337d..197936ab9 100644 --- a/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol +++ b/contracts/governance/compatibility/IGovernorCompatibilityBravo.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (governance/compatibility/IGovernorCompatibilityBravo.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/compatibility/IGovernorCompatibilityBravo.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IGovernor.sol"; @@ -11,6 +11,11 @@ import "../IGovernor.sol"; * _Available since v4.3._ */ abstract contract IGovernorCompatibilityBravo is IGovernor { + /** + * @dev Mismatch between the parameters length for a proposal call. + */ + error GovernorInvalidSignaturesLength(uint256 signatures, uint256 calldatas); + /** * @dev Proposal structure from Compound Governor Bravo. Not actually used by the compatibility layer, as * {{proposal}} returns a very different structure. @@ -91,7 +96,7 @@ abstract contract IGovernorCompatibilityBravo is IGovernor { function execute(uint256 proposalId) public payable virtual; /** - * @dev Cancels a proposal only if sender is the proposer, or proposer delegates dropped below proposal threshold. + * @dev Cancels a proposal only if the sender is the proposer or the proposer delegates' voting power dropped below the proposal threshold. */ function cancel(uint256 proposalId) public virtual; diff --git a/contracts/governance/extensions/GovernorCountingSimple.sol b/contracts/governance/extensions/GovernorCountingSimple.sol index f3eea9d7f..315f4ad45 100644 --- a/contracts/governance/extensions/GovernorCountingSimple.sol +++ b/contracts/governance/extensions/GovernorCountingSimple.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/extensions/GovernorCountingSimple.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorCountingSimple.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Governor.sol"; @@ -84,7 +84,9 @@ abstract contract GovernorCountingSimple is Governor { ) internal virtual override { ProposalVote storage proposalVote = _proposalVotes[proposalId]; - require(!proposalVote.hasVoted[account], "GovernorVotingSimple: vote already cast"); + if (proposalVote.hasVoted[account]) { + revert GovernorAlreadyCastVote(account); + } proposalVote.hasVoted[account] = true; if (support == uint8(VoteType.Against)) { @@ -94,7 +96,7 @@ abstract contract GovernorCountingSimple is Governor { } else if (support == uint8(VoteType.Abstain)) { proposalVote.abstainVotes += weight; } else { - revert("GovernorVotingSimple: invalid value for enum VoteType"); + revert GovernorInvalidVoteType(); } } } diff --git a/contracts/governance/extensions/GovernorPreventLateQuorum.sol b/contracts/governance/extensions/GovernorPreventLateQuorum.sol index a26bbf059..3e730174e 100644 --- a/contracts/governance/extensions/GovernorPreventLateQuorum.sol +++ b/contracts/governance/extensions/GovernorPreventLateQuorum.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (governance/extensions/GovernorPreventLateQuorum.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorPreventLateQuorum.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Governor.sol"; import "../../utils/math/Math.sol"; @@ -12,17 +12,16 @@ import "../../utils/math/Math.sol"; * and try to oppose the decision. * * If a vote causes quorum to be reached, the proposal's voting period may be extended so that it does not end before at - * least a given number of blocks have passed (the "vote extension" parameter). This parameter can be set by the - * governance executor (e.g. through a governance proposal). + * least a specified time has passed (the "vote extension" parameter). This parameter can be set through a governance + * proposal. * * _Available since v4.5._ */ abstract contract GovernorPreventLateQuorum is Governor { - using SafeCast for uint256; - using Timers for Timers.BlockNumber; + uint48 private _voteExtension; - uint64 private _voteExtension; - mapping(uint256 => Timers.BlockNumber) private _extendedDeadlines; + /// @custom:oz-retyped-from mapping(uint256 => Timers.BlockNumber) + mapping(uint256 => uint48) private _extendedDeadlines; /// @dev Emitted when a proposal deadline is pushed back due to reaching quorum late in its voting period. event ProposalExtended(uint256 indexed proposalId, uint64 extendedDeadline); @@ -31,11 +30,11 @@ abstract contract GovernorPreventLateQuorum is Governor { event LateQuorumVoteExtensionSet(uint64 oldVoteExtension, uint64 newVoteExtension); /** - * @dev Initializes the vote extension parameter: the number of blocks that are required to pass since a proposal - * reaches quorum until its voting period ends. If necessary the voting period will be extended beyond the one set - * at proposal creation. + * @dev Initializes the vote extension parameter: the time in either number of blocks or seconds (depending on the governor + * clock mode) that is required to pass since the moment a proposal reaches quorum until its voting period ends. If + * necessary the voting period will be extended beyond the one set during proposal creation. */ - constructor(uint64 initialVoteExtension) { + constructor(uint48 initialVoteExtension) { _setLateQuorumVoteExtension(initialVoteExtension); } @@ -44,7 +43,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * proposal reached quorum late in the voting period. See {Governor-proposalDeadline}. */ function proposalDeadline(uint256 proposalId) public view virtual override returns (uint256) { - return Math.max(super.proposalDeadline(proposalId), _extendedDeadlines[proposalId].getDeadline()); + return Math.max(super.proposalDeadline(proposalId), _extendedDeadlines[proposalId]); } /** @@ -62,16 +61,14 @@ abstract contract GovernorPreventLateQuorum is Governor { ) internal virtual override returns (uint256) { uint256 result = super._castVote(proposalId, account, support, reason, params); - Timers.BlockNumber storage extendedDeadline = _extendedDeadlines[proposalId]; + if (_extendedDeadlines[proposalId] == 0 && _quorumReached(proposalId)) { + uint48 extendedDeadline = clock() + lateQuorumVoteExtension(); - if (extendedDeadline.isUnset() && _quorumReached(proposalId)) { - uint64 extendedDeadlineValue = block.number.toUint64() + lateQuorumVoteExtension(); - - if (extendedDeadlineValue > proposalDeadline(proposalId)) { - emit ProposalExtended(proposalId, extendedDeadlineValue); + if (extendedDeadline > proposalDeadline(proposalId)) { + emit ProposalExtended(proposalId, extendedDeadline); } - extendedDeadline.setDeadline(extendedDeadlineValue); + _extendedDeadlines[proposalId] = extendedDeadline; } return result; @@ -81,7 +78,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * @dev Returns the current value of the vote extension parameter: the number of blocks that are required to pass * from the time a proposal reaches quorum until its voting period ends. */ - function lateQuorumVoteExtension() public view virtual returns (uint64) { + function lateQuorumVoteExtension() public view virtual returns (uint48) { return _voteExtension; } @@ -91,7 +88,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * * Emits a {LateQuorumVoteExtensionSet} event. */ - function setLateQuorumVoteExtension(uint64 newVoteExtension) public virtual onlyGovernance { + function setLateQuorumVoteExtension(uint48 newVoteExtension) public virtual onlyGovernance { _setLateQuorumVoteExtension(newVoteExtension); } @@ -101,7 +98,7 @@ abstract contract GovernorPreventLateQuorum is Governor { * * Emits a {LateQuorumVoteExtensionSet} event. */ - function _setLateQuorumVoteExtension(uint64 newVoteExtension) internal virtual { + function _setLateQuorumVoteExtension(uint48 newVoteExtension) internal virtual { emit LateQuorumVoteExtensionSet(_voteExtension, newVoteExtension); _voteExtension = newVoteExtension; } diff --git a/contracts/governance/extensions/GovernorProposalThreshold.sol b/contracts/governance/extensions/GovernorProposalThreshold.sol deleted file mode 100644 index 3feebace0..000000000 --- a/contracts/governance/extensions/GovernorProposalThreshold.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (governance/extensions/GovernorProposalThreshold.sol) - -pragma solidity ^0.8.0; - -import "../Governor.sol"; - -/** - * @dev Extension of {Governor} for proposal restriction to token holders with a minimum balance. - * - * _Available since v4.3._ - * _Deprecated since v4.4._ - */ -abstract contract GovernorProposalThreshold is Governor { - function propose( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - string memory description - ) public virtual override returns (uint256) { - return super.propose(targets, values, calldatas, description); - } -} diff --git a/contracts/governance/extensions/GovernorSettings.sol b/contracts/governance/extensions/GovernorSettings.sol index 527f41cd8..6168689ad 100644 --- a/contracts/governance/extensions/GovernorSettings.sol +++ b/contracts/governance/extensions/GovernorSettings.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (governance/extensions/GovernorSettings.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorSettings.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Governor.sol"; @@ -11,9 +11,12 @@ import "../Governor.sol"; * _Available since v4.4._ */ abstract contract GovernorSettings is Governor { - uint256 private _votingDelay; - uint256 private _votingPeriod; + // amount of token uint256 private _proposalThreshold; + // timepoint: limited to uint48 in core (same as clock() type) + uint48 private _votingDelay; + // duration: limited to uint32 in core + uint32 private _votingPeriod; event VotingDelaySet(uint256 oldVotingDelay, uint256 newVotingDelay); event VotingPeriodSet(uint256 oldVotingPeriod, uint256 newVotingPeriod); @@ -22,7 +25,7 @@ abstract contract GovernorSettings is Governor { /** * @dev Initialize the governance parameters. */ - constructor(uint256 initialVotingDelay, uint256 initialVotingPeriod, uint256 initialProposalThreshold) { + constructor(uint48 initialVotingDelay, uint32 initialVotingPeriod, uint256 initialProposalThreshold) { _setVotingDelay(initialVotingDelay); _setVotingPeriod(initialVotingPeriod); _setProposalThreshold(initialProposalThreshold); @@ -54,7 +57,7 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingDelaySet} event. */ - function setVotingDelay(uint256 newVotingDelay) public virtual onlyGovernance { + function setVotingDelay(uint48 newVotingDelay) public virtual onlyGovernance { _setVotingDelay(newVotingDelay); } @@ -63,7 +66,7 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingPeriodSet} event. */ - function setVotingPeriod(uint256 newVotingPeriod) public virtual onlyGovernance { + function setVotingPeriod(uint32 newVotingPeriod) public virtual onlyGovernance { _setVotingPeriod(newVotingPeriod); } @@ -81,7 +84,7 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingDelaySet} event. */ - function _setVotingDelay(uint256 newVotingDelay) internal virtual { + function _setVotingDelay(uint48 newVotingDelay) internal virtual { emit VotingDelaySet(_votingDelay, newVotingDelay); _votingDelay = newVotingDelay; } @@ -91,9 +94,11 @@ abstract contract GovernorSettings is Governor { * * Emits a {VotingPeriodSet} event. */ - function _setVotingPeriod(uint256 newVotingPeriod) internal virtual { + function _setVotingPeriod(uint32 newVotingPeriod) internal virtual { // voting period must be at least one block long - require(newVotingPeriod > 0, "GovernorSettings: voting period too low"); + if (newVotingPeriod == 0) { + revert GovernorInvalidVotingPeriod(0); + } emit VotingPeriodSet(_votingPeriod, newVotingPeriod); _votingPeriod = newVotingPeriod; } diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol index 2fa539ead..ed4d916a6 100644 --- a/contracts/governance/extensions/GovernorTimelockCompound.sol +++ b/contracts/governance/extensions/GovernorTimelockCompound.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (governance/extensions/GovernorTimelockCompound.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorTimelockCompound.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IGovernorTimelock.sol"; import "../Governor.sol"; @@ -21,16 +21,10 @@ import "../../vendor/compound/ICompoundTimelock.sol"; * _Available since v4.3._ */ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { - using SafeCast for uint256; - using Timers for Timers.Timestamp; - - struct ProposalTimelock { - Timers.Timestamp timer; - } - ICompoundTimelock private _timelock; - mapping(uint256 => ProposalTimelock) private _proposalTimelocks; + /// @custom:oz-retyped-from mapping(uint256 => GovernorTimelockCompound.ProposalTimelock) + mapping(uint256 => uint256) private _proposalTimelocks; /** * @dev Emitted when the timelock controller used for proposal execution is modified. @@ -52,18 +46,18 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { } /** - * @dev Overridden version of the {Governor-state} function with added support for the `Queued` and `Expired` status. + * @dev Overridden version of the {Governor-state} function with added support for the `Queued` and `Expired` state. */ function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) { - ProposalState status = super.state(proposalId); + ProposalState currentState = super.state(proposalId); - if (status != ProposalState.Succeeded) { - return status; + if (currentState != ProposalState.Succeeded) { + return currentState; } uint256 eta = proposalEta(proposalId); if (eta == 0) { - return status; + return currentState; } else if (block.timestamp >= eta + _timelock.GRACE_PERIOD()) { return ProposalState.Expired; } else { @@ -82,7 +76,7 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { * @dev Public accessor to check the eta of a queued proposal */ function proposalEta(uint256 proposalId) public view virtual override returns (uint256) { - return _proposalTimelocks[proposalId].timer.getDeadline(); + return _proposalTimelocks[proposalId]; } /** @@ -96,15 +90,22 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { ) public virtual override returns (uint256) { uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful"); + ProposalState currentState = state(proposalId); + if (currentState != ProposalState.Succeeded) { + revert GovernorUnexpectedProposalState( + proposalId, + currentState, + _encodeStateBitmap(ProposalState.Succeeded) + ); + } uint256 eta = block.timestamp + _timelock.delay(); - _proposalTimelocks[proposalId].timer.setDeadline(eta.toUint64()); + _proposalTimelocks[proposalId] = eta; + for (uint256 i = 0; i < targets.length; ++i) { - require( - !_timelock.queuedTransactions(keccak256(abi.encode(targets[i], values[i], "", calldatas[i], eta))), - "GovernorTimelockCompound: identical proposal action already queued" - ); + if (_timelock.queuedTransactions(keccak256(abi.encode(targets[i], values[i], "", calldatas[i], eta)))) { + revert GovernorAlreadyQueuedProposal(proposalId); + } _timelock.queueTransaction(targets[i], values[i], "", calldatas[i], eta); } @@ -124,7 +125,9 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { bytes32 /*descriptionHash*/ ) internal virtual override { uint256 eta = proposalEta(proposalId); - require(eta > 0, "GovernorTimelockCompound: proposal not yet queued"); + if (eta == 0) { + revert GovernorNotQueuedProposal(proposalId); + } Address.sendValue(payable(_timelock), msg.value); for (uint256 i = 0; i < targets.length; ++i) { _timelock.executeTransaction(targets[i], values[i], "", calldatas[i], eta); @@ -145,10 +148,12 @@ abstract contract GovernorTimelockCompound is IGovernorTimelock, Governor { uint256 eta = proposalEta(proposalId); if (eta > 0) { + // update state first + delete _proposalTimelocks[proposalId]; + // do external call later for (uint256 i = 0; i < targets.length; ++i) { _timelock.cancelTransaction(targets[i], values[i], "", calldatas[i], eta); } - _proposalTimelocks[proposalId].timer.reset(); } return proposalId; diff --git a/contracts/governance/extensions/GovernorTimelockControl.sol b/contracts/governance/extensions/GovernorTimelockControl.sol index 6aa2556ab..fadbcc701 100644 --- a/contracts/governance/extensions/GovernorTimelockControl.sol +++ b/contracts/governance/extensions/GovernorTimelockControl.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (governance/extensions/GovernorTimelockControl.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorTimelockControl.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IGovernorTimelock.sol"; import "../Governor.sol"; @@ -47,24 +47,26 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { } /** - * @dev Overridden version of the {Governor-state} function with added support for the `Queued` status. + * @dev Overridden version of the {Governor-state} function with added support for the `Queued` state. */ function state(uint256 proposalId) public view virtual override(IGovernor, Governor) returns (ProposalState) { - ProposalState status = super.state(proposalId); + ProposalState currentState = super.state(proposalId); - if (status != ProposalState.Succeeded) { - return status; + if (currentState != ProposalState.Succeeded) { + return currentState; } // core tracks execution, so we just have to check if successful proposal have been queued. bytes32 queueid = _timelockIds[proposalId]; if (queueid == bytes32(0)) { - return status; - } else if (_timelock.isOperationDone(queueid)) { - return ProposalState.Executed; + return currentState; } else if (_timelock.isOperationPending(queueid)) { return ProposalState.Queued; + } else if (_timelock.isOperationDone(queueid)) { + // This can happen if the proposal is executed directly on the timelock. + return ProposalState.Executed; } else { + // This can happen if the proposal is canceled directly on the timelock. return ProposalState.Canceled; } } @@ -95,7 +97,14 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { ) public virtual override returns (uint256) { uint256 proposalId = hashProposal(targets, values, calldatas, descriptionHash); - require(state(proposalId) == ProposalState.Succeeded, "Governor: proposal not successful"); + ProposalState currentState = state(proposalId); + if (currentState != ProposalState.Succeeded) { + revert GovernorUnexpectedProposalState( + proposalId, + currentState, + _encodeStateBitmap(ProposalState.Succeeded) + ); + } uint256 delay = _timelock.getMinDelay(); _timelockIds[proposalId] = _timelock.hashOperationBatch(targets, values, calldatas, 0, descriptionHash); @@ -110,13 +119,16 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { * @dev Overridden execute function that run the already queued proposal through the timelock. */ function _execute( - uint256 /* proposalId */, + uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash ) internal virtual override { + // execute _timelock.executeBatch{value: msg.value}(targets, values, calldatas, 0, descriptionHash); + // cleanup for refund + delete _timelockIds[proposalId]; } /** @@ -133,9 +145,12 @@ abstract contract GovernorTimelockControl is IGovernorTimelock, Governor { bytes32 descriptionHash ) internal virtual override returns (uint256) { uint256 proposalId = super._cancel(targets, values, calldatas, descriptionHash); + bytes32 timelockId = _timelockIds[proposalId]; - if (_timelockIds[proposalId] != 0) { - _timelock.cancel(_timelockIds[proposalId]); + if (timelockId != 0) { + // cancel + _timelock.cancel(timelockId); + // cleanup delete _timelockIds[proposalId]; } diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index b328c25d1..5d8318f46 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (governance/extensions/GovernorVotes.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorVotes.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Governor.sol"; -import "../utils/IVotes.sol"; +import "../../interfaces/IERC5805.sol"; /** * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} token. @@ -12,10 +12,34 @@ import "../utils/IVotes.sol"; * _Available since v4.3._ */ abstract contract GovernorVotes is Governor { - IVotes public immutable token; + IERC5805 public immutable token; constructor(IVotes tokenAddress) { - token = tokenAddress; + token = IERC5805(address(tokenAddress)); + } + + /** + * @dev Clock (as specified in EIP-6372) is set to match the token's clock. Fallback to block numbers if the token + * does not implement EIP-6372. + */ + function clock() public view virtual override returns (uint48) { + try token.clock() returns (uint48 timepoint) { + return timepoint; + } catch { + return SafeCast.toUint48(block.number); + } + } + + /** + * @dev Machine-readable description of the clock as specified in EIP-6372. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + try token.CLOCK_MODE() returns (string memory clockmode) { + return clockmode; + } catch { + return "mode=blocknumber&from=default"; + } } /** @@ -23,9 +47,9 @@ abstract contract GovernorVotes is Governor { */ function _getVotes( address account, - uint256 blockNumber, + uint256 timepoint, bytes memory /*params*/ ) internal view virtual override returns (uint256) { - return token.getPastVotes(account, blockNumber); + return token.getPastVotes(account, timepoint); } } diff --git a/contracts/governance/extensions/GovernorVotesComp.sol b/contracts/governance/extensions/GovernorVotesComp.sol deleted file mode 100644 index 8c4a1f8c1..000000000 --- a/contracts/governance/extensions/GovernorVotesComp.sol +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (governance/extensions/GovernorVotesComp.sol) - -pragma solidity ^0.8.0; - -import "../Governor.sol"; -import "../../token/ERC20/extensions/ERC20VotesComp.sol"; - -/** - * @dev Extension of {Governor} for voting weight extraction from a Comp token. - * - * _Available since v4.3._ - */ -abstract contract GovernorVotesComp is Governor { - ERC20VotesComp public immutable token; - - constructor(ERC20VotesComp token_) { - token = token_; - } - - /** - * Read the voting weight from the token's built in snapshot mechanism (see {Governor-_getVotes}). - */ - function _getVotes( - address account, - uint256 blockNumber, - bytes memory /*params*/ - ) internal view virtual override returns (uint256) { - return token.getPriorVotes(account, blockNumber); - } -} diff --git a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol index 8efefce39..0094fecd6 100644 --- a/contracts/governance/extensions/GovernorVotesQuorumFraction.sol +++ b/contracts/governance/extensions/GovernorVotesQuorumFraction.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/extensions/GovernorVotesQuorumFraction.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (governance/extensions/GovernorVotesQuorumFraction.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./GovernorVotes.sol"; -import "../../utils/Checkpoints.sol"; import "../../utils/math/SafeCast.sol"; +import "../../utils/structs/Checkpoints.sol"; /** * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token and a quorum expressed as a @@ -14,13 +14,18 @@ import "../../utils/math/SafeCast.sol"; * _Available since v4.3._ */ abstract contract GovernorVotesQuorumFraction is GovernorVotes { - using Checkpoints for Checkpoints.History; + using Checkpoints for Checkpoints.Trace224; - uint256 private _quorumNumerator; // DEPRECATED - Checkpoints.History private _quorumNumeratorHistory; + /// @custom:oz-retyped-from Checkpoints.History + Checkpoints.Trace224 private _quorumNumeratorHistory; event QuorumNumeratorUpdated(uint256 oldQuorumNumerator, uint256 newQuorumNumerator); + /** + * @dev The quorum set is not a valid fraction. + */ + error GovernorInvalidQuorumFraction(uint256 quorumNumerator, uint256 quorumDenominator); + /** * @dev Initialize quorum as a fraction of the token's total supply. * @@ -36,27 +41,24 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { * @dev Returns the current quorum numerator. See {quorumDenominator}. */ function quorumNumerator() public view virtual returns (uint256) { - return _quorumNumeratorHistory._checkpoints.length == 0 ? _quorumNumerator : _quorumNumeratorHistory.latest(); + return _quorumNumeratorHistory.latest(); } /** - * @dev Returns the quorum numerator at a specific block number. See {quorumDenominator}. + * @dev Returns the quorum numerator at a specific timepoint. See {quorumDenominator}. */ - function quorumNumerator(uint256 blockNumber) public view virtual returns (uint256) { + function quorumNumerator(uint256 timepoint) public view virtual returns (uint256) { // If history is empty, fallback to old storage uint256 length = _quorumNumeratorHistory._checkpoints.length; - if (length == 0) { - return _quorumNumerator; - } // Optimistic search, check the latest checkpoint - Checkpoints.Checkpoint memory latest = _quorumNumeratorHistory._checkpoints[length - 1]; - if (latest._blockNumber <= blockNumber) { + Checkpoints.Checkpoint224 memory latest = _quorumNumeratorHistory._checkpoints[length - 1]; + if (latest._key <= timepoint) { return latest._value; } // Otherwise, do the binary search - return _quorumNumeratorHistory.getAtBlock(blockNumber); + return _quorumNumeratorHistory.upperLookupRecent(SafeCast.toUint32(timepoint)); } /** @@ -67,10 +69,10 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { } /** - * @dev Returns the quorum for a block number, in terms of number of votes: `supply * numerator / denominator`. + * @dev Returns the quorum for a timepoint, in terms of number of votes: `supply * numerator / denominator`. */ - function quorum(uint256 blockNumber) public view virtual override returns (uint256) { - return (token.getPastTotalSupply(blockNumber) * quorumNumerator(blockNumber)) / quorumDenominator(); + function quorum(uint256 timepoint) public view virtual override returns (uint256) { + return (token.getPastTotalSupply(timepoint) * quorumNumerator(timepoint)) / quorumDenominator(); } /** @@ -97,22 +99,13 @@ abstract contract GovernorVotesQuorumFraction is GovernorVotes { * - New numerator must be smaller or equal to the denominator. */ function _updateQuorumNumerator(uint256 newQuorumNumerator) internal virtual { - require( - newQuorumNumerator <= quorumDenominator(), - "GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator" - ); - - uint256 oldQuorumNumerator = quorumNumerator(); - - // Make sure we keep track of the original numerator in contracts upgraded from a version without checkpoints. - if (oldQuorumNumerator != 0 && _quorumNumeratorHistory._checkpoints.length == 0) { - _quorumNumeratorHistory._checkpoints.push( - Checkpoints.Checkpoint({_blockNumber: 0, _value: SafeCast.toUint224(oldQuorumNumerator)}) - ); + uint256 denominator = quorumDenominator(); + if (newQuorumNumerator > denominator) { + revert GovernorInvalidQuorumFraction(newQuorumNumerator, denominator); } - // Set new quorum for future proposals - _quorumNumeratorHistory.push(newQuorumNumerator); + uint256 oldQuorumNumerator = quorumNumerator(); + _quorumNumeratorHistory.push(SafeCast.toUint32(clock()), SafeCast.toUint224(newQuorumNumerator)); emit QuorumNumeratorUpdated(oldQuorumNumerator, newQuorumNumerator); } diff --git a/contracts/governance/extensions/IGovernorTimelock.sol b/contracts/governance/extensions/IGovernorTimelock.sol index 40402f614..c51429481 100644 --- a/contracts/governance/extensions/IGovernorTimelock.sol +++ b/contracts/governance/extensions/IGovernorTimelock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (governance/extensions/IGovernorTimelock.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IGovernor.sol"; @@ -11,6 +11,16 @@ import "../IGovernor.sol"; * _Available since v4.3._ */ abstract contract IGovernorTimelock is IGovernor { + /** + * @dev The proposal hasn't been queued yet. + */ + error GovernorNotQueuedProposal(uint256 proposalId); + + /** + * @dev The proposal has already been queued. + */ + error GovernorAlreadyQueuedProposal(uint256 proposalId); + event ProposalQueued(uint256 proposalId, uint256 eta); function timelock() public view virtual returns (address); diff --git a/contracts/governance/utils/IVotes.sol b/contracts/governance/utils/IVotes.sol index 0bef3f920..a8a20856f 100644 --- a/contracts/governance/utils/IVotes.sol +++ b/contracts/governance/utils/IVotes.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (governance/utils/IVotes.sol) -pragma solidity ^0.8.0; +// OpenZeppelin Contracts (last updated v4.9.0) (governance/utils/IVotes.sol) +pragma solidity ^0.8.19; /** * @dev Common interface for {ERC20Votes}, {ERC721Votes}, and other {Votes}-enabled contracts. @@ -8,6 +8,11 @@ pragma solidity ^0.8.0; * _Available since v4.5._ */ interface IVotes { + /** + * @dev The signature used has expired. + */ + error VotesExpiredSignature(uint256 expiry); + /** * @dev Emitted when an account changes their delegate. */ @@ -24,18 +29,20 @@ interface IVotes { function getVotes(address account) external view returns (uint256); /** - * @dev Returns the amount of votes that `account` had at the end of a past block (`blockNumber`). + * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. */ - function getPastVotes(address account, uint256 blockNumber) external view returns (uint256); + function getPastVotes(address account, uint256 timepoint) external view returns (uint256); /** - * @dev Returns the total supply of votes available at the end of a past block (`blockNumber`). + * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. * * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. * Votes that have not been delegated are still part of total supply, even though they would not participate in a * vote. */ - function getPastTotalSupply(uint256 blockNumber) external view returns (uint256); + function getPastTotalSupply(uint256 timepoint) external view returns (uint256); /** * @dev Returns the delegate that `account` has chosen. diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index ccac72bce..09eb4e22c 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -1,13 +1,12 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (governance/utils/Votes.sol) -pragma solidity ^0.8.0; +// OpenZeppelin Contracts (last updated v4.9.0) (governance/utils/Votes.sol) +pragma solidity ^0.8.19; +import "../../interfaces/IERC5805.sol"; import "../../utils/Context.sol"; import "../../utils/Nonces.sol"; -import "../../utils/Checkpoints.sol"; import "../../utils/cryptography/EIP712.sol"; -import "./IVotes.sol"; -import "../../utils/math/SafeCast.sol"; +import "../../utils/structs/Checkpoints.sol"; /** * @dev This is a base abstract contract that tracks voting units, which are a measure of voting power that can be @@ -29,36 +28,76 @@ import "../../utils/math/SafeCast.sol"; * * _Available since v4.5._ */ -abstract contract Votes is IVotes, Context, EIP712, Nonces { - using Checkpoints for Checkpoints.History; +abstract contract Votes is Context, EIP712, Nonces, IERC5805 { + using Checkpoints for Checkpoints.Trace224; bytes32 private constant _DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); mapping(address => address) private _delegation; - mapping(address => Checkpoints.History) private _delegateCheckpoints; - Checkpoints.History private _totalCheckpoints; + + /// @custom:oz-retyped-from mapping(address => Checkpoints.History) + mapping(address => Checkpoints.Trace224) private _delegateCheckpoints; + + /// @custom:oz-retyped-from Checkpoints.History + Checkpoints.Trace224 private _totalCheckpoints; + + /** + * @dev The clock was incorrectly modified. + */ + error ERC6372InconsistentClock(); + + /** + * @dev Lookup to future votes is not available. + */ + error ERC5805FutureLookup(uint256 timepoint, uint48 clock); + + /** + * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based + * checkpoints (and voting), in which case {CLOCK_MODE} should be overridden as well to match. + */ + function clock() public view virtual returns (uint48) { + return SafeCast.toUint48(block.number); + } + + /** + * @dev Machine-readable description of the clock as specified in EIP-6372. + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual returns (string memory) { + // Check that the clock was not modified + if (clock() != block.number) { + revert ERC6372InconsistentClock(); + } + return "mode=blocknumber&from=default"; + } /** * @dev Returns the current amount of votes that `account` has. */ - function getVotes(address account) public view virtual override returns (uint256) { + function getVotes(address account) public view virtual returns (uint256) { return _delegateCheckpoints[account].latest(); } /** - * @dev Returns the amount of votes that `account` had at the end of a past block (`blockNumber`). + * @dev Returns the amount of votes that `account` had at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. * * Requirements: * - * - `blockNumber` must have been already mined + * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ - function getPastVotes(address account, uint256 blockNumber) public view virtual override returns (uint256) { - return _delegateCheckpoints[account].getAtProbablyRecentBlock(blockNumber); + function getPastVotes(address account, uint256 timepoint) public view virtual returns (uint256) { + uint48 currentTimepoint = clock(); + if (timepoint >= currentTimepoint) { + revert ERC5805FutureLookup(timepoint, currentTimepoint); + } + return _delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint32(timepoint)); } /** - * @dev Returns the total supply of votes available at the end of a past block (`blockNumber`). + * @dev Returns the total supply of votes available at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. * * NOTE: This value is the sum of all available votes, which is not necessarily the sum of all delegated votes. * Votes that have not been delegated are still part of total supply, even though they would not participate in a @@ -66,10 +105,14 @@ abstract contract Votes is IVotes, Context, EIP712, Nonces { * * Requirements: * - * - `blockNumber` must have been already mined + * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ - function getPastTotalSupply(uint256 blockNumber) public view virtual override returns (uint256) { - return _totalCheckpoints.getAtProbablyRecentBlock(blockNumber); + function getPastTotalSupply(uint256 timepoint) public view virtual returns (uint256) { + uint48 currentTimepoint = clock(); + if (timepoint >= currentTimepoint) { + revert ERC5805FutureLookup(timepoint, currentTimepoint); + } + return _totalCheckpoints.upperLookupRecent(SafeCast.toUint32(timepoint)); } /** @@ -82,14 +125,14 @@ abstract contract Votes is IVotes, Context, EIP712, Nonces { /** * @dev Returns the delegate that `account` has chosen. */ - function delegates(address account) public view virtual override returns (address) { + function delegates(address account) public view virtual returns (address) { return _delegation[account]; } /** * @dev Delegates votes from the sender to `delegatee`. */ - function delegate(address delegatee) public virtual override { + function delegate(address delegatee) public virtual { address account = _msgSender(); _delegate(account, delegatee); } @@ -104,15 +147,17 @@ abstract contract Votes is IVotes, Context, EIP712, Nonces { uint8 v, bytes32 r, bytes32 s - ) public virtual override { - require(block.timestamp <= expiry, "Votes: signature expired"); + ) public virtual { + if (block.timestamp > expiry) { + revert VotesExpiredSignature(expiry); + } address signer = ECDSA.recover( _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), v, r, s ); - require(nonce == _useNonce(signer), "Votes: invalid nonce"); + _useCheckedNonce(signer, nonce); _delegate(signer, delegatee); } @@ -135,10 +180,10 @@ abstract contract Votes is IVotes, Context, EIP712, Nonces { */ function _transferVotingUnits(address from, address to, uint256 amount) internal virtual { if (from == address(0)) { - _totalCheckpoints.push(_add, amount); + _push(_totalCheckpoints, _add, SafeCast.toUint224(amount)); } if (to == address(0)) { - _totalCheckpoints.push(_subtract, amount); + _push(_totalCheckpoints, _subtract, SafeCast.toUint224(amount)); } _moveDelegateVotes(delegates(from), delegates(to), amount); } @@ -149,11 +194,19 @@ abstract contract Votes is IVotes, Context, EIP712, Nonces { function _moveDelegateVotes(address from, address to, uint256 amount) private { if (from != to && amount > 0) { if (from != address(0)) { - (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[from].push(_subtract, amount); + (uint256 oldValue, uint256 newValue) = _push( + _delegateCheckpoints[from], + _subtract, + SafeCast.toUint224(amount) + ); emit DelegateVotesChanged(from, oldValue, newValue); } if (to != address(0)) { - (uint256 oldValue, uint256 newValue) = _delegateCheckpoints[to].push(_add, amount); + (uint256 oldValue, uint256 newValue) = _push( + _delegateCheckpoints[to], + _add, + SafeCast.toUint224(amount) + ); emit DelegateVotesChanged(to, oldValue, newValue); } } @@ -169,26 +222,29 @@ abstract contract Votes is IVotes, Context, EIP712, Nonces { /** * @dev Get the `pos`-th checkpoint for `account`. */ - function _checkpoints(address account, uint32 pos) internal view virtual returns (Checkpoints.Checkpoint memory) { - return _delegateCheckpoints[account].getAtPosition(pos); + function _checkpoints( + address account, + uint32 pos + ) internal view virtual returns (Checkpoints.Checkpoint224 memory) { + return _delegateCheckpoints[account].at(pos); } - function _add(uint256 a, uint256 b) private pure returns (uint256) { + function _push( + Checkpoints.Trace224 storage store, + function(uint224, uint224) view returns (uint224) op, + uint224 delta + ) private returns (uint224, uint224) { + return store.push(SafeCast.toUint32(clock()), op(store.latest(), delta)); + } + + function _add(uint224 a, uint224 b) private pure returns (uint224) { return a + b; } - function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + function _subtract(uint224 a, uint224 b) private pure returns (uint224) { return a - b; } - /** - * @dev Returns the contract's {EIP712} domain separator. - */ - // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view returns (bytes32) { - return _domainSeparatorV4(); - } - /** * @dev Must return the voting units held by an account. */ diff --git a/contracts/interfaces/IERC1155.sol b/contracts/interfaces/IERC1155.sol index f89113212..8f7527f91 100644 --- a/contracts/interfaces/IERC1155.sol +++ b/contracts/interfaces/IERC1155.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC1155.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC1155/IERC1155.sol"; diff --git a/contracts/interfaces/IERC1155MetadataURI.sol b/contracts/interfaces/IERC1155MetadataURI.sol index 2aa885feb..61b36c2e8 100644 --- a/contracts/interfaces/IERC1155MetadataURI.sol +++ b/contracts/interfaces/IERC1155MetadataURI.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC1155MetadataURI.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC1155/extensions/IERC1155MetadataURI.sol"; diff --git a/contracts/interfaces/IERC1155Receiver.sol b/contracts/interfaces/IERC1155Receiver.sol index a6d4ead16..b5cd186b7 100644 --- a/contracts/interfaces/IERC1155Receiver.sol +++ b/contracts/interfaces/IERC1155Receiver.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC1155Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC1155/IERC1155Receiver.sol"; diff --git a/contracts/interfaces/IERC1271.sol b/contracts/interfaces/IERC1271.sol index 5ec44c721..e7fca3079 100644 --- a/contracts/interfaces/IERC1271.sol +++ b/contracts/interfaces/IERC1271.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC1271.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC1271 standard signature validation method for diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index 1a8dc79f4..95c5b8992 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -1,24 +1,26 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1363.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC20.sol"; import "./IERC165.sol"; +/** + * @dev Interface of an ERC1363 compliant contract, as defined in the + * https://eips.ethereum.org/EIPS/eip-1363[EIP]. + * + * Defines a interface for ERC20 tokens that supports executing recipient + * code after `transfer` or `transferFrom`, or spender code after `approve`. + */ interface IERC1363 is IERC165, IERC20 { /* - * Note: the ERC-165 identifier for this interface is 0x4bbee2df. - * 0x4bbee2df === + * Note: the ERC-165 identifier for this interface is 0xb0202a11. + * 0xb0202a11 === * bytes4(keccak256('transferAndCall(address,uint256)')) ^ * bytes4(keccak256('transferAndCall(address,uint256,bytes)')) ^ * bytes4(keccak256('transferFromAndCall(address,address,uint256)')) ^ - * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) - */ - - /* - * Note: the ERC-165 identifier for this interface is 0xfb9ec8ce. - * 0xfb9ec8ce === + * bytes4(keccak256('transferFromAndCall(address,address,uint256,bytes)')) ^ * bytes4(keccak256('approveAndCall(address,uint256)')) ^ * bytes4(keccak256('approveAndCall(address,uint256,bytes)')) */ @@ -26,53 +28,53 @@ interface IERC1363 is IERC165, IERC20 { /** * @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred + * @param amount uint256 The amount of tokens to be transferred * @return true unless throwing */ - function transferAndCall(address to, uint256 value) external returns (bool); + function transferAndCall(address to, uint256 amount) external returns (bool); /** * @dev Transfer tokens from `msg.sender` to another address and then call `onTransferReceived` on receiver * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred + * @param amount uint256 The amount of tokens to be transferred * @param data bytes Additional data with no specified format, sent in call to `to` * @return true unless throwing */ - function transferAndCall(address to, uint256 value, bytes memory data) external returns (bool); + function transferAndCall(address to, uint256 amount, bytes memory data) external returns (bool); /** * @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver * @param from address The address which you want to send tokens from * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred + * @param amount uint256 The amount of tokens to be transferred * @return true unless throwing */ - function transferFromAndCall(address from, address to, uint256 value) external returns (bool); + function transferFromAndCall(address from, address to, uint256 amount) external returns (bool); /** * @dev Transfer tokens from one address to another and then call `onTransferReceived` on receiver * @param from address The address which you want to send tokens from * @param to address The address which you want to transfer to - * @param value uint256 The amount of tokens to be transferred + * @param amount uint256 The amount of tokens to be transferred * @param data bytes Additional data with no specified format, sent in call to `to` * @return true unless throwing */ - function transferFromAndCall(address from, address to, uint256 value, bytes memory data) external returns (bool); + function transferFromAndCall(address from, address to, uint256 amount, bytes memory data) external returns (bool); /** * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender * and then call `onApprovalReceived` on spender. * @param spender address The address which will spend the funds - * @param value uint256 The amount of tokens to be spent + * @param amount uint256 The amount of tokens to be spent */ - function approveAndCall(address spender, uint256 value) external returns (bool); + function approveAndCall(address spender, uint256 amount) external returns (bool); /** * @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender * and then call `onApprovalReceived` on spender. * @param spender address The address which will spend the funds - * @param value uint256 The amount of tokens to be spent + * @param amount uint256 The amount of tokens to be spent * @param data bytes Additional data with no specified format, sent in call to `spender` */ - function approveAndCall(address spender, uint256 value, bytes memory data) external returns (bool); + function approveAndCall(address spender, uint256 amount, bytes memory data) external returns (bool); } diff --git a/contracts/interfaces/IERC1363Receiver.sol b/contracts/interfaces/IERC1363Receiver.sol index bc5eaddb0..70ed857e9 100644 --- a/contracts/interfaces/IERC1363Receiver.sol +++ b/contracts/interfaces/IERC1363Receiver.sol @@ -1,8 +1,12 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363Receiver.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1363Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; +/** + * @dev Interface for any contract that wants to support {IERC1363-transferAndCall} + * or {IERC1363-transferFromAndCall} from {ERC1363} token contracts. + */ interface IERC1363Receiver { /* * Note: the ERC-165 identifier for this interface is 0x88a7ca5c. @@ -18,15 +22,14 @@ interface IERC1363Receiver { * Note: the token contract address is always the message sender. * @param operator address The address which called `transferAndCall` or `transferFromAndCall` function * @param from address The address which are token transferred from - * @param value uint256 The amount of tokens transferred + * @param amount uint256 The amount of tokens transferred * @param data bytes Additional data with no specified format - * @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` - * unless throwing + * @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` unless throwing */ function onTransferReceived( address operator, address from, - uint256 value, + uint256 amount, bytes memory data ) external returns (bytes4); } diff --git a/contracts/interfaces/IERC1363Spender.sol b/contracts/interfaces/IERC1363Spender.sol index 28775e140..0c89e58eb 100644 --- a/contracts/interfaces/IERC1363Spender.sol +++ b/contracts/interfaces/IERC1363Spender.sol @@ -1,8 +1,12 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1363Spender.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1363Spender.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; +/** + * @dev Interface for any contract that wants to support {IERC1363-approveAndCall} + * from {ERC1363} token contracts. + */ interface IERC1363Spender { /* * Note: the ERC-165 identifier for this interface is 0x7b04a2d0. @@ -17,10 +21,9 @@ interface IERC1363Spender { * transaction being reverted. * Note: the token contract address is always the message sender. * @param owner address The address which called `approveAndCall` function - * @param value uint256 The amount of tokens to be spent + * @param amount uint256 The amount of tokens to be spent * @param data bytes Additional data with no specified format - * @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))` - * unless throwing + * @return `bytes4(keccak256("onApprovalReceived(address,uint256,bytes)"))`unless throwing */ - function onApprovalReceived(address owner, uint256 value, bytes memory data) external returns (bytes4); + function onApprovalReceived(address owner, uint256 amount, bytes memory data) external returns (bytes4); } diff --git a/contracts/interfaces/IERC165.sol b/contracts/interfaces/IERC165.sol index b97c4daa2..f4d90264c 100644 --- a/contracts/interfaces/IERC165.sol +++ b/contracts/interfaces/IERC165.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC165.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/introspection/IERC165.sol"; diff --git a/contracts/interfaces/IERC1820Implementer.sol b/contracts/interfaces/IERC1820Implementer.sol index a83a7a304..6f0ec661d 100644 --- a/contracts/interfaces/IERC1820Implementer.sol +++ b/contracts/interfaces/IERC1820Implementer.sol @@ -1,6 +1,20 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1820Implementer.sol) +// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC1820Implementer.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import "../utils/introspection/IERC1820Implementer.sol"; +/** + * @dev Interface for an ERC1820 implementer, as defined in the + * https://eips.ethereum.org/EIPS/eip-1820#interface-implementation-erc1820implementerinterface[EIP]. + * Used by contracts that will be registered as implementers in the + * {IERC1820Registry}. + */ +interface IERC1820Implementer { + /** + * @dev Returns a special value (`ERC1820_ACCEPT_MAGIC`) if this contract + * implements `interfaceHash` for `account`. + * + * See {IERC1820Registry-setInterfaceImplementer}. + */ + function canImplementInterfaceForAddress(bytes32 interfaceHash, address account) external view returns (bytes32); +} diff --git a/contracts/interfaces/IERC1820Registry.sol b/contracts/interfaces/IERC1820Registry.sol index 1b1ba9fcf..d9dd49355 100644 --- a/contracts/interfaces/IERC1820Registry.sol +++ b/contracts/interfaces/IERC1820Registry.sol @@ -1,6 +1,112 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC1820Registry.sol) +// OpenZeppelin Contracts (last updated v4.8.0) (utils/introspection/IERC1820Registry.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import "../utils/introspection/IERC1820Registry.sol"; +/** + * @dev Interface of the global ERC1820 Registry, as defined in the + * https://eips.ethereum.org/EIPS/eip-1820[EIP]. Accounts may register + * implementers for interfaces in this registry, as well as query support. + * + * Implementers may be shared by multiple accounts, and can also implement more + * than a single interface for each account. Contracts can implement interfaces + * for themselves, but externally-owned accounts (EOA) must delegate this to a + * contract. + * + * {IERC165} interfaces can also be queried via the registry. + * + * For an in-depth explanation and source code analysis, see the EIP text. + */ +interface IERC1820Registry { + event InterfaceImplementerSet(address indexed account, bytes32 indexed interfaceHash, address indexed implementer); + + event ManagerChanged(address indexed account, address indexed newManager); + + /** + * @dev Sets `newManager` as the manager for `account`. A manager of an + * account is able to set interface implementers for it. + * + * By default, each account is its own manager. Passing a value of `0x0` in + * `newManager` will reset the manager to this initial state. + * + * Emits a {ManagerChanged} event. + * + * Requirements: + * + * - the caller must be the current manager for `account`. + */ + function setManager(address account, address newManager) external; + + /** + * @dev Returns the manager for `account`. + * + * See {setManager}. + */ + function getManager(address account) external view returns (address); + + /** + * @dev Sets the `implementer` contract as ``account``'s implementer for + * `interfaceHash`. + * + * `account` being the zero address is an alias for the caller's address. + * The zero address can also be used in `implementer` to remove an old one. + * + * See {interfaceHash} to learn how these are created. + * + * Emits an {InterfaceImplementerSet} event. + * + * Requirements: + * + * - the caller must be the current manager for `account`. + * - `interfaceHash` must not be an {IERC165} interface id (i.e. it must not + * end in 28 zeroes). + * - `implementer` must implement {IERC1820Implementer} and return true when + * queried for support, unless `implementer` is the caller. See + * {IERC1820Implementer-canImplementInterfaceForAddress}. + */ + function setInterfaceImplementer(address account, bytes32 _interfaceHash, address implementer) external; + + /** + * @dev Returns the implementer of `interfaceHash` for `account`. If no such + * implementer is registered, returns the zero address. + * + * If `interfaceHash` is an {IERC165} interface id (i.e. it ends with 28 + * zeroes), `account` will be queried for support of it. + * + * `account` being the zero address is an alias for the caller's address. + */ + function getInterfaceImplementer(address account, bytes32 _interfaceHash) external view returns (address); + + /** + * @dev Returns the interface hash for an `interfaceName`, as defined in the + * corresponding + * https://eips.ethereum.org/EIPS/eip-1820#interface-name[section of the EIP]. + */ + function interfaceHash(string calldata interfaceName) external pure returns (bytes32); + + /** + * @notice Updates the cache with whether the contract implements an ERC165 interface or not. + * @param account Address of the contract for which to update the cache. + * @param interfaceId ERC165 interface for which to update the cache. + */ + function updateERC165Cache(address account, bytes4 interfaceId) external; + + /** + * @notice Checks whether a contract implements an ERC165 interface or not. + * If the result is not cached a direct lookup on the contract address is performed. + * If the result is not cached or the cached value is out-of-date, the cache MUST be updated manually by calling + * {updateERC165Cache} with the contract address. + * @param account Address of the contract to check. + * @param interfaceId ERC165 interface to check. + * @return True if `account` implements `interfaceId`, false otherwise. + */ + function implementsERC165Interface(address account, bytes4 interfaceId) external view returns (bool); + + /** + * @notice Checks whether a contract implements an ERC165 interface or not without using or updating the cache. + * @param account Address of the contract to check. + * @param interfaceId ERC165 interface to check. + * @return True if `account` implements `interfaceId`, false otherwise. + */ + function implementsERC165InterfaceNoCache(address account, bytes4 interfaceId) external view returns (bool); +} diff --git a/contracts/interfaces/IERC1967.sol b/contracts/interfaces/IERC1967.sol new file mode 100644 index 000000000..190d86b2c --- /dev/null +++ b/contracts/interfaces/IERC1967.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC1967.sol) + +pragma solidity ^0.8.19; + +/** + * @dev ERC-1967: Proxy Storage Slots. This interface contains the events defined in the ERC. + * + * _Available since v4.8.3._ + */ +interface IERC1967 { + /** + * @dev Emitted when the implementation is upgraded. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Emitted when the admin account has changed. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Emitted when the beacon is changed. + */ + event BeaconUpgraded(address indexed beacon); +} diff --git a/contracts/interfaces/IERC20.sol b/contracts/interfaces/IERC20.sol index a819316d1..dd559e980 100644 --- a/contracts/interfaces/IERC20.sol +++ b/contracts/interfaces/IERC20.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC20.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC20/IERC20.sol"; diff --git a/contracts/interfaces/IERC20Metadata.sol b/contracts/interfaces/IERC20Metadata.sol index aa5c63910..061fd4a2f 100644 --- a/contracts/interfaces/IERC20Metadata.sol +++ b/contracts/interfaces/IERC20Metadata.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC20Metadata.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/interfaces/IERC2309.sol b/contracts/interfaces/IERC2309.sol index b3fec44e2..9d8886994 100644 --- a/contracts/interfaces/IERC2309.sol +++ b/contracts/interfaces/IERC2309.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (interfaces/IERC2309.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev ERC-2309: ERC-721 Consecutive Transfer Extension. diff --git a/contracts/interfaces/IERC2612.sol b/contracts/interfaces/IERC2612.sol index 6dfdf6f63..582eea81d 100644 --- a/contracts/interfaces/IERC2612.sol +++ b/contracts/interfaces/IERC2612.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC2612.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC2612.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC20/extensions/IERC20Permit.sol"; diff --git a/contracts/interfaces/IERC2981.sol b/contracts/interfaces/IERC2981.sol index 1c9448a91..1b1476782 100644 --- a/contracts/interfaces/IERC2981.sol +++ b/contracts/interfaces/IERC2981.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (interfaces/IERC2981.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC2981.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/introspection/IERC165.sol"; diff --git a/contracts/interfaces/IERC3156.sol b/contracts/interfaces/IERC3156.sol index 12381906d..280b25be1 100644 --- a/contracts/interfaces/IERC3156.sol +++ b/contracts/interfaces/IERC3156.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC3156.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC3156FlashBorrower.sol"; import "./IERC3156FlashLender.sol"; diff --git a/contracts/interfaces/IERC3156FlashBorrower.sol b/contracts/interfaces/IERC3156FlashBorrower.sol index c3b4f1eb1..3f216194b 100644 --- a/contracts/interfaces/IERC3156FlashBorrower.sol +++ b/contracts/interfaces/IERC3156FlashBorrower.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (interfaces/IERC3156FlashBorrower.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC3156FlashBorrower.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC3156 FlashBorrower, as defined in @@ -17,7 +17,7 @@ interface IERC3156FlashBorrower { * @param amount The amount of tokens lent. * @param fee The additional amount of tokens to repay. * @param data Arbitrary data structure, intended to contain user-defined parameters. - * @return The keccak256 hash of "IERC3156FlashBorrower.onFlashLoan" + * @return The keccak256 hash of "ERC3156FlashBorrower.onFlashLoan" */ function onFlashLoan( address initiator, diff --git a/contracts/interfaces/IERC3156FlashLender.sol b/contracts/interfaces/IERC3156FlashLender.sol index 31012830f..89a486fdb 100644 --- a/contracts/interfaces/IERC3156FlashLender.sol +++ b/contracts/interfaces/IERC3156FlashLender.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC3156FlashLender.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC3156FlashBorrower.sol"; diff --git a/contracts/interfaces/IERC4626.sol b/contracts/interfaces/IERC4626.sol index 08e5de717..946de9155 100644 --- a/contracts/interfaces/IERC4626.sol +++ b/contracts/interfaces/IERC4626.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (interfaces/IERC4626.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC4626.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC20/IERC20.sol"; import "../token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/contracts/interfaces/IERC4906.sol b/contracts/interfaces/IERC4906.sol new file mode 100644 index 000000000..96e709228 --- /dev/null +++ b/contracts/interfaces/IERC4906.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC4906.sol) + +pragma solidity ^0.8.19; + +import "./IERC165.sol"; +import "./IERC721.sol"; + +/// @title EIP-721 Metadata Update Extension +interface IERC4906 is IERC165, IERC721 { + /// @dev This event emits when the metadata of a token is changed. + /// So that the third-party platforms such as NFT market could + /// timely update the images and related attributes of the NFT. + event MetadataUpdate(uint256 _tokenId); + + /// @dev This event emits when the metadata of a range of tokens is changed. + /// So that the third-party platforms such as NFT market could + /// timely update the images and related attributes of the NFTs. + event BatchMetadataUpdate(uint256 _fromTokenId, uint256 _toTokenId); +} diff --git a/contracts/interfaces/IERC5267.sol b/contracts/interfaces/IERC5267.sol new file mode 100644 index 000000000..c19b4a6da --- /dev/null +++ b/contracts/interfaces/IERC5267.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC5267.sol) + +pragma solidity ^0.8.19; + +interface IERC5267 { + /** + * @dev MAY be emitted to signal that the domain could have changed. + */ + event EIP712DomainChanged(); + + /** + * @dev returns the fields and values that describe the domain separator used by this contract for EIP-712 + * signature. + */ + function eip712Domain() + external + view + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ); +} diff --git a/contracts/interfaces/IERC5313.sol b/contracts/interfaces/IERC5313.sol new file mode 100644 index 000000000..1d17080a1 --- /dev/null +++ b/contracts/interfaces/IERC5313.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC5313.sol) + +pragma solidity ^0.8.19; + +/** + * @dev Interface for the Light Contract Ownership Standard. + * + * A standardized minimal interface required to identify an account that controls a contract + * + * _Available since v4.9._ + */ +interface IERC5313 { + /** + * @dev Gets the address of the owner. + */ + function owner() external view returns (address); +} diff --git a/contracts/interfaces/IERC5805.sol b/contracts/interfaces/IERC5805.sol new file mode 100644 index 000000000..3c0a3a6c6 --- /dev/null +++ b/contracts/interfaces/IERC5805.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC5805.sol) + +pragma solidity ^0.8.19; + +import "../governance/utils/IVotes.sol"; +import "./IERC6372.sol"; + +interface IERC5805 is IERC6372, IVotes {} diff --git a/contracts/interfaces/IERC6372.sol b/contracts/interfaces/IERC6372.sol new file mode 100644 index 000000000..c6c78d10a --- /dev/null +++ b/contracts/interfaces/IERC6372.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (interfaces/IERC6372.sol) + +pragma solidity ^0.8.19; + +interface IERC6372 { + /** + * @dev Clock used for flagging checkpoints. Can be overridden to implement timestamp based checkpoints (and voting). + */ + function clock() external view returns (uint48); + + /** + * @dev Description of the clock + */ + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() external view returns (string memory); +} diff --git a/contracts/interfaces/IERC721.sol b/contracts/interfaces/IERC721.sol index 822b311c5..e840f28c7 100644 --- a/contracts/interfaces/IERC721.sol +++ b/contracts/interfaces/IERC721.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC721.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC721/IERC721.sol"; diff --git a/contracts/interfaces/IERC721Enumerable.sol b/contracts/interfaces/IERC721Enumerable.sol index e39a5a01b..fafda5998 100644 --- a/contracts/interfaces/IERC721Enumerable.sol +++ b/contracts/interfaces/IERC721Enumerable.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC721Enumerable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC721/extensions/IERC721Enumerable.sol"; diff --git a/contracts/interfaces/IERC721Metadata.sol b/contracts/interfaces/IERC721Metadata.sol index afe2707c9..f14433e8d 100644 --- a/contracts/interfaces/IERC721Metadata.sol +++ b/contracts/interfaces/IERC721Metadata.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC721Metadata.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC721/extensions/IERC721Metadata.sol"; diff --git a/contracts/interfaces/IERC721Receiver.sol b/contracts/interfaces/IERC721Receiver.sol index c9c153a24..9e62fa734 100644 --- a/contracts/interfaces/IERC721Receiver.sol +++ b/contracts/interfaces/IERC721Receiver.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (interfaces/IERC721Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC721/IERC721Receiver.sol"; diff --git a/contracts/interfaces/IERC777.sol b/contracts/interfaces/IERC777.sol index b97ba7b80..f868701ba 100644 --- a/contracts/interfaces/IERC777.sol +++ b/contracts/interfaces/IERC777.sol @@ -1,6 +1,199 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC777.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import "../token/ERC777/IERC777.sol"; +/** + * @dev Interface of the ERC777Token standard as defined in the EIP. + * + * This contract uses the + * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 registry standard] to let + * token holders and recipients react to token movements by using setting implementers + * for the associated interfaces in said registry. See {IERC1820Registry} and + * {IERC1820Implementer}. + */ +interface IERC777 { + /** + * @dev Emitted when `amount` tokens are created by `operator` and assigned to `to`. + * + * Note that some additional user `data` and `operatorData` can be logged in the event. + */ + event Minted(address indexed operator, address indexed to, uint256 amount, bytes data, bytes operatorData); + + /** + * @dev Emitted when `operator` destroys `amount` tokens from `account`. + * + * Note that some additional user `data` and `operatorData` can be logged in the event. + */ + event Burned(address indexed operator, address indexed from, uint256 amount, bytes data, bytes operatorData); + + /** + * @dev Emitted when `operator` is made operator for `tokenHolder`. + */ + event AuthorizedOperator(address indexed operator, address indexed tokenHolder); + + /** + * @dev Emitted when `operator` is revoked its operator status for `tokenHolder`. + */ + event RevokedOperator(address indexed operator, address indexed tokenHolder); + + /** + * @dev Returns the name of the token. + */ + function name() external view returns (string memory); + + /** + * @dev Returns the symbol of the token, usually a shorter version of the + * name. + */ + function symbol() external view returns (string memory); + + /** + * @dev Returns the smallest part of the token that is not divisible. This + * means all token operations (creation, movement and destruction) must have + * amounts that are a multiple of this number. + * + * For most token contracts, this value will equal 1. + */ + function granularity() external view returns (uint256); + + /** + * @dev Returns the amount of tokens in existence. + */ + function totalSupply() external view returns (uint256); + + /** + * @dev Returns the amount of tokens owned by an account (`owner`). + */ + function balanceOf(address owner) external view returns (uint256); + + /** + * @dev Moves `amount` tokens from the caller's account to `recipient`. + * + * If send or receive hooks are registered for the caller and `recipient`, + * the corresponding functions will be called with `data` and empty + * `operatorData`. See {IERC777Sender} and {IERC777Recipient}. + * + * Emits a {Sent} event. + * + * Requirements + * + * - the caller must have at least `amount` tokens. + * - `recipient` cannot be the zero address. + * - if `recipient` is a contract, it must implement the {IERC777Recipient} + * interface. + */ + function send(address recipient, uint256 amount, bytes calldata data) external; + + /** + * @dev Destroys `amount` tokens from the caller's account, reducing the + * total supply. + * + * If a send hook is registered for the caller, the corresponding function + * will be called with `data` and empty `operatorData`. See {IERC777Sender}. + * + * Emits a {Burned} event. + * + * Requirements + * + * - the caller must have at least `amount` tokens. + */ + function burn(uint256 amount, bytes calldata data) external; + + /** + * @dev Returns true if an account is an operator of `tokenHolder`. + * Operators can send and burn tokens on behalf of their owners. All + * accounts are their own operator. + * + * See {operatorSend} and {operatorBurn}. + */ + function isOperatorFor(address operator, address tokenHolder) external view returns (bool); + + /** + * @dev Make an account an operator of the caller. + * + * See {isOperatorFor}. + * + * Emits an {AuthorizedOperator} event. + * + * Requirements + * + * - `operator` cannot be calling address. + */ + function authorizeOperator(address operator) external; + + /** + * @dev Revoke an account's operator status for the caller. + * + * See {isOperatorFor} and {defaultOperators}. + * + * Emits a {RevokedOperator} event. + * + * Requirements + * + * - `operator` cannot be calling address. + */ + function revokeOperator(address operator) external; + + /** + * @dev Returns the list of default operators. These accounts are operators + * for all token holders, even if {authorizeOperator} was never called on + * them. + * + * This list is immutable, but individual holders may revoke these via + * {revokeOperator}, in which case {isOperatorFor} will return false. + */ + function defaultOperators() external view returns (address[] memory); + + /** + * @dev Moves `amount` tokens from `sender` to `recipient`. The caller must + * be an operator of `sender`. + * + * If send or receive hooks are registered for `sender` and `recipient`, + * the corresponding functions will be called with `data` and + * `operatorData`. See {IERC777Sender} and {IERC777Recipient}. + * + * Emits a {Sent} event. + * + * Requirements + * + * - `sender` cannot be the zero address. + * - `sender` must have at least `amount` tokens. + * - the caller must be an operator for `sender`. + * - `recipient` cannot be the zero address. + * - if `recipient` is a contract, it must implement the {IERC777Recipient} + * interface. + */ + function operatorSend( + address sender, + address recipient, + uint256 amount, + bytes calldata data, + bytes calldata operatorData + ) external; + + /** + * @dev Destroys `amount` tokens from `account`, reducing the total supply. + * The caller must be an operator of `account`. + * + * If a send hook is registered for `account`, the corresponding function + * will be called with `data` and `operatorData`. See {IERC777Sender}. + * + * Emits a {Burned} event. + * + * Requirements + * + * - `account` cannot be the zero address. + * - `account` must have at least `amount` tokens. + * - the caller must be an operator for `account`. + */ + function operatorBurn(address account, uint256 amount, bytes calldata data, bytes calldata operatorData) external; + + event Sent( + address indexed operator, + address indexed from, + address indexed to, + uint256 amount, + bytes data, + bytes operatorData + ); +} diff --git a/contracts/interfaces/IERC777Recipient.sol b/contracts/interfaces/IERC777Recipient.sol index 0ce2704a8..be2ab871d 100644 --- a/contracts/interfaces/IERC777Recipient.sol +++ b/contracts/interfaces/IERC777Recipient.sol @@ -1,6 +1,34 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC777Recipient.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import "../token/ERC777/IERC777Recipient.sol"; +/** + * @dev Interface of the ERC777TokensRecipient standard as defined in the EIP. + * + * Accounts can be notified of {IERC777} tokens being sent to them by having a + * contract implement this interface (contract holders can be their own + * implementer) and registering it on the + * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. + * + * See {IERC1820Registry} and {IERC1820Implementer}. + */ +interface IERC777Recipient { + /** + * @dev Called by an {IERC777} token contract whenever tokens are being + * moved or created into a registered account (`to`). The type of operation + * is conveyed by `from` being the zero address or not. + * + * This call occurs _after_ the token contract's state is updated, so + * {IERC777-balanceOf}, etc., can be used to query the post-operation state. + * + * This function may revert to prevent the operation from being executed. + */ + function tokensReceived( + address operator, + address from, + address to, + uint256 amount, + bytes calldata userData, + bytes calldata operatorData + ) external; +} diff --git a/contracts/interfaces/IERC777Sender.sol b/contracts/interfaces/IERC777Sender.sol index f1f17a22e..d4172b107 100644 --- a/contracts/interfaces/IERC777Sender.sol +++ b/contracts/interfaces/IERC777Sender.sol @@ -1,6 +1,34 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (interfaces/IERC777Sender.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import "../token/ERC777/IERC777Sender.sol"; +/** + * @dev Interface of the ERC777TokensSender standard as defined in the EIP. + * + * {IERC777} Token holders can be notified of operations performed on their + * tokens by having a contract implement this interface (contract holders can be + * their own implementer) and registering it on the + * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. + * + * See {IERC1820Registry} and {IERC1820Implementer}. + */ +interface IERC777Sender { + /** + * @dev Called by an {IERC777} token contract whenever a registered holder's + * (`from`) tokens are about to be moved or destroyed. The type of operation + * is conveyed by `to` being the zero address or not. + * + * This call occurs _before_ the token contract's state is updated, so + * {IERC777-balanceOf}, etc., can be used to query the pre-operation state. + * + * This function may revert to prevent the operation from being executed. + */ + function tokensToSend( + address operator, + address from, + address to, + uint256 amount, + bytes calldata userData, + bytes calldata operatorData + ) external; +} diff --git a/contracts/interfaces/README.adoc b/contracts/interfaces/README.adoc index 5b4cedf95..4525bc9a2 100644 --- a/contracts/interfaces/README.adoc +++ b/contracts/interfaces/README.adoc @@ -22,6 +22,8 @@ are useful to interact with third party contracts that implement them. - {IERC1155MetadataURI} - {IERC1271} - {IERC1363} +- {IERC1363Receiver} +- {IERC1363Spender} - {IERC1820Implementer} - {IERC1820Registry} - {IERC1822Proxiable} @@ -30,6 +32,11 @@ are useful to interact with third party contracts that implement them. - {IERC3156FlashLender} - {IERC3156FlashBorrower} - {IERC4626} +- {IERC4906} +- {IERC5267} +- {IERC5313} +- {IERC5805} +- {IERC6372} == Detailed ABI @@ -39,6 +46,8 @@ are useful to interact with third party contracts that implement them. {{IERC1363Receiver}} +{{IERC1363Spender}} + {{IERC1820Implementer}} {{IERC1820Registry}} @@ -53,4 +62,12 @@ are useful to interact with third party contracts that implement them. {{IERC3156FlashBorrower}} -{{IERC4626}} \ No newline at end of file +{{IERC4626}} + +{{IERC5313}} + +{{IERC5267}} + +{{IERC5805}} + +{{IERC6372}} diff --git a/contracts/interfaces/draft-IERC1822.sol b/contracts/interfaces/draft-IERC1822.sol index 3b73d744c..f452b5e34 100644 --- a/contracts/interfaces/draft-IERC1822.sol +++ b/contracts/interfaces/draft-IERC1822.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (interfaces/draft-IERC1822.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev ERC1822: Universal Upgradeable Proxy Standard (UUPS) documents a method for upgradeability through a simplified diff --git a/contracts/interfaces/draft-IERC2612.sol b/contracts/interfaces/draft-IERC2612.sol deleted file mode 100644 index 1ea7bf1c0..000000000 --- a/contracts/interfaces/draft-IERC2612.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -// EIP-2612 is Final as of 2022-11-01. This file is deprecated. - -import "./IERC2612.sol"; diff --git a/contracts/interfaces/draft-IERC6093.sol b/contracts/interfaces/draft-IERC6093.sol new file mode 100644 index 000000000..fbe31051a --- /dev/null +++ b/contracts/interfaces/draft-IERC6093.sol @@ -0,0 +1,162 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +/** + * @dev Standard ERC20 Errors + * Interface of the ERC6093 custom errors for ERC20 tokens + * as defined in https://eips.ethereum.org/EIPS/eip-6093 + */ +interface IERC20Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientBalance(address sender, uint256 balance, uint256 needed); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC20InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC20InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `spender`’s `allowance`. Used in transfers. + * @param spender Address that may be allowed to operate on tokens without being their owner. + * @param allowance Amount of tokens a `spender` is allowed to operate with. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC20InsufficientAllowance(address spender, uint256 allowance, uint256 needed); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC20InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `spender` to be approved. Used in approvals. + * @param spender Address that may be allowed to operate on tokens without being their owner. + */ + error ERC20InvalidSpender(address spender); +} + +/** + * @dev Standard ERC721 Errors + * Interface of the ERC6093 custom errors for ERC721 tokens + * as defined in https://eips.ethereum.org/EIPS/eip-6093 + */ +interface IERC721Errors { + /** + * @dev Indicates that an address can't be an owner. For example, `address(0)` is a forbidden owner in EIP-20. + * Used in balance queries. + * @param owner Address of the current owner of a token. + */ + error ERC721InvalidOwner(address owner); + + /** + * @dev Indicates a `tokenId` whose `owner` is the zero address. + * @param tokenId Identifier number of a token. + */ + error ERC721NonexistentToken(uint256 tokenId); + + /** + * @dev Indicates an error related to the ownership over a particular token. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param tokenId Identifier number of a token. + * @param owner Address of the current owner of a token. + */ + error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC721InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC721InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param tokenId Identifier number of a token. + */ + error ERC721InsufficientApproval(address operator, uint256 tokenId); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC721InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC721InvalidOperator(address operator); +} + +/** + * @dev Standard ERC1155 Errors + * Interface of the ERC6093 custom errors for ERC1155 tokens + * as defined in https://eips.ethereum.org/EIPS/eip-6093 + */ +interface IERC1155Errors { + /** + * @dev Indicates an error related to the current `balance` of a `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + * @param balance Current balance for the interacting account. + * @param needed Minimum amount required to perform a transfer. + */ + error ERC1155InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 tokenId); + + /** + * @dev Indicates a failure with the token `sender`. Used in transfers. + * @param sender Address whose tokens are being transferred. + */ + error ERC1155InvalidSender(address sender); + + /** + * @dev Indicates a failure with the token `receiver`. Used in transfers. + * @param receiver Address to which tokens are being transferred. + */ + error ERC1155InvalidReceiver(address receiver); + + /** + * @dev Indicates a failure with the `operator`’s approval. Used in transfers. + * @param operator Address that may be allowed to operate on tokens without being their owner. + * @param owner Address of the current owner of a token. + */ + error ERC1155MissingApprovalForAll(address operator, address owner); + + /** + * @dev Indicates a failure with the `approver` of a token to be approved. Used in approvals. + * @param approver Address initiating an approval operation. + */ + error ERC1155InvalidApprover(address approver); + + /** + * @dev Indicates a failure with the `operator` to be approved. Used in approvals. + * @param operator Address that may be allowed to operate on tokens without being their owner. + */ + error ERC1155InvalidOperator(address operator); + + /** + * @dev Indicates an array length mismatch between ids and values in a safeBatchTransferFrom operation. + * Used in batch transfers. + * @param idsLength Length of the array of token identifiers + * @param valuesLength Length of the array of token amounts + */ + error ERC1155InvalidArrayLength(uint256 idsLength, uint256 valuesLength); +} diff --git a/contracts/metatx/ERC2771Context.sol b/contracts/metatx/ERC2771Context.sol index 8cc14b9f4..e02ffcc19 100644 --- a/contracts/metatx/ERC2771Context.sol +++ b/contracts/metatx/ERC2771Context.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (metatx/ERC2771Context.sol) -pragma solidity ^0.8.9; +pragma solidity ^0.8.19; import "../utils/Context.sol"; diff --git a/contracts/metatx/MinimalForwarder.sol b/contracts/metatx/MinimalForwarder.sol index 9298ae675..b5267aa10 100644 --- a/contracts/metatx/MinimalForwarder.sol +++ b/contracts/metatx/MinimalForwarder.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (metatx/MinimalForwarder.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (metatx/MinimalForwarder.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/cryptography/ECDSA.sol"; import "../utils/cryptography/EIP712.sol"; @@ -31,6 +31,16 @@ contract MinimalForwarder is EIP712 { mapping(address => uint256) private _nonces; + /** + * @dev The request `from` doesn't match with the recovered `signer`. + */ + error MinimalForwarderInvalidSigner(address signer, address from); + + /** + * @dev The request nonce doesn't match with the `current` nonce for the request signer. + */ + error MinimalForwarderInvalidNonce(address signer, uint256 current); + constructor() EIP712("MinimalForwarder", "0.0.1") {} function getNonce(address from) public view returns (uint256) { @@ -38,17 +48,25 @@ contract MinimalForwarder is EIP712 { } function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) { - address signer = _hashTypedDataV4( - keccak256(abi.encode(_TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data))) - ).recover(signature); - return _nonces[req.from] == req.nonce && signer == req.from; + address signer = _recover(req, signature); + (bool correctFrom, bool correctNonce) = _validateReq(req, signer); + return correctFrom && correctNonce; } function execute( ForwardRequest calldata req, bytes calldata signature ) public payable returns (bool, bytes memory) { - require(verify(req, signature), "MinimalForwarder: signature does not match request"); + address signer = _recover(req, signature); + (bool correctFrom, bool correctNonce) = _validateReq(req, signer); + + if (!correctFrom) { + revert MinimalForwarderInvalidSigner(signer, req.from); + } + if (!correctNonce) { + revert MinimalForwarderInvalidNonce(signer, _nonces[req.from]); + } + _nonces[req.from] = req.nonce + 1; (bool success, bytes memory returndata) = req.to.call{gas: req.gas, value: req.value}( @@ -69,4 +87,18 @@ contract MinimalForwarder is EIP712 { return (success, returndata); } + + function _recover(ForwardRequest calldata req, bytes calldata signature) internal view returns (address) { + return + _hashTypedDataV4( + keccak256(abi.encode(_TYPEHASH, req.from, req.to, req.value, req.gas, req.nonce, keccak256(req.data))) + ).recover(signature); + } + + function _validateReq( + ForwardRequest calldata req, + address signer + ) internal view returns (bool correctFrom, bool correctNonce) { + return (signer == req.from, _nonces[req.from] == req.nonce); + } } diff --git a/contracts/mocks/AccessControlCrossChainMock.sol b/contracts/mocks/AccessControlCrossChainMock.sol deleted file mode 100644 index cd4c3a5d9..000000000 --- a/contracts/mocks/AccessControlCrossChainMock.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.4; - -import "../access/AccessControlCrossChain.sol"; -import "../crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol"; - -contract AccessControlCrossChainMock is AccessControlCrossChain, CrossChainEnabledArbitrumL2 {} diff --git a/contracts/mocks/AddressFnPointersMock.sol b/contracts/mocks/AddressFnPointersMock.sol new file mode 100644 index 000000000..c696b3ec1 --- /dev/null +++ b/contracts/mocks/AddressFnPointersMock.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "../utils/Address.sol"; + +/** + * @dev A mock to expose `Address`'s functions with function pointers. + */ +contract AddressFnPointerMock { + error CustomRevert(); + + function functionCall(address target, bytes memory data) external returns (bytes memory) { + return Address.functionCall(target, data, _customRevert); + } + + function functionCallWithValue(address target, bytes memory data, uint256 value) external returns (bytes memory) { + return Address.functionCallWithValue(target, data, value, _customRevert); + } + + function functionStaticCall(address target, bytes memory data) external view returns (bytes memory) { + return Address.functionStaticCall(target, data, _customRevert); + } + + function functionDelegateCall(address target, bytes memory data) external returns (bytes memory) { + return Address.functionDelegateCall(target, data, _customRevert); + } + + function verifyCallResultFromTarget( + address target, + bool success, + bytes memory returndata + ) external view returns (bytes memory) { + return Address.verifyCallResultFromTarget(target, success, returndata, _customRevert); + } + + function verifyCallResult(bool success, bytes memory returndata) external view returns (bytes memory) { + return Address.verifyCallResult(success, returndata, _customRevert); + } + + function verifyCallResultVoid(bool success, bytes memory returndata) external view returns (bytes memory) { + return Address.verifyCallResult(success, returndata, _customRevertVoid); + } + + function _customRevert() internal pure { + revert CustomRevert(); + } + + function _customRevertVoid() internal pure {} +} diff --git a/contracts/mocks/ArraysMock.sol b/contracts/mocks/ArraysMock.sol index 2ea17a09f..b341edc62 100644 --- a/contracts/mocks/ArraysMock.sol +++ b/contracts/mocks/ArraysMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/Arrays.sol"; diff --git a/contracts/mocks/CallReceiverMock.sol b/contracts/mocks/CallReceiverMock.sol index 344a1054b..b87f4e59e 100644 --- a/contracts/mocks/CallReceiverMock.sol +++ b/contracts/mocks/CallReceiverMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract CallReceiverMock { event MockFunctionCalled(); @@ -14,6 +14,10 @@ contract CallReceiverMock { return "0x1234"; } + function mockFunctionEmptyReturn() public payable { + emit MockFunctionCalled(); + } + function mockFunctionWithArgs(uint256 a, uint256 b) public payable returns (string memory) { emit MockFunctionCalledWithArgs(a, b); diff --git a/contracts/mocks/ConditionalEscrowMock.sol b/contracts/mocks/ConditionalEscrowMock.sol deleted file mode 100644 index ececf0521..000000000 --- a/contracts/mocks/ConditionalEscrowMock.sol +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../utils/escrow/ConditionalEscrow.sol"; - -// mock class using ConditionalEscrow -contract ConditionalEscrowMock is ConditionalEscrow { - mapping(address => bool) private _allowed; - - function setAllowed(address payee, bool allowed) public { - _allowed[payee] = allowed; - } - - function withdrawalAllowed(address payee) public view override returns (bool) { - return _allowed[payee]; - } -} diff --git a/contracts/mocks/ContextMock.sol b/contracts/mocks/ContextMock.sol index 7759f3506..2e7751d0e 100644 --- a/contracts/mocks/ContextMock.sol +++ b/contracts/mocks/ContextMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/Context.sol"; diff --git a/contracts/mocks/DummyImplementation.sol b/contracts/mocks/DummyImplementation.sol index ddcca6604..71761a755 100644 --- a/contracts/mocks/DummyImplementation.sol +++ b/contracts/mocks/DummyImplementation.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; + +import "../proxy/ERC1967/ERC1967Utils.sol"; abstract contract Impl { function version() public pure virtual returns (string memory); @@ -44,6 +46,11 @@ contract DummyImplementation { function reverts() public pure { require(false, "DummyImplementation reverted"); } + + // Use for forcing an unsafe TransparentUpgradeableProxy admin override + function unsafeOverrideAdmin(address newAdmin) public { + StorageSlot.getAddressSlot(ERC1967Utils.ADMIN_SLOT).value = newAdmin; + } } contract DummyImplementationV2 is DummyImplementation { diff --git a/contracts/mocks/EIP712Verifier.sol b/contracts/mocks/EIP712Verifier.sol index dcef9efbb..ea28162ba 100644 --- a/contracts/mocks/EIP712Verifier.sol +++ b/contracts/mocks/EIP712Verifier.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/cryptography/ECDSA.sol"; import "../utils/cryptography/EIP712.sol"; diff --git a/contracts/mocks/ERC1271WalletMock.sol b/contracts/mocks/ERC1271WalletMock.sol index 015288998..2bc390636 100644 --- a/contracts/mocks/ERC1271WalletMock.sol +++ b/contracts/mocks/ERC1271WalletMock.sol @@ -1,23 +1,21 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../access/Ownable.sol"; import "../interfaces/IERC1271.sol"; import "../utils/cryptography/ECDSA.sol"; contract ERC1271WalletMock is Ownable, IERC1271 { - constructor(address originalOwner) { - transferOwnership(originalOwner); - } + constructor(address originalOwner) Ownable(originalOwner) {} - function isValidSignature(bytes32 hash, bytes memory signature) public view override returns (bytes4 magicValue) { + function isValidSignature(bytes32 hash, bytes memory signature) public view returns (bytes4 magicValue) { return ECDSA.recover(hash, signature) == owner() ? this.isValidSignature.selector : bytes4(0); } } contract ERC1271MaliciousMock is IERC1271 { - function isValidSignature(bytes32, bytes memory) public pure override returns (bytes4) { + function isValidSignature(bytes32, bytes memory) public pure returns (bytes4) { assembly { mstore(0, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff) return(0, 32) diff --git a/contracts/mocks/ERC165/ERC165InterfacesSupported.sol b/contracts/mocks/ERC165/ERC165InterfacesSupported.sol index 7a5e5bc67..d21d7c2d0 100644 --- a/contracts/mocks/ERC165/ERC165InterfacesSupported.sol +++ b/contracts/mocks/ERC165/ERC165InterfacesSupported.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/introspection/IERC165.sol"; diff --git a/contracts/mocks/ERC165/ERC165MaliciousData.sol b/contracts/mocks/ERC165/ERC165MaliciousData.sol index 2446f3df2..2f3b57d66 100644 --- a/contracts/mocks/ERC165/ERC165MaliciousData.sol +++ b/contracts/mocks/ERC165/ERC165MaliciousData.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract ERC165MaliciousData { function supportsInterface(bytes4) public pure returns (bool) { diff --git a/contracts/mocks/ERC165/ERC165MissingData.sol b/contracts/mocks/ERC165/ERC165MissingData.sol index 59cd51ae6..c58ca8fd0 100644 --- a/contracts/mocks/ERC165/ERC165MissingData.sol +++ b/contracts/mocks/ERC165/ERC165MissingData.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract ERC165MissingData { function supportsInterface(bytes4 interfaceId) public view {} // missing return diff --git a/contracts/mocks/ERC165/ERC165NotSupported.sol b/contracts/mocks/ERC165/ERC165NotSupported.sol index 486c7f0a4..9cd21bc2d 100644 --- a/contracts/mocks/ERC165/ERC165NotSupported.sol +++ b/contracts/mocks/ERC165/ERC165NotSupported.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract ERC165NotSupported {} diff --git a/contracts/mocks/ERC165/ERC165ReturnBomb.sol b/contracts/mocks/ERC165/ERC165ReturnBomb.sol index e53235d2c..d2a64151f 100644 --- a/contracts/mocks/ERC165/ERC165ReturnBomb.sol +++ b/contracts/mocks/ERC165/ERC165ReturnBomb.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/introspection/IERC165.sol"; diff --git a/contracts/mocks/ERC2771ContextMock.sol b/contracts/mocks/ERC2771ContextMock.sol index 387df785e..8c2cb43fd 100644 --- a/contracts/mocks/ERC2771ContextMock.sol +++ b/contracts/mocks/ERC2771ContextMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; +pragma solidity ^0.8.19; import "./ContextMock.sol"; import "../metatx/ERC2771Context.sol"; diff --git a/contracts/mocks/ERC3156FlashBorrowerMock.sol b/contracts/mocks/ERC3156FlashBorrowerMock.sol index 6a4410fcb..abf836606 100644 --- a/contracts/mocks/ERC3156FlashBorrowerMock.sol +++ b/contracts/mocks/ERC3156FlashBorrowerMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../token/ERC20/IERC20.sol"; import "../interfaces/IERC3156.sol"; @@ -33,7 +33,7 @@ contract ERC3156FlashBorrowerMock is IERC3156FlashBorrower { uint256 amount, uint256 fee, bytes calldata data - ) public override returns (bytes32) { + ) public returns (bytes32) { require(msg.sender == token); emit BalanceOf(token, address(this), IERC20(token).balanceOf(address(this))); diff --git a/contracts/mocks/EtherReceiverMock.sol b/contracts/mocks/EtherReceiverMock.sol index a11e646fb..d06d35b21 100644 --- a/contracts/mocks/EtherReceiverMock.sol +++ b/contracts/mocks/EtherReceiverMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract EtherReceiverMock { bool private _acceptEther; diff --git a/contracts/mocks/InitializableMock.sol b/contracts/mocks/InitializableMock.sol index 34040b6e5..513aac052 100644 --- a/contracts/mocks/InitializableMock.sol +++ b/contracts/mocks/InitializableMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../proxy/utils/Initializable.sol"; diff --git a/contracts/mocks/MulticallTest.sol b/contracts/mocks/MulticallTest.sol index fcbec6ad8..cf89c58df 100644 --- a/contracts/mocks/MulticallTest.sol +++ b/contracts/mocks/MulticallTest.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./token/ERC20MulticallMock.sol"; @@ -12,7 +12,7 @@ contract MulticallTest { ) external { bytes[] memory calls = new bytes[](recipients.length); for (uint256 i = 0; i < recipients.length; i++) { - calls[i] = abi.encodeWithSignature("transfer(address,uint256)", recipients[i], amounts[i]); + calls[i] = abi.encodeCall(multicallToken.transfer, (recipients[i], amounts[i])); } bytes[] memory results = multicallToken.multicall(calls); diff --git a/contracts/mocks/MultipleInheritanceInitializableMocks.sol b/contracts/mocks/MultipleInheritanceInitializableMocks.sol index e79cd92c8..cb62942ce 100644 --- a/contracts/mocks/MultipleInheritanceInitializableMocks.sol +++ b/contracts/mocks/MultipleInheritanceInitializableMocks.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../proxy/utils/Initializable.sol"; diff --git a/contracts/mocks/PausableMock.sol b/contracts/mocks/PausableMock.sol index 98bcfd593..85d45a3af 100644 --- a/contracts/mocks/PausableMock.sol +++ b/contracts/mocks/PausableMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../security/Pausable.sol"; diff --git a/contracts/mocks/PullPaymentMock.sol b/contracts/mocks/PullPaymentMock.sol deleted file mode 100644 index 8a708e30c..000000000 --- a/contracts/mocks/PullPaymentMock.sol +++ /dev/null @@ -1,15 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../security/PullPayment.sol"; - -// mock class using PullPayment -contract PullPaymentMock is PullPayment { - constructor() payable {} - - // test helper function to call asyncTransfer - function callTransfer(address dest, uint256 amount) public { - _asyncTransfer(dest, amount); - } -} diff --git a/contracts/mocks/ReentrancyAttack.sol b/contracts/mocks/ReentrancyAttack.sol index 4de181205..df2924301 100644 --- a/contracts/mocks/ReentrancyAttack.sol +++ b/contracts/mocks/ReentrancyAttack.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/Context.sol"; contract ReentrancyAttack is Context { - function callSender(bytes4 data) public { - (bool success, ) = _msgSender().call(abi.encodeWithSelector(data)); + function callSender(bytes calldata data) public { + (bool success, ) = _msgSender().call(data); require(success, "ReentrancyAttack: failed call"); } } diff --git a/contracts/mocks/ReentrancyMock.sol b/contracts/mocks/ReentrancyMock.sol index 161e1d3d8..053e53d77 100644 --- a/contracts/mocks/ReentrancyMock.sol +++ b/contracts/mocks/ReentrancyMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../security/ReentrancyGuard.sol"; import "./ReentrancyAttack.sol"; @@ -26,15 +26,14 @@ contract ReentrancyMock is ReentrancyGuard { function countThisRecursive(uint256 n) public nonReentrant { if (n > 0) { _count(); - (bool success, ) = address(this).call(abi.encodeWithSignature("countThisRecursive(uint256)", n - 1)); + (bool success, ) = address(this).call(abi.encodeCall(this.countThisRecursive, (n - 1))); require(success, "ReentrancyMock: failed call"); } } function countAndCall(ReentrancyAttack attacker) public nonReentrant { _count(); - bytes4 func = bytes4(keccak256("callback()")); - attacker.callSender(func); + attacker.callSender(abi.encodeCall(this.callback, ())); } function _count() private { diff --git a/contracts/mocks/RegressionImplementation.sol b/contracts/mocks/RegressionImplementation.sol index be6b501c1..f258bbc0e 100644 --- a/contracts/mocks/RegressionImplementation.sol +++ b/contracts/mocks/RegressionImplementation.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../proxy/utils/Initializable.sol"; diff --git a/contracts/mocks/SafeMathMemoryCheck.sol b/contracts/mocks/SafeMathMemoryCheck.sol deleted file mode 100644 index 96946881a..000000000 --- a/contracts/mocks/SafeMathMemoryCheck.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../utils/math/SafeMath.sol"; - -library SafeMathMemoryCheck { - function addMemoryCheck() internal pure returns (uint256 mem) { - uint256 length = 32; - assembly { - mem := mload(0x40) - } - for (uint256 i = 0; i < length; ++i) { - SafeMath.add(1, 1); - } - assembly { - mem := sub(mload(0x40), mem) - } - } - - function subMemoryCheck() internal pure returns (uint256 mem) { - uint256 length = 32; - assembly { - mem := mload(0x40) - } - for (uint256 i = 0; i < length; ++i) { - SafeMath.sub(1, 1); - } - assembly { - mem := sub(mload(0x40), mem) - } - } - - function mulMemoryCheck() internal pure returns (uint256 mem) { - uint256 length = 32; - assembly { - mem := mload(0x40) - } - for (uint256 i = 0; i < length; ++i) { - SafeMath.mul(1, 1); - } - assembly { - mem := sub(mload(0x40), mem) - } - } - - function divMemoryCheck() internal pure returns (uint256 mem) { - uint256 length = 32; - assembly { - mem := mload(0x40) - } - for (uint256 i = 0; i < length; ++i) { - SafeMath.div(1, 1); - } - assembly { - mem := sub(mload(0x40), mem) - } - } - - function modMemoryCheck() internal pure returns (uint256 mem) { - uint256 length = 32; - assembly { - mem := mload(0x40) - } - for (uint256 i = 0; i < length; ++i) { - SafeMath.mod(1, 1); - } - assembly { - mem := sub(mload(0x40), mem) - } - } -} diff --git a/contracts/mocks/SingleInheritanceInitializableMocks.sol b/contracts/mocks/SingleInheritanceInitializableMocks.sol index 6c82dd20c..2b5fad4eb 100644 --- a/contracts/mocks/SingleInheritanceInitializableMocks.sol +++ b/contracts/mocks/SingleInheritanceInitializableMocks.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../proxy/utils/Initializable.sol"; diff --git a/contracts/mocks/StorageSlotMock.sol b/contracts/mocks/StorageSlotMock.sol index 5d099fca8..62dd23d6a 100644 --- a/contracts/mocks/StorageSlotMock.sol +++ b/contracts/mocks/StorageSlotMock.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/StorageSlot.sol"; contract StorageSlotMock { - using StorageSlot for bytes32; + using StorageSlot for *; function setBoolean(bytes32 slot, bool value) public { slot.getBooleanSlot().value = value; @@ -38,4 +38,40 @@ contract StorageSlotMock { function getUint256(bytes32 slot) public view returns (uint256) { return slot.getUint256Slot().value; } + + mapping(uint256 => string) public stringMap; + + function setString(bytes32 slot, string calldata value) public { + slot.getStringSlot().value = value; + } + + function setStringStorage(uint256 key, string calldata value) public { + stringMap[key].getStringSlot().value = value; + } + + function getString(bytes32 slot) public view returns (string memory) { + return slot.getStringSlot().value; + } + + function getStringStorage(uint256 key) public view returns (string memory) { + return stringMap[key].getStringSlot().value; + } + + mapping(uint256 => bytes) public bytesMap; + + function setBytes(bytes32 slot, bytes calldata value) public { + slot.getBytesSlot().value = value; + } + + function setBytesStorage(uint256 key, bytes calldata value) public { + bytesMap[key].getBytesSlot().value = value; + } + + function getBytes(bytes32 slot) public view returns (bytes memory) { + return slot.getBytesSlot().value; + } + + function getBytesStorage(uint256 key) public view returns (bytes memory) { + return bytesMap[key].getBytesSlot().value; + } } diff --git a/contracts/mocks/TimelockReentrant.sol b/contracts/mocks/TimelockReentrant.sol new file mode 100644 index 000000000..803a2b037 --- /dev/null +++ b/contracts/mocks/TimelockReentrant.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../utils/Address.sol"; + +contract TimelockReentrant { + address private _reenterTarget; + bytes private _reenterData; + bool _reentered; + + function disableReentrancy() external { + _reentered = true; + } + + function enableRentrancy(address target, bytes calldata data) external { + _reenterTarget = target; + _reenterData = data; + } + + function reenter() external { + if (!_reentered) { + _reentered = true; + Address.functionCall(_reenterTarget, _reenterData); + } + } +} diff --git a/contracts/mocks/TimersBlockNumberImpl.sol b/contracts/mocks/TimersBlockNumberImpl.sol deleted file mode 100644 index 84633e6f8..000000000 --- a/contracts/mocks/TimersBlockNumberImpl.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../utils/Timers.sol"; - -contract TimersBlockNumberImpl { - using Timers for Timers.BlockNumber; - - Timers.BlockNumber private _timer; - - function getDeadline() public view returns (uint64) { - return _timer.getDeadline(); - } - - function setDeadline(uint64 timestamp) public { - _timer.setDeadline(timestamp); - } - - function reset() public { - _timer.reset(); - } - - function isUnset() public view returns (bool) { - return _timer.isUnset(); - } - - function isStarted() public view returns (bool) { - return _timer.isStarted(); - } - - function isPending() public view returns (bool) { - return _timer.isPending(); - } - - function isExpired() public view returns (bool) { - return _timer.isExpired(); - } -} diff --git a/contracts/mocks/TimersTimestampImpl.sol b/contracts/mocks/TimersTimestampImpl.sol deleted file mode 100644 index 07f9a1b3f..000000000 --- a/contracts/mocks/TimersTimestampImpl.sol +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../utils/Timers.sol"; - -contract TimersTimestampImpl { - using Timers for Timers.Timestamp; - - Timers.Timestamp private _timer; - - function getDeadline() public view returns (uint64) { - return _timer.getDeadline(); - } - - function setDeadline(uint64 timestamp) public { - _timer.setDeadline(timestamp); - } - - function reset() public { - _timer.reset(); - } - - function isUnset() public view returns (bool) { - return _timer.isUnset(); - } - - function isStarted() public view returns (bool) { - return _timer.isStarted(); - } - - function isPending() public view returns (bool) { - return _timer.isPending(); - } - - function isExpired() public view returns (bool) { - return _timer.isExpired(); - } -} diff --git a/contracts/mocks/VotesMock.sol b/contracts/mocks/VotesMock.sol index 9b035eabc..697d33448 100644 --- a/contracts/mocks/VotesMock.sol +++ b/contracts/mocks/VotesMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../governance/utils/Votes.sol"; @@ -29,3 +29,14 @@ abstract contract VotesMock is Votes { _transferVotingUnits(account, address(0), votes); } } + +abstract contract VotesTimestampMock is VotesMock { + function clock() public view override returns (uint48) { + return uint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + return "mode=timestamp"; + } +} diff --git a/contracts/mocks/compound/CompTimelock.sol b/contracts/mocks/compound/CompTimelock.sol index 49ffa4b77..9dc586ddb 100644 --- a/contracts/mocks/compound/CompTimelock.sol +++ b/contracts/mocks/compound/CompTimelock.sol @@ -24,7 +24,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract CompTimelock { event NewAdmin(address indexed newAdmin); diff --git a/contracts/mocks/crosschain/bridges.sol b/contracts/mocks/crosschain/bridges.sol deleted file mode 100644 index 41baffed8..000000000 --- a/contracts/mocks/crosschain/bridges.sol +++ /dev/null @@ -1,94 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../../utils/Address.sol"; -import "../../vendor/polygon/IFxMessageProcessor.sol"; - -abstract contract BaseRelayMock { - // needed to parse custom errors - error NotCrossChainCall(); - error InvalidCrossChainSender(address sender, address expected); - - address internal _currentSender; - - function relayAs(address target, bytes calldata data, address sender) external virtual { - address previousSender = _currentSender; - - _currentSender = sender; - - (bool success, bytes memory returndata) = target.call(data); - Address.verifyCallResultFromTarget(target, success, returndata, "low-level call reverted"); - - _currentSender = previousSender; - } -} - -/** - * AMB - */ -contract BridgeAMBMock is BaseRelayMock { - function messageSender() public view returns (address) { - return _currentSender; - } -} - -/** - * Arbitrum - */ -contract BridgeArbitrumL1Mock is BaseRelayMock { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment - address public immutable inbox = address(new BridgeArbitrumL1Inbox()); - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment - address public immutable outbox = address(new BridgeArbitrumL1Outbox()); - - function activeOutbox() public view returns (address) { - return outbox; - } - - function currentSender() public view returns (address) { - return _currentSender; - } -} - -contract BridgeArbitrumL1Inbox { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment - address public immutable bridge = msg.sender; -} - -contract BridgeArbitrumL1Outbox { - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment - address public immutable bridge = msg.sender; - - function l2ToL1Sender() public view returns (address) { - return BridgeArbitrumL1Mock(bridge).currentSender(); - } -} - -contract BridgeArbitrumL2Mock is BaseRelayMock { - function wasMyCallersAddressAliased() public view returns (bool) { - return _currentSender != address(0); - } - - function myCallersAddressWithoutAliasing() public view returns (address) { - return _currentSender; - } -} - -/** - * Optimism - */ -contract BridgeOptimismMock is BaseRelayMock { - function xDomainMessageSender() public view returns (address) { - return _currentSender; - } -} - -/** - * Polygon - */ -contract BridgePolygonChildMock is BaseRelayMock { - function relayAs(address target, bytes calldata data, address sender) external override { - IFxMessageProcessor(target).processMessageFromRoot(0, sender, data); - } -} diff --git a/contracts/mocks/crosschain/receivers.sol b/contracts/mocks/crosschain/receivers.sol deleted file mode 100644 index 601a2ac10..000000000 --- a/contracts/mocks/crosschain/receivers.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.4; - -import "../../access/Ownable.sol"; -import "../../crosschain/amb/CrossChainEnabledAMB.sol"; -import "../../crosschain/arbitrum/CrossChainEnabledArbitrumL1.sol"; -import "../../crosschain/arbitrum/CrossChainEnabledArbitrumL2.sol"; -import "../../crosschain/optimism/CrossChainEnabledOptimism.sol"; -import "../../crosschain/polygon/CrossChainEnabledPolygonChild.sol"; - -abstract contract Receiver is CrossChainEnabled { - // we don't use Ownable because it messes up testing for the upgradeable contracts - /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment - address public immutable owner = msg.sender; - - function crossChainRestricted() external onlyCrossChain {} - - function crossChainOwnerRestricted() external onlyCrossChainSender(owner) {} -} - -/** - * AMB - */ -contract CrossChainEnabledAMBMock is Receiver, CrossChainEnabledAMB { - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) CrossChainEnabledAMB(bridge) {} -} - -/** - * Arbitrum - */ -contract CrossChainEnabledArbitrumL1Mock is Receiver, CrossChainEnabledArbitrumL1 { - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) CrossChainEnabledArbitrumL1(bridge) {} -} - -contract CrossChainEnabledArbitrumL2Mock is Receiver, CrossChainEnabledArbitrumL2 {} - -/** - * Optimism - */ -contract CrossChainEnabledOptimismMock is Receiver, CrossChainEnabledOptimism { - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) CrossChainEnabledOptimism(bridge) {} -} - -/** - * Polygon - */ -contract CrossChainEnabledPolygonChildMock is Receiver, CrossChainEnabledPolygonChild { - /// @custom:oz-upgrades-unsafe-allow constructor - constructor(address bridge) CrossChainEnabledPolygonChild(bridge) {} -} diff --git a/contracts/mocks/docs/ERC4626Fees.sol b/contracts/mocks/docs/ERC4626Fees.sol new file mode 100644 index 000000000..703e4245a --- /dev/null +++ b/contracts/mocks/docs/ERC4626Fees.sol @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../../token/ERC20/extensions/ERC4626.sol"; + +abstract contract ERC4626Fees is ERC4626 { + using Math for uint256; + + /** @dev See {IERC4626-previewDeposit}. */ + function previewDeposit(uint256 assets) public view virtual override returns (uint256) { + uint256 fee = _feeOnTotal(assets, _entryFeeBasePoint()); + return super.previewDeposit(assets - fee); + } + + /** @dev See {IERC4626-previewMint}. */ + function previewMint(uint256 shares) public view virtual override returns (uint256) { + uint256 assets = super.previewMint(shares); + return assets + _feeOnRaw(assets, _entryFeeBasePoint()); + } + + /** @dev See {IERC4626-previewWithdraw}. */ + function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { + uint256 fee = _feeOnRaw(assets, _exitFeeBasePoint()); + return super.previewWithdraw(assets + fee); + } + + /** @dev See {IERC4626-previewRedeem}. */ + function previewRedeem(uint256 shares) public view virtual override returns (uint256) { + uint256 assets = super.previewRedeem(shares); + return assets - _feeOnTotal(assets, _exitFeeBasePoint()); + } + + /** @dev See {IERC4626-_deposit}. */ + function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual override { + uint256 fee = _feeOnTotal(assets, _entryFeeBasePoint()); + address recipient = _entryFeeRecipient(); + + super._deposit(caller, receiver, assets, shares); + + if (fee > 0 && recipient != address(this)) { + SafeERC20.safeTransfer(IERC20(asset()), recipient, fee); + } + } + + /** @dev See {IERC4626-_deposit}. */ + function _withdraw( + address caller, + address receiver, + address owner, + uint256 assets, + uint256 shares + ) internal virtual override { + uint256 fee = _feeOnRaw(assets, _exitFeeBasePoint()); + address recipient = _exitFeeRecipient(); + + super._withdraw(caller, receiver, owner, assets, shares); + + if (fee > 0 && recipient != address(this)) { + SafeERC20.safeTransfer(IERC20(asset()), recipient, fee); + } + } + + function _entryFeeBasePoint() internal view virtual returns (uint256) { + return 0; + } + + function _entryFeeRecipient() internal view virtual returns (address) { + return address(0); + } + + function _exitFeeBasePoint() internal view virtual returns (uint256) { + return 0; + } + + function _exitFeeRecipient() internal view virtual returns (address) { + return address(0); + } + + function _feeOnRaw(uint256 assets, uint256 feeBasePoint) private pure returns (uint256) { + return assets.mulDiv(feeBasePoint, 1e5, Math.Rounding.Up); + } + + function _feeOnTotal(uint256 assets, uint256 feeBasePoint) private pure returns (uint256) { + return assets.mulDiv(feeBasePoint, feeBasePoint + 1e5, Math.Rounding.Up); + } +} diff --git a/contracts/mocks/wizard/MyGovernor3.sol b/contracts/mocks/docs/governance/MyGovernor.sol similarity index 70% rename from contracts/mocks/wizard/MyGovernor3.sol rename to contracts/mocks/docs/governance/MyGovernor.sol index 320342290..095523b3d 100644 --- a/contracts/mocks/wizard/MyGovernor3.sol +++ b/contracts/mocks/docs/governance/MyGovernor.sol @@ -1,18 +1,18 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; +pragma solidity ^0.8.19; -import "../../governance/Governor.sol"; -import "../../governance/compatibility/GovernorCompatibilityBravo.sol"; -import "../../governance/extensions/GovernorVotes.sol"; -import "../../governance/extensions/GovernorVotesQuorumFraction.sol"; -import "../../governance/extensions/GovernorTimelockControl.sol"; +import "../../../governance/Governor.sol"; +import "../../../governance/compatibility/GovernorCompatibilityBravo.sol"; +import "../../../governance/extensions/GovernorVotes.sol"; +import "../../../governance/extensions/GovernorVotesQuorumFraction.sol"; +import "../../../governance/extensions/GovernorTimelockControl.sol"; contract MyGovernor is Governor, - GovernorTimelockControl, GovernorCompatibilityBravo, GovernorVotes, - GovernorVotesQuorumFraction + GovernorVotesQuorumFraction, + GovernorTimelockControl { constructor( IVotes _token, @@ -20,24 +20,18 @@ contract MyGovernor is ) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) GovernorTimelockControl(_timelock) {} function votingDelay() public pure override returns (uint256) { - return 1; // 1 block + return 7200; // 1 day } function votingPeriod() public pure override returns (uint256) { - return 45818; // 1 week + return 50400; // 1 week } function proposalThreshold() public pure override returns (uint256) { - return 1000e18; + return 0; } - // The following functions are overrides required by Solidity. - - function quorum( - uint256 blockNumber - ) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) { - return super.quorum(blockNumber); - } + // The functions below are overrides required by Solidity. function state( uint256 proposalId @@ -54,6 +48,15 @@ contract MyGovernor is return super.propose(targets, values, calldatas, description); } + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) { + return super.cancel(targets, values, calldatas, descriptionHash); + } + function _execute( uint256 proposalId, address[] memory targets, diff --git a/contracts/mocks/docs/governance/MyToken.sol b/contracts/mocks/docs/governance/MyToken.sol new file mode 100644 index 000000000..f7707ff9d --- /dev/null +++ b/contracts/mocks/docs/governance/MyToken.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../../token/ERC20/ERC20.sol"; +import "../../../token/ERC20/extensions/ERC20Permit.sol"; +import "../../../token/ERC20/extensions/ERC20Votes.sol"; + +contract MyToken is ERC20, ERC20Permit, ERC20Votes { + constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {} + + // The functions below are overrides required by Solidity. + + function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) { + super._update(from, to, amount); + } + + function nonces(address owner) public view virtual override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} diff --git a/contracts/mocks/docs/governance/MyTokenTimestampBased.sol b/contracts/mocks/docs/governance/MyTokenTimestampBased.sol new file mode 100644 index 000000000..1c688c250 --- /dev/null +++ b/contracts/mocks/docs/governance/MyTokenTimestampBased.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../../token/ERC20/ERC20.sol"; +import "../../../token/ERC20/extensions/ERC20Permit.sol"; +import "../../../token/ERC20/extensions/ERC20Votes.sol"; + +contract MyTokenTimestampBased is ERC20, ERC20Permit, ERC20Votes { + constructor() ERC20("MyTokenTimestampBased", "MTK") ERC20Permit("MyTokenTimestampBased") {} + + // Overrides IERC6372 functions to make the token & governor timestamp-based + + function clock() public view override returns (uint48) { + return uint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public pure override returns (string memory) { + return "mode=timestamp"; + } + + // The functions below are overrides required by Solidity. + + function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) { + super._update(from, to, amount); + } + + function nonces(address owner) public view virtual override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} diff --git a/contracts/mocks/docs/governance/MyTokenWrapped.sol b/contracts/mocks/docs/governance/MyTokenWrapped.sol new file mode 100644 index 000000000..6637dccc3 --- /dev/null +++ b/contracts/mocks/docs/governance/MyTokenWrapped.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../../token/ERC20/ERC20.sol"; +import "../../../token/ERC20/extensions/ERC20Permit.sol"; +import "../../../token/ERC20/extensions/ERC20Votes.sol"; +import "../../../token/ERC20/extensions/ERC20Wrapper.sol"; + +contract MyTokenWrapped is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { + constructor( + IERC20 wrappedToken + ) ERC20("MyTokenWrapped", "MTK") ERC20Permit("MyTokenWrapped") ERC20Wrapper(wrappedToken) {} + + // The functions below are overrides required by Solidity. + + function decimals() public view override(ERC20, ERC20Wrapper) returns (uint8) { + return super.decimals(); + } + + function _update(address from, address to, uint256 amount) internal override(ERC20, ERC20Votes) { + super._update(from, to, amount); + } + + function nonces(address owner) public view virtual override(ERC20Permit, Nonces) returns (uint256) { + return super.nonces(owner); + } +} diff --git a/contracts/mocks/governance/GovernorCompMock.sol b/contracts/mocks/governance/GovernorCompMock.sol deleted file mode 100644 index cc368c42f..000000000 --- a/contracts/mocks/governance/GovernorCompMock.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../../governance/extensions/GovernorCountingSimple.sol"; -import "../../governance/extensions/GovernorVotesComp.sol"; - -abstract contract GovernorCompMock is GovernorVotesComp, GovernorCountingSimple { - function quorum(uint256) public pure override returns (uint256) { - return 0; - } - - function votingDelay() public pure override returns (uint256) { - return 4; - } - - function votingPeriod() public pure override returns (uint256) { - return 16; - } -} diff --git a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol index 4356bce7f..6e14a357d 100644 --- a/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol +++ b/contracts/mocks/governance/GovernorCompatibilityBravoMock.sol @@ -1,17 +1,17 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/compatibility/GovernorCompatibilityBravo.sol"; import "../../governance/extensions/GovernorTimelockCompound.sol"; import "../../governance/extensions/GovernorSettings.sol"; -import "../../governance/extensions/GovernorVotesComp.sol"; +import "../../governance/extensions/GovernorVotes.sol"; abstract contract GovernorCompatibilityBravoMock is GovernorCompatibilityBravo, GovernorSettings, GovernorTimelockCompound, - GovernorVotesComp + GovernorVotes { function quorum(uint256) public pure override returns (uint256) { return 0; @@ -66,6 +66,15 @@ abstract contract GovernorCompatibilityBravoMock is return super.execute(targets, values, calldatas, salt); } + function cancel( + address[] memory targets, + uint256[] memory values, + bytes[] memory calldatas, + bytes32 descriptionHash + ) public override(Governor, GovernorCompatibilityBravo, IGovernor) returns (uint256) { + return super.cancel(targets, values, calldatas, descriptionHash); + } + function _execute( uint256 proposalId, address[] memory targets, diff --git a/contracts/mocks/governance/GovernorMock.sol b/contracts/mocks/governance/GovernorMock.sol index 8a1db4704..9104c3f32 100644 --- a/contracts/mocks/governance/GovernorMock.sol +++ b/contracts/mocks/governance/GovernorMock.sol @@ -1,28 +1,13 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import "../../governance/extensions/GovernorProposalThreshold.sol"; import "../../governance/extensions/GovernorSettings.sol"; import "../../governance/extensions/GovernorCountingSimple.sol"; import "../../governance/extensions/GovernorVotesQuorumFraction.sol"; -abstract contract GovernorMock is - GovernorProposalThreshold, - GovernorSettings, - GovernorVotesQuorumFraction, - GovernorCountingSimple -{ +abstract contract GovernorMock is GovernorSettings, GovernorVotesQuorumFraction, GovernorCountingSimple { function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { return super.proposalThreshold(); } - - function propose( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - string memory description - ) public override(Governor, GovernorProposalThreshold) returns (uint256) { - return super.propose(targets, values, calldatas, description); - } } diff --git a/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol b/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol index 79d894896..4c1b408e6 100644 --- a/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol +++ b/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/extensions/GovernorPreventLateQuorum.sol"; import "../../governance/extensions/GovernorSettings.sol"; diff --git a/contracts/mocks/governance/GovernorTimelockCompoundMock.sol b/contracts/mocks/governance/GovernorTimelockCompoundMock.sol index b37462819..134d66133 100644 --- a/contracts/mocks/governance/GovernorTimelockCompoundMock.sol +++ b/contracts/mocks/governance/GovernorTimelockCompoundMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/extensions/GovernorTimelockCompound.sol"; import "../../governance/extensions/GovernorSettings.sol"; diff --git a/contracts/mocks/governance/GovernorTimelockControlMock.sol b/contracts/mocks/governance/GovernorTimelockControlMock.sol index 06309145a..28376835a 100644 --- a/contracts/mocks/governance/GovernorTimelockControlMock.sol +++ b/contracts/mocks/governance/GovernorTimelockControlMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/extensions/GovernorTimelockControl.sol"; import "../../governance/extensions/GovernorSettings.sol"; diff --git a/contracts/mocks/governance/GovernorVoteMock.sol b/contracts/mocks/governance/GovernorVoteMock.sol index 9b533bddf..1d0722ecf 100644 --- a/contracts/mocks/governance/GovernorVoteMock.sol +++ b/contracts/mocks/governance/GovernorVoteMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/extensions/GovernorCountingSimple.sol"; import "../../governance/extensions/GovernorVotes.sol"; diff --git a/contracts/mocks/governance/GovernorWithParamsMock.sol b/contracts/mocks/governance/GovernorWithParamsMock.sol index 361c2873e..f8de83710 100644 --- a/contracts/mocks/governance/GovernorWithParamsMock.sol +++ b/contracts/mocks/governance/GovernorWithParamsMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../governance/extensions/GovernorCountingSimple.sol"; import "../../governance/extensions/GovernorVotes.sol"; diff --git a/contracts/mocks/proxy/BadBeacon.sol b/contracts/mocks/proxy/BadBeacon.sol index bedcfed84..2c8ffe291 100644 --- a/contracts/mocks/proxy/BadBeacon.sol +++ b/contracts/mocks/proxy/BadBeacon.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; contract BadBeaconNoImpl {} diff --git a/contracts/mocks/proxy/ClashingImplementation.sol b/contracts/mocks/proxy/ClashingImplementation.sol index 80aca0c29..89904b91f 100644 --- a/contracts/mocks/proxy/ClashingImplementation.sol +++ b/contracts/mocks/proxy/ClashingImplementation.sol @@ -1,15 +1,16 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** - * @dev Implementation contract with an admin() function made to clash with - * @dev TransparentUpgradeableProxy's to test correct functioning of the - * @dev Transparent Proxy feature. + * @dev Implementation contract with a payable changeAdmin(address) function made to clash with + * TransparentUpgradeableProxy's to test correct functioning of the Transparent Proxy feature. */ contract ClashingImplementation { - function admin() external pure returns (address) { - return 0x0000000000000000000000000000000011111142; + event ClashingImplementationCall(); + + function upgradeTo(address) external payable { + emit ClashingImplementationCall(); } function delegatedFunction() external pure returns (bool) { diff --git a/contracts/mocks/proxy/UUPSLegacy.sol b/contracts/mocks/proxy/UUPSLegacy.sol deleted file mode 100644 index ba68e6046..000000000 --- a/contracts/mocks/proxy/UUPSLegacy.sol +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.1; - -import "./UUPSUpgradeableMock.sol"; - -// This contract implements the pre-4.5 UUPS upgrade function with a rollback test. -// It's used to test that newer UUPS contracts are considered valid upgrades by older UUPS contracts. -contract UUPSUpgradeableLegacyMock is UUPSUpgradeableMock { - // Inlined from ERC1967Upgrade - bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; - - // ERC1967Upgrade._setImplementation is private so we reproduce it here. - // An extra underscore prevents a name clash error. - function __setImplementation(address newImplementation) private { - require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract"); - StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; - } - - function _upgradeToAndCallSecureLegacyV1(address newImplementation, bytes memory data, bool forceCall) internal { - address oldImplementation = _getImplementation(); - - // Initial upgrade and setup call - __setImplementation(newImplementation); - if (data.length > 0 || forceCall) { - Address.functionDelegateCall(newImplementation, data); - } - - // Perform rollback test if not already in progress - StorageSlot.BooleanSlot storage rollbackTesting = StorageSlot.getBooleanSlot(_ROLLBACK_SLOT); - if (!rollbackTesting.value) { - // Trigger rollback using upgradeTo from the new implementation - rollbackTesting.value = true; - Address.functionDelegateCall( - newImplementation, - abi.encodeWithSignature("upgradeTo(address)", oldImplementation) - ); - rollbackTesting.value = false; - // Check rollback was effective - require(oldImplementation == _getImplementation(), "ERC1967Upgrade: upgrade breaks further upgrades"); - // Finally reset to the new implementation and log the upgrade - _upgradeTo(newImplementation); - } - } - - // hooking into the old mechanism - function upgradeTo(address newImplementation) external override { - _upgradeToAndCallSecureLegacyV1(newImplementation, bytes(""), false); - } - - function upgradeToAndCall(address newImplementation, bytes memory data) external payable override { - _upgradeToAndCallSecureLegacyV1(newImplementation, data, false); - } -} diff --git a/contracts/mocks/proxy/UUPSUpgradeableMock.sol b/contracts/mocks/proxy/UUPSUpgradeableMock.sol index deb20a000..77744998b 100644 --- a/contracts/mocks/proxy/UUPSUpgradeableMock.sol +++ b/contracts/mocks/proxy/UUPSUpgradeableMock.sol @@ -1,19 +1,18 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../proxy/utils/UUPSUpgradeable.sol"; -import "../../utils/Counters.sol"; contract NonUpgradeableMock { - Counters.Counter internal _counter; + uint256 internal _counter; function current() external view returns (uint256) { - return Counters.current(_counter); + return _counter; } function increment() external { - return Counters.increment(_counter); + ++_counter; } } @@ -23,11 +22,17 @@ contract UUPSUpgradeableMock is NonUpgradeableMock, UUPSUpgradeable { } contract UUPSUpgradeableUnsafeMock is UUPSUpgradeableMock { - function upgradeTo(address newImplementation) external override { - ERC1967Upgrade._upgradeToAndCall(newImplementation, bytes(""), false); + function upgradeTo(address newImplementation) public override { + ERC1967Utils.upgradeToAndCall(newImplementation, bytes(""), false); } - function upgradeToAndCall(address newImplementation, bytes memory data) external payable override { - ERC1967Upgrade._upgradeToAndCall(newImplementation, data, false); + function upgradeToAndCall(address newImplementation, bytes memory data) public payable override { + ERC1967Utils.upgradeToAndCall(newImplementation, data, false); + } +} + +contract UUPSUnsupportedProxiableUUID is UUPSUpgradeableMock { + function proxiableUUID() external pure override returns (bytes32) { + return keccak256("invalid UUID"); } } diff --git a/contracts/mocks/token/ERC1155ReceiverMock.sol b/contracts/mocks/token/ERC1155ReceiverMock.sol index 317d72425..a5d7233cf 100644 --- a/contracts/mocks/token/ERC1155ReceiverMock.sol +++ b/contracts/mocks/token/ERC1155ReceiverMock.sol @@ -1,24 +1,31 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC1155/IERC1155Receiver.sol"; import "../../utils/introspection/ERC165.sol"; contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { - bytes4 private _recRetval; - bool private _recReverts; - bytes4 private _batRetval; - bool private _batReverts; + enum RevertType { + None, + RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, + Panic + } + + bytes4 private immutable _recRetval; + bytes4 private immutable _batRetval; + RevertType private immutable _error; event Received(address operator, address from, uint256 id, uint256 value, bytes data, uint256 gas); event BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data, uint256 gas); + error CustomError(bytes4); - constructor(bytes4 recRetval, bool recReverts, bytes4 batRetval, bool batReverts) { + constructor(bytes4 recRetval, bytes4 batRetval, RevertType error) { _recRetval = recRetval; - _recReverts = recReverts; _batRetval = batRetval; - _batReverts = batReverts; + _error = error; } function onERC1155Received( @@ -27,8 +34,18 @@ contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { uint256 id, uint256 value, bytes calldata data - ) external override returns (bytes4) { - require(!_recReverts, "ERC1155ReceiverMock: reverting on receive"); + ) external returns (bytes4) { + if (_error == RevertType.RevertWithoutMessage) { + revert(); + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC1155ReceiverMock: reverting on receive"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_recRetval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; + } + emit Received(operator, from, id, value, data, gasleft()); return _recRetval; } @@ -39,8 +56,18 @@ contract ERC1155ReceiverMock is ERC165, IERC1155Receiver { uint256[] calldata ids, uint256[] calldata values, bytes calldata data - ) external override returns (bytes4) { - require(!_batReverts, "ERC1155ReceiverMock: reverting on batch receive"); + ) external returns (bytes4) { + if (_error == RevertType.RevertWithoutMessage) { + revert(); + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC1155ReceiverMock: reverting on batch receive"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_recRetval); + } else if (_error == RevertType.Panic) { + uint256 a = uint256(0) / uint256(0); + a; + } + emit BatchReceived(operator, from, ids, values, data, gasleft()); return _batRetval; } diff --git a/contracts/mocks/token/ERC20ApprovalMock.sol b/contracts/mocks/token/ERC20ApprovalMock.sol new file mode 100644 index 000000000..da8f51e1e --- /dev/null +++ b/contracts/mocks/token/ERC20ApprovalMock.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../token/ERC20/ERC20.sol"; + +abstract contract ERC20ApprovalMock is ERC20 { + function _approve(address owner, address spender, uint256 amount, bool) internal virtual override { + super._approve(owner, spender, amount, true); + } +} diff --git a/contracts/mocks/token/ERC20DecimalsMock.sol b/contracts/mocks/token/ERC20DecimalsMock.sol index 32f287470..5699d31d2 100644 --- a/contracts/mocks/token/ERC20DecimalsMock.sol +++ b/contracts/mocks/token/ERC20DecimalsMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; diff --git a/contracts/mocks/token/ERC20ExcessDecimalsMock.sol b/contracts/mocks/token/ERC20ExcessDecimalsMock.sol new file mode 100644 index 000000000..bed8c5fcf --- /dev/null +++ b/contracts/mocks/token/ERC20ExcessDecimalsMock.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +contract ERC20ExcessDecimalsMock { + function decimals() public pure returns (uint256) { + return type(uint256).max; + } +} diff --git a/contracts/mocks/token/ERC20FlashMintMock.sol b/contracts/mocks/token/ERC20FlashMintMock.sol index b4de7b771..05c4db57d 100644 --- a/contracts/mocks/token/ERC20FlashMintMock.sol +++ b/contracts/mocks/token/ERC20FlashMintMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/extensions/ERC20FlashMint.sol"; diff --git a/contracts/mocks/token/ERC20ForceApproveMock.sol b/contracts/mocks/token/ERC20ForceApproveMock.sol new file mode 100644 index 000000000..42b417f41 --- /dev/null +++ b/contracts/mocks/token/ERC20ForceApproveMock.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../../token/ERC20/ERC20.sol"; + +// contract that replicate USDT (0xdac17f958d2ee523a2206206994597c13d831ec7) approval beavior +abstract contract ERC20ForceApproveMock is ERC20 { + function approve(address spender, uint256 amount) public virtual override returns (bool) { + require(amount == 0 || allowance(msg.sender, spender) == 0, "USDT approval failure"); + return super.approve(spender, amount); + } +} diff --git a/contracts/mocks/ERC20Mock.sol b/contracts/mocks/token/ERC20Mock.sol similarity index 81% rename from contracts/mocks/ERC20Mock.sol rename to contracts/mocks/token/ERC20Mock.sol index cc75923df..5fb134f60 100644 --- a/contracts/mocks/ERC20Mock.sol +++ b/contracts/mocks/token/ERC20Mock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import {ERC20} from "../token/ERC20/ERC20.sol"; +import "../../token/ERC20/ERC20.sol"; contract ERC20Mock is ERC20 { constructor() ERC20("ERC20Mock", "E20M") {} diff --git a/contracts/mocks/token/ERC20MulticallMock.sol b/contracts/mocks/token/ERC20MulticallMock.sol index 145e97a62..b8259a75a 100644 --- a/contracts/mocks/token/ERC20MulticallMock.sol +++ b/contracts/mocks/token/ERC20MulticallMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; import "../../utils/Multicall.sol"; diff --git a/contracts/mocks/token/ERC20NoReturnMock.sol b/contracts/mocks/token/ERC20NoReturnMock.sol index 023dab2cb..7d597ad89 100644 --- a/contracts/mocks/token/ERC20NoReturnMock.sol +++ b/contracts/mocks/token/ERC20NoReturnMock.sol @@ -1,21 +1,28 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -contract ERC20NoReturnMock { - mapping(address => uint256) private _allowances; +import "../../token/ERC20/ERC20.sol"; - function transfer(address, uint256) public {} - - function transferFrom(address, address, uint256) public {} - - function approve(address, uint256) public {} - - function setAllowance(address account, uint256 allowance_) public { - _allowances[account] = allowance_; +abstract contract ERC20NoReturnMock is ERC20 { + function transfer(address to, uint256 amount) public override returns (bool) { + super.transfer(to, amount); + assembly { + return(0, 0) + } } - function allowance(address owner, address) public view returns (uint256) { - return _allowances[owner]; + function transferFrom(address from, address to, uint256 amount) public override returns (bool) { + super.transferFrom(from, to, amount); + assembly { + return(0, 0) + } + } + + function approve(address spender, uint256 amount) public override returns (bool) { + super.approve(spender, amount); + assembly { + return(0, 0) + } } } diff --git a/contracts/mocks/token/ERC20PermitNoRevertMock.sol b/contracts/mocks/token/ERC20PermitNoRevertMock.sol index 2b6e2503b..510176cf5 100644 --- a/contracts/mocks/token/ERC20PermitNoRevertMock.sol +++ b/contracts/mocks/token/ERC20PermitNoRevertMock.sol @@ -1,13 +1,11 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC20/ERC20.sol"; -import "../../token/ERC20/extensions/draft-ERC20Permit.sol"; - -contract ERC20PermitNoRevertMock is ERC20, ERC20Permit { - constructor() ERC20("ERC20PermitNoRevertMock", "ERC20PermitNoRevertMock") ERC20Permit("ERC20PermitNoRevertMock") {} +import "../../token/ERC20/extensions/ERC20Permit.sol"; +abstract contract ERC20PermitNoRevertMock is ERC20Permit { function permitThatMayRevert( address owner, address spender, diff --git a/contracts/mocks/token/ERC20Reentrant.sol b/contracts/mocks/token/ERC20Reentrant.sol new file mode 100644 index 000000000..00ba74260 --- /dev/null +++ b/contracts/mocks/token/ERC20Reentrant.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "../../token/ERC20/ERC20.sol"; +import "../../utils/Address.sol"; + +contract ERC20Reentrant is ERC20("TEST", "TST") { + enum Type { + No, + Before, + After + } + + Type private _reenterType; + address private _reenterTarget; + bytes private _reenterData; + + function scheduleReenter(Type when, address target, bytes calldata data) external { + _reenterType = when; + _reenterTarget = target; + _reenterData = data; + } + + function functionCall(address target, bytes memory data) public returns (bytes memory) { + return Address.functionCall(target, data); + } + + function _update(address from, address to, uint256 amount) internal override { + if (_reenterType == Type.Before) { + _reenterType = Type.No; + functionCall(_reenterTarget, _reenterData); + } + super._update(from, to, amount); + if (_reenterType == Type.After) { + _reenterType = Type.No; + functionCall(_reenterTarget, _reenterData); + } + } +} diff --git a/contracts/mocks/token/ERC20ReturnFalseMock.sol b/contracts/mocks/token/ERC20ReturnFalseMock.sol index 3f8f64037..763a120e9 100644 --- a/contracts/mocks/token/ERC20ReturnFalseMock.sol +++ b/contracts/mocks/token/ERC20ReturnFalseMock.sol @@ -1,27 +1,19 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -contract ERC20ReturnFalseMock { - mapping(address => uint256) private _allowances; +import "../../token/ERC20/ERC20.sol"; - function transfer(address, uint256) public pure returns (bool) { +abstract contract ERC20ReturnFalseMock is ERC20 { + function transfer(address, uint256) public pure override returns (bool) { return false; } - function transferFrom(address, address, uint256) public pure returns (bool) { + function transferFrom(address, address, uint256) public pure override returns (bool) { return false; } - function approve(address, uint256) public pure returns (bool) { + function approve(address, uint256) public pure override returns (bool) { return false; } - - function setAllowance(address account, uint256 allowance_) public { - _allowances[account] = allowance_; - } - - function allowance(address owner, address) public view returns (uint256) { - return _allowances[owner]; - } } diff --git a/contracts/mocks/token/ERC20ReturnTrueMock.sol b/contracts/mocks/token/ERC20ReturnTrueMock.sol deleted file mode 100644 index 8953aad64..000000000 --- a/contracts/mocks/token/ERC20ReturnTrueMock.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -contract ERC20ReturnTrueMock { - mapping(address => uint256) private _allowances; - - function transfer(address, uint256) public pure returns (bool) { - return true; - } - - function transferFrom(address, address, uint256) public pure returns (bool) { - return true; - } - - function approve(address, uint256) public pure returns (bool) { - return true; - } - - function setAllowance(address account, uint256 allowance_) public { - _allowances[account] = allowance_; - } - - function allowance(address owner, address) public view returns (uint256) { - return _allowances[owner]; - } -} diff --git a/contracts/mocks/token/ERC20VotesLegacyMock.sol b/contracts/mocks/token/ERC20VotesLegacyMock.sol new file mode 100644 index 000000000..88ba84236 --- /dev/null +++ b/contracts/mocks/token/ERC20VotesLegacyMock.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../../token/ERC20/extensions/ERC20Permit.sol"; +import "../../utils/math/Math.sol"; +import "../../governance/utils/IVotes.sol"; +import "../../utils/math/SafeCast.sol"; +import "../../utils/cryptography/ECDSA.sol"; + +/** + * @dev Copied from the master branch at commit 86de1e8b6c3fa6b4efa4a5435869d2521be0f5f5 + */ +abstract contract ERC20VotesLegacyMock is IVotes, ERC20Permit { + struct Checkpoint { + uint32 fromBlock; + uint224 votes; + } + + bytes32 private constant _DELEGATION_TYPEHASH = + keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); + + mapping(address => address) private _delegates; + mapping(address => Checkpoint[]) private _checkpoints; + Checkpoint[] private _totalSupplyCheckpoints; + + /** + * @dev Get the `pos`-th checkpoint for `account`. + */ + function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoint memory) { + return _checkpoints[account][pos]; + } + + /** + * @dev Get number of checkpoints for `account`. + */ + function numCheckpoints(address account) public view virtual returns (uint32) { + return SafeCast.toUint32(_checkpoints[account].length); + } + + /** + * @dev Get the address `account` is currently delegating to. + */ + function delegates(address account) public view virtual returns (address) { + return _delegates[account]; + } + + /** + * @dev Gets the current votes balance for `account` + */ + function getVotes(address account) public view virtual returns (uint256) { + uint256 pos = _checkpoints[account].length; + unchecked { + return pos == 0 ? 0 : _checkpoints[account][pos - 1].votes; + } + } + + /** + * @dev Retrieve the number of votes for `account` at the end of `blockNumber`. + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastVotes(address account, uint256 blockNumber) public view virtual returns (uint256) { + require(blockNumber < block.number, "ERC20Votes: block not yet mined"); + return _checkpointsLookup(_checkpoints[account], blockNumber); + } + + /** + * @dev Retrieve the `totalSupply` at the end of `blockNumber`. Note, this value is the sum of all balances. + * It is NOT the sum of all the delegated votes! + * + * Requirements: + * + * - `blockNumber` must have been already mined + */ + function getPastTotalSupply(uint256 blockNumber) public view virtual returns (uint256) { + require(blockNumber < block.number, "ERC20Votes: block not yet mined"); + return _checkpointsLookup(_totalSupplyCheckpoints, blockNumber); + } + + /** + * @dev Lookup a value in a list of (sorted) checkpoints. + */ + function _checkpointsLookup(Checkpoint[] storage ckpts, uint256 blockNumber) private view returns (uint256) { + // We run a binary search to look for the earliest checkpoint taken after `blockNumber`. + // + // Initially we check if the block is recent to narrow the search range. + // During the loop, the index of the wanted checkpoint remains in the range [low-1, high). + // With each iteration, either `low` or `high` is moved towards the middle of the range to maintain the invariant. + // - If the middle checkpoint is after `blockNumber`, we look in [low, mid) + // - If the middle checkpoint is before or equal to `blockNumber`, we look in [mid+1, high) + // Once we reach a single value (when low == high), we've found the right checkpoint at the index high-1, if not + // out of bounds (in which case we're looking too far in the past and the result is 0). + // Note that if the latest checkpoint available is exactly for `blockNumber`, we end up with an index that is + // past the end of the array, so we technically don't find a checkpoint after `blockNumber`, but it works out + // the same. + uint256 length = ckpts.length; + + uint256 low = 0; + uint256 high = length; + + if (length > 5) { + uint256 mid = length - Math.sqrt(length); + if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + while (low < high) { + uint256 mid = Math.average(low, high); + if (_unsafeAccess(ckpts, mid).fromBlock > blockNumber) { + high = mid; + } else { + low = mid + 1; + } + } + + unchecked { + return high == 0 ? 0 : _unsafeAccess(ckpts, high - 1).votes; + } + } + + /** + * @dev Delegate votes from the sender to `delegatee`. + */ + function delegate(address delegatee) public virtual { + _delegate(_msgSender(), delegatee); + } + + /** + * @dev Delegates votes from signer to `delegatee` + */ + function delegateBySig( + address delegatee, + uint256 nonce, + uint256 expiry, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual { + require(block.timestamp <= expiry, "ERC20Votes: signature expired"); + address signer = ECDSA.recover( + _hashTypedDataV4(keccak256(abi.encode(_DELEGATION_TYPEHASH, delegatee, nonce, expiry))), + v, + r, + s + ); + require(nonce == _useNonce(signer), "ERC20Votes: invalid nonce"); + _delegate(signer, delegatee); + } + + /** + * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). + */ + function _maxSupply() internal view virtual returns (uint224) { + return type(uint224).max; + } + + /** + * @dev Move voting power when tokens are transferred. + * + * Emits a {IVotes-DelegateVotesChanged} event. + */ + function _update(address from, address to, uint256 amount) internal virtual override { + super._update(from, to, amount); + + if (from == address(0)) { + require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes"); + _writeCheckpoint(_totalSupplyCheckpoints, _add, amount); + } + + if (to == address(0)) { + _writeCheckpoint(_totalSupplyCheckpoints, _subtract, amount); + } + + _moveVotingPower(delegates(from), delegates(to), amount); + } + + /** + * @dev Change delegation for `delegator` to `delegatee`. + * + * Emits events {IVotes-DelegateChanged} and {IVotes-DelegateVotesChanged}. + */ + function _delegate(address delegator, address delegatee) internal virtual { + address currentDelegate = delegates(delegator); + uint256 delegatorBalance = balanceOf(delegator); + _delegates[delegator] = delegatee; + + emit DelegateChanged(delegator, currentDelegate, delegatee); + + _moveVotingPower(currentDelegate, delegatee, delegatorBalance); + } + + function _moveVotingPower(address src, address dst, uint256 amount) private { + if (src != dst && amount > 0) { + if (src != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[src], _subtract, amount); + emit DelegateVotesChanged(src, oldWeight, newWeight); + } + + if (dst != address(0)) { + (uint256 oldWeight, uint256 newWeight) = _writeCheckpoint(_checkpoints[dst], _add, amount); + emit DelegateVotesChanged(dst, oldWeight, newWeight); + } + } + } + + function _writeCheckpoint( + Checkpoint[] storage ckpts, + function(uint256, uint256) view returns (uint256) op, + uint256 delta + ) private returns (uint256 oldWeight, uint256 newWeight) { + uint256 pos = ckpts.length; + + unchecked { + Checkpoint memory oldCkpt = pos == 0 ? Checkpoint(0, 0) : _unsafeAccess(ckpts, pos - 1); + + oldWeight = oldCkpt.votes; + newWeight = op(oldWeight, delta); + + if (pos > 0 && oldCkpt.fromBlock == block.number) { + _unsafeAccess(ckpts, pos - 1).votes = SafeCast.toUint224(newWeight); + } else { + ckpts.push( + Checkpoint({fromBlock: SafeCast.toUint32(block.number), votes: SafeCast.toUint224(newWeight)}) + ); + } + } + } + + function _add(uint256 a, uint256 b) private pure returns (uint256) { + return a + b; + } + + function _subtract(uint256 a, uint256 b) private pure returns (uint256) { + return a - b; + } + + /** + * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. + */ + function _unsafeAccess(Checkpoint[] storage ckpts, uint256 pos) private pure returns (Checkpoint storage result) { + assembly { + mstore(0, ckpts.slot) + result.slot := add(keccak256(0, 0x20), pos) + } + } +} diff --git a/contracts/mocks/token/ERC4626DecimalsMock.sol b/contracts/mocks/token/ERC4626DecimalsMock.sol deleted file mode 100644 index 9cce5139c..000000000 --- a/contracts/mocks/token/ERC4626DecimalsMock.sol +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../../token/ERC20/extensions/ERC4626.sol"; - -abstract contract ERC4626DecimalsMock is ERC4626 { - using Math for uint256; - - uint8 private immutable _decimals; - - constructor(uint8 decimals_) { - _decimals = decimals_; - } - - function decimals() public view virtual override returns (uint8) { - return _decimals; - } - - function _initialConvertToShares( - uint256 assets, - Math.Rounding rounding - ) internal view virtual override returns (uint256 shares) { - return assets.mulDiv(10 ** decimals(), 10 ** super.decimals(), rounding); - } - - function _initialConvertToAssets( - uint256 shares, - Math.Rounding rounding - ) internal view virtual override returns (uint256 assets) { - return shares.mulDiv(10 ** super.decimals(), 10 ** decimals(), rounding); - } -} diff --git a/contracts/mocks/token/ERC4626LimitsMock.sol b/contracts/mocks/token/ERC4626LimitsMock.sol new file mode 100644 index 000000000..a47826bb8 --- /dev/null +++ b/contracts/mocks/token/ERC4626LimitsMock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../../token/ERC20/extensions/ERC4626.sol"; + +abstract contract ERC4626LimitsMock is ERC4626 { + uint256 _maxDeposit; + uint256 _maxMint; + + constructor() { + _maxDeposit = 100 ether; + _maxMint = 100 ether; + } + + function maxDeposit(address) public view override returns (uint256) { + return _maxDeposit; + } + + function maxMint(address) public view override returns (uint256) { + return _maxMint; + } +} diff --git a/contracts/mocks/ERC4626Mock.sol b/contracts/mocks/token/ERC4626Mock.sol similarity index 82% rename from contracts/mocks/ERC4626Mock.sol rename to contracts/mocks/token/ERC4626Mock.sol index ef2d1a4cb..3713e0791 100644 --- a/contracts/mocks/ERC4626Mock.sol +++ b/contracts/mocks/token/ERC4626Mock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import "../token/ERC20/extensions/ERC4626.sol"; +import "../../token/ERC20/extensions/ERC4626.sol"; contract ERC4626Mock is ERC4626 { constructor(address underlying) ERC20("ERC4626Mock", "E4626M") ERC4626(IERC20(underlying)) {} diff --git a/contracts/mocks/token/ERC4626OffsetMock.sol b/contracts/mocks/token/ERC4626OffsetMock.sol new file mode 100644 index 000000000..b3c31cdd2 --- /dev/null +++ b/contracts/mocks/token/ERC4626OffsetMock.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../../token/ERC20/extensions/ERC4626.sol"; + +abstract contract ERC4626OffsetMock is ERC4626 { + uint8 private immutable _offset; + + constructor(uint8 offset_) { + _offset = offset_; + } + + function _decimalsOffset() internal view virtual override returns (uint8) { + return _offset; + } +} diff --git a/contracts/mocks/token/ERC4646FeesMock.sol b/contracts/mocks/token/ERC4646FeesMock.sol new file mode 100644 index 000000000..4c6c3c662 --- /dev/null +++ b/contracts/mocks/token/ERC4646FeesMock.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../docs/ERC4626Fees.sol"; + +abstract contract ERC4626FeesMock is ERC4626Fees { + uint256 private immutable _entryFeeBasePointValue; + address private immutable _entryFeeRecipientValue; + uint256 private immutable _exitFeeBasePointValue; + address private immutable _exitFeeRecipientValue; + + constructor( + uint256 entryFeeBasePoint, + address entryFeeRecipient, + uint256 exitFeeBasePoint, + address exitFeeRecipient + ) { + _entryFeeBasePointValue = entryFeeBasePoint; + _entryFeeRecipientValue = entryFeeRecipient; + _exitFeeBasePointValue = exitFeeBasePoint; + _exitFeeRecipientValue = exitFeeRecipient; + } + + function _entryFeeBasePoint() internal view virtual override returns (uint256) { + return _entryFeeBasePointValue; + } + + function _entryFeeRecipient() internal view virtual override returns (address) { + return _entryFeeRecipientValue; + } + + function _exitFeeBasePoint() internal view virtual override returns (uint256) { + return _exitFeeBasePointValue; + } + + function _exitFeeRecipient() internal view virtual override returns (address) { + return _exitFeeRecipientValue; + } +} diff --git a/contracts/mocks/token/ERC721ConsecutiveEnumerableMock.sol b/contracts/mocks/token/ERC721ConsecutiveEnumerableMock.sol index 55c40ac17..a2ee8199a 100644 --- a/contracts/mocks/token/ERC721ConsecutiveEnumerableMock.sol +++ b/contracts/mocks/token/ERC721ConsecutiveEnumerableMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC721/extensions/ERC721Consecutive.sol"; import "../../token/ERC721/extensions/ERC721Enumerable.sol"; diff --git a/contracts/mocks/token/ERC721ConsecutiveMock.sol b/contracts/mocks/token/ERC721ConsecutiveMock.sol index 427f44a19..4aae388e0 100644 --- a/contracts/mocks/token/ERC721ConsecutiveMock.sol +++ b/contracts/mocks/token/ERC721ConsecutiveMock.sol @@ -1,23 +1,27 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC721/extensions/ERC721Consecutive.sol"; -import "../../token/ERC721/extensions/ERC721Enumerable.sol"; import "../../token/ERC721/extensions/ERC721Pausable.sol"; -import "../../token/ERC721/extensions/draft-ERC721Votes.sol"; +import "../../token/ERC721/extensions/ERC721Votes.sol"; /** * @title ERC721ConsecutiveMock */ contract ERC721ConsecutiveMock is ERC721Consecutive, ERC721Pausable, ERC721Votes { + uint96 private immutable _offset; + constructor( string memory name, string memory symbol, + uint96 offset, address[] memory delegates, address[] memory receivers, uint96[] memory amounts ) ERC721(name, symbol) EIP712(name, "1") { + _offset = offset; + for (uint256 i = 0; i < delegates.length; ++i) { _delegate(delegates[i], delegates[i]); } @@ -27,6 +31,10 @@ contract ERC721ConsecutiveMock is ERC721Consecutive, ERC721Pausable, ERC721Votes } } + function _firstConsecutiveId() internal view virtual override returns (uint96) { + return _offset; + } + function _ownerOf(uint256 tokenId) internal view virtual override(ERC721, ERC721Consecutive) returns (address) { return super._ownerOf(tokenId); } diff --git a/contracts/mocks/token/ERC721ReceiverMock.sol b/contracts/mocks/token/ERC721ReceiverMock.sol index dd25788d4..416613174 100644 --- a/contracts/mocks/token/ERC721ReceiverMock.sol +++ b/contracts/mocks/token/ERC721ReceiverMock.sol @@ -1,23 +1,25 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC721/IERC721Receiver.sol"; contract ERC721ReceiverMock is IERC721Receiver { - enum Error { + enum RevertType { None, - RevertWithMessage, RevertWithoutMessage, + RevertWithMessage, + RevertWithCustomError, Panic } bytes4 private immutable _retval; - Error private immutable _error; + RevertType private immutable _error; event Received(address operator, address from, uint256 tokenId, bytes data, uint256 gas); + error CustomError(bytes4); - constructor(bytes4 retval, Error error) { + constructor(bytes4 retval, RevertType error) { _retval = retval; _error = error; } @@ -27,15 +29,18 @@ contract ERC721ReceiverMock is IERC721Receiver { address from, uint256 tokenId, bytes memory data - ) public override returns (bytes4) { - if (_error == Error.RevertWithMessage) { - revert("ERC721ReceiverMock: reverting"); - } else if (_error == Error.RevertWithoutMessage) { + ) public returns (bytes4) { + if (_error == RevertType.RevertWithoutMessage) { revert(); - } else if (_error == Error.Panic) { + } else if (_error == RevertType.RevertWithMessage) { + revert("ERC721ReceiverMock: reverting"); + } else if (_error == RevertType.RevertWithCustomError) { + revert CustomError(_retval); + } else if (_error == RevertType.Panic) { uint256 a = uint256(0) / uint256(0); a; } + emit Received(operator, from, tokenId, data, gasleft()); return _retval; } diff --git a/contracts/mocks/token/ERC721URIStorageMock.sol b/contracts/mocks/token/ERC721URIStorageMock.sol index 455c933c8..1808e47f7 100644 --- a/contracts/mocks/token/ERC721URIStorageMock.sol +++ b/contracts/mocks/token/ERC721URIStorageMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../token/ERC721/extensions/ERC721URIStorage.sol"; diff --git a/contracts/mocks/token/ERC777Mock.sol b/contracts/mocks/token/ERC777Mock.sol deleted file mode 100644 index 685277e8b..000000000 --- a/contracts/mocks/token/ERC777Mock.sol +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../../token/ERC777/ERC777.sol"; - -abstract contract ERC777Mock is ERC777 { - event BeforeTokenTransfer(); - - function _beforeTokenTransfer(address, address, address, uint256) internal override { - emit BeforeTokenTransfer(); - } -} diff --git a/contracts/mocks/token/ERC777SenderRecipientMock.sol b/contracts/mocks/token/ERC777SenderRecipientMock.sol deleted file mode 100644 index 3bec6d94b..000000000 --- a/contracts/mocks/token/ERC777SenderRecipientMock.sol +++ /dev/null @@ -1,152 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -import "../../token/ERC777/IERC777.sol"; -import "../../token/ERC777/IERC777Sender.sol"; -import "../../token/ERC777/IERC777Recipient.sol"; -import "../../utils/Context.sol"; -import "../../utils/introspection/IERC1820Registry.sol"; -import "../../utils/introspection/ERC1820Implementer.sol"; - -contract ERC777SenderRecipientMock is Context, IERC777Sender, IERC777Recipient, ERC1820Implementer { - event TokensToSendCalled( - address operator, - address from, - address to, - uint256 amount, - bytes data, - bytes operatorData, - address token, - uint256 fromBalance, - uint256 toBalance - ); - - event TokensReceivedCalled( - address operator, - address from, - address to, - uint256 amount, - bytes data, - bytes operatorData, - address token, - uint256 fromBalance, - uint256 toBalance - ); - - // Emitted in ERC777Mock. Here for easier decoding - event BeforeTokenTransfer(); - - bool private _shouldRevertSend; - bool private _shouldRevertReceive; - - IERC1820Registry private _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24); - - bytes32 private constant _TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender"); - bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient"); - - function tokensToSend( - address operator, - address from, - address to, - uint256 amount, - bytes calldata userData, - bytes calldata operatorData - ) external override { - if (_shouldRevertSend) { - revert(); - } - - IERC777 token = IERC777(_msgSender()); - - uint256 fromBalance = token.balanceOf(from); - // when called due to burn, to will be the zero address, which will have a balance of 0 - uint256 toBalance = token.balanceOf(to); - - emit TokensToSendCalled( - operator, - from, - to, - amount, - userData, - operatorData, - address(token), - fromBalance, - toBalance - ); - } - - function tokensReceived( - address operator, - address from, - address to, - uint256 amount, - bytes calldata userData, - bytes calldata operatorData - ) external override { - if (_shouldRevertReceive) { - revert(); - } - - IERC777 token = IERC777(_msgSender()); - - uint256 fromBalance = token.balanceOf(from); - // when called due to burn, to will be the zero address, which will have a balance of 0 - uint256 toBalance = token.balanceOf(to); - - emit TokensReceivedCalled( - operator, - from, - to, - amount, - userData, - operatorData, - address(token), - fromBalance, - toBalance - ); - } - - function senderFor(address account) public { - _registerInterfaceForAddress(_TOKENS_SENDER_INTERFACE_HASH, account); - - address self = address(this); - if (account == self) { - registerSender(self); - } - } - - function registerSender(address sender) public { - _erc1820.setInterfaceImplementer(address(this), _TOKENS_SENDER_INTERFACE_HASH, sender); - } - - function recipientFor(address account) public { - _registerInterfaceForAddress(_TOKENS_RECIPIENT_INTERFACE_HASH, account); - - address self = address(this); - if (account == self) { - registerRecipient(self); - } - } - - function registerRecipient(address recipient) public { - _erc1820.setInterfaceImplementer(address(this), _TOKENS_RECIPIENT_INTERFACE_HASH, recipient); - } - - function setShouldRevertSend(bool shouldRevert) public { - _shouldRevertSend = shouldRevert; - } - - function setShouldRevertReceive(bool shouldRevert) public { - _shouldRevertReceive = shouldRevert; - } - - function send(IERC777 token, address to, uint256 amount, bytes memory data) public { - // This is 777's send function, not the Solidity send function - token.send(to, amount, data); // solhint-disable-line check-send-result - } - - function burn(IERC777 token, uint256 amount, bytes memory data) public { - token.burn(amount, data); - } -} diff --git a/contracts/mocks/token/VotesTimestamp.sol b/contracts/mocks/token/VotesTimestamp.sol new file mode 100644 index 000000000..7e3d8ca6d --- /dev/null +++ b/contracts/mocks/token/VotesTimestamp.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "../../token/ERC20/extensions/ERC20Votes.sol"; +import "../../token/ERC721/extensions/ERC721Votes.sol"; + +abstract contract ERC20VotesTimestampMock is ERC20Votes { + function clock() public view virtual override returns (uint48) { + return SafeCast.toUint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + return "mode=timestamp"; + } +} + +abstract contract ERC721VotesTimestampMock is ERC721Votes { + function clock() public view virtual override returns (uint48) { + return SafeCast.toUint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + return "mode=timestamp"; + } +} diff --git a/contracts/mocks/wizard/MyGovernor1.sol b/contracts/mocks/wizard/MyGovernor1.sol deleted file mode 100644 index 37ecfd57d..000000000 --- a/contracts/mocks/wizard/MyGovernor1.sol +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "../../governance/Governor.sol"; -import "../../governance/extensions/GovernorCountingSimple.sol"; -import "../../governance/extensions/GovernorVotes.sol"; -import "../../governance/extensions/GovernorVotesQuorumFraction.sol"; -import "../../governance/extensions/GovernorTimelockControl.sol"; - -contract MyGovernor1 is - Governor, - GovernorTimelockControl, - GovernorVotes, - GovernorVotesQuorumFraction, - GovernorCountingSimple -{ - constructor( - IVotes _token, - TimelockController _timelock - ) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) GovernorTimelockControl(_timelock) {} - - function votingDelay() public pure override returns (uint256) { - return 1; // 1 block - } - - function votingPeriod() public pure override returns (uint256) { - return 45818; // 1 week - } - - // The following functions are overrides required by Solidity. - - function quorum( - uint256 blockNumber - ) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) { - return super.quorum(blockNumber); - } - - function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { - return super.state(proposalId); - } - - function propose( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - string memory description - ) public override(Governor, IGovernor) returns (uint256) { - return super.propose(targets, values, calldatas, description); - } - - function _execute( - uint256 proposalId, - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) { - super._execute(proposalId, targets, values, calldatas, descriptionHash); - } - - function _cancel( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) returns (uint256) { - return super._cancel(targets, values, calldatas, descriptionHash); - } - - function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { - return super._executor(); - } - - function supportsInterface( - bytes4 interfaceId - ) public view override(Governor, GovernorTimelockControl) returns (bool) { - return super.supportsInterface(interfaceId); - } -} diff --git a/contracts/mocks/wizard/MyGovernor2.sol b/contracts/mocks/wizard/MyGovernor2.sol deleted file mode 100644 index 1472b67d5..000000000 --- a/contracts/mocks/wizard/MyGovernor2.sol +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "../../governance/Governor.sol"; -import "../../governance/extensions/GovernorProposalThreshold.sol"; -import "../../governance/extensions/GovernorCountingSimple.sol"; -import "../../governance/extensions/GovernorVotes.sol"; -import "../../governance/extensions/GovernorVotesQuorumFraction.sol"; -import "../../governance/extensions/GovernorTimelockControl.sol"; - -contract MyGovernor2 is - Governor, - GovernorTimelockControl, - GovernorProposalThreshold, - GovernorVotes, - GovernorVotesQuorumFraction, - GovernorCountingSimple -{ - constructor( - IVotes _token, - TimelockController _timelock - ) Governor("MyGovernor") GovernorVotes(_token) GovernorVotesQuorumFraction(4) GovernorTimelockControl(_timelock) {} - - function votingDelay() public pure override returns (uint256) { - return 1; // 1 block - } - - function votingPeriod() public pure override returns (uint256) { - return 45818; // 1 week - } - - function proposalThreshold() public pure override returns (uint256) { - return 1000e18; - } - - // The following functions are overrides required by Solidity. - - function quorum( - uint256 blockNumber - ) public view override(IGovernor, GovernorVotesQuorumFraction) returns (uint256) { - return super.quorum(blockNumber); - } - - function state(uint256 proposalId) public view override(Governor, GovernorTimelockControl) returns (ProposalState) { - return super.state(proposalId); - } - - function propose( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - string memory description - ) public override(Governor, GovernorProposalThreshold, IGovernor) returns (uint256) { - return super.propose(targets, values, calldatas, description); - } - - function _execute( - uint256 proposalId, - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) { - super._execute(proposalId, targets, values, calldatas, descriptionHash); - } - - function _cancel( - address[] memory targets, - uint256[] memory values, - bytes[] memory calldatas, - bytes32 descriptionHash - ) internal override(Governor, GovernorTimelockControl) returns (uint256) { - return super._cancel(targets, values, calldatas, descriptionHash); - } - - function _executor() internal view override(Governor, GovernorTimelockControl) returns (address) { - return super._executor(); - } - - function supportsInterface( - bytes4 interfaceId - ) public view override(Governor, GovernorTimelockControl) returns (bool) { - return super.supportsInterface(interfaceId); - } -} diff --git a/contracts/package.json b/contracts/package.json index e3e608341..df141192d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@openzeppelin/contracts", "description": "Secure Smart Contract library for Solidity", - "version": "4.8.0", + "version": "4.9.2", "files": [ "**/*.sol", "/build/contracts/*.json", diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index 712519892..d859d5645 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (proxy/Clones.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/Clones.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev https://eips.ethereum.org/EIPS/eip-1167[EIP 1167] is a standard for @@ -17,6 +17,11 @@ pragma solidity ^0.8.0; * _Available since v3.4._ */ library Clones { + /** + * @dev A clone instance deployment failed. + */ + error ERC1167FailedCreateClone(); + /** * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`. * @@ -32,7 +37,9 @@ library Clones { mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) instance := create(0, 0x09, 0x37) } - require(instance != address(0), "ERC1167: create failed"); + if (instance == address(0)) { + revert ERC1167FailedCreateClone(); + } } /** @@ -52,7 +59,9 @@ library Clones { mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3)) instance := create2(0, 0x09, 0x37, salt) } - require(instance != address(0), "ERC1167: create2 failed"); + if (instance == address(0)) { + revert ERC1167FailedCreateClone(); + } } /** diff --git a/contracts/proxy/ERC1967/ERC1967Proxy.sol b/contracts/proxy/ERC1967/ERC1967Proxy.sol index 642d811cc..5a752f13d 100644 --- a/contracts/proxy/ERC1967/ERC1967Proxy.sol +++ b/contracts/proxy/ERC1967/ERC1967Proxy.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (proxy/ERC1967/ERC1967Proxy.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Proxy.sol"; -import "./ERC1967Upgrade.sol"; +import "./ERC1967Utils.sol"; /** * @dev This contract implements an upgradeable proxy. It is upgradeable because calls are delegated to an @@ -12,7 +12,7 @@ import "./ERC1967Upgrade.sol"; * https://eips.ethereum.org/EIPS/eip-1967[EIP1967], so that it doesn't conflict with the storage layout of the * implementation behind the proxy. */ -contract ERC1967Proxy is Proxy, ERC1967Upgrade { +contract ERC1967Proxy is Proxy { /** * @dev Initializes the upgradeable proxy with an initial implementation specified by `_logic`. * @@ -20,7 +20,7 @@ contract ERC1967Proxy is Proxy, ERC1967Upgrade { * function call, and allows initializing the storage of the proxy like a Solidity constructor. */ constructor(address _logic, bytes memory _data) payable { - _upgradeToAndCall(_logic, _data, false); + ERC1967Utils.upgradeToAndCall(_logic, _data, false); } /** @@ -31,6 +31,6 @@ contract ERC1967Proxy is Proxy, ERC1967Upgrade { * `0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc` */ function _implementation() internal view virtual override returns (address impl) { - return ERC1967Upgrade._getImplementation(); + return ERC1967Utils.getImplementation(); } } diff --git a/contracts/proxy/ERC1967/ERC1967Upgrade.sol b/contracts/proxy/ERC1967/ERC1967Upgrade.sol deleted file mode 100644 index 58f65fe88..000000000 --- a/contracts/proxy/ERC1967/ERC1967Upgrade.sol +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (proxy/ERC1967/ERC1967Upgrade.sol) - -pragma solidity ^0.8.2; - -import "../beacon/IBeacon.sol"; -import "../../interfaces/draft-IERC1822.sol"; -import "../../utils/Address.sol"; -import "../../utils/StorageSlot.sol"; - -/** - * @dev This abstract contract provides getters and event emitting update functions for - * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. - * - * _Available since v4.1._ - * - * @custom:oz-upgrades-unsafe-allow delegatecall - */ -abstract contract ERC1967Upgrade { - // This is the keccak-256 hash of "eip1967.proxy.rollback" subtracted by 1 - bytes32 private constant _ROLLBACK_SLOT = 0x4910fdfa16fed3260ed0e7147f7cc6da11a60208b5b9406d12a635614ffd9143; - - /** - * @dev Storage slot with the address of the current implementation. - * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is - * validated in the constructor. - */ - bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - /** - * @dev Emitted when the implementation is upgraded. - */ - event Upgraded(address indexed implementation); - - /** - * @dev Returns the current implementation address. - */ - function _getImplementation() internal view returns (address) { - return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; - } - - /** - * @dev Stores a new address in the EIP1967 implementation slot. - */ - function _setImplementation(address newImplementation) private { - require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract"); - StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; - } - - /** - * @dev Perform implementation upgrade - * - * Emits an {Upgraded} event. - */ - function _upgradeTo(address newImplementation) internal { - _setImplementation(newImplementation); - emit Upgraded(newImplementation); - } - - /** - * @dev Perform implementation upgrade with additional setup call. - * - * Emits an {Upgraded} event. - */ - function _upgradeToAndCall(address newImplementation, bytes memory data, bool forceCall) internal { - _upgradeTo(newImplementation); - if (data.length > 0 || forceCall) { - Address.functionDelegateCall(newImplementation, data); - } - } - - /** - * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. - * - * Emits an {Upgraded} event. - */ - function _upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) internal { - // Upgrades from old implementations will perform a rollback test. This test requires the new - // implementation to upgrade back to the old, non-ERC1822 compliant, implementation. Removing - // this special case will break upgrade paths from old UUPS implementation to new ones. - if (StorageSlot.getBooleanSlot(_ROLLBACK_SLOT).value) { - _setImplementation(newImplementation); - } else { - try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { - require(slot == _IMPLEMENTATION_SLOT, "ERC1967Upgrade: unsupported proxiableUUID"); - } catch { - revert("ERC1967Upgrade: new implementation is not UUPS"); - } - _upgradeToAndCall(newImplementation, data, forceCall); - } - } - - /** - * @dev Storage slot with the admin of the contract. - * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is - * validated in the constructor. - */ - bytes32 internal constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; - - /** - * @dev Emitted when the admin account has changed. - */ - event AdminChanged(address previousAdmin, address newAdmin); - - /** - * @dev Returns the current admin. - * - * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the - * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. - * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` - */ - function _getAdmin() internal view returns (address) { - return StorageSlot.getAddressSlot(_ADMIN_SLOT).value; - } - - /** - * @dev Stores a new address in the EIP1967 admin slot. - */ - function _setAdmin(address newAdmin) private { - require(newAdmin != address(0), "ERC1967: new admin is the zero address"); - StorageSlot.getAddressSlot(_ADMIN_SLOT).value = newAdmin; - } - - /** - * @dev Changes the admin of the proxy. - * - * Emits an {AdminChanged} event. - */ - function _changeAdmin(address newAdmin) internal { - emit AdminChanged(_getAdmin(), newAdmin); - _setAdmin(newAdmin); - } - - /** - * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. - * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)) and is validated in the constructor. - */ - bytes32 internal constant _BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; - - /** - * @dev Emitted when the beacon is upgraded. - */ - event BeaconUpgraded(address indexed beacon); - - /** - * @dev Returns the current beacon. - */ - function _getBeacon() internal view returns (address) { - return StorageSlot.getAddressSlot(_BEACON_SLOT).value; - } - - /** - * @dev Stores a new beacon in the EIP1967 beacon slot. - */ - function _setBeacon(address newBeacon) private { - require(newBeacon.code.length > 0, "ERC1967: new beacon is not a contract"); - require( - IBeacon(newBeacon).implementation().code.length > 0, - "ERC1967: beacon implementation is not a contract" - ); - StorageSlot.getAddressSlot(_BEACON_SLOT).value = newBeacon; - } - - /** - * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does - * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that). - * - * Emits a {BeaconUpgraded} event. - */ - function _upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal { - _setBeacon(newBeacon); - emit BeaconUpgraded(newBeacon); - if (data.length > 0 || forceCall) { - Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); - } - } -} diff --git a/contracts/proxy/ERC1967/ERC1967Utils.sol b/contracts/proxy/ERC1967/ERC1967Utils.sol new file mode 100644 index 000000000..c244982bf --- /dev/null +++ b/contracts/proxy/ERC1967/ERC1967Utils.sol @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/ERC1967/ERC1967Utils.sol) + +pragma solidity ^0.8.20; + +import "../beacon/IBeacon.sol"; +import "../../utils/Address.sol"; +import "../../utils/StorageSlot.sol"; + +/** + * @dev This abstract contract provides getters and event emitting update functions for + * https://eips.ethereum.org/EIPS/eip-1967[EIP1967] slots. + * + * _Available since v4.1._ + */ +library ERC1967Utils { + // We re-declare ERC-1967 events here because they can't be used directly from IERC1967. + // This will be fixed in Solidity 0.8.21. At that point we should remove these events. + /** + * @dev Emitted when the implementation is upgraded. + */ + event Upgraded(address indexed implementation); + + /** + * @dev Emitted when the admin account has changed. + */ + event AdminChanged(address previousAdmin, address newAdmin); + + /** + * @dev Emitted when the beacon is changed. + */ + event BeaconUpgraded(address indexed beacon); + + /** + * @dev Storage slot with the address of the current implementation. + * This is the keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1, and is + * validated in the constructor. + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + /** + * @dev The `implementation` of the proxy is invalid. + */ + error ERC1967InvalidImplementation(address implementation); + + /** + * @dev The `admin` of the proxy is invalid. + */ + error ERC1967InvalidAdmin(address admin); + + /** + * @dev The `beacon` of the proxy is invalid. + */ + error ERC1967InvalidBeacon(address beacon); + + /** + * @dev Returns the current implementation address. + */ + function getImplementation() internal view returns (address) { + return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 implementation slot. + */ + function _setImplementation(address newImplementation) private { + if (newImplementation.code.length == 0) { + revert ERC1967InvalidImplementation(newImplementation); + } + StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation; + } + + /** + * @dev Perform implementation upgrade + * + * Emits an {IERC1967-Upgraded} event. + */ + function upgradeTo(address newImplementation) internal { + _setImplementation(newImplementation); + emit Upgraded(newImplementation); + } + + /** + * @dev Perform implementation upgrade with additional setup call. + * + * Emits an {IERC1967-Upgraded} event. + */ + function upgradeToAndCall(address newImplementation, bytes memory data, bool forceCall) internal { + upgradeTo(newImplementation); + if (data.length > 0 || forceCall) { + Address.functionDelegateCall(newImplementation, data); + } + } + + /** + * @dev Storage slot with the admin of the contract. + * This is the keccak-256 hash of "eip1967.proxy.admin" subtracted by 1, and is + * validated in the constructor. + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103; + + /** + * @dev Returns the current admin. + * + * TIP: To get this value clients can read directly from the storage slot shown below (specified by EIP1967) using the + * https://eth.wiki/json-rpc/API#eth_getstorageat[`eth_getStorageAt`] RPC call. + * `0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103` + */ + function getAdmin() internal view returns (address) { + return StorageSlot.getAddressSlot(ADMIN_SLOT).value; + } + + /** + * @dev Stores a new address in the EIP1967 admin slot. + */ + function _setAdmin(address newAdmin) private { + if (newAdmin == address(0)) { + revert ERC1967InvalidAdmin(address(0)); + } + StorageSlot.getAddressSlot(ADMIN_SLOT).value = newAdmin; + } + + /** + * @dev Changes the admin of the proxy. + * + * Emits an {IERC1967-AdminChanged} event. + */ + function changeAdmin(address newAdmin) internal { + emit AdminChanged(getAdmin(), newAdmin); + _setAdmin(newAdmin); + } + + /** + * @dev The storage slot of the UpgradeableBeacon contract which defines the implementation for this proxy. + * This is bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1) and is validated in the constructor. + */ + // solhint-disable-next-line private-vars-leading-underscore + bytes32 internal constant BEACON_SLOT = 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50; + + /** + * @dev Returns the current beacon. + */ + function getBeacon() internal view returns (address) { + return StorageSlot.getAddressSlot(BEACON_SLOT).value; + } + + /** + * @dev Stores a new beacon in the EIP1967 beacon slot. + */ + function _setBeacon(address newBeacon) private { + if (newBeacon.code.length == 0) { + revert ERC1967InvalidBeacon(newBeacon); + } + + address beaconImplementation = IBeacon(newBeacon).implementation(); + if (beaconImplementation.code.length == 0) { + revert ERC1967InvalidImplementation(beaconImplementation); + } + + StorageSlot.getAddressSlot(BEACON_SLOT).value = newBeacon; + } + + /** + * @dev Perform beacon upgrade with additional setup call. Note: This upgrades the address of the beacon, it does + * not upgrade the implementation contained in the beacon (see {UpgradeableBeacon-_setImplementation} for that). + * + * Emits an {IERC1967-BeaconUpgraded} event. + */ + function upgradeBeaconToAndCall(address newBeacon, bytes memory data, bool forceCall) internal { + _setBeacon(newBeacon); + emit BeaconUpgraded(newBeacon); + if (data.length > 0 || forceCall) { + Address.functionDelegateCall(IBeacon(newBeacon).implementation(), data); + } + } +} diff --git a/contracts/proxy/Proxy.sol b/contracts/proxy/Proxy.sol index 988cf72a0..c2875aeac 100644 --- a/contracts/proxy/Proxy.sol +++ b/contracts/proxy/Proxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (proxy/Proxy.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev This abstract contract provides a fallback function that delegates all calls to another contract using the EVM diff --git a/contracts/proxy/README.adoc b/contracts/proxy/README.adoc index 5ada16e38..3c4a78d19 100644 --- a/contracts/proxy/README.adoc +++ b/contracts/proxy/README.adoc @@ -11,12 +11,12 @@ Most of the proxies below are built on an abstract base contract. In order to avoid clashes with the storage variables of the implementation contract behind a proxy, we use https://eips.ethereum.org/EIPS/eip-1967[EIP1967] storage slots. -- {ERC1967Upgrade}: Internal functions to get and set the storage slots defined in EIP1967. +- {ERC1967Utils}: Internal functions to get and set the storage slots defined in EIP1967. - {ERC1967Proxy}: A proxy using EIP1967 storage slots. Not upgradeable by default. There are two alternative ways to add upgradeability to an ERC1967 proxy. Their differences are explained below in <>. -- {TransparentUpgradeableProxy}: A proxy with a built in admin and upgrade interface. +- {TransparentUpgradeableProxy}: A proxy with a built-in immutable admin and upgrade interface. - {UUPSUpgradeable}: An upgradeability mechanism to be included in the implementation contract. CAUTION: Using upgradeable proxies correctly and securely is a difficult task that requires deep knowledge of the proxy pattern, Solidity, and the EVM. Unless you want a lot of low level control, we recommend using the xref:upgrades-plugins::index.adoc[OpenZeppelin Upgrades Plugins] for Truffle and Hardhat. @@ -56,9 +56,11 @@ The current implementation of this security mechanism uses https://eips.ethereum == ERC1967 +{{IERC1967}} + {{ERC1967Proxy}} -{{ERC1967Upgrade}} +{{ERC1967Utils}} == Transparent Proxy diff --git a/contracts/proxy/beacon/BeaconProxy.sol b/contracts/proxy/beacon/BeaconProxy.sol index d217b15ea..dc6b5c90b 100644 --- a/contracts/proxy/beacon/BeaconProxy.sol +++ b/contracts/proxy/beacon/BeaconProxy.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (proxy/beacon/BeaconProxy.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IBeacon.sol"; import "../Proxy.sol"; -import "../ERC1967/ERC1967Upgrade.sol"; +import "../ERC1967/ERC1967Utils.sol"; /** * @dev This contract implements a proxy that gets the implementation address for each call from an {UpgradeableBeacon}. @@ -15,7 +15,7 @@ import "../ERC1967/ERC1967Upgrade.sol"; * * _Available since v3.4._ */ -contract BeaconProxy is Proxy, ERC1967Upgrade { +contract BeaconProxy is Proxy { /** * @dev Initializes the proxy with `beacon`. * @@ -28,34 +28,13 @@ contract BeaconProxy is Proxy, ERC1967Upgrade { * - `beacon` must be a contract with the interface {IBeacon}. */ constructor(address beacon, bytes memory data) payable { - _upgradeBeaconToAndCall(beacon, data, false); - } - - /** - * @dev Returns the current beacon address. - */ - function _beacon() internal view virtual returns (address) { - return _getBeacon(); + ERC1967Utils.upgradeBeaconToAndCall(beacon, data, false); } /** * @dev Returns the current implementation address of the associated beacon. */ function _implementation() internal view virtual override returns (address) { - return IBeacon(_getBeacon()).implementation(); - } - - /** - * @dev Changes the proxy to use a new beacon. Deprecated: see {_upgradeBeaconToAndCall}. - * - * If `data` is nonempty, it's used as data in a delegate call to the implementation returned by the beacon. - * - * Requirements: - * - * - `beacon` must be a contract. - * - The implementation returned by `beacon` must be a contract. - */ - function _setBeacon(address beacon, bytes memory data) internal virtual { - _upgradeBeaconToAndCall(beacon, data, false); + return IBeacon(ERC1967Utils.getBeacon()).implementation(); } } diff --git a/contracts/proxy/beacon/IBeacon.sol b/contracts/proxy/beacon/IBeacon.sol index fba3ee2ab..fcd655d6c 100644 --- a/contracts/proxy/beacon/IBeacon.sol +++ b/contracts/proxy/beacon/IBeacon.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (proxy/beacon/IBeacon.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev This is the interface that {BeaconProxy} expects of its beacon. diff --git a/contracts/proxy/beacon/UpgradeableBeacon.sol b/contracts/proxy/beacon/UpgradeableBeacon.sol index 9eeb149a6..c5e64ea59 100644 --- a/contracts/proxy/beacon/UpgradeableBeacon.sol +++ b/contracts/proxy/beacon/UpgradeableBeacon.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (proxy/beacon/UpgradeableBeacon.sol) -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; import "./IBeacon.sol"; import "../../access/Ownable.sol"; @@ -15,23 +15,27 @@ import "../../access/Ownable.sol"; contract UpgradeableBeacon is IBeacon, Ownable { address private _implementation; + /** + * @dev The `implementation` of the beacon is invalid. + */ + error BeaconInvalidImplementation(address implementation); + /** * @dev Emitted when the implementation returned by the beacon is changed. */ event Upgraded(address indexed implementation); /** - * @dev Sets the address of the initial implementation, and the deployer account as the owner who can upgrade the - * beacon. + * @dev Sets the address of the initial implementation, and the initial owner who can upgrade the beacon. */ - constructor(address implementation_) { + constructor(address implementation_, address initialOwner) Ownable(initialOwner) { _setImplementation(implementation_); } /** * @dev Returns the current implementation address. */ - function implementation() public view virtual override returns (address) { + function implementation() public view virtual returns (address) { return _implementation; } @@ -58,7 +62,9 @@ contract UpgradeableBeacon is IBeacon, Ownable { * - `newImplementation` must be a contract. */ function _setImplementation(address newImplementation) private { - require(newImplementation.code.length > 0, "UpgradeableBeacon: implementation is not a contract"); + if (newImplementation.code.length == 0) { + revert BeaconInvalidImplementation(newImplementation); + } _implementation = newImplementation; } } diff --git a/contracts/proxy/transparent/ProxyAdmin.sol b/contracts/proxy/transparent/ProxyAdmin.sol index 7f340f629..e8578a585 100644 --- a/contracts/proxy/transparent/ProxyAdmin.sol +++ b/contracts/proxy/transparent/ProxyAdmin.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (proxy/transparent/ProxyAdmin.sol) +// OpenZeppelin Contracts (last updated v4.8.3) (proxy/transparent/ProxyAdmin.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./TransparentUpgradeableProxy.sol"; import "../../access/Ownable.sol"; @@ -12,15 +12,9 @@ import "../../access/Ownable.sol"; */ contract ProxyAdmin is Ownable { /** - * @dev Changes the admin of `proxy` to `newAdmin`. - * - * Requirements: - * - * - This contract must be the current admin of `proxy`. + * @dev Sets the initial owner who can perform upgrades. */ - function changeProxyAdmin(TransparentUpgradeableProxy proxy, address newAdmin) public virtual onlyOwner { - proxy.changeAdmin(newAdmin); - } + constructor(address initialOwner) Ownable(initialOwner) {} /** * @dev Upgrades `proxy` to `implementation`. See {TransparentUpgradeableProxy-upgradeTo}. @@ -29,7 +23,7 @@ contract ProxyAdmin is Ownable { * * - This contract must be the admin of `proxy`. */ - function upgrade(TransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner { + function upgrade(ITransparentUpgradeableProxy proxy, address implementation) public virtual onlyOwner { proxy.upgradeTo(implementation); } @@ -42,7 +36,7 @@ contract ProxyAdmin is Ownable { * - This contract must be the admin of `proxy`. */ function upgradeAndCall( - TransparentUpgradeableProxy proxy, + ITransparentUpgradeableProxy proxy, address implementation, bytes memory data ) public payable virtual onlyOwner { diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index ffc97ff48..71ce665d0 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -1,12 +1,25 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (proxy/transparent/TransparentUpgradeableProxy.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/transparent/TransparentUpgradeableProxy.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC1967/ERC1967Proxy.sol"; +import "../../interfaces/IERC1967.sol"; /** - * @dev This contract implements a proxy that is upgradeable by an admin. + * @dev Interface for {TransparentUpgradeableProxy}. In order to implement transparency, {TransparentUpgradeableProxy} + * does not implement this interface directly, and some of its functions are implemented by an internal dispatch + * mechanism. The compiler is unaware that these functions are implemented by {TransparentUpgradeableProxy} and will not + * include them in the ABI so this interface must be used to interact with it. + */ +interface ITransparentUpgradeableProxy is IERC1967 { + function upgradeTo(address) external; + + function upgradeToAndCall(address, bytes memory) external payable; +} + +/** + * @dev This contract implements a proxy that is upgradeable by an immutable admin. * * To avoid https://medium.com/nomic-labs-blog/malicious-backdoors-in-ethereum-proxies-62629adf3357[proxy selector * clashing], which can potentially be used in an attack, this contract uses the @@ -16,79 +29,113 @@ import "../ERC1967/ERC1967Proxy.sol"; * 1. If any account other than the admin calls the proxy, the call will be forwarded to the implementation, even if * that call matches one of the admin functions exposed by the proxy itself. * 2. If the admin calls the proxy, it can access the admin functions, but its calls will never be forwarded to the - * implementation. If the admin tries to call a function on the implementation it will fail with an error that says - * "admin cannot fallback to proxy target". + * implementation. If the admin tries to call a function on the implementation it will fail with an error indicating the + * proxy admin cannot fallback to the target implementation. * - * These properties mean that the admin account can only be used for admin actions like upgrading the proxy or changing - * the admin, so it's best if it's a dedicated account that is not used for anything else. This will avoid headaches due - * to sudden errors when trying to call a function from the proxy implementation. + * These properties mean that the admin account can only be used for upgrading the proxy, so it's best if it's a dedicated + * account that is not used for anything else. This will avoid headaches due to sudden errors when trying to call a function + * from the proxy implementation. * * Our recommendation is for the dedicated account to be an instance of the {ProxyAdmin} contract. If set up this way, - * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy. + * you should think of the `ProxyAdmin` instance as the real administrative interface of your proxy, which extends from the + * {Ownable} contract to allow for changing the proxy's admin owner. + * + * NOTE: The real interface of this proxy is that defined in `ITransparentUpgradeableProxy`. This contract does not + * inherit from that interface, and instead the admin functions are implicitly implemented using a custom dispatch + * mechanism in `_fallback`. Consequently, the compiler will not produce an ABI for this contract. This is necessary to + * fully implement transparency without decoding reverts caused by selector clashes between the proxy and the + * implementation. + * + * IMPORTANT: This contract avoids unnecessary storage reads by setting the admin only during construction as an immutable variable, + * preventing any changes thereafter. However, the admin slot defined in ERC-1967 can still be overwritten by the implementation + * logic pointed to by this proxy. In such cases, the contract may end up in an undesirable state where the admin slot is different + * from the actual admin. + * + * WARNING: It is not recommended to extend this contract to add additional external functions. If you do so, the compiler + * will not check that there are no selector conflicts, due to the note above. A selector clash between any new function + * and the functions declared in {ITransparentUpgradeableProxy} will be resolved in favor of the new one. This could + * render the admin operations inaccessible, which could prevent upgradeability. Transparency may also be compromised. */ contract TransparentUpgradeableProxy is ERC1967Proxy { + // An immutable address for the admin avoid unnecessary SLOADs before each call + // at the expense of removing the ability to change the admin once it's set. + // This is acceptable if the admin is always a ProxyAdmin instance or similar contract + // with its own ability to transfer the permissions to another account. + address private immutable _admin; + + /** + * @dev The proxy caller is the current admin, and can't fallback to the proxy target. + */ + error ProxyDeniedAdminAccess(); + + /** + * @dev msg.value is not 0. + */ + error ProxyNonPayableFunction(); + /** * @dev Initializes an upgradeable proxy managed by `_admin`, backed by the implementation at `_logic`, and * optionally initialized with `_data` as explained in {ERC1967Proxy-constructor}. */ constructor(address _logic, address admin_, bytes memory _data) payable ERC1967Proxy(_logic, _data) { - _changeAdmin(admin_); + _admin = admin_; + // Set the storage value and emit an event for ERC-1967 compatibility + ERC1967Utils.changeAdmin(admin_); } /** - * @dev Modifier used internally that will delegate the call to the implementation unless the sender is the admin. + * @dev If caller is the admin process the call internally, otherwise transparently fallback to the proxy behavior */ - modifier ifAdmin() { - if (msg.sender == _getAdmin()) { - _; + function _fallback() internal virtual override { + if (msg.sender == _admin) { + bytes memory ret; + bytes4 selector = msg.sig; + if (selector == ITransparentUpgradeableProxy.upgradeTo.selector) { + ret = _dispatchUpgradeTo(); + } else if (selector == ITransparentUpgradeableProxy.upgradeToAndCall.selector) { + ret = _dispatchUpgradeToAndCall(); + } else { + revert ProxyDeniedAdminAccess(); + } + assembly { + return(add(ret, 0x20), mload(ret)) + } } else { - _fallback(); + super._fallback(); } } - /** - * @dev Changes the admin of the proxy. - * - * Emits an {AdminChanged} event. - * - * NOTE: Only the admin can call this function. See {ProxyAdmin-changeProxyAdmin}. - */ - function changeAdmin(address newAdmin) external virtual ifAdmin { - _changeAdmin(newAdmin); - } - /** * @dev Upgrade the implementation of the proxy. - * - * NOTE: Only the admin can call this function. See {ProxyAdmin-upgrade}. */ - function upgradeTo(address newImplementation) external ifAdmin { - _upgradeToAndCall(newImplementation, bytes(""), false); + function _dispatchUpgradeTo() private returns (bytes memory) { + _requireZeroValue(); + + address newImplementation = abi.decode(msg.data[4:], (address)); + ERC1967Utils.upgradeToAndCall(newImplementation, bytes(""), false); + + return ""; } /** * @dev Upgrade the implementation of the proxy, and then call a function from the new implementation as specified * by `data`, which should be an encoded function call. This is useful to initialize new storage variables in the * proxied contract. - * - * NOTE: Only the admin can call this function. See {ProxyAdmin-upgradeAndCall}. */ - function upgradeToAndCall(address newImplementation, bytes calldata data) external payable ifAdmin { - _upgradeToAndCall(newImplementation, data, true); + function _dispatchUpgradeToAndCall() private returns (bytes memory) { + (address newImplementation, bytes memory data) = abi.decode(msg.data[4:], (address, bytes)); + ERC1967Utils.upgradeToAndCall(newImplementation, data, true); + + return ""; } /** - * @dev Returns the current admin. + * @dev To keep this contract fully transparent, the fallback is payable. This helper is here to enforce + * non-payability of function implemented through dispatchers while still allowing value to pass through. */ - function _admin() internal view virtual returns (address) { - return _getAdmin(); - } - - /** - * @dev Makes sure the admin cannot access the fallback function. See {Proxy-_beforeFallback}. - */ - function _beforeFallback() internal virtual override { - require(msg.sender != _getAdmin(), "TransparentUpgradeableProxy: admin cannot fallback to proxy target"); - super._beforeFallback(); + function _requireZeroValue() private { + if (msg.value != 0) { + revert ProxyNonPayableFunction(); + } } } diff --git a/contracts/proxy/utils/Initializable.sol b/contracts/proxy/utils/Initializable.sol index 4ad0b9826..3ae5e4a65 100644 --- a/contracts/proxy/utils/Initializable.sol +++ b/contracts/proxy/utils/Initializable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (proxy/utils/Initializable.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/Initializable.sol) -pragma solidity ^0.8.2; +pragma solidity ^0.8.19; import "../../utils/Address.sol"; @@ -18,12 +18,13 @@ import "../../utils/Address.sol"; * For example: * * [.hljs-theme-light.nopadding] - * ``` + * ```solidity * contract MyToken is ERC20Upgradeable { * function initialize() initializer public { * __ERC20_init("MyToken", "MTK"); * } * } + * * contract MyTokenV2 is MyToken, ERC20PermitUpgradeable { * function initializeV2() reinitializer(2) public { * __ERC20Permit_init("MyToken"); @@ -66,6 +67,16 @@ abstract contract Initializable { */ bool private _initializing; + /** + * @dev The contract is already initialized. + */ + error AlreadyInitialized(); + + /** + * @dev The contract is not initializing. + */ + error NotInitializing(); + /** * @dev Triggered when the contract has been initialized or reinitialized. */ @@ -82,10 +93,9 @@ abstract contract Initializable { */ modifier initializer() { bool isTopLevelCall = !_initializing; - require( - (isTopLevelCall && _initialized < 1) || (address(this).code.length == 0 && _initialized == 1), - "Initializable: contract is already initialized" - ); + if (!(isTopLevelCall && _initialized < 1) && !(address(this).code.length == 0 && _initialized == 1)) { + revert AlreadyInitialized(); + } _initialized = 1; if (isTopLevelCall) { _initializing = true; @@ -116,7 +126,9 @@ abstract contract Initializable { * Emits an {Initialized} event. */ modifier reinitializer(uint8 version) { - require(!_initializing && _initialized < version, "Initializable: contract is already initialized"); + if (_initializing || _initialized >= version) { + revert AlreadyInitialized(); + } _initialized = version; _initializing = true; _; @@ -129,7 +141,9 @@ abstract contract Initializable { * {initializer} and {reinitializer} modifiers, directly or indirectly. */ modifier onlyInitializing() { - require(_initializing, "Initializable: contract is not initializing"); + if (!_initializing) { + revert NotInitializing(); + } _; } @@ -142,7 +156,9 @@ abstract contract Initializable { * Emits an {Initialized} event the first time it is successfully executed. */ function _disableInitializers() internal virtual { - require(!_initializing, "Initializable: contract is initializing"); + if (_initializing) { + revert AlreadyInitialized(); + } if (_initialized != type(uint8).max) { _initialized = type(uint8).max; emit Initialized(type(uint8).max); diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index 8b7366328..07de178bc 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (proxy/utils/UUPSUpgradeable.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (proxy/utils/UUPSUpgradeable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../interfaces/draft-IERC1822.sol"; -import "../ERC1967/ERC1967Upgrade.sol"; +import "../ERC1967/ERC1967Utils.sol"; /** * @dev An upgradeability mechanism designed for UUPS proxies. The functions included here can perform an upgrade of an @@ -18,10 +18,20 @@ import "../ERC1967/ERC1967Upgrade.sol"; * * _Available since v4.1._ */ -abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { +abstract contract UUPSUpgradeable is IERC1822Proxiable { /// @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment address private immutable __self = address(this); + /** + * @dev The call is from an unauthorized context. + */ + error UUPSUnauthorizedCallContext(); + + /** + * @dev The storage `slot` is unsupported as a UUID. + */ + error UUPSUnsupportedProxiableUUID(bytes32 slot); + /** * @dev Check that the execution is being performed through a delegatecall call and that the execution context is * a proxy contract with an implementation (as defined in ERC1967) pointing to self. This should only be the case @@ -30,8 +40,12 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * fail. */ modifier onlyProxy() { - require(address(this) != __self, "Function must be called through delegatecall"); - require(_getImplementation() == __self, "Function must be called through active proxy"); + if ( + address(this) == __self || // Must be called through delegatecall + ERC1967Utils.getImplementation() != __self // Must be called through an active proxy + ) { + revert UUPSUnauthorizedCallContext(); + } _; } @@ -40,7 +54,10 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * callable on the implementing contract but not through proxies. */ modifier notDelegated() { - require(address(this) == __self, "UUPSUpgradeable: must not be called through delegatecall"); + if (address(this) != __self) { + // Must not be called through delegatecall + revert UUPSUnauthorizedCallContext(); + } _; } @@ -52,8 +69,8 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * bricking a proxy that upgrades to it, by delegating to itself until out of gas. Thus it is critical that this * function revert if invoked through a proxy. This is guaranteed by the `notDelegated` modifier. */ - function proxiableUUID() external view virtual override notDelegated returns (bytes32) { - return _IMPLEMENTATION_SLOT; + function proxiableUUID() external view virtual notDelegated returns (bytes32) { + return ERC1967Utils.IMPLEMENTATION_SLOT; } /** @@ -62,8 +79,10 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * Calls {_authorizeUpgrade}. * * Emits an {Upgraded} event. + * + * @custom:oz-upgrades-unsafe-allow-reachable delegatecall */ - function upgradeTo(address newImplementation) external virtual onlyProxy { + function upgradeTo(address newImplementation) public virtual onlyProxy { _authorizeUpgrade(newImplementation); _upgradeToAndCallUUPS(newImplementation, new bytes(0), false); } @@ -75,8 +94,10 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * Calls {_authorizeUpgrade}. * * Emits an {Upgraded} event. + * + * @custom:oz-upgrades-unsafe-allow-reachable delegatecall */ - function upgradeToAndCall(address newImplementation, bytes memory data) external payable virtual onlyProxy { + function upgradeToAndCall(address newImplementation, bytes memory data) public payable virtual onlyProxy { _authorizeUpgrade(newImplementation); _upgradeToAndCallUUPS(newImplementation, data, true); } @@ -88,8 +109,25 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable, ERC1967Upgrade { * Normally, this function will use an xref:access.adoc[access control] modifier such as {Ownable-onlyOwner}. * * ```solidity - * function _authorizeUpgrade(address) internal override onlyOwner {} + * function _authorizeUpgrade(address) internal onlyOwner {} * ``` */ function _authorizeUpgrade(address newImplementation) internal virtual; + + /** + * @dev Perform implementation upgrade with security checks for UUPS proxies, and additional setup call. + * + * Emits an {IERC1967-Upgraded} event. + */ + function _upgradeToAndCallUUPS(address newImplementation, bytes memory data, bool forceCall) private { + try IERC1822Proxiable(newImplementation).proxiableUUID() returns (bytes32 slot) { + if (slot != ERC1967Utils.IMPLEMENTATION_SLOT) { + revert UUPSUnsupportedProxiableUUID(slot); + } + ERC1967Utils.upgradeToAndCall(newImplementation, data, forceCall); + } catch { + // The implementation is not UUPS + revert ERC1967Utils.ERC1967InvalidImplementation(newImplementation); + } + } } diff --git a/contracts/security/Pausable.sol b/contracts/security/Pausable.sol index bdd118432..dc0afa663 100644 --- a/contracts/security/Pausable.sol +++ b/contracts/security/Pausable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (security/Pausable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../utils/Context.sol"; @@ -15,6 +15,8 @@ import "../utils/Context.sol"; * simply including this module, only once the modifiers are put in place. */ abstract contract Pausable is Context { + bool private _paused; + /** * @dev Emitted when the pause is triggered by `account`. */ @@ -25,7 +27,15 @@ abstract contract Pausable is Context { */ event Unpaused(address account); - bool private _paused; + /** + * @dev The operation failed because the contract is paused. + */ + error EnforcedPause(); + + /** + * @dev The operation failed because the contract is not paused. + */ + error ExpectedPause(); /** * @dev Initializes the contract in unpaused state. @@ -69,14 +79,18 @@ abstract contract Pausable is Context { * @dev Throws if the contract is paused. */ function _requireNotPaused() internal view virtual { - require(!paused(), "Pausable: paused"); + if (paused()) { + revert EnforcedPause(); + } } /** * @dev Throws if the contract is not paused. */ function _requirePaused() internal view virtual { - require(paused(), "Pausable: not paused"); + if (!paused()) { + revert ExpectedPause(); + } } /** diff --git a/contracts/security/PullPayment.sol b/contracts/security/PullPayment.sol deleted file mode 100644 index 65b4980f6..000000000 --- a/contracts/security/PullPayment.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (security/PullPayment.sol) - -pragma solidity ^0.8.0; - -import "../utils/escrow/Escrow.sol"; - -/** - * @dev Simple implementation of a - * https://consensys.github.io/smart-contract-best-practices/development-recommendations/general/external-calls/#favor-pull-over-push-for-external-calls[pull-payment] - * strategy, where the paying contract doesn't interact directly with the - * receiver account, which must withdraw its payments itself. - * - * Pull-payments are often considered the best practice when it comes to sending - * Ether, security-wise. It prevents recipients from blocking execution, and - * eliminates reentrancy concerns. - * - * TIP: If you would like to learn more about reentrancy and alternative ways - * to protect against it, check out our blog post - * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul]. - * - * To use, derive from the `PullPayment` contract, and use {_asyncTransfer} - * instead of Solidity's `transfer` function. Payees can query their due - * payments with {payments}, and retrieve them with {withdrawPayments}. - */ -abstract contract PullPayment { - Escrow private immutable _escrow; - - constructor() { - _escrow = new Escrow(); - } - - /** - * @dev Withdraw accumulated payments, forwarding all gas to the recipient. - * - * Note that _any_ account can call this function, not just the `payee`. - * This means that contracts unaware of the `PullPayment` protocol can still - * receive funds this way, by having a separate account call - * {withdrawPayments}. - * - * WARNING: Forwarding all gas opens the door to reentrancy vulnerabilities. - * Make sure you trust the recipient, or are either following the - * checks-effects-interactions pattern or using {ReentrancyGuard}. - * - * @param payee Whose payments will be withdrawn. - * - * Causes the `escrow` to emit a {Withdrawn} event. - */ - function withdrawPayments(address payable payee) public virtual { - _escrow.withdraw(payee); - } - - /** - * @dev Returns the payments owed to an address. - * @param dest The creditor's address. - */ - function payments(address dest) public view returns (uint256) { - return _escrow.depositsOf(dest); - } - - /** - * @dev Called by the payer to store the sent amount as credit to be pulled. - * Funds sent in this way are stored in an intermediate {Escrow} contract, so - * there is no danger of them being spent before withdrawal. - * - * @param dest The destination address of the funds. - * @param amount The amount to transfer. - * - * Causes the `escrow` to emit a {Deposited} event. - */ - function _asyncTransfer(address dest, uint256 amount) internal virtual { - _escrow.deposit{value: amount}(dest); - } -} diff --git a/contracts/security/README.adoc b/contracts/security/README.adoc index 66f398fec..7f4799eb8 100644 --- a/contracts/security/README.adoc +++ b/contracts/security/README.adoc @@ -5,7 +5,6 @@ NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/ These contracts aim to cover common security practices. -* {PullPayment}: A pattern that can be used to avoid reentrancy attacks. * {ReentrancyGuard}: A modifier that can prevent reentrancy during certain functions. * {Pausable}: A common emergency response mechanism that can pause functionality while a remediation is pending. @@ -13,8 +12,6 @@ TIP: For an overview on reentrancy and the possible mechanisms to prevent it, re == Contracts -{{PullPayment}} - {{ReentrancyGuard}} {{Pausable}} diff --git a/contracts/security/ReentrancyGuard.sol b/contracts/security/ReentrancyGuard.sol index f9281ec64..40ae5b050 100644 --- a/contracts/security/ReentrancyGuard.sol +++ b/contracts/security/ReentrancyGuard.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (security/ReentrancyGuard.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (security/ReentrancyGuard.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Contract module that helps prevent reentrant calls to a function. @@ -36,6 +36,11 @@ abstract contract ReentrancyGuard { uint256 private _status; + /** + * @dev Unauthorized reentrant call. + */ + error ReentrancyGuardReentrantCall(); + constructor() { _status = _NOT_ENTERED; } @@ -55,7 +60,9 @@ abstract contract ReentrancyGuard { function _nonReentrantBefore() private { // On the first call to nonReentrant, _status will be _NOT_ENTERED - require(_status != _ENTERED, "ReentrancyGuard: reentrant call"); + if (_status == _ENTERED) { + revert ReentrancyGuardReentrantCall(); + } // Any calls to nonReentrant after this point will fail _status = _ENTERED; diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index c72026734..e4bd74826 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -1,13 +1,15 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC1155/ERC1155.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/ERC1155.sol) -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; import "./IERC1155.sol"; import "./IERC1155Receiver.sol"; import "./extensions/IERC1155MetadataURI.sol"; import "../../utils/Context.sol"; import "../../utils/introspection/ERC165.sol"; +import "../../utils/Arrays.sol"; +import "../../interfaces/draft-IERC6093.sol"; /** * @dev Implementation of the basic standard multi-token. @@ -16,7 +18,10 @@ import "../../utils/introspection/ERC165.sol"; * * _Available since v3.1._ */ -contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { +abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IERC1155Errors { + using Arrays for uint256[]; + using Arrays for address[]; + // Mapping from token ID to account balances mapping(uint256 => mapping(address => uint256)) private _balances; @@ -53,7 +58,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * Clients calling this function must replace the `\{id\}` substring with the * actual token type ID. */ - function uri(uint256) public view virtual override returns (string memory) { + function uri(uint256) public view virtual returns (string memory) { return _uri; } @@ -64,8 +69,7 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * * - `account` cannot be the zero address. */ - function balanceOf(address account, uint256 id) public view virtual override returns (uint256) { - require(account != address(0), "ERC1155: address zero is not a valid owner"); + function balanceOf(address account, uint256 id) public view virtual returns (uint256) { return _balances[id][account]; } @@ -79,13 +83,15 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { function balanceOfBatch( address[] memory accounts, uint256[] memory ids - ) public view virtual override returns (uint256[] memory) { - require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch"); + ) public view virtual returns (uint256[] memory) { + if (accounts.length != ids.length) { + revert ERC1155InvalidArrayLength(ids.length, accounts.length); + } uint256[] memory batchBalances = new uint256[](accounts.length); for (uint256 i = 0; i < accounts.length; ++i) { - batchBalances[i] = balanceOf(accounts[i], ids[i]); + batchBalances[i] = balanceOf(accounts.unsafeMemoryAccess(i), ids.unsafeMemoryAccess(i)); } return batchBalances; @@ -94,31 +100,24 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { /** * @dev See {IERC1155-setApprovalForAll}. */ - function setApprovalForAll(address operator, bool approved) public virtual override { + function setApprovalForAll(address operator, bool approved) public virtual { _setApprovalForAll(_msgSender(), operator, approved); } /** * @dev See {IERC1155-isApprovedForAll}. */ - function isApprovedForAll(address account, address operator) public view virtual override returns (bool) { + function isApprovedForAll(address account, address operator) public view virtual returns (bool) { return _operatorApprovals[account][operator]; } /** * @dev See {IERC1155-safeTransferFrom}. */ - function safeTransferFrom( - address from, - address to, - uint256 id, - uint256 amount, - bytes memory data - ) public virtual override { - require( - from == _msgSender() || isApprovedForAll(from, _msgSender()), - "ERC1155: caller is not token owner or approved" - ); + function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data) public virtual { + if (from != _msgSender() && !isApprovedForAll(from, _msgSender())) { + revert ERC1155MissingApprovalForAll(_msgSender(), from); + } _safeTransferFrom(from, to, id, amount, data); } @@ -131,11 +130,10 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { uint256[] memory ids, uint256[] memory amounts, bytes memory data - ) public virtual override { - require( - from == _msgSender() || isApprovedForAll(from, _msgSender()), - "ERC1155: caller is not token owner or approved" - ); + ) public virtual { + if (from != _msgSender() && !isApprovedForAll(from, _msgSender())) { + revert ERC1155MissingApprovalForAll(_msgSender(), from); + } _safeBatchTransferFrom(from, to, ids, amounts, data); } @@ -156,17 +154,21 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { uint256[] memory amounts, bytes memory data ) internal virtual { - require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); + if (ids.length != amounts.length) { + revert ERC1155InvalidArrayLength(ids.length, amounts.length); + } address operator = _msgSender(); for (uint256 i = 0; i < ids.length; ++i) { - uint256 id = ids[i]; - uint256 amount = amounts[i]; + uint256 id = ids.unsafeMemoryAccess(i); + uint256 amount = amounts.unsafeMemoryAccess(i); if (from != address(0)) { uint256 fromBalance = _balances[id][from]; - require(fromBalance >= amount, "ERC1155: insufficient balance for transfer"); + if (fromBalance < amount) { + revert ERC1155InsufficientBalance(from, fromBalance, amount, id); + } unchecked { _balances[id][from] = fromBalance - amount; } @@ -178,8 +180,8 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { } if (ids.length == 1) { - uint256 id = ids[0]; - uint256 amount = amounts[0]; + uint256 id = ids.unsafeMemoryAccess(0); + uint256 amount = amounts.unsafeMemoryAccess(0); emit TransferSingle(operator, from, to, id, amount); if (to != address(0)) { _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); @@ -205,10 +207,13 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * acceptance magic value. */ function _safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes memory data) internal { - require(to != address(0), "ERC1155: transfer to the zero address"); - require(from != address(0), "ERC1155: transfer from the zero address"); - uint256[] memory ids = _asSingletonArray(id); - uint256[] memory amounts = _asSingletonArray(amount); + if (to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + (uint256[] memory ids, uint256[] memory amounts) = _asSingletonArrays(id, amount); _update(from, to, ids, amounts, data); } @@ -229,8 +234,12 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { uint256[] memory amounts, bytes memory data ) internal { - require(to != address(0), "ERC1155: transfer to the zero address"); - require(from != address(0), "ERC1155: transfer from the zero address"); + if (to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + if (from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } _update(from, to, ids, amounts, data); } @@ -269,9 +278,10 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * acceptance magic value. */ function _mint(address to, uint256 id, uint256 amount, bytes memory data) internal { - require(to != address(0), "ERC1155: mint to the zero address"); - uint256[] memory ids = _asSingletonArray(id); - uint256[] memory amounts = _asSingletonArray(amount); + if (to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } + (uint256[] memory ids, uint256[] memory amounts) = _asSingletonArrays(id, amount); _update(address(0), to, ids, amounts, data); } @@ -287,7 +297,9 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * acceptance magic value. */ function _mintBatch(address to, uint256[] memory ids, uint256[] memory amounts, bytes memory data) internal { - require(to != address(0), "ERC1155: mint to the zero address"); + if (to == address(0)) { + revert ERC1155InvalidReceiver(address(0)); + } _update(address(0), to, ids, amounts, data); } @@ -302,9 +314,10 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * - `from` must have at least `amount` tokens of token type `id`. */ function _burn(address from, uint256 id, uint256 amount) internal { - require(from != address(0), "ERC1155: burn from the zero address"); - uint256[] memory ids = _asSingletonArray(id); - uint256[] memory amounts = _asSingletonArray(amount); + if (from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } + (uint256[] memory ids, uint256[] memory amounts) = _asSingletonArrays(id, amount); _update(from, address(0), ids, amounts, ""); } @@ -318,7 +331,9 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * - `ids` and `amounts` must have the same length. */ function _burnBatch(address from, uint256[] memory ids, uint256[] memory amounts) internal { - require(from != address(0), "ERC1155: burn from the zero address"); + if (from == address(0)) { + revert ERC1155InvalidSender(address(0)); + } _update(from, address(0), ids, amounts, ""); } @@ -328,7 +343,9 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { * Emits an {ApprovalForAll} event. */ function _setApprovalForAll(address owner, address operator, bool approved) internal virtual { - require(owner != operator, "ERC1155: setting approval status for self"); + if (owner == operator) { + revert ERC1155InvalidOperator(operator); + } _operatorApprovals[owner][operator] = approved; emit ApprovalForAll(owner, operator, approved); } @@ -344,12 +361,19 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { if (to.code.length > 0) { try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) { if (response != IERC1155Receiver.onERC1155Received.selector) { - revert("ERC1155: ERC1155Receiver rejected tokens"); + // Tokens rejected + revert ERC1155InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + // non-ERC1155Receiver implementer + revert ERC1155InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } } - } catch Error(string memory reason) { - revert(reason); - } catch { - revert("ERC1155: transfer to non-ERC1155Receiver implementer"); } } } @@ -367,20 +391,38 @@ contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI { bytes4 response ) { if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { - revert("ERC1155: ERC1155Receiver rejected tokens"); + // Tokens rejected + revert ERC1155InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + // non-ERC1155Receiver implementer + revert ERC1155InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } } - } catch Error(string memory reason) { - revert(reason); - } catch { - revert("ERC1155: transfer to non-ERC1155Receiver implementer"); } } } - function _asSingletonArray(uint256 element) private pure returns (uint256[] memory) { - uint256[] memory array = new uint256[](1); - array[0] = element; + function _asSingletonArrays( + uint256 element1, + uint256 element2 + ) private pure returns (uint256[] memory array1, uint256[] memory array2) { + /// @solidity memory-safe-assembly + assembly { + array1 := mload(0x40) + mstore(array1, 1) + mstore(add(array1, 0x20), element1) - return array; + array2 := add(array1, 0x40) + mstore(array2, 1) + mstore(add(array2, 0x20), element2) + + mstore(0x40, add(array2, 0x40)) + } } } diff --git a/contracts/token/ERC1155/IERC1155.sol b/contracts/token/ERC1155/IERC1155.sol index eae0b7029..16b2b71a3 100644 --- a/contracts/token/ERC1155/IERC1155.sol +++ b/contracts/token/ERC1155/IERC1155.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC1155/IERC1155.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/IERC1155.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/introspection/IERC165.sol"; @@ -86,6 +86,11 @@ interface IERC1155 is IERC165 { /** * @dev Transfers `amount` tokens of token type `id` from `from` to `to`. * + * WARNING: This function can potentially allow a reentrancy attack when transferring tokens + * to an untrusted contract, when invoking {onERC1155Received} on the receiver. + * Ensure to follow the checks-effects-interactions pattern and consider employing + * reentrancy guards when interacting with untrusted contracts. + * * Emits a {TransferSingle} event. * * Requirements: @@ -101,6 +106,12 @@ interface IERC1155 is IERC165 { /** * @dev xref:ROOT:erc1155.adoc#batch-operations[Batched] version of {safeTransferFrom}. * + * + * WARNING: This function can potentially allow a reentrancy attack when transferring tokens + * to an untrusted contract, when invoking {onERC1155BatchReceived} on the receiver. + * Ensure to follow the checks-effects-interactions pattern and consider employing + * reentrancy guards when interacting with untrusted contracts. + * * Emits a {TransferBatch} event. * * Requirements: diff --git a/contracts/token/ERC1155/IERC1155Receiver.sol b/contracts/token/ERC1155/IERC1155Receiver.sol index 0dd271d04..a272f61b0 100644 --- a/contracts/token/ERC1155/IERC1155Receiver.sol +++ b/contracts/token/ERC1155/IERC1155Receiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/IERC1155Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/introspection/IERC165.sol"; diff --git a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol index cc81957a7..d7bee05d9 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Burnable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Burnable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC1155/extensions/ERC1155Burnable.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC1155/extensions/ERC1155Burnable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC1155.sol"; @@ -13,19 +13,17 @@ import "../ERC1155.sol"; */ abstract contract ERC1155Burnable is ERC1155 { function burn(address account, uint256 id, uint256 value) public virtual { - require( - account == _msgSender() || isApprovedForAll(account, _msgSender()), - "ERC1155: caller is not token owner or approved" - ); + if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) { + revert ERC1155MissingApprovalForAll(_msgSender(), account); + } _burn(account, id, value); } function burnBatch(address account, uint256[] memory ids, uint256[] memory values) public virtual { - require( - account == _msgSender() || isApprovedForAll(account, _msgSender()), - "ERC1155: caller is not token owner or approved" - ); + if (account != _msgSender() && !isApprovedForAll(account, _msgSender())) { + revert ERC1155MissingApprovalForAll(_msgSender(), account); + } _burnBatch(account, ids, values); } diff --git a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol index f37d90e25..f8357062c 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC1155/extensions/ERC1155Pausable.sol) +// OpenZeppelin Contracts (last updated v4.8.2) (token/ERC1155/extensions/ERC1155Pausable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC1155.sol"; import "../../../security/Pausable.sol"; @@ -13,6 +13,12 @@ import "../../../security/Pausable.sol"; * period, or having an emergency switch for freezing all token transfers in the * event of a large bug. * + * IMPORTANT: This contract does not include public pause and unpause functions. In + * addition to inheriting this contract, you must define both functions, invoking the + * {Pausable-_pause} and {Pausable-_unpause} internal functions, with appropriate + * access control, e.g. using {AccessControl} or {Ownable}. Not doing so will + * make the contract unpausable. + * * _Available since v3.1._ */ abstract contract ERC1155Pausable is ERC1155, Pausable { @@ -29,8 +35,7 @@ abstract contract ERC1155Pausable is ERC1155, Pausable { uint256[] memory ids, uint256[] memory amounts, bytes memory data - ) internal virtual override { - require(!paused(), "ERC1155Pausable: token transfer while paused"); + ) internal virtual override whenNotPaused { super._update(from, to, ids, amounts, data); } } diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol index 6596c4aed..f32fbb74b 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Supply.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC1155/extensions/ERC1155Supply.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC1155.sol"; @@ -12,6 +12,9 @@ import "../ERC1155.sol"; * clearly identified. Note: While a totalSupply of 1 might mean the * corresponding is an NFT, there is no guarantees that no other token with the * same id are not going to be minted. + * + * NOTE: This contract implies a global limit of 2**256 - 1 to the number of tokens + * that can be minted. */ abstract contract ERC1155Supply is ERC1155 { mapping(uint256 => uint256) private _totalSupply; @@ -35,7 +38,7 @@ abstract contract ERC1155Supply is ERC1155 { * @dev Indicates whether any token exist with a given id, or not. */ function exists(uint256 id) public view virtual returns (bool) { - return ERC1155Supply.totalSupply(id) > 0; + return totalSupply(id) > 0; } /** @@ -63,11 +66,8 @@ abstract contract ERC1155Supply is ERC1155 { for (uint256 i = 0; i < ids.length; ++i) { uint256 id = ids[i]; uint256 amount = amounts[i]; - uint256 supply = _totalSupply[id]; - require(supply >= amount, "ERC1155: burn amount exceeds totalSupply"); + _totalSupply[id] -= amount; unchecked { - // Overflow not possible: amounts[i] <= totalSupply(i) - _totalSupply[id] = supply - amount; // Overflow not possible: sum(amounts[i]) <= sum(totalSupply(i)) <= totalSupplyAll totalBurnAmount += amount; } diff --git a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol index 623504f3d..79782a42b 100644 --- a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol +++ b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC1155/extensions/ERC1155URIStorage.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../../utils/Strings.sol"; import "../ERC1155.sol"; @@ -42,8 +42,8 @@ abstract contract ERC1155URIStorage is ERC1155 { function uri(uint256 tokenId) public view virtual override returns (string memory) { string memory tokenURI = _tokenURIs[tokenId]; - // If token URI is set, concatenate base URI and tokenURI (via abi.encodePacked). - return bytes(tokenURI).length > 0 ? string(abi.encodePacked(_baseURI, tokenURI)) : super.uri(tokenId); + // If token URI is set, concatenate base URI and tokenURI (via string.concat). + return bytes(tokenURI).length > 0 ? string.concat(_baseURI, tokenURI) : super.uri(tokenId); } /** diff --git a/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol b/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol index 520a29715..2c998995e 100644 --- a/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol +++ b/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC1155/extensions/IERC1155MetadataURI.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC1155.sol"; diff --git a/contracts/token/ERC1155/utils/ERC1155Holder.sol b/contracts/token/ERC1155/utils/ERC1155Holder.sol index 7249de841..d24a1a53a 100644 --- a/contracts/token/ERC1155/utils/ERC1155Holder.sol +++ b/contracts/token/ERC1155/utils/ERC1155Holder.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/utils/ERC1155Holder.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./ERC1155Receiver.sol"; @@ -13,7 +13,7 @@ import "./ERC1155Receiver.sol"; * * @dev _Available since v3.1._ */ -contract ERC1155Holder is ERC1155Receiver { +abstract contract ERC1155Holder is ERC1155Receiver { function onERC1155Received( address, address, diff --git a/contracts/token/ERC1155/utils/ERC1155Receiver.sol b/contracts/token/ERC1155/utils/ERC1155Receiver.sol index 2e6804a2d..6fb21491f 100644 --- a/contracts/token/ERC1155/utils/ERC1155Receiver.sol +++ b/contracts/token/ERC1155/utils/ERC1155Receiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC1155/utils/ERC1155Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC1155Receiver.sol"; import "../../../utils/introspection/ERC165.sol"; diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index 982eabb7d..5ddebf22b 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -1,11 +1,12 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/ERC20.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/ERC20.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC20.sol"; import "./extensions/IERC20Metadata.sol"; import "../../utils/Context.sol"; +import "../../interfaces/draft-IERC6093.sol"; /** * @dev Implementation of the {IERC20} interface. @@ -34,7 +35,7 @@ import "../../utils/Context.sol"; * functions have been added to mitigate the well-known issues around setting * allowances. See {IERC20-approve}. */ -contract ERC20 is Context, IERC20, IERC20Metadata { +abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { mapping(address => uint256) private _balances; mapping(address => mapping(address => uint256)) private _allowances; @@ -44,6 +45,11 @@ contract ERC20 is Context, IERC20, IERC20Metadata { string private _name; string private _symbol; + /** + * @dev Indicates a failed `decreaseAllowance` request. + */ + error ERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); + /** * @dev Sets the values for {name} and {symbol}. * @@ -58,7 +64,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { /** * @dev Returns the name of the token. */ - function name() public view virtual override returns (string memory) { + function name() public view virtual returns (string memory) { return _name; } @@ -66,7 +72,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * @dev Returns the symbol of the token, usually a shorter version of the * name. */ - function symbol() public view virtual override returns (string memory) { + function symbol() public view virtual returns (string memory) { return _symbol; } @@ -83,21 +89,21 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * no way affects any of the arithmetic of the contract, including * {IERC20-balanceOf} and {IERC20-transfer}. */ - function decimals() public view virtual override returns (uint8) { + function decimals() public view virtual returns (uint8) { return 18; } /** * @dev See {IERC20-totalSupply}. */ - function totalSupply() public view virtual override returns (uint256) { + function totalSupply() public view virtual returns (uint256) { return _totalSupply; } /** * @dev See {IERC20-balanceOf}. */ - function balanceOf(address account) public view virtual override returns (uint256) { + function balanceOf(address account) public view virtual returns (uint256) { return _balances[account]; } @@ -109,7 +115,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * - `to` cannot be the zero address. * - the caller must have a balance of at least `amount`. */ - function transfer(address to, uint256 amount) public virtual override returns (bool) { + function transfer(address to, uint256 amount) public virtual returns (bool) { address owner = _msgSender(); _transfer(owner, to, amount); return true; @@ -118,7 +124,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { /** * @dev See {IERC20-allowance}. */ - function allowance(address owner, address spender) public view virtual override returns (uint256) { + function allowance(address owner, address spender) public view virtual returns (uint256) { return _allowances[owner][spender]; } @@ -132,7 +138,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * * - `spender` cannot be the zero address. */ - function approve(address spender, uint256 amount) public virtual override returns (bool) { + function approve(address spender, uint256 amount) public virtual returns (bool) { address owner = _msgSender(); _approve(owner, spender, amount); return true; @@ -154,7 +160,7 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * - the caller must have allowance for ``from``'s tokens of at least * `amount`. */ - function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) { + function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) { address spender = _msgSender(); _spendAllowance(from, spender, amount); _transfer(from, to, amount); @@ -191,14 +197,16 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * * - `spender` cannot be the zero address. * - `spender` must have allowance for the caller of at least - * `subtractedValue`. + * `requestedDecrease`. */ - function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) { + function decreaseAllowance(address spender, uint256 requestedDecrease) public virtual returns (bool) { address owner = _msgSender(); uint256 currentAllowance = allowance(owner, spender); - require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero"); + if (currentAllowance < requestedDecrease) { + revert ERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); + } unchecked { - _approve(owner, spender, currentAllowance - subtractedValue); + _approve(owner, spender, currentAllowance - requestedDecrease); } return true; @@ -215,8 +223,12 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * NOTE: This function is not virtual, {_update} should be overridden instead. */ function _transfer(address from, address to, uint256 amount) internal { - require(from != address(0), "ERC20: transfer from the zero address"); - require(to != address(0), "ERC20: transfer to the zero address"); + if (from == address(0)) { + revert ERC20InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } _update(from, to, amount); } @@ -231,7 +243,9 @@ contract ERC20 is Context, IERC20, IERC20Metadata { _totalSupply += amount; } else { uint256 fromBalance = _balances[from]; - require(fromBalance >= amount, "ERC20: transfer amount exceeds balance"); + if (fromBalance < amount) { + revert ERC20InsufficientBalance(from, fromBalance, amount); + } unchecked { // Overflow not possible: amount <= fromBalance <= totalSupply. _balances[from] = fromBalance - amount; @@ -262,7 +276,9 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * NOTE: This function is not virtual, {_update} should be overridden instead. */ function _mint(address account, uint256 amount) internal { - require(account != address(0), "ERC20: mint to the zero address"); + if (account == address(0)) { + revert ERC20InvalidReceiver(address(0)); + } _update(address(0), account, amount); } @@ -275,7 +291,9 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * NOTE: This function is not virtual, {_update} should be overridden instead */ function _burn(address account, uint256 amount) internal { - require(account != address(0), "ERC20: burn from the zero address"); + if (account == address(0)) { + revert ERC20InvalidSender(address(0)); + } _update(account, address(0), amount); } @@ -293,11 +311,37 @@ contract ERC20 is Context, IERC20, IERC20Metadata { * - `spender` cannot be the zero address. */ function _approve(address owner, address spender, uint256 amount) internal virtual { - require(owner != address(0), "ERC20: approve from the zero address"); - require(spender != address(0), "ERC20: approve to the zero address"); + _approve(owner, spender, amount, true); + } + /** + * @dev Alternative version of {_approve} with an optional flag that can enable or disable the Approval event. + * + * By default (when calling {_approve}) the flag is set to true. On the other hand, approval changes made by + * `_spendAllowance` during the `transferFrom` operation set the flag to false. This saves gas by not emitting any + * `Approval` event during `transferFrom` operations. + * + * Anyone who wishes to continue emitting `Approval` events on the`transferFrom` operation can force the flag to true + * using the following override: + * ``` + * function _approve(address owner, address spender, uint256 amount, bool) internal virtual override { + * super._approve(owner, spender, amount, true); + * } + * ``` + * + * Requirements are the same as {_approve}. + */ + function _approve(address owner, address spender, uint256 amount, bool emitEvent) internal virtual { + if (owner == address(0)) { + revert ERC20InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC20InvalidSpender(address(0)); + } _allowances[owner][spender] = amount; - emit Approval(owner, spender, amount); + if (emitEvent) { + emit Approval(owner, spender, amount); + } } /** @@ -311,9 +355,11 @@ contract ERC20 is Context, IERC20, IERC20Metadata { function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { uint256 currentAllowance = allowance(owner, spender); if (currentAllowance != type(uint256).max) { - require(currentAllowance >= amount, "ERC20: insufficient allowance"); + if (currentAllowance < amount) { + revert ERC20InsufficientAllowance(spender, currentAllowance, amount); + } unchecked { - _approve(owner, spender, currentAllowance - amount); + _approve(owner, spender, currentAllowance - amount, false); } } } diff --git a/contracts/token/ERC20/IERC20.sol b/contracts/token/ERC20/IERC20.sol index 66c4e4d88..a19535a30 100644 --- a/contracts/token/ERC20/IERC20.sol +++ b/contracts/token/ERC20/IERC20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/IERC20.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC20 standard as defined in the EIP. diff --git a/contracts/token/ERC20/README.adoc b/contracts/token/ERC20/README.adoc index 326aae684..9482b581b 100644 --- a/contracts/token/ERC20/README.adoc +++ b/contracts/token/ERC20/README.adoc @@ -18,18 +18,19 @@ Additionally there are multiple custom extensions, including: * {ERC20Burnable}: destruction of own tokens. * {ERC20Capped}: enforcement of a cap to the total supply when minting tokens. * {ERC20Pausable}: ability to pause token transfers. -* {ERC20Snapshot}: efficient storage of past token balances to be later queried at any point in time. * {ERC20Permit}: gasless approval of tokens (standardized as ERC2612). * {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC3156). * {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}. * {ERC4626}: 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. +Finally, there are some utilities to interact with ERC20 contracts in various ways: * {SafeERC20}: a wrapper around the interface that eliminates the need to handle boolean return values. -* {TokenTimelock}: hold tokens for a beneficiary until a specified time. + +Other utilities that support ERC20 assets can be found in codebase: + +* ERC20 tokens can be timelocked (held tokens for a beneficiary until a specified time) or vested (released following a given schedule) using a {VestingWallet}. NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC20 (such as <>) and expose them as external functions in the way they prefer. @@ -51,12 +52,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC20Permit}} -{{ERC20Snapshot}} - {{ERC20Votes}} -{{ERC20VotesComp}} - {{ERC20Wrapper}} {{ERC20FlashMint}} @@ -66,5 +63,3 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel == Utilities {{SafeERC20}} - -{{TokenTimelock}} diff --git a/contracts/token/ERC20/extensions/ERC20Burnable.sol b/contracts/token/ERC20/extensions/ERC20Burnable.sol index 1cd08ee81..cae186b64 100644 --- a/contracts/token/ERC20/extensions/ERC20Burnable.sol +++ b/contracts/token/ERC20/extensions/ERC20Burnable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/extensions/ERC20Burnable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; import "../../../utils/Context.sol"; diff --git a/contracts/token/ERC20/extensions/ERC20Capped.sol b/contracts/token/ERC20/extensions/ERC20Capped.sol index b7ddd788c..41e9ce5cf 100644 --- a/contracts/token/ERC20/extensions/ERC20Capped.sol +++ b/contracts/token/ERC20/extensions/ERC20Capped.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/ERC20Capped.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; @@ -11,12 +11,24 @@ import "../ERC20.sol"; abstract contract ERC20Capped is ERC20 { uint256 private immutable _cap; + /** + * @dev Total supply cap has been exceeded. + */ + error ERC20ExceededCap(uint256 increasedSupply, uint256 cap); + + /** + * @dev The supplied cap is not a valid cap. + */ + error ERC20InvalidCap(uint256 cap); + /** * @dev Sets the value of the `cap`. This value is immutable, it can only be * set once during construction. */ constructor(uint256 cap_) { - require(cap_ > 0, "ERC20Capped: cap is 0"); + if (cap_ == 0) { + revert ERC20InvalidCap(0); + } _cap = cap_; } @@ -28,13 +40,17 @@ abstract contract ERC20Capped is ERC20 { } /** - * @dev See {ERC20-_transfer}. + * @dev See {ERC20-_update}. */ function _update(address from, address to, uint256 amount) internal virtual override { - if (from == address(0)) { - require(totalSupply() + amount <= cap(), "ERC20Capped: cap exceeded"); - } - super._update(from, to, amount); + + if (from == address(0)) { + uint256 maxSupply = cap(); + uint256 supply = totalSupply(); + if (supply > maxSupply) { + revert ERC20ExceededCap(supply, maxSupply); + } + } } } diff --git a/contracts/token/ERC20/extensions/ERC20FlashMint.sol b/contracts/token/ERC20/extensions/ERC20FlashMint.sol index 063fe99fb..09c20bacc 100644 --- a/contracts/token/ERC20/extensions/ERC20FlashMint.sol +++ b/contracts/token/ERC20/extensions/ERC20FlashMint.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/ERC20FlashMint.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../../interfaces/IERC3156FlashBorrower.sol"; import "../../../interfaces/IERC3156FlashLender.sol"; @@ -19,13 +19,28 @@ import "../ERC20.sol"; abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { bytes32 private constant _RETURN_VALUE = keccak256("ERC3156FlashBorrower.onFlashLoan"); + /** + * @dev The loan token is not valid. + */ + error ERC3156UnsupportedToken(address token); + + /** + * @dev The requested loan exceeds the max loan amount for `token`. + */ + error ERC3156ExceededMaxLoan(uint256 maxLoan); + + /** + * @dev The receiver of a flashloan is not a valid {onFlashLoan} implementer. + */ + error ERC3156InvalidReceiver(address receiver); + /** * @dev Returns the maximum amount of tokens available for loan. * @param token The address of the token that is requested. * @return The amount of token that can be loaned. */ - function maxFlashLoan(address token) public view virtual override returns (uint256) { - return token == address(this) ? type(uint256).max - ERC20.totalSupply() : 0; + function maxFlashLoan(address token) public view virtual returns (uint256) { + return token == address(this) ? type(uint256).max - totalSupply() : 0; } /** @@ -36,8 +51,10 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { * @param amount The amount of tokens to be loaned. * @return The fees applied to the corresponding flash loan. */ - function flashFee(address token, uint256 amount) public view virtual override returns (uint256) { - require(token == address(this), "ERC20FlashMint: wrong token"); + function flashFee(address token, uint256 amount) public view virtual returns (uint256) { + if (token != address(this)) { + revert ERC3156UnsupportedToken(token); + } return _flashFee(token, amount); } @@ -88,14 +105,16 @@ abstract contract ERC20FlashMint is ERC20, IERC3156FlashLender { address token, uint256 amount, bytes calldata data - ) public virtual override returns (bool) { - require(amount <= maxFlashLoan(token), "ERC20FlashMint: amount exceeds maxFlashLoan"); + ) public virtual returns (bool) { + uint256 maxLoan = maxFlashLoan(token); + if (amount > maxLoan) { + revert ERC3156ExceededMaxLoan(maxLoan); + } uint256 fee = flashFee(token, amount); _mint(address(receiver), amount); - require( - receiver.onFlashLoan(msg.sender, token, amount, fee, data) == _RETURN_VALUE, - "ERC20FlashMint: invalid return value" - ); + if (receiver.onFlashLoan(msg.sender, token, amount, fee, data) != _RETURN_VALUE) { + revert ERC3156InvalidReceiver(address(receiver)); + } address flashFeeReceiver = _flashFeeReceiver(); _spendAllowance(address(receiver), address(this), amount + fee); if (fee == 0 || flashFeeReceiver == address(0)) { diff --git a/contracts/token/ERC20/extensions/ERC20Pausable.sol b/contracts/token/ERC20/extensions/ERC20Pausable.sol index 85dd164d3..5ef50f9c6 100644 --- a/contracts/token/ERC20/extensions/ERC20Pausable.sol +++ b/contracts/token/ERC20/extensions/ERC20Pausable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/ERC20Pausable.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Pausable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; import "../../../security/Pausable.sol"; @@ -12,6 +12,12 @@ import "../../../security/Pausable.sol"; * Useful for scenarios such as preventing trades until the end of an evaluation * period, or having an emergency switch for freezing all token transfers in the * event of a large bug. + * + * IMPORTANT: This contract does not include public pause and unpause functions. In + * addition to inheriting this contract, you must define both functions, invoking the + * {Pausable-_pause} and {Pausable-_unpause} internal functions, with appropriate + * access control, e.g. using {AccessControl} or {Ownable}. Not doing so will + * make the contract unpausable. */ abstract contract ERC20Pausable is ERC20, Pausable { /** @@ -21,8 +27,7 @@ abstract contract ERC20Pausable is ERC20, Pausable { * * - the contract must not be paused. */ - function _update(address from, address to, uint256 amount) internal virtual override { - require(!paused(), "ERC20Pausable: token transfer while paused"); + function _update(address from, address to, uint256 amount) internal virtual override whenNotPaused { super._update(from, to, amount); } } diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol index 70b48a8d4..4378eb7c1 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/extensions/ERC20Permit.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Permit.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC20Permit.sol"; import "../ERC20.sol"; @@ -23,14 +23,16 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { // solhint-disable-next-line var-name-mixedcase bytes32 private constant _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + /** - * @dev In previous versions `_PERMIT_TYPEHASH` was declared as `immutable`. - * However, to ensure consistency with the upgradeable transpiler, we will continue - * to reserve a slot. - * @custom:oz-renamed-from _PERMIT_TYPEHASH + * @dev Permit deadline has expired. */ - // solhint-disable-next-line var-name-mixedcase - bytes32 private _PERMIT_TYPEHASH_DEPRECATED_SLOT; + error ERC2612ExpiredSignature(uint256 deadline); + + /** + * @dev Mismatched signature. + */ + error ERC2612InvalidSigner(address signer, address owner); /** * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. @@ -50,15 +52,19 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { uint8 v, bytes32 r, bytes32 s - ) public virtual override { - require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + ) public virtual { + if (block.timestamp > deadline) { + revert ERC2612ExpiredSignature(deadline); + } bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); bytes32 hash = _hashTypedDataV4(structHash); address signer = ECDSA.recover(hash, v, r, s); - require(signer == owner, "ERC20Permit: invalid signature"); + if (signer != owner) { + revert ERC2612InvalidSigner(signer, owner); + } _approve(owner, spender, value); } @@ -74,7 +80,7 @@ abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { * @dev See {IERC20Permit-DOMAIN_SEPARATOR}. */ // solhint-disable-next-line func-name-mixedcase - function DOMAIN_SEPARATOR() external view override returns (bytes32) { + function DOMAIN_SEPARATOR() external view virtual returns (bytes32) { return _domainSeparatorV4(); } } diff --git a/contracts/token/ERC20/extensions/ERC20Snapshot.sol b/contracts/token/ERC20/extensions/ERC20Snapshot.sol deleted file mode 100644 index 4eae931bc..000000000 --- a/contracts/token/ERC20/extensions/ERC20Snapshot.sol +++ /dev/null @@ -1,189 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC20/extensions/ERC20Snapshot.sol) - -pragma solidity ^0.8.0; - -import "../ERC20.sol"; -import "../../../utils/Arrays.sol"; -import "../../../utils/Counters.sol"; - -/** - * @dev This contract extends an ERC20 token with a snapshot mechanism. When a snapshot is created, the balances and - * total supply at the time are recorded for later access. - * - * This can be used to safely create mechanisms based on token balances such as trustless dividends or weighted voting. - * In naive implementations it's possible to perform a "double spend" attack by reusing the same balance from different - * accounts. By using snapshots to calculate dividends or voting power, those attacks no longer apply. It can also be - * used to create an efficient ERC20 forking mechanism. - * - * Snapshots are created by the internal {_snapshot} function, which will emit the {Snapshot} event and return a - * snapshot id. To get the total supply at the time of a snapshot, call the function {totalSupplyAt} with the snapshot - * id. To get the balance of an account at the time of a snapshot, call the {balanceOfAt} function with the snapshot id - * and the account address. - * - * NOTE: Snapshot policy can be customized by overriding the {_getCurrentSnapshotId} method. For example, having it - * return `block.number` will trigger the creation of snapshot at the beginning of each new block. When overriding this - * function, be careful about the monotonicity of its result. Non-monotonic snapshot ids will break the contract. - * - * Implementing snapshots for every block using this method will incur significant gas costs. For a gas-efficient - * alternative consider {ERC20Votes}. - * - * ==== Gas Costs - * - * Snapshots are efficient. Snapshot creation is _O(1)_. Retrieval of balances or total supply from a snapshot is _O(log - * n)_ in the number of snapshots that have been created, although _n_ for a specific account will generally be much - * smaller since identical balances in subsequent snapshots are stored as a single entry. - * - * There is a constant overhead for normal ERC20 transfers due to the additional snapshot bookkeeping. This overhead is - * only significant for the first transfer that immediately follows a snapshot for a particular account. Subsequent - * transfers will have normal cost until the next snapshot, and so on. - */ - -abstract contract ERC20Snapshot is ERC20 { - // Inspired by Jordi Baylina's MiniMeToken to record historical balances: - // https://github.com/Giveth/minime/blob/ea04d950eea153a04c51fa510b068b9dded390cb/contracts/MiniMeToken.sol - - using Arrays for uint256[]; - using Counters for Counters.Counter; - - // Snapshotted values have arrays of ids and the value corresponding to that id. These could be an array of a - // Snapshot struct, but that would impede usage of functions that work on an array. - struct Snapshots { - uint256[] ids; - uint256[] values; - } - - mapping(address => Snapshots) private _accountBalanceSnapshots; - Snapshots private _totalSupplySnapshots; - - // Snapshot ids increase monotonically, with the first value being 1. An id of 0 is invalid. - Counters.Counter private _currentSnapshotId; - - /** - * @dev Emitted by {_snapshot} when a snapshot identified by `id` is created. - */ - event Snapshot(uint256 id); - - /** - * @dev Creates a new snapshot and returns its snapshot id. - * - * Emits a {Snapshot} event that contains the same id. - * - * {_snapshot} is `internal` and you have to decide how to expose it externally. Its usage may be restricted to a - * set of accounts, for example using {AccessControl}, or it may be open to the public. - * - * [WARNING] - * ==== - * While an open way of calling {_snapshot} is required for certain trust minimization mechanisms such as forking, - * you must consider that it can potentially be used by attackers in two ways. - * - * First, it can be used to increase the cost of retrieval of values from snapshots, although it will grow - * logarithmically thus rendering this attack ineffective in the long term. Second, it can be used to target - * specific accounts and increase the cost of ERC20 transfers for them, in the ways specified in the Gas Costs - * section above. - * - * We haven't measured the actual numbers; if this is something you're interested in please reach out to us. - * ==== - */ - function _snapshot() internal virtual returns (uint256) { - _currentSnapshotId.increment(); - - uint256 currentId = _getCurrentSnapshotId(); - emit Snapshot(currentId); - return currentId; - } - - /** - * @dev Get the current snapshotId - */ - function _getCurrentSnapshotId() internal view virtual returns (uint256) { - return _currentSnapshotId.current(); - } - - /** - * @dev Retrieves the balance of `account` at the time `snapshotId` was created. - */ - function balanceOfAt(address account, uint256 snapshotId) public view virtual returns (uint256) { - (bool snapshotted, uint256 value) = _valueAt(snapshotId, _accountBalanceSnapshots[account]); - - return snapshotted ? value : balanceOf(account); - } - - /** - * @dev Retrieves the total supply at the time `snapshotId` was created. - */ - function totalSupplyAt(uint256 snapshotId) public view virtual returns (uint256) { - (bool snapshotted, uint256 value) = _valueAt(snapshotId, _totalSupplySnapshots); - - return snapshotted ? value : totalSupply(); - } - - // Update balance and/or total supply snapshots before the values are modified. This is executed - // for _mint, _burn, and _transfer operations. - function _update(address from, address to, uint256 amount) internal virtual override { - if (from == address(0)) { - _updateTotalSupplySnapshot(); - } else { - _updateAccountSnapshot(from); - } - - if (to == address(0)) { - _updateTotalSupplySnapshot(); - } else { - _updateAccountSnapshot(to); - } - - super._update(from, to, amount); - } - - function _valueAt(uint256 snapshotId, Snapshots storage snapshots) private view returns (bool, uint256) { - require(snapshotId > 0, "ERC20Snapshot: id is 0"); - require(snapshotId <= _getCurrentSnapshotId(), "ERC20Snapshot: nonexistent id"); - - // When a valid snapshot is queried, there are three possibilities: - // a) The queried value was not modified after the snapshot was taken. Therefore, a snapshot entry was never - // created for this id, and all stored snapshot ids are smaller than the requested one. The value that corresponds - // to this id is the current one. - // b) The queried value was modified after the snapshot was taken. Therefore, there will be an entry with the - // requested id, and its value is the one to return. - // c) More snapshots were created after the requested one, and the queried value was later modified. There will be - // no entry for the requested id: the value that corresponds to it is that of the smallest snapshot id that is - // larger than the requested one. - // - // In summary, we need to find an element in an array, returning the index of the smallest value that is larger if - // it is not found, unless said value doesn't exist (e.g. when all values are smaller). Arrays.findUpperBound does - // exactly this. - - uint256 index = snapshots.ids.findUpperBound(snapshotId); - - if (index == snapshots.ids.length) { - return (false, 0); - } else { - return (true, snapshots.values[index]); - } - } - - function _updateAccountSnapshot(address account) private { - _updateSnapshot(_accountBalanceSnapshots[account], balanceOf(account)); - } - - function _updateTotalSupplySnapshot() private { - _updateSnapshot(_totalSupplySnapshots, totalSupply()); - } - - function _updateSnapshot(Snapshots storage snapshots, uint256 currentValue) private { - uint256 currentId = _getCurrentSnapshotId(); - if (_lastSnapshotId(snapshots.ids) < currentId) { - snapshots.ids.push(currentId); - snapshots.values.push(currentValue); - } - } - - function _lastSnapshotId(uint256[] storage ids) private view returns (uint256) { - if (ids.length == 0) { - return 0; - } else { - return ids[ids.length - 1]; - } - } -} diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index 225d08c44..98f798efe 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/extensions/ERC20Votes.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Votes.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; import "../../../governance/utils/Votes.sol"; @@ -11,7 +11,7 @@ import "../../../utils/math/SafeCast.sol"; * @dev Extension of ERC20 to support Compound-like voting and delegation. This version is more generic than Compound's, * and supports token supply up to 2^224^ - 1, while COMP is limited to 2^96^ - 1. * - * NOTE: If exact COMP compatibility is required, use the {ERC20VotesComp} variant of this module. + * NOTE: This contract does not provide interface compatibility with Compound's COMP token. * * This extension keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either * by calling the {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting @@ -23,6 +23,11 @@ import "../../../utils/math/SafeCast.sol"; * _Available since v4.2._ */ abstract contract ERC20Votes is ERC20, Votes { + /** + * @dev Total supply cap has been exceeded, introducing a risk of votes overflowing. + */ + error ERC20ExceededSafeSupply(uint256 increasedSupply, uint256 cap); + /** * @dev Maximum token supply. Defaults to `type(uint224).max` (2^224^ - 1). */ @@ -38,11 +43,22 @@ abstract contract ERC20Votes is ERC20, Votes { function _update(address from, address to, uint256 amount) internal virtual override { super._update(from, to, amount); if (from == address(0)) { - require(totalSupply() <= _maxSupply(), "ERC20Votes: total supply risks overflowing votes"); + uint256 supply = totalSupply(); + uint256 cap = _maxSupply(); + if (supply > cap) { + revert ERC20ExceededSafeSupply(supply, cap); + } } _transferVotingUnits(from, to, amount); } + /** + * @dev Returns the balance of `account`. + */ + function _getVotingUnits(address account) internal view virtual override returns (uint256) { + return balanceOf(account); + } + /** * @dev Get number of checkpoints for `account`. */ @@ -53,14 +69,7 @@ abstract contract ERC20Votes is ERC20, Votes { /** * @dev Get the `pos`-th checkpoint for `account`. */ - function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoints.Checkpoint memory) { + function checkpoints(address account, uint32 pos) public view virtual returns (Checkpoints.Checkpoint224 memory) { return _checkpoints(account, pos); } - - /** - * @dev Returns the balance of `account`. - */ - function _getVotingUnits(address account) internal view virtual override returns (uint256) { - return balanceOf(account); - } } diff --git a/contracts/token/ERC20/extensions/ERC20VotesComp.sol b/contracts/token/ERC20/extensions/ERC20VotesComp.sol deleted file mode 100644 index 0461310a4..000000000 --- a/contracts/token/ERC20/extensions/ERC20VotesComp.sol +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/extensions/ERC20VotesComp.sol) - -pragma solidity ^0.8.0; - -import "./ERC20Votes.sol"; - -/** - * @dev Extension of ERC20 to support Compound's voting and delegation. This version exactly matches Compound's - * interface, with the drawback of only supporting supply up to (2^96^ - 1). - * - * NOTE: You should use this contract if you need exact compatibility with COMP (for example in order to use your token - * with Governor Alpha or Bravo) and if you are sure the supply cap of 2^96^ is enough for you. Otherwise, use the - * {ERC20Votes} variant of this module. - * - * This extension keeps a history (checkpoints) of each account's vote power. Vote power can be delegated either - * by calling the {delegate} function directly, or by providing a signature to be used with {delegateBySig}. Voting - * power can be queried through the public accessors {getCurrentVotes} and {getPriorVotes}. - * - * By default, token balance does not account for voting power. This makes transfers cheaper. The downside is that it - * requires users to delegate to themselves in order to activate checkpoints and have their voting power tracked. - * - * _Available since v4.2._ - */ -abstract contract ERC20VotesComp is ERC20Votes { - /** - * @dev Comp version of the {getVotes} accessor, with `uint96` return type. - */ - function getCurrentVotes(address account) external view virtual returns (uint96) { - return SafeCast.toUint96(getVotes(account)); - } - - /** - * @dev Comp version of the {getPastVotes} accessor, with `uint96` return type. - */ - function getPriorVotes(address account, uint256 blockNumber) external view virtual returns (uint96) { - return SafeCast.toUint96(getPastVotes(account, blockNumber)); - } - - /** - * @dev Maximum token supply. Reduced to `type(uint96).max` (2^96^ - 1) to fit COMP interface. - */ - function _maxSupply() internal view virtual override returns (uint224) { - return type(uint96).max; - } -} diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol index 8b153ffca..389965e9c 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/extensions/ERC20Wrapper.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC20Wrapper.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; import "../utils/SafeERC20.sol"; @@ -16,28 +16,47 @@ import "../utils/SafeERC20.sol"; * _Available since v4.2._ */ abstract contract ERC20Wrapper is ERC20 { - IERC20 public immutable underlying; + IERC20 private immutable _underlying; + + /** + * @dev The underlying token couldn't be wrapped. + */ + error ERC20InvalidUnderlying(address token); constructor(IERC20 underlyingToken) { - underlying = underlyingToken; + if (underlyingToken == this) { + revert ERC20InvalidUnderlying(address(this)); + } + _underlying = underlyingToken; } /** * @dev See {ERC20-decimals}. */ function decimals() public view virtual override returns (uint8) { - try IERC20Metadata(address(underlying)).decimals() returns (uint8 value) { + try IERC20Metadata(address(_underlying)).decimals() returns (uint8 value) { return value; } catch { return super.decimals(); } } + /** + * @dev Returns the address of the underlying ERC-20 token that is being wrapped. + */ + function underlying() public view returns (IERC20) { + return _underlying; + } + /** * @dev Allow a user to deposit underlying tokens and mint the corresponding number of wrapped tokens. */ function depositFor(address account, uint256 amount) public virtual returns (bool) { - SafeERC20.safeTransferFrom(underlying, _msgSender(), address(this), amount); + address sender = _msgSender(); + if (sender == address(this)) { + revert ERC20InvalidSender(address(this)); + } + SafeERC20.safeTransferFrom(_underlying, sender, address(this), amount); _mint(account, amount); return true; } @@ -47,7 +66,7 @@ abstract contract ERC20Wrapper is ERC20 { */ function withdrawTo(address account, uint256 amount) public virtual returns (bool) { _burn(_msgSender(), amount); - SafeERC20.safeTransfer(underlying, account, amount); + SafeERC20.safeTransfer(_underlying, account, amount); return true; } @@ -56,7 +75,7 @@ abstract contract ERC20Wrapper is ERC20 { * function that can be exposed with access control if desired. */ function _recover(address account) internal virtual returns (uint256) { - uint256 value = underlying.balanceOf(address(this)) - totalSupply(); + uint256 value = _underlying.balanceOf(address(this)) - totalSupply(); _mint(account, value); return value; } diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 663377dad..9ea6789f7 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/ERC4626.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/ERC4626.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC20.sol"; import "../utils/SafeERC20.sol"; @@ -17,28 +17,68 @@ import "../../../utils/math/Math.sol"; * 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. * - * CAUTION: When the vault is empty or nearly empty, deposits are at high risk of being stolen through frontrunning with - * a "donation" to the vault that inflates the price of a share. This is variously known as a donation or inflation + * [CAUTION] + * ==== + * In empty (or nearly empty) ERC-4626 vaults, deposits are at high risk of being stolen through frontrunning + * with a "donation" to the vault that inflates the price of a share. This is variously known as a donation or inflation * attack and is essentially a problem of slippage. Vault deployers can protect against this attack by making an initial * deposit of a non-trivial amount of the asset, such that price manipulation becomes infeasible. Withdrawals may - * similarly be affected by slippage. Users can protect against this attack as well unexpected slippage in general by + * similarly be affected by slippage. Users can protect against this attack as well as unexpected slippage in general by * verifying the amount received is as expected, using a wrapper that performs these checks such as * https://github.com/fei-protocol/ERC4626#erc4626router-and-base[ERC4626Router]. * + * Since v4.9, this implementation uses virtual assets and shares to mitigate that risk. The `_decimalsOffset()` + * corresponds to an offset in the decimal representation between the underlying asset's decimals and the vault + * decimals. This offset also determines the rate of virtual shares to virtual assets in the vault, which itself + * determines the initial exchange rate. While not fully preventing the attack, analysis shows that the default offset + * (0) makes it non-profitable, as a result of the value being captured by the virtual shares (out of the attacker's + * donation) matching the attacker's expected gains. With a larger offset, the attack becomes orders of magnitude more + * expensive than it is profitable. More details about the underlying math can be found + * xref:erc4626.adoc#inflation-attack[here]. + * + * The drawback of this approach is that the virtual shares do capture (a very small) part of the value being accrued + * to the vault. Also, if the vault experiences losses, the users try to exit the vault, the virtual shares and assets + * will cause the first user to exit to experience reduced losses in detriment to the last users that will experience + * bigger losses. Developers willing to revert back to the pre-v4.9 behavior just need to override the + * `_convertToShares` and `_convertToAssets` functions. + * + * To learn more, check out our xref:ROOT:erc4626.adoc[ERC-4626 guide]. + * ==== + * * _Available since v4.7._ */ abstract contract ERC4626 is ERC20, IERC4626 { using Math for uint256; IERC20 private immutable _asset; - uint8 private immutable _decimals; + uint8 private immutable _underlyingDecimals; + + /** + * @dev Attempted to deposit more assets than the max amount for `receiver`. + */ + error ERC4626ExceededMaxDeposit(address receiver, uint256 assets, uint256 max); + + /** + * @dev Attempted to mint more shares than the max amount for `receiver`. + */ + error ERC4626ExceededMaxMint(address receiver, uint256 shares, uint256 max); + + /** + * @dev Attempted to withdraw more assets than the max amount for `receiver`. + */ + error ERC4626ExceededMaxWithdraw(address owner, uint256 assets, uint256 max); + + /** + * @dev Attempted to redeem more shares than the max amount for `receiver`. + */ + error ERC4626ExceededMaxRedeem(address owner, uint256 shares, uint256 max); /** * @dev Set the underlying asset contract. This must be an ERC20-compatible contract (ERC20 or ERC777). */ constructor(IERC20 asset_) { (bool success, uint8 assetDecimals) = _tryGetAssetDecimals(asset_); - _decimals = success ? assetDecimals : super.decimals(); + _underlyingDecimals = success ? assetDecimals : 18; _asset = asset_; } @@ -47,7 +87,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { */ function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool, uint8) { (bool success, bytes memory encodedDecimals) = address(asset_).staticcall( - abi.encodeWithSelector(IERC20Metadata.decimals.selector) + abi.encodeCall(IERC20Metadata.decimals, ()) ); if (success && encodedDecimals.length >= 32) { uint256 returnedDecimals = abi.decode(encodedDecimals, (uint256)); @@ -59,78 +99,82 @@ abstract contract ERC4626 is ERC20, IERC4626 { } /** - * @dev Decimals are read from the underlying asset in the constructor and cached. If this fails (e.g., the asset - * has not been created yet), the cached value is set to a default obtained by `super.decimals()` (which depends on - * inheritance but is most likely 18). Override this function in order to set a guaranteed hardcoded value. + * @dev Decimals are computed by adding the decimal offset on top of the underlying asset's decimals. This + * "original" value is cached during construction of the vault contract. If this read operation fails (e.g., the + * asset has not been created yet), a default of 18 is used to represent the underlying asset's decimals. + * * See {IERC20Metadata-decimals}. */ function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) { - return _decimals; + return _underlyingDecimals + _decimalsOffset(); } /** @dev See {IERC4626-asset}. */ - function asset() public view virtual override returns (address) { + function asset() public view virtual returns (address) { return address(_asset); } /** @dev See {IERC4626-totalAssets}. */ - function totalAssets() public view virtual override returns (uint256) { + function totalAssets() public view virtual returns (uint256) { return _asset.balanceOf(address(this)); } /** @dev See {IERC4626-convertToShares}. */ - function convertToShares(uint256 assets) public view virtual override returns (uint256) { + function convertToShares(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Down); } /** @dev See {IERC4626-convertToAssets}. */ - function convertToAssets(uint256 shares) public view virtual override returns (uint256) { + function convertToAssets(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Down); } /** @dev See {IERC4626-maxDeposit}. */ - function maxDeposit(address) public view virtual override returns (uint256) { - return _isVaultHealthy() ? type(uint256).max : 0; + function maxDeposit(address) public view virtual returns (uint256) { + return type(uint256).max; } /** @dev See {IERC4626-maxMint}. */ - function maxMint(address) public view virtual override returns (uint256) { + function maxMint(address) public view virtual returns (uint256) { return type(uint256).max; } /** @dev See {IERC4626-maxWithdraw}. */ - function maxWithdraw(address owner) public view virtual override returns (uint256) { + function maxWithdraw(address owner) public view virtual returns (uint256) { return _convertToAssets(balanceOf(owner), Math.Rounding.Down); } /** @dev See {IERC4626-maxRedeem}. */ - function maxRedeem(address owner) public view virtual override returns (uint256) { + function maxRedeem(address owner) public view virtual returns (uint256) { return balanceOf(owner); } /** @dev See {IERC4626-previewDeposit}. */ - function previewDeposit(uint256 assets) public view virtual override returns (uint256) { + function previewDeposit(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Down); } /** @dev See {IERC4626-previewMint}. */ - function previewMint(uint256 shares) public view virtual override returns (uint256) { + function previewMint(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Up); } /** @dev See {IERC4626-previewWithdraw}. */ - function previewWithdraw(uint256 assets) public view virtual override returns (uint256) { + function previewWithdraw(uint256 assets) public view virtual returns (uint256) { return _convertToShares(assets, Math.Rounding.Up); } /** @dev See {IERC4626-previewRedeem}. */ - function previewRedeem(uint256 shares) public view virtual override returns (uint256) { + function previewRedeem(uint256 shares) public view virtual returns (uint256) { return _convertToAssets(shares, Math.Rounding.Down); } /** @dev See {IERC4626-deposit}. */ - function deposit(uint256 assets, address receiver) public virtual override returns (uint256) { - require(assets <= maxDeposit(receiver), "ERC4626: deposit more than max"); + function deposit(uint256 assets, address receiver) public virtual returns (uint256) { + uint256 maxAssets = maxDeposit(receiver); + if (assets > maxAssets) { + revert ERC4626ExceededMaxDeposit(receiver, assets, maxAssets); + } uint256 shares = previewDeposit(assets); _deposit(_msgSender(), receiver, assets, shares); @@ -143,8 +187,11 @@ abstract contract ERC4626 is ERC20, IERC4626 { * As opposed to {deposit}, minting is allowed even if the vault is in a state where the price of a share is zero. * In this case, the shares will be minted without requiring any assets to be deposited. */ - function mint(uint256 shares, address receiver) public virtual override returns (uint256) { - require(shares <= maxMint(receiver), "ERC4626: mint more than max"); + function mint(uint256 shares, address receiver) public virtual returns (uint256) { + uint256 maxShares = maxMint(receiver); + if (shares > maxShares) { + revert ERC4626ExceededMaxMint(receiver, shares, maxShares); + } uint256 assets = previewMint(shares); _deposit(_msgSender(), receiver, assets, shares); @@ -153,8 +200,11 @@ abstract contract ERC4626 is ERC20, IERC4626 { } /** @dev See {IERC4626-withdraw}. */ - function withdraw(uint256 assets, address receiver, address owner) public virtual override returns (uint256) { - require(assets <= maxWithdraw(owner), "ERC4626: withdraw more than max"); + function withdraw(uint256 assets, address receiver, address owner) public virtual returns (uint256) { + uint256 maxAssets = maxWithdraw(owner); + if (assets > maxAssets) { + revert ERC4626ExceededMaxWithdraw(owner, assets, maxAssets); + } uint256 shares = previewWithdraw(assets); _withdraw(_msgSender(), receiver, owner, assets, shares); @@ -163,8 +213,11 @@ abstract contract ERC4626 is ERC20, IERC4626 { } /** @dev See {IERC4626-redeem}. */ - function redeem(uint256 shares, address receiver, address owner) public virtual override returns (uint256) { - require(shares <= maxRedeem(owner), "ERC4626: redeem more than max"); + function redeem(uint256 shares, address receiver, address owner) public virtual returns (uint256) { + uint256 maxShares = maxRedeem(owner); + if (shares > maxShares) { + revert ERC4626ExceededMaxRedeem(owner, shares, maxShares); + } uint256 assets = previewRedeem(shares); _withdraw(_msgSender(), receiver, owner, assets, shares); @@ -174,56 +227,23 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** * @dev Internal conversion 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 amount of shares. */ function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) { - uint256 supply = totalSupply(); - return - (assets == 0 || supply == 0) - ? _initialConvertToShares(assets, rounding) - : assets.mulDiv(supply, totalAssets(), rounding); - } - - /** - * @dev Internal conversion function (from assets to shares) to apply when the vault is empty. - * - * NOTE: Make sure to keep this function consistent with {_initialConvertToAssets} when overriding it. - */ - function _initialConvertToShares( - uint256 assets, - Math.Rounding /*rounding*/ - ) internal view virtual returns (uint256 shares) { - return assets; + return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding); } /** * @dev Internal conversion function (from shares to assets) with support for rounding direction. */ function _convertToAssets(uint256 shares, Math.Rounding rounding) internal view virtual returns (uint256) { - uint256 supply = totalSupply(); - return - (supply == 0) ? _initialConvertToAssets(shares, rounding) : shares.mulDiv(totalAssets(), supply, rounding); - } - - /** - * @dev Internal conversion function (from shares to assets) to apply when the vault is empty. - * - * NOTE: Make sure to keep this function consistent with {_initialConvertToShares} when overriding it. - */ - function _initialConvertToAssets( - uint256 shares, - Math.Rounding /*rounding*/ - ) internal view virtual returns (uint256) { - return shares; + return shares.mulDiv(totalAssets() + 1, totalSupply() + 10 ** _decimalsOffset(), rounding); } /** * @dev Deposit/mint common workflow. */ function _deposit(address caller, address receiver, uint256 assets, uint256 shares) internal virtual { - // If _asset is ERC777, `transferFrom` can trigger a reenterancy BEFORE the transfer happens through the + // If _asset is ERC777, `transferFrom` can trigger a reentrancy 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. // @@ -262,10 +282,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { emit Withdraw(caller, receiver, owner, assets, shares); } - /** - * @dev Checks if vault is "healthy" in the sense of having assets backing the circulating shares. - */ - function _isVaultHealthy() private view returns (bool) { - return totalAssets() > 0 || totalSupply() == 0; + function _decimalsOffset() internal view virtual returns (uint8) { + return 0; } } diff --git a/contracts/token/ERC20/extensions/IERC20Metadata.sol b/contracts/token/ERC20/extensions/IERC20Metadata.sol index 83ba6ac5e..1cf7e0b2e 100644 --- a/contracts/token/ERC20/extensions/IERC20Metadata.sol +++ b/contracts/token/ERC20/extensions/IERC20Metadata.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Metadata.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC20.sol"; diff --git a/contracts/token/ERC20/extensions/IERC20Permit.sol b/contracts/token/ERC20/extensions/IERC20Permit.sol index bb43e53b6..eb3e3f005 100644 --- a/contracts/token/ERC20/extensions/IERC20Permit.sol +++ b/contracts/token/ERC20/extensions/IERC20Permit.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC20/extensions/IERC20Permit.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/extensions/IERC20Permit.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC20 Permit extension allowing approvals to be made via signatures, as defined in diff --git a/contracts/token/ERC20/extensions/draft-ERC20Permit.sol b/contracts/token/ERC20/extensions/draft-ERC20Permit.sol deleted file mode 100644 index 6579ef33f..000000000 --- a/contracts/token/ERC20/extensions/draft-ERC20Permit.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/extensions/draft-ERC20Permit.sol) - -pragma solidity ^0.8.0; - -// EIP-2612 is Final as of 2022-11-01. This file is deprecated. - -import "./ERC20Permit.sol"; diff --git a/contracts/token/ERC20/extensions/draft-IERC20Permit.sol b/contracts/token/ERC20/extensions/draft-IERC20Permit.sol deleted file mode 100644 index 1df6c537d..000000000 --- a/contracts/token/ERC20/extensions/draft-IERC20Permit.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.0; - -// EIP-2612 is Final as of 2022-11-01. This file is deprecated. - -import "./IERC20Permit.sol"; diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index 028711ddf..599307e7f 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC20/utils/SafeERC20.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC20/utils/SafeERC20.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC20.sol"; import "../extensions/IERC20Permit.sol"; @@ -19,46 +19,73 @@ import "../../../utils/Address.sol"; library SafeERC20 { using Address for address; - function safeTransfer(IERC20 token, address to, uint256 value) internal { - _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value)); - } + /** + * @dev An operation with an ERC20 token failed. + */ + error SafeERC20FailedOperation(address token); - function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { - _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value)); + /** + * @dev Indicates a failed `decreaseAllowance` request. + */ + error SafeERC20FailedDecreaseAllowance(address spender, uint256 currentAllowance, uint256 requestedDecrease); + + /** + * @dev Transfer `value` amount of `token` from the calling contract to `to`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeTransfer(IERC20 token, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value))); } /** - * @dev Deprecated. This function has issues similar to the ones found in - * {IERC20-approve}, and its usage is discouraged. - * - * Whenever possible, use {safeIncreaseAllowance} and - * {safeDecreaseAllowance} instead. + * @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the + * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. */ - function safeApprove(IERC20 token, address spender, uint256 value) internal { - // safeApprove should only be called when setting an initial allowance, - // or when resetting it to zero. To increase and decrease it, use - // 'safeIncreaseAllowance' and 'safeDecreaseAllowance' - require( - (value == 0) || (token.allowance(address(this), spender) == 0), - "SafeERC20: approve from non-zero to non-zero allowance" - ); - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value)); + function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); } + /** + * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { - uint256 newAllowance = token.allowance(address(this), spender) + value; - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + uint256 oldAllowance = token.allowance(address(this), spender); + forceApprove(token, spender, oldAllowance + value); } - function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal { + /** + * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no value, + * non-reverting calls are assumed to be successful. + */ + function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal { unchecked { - uint256 oldAllowance = token.allowance(address(this), spender); - require(oldAllowance >= value, "SafeERC20: decreased allowance below zero"); - uint256 newAllowance = oldAllowance - value; - _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance)); + uint256 currentAllowance = token.allowance(address(this), spender); + if (currentAllowance < requestedDecrease) { + revert SafeERC20FailedDecreaseAllowance(spender, currentAllowance, requestedDecrease); + } + forceApprove(token, spender, currentAllowance - requestedDecrease); } } + /** + * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, + * non-reverting calls are assumed to be successful. Compatible with tokens that require the approval to be set to + * 0 before setting it to a non-zero value. + */ + function forceApprove(IERC20 token, address spender, uint256 value) internal { + bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value)); + + if (!_callOptionalReturnBool(token, approvalCall)) { + _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0))); + _callOptionalReturn(token, approvalCall); + } + } + + /** + * @dev Use a ERC-2612 signature to set the `owner` approval toward `spender` on `token`. + * Revert on invalid signature. + */ function safePermit( IERC20Permit token, address owner, @@ -72,7 +99,9 @@ library SafeERC20 { uint256 nonceBefore = token.nonces(owner); token.permit(owner, spender, value, deadline, v, r, s); uint256 nonceAfter = token.nonces(owner); - require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed"); + if (nonceAfter != nonceBefore + 1) { + revert SafeERC20FailedOperation(address(token)); + } } /** @@ -86,10 +115,26 @@ library SafeERC20 { // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that // the target address contains contract code and also asserts for success in the low-level call. - bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed"); - if (returndata.length > 0) { - // Return data is optional - require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed"); + bytes memory returndata = address(token).functionCall(data); + if (returndata.length != 0 && !abi.decode(returndata, (bool))) { + revert SafeERC20FailedOperation(address(token)); } } + + /** + * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement + * on the return value: the return value is optional (but if data is returned, it must not be false). + * @param token The token targeted by the call. + * @param data The call data (encoded using abi.encode or one of its variants). + * + * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead. + */ + function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) { + // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since + // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false + // and not revert is the subcall reverts. + + (bool success, bytes memory returndata) = address(token).call(data); + return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0; + } } diff --git a/contracts/token/ERC20/utils/TokenTimelock.sol b/contracts/token/ERC20/utils/TokenTimelock.sol deleted file mode 100644 index ed855b7bc..000000000 --- a/contracts/token/ERC20/utils/TokenTimelock.sol +++ /dev/null @@ -1,72 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC20/utils/TokenTimelock.sol) - -pragma solidity ^0.8.0; - -import "./SafeERC20.sol"; - -/** - * @dev A token holder contract that will allow a beneficiary to extract the - * tokens after a given release time. - * - * Useful for simple vesting schedules like "advisors get all of their tokens - * after 1 year". - */ -contract TokenTimelock { - using SafeERC20 for IERC20; - - // ERC20 basic token contract being held - IERC20 private immutable _token; - - // beneficiary of tokens after they are released - address private immutable _beneficiary; - - // timestamp when token release is enabled - uint256 private immutable _releaseTime; - - /** - * @dev Deploys a timelock instance that is able to hold the token specified, and will only release it to - * `beneficiary_` when {release} is invoked after `releaseTime_`. The release time is specified as a Unix timestamp - * (in seconds). - */ - constructor(IERC20 token_, address beneficiary_, uint256 releaseTime_) { - require(releaseTime_ > block.timestamp, "TokenTimelock: release time is before current time"); - _token = token_; - _beneficiary = beneficiary_; - _releaseTime = releaseTime_; - } - - /** - * @dev Returns the token being held. - */ - function token() public view virtual returns (IERC20) { - return _token; - } - - /** - * @dev Returns the beneficiary that will receive the tokens. - */ - function beneficiary() public view virtual returns (address) { - return _beneficiary; - } - - /** - * @dev Returns the time when the tokens are released in seconds since Unix epoch (i.e. Unix timestamp). - */ - function releaseTime() public view virtual returns (uint256) { - return _releaseTime; - } - - /** - * @dev Transfers tokens held by the timelock to the beneficiary. Will only succeed if invoked after the release - * time. - */ - function release() public virtual { - require(block.timestamp >= releaseTime(), "TokenTimelock: current time is before release time"); - - uint256 amount = token().balanceOf(address(this)); - require(amount > 0, "TokenTimelock: no tokens to release"); - - token().safeTransfer(beneficiary(), amount); - } -} diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index 0d159f609..21ed95813 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/ERC721.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/ERC721.sol) -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; import "./IERC721.sol"; import "./IERC721Receiver.sol"; @@ -9,13 +9,14 @@ import "./extensions/IERC721Metadata.sol"; import "../../utils/Context.sol"; import "../../utils/Strings.sol"; import "../../utils/introspection/ERC165.sol"; +import "../../interfaces/draft-IERC6093.sol"; /** * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including * the Metadata extension, but not including the Enumerable extension, which is available separately as * {ERC721Enumerable}. */ -contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { +abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Errors { using Strings for uint256; // Token name @@ -57,42 +58,46 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721-balanceOf}. */ - function balanceOf(address owner) public view virtual override returns (uint256) { - require(owner != address(0), "ERC721: address zero is not a valid owner"); + function balanceOf(address owner) public view virtual returns (uint256) { + if (owner == address(0)) { + revert ERC721InvalidOwner(address(0)); + } return _balances[owner]; } /** * @dev See {IERC721-ownerOf}. */ - function ownerOf(uint256 tokenId) public view virtual override returns (address) { + function ownerOf(uint256 tokenId) public view virtual returns (address) { address owner = _ownerOf(tokenId); - require(owner != address(0), "ERC721: invalid token ID"); + if (owner == address(0)) { + revert ERC721NonexistentToken(tokenId); + } return owner; } /** * @dev See {IERC721Metadata-name}. */ - function name() public view virtual override returns (string memory) { + function name() public view virtual returns (string memory) { return _name; } /** * @dev See {IERC721Metadata-symbol}. */ - function symbol() public view virtual override returns (string memory) { + function symbol() public view virtual returns (string memory) { return _symbol; } /** * @dev See {IERC721Metadata-tokenURI}. */ - function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + function tokenURI(uint256 tokenId) public view virtual returns (string memory) { _requireMinted(tokenId); string memory baseURI = _baseURI(); - return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + return bytes(baseURI).length > 0 ? string.concat(baseURI, tokenId.toString()) : ""; } /** @@ -107,14 +112,15 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721-approve}. */ - function approve(address to, uint256 tokenId) public virtual override { - address owner = ERC721.ownerOf(tokenId); - require(to != owner, "ERC721: approval to current owner"); + function approve(address to, uint256 tokenId) public virtual { + address owner = ownerOf(tokenId); + if (to == owner) { + revert ERC721InvalidOperator(owner); + } - require( - _msgSender() == owner || isApprovedForAll(owner, _msgSender()), - "ERC721: approve caller is not token owner or approved for all" - ); + if (_msgSender() != owner && !isApprovedForAll(owner, _msgSender())) { + revert ERC721InvalidApprover(_msgSender()); + } _approve(to, tokenId); } @@ -122,7 +128,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721-getApproved}. */ - function getApproved(uint256 tokenId) public view virtual override returns (address) { + function getApproved(uint256 tokenId) public view virtual returns (address) { _requireMinted(tokenId); return _tokenApprovals[tokenId]; @@ -131,23 +137,24 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721-setApprovalForAll}. */ - function setApprovalForAll(address operator, bool approved) public virtual override { + function setApprovalForAll(address operator, bool approved) public virtual { _setApprovalForAll(_msgSender(), operator, approved); } /** * @dev See {IERC721-isApprovedForAll}. */ - function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) { + function isApprovedForAll(address owner, address operator) public view virtual returns (bool) { return _operatorApprovals[owner][operator]; } /** * @dev See {IERC721-transferFrom}. */ - function transferFrom(address from, address to, uint256 tokenId) public virtual override { - //solhint-disable-next-line max-line-length - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + function transferFrom(address from, address to, uint256 tokenId) public virtual { + if (!_isApprovedOrOwner(_msgSender(), tokenId)) { + revert ERC721InsufficientApproval(_msgSender(), tokenId); + } _transfer(from, to, tokenId); } @@ -155,15 +162,17 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { /** * @dev See {IERC721-safeTransferFrom}. */ - function safeTransferFrom(address from, address to, uint256 tokenId) public virtual override { + function safeTransferFrom(address from, address to, uint256 tokenId) public virtual { safeTransferFrom(from, to, tokenId, ""); } /** * @dev See {IERC721-safeTransferFrom}. */ - function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual override { - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data) public virtual { + if (!_isApprovedOrOwner(_msgSender(), tokenId)) { + revert ERC721InsufficientApproval(_msgSender(), tokenId); + } _safeTransfer(from, to, tokenId, data); } @@ -187,7 +196,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { */ function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data) internal virtual { _transfer(from, to, tokenId); - require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer"); + if (!_checkOnERC721Received(from, to, tokenId, data)) { + revert ERC721InvalidReceiver(to); + } } /** @@ -217,7 +228,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * - `tokenId` must exist. */ function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) { - address owner = ERC721.ownerOf(tokenId); + address owner = ownerOf(tokenId); return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender); } @@ -241,10 +252,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { */ function _safeMint(address to, uint256 tokenId, bytes memory data) internal virtual { _mint(to, tokenId); - require( - _checkOnERC721Received(address(0), to, tokenId, data), - "ERC721: transfer to non ERC721Receiver implementer" - ); + if (!_checkOnERC721Received(address(0), to, tokenId, data)) { + revert ERC721InvalidReceiver(to); + } } /** @@ -260,13 +270,19 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * Emits a {Transfer} event. */ function _mint(address to, uint256 tokenId) internal virtual { - require(to != address(0), "ERC721: mint to the zero address"); - require(!_exists(tokenId), "ERC721: token already minted"); + if (to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + if (_exists(tokenId)) { + revert ERC721InvalidSender(address(0)); + } _beforeTokenTransfer(address(0), to, tokenId, 1); // Check that tokenId was not minted by `_beforeTokenTransfer` hook - require(!_exists(tokenId), "ERC721: token already minted"); + if (_exists(tokenId)) { + revert ERC721InvalidSender(address(0)); + } unchecked { // Will not overflow unless all 2**256 token ids are minted to the same owner. @@ -295,21 +311,20 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * Emits a {Transfer} event. */ function _burn(uint256 tokenId) internal virtual { - address owner = ERC721.ownerOf(tokenId); + address owner = ownerOf(tokenId); _beforeTokenTransfer(owner, address(0), tokenId, 1); // Update ownership in case tokenId was transferred by `_beforeTokenTransfer` hook - owner = ERC721.ownerOf(tokenId); + owner = ownerOf(tokenId); // Clear approvals delete _tokenApprovals[tokenId]; - unchecked { - // Cannot overflow, as that would require more tokens to be burned/transferred - // out than the owner initially received through minting and transferring in. - _balances[owner] -= 1; - } + // Decrease balance with checked arithmetic, because an `ownerOf` override may + // invalidate the assumption that `_balances[from] >= 1`. + _balances[owner] -= 1; + delete _owners[tokenId]; emit Transfer(owner, address(0), tokenId); @@ -329,26 +344,35 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * Emits a {Transfer} event. */ function _transfer(address from, address to, uint256 tokenId) internal virtual { - require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); - require(to != address(0), "ERC721: transfer to the zero address"); + address owner = ownerOf(tokenId); + if (owner != from) { + revert ERC721IncorrectOwner(from, tokenId, owner); + } + if (to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } _beforeTokenTransfer(from, to, tokenId, 1); // Check that tokenId was not transferred by `_beforeTokenTransfer` hook - require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner"); + owner = ownerOf(tokenId); + if (owner != from) { + revert ERC721IncorrectOwner(from, tokenId, owner); + } // Clear approvals from the previous owner delete _tokenApprovals[tokenId]; + // Decrease balance with checked arithmetic, because an `ownerOf` override may + // invalidate the assumption that `_balances[from] >= 1`. + _balances[from] -= 1; + unchecked { - // `_balances[from]` cannot overflow for the same reason as described in `_burn`: - // `from`'s balance is the number of token held, which is at least one before the current - // transfer. // `_balances[to]` could overflow in the conditions described in `_mint`. That would require // all 2**256 token ids to be minted, which in practice is impossible. - _balances[from] -= 1; _balances[to] += 1; } + _owners[tokenId] = to; emit Transfer(from, to, tokenId); @@ -363,7 +387,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { */ function _approve(address to, uint256 tokenId) internal virtual { _tokenApprovals[tokenId] = to; - emit Approval(ERC721.ownerOf(tokenId), to, tokenId); + emit Approval(ownerOf(tokenId), to, tokenId); } /** @@ -372,7 +396,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * Emits an {ApprovalForAll} event. */ function _setApprovalForAll(address owner, address operator, bool approved) internal virtual { - require(owner != operator, "ERC721: approve to caller"); + if (owner == operator) { + revert ERC721InvalidOperator(owner); + } _operatorApprovals[owner][operator] = approved; emit ApprovalForAll(owner, operator, approved); } @@ -381,7 +407,9 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * @dev Reverts if the `tokenId` has not been minted yet. */ function _requireMinted(uint256 tokenId) internal view virtual { - require(_exists(tokenId), "ERC721: invalid token ID"); + if (!_exists(tokenId)) { + revert ERC721NonexistentToken(tokenId); + } } /** @@ -405,7 +433,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { return retval == IERC721Receiver.onERC721Received.selector; } catch (bytes memory reason) { if (reason.length == 0) { - revert("ERC721: transfer to non ERC721Receiver implementer"); + revert ERC721InvalidReceiver(to); } else { /// @solidity memory-safe-assembly assembly { @@ -432,21 +460,7 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ - function _beforeTokenTransfer( - address from, - address to, - uint256 /* firstTokenId */, - uint256 batchSize - ) internal virtual { - if (batchSize > 1) { - if (from != address(0)) { - _balances[from] -= batchSize; - } - if (to != address(0)) { - _balances[to] += batchSize; - } - } - } + function _beforeTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual {} /** * @dev Hook that is called after any token transfer. This includes minting and burning. If {ERC721Consecutive} is @@ -463,4 +477,16 @@ contract ERC721 is Context, ERC165, IERC721, IERC721Metadata { * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. */ function _afterTokenTransfer(address from, address to, uint256 firstTokenId, uint256 batchSize) internal virtual {} + + /** + * @dev Unsafe write access to the balances, used by extensions that "mint" tokens using an {ownerOf} override. + * + * WARNING: Anyone calling this MUST ensure that the balances remain consistent with the ownership. The invariant + * being that for any address `a` the value returned by `balanceOf(a)` must be equal to the number of tokens such + * that `ownerOf(tokenId)` is `a`. + */ + // solhint-disable-next-line func-name-mixedcase + function __unsafe_increaseBalance(address account, uint256 amount) internal { + _balances[account] += amount; + } } diff --git a/contracts/token/ERC721/IERC721.sol b/contracts/token/ERC721/IERC721.sol index 7b60a9f51..ba4c108ef 100644 --- a/contracts/token/ERC721/IERC721.sol +++ b/contracts/token/ERC721/IERC721.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/IERC721.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/IERC721.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../utils/introspection/IERC165.sol"; diff --git a/contracts/token/ERC721/IERC721Receiver.sol b/contracts/token/ERC721/IERC721Receiver.sol index de672099e..3839b6f6c 100644 --- a/contracts/token/ERC721/IERC721Receiver.sol +++ b/contracts/token/ERC721/IERC721Receiver.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC721/IERC721Receiver.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @title ERC721 token receiver interface diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index 253bbd950..40ae919d9 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -28,6 +28,7 @@ Additionally there are a few of other extensions: * {ERC721Royalty}: A way to signal royalty information following ERC2981. * {ERC721Pausable}: A primitive to pause contract operation. * {ERC721Burnable}: A way for token holders to burn their own tokens. +* {ERC721Wrapper}: Wrapper to create an ERC721 backed by another ERC721, with deposit and withdraw methods. Useful in conjunction with {ERC721Votes}. NOTE: This core set of contracts is designed to be unopinionated, allowing developers to access the internal functions in ERC721 (such as <>) and expose them as external functions in the way they prefer. @@ -59,6 +60,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel {{ERC721Royalty}} +{{ERC721Wrapper}} + == Utilities {{ERC721Holder}} diff --git a/contracts/token/ERC721/extensions/ERC721Burnable.sol b/contracts/token/ERC721/extensions/ERC721Burnable.sol index 0dc7dae2c..217f039ca 100644 --- a/contracts/token/ERC721/extensions/ERC721Burnable.sol +++ b/contracts/token/ERC721/extensions/ERC721Burnable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Burnable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "../../../utils/Context.sol"; @@ -19,8 +19,9 @@ abstract contract ERC721Burnable is Context, ERC721 { * - The caller must own `tokenId` or be an approved operator. */ function burn(uint256 tokenId) public virtual { - //solhint-disable-next-line max-line-length - require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved"); + if (!_isApprovedOrOwner(_msgSender(), tokenId)) { + revert ERC721InsufficientApproval(_msgSender(), tokenId); + } _burn(tokenId); } } diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index fe67032ef..f1308cdab 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Consecutive.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721Consecutive.sol) -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "../../../interfaces/IERC2309.sol"; -import "../../../utils/Checkpoints.sol"; import "../../../utils/structs/BitMaps.sol"; +import "../../../utils/structs/Checkpoints.sol"; /** * @dev Implementation of the ERC2309 "Consecutive Transfer Extension" as defined in @@ -20,8 +20,8 @@ import "../../../utils/structs/BitMaps.sol"; * regained after construction. During construction, only batch minting is allowed. * * IMPORTANT: This extension bypasses the hooks {_beforeTokenTransfer} and {_afterTokenTransfer} for tokens minted in - * batch. When using this extension, you should consider the {_beforeConsecutiveTokenTransfer} and - * {_afterConsecutiveTokenTransfer} hooks in addition to {_beforeTokenTransfer} and {_afterTokenTransfer}. + * batch. The hooks will be only called once per batch, so you should take `batchSize` parameter into consideration + * when relying on hooks. * * IMPORTANT: When overriding {_afterTokenTransfer}, be careful about call ordering. {ownerOf} may return invalid * values during the {_afterTokenTransfer} execution if the super call is not called first. To be safe, execute the @@ -36,6 +36,28 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { Checkpoints.Trace160 private _sequentialOwnership; BitMaps.BitMap private _sequentialBurn; + /** + * @dev Batch mint is restricted to the constructor. + * Any batch mint not emitting the {IERC721-Transfer} event outside of the constructor + * is non-ERC721 compliant. + */ + error ERC721ForbiddenBatchMint(); + + /** + * @dev Exceeds the max amount of mints per batch. + */ + error ERC721ExceededMaxBatchMint(uint256 batchSize, uint256 maxBatch); + + /** + * @dev Individual minting is not allowed. + */ + error ERC721ForbiddenMint(); + + /** + * @dev Batch burn is not supported. + */ + error ERC721ForbiddenBatchBurn(); + /** * @dev Maximum size of a batch of consecutive tokens. This is designed to limit stress on off-chain indexing * services that have to record one entry per token, and have protections against "unreasonably large" batches of @@ -56,7 +78,7 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { address owner = super._ownerOf(tokenId); // If token is owned by the core, or beyond consecutive range, return base value - if (owner != address(0) || tokenId > type(uint96).max) { + if (owner != address(0) || tokenId > type(uint96).max || tokenId < _firstConsecutiveId()) { return owner; } @@ -82,37 +104,52 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { * Emits a {IERC2309-ConsecutiveTransfer} event. */ function _mintConsecutive(address to, uint96 batchSize) internal virtual returns (uint96) { - uint96 first = _totalConsecutiveSupply(); + uint96 next = _nextConsecutiveId(); // minting a batch of size 0 is a no-op if (batchSize > 0) { - require(address(this).code.length == 0, "ERC721Consecutive: batch minting restricted to constructor"); - require(to != address(0), "ERC721Consecutive: mint to the zero address"); - require(batchSize <= _maxBatchSize(), "ERC721Consecutive: batch too large"); + if (address(this).code.length > 0) { + revert ERC721ForbiddenBatchMint(); + } + if (to == address(0)) { + revert ERC721InvalidReceiver(address(0)); + } + + uint256 maxBatchSize = _maxBatchSize(); + if (batchSize > maxBatchSize) { + revert ERC721ExceededMaxBatchMint(batchSize, maxBatchSize); + } // hook before - _beforeTokenTransfer(address(0), to, first, batchSize); + _beforeTokenTransfer(address(0), to, next, batchSize); // push an ownership checkpoint & emit event - uint96 last = first + batchSize - 1; + uint96 last = next + batchSize - 1; _sequentialOwnership.push(last, uint160(to)); - emit ConsecutiveTransfer(first, last, address(0), to); + + // The invariant required by this function is preserved because the new sequentialOwnership checkpoint + // is attributing ownership of `batchSize` new tokens to account `to`. + __unsafe_increaseBalance(to, batchSize); + + emit ConsecutiveTransfer(next, last, address(0), to); // hook after - _afterTokenTransfer(address(0), to, first, batchSize); + _afterTokenTransfer(address(0), to, next, batchSize); } - return first; + return next; } /** * @dev See {ERC721-_mint}. Override version that restricts normal minting to after construction. * - * Warning: Using {ERC721Consecutive} prevents using {_mint} during construction in favor of {_mintConsecutive}. + * WARNING: Using {ERC721Consecutive} prevents using {_mint} during construction in favor of {_mintConsecutive}. * After construction, {_mintConsecutive} is no longer available and {_mint} becomes available. */ function _mint(address to, uint256 tokenId) internal virtual override { - require(address(this).code.length > 0, "ERC721Consecutive: can't mint during construction"); + if (address(this).code.length == 0) { + revert ERC721ForbiddenMint(); + } super._mint(to, tokenId); } @@ -127,17 +164,32 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { ) internal virtual override { if ( to == address(0) && // if we burn - firstTokenId < _totalConsecutiveSupply() && // and the tokenId was minted in a batch - !_sequentialBurn.get(firstTokenId) // and the token was never marked as burnt - ) { - require(batchSize == 1, "ERC721Consecutive: batch burn not supported"); + firstTokenId >= _firstConsecutiveId() && + firstTokenId < _nextConsecutiveId() && + !_sequentialBurn.get(firstTokenId) + ) // and the token was never marked as burnt + { + if (batchSize != 1) { + revert ERC721ForbiddenBatchBurn(); + } _sequentialBurn.set(firstTokenId); } super._afterTokenTransfer(from, to, firstTokenId, batchSize); } - function _totalConsecutiveSupply() private view returns (uint96) { + /** + * @dev Used to offset the first token id in {_nextConsecutiveId} + */ + function _firstConsecutiveId() internal view virtual returns (uint96) { + return 0; + } + + /** + * @dev Returns the next tokenId to mint using {_mintConsecutive}. It will return {_firstConsecutiveId} + * if no consecutive tokenId has been minted before. + */ + function _nextConsecutiveId() private view returns (uint96) { (bool exists, uint96 latestId, ) = _sequentialOwnership.latestCheckpoint(); - return exists ? latestId + 1 : 0; + return exists ? latestId + 1 : _firstConsecutiveId(); } } diff --git a/contracts/token/ERC721/extensions/ERC721Enumerable.sol b/contracts/token/ERC721/extensions/ERC721Enumerable.sol index aab81a9f3..18e2ba5d6 100644 --- a/contracts/token/ERC721/extensions/ERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/ERC721Enumerable.sol @@ -1,15 +1,17 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Enumerable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "./IERC721Enumerable.sol"; /** - * @dev This implements an optional extension of {ERC721} defined in the EIP that adds - * enumerability of all the token ids in the contract as well as all token ids owned by each - * account. + * @dev This implements an optional extension of {ERC721} defined in the EIP that adds enumerability + * of all the token ids in the contract as well as all token ids owned by each account. + * + * CAUTION: `ERC721` extensions that implement custom `balanceOf` logic, such as `ERC721Consecutive`, + * interfere with enumerability and should not be used together with `ERC721Enumerable`. */ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { // Mapping from owner to list of owned token IDs @@ -24,6 +26,18 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { // Mapping from token id to position in the allTokens array mapping(uint256 => uint256) private _allTokensIndex; + /** + * @dev An `owner`'s token query was out of bounds for `index`. + * + * NOTE: The owner being `address(0)` indicates a global out of bounds index. + */ + error ERC721OutOfBoundsIndex(address owner, uint256 index); + + /** + * @dev Batch mint is not allowed. + */ + error ERC721EnumerableForbiddenBatchMint(); + /** * @dev See {IERC165-supportsInterface}. */ @@ -34,23 +48,27 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { /** * @dev See {IERC721Enumerable-tokenOfOwnerByIndex}. */ - function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual override returns (uint256) { - require(index < ERC721.balanceOf(owner), "ERC721Enumerable: owner index out of bounds"); + function tokenOfOwnerByIndex(address owner, uint256 index) public view virtual returns (uint256) { + if (index >= balanceOf(owner)) { + revert ERC721OutOfBoundsIndex(owner, index); + } return _ownedTokens[owner][index]; } /** * @dev See {IERC721Enumerable-totalSupply}. */ - function totalSupply() public view virtual override returns (uint256) { + function totalSupply() public view virtual returns (uint256) { return _allTokens.length; } /** * @dev See {IERC721Enumerable-tokenByIndex}. */ - function tokenByIndex(uint256 index) public view virtual override returns (uint256) { - require(index < ERC721Enumerable.totalSupply(), "ERC721Enumerable: global index out of bounds"); + function tokenByIndex(uint256 index) public view virtual returns (uint256) { + if (index >= totalSupply()) { + revert ERC721OutOfBoundsIndex(address(0), index); + } return _allTokens[index]; } @@ -67,7 +85,7 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { if (batchSize > 1) { // Will only trigger during construction. Batch transferring (minting) is not available afterwards. - revert("ERC721Enumerable: consecutive transfers not supported"); + revert ERC721EnumerableForbiddenBatchMint(); } uint256 tokenId = firstTokenId; @@ -90,7 +108,7 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { * @param tokenId uint256 ID of the token to be added to the tokens list of the given address */ function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private { - uint256 length = ERC721.balanceOf(to); + uint256 length = balanceOf(to); _ownedTokens[to][length] = tokenId; _ownedTokensIndex[tokenId] = length; } @@ -116,7 +134,7 @@ abstract contract ERC721Enumerable is ERC721, IERC721Enumerable { // To prevent a gap in from's tokens array, we store the last token in the index of the token to delete, and // then delete the last slot (swap and pop). - uint256 lastTokenIndex = ERC721.balanceOf(from) - 1; + uint256 lastTokenIndex = balanceOf(from) - 1; uint256 tokenIndex = _ownedTokensIndex[tokenId]; // When the token to delete is the last token, the swap operation is unnecessary diff --git a/contracts/token/ERC721/extensions/ERC721Pausable.sol b/contracts/token/ERC721/extensions/ERC721Pausable.sol index 4726540f4..a9472c5dc 100644 --- a/contracts/token/ERC721/extensions/ERC721Pausable.sol +++ b/contracts/token/ERC721/extensions/ERC721Pausable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Pausable.sol) +// OpenZeppelin Contracts (last updated v4.8.2) (token/ERC721/extensions/ERC721Pausable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "../../../security/Pausable.sol"; @@ -12,6 +12,12 @@ import "../../../security/Pausable.sol"; * Useful for scenarios such as preventing trades until the end of an evaluation * period, or having an emergency switch for freezing all token transfers in the * event of a large bug. + * + * IMPORTANT: This contract does not include public pause and unpause functions. In + * addition to inheriting this contract, you must define both functions, invoking the + * {Pausable-_pause} and {Pausable-_unpause} internal functions, with appropriate + * access control, e.g. using {AccessControl} or {Ownable}. Not doing so will + * make the contract unpausable. */ abstract contract ERC721Pausable is ERC721, Pausable { /** @@ -29,6 +35,6 @@ abstract contract ERC721Pausable is ERC721, Pausable { ) internal virtual override { super._beforeTokenTransfer(from, to, firstTokenId, batchSize); - require(!paused(), "ERC721Pausable: token transfer while paused"); + _requireNotPaused(); } } diff --git a/contracts/token/ERC721/extensions/ERC721Royalty.sol b/contracts/token/ERC721/extensions/ERC721Royalty.sol index 298e34205..7d6ef6c04 100644 --- a/contracts/token/ERC721/extensions/ERC721Royalty.sol +++ b/contracts/token/ERC721/extensions/ERC721Royalty.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Royalty.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "../../common/ERC2981.sol"; diff --git a/contracts/token/ERC721/extensions/ERC721URIStorage.sol b/contracts/token/ERC721/extensions/ERC721URIStorage.sol index e83a5ede2..ae625fc28 100644 --- a/contracts/token/ERC721/extensions/ERC721URIStorage.sol +++ b/contracts/token/ERC721/extensions/ERC721URIStorage.sol @@ -1,19 +1,27 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/extensions/ERC721URIStorage.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721URIStorage.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; +import "../../../interfaces/IERC4906.sol"; /** * @dev ERC721 token with storage based token URI management. */ -abstract contract ERC721URIStorage is ERC721 { +abstract contract ERC721URIStorage is IERC4906, ERC721 { using Strings for uint256; // Optional mapping for token URIs mapping(uint256 => string) private _tokenURIs; + /** + * @dev See {IERC165-supportsInterface} + */ + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) { + return interfaceId == bytes4(0x49064906) || super.supportsInterface(interfaceId); + } + /** * @dev See {IERC721Metadata-tokenURI}. */ @@ -27,9 +35,9 @@ abstract contract ERC721URIStorage is ERC721 { if (bytes(base).length == 0) { return _tokenURI; } - // If both are set, concatenate the baseURI and tokenURI (via abi.encodePacked). + // If both are set, concatenate the baseURI and tokenURI (via string.concat). if (bytes(_tokenURI).length > 0) { - return string(abi.encodePacked(base, _tokenURI)); + return string.concat(base, _tokenURI); } return super.tokenURI(tokenId); @@ -38,13 +46,19 @@ abstract contract ERC721URIStorage is ERC721 { /** * @dev Sets `_tokenURI` as the tokenURI of `tokenId`. * + * Emits {MetadataUpdate}. + * * Requirements: * * - `tokenId` must exist. */ function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual { - require(_exists(tokenId), "ERC721URIStorage: URI set of nonexistent token"); + if (!_exists(tokenId)) { + revert ERC721NonexistentToken(tokenId); + } _tokenURIs[tokenId] = _tokenURI; + + emit MetadataUpdate(tokenId); } /** diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 8e6500ec3..89b2e073c 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/ERC721Votes.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721Votes.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../ERC721.sol"; import "../../../governance/utils/Votes.sol"; @@ -34,6 +34,8 @@ abstract contract ERC721Votes is ERC721, Votes { /** * @dev Returns the balance of `account`. + * + * WARNING: Overriding this function will likely result in incorrect vote tracking. */ function _getVotingUnits(address account) internal view virtual override returns (uint256) { return balanceOf(account); diff --git a/contracts/token/ERC721/extensions/ERC721Wrapper.sol b/contracts/token/ERC721/extensions/ERC721Wrapper.sol new file mode 100644 index 000000000..47a42c1f0 --- /dev/null +++ b/contracts/token/ERC721/extensions/ERC721Wrapper.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/extensions/ERC721Wrapper.sol) + +pragma solidity ^0.8.19; + +import "../ERC721.sol"; + +/** + * @dev Extension of the ERC721 token contract to support token wrapping. + * + * Users can deposit and withdraw an "underlying token" and receive a "wrapped token" with a matching tokenId. This is useful + * in conjunction with other modules. For example, combining this wrapping mechanism with {ERC721Votes} will allow the + * wrapping of an existing "basic" ERC721 into a governance token. + * + * _Available since v4.9.0_ + */ +abstract contract ERC721Wrapper is ERC721, IERC721Receiver { + IERC721 private immutable _underlying; + + /** + * @dev The received ERC721 token couldn't be wrapped. + */ + error ERC721UnsupportedToken(address token); + + constructor(IERC721 underlyingToken) { + _underlying = underlyingToken; + } + + /** + * @dev Allow a user to deposit underlying tokens and mint the corresponding tokenIds. + */ + function depositFor(address account, uint256[] memory tokenIds) public virtual returns (bool) { + uint256 length = tokenIds.length; + for (uint256 i = 0; i < length; ++i) { + uint256 tokenId = tokenIds[i]; + + // This is an "unsafe" transfer that doesn't call any hook on the receiver. With underlying() being trusted + // (by design of this contract) and no other contracts expected to be called from there, we are safe. + // slither-disable-next-line reentrancy-no-eth + underlying().transferFrom(_msgSender(), address(this), tokenId); + _safeMint(account, tokenId); + } + + return true; + } + + /** + * @dev Allow a user to burn wrapped tokens and withdraw the corresponding tokenIds of the underlying tokens. + */ + function withdrawTo(address account, uint256[] memory tokenIds) public virtual returns (bool) { + uint256 length = tokenIds.length; + for (uint256 i = 0; i < length; ++i) { + uint256 tokenId = tokenIds[i]; + if (!_isApprovedOrOwner(_msgSender(), tokenId)) { + revert ERC721InsufficientApproval(_msgSender(), tokenId); + } + _burn(tokenId); + // Checks were already performed at this point, and there's no way to retake ownership or approval from + // the wrapped tokenId after this point, so it's safe to remove the reentrancy check for the next line. + // slither-disable-next-line reentrancy-no-eth + underlying().safeTransferFrom(address(this), account, tokenId); + } + + return true; + } + + /** + * @dev Overrides {IERC721Receiver-onERC721Received} to allow minting on direct ERC721 transfers to + * this contract. + * + * In case there's data attached, it validates that the operator is this contract, so only trusted data + * is accepted from {depositFor}. + * + * WARNING: Doesn't work with unsafe transfers (eg. {IERC721-transferFrom}). Use {ERC721Wrapper-_recover} + * for recovering in that scenario. + */ + function onERC721Received(address, address from, uint256 tokenId, bytes memory) public virtual returns (bytes4) { + if (address(underlying()) != _msgSender()) { + revert ERC721UnsupportedToken(_msgSender()); + } + _safeMint(from, tokenId); + return IERC721Receiver.onERC721Received.selector; + } + + /** + * @dev Mint a wrapped token to cover any underlyingToken that would have been transferred by mistake. Internal + * function that can be exposed with access control if desired. + */ + function _recover(address account, uint256 tokenId) internal virtual returns (uint256) { + address owner = underlying().ownerOf(tokenId); + if (owner != address(this)) { + revert ERC721IncorrectOwner(address(this), tokenId, owner); + } + _safeMint(account, tokenId); + return tokenId; + } + + /** + * @dev Returns the underlying token. + */ + function underlying() public view virtual returns (IERC721) { + return _underlying; + } +} diff --git a/contracts/token/ERC721/extensions/IERC721Enumerable.sol b/contracts/token/ERC721/extensions/IERC721Enumerable.sol index dfea427ba..d5fe6330c 100644 --- a/contracts/token/ERC721/extensions/IERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/IERC721Enumerable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.5.0) (token/ERC721/extensions/IERC721Enumerable.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC721.sol"; diff --git a/contracts/token/ERC721/extensions/IERC721Metadata.sol b/contracts/token/ERC721/extensions/IERC721Metadata.sol index dca77ba5b..0b085812c 100644 --- a/contracts/token/ERC721/extensions/IERC721Metadata.sol +++ b/contracts/token/ERC721/extensions/IERC721Metadata.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC721.sol"; diff --git a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol b/contracts/token/ERC721/extensions/draft-ERC721Votes.sol deleted file mode 100644 index c6aa7c564..000000000 --- a/contracts/token/ERC721/extensions/draft-ERC721Votes.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC721/extensions/draft-ERC721Votes.sol) - -pragma solidity ^0.8.0; - -// ERC721Votes was marked as draft due to the EIP-712 dependency. -// EIP-712 is Final as of 2022-08-11. This file is deprecated. - -import "./ERC721Votes.sol"; diff --git a/contracts/token/ERC721/utils/ERC721Holder.sol b/contracts/token/ERC721/utils/ERC721Holder.sol index cfa533a47..a3ee8b5f0 100644 --- a/contracts/token/ERC721/utils/ERC721Holder.sol +++ b/contracts/token/ERC721/utils/ERC721Holder.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC721/utils/ERC721Holder.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/ERC721/utils/ERC721Holder.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../IERC721Receiver.sol"; @@ -11,13 +11,13 @@ import "../IERC721Receiver.sol"; * Accepts all token transfers. * Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}. */ -contract ERC721Holder is IERC721Receiver { +abstract contract ERC721Holder is IERC721Receiver { /** * @dev See {IERC721Receiver-onERC721Received}. * * Always returns `IERC721Receiver.onERC721Received.selector`. */ - function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + function onERC721Received(address, address, uint256, bytes memory) public virtual returns (bytes4) { return this.onERC721Received.selector; } } diff --git a/contracts/token/ERC777/ERC777.sol b/contracts/token/ERC777/ERC777.sol deleted file mode 100644 index 1f426c3ba..000000000 --- a/contracts/token/ERC777/ERC777.sol +++ /dev/null @@ -1,515 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC777/ERC777.sol) - -pragma solidity ^0.8.1; - -import "./IERC777.sol"; -import "./IERC777Recipient.sol"; -import "./IERC777Sender.sol"; -import "../ERC20/IERC20.sol"; -import "../../utils/Address.sol"; -import "../../utils/Context.sol"; -import "../../utils/introspection/IERC1820Registry.sol"; - -/** - * @dev Implementation of the {IERC777} interface. - * - * This implementation is agnostic to the way tokens are created. This means - * that a supply mechanism has to be added in a derived contract using {_mint}. - * - * Support for ERC20 is included in this contract, as specified by the EIP: both - * the ERC777 and ERC20 interfaces can be safely used when interacting with it. - * Both {IERC777-Sent} and {IERC20-Transfer} events are emitted on token - * movements. - * - * Additionally, the {IERC777-granularity} value is hard-coded to `1`, meaning that there - * are no special restrictions in the amount of tokens that created, moved, or - * destroyed. This makes integration with ERC20 applications seamless. - */ -contract ERC777 is Context, IERC777, IERC20 { - using Address for address; - - IERC1820Registry internal constant _ERC1820_REGISTRY = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24); - - mapping(address => uint256) private _balances; - - uint256 private _totalSupply; - - string private _name; - string private _symbol; - - bytes32 private constant _TOKENS_SENDER_INTERFACE_HASH = keccak256("ERC777TokensSender"); - bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH = keccak256("ERC777TokensRecipient"); - - // This isn't ever read from - it's only used to respond to the defaultOperators query. - address[] private _defaultOperatorsArray; - - // Immutable, but accounts may revoke them (tracked in __revokedDefaultOperators). - mapping(address => bool) private _defaultOperators; - - // For each account, a mapping of its operators and revoked default operators. - mapping(address => mapping(address => bool)) private _operators; - mapping(address => mapping(address => bool)) private _revokedDefaultOperators; - - // ERC20-allowances - mapping(address => mapping(address => uint256)) private _allowances; - - /** - * @dev `defaultOperators` may be an empty array. - */ - constructor(string memory name_, string memory symbol_, address[] memory defaultOperators_) { - _name = name_; - _symbol = symbol_; - - _defaultOperatorsArray = defaultOperators_; - for (uint256 i = 0; i < defaultOperators_.length; i++) { - _defaultOperators[defaultOperators_[i]] = true; - } - - // register interfaces - _ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC777Token"), address(this)); - _ERC1820_REGISTRY.setInterfaceImplementer(address(this), keccak256("ERC20Token"), address(this)); - } - - /** - * @dev See {IERC777-name}. - */ - function name() public view virtual override returns (string memory) { - return _name; - } - - /** - * @dev See {IERC777-symbol}. - */ - function symbol() public view virtual override returns (string memory) { - return _symbol; - } - - /** - * @dev See {ERC20-decimals}. - * - * Always returns 18, as per the - * [ERC777 EIP](https://eips.ethereum.org/EIPS/eip-777#backward-compatibility). - */ - function decimals() public pure virtual returns (uint8) { - return 18; - } - - /** - * @dev See {IERC777-granularity}. - * - * This implementation always returns `1`. - */ - function granularity() public view virtual override returns (uint256) { - return 1; - } - - /** - * @dev See {IERC777-totalSupply}. - */ - function totalSupply() public view virtual override(IERC20, IERC777) returns (uint256) { - return _totalSupply; - } - - /** - * @dev Returns the amount of tokens owned by an account (`tokenHolder`). - */ - function balanceOf(address tokenHolder) public view virtual override(IERC20, IERC777) returns (uint256) { - return _balances[tokenHolder]; - } - - /** - * @dev See {IERC777-send}. - * - * Also emits a {IERC20-Transfer} event for ERC20 compatibility. - */ - function send(address recipient, uint256 amount, bytes memory data) public virtual override { - _send(_msgSender(), recipient, amount, data, "", true); - } - - /** - * @dev See {IERC20-transfer}. - * - * Unlike `send`, `recipient` is _not_ required to implement the {IERC777Recipient} - * interface if it is a contract. - * - * Also emits a {Sent} event. - */ - function transfer(address recipient, uint256 amount) public virtual override returns (bool) { - _send(_msgSender(), recipient, amount, "", "", false); - return true; - } - - /** - * @dev See {IERC777-burn}. - * - * Also emits a {IERC20-Transfer} event for ERC20 compatibility. - */ - function burn(uint256 amount, bytes memory data) public virtual override { - _burn(_msgSender(), amount, data, ""); - } - - /** - * @dev See {IERC777-isOperatorFor}. - */ - function isOperatorFor(address operator, address tokenHolder) public view virtual override returns (bool) { - return - operator == tokenHolder || - (_defaultOperators[operator] && !_revokedDefaultOperators[tokenHolder][operator]) || - _operators[tokenHolder][operator]; - } - - /** - * @dev See {IERC777-authorizeOperator}. - */ - function authorizeOperator(address operator) public virtual override { - require(_msgSender() != operator, "ERC777: authorizing self as operator"); - - if (_defaultOperators[operator]) { - delete _revokedDefaultOperators[_msgSender()][operator]; - } else { - _operators[_msgSender()][operator] = true; - } - - emit AuthorizedOperator(operator, _msgSender()); - } - - /** - * @dev See {IERC777-revokeOperator}. - */ - function revokeOperator(address operator) public virtual override { - require(operator != _msgSender(), "ERC777: revoking self as operator"); - - if (_defaultOperators[operator]) { - _revokedDefaultOperators[_msgSender()][operator] = true; - } else { - delete _operators[_msgSender()][operator]; - } - - emit RevokedOperator(operator, _msgSender()); - } - - /** - * @dev See {IERC777-defaultOperators}. - */ - function defaultOperators() public view virtual override returns (address[] memory) { - return _defaultOperatorsArray; - } - - /** - * @dev See {IERC777-operatorSend}. - * - * Emits {Sent} and {IERC20-Transfer} events. - */ - function operatorSend( - address sender, - address recipient, - uint256 amount, - bytes memory data, - bytes memory operatorData - ) public virtual override { - require(isOperatorFor(_msgSender(), sender), "ERC777: caller is not an operator for holder"); - _send(sender, recipient, amount, data, operatorData, true); - } - - /** - * @dev See {IERC777-operatorBurn}. - * - * Emits {Burned} and {IERC20-Transfer} events. - */ - function operatorBurn( - address account, - uint256 amount, - bytes memory data, - bytes memory operatorData - ) public virtual override { - require(isOperatorFor(_msgSender(), account), "ERC777: caller is not an operator for holder"); - _burn(account, amount, data, operatorData); - } - - /** - * @dev See {IERC20-allowance}. - * - * Note that operator and allowance concepts are orthogonal: operators may - * not have allowance, and accounts with allowance may not be operators - * themselves. - */ - function allowance(address holder, address spender) public view virtual override returns (uint256) { - return _allowances[holder][spender]; - } - - /** - * @dev See {IERC20-approve}. - * - * NOTE: If `value` is the maximum `uint256`, the allowance is not updated on - * `transferFrom`. This is semantically equivalent to an infinite approval. - * - * Note that accounts cannot have allowance issued by their operators. - */ - function approve(address spender, uint256 value) public virtual override returns (bool) { - address holder = _msgSender(); - _approve(holder, spender, value); - return true; - } - - /** - * @dev See {IERC20-transferFrom}. - * - * NOTE: Does not update the allowance if the current allowance - * is the maximum `uint256`. - * - * Note that operator and allowance concepts are orthogonal: operators cannot - * call `transferFrom` (unless they have allowance), and accounts with - * allowance cannot call `operatorSend` (unless they are operators). - * - * Emits {Sent}, {IERC20-Transfer} and {IERC20-Approval} events. - */ - function transferFrom(address holder, address recipient, uint256 amount) public virtual override returns (bool) { - address spender = _msgSender(); - _spendAllowance(holder, spender, amount); - _send(holder, recipient, amount, "", "", false); - return true; - } - - /** - * @dev Creates `amount` tokens and assigns them to `account`, increasing - * the total supply. - * - * If a send hook is registered for `account`, the corresponding function - * will be called with the caller address as the `operator` and with - * `userData` and `operatorData`. - * - * See {IERC777Sender} and {IERC777Recipient}. - * - * Emits {Minted} and {IERC20-Transfer} events. - * - * Requirements - * - * - `account` cannot be the zero address. - * - if `account` is a contract, it must implement the {IERC777Recipient} - * interface. - */ - function _mint(address account, uint256 amount, bytes memory userData, bytes memory operatorData) internal virtual { - _mint(account, amount, userData, operatorData, true); - } - - /** - * @dev Creates `amount` tokens and assigns them to `account`, increasing - * the total supply. - * - * If `requireReceptionAck` is set to true, and if a send hook is - * registered for `account`, the corresponding function will be called with - * `operator`, `data` and `operatorData`. - * - * See {IERC777Sender} and {IERC777Recipient}. - * - * Emits {Minted} and {IERC20-Transfer} events. - * - * Requirements - * - * - `account` cannot be the zero address. - * - if `account` is a contract, it must implement the {IERC777Recipient} - * interface. - */ - function _mint( - address account, - uint256 amount, - bytes memory userData, - bytes memory operatorData, - bool requireReceptionAck - ) internal virtual { - require(account != address(0), "ERC777: mint to the zero address"); - - address operator = _msgSender(); - - _beforeTokenTransfer(operator, address(0), account, amount); - - // Update state variables - _totalSupply += amount; - _balances[account] += amount; - - _callTokensReceived(operator, address(0), account, amount, userData, operatorData, requireReceptionAck); - - emit Minted(operator, account, amount, userData, operatorData); - emit Transfer(address(0), account, amount); - } - - /** - * @dev Send tokens - * @param from address token holder address - * @param to address recipient address - * @param amount uint256 amount of tokens to transfer - * @param userData bytes extra information provided by the token holder (if any) - * @param operatorData bytes extra information provided by the operator (if any) - * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient - */ - function _send( - address from, - address to, - uint256 amount, - bytes memory userData, - bytes memory operatorData, - bool requireReceptionAck - ) internal virtual { - require(from != address(0), "ERC777: transfer from the zero address"); - require(to != address(0), "ERC777: transfer to the zero address"); - - address operator = _msgSender(); - - _callTokensToSend(operator, from, to, amount, userData, operatorData); - - _move(operator, from, to, amount, userData, operatorData); - - _callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck); - } - - /** - * @dev Burn tokens - * @param from address token holder address - * @param amount uint256 amount of tokens to burn - * @param data bytes extra information provided by the token holder - * @param operatorData bytes extra information provided by the operator (if any) - */ - function _burn(address from, uint256 amount, bytes memory data, bytes memory operatorData) internal virtual { - require(from != address(0), "ERC777: burn from the zero address"); - - address operator = _msgSender(); - - _callTokensToSend(operator, from, address(0), amount, data, operatorData); - - _beforeTokenTransfer(operator, from, address(0), amount); - - // Update state variables - uint256 fromBalance = _balances[from]; - require(fromBalance >= amount, "ERC777: burn amount exceeds balance"); - unchecked { - _balances[from] = fromBalance - amount; - } - _totalSupply -= amount; - - emit Burned(operator, from, amount, data, operatorData); - emit Transfer(from, address(0), amount); - } - - function _move( - address operator, - address from, - address to, - uint256 amount, - bytes memory userData, - bytes memory operatorData - ) private { - _beforeTokenTransfer(operator, from, to, amount); - - uint256 fromBalance = _balances[from]; - require(fromBalance >= amount, "ERC777: transfer amount exceeds balance"); - unchecked { - _balances[from] = fromBalance - amount; - } - _balances[to] += amount; - - emit Sent(operator, from, to, amount, userData, operatorData); - emit Transfer(from, to, amount); - } - - /** - * @dev See {ERC20-_approve}. - * - * Note that accounts cannot have allowance issued by their operators. - */ - function _approve(address holder, address spender, uint256 value) internal virtual { - require(holder != address(0), "ERC777: approve from the zero address"); - require(spender != address(0), "ERC777: approve to the zero address"); - - _allowances[holder][spender] = value; - emit Approval(holder, spender, value); - } - - /** - * @dev Call from.tokensToSend() if the interface is registered - * @param operator address operator requesting the transfer - * @param from address token holder address - * @param to address recipient address - * @param amount uint256 amount of tokens to transfer - * @param userData bytes extra information provided by the token holder (if any) - * @param operatorData bytes extra information provided by the operator (if any) - */ - function _callTokensToSend( - address operator, - address from, - address to, - uint256 amount, - bytes memory userData, - bytes memory operatorData - ) private { - address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(from, _TOKENS_SENDER_INTERFACE_HASH); - if (implementer != address(0)) { - IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData); - } - } - - /** - * @dev Call to.tokensReceived() if the interface is registered. Reverts if the recipient is a contract but - * tokensReceived() was not registered for the recipient - * @param operator address operator requesting the transfer - * @param from address token holder address - * @param to address recipient address - * @param amount uint256 amount of tokens to transfer - * @param userData bytes extra information provided by the token holder (if any) - * @param operatorData bytes extra information provided by the operator (if any) - * @param requireReceptionAck if true, contract recipients are required to implement ERC777TokensRecipient - */ - function _callTokensReceived( - address operator, - address from, - address to, - uint256 amount, - bytes memory userData, - bytes memory operatorData, - bool requireReceptionAck - ) private { - address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(to, _TOKENS_RECIPIENT_INTERFACE_HASH); - if (implementer != address(0)) { - IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData); - } else if (requireReceptionAck) { - require( - to.code.length == 0, - "ERC777: token recipient contract has no implementer for ERC777TokensRecipient" - ); - } - } - - /** - * @dev Updates `owner` s allowance for `spender` based on spent `amount`. - * - * Does not update the allowance amount in case of infinite allowance. - * Revert if not enough allowance is available. - * - * Might emit an {IERC20-Approval} event. - */ - function _spendAllowance(address owner, address spender, uint256 amount) internal virtual { - uint256 currentAllowance = allowance(owner, spender); - if (currentAllowance != type(uint256).max) { - require(currentAllowance >= amount, "ERC777: insufficient allowance"); - unchecked { - _approve(owner, spender, currentAllowance - amount); - } - } - } - - /** - * @dev Hook that is called before any token transfer. This includes - * calls to {send}, {transfer}, {operatorSend}, minting and burning. - * - * Calling conditions: - * - * - when `from` and `to` are both non-zero, `amount` of ``from``'s tokens - * will be to transferred to `to`. - * - when `from` is zero, `amount` tokens will be minted for `to`. - * - when `to` is zero, `amount` of ``from``'s tokens will be burned. - * - `from` and `to` are never both zero. - * - * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks]. - */ - function _beforeTokenTransfer(address operator, address from, address to, uint256 amount) internal virtual {} -} diff --git a/contracts/token/ERC777/IERC777.sol b/contracts/token/ERC777/IERC777.sol deleted file mode 100644 index d3bede626..000000000 --- a/contracts/token/ERC777/IERC777.sol +++ /dev/null @@ -1,200 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (token/ERC777/IERC777.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC777Token standard as defined in the EIP. - * - * This contract uses the - * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 registry standard] to let - * token holders and recipients react to token movements by using setting implementers - * for the associated interfaces in said registry. See {IERC1820Registry} and - * {ERC1820Implementer}. - */ -interface IERC777 { - /** - * @dev Emitted when `amount` tokens are created by `operator` and assigned to `to`. - * - * Note that some additional user `data` and `operatorData` can be logged in the event. - */ - event Minted(address indexed operator, address indexed to, uint256 amount, bytes data, bytes operatorData); - - /** - * @dev Emitted when `operator` destroys `amount` tokens from `account`. - * - * Note that some additional user `data` and `operatorData` can be logged in the event. - */ - event Burned(address indexed operator, address indexed from, uint256 amount, bytes data, bytes operatorData); - - /** - * @dev Emitted when `operator` is made operator for `tokenHolder`. - */ - event AuthorizedOperator(address indexed operator, address indexed tokenHolder); - - /** - * @dev Emitted when `operator` is revoked its operator status for `tokenHolder`. - */ - event RevokedOperator(address indexed operator, address indexed tokenHolder); - - /** - * @dev Returns the name of the token. - */ - function name() external view returns (string memory); - - /** - * @dev Returns the symbol of the token, usually a shorter version of the - * name. - */ - function symbol() external view returns (string memory); - - /** - * @dev Returns the smallest part of the token that is not divisible. This - * means all token operations (creation, movement and destruction) must have - * amounts that are a multiple of this number. - * - * For most token contracts, this value will equal 1. - */ - function granularity() external view returns (uint256); - - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the amount of tokens owned by an account (`owner`). - */ - function balanceOf(address owner) external view returns (uint256); - - /** - * @dev Moves `amount` tokens from the caller's account to `recipient`. - * - * If send or receive hooks are registered for the caller and `recipient`, - * the corresponding functions will be called with `data` and empty - * `operatorData`. See {IERC777Sender} and {IERC777Recipient}. - * - * Emits a {Sent} event. - * - * Requirements - * - * - the caller must have at least `amount` tokens. - * - `recipient` cannot be the zero address. - * - if `recipient` is a contract, it must implement the {IERC777Recipient} - * interface. - */ - function send(address recipient, uint256 amount, bytes calldata data) external; - - /** - * @dev Destroys `amount` tokens from the caller's account, reducing the - * total supply. - * - * If a send hook is registered for the caller, the corresponding function - * will be called with `data` and empty `operatorData`. See {IERC777Sender}. - * - * Emits a {Burned} event. - * - * Requirements - * - * - the caller must have at least `amount` tokens. - */ - function burn(uint256 amount, bytes calldata data) external; - - /** - * @dev Returns true if an account is an operator of `tokenHolder`. - * Operators can send and burn tokens on behalf of their owners. All - * accounts are their own operator. - * - * See {operatorSend} and {operatorBurn}. - */ - function isOperatorFor(address operator, address tokenHolder) external view returns (bool); - - /** - * @dev Make an account an operator of the caller. - * - * See {isOperatorFor}. - * - * Emits an {AuthorizedOperator} event. - * - * Requirements - * - * - `operator` cannot be calling address. - */ - function authorizeOperator(address operator) external; - - /** - * @dev Revoke an account's operator status for the caller. - * - * See {isOperatorFor} and {defaultOperators}. - * - * Emits a {RevokedOperator} event. - * - * Requirements - * - * - `operator` cannot be calling address. - */ - function revokeOperator(address operator) external; - - /** - * @dev Returns the list of default operators. These accounts are operators - * for all token holders, even if {authorizeOperator} was never called on - * them. - * - * This list is immutable, but individual holders may revoke these via - * {revokeOperator}, in which case {isOperatorFor} will return false. - */ - function defaultOperators() external view returns (address[] memory); - - /** - * @dev Moves `amount` tokens from `sender` to `recipient`. The caller must - * be an operator of `sender`. - * - * If send or receive hooks are registered for `sender` and `recipient`, - * the corresponding functions will be called with `data` and - * `operatorData`. See {IERC777Sender} and {IERC777Recipient}. - * - * Emits a {Sent} event. - * - * Requirements - * - * - `sender` cannot be the zero address. - * - `sender` must have at least `amount` tokens. - * - the caller must be an operator for `sender`. - * - `recipient` cannot be the zero address. - * - if `recipient` is a contract, it must implement the {IERC777Recipient} - * interface. - */ - function operatorSend( - address sender, - address recipient, - uint256 amount, - bytes calldata data, - bytes calldata operatorData - ) external; - - /** - * @dev Destroys `amount` tokens from `account`, reducing the total supply. - * The caller must be an operator of `account`. - * - * If a send hook is registered for `account`, the corresponding function - * will be called with `data` and `operatorData`. See {IERC777Sender}. - * - * Emits a {Burned} event. - * - * Requirements - * - * - `account` cannot be the zero address. - * - `account` must have at least `amount` tokens. - * - the caller must be an operator for `account`. - */ - function operatorBurn(address account, uint256 amount, bytes calldata data, bytes calldata operatorData) external; - - event Sent( - address indexed operator, - address indexed from, - address indexed to, - uint256 amount, - bytes data, - bytes operatorData - ); -} diff --git a/contracts/token/ERC777/IERC777Recipient.sol b/contracts/token/ERC777/IERC777Recipient.sol deleted file mode 100644 index 717dd8f8c..000000000 --- a/contracts/token/ERC777/IERC777Recipient.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC777/IERC777Recipient.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC777TokensRecipient standard as defined in the EIP. - * - * Accounts can be notified of {IERC777} tokens being sent to them by having a - * contract implement this interface (contract holders can be their own - * implementer) and registering it on the - * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. - * - * See {IERC1820Registry} and {ERC1820Implementer}. - */ -interface IERC777Recipient { - /** - * @dev Called by an {IERC777} token contract whenever tokens are being - * moved or created into a registered account (`to`). The type of operation - * is conveyed by `from` being the zero address or not. - * - * This call occurs _after_ the token contract's state is updated, so - * {IERC777-balanceOf}, etc., can be used to query the post-operation state. - * - * This function may revert to prevent the operation from being executed. - */ - function tokensReceived( - address operator, - address from, - address to, - uint256 amount, - bytes calldata userData, - bytes calldata operatorData - ) external; -} diff --git a/contracts/token/ERC777/IERC777Sender.sol b/contracts/token/ERC777/IERC777Sender.sol deleted file mode 100644 index 969e3e367..000000000 --- a/contracts/token/ERC777/IERC777Sender.sol +++ /dev/null @@ -1,35 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (token/ERC777/IERC777Sender.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the ERC777TokensSender standard as defined in the EIP. - * - * {IERC777} Token holders can be notified of operations performed on their - * tokens by having a contract implement this interface (contract holders can be - * their own implementer) and registering it on the - * https://eips.ethereum.org/EIPS/eip-1820[ERC1820 global registry]. - * - * See {IERC1820Registry} and {ERC1820Implementer}. - */ -interface IERC777Sender { - /** - * @dev Called by an {IERC777} token contract whenever a registered holder's - * (`from`) tokens are about to be moved or destroyed. The type of operation - * is conveyed by `to` being the zero address or not. - * - * This call occurs _before_ the token contract's state is updated, so - * {IERC777-balanceOf}, etc., can be used to query the pre-operation state. - * - * This function may revert to prevent the operation from being executed. - */ - function tokensToSend( - address operator, - address from, - address to, - uint256 amount, - bytes calldata userData, - bytes calldata operatorData - ) external; -} diff --git a/contracts/token/ERC777/README.adoc b/contracts/token/ERC777/README.adoc deleted file mode 100644 index 09c9e58ad..000000000 --- a/contracts/token/ERC777/README.adoc +++ /dev/null @@ -1,24 +0,0 @@ -= ERC 777 - -[.readme-notice] -NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc777 - -This set of interfaces and contracts are all related to the https://eips.ethereum.org/EIPS/eip-777[ERC777 token standard]. - -TIP: For an overview of ERC777 tokens and a walk through on how to create a token contract read our xref:ROOT:erc777.adoc[ERC777 guide]. - -The token behavior itself is implemented in the core contracts: {IERC777}, {ERC777}. - -Additionally there are interfaces used to develop contracts that react to token movements: {IERC777Sender}, {IERC777Recipient}. - -== Core - -{{IERC777}} - -{{ERC777}} - -== Hooks - -{{IERC777Sender}} - -{{IERC777Recipient}} diff --git a/contracts/token/common/ERC2981.sol b/contracts/token/common/ERC2981.sol index f49cc8d98..21869ee25 100644 --- a/contracts/token/common/ERC2981.sol +++ b/contracts/token/common/ERC2981.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (token/common/ERC2981.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (token/common/ERC2981.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../../interfaces/IERC2981.sol"; import "../../utils/introspection/ERC165.sol"; @@ -30,6 +30,26 @@ abstract contract ERC2981 is IERC2981, ERC165 { RoyaltyInfo private _defaultRoyaltyInfo; mapping(uint256 => RoyaltyInfo) private _tokenRoyaltyInfo; + /** + * @dev The default royalty set is invalid (eg. (numerator / denominator) >= 1). + */ + error ERC2981InvalidDefaultRoyalty(uint256 numerator, uint256 denominator); + + /** + * @dev The default royalty receiver is invalid. + */ + error ERC2981InvalidDefaultRoyaltyReceiver(address receiver); + + /** + * @dev The royalty set for an specific `tokenId` is invalid (eg. (numerator / denominator) >= 1). + */ + error ERC2981InvalidTokenRoyalty(uint256 tokenId, uint256 numerator, uint256 denominator); + + /** + * @dev The royalty receiver for `tokenId` is invalid. + */ + error ERC2981InvalidTokenRoyaltyReceiver(uint256 tokenId, address receiver); + /** * @dev See {IERC165-supportsInterface}. */ @@ -40,7 +60,7 @@ abstract contract ERC2981 is IERC2981, ERC165 { /** * @inheritdoc IERC2981 */ - function royaltyInfo(uint256 tokenId, uint256 salePrice) public view virtual override returns (address, uint256) { + function royaltyInfo(uint256 tokenId, uint256 salePrice) public view virtual returns (address, uint256) { RoyaltyInfo memory royalty = _tokenRoyaltyInfo[tokenId]; if (royalty.receiver == address(0)) { @@ -70,8 +90,14 @@ abstract contract ERC2981 is IERC2981, ERC165 { * - `feeNumerator` cannot be greater than the fee denominator. */ function _setDefaultRoyalty(address receiver, uint96 feeNumerator) internal virtual { - require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice"); - require(receiver != address(0), "ERC2981: invalid receiver"); + uint256 denominator = _feeDenominator(); + if (feeNumerator > denominator) { + // Royalty fee will exceed the sale price + revert ERC2981InvalidDefaultRoyalty(feeNumerator, denominator); + } + if (receiver == address(0)) { + revert ERC2981InvalidDefaultRoyaltyReceiver(address(0)); + } _defaultRoyaltyInfo = RoyaltyInfo(receiver, feeNumerator); } @@ -92,8 +118,14 @@ abstract contract ERC2981 is IERC2981, ERC165 { * - `feeNumerator` cannot be greater than the fee denominator. */ function _setTokenRoyalty(uint256 tokenId, address receiver, uint96 feeNumerator) internal virtual { - require(feeNumerator <= _feeDenominator(), "ERC2981: royalty fee will exceed salePrice"); - require(receiver != address(0), "ERC2981: Invalid parameters"); + uint256 denominator = _feeDenominator(); + if (feeNumerator > denominator) { + // Royalty fee will exceed the sale price + revert ERC2981InvalidTokenRoyalty(tokenId, feeNumerator, denominator); + } + if (receiver == address(0)) { + revert ERC2981InvalidTokenRoyaltyReceiver(tokenId, address(0)); + } _tokenRoyaltyInfo[tokenId] = RoyaltyInfo(receiver, feeNumerator); } diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index e08cdc754..859332b39 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -1,12 +1,27 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/Address.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Address.sol) -pragma solidity ^0.8.1; +pragma solidity ^0.8.19; /** * @dev Collection of functions related to the address type */ library Address { + /** + * @dev The ETH balance of the account is not enough to perform the operation. + */ + error AddressInsufficientBalance(address account); + + /** + * @dev There's no code at `target` (it is not a contract). + */ + error AddressEmptyCode(address target); + + /** + * @dev A call to an address target failed. The target may have reverted. + */ + error FailedInnerCall(); + /** * @dev Replacement for Solidity's `transfer`: sends `amount` wei to * `recipient`, forwarding all available gas and reverting on errors. @@ -21,13 +36,17 @@ library Address { * IMPORTANT: because control is transferred to `recipient`, care must be * taken to not create reentrancy vulnerabilities. Consider using * {ReentrancyGuard} or the - * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. + * https://solidity.readthedocs.io/en/v0.8.0/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern]. */ function sendValue(address payable recipient, uint256 amount) internal { - require(address(this).balance >= amount, "Address: insufficient balance"); + if (address(this).balance < amount) { + revert AddressInsufficientBalance(address(this)); + } (bool success, ) = recipient.call{value: amount}(""); - require(success, "Address: unable to send value, recipient may have reverted"); + if (!success) { + revert FailedInnerCall(); + } } /** @@ -49,21 +68,25 @@ library Address { * _Available since v3.1._ */ function functionCall(address target, bytes memory data) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, "Address: low-level call failed"); + return functionCallWithValue(target, data, 0, defaultRevert); } /** - * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with - * `errorMessage` as a fallback revert reason when `target` reverts. + * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with a + * `customRevert` function as a fallback when `target` reverts. * - * _Available since v3.1._ + * Requirements: + * + * - `customRevert` must be a reverting function. + * + * _Available since v5.0._ */ function functionCall( address target, bytes memory data, - string memory errorMessage + function() internal view customRevert ) internal returns (bytes memory) { - return functionCallWithValue(target, data, 0, errorMessage); + return functionCallWithValue(target, data, 0, customRevert); } /** @@ -78,24 +101,30 @@ library Address { * _Available since v3.1._ */ function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) { - return functionCallWithValue(target, data, value, "Address: low-level call with value failed"); + return functionCallWithValue(target, data, value, defaultRevert); } /** * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but - * with `errorMessage` as a fallback revert reason when `target` reverts. + * with a `customRevert` function as a fallback revert reason when `target` reverts. * - * _Available since v3.1._ + * Requirements: + * + * - `customRevert` must be a reverting function. + * + * _Available since v5.0._ */ function functionCallWithValue( address target, bytes memory data, uint256 value, - string memory errorMessage + function() internal view customRevert ) internal returns (bytes memory) { - require(address(this).balance >= value, "Address: insufficient balance for call"); + if (address(this).balance < value) { + revert AddressInsufficientBalance(address(this)); + } (bool success, bytes memory returndata) = target.call{value: value}(data); - return verifyCallResultFromTarget(target, success, returndata, errorMessage); + return verifyCallResultFromTarget(target, success, returndata, customRevert); } /** @@ -105,7 +134,7 @@ library Address { * _Available since v3.3._ */ function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) { - return functionStaticCall(target, data, "Address: low-level static call failed"); + return functionStaticCall(target, data, defaultRevert); } /** @@ -117,10 +146,10 @@ library Address { function functionStaticCall( address target, bytes memory data, - string memory errorMessage + function() internal view customRevert ) internal view returns (bytes memory) { (bool success, bytes memory returndata) = target.staticcall(data); - return verifyCallResultFromTarget(target, success, returndata, errorMessage); + return verifyCallResultFromTarget(target, success, returndata, customRevert); } /** @@ -130,7 +159,7 @@ library Address { * _Available since v3.4._ */ function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) { - return functionDelegateCall(target, data, "Address: low-level delegate call failed"); + return functionDelegateCall(target, data, defaultRevert); } /** @@ -142,55 +171,78 @@ library Address { function functionDelegateCall( address target, bytes memory data, - string memory errorMessage + function() internal view customRevert ) internal returns (bytes memory) { (bool success, bytes memory returndata) = target.delegatecall(data); - return verifyCallResultFromTarget(target, success, returndata, errorMessage); + return verifyCallResultFromTarget(target, success, returndata, customRevert); } /** * @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling - * the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract. + * the revert reason or using the provided `customRevert`) in case of unsuccessful call or if target was not a contract. * - * _Available since v4.8._ + * _Available since v5.0._ */ function verifyCallResultFromTarget( address target, bool success, bytes memory returndata, - string memory errorMessage + function() internal view customRevert ) internal view returns (bytes memory) { if (success) { if (returndata.length == 0) { // only check if target is a contract if the call was successful and the return data is empty // otherwise we already know that it was a contract - require(target.code.length > 0, "Address: call to non-contract"); + if (target.code.length == 0) { + revert AddressEmptyCode(target); + } } return returndata; } else { - _revert(returndata, errorMessage); + _revert(returndata, customRevert); } } /** * @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the - * revert reason or using the provided one. + * revert reason or with a default revert error. * - * _Available since v4.3._ + * _Available since v5.0._ + */ + function verifyCallResult(bool success, bytes memory returndata) internal view returns (bytes memory) { + return verifyCallResult(success, returndata, defaultRevert); + } + + /** + * @dev Same as {xref-Address-verifyCallResult-bool-bytes-}[`verifyCallResult`], but with a + * `customRevert` function as a fallback when `success` is `false`. + * + * Requirements: + * + * - `customRevert` must be a reverting function. + * + * _Available since v5.0._ */ function verifyCallResult( bool success, bytes memory returndata, - string memory errorMessage - ) internal pure returns (bytes memory) { + function() internal view customRevert + ) internal view returns (bytes memory) { if (success) { return returndata; } else { - _revert(returndata, errorMessage); + _revert(returndata, customRevert); } } - function _revert(bytes memory returndata, string memory errorMessage) private pure { + /** + * @dev Default reverting function when no `customRevert` is provided in a function call. + */ + function defaultRevert() internal pure { + revert FailedInnerCall(); + } + + function _revert(bytes memory returndata, function() internal view customRevert) private view { // Look for revert reason and bubble it up if present if (returndata.length > 0) { // The easiest way to bubble the revert reason is using memory via assembly @@ -200,7 +252,8 @@ library Address { revert(add(32, returndata), returndata_size) } } else { - revert(errorMessage); + customRevert(); + revert FailedInnerCall(); } } } diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index 24964759a..f4ef45645 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/Arrays.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Arrays.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./StorageSlot.sol"; import "./math/Math.sol"; @@ -102,4 +102,26 @@ library Arrays { } return slot.getUint256Slot(); } + + /** + * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. + * + * WARNING: Only use if you are certain `pos` is lower than the array length. + */ + function unsafeMemoryAccess(uint256[] memory arr, uint256 pos) internal pure returns (uint256 res) { + assembly { + res := mload(add(add(arr, 0x20), mul(pos, 0x20))) + } + } + + /** + * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check. + * + * WARNING: Only use if you are certain `pos` is lower than the array length. + */ + function unsafeMemoryAccess(address[] memory arr, uint256 pos) internal pure returns (address res) { + assembly { + res := mload(add(add(arr, 0x20), mul(pos, 0x20))) + } + } } diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index 4e08cd563..9ba6defe0 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.7.0) (utils/Base64.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Provides a set of functions to operate with Base64 strings. diff --git a/contracts/utils/Context.sol b/contracts/utils/Context.sol index f304065b4..2d517987d 100644 --- a/contracts/utils/Context.sol +++ b/contracts/utils/Context.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/Context.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Provides information about the current execution context, including the diff --git a/contracts/utils/Counters.sol b/contracts/utils/Counters.sol deleted file mode 100644 index 8a4f2a2e7..000000000 --- a/contracts/utils/Counters.sol +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Counters.sol) - -pragma solidity ^0.8.0; - -/** - * @title Counters - * @author Matt Condon (@shrugs) - * @dev Provides counters that can only be incremented, decremented or reset. This can be used e.g. to track the number - * of elements in a mapping, issuing ERC721 ids, or counting request ids. - * - * Include with `using Counters for Counters.Counter;` - */ -library Counters { - struct Counter { - // This variable should never be directly accessed by users of the library: interactions must be restricted to - // the library's function. As of Solidity v0.5.2, this cannot be enforced, though there is a proposal to add - // this feature: see https://github.com/ethereum/solidity/issues/4637 - uint256 _value; // default: 0 - } - - function current(Counter storage counter) internal view returns (uint256) { - return counter._value; - } - - function increment(Counter storage counter) internal { - unchecked { - counter._value += 1; - } - } - - function decrement(Counter storage counter) internal { - uint256 value = counter._value; - require(value > 0, "Counter: decrement overflow"); - unchecked { - counter._value = value - 1; - } - } - - function reset(Counter storage counter) internal { - counter._value = 0; - } -} diff --git a/contracts/utils/Create2.sol b/contracts/utils/Create2.sol index 2255a4df8..24d27ea0b 100644 --- a/contracts/utils/Create2.sol +++ b/contracts/utils/Create2.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/Create2.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Create2.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Helper to make usage of the `CREATE2` EVM opcode easier and safer. @@ -13,6 +13,21 @@ pragma solidity ^0.8.0; * information. */ library Create2 { + /** + * @dev Not enough balance for performing a CREATE2 deploy. + */ + error Create2InsufficientBalance(uint256 balance, uint256 needed); + + /** + * @dev There's no code to deploy. + */ + error Create2EmptyBytecode(); + + /** + * @dev The deployment failed. + */ + error Create2FailedDeployment(); + /** * @dev Deploys a contract using `CREATE2`. The address where the contract * will be deployed can be known in advance via {computeAddress}. @@ -28,13 +43,19 @@ library Create2 { * - if `amount` is non-zero, `bytecode` must have a `payable` constructor. */ function deploy(uint256 amount, bytes32 salt, bytes memory bytecode) internal returns (address addr) { - require(address(this).balance >= amount, "Create2: insufficient balance"); - require(bytecode.length != 0, "Create2: bytecode length is zero"); + if (address(this).balance < amount) { + revert Create2InsufficientBalance(address(this).balance, amount); + } + if (bytecode.length == 0) { + revert Create2EmptyBytecode(); + } /// @solidity memory-safe-assembly assembly { addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt) } - require(addr != address(0), "Create2: Failed on deploy"); + if (addr == address(0)) { + revert Create2FailedDeployment(); + } } /** diff --git a/contracts/utils/Multicall.sol b/contracts/utils/Multicall.sol index 7470c5595..8e0ef8195 100644 --- a/contracts/utils/Multicall.sol +++ b/contracts/utils/Multicall.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.5.0) (utils/Multicall.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Multicall.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./Address.sol"; diff --git a/contracts/utils/Nonces.sol b/contracts/utils/Nonces.sol index ce65909ff..f8ea1dfd3 100644 --- a/contracts/utils/Nonces.sol +++ b/contracts/utils/Nonces.sol @@ -1,21 +1,22 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "./Counters.sol"; +pragma solidity ^0.8.19; /** * @dev Provides tracking nonces for addresses. Nonces will only increment. */ abstract contract Nonces { - using Counters for Counters.Counter; + /** + * @dev The nonce used for an `account` is not the expected current nonce. + */ + error InvalidAccountNonce(address account, uint256 currentNonce); - mapping(address => Counters.Counter) private _nonces; + mapping(address => uint256) private _nonces; /** * @dev Returns an address nonce. */ function nonces(address owner) public view virtual returns (uint256) { - return _nonces[owner].current(); + return _nonces[owner]; } /** @@ -23,9 +24,23 @@ abstract contract Nonces { * * Returns the current value and increments nonce. */ - function _useNonce(address owner) internal virtual returns (uint256 current) { - Counters.Counter storage nonce = _nonces[owner]; - current = nonce.current(); - nonce.increment(); + function _useNonce(address owner) internal virtual returns (uint256) { + // For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be + // decremented or reset. This guarantees that the nonce never overflows. + unchecked { + // It is important to do x++ and not ++x here. + return _nonces[owner]++; + } + } + + /** + * @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`. + */ + function _useCheckedNonce(address owner, uint256 nonce) internal virtual returns (uint256) { + uint256 current = _useNonce(owner); + if (nonce != current) { + revert InvalidAccountNonce(owner, current); + } + return current; } } diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index babaef522..aa5182bd1 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -10,7 +10,6 @@ The {Address}, {Arrays}, {Base64} and {Strings} libraries provide more operation For new data types: - * {Counters}: a simple way to get a counter that can only be incremented, decremented or reset. Very useful for ID generation, counting contract activity, among others. * {EnumerableMap}: like Solidity's https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`] type, but with key-value _enumeration_: this will let you know how many entries a mapping has, and iterate over them (which is not possible with `mapping`). * {EnumerableSet}: like {EnumerableMap}, but for https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets]. Can be used to store privileged accounts, issued IDs, etc. @@ -31,10 +30,6 @@ Finally, {Create2} contains all necessary utilities to safely use the https://bl {{SafeCast}} -{{SafeMath}} - -{{SignedSafeMath}} - == Cryptography {{ECDSA}} @@ -45,39 +40,18 @@ Finally, {Create2} contains all necessary utilities to safely use the https://bl {{EIP712}} -== Escrow - -{{ConditionalEscrow}} - -{{Escrow}} - -{{RefundEscrow}} - == Introspection This set of interfaces and contracts deal with https://en.wikipedia.org/wiki/Type_introspection[type introspection] of contracts, that is, examining which functions can be called on them. This is usually referred to as a contract's _interface_. Ethereum contracts have no native concept of an interface, so applications must usually simply trust they are not making an incorrect call. For trusted setups this is a non-issue, but often unknown and untrusted third-party addresses need to be interacted with. There may even not be any direct calls to them! (e.g. `ERC20` tokens may be sent to a contract that lacks a way to transfer them out of it, locking them forever). In these cases, a contract _declaring_ its interface can be very helpful in preventing errors. -There are two main ways to approach this. - -* Locally, where a contract implements `IERC165` and declares an interface, and a second one queries it directly via `ERC165Checker`. -* Globally, where a global and unique registry (`IERC1820Registry`) is used to register implementers of a certain interface (`IERC1820Implementer`). It is then the registry that is queried, which allows for more complex setups, like contracts implementing interfaces for externally-owned accounts. - -Note that, in all cases, accounts simply _declare_ their interfaces, but they are not required to actually implement them. This mechanism can therefore be used to both prevent errors and allow for complex interactions (see `ERC777`), but it must not be relied on for security. - {{IERC165}} {{ERC165}} {{ERC165Checker}} -{{IERC1820Registry}} - -{{IERC1820Implementer}} - -{{ERC1820Implementer}} - == Data Structures {{BitMaps}} @@ -100,10 +74,10 @@ Note that, in all cases, accounts simply _declare_ their interfaces, but they ar {{Base64}} -{{Counters}} - {{Strings}} +{{ShortStrings}} + {{StorageSlot}} {{Multicall}} diff --git a/contracts/utils/ShortStrings.sol b/contracts/utils/ShortStrings.sol new file mode 100644 index 000000000..a6cb1e620 --- /dev/null +++ b/contracts/utils/ShortStrings.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.9.0) (utils/ShortStrings.sol) + +pragma solidity ^0.8.19; + +import "./StorageSlot.sol"; + +// | string | 0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA | +// | length | 0x BB | +type ShortString is bytes32; + +/** + * @dev This library provides functions to convert short memory strings + * into a `ShortString` type that can be used as an immutable variable. + * + * Strings of arbitrary length can be optimized using this library if + * they are short enough (up to 31 bytes) by packing them with their + * length (1 byte) in a single EVM word (32 bytes). Additionally, a + * fallback mechanism can be used for every other case. + * + * Usage example: + * + * ```solidity + * contract Named { + * using ShortStrings for *; + * + * ShortString private immutable _name; + * string private _nameFallback; + * + * constructor(string memory contractName) { + * _name = contractName.toShortStringWithFallback(_nameFallback); + * } + * + * function name() external view returns (string memory) { + * return _name.toStringWithFallback(_nameFallback); + * } + * } + * ``` + */ +library ShortStrings { + // Used as an identifier for strings longer than 31 bytes. + bytes32 private constant _FALLBACK_SENTINEL = 0x00000000000000000000000000000000000000000000000000000000000000FF; + + error StringTooLong(string str); + error InvalidShortString(); + + /** + * @dev Encode a string of at most 31 chars into a `ShortString`. + * + * This will trigger a `StringTooLong` error is the input string is too long. + */ + function toShortString(string memory str) internal pure returns (ShortString) { + bytes memory bstr = bytes(str); + if (bstr.length > 31) { + revert StringTooLong(str); + } + return ShortString.wrap(bytes32(uint256(bytes32(bstr)) | bstr.length)); + } + + /** + * @dev Decode a `ShortString` back to a "normal" string. + */ + function toString(ShortString sstr) internal pure returns (string memory) { + uint256 len = byteLength(sstr); + // using `new string(len)` would work locally but is not memory safe. + string memory str = new string(32); + /// @solidity memory-safe-assembly + assembly { + mstore(str, len) + mstore(add(str, 0x20), sstr) + } + return str; + } + + /** + * @dev Return the length of a `ShortString`. + */ + function byteLength(ShortString sstr) internal pure returns (uint256) { + uint256 result = uint256(ShortString.unwrap(sstr)) & 0xFF; + if (result > 31) { + revert InvalidShortString(); + } + return result; + } + + /** + * @dev Encode a string into a `ShortString`, or write it to storage if it is too long. + */ + function toShortStringWithFallback(string memory value, string storage store) internal returns (ShortString) { + if (bytes(value).length < 32) { + return toShortString(value); + } else { + StorageSlot.getStringSlot(store).value = value; + return ShortString.wrap(_FALLBACK_SENTINEL); + } + } + + /** + * @dev Decode a string that was encoded to `ShortString` or written to storage using {setWithFallback}. + */ + function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) { + if (ShortString.unwrap(value) != _FALLBACK_SENTINEL) { + return toString(value); + } else { + return store; + } + } + + /** + * @dev Return the length of a string that was encoded to `ShortString` or written to storage using {setWithFallback}. + * + * WARNING: This will return the "byte length" of the string. This may not reflect the actual length in terms of + * actual characters as the UTF-8 encoding of a single character can span over multiple bytes. + */ + function byteLengthWithFallback(ShortString value, string storage store) internal view returns (uint256) { + if (ShortString.unwrap(value) != _FALLBACK_SENTINEL) { + return byteLength(value); + } else { + return bytes(store).length; + } + } +} diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index 5941ee9d4..b0e918967 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (utils/StorageSlot.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/StorageSlot.sol) +// This file was procedurally generated from scripts/generate/templates/StorageSlot.js. -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Library for reading and writing primitive types to specific storage slots. @@ -12,7 +13,7 @@ pragma solidity ^0.8.0; * The functions in this library return Slot structs that contain a `value` member that can be used to read or write. * * Example usage to set ERC1967 implementation slot: - * ``` + * ```solidity * contract ERC1967 { * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; * @@ -21,13 +22,14 @@ pragma solidity ^0.8.0; * } * * function _setImplementation(address newImplementation) internal { - * require(newImplementation.code.length > 0, "ERC1967: new implementation is not a contract"); + * require(newImplementation.code.length > 0); * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; * } * } * ``` * - * _Available since v4.1 for `address`, `bool`, `bytes32`, and `uint256`._ + * _Available since v4.1 for `address`, `bool`, `bytes32`, `uint256`._ + * _Available since v4.9 for `string`, `bytes`._ */ library StorageSlot { struct AddressSlot { @@ -46,6 +48,14 @@ library StorageSlot { uint256 value; } + struct StringSlot { + string value; + } + + struct BytesSlot { + bytes value; + } + /** * @dev Returns an `AddressSlot` with member `value` located at `slot`. */ @@ -85,4 +95,44 @@ library StorageSlot { r.slot := slot } } + + /** + * @dev Returns an `StringSlot` with member `value` located at `slot`. + */ + function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `StringSlot` representation of the string storage pointer `store`. + */ + function getStringSlot(string storage store) internal pure returns (StringSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } + + /** + * @dev Returns an `BytesSlot` with member `value` located at `slot`. + */ + function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } + } + + /** + * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`. + */ + function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } + } } diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 3a037f477..afbf463d1 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/Strings.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/Strings.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./math/Math.sol"; import "./math/SignedMath.sol"; @@ -13,6 +13,11 @@ library Strings { bytes16 private constant _SYMBOLS = "0123456789abcdef"; uint8 private constant _ADDRESS_LENGTH = 20; + /** + * @dev The `value` string doesn't fit in the specified `length`. + */ + error StringsInsufficientHexLength(uint256 value, uint256 length); + /** * @dev Converts a `uint256` to its ASCII `string` decimal representation. */ @@ -41,8 +46,8 @@ library Strings { /** * @dev Converts a `int256` to its ASCII `string` decimal representation. */ - function toString(int256 value) internal pure returns (string memory) { - return string(abi.encodePacked(value < 0 ? "-" : "", toString(SignedMath.abs(value)))); + function toStringSigned(int256 value) internal pure returns (string memory) { + return string.concat(value < 0 ? "-" : "", toString(SignedMath.abs(value))); } /** @@ -58,14 +63,17 @@ library Strings { * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length. */ function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + uint256 localValue = value; bytes memory buffer = new bytes(2 * length + 2); buffer[0] = "0"; buffer[1] = "x"; for (uint256 i = 2 * length + 1; i > 1; --i) { - buffer[i] = _SYMBOLS[value & 0xf]; - value >>= 4; + buffer[i] = _SYMBOLS[localValue & 0xf]; + localValue >>= 4; + } + if (localValue != 0) { + revert StringsInsufficientHexLength(value, length); } - require(value == 0, "Strings: hex length insufficient"); return string(buffer); } @@ -80,6 +88,6 @@ library Strings { * @dev Returns true if the two strings are equal. */ function equal(string memory a, string memory b) internal pure returns (bool) { - return keccak256(bytes(a)) == keccak256(bytes(b)); + return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); } } diff --git a/contracts/utils/Timers.sol b/contracts/utils/Timers.sol deleted file mode 100644 index 4bc86f202..000000000 --- a/contracts/utils/Timers.sol +++ /dev/null @@ -1,73 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/Timers.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Tooling for timepoints, timers and delays - */ -library Timers { - struct Timestamp { - uint64 _deadline; - } - - function getDeadline(Timestamp memory timer) internal pure returns (uint64) { - return timer._deadline; - } - - function setDeadline(Timestamp storage timer, uint64 timestamp) internal { - timer._deadline = timestamp; - } - - function reset(Timestamp storage timer) internal { - timer._deadline = 0; - } - - function isUnset(Timestamp memory timer) internal pure returns (bool) { - return timer._deadline == 0; - } - - function isStarted(Timestamp memory timer) internal pure returns (bool) { - return timer._deadline > 0; - } - - function isPending(Timestamp memory timer) internal view returns (bool) { - return timer._deadline > block.timestamp; - } - - function isExpired(Timestamp memory timer) internal view returns (bool) { - return isStarted(timer) && timer._deadline <= block.timestamp; - } - - struct BlockNumber { - uint64 _deadline; - } - - function getDeadline(BlockNumber memory timer) internal pure returns (uint64) { - return timer._deadline; - } - - function setDeadline(BlockNumber storage timer, uint64 timestamp) internal { - timer._deadline = timestamp; - } - - function reset(BlockNumber storage timer) internal { - timer._deadline = 0; - } - - function isUnset(BlockNumber memory timer) internal pure returns (bool) { - return timer._deadline == 0; - } - - function isStarted(BlockNumber memory timer) internal pure returns (bool) { - return timer._deadline > 0; - } - - function isPending(BlockNumber memory timer) internal view returns (bool) { - return timer._deadline > block.number; - } - - function isExpired(BlockNumber memory timer) internal view returns (bool) { - return isStarted(timer) && timer._deadline <= block.number; - } -} diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index 3f996520e..b8f1affee 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/ECDSA.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/ECDSA.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "../Strings.sol"; @@ -16,19 +16,33 @@ library ECDSA { NoError, InvalidSignature, InvalidSignatureLength, - InvalidSignatureS, - InvalidSignatureV // Deprecated in v4.8 + InvalidSignatureS } - function _throwError(RecoverError error) private pure { + /** + * @dev The signature derives the `address(0)`. + */ + error ECDSAInvalidSignature(); + + /** + * @dev The signature has an invalid length. + */ + error ECDSAInvalidSignatureLength(uint256 length); + + /** + * @dev The signature has an S value that is in the upper half order. + */ + error ECDSAInvalidSignatureS(bytes32 s); + + function _throwError(RecoverError error, bytes32 errorArg) private pure { if (error == RecoverError.NoError) { return; // no error: do nothing } else if (error == RecoverError.InvalidSignature) { - revert("ECDSA: invalid signature"); + revert ECDSAInvalidSignature(); } else if (error == RecoverError.InvalidSignatureLength) { - revert("ECDSA: invalid signature length"); + revert ECDSAInvalidSignatureLength(uint256(errorArg)); } else if (error == RecoverError.InvalidSignatureS) { - revert("ECDSA: invalid signature 's' value"); + revert ECDSAInvalidSignatureS(errorArg); } } @@ -36,7 +50,7 @@ library ECDSA { * @dev Returns the address that signed a hashed message (`hash`) with * `signature` or error string. This address can then be used for verification purposes. * - * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: * this function rejects them by requiring the `s` value to be in the lower * half order, and the `v` value to be either 27 or 28. * @@ -52,7 +66,7 @@ library ECDSA { * * _Available since v4.3._ */ - function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) { + function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError, bytes32) { if (signature.length == 65) { bytes32 r; bytes32 s; @@ -67,7 +81,7 @@ library ECDSA { } return tryRecover(hash, v, r, s); } else { - return (address(0), RecoverError.InvalidSignatureLength); + return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); } } @@ -75,7 +89,7 @@ library ECDSA { * @dev Returns the address that signed a hashed message (`hash`) with * `signature`. This address can then be used for verification purposes. * - * The `ecrecover` EVM opcode allows for malleable (non-unique) signatures: + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: * this function rejects them by requiring the `s` value to be in the lower * half order, and the `v` value to be either 27 or 28. * @@ -86,8 +100,8 @@ library ECDSA { * be too long), and then calling {toEthSignedMessageHash} on it. */ function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, signature); - _throwError(error); + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); + _throwError(error, errorArg); return recovered; } @@ -98,10 +112,13 @@ library ECDSA { * * _Available since v4.3._ */ - function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError) { - bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); - uint8 v = uint8((uint256(vs) >> 255) + 27); - return tryRecover(hash, v, r, s); + function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError, bytes32) { + unchecked { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + // We do not check for an overflow here since the shift operation results in 0 or 1. + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } } /** @@ -110,8 +127,8 @@ library ECDSA { * _Available since v4.2._ */ function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, r, vs); - _throwError(error); + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); + _throwError(error, errorArg); return recovered; } @@ -121,7 +138,12 @@ library ECDSA { * * _Available since v4.3._ */ - function tryRecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address, RecoverError) { + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address, RecoverError, bytes32) { // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most @@ -132,16 +154,16 @@ library ECDSA { // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept // these malleable signatures as well. if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - return (address(0), RecoverError.InvalidSignatureS); + return (address(0), RecoverError.InvalidSignatureS, s); } // If the signature is valid (and not malleable), return the signer address address signer = ecrecover(hash, v, r, s); if (signer == address(0)) { - return (address(0), RecoverError.InvalidSignature); + return (address(0), RecoverError.InvalidSignature, bytes32(0)); } - return (signer, RecoverError.NoError); + return (signer, RecoverError.NoError, bytes32(0)); } /** @@ -149,8 +171,8 @@ library ECDSA { * `r` and `s` signature fields separately. */ function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { - (address recovered, RecoverError error) = tryRecover(hash, v, r, s); - _throwError(error); + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); + _throwError(error, errorArg); return recovered; } @@ -162,10 +184,15 @@ library ECDSA { * * See {recover}. */ - function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32 message) { // 32 is the length in bytes of hash, // enforced by the type signature above - return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + /// @solidity memory-safe-assembly + assembly { + mstore(0x00, "\x19Ethereum Signed Message:\n32") + mstore(0x1c, hash) + message := keccak256(0x00, 0x3c) + } } /** @@ -189,7 +216,24 @@ library ECDSA { * * See {recover}. */ - function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) { - return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 data) { + /// @solidity memory-safe-assembly + assembly { + let ptr := mload(0x40) + mstore(ptr, hex"19_01") + mstore(add(ptr, 0x02), domainSeparator) + mstore(add(ptr, 0x22), structHash) + data := keccak256(ptr, 0x42) + } + } + + /** + * @dev Returns an Ethereum Signed Data with intended validator, created from a + * `validator` and `data` according to the version 0 of EIP-191. + * + * See {recover}. + */ + function toDataWithIntendedValidatorHash(address validator, bytes memory data) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(hex"19_00", validator, data)); } } diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index eb211a7e2..2628014f1 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/EIP712.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/EIP712.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./ECDSA.sol"; +import "../ShortStrings.sol"; +import "../../interfaces/IERC5267.sol"; /** * @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data. @@ -22,21 +24,33 @@ import "./ECDSA.sol"; * NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method * https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask]. * + * NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain + * separator of the implementation contract. This will cause the `_domainSeparatorV4` function to always rebuild the + * separator from the immutable values, which is cheaper than accessing a cached version in cold storage. + * * _Available since v3.4._ + * + * @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment */ -abstract contract EIP712 { - /* solhint-disable var-name-mixedcase */ +abstract contract EIP712 is IERC5267 { + using ShortStrings for *; + + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to // invalidate the cached domain separator if the chain id changes. - bytes32 private immutable _CACHED_DOMAIN_SEPARATOR; - uint256 private immutable _CACHED_CHAIN_ID; - address private immutable _CACHED_THIS; + bytes32 private immutable _cachedDomainSeparator; + uint256 private immutable _cachedChainId; + address private immutable _cachedThis; - bytes32 private immutable _HASHED_NAME; - bytes32 private immutable _HASHED_VERSION; - bytes32 private immutable _TYPE_HASH; + bytes32 private immutable _hashedName; + bytes32 private immutable _hashedVersion; - /* solhint-enable var-name-mixedcase */ + ShortString private immutable _name; + ShortString private immutable _version; + string private _nameFallback; + string private _versionFallback; /** * @dev Initializes the domain separator and parameter caches. @@ -51,36 +65,29 @@ abstract contract EIP712 { * contract upgrade]. */ constructor(string memory name, string memory version) { - bytes32 hashedName = keccak256(bytes(name)); - bytes32 hashedVersion = keccak256(bytes(version)); - bytes32 typeHash = keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ); - _HASHED_NAME = hashedName; - _HASHED_VERSION = hashedVersion; - _CACHED_CHAIN_ID = block.chainid; - _CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion); - _CACHED_THIS = address(this); - _TYPE_HASH = typeHash; + _name = name.toShortStringWithFallback(_nameFallback); + _version = version.toShortStringWithFallback(_versionFallback); + _hashedName = keccak256(bytes(name)); + _hashedVersion = keccak256(bytes(version)); + + _cachedChainId = block.chainid; + _cachedDomainSeparator = _buildDomainSeparator(); + _cachedThis = address(this); } /** * @dev Returns the domain separator for the current chain. */ function _domainSeparatorV4() internal view returns (bytes32) { - if (address(this) == _CACHED_THIS && block.chainid == _CACHED_CHAIN_ID) { - return _CACHED_DOMAIN_SEPARATOR; + if (address(this) == _cachedThis && block.chainid == _cachedChainId) { + return _cachedDomainSeparator; } else { - return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION); + return _buildDomainSeparator(); } } - function _buildDomainSeparator( - bytes32 typeHash, - bytes32 nameHash, - bytes32 versionHash - ) private view returns (bytes32) { - return keccak256(abi.encode(typeHash, nameHash, versionHash, block.chainid, address(this))); + function _buildDomainSeparator() private view returns (bytes32) { + return keccak256(abi.encode(_TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))); } /** @@ -101,4 +108,60 @@ abstract contract EIP712 { function _hashTypedDataV4(bytes32 structHash) internal view virtual returns (bytes32) { return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); } + + /** + * @dev See {EIP-5267}. + * + * _Available since v4.9._ + */ + function eip712Domain() + public + view + virtual + returns ( + bytes1 fields, + string memory name, + string memory version, + uint256 chainId, + address verifyingContract, + bytes32 salt, + uint256[] memory extensions + ) + { + return ( + hex"0f", // 01111 + _EIP712Name(), + _EIP712Version(), + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } + + /** + * @dev The name parameter for the EIP712 domain. + * + * NOTE: By default this function reads _name which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + * + * _Available since v5.0._ + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Name() internal view returns (string memory) { + return _name.toStringWithFallback(_nameFallback); + } + + /** + * @dev The version parameter for the EIP712 domain. + * + * NOTE: By default this function reads _version which is an immutable value. + * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). + * + * _Available since v5.0._ + */ + // solhint-disable-next-line func-name-mixedcase + function _EIP712Version() internal view returns (string memory) { + return _version.toStringWithFallback(_versionFallback); + } } diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index 3862fdbfe..94586ff7b 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/MerkleProof.sol) +// OpenZeppelin Contracts (last updated v4.9.2) (utils/cryptography/MerkleProof.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev These functions deal with verification of Merkle Tree proofs. @@ -18,6 +18,11 @@ pragma solidity ^0.8.0; * against this attack out of the box. */ library MerkleProof { + /** + *@dev The multiproof provided is not valid. + */ + error MerkleProofInvalidMultiproof(); + /** * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree * defined by `root`. For this, a `proof` must be provided, containing @@ -121,10 +126,13 @@ library MerkleProof { // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of // the merkle tree. uint256 leavesLen = leaves.length; + uint256 proofLen = proof.length; uint256 totalHashes = proofFlags.length; // Check proof validity. - require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof"); + if (leavesLen + proofLen - 1 != totalHashes) { + revert MerkleProofInvalidMultiproof(); + } // 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". @@ -146,6 +154,9 @@ library MerkleProof { } if (totalHashes > 0) { + if (proofPos != proofLen) { + revert MerkleProofInvalidMultiproof(); + } unchecked { return hashes[totalHashes - 1]; } @@ -173,10 +184,13 @@ library MerkleProof { // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of // the merkle tree. uint256 leavesLen = leaves.length; + uint256 proofLen = proof.length; uint256 totalHashes = proofFlags.length; // Check proof validity. - require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof"); + if (leavesLen + proofLen - 1 != totalHashes) { + revert MerkleProofInvalidMultiproof(); + } // 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". @@ -198,6 +212,9 @@ library MerkleProof { } if (totalHashes > 0) { + if (proofPos != proofLen) { + revert MerkleProofInvalidMultiproof(); + } unchecked { return hashes[totalHashes - 1]; } diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index e06778deb..25fdee5b3 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/SignatureChecker.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/cryptography/SignatureChecker.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./ECDSA.sol"; import "../../interfaces/IERC1271.sol"; @@ -22,16 +22,29 @@ library SignatureChecker { * change through time. It could return true at block N and false at block N+1 (or the opposite). */ function isValidSignatureNow(address signer, bytes32 hash, bytes memory signature) internal view returns (bool) { - (address recovered, ECDSA.RecoverError error) = ECDSA.tryRecover(hash, signature); - if (error == ECDSA.RecoverError.NoError && recovered == signer) { - return true; - } + (address recovered, ECDSA.RecoverError error, ) = ECDSA.tryRecover(hash, signature); + return + (error == ECDSA.RecoverError.NoError && recovered == signer) || + isValidERC1271SignatureNow(signer, hash, signature); + } + /** + * @dev Checks if a signature is valid for a given signer and data hash. The signature is validated + * against the signer smart contract using ERC1271. + * + * NOTE: Unlike ECDSA signatures, contract signatures are revocable, and the outcome of this function can thus + * change through time. It could return true at block N and false at block N+1 (or the opposite). + */ + function isValidERC1271SignatureNow( + address signer, + bytes32 hash, + bytes memory signature + ) internal view returns (bool) { (bool success, bytes memory result) = signer.staticcall( - abi.encodeWithSelector(IERC1271.isValidSignature.selector, hash, signature) + abi.encodeCall(IERC1271.isValidSignature, (hash, signature)) ); return (success && - result.length == 32 && + result.length >= 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); } } diff --git a/contracts/utils/cryptography/draft-EIP712.sol b/contracts/utils/cryptography/draft-EIP712.sol deleted file mode 100644 index fdae3ba3e..000000000 --- a/contracts/utils/cryptography/draft-EIP712.sol +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/cryptography/draft-EIP712.sol) - -pragma solidity ^0.8.0; - -// EIP-712 is Final as of 2022-08-11. This file is deprecated. - -import "./EIP712.sol"; diff --git a/contracts/utils/escrow/ConditionalEscrow.sol b/contracts/utils/escrow/ConditionalEscrow.sol deleted file mode 100644 index 87f53815b..000000000 --- a/contracts/utils/escrow/ConditionalEscrow.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/escrow/ConditionalEscrow.sol) - -pragma solidity ^0.8.0; - -import "./Escrow.sol"; - -/** - * @title ConditionalEscrow - * @dev Base abstract escrow to only allow withdrawal if a condition is met. - * @dev Intended usage: See {Escrow}. Same usage guidelines apply here. - */ -abstract contract ConditionalEscrow is Escrow { - /** - * @dev Returns whether an address is allowed to withdraw their funds. To be - * implemented by derived contracts. - * @param payee The destination address of the funds. - */ - function withdrawalAllowed(address payee) public view virtual returns (bool); - - function withdraw(address payable payee) public virtual override { - require(withdrawalAllowed(payee), "ConditionalEscrow: payee is not allowed to withdraw"); - super.withdraw(payee); - } -} diff --git a/contracts/utils/escrow/Escrow.sol b/contracts/utils/escrow/Escrow.sol deleted file mode 100644 index 48dd51ab7..000000000 --- a/contracts/utils/escrow/Escrow.sol +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.7.0) (utils/escrow/Escrow.sol) - -pragma solidity ^0.8.0; - -import "../../access/Ownable.sol"; -import "../Address.sol"; - -/** - * @title Escrow - * @dev Base escrow contract, holds funds designated for a payee until they - * withdraw them. - * - * Intended usage: This contract (and derived escrow contracts) should be a - * standalone contract, that only interacts with the contract that instantiated - * it. That way, it is guaranteed that all Ether will be handled according to - * the `Escrow` rules, and there is no need to check for payable functions or - * transfers in the inheritance tree. The contract that uses the escrow as its - * payment method should be its owner, and provide public methods redirecting - * to the escrow's deposit and withdraw. - */ -contract Escrow is Ownable { - using Address for address payable; - - event Deposited(address indexed payee, uint256 weiAmount); - event Withdrawn(address indexed payee, uint256 weiAmount); - - mapping(address => uint256) private _deposits; - - function depositsOf(address payee) public view returns (uint256) { - return _deposits[payee]; - } - - /** - * @dev Stores the sent amount as credit to be withdrawn. - * @param payee The destination address of the funds. - * - * Emits a {Deposited} event. - */ - function deposit(address payee) public payable virtual onlyOwner { - uint256 amount = msg.value; - _deposits[payee] += amount; - emit Deposited(payee, amount); - } - - /** - * @dev Withdraw accumulated balance for a payee, forwarding all gas to the - * recipient. - * - * WARNING: Forwarding all gas opens the door to reentrancy vulnerabilities. - * Make sure you trust the recipient, or are either following the - * checks-effects-interactions pattern or using {ReentrancyGuard}. - * - * @param payee The address whose funds will be withdrawn and transferred to. - * - * Emits a {Withdrawn} event. - */ - function withdraw(address payable payee) public virtual onlyOwner { - uint256 payment = _deposits[payee]; - - _deposits[payee] = 0; - - payee.sendValue(payment); - - emit Withdrawn(payee, payment); - } -} diff --git a/contracts/utils/escrow/RefundEscrow.sol b/contracts/utils/escrow/RefundEscrow.sol deleted file mode 100644 index 0e9621fee..000000000 --- a/contracts/utils/escrow/RefundEscrow.sol +++ /dev/null @@ -1,100 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/escrow/RefundEscrow.sol) - -pragma solidity ^0.8.0; - -import "./ConditionalEscrow.sol"; - -/** - * @title RefundEscrow - * @dev Escrow that holds funds for a beneficiary, deposited from multiple - * parties. - * @dev Intended usage: See {Escrow}. Same usage guidelines apply here. - * @dev The owner account (that is, the contract that instantiates this - * contract) may deposit, close the deposit period, and allow for either - * withdrawal by the beneficiary, or refunds to the depositors. All interactions - * with `RefundEscrow` will be made through the owner contract. - */ -contract RefundEscrow is ConditionalEscrow { - using Address for address payable; - - enum State { - Active, - Refunding, - Closed - } - - event RefundsClosed(); - event RefundsEnabled(); - - State private _state; - address payable private immutable _beneficiary; - - /** - * @dev Constructor. - * @param beneficiary_ The beneficiary of the deposits. - */ - constructor(address payable beneficiary_) { - require(beneficiary_ != address(0), "RefundEscrow: beneficiary is the zero address"); - _beneficiary = beneficiary_; - _state = State.Active; - } - - /** - * @return The current state of the escrow. - */ - function state() public view virtual returns (State) { - return _state; - } - - /** - * @return The beneficiary of the escrow. - */ - function beneficiary() public view virtual returns (address payable) { - return _beneficiary; - } - - /** - * @dev Stores funds that may later be refunded. - * @param refundee The address funds will be sent to if a refund occurs. - */ - function deposit(address refundee) public payable virtual override { - require(state() == State.Active, "RefundEscrow: can only deposit while active"); - super.deposit(refundee); - } - - /** - * @dev Allows for the beneficiary to withdraw their funds, rejecting - * further deposits. - */ - function close() public virtual onlyOwner { - require(state() == State.Active, "RefundEscrow: can only close while active"); - _state = State.Closed; - emit RefundsClosed(); - } - - /** - * @dev Allows for refunds to take place, rejecting further deposits. - */ - function enableRefunds() public virtual onlyOwner { - require(state() == State.Active, "RefundEscrow: can only enable refunds while active"); - _state = State.Refunding; - emit RefundsEnabled(); - } - - /** - * @dev Withdraws the beneficiary's funds. - */ - function beneficiaryWithdraw() public virtual { - require(state() == State.Closed, "RefundEscrow: beneficiary can only withdraw while closed"); - beneficiary().sendValue(address(this).balance); - } - - /** - * @dev Returns whether refundees can withdraw their deposits (be refunded). The overridden function receives a - * 'payee' argument, but we ignore it here since the condition is global, not per-payee. - */ - function withdrawalAllowed(address) public view override returns (bool) { - return state() == State.Refunding; - } -} diff --git a/contracts/utils/introspection/ERC165.sol b/contracts/utils/introspection/ERC165.sol index 49395f8a1..111913007 100644 --- a/contracts/utils/introspection/ERC165.sol +++ b/contracts/utils/introspection/ERC165.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC165.sol"; @@ -21,7 +21,7 @@ abstract contract ERC165 is IERC165 { /** * @dev See {IERC165-supportsInterface}. */ - function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { return interfaceId == type(IERC165).interfaceId; } } diff --git a/contracts/utils/introspection/ERC165Checker.sol b/contracts/utils/introspection/ERC165Checker.sol index 40ffd68f7..c27b2f17d 100644 --- a/contracts/utils/introspection/ERC165Checker.sol +++ b/contracts/utils/introspection/ERC165Checker.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/introspection/ERC165Checker.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/introspection/ERC165Checker.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./IERC165.sol"; @@ -101,11 +101,15 @@ library ERC165Checker { * @dev Assumes that account contains a contract that supports ERC165, otherwise * the behavior of this method is undefined. This precondition can be checked * with {supportsERC165}. + * + * Some precompiled contracts will falsely indicate support for a given interface, so caution + * should be exercised when using this function. + * * Interface identification is specified in ERC-165. */ function supportsERC165InterfaceUnchecked(address account, bytes4 interfaceId) internal view returns (bool) { // prepare call - bytes memory encodedParams = abi.encodeWithSelector(IERC165.supportsInterface.selector, interfaceId); + bytes memory encodedParams = abi.encodeCall(IERC165.supportsInterface, (interfaceId)); // perform static call bool success; diff --git a/contracts/utils/introspection/ERC1820Implementer.sol b/contracts/utils/introspection/ERC1820Implementer.sol deleted file mode 100644 index ac5a884c0..000000000 --- a/contracts/utils/introspection/ERC1820Implementer.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC1820Implementer.sol) - -pragma solidity ^0.8.0; - -import "./IERC1820Implementer.sol"; - -/** - * @dev Implementation of the {IERC1820Implementer} interface. - * - * Contracts may inherit from this and call {_registerInterfaceForAddress} to - * declare their willingness to be implementers. - * {IERC1820Registry-setInterfaceImplementer} should then be called for the - * registration to be complete. - */ -contract ERC1820Implementer is IERC1820Implementer { - bytes32 private constant _ERC1820_ACCEPT_MAGIC = keccak256("ERC1820_ACCEPT_MAGIC"); - - mapping(bytes32 => mapping(address => bool)) private _supportedInterfaces; - - /** - * @dev See {IERC1820Implementer-canImplementInterfaceForAddress}. - */ - function canImplementInterfaceForAddress( - bytes32 interfaceHash, - address account - ) public view virtual override returns (bytes32) { - return _supportedInterfaces[interfaceHash][account] ? _ERC1820_ACCEPT_MAGIC : bytes32(0x00); - } - - /** - * @dev Declares the contract as willing to be an implementer of - * `interfaceHash` for `account`. - * - * See {IERC1820Registry-setInterfaceImplementer} and - * {IERC1820Registry-interfaceHash}. - */ - function _registerInterfaceForAddress(bytes32 interfaceHash, address account) internal virtual { - _supportedInterfaces[interfaceHash][account] = true; - } -} diff --git a/contracts/utils/introspection/IERC165.sol b/contracts/utils/introspection/IERC165.sol index e8cdbdbf6..da6f186d4 100644 --- a/contracts/utils/introspection/IERC165.sol +++ b/contracts/utils/introspection/IERC165.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Interface of the ERC165 standard, as defined in the diff --git a/contracts/utils/introspection/IERC1820Implementer.sol b/contracts/utils/introspection/IERC1820Implementer.sol deleted file mode 100644 index c4d0b3028..000000000 --- a/contracts/utils/introspection/IERC1820Implementer.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC1820Implementer.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface for an ERC1820 implementer, as defined in the - * https://eips.ethereum.org/EIPS/eip-1820#interface-implementation-erc1820implementerinterface[EIP]. - * Used by contracts that will be registered as implementers in the - * {IERC1820Registry}. - */ -interface IERC1820Implementer { - /** - * @dev Returns a special value (`ERC1820_ACCEPT_MAGIC`) if this contract - * implements `interfaceHash` for `account`. - * - * See {IERC1820Registry-setInterfaceImplementer}. - */ - function canImplementInterfaceForAddress(bytes32 interfaceHash, address account) external view returns (bytes32); -} diff --git a/contracts/utils/introspection/IERC1820Registry.sol b/contracts/utils/introspection/IERC1820Registry.sol deleted file mode 100644 index a146bc2a6..000000000 --- a/contracts/utils/introspection/IERC1820Registry.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/introspection/IERC1820Registry.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Interface of the global ERC1820 Registry, as defined in the - * https://eips.ethereum.org/EIPS/eip-1820[EIP]. Accounts may register - * implementers for interfaces in this registry, as well as query support. - * - * Implementers may be shared by multiple accounts, and can also implement more - * than a single interface for each account. Contracts can implement interfaces - * for themselves, but externally-owned accounts (EOA) must delegate this to a - * contract. - * - * {IERC165} interfaces can also be queried via the registry. - * - * For an in-depth explanation and source code analysis, see the EIP text. - */ -interface IERC1820Registry { - event InterfaceImplementerSet(address indexed account, bytes32 indexed interfaceHash, address indexed implementer); - - event ManagerChanged(address indexed account, address indexed newManager); - - /** - * @dev Sets `newManager` as the manager for `account`. A manager of an - * account is able to set interface implementers for it. - * - * By default, each account is its own manager. Passing a value of `0x0` in - * `newManager` will reset the manager to this initial state. - * - * Emits a {ManagerChanged} event. - * - * Requirements: - * - * - the caller must be the current manager for `account`. - */ - function setManager(address account, address newManager) external; - - /** - * @dev Returns the manager for `account`. - * - * See {setManager}. - */ - function getManager(address account) external view returns (address); - - /** - * @dev Sets the `implementer` contract as ``account``'s implementer for - * `interfaceHash`. - * - * `account` being the zero address is an alias for the caller's address. - * The zero address can also be used in `implementer` to remove an old one. - * - * See {interfaceHash} to learn how these are created. - * - * Emits an {InterfaceImplementerSet} event. - * - * Requirements: - * - * - the caller must be the current manager for `account`. - * - `interfaceHash` must not be an {IERC165} interface id (i.e. it must not - * end in 28 zeroes). - * - `implementer` must implement {IERC1820Implementer} and return true when - * queried for support, unless `implementer` is the caller. See - * {IERC1820Implementer-canImplementInterfaceForAddress}. - */ - function setInterfaceImplementer(address account, bytes32 _interfaceHash, address implementer) external; - - /** - * @dev Returns the implementer of `interfaceHash` for `account`. If no such - * implementer is registered, returns the zero address. - * - * If `interfaceHash` is an {IERC165} interface id (i.e. it ends with 28 - * zeroes), `account` will be queried for support of it. - * - * `account` being the zero address is an alias for the caller's address. - */ - function getInterfaceImplementer(address account, bytes32 _interfaceHash) external view returns (address); - - /** - * @dev Returns the interface hash for an `interfaceName`, as defined in the - * corresponding - * https://eips.ethereum.org/EIPS/eip-1820#interface-name[section of the EIP]. - */ - function interfaceHash(string calldata interfaceName) external pure returns (bytes32); - - /** - * @notice Updates the cache with whether the contract implements an ERC165 interface or not. - * @param account Address of the contract for which to update the cache. - * @param interfaceId ERC165 interface for which to update the cache. - */ - function updateERC165Cache(address account, bytes4 interfaceId) external; - - /** - * @notice Checks whether a contract implements an ERC165 interface or not. - * If the result is not cached a direct lookup on the contract address is performed. - * If the result is not cached or the cached value is out-of-date, the cache MUST be updated manually by calling - * {updateERC165Cache} with the contract address. - * @param account Address of the contract to check. - * @param interfaceId ERC165 interface to check. - * @return True if `account` implements `interfaceId`, false otherwise. - */ - function implementsERC165Interface(address account, bytes4 interfaceId) external view returns (bool); - - /** - * @notice Checks whether a contract implements an ERC165 interface or not without using or updating the cache. - * @param account Address of the contract to check. - * @param interfaceId ERC165 interface to check. - * @return True if `account` implements `interfaceId`, false otherwise. - */ - function implementsERC165InterfaceNoCache(address account, bytes4 interfaceId) external view returns (bool); -} diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 8400d0669..d372295d7 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -1,18 +1,89 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/math/Math.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/math/Math.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Standard math utilities missing in the Solidity language. */ library Math { + /** + * @dev Muldiv operation overflow. + */ + error MathOverflowedMulDiv(); + enum Rounding { Down, // Toward negative infinity Up, // Toward infinity Zero // Toward zero } + /** + * @dev Returns the addition of two unsigned integers, with an overflow flag. + * + * _Available since v5.0._ + */ + function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + uint256 c = a + b; + if (c < a) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the subtraction of two unsigned integers, with an overflow flag. + * + * _Available since v5.0._ + */ + function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b > a) return (false, 0); + return (true, a - b); + } + } + + /** + * @dev Returns the multiplication of two unsigned integers, with an overflow flag. + * + * _Available since v5.0._ + */ + function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + // Gas optimization: this is cheaper than requiring 'a' not being zero, but the + // benefit is lost if 'b' is also tested. + // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 + if (a == 0) return (true, 0); + uint256 c = a * b; + if (c / a != b) return (false, 0); + return (true, c); + } + } + + /** + * @dev Returns the division of two unsigned integers, with a division by zero flag. + * + * _Available since v5.0._ + */ + function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a / b); + } + } + + /** + * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. + * + * _Available since v5.0._ + */ + function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { + unchecked { + if (b == 0) return (false, 0); + return (true, a % b); + } + } + /** * @dev Returns the largest of two numbers. */ @@ -43,6 +114,11 @@ library Math { * of rounding down. */ function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) { + if (b == 0) { + // Guarantee the same behavior as in a regular Solidity division. + return a / b; + } + // (a + b - 1) / b can overflow on addition, so we distribute. return a == 0 ? 0 : (a - 1) / b + 1; } @@ -67,11 +143,16 @@ library Math { // Handle non-overflow cases, 256 by 256 division. if (prod1 == 0) { + // Solidity will revert if denominator == 0, unlike the div opcode on its own. + // The surrounding unchecked block does not change this fact. + // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic. return prod0 / denominator; } // Make sure the result is less than 2^256. Also prevents denominator == 0. - require(denominator > prod1, "Math: mulDiv overflow"); + if (denominator <= prod1) { + revert MathOverflowedMulDiv(); + } /////////////////////////////////////////////// // 512 by 256 division. diff --git a/contracts/utils/math/SafeCast.sol b/contracts/utils/math/SafeCast.sol index 435a5f945..d3b86b088 100644 --- a/contracts/utils/math/SafeCast.sol +++ b/contracts/utils/math/SafeCast.sol @@ -2,7 +2,7 @@ // OpenZeppelin Contracts (last updated v4.8.0) (utils/math/SafeCast.sol) // This file was procedurally generated from scripts/generate/templates/SafeCast.js. -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow @@ -15,11 +15,28 @@ pragma solidity ^0.8.0; * * Using this library instead of the unchecked operations eliminates an entire * class of bugs, so it's recommended to use it always. - * - * Can be combined with {SafeMath} and {SignedSafeMath} to extend it to smaller types, by performing - * all math on `uint256` and `int256` and then downcasting. */ library SafeCast { + /** + * @dev Value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + + /** + * @dev An int value doesn't fit in an uint of `bits` size. + */ + error SafeCastOverflowedIntToUint(int256 value); + + /** + * @dev Value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + + /** + * @dev An uint value doesn't fit in an int of `bits` size. + */ + error SafeCastOverflowedUintToInt(uint256 value); + /** * @dev Returns the downcasted uint248 from uint256, reverting on * overflow (when the input is greater than largest uint248). @@ -33,7 +50,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint248(uint256 value) internal pure returns (uint248) { - require(value <= type(uint248).max, "SafeCast: value doesn't fit in 248 bits"); + if (value > type(uint248).max) { + revert SafeCastOverflowedUintDowncast(248, value); + } return uint248(value); } @@ -50,7 +69,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint240(uint256 value) internal pure returns (uint240) { - require(value <= type(uint240).max, "SafeCast: value doesn't fit in 240 bits"); + if (value > type(uint240).max) { + revert SafeCastOverflowedUintDowncast(240, value); + } return uint240(value); } @@ -67,7 +88,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint232(uint256 value) internal pure returns (uint232) { - require(value <= type(uint232).max, "SafeCast: value doesn't fit in 232 bits"); + if (value > type(uint232).max) { + revert SafeCastOverflowedUintDowncast(232, value); + } return uint232(value); } @@ -84,7 +107,9 @@ library SafeCast { * _Available since v4.2._ */ function toUint224(uint256 value) internal pure returns (uint224) { - require(value <= type(uint224).max, "SafeCast: value doesn't fit in 224 bits"); + if (value > type(uint224).max) { + revert SafeCastOverflowedUintDowncast(224, value); + } return uint224(value); } @@ -101,7 +126,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint216(uint256 value) internal pure returns (uint216) { - require(value <= type(uint216).max, "SafeCast: value doesn't fit in 216 bits"); + if (value > type(uint216).max) { + revert SafeCastOverflowedUintDowncast(216, value); + } return uint216(value); } @@ -118,7 +145,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint208(uint256 value) internal pure returns (uint208) { - require(value <= type(uint208).max, "SafeCast: value doesn't fit in 208 bits"); + if (value > type(uint208).max) { + revert SafeCastOverflowedUintDowncast(208, value); + } return uint208(value); } @@ -135,7 +164,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint200(uint256 value) internal pure returns (uint200) { - require(value <= type(uint200).max, "SafeCast: value doesn't fit in 200 bits"); + if (value > type(uint200).max) { + revert SafeCastOverflowedUintDowncast(200, value); + } return uint200(value); } @@ -152,7 +183,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint192(uint256 value) internal pure returns (uint192) { - require(value <= type(uint192).max, "SafeCast: value doesn't fit in 192 bits"); + if (value > type(uint192).max) { + revert SafeCastOverflowedUintDowncast(192, value); + } return uint192(value); } @@ -169,7 +202,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint184(uint256 value) internal pure returns (uint184) { - require(value <= type(uint184).max, "SafeCast: value doesn't fit in 184 bits"); + if (value > type(uint184).max) { + revert SafeCastOverflowedUintDowncast(184, value); + } return uint184(value); } @@ -186,7 +221,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint176(uint256 value) internal pure returns (uint176) { - require(value <= type(uint176).max, "SafeCast: value doesn't fit in 176 bits"); + if (value > type(uint176).max) { + revert SafeCastOverflowedUintDowncast(176, value); + } return uint176(value); } @@ -203,7 +240,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint168(uint256 value) internal pure returns (uint168) { - require(value <= type(uint168).max, "SafeCast: value doesn't fit in 168 bits"); + if (value > type(uint168).max) { + revert SafeCastOverflowedUintDowncast(168, value); + } return uint168(value); } @@ -220,7 +259,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint160(uint256 value) internal pure returns (uint160) { - require(value <= type(uint160).max, "SafeCast: value doesn't fit in 160 bits"); + if (value > type(uint160).max) { + revert SafeCastOverflowedUintDowncast(160, value); + } return uint160(value); } @@ -237,7 +278,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint152(uint256 value) internal pure returns (uint152) { - require(value <= type(uint152).max, "SafeCast: value doesn't fit in 152 bits"); + if (value > type(uint152).max) { + revert SafeCastOverflowedUintDowncast(152, value); + } return uint152(value); } @@ -254,7 +297,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint144(uint256 value) internal pure returns (uint144) { - require(value <= type(uint144).max, "SafeCast: value doesn't fit in 144 bits"); + if (value > type(uint144).max) { + revert SafeCastOverflowedUintDowncast(144, value); + } return uint144(value); } @@ -271,7 +316,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint136(uint256 value) internal pure returns (uint136) { - require(value <= type(uint136).max, "SafeCast: value doesn't fit in 136 bits"); + if (value > type(uint136).max) { + revert SafeCastOverflowedUintDowncast(136, value); + } return uint136(value); } @@ -288,7 +335,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint128(uint256 value) internal pure returns (uint128) { - require(value <= type(uint128).max, "SafeCast: value doesn't fit in 128 bits"); + if (value > type(uint128).max) { + revert SafeCastOverflowedUintDowncast(128, value); + } return uint128(value); } @@ -305,7 +354,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint120(uint256 value) internal pure returns (uint120) { - require(value <= type(uint120).max, "SafeCast: value doesn't fit in 120 bits"); + if (value > type(uint120).max) { + revert SafeCastOverflowedUintDowncast(120, value); + } return uint120(value); } @@ -322,7 +373,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint112(uint256 value) internal pure returns (uint112) { - require(value <= type(uint112).max, "SafeCast: value doesn't fit in 112 bits"); + if (value > type(uint112).max) { + revert SafeCastOverflowedUintDowncast(112, value); + } return uint112(value); } @@ -339,7 +392,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint104(uint256 value) internal pure returns (uint104) { - require(value <= type(uint104).max, "SafeCast: value doesn't fit in 104 bits"); + if (value > type(uint104).max) { + revert SafeCastOverflowedUintDowncast(104, value); + } return uint104(value); } @@ -356,7 +411,9 @@ library SafeCast { * _Available since v4.2._ */ function toUint96(uint256 value) internal pure returns (uint96) { - require(value <= type(uint96).max, "SafeCast: value doesn't fit in 96 bits"); + if (value > type(uint96).max) { + revert SafeCastOverflowedUintDowncast(96, value); + } return uint96(value); } @@ -373,7 +430,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint88(uint256 value) internal pure returns (uint88) { - require(value <= type(uint88).max, "SafeCast: value doesn't fit in 88 bits"); + if (value > type(uint88).max) { + revert SafeCastOverflowedUintDowncast(88, value); + } return uint88(value); } @@ -390,7 +449,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint80(uint256 value) internal pure returns (uint80) { - require(value <= type(uint80).max, "SafeCast: value doesn't fit in 80 bits"); + if (value > type(uint80).max) { + revert SafeCastOverflowedUintDowncast(80, value); + } return uint80(value); } @@ -407,7 +468,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint72(uint256 value) internal pure returns (uint72) { - require(value <= type(uint72).max, "SafeCast: value doesn't fit in 72 bits"); + if (value > type(uint72).max) { + revert SafeCastOverflowedUintDowncast(72, value); + } return uint72(value); } @@ -424,7 +487,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint64(uint256 value) internal pure returns (uint64) { - require(value <= type(uint64).max, "SafeCast: value doesn't fit in 64 bits"); + if (value > type(uint64).max) { + revert SafeCastOverflowedUintDowncast(64, value); + } return uint64(value); } @@ -441,7 +506,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint56(uint256 value) internal pure returns (uint56) { - require(value <= type(uint56).max, "SafeCast: value doesn't fit in 56 bits"); + if (value > type(uint56).max) { + revert SafeCastOverflowedUintDowncast(56, value); + } return uint56(value); } @@ -458,7 +525,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint48(uint256 value) internal pure returns (uint48) { - require(value <= type(uint48).max, "SafeCast: value doesn't fit in 48 bits"); + if (value > type(uint48).max) { + revert SafeCastOverflowedUintDowncast(48, value); + } return uint48(value); } @@ -475,7 +544,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint40(uint256 value) internal pure returns (uint40) { - require(value <= type(uint40).max, "SafeCast: value doesn't fit in 40 bits"); + if (value > type(uint40).max) { + revert SafeCastOverflowedUintDowncast(40, value); + } return uint40(value); } @@ -492,7 +563,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint32(uint256 value) internal pure returns (uint32) { - require(value <= type(uint32).max, "SafeCast: value doesn't fit in 32 bits"); + if (value > type(uint32).max) { + revert SafeCastOverflowedUintDowncast(32, value); + } return uint32(value); } @@ -509,7 +582,9 @@ library SafeCast { * _Available since v4.7._ */ function toUint24(uint256 value) internal pure returns (uint24) { - require(value <= type(uint24).max, "SafeCast: value doesn't fit in 24 bits"); + if (value > type(uint24).max) { + revert SafeCastOverflowedUintDowncast(24, value); + } return uint24(value); } @@ -526,7 +601,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint16(uint256 value) internal pure returns (uint16) { - require(value <= type(uint16).max, "SafeCast: value doesn't fit in 16 bits"); + if (value > type(uint16).max) { + revert SafeCastOverflowedUintDowncast(16, value); + } return uint16(value); } @@ -543,7 +620,9 @@ library SafeCast { * _Available since v2.5._ */ function toUint8(uint256 value) internal pure returns (uint8) { - require(value <= type(uint8).max, "SafeCast: value doesn't fit in 8 bits"); + if (value > type(uint8).max) { + revert SafeCastOverflowedUintDowncast(8, value); + } return uint8(value); } @@ -557,7 +636,9 @@ library SafeCast { * _Available since v3.0._ */ function toUint256(int256 value) internal pure returns (uint256) { - require(value >= 0, "SafeCast: value must be positive"); + if (value < 0) { + revert SafeCastOverflowedIntToUint(value); + } return uint256(value); } @@ -576,7 +657,9 @@ library SafeCast { */ function toInt248(int256 value) internal pure returns (int248 downcasted) { downcasted = int248(value); - require(downcasted == value, "SafeCast: value doesn't fit in 248 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(248, value); + } } /** @@ -594,7 +677,9 @@ library SafeCast { */ function toInt240(int256 value) internal pure returns (int240 downcasted) { downcasted = int240(value); - require(downcasted == value, "SafeCast: value doesn't fit in 240 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(240, value); + } } /** @@ -612,7 +697,9 @@ library SafeCast { */ function toInt232(int256 value) internal pure returns (int232 downcasted) { downcasted = int232(value); - require(downcasted == value, "SafeCast: value doesn't fit in 232 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(232, value); + } } /** @@ -630,7 +717,9 @@ library SafeCast { */ function toInt224(int256 value) internal pure returns (int224 downcasted) { downcasted = int224(value); - require(downcasted == value, "SafeCast: value doesn't fit in 224 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(224, value); + } } /** @@ -648,7 +737,9 @@ library SafeCast { */ function toInt216(int256 value) internal pure returns (int216 downcasted) { downcasted = int216(value); - require(downcasted == value, "SafeCast: value doesn't fit in 216 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(216, value); + } } /** @@ -666,7 +757,9 @@ library SafeCast { */ function toInt208(int256 value) internal pure returns (int208 downcasted) { downcasted = int208(value); - require(downcasted == value, "SafeCast: value doesn't fit in 208 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(208, value); + } } /** @@ -684,7 +777,9 @@ library SafeCast { */ function toInt200(int256 value) internal pure returns (int200 downcasted) { downcasted = int200(value); - require(downcasted == value, "SafeCast: value doesn't fit in 200 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(200, value); + } } /** @@ -702,7 +797,9 @@ library SafeCast { */ function toInt192(int256 value) internal pure returns (int192 downcasted) { downcasted = int192(value); - require(downcasted == value, "SafeCast: value doesn't fit in 192 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(192, value); + } } /** @@ -720,7 +817,9 @@ library SafeCast { */ function toInt184(int256 value) internal pure returns (int184 downcasted) { downcasted = int184(value); - require(downcasted == value, "SafeCast: value doesn't fit in 184 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(184, value); + } } /** @@ -738,7 +837,9 @@ library SafeCast { */ function toInt176(int256 value) internal pure returns (int176 downcasted) { downcasted = int176(value); - require(downcasted == value, "SafeCast: value doesn't fit in 176 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(176, value); + } } /** @@ -756,7 +857,9 @@ library SafeCast { */ function toInt168(int256 value) internal pure returns (int168 downcasted) { downcasted = int168(value); - require(downcasted == value, "SafeCast: value doesn't fit in 168 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(168, value); + } } /** @@ -774,7 +877,9 @@ library SafeCast { */ function toInt160(int256 value) internal pure returns (int160 downcasted) { downcasted = int160(value); - require(downcasted == value, "SafeCast: value doesn't fit in 160 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(160, value); + } } /** @@ -792,7 +897,9 @@ library SafeCast { */ function toInt152(int256 value) internal pure returns (int152 downcasted) { downcasted = int152(value); - require(downcasted == value, "SafeCast: value doesn't fit in 152 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(152, value); + } } /** @@ -810,7 +917,9 @@ library SafeCast { */ function toInt144(int256 value) internal pure returns (int144 downcasted) { downcasted = int144(value); - require(downcasted == value, "SafeCast: value doesn't fit in 144 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(144, value); + } } /** @@ -828,7 +937,9 @@ library SafeCast { */ function toInt136(int256 value) internal pure returns (int136 downcasted) { downcasted = int136(value); - require(downcasted == value, "SafeCast: value doesn't fit in 136 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(136, value); + } } /** @@ -846,7 +957,9 @@ library SafeCast { */ function toInt128(int256 value) internal pure returns (int128 downcasted) { downcasted = int128(value); - require(downcasted == value, "SafeCast: value doesn't fit in 128 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(128, value); + } } /** @@ -864,7 +977,9 @@ library SafeCast { */ function toInt120(int256 value) internal pure returns (int120 downcasted) { downcasted = int120(value); - require(downcasted == value, "SafeCast: value doesn't fit in 120 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(120, value); + } } /** @@ -882,7 +997,9 @@ library SafeCast { */ function toInt112(int256 value) internal pure returns (int112 downcasted) { downcasted = int112(value); - require(downcasted == value, "SafeCast: value doesn't fit in 112 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(112, value); + } } /** @@ -900,7 +1017,9 @@ library SafeCast { */ function toInt104(int256 value) internal pure returns (int104 downcasted) { downcasted = int104(value); - require(downcasted == value, "SafeCast: value doesn't fit in 104 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(104, value); + } } /** @@ -918,7 +1037,9 @@ library SafeCast { */ function toInt96(int256 value) internal pure returns (int96 downcasted) { downcasted = int96(value); - require(downcasted == value, "SafeCast: value doesn't fit in 96 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(96, value); + } } /** @@ -936,7 +1057,9 @@ library SafeCast { */ function toInt88(int256 value) internal pure returns (int88 downcasted) { downcasted = int88(value); - require(downcasted == value, "SafeCast: value doesn't fit in 88 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(88, value); + } } /** @@ -954,7 +1077,9 @@ library SafeCast { */ function toInt80(int256 value) internal pure returns (int80 downcasted) { downcasted = int80(value); - require(downcasted == value, "SafeCast: value doesn't fit in 80 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(80, value); + } } /** @@ -972,7 +1097,9 @@ library SafeCast { */ function toInt72(int256 value) internal pure returns (int72 downcasted) { downcasted = int72(value); - require(downcasted == value, "SafeCast: value doesn't fit in 72 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(72, value); + } } /** @@ -990,7 +1117,9 @@ library SafeCast { */ function toInt64(int256 value) internal pure returns (int64 downcasted) { downcasted = int64(value); - require(downcasted == value, "SafeCast: value doesn't fit in 64 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(64, value); + } } /** @@ -1008,7 +1137,9 @@ library SafeCast { */ function toInt56(int256 value) internal pure returns (int56 downcasted) { downcasted = int56(value); - require(downcasted == value, "SafeCast: value doesn't fit in 56 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(56, value); + } } /** @@ -1026,7 +1157,9 @@ library SafeCast { */ function toInt48(int256 value) internal pure returns (int48 downcasted) { downcasted = int48(value); - require(downcasted == value, "SafeCast: value doesn't fit in 48 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(48, value); + } } /** @@ -1044,7 +1177,9 @@ library SafeCast { */ function toInt40(int256 value) internal pure returns (int40 downcasted) { downcasted = int40(value); - require(downcasted == value, "SafeCast: value doesn't fit in 40 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(40, value); + } } /** @@ -1062,7 +1197,9 @@ library SafeCast { */ function toInt32(int256 value) internal pure returns (int32 downcasted) { downcasted = int32(value); - require(downcasted == value, "SafeCast: value doesn't fit in 32 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(32, value); + } } /** @@ -1080,7 +1217,9 @@ library SafeCast { */ function toInt24(int256 value) internal pure returns (int24 downcasted) { downcasted = int24(value); - require(downcasted == value, "SafeCast: value doesn't fit in 24 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(24, value); + } } /** @@ -1098,7 +1237,9 @@ library SafeCast { */ function toInt16(int256 value) internal pure returns (int16 downcasted) { downcasted = int16(value); - require(downcasted == value, "SafeCast: value doesn't fit in 16 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(16, value); + } } /** @@ -1116,7 +1257,9 @@ library SafeCast { */ function toInt8(int256 value) internal pure returns (int8 downcasted) { downcasted = int8(value); - require(downcasted == value, "SafeCast: value doesn't fit in 8 bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(8, value); + } } /** @@ -1130,7 +1273,9 @@ library SafeCast { */ function toInt256(uint256 value) internal pure returns (int256) { // Note: Unsafe cast below is okay because `type(int256).max` is guaranteed to be positive - require(value <= uint256(type(int256).max), "SafeCast: value doesn't fit in an int256"); + if (value > uint256(type(int256).max)) { + revert SafeCastOverflowedUintToInt(value); + } return int256(value); } } diff --git a/contracts/utils/math/SafeMath.sol b/contracts/utils/math/SafeMath.sol deleted file mode 100644 index 2f48fb736..000000000 --- a/contracts/utils/math/SafeMath.sol +++ /dev/null @@ -1,215 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (utils/math/SafeMath.sol) - -pragma solidity ^0.8.0; - -// CAUTION -// This version of SafeMath should only be used with Solidity 0.8 or later, -// because it relies on the compiler's built in overflow checks. - -/** - * @dev Wrappers over Solidity's arithmetic operations. - * - * NOTE: `SafeMath` is generally not needed starting with Solidity 0.8, since the compiler - * now has built in overflow checking. - */ -library SafeMath { - /** - * @dev Returns the addition of two unsigned integers, with an overflow flag. - * - * _Available since v3.4._ - */ - function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - uint256 c = a + b; - if (c < a) return (false, 0); - return (true, c); - } - } - - /** - * @dev Returns the subtraction of two unsigned integers, with an overflow flag. - * - * _Available since v3.4._ - */ - function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - if (b > a) return (false, 0); - return (true, a - b); - } - } - - /** - * @dev Returns the multiplication of two unsigned integers, with an overflow flag. - * - * _Available since v3.4._ - */ - function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - // Gas optimization: this is cheaper than requiring 'a' not being zero, but the - // benefit is lost if 'b' is also tested. - // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 - if (a == 0) return (true, 0); - uint256 c = a * b; - if (c / a != b) return (false, 0); - return (true, c); - } - } - - /** - * @dev Returns the division of two unsigned integers, with a division by zero flag. - * - * _Available since v3.4._ - */ - function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - if (b == 0) return (false, 0); - return (true, a / b); - } - } - - /** - * @dev Returns the remainder of dividing two unsigned integers, with a division by zero flag. - * - * _Available since v3.4._ - */ - function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) { - unchecked { - if (b == 0) return (false, 0); - return (true, a % b); - } - } - - /** - * @dev Returns the addition of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `+` operator. - * - * Requirements: - * - * - Addition cannot overflow. - */ - function add(uint256 a, uint256 b) internal pure returns (uint256) { - return a + b; - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting on - * overflow (when the result is negative). - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b) internal pure returns (uint256) { - return a - b; - } - - /** - * @dev Returns the multiplication of two unsigned integers, reverting on - * overflow. - * - * Counterpart to Solidity's `*` operator. - * - * Requirements: - * - * - Multiplication cannot overflow. - */ - function mul(uint256 a, uint256 b) internal pure returns (uint256) { - return a * b; - } - - /** - * @dev Returns the integer division of two unsigned integers, reverting on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b) internal pure returns (uint256) { - return a / b; - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * reverting when dividing by zero. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b) internal pure returns (uint256) { - return a % b; - } - - /** - * @dev Returns the subtraction of two unsigned integers, reverting with custom message on - * overflow (when the result is negative). - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {trySub}. - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - * - Subtraction cannot overflow. - */ - function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - unchecked { - require(b <= a, errorMessage); - return a - b; - } - } - - /** - * @dev Returns the integer division of two unsigned integers, reverting with custom message on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. Note: this function uses a - * `revert` opcode (which leaves remaining gas untouched) while Solidity - * uses an invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - unchecked { - require(b > 0, errorMessage); - return a / b; - } - } - - /** - * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), - * reverting with custom message when dividing by zero. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryMod}. - * - * Counterpart to Solidity's `%` operator. This function uses a `revert` - * opcode (which leaves remaining gas untouched) while Solidity uses an - * invalid opcode to revert (consuming all remaining gas). - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { - unchecked { - require(b > 0, errorMessage); - return a % b; - } - } -} diff --git a/contracts/utils/math/SignedMath.sol b/contracts/utils/math/SignedMath.sol index 3ea9f8bf0..80413531d 100644 --- a/contracts/utils/math/SignedMath.sol +++ b/contracts/utils/math/SignedMath.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.8.0) (utils/math/SignedMath.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Standard signed math utilities missing in the Solidity language. diff --git a/contracts/utils/math/SignedSafeMath.sol b/contracts/utils/math/SignedSafeMath.sol deleted file mode 100644 index 6704d4ce2..000000000 --- a/contracts/utils/math/SignedSafeMath.sol +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts v4.4.1 (utils/math/SignedSafeMath.sol) - -pragma solidity ^0.8.0; - -/** - * @dev Wrappers over Solidity's arithmetic operations. - * - * NOTE: `SignedSafeMath` is no longer needed starting with Solidity 0.8. The compiler - * now has built in overflow checking. - */ -library SignedSafeMath { - /** - * @dev Returns the multiplication of two signed integers, reverting on - * overflow. - * - * Counterpart to Solidity's `*` operator. - * - * Requirements: - * - * - Multiplication cannot overflow. - */ - function mul(int256 a, int256 b) internal pure returns (int256) { - return a * b; - } - - /** - * @dev Returns the integer division of two signed integers. Reverts on - * division by zero. The result is rounded towards zero. - * - * Counterpart to Solidity's `/` operator. - * - * Requirements: - * - * - The divisor cannot be zero. - */ - function div(int256 a, int256 b) internal pure returns (int256) { - return a / b; - } - - /** - * @dev Returns the subtraction of two signed integers, reverting on - * overflow. - * - * Counterpart to Solidity's `-` operator. - * - * Requirements: - * - * - Subtraction cannot overflow. - */ - function sub(int256 a, int256 b) internal pure returns (int256) { - return a - b; - } - - /** - * @dev Returns the addition of two signed integers, reverting on - * overflow. - * - * Counterpart to Solidity's `+` operator. - * - * Requirements: - * - * - Addition cannot overflow. - */ - function add(int256 a, int256 b) internal pure returns (int256) { - return a + b; - } -} diff --git a/contracts/utils/structs/BitMaps.sol b/contracts/utils/structs/BitMaps.sol index eb67bfab0..9786c7f8e 100644 --- a/contracts/utils/structs/BitMaps.sol +++ b/contracts/utils/structs/BitMaps.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/BitMaps.sol) -pragma solidity ^0.8.0; +// OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/BitMaps.sol) +pragma solidity ^0.8.19; /** * @dev Library for managing uint256 to bool mapping in a compact and efficient way, providing the keys are sequential. diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/structs/Checkpoints.sol similarity index 60% rename from contracts/utils/Checkpoints.sol rename to contracts/utils/structs/Checkpoints.sol index 76203edd2..7608d787e 100644 --- a/contracts/utils/Checkpoints.sol +++ b/contracts/utils/structs/Checkpoints.sol @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/Checkpoints.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/Checkpoints.sol) // This file was procedurally generated from scripts/generate/templates/Checkpoints.js. -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import "./math/Math.sol"; -import "./math/SafeCast.sol"; +import "../math/Math.sol"; +import "../math/SafeCast.sol"; /** * @dev This library defines the `History` struct, for checkpointing values as they change at different points in @@ -17,201 +17,10 @@ import "./math/SafeCast.sol"; * _Available since v4.5._ */ library Checkpoints { - struct History { - Checkpoint[] _checkpoints; - } - - struct Checkpoint { - uint32 _blockNumber; - uint224 _value; - } - /** - * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one - * before it is returned, or zero otherwise. Because the number returned corresponds to that at the end of the - * block, the requested block number must be in the past, excluding the current block. + * @dev A value was attempted to be inserted on a past checkpoint. */ - function getAtBlock(History storage self, uint256 blockNumber) internal view returns (uint256) { - require(blockNumber < block.number, "Checkpoints: block not yet mined"); - uint32 key = SafeCast.toUint32(blockNumber); - - uint256 len = self._checkpoints.length; - uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, len); - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one - * before it is returned, or zero otherwise. Similar to {upperLookup} but optimized for the case when the searched - * checkpoint is probably "recent", defined as being among the last sqrt(N) checkpoints where N is the number of - * checkpoints. - */ - function getAtProbablyRecentBlock(History storage self, uint256 blockNumber) internal view returns (uint256) { - require(blockNumber < block.number, "Checkpoints: block not yet mined"); - uint32 key = SafeCast.toUint32(blockNumber); - - uint256 len = self._checkpoints.length; - - uint256 low = 0; - uint256 high = len; - - if (len > 5) { - uint256 mid = len - Math.sqrt(len); - if (key < _unsafeAccess(self._checkpoints, mid)._blockNumber) { - high = mid; - } else { - low = mid + 1; - } - } - - uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); - - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns checkpoint at given position. - */ - function getAtPosition(History storage self, uint32 pos) internal view returns (Checkpoint memory) { - return self._checkpoints[pos]; - } - - /** - * @dev Pushes a value onto a History so that it is stored as the checkpoint for the current block. - * - * Returns previous value and new value. - */ - function push(History storage self, uint256 value) internal returns (uint256, uint256) { - return _insert(self._checkpoints, SafeCast.toUint32(block.number), SafeCast.toUint224(value)); - } - - /** - * @dev Pushes a value onto a History, by updating the latest value using binary operation `op`. The new value will - * be set to `op(latest, delta)`. - * - * Returns previous value and new value. - */ - function push( - History storage self, - function(uint256, uint256) view returns (uint256) op, - uint256 delta - ) internal returns (uint256, uint256) { - return push(self, op(latest(self), delta)); - } - - /** - * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. - */ - function latest(History storage self) internal view returns (uint224) { - uint256 pos = self._checkpoints.length; - return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; - } - - /** - * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value - * in the most recent checkpoint. - */ - function latestCheckpoint( - History storage self - ) internal view returns (bool exists, uint32 _blockNumber, uint224 _value) { - uint256 pos = self._checkpoints.length; - if (pos == 0) { - return (false, 0, 0); - } else { - Checkpoint memory ckpt = _unsafeAccess(self._checkpoints, pos - 1); - return (true, ckpt._blockNumber, ckpt._value); - } - } - - /** - * @dev Returns the number of checkpoint. - */ - function length(History storage self) internal view returns (uint256) { - return self._checkpoints.length; - } - - /** - * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, - * or by updating the last one. - */ - function _insert(Checkpoint[] storage self, uint32 key, uint224 value) private returns (uint224, uint224) { - uint256 pos = self.length; - - if (pos > 0) { - // Copying to memory is important here. - Checkpoint memory last = _unsafeAccess(self, pos - 1); - - // Checkpoint keys must be non-decreasing. - require(last._blockNumber <= key, "Checkpoint: decreasing keys"); - - // Update or push new checkpoint - if (last._blockNumber == key) { - _unsafeAccess(self, pos - 1)._value = value; - } else { - self.push(Checkpoint({_blockNumber: key, _value: value})); - } - return (last._value, value); - } else { - self.push(Checkpoint({_blockNumber: key, _value: value})); - return (0, value); - } - } - - /** - * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or `high` if there is none. - * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ - function _upperBinaryLookup( - Checkpoint[] storage self, - uint32 key, - uint256 low, - uint256 high - ) private view returns (uint256) { - while (low < high) { - uint256 mid = Math.average(low, high); - if (_unsafeAccess(self, mid)._blockNumber > key) { - high = mid; - } else { - low = mid + 1; - } - } - return high; - } - - /** - * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or `high` if there is none. - * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. - * - * WARNING: `high` should not be greater than the array's length. - */ - function _lowerBinaryLookup( - Checkpoint[] storage self, - uint32 key, - uint256 low, - uint256 high - ) private view returns (uint256) { - while (low < high) { - uint256 mid = Math.average(low, high); - if (_unsafeAccess(self, mid)._blockNumber < key) { - low = mid + 1; - } else { - high = mid; - } - } - return high; - } - - /** - * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds. - */ - function _unsafeAccess(Checkpoint[] storage self, uint256 pos) private pure returns (Checkpoint storage result) { - assembly { - mstore(0, self.slot) - result.slot := add(keccak256(0, 0x20), pos) - } - } + error CheckpointUnorderedInsertion(); struct Trace224 { Checkpoint224[] _checkpoints; @@ -232,7 +41,7 @@ library Checkpoints { } /** - * @dev Returns the value in the oldest checkpoint with key greater or equal than the search key, or zero if there is none. + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if there is none. */ function lowerLookup(Trace224 storage self, uint32 key) internal view returns (uint224) { uint256 len = self._checkpoints.length; @@ -241,7 +50,7 @@ library Checkpoints { } /** - * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. */ function upperLookup(Trace224 storage self, uint32 key) internal view returns (uint224) { uint256 len = self._checkpoints.length; @@ -249,6 +58,31 @@ library Checkpoints { return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; } + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). + */ + function upperLookupRecent(Trace224 storage self, uint32 key) internal view returns (uint224) { + uint256 len = self._checkpoints.length; + + uint256 low = 0; + uint256 high = len; + + if (len > 5) { + uint256 mid = len - Math.sqrt(len); + if (key < _unsafeAccess(self._checkpoints, mid)._key) { + high = mid; + } else { + low = mid + 1; + } + } + + uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); + + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + /** * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. */ @@ -278,6 +112,13 @@ library Checkpoints { return self._checkpoints.length; } + /** + * @dev Returns checkpoint at given position. + */ + function at(Trace224 storage self, uint32 pos) internal view returns (Checkpoint224 memory) { + return self._checkpoints[pos]; + } + /** * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, * or by updating the last one. @@ -290,7 +131,9 @@ library Checkpoints { Checkpoint224 memory last = _unsafeAccess(self, pos - 1); // Checkpoint keys must be non-decreasing. - require(last._key <= key, "Checkpoint: decreasing keys"); + if (last._key > key) { + revert CheckpointUnorderedInsertion(); + } // Update or push new checkpoint if (last._key == key) { @@ -306,7 +149,7 @@ library Checkpoints { } /** - * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or `high` if there is none. + * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` if there is none. * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. * * WARNING: `high` should not be greater than the array's length. @@ -329,7 +172,7 @@ library Checkpoints { } /** - * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or `high` if there is none. + * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or `high` if there is none. * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. * * WARNING: `high` should not be greater than the array's length. @@ -383,7 +226,7 @@ library Checkpoints { } /** - * @dev Returns the value in the oldest checkpoint with key greater or equal than the search key, or zero if there is none. + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if there is none. */ function lowerLookup(Trace160 storage self, uint96 key) internal view returns (uint160) { uint256 len = self._checkpoints.length; @@ -392,7 +235,7 @@ library Checkpoints { } /** - * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. */ function upperLookup(Trace160 storage self, uint96 key) internal view returns (uint160) { uint256 len = self._checkpoints.length; @@ -400,6 +243,31 @@ library Checkpoints { return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; } + /** + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). + */ + function upperLookupRecent(Trace160 storage self, uint96 key) internal view returns (uint160) { + uint256 len = self._checkpoints.length; + + uint256 low = 0; + uint256 high = len; + + if (len > 5) { + uint256 mid = len - Math.sqrt(len); + if (key < _unsafeAccess(self._checkpoints, mid)._key) { + high = mid; + } else { + low = mid + 1; + } + } + + uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high); + + return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value; + } + /** * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. */ @@ -429,6 +297,13 @@ library Checkpoints { return self._checkpoints.length; } + /** + * @dev Returns checkpoint at given position. + */ + function at(Trace160 storage self, uint32 pos) internal view returns (Checkpoint160 memory) { + return self._checkpoints[pos]; + } + /** * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, * or by updating the last one. @@ -441,7 +316,9 @@ library Checkpoints { Checkpoint160 memory last = _unsafeAccess(self, pos - 1); // Checkpoint keys must be non-decreasing. - require(last._key <= key, "Checkpoint: decreasing keys"); + if (last._key > key) { + revert CheckpointUnorderedInsertion(); + } // Update or push new checkpoint if (last._key == key) { @@ -457,7 +334,7 @@ library Checkpoints { } /** - * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or `high` if there is none. + * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high` if there is none. * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. * * WARNING: `high` should not be greater than the array's length. @@ -480,7 +357,7 @@ library Checkpoints { } /** - * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or `high` if there is none. + * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or `high` if there is none. * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`. * * WARNING: `high` should not be greater than the array's length. diff --git a/contracts/utils/structs/DoubleEndedQueue.sol b/contracts/utils/structs/DoubleEndedQueue.sol index a19221b8d..118f9bba9 100644 --- a/contracts/utils/structs/DoubleEndedQueue.sol +++ b/contracts/utils/structs/DoubleEndedQueue.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (utils/structs/DoubleEndedQueue.sol) -pragma solidity ^0.8.4; +// OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/DoubleEndedQueue.sol) +pragma solidity ^0.8.19; import "../math/SafeCast.sol"; @@ -12,7 +12,7 @@ import "../math/SafeCast.sol"; * * The struct is called `Bytes32Deque`. Other types can be cast to and from `bytes32`. This data structure can only be * used in storage, and not in memory. - * ``` + * ```solidity * DoubleEndedQueue.Bytes32Deque queue; * ``` * @@ -22,7 +22,7 @@ library DoubleEndedQueue { /** * @dev An operation (e.g. {front}) couldn't be completed due to the queue being empty. */ - error Empty(); + error QueueEmpty(); /** * @dev A push operation couldn't be completed due to the queue being full. @@ -32,7 +32,7 @@ library DoubleEndedQueue { /** * @dev An operation (e.g. {at}) couldn't be completed due to an index being out of bounds. */ - error OutOfBounds(); + error QueueOutOfBounds(); /** * @dev Indices are signed integers because the queue can grow in any direction. They are 128 bits so begin and end @@ -67,12 +67,12 @@ library DoubleEndedQueue { /** * @dev Removes the item at the end of the queue and returns it. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function popBack(Bytes32Deque storage deque) internal returns (bytes32 value) { unchecked { uint128 backIndex = deque._end; - if (backIndex == deque._begin) revert Empty(); + if (backIndex == deque._begin) revert QueueEmpty(); --backIndex; value = deque._data[backIndex]; delete deque._data[backIndex]; @@ -95,12 +95,12 @@ library DoubleEndedQueue { /** * @dev Removes the item at the beginning of the queue and returns it. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function popFront(Bytes32Deque storage deque) internal returns (bytes32 value) { unchecked { uint128 frontIndex = deque._begin; - if (frontIndex == deque._end) revert Empty(); + if (frontIndex == deque._end) revert QueueEmpty(); value = deque._data[frontIndex]; delete deque._data[frontIndex]; deque._begin = frontIndex + 1; @@ -110,20 +110,20 @@ library DoubleEndedQueue { /** * @dev Returns the item at the beginning of the queue. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function front(Bytes32Deque storage deque) internal view returns (bytes32 value) { - if (empty(deque)) revert Empty(); + if (empty(deque)) revert QueueEmpty(); return deque._data[deque._begin]; } /** * @dev Returns the item at the end of the queue. * - * Reverts with `Empty` if the queue is empty. + * Reverts with `QueueEmpty` if the queue is empty. */ function back(Bytes32Deque storage deque) internal view returns (bytes32 value) { - if (empty(deque)) revert Empty(); + if (empty(deque)) revert QueueEmpty(); unchecked { return deque._data[deque._end - 1]; } @@ -133,10 +133,10 @@ library DoubleEndedQueue { * @dev Return the item at a position in the queue given by `index`, with the first item at 0 and last item at * `length(deque) - 1`. * - * Reverts with `OutOfBounds` if the index is out of bounds. + * Reverts with `QueueOutOfBounds` if the index is out of bounds. */ function at(Bytes32Deque storage deque, uint256 index) internal view returns (bytes32 value) { - if (index >= length(deque)) revert OutOfBounds(); + if (index >= length(deque)) revert QueueOutOfBounds(); unchecked { return deque._data[deque._begin + SafeCast.toUint128(index)]; } diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index 8b188c734..a474e82b3 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/EnumerableMap.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/EnumerableMap.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableMap.js. -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./EnumerableSet.sol"; @@ -17,7 +17,7 @@ import "./EnumerableSet.sol"; * (O(1)). * - Entries are enumerated in O(n). No guarantees are made on the ordering. * - * ``` + * ```solidity * contract Example { * // Add the library methods * using EnumerableMap for EnumerableMap.UintToAddressMap; @@ -57,6 +57,11 @@ library EnumerableMap { // This means that we can only create new EnumerableMaps for types that fit // in bytes32. + /** + * @dev Query for a nonexistent map key. + */ + error EnumerableMapNonexistentKey(bytes32 key); + struct Bytes32ToBytes32Map { // Storage of keys EnumerableSet.Bytes32Set _keys; @@ -136,23 +141,9 @@ library EnumerableMap { */ function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { bytes32 value = map._values[key]; - require(value != 0 || contains(map, key), "EnumerableMap: nonexistent key"); - return value; - } - - /** - * @dev Same as {get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ - function get( - Bytes32ToBytes32Map storage map, - bytes32 key, - string memory errorMessage - ) internal view returns (bytes32) { - bytes32 value = map._values[key]; - require(value != 0 || contains(map, key), errorMessage); + if (value == 0 && !contains(map, key)) { + revert EnumerableMapNonexistentKey(key); + } return value; } @@ -242,16 +233,6 @@ library EnumerableMap { return uint256(get(map._inner, bytes32(key))); } - /** - * @dev Same as {get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ - function get(UintToUintMap storage map, uint256 key, string memory errorMessage) internal view returns (uint256) { - return uint256(get(map._inner, bytes32(key), errorMessage)); - } - /** * @dev Return the an array containing all the keys * @@ -346,20 +327,6 @@ library EnumerableMap { return address(uint160(uint256(get(map._inner, bytes32(key))))); } - /** - * @dev Same as {get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ - function get( - UintToAddressMap storage map, - uint256 key, - string memory errorMessage - ) internal view returns (address) { - return address(uint160(uint256(get(map._inner, bytes32(key), errorMessage)))); - } - /** * @dev Return the an array containing all the keys * @@ -454,20 +421,6 @@ library EnumerableMap { return uint256(get(map._inner, bytes32(uint256(uint160(key))))); } - /** - * @dev Same as {get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ - function get( - AddressToUintMap storage map, - address key, - string memory errorMessage - ) internal view returns (uint256) { - return uint256(get(map._inner, bytes32(uint256(uint160(key))), errorMessage)); - } - /** * @dev Return the an array containing all the keys * @@ -562,20 +515,6 @@ library EnumerableMap { return uint256(get(map._inner, key)); } - /** - * @dev Same as {get}, with a custom error message when `key` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ - function get( - Bytes32ToUintMap storage map, - bytes32 key, - string memory errorMessage - ) internal view returns (uint256) { - return uint256(get(map._inner, key, errorMessage)); - } - /** * @dev Return the an array containing all the keys * diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 4c701c26c..b10b3cdcd 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.8.0) (utils/structs/EnumerableSet.sol) +// OpenZeppelin Contracts (last updated v4.9.0) (utils/structs/EnumerableSet.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Library for managing @@ -15,7 +15,7 @@ pragma solidity ^0.8.0; * (O(1)). * - Elements are enumerated in O(n). No guarantees are made on the ordering. * - * ``` + * ```solidity * contract Example { * // Add the library methods * using EnumerableSet for EnumerableSet.AddressSet; diff --git a/contracts/vendor/amb/IAMB.sol b/contracts/vendor/amb/IAMB.sol deleted file mode 100644 index 73a2bd24b..000000000 --- a/contracts/vendor/amb/IAMB.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (vendor/amb/IAMB.sol) -pragma solidity ^0.8.0; - -interface IAMB { - event UserRequestForAffirmation(bytes32 indexed messageId, bytes encodedData); - event UserRequestForSignature(bytes32 indexed messageId, bytes encodedData); - event AffirmationCompleted( - address indexed sender, - address indexed executor, - bytes32 indexed messageId, - bool status - ); - event RelayedMessage(address indexed sender, address indexed executor, bytes32 indexed messageId, bool status); - - function messageSender() external view returns (address); - - function maxGasPerTx() external view returns (uint256); - - function transactionHash() external view returns (bytes32); - - function messageId() external view returns (bytes32); - - function messageSourceChainId() external view returns (bytes32); - - function messageCallStatus(bytes32 _messageId) external view returns (bool); - - function failedMessageDataHash(bytes32 _messageId) external view returns (bytes32); - - function failedMessageReceiver(bytes32 _messageId) external view returns (address); - - function failedMessageSender(bytes32 _messageId) external view returns (address); - - function requireToPassMessage(address _contract, bytes calldata _data, uint256 _gas) external returns (bytes32); - - function requireToConfirmMessage(address _contract, bytes calldata _data, uint256 _gas) external returns (bytes32); - - function sourceChainId() external view returns (uint256); - - function destinationChainId() external view returns (uint256); -} diff --git a/contracts/vendor/arbitrum/IArbSys.sol b/contracts/vendor/arbitrum/IArbSys.sol deleted file mode 100644 index 9b79d5c16..000000000 --- a/contracts/vendor/arbitrum/IArbSys.sol +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright 2021-2022, Offchain Labs, Inc. -// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE -// SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IArbSys.sol) - -pragma solidity >=0.4.21 <0.9.0; - -/** - * @title System level functionality - * @notice For use by contracts to interact with core L2-specific functionality. - * Precompiled contract that exists in every Arbitrum chain at address(100), 0x0000000000000000000000000000000000000064. - */ -interface IArbSys { - /** - * @notice Get Arbitrum block number (distinct from L1 block number; Arbitrum genesis block has block number 0) - * @return block number as int - */ - function arbBlockNumber() external view returns (uint256); - - /** - * @notice Get Arbitrum block hash (reverts unless currentBlockNum-256 <= arbBlockNum < currentBlockNum) - * @return block hash - */ - function arbBlockHash(uint256 arbBlockNum) external view returns (bytes32); - - /** - * @notice Gets the rollup's unique chain identifier - * @return Chain identifier as int - */ - function arbChainID() external view returns (uint256); - - /** - * @notice Get internal version number identifying an ArbOS build - * @return version number as int - */ - function arbOSVersion() external view returns (uint256); - - /** - * @notice Returns 0 since Nitro has no concept of storage gas - * @return uint 0 - */ - function getStorageGasAvailable() external view returns (uint256); - - /** - * @notice (deprecated) check if current call is top level (meaning it was triggered by an EoA or a L1 contract) - * @dev this call has been deprecated and may be removed in a future release - * @return true if current execution frame is not a call by another L2 contract - */ - function isTopLevelCall() external view returns (bool); - - /** - * @notice map L1 sender contract address to its L2 alias - * @param sender sender address - * @param unused argument no longer used - * @return aliased sender address - */ - function mapL1SenderContractAddressToL2Alias(address sender, address unused) external pure returns (address); - - /** - * @notice check if the caller (of this caller of this) is an aliased L1 contract address - * @return true iff the caller's address is an alias for an L1 contract address - */ - function wasMyCallersAddressAliased() external view returns (bool); - - /** - * @notice return the address of the caller (of this caller of this), without applying L1 contract address aliasing - * @return address of the caller's caller, without applying L1 contract address aliasing - */ - function myCallersAddressWithoutAliasing() external view returns (address); - - /** - * @notice Send given amount of Eth to dest from sender. - * This is a convenience function, which is equivalent to calling sendTxToL1 with empty data. - * @param destination recipient address on L1 - * @return unique identifier for this L2-to-L1 transaction. - */ - function withdrawEth(address destination) external payable returns (uint256); - - /** - * @notice Send a transaction to L1 - * @dev it is not possible to execute on the L1 any L2-to-L1 transaction which contains data - * to a contract address without any code (as enforced by the Bridge contract). - * @param destination recipient address on L1 - * @param data (optional) calldata for L1 contract call - * @return a unique identifier for this L2-to-L1 transaction. - */ - function sendTxToL1(address destination, bytes calldata data) external payable returns (uint256); - - /** - * @notice Get send Merkle tree state - * @return size number of sends in the history - * @return root root hash of the send history - * @return partials hashes of partial subtrees in the send history tree - */ - function sendMerkleTreeState() external view returns (uint256 size, bytes32 root, bytes32[] memory partials); - - /** - * @notice creates a send txn from L2 to L1 - * @param position = (level << 192) + leaf = (0 << 192) + leaf = leaf - */ - event L2ToL1Tx( - address caller, - address indexed destination, - uint256 indexed hash, - uint256 indexed position, - uint256 arbBlockNum, - uint256 ethBlockNum, - uint256 timestamp, - uint256 callvalue, - bytes data - ); - - /// @dev DEPRECATED in favour of the new L2ToL1Tx event above after the nitro upgrade - event L2ToL1Transaction( - address caller, - address indexed destination, - uint256 indexed uniqueId, - uint256 indexed batchNumber, - uint256 indexInBatch, - uint256 arbBlockNum, - uint256 ethBlockNum, - uint256 timestamp, - uint256 callvalue, - bytes data - ); - - /** - * @notice logs a merkle branch for proof synthesis - * @param reserved an index meant only to align the 4th index with L2ToL1Transaction's 4th event - * @param hash the merkle hash - * @param position = (level << 192) + leaf - */ - event SendMerkleUpdate(uint256 indexed reserved, bytes32 indexed hash, uint256 indexed position); -} diff --git a/contracts/vendor/arbitrum/IBridge.sol b/contracts/vendor/arbitrum/IBridge.sol deleted file mode 100644 index e71bedce0..000000000 --- a/contracts/vendor/arbitrum/IBridge.sol +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2021-2022, Offchain Labs, Inc. -// For license information, see https://github.com/nitro/blob/master/LICENSE -// SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IBridge.sol) - -// solhint-disable-next-line compiler-version -pragma solidity >=0.6.9 <0.9.0; - -interface IBridge { - event MessageDelivered( - uint256 indexed messageIndex, - bytes32 indexed beforeInboxAcc, - address inbox, - uint8 kind, - address sender, - bytes32 messageDataHash, - uint256 baseFeeL1, - uint64 timestamp - ); - - event BridgeCallTriggered(address indexed outbox, address indexed to, uint256 value, bytes data); - - event InboxToggle(address indexed inbox, bool enabled); - - event OutboxToggle(address indexed outbox, bool enabled); - - event SequencerInboxUpdated(address newSequencerInbox); - - function allowedDelayedInboxList(uint256) external returns (address); - - function allowedOutboxList(uint256) external returns (address); - - /// @dev Accumulator for delayed inbox messages; tail represents hash of the current state; each element represents the inclusion of a new message. - function delayedInboxAccs(uint256) external view returns (bytes32); - - /// @dev Accumulator for sequencer inbox messages; tail represents hash of the current state; each element represents the inclusion of a new message. - function sequencerInboxAccs(uint256) external view returns (bytes32); - - // OpenZeppelin: changed return type from IOwnable - function rollup() external view returns (address); - - function sequencerInbox() external view returns (address); - - function activeOutbox() external view returns (address); - - function allowedDelayedInboxes(address inbox) external view returns (bool); - - function allowedOutboxes(address outbox) external view returns (bool); - - function sequencerReportedSubMessageCount() external view returns (uint256); - - /** - * @dev Enqueue a message in the delayed inbox accumulator. - * These messages are later sequenced in the SequencerInbox, either - * by the sequencer as part of a normal batch, or by force inclusion. - */ - function enqueueDelayedMessage( - uint8 kind, - address sender, - bytes32 messageDataHash - ) external payable returns (uint256); - - function executeCall( - address to, - uint256 value, - bytes calldata data - ) external returns (bool success, bytes memory returnData); - - function delayedMessageCount() external view returns (uint256); - - function sequencerMessageCount() external view returns (uint256); - - // ---------- onlySequencerInbox functions ---------- - - function enqueueSequencerMessage( - bytes32 dataHash, - uint256 afterDelayedMessagesRead, - uint256 prevMessageCount, - uint256 newMessageCount - ) external returns (uint256 seqMessageIndex, bytes32 beforeAcc, bytes32 delayedAcc, bytes32 acc); - - /** - * @dev Allows the sequencer inbox to submit a delayed message of the batchPostingReport type - * This is done through a separate function entrypoint instead of allowing the sequencer inbox - * to call `enqueueDelayedMessage` to avoid the gas overhead of an extra SLOAD in either - * every delayed inbox or every sequencer inbox call. - */ - function submitBatchSpendingReport(address batchPoster, bytes32 dataHash) external returns (uint256 msgNum); - - // ---------- onlyRollupOrOwner functions ---------- - - function setSequencerInbox(address _sequencerInbox) external; - - function setDelayedInbox(address inbox, bool enabled) external; - - function setOutbox(address inbox, bool enabled) external; - - // ---------- initializer ---------- - - // OpenZeppelin: changed rollup_ type from IOwnable - function initialize(address rollup_) external; -} diff --git a/contracts/vendor/arbitrum/IDelayedMessageProvider.sol b/contracts/vendor/arbitrum/IDelayedMessageProvider.sol deleted file mode 100644 index 914c25fb7..000000000 --- a/contracts/vendor/arbitrum/IDelayedMessageProvider.sol +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2021-2022, Offchain Labs, Inc. -// For license information, see https://github.com/nitro/blob/master/LICENSE -// SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IDelayedMessageProvider.sol) - -// solhint-disable-next-line compiler-version -pragma solidity >=0.6.9 <0.9.0; - -interface IDelayedMessageProvider { - /// @dev event emitted when a inbox message is added to the Bridge's delayed accumulator - event InboxMessageDelivered(uint256 indexed messageNum, bytes data); - - /// @dev event emitted when a inbox message is added to the Bridge's delayed accumulator - /// same as InboxMessageDelivered but the batch data is available in tx.input - event InboxMessageDeliveredFromOrigin(uint256 indexed messageNum); -} diff --git a/contracts/vendor/arbitrum/IInbox.sol b/contracts/vendor/arbitrum/IInbox.sol deleted file mode 100644 index a8b67511c..000000000 --- a/contracts/vendor/arbitrum/IInbox.sol +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright 2021-2022, Offchain Labs, Inc. -// For license information, see https://github.com/nitro/blob/master/LICENSE -// SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IInbox.sol) - -// solhint-disable-next-line compiler-version -pragma solidity >=0.6.9 <0.9.0; - -import "./IBridge.sol"; -import "./IDelayedMessageProvider.sol"; - -interface IInbox is IDelayedMessageProvider { - function bridge() external view returns (IBridge); - - // OpenZeppelin: changed return type from ISequencerInbox - function sequencerInbox() external view returns (address); - - /** - * @notice Send a generic L2 message to the chain - * @dev This method is an optimization to avoid having to emit the entirety of the messageData in a log. Instead validators are expected to be able to parse the data from the transaction's input - * @param messageData Data of the message being sent - */ - function sendL2MessageFromOrigin(bytes calldata messageData) external returns (uint256); - - /** - * @notice Send a generic L2 message to the chain - * @dev This method can be used to send any type of message that doesn't require L1 validation - * @param messageData Data of the message being sent - */ - function sendL2Message(bytes calldata messageData) external returns (uint256); - - function sendL1FundedUnsignedTransaction( - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 nonce, - address to, - bytes calldata data - ) external payable returns (uint256); - - function sendL1FundedContractTransaction( - uint256 gasLimit, - uint256 maxFeePerGas, - address to, - bytes calldata data - ) external payable returns (uint256); - - function sendUnsignedTransaction( - uint256 gasLimit, - uint256 maxFeePerGas, - uint256 nonce, - address to, - uint256 value, - bytes calldata data - ) external returns (uint256); - - function sendContractTransaction( - uint256 gasLimit, - uint256 maxFeePerGas, - address to, - uint256 value, - bytes calldata data - ) external returns (uint256); - - /** - * @notice Get the L1 fee for submitting a retryable - * @dev This fee can be paid by funds already in the L2 aliased address or by the current message value - * @dev This formula may change in the future, to future proof your code query this method instead of inlining!! - * @param dataLength The length of the retryable's calldata, in bytes - * @param baseFee The block basefee when the retryable is included in the chain, if 0 current block.basefee will be used - */ - function calculateRetryableSubmissionFee(uint256 dataLength, uint256 baseFee) external view returns (uint256); - - /** - * @notice Deposit eth from L1 to L2 to address of the sender if sender is an EOA, and to its aliased address if the sender is a contract - * @dev This does not trigger the fallback function when receiving in the L2 side. - * Look into retryable tickets if you are interested in this functionality. - * @dev This function should not be called inside contract constructors - */ - function depositEth() external payable returns (uint256); - - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev all msg.value will deposited to callValueRefundAddress on L2 - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @param to destination L2 contract address - * @param l2CallValue call value for retryable L2 message - * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance - * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled - * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of L2 message - * @return unique message number of the retryable transaction - */ - function createRetryableTicket( - address to, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); - - /** - * @notice Put a message in the L2 inbox that can be reexecuted for some fixed amount of time if it reverts - * @dev Same as createRetryableTicket, but does not guarantee that submission will succeed by requiring the needed funds - * come from the deposit alone, rather than falling back on the user's L2 balance - * @dev Advanced usage only (does not rewrite aliases for excessFeeRefundAddress and callValueRefundAddress). - * createRetryableTicket method is the recommended standard. - * @dev Gas limit and maxFeePerGas should not be set to 1 as that is used to trigger the RetryableData error - * @param to destination L2 contract address - * @param l2CallValue call value for retryable L2 message - * @param maxSubmissionCost Max gas deducted from user's L2 balance to cover base submission fee - * @param excessFeeRefundAddress gasLimit x maxFeePerGas - execution cost gets credited here on L2 balance - * @param callValueRefundAddress l2Callvalue gets credited here on L2 if retryable txn times out or gets cancelled - * @param gasLimit Max gas deducted from user's L2 balance to cover L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param maxFeePerGas price bid for L2 execution. Should not be set to 1 (magic value used to trigger the RetryableData error) - * @param data ABI encoded data of L2 message - * @return unique message number of the retryable transaction - */ - function unsafeCreateRetryableTicket( - address to, - uint256 l2CallValue, - uint256 maxSubmissionCost, - address excessFeeRefundAddress, - address callValueRefundAddress, - uint256 gasLimit, - uint256 maxFeePerGas, - bytes calldata data - ) external payable returns (uint256); - - // ---------- onlyRollupOrOwner functions ---------- - - /// @notice pauses all inbox functionality - function pause() external; - - /// @notice unpauses all inbox functionality - function unpause() external; - - // ---------- initializer ---------- - - /** - * @dev function to be called one time during the inbox upgrade process - * this is used to fix the storage slots - */ - function postUpgradeInit(IBridge _bridge) external; - - // OpenZeppelin: changed _sequencerInbox type from ISequencerInbox - function initialize(IBridge _bridge, address _sequencerInbox) external; -} diff --git a/contracts/vendor/arbitrum/IOutbox.sol b/contracts/vendor/arbitrum/IOutbox.sol deleted file mode 100644 index 22fa58f40..000000000 --- a/contracts/vendor/arbitrum/IOutbox.sol +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2021-2022, Offchain Labs, Inc. -// For license information, see https://github.com/nitro/blob/master/LICENSE -// SPDX-License-Identifier: BUSL-1.1 -// OpenZeppelin Contracts (last updated v4.8.0) (vendor/arbitrum/IOutbox.sol) - -// solhint-disable-next-line compiler-version -pragma solidity >=0.6.9 <0.9.0; - -import "./IBridge.sol"; - -interface IOutbox { - event SendRootUpdated(bytes32 indexed blockHash, bytes32 indexed outputRoot); - event OutBoxTransactionExecuted( - address indexed to, - address indexed l2Sender, - uint256 indexed zero, - uint256 transactionIndex - ); - - function rollup() external view returns (address); // the rollup contract - - function bridge() external view returns (IBridge); // the bridge contract - - function spent(uint256) external view returns (bytes32); // packed spent bitmap - - function roots(bytes32) external view returns (bytes32); // maps root hashes => L2 block hash - - // solhint-disable-next-line func-name-mixedcase - function OUTBOX_VERSION() external view returns (uint128); // the outbox version - - function updateSendRoot(bytes32 sendRoot, bytes32 l2BlockHash) external; - - /// @notice When l2ToL1Sender returns a nonzero address, the message was originated by an L2 account - /// When the return value is zero, that means this is a system message - /// @dev the l2ToL1Sender behaves as the tx.origin, the msg.sender should be validated to protect against reentrancies - function l2ToL1Sender() external view returns (address); - - /// @return l2Block return L2 block when the L2 tx was initiated or 0 if no L2 to L1 transaction is active - function l2ToL1Block() external view returns (uint256); - - /// @return l1Block return L1 block when the L2 tx was initiated or 0 if no L2 to L1 transaction is active - function l2ToL1EthBlock() external view returns (uint256); - - /// @return timestamp return L2 timestamp when the L2 tx was initiated or 0 if no L2 to L1 transaction is active - function l2ToL1Timestamp() external view returns (uint256); - - /// @return outputId returns the unique output identifier of the L2 to L1 tx or 0 if no L2 to L1 transaction is active - function l2ToL1OutputId() external view returns (bytes32); - - /** - * @notice Executes a messages in an Outbox entry. - * @dev Reverts if dispute period hasn't expired, since the outbox entry - * is only created once the rollup confirms the respective assertion. - * @dev it is not possible to execute any L2-to-L1 transaction which contains data - * to a contract address without any code (as enforced by the Bridge contract). - * @param proof Merkle proof of message inclusion in send root - * @param index Merkle path to message - * @param l2Sender sender if original message (i.e., caller of ArbSys.sendTxToL1) - * @param to destination address for L1 contract call - * @param l2Block l2 block number at which sendTxToL1 call was made - * @param l1Block l1 block number at which sendTxToL1 call was made - * @param l2Timestamp l2 Timestamp at which sendTxToL1 call was made - * @param value wei in L1 message - * @param data abi-encoded L1 message data - */ - function executeTransaction( - bytes32[] calldata proof, - uint256 index, - address l2Sender, - address to, - uint256 l2Block, - uint256 l1Block, - uint256 l2Timestamp, - uint256 value, - bytes calldata data - ) external; - - /** - * @dev function used to simulate the result of a particular function call from the outbox - * it is useful for things such as gas estimates. This function includes all costs except for - * proof validation (which can be considered offchain as a somewhat of a fixed cost - it's - * not really a fixed cost, but can be treated as so with a fixed overhead for gas estimation). - * We can't include the cost of proof validation since this is intended to be used to simulate txs - * that are included in yet-to-be confirmed merkle roots. The simulation entrypoint could instead pretend - * to confirm a pending merkle root, but that would be less practical for integrating with tooling. - * It is only possible to trigger it when the msg sender is address zero, which should be impossible - * unless under simulation in an eth_call or eth_estimateGas - */ - function executeTransactionSimulation( - uint256 index, - address l2Sender, - address to, - uint256 l2Block, - uint256 l1Block, - uint256 l2Timestamp, - uint256 value, - bytes calldata data - ) external; - - /** - * @param index Merkle path to message - * @return true if the message has been spent - */ - function isSpent(uint256 index) external view returns (bool); - - function calculateItemHash( - address l2Sender, - address to, - uint256 l2Block, - uint256 l1Block, - uint256 l2Timestamp, - uint256 value, - bytes calldata data - ) external pure returns (bytes32); - - function calculateMerkleRoot(bytes32[] memory proof, uint256 path, bytes32 item) external pure returns (bytes32); -} diff --git a/contracts/vendor/compound/ICompoundTimelock.sol b/contracts/vendor/compound/ICompoundTimelock.sol index fb33a6805..1b04290b4 100644 --- a/contracts/vendor/compound/ICompoundTimelock.sol +++ b/contracts/vendor/compound/ICompoundTimelock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Contracts (last updated v4.6.0) (vendor/compound/ICompoundTimelock.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol[Compound's timelock] interface diff --git a/contracts/vendor/optimism/ICrossDomainMessenger.sol b/contracts/vendor/optimism/ICrossDomainMessenger.sol deleted file mode 100644 index cc01a48ab..000000000 --- a/contracts/vendor/optimism/ICrossDomainMessenger.sol +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (vendor/optimism/ICrossDomainMessenger.sol) -pragma solidity >0.5.0 <0.9.0; - -/** - * @title ICrossDomainMessenger - */ -interface ICrossDomainMessenger { - /********** - * Events * - **********/ - - event SentMessage(address indexed target, address sender, bytes message, uint256 messageNonce, uint256 gasLimit); - event RelayedMessage(bytes32 indexed msgHash); - event FailedRelayedMessage(bytes32 indexed msgHash); - - /************* - * Variables * - *************/ - - function xDomainMessageSender() external view returns (address); - - /******************** - * Public Functions * - ********************/ - - /** - * Sends a cross domain message to the target messenger. - * @param _target Target contract address. - * @param _message Message to send to the target. - * @param _gasLimit Gas limit for the provided message. - */ - function sendMessage(address _target, bytes calldata _message, uint32 _gasLimit) external; -} diff --git a/contracts/vendor/optimism/LICENSE b/contracts/vendor/optimism/LICENSE deleted file mode 100644 index 6a7da5218..000000000 --- a/contracts/vendor/optimism/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -(The MIT License) - -Copyright 2020-2021 Optimism - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/contracts/vendor/polygon/IFxMessageProcessor.sol b/contracts/vendor/polygon/IFxMessageProcessor.sol deleted file mode 100644 index be73e6f53..000000000 --- a/contracts/vendor/polygon/IFxMessageProcessor.sol +++ /dev/null @@ -1,7 +0,0 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (vendor/polygon/IFxMessageProcessor.sol) -pragma solidity ^0.8.0; - -interface IFxMessageProcessor { - function processMessageFromRoot(uint256 stateId, address rootMessageSender, bytes calldata data) external; -} diff --git a/docs/modules/ROOT/images/erc4626-attack-3a.png b/docs/modules/ROOT/images/erc4626-attack-3a.png new file mode 100644 index 000000000..4cb52237d Binary files /dev/null and b/docs/modules/ROOT/images/erc4626-attack-3a.png differ diff --git a/docs/modules/ROOT/images/erc4626-attack-3b.png b/docs/modules/ROOT/images/erc4626-attack-3b.png new file mode 100644 index 000000000..3dc5256b5 Binary files /dev/null and b/docs/modules/ROOT/images/erc4626-attack-3b.png differ diff --git a/docs/modules/ROOT/images/erc4626-attack-6.png b/docs/modules/ROOT/images/erc4626-attack-6.png new file mode 100644 index 000000000..1587fb5c1 Binary files /dev/null and b/docs/modules/ROOT/images/erc4626-attack-6.png differ diff --git a/docs/modules/ROOT/images/erc4626-attack.png b/docs/modules/ROOT/images/erc4626-attack.png new file mode 100644 index 000000000..dc059b228 Binary files /dev/null and b/docs/modules/ROOT/images/erc4626-attack.png differ diff --git a/docs/modules/ROOT/images/erc4626-deposit.png b/docs/modules/ROOT/images/erc4626-deposit.png new file mode 100644 index 000000000..b6c75e679 Binary files /dev/null and b/docs/modules/ROOT/images/erc4626-deposit.png differ diff --git a/docs/modules/ROOT/images/erc4626-mint.png b/docs/modules/ROOT/images/erc4626-mint.png new file mode 100644 index 000000000..f89ab9007 Binary files /dev/null and b/docs/modules/ROOT/images/erc4626-mint.png differ diff --git a/docs/modules/ROOT/images/erc4626-rate-linear.png b/docs/modules/ROOT/images/erc4626-rate-linear.png new file mode 100644 index 000000000..09e8045e1 Binary files /dev/null and b/docs/modules/ROOT/images/erc4626-rate-linear.png differ diff --git a/docs/modules/ROOT/images/erc4626-rate-loglog.png b/docs/modules/ROOT/images/erc4626-rate-loglog.png new file mode 100644 index 000000000..4eb19efea Binary files /dev/null and b/docs/modules/ROOT/images/erc4626-rate-loglog.png differ diff --git a/docs/modules/ROOT/images/erc4626-rate-loglogext.png b/docs/modules/ROOT/images/erc4626-rate-loglogext.png new file mode 100644 index 000000000..127bc7f2f Binary files /dev/null and b/docs/modules/ROOT/images/erc4626-rate-loglogext.png differ diff --git a/docs/modules/ROOT/images/tally-admin.png b/docs/modules/ROOT/images/tally-admin.png deleted file mode 100644 index 826525930..000000000 Binary files a/docs/modules/ROOT/images/tally-admin.png and /dev/null differ diff --git a/docs/modules/ROOT/images/tally-exec.png b/docs/modules/ROOT/images/tally-exec.png new file mode 100644 index 000000000..e24a145d0 Binary files /dev/null and b/docs/modules/ROOT/images/tally-exec.png differ diff --git a/docs/modules/ROOT/images/tally-vote.png b/docs/modules/ROOT/images/tally-vote.png index 76e9b3a98..7d270fce0 100644 Binary files a/docs/modules/ROOT/images/tally-vote.png and b/docs/modules/ROOT/images/tally-vote.png differ diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index a6249d334..fc38e8953 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -11,13 +11,13 @@ ** xref:erc20.adoc[ERC20] *** xref:erc20-supply.adoc[Creating Supply] ** xref:erc721.adoc[ERC721] -** xref:erc777.adoc[ERC777] ** xref:erc1155.adoc[ERC1155] +** xref:erc4626.adoc[ERC4626] * xref:governance.adoc[Governance] -* xref:crosschain.adoc[Crosschain] - * xref:utilities.adoc[Utilities] -* xref:fag.adoc[FAQ] +* xref:subgraphs::index.adoc[Subgraphs] + +* xref:faq.adoc[FAQ] diff --git a/docs/modules/ROOT/pages/access-control.adoc b/docs/modules/ROOT/pages/access-control.adoc index f3ddb6234..11a5fab0c 100644 --- a/docs/modules/ROOT/pages/access-control.adoc +++ b/docs/modules/ROOT/pages/access-control.adoc @@ -13,7 +13,7 @@ OpenZeppelin Contracts provides xref:api:access.adoc#Ownable[`Ownable`] for impl ---- // contracts/MyContract.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/Ownable.sol"; @@ -62,7 +62,7 @@ Here's a simple example of using `AccessControl` in an xref:tokens.adoc#ERC20[`E ---- // contracts/MyToken.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -94,7 +94,7 @@ Let's augment our ERC20 token example by also defining a 'burner' role, which le ---- // contracts/MyToken.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -131,13 +131,15 @@ Every role has an associated admin role, which grants permission to call the `gr This mechanism can be used to create complex permissioning structures resembling organizational charts, but it also provides an easy way to manage simpler applications. `AccessControl` includes a special role, called `DEFAULT_ADMIN_ROLE`, which acts as the **default admin role for all roles**. An account with this role will be able to manage any other role, unless `_setRoleAdmin` is used to select a new admin role. +Since it is the admin for all roles by default, and in fact it is also its own admin, this role carries significant risk. To mitigate this risk we provide xref:api:access.adoc#AccessControlDefaultAdminRules[`AccessControlDefaultAdminRules`], a recommended extension of `AccessControl` that adds a number of enforced security measures for this role: the admin is restricted to a single account, with a 2-step transfer procedure with a delay in between steps. + Let's take a look at the ERC20 token example, this time taking advantage of the default admin role: [source,solidity] ---- // contracts/MyToken.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; @@ -169,7 +171,7 @@ Dynamic role allocation is often a desirable property, for example in systems wh [[querying-privileged-accounts]] === Querying Privileged Accounts -Because accounts might <> dynamically, it is not always possible to determine which accounts hold a particular role. This is important as it allows to prove certain properties about a system, such as that an administrative account is a multisig or a DAO, or that a certain role has been removed from all users, effectively disabling any associated functionality. +Because accounts might <> dynamically, it is not always possible to determine which accounts hold a particular role. This is important as it allows proving certain properties about a system, such as that an administrative account is a multisig or a DAO, or that a certain role has been removed from all users, effectively disabling any associated functionality. Under the hood, `AccessControl` uses `EnumerableSet`, a more powerful variant of Solidity's `mapping` type, which allows for key enumeration. `getRoleMemberCount` can be used to retrieve the number of accounts that have a particular role, and `getRoleMember` can then be called to get the address of each of these accounts. diff --git a/docs/modules/ROOT/pages/crosschain.adoc b/docs/modules/ROOT/pages/crosschain.adoc deleted file mode 100644 index cbe24df77..000000000 --- a/docs/modules/ROOT/pages/crosschain.adoc +++ /dev/null @@ -1,210 +0,0 @@ -= Adding cross-chain support to contracts - -If your contract is targeting to be used in the context of multichain operations, you may need specific tools to identify and process these cross-chain operations. - -OpenZeppelin provides the xref:api:crosschain.adoc#CrossChainEnabled[`CrossChainEnabled`] abstract contract, that includes dedicated internal functions. - -In this guide, we will go through an example use case: _how to build an upgradeable & mintable ERC20 token controlled by a governor present on a foreign chain_. - -== Starting point, our ERC20 contract - -Let's start with a small ERC20 contract, that we bootstrapped using the https://wizard.openzeppelin.com/[OpenZeppelin Contracts Wizard], and extended with an owner that has the ability to mint. Note that for demonstration purposes we have not used the built-in `Ownable` contract. - -[source,solidity] ----- -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - -contract MyToken is Initializable, ERC20Upgradeable, UUPSUpgradeable { - address public owner; - - modifier onlyOwner() { - require(owner == _msgSender(), "Not authorized"); - _; - } - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() initializer {} - - function initialize(address initialOwner) initializer public { - __ERC20_init("MyToken", "MTK"); - __UUPSUpgradeable_init(); - - owner = initialOwner; - } - - function mint(address to, uint256 amount) public onlyOwner { - _mint(to, amount); - } - - function _authorizeUpgrade(address newImplementation) internal override onlyOwner { - } -} ----- - -This token is mintable and upgradeable by the owner of the contract. - -== Preparing our contract for cross-chain operations. - -Let's now imagine that this contract is going to live on one chain, but we want the minting and the upgrading to be performed by a xref:governance.adoc[`governor`] contract on another chain. - -For example, we could have our token on xDai, with our governor on mainnet, or we could have our token on mainnet, with our governor on optimism - -In order to do that, we will start by adding xref:api:crosschain.adoc#CrossChainEnabled[`CrossChainEnabled`] to our contract. You will notice that the contract is now abstract. This is because `CrossChainEnabled` is an abstract contract: it is not tied to any particular chain and it deals with cross-chain interactions in an abstract way. This is what enables us to easily reuse the code for different chains. We will specialize it later by inheriting from a chain-specific implementation of the abstraction. - -```diff - import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -+import "@openzeppelin/contracts-upgradeable/crosschain/CrossChainEnabled.sol"; - --contract MyToken is Initializable, ERC20Upgradeable, UUPSUpgradeable { -+abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, CrossChainEnabled { -``` - -Once that is done, we can use the `onlyCrossChainSender` modifier, provided by `CrossChainEnabled` in order to protect the minting and upgrading operations. - -```diff -- function mint(address to, uint256 amount) public onlyOwner { -+ function mint(address to, uint256 amount) public onlyCrossChainSender(owner) { - -- function _authorizeUpgrade(address newImplementation) internal override onlyOwner { -+ function _authorizeUpgrade(address newImplementation) internal override onlyCrossChainSender(owner) { -``` - -This change will effectively restrict the mint and upgrade operations to the `owner` on the remote chain. - -== Specializing for a specific chain - -Once the abstract cross-chain version of our token is ready we can easily specialize it for the chain we want, or more precisely for the bridge system that we want to rely on. - -This is done using one of the many `CrossChainEnabled` implementations. - -For example, if our token is on xDai, and our governor on mainnet, we can use the https://docs.tokenbridge.net/amb-bridge/about-amb-bridge[AMB] bridge available on xDai at https://blockscout.com/xdai/mainnet/address/0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59[0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59] - -[source,solidity] ----- -[...] - -import "@openzeppelin/contracts-upgradeable/crosschain/amb/CrossChainEnabledAMB.sol"; - -contract MyTokenXDAI is - MyTokenCrossChain, - CrossChainEnabledAMB(0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59) -{} ----- - -If the token is on Ethereum mainnet, and our governor on Optimism, we use the Optimism https://community.optimism.io/docs/protocol/protocol-2.0/#l1crossdomainmessenger[CrossDomainMessenger] available on mainnet at https://etherscan.io/address/0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1[0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1]. - -[source,solidity] ----- -[...] - -import "@openzeppelin/contracts-upgradeable/crosschain/optimismCrossChainEnabledOptimism.sol"; - -contract MyTokenOptimism is - MyTokenCrossChain, - CrossChainEnabledOptimism(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1) -{} ----- - -== Mixing cross domain addresses is dangerous - -When designing a contract with cross-chain support, it is essential to understand possible fallbacks and the security assumption that are being made. - -In this guide, we are particularly focusing on restricting access to a specific caller. This is usually done (as shown above) using `msg.sender` or `_msgSender()`. However, when going cross-chain, it is not just that simple. Even without considering possible bridge issues, it is important to keep in mind that the same address can correspond to very different entities when considering a multi-chain space. EOA wallets can only execute operations if the wallet's private-key signs the transaction. To our knowledge this is the case in all EVM chains, so a cross-chain message coming from such a wallet is arguably equivalent to a non-cross-chain message by the same wallet. The situation is however very different for smart contracts. - -Due to the way smart contract addresses are computed, and the fact that smart contracts on different chains live independent lives, you could have two very different contracts live at the same address on different chains. You could imagine two multisig wallets with different signers using the same address on different chains. You could also see a very basic smart wallet live on one chain at the same address as a full-fledged governor on another chain. Therefore, you should be careful that whenever you give permissions to a specific address, you control with chain this address can act from. - -== Going further with access control - -In the previous example, we have both an `onlyOwner()` modifier and the `onlyCrossChainSender(owner)` mechanism. We didn't use the xref:access-control.adoc#ownership-and-ownable[`Ownable`] pattern because the ownership transfer mechanism in includes is not designed to work with the owner being a cross-chain entity. Unlike xref:access-control.adoc#ownership-and-ownable[`Ownable`], xref:access-control.adoc#role-based-access-control[`AccessControl`] is more effective at capturing the nuances and can effectively be used to build cross-chain-aware contracts. - -Using xref:api:access.adoc#AccessControlCrossChain[`AccessControlCrossChain`] includes both the xref:api:access.adoc#AccessControl[`AccessControl`] core and the xref:api:crosschain.adoc#CrossChainEnabled[`CrossChainEnabled`] abstraction. It also includes some binding to make role management compatible with cross-chain operations. - -In the case of the `mint` function, the caller must have the `MINTER_ROLE` when the call originates from the same chain. If the caller is on a remote chain, then the caller should not have the `MINTER_ROLE`, but the "aliased" version (`MINTER_ROLE ^ CROSSCHAIN_ALIAS`). This mitigates the danger described in the previous section by strictly separating local accounts from remote accounts from a different chain. See the xref:api:access.adoc#AccessControlCrossChain[`AccessControlCrossChain`] documentation for more details. - - -```diff - import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; - import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -+import "@openzeppelin/contracts-upgradeable/access/AccessControlCrossChainUpgradeable.sol"; - --abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, CrossChainEnabled { -+abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, UUPSUpgradeable, AccessControlCrossChainUpgradeable { - -- address public owner; -- modifier onlyOwner() { -- require(owner == _msgSender(), "Not authorized"); -- _; -- } - -+ bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); -+ bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); - - function initialize(address initialOwner) initializer public { - __ERC20_init("MyToken", "MTK"); - __UUPSUpgradeable_init(); -+ __AccessControl_init(); -+ _grantRole(_crossChainRoleAlias(DEFAULT_ADMIN_ROLE), initialOwner); // initialOwner is on a remote chain -- owner = initialOwner; - } - -- function mint(address to, uint256 amount) public onlyCrossChainSender(owner) { -+ function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { - -- function _authorizeUpgrade(address newImplementation) internal override onlyCrossChainSender(owner) { -+ function _authorizeUpgrade(address newImplementation) internal override onlyRole(UPGRADER_ROLE) { -``` - -This results in the following, final, code: - -[source,solidity] ----- -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.4; - -import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/AccessControlCrossChainUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - -abstract contract MyTokenCrossChain is Initializable, ERC20Upgradeable, AccessControlCrossChainUpgradeable, UUPSUpgradeable { - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - bytes32 public constant UPGRADER_ROLE = keccak256("UPGRADER_ROLE"); - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() initializer {} - - function initialize(address initialOwner) initializer public { - __ERC20_init("MyToken", "MTK"); - __AccessControl_init(); - __UUPSUpgradeable_init(); - - _grantRole(_crossChainRoleAlias(DEFAULT_ADMIN_ROLE), initialOwner); // initialOwner is on a remote chain - } - - function mint(address to, uint256 amount) public onlyRole(MINTER_ROLE) { - _mint(to, amount); - } - - function _authorizeUpgrade(address newImplementation) internal onlyRole(UPGRADER_ROLE) override { - } -} - -import "@openzeppelin/contracts-upgradeable/crosschain/amb/CrossChainEnabledAMB.sol"; - -contract MyTokenXDAI is - MyTokenCrossChain, - CrossChainEnabledAMB(0x75Df5AF045d91108662D8080fD1FEFAd6aA0bb59) -{} - -import "@openzeppelin/contracts-upgradeable/crosschain/optimismCrossChainEnabledOptimism.sol"; - -contract MyTokenOptimism is - MyTokenCrossChain, - CrossChainEnabledOptimism(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1) -{} ----- diff --git a/docs/modules/ROOT/pages/erc1155.adoc b/docs/modules/ROOT/pages/erc1155.adoc index a01542c5a..067bde89e 100644 --- a/docs/modules/ROOT/pages/erc1155.adoc +++ b/docs/modules/ROOT/pages/erc1155.adoc @@ -2,7 +2,7 @@ ERC1155 is a novel token standard that aims to take the best from previous standards to create a xref:tokens.adoc#different-kinds-of-tokens[*fungibility-agnostic*] and *gas-efficient* xref:tokens.adoc#but_first_coffee_a_primer_on_token_contracts[token contract]. -TIP: ERC1155 draws ideas from all of xref:erc20.adoc[ERC20], xref:erc721.adoc[ERC721], and xref:erc777.adoc[ERC777]. If you're unfamiliar with those standards, head to their guides before moving on. +TIP: ERC1155 draws ideas from all of xref:erc20.adoc[ERC20], xref:erc721.adoc[ERC721], and https://eips.ethereum.org/EIPS/eip-777[ERC777]. If you're unfamiliar with those standards, head to their guides before moving on. [[multi-token-standard]] == Multi Token Standard @@ -22,9 +22,9 @@ In the spirit of the standard, we've also included batch operations in the non-s == Constructing an ERC1155 Token Contract -We'll use ERC1155 to track multiple items in our game, which will each have their own unique attributes. We mint all items to the deployer of the contract, which we can later transfer to players. Players are free to keep their tokens or trade them with other people as they see fit, as they would any other asset on the blockchain! +We'll use ERC1155 to track multiple items in our game, which will each have their own unique attributes. We mint all items to the deployer of the contract, which we can later transfer to players. Players are free to keep their tokens or trade them with other people as they see fit, as they would any other asset on the blockchain! -For simplicity, we will mint all items in the constructor, but you could add minting functionality to the contract to mint on demand to players. +For simplicity, we will mint all items in the constructor, but you could add minting functionality to the contract to mint on demand to players. TIP: For an overview of minting mechanisms, check out xref:erc20-supply.adoc[Creating ERC20 Supply]. @@ -34,7 +34,7 @@ Here's what a contract for tokenized items might look like: ---- // contracts/GameItems.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; @@ -112,7 +112,7 @@ The JSON document for token ID 2 might look something like: For more information about the metadata JSON Schema, check out the https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md#erc-1155-metadata-uri-json-schema[ERC-1155 Metadata URI JSON Schema]. -NOTE: You'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game! +NOTE: You'll notice that the item's information is included in the metadata, but that information isn't on-chain! So a game developer could change the underlying metadata, changing the rules of the game! TIP: If you'd like to put all item information on-chain, you can extend ERC721 to do so (though it will be rather costly) by providing a xref:utilities.adoc#base64[`Base64`] Data URI with the JSON schema encoded. You could also leverage IPFS to store the URI information, but these techniques are out of the scope of this overview guide @@ -134,7 +134,7 @@ In order for our contract to receive ERC1155 tokens we can inherit from the conv ---- // contracts/MyContract.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; diff --git a/docs/modules/ROOT/pages/erc20.adoc b/docs/modules/ROOT/pages/erc20.adoc index e72491ab6..817d364b1 100644 --- a/docs/modules/ROOT/pages/erc20.adoc +++ b/docs/modules/ROOT/pages/erc20.adoc @@ -15,7 +15,7 @@ Here's what our GLD token might look like. ---- // contracts/GLDToken.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/docs/modules/ROOT/pages/erc4626.adoc b/docs/modules/ROOT/pages/erc4626.adoc new file mode 100644 index 000000000..c8adce736 --- /dev/null +++ b/docs/modules/ROOT/pages/erc4626.adoc @@ -0,0 +1,214 @@ += ERC4626 +:stem: latexmath + +https://eips.ethereum.org/EIPS/eip-4626[ERC4626] is an extension of xref:erc20.adoc[ERC20] that proposes a standard interface for token vaults. This standard interface can be used by widely different contracts (including lending markets, aggregators, and intrinsically interest bearing tokens), which brings a number of subtleties. Navigating these potential issues is essential to implementing a compliant and composable token vault. + +We provide a base implementation of ERC4626 that includes a simple vault. This contract is designed in a way that allows developers to easily re-configure the vault's behavior, with minimal overrides, while staying compliant. In this guide, we will discuss some security considerations that affect ERC4626. We will also discuss common customizations of the vault. + +[[inflation-attack]] +== Security concern: Inflation attack + +=== Visualizing the vault + +In exchange for the assets deposited into an ERC4626 vault, a user receives shares. These shares can later be burned to redeem the corresponding underlying assets. The number of shares a user gets depends on the amount of assets they put in and on the exchange rate of the vault. This exchange rate is defined by the current liquidity held by the vault. + +- If a vault has 100 tokens to back 200 shares, then each share is worth 0.5 assets. +- If a vault has 200 tokens to back 100 shares, then each share is worth 2.0 assets. + +In other words, the exchange rate can be defined as the slope of the line that passes through the origin and the current number of assets and shares in the vault. Deposits and withdrawals move the vault in this line. + +image::erc4626-rate-linear.png[Exchange rates in linear scale] + +When plotted in log-log scale, the rate is defined similarly, but appears differently (because the point (0,0) is infinitely far away). Rates are represented by "diagonal" lines with different offsets. + +image::erc4626-rate-loglog.png[Exchange rates in logarithmic scale] + +In such a reprentation, widely different rates can be clearly visible in the same graph. This wouldn't be the case in linear scale. + +image::erc4626-rate-loglogext.png[More exchange rates in logarithmic scale] + +=== The attack + +When depositing tokens, the number of shares a user gets is rounded down. This rounding takes away value from the user in favor or the vault (i.e. in favor of all the current share holders). This rounding is often negligible because of the amount at stake. If you deposit 1e9 shares worth of tokens, the rounding will have you lose at most 0.0000001% of your deposit. However if you deposit 10 shares worth of tokens, you could lose 10% of your deposit. Even worse, if you deposit <1 share worth of tokens, then you get 0 shares, and you basically made a donation. + +For a given amount of assets, the more shares you receive the safer you are. If you want to limit your losses to at most 1%, you need to receive at least 100 shares. + +image::erc4626-deposit.png[Depositing assets] + +In the figure we can see that for a given deposit of 500 assets, the number of shares we get and the corresponding rounding losses depend on the exchange rate. If the exchange rate is that of the orange curve, we are getting less than a share, so we lose 100% of our deposit. However, if the exchange rate is that of the green curve, we get 5000 shares, which limits our rounding losses to at most 0.02%. + +image::erc4626-mint.png[Minting shares] + +Symmetrically, if we focus on limiting our losses to a maximum of 0.5%, we need to get at least 200 shares. With the green exchange rate that requires just 20 tokens, but with the orange rate that requires 200000 tokens. + +We can clearly see that that the blue and green curves correspond to vaults that are safer than the yellow and orange curves. + +The idea of an inflation attack is that an attacker can donate assets to the vault to move the rate curve to the right, and make the vault unsafe. + +image::erc4626-attack.png[Inflation attack without protection] + +Figure 6 shows how an attacker can manipulate the rate of an empty vault. First the attacker must deposit a small amount of tokens (1 token) and follow up with a donation of 1e5 tokens directly to the vault to move the exchange rate "right". This puts the vault in a state where any deposit smaller than 1e5 would be completely lost to the vault. Given that the attacker is the only share holder (from their donation), the attacker would steal all the tokens deposited. + +An attacker would typically wait for a user to do the first deposit into the vault, and would frontrun that operation with the attack described above. The risk is low, and the size of the "donation" required to manipulate the vault is equivalent to the size of the deposit that is being attacked. + +In math that gives: + +- stem:[a_0] the attacker deposit +- stem:[a_1] the attacker donation +- stem:[u] the user deposit + +[%header,cols=4*] +|=== +| +| Assets +| Shares +| Rate + +| initial +| stem:[0] +| stem:[0] +| - + +| after attacker's deposit +| stem:[a_0] +| stem:[a_0] +| stem:[1] + +| after attacker's donation +| stem:[a_0+a_1] +| stem:[a_0] +| stem:[\frac{a_0}{a_0+a_1}] +|=== + +This means a deposit of stem:[u] will give stem:[\frac{u \times a_0}{a_0 + a_1}] shares. + +For the attacker to dilute that deposit to 0 shares, causing the user to lose all its deposit, it must ensure that + +[stem] +++++ +\frac{u \times a_0}{a_0+a_1} < 1 \iff u < 1 + \frac{a_1}{a_0} +++++ + +Using stem:[a_0 = 1] and stem:[a_1 = u] is enough. So the attacker only needs stem:[u+1] assets to perform a successful attack. + +It is easy to generalize the above results to scenarios where the attacker is going after a smaller fraction of the user's deposit. In order to target stem:[\frac{u}{n}], the user needs to suffer rounding of a similar fraction, which means the user must receive at most stem:[n] shares. This results in: + +[stem] +++++ +\frac{u \times a_0}{a_0+a_1} < n \iff \frac{u}{n} < 1 + \frac{a_1}{a_0} +++++ + +In this scenario, the attack is stem:[n] times less powerful (in how much it is stealing) and costs stem:[n] times less to execute. In both cases, the amount of funds the attacker needs to commit is equivalent to its potential earnings. + +=== Defending with a virtual offset + +The defense we propose is based on the approach used in link:https://github.com/boringcrypto/YieldBox[YieldBox]. It consists of two parts: + +- Use an offset between the "precision" of the representation of shares and assets. Said otherwise, we use more decimal places to represent the shares than the underlying token does to represent the assets. +- Include virtual shares and virtual assets in the exchange rate computation. These virtual assets enforce the conversion rate when the vault is empty. + +These two parts work together in enforcing the security of the vault. First, the increased precision corresponds to a high rate, which we saw is safer as it reduces the rounding error when computing the amount of shares. Second, the virtual assets and shares (in addition to simplifying a lot of the computations) capture part of the donation, making it unprofitable for a developer to perform an attack. + +Following the previous math definitions, we have: + +- stem:[\delta] the vault offset +- stem:[a_0] the attacker deposit +- stem:[a_1] the attacker donation +- stem:[u] the user deposit + +[%header,cols=4*] +|=== +| +| Assets +| Shares +| Rate + +| initial +| stem:[1] +| stem:[10^\delta] +| stem:[10^\delta] + +| after attacker's deposit +| stem:[1+a_0] +| stem:[10^\delta \times (1+a_0)] +| stem:[10^\delta] + +| after attacker's donation +| stem:[1+a_0+a_1] +| stem:[10^\delta \times (1+a_0)] +| stem:[10^\delta \times \frac{1+a_0}{1+a_0+a_1}] +|=== + +One important thing to note is that the attacker only owns a fraction stem:[\frac{a_0}{1 + a_0}] of the shares, so when doing the donation, he will only be able to recover that fraction stem:[\frac{a_1 \times a_0}{1 + a_0}] of the donation. The remaining stem:[\frac{a_1}{1+a_0}] are captured by the vault. + +[stem] +++++ +\mathit{loss} = \frac{a_1}{1+a_0} +++++ + +When the user deposits stem:[u], he receives + +[stem] +++++ +10^\delta \times u \times \frac{1+a_0}{1+a_0+a_1} +++++ + +For the attacker to dilute that deposit to 0 shares, causing the user to lose all its deposit, it must ensure that + +[stem] +++++ +10^\delta \times u \times \frac{1+a_0}{1+a_0+a_1} < 1 +++++ + +[stem] +++++ +\iff 10^\delta \times u < \frac{1+a_0+a_1}{1+a_0} +++++ + +[stem] +++++ +\iff 10^\delta \times u < 1 + \frac{a_1}{1+a_0} +++++ + +[stem] +++++ +\iff 10^\delta \times u \le \mathit{loss} +++++ + +- If the offset is 0, the attacker loss is at least equal to the user's deposit. +- If the offset is greater than 0, the attacker will have to suffer losses that are orders of magnitude bigger than the amount of value that can hypothetically be stolen from the user. + +This shows that even with an offset of 0, the virtual shares and assets make this attack non profitable for the attacker. Bigger offsets increase the security even further by making any attack on the user extremely wasteful. + +The following figure shows how the offset impacts the initial rate and limits the ability of an attacker with limited funds to inflate it effectively. + +image::erc4626-attack-3a.png[Inflation attack without offset=3] +stem:[\delta = 3], stem:[a_0 = 1], stem:[a_1 = 10^5] + +image::erc4626-attack-3b.png[Inflation attack without offset=3 and an attacker deposit that limits its losses] +stem:[\delta = 3], stem:[a_0 = 100], stem:[a_1 = 10^5] + +image::erc4626-attack-6.png[Inflation attack without offset=6] +stem:[\delta = 6], stem:[a_0 = 1], stem:[a_1 = 10^5] + + +[[fees]] +== Custom behavior: Adding fees to the vault + +In an ERC4626 vaults, fees can be captured during the deposit/mint and/or during the withdraw/redeem steps. In both cases it is essential to remain compliant with the ERC4626 requirements with regard to the preview functions. + +For example, if calling `deposit(100, receiver)`, the caller should deposit exactly 100 underlying tokens, including fees, and the receiver should receive a number of shares that matches the value returned by `previewDeposit(100)`. Similarly, `previewMint` should account for the fees that the user will have to pay on top of share's cost. + +As for the `Deposit` event, while this is less clear in the EIP spec itself, there seems to be consensus that it should include the number of assets paid for by the user, including the fees. + +On the other hand, when withdrawing assets, the number given by the user should correspond to what he receives. Any fees should be added to the quote (in shares) performed by `previewWithdraw`. + +The `Withdraw` event should include the number of shares the user burns (including fees) and the number of assets the user actually receives (after fees are deducted). + +The consequence of this design is that both the `Deposit` and `Withdraw` events will describe two exchange rates. The spread between the "Buy-in" and the "Exit" prices correspond to the fees taken by the vault. + +The following example describes how fees proportional to the deposited/withdrawn amount can be implemented: + +```solidity +include::api:example$ERC4626Fees.sol[] +``` diff --git a/docs/modules/ROOT/pages/erc721.adoc b/docs/modules/ROOT/pages/erc721.adoc index 812630ab3..6d839c81e 100644 --- a/docs/modules/ROOT/pages/erc721.adoc +++ b/docs/modules/ROOT/pages/erc721.adoc @@ -14,14 +14,12 @@ Here's what a contract for tokenized items might look like: ---- // contracts/GameItem.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; -import "@openzeppelin/contracts/utils/Counters.sol"; contract GameItem is ERC721URIStorage { - using Counters for Counters.Counter; - Counters.Counter private _tokenIds; + uint256 private _nextTokenId; constructor() ERC721("GameItem", "ITM") {} @@ -29,12 +27,11 @@ contract GameItem is ERC721URIStorage { public returns (uint256) { - uint256 newItemId = _tokenIds.current(); - _mint(player, newItemId); - _setTokenURI(newItemId, tokenURI); + uint256 tokenId = _nextTokenId++; + _mint(player, tokenId); + _setTokenURI(tokenId, tokenURI); - _tokenIds.increment(); - return newItemId; + return tokenId; } } ---- diff --git a/docs/modules/ROOT/pages/erc777.adoc b/docs/modules/ROOT/pages/erc777.adoc deleted file mode 100644 index d79fbee28..000000000 --- a/docs/modules/ROOT/pages/erc777.adoc +++ /dev/null @@ -1,73 +0,0 @@ -= ERC777 - -Like xref:erc20.adoc[ERC20], ERC777 is a standard for xref:tokens.adoc#different-kinds-of-tokens[_fungible_ tokens], and is focused around allowing more complex interactions when trading tokens. More generally, it brings tokens and Ether closer together by providing the equivalent of a `msg.value` field, but for tokens. - -The standard also brings multiple quality-of-life improvements, such as getting rid of the confusion around `decimals`, minting and burning with proper events, among others, but its killer feature is *receive hooks*. A hook is simply a function in a contract that is called when tokens are sent to it, meaning *accounts and contracts can react to receiving tokens*. - -This enables a lot of interesting use cases, including atomic purchases using tokens (no need to do `approve` and `transferFrom` in two separate transactions), rejecting reception of tokens (by reverting on the hook call), redirecting the received tokens to other addresses (similarly to how xref:api:payment#PaymentSplitter[`PaymentSplitter`] does it), among many others. - -Furthermore, since contracts are required to implement these hooks in order to receive tokens, _no tokens can get stuck in a contract that is unaware of the ERC777 protocol_, as has happened countless times when using ERC20s. - -== What If I Already Use ERC20? - -The standard has you covered! The ERC777 standard is *backwards compatible with ERC20*, meaning you can interact with these tokens as if they were ERC20, using the standard functions, while still getting all of the niceties, including send hooks. See the https://eips.ethereum.org/EIPS/eip-777#backward-compatibility[EIP's Backwards Compatibility section] to learn more. - -== Constructing an ERC777 Token Contract - -We will replicate the `GLD` example of the xref:erc20.adoc#constructing-an-erc20-token-contract[ERC20 guide], this time using ERC777. As always, check out the xref:api:token/ERC777.adoc[`API reference`] to learn more about the details of each function. - -[source,solidity] ----- -// contracts/GLDToken.sol -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; - -import "@openzeppelin/contracts/token/ERC777/ERC777.sol"; - -contract GLDToken is ERC777 { - constructor(uint256 initialSupply, address[] memory defaultOperators) - ERC777("Gold", "GLD", defaultOperators) - { - _mint(msg.sender, initialSupply, "", ""); - } -} ----- - -In this case, we'll be extending from the xref:api:token/ERC777.adoc#ERC777[`ERC777`] contract, which provides an implementation with compatibility support for ERC20. The API is quite similar to that of xref:api:token/ERC777.adoc#ERC777[`ERC777`], and we'll once again make use of xref:api:token/ERC777.adoc#ERC777-_mint-address-address-uint256-bytes-bytes-[`_mint`] to assign the `initialSupply` to the deployer account. Unlike xref:api:token/ERC20.adoc#ERC20-_mint-address-uint256-[ERC20's `_mint`], this one includes some extra parameters, but you can safely ignore those for now. - -You'll notice both xref:api:token/ERC777.adoc#IERC777-name--[`name`] and xref:api:token/ERC777.adoc#IERC777-symbol--[`symbol`] are assigned, but not xref:api:token/ERC777.adoc#ERC777-decimals--[`decimals`]. The ERC777 specification makes it mandatory to include support for these functions (unlike ERC20, where it is optional and we had to include xref:api:token/ERC20.adoc#ERC20Detailed[`ERC20Detailed`]), but also mandates that `decimals` always returns a fixed value of `18`, so there's no need to set it ourselves. For a review of ``decimals``'s role and importance, refer back to our xref:erc20.adoc#a-note-on-decimals[ERC20 guide]. - -Finally, we'll need to set the xref:api:token/ERC777.adoc#IERC777-defaultOperators--[`defaultOperators`]: special accounts (usually other smart contracts) that will be able to transfer tokens on behalf of their holders. If you're not planning on using operators in your token, you can simply pass an empty array. _Stay tuned for an upcoming in-depth guide on ERC777 operators!_ - -That's it for a basic token contract! We can now deploy it, and use the same xref:api:token/ERC777.adoc#IERC777-balanceOf-address-[`balanceOf`] method to query the deployer's balance: - -[source,javascript] ----- -> GLDToken.balanceOf(deployerAddress) -1000 ----- - -To move tokens from one account to another, we can use both xref:api:token/ERC777.adoc#ERC777-transfer-address-uint256-[``ERC20``'s `transfer`] method, or the new xref:api:token/ERC777.adoc#ERC777-send-address-uint256-bytes-[``ERC777``'s `send`], which fulfills a very similar role, but adds an optional `data` field: - -[source,javascript] ----- -> GLDToken.transfer(otherAddress, 300) -> GLDToken.send(otherAddress, 300, "") -> GLDToken.balanceOf(otherAddress) -600 -> GLDToken.balanceOf(deployerAddress) -400 ----- - -== Sending Tokens to Contracts - -A key difference when using xref:api:token/ERC777.adoc#ERC777-send-address-uint256-bytes-[`send`] is that token transfers to other contracts may revert with the following message: - -[source,text] ----- -ERC777: token recipient contract has no implementer for ERC777TokensRecipient ----- - -This is a good thing! It means that the recipient contract has not registered itself as aware of the ERC777 protocol, so transfers to it are disabled to *prevent tokens from being locked forever*. As an example, https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d[the Golem contract currently holds over 350k `GNT` tokens], worth multiple tens of thousands of dollars, and lacks methods to get them out of there. This has happened to virtually every ERC20-backed project, usually due to user error. - -_An upcoming guide will cover how a contract can register itself as a recipient, send and receive hooks, and other advanced features of ERC777!_ diff --git a/docs/modules/ROOT/pages/extending-contracts.adoc b/docs/modules/ROOT/pages/extending-contracts.adoc index 7330fc997..df93f2133 100644 --- a/docs/modules/ROOT/pages/extending-contracts.adoc +++ b/docs/modules/ROOT/pages/extending-contracts.adoc @@ -20,7 +20,7 @@ For example, imagine you want to change xref:api:access.adoc#AccessControl[`Acce ```solidity // contracts/ModifiedAccessControl.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; @@ -48,7 +48,7 @@ Here is a modified version of xref:api:access.adoc#AccessControl[`AccessControl` ```solidity // contracts/ModifiedAccessControl.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/access/AccessControl.sol"; @@ -66,7 +66,7 @@ contract ModifiedAccessControl is AccessControl { The `super.revokeRole` statement at the end will invoke ``AccessControl``'s original version of `revokeRole`, the same code that would've run if there were no overrides in place. -NOTE: As of v3.0.0, `view` functions are not `virtual` in OpenZeppelin, and therefore cannot be overridden. We're considering https://github.com/OpenZeppelin/openzeppelin-contracts/issues/2154[lifting this restriction] in an upcoming release. Let us know if this is something you care about! +NOTE: The same rule is implemented and extended in xref:api:access.adoc#AccessControlDefaultAdminRules[`AccessControlDefaultAdminRules`], an extension that also adds enforced security measures for the `DEFAULT_ADMIN_ROLE`. [[using-hooks]] == Using Hooks @@ -80,7 +80,7 @@ Hooks are simply functions that are called before or after some action takes pla Here's how you would implement the `IERC721Receiver` pattern in `ERC20`, using the xref:api:token/ERC20.adoc#ERC20-_beforeTokenTransfer-address-address-uint256-[`_beforeTokenTransfer`] hook: ```solidity -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index aa27c0ba8..3d2bc05f3 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -20,7 +20,9 @@ The ERC20 extension to keep track of votes and vote delegation is one such case. === Governor & GovernorCompatibilityBravo -An OpenZeppelin Governor contract is by default not interface-compatible with GovernorAlpha or Bravo, since some of the functions are different or missing, although it shares all of the same events. However, it’s possible to opt in to full compatibility by inheriting from the GovernorCompatibilityBravo module. The contract will be cheaper to deploy and use without this module. +An OpenZeppelin Governor contract is by default not interface-compatible with Compound's GovernorAlpha or Bravo. Even though events are fully compatible, proposal lifecycle functions (creation, execution, etc.) have different signatures that are meant to optimize storage use. Other functions from GovernorAlpha are Bravo are likewise not available. It’s possible to opt in to a higher level of compatibility by inheriting from the GovernorCompatibilityBravo module, which covers the proposal lifecycle functions such as `propose` and `execute`. + +Note that even with the use of this module, there will still be differences in the way that `proposalId`s are calculated. Governor uses the hash of the proposal parameters with the purpose of keeping its data off-chain by event indexing, while the original Bravo implementation uses sequential `proposalId`s. Due to this and other differences, several of the functions from GovernorBravo are not included in the compatibility module. === GovernorTimelockControl & GovernorTimelockCompound @@ -41,91 +43,24 @@ In the rest of this guide, we will focus on a fresh deploy of the vanilla OpenZe The voting power of each account in our governance setup will be determined by an ERC20 token. The token has to implement the ERC20Votes extension. This extension will keep track of historical balances so that voting power is retrieved from past snapshots rather than current balance, which is an important protection that prevents double voting. ```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; - -contract MyToken is ERC20, ERC20Permit, ERC20Votes { - constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._burn(account, amount); - } -} +include::api:example$governance/MyToken.sol[] ``` If your project already has a live token that does not include ERC20Votes and is not upgradeable, you can wrap it in a governance token by using ERC20Wrapper. This will allow token holders to participate in governance by wrapping their tokens 1-to-1. ```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol"; -import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; - -contract MyToken is ERC20, ERC20Permit, ERC20Votes, ERC20Wrapper { - constructor(IERC20 wrappedToken) - ERC20("MyToken", "MTK") - ERC20Permit("MyToken") - ERC20Wrapper(wrappedToken) - {} - - // The functions below are overrides required by Solidity. - - function _afterTokenTransfer(address from, address to, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._afterTokenTransfer(from, to, amount); - } - - function _mint(address to, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._mint(to, amount); - } - - function _burn(address account, uint256 amount) - internal - override(ERC20, ERC20Votes) - { - super._burn(account, amount); - } -} +include::api:example$governance/MyTokenWrapped.sol[] ``` -NOTE: Voting power could be determined in different ways: multiple ERC20 tokens, ERC721 tokens, sybil resistant identities, etc. All of these options are potentially supported by writing a custom Votes module for your Governor. The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. +NOTE: The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. ERC721 tokens that don't provide this functionality can be wrapped into a voting tokens using a combination of xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`] and xref:api:token/ERC721Wrapper.adoc#ERC721Wrapper[`ERC721Wrapper`]. + +NOTE: The internal clock used by the token to store voting balances will dictate the operating mode of the Governor contract attached to it. By default, block numbers are used. Since v4.9, developers can override the xref:api:interfaces.adoc#IERC6372[IERC6372] clock to use timestamps instead of block numbers. === Governor Initially, we will build a Governor without a timelock. The core logic is given by the Governor contract, but we still need to choose: 1) how voting power is determined, 2) how many votes are needed for quorum, 3) what options people have when casting a vote and how those votes are counted, and 4) what type of token should be used to vote. Each of these aspects is customizable by writing your own module, or more easily choosing one from OpenZeppelin Contracts. -For 1) we will use the GovernorVotes module, which hooks to an IVotes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. +For 1) we will use the GovernorVotes module, which hooks to an IVotes instance to determine the voting power of an account based on the token balance they hold when a proposal becomes active. This module requires as a constructor parameter the address of the token. This module also discovers the clock mode (ERC6372) used by the token and applies it to the Governor. For 2) we will use GovernorVotesQuorumFraction which works together with ERC20Votes to define quorum as a percentage of the total supply at the block a proposal’s voting power is retrieved. This requires a constructor parameter to set the percentage. Most Governors nowadays use 4%, so we will initialize the module with parameter 4 (this indicates the percentage, resulting in 4%). @@ -137,93 +72,12 @@ votingDelay: How long after a proposal is created should voting power be fixed. votingPeriod: How long does a proposal remain open to votes. -These parameters are specified in number of blocks. Assuming block time of around 13.14 seconds, we will set votingDelay = 1 day = 6570 blocks, and votingPeriod = 1 week = 45992 blocks. +These parameters are specified in the unit defined in the token's clock. Assuming the token uses block numbers, and assuming block time of around 12 seconds, we will have set votingDelay = 1 day = 7200 blocks, and votingPeriod = 1 week = 50400 blocks. We can optionally set a proposal threshold as well. This restricts proposal creation to accounts who have enough voting power. ```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.2; - -import "@openzeppelin/contracts/governance/Governor.sol"; -import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol"; -import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; -import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; -import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; - -contract MyGovernor is Governor, GovernorCompatibilityBravo, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl { - constructor(IVotes _token, TimelockController _timelock) - Governor("MyGovernor") - GovernorVotes(_token) - GovernorVotesQuorumFraction(4) - GovernorTimelockControl(_timelock) - {} - - function votingDelay() public pure override returns (uint256) { - return 6575; // 1 day - } - - function votingPeriod() public pure override returns (uint256) { - return 46027; // 1 week - } - - function proposalThreshold() public pure override returns (uint256) { - return 0; - } - - // The functions below are overrides required by Solidity. - - function state(uint256 proposalId) - public - view - override(Governor, IGovernor, GovernorTimelockControl) - returns (ProposalState) - { - return super.state(proposalId); - } - - function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description) - public - override(Governor, GovernorCompatibilityBravo, IGovernor) - returns (uint256) - { - return super.propose(targets, values, calldatas, description); - } - - function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockControl) - { - super._execute(proposalId, targets, values, calldatas, descriptionHash); - } - - function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) - internal - override(Governor, GovernorTimelockControl) - returns (uint256) - { - return super._cancel(targets, values, calldatas, descriptionHash); - } - - function _executor() - internal - view - override(Governor, GovernorTimelockControl) - returns (address) - { - return super._executor(); - } - - function supportsInterface(bytes4 interfaceId) - public - view - override(Governor, IERC165, GovernorTimelockControl) - returns (bool) - { - return super.supportsInterface(interfaceId); - } -} - +include::api:example$governance/MyGovernor.sol[] ``` === Timelock @@ -261,7 +115,7 @@ const grantAmount = ...; const transferCalldata = token.interface.encodeFunctionData(‘transfer’, [teamAddress, grantAmount]); ``` -Now we are ready to call the propose function of the governor. Note that we don’t pass in one array of actions, but instead three arrays corresponding to the list of targets, the list of values, and the list of calldatas. In this case it’s a single action, so it’s simple: +Now we are ready to call the propose function of the Governor. Note that we don’t pass in one array of actions, but instead three arrays corresponding to the list of targets, the list of values, and the list of calldatas. In this case it’s a single action, so it’s simple: ```javascript await governor.propose( @@ -284,9 +138,9 @@ image::tally-vote.png[Voting in Tally] === Execute the Proposal -Once the voting period is over, if quorum was reached (enough voting power participated) and the majority voted in favor, the proposal is considered successful and can proceed to be executed. This can also be done in Tally in the "Administration Panel" section of a project. +Once the voting period is over, if quorum was reached (enough voting power participated) and the majority voted in favor, the proposal is considered successful and can proceed to be executed. Once a proposal passes, it can be queued and executed from the same place you voted. -image::tally-admin.png[Administration Panel in Tally] +image::tally-exec.png[Administration Panel in Tally] We will see now how to do this manually using Ethers.js. @@ -305,7 +159,7 @@ await governor.queue( ); ``` -This will cause the governor to interact with the timelock contract and queue the actions for execution after the required delay. +This will cause the Governor to interact with the timelock contract and queue the actions for execution after the required delay. After enough time has passed (according to the timelock parameters), the proposal can be executed. If there was no timelock to begin with, this step can be ran immediately after the proposal succeeds. @@ -319,3 +173,65 @@ await governor.execute( ``` Executing the proposal will transfer the ERC20 tokens to the chosen recipient. To wrap up: we set up a system where a treasury is controlled by the collective decision of the token holders of a project, and all actions are executed via proposals enforced by on-chain votes. + +== Timestamp based governance + +=== Motivation + +It is sometimes difficult to deal with durations expressed in number of blocks because of inconsistent or unpredictable time between blocks. This is particularly true of some L2 networks where blocks are produced based on blockchain usage. Using number of blocks can also lead to the governance rules being affected by network upgrades that modify the expected time between blocks. + +The difficulty of replacing block numbers with timestamps is that the Governor and the token must both use the same format when querying past votes. If a token is designed around block numbers, it is not possible for a Governor to reliably do timestamp based lookups. + +Therefore, designing a timestamp based voting system starts with the token. + +=== Token + +Since v4.9, all voting contracts (including xref:api:token/ERC20.adoc#ERC20Votes[`ERC20Votes`] and xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]) rely on xref:api:interfaces.adoc#IERC6372[IERC6372] for clock management. In order to change from operating with block numbers to operating with timestamps, all that is required is to override the `clock()` and `CLOCK_MODE()` functions. + +```solidity +include::api:example$governance/MyTokenTimestampBased.sol[] +``` + +=== Governor + +The Governor will automatically detect the clock mode used by the token and adapt to it. There is no need to override anything in the Governor contract. However, the clock mode does affect how some values are interpreted. It is therefore necessary to set the `votingDelay()` and `votingPeriod()` accordingly. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/governance/Governor.sol"; +import "@openzeppelin/contracts/governance/compatibility/GovernorCompatibilityBravo.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol"; +import "@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol"; + +contract MyGovernor is Governor, GovernorCompatibilityBravo, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl { + constructor(IVotes _token, TimelockController _timelock) + Governor("MyGovernor") + GovernorVotes(_token) + GovernorVotesQuorumFraction(4) + GovernorTimelockControl(_timelock) + {} + + function votingDelay() public pure virtual override returns (uint256) { + return 1 days; + } + + function votingPeriod() public pure virtual override returns (uint256) { + return 1 weeks; + } + + function proposalThreshold() public pure virtual override returns (uint256) { + return 0; + } + + // ... +} +``` + +=== Disclaimer + +Timestamp based voting is a recent feature that was formalized in EIP-6372 and EIP-5805, and introduced in v4.9. At the time this feature is released, governance tooling such as https://www.tally.xyz[Tally] does not support it yet. While support for timestamps should come soon, users can expect invalid reporting of deadlines & durations. This invalid reporting by offchain tools does not affect the onchain security and functionality of the governance contract. + +Governors with timestamp support (v4.9 and above) are compatible with old tokens (before v4.9) and will operate in "block number" mode (which is the mode all old tokens operate on). On the other hand, old Governor instances (before v4.9) are not compatible with new tokens operating using timestamps. If you update your token code to use timestamps, make sure to also update your Governor code. diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 5b64f0508..b46a8d5a3 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -26,7 +26,7 @@ Once installed, you can use the contracts in the library by importing them: ---- // contracts/MyNFT.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; @@ -45,6 +45,8 @@ To keep your system secure, you should **always** use the installed code as-is, Please report any security issues you find via our https://www.immunefi.com/bounty/openzeppelin[bug bounty program on Immunefi] or directly to security@openzeppelin.org. +The https://contracts.openzeppelin.com/security[Security Center] contains more details about the secure development process. + [[next-steps]] == Learn More diff --git a/docs/modules/ROOT/pages/tokens.adoc b/docs/modules/ROOT/pages/tokens.adoc index b168756df..10626f548 100644 --- a/docs/modules/ROOT/pages/tokens.adoc +++ b/docs/modules/ROOT/pages/tokens.adoc @@ -28,5 +28,4 @@ You've probably heard of the ERC20 or ERC721 token standards, and that's why you * xref:erc20.adoc[ERC20]: the most widespread token standard for fungible assets, albeit somewhat limited by its simplicity. * xref:erc721.adoc[ERC721]: the de-facto solution for non-fungible tokens, often used for collectibles and games. - * xref:erc777.adoc[ERC777]: a richer standard for fungible tokens, enabling new use cases and building on past learnings. Backwards compatible with ERC20. * xref:erc1155.adoc[ERC1155]: a novel standard for multi-tokens, allowing for a single contract to represent multiple fungible and non-fungible tokens, along with batched operations for increased gas efficiency. diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 88207f0e1..0b178d9fe 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -87,9 +87,7 @@ Easy! Want to split some payments between multiple people? Maybe you have an app that sends 30% of art purchases to the original creator and 70% of the profits to the current owner; you can build that with xref:api:finance.adoc#PaymentSplitter[`PaymentSplitter`]! -In Solidity, there are some security concerns with blindly sending money to accounts, since it allows them to execute arbitrary code. You can read up on these security concerns in the https://consensys.github.io/smart-contract-best-practices/[Ethereum Smart Contract Best Practices] website. One of the ways to fix reentrancy and stalling problems is, instead of immediately sending Ether to accounts that need it, you can use xref:api:security.adoc#PullPayment[`PullPayment`], which offers an xref:api:security.adoc#PullPayment-_asyncTransfer-address-uint256-[`_asyncTransfer`] function for sending money to something and requesting that they xref:api:security.adoc#PullPayment-withdrawPayments-address-payable-[`withdrawPayments()`] it later. - -If you want to Escrow some funds, check out xref:api:utils.adoc#Escrow[`Escrow`] and xref:api:utils.adoc#ConditionalEscrow[`ConditionalEscrow`] for governing the release of some escrowed Ether. +In Solidity, there are some security concerns with blindly sending money to accounts, since it allows them to execute arbitrary code. You can read up on these security concerns in the https://consensys.github.io/smart-contract-best-practices/[Ethereum Smart Contract Best Practices] website. [[collections]] == Collections @@ -99,11 +97,9 @@ If you need support for more powerful collections than Solidity's native arrays [[misc]] == Misc -Want to keep track of some numbers that increment by 1 every time you want another one? Check out xref:api:utils.adoc#Counters[`Counters`]. This is useful for lots of things, like creating incremental identifiers, as shown on the xref:erc721.adoc[ERC721 guide]. - === Base64 -xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data into its Base64 `string` representation. +xref:api:utils.adoc#Base64[`Base64`] util allows you to transform `bytes32` data into its Base64 `string` representation. This is especially useful for building URL-safe tokenURIs for both xref:api:token/ERC721.adoc#IERC721Metadata-tokenURI-uint256-[`ERC721`] or xref:api:token/ERC1155.adoc#IERC1155MetadataURI-uri-uint256-[`ERC1155`]. This library provides a clever way to serve URL-safe https://developer.mozilla.org/docs/Web/HTTP/Basics_of_HTTP/Data_URIs/[Data URI] compliant strings to serve on-chain data structures. @@ -122,7 +118,7 @@ contract My721Token is ERC721 { using Strings for uint256; constructor() ERC721("My721Token", "MTK") {} - + ... function tokenURI(uint256 tokenId) @@ -140,7 +136,7 @@ contract My721Token is ERC721 { return string( abi.encodePacked( - "data:application/json;base64,", + "data:application/json;base64,", Base64.encode(dataURI) ) ); @@ -158,7 +154,7 @@ Consider this dummy contract: ---- // contracts/Box.sol // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "@openzeppelin/contracts/utils/Multicall.sol"; diff --git a/foundry.toml b/foundry.toml index c0da48773..d0aa4ea39 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,3 +1,3 @@ [fuzz] runs = 10000 -max_test_rejects = 100000 +max_test_rejects = 150000 diff --git a/hardhat.config.js b/hardhat.config.js index 74f40c4d1..6cb8b9144 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -40,7 +40,7 @@ const argv = require('yargs/yargs')() compiler: { alias: 'compileVersion', type: 'string', - default: '0.8.13', + default: '0.8.20', }, coinmarketcap: { alias: 'coinmarketcapApiKey', @@ -72,9 +72,13 @@ module.exports = { runs: 200, }, viaIR: withOptimizations && argv.ir, + outputSelection: { '*': { '*': ['storageLayout'] } }, }, }, warnings: { + 'contracts-exposed/**/*': { + 'code-size': 'off', + }, '*': { 'code-size': withOptimizations, 'unused-param': !argv.coverage, // coverage causes unused-param warnings @@ -88,11 +92,8 @@ module.exports = { }, }, exposed: { - exclude: [ - 'vendor/**/*', - // overflow clash - 'utils/Timers.sol', - ], + initializers: true, + exclude: ['vendor/**/*'], }, docgen: require('./docs/config'), }; diff --git a/hardhat/env-artifacts.js b/hardhat/env-artifacts.js new file mode 100644 index 000000000..fbbea2e2d --- /dev/null +++ b/hardhat/env-artifacts.js @@ -0,0 +1,24 @@ +const { HardhatError } = require('hardhat/internal/core/errors'); + +// Modifies `artifacts.require(X)` so that instead of X it loads the XUpgradeable contract. +// This allows us to run the same test suite on both the original and the transpiled and renamed Upgradeable contracts. + +extendEnvironment(env => { + const artifactsRequire = env.artifacts.require; + + env.artifacts.require = name => { + for (const suffix of ['UpgradeableWithInit', 'Upgradeable', '']) { + try { + return artifactsRequire(name + suffix); + } catch (e) { + // HH700: Artifact not found - from https://hardhat.org/hardhat-runner/docs/errors#HH700 + if (HardhatError.isHardhatError(e) && e.number === 700 && suffix !== '') { + continue; + } else { + throw e; + } + } + } + throw new Error('Unreachable'); + }; +}); diff --git a/hardhat/task-test-get-files.js b/hardhat/task-test-get-files.js new file mode 100644 index 000000000..108f40a42 --- /dev/null +++ b/hardhat/task-test-get-files.js @@ -0,0 +1,25 @@ +const { internalTask } = require('hardhat/config'); +const { TASK_TEST_GET_TEST_FILES } = require('hardhat/builtin-tasks/task-names'); + +// Modifies `hardhat test` to skip the proxy tests after proxies are removed by the transpiler for upgradeability. + +internalTask(TASK_TEST_GET_TEST_FILES).setAction(async (args, hre, runSuper) => { + const path = require('path'); + const { promises: fs } = require('fs'); + + const hasProxies = await fs + .access(path.join(hre.config.paths.sources, 'proxy/Proxy.sol')) + .then(() => true) + .catch(() => false); + + const ignoredIfProxy = [ + 'proxy/beacon/BeaconProxy.test.js', + 'proxy/beacon/UpgradeableBeacon.test.js', + 'proxy/ERC1967/ERC1967Proxy.test.js', + 'proxy/transparent/ProxyAdmin.test.js', + 'proxy/transparent/TransparentUpgradeableProxy.test.js', + 'proxy/utils/UUPSUpgradeable.test.js', + ].map(p => path.join(hre.config.paths.tests, p)); + + return (await runSuper(args)).filter(file => hasProxies || !ignoredIfProxy.includes(file)); +}); diff --git a/package-lock.json b/package-lock.json index 4131a8deb..d4cb52694 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openzeppelin-solidity", - "version": "4.8.0", + "version": "4.9.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "openzeppelin-solidity", - "version": "4.8.0", + "version": "4.9.2", "license": "MIT", "devDependencies": { "@changesets/changelog-github": "^0.4.8", @@ -18,6 +18,7 @@ "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.3", "@openzeppelin/test-helpers": "^0.5.13", + "@openzeppelin/upgrades-core": "^1.20.6", "array.prototype.at": "^1.1.1", "chai": "^4.2.0", "eslint": "^8.30.0", @@ -28,7 +29,7 @@ "glob": "^8.0.3", "graphlib": "^2.1.8", "hardhat": "^2.9.1", - "hardhat-exposed": "^0.3.0", + "hardhat-exposed": "^0.3.3", "hardhat-gas-reporter": "^1.0.4", "hardhat-ignore-warnings": "^0.2.0", "keccak256": "^1.0.2", @@ -36,6 +37,7 @@ "lodash.zip": "^4.2.0", "merkletreejs": "^0.2.13", "micromatch": "^4.0.2", + "p-limit": "^3.1.0", "prettier": "^2.8.1", "prettier-plugin-solidity": "^1.1.0", "rimraf": "^3.0.2", @@ -44,14 +46,15 @@ "solidity-ast": "^0.4.25", "solidity-coverage": "^0.8.0", "solidity-docgen": "^0.6.0-beta.29", + "undici": "^5.22.1", "web3": "^1.3.0", "yargs": "^17.0.0" } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", "dev": true, "dependencies": { "@babel/highlight": "^7.18.6" @@ -83,81 +86,10 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/runtime": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", - "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz", + "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==", "dev": true, "dependencies": { "regenerator-runtime": "^0.13.11" @@ -166,6 +98,32 @@ "node": ">=6.9.0" } }, + "node_modules/@chainsafe/as-sha256": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz", + "integrity": "sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg==", + "dev": true + }, + "node_modules/@chainsafe/persistent-merkle-tree": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz", + "integrity": "sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ==", + "dev": true, + "dependencies": { + "@chainsafe/as-sha256": "^0.3.1" + } + }, + "node_modules/@chainsafe/ssz": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.9.4.tgz", + "integrity": "sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ==", + "dev": true, + "dependencies": { + "@chainsafe/as-sha256": "^0.3.1", + "@chainsafe/persistent-merkle-tree": "^0.4.2", + "case": "^1.6.3" + } + }, "node_modules/@changesets/apply-release-plan": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-6.1.3.tgz", @@ -187,24 +145,6 @@ "semver": "^5.4.1" } }, - "node_modules/@changesets/apply-release-plan/node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@changesets/apply-release-plan/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/@changesets/apply-release-plan/node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -258,9 +198,9 @@ } }, "node_modules/@changesets/cli": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.0.tgz", - "integrity": "sha512-0cbTiDms+ICTVtEwAFLNW0jBNex9f5+fFv3I771nBvdnV/mOjd1QJ4+f8KtVSOrwD9SJkk9xbDkWFb0oXd8d1Q==", + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.1.tgz", + "integrity": "sha512-XnTa+b51vt057fyAudvDKGB0Sh72xutQZNAdXkCqPBKO2zvs2yYZx5hFZj1u9cbtpwM6Sxtcr02/FQJfZOzemQ==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", @@ -301,81 +241,19 @@ "changeset": "bin.js" } }, - "node_modules/@changesets/cli/node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "node_modules/@changesets/cli/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, "engines": { "node": ">=6" - } - }, - "node_modules/@changesets/cli/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/cli/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/cli/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@changesets/cli/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@changesets/cli/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@changesets/cli/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/cli/node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@changesets/cli/node_modules/semver": { @@ -387,18 +265,6 @@ "semver": "bin/semver" } }, - "node_modules/@changesets/cli/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@changesets/config": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@changesets/config/-/config-2.3.0.tgz", @@ -436,65 +302,6 @@ "semver": "^5.4.1" } }, - "node_modules/@changesets/get-dependents-graph/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/get-dependents-graph/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/get-dependents-graph/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@changesets/get-dependents-graph/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@changesets/get-dependents-graph/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@changesets/get-dependents-graph/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@changesets/get-dependents-graph/node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -504,18 +311,6 @@ "semver": "bin/semver" } }, - "node_modules/@changesets/get-dependents-graph/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@changesets/get-github-info": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.5.2.tgz", @@ -571,77 +366,6 @@ "chalk": "^2.1.0" } }, - "node_modules/@changesets/logger/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/logger/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/logger/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@changesets/logger/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@changesets/logger/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@changesets/logger/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/logger/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@changesets/parse": { "version": "0.3.16", "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.16.tgz", @@ -681,77 +405,6 @@ "p-filter": "^2.1.0" } }, - "node_modules/@changesets/read/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/read/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/read/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@changesets/read/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@changesets/read/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@changesets/read/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@changesets/read/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@changesets/types": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/@changesets/types/-/types-5.2.1.tgz", @@ -871,15 +524,39 @@ "deprecated": "Please use @ensdomains/ens-contracts", "dev": true }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", + "espree": "^9.5.2", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -912,6 +589,15 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/@eslint/js": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz", + "integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/@ethereumjs/common": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.5.0.tgz", @@ -1727,12 +1413,6 @@ "fs-extra": "^8.1.0" } }, - "node_modules/@manypkg/find-root/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "node_modules/@manypkg/find-root/node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -1781,26 +1461,6 @@ "node": ">=6 <7 || >=8" } }, - "node_modules/@manypkg/get-packages/node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@metamask/eth-sig-util": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@metamask/eth-sig-util/-/eth-sig-util-4.0.1.tgz", @@ -1842,9 +1502,9 @@ } }, "node_modules/@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", "dev": true, "funding": [ { @@ -1854,9 +1514,9 @@ ] }, "node_modules/@noble/secp256k1": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.6.3.tgz", - "integrity": "sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", "dev": true, "funding": [ { @@ -1901,34 +1561,84 @@ } }, "node_modules/@nomicfoundation/ethereumjs-block": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-4.0.0.tgz", - "integrity": "sha512-bk8uP8VuexLgyIZAHExH1QEovqx0Lzhc9Ntm63nCRKLHXIZkobaFaeCVwTESV7YkPKUk7NiK11s8ryed4CS9yA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-5.0.1.tgz", + "integrity": "sha512-u1Yioemi6Ckj3xspygu/SfFvm8vZEO8/Yx5a1QLzi6nVU0jz3Pg2OmHKJ5w+D9Ogk1vhwRiqEBAqcb0GVhCyHw==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "ethereum-cryptography": "0.1.3" + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "ethereum-cryptography": "0.1.3", + "ethers": "^5.7.1" }, "engines": { "node": ">=14" } }, + "node_modules/@nomicfoundation/ethereumjs-block/node_modules/ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" + } + }, "node_modules/@nomicfoundation/ethereumjs-blockchain": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-6.0.0.tgz", - "integrity": "sha512-pLFEoea6MWd81QQYSReLlLfH7N9v7lH66JC/NMPN848ySPPQA5renWnE7wPByfQFzNrPBuDDRFFULMDmj1C0xw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-7.0.1.tgz", + "integrity": "sha512-NhzndlGg829XXbqJEYrF1VeZhAwSPgsK/OB7TVrdzft3y918hW5KNd7gIZ85sn6peDZOdjBsAXIpXZ38oBYE5A==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-ethash": "^2.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-ethash": "3.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "abstract-level": "^1.0.3", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", @@ -1941,24 +1651,24 @@ } }, "node_modules/@nomicfoundation/ethereumjs-common": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-3.0.0.tgz", - "integrity": "sha512-WS7qSshQfxoZOpHG/XqlHEGRG1zmyjYrvmATvc4c62+gZXgre1ymYP8ZNgx/3FyZY0TWe9OjFlKOfLqmgOeYwA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.1.tgz", + "integrity": "sha512-OBErlkfp54GpeiE06brBW/TTbtbuBJV5YI5Nz/aB2evTDo+KawyEzPjBlSr84z/8MFfj8wS2wxzQX1o32cev5g==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-util": "9.0.1", "crc-32": "^1.2.0" } }, "node_modules/@nomicfoundation/ethereumjs-ethash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-2.0.0.tgz", - "integrity": "sha512-WpDvnRncfDUuXdsAXlI4lXbqUDOA+adYRQaEezIkxqDkc+LDyYDbd/xairmY98GnQzo1zIqsIL6GB5MoMSJDew==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-3.0.1.tgz", + "integrity": "sha512-KDjGIB5igzWOp8Ik5I6QiRH5DH+XgILlplsHR7TEuWANZA759G6krQ6o8bvj+tRUz08YygMQu/sGd9mJ1DYT8w==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "abstract-level": "^1.0.3", "bigint-crypto-utils": "^3.0.23", "ethereum-cryptography": "0.1.3" @@ -1968,15 +1678,15 @@ } }, "node_modules/@nomicfoundation/ethereumjs-evm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-1.0.0.tgz", - "integrity": "sha512-hVS6qRo3V1PLKCO210UfcEQHvlG7GqR8iFzp0yyjTg2TmJQizcChKgWo8KFsdMw6AyoLgLhHGHw4HdlP8a4i+Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-2.0.1.tgz", + "integrity": "sha512-oL8vJcnk0Bx/onl+TgQOQ1t/534GKFaEG17fZmwtPFeH8S5soiBYPCLUrvANOl4sCp9elYxIMzIiTtMtNNN8EQ==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@types/async-eventemitter": "^0.2.1", - "async-eventemitter": "^0.2.4", + "@ethersproject/providers": "^5.7.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", "mcl-wasm": "^0.7.1", @@ -1987,9 +1697,9 @@ } }, "node_modules/@nomicfoundation/ethereumjs-rlp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-4.0.0.tgz", - "integrity": "sha512-GaSOGk5QbUk4eBP5qFbpXoZoZUj/NrW7MRa0tKY4Ew4c2HAS0GXArEMAamtFrkazp0BO4K5p2ZCG3b2FmbShmw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.1.tgz", + "integrity": "sha512-xtxrMGa8kP4zF5ApBQBtjlSbN5E2HI8m8FYgVSYAnO6ssUoY5pVPGy2H8+xdf/bmMa22Ce8nWMH3aEW8CcqMeQ==", "dev": true, "bin": { "rlp": "bin/rlp" @@ -1999,28 +1709,76 @@ } }, "node_modules/@nomicfoundation/ethereumjs-statemanager": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-1.0.0.tgz", - "integrity": "sha512-jCtqFjcd2QejtuAMjQzbil/4NHf5aAWxUc+CvS0JclQpl+7M0bxMofR2AJdtz+P3u0ke2euhYREDiE7iSO31vQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-2.0.1.tgz", + "integrity": "sha512-B5ApMOnlruVOR7gisBaYwFX+L/AP7i/2oAahatssjPIBVDF6wTX1K7Qpa39E/nzsH8iYuL3krkYeUFIdO3EMUQ==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", - "functional-red-black-tree": "^1.0.1" + "ethers": "^5.7.1", + "js-sdsl": "^4.1.4" + } + }, + "node_modules/@nomicfoundation/ethereumjs-statemanager/node_modules/ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" } }, "node_modules/@nomicfoundation/ethereumjs-trie": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-5.0.0.tgz", - "integrity": "sha512-LIj5XdE+s+t6WSuq/ttegJzZ1vliwg6wlb+Y9f4RlBpuK35B9K02bO7xU+E6Rgg9RGptkWd6TVLdedTI4eNc2A==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-6.0.1.tgz", + "integrity": "sha512-A64It/IMpDVODzCgxDgAAla8jNjNtsoQZIzZUfIV5AY6Coi4nvn7+VReBn5itlxMiL2yaTlQr9TRWp3CSI6VoA==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "@types/readable-stream": "^2.3.13", "ethereum-cryptography": "0.1.3", "readable-stream": "^3.6.0" }, @@ -2029,14 +1787,16 @@ } }, "node_modules/@nomicfoundation/ethereumjs-tx": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-4.0.0.tgz", - "integrity": "sha512-Gg3Lir2lNUck43Kp/3x6TfBNwcWC9Z1wYue9Nz3v4xjdcv6oDW9QSMJxqsKw9QEGoBBZ+gqwpW7+F05/rs/g1w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.1.tgz", + "integrity": "sha512-0HwxUF2u2hrsIM1fsasjXvlbDOq1ZHFV2dd1yGq8CA+MEYhaxZr8OTScpVkkxqMwBcc5y83FyPl0J9MZn3kY0w==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@chainsafe/ssz": "^0.9.2", + "@ethersproject/providers": "^5.7.2", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "ethereum-cryptography": "0.1.3" }, "engines": { @@ -2044,38 +1804,55 @@ } }, "node_modules/@nomicfoundation/ethereumjs-util": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-8.0.0.tgz", - "integrity": "sha512-2emi0NJ/HmTG+CGY58fa+DQuAoroFeSH9gKu9O6JnwTtlzJtgfTixuoOqLEgyyzZVvwfIpRueuePb8TonL1y+A==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.1.tgz", + "integrity": "sha512-TwbhOWQ8QoSCFhV/DDfSmyfFIHjPjFBj957219+V3jTZYZ2rf9PmDtNOeZWAE3p3vlp8xb02XGpd0v6nTUPbsA==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-rlp": "^4.0.0-beta.2", + "@chainsafe/ssz": "^0.10.0", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", "ethereum-cryptography": "0.1.3" }, "engines": { "node": ">=14" } }, - "node_modules/@nomicfoundation/ethereumjs-vm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-6.0.0.tgz", - "integrity": "sha512-JMPxvPQ3fzD063Sg3Tp+UdwUkVxMoo1uML6KSzFhMH3hoQi/LMuXBoEHAoW83/vyNS9BxEe6jm6LmT5xdeEJ6w==", + "node_modules/@nomicfoundation/ethereumjs-util/node_modules/@chainsafe/persistent-merkle-tree": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.5.0.tgz", + "integrity": "sha512-l0V1b5clxA3iwQLXP40zYjyZYospQLZXzBVIhhr9kDg/1qHZfzzHw0jj4VPBijfYCArZDlPkRi1wZaV2POKeuw==", "dev": true, "dependencies": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-blockchain": "^6.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-evm": "^1.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-statemanager": "^1.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@types/async-eventemitter": "^0.2.1", - "async-eventemitter": "^0.2.4", + "@chainsafe/as-sha256": "^0.3.1" + } + }, + "node_modules/@nomicfoundation/ethereumjs-util/node_modules/@chainsafe/ssz": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.10.2.tgz", + "integrity": "sha512-/NL3Lh8K+0q7A3LsiFq09YXS9fPE+ead2rr7vM2QK8PLzrNsw3uqrif9bpRX5UxgeRjM+vYi+boCM3+GM4ovXg==", + "dev": true, + "dependencies": { + "@chainsafe/as-sha256": "^0.3.1", + "@chainsafe/persistent-merkle-tree": "^0.5.0" + } + }, + "node_modules/@nomicfoundation/ethereumjs-vm": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-7.0.1.tgz", + "integrity": "sha512-rArhyn0jPsS/D+ApFsz3yVJMQ29+pVzNZ0VJgkzAZ+7FqXSRtThl1C1prhmlVr3YNUlfpZ69Ak+RUT4g7VoOuQ==", + "dev": true, + "dependencies": { + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-blockchain": "7.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-evm": "2.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-statemanager": "2.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", - "functional-red-black-tree": "^1.0.1", "mcl-wasm": "^0.7.1", "rustbn.js": "~0.2.0" }, @@ -2084,9 +1861,9 @@ } }, "node_modules/@nomicfoundation/hardhat-network-helpers": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.7.tgz", - "integrity": "sha512-X+3mNvn8B7BY5hpIaLO+TrfzWq12bpux+ajGGdmdcfC78NXmYmOZkAtiz1QZx1YIZGMS1LaXzPXyBExxKFpCaw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.8.tgz", + "integrity": "sha512-MNqQbzUJZnCMIYvlniC3U+kcavz/PhhQSsY90tbEtUyMj/IQqsLwIRZa4ctjABh3Bz0KCh9OXUZ7Yk/d9hr45Q==", "dev": true, "dependencies": { "ethereumjs-util": "^7.1.4" @@ -2096,30 +1873,30 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.0.tgz", - "integrity": "sha512-xGWAiVCGOycvGiP/qrlf9f9eOn7fpNbyJygcB0P21a1MDuVPlKt0Srp7rvtBEutYQ48ouYnRXm33zlRnlTOPHg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", + "integrity": "sha512-1LMtXj1puAxyFusBgUIy5pZk3073cNXYnXUpuNKFghHbIit/xZgbk0AokpUADbNm3gyD6bFWl3LRFh3dhVdREg==", "dev": true, "engines": { "node": ">= 12" }, "optionalDependencies": { - "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.0", - "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.0", - "@nomicfoundation/solidity-analyzer-freebsd-x64": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-arm64-msvc": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-ia32-msvc": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.0" + "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.1", + "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.1", + "@nomicfoundation/solidity-analyzer-freebsd-x64": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-arm64-msvc": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-ia32-msvc": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.1" } }, "node_modules/@nomicfoundation/solidity-analyzer-darwin-arm64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.0.tgz", - "integrity": "sha512-vEF3yKuuzfMHsZecHQcnkUrqm8mnTWfJeEVFHpg+cO+le96xQA4lAJYdUan8pXZohQxv1fSReQsn4QGNuBNuCw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.1.tgz", + "integrity": "sha512-KcTodaQw8ivDZyF+D76FokN/HdpgGpfjc/gFCImdLUyqB6eSWVaZPazMbeAjmfhx3R0zm/NYVzxwAokFKgrc0w==", "cpu": [ "arm64" ], @@ -2133,9 +1910,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-darwin-x64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.0.tgz", - "integrity": "sha512-dlHeIg0pTL4dB1l9JDwbi/JG6dHQaU1xpDK+ugYO8eJ1kxx9Dh2isEUtA4d02cQAl22cjOHTvifAk96A+ItEHA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.1.tgz", + "integrity": "sha512-XhQG4BaJE6cIbjAVtzGOGbK3sn1BO9W29uhk9J8y8fZF1DYz0Doj8QDMfpMu+A6TjPDs61lbsmeYodIDnfveSA==", "cpu": [ "x64" ], @@ -2149,9 +1926,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-freebsd-x64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-freebsd-x64/-/solidity-analyzer-freebsd-x64-0.1.0.tgz", - "integrity": "sha512-WFCZYMv86WowDA4GiJKnebMQRt3kCcFqHeIomW6NMyqiKqhK1kIZCxSLDYsxqlx396kKLPN1713Q1S8tu68GKg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-freebsd-x64/-/solidity-analyzer-freebsd-x64-0.1.1.tgz", + "integrity": "sha512-GHF1VKRdHW3G8CndkwdaeLkVBi5A9u2jwtlS7SLhBc8b5U/GcoL39Q+1CSO3hYqePNP+eV5YI7Zgm0ea6kMHoA==", "cpu": [ "x64" ], @@ -2165,9 +1942,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-gnu": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.0.tgz", - "integrity": "sha512-DTw6MNQWWlCgc71Pq7CEhEqkb7fZnS7oly13pujs4cMH1sR0JzNk90Mp1zpSCsCs4oKan2ClhMlLKtNat/XRKQ==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.1.tgz", + "integrity": "sha512-g4Cv2fO37ZsUENQ2vwPnZc2zRenHyAxHcyBjKcjaSmmkKrFr64yvzeNO8S3GBFCo90rfochLs99wFVGT/0owpg==", "cpu": [ "arm64" ], @@ -2181,9 +1958,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-linux-arm64-musl": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.0.tgz", - "integrity": "sha512-wUpUnR/3GV5Da88MhrxXh/lhb9kxh9V3Jya2NpBEhKDIRCDmtXMSqPMXHZmOR9DfCwCvG6vLFPr/+YrPCnUN0w==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.1.tgz", + "integrity": "sha512-WJ3CE5Oek25OGE3WwzK7oaopY8xMw9Lhb0mlYuJl/maZVo+WtP36XoQTb7bW/i8aAdHW5Z+BqrHMux23pvxG3w==", "cpu": [ "arm64" ], @@ -2197,9 +1974,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-gnu": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.0.tgz", - "integrity": "sha512-lR0AxK1x/MeKQ/3Pt923kPvwigmGX3OxeU5qNtQ9pj9iucgk4PzhbS3ruUeSpYhUxG50jN4RkIGwUMoev5lguw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.1.tgz", + "integrity": "sha512-5WN7leSr5fkUBBjE4f3wKENUy9HQStu7HmWqbtknfXkkil+eNWiBV275IOlpXku7v3uLsXTOKpnnGHJYI2qsdA==", "cpu": [ "x64" ], @@ -2213,9 +1990,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-linux-x64-musl": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.0.tgz", - "integrity": "sha512-A1he/8gy/JeBD3FKvmI6WUJrGrI5uWJNr5Xb9WdV+DK0F8msuOqpEByLlnTdLkXMwW7nSl3awvLezOs9xBHJEg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.1.tgz", + "integrity": "sha512-KdYMkJOq0SYPQMmErv/63CwGwMm5XHenEna9X9aB8mQmhDBrYrlAOSsIPgFCUSL0hjxE3xHP65/EPXR/InD2+w==", "cpu": [ "x64" ], @@ -2229,9 +2006,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-win32-arm64-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-arm64-msvc/-/solidity-analyzer-win32-arm64-msvc-0.1.0.tgz", - "integrity": "sha512-7x5SXZ9R9H4SluJZZP8XPN+ju7Mx+XeUMWZw7ZAqkdhP5mK19I4vz3x0zIWygmfE8RT7uQ5xMap0/9NPsO+ykw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-arm64-msvc/-/solidity-analyzer-win32-arm64-msvc-0.1.1.tgz", + "integrity": "sha512-VFZASBfl4qiBYwW5xeY20exWhmv6ww9sWu/krWSesv3q5hA0o1JuzmPHR4LPN6SUZj5vcqci0O6JOL8BPw+APg==", "cpu": [ "arm64" ], @@ -2245,9 +2022,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-win32-ia32-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-ia32-msvc/-/solidity-analyzer-win32-ia32-msvc-0.1.0.tgz", - "integrity": "sha512-m7w3xf+hnE774YRXu+2mGV7RiF3QJtUoiYU61FascCkQhX3QMQavh7saH/vzb2jN5D24nT/jwvaHYX/MAM9zUw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-ia32-msvc/-/solidity-analyzer-win32-ia32-msvc-0.1.1.tgz", + "integrity": "sha512-JnFkYuyCSA70j6Si6cS1A9Gh1aHTEb8kOTBApp/c7NRTFGNMH8eaInKlyuuiIbvYFhlXW4LicqyYuWNNq9hkpQ==", "cpu": [ "ia32" ], @@ -2261,9 +2038,9 @@ } }, "node_modules/@nomicfoundation/solidity-analyzer-win32-x64-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.0.tgz", - "integrity": "sha512-xCuybjY0sLJQnJhupiFAXaek2EqF0AP0eBjgzaalPXSNvCEN6ZYHvUzdA50ENDVeSYFXcUsYf3+FsD3XKaeptA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.1.tgz", + "integrity": "sha512-HrVJr6+WjIXGnw3Q9u6KQcbZCtk0caVWhCdFADySvRyUxJ8PnzlaP+MhwNE8oyT8OZ6ejHBRrrgjSqDCFXGirw==", "cpu": [ "x64" ], @@ -2376,6 +2153,73 @@ "oz-docs": "oz-docs.js" } }, + "node_modules/@openzeppelin/docs-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@openzeppelin/docs-utils/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@openzeppelin/docs-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@openzeppelin/docs-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@openzeppelin/docs-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@openzeppelin/docs-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@openzeppelin/test-helpers": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/@openzeppelin/test-helpers/-/test-helpers-0.5.16.tgz", @@ -2394,6 +2238,15 @@ "web3-utils": "^1.2.5" } }, + "node_modules/@openzeppelin/test-helpers/node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/@openzeppelin/test-helpers/node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -2403,6 +2256,91 @@ "semver": "bin/semver" } }, + "node_modules/@openzeppelin/upgrades-core": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.26.2.tgz", + "integrity": "sha512-TJORrgyun5qflPos/47P3j61gDw+7W+tEirSBOYRxfVL1WGjX1n8iaLrijPIqzyeS1MKguN1nckAMspQ4SKrxw==", + "dev": true, + "dependencies": { + "cbor": "^8.0.0", + "chalk": "^4.1.0", + "compare-versions": "^5.0.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.0.3", + "proper-lockfile": "^4.1.1", + "solidity-ast": "^0.4.15" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@scure/base": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", @@ -2416,9 +2354,9 @@ ] }, "node_modules/@scure/bip32": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.0.tgz", - "integrity": "sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", "dev": true, "funding": [ { @@ -2427,15 +2365,15 @@ } ], "dependencies": { - "@noble/hashes": "~1.1.1", - "@noble/secp256k1": "~1.6.0", + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", "@scure/base": "~1.1.0" } }, "node_modules/@scure/bip39": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", - "integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", "dev": true, "funding": [ { @@ -2444,7 +2382,7 @@ } ], "dependencies": { - "@noble/hashes": "~1.1.1", + "@noble/hashes": "~1.2.0", "@scure/base": "~1.1.0" } }, @@ -2584,30 +2522,30 @@ } }, "node_modules/@truffle/abi-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.6.tgz", - "integrity": "sha512-61aTH2QmwVA1INaPMufRHTsS6jsEhS+GCkuCDdvBDmwctSnCKGDOr185BGt65QrpMRxYmIoH6WFBSNMYxW9GRw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-1.0.0.tgz", + "integrity": "sha512-h1wGFB28YfByAWm/uBeMCwqDlGsrcMYTumLC/sB/qYhHisi1LK6tV47FEF7zKyf6Al2CtsO28v02+wfLXbUVRg==", "dev": true, "dependencies": { "change-case": "3.0.2", "fast-check": "3.1.1", - "web3-utils": "1.8.1" + "web3-utils": "1.10.0" } }, "node_modules/@truffle/blockchain-utils": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@truffle/blockchain-utils/-/blockchain-utils-0.1.6.tgz", - "integrity": "sha512-SldoNRIFSm3+HMBnSc2jFsu5TWDkCN4X6vL3wrd0t6DIeF7nD6EoPPjxwbFSoqCnkkRxMuZeL6sUx7UMJS/wSA==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@truffle/blockchain-utils/-/blockchain-utils-0.1.7.tgz", + "integrity": "sha512-1nibqGjEHC7KAyDThEFvbm2+EO8zAHee/VjCtxkYBE3ySwP50joh0QCEBjy7K/9z+icpMoDucfxmgaKToBFUgQ==", "dev": true }, "node_modules/@truffle/codec": { - "version": "0.14.11", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.11.tgz", - "integrity": "sha512-NgfMNYemgMXqoEcJA5ZsEhxChCwq33rSxtNxlececEH/1Nf0r+ryfrfmLlyPmv8f3jorVf1GWa0zI0AedGCGYQ==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.15.1.tgz", + "integrity": "sha512-OBANcmefxEXLApWl/uU1SOHQJixO8pDaRTybP0YMvxPhgyj7G7+wC+fUvnZdmrTJD2WJFLuoYvZbxILmycEvPg==", "dev": true, "dependencies": { - "@truffle/abi-utils": "^0.3.6", - "@truffle/compile-common": "^0.9.1", + "@truffle/abi-utils": "^1.0.0", + "@truffle/compile-common": "^0.9.5", "big.js": "^6.0.3", "bn.js": "^5.1.3", "cbor": "^5.2.0", @@ -2615,7 +2553,16 @@ "lodash": "^4.17.21", "semver": "7.3.7", "utf8": "^3.0.0", - "web3-utils": "1.8.1" + "web3-utils": "1.10.0" + } + }, + "node_modules/@truffle/codec/node_modules/bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "dev": true, + "engines": { + "node": "*" } }, "node_modules/@truffle/codec/node_modules/bn.js": { @@ -2624,6 +2571,19 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true }, + "node_modules/@truffle/codec/node_modules/cbor": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", + "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "dev": true, + "dependencies": { + "bignumber.js": "^9.0.1", + "nofilter": "^1.0.4" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@truffle/codec/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2636,6 +2596,15 @@ "node": ">=10" } }, + "node_modules/@truffle/codec/node_modules/nofilter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", + "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/@truffle/codec/node_modules/semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -2658,71 +2627,71 @@ "dev": true }, "node_modules/@truffle/compile-common": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.1.tgz", - "integrity": "sha512-mhdkX6ExZImHSBO3jGm6aAn8NpVtMTdjq50jRXY/O59/ZNC0J9WpRapxrAKUVNc+XydMdBlfeEpXoqTJg7cbXw==", + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.5.tgz", + "integrity": "sha512-qOIT7nYzQsrnpjk8LurKE6EYYvvJIk3rCHfn+xed88aG6F1l4WYtkUKl+Dcwgxgv3LH0khcQOpjqkXK5kUIJ8A==", "dev": true, "dependencies": { - "@truffle/error": "^0.1.1", + "@truffle/error": "^0.2.0", "colors": "1.4.0" } }, + "node_modules/@truffle/compile-common/node_modules/@truffle/error": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.2.0.tgz", + "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", + "dev": true + }, "node_modules/@truffle/contract": { - "version": "4.6.10", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.10.tgz", - "integrity": "sha512-69IZSXeQKRP3EutILqe+vLY5A5gUpeXUiZhm/Fy/qHHkP238vMjtOkTZGkY6bonYqmgk+vDY7KSYSYKzDNPdCA==", + "version": "4.6.22", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.22.tgz", + "integrity": "sha512-081tM5CBBLgTQX0Fhzp0nlZHnfgojRXweV7/d6v7LHe6QGrGBmgvUy3EIbO+R3P1uaxeGVijMvB4Ok8md9IpYQ==", "dev": true, "dependencies": { "@ensdomains/ensjs": "^2.1.0", - "@truffle/blockchain-utils": "^0.1.6", - "@truffle/contract-schema": "^3.4.11", - "@truffle/debug-utils": "^6.0.42", - "@truffle/error": "^0.1.1", - "@truffle/interface-adapter": "^0.5.26", + "@truffle/blockchain-utils": "^0.1.7", + "@truffle/contract-schema": "^3.4.14", + "@truffle/debug-utils": "^6.0.50", + "@truffle/error": "^0.2.0", + "@truffle/interface-adapter": "^0.5.33", "bignumber.js": "^7.2.1", "debug": "^4.3.1", "ethers": "^4.0.32", - "web3": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-utils": "1.8.1" + "web3": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-utils": "1.10.0" } }, "node_modules/@truffle/contract-schema": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.11.tgz", - "integrity": "sha512-wReyVZUPyU9Zy5PSCugBLG1nnruBmRAJ/gmoirQiJ9N2n+s1iGBTY49tkDqFMz3XUUE0kplfdb9YKZJlLkTWzQ==", + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.14.tgz", + "integrity": "sha512-IwVQZG9RVNwTdn321+jbFIcky3/kZLkCtq8tqil4jZwivvmZQg8rIVC8GJ7Lkrmixl9/yTyQNL6GtIUUvkZxyA==", "dev": true, "dependencies": { "ajv": "^6.10.0", "debug": "^4.3.1" } }, + "node_modules/@truffle/contract/node_modules/@truffle/error": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.2.0.tgz", + "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", + "dev": true + }, "node_modules/@truffle/debug-utils": { - "version": "6.0.42", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.42.tgz", - "integrity": "sha512-9v70tj+My0Z2UZJ9OsuUlfo4Dt2AJqAQa/YWtGe28H8zsi+o9Dca0RsKWecuprdllgzrEs7ad8QUtSINhwjIlg==", + "version": "6.0.50", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.50.tgz", + "integrity": "sha512-OWdSoOsPW7/jvcO7ASBRzXDzXQNb7dg8UqwoBAI7j7UpdQoCAhz7JQsusSNiFN6g1qrxEpGzeh5iIMDq9WxO3w==", "dev": true, "dependencies": { - "@truffle/codec": "^0.14.11", + "@truffle/codec": "^0.15.1", "@trufflesuite/chromafi": "^3.0.0", "bn.js": "^5.1.3", "chalk": "^2.4.2", "debug": "^4.3.1", - "highlightjs-solidity": "^2.0.5" - } - }, - "node_modules/@truffle/debug-utils/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" + "highlightjs-solidity": "^2.0.6" } }, "node_modules/@truffle/debug-utils/node_modules/bn.js": { @@ -2731,65 +2700,6 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true }, - "node_modules/@truffle/debug-utils/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@truffle/debug-utils/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@truffle/debug-utils/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@truffle/debug-utils/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@truffle/debug-utils/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@truffle/debug-utils/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@truffle/error": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.1.1.tgz", @@ -2797,14 +2707,14 @@ "dev": true }, "node_modules/@truffle/interface-adapter": { - "version": "0.5.26", - "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.26.tgz", - "integrity": "sha512-fBhoqtT+CT4XKXcOijvw0RIMgyUi3FJg+n5i5PyGBsoRzqbLZd9cZq+oMNjOZPdf3GH68hsOFOaQO5tZH7oZow==", + "version": "0.5.33", + "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.33.tgz", + "integrity": "sha512-vbVcH2I8hX+wM0Xj9uAjpgxMHqfT+y6m26zSkOVvZ2wo9Ez1slaOJkK1/TZK+7nJitGZSXeJeB4purMDuADvGA==", "dev": true, "dependencies": { "bn.js": "^5.1.3", "ethers": "^4.0.32", - "web3": "1.8.1" + "web3": "1.10.0" } }, "node_modules/@truffle/interface-adapter/node_modules/bn.js": { @@ -2829,83 +2739,15 @@ "strip-indent": "^2.0.0" } }, - "node_modules/@trufflesuite/chromafi/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@trufflesuite/chromafi/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@trufflesuite/chromafi/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@trufflesuite/chromafi/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@trufflesuite/chromafi/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@trufflesuite/chromafi/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@trufflesuite/chromafi/node_modules/detect-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==", "dev": true, "engines": { "node": ">=4" } }, - "node_modules/@trufflesuite/chromafi/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@types/async-eventemitter": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@types/async-eventemitter/-/async-eventemitter-0.2.1.tgz", - "integrity": "sha512-M2P4Ng26QbAeITiH7w1d7OxtldgfAe0wobpyJzVK/XOb0cUGKU2R4pfAhqcJBXAe2ife5ZOhSv4wk7p+ffURtg==", - "dev": true - }, "node_modules/@types/bignumber.js": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/bignumber.js/-/bignumber.js-5.0.0.tgz", @@ -2938,9 +2780,9 @@ } }, "node_modules/@types/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", "dev": true }, "node_modules/@types/concat-stream": { @@ -2986,21 +2828,6 @@ "ci-info": "^3.1.0" } }, - "node_modules/@types/is-ci/node_modules/ci-info": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", - "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, "node_modules/@types/keyv": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", @@ -3029,9 +2856,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", "dev": true }, "node_modules/@types/normalize-package-data": { @@ -3055,6 +2882,22 @@ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", "dev": true }, + "node_modules/@types/readable-stream": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", + "integrity": "sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ==", + "dev": true, + "dependencies": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + } + }, + "node_modules/@types/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, "node_modules/@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -3159,9 +3002,9 @@ } }, "node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -3255,9 +3098,9 @@ } }, "node_modules/ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "engines": { "node": ">=6" @@ -3300,25 +3143,25 @@ } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "color-convert": "^1.9.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=4" } }, "node_modules/antlr4": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.7.1.tgz", - "integrity": "sha512-haHyTW7Y9joE5MVs37P2lNYfU2RWBLfcRDD8OWldcdZm5TiCE91B5Xl1oWSwiDUSd4rlExpt2pu1fksYQjRBYQ==", - "dev": true + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.0.tgz", + "integrity": "sha512-zooUbt+UscjnWyOrsuY/tVFL4rwrAGwOivpQmvmUDE22hy/lUA467Rc1rcixyRwcRUIXFYBwv7+dClDSHdmmew==", + "dev": true, + "engines": { + "node": ">=16" + } }, "node_modules/antlr4ts": { "version": "0.5.0-alpha.4", @@ -3348,6 +3191,19 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -3448,18 +3304,6 @@ "safer-buffer": "~2.1.0" } }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -3485,31 +3329,19 @@ "dev": true }, "node_modules/astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/async-eventemitter": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/async-eventemitter/-/async-eventemitter-0.2.4.tgz", - "integrity": "sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw==", - "dev": true, - "dependencies": { - "async": "^2.4.0" - } + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true }, "node_modules/async-limiter": { "version": "1.0.1", @@ -3545,9 +3377,9 @@ } }, "node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "dev": true }, "node_modules/balanced-match": { @@ -3641,24 +3473,12 @@ } }, "node_modules/bigint-crypto-utils": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/bigint-crypto-utils/-/bigint-crypto-utils-3.1.8.tgz", - "integrity": "sha512-+VMV9Laq8pXLBKKKK49nOoq9bfR3j7NNQAtbA617a4nw9bVLo8rsqkKMBgM2AJWlNX9fEIyYaYX+d0laqYV4tw==", - "dev": true, - "dependencies": { - "bigint-mod-arith": "^3.1.0" - }, - "engines": { - "node": ">=10.4.0" - } - }, - "node_modules/bigint-mod-arith": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bigint-mod-arith/-/bigint-mod-arith-3.1.2.tgz", - "integrity": "sha512-nx8J8bBeiRR+NlsROFH9jHswW5HO8mgfOSqW0AmjicMMvaONDa8AO+5ViKDUUNytBPWiwfvZP4/Bj4Y3lUfvgQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/bigint-crypto-utils/-/bigint-crypto-utils-3.2.2.tgz", + "integrity": "sha512-U1RbE3aX9ayCUVcIPHuPDPKcK3SFOXf93J1UK/iHlJuQB7bhagPIX06/CLpLEsDThJ7KA4Dhrnzynl+d2weTiw==", "dev": true, "engines": { - "node": ">=10.4.0" + "node": ">=14.0.0" } }, "node_modules/bignumber.js": { @@ -3698,13 +3518,13 @@ "dev": true }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -3712,7 +3532,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -3736,6 +3556,21 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -3765,9 +3600,9 @@ } }, "node_modules/breakword": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.5.tgz", - "integrity": "sha512-ex5W9DoOQ/LUEU3PMdLs9ua/CYZl1678NUkKOdUSi8Aw5F1idieaiRURCBFJCwVcrD1J8Iy3vfWSloaMwO2qFg==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.6.tgz", + "integrity": "sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==", "dev": true, "dependencies": { "wcwidth": "^1.0.1" @@ -3811,68 +3646,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "node_modules/browserify-rsa/node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "node_modules/browserify-sign/node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, "node_modules/bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", @@ -4039,39 +3812,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", - "dev": true, - "dependencies": { - "callsites": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/caller-callsite/node_modules/callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", - "dev": true, - "dependencies": { - "caller-callsite": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -4126,13 +3866,13 @@ "node": ">=6" } }, - "node_modules/camelcase-keys/node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "node_modules/case": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.3.tgz", + "integrity": "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==", "dev": true, "engines": { - "node": ">=8" + "node": ">= 0.8.0" } }, "node_modules/caseless": { @@ -4151,25 +3891,15 @@ } }, "node_modules/cbor": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", - "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", "dev": true, "dependencies": { - "bignumber.js": "^9.0.1", - "nofilter": "^1.0.4" + "nofilter": "^3.1.0" }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/cbor/node_modules/bignumber.js": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", - "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", - "dev": true, - "engines": { - "node": "*" + "node": ">=12.19" } }, "node_modules/chai": { @@ -4201,16 +3931,17 @@ } }, "node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/change-case": { @@ -4335,10 +4066,19 @@ "dev": true }, "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } }, "node_modules/cids": { "version": "0.7.5", @@ -4386,16 +4126,16 @@ "dev": true }, "node_modules/classic-level": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.2.0.tgz", - "integrity": "sha512-qw5B31ANxSluWz9xBzklRWTUAJ1SXIdaVKTVS7HcTGKOAmExx65Wo5BUICW+YGORe2FOUaDghoI9ZDxj82QcFg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.3.0.tgz", + "integrity": "sha512-iwFAJQYtqRTRM0F6L8h4JCt00ZSGdOyqh7yVrhhjrOpFhmBjNlRUey64MCiyo6UmQHMJ+No3c81nujPv+n9yrg==", "dev": true, "hasInstallScript": true, "dependencies": { "abstract-level": "^1.0.2", "catering": "^2.1.0", "module-error": "^1.0.1", - "napi-macros": "~2.0.0", + "napi-macros": "^2.2.2", "node-gyp-build": "^4.3.0" }, "engines": { @@ -4411,18 +4151,6 @@ "node": ">=6" } }, - "node_modules/cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "dependencies": { - "restore-cursor": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/cli-table3": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", @@ -4439,12 +4167,6 @@ "colors": "^1.1.2" } }, - "node_modules/cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "dev": true - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4534,21 +4256,18 @@ } }, "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "color-name": "1.1.3" } }, "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, "node_modules/colors": { @@ -4579,9 +4298,18 @@ "dev": true }, "node_modules/commander": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", - "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/compare-versions": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", + "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", "dev": true }, "node_modules/concat-map": { @@ -4606,9 +4334,9 @@ } }, "node_modules/concat-stream/node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "dependencies": { "core-util-is": "~1.0.0", @@ -4669,9 +4397,9 @@ } }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, "engines": { "node": ">= 0.6" @@ -4712,40 +4440,39 @@ } }, "node_modules/cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", "dev": true, "dependencies": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" } }, - "node_modules/cosmiconfig/node_modules/import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "dependencies": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" + "argparse": "^2.0.1" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cosmiconfig/node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "dev": true, - "engines": { - "node": ">=4" + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/crc-32": { @@ -4760,16 +4487,6 @@ "node": ">=0.8" } }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -4798,12 +4515,12 @@ } }, "node_modules/cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", "dev": true, "dependencies": { - "node-fetch": "2.6.7" + "node-fetch": "^2.6.11" } }, "node_modules/cross-spawn": { @@ -4844,28 +4561,6 @@ "sha3": "^2.1.1" } }, - "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - }, - "engines": { - "node": "*" - } - }, "node_modules/crypto-js": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", @@ -4985,15 +4680,12 @@ } }, "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.10.0" } }, "node_modules/decamelize-keys": { @@ -5012,15 +4704,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decamelize-keys/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decamelize-keys/node_modules/map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", @@ -5106,9 +4789,9 @@ } }, "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "dev": true, "dependencies": { "has-property-descriptors": "^1.0.0", @@ -5139,16 +4822,6 @@ "node": ">= 0.8" } }, - "node_modules/des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -5160,12 +4833,12 @@ } }, "node_modules/detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/detect-port": { @@ -5191,17 +4864,6 @@ "node": ">=0.3.1" } }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, "node_modules/difflib": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", @@ -5286,14 +4948,14 @@ } }, "node_modules/domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dev": true, "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" + "domhandler": "^5.0.3" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" @@ -5384,19 +5046,10 @@ "node": ">=8.6" } }, - "node_modules/enquirer/node_modules/ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, "engines": { "node": ">=0.12" @@ -5424,54 +5077,45 @@ } }, "node_modules/es-abstract": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz", - "integrity": "sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ==", + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", "dev": true, "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", "gopd": "^1.0.1", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-abstract/node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" }, "engines": { "node": ">= 0.4" @@ -5486,6 +5130,20 @@ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", "dev": true }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-shim-unscopables": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", @@ -5570,15 +5228,12 @@ "dev": true }, "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8.0" } }, "node_modules/escodegen": { @@ -5677,12 +5332,15 @@ } }, "node_modules/eslint": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz", - "integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz", + "integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.4.1", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.41.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -5692,24 +5350,22 @@ "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", @@ -5717,7 +5373,6 @@ "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", - "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" @@ -5733,9 +5388,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz", - "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -5745,9 +5400,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -5755,42 +5410,21 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" - } - }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true, - "engines": { - "node": ">=10" + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/ansi-regex": { @@ -5802,6 +5436,21 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/eslint/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5824,6 +5473,36 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5852,6 +5531,15 @@ "node": ">=10.13.0" } }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/eslint/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -5879,21 +5567,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -5921,15 +5594,27 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", "dev": true, "dependencies": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -5952,9 +5637,9 @@ } }, "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "dependencies": { "estraverse": "^5.1.0" @@ -6067,18 +5752,6 @@ "node": ">=6" } }, - "node_modules/eth-gas-reporter/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/eth-gas-reporter/node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -6088,32 +5761,6 @@ "node": ">=6" } }, - "node_modules/eth-gas-reporter/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/eth-gas-reporter/node_modules/chalk/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/eth-gas-reporter/node_modules/chokidar": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", @@ -6146,21 +5793,6 @@ "wrap-ansi": "^5.1.0" } }, - "node_modules/eth-gas-reporter/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/eth-gas-reporter/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/eth-gas-reporter/node_modules/debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -6171,15 +5803,6 @@ "ms": "^2.1.1" } }, - "node_modules/eth-gas-reporter/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/eth-gas-reporter/node_modules/diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -6195,25 +5818,16 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, - "node_modules/eth-gas-reporter/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/eth-gas-reporter/node_modules/ethereum-cryptography": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz", - "integrity": "sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", "dev": true, "dependencies": { - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.6.3", - "@scure/bip32": "1.1.0", - "@scure/bip39": "1.1.0" + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" } }, "node_modules/eth-gas-reporter/node_modules/find-up": { @@ -6272,15 +5886,6 @@ "node": "*" } }, - "node_modules/eth-gas-reporter/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/eth-gas-reporter/node_modules/js-yaml": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", @@ -6407,6 +6012,21 @@ "node": ">= 0.4" } }, + "node_modules/eth-gas-reporter/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eth-gas-reporter/node_modules/p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -6930,6 +6550,30 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -6954,6 +6598,36 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/express/node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -7027,9 +6701,9 @@ "dev": true }, "node_modules/fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, "node_modules/fast-glob": { @@ -7069,27 +6743,6 @@ "reusify": "^1.0.4" } }, - "node_modules/figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -7387,13 +7040,14 @@ } }, "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dev": true, "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" }, "funding": { @@ -7459,81 +7113,10 @@ "testrpc-sc": "index.js" } }, - "node_modules/ghost-testrpc/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ghost-testrpc/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/ghost-testrpc/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/ghost-testrpc/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/ghost-testrpc/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/ghost-testrpc/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ghost-testrpc/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -7571,9 +7154,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -7631,9 +7214,9 @@ } }, "node_modules/globals": { - "version": "13.19.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", - "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -7645,43 +7228,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", - "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "dependencies": { - "@types/glob": "^7.1.1", "array-union": "^2.1.0", "dir-glob": "^3.0.1", - "fast-glob": "^3.0.3", - "glob": "^7.1.3", - "ignore": "^5.1.1", - "merge2": "^1.2.3", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", "slash": "^3.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/globby/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/gopd": { @@ -7724,9 +7303,9 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, "node_modules/grapheme-splitter": { @@ -7735,6 +7314,12 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "node_modules/graphlib": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", @@ -7816,23 +7401,23 @@ } }, "node_modules/hardhat": { - "version": "2.12.5", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.5.tgz", - "integrity": "sha512-f/t7+hLlhsnQZ6LDXyV+8rHGRZFZY1sgFvgrwr9fBjMdGp1Bu6hHq1KXS4/VFZfZcVdL1DAWWEkryinZhqce+A==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.14.1.tgz", + "integrity": "sha512-H3Qp/UKyQGmPDDBSfMoSyH18rRnac90rsb0LNer+sKe6at6rxLe4D5j+M+1icqZQF02iLPjNRwc/PA8OPf757A==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.1.2", "@metamask/eth-sig-util": "^4.0.0", - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-blockchain": "^6.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-evm": "^1.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-statemanager": "^1.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@nomicfoundation/ethereumjs-vm": "^6.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-blockchain": "7.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-evm": "2.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-statemanager": "2.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "@nomicfoundation/ethereumjs-vm": "7.0.1", "@nomicfoundation/solidity-analyzer": "^0.1.0", "@sentry/node": "^5.18.1", "@types/bn.js": "^5.1.0", @@ -7868,15 +7453,15 @@ "source-map-support": "^0.5.13", "stacktrace-parser": "^0.1.10", "tsort": "0.0.1", - "undici": "^5.4.0", + "undici": "^5.14.0", "uuid": "^8.3.2", "ws": "^7.4.6" }, "bin": { - "hardhat": "internal/cli/cli.js" + "hardhat": "internal/cli/bootstrap.js" }, "engines": { - "node": "^14.0.0 || ^16.0.0 || ^18.0.0" + "node": ">=14.0.0" }, "peerDependencies": { "ts-node": "*", @@ -7892,9 +7477,9 @@ } }, "node_modules/hardhat-exposed": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.0.tgz", - "integrity": "sha512-1p2Aou7QW3VVI0iJhh3q9hgPyF66zggeW7v/PrcipniQqaXK+KxJnnJvzGsLvXYzB8lVp23GIK7MXoTjjyXkHQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.6.tgz", + "integrity": "sha512-jb03M+GolhEfjDYJyPkGJzcAs1/eZrVufMADXQDwng2iuUJD1zgzM3k1oq2YQihRxguQDsLE1nuHIn6N8o5GmQ==", "dev": true, "dependencies": { "micromatch": "^4.0.4", @@ -7919,9 +7504,9 @@ } }, "node_modules/hardhat-ignore-warnings": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/hardhat-ignore-warnings/-/hardhat-ignore-warnings-0.2.6.tgz", - "integrity": "sha512-GQgvjprONI8VF8b85+QJ8H9v3L9TCCtQvUx+9QaRL+sCPw1cOZHfhlEz9V6Lq7GNCQMqBORVzNzUzoP/tKAEQQ==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/hardhat-ignore-warnings/-/hardhat-ignore-warnings-0.2.8.tgz", + "integrity": "sha512-vPX94rJyTzYsCOzGIYdOcJgn3iQI6qa+CI9ZZfgDhdXJpda8ljpOT7bdUKAYC4LyoP0Z5fWTmupXoPaQrty0gw==", "dev": true, "dependencies": { "minimatch": "^5.1.0", @@ -7939,9 +7524,9 @@ } }, "node_modules/hardhat-ignore-warnings/node_modules/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "dependencies": { "brace-expansion": "^2.0.1" @@ -7950,45 +7535,10 @@ "node": ">=10" } }, - "node_modules/hardhat/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hardhat/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hardhat/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/hardhat/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "node_modules/hardhat/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, "node_modules/hardhat/node_modules/commander": { @@ -7997,25 +7547,16 @@ "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", "dev": true }, - "node_modules/hardhat/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/hardhat/node_modules/ethereum-cryptography": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz", - "integrity": "sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", "dev": true, "dependencies": { - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.6.3", - "@scure/bip32": "1.1.0", - "@scure/bip39": "1.1.0" + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" } }, "node_modules/hardhat/node_modules/find-up": { @@ -8050,15 +7591,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/hardhat/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/hardhat/node_modules/jsonfile": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", @@ -8198,18 +7730,6 @@ "semver": "bin/semver" } }, - "node_modules/hardhat/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -8232,12 +7752,12 @@ } }, "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/has-property-descriptors": { @@ -8252,6 +7772,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -8338,9 +7870,9 @@ } }, "node_modules/highlightjs-solidity": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.5.tgz", - "integrity": "sha512-ReXxQSGQkODMUgHcWzVSnfDCDrL2HshOYgw3OlIYmfHeRzUPkfJTUIp95pK4CmbiNG2eMTOmNLpfCz9Zq7Cwmg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.6.tgz", + "integrity": "sha512-DySXWfQghjm2l6a/flF+cteroJqD4gI8GSdL4PtvxZSsAHie8m3yVe2JFoRg03ROKT6hp2Lc/BxXkqerNmtQYg==", "dev": true }, "node_modules/hmac-drbg": { @@ -8361,9 +7893,9 @@ "dev": true }, "node_modules/htmlparser2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", - "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -8374,9 +7906,9 @@ ], "dependencies": { "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", + "domhandler": "^5.0.3", "domutils": "^3.0.1", - "entities": "^4.3.0" + "entities": "^4.4.0" } }, "node_modules/http-basic": { @@ -8395,9 +7927,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "node_modules/http-errors": { @@ -8465,6 +7997,18 @@ "node": ">=10.19.0" } }, + "node_modules/http2-wrapper/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8538,9 +8082,9 @@ } }, "node_modules/immutable": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.1.tgz", - "integrity": "sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", "dev": true }, "node_modules/import-fresh": { @@ -8559,6 +8103,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -8599,138 +8152,13 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "node_modules/inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "dependencies": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/inquirer/node_modules/ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/inquirer/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/inquirer/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/inquirer/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/inquirer/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/inquirer/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/inquirer/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/internal-slot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", - "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "has": "^1.0.3", "side-channel": "^1.0.4" }, @@ -8790,6 +8218,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -8883,21 +8325,6 @@ "is-ci": "bin.js" } }, - "node_modules/is-ci/node_modules/ci-info": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", - "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, "node_modules/is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -8913,15 +8340,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -9038,12 +8456,12 @@ } }, "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/is-port-reachable": { @@ -9217,9 +8635,9 @@ "dev": true }, "node_modules/js-sdsl": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", - "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", "dev": true, "funding": { "type": "opencollective", @@ -9263,12 +8681,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -9555,6 +8967,18 @@ "node": ">=0.10.0" } }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/load-yaml-file": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", @@ -9570,15 +8994,6 @@ "node": ">=6" } }, - "node_modules/load-yaml-file/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -9621,6 +9036,12 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, "node_modules/lodash.zip": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", @@ -9643,6 +9064,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/log-symbols/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -9659,6 +9095,45 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -9805,92 +9280,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/meow/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/meow/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/meow/node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/meow/node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/meow/node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -9903,19 +9292,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/meow/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -9978,19 +9354,6 @@ "node": ">=8.6" } }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -10024,15 +9387,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -10085,9 +9439,9 @@ } }, "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10107,15 +9461,6 @@ "node": ">= 6" } }, - "node_modules/minimist-options/node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/minipass": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", @@ -10136,9 +9481,9 @@ } }, "node_modules/mixme": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.4.tgz", - "integrity": "sha512-3KYa4m4Vlqx98GPdOHghxSdNtTvcP8E0kkaJ5Dlh+h2DRzF7zpuVVcA8B0QpKd11YJeP9QQ7ASkKzOeu195Wzw==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.9.tgz", + "integrity": "sha512-VC5fg6ySUscaWUpI4gxCBTQMH2RdUpNrk+MsbpCYtIvf9SBJdiUey4qE7BXviJsJR4nDQxCZ+3yaYNW3guz/Pw==", "dev": true, "engines": { "node": ">= 8.0.0" @@ -10253,6 +9598,18 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mocha/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -10301,6 +9658,15 @@ "node": "*" } }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/mocha/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -10364,21 +9730,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "node_modules/mocha/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mocha/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -10453,6 +9804,15 @@ "node": ">=10" } }, + "node_modules/mocha/node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/mock-fs": { "version": "4.14.0", "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.14.0.tgz", @@ -10517,12 +9877,6 @@ "buffer": "^5.5.0" } }, - "node_modules/mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", - "dev": true - }, "node_modules/nano-base32": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nano-base32/-/nano-base32-1.0.1.tgz", @@ -10548,9 +9902,9 @@ } }, "node_modules/napi-macros": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", - "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", + "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==", "dev": true }, "node_modules/natural-compare": { @@ -10580,12 +9934,6 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "node_modules/no-case": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", @@ -10630,9 +9978,9 @@ } }, "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", "dev": true, "dependencies": { "whatwg-url": "^5.0.0" @@ -10650,9 +9998,9 @@ } }, "node_modules/node-gyp-build": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", - "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", "dev": true, "bin": { "node-gyp-build": "bin.js", @@ -10673,12 +10021,12 @@ } }, "node_modules/nofilter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", - "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", "dev": true, "engines": { - "node": ">=8" + "node": ">=12.19" } }, "node_modules/nopt": { @@ -10795,9 +10143,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -10812,16 +10160,35 @@ "node": ">= 0.4" } }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object.getownpropertydescriptors": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", - "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz", + "integrity": "sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==", "dev": true, "dependencies": { "array.prototype.reduce": "^1.0.5", "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.21.2", + "safe-array-concat": "^1.0.0" }, "engines": { "node": ">= 0.8" @@ -10866,18 +10233,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -10953,15 +10308,15 @@ } }, "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "dependencies": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10979,6 +10334,21 @@ "node": ">=8" } }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-map": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", @@ -11030,19 +10400,6 @@ "node": ">=6" } }, - "node_modules/parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "dependencies": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, "node_modules/parse-cache-control": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", @@ -11056,16 +10413,21 @@ "dev": true }, "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "dependencies": { + "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/parse5": { @@ -11139,12 +10501,6 @@ "node": ">=0.10.0" } }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -11260,6 +10616,15 @@ "node": ">=8" } }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/preferred-pm": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.3.tgz", @@ -11306,21 +10671,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/preferred-pm/node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/preferred-pm/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -11346,9 +10696,9 @@ } }, "node_modules/prettier": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", - "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "bin": { "prettier": "bin-prettier.js" @@ -11361,12 +10711,12 @@ } }, "node_modules/prettier-plugin-solidity": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.1.tgz", - "integrity": "sha512-uD24KO26tAHF+zMN2nt1OUzfknzza5AgxjogQQrMLZc7j8xiQrDoNWNeOlfFC0YLTwo12CLD10b9niLyP6AqXg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.3.tgz", + "integrity": "sha512-fQ9yucPi2sBbA2U2Xjh6m4isUTJ7S7QLc/XDDsktqqxYfTwdYKJ0EnnywXHwCGAaYbQNK+HIYPL1OemxuMsgeg==", "dev": true, "dependencies": { - "@solidity-parser/parser": "^0.14.5", + "@solidity-parser/parser": "^0.16.0", "semver": "^7.3.8", "solidity-comments-extractor": "^0.0.7" }, @@ -11377,39 +10727,15 @@ "prettier": ">=2.3.0 || >=3.0.0-alpha.0" } }, - "node_modules/prettier-plugin-solidity/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/prettier-plugin-solidity/node_modules/@solidity-parser/parser": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz", + "integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==", "dev": true, "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "antlr4ts": "^0.5.0-alpha.4" } }, - "node_modules/prettier-plugin-solidity/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prettier-plugin-solidity/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -11425,15 +10751,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", @@ -11443,6 +10760,17 @@ "asap": "~2.0.6" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -11468,20 +10796,6 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -11518,9 +10832,9 @@ ] }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dev": true, "dependencies": { "side-channel": "^1.0.4" @@ -11567,15 +10881,12 @@ ] }, "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/randombytes": { @@ -11587,16 +10898,6 @@ "safe-buffer": "^5.1.0" } }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -11607,9 +10908,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "dependencies": { "bytes": "3.1.2", @@ -11622,78 +10923,53 @@ } }, "node_modules/read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", "dev": true, "dependencies": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", "dev": true, "dependencies": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "dev": true, - "dependencies": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" + "node": ">=8" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg-up/node_modules/path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true, - "dependencies": { - "pinkie-promise": "^2.0.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/read-pkg/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/read-yaml-file": { @@ -11711,19 +10987,10 @@ "node": ">=6" } }, - "node_modules/read-yaml-file/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "dependencies": { "inherits": "^2.0.3", @@ -11802,14 +11069,14 @@ "dev": true }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" @@ -11818,18 +11085,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/req-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/req-cwd/-/req-cwd-2.0.0.tgz", @@ -11990,12 +11245,12 @@ "dev": true }, "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/responselike": { @@ -12019,17 +11274,13 @@ "node": ">=8" } }, - "node_modules/restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", "dev": true, - "dependencies": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, "engines": { - "node": ">=4" + "node": ">= 4" } }, "node_modules/reusify": { @@ -12114,15 +11365,6 @@ "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true }, - "node_modules/run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12175,18 +11417,30 @@ "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==", "dev": true }, - "node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "node_modules/safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", "dev": true, "dependencies": { - "tslib": "^1.9.0" + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" }, "engines": { - "npm": ">=2.0.0" + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -12252,12 +11506,6 @@ "istanbul": "lib/cli.js" } }, - "node_modules/sc-istanbul/node_modules/async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", - "dev": true - }, "node_modules/sc-istanbul/node_modules/esprima": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", @@ -12348,9 +11596,9 @@ } }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -12689,46 +11937,64 @@ } }, "node_modules/slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "engines": { - "node": ">=6" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/slice-ansi/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/slice-ansi/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/smartwrap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/smartwrap/-/smartwrap-2.0.2.tgz", @@ -12758,13 +12024,19 @@ "node": ">=8" } }, - "node_modules/smartwrap/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "node_modules/smartwrap/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=6" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/smartwrap/node_modules/cliui": { @@ -12778,15 +12050,24 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/smartwrap/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "node_modules/smartwrap/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, "engines": { - "node": ">=0.10.0" + "node": ">=7.0.0" } }, + "node_modules/smartwrap/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/smartwrap/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -12864,19 +12145,6 @@ "node": ">=8" } }, - "node_modules/smartwrap/node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/snake-case": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-2.1.0.tgz", @@ -12931,11 +12199,15 @@ "wrap-ansi": "^2.0.0" } }, - "node_modules/solc/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "node_modules/solc/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", "dev": true, + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, "engines": { "node": ">=0.10.0" } @@ -13000,6 +12272,68 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/solc/node_modules/path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/solc/node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/solc/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/solc/node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dev": true, + "dependencies": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/solc/node_modules/read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "dev": true, + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/solc/node_modules/require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -13111,395 +12445,128 @@ } }, "node_modules/solhint": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.3.7.tgz", - "integrity": "sha512-NjjjVmXI3ehKkb3aNtRJWw55SUVJ8HMKKodwe0HnejA+k0d2kmhw7jvpa+MCTbcEgt8IWSwx0Hu6aCo/iYOZzQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.4.1.tgz", + "integrity": "sha512-pzZn2RlZhws1XwvLPVSsxfHrwsteFf5eySOhpAytzXwKQYbTCJV6z8EevYDiSVKMpWrvbKpEtJ055CuEmzp4Xg==", "dev": true, "dependencies": { - "@solidity-parser/parser": "^0.14.1", - "ajv": "^6.6.1", - "antlr4": "4.7.1", - "ast-parents": "0.0.1", - "chalk": "^2.4.2", - "commander": "2.18.0", - "cosmiconfig": "^5.0.7", - "eslint": "^5.6.0", - "fast-diff": "^1.1.2", - "glob": "^7.1.3", - "ignore": "^4.0.6", - "js-yaml": "^3.12.0", - "lodash": "^4.17.11", - "semver": "^6.3.0" + "@solidity-parser/parser": "^0.16.0", + "ajv": "^6.12.6", + "antlr4": "^4.11.0", + "ast-parents": "^0.0.1", + "chalk": "^4.1.2", + "commander": "^10.0.0", + "cosmiconfig": "^8.0.0", + "fast-diff": "^1.2.0", + "glob": "^8.0.3", + "ignore": "^5.2.4", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "pluralize": "^8.0.0", + "semver": "^6.3.0", + "strip-ansi": "^6.0.1", + "table": "^6.8.1", + "text-table": "^0.2.0" }, "bin": { "solhint": "solhint.js" }, "optionalDependencies": { - "prettier": "^1.14.3" + "prettier": "^2.8.3" } }, - "node_modules/solhint/node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "node_modules/solhint/node_modules/@solidity-parser/parser": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz", + "integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==", + "dev": true, + "dependencies": { + "antlr4ts": "^0.5.0-alpha.4" + } + }, + "node_modules/solhint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" + "node": ">=8" } }, "node_modules/solhint/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/solhint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "node_modules/solhint/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/solhint/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "dependencies": { - "color-name": "1.1.3" + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, "node_modules/solhint/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/solhint/node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/solhint/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/solhint/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/solhint/node_modules/eslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", - "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.9.1", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.1", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", - "js-yaml": "^3.13.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.11", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^6.14.0 || ^8.10.0 || >=9.10.0" - } - }, - "node_modules/solhint/node_modules/eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/solhint/node_modules/eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/solhint/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/eslint/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/solhint/node_modules/espree": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", - "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", - "dev": true, - "dependencies": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/solhint/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/solhint/node_modules/file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "dependencies": { - "flat-cache": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "dependencies": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "node_modules/solhint/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/solhint/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/solhint/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=4" + "node": ">=8" } }, - "node_modules/solhint/node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/solhint/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "node_modules/solhint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/solhint/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/solhint/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/solhint/node_modules/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true, - "optional": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true, - "engines": { - "node": ">=6.5.0" - } - }, - "node_modules/solhint/node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" + "argparse": "^2.0.1" }, "bin": { - "rimraf": "bin.js" + "js-yaml": "bin/js-yaml.js" } }, "node_modules/solhint/node_modules/semver": { @@ -13511,76 +12578,34 @@ "semver": "bin/semver.js" } }, - "node_modules/solhint/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "node_modules/solhint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "shebang-regex": "^1.0.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/solhint/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/solhint/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, "node_modules/solhint/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" - } - }, - "node_modules/solhint/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/solhint/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" + "node": ">=8" } }, "node_modules/solidity-ast": { - "version": "0.4.40", - "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.40.tgz", - "integrity": "sha512-M8uLBT2jgFB7B0iVAC5a2l71J8vim7aEm03AZkaHbDqyrl1pE+i5PriMEw6WlwGfHp3/Ym7cn9BqvVLQgRk+Yw==", + "version": "0.4.49", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.49.tgz", + "integrity": "sha512-Pr5sCAj1SFqzwFZw1HPKSq0PehlQNdM8GwKyAVYh2DOn7/cCK8LUKD1HeHnKtTgBW7hi9h4nnnan7hpAg5RhWQ==", "dev": true }, "node_modules/solidity-comments": { @@ -13822,18 +12847,6 @@ "node": ">=6" } }, - "node_modules/solidity-coverage/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/solidity-coverage/node_modules/camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -13843,20 +12856,6 @@ "node": ">=6" } }, - "node_modules/solidity-coverage/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/solidity-coverage/node_modules/chokidar": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", @@ -13889,21 +12888,6 @@ "wrap-ansi": "^5.1.0" } }, - "node_modules/solidity-coverage/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/solidity-coverage/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "node_modules/solidity-coverage/node_modules/debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -13914,15 +12898,6 @@ "ms": "^2.1.1" } }, - "node_modules/solidity-coverage/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/solidity-coverage/node_modules/diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -13938,15 +12913,6 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, - "node_modules/solidity-coverage/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/solidity-coverage/node_modules/find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -14001,29 +12967,42 @@ } }, "node_modules/solidity-coverage/node_modules/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/solidity-coverage/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/solidity-coverage/node_modules/globby": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", + "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", "dev": true, + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, "engines": { - "node": ">=4" + "node": ">=8" } }, "node_modules/solidity-coverage/node_modules/js-yaml": { @@ -14064,18 +13043,6 @@ "node": ">=8" } }, - "node_modules/solidity-coverage/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/solidity-coverage/node_modules/mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -14131,16 +13098,33 @@ "url": "https://opencollective.com/mochajs" } }, - "node_modules/solidity-coverage/node_modules/mocha/node_modules/supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "node_modules/solidity-coverage/node_modules/mocha/node_modules/glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "dependencies": { - "has-flag": "^3.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" }, "engines": { - "node": ">=6" + "node": "*" + } + }, + "node_modules/solidity-coverage/node_modules/mocha/node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/solidity-coverage/node_modules/ms": { @@ -14164,6 +13148,21 @@ "node": ">= 0.4" } }, + "node_modules/solidity-coverage/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/solidity-coverage/node_modules/p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -14233,15 +13232,15 @@ } }, "node_modules/solidity-coverage/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", "dev": true, "dependencies": { "has-flag": "^3.0.0" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/solidity-coverage/node_modules/which": { @@ -14319,9 +13318,9 @@ } }, "node_modules/solidity-docgen": { - "version": "0.6.0-beta.34", - "resolved": "https://registry.npmjs.org/solidity-docgen/-/solidity-docgen-0.6.0-beta.34.tgz", - "integrity": "sha512-igdGrkg8gT1jn+B2NwzjEtSf+7NTrSi/jz88zO7MZWgETmcWbXaxgAsQP4BQeC4YFeH0Pie1NsLP7+9qDgvFtA==", + "version": "0.6.0-beta.35", + "resolved": "https://registry.npmjs.org/solidity-docgen/-/solidity-docgen-0.6.0-beta.35.tgz", + "integrity": "sha512-9QdwK1THk/MWIdq1PEW/6dvtND0pUqpFTsbKwwU9YQIMYuRhH1lek9SsgnsGGYtdJ0VTrXXcVT30q20a8Y610A==", "dev": true, "dependencies": { "handlebars": "^4.7.7", @@ -14434,9 +13433,9 @@ "dev": true }, "node_modules/spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -14460,9 +13459,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "node_modules/sprintf-js": { @@ -14590,6 +13589,23 @@ "node": ">=4" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/string.prototype.trimend": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", @@ -14631,15 +13647,12 @@ } }, "node_modules/strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, - "dependencies": { - "is-utf8": "^0.2.0" - }, "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/strip-hex-prefix": { @@ -14677,15 +13690,15 @@ } }, "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "has-flag": "^3.0.0" }, "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/swap-case": { @@ -14805,6 +13818,18 @@ "node": ">=8" } }, + "node_modules/swarm-js/node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/sync-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/sync-request/-/sync-request-6.1.0.tgz", @@ -14829,59 +13854,94 @@ } }, "node_modules/table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", "dev": true, "dependencies": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6.0.0" + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/table/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/table/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, + "node_modules/table/node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/table/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/table/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=6" + "node": ">=8" } }, "node_modules/tar": { @@ -14955,12 +14015,6 @@ "integrity": "sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==", "dev": true }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "node_modules/timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -15027,9 +14081,9 @@ } }, "node_modules/tough-cookie/node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true, "engines": { "node": ">=6" @@ -15072,18 +14126,18 @@ "dev": true }, "node_modules/tty-table": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.1.6.tgz", - "integrity": "sha512-kRj5CBzOrakV4VRRY5kUWbNYvo/FpOsz65DzI5op9P+cHov3+IqPbo1JE1ZnQGkHdZgNFDsrEjrfqqy/Ply9fw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.2.1.tgz", + "integrity": "sha512-xz0uKo+KakCQ+Dxj1D/tKn2FSyreSYWzdkL/BYhgN6oMW808g8QRMuh1atAV9fjTPbWBjfbkKQpI/5rEcnAc7g==", "dev": true, "dependencies": { "chalk": "^4.1.2", - "csv": "^5.5.0", - "kleur": "^4.1.4", + "csv": "^5.5.3", + "kleur": "^4.1.5", "smartwrap": "^2.0.2", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wcwidth": "^1.0.1", - "yargs": "^17.1.1" + "yargs": "^17.7.1" }, "bin": { "tty-table": "adapters/terminal-adapter.js" @@ -15101,6 +14155,21 @@ "node": ">=8" } }, + "node_modules/tty-table/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/tty-table/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -15117,6 +14186,33 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/tty-table/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/tty-table/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/tty-table/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/tty-table/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -15129,6 +14225,18 @@ "node": ">=8" } }, + "node_modules/tty-table/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -15205,6 +14313,20 @@ "node": ">= 0.6" } }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -15261,15 +14383,15 @@ "dev": true }, "node_modules/undici": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.14.0.tgz", - "integrity": "sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==", + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", + "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", "dev": true, "dependencies": { "busboy": "^1.6.0" }, "engines": { - "node": ">=12.18" + "node": ">=14.0" } }, "node_modules/universalify": { @@ -15425,28 +14547,28 @@ } }, "node_modules/web3": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.1.tgz", - "integrity": "sha512-tAqFsQhGv340C9OgRJIuoScN7f7wa1tUvsnnDUMt9YE6J4gcm7TV2Uwv+KERnzvV+xgdeuULYpsioRRNKrUvoQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.10.0.tgz", + "integrity": "sha512-YfKY9wSkGcM8seO+daR89oVTcbu18NsVfvOngzqMYGUU0pPSQmE57qQDvQzUeoIOHAnXEBNzrhjQJmm8ER0rng==", "dev": true, "hasInstallScript": true, "dependencies": { - "web3-bzz": "1.8.1", - "web3-core": "1.8.1", - "web3-eth": "1.8.1", - "web3-eth-personal": "1.8.1", - "web3-net": "1.8.1", - "web3-shh": "1.8.1", - "web3-utils": "1.8.1" + "web3-bzz": "1.10.0", + "web3-core": "1.10.0", + "web3-eth": "1.10.0", + "web3-eth-personal": "1.10.0", + "web3-net": "1.10.0", + "web3-shh": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-bzz": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.1.tgz", - "integrity": "sha512-dJJHS84nvpoxv6ijTMkdUSlRr5beCXNtx4UZcrFLHBva8dT63QEtKdLyDt2AyMJJdVzTCk78uir/6XtVWrdS6w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.10.0.tgz", + "integrity": "sha512-o9IR59io3pDUsXTsps5pO5hW1D5zBmg46iNc2t4j2DkaYHNdDLwk2IP9ukoM2wg47QILfPEJYzhTfkS/CcX0KA==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -15458,63 +14580,57 @@ "node": ">=8.0.0" } }, - "node_modules/web3-bzz/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "node_modules/web3-core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.1.tgz", - "integrity": "sha512-LbRZlJH2N6nS3n3Eo9Y++25IvzMY7WvYnp4NM/Ajhh97dAdglYs6rToQ2DbL2RLvTYmTew4O/y9WmOk4nq9COw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.10.0.tgz", + "integrity": "sha512-fWySwqy2hn3TL89w5TM8wXF1Z2Q6frQTKHWmP0ppRQorEK8NcHJRfeMiv/mQlSKoTS1F6n/nv2uyZsixFycjYQ==", "dev": true, "dependencies": { - "@types/bn.js": "^5.1.0", + "@types/bn.js": "^5.1.1", "@types/node": "^12.12.6", "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-requestmanager": "1.8.1", - "web3-utils": "1.8.1" + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-requestmanager": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-helpers": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.1.tgz", - "integrity": "sha512-ClzNO6T1S1gifC+BThw0+GTfcsjLEY8T1qUp6Ly2+w4PntAdNtKahxWKApWJ0l9idqot/fFIDXwO3Euu7I0Xqw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.10.0.tgz", + "integrity": "sha512-pIxAzFDS5vnbXvfvLSpaA1tfRykAe9adw43YCKsEYQwH0gCLL0kMLkaCX3q+Q8EVmAh+e1jWL/nl9U0de1+++g==", "dev": true, "dependencies": { - "web3-eth-iban": "1.8.1", - "web3-utils": "1.8.1" + "web3-eth-iban": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-method": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.1.tgz", - "integrity": "sha512-oYGRodktfs86NrnFwaWTbv2S38JnpPslFwSSARwFv4W9cjbGUW3LDeA5MKD/dRY+ssZ5OaekeMsUCLoGhX68yA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.10.0.tgz", + "integrity": "sha512-4R700jTLAMKDMhQ+nsVfIXvH6IGJlJzGisIfMKWAIswH31h5AZz7uDUW2YctI+HrYd+5uOAlS4OJeeT9bIpvkA==", "dev": true, "dependencies": { "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-utils": "1.8.1" + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-promievent": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.1.tgz", - "integrity": "sha512-9mxqHlgB0MrZI4oUIRFkuoJMNj3E7btjrMv3sMer/Z9rYR1PfoSc1aAokw4rxKIcAh+ylVtd/acaB2HKB7aRPg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.10.0.tgz", + "integrity": "sha512-68N7k5LWL5R38xRaKFrTFT2pm2jBNFaM4GioS00YjAKXRQ3KjmhijOMG3TICz6Aa5+6GDWYelDNx21YAeZ4YTg==", "dev": true, "dependencies": { "eventemitter3": "4.0.4" @@ -15524,40 +14640,34 @@ } }, "node_modules/web3-core-requestmanager": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.1.tgz", - "integrity": "sha512-x+VC2YPPwZ1khvqA6TA69LvfFCOZXsoUVOxmTx/vIN22PrY9KzKhxcE7pBSiGhmab1jtmRYXUbcQSVpAXqL8cw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.10.0.tgz", + "integrity": "sha512-3z/JKE++Os62APml4dvBM+GAuId4h3L9ckUrj7ebEtS2AR0ixyQPbrBodgL91Sv7j7cQ3Y+hllaluqjguxvSaQ==", "dev": true, "dependencies": { - "util": "^0.12.0", - "web3-core-helpers": "1.8.1", - "web3-providers-http": "1.8.1", - "web3-providers-ipc": "1.8.1", - "web3-providers-ws": "1.8.1" + "util": "^0.12.5", + "web3-core-helpers": "1.10.0", + "web3-providers-http": "1.10.0", + "web3-providers-ipc": "1.10.0", + "web3-providers-ws": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-core-subscriptions": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.1.tgz", - "integrity": "sha512-bmCMq5OeA3E2vZUh8Js1HcJbhwtsE+yeMqGC4oIZB3XsL5SLqyKLB/pU+qUYqQ9o4GdcrFTDPhPg1bgvf7p1Pw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.10.0.tgz", + "integrity": "sha512-HGm1PbDqsxejI075gxBc5OSkwymilRWZufIy9zEpnWKNmfbuv5FfHgW1/chtJP6aP3Uq2vHkvTDl3smQBb8l+g==", "dev": true, "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, - "node_modules/web3-core/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "node_modules/web3-core/node_modules/bignumber.js": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", @@ -15568,58 +14678,57 @@ } }, "node_modules/web3-eth": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.1.tgz", - "integrity": "sha512-LgyzbhFqiFRd8M8sBXoFN4ztzOnkeckl3H/9lH5ek7AdoRMhBg7tYpYRP3E5qkhd/q+yiZmcUgy1AF6NHrC1wg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.10.0.tgz", + "integrity": "sha512-Z5vT6slNMLPKuwRyKGbqeGYC87OAy8bOblaqRTgg94CXcn/mmqU7iPIlG4506YdcdK3x6cfEDG7B6w+jRxypKA==", "dev": true, "dependencies": { - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-eth-accounts": "1.8.1", - "web3-eth-contract": "1.8.1", - "web3-eth-ens": "1.8.1", - "web3-eth-iban": "1.8.1", - "web3-eth-personal": "1.8.1", - "web3-net": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-eth-accounts": "1.10.0", + "web3-eth-contract": "1.10.0", + "web3-eth-ens": "1.10.0", + "web3-eth-iban": "1.10.0", + "web3-eth-personal": "1.10.0", + "web3-net": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-abi": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.1.tgz", - "integrity": "sha512-0mZvCRTIG0UhDhJwNQJgJxu4b4DyIpuMA0GTfqxqeuqzX4Q/ZvmoNurw0ExTfXaGPP82UUmmdkRi6FdZOx+C6w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.10.0.tgz", + "integrity": "sha512-cwS+qRBWpJ43aI9L3JS88QYPfFcSJJ3XapxOQ4j40v6mk7ATpA8CVK1vGTzpihNlOfMVRBkR95oAj7oL6aiDOg==", "dev": true, "dependencies": { "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.8.1" + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-accounts": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.1.tgz", - "integrity": "sha512-mgzxSYgN54/NsOFBO1Fq1KkXp1S5KlBvI/DlgvajU72rupoFMq6Cu6Yp9GUaZ/w2ij9PzEJuFJk174XwtfMCmg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.10.0.tgz", + "integrity": "sha512-wiq39Uc3mOI8rw24wE2n15hboLE0E9BsQLdlmsL4Zua9diDS6B5abXG0XhFcoNsXIGMWXVZz4TOq3u4EdpXF/Q==", "dev": true, "dependencies": { "@ethereumjs/common": "2.5.0", "@ethereumjs/tx": "3.3.2", - "crypto-browserify": "3.12.0", "eth-lib": "0.2.8", - "ethereumjs-util": "^7.0.10", + "ethereumjs-util": "^7.1.5", "scrypt-js": "^3.0.1", "uuid": "^9.0.0", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" @@ -15646,51 +14755,51 @@ } }, "node_modules/web3-eth-contract": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.1.tgz", - "integrity": "sha512-1wphnl+/xwCE2io44JKnN+ti3oa47BKRiVzvWd42icwRbcpFfRxH9QH+aQX3u8VZIISNH7dAkTWpGIIJgGFTmg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.10.0.tgz", + "integrity": "sha512-MIC5FOzP/+2evDksQQ/dpcXhSqa/2hFNytdl/x61IeWxhh6vlFeSjq0YVTAyIzdjwnL7nEmZpjfI6y6/Ufhy7w==", "dev": true, "dependencies": { - "@types/bn.js": "^5.1.0", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-utils": "1.8.1" + "@types/bn.js": "^5.1.1", + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-ens": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.1.tgz", - "integrity": "sha512-FT8xTI9uN8RxeBQa/W8pLa2aoFh4+EE34w7W2271LICKzla1dtLyb6XSdn48vsUcPmhWsTVk9mO9RTU0l4LGQQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.10.0.tgz", + "integrity": "sha512-3hpGgzX3qjgxNAmqdrC2YUQMTfnZbs4GeLEmy8aCWziVwogbuqQZ+Gzdfrym45eOZodk+lmXyLuAdqkNlvkc1g==", "dev": true, "dependencies": { "content-hash": "^2.5.2", "eth-ens-namehash": "2.0.8", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-eth-contract": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-eth-contract": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-eth-iban": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.1.tgz", - "integrity": "sha512-DomoQBfvIdtM08RyMGkMVBOH0vpOIxSSQ+jukWk/EkMLGMWJtXw/K2c2uHAeq3L/VPWNB7zXV2DUEGV/lNE2Dg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.10.0.tgz", + "integrity": "sha512-0l+SP3IGhInw7Q20LY3IVafYEuufo4Dn75jAHT7c2aDJsIolvf2Lc6ugHkBajlwUneGfbRQs/ccYPQ9JeMUbrg==", "dev": true, "dependencies": { "bn.js": "^5.2.1", - "web3-utils": "1.8.1" + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" @@ -15703,78 +14812,72 @@ "dev": true }, "node_modules/web3-eth-personal": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.1.tgz", - "integrity": "sha512-myIYMvj7SDIoV9vE5BkVdon3pya1WinaXItugoii2VoTcQNPOtBxmYVH+XS5ErzCJlnxzphpQrkywyY64bbbCA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.10.0.tgz", + "integrity": "sha512-anseKn98w/d703eWq52uNuZi7GhQeVjTC5/svrBWEKob0WZ5kPdo+EZoFN0sp5a5ubbrk/E0xSl1/M5yORMtpg==", "dev": true, "dependencies": { "@types/node": "^12.12.6", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-net": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-net": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, - "node_modules/web3-eth-personal/node_modules/@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "node_modules/web3-net": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.1.tgz", - "integrity": "sha512-LyEJAwogdFo0UAXZqoSJGFjopdt+kLw0P00FSZn2yszbgcoI7EwC+nXiOsEe12xz4LqpYLOtbR7+gxgiTVjjHQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.10.0.tgz", + "integrity": "sha512-NLH/N3IshYWASpxk4/18Ge6n60GEvWBVeM8inx2dmZJVmRI6SJIlUxbL8jySgiTn3MMZlhbdvrGo8fpUW7a1GA==", "dev": true, "dependencies": { - "web3-core": "1.8.1", - "web3-core-method": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.10.0", + "web3-core-method": "1.10.0", + "web3-utils": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-http": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.1.tgz", - "integrity": "sha512-1Zyts4O9W/UNEPkp+jyL19Jc3D15S4yp8xuLTjVhcUEAlHo24NDWEKxtZGUuHk4HrKL2gp8OlsDbJ7MM+ESDgg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.10.0.tgz", + "integrity": "sha512-eNr965YB8a9mLiNrkjAWNAPXgmQWfpBfkkn7tpEFlghfww0u3I0tktMZiaToJVcL2+Xq+81cxbkpeWJ5XQDwOA==", "dev": true, "dependencies": { "abortcontroller-polyfill": "^1.7.3", "cross-fetch": "^3.1.4", "es6-promise": "^4.2.8", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-ipc": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.1.tgz", - "integrity": "sha512-nw/W5nclvi+P2z2dYkLWReKLnocStflWqFl+qjtv0xn3MrUTyXMzSF0+61i77+16xFsTgzo4wS/NWIOVkR0EFA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.10.0.tgz", + "integrity": "sha512-OfXG1aWN8L1OUqppshzq8YISkWrYHaATW9H8eh0p89TlWMc1KZOL9vttBuaBEi96D/n0eYDn2trzt22bqHWfXA==", "dev": true, "dependencies": { "oboe": "2.1.5", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-providers-ws": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.1.tgz", - "integrity": "sha512-TNefIDAMpdx57+YdWpYZ/xdofS0P+FfKaDYXhn24ie/tH9G+AB+UBSOKnjN0KSadcRSCMBwGPRiEmNHPavZdsA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.10.0.tgz", + "integrity": "sha512-sK0fNcglW36yD5xjnjtSGBnEtf59cbw4vZzJ+CmOWIKGIR96mP5l684g0WD0Eo+f4NQc2anWWXG74lRc9OVMCQ==", "dev": true, "dependencies": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.1", + "web3-core-helpers": "1.10.0", "websocket": "^1.0.32" }, "engines": { @@ -15782,25 +14885,25 @@ } }, "node_modules/web3-shh": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.1.tgz", - "integrity": "sha512-sqHgarnfcY2Qt3PYS4R6YveHrDy7hmL09yeLLHHCI+RKirmjLVqV0rc5LJWUtlbYI+kDoa5gbgde489M9ZAC0g==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.10.0.tgz", + "integrity": "sha512-uNUUuNsO2AjX41GJARV9zJibs11eq6HtOe6Wr0FtRUcj8SN6nHeYIzwstAvJ4fXA53gRqFMTxdntHEt9aXVjpg==", "dev": true, "hasInstallScript": true, "dependencies": { - "web3-core": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-net": "1.8.1" + "web3-core": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-net": "1.10.0" }, "engines": { "node": ">=8.0.0" } }, "node_modules/web3-utils": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.1.tgz", - "integrity": "sha512-LgnM9p6V7rHHUGfpMZod+NST8cRfGzJ1BTXAyNo7A9cJX9LczBfSRxJp+U/GInYe9mby40t3v22AJdlELibnsQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.0.tgz", + "integrity": "sha512-kSaCM0uMcZTNUSmn5vMEhlo02RObGNRRCkdX0V9UTAU0+lrvn0HSaudyCo6CQzuXUsnuY2ERJGCGPfeWmv19Rg==", "dev": true, "dependencies": { "bn.js": "^5.2.1", @@ -15901,9 +15004,9 @@ } }, "node_modules/which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "dev": true }, "node_modules/which-pm": { @@ -16007,6 +15110,39 @@ "node": ">=8" } }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -16048,18 +15184,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "dependencies": { - "mkdirp": "^0.5.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/ws": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", @@ -16160,9 +15284,9 @@ "dev": true }, "node_modules/yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -16178,12 +15302,25 @@ } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs-parser/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "engines": { - "node": ">=10" + "node": ">=6" } }, "node_modules/yargs-unparser": { @@ -16213,6 +15350,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yargs-unparser/node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yargs-unparser/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/yargs/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -16281,9 +15439,9 @@ }, "dependencies": { "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", "dev": true, "requires": { "@babel/highlight": "^7.18.6" @@ -16304,75 +15462,43 @@ "@babel/helper-validator-identifier": "^7.18.6", "chalk": "^2.0.0", "js-tokens": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "@babel/runtime": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.7.tgz", - "integrity": "sha512-UF0tvkUtxwAgZ5W/KrkHf0Rn0fdnLDU9ScxBrEVNUprE/MzirjK4MJUX1/BVDv00Sv8cljtukVK1aky++X1SjQ==", + "version": "7.22.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.3.tgz", + "integrity": "sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==", "dev": true, "requires": { "regenerator-runtime": "^0.13.11" } }, + "@chainsafe/as-sha256": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz", + "integrity": "sha512-hldFFYuf49ed7DAakWVXSJODuq3pzJEguD8tQ7h+sGkM18vja+OFoJI9krnGmgzyuZC2ETX0NOIcCTy31v2Mtg==", + "dev": true + }, + "@chainsafe/persistent-merkle-tree": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.4.2.tgz", + "integrity": "sha512-lLO3ihKPngXLTus/L7WHKaw9PnNJWizlOF1H9NNzHP6Xvh82vzg9F2bzkXhYIFshMZ2gTCEz8tq6STe7r5NDfQ==", + "dev": true, + "requires": { + "@chainsafe/as-sha256": "^0.3.1" + } + }, + "@chainsafe/ssz": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.9.4.tgz", + "integrity": "sha512-77Qtg2N1ayqs4Bg/wvnWfg5Bta7iy7IRh8XqXh7oNMeP2HBbBwx8m6yTpA8p0EHItWPEBkgZd5S5/LSlp3GXuQ==", + "dev": true, + "requires": { + "@chainsafe/as-sha256": "^0.3.1", + "@chainsafe/persistent-merkle-tree": "^0.4.2", + "case": "^1.6.3" + } + }, "@changesets/apply-release-plan": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-6.1.3.tgz", @@ -16394,18 +15520,6 @@ "semver": "^5.4.1" }, "dependencies": { - "detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -16457,9 +15571,9 @@ } }, "@changesets/cli": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.0.tgz", - "integrity": "sha512-0cbTiDms+ICTVtEwAFLNW0jBNex9f5+fFv3I771nBvdnV/mOjd1QJ4+f8KtVSOrwD9SJkk9xbDkWFb0oXd8d1Q==", + "version": "2.26.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.1.tgz", + "integrity": "sha512-XnTa+b51vt057fyAudvDKGB0Sh72xutQZNAdXkCqPBKO2zvs2yYZx5hFZj1u9cbtpwM6Sxtcr02/FQJfZOzemQ==", "dev": true, "requires": { "@babel/runtime": "^7.20.1", @@ -16497,79 +15611,20 @@ "tty-table": "^4.1.5" }, "dependencies": { - "ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "p-try": "^2.0.0" } }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -16610,67 +15665,11 @@ "semver": "^5.4.1" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -16727,64 +15726,6 @@ "dev": true, "requires": { "chalk": "^2.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "@changesets/parse": { @@ -16824,64 +15765,6 @@ "chalk": "^2.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "@changesets/types": { @@ -16993,15 +15876,30 @@ "integrity": "sha512-bvaTH34PMCbv6anRa9I/0zjLJgY4EuznbEMgbV77JBCQ9KNC46rzi0avuxpOfu+xDjPEtSFGqVEOr5GlUSGudA==", "dev": true }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "dev": true + }, "@eslint/eslintrc": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.4.1.tgz", - "integrity": "sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.4.0", + "espree": "^9.5.2", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", @@ -17027,6 +15925,12 @@ } } }, + "@eslint/js": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz", + "integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA==", + "dev": true + }, "@ethereumjs/common": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-2.5.0.tgz", @@ -17522,12 +16426,6 @@ "fs-extra": "^8.1.0" }, "dependencies": { - "@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -17571,20 +16469,6 @@ "jsonfile": "^4.0.0", "universalify": "^0.1.0" } - }, - "globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - } } } }, @@ -17628,15 +16512,15 @@ } }, "@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz", + "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==", "dev": true }, "@noble/secp256k1": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.6.3.tgz", - "integrity": "sha512-T04e4iTurVy7I8Sw4+c5OSN9/RkPlo1uKxAomtxQNLq8j1uPAqnsqG1bqvY3Jv7c13gyr6dui0zmh/I3+f/JaQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", "dev": true }, "@nodelib/fs.scandir": { @@ -17666,31 +16550,73 @@ } }, "@nomicfoundation/ethereumjs-block": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-4.0.0.tgz", - "integrity": "sha512-bk8uP8VuexLgyIZAHExH1QEovqx0Lzhc9Ntm63nCRKLHXIZkobaFaeCVwTESV7YkPKUk7NiK11s8ryed4CS9yA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-block/-/ethereumjs-block-5.0.1.tgz", + "integrity": "sha512-u1Yioemi6Ckj3xspygu/SfFvm8vZEO8/Yx5a1QLzi6nVU0jz3Pg2OmHKJ5w+D9Ogk1vhwRiqEBAqcb0GVhCyHw==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "ethereum-cryptography": "0.1.3" + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "ethereum-cryptography": "0.1.3", + "ethers": "^5.7.1" + }, + "dependencies": { + "ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "dev": true, + "requires": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" + } + } } }, "@nomicfoundation/ethereumjs-blockchain": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-6.0.0.tgz", - "integrity": "sha512-pLFEoea6MWd81QQYSReLlLfH7N9v7lH66JC/NMPN848ySPPQA5renWnE7wPByfQFzNrPBuDDRFFULMDmj1C0xw==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-blockchain/-/ethereumjs-blockchain-7.0.1.tgz", + "integrity": "sha512-NhzndlGg829XXbqJEYrF1VeZhAwSPgsK/OB7TVrdzft3y918hW5KNd7gIZ85sn6peDZOdjBsAXIpXZ38oBYE5A==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-ethash": "^2.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-ethash": "3.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "abstract-level": "^1.0.3", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", @@ -17700,39 +16626,39 @@ } }, "@nomicfoundation/ethereumjs-common": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-3.0.0.tgz", - "integrity": "sha512-WS7qSshQfxoZOpHG/XqlHEGRG1zmyjYrvmATvc4c62+gZXgre1ymYP8ZNgx/3FyZY0TWe9OjFlKOfLqmgOeYwA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-common/-/ethereumjs-common-4.0.1.tgz", + "integrity": "sha512-OBErlkfp54GpeiE06brBW/TTbtbuBJV5YI5Nz/aB2evTDo+KawyEzPjBlSr84z/8MFfj8wS2wxzQX1o32cev5g==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-util": "9.0.1", "crc-32": "^1.2.0" } }, "@nomicfoundation/ethereumjs-ethash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-2.0.0.tgz", - "integrity": "sha512-WpDvnRncfDUuXdsAXlI4lXbqUDOA+adYRQaEezIkxqDkc+LDyYDbd/xairmY98GnQzo1zIqsIL6GB5MoMSJDew==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-ethash/-/ethereumjs-ethash-3.0.1.tgz", + "integrity": "sha512-KDjGIB5igzWOp8Ik5I6QiRH5DH+XgILlplsHR7TEuWANZA759G6krQ6o8bvj+tRUz08YygMQu/sGd9mJ1DYT8w==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "abstract-level": "^1.0.3", "bigint-crypto-utils": "^3.0.23", "ethereum-cryptography": "0.1.3" } }, "@nomicfoundation/ethereumjs-evm": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-1.0.0.tgz", - "integrity": "sha512-hVS6qRo3V1PLKCO210UfcEQHvlG7GqR8iFzp0yyjTg2TmJQizcChKgWo8KFsdMw6AyoLgLhHGHw4HdlP8a4i+Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-evm/-/ethereumjs-evm-2.0.1.tgz", + "integrity": "sha512-oL8vJcnk0Bx/onl+TgQOQ1t/534GKFaEG17fZmwtPFeH8S5soiBYPCLUrvANOl4sCp9elYxIMzIiTtMtNNN8EQ==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@types/async-eventemitter": "^0.2.1", - "async-eventemitter": "^0.2.4", + "@ethersproject/providers": "^5.7.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", "mcl-wasm": "^0.7.1", @@ -17740,178 +16666,239 @@ } }, "@nomicfoundation/ethereumjs-rlp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-4.0.0.tgz", - "integrity": "sha512-GaSOGk5QbUk4eBP5qFbpXoZoZUj/NrW7MRa0tKY4Ew4c2HAS0GXArEMAamtFrkazp0BO4K5p2ZCG3b2FmbShmw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-rlp/-/ethereumjs-rlp-5.0.1.tgz", + "integrity": "sha512-xtxrMGa8kP4zF5ApBQBtjlSbN5E2HI8m8FYgVSYAnO6ssUoY5pVPGy2H8+xdf/bmMa22Ce8nWMH3aEW8CcqMeQ==", "dev": true }, "@nomicfoundation/ethereumjs-statemanager": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-1.0.0.tgz", - "integrity": "sha512-jCtqFjcd2QejtuAMjQzbil/4NHf5aAWxUc+CvS0JclQpl+7M0bxMofR2AJdtz+P3u0ke2euhYREDiE7iSO31vQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-statemanager/-/ethereumjs-statemanager-2.0.1.tgz", + "integrity": "sha512-B5ApMOnlruVOR7gisBaYwFX+L/AP7i/2oAahatssjPIBVDF6wTX1K7Qpa39E/nzsH8iYuL3krkYeUFIdO3EMUQ==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", - "functional-red-black-tree": "^1.0.1" + "ethers": "^5.7.1", + "js-sdsl": "^4.1.4" + }, + "dependencies": { + "ethers": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-5.7.2.tgz", + "integrity": "sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==", + "dev": true, + "requires": { + "@ethersproject/abi": "5.7.0", + "@ethersproject/abstract-provider": "5.7.0", + "@ethersproject/abstract-signer": "5.7.0", + "@ethersproject/address": "5.7.0", + "@ethersproject/base64": "5.7.0", + "@ethersproject/basex": "5.7.0", + "@ethersproject/bignumber": "5.7.0", + "@ethersproject/bytes": "5.7.0", + "@ethersproject/constants": "5.7.0", + "@ethersproject/contracts": "5.7.0", + "@ethersproject/hash": "5.7.0", + "@ethersproject/hdnode": "5.7.0", + "@ethersproject/json-wallets": "5.7.0", + "@ethersproject/keccak256": "5.7.0", + "@ethersproject/logger": "5.7.0", + "@ethersproject/networks": "5.7.1", + "@ethersproject/pbkdf2": "5.7.0", + "@ethersproject/properties": "5.7.0", + "@ethersproject/providers": "5.7.2", + "@ethersproject/random": "5.7.0", + "@ethersproject/rlp": "5.7.0", + "@ethersproject/sha2": "5.7.0", + "@ethersproject/signing-key": "5.7.0", + "@ethersproject/solidity": "5.7.0", + "@ethersproject/strings": "5.7.0", + "@ethersproject/transactions": "5.7.0", + "@ethersproject/units": "5.7.0", + "@ethersproject/wallet": "5.7.0", + "@ethersproject/web": "5.7.1", + "@ethersproject/wordlists": "5.7.0" + } + } } }, "@nomicfoundation/ethereumjs-trie": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-5.0.0.tgz", - "integrity": "sha512-LIj5XdE+s+t6WSuq/ttegJzZ1vliwg6wlb+Y9f4RlBpuK35B9K02bO7xU+E6Rgg9RGptkWd6TVLdedTI4eNc2A==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-trie/-/ethereumjs-trie-6.0.1.tgz", + "integrity": "sha512-A64It/IMpDVODzCgxDgAAla8jNjNtsoQZIzZUfIV5AY6Coi4nvn7+VReBn5itlxMiL2yaTlQr9TRWp3CSI6VoA==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "@types/readable-stream": "^2.3.13", "ethereum-cryptography": "0.1.3", "readable-stream": "^3.6.0" } }, "@nomicfoundation/ethereumjs-tx": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-4.0.0.tgz", - "integrity": "sha512-Gg3Lir2lNUck43Kp/3x6TfBNwcWC9Z1wYue9Nz3v4xjdcv6oDW9QSMJxqsKw9QEGoBBZ+gqwpW7+F05/rs/g1w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-tx/-/ethereumjs-tx-5.0.1.tgz", + "integrity": "sha512-0HwxUF2u2hrsIM1fsasjXvlbDOq1ZHFV2dd1yGq8CA+MEYhaxZr8OTScpVkkxqMwBcc5y83FyPl0J9MZn3kY0w==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", + "@chainsafe/ssz": "^0.9.2", + "@ethersproject/providers": "^5.7.2", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "ethereum-cryptography": "0.1.3" } }, "@nomicfoundation/ethereumjs-util": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-8.0.0.tgz", - "integrity": "sha512-2emi0NJ/HmTG+CGY58fa+DQuAoroFeSH9gKu9O6JnwTtlzJtgfTixuoOqLEgyyzZVvwfIpRueuePb8TonL1y+A==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-util/-/ethereumjs-util-9.0.1.tgz", + "integrity": "sha512-TwbhOWQ8QoSCFhV/DDfSmyfFIHjPjFBj957219+V3jTZYZ2rf9PmDtNOeZWAE3p3vlp8xb02XGpd0v6nTUPbsA==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-rlp": "^4.0.0-beta.2", + "@chainsafe/ssz": "^0.10.0", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", "ethereum-cryptography": "0.1.3" + }, + "dependencies": { + "@chainsafe/persistent-merkle-tree": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@chainsafe/persistent-merkle-tree/-/persistent-merkle-tree-0.5.0.tgz", + "integrity": "sha512-l0V1b5clxA3iwQLXP40zYjyZYospQLZXzBVIhhr9kDg/1qHZfzzHw0jj4VPBijfYCArZDlPkRi1wZaV2POKeuw==", + "dev": true, + "requires": { + "@chainsafe/as-sha256": "^0.3.1" + } + }, + "@chainsafe/ssz": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@chainsafe/ssz/-/ssz-0.10.2.tgz", + "integrity": "sha512-/NL3Lh8K+0q7A3LsiFq09YXS9fPE+ead2rr7vM2QK8PLzrNsw3uqrif9bpRX5UxgeRjM+vYi+boCM3+GM4ovXg==", + "dev": true, + "requires": { + "@chainsafe/as-sha256": "^0.3.1", + "@chainsafe/persistent-merkle-tree": "^0.5.0" + } + } } }, "@nomicfoundation/ethereumjs-vm": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-6.0.0.tgz", - "integrity": "sha512-JMPxvPQ3fzD063Sg3Tp+UdwUkVxMoo1uML6KSzFhMH3hoQi/LMuXBoEHAoW83/vyNS9BxEe6jm6LmT5xdeEJ6w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/ethereumjs-vm/-/ethereumjs-vm-7.0.1.tgz", + "integrity": "sha512-rArhyn0jPsS/D+ApFsz3yVJMQ29+pVzNZ0VJgkzAZ+7FqXSRtThl1C1prhmlVr3YNUlfpZ69Ak+RUT4g7VoOuQ==", "dev": true, "requires": { - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-blockchain": "^6.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-evm": "^1.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-statemanager": "^1.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@types/async-eventemitter": "^0.2.1", - "async-eventemitter": "^0.2.4", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-blockchain": "7.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-evm": "2.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-statemanager": "2.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", "debug": "^4.3.3", "ethereum-cryptography": "0.1.3", - "functional-red-black-tree": "^1.0.1", "mcl-wasm": "^0.7.1", "rustbn.js": "~0.2.0" } }, "@nomicfoundation/hardhat-network-helpers": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.7.tgz", - "integrity": "sha512-X+3mNvn8B7BY5hpIaLO+TrfzWq12bpux+ajGGdmdcfC78NXmYmOZkAtiz1QZx1YIZGMS1LaXzPXyBExxKFpCaw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@nomicfoundation/hardhat-network-helpers/-/hardhat-network-helpers-1.0.8.tgz", + "integrity": "sha512-MNqQbzUJZnCMIYvlniC3U+kcavz/PhhQSsY90tbEtUyMj/IQqsLwIRZa4ctjABh3Bz0KCh9OXUZ7Yk/d9hr45Q==", "dev": true, "requires": { "ethereumjs-util": "^7.1.4" } }, "@nomicfoundation/solidity-analyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.0.tgz", - "integrity": "sha512-xGWAiVCGOycvGiP/qrlf9f9eOn7fpNbyJygcB0P21a1MDuVPlKt0Srp7rvtBEutYQ48ouYnRXm33zlRnlTOPHg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.1.tgz", + "integrity": "sha512-1LMtXj1puAxyFusBgUIy5pZk3073cNXYnXUpuNKFghHbIit/xZgbk0AokpUADbNm3gyD6bFWl3LRFh3dhVdREg==", "dev": true, "requires": { - "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.0", - "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.0", - "@nomicfoundation/solidity-analyzer-freebsd-x64": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.0", - "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-arm64-msvc": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-ia32-msvc": "0.1.0", - "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.0" + "@nomicfoundation/solidity-analyzer-darwin-arm64": "0.1.1", + "@nomicfoundation/solidity-analyzer-darwin-x64": "0.1.1", + "@nomicfoundation/solidity-analyzer-freebsd-x64": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-arm64-musl": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-x64-gnu": "0.1.1", + "@nomicfoundation/solidity-analyzer-linux-x64-musl": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-arm64-msvc": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-ia32-msvc": "0.1.1", + "@nomicfoundation/solidity-analyzer-win32-x64-msvc": "0.1.1" } }, "@nomicfoundation/solidity-analyzer-darwin-arm64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.0.tgz", - "integrity": "sha512-vEF3yKuuzfMHsZecHQcnkUrqm8mnTWfJeEVFHpg+cO+le96xQA4lAJYdUan8pXZohQxv1fSReQsn4QGNuBNuCw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-arm64/-/solidity-analyzer-darwin-arm64-0.1.1.tgz", + "integrity": "sha512-KcTodaQw8ivDZyF+D76FokN/HdpgGpfjc/gFCImdLUyqB6eSWVaZPazMbeAjmfhx3R0zm/NYVzxwAokFKgrc0w==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-darwin-x64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.0.tgz", - "integrity": "sha512-dlHeIg0pTL4dB1l9JDwbi/JG6dHQaU1xpDK+ugYO8eJ1kxx9Dh2isEUtA4d02cQAl22cjOHTvifAk96A+ItEHA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-darwin-x64/-/solidity-analyzer-darwin-x64-0.1.1.tgz", + "integrity": "sha512-XhQG4BaJE6cIbjAVtzGOGbK3sn1BO9W29uhk9J8y8fZF1DYz0Doj8QDMfpMu+A6TjPDs61lbsmeYodIDnfveSA==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-freebsd-x64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-freebsd-x64/-/solidity-analyzer-freebsd-x64-0.1.0.tgz", - "integrity": "sha512-WFCZYMv86WowDA4GiJKnebMQRt3kCcFqHeIomW6NMyqiKqhK1kIZCxSLDYsxqlx396kKLPN1713Q1S8tu68GKg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-freebsd-x64/-/solidity-analyzer-freebsd-x64-0.1.1.tgz", + "integrity": "sha512-GHF1VKRdHW3G8CndkwdaeLkVBi5A9u2jwtlS7SLhBc8b5U/GcoL39Q+1CSO3hYqePNP+eV5YI7Zgm0ea6kMHoA==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-linux-arm64-gnu": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.0.tgz", - "integrity": "sha512-DTw6MNQWWlCgc71Pq7CEhEqkb7fZnS7oly13pujs4cMH1sR0JzNk90Mp1zpSCsCs4oKan2ClhMlLKtNat/XRKQ==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-gnu/-/solidity-analyzer-linux-arm64-gnu-0.1.1.tgz", + "integrity": "sha512-g4Cv2fO37ZsUENQ2vwPnZc2zRenHyAxHcyBjKcjaSmmkKrFr64yvzeNO8S3GBFCo90rfochLs99wFVGT/0owpg==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-linux-arm64-musl": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.0.tgz", - "integrity": "sha512-wUpUnR/3GV5Da88MhrxXh/lhb9kxh9V3Jya2NpBEhKDIRCDmtXMSqPMXHZmOR9DfCwCvG6vLFPr/+YrPCnUN0w==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-arm64-musl/-/solidity-analyzer-linux-arm64-musl-0.1.1.tgz", + "integrity": "sha512-WJ3CE5Oek25OGE3WwzK7oaopY8xMw9Lhb0mlYuJl/maZVo+WtP36XoQTb7bW/i8aAdHW5Z+BqrHMux23pvxG3w==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-linux-x64-gnu": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.0.tgz", - "integrity": "sha512-lR0AxK1x/MeKQ/3Pt923kPvwigmGX3OxeU5qNtQ9pj9iucgk4PzhbS3ruUeSpYhUxG50jN4RkIGwUMoev5lguw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-gnu/-/solidity-analyzer-linux-x64-gnu-0.1.1.tgz", + "integrity": "sha512-5WN7leSr5fkUBBjE4f3wKENUy9HQStu7HmWqbtknfXkkil+eNWiBV275IOlpXku7v3uLsXTOKpnnGHJYI2qsdA==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-linux-x64-musl": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.0.tgz", - "integrity": "sha512-A1he/8gy/JeBD3FKvmI6WUJrGrI5uWJNr5Xb9WdV+DK0F8msuOqpEByLlnTdLkXMwW7nSl3awvLezOs9xBHJEg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-linux-x64-musl/-/solidity-analyzer-linux-x64-musl-0.1.1.tgz", + "integrity": "sha512-KdYMkJOq0SYPQMmErv/63CwGwMm5XHenEna9X9aB8mQmhDBrYrlAOSsIPgFCUSL0hjxE3xHP65/EPXR/InD2+w==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-win32-arm64-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-arm64-msvc/-/solidity-analyzer-win32-arm64-msvc-0.1.0.tgz", - "integrity": "sha512-7x5SXZ9R9H4SluJZZP8XPN+ju7Mx+XeUMWZw7ZAqkdhP5mK19I4vz3x0zIWygmfE8RT7uQ5xMap0/9NPsO+ykw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-arm64-msvc/-/solidity-analyzer-win32-arm64-msvc-0.1.1.tgz", + "integrity": "sha512-VFZASBfl4qiBYwW5xeY20exWhmv6ww9sWu/krWSesv3q5hA0o1JuzmPHR4LPN6SUZj5vcqci0O6JOL8BPw+APg==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-win32-ia32-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-ia32-msvc/-/solidity-analyzer-win32-ia32-msvc-0.1.0.tgz", - "integrity": "sha512-m7w3xf+hnE774YRXu+2mGV7RiF3QJtUoiYU61FascCkQhX3QMQavh7saH/vzb2jN5D24nT/jwvaHYX/MAM9zUw==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-ia32-msvc/-/solidity-analyzer-win32-ia32-msvc-0.1.1.tgz", + "integrity": "sha512-JnFkYuyCSA70j6Si6cS1A9Gh1aHTEb8kOTBApp/c7NRTFGNMH8eaInKlyuuiIbvYFhlXW4LicqyYuWNNq9hkpQ==", "dev": true, "optional": true }, "@nomicfoundation/solidity-analyzer-win32-x64-msvc": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.0.tgz", - "integrity": "sha512-xCuybjY0sLJQnJhupiFAXaek2EqF0AP0eBjgzaalPXSNvCEN6ZYHvUzdA50ENDVeSYFXcUsYf3+FsD3XKaeptA==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer-win32-x64-msvc/-/solidity-analyzer-win32-x64-msvc-0.1.1.tgz", + "integrity": "sha512-HrVJr6+WjIXGnw3Q9u6KQcbZCtk0caVWhCdFADySvRyUxJ8PnzlaP+MhwNE8oyT8OZ6ejHBRrrgjSqDCFXGirw==", "dev": true, "optional": true }, @@ -17993,6 +16980,57 @@ "js-yaml": "^3.13.1", "lodash.startcase": "^4.4.0", "minimist": "^1.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "@openzeppelin/test-helpers": { @@ -18013,6 +17051,12 @@ "web3-utils": "^1.2.5" }, "dependencies": { + "ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true + }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -18021,6 +17065,72 @@ } } }, + "@openzeppelin/upgrades-core": { + "version": "1.26.2", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.26.2.tgz", + "integrity": "sha512-TJORrgyun5qflPos/47P3j61gDw+7W+tEirSBOYRxfVL1WGjX1n8iaLrijPIqzyeS1MKguN1nckAMspQ4SKrxw==", + "dev": true, + "requires": { + "cbor": "^8.0.0", + "chalk": "^4.1.0", + "compare-versions": "^5.0.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.0.3", + "proper-lockfile": "^4.1.1", + "solidity-ast": "^0.4.15" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "@scure/base": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", @@ -18028,23 +17138,23 @@ "dev": true }, "@scure/bip32": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.0.tgz", - "integrity": "sha512-ftTW3kKX54YXLCxH6BB7oEEoJfoE2pIgw7MINKAs5PsS6nqKPuKk1haTF/EuHmYqG330t5GSrdmtRuHaY1a62Q==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz", + "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==", "dev": true, "requires": { - "@noble/hashes": "~1.1.1", - "@noble/secp256k1": "~1.6.0", + "@noble/hashes": "~1.2.0", + "@noble/secp256k1": "~1.7.0", "@scure/base": "~1.1.0" } }, "@scure/bip39": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz", - "integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz", + "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==", "dev": true, "requires": { - "@noble/hashes": "~1.1.1", + "@noble/hashes": "~1.2.0", "@scure/base": "~1.1.0" } }, @@ -18154,30 +17264,30 @@ } }, "@truffle/abi-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-0.3.6.tgz", - "integrity": "sha512-61aTH2QmwVA1INaPMufRHTsS6jsEhS+GCkuCDdvBDmwctSnCKGDOr185BGt65QrpMRxYmIoH6WFBSNMYxW9GRw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@truffle/abi-utils/-/abi-utils-1.0.0.tgz", + "integrity": "sha512-h1wGFB28YfByAWm/uBeMCwqDlGsrcMYTumLC/sB/qYhHisi1LK6tV47FEF7zKyf6Al2CtsO28v02+wfLXbUVRg==", "dev": true, "requires": { "change-case": "3.0.2", "fast-check": "3.1.1", - "web3-utils": "1.8.1" + "web3-utils": "1.10.0" } }, "@truffle/blockchain-utils": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@truffle/blockchain-utils/-/blockchain-utils-0.1.6.tgz", - "integrity": "sha512-SldoNRIFSm3+HMBnSc2jFsu5TWDkCN4X6vL3wrd0t6DIeF7nD6EoPPjxwbFSoqCnkkRxMuZeL6sUx7UMJS/wSA==", + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/@truffle/blockchain-utils/-/blockchain-utils-0.1.7.tgz", + "integrity": "sha512-1nibqGjEHC7KAyDThEFvbm2+EO8zAHee/VjCtxkYBE3ySwP50joh0QCEBjy7K/9z+icpMoDucfxmgaKToBFUgQ==", "dev": true }, "@truffle/codec": { - "version": "0.14.11", - "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.14.11.tgz", - "integrity": "sha512-NgfMNYemgMXqoEcJA5ZsEhxChCwq33rSxtNxlececEH/1Nf0r+ryfrfmLlyPmv8f3jorVf1GWa0zI0AedGCGYQ==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@truffle/codec/-/codec-0.15.1.tgz", + "integrity": "sha512-OBANcmefxEXLApWl/uU1SOHQJixO8pDaRTybP0YMvxPhgyj7G7+wC+fUvnZdmrTJD2WJFLuoYvZbxILmycEvPg==", "dev": true, "requires": { - "@truffle/abi-utils": "^0.3.6", - "@truffle/compile-common": "^0.9.1", + "@truffle/abi-utils": "^1.0.0", + "@truffle/compile-common": "^0.9.5", "big.js": "^6.0.3", "bn.js": "^5.1.3", "cbor": "^5.2.0", @@ -18185,15 +17295,31 @@ "lodash": "^4.17.21", "semver": "7.3.7", "utf8": "^3.0.0", - "web3-utils": "1.8.1" + "web3-utils": "1.10.0" }, "dependencies": { + "bignumber.js": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", + "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", + "dev": true + }, "bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true }, + "cbor": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", + "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "dev": true, + "requires": { + "bignumber.js": "^9.0.1", + "nofilter": "^1.0.4" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -18203,6 +17329,12 @@ "yallist": "^4.0.0" } }, + "nofilter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", + "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "dev": true + }, "semver": { "version": "7.3.7", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", @@ -18221,41 +17353,57 @@ } }, "@truffle/compile-common": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.1.tgz", - "integrity": "sha512-mhdkX6ExZImHSBO3jGm6aAn8NpVtMTdjq50jRXY/O59/ZNC0J9WpRapxrAKUVNc+XydMdBlfeEpXoqTJg7cbXw==", + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@truffle/compile-common/-/compile-common-0.9.5.tgz", + "integrity": "sha512-qOIT7nYzQsrnpjk8LurKE6EYYvvJIk3rCHfn+xed88aG6F1l4WYtkUKl+Dcwgxgv3LH0khcQOpjqkXK5kUIJ8A==", "dev": true, "requires": { - "@truffle/error": "^0.1.1", + "@truffle/error": "^0.2.0", "colors": "1.4.0" + }, + "dependencies": { + "@truffle/error": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.2.0.tgz", + "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", + "dev": true + } } }, "@truffle/contract": { - "version": "4.6.10", - "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.10.tgz", - "integrity": "sha512-69IZSXeQKRP3EutILqe+vLY5A5gUpeXUiZhm/Fy/qHHkP238vMjtOkTZGkY6bonYqmgk+vDY7KSYSYKzDNPdCA==", + "version": "4.6.22", + "resolved": "https://registry.npmjs.org/@truffle/contract/-/contract-4.6.22.tgz", + "integrity": "sha512-081tM5CBBLgTQX0Fhzp0nlZHnfgojRXweV7/d6v7LHe6QGrGBmgvUy3EIbO+R3P1uaxeGVijMvB4Ok8md9IpYQ==", "dev": true, "requires": { "@ensdomains/ensjs": "^2.1.0", - "@truffle/blockchain-utils": "^0.1.6", - "@truffle/contract-schema": "^3.4.11", - "@truffle/debug-utils": "^6.0.42", - "@truffle/error": "^0.1.1", - "@truffle/interface-adapter": "^0.5.26", + "@truffle/blockchain-utils": "^0.1.7", + "@truffle/contract-schema": "^3.4.14", + "@truffle/debug-utils": "^6.0.50", + "@truffle/error": "^0.2.0", + "@truffle/interface-adapter": "^0.5.33", "bignumber.js": "^7.2.1", "debug": "^4.3.1", "ethers": "^4.0.32", - "web3": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-utils": "1.8.1" + "web3": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-utils": "1.10.0" + }, + "dependencies": { + "@truffle/error": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@truffle/error/-/error-0.2.0.tgz", + "integrity": "sha512-Fe0/z4WWb7IP2gBnv3l6zqP87Y0kSMs7oiSLakKJq17q3GUunrHSdioKuNspdggxkXIBhEQLhi8C+LJdwmHKWQ==", + "dev": true + } } }, "@truffle/contract-schema": { - "version": "3.4.11", - "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.11.tgz", - "integrity": "sha512-wReyVZUPyU9Zy5PSCugBLG1nnruBmRAJ/gmoirQiJ9N2n+s1iGBTY49tkDqFMz3XUUE0kplfdb9YKZJlLkTWzQ==", + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/@truffle/contract-schema/-/contract-schema-3.4.14.tgz", + "integrity": "sha512-IwVQZG9RVNwTdn321+jbFIcky3/kZLkCtq8tqil4jZwivvmZQg8rIVC8GJ7Lkrmixl9/yTyQNL6GtIUUvkZxyA==", "dev": true, "requires": { "ajv": "^6.10.0", @@ -18263,80 +17411,24 @@ } }, "@truffle/debug-utils": { - "version": "6.0.42", - "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.42.tgz", - "integrity": "sha512-9v70tj+My0Z2UZJ9OsuUlfo4Dt2AJqAQa/YWtGe28H8zsi+o9Dca0RsKWecuprdllgzrEs7ad8QUtSINhwjIlg==", + "version": "6.0.50", + "resolved": "https://registry.npmjs.org/@truffle/debug-utils/-/debug-utils-6.0.50.tgz", + "integrity": "sha512-OWdSoOsPW7/jvcO7ASBRzXDzXQNb7dg8UqwoBAI7j7UpdQoCAhz7JQsusSNiFN6g1qrxEpGzeh5iIMDq9WxO3w==", "dev": true, "requires": { - "@truffle/codec": "^0.14.11", + "@truffle/codec": "^0.15.1", "@trufflesuite/chromafi": "^3.0.0", "bn.js": "^5.1.3", "chalk": "^2.4.2", "debug": "^4.3.1", - "highlightjs-solidity": "^2.0.5" + "highlightjs-solidity": "^2.0.6" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "bn.js": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, @@ -18347,14 +17439,14 @@ "dev": true }, "@truffle/interface-adapter": { - "version": "0.5.26", - "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.26.tgz", - "integrity": "sha512-fBhoqtT+CT4XKXcOijvw0RIMgyUi3FJg+n5i5PyGBsoRzqbLZd9cZq+oMNjOZPdf3GH68hsOFOaQO5tZH7oZow==", + "version": "0.5.33", + "resolved": "https://registry.npmjs.org/@truffle/interface-adapter/-/interface-adapter-0.5.33.tgz", + "integrity": "sha512-vbVcH2I8hX+wM0Xj9uAjpgxMHqfT+y6m26zSkOVvZ2wo9Ez1slaOJkK1/TZK+7nJitGZSXeJeB4purMDuADvGA==", "dev": true, "requires": { "bn.js": "^5.1.3", "ethers": "^4.0.32", - "web3": "1.8.1" + "web3": "1.10.0" }, "dependencies": { "bn.js": { @@ -18381,70 +17473,14 @@ "strip-indent": "^2.0.0" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "detect-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==", "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, - "@types/async-eventemitter": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@types/async-eventemitter/-/async-eventemitter-0.2.1.tgz", - "integrity": "sha512-M2P4Ng26QbAeITiH7w1d7OxtldgfAe0wobpyJzVK/XOb0cUGKU2R4pfAhqcJBXAe2ife5ZOhSv4wk7p+ffURtg==", - "dev": true - }, "@types/bignumber.js": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/bignumber.js/-/bignumber.js-5.0.0.tgz", @@ -18476,9 +17512,9 @@ } }, "@types/chai": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz", - "integrity": "sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==", "dev": true }, "@types/concat-stream": { @@ -18522,14 +17558,6 @@ "dev": true, "requires": { "ci-info": "^3.1.0" - }, - "dependencies": { - "ci-info": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", - "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==", - "dev": true - } } }, "@types/keyv": { @@ -18560,9 +17588,9 @@ "dev": true }, "@types/node": { - "version": "18.11.18", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", - "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", "dev": true }, "@types/normalize-package-data": { @@ -18586,6 +17614,24 @@ "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", "dev": true }, + "@types/readable-stream": { + "version": "2.3.15", + "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.15.tgz", + "integrity": "sha512-oM5JSKQCcICF1wvGgmecmHldZ48OZamtMxcGGVICOJA8o8cahXC1zEVAif8iwoc5j8etxFaRFnf095+CDsuoFQ==", + "dev": true, + "requires": { + "@types/node": "*", + "safe-buffer": "~5.1.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, "@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -18669,9 +17715,9 @@ } }, "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", "dev": true }, "acorn-jsx": { @@ -18738,9 +17784,9 @@ "optional": true }, "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true }, "ansi-escapes": { @@ -18767,18 +17813,18 @@ "dev": true }, "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "^2.0.1" + "color-convert": "^1.9.0" } }, "antlr4": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.7.1.tgz", - "integrity": "sha512-haHyTW7Y9joE5MVs37P2lNYfU2RWBLfcRDD8OWldcdZm5TiCE91B5Xl1oWSwiDUSd4rlExpt2pu1fksYQjRBYQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/antlr4/-/antlr4-4.13.0.tgz", + "integrity": "sha512-zooUbt+UscjnWyOrsuY/tVFL4rwrAGwOivpQmvmUDE22hy/lUA467Rc1rcixyRwcRUIXFYBwv7+dClDSHdmmew==", "dev": true }, "antlr4ts": { @@ -18806,6 +17852,16 @@ "sprintf-js": "~1.0.2" } }, + "array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + } + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -18882,18 +17938,6 @@ "safer-buffer": "~2.1.0" } }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -18913,28 +17957,16 @@ "dev": true }, "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true }, "async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, - "async-eventemitter": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/async-eventemitter/-/async-eventemitter-0.2.4.tgz", - "integrity": "sha512-pd20BwL7Yt1zwDFy+8MX8F1+WCT8aQeKj0kQnTrH9WaeRETlRamVhD0JtRPmrV4GfOJ2F9CvdQkZeZhnh2TuHw==", - "dev": true, - "requires": { - "async": "^2.4.0" - } + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "dev": true }, "async-limiter": { "version": "1.0.1", @@ -18961,9 +17993,9 @@ "dev": true }, "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", + "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", "dev": true }, "balanced-match": { @@ -19032,18 +18064,9 @@ "dev": true }, "bigint-crypto-utils": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/bigint-crypto-utils/-/bigint-crypto-utils-3.1.8.tgz", - "integrity": "sha512-+VMV9Laq8pXLBKKKK49nOoq9bfR3j7NNQAtbA617a4nw9bVLo8rsqkKMBgM2AJWlNX9fEIyYaYX+d0laqYV4tw==", - "dev": true, - "requires": { - "bigint-mod-arith": "^3.1.0" - } - }, - "bigint-mod-arith": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bigint-mod-arith/-/bigint-mod-arith-3.1.2.tgz", - "integrity": "sha512-nx8J8bBeiRR+NlsROFH9jHswW5HO8mgfOSqW0AmjicMMvaONDa8AO+5ViKDUUNytBPWiwfvZP4/Bj4Y3lUfvgQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/bigint-crypto-utils/-/bigint-crypto-utils-3.2.2.tgz", + "integrity": "sha512-U1RbE3aX9ayCUVcIPHuPDPKcK3SFOXf93J1UK/iHlJuQB7bhagPIX06/CLpLEsDThJ7KA4Dhrnzynl+d2weTiw==", "dev": true }, "bignumber.js": { @@ -19077,13 +18100,13 @@ "dev": true }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dev": true, "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -19091,7 +18114,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -19110,6 +18133,15 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } } } }, @@ -19139,9 +18171,9 @@ } }, "breakword": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.5.tgz", - "integrity": "sha512-ex5W9DoOQ/LUEU3PMdLs9ua/CYZl1678NUkKOdUSi8Aw5F1idieaiRURCBFJCwVcrD1J8Iy3vfWSloaMwO2qFg==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/breakword/-/breakword-1.0.6.tgz", + "integrity": "sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==", "dev": true, "requires": { "wcwidth": "^1.0.1" @@ -19185,72 +18217,6 @@ "safe-buffer": "^5.0.1" } }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - }, - "dependencies": { - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - } - } - }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - } - } - }, "bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", @@ -19377,32 +18343,6 @@ "get-intrinsic": "^1.0.2" } }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", - "dev": true, - "requires": { - "callsites": "^2.0.0" - }, - "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", - "dev": true - } - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -19441,15 +18381,15 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true - }, - "quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true } } }, + "case": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/case/-/case-1.6.3.tgz", + "integrity": "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==", + "dev": true + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -19463,21 +18403,12 @@ "dev": true }, "cbor": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-5.2.0.tgz", - "integrity": "sha512-5IMhi9e1QU76ppa5/ajP1BmMWZ2FHkhAhjeVKQ/EFCgYSEaeVaoGtL7cxJskf9oCCk+XjzaIdc3IuU/dbA/o2A==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", + "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", "dev": true, "requires": { - "bignumber.js": "^9.0.1", - "nofilter": "^1.0.4" - }, - "dependencies": { - "bignumber.js": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", - "integrity": "sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig==", - "dev": true - } + "nofilter": "^3.1.0" } }, "chai": { @@ -19503,13 +18434,14 @@ "requires": {} }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" } }, "change-case": { @@ -19608,9 +18540,9 @@ "dev": true }, "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", "dev": true }, "cids": { @@ -19655,15 +18587,15 @@ "dev": true }, "classic-level": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.2.0.tgz", - "integrity": "sha512-qw5B31ANxSluWz9xBzklRWTUAJ1SXIdaVKTVS7HcTGKOAmExx65Wo5BUICW+YGORe2FOUaDghoI9ZDxj82QcFg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/classic-level/-/classic-level-1.3.0.tgz", + "integrity": "sha512-iwFAJQYtqRTRM0F6L8h4JCt00ZSGdOyqh7yVrhhjrOpFhmBjNlRUey64MCiyo6UmQHMJ+No3c81nujPv+n9yrg==", "dev": true, "requires": { "abstract-level": "^1.0.2", "catering": "^2.1.0", "module-error": "^1.0.1", - "napi-macros": "~2.0.0", + "napi-macros": "^2.2.2", "node-gyp-build": "^4.3.0" } }, @@ -19673,15 +18605,6 @@ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, "cli-table3": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", @@ -19693,12 +18616,6 @@ "string-width": "^2.1.1" } }, - "cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "dev": true - }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -19766,18 +18683,18 @@ "dev": true }, "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, "requires": { - "color-name": "~1.1.4" + "color-name": "1.1.3" } }, "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, "colors": { @@ -19802,9 +18719,15 @@ "dev": true }, "commander": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", - "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true + }, + "compare-versions": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-5.0.3.tgz", + "integrity": "sha512-4UZlZP8Z99MGEY+Ovg/uJxJuvoXuN4M6B3hKaiackiHrgzQFEe3diJi1mf1PNHbFujM7FvLrK2bpgIaImbtZ1A==", "dev": true }, "concat-map": { @@ -19826,9 +18749,9 @@ }, "dependencies": { "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "requires": { "core-util-is": "~1.0.0", @@ -19888,9 +18811,9 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true }, "cookie": { @@ -19922,32 +18845,31 @@ } }, "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.1.3.tgz", + "integrity": "sha512-/UkO2JKI18b5jVMJUp0lvKFMpa/Gye+ZgZjKD+DGEN9y7NRcf/nK1A0sp67ONmKtnDCNMS44E6jrk0Yc3bDuUw==", "dev": true, "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" }, "dependencies": { - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" + "argparse": "^2.0.1" } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "dev": true } } }, @@ -19957,16 +18879,6 @@ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "dev": true }, - "create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, "create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -19995,12 +18907,12 @@ } }, "cross-fetch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", - "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", + "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", "dev": true, "requires": { - "node-fetch": "2.6.7" + "node-fetch": "^2.6.11" } }, "cross-spawn": { @@ -20035,25 +18947,6 @@ "sha3": "^2.1.1" } }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, "crypto-js": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", @@ -20150,9 +19043,9 @@ } }, "decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true }, "decamelize-keys": { @@ -20165,12 +19058,6 @@ "map-obj": "^1.0.0" }, "dependencies": { - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", @@ -20233,9 +19120,9 @@ "dev": true }, "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", "dev": true, "requires": { "has-property-descriptors": "^1.0.0", @@ -20254,16 +19141,6 @@ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, "destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -20271,9 +19148,9 @@ "dev": true }, "detect-indent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", - "integrity": "sha512-rlpvsxUtM0PQvy9iZe640/IWwWYyBsTApREbA1pHOpmOUIl9MkP/U4z7vTtg4Oaojvqhxt7sdufnT0EzGaR31g==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", "dev": true }, "detect-port": { @@ -20292,17 +19169,6 @@ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", "dev": true }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, "difflib": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/difflib/-/difflib-0.2.4.tgz", @@ -20363,14 +19229,14 @@ } }, "domutils": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz", - "integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", "dev": true, "requires": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", - "domhandler": "^5.0.1" + "domhandler": "^5.0.3" } }, "dot-case": { @@ -20447,20 +19313,12 @@ "dev": true, "requires": { "ansi-colors": "^4.1.1" - }, - "dependencies": { - "ansi-colors": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", - "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", - "dev": true - } } }, "entities": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz", - "integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true }, "env-paths": { @@ -20479,50 +19337,45 @@ } }, "es-abstract": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.5.tgz", - "integrity": "sha512-7h8MM2EQhsCA7pU/Nv78qOXFpD8Rhqd12gYiSJVkrH9+e8VuA8JlPJK/hQjjlLv6pJvx/z1iRFKzYb0XT/RuAQ==", + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", "dev": true, "requires": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", "gopd": "^1.0.1", "has": "^1.0.3", "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", "is-negative-zero": "^2.0.2", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.2", "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", + "object-inspect": "^1.12.3", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.4.3", "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", "string.prototype.trimend": "^1.0.6", "string.prototype.trimstart": "^1.0.6", - "unbox-primitive": "^1.0.2" - }, - "dependencies": { - "object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - } + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" } }, "es-array-method-boxes-properly": { @@ -20531,6 +19384,17 @@ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", "dev": true }, + "es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + } + }, "es-shim-unscopables": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", @@ -20602,9 +19466,9 @@ "dev": true }, "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true }, "escodegen": { @@ -20674,12 +19538,15 @@ } }, "eslint": { - "version": "8.31.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.31.0.tgz", - "integrity": "sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==", + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.41.0.tgz", + "integrity": "sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.4.1", + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.41.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -20689,24 +19556,22 @@ "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", + "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", - "grapheme-splitter": "^1.0.4", + "graphemer": "^1.4.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", @@ -20714,7 +19579,6 @@ "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", - "regexpp": "^3.2.0", "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", "text-table": "^0.2.0" @@ -20726,6 +19590,15 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -20742,6 +19615,27 @@ "supports-color": "^7.1.0" } }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -20761,6 +19655,12 @@ "is-glob": "^4.0.3" } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -20779,15 +19679,6 @@ "p-locate": "^5.0.0" } }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, "p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -20805,58 +19696,50 @@ "requires": { "ansi-regex": "^5.0.1" } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, "eslint-config-prettier": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz", - "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==", + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz", + "integrity": "sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==", "dev": true, "requires": {} }, "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", - "dev": true - } - } - }, "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", "dev": true }, "espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", "dev": true, "requires": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.1" } }, "esprima": { @@ -20866,9 +19749,9 @@ "dev": true }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -20954,43 +19837,12 @@ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "chokidar": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", @@ -21018,21 +19870,6 @@ "wrap-ansi": "^5.1.0" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -21042,12 +19879,6 @@ "ms": "^2.1.1" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -21060,22 +19891,16 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "ethereum-cryptography": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz", - "integrity": "sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", "dev": true, "requires": { - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.6.3", - "@scure/bip32": "1.1.0", - "@scure/bip39": "1.1.0" + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" } }, "find-up": { @@ -21117,12 +19942,6 @@ "path-is-absolute": "^1.0.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, "js-yaml": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", @@ -21220,6 +20039,15 @@ "object-keys": "^1.0.11" } }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -21708,6 +20536,26 @@ "vary": "~1.1.2" }, "dependencies": { + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, "cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -21728,6 +20576,27 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } } } }, @@ -21793,9 +20662,9 @@ "dev": true }, "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, "fast-glob": { @@ -21832,23 +20701,6 @@ "reusify": "^1.0.4" } }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - }, - "dependencies": { - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - } - } - }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -22079,13 +20931,14 @@ "dev": true }, "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", "dev": true, "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", + "has-proto": "^1.0.1", "has-symbols": "^1.0.3" } }, @@ -22128,70 +20981,12 @@ "requires": { "chalk": "^2.4.2", "node-emoji": "^1.10.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } } }, "glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -22211,9 +21006,9 @@ } }, "minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -22272,44 +21067,35 @@ } }, "globals": { - "version": "13.19.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.19.0.tgz", - "integrity": "sha512-dkQ957uSRWHw7CFXLUtUHQI3g3aWApYhfNR2O6jn/907riyTYKVBmxYVROkBcY614FSSeSJh7Xm7SrUWCxvJMQ==", + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, - "globby": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", - "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dev": true, + "requires": { + "define-properties": "^1.1.3" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "requires": { - "@types/glob": "^7.1.1", "array-union": "^2.1.0", "dir-glob": "^3.0.1", - "fast-glob": "^3.0.3", - "glob": "^7.1.3", - "ignore": "^5.1.1", - "merge2": "^1.2.3", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", "slash": "^3.0.0" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } } }, "gopd": { @@ -22343,9 +21129,9 @@ } }, "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, "grapheme-splitter": { @@ -22354,6 +21140,12 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, "graphlib": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", @@ -22413,23 +21205,23 @@ "dev": true }, "hardhat": { - "version": "2.12.5", - "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.12.5.tgz", - "integrity": "sha512-f/t7+hLlhsnQZ6LDXyV+8rHGRZFZY1sgFvgrwr9fBjMdGp1Bu6hHq1KXS4/VFZfZcVdL1DAWWEkryinZhqce+A==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/hardhat/-/hardhat-2.14.1.tgz", + "integrity": "sha512-H3Qp/UKyQGmPDDBSfMoSyH18rRnac90rsb0LNer+sKe6at6rxLe4D5j+M+1icqZQF02iLPjNRwc/PA8OPf757A==", "dev": true, "requires": { "@ethersproject/abi": "^5.1.2", "@metamask/eth-sig-util": "^4.0.0", - "@nomicfoundation/ethereumjs-block": "^4.0.0", - "@nomicfoundation/ethereumjs-blockchain": "^6.0.0", - "@nomicfoundation/ethereumjs-common": "^3.0.0", - "@nomicfoundation/ethereumjs-evm": "^1.0.0", - "@nomicfoundation/ethereumjs-rlp": "^4.0.0", - "@nomicfoundation/ethereumjs-statemanager": "^1.0.0", - "@nomicfoundation/ethereumjs-trie": "^5.0.0", - "@nomicfoundation/ethereumjs-tx": "^4.0.0", - "@nomicfoundation/ethereumjs-util": "^8.0.0", - "@nomicfoundation/ethereumjs-vm": "^6.0.0", + "@nomicfoundation/ethereumjs-block": "5.0.1", + "@nomicfoundation/ethereumjs-blockchain": "7.0.1", + "@nomicfoundation/ethereumjs-common": "4.0.1", + "@nomicfoundation/ethereumjs-evm": "2.0.1", + "@nomicfoundation/ethereumjs-rlp": "5.0.1", + "@nomicfoundation/ethereumjs-statemanager": "2.0.1", + "@nomicfoundation/ethereumjs-trie": "6.0.1", + "@nomicfoundation/ethereumjs-tx": "5.0.1", + "@nomicfoundation/ethereumjs-util": "9.0.1", + "@nomicfoundation/ethereumjs-vm": "7.0.1", "@nomicfoundation/solidity-analyzer": "^0.1.0", "@sentry/node": "^5.18.1", "@types/bn.js": "^5.1.0", @@ -22465,44 +21257,15 @@ "source-map-support": "^0.5.13", "stacktrace-parser": "^0.1.10", "tsort": "0.0.1", - "undici": "^5.4.0", + "undici": "^5.14.0", "uuid": "^8.3.2", "ws": "^7.4.6" }, "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, "commander": { @@ -22511,22 +21274,16 @@ "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "ethereum-cryptography": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.1.2.tgz", - "integrity": "sha512-XDSJlg4BD+hq9N2FjvotwUET9Tfxpxc3kWGE2AqUG5vcbeunnbImVk3cj6e/xT3phdW21mE8R5IugU4fspQDcQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz", + "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==", "dev": true, "requires": { - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.6.3", - "@scure/bip32": "1.1.0", - "@scure/bip39": "1.1.0" + "@noble/hashes": "1.2.0", + "@noble/secp256k1": "1.7.1", + "@scure/bip32": "1.1.5", + "@scure/bip39": "1.1.1" } }, "find-up": { @@ -22552,12 +21309,6 @@ "path-is-absolute": "^1.0.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, "jsonfile": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", @@ -22665,22 +21416,13 @@ "dev": true } } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } } } }, "hardhat-exposed": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.0.tgz", - "integrity": "sha512-1p2Aou7QW3VVI0iJhh3q9hgPyF66zggeW7v/PrcipniQqaXK+KxJnnJvzGsLvXYzB8lVp23GIK7MXoTjjyXkHQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/hardhat-exposed/-/hardhat-exposed-0.3.6.tgz", + "integrity": "sha512-jb03M+GolhEfjDYJyPkGJzcAs1/eZrVufMADXQDwng2iuUJD1zgzM3k1oq2YQihRxguQDsLE1nuHIn6N8o5GmQ==", "dev": true, "requires": { "micromatch": "^4.0.4", @@ -22699,9 +21441,9 @@ } }, "hardhat-ignore-warnings": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/hardhat-ignore-warnings/-/hardhat-ignore-warnings-0.2.6.tgz", - "integrity": "sha512-GQgvjprONI8VF8b85+QJ8H9v3L9TCCtQvUx+9QaRL+sCPw1cOZHfhlEz9V6Lq7GNCQMqBORVzNzUzoP/tKAEQQ==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/hardhat-ignore-warnings/-/hardhat-ignore-warnings-0.2.8.tgz", + "integrity": "sha512-vPX94rJyTzYsCOzGIYdOcJgn3iQI6qa+CI9ZZfgDhdXJpda8ljpOT7bdUKAYC4LyoP0Z5fWTmupXoPaQrty0gw==", "dev": true, "requires": { "minimatch": "^5.1.0", @@ -22719,9 +21461,9 @@ } }, "minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-bNH9mmM9qsJ2X4r2Nat1B//1dJVcn3+iBLa3IgqJ7EbGaDNepL9QSHOxN4ng33s52VMMhhIfgCYDk3C4ZmlDAg==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "requires": { "brace-expansion": "^2.0.1" @@ -22745,9 +21487,9 @@ "dev": true }, "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true }, "has-property-descriptors": { @@ -22759,6 +21501,12 @@ "get-intrinsic": "^1.1.1" } }, + "has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true + }, "has-symbols": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", @@ -22824,9 +21572,9 @@ "dev": true }, "highlightjs-solidity": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.5.tgz", - "integrity": "sha512-ReXxQSGQkODMUgHcWzVSnfDCDrL2HshOYgw3OlIYmfHeRzUPkfJTUIp95pK4CmbiNG2eMTOmNLpfCz9Zq7Cwmg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.6.tgz", + "integrity": "sha512-DySXWfQghjm2l6a/flF+cteroJqD4gI8GSdL4PtvxZSsAHie8m3yVe2JFoRg03ROKT6hp2Lc/BxXkqerNmtQYg==", "dev": true }, "hmac-drbg": { @@ -22847,15 +21595,15 @@ "dev": true }, "htmlparser2": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz", - "integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", "dev": true, "requires": { "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", + "domhandler": "^5.0.3", "domutils": "^3.0.1", - "entities": "^4.3.0" + "entities": "^4.4.0" } }, "http-basic": { @@ -22871,9 +21619,9 @@ } }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "http-errors": { @@ -22931,6 +21679,14 @@ "requires": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" + }, + "dependencies": { + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true + } } }, "https-proxy-agent": { @@ -22980,9 +21736,9 @@ "dev": true }, "immutable": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.2.1.tgz", - "integrity": "sha512-7WYV7Q5BTs0nlQm7tl92rDYYoyELLKHoDMBKhrxEoiV4mrfVdRz8hzPiYOzH7yWjzoVEamxRuAqhxL2PLRwZYQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", "dev": true }, "import-fresh": { @@ -22993,6 +21749,14 @@ "requires": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } } }, "imurmurhash": { @@ -23029,113 +21793,13 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "inquirer": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz", - "integrity": "sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, "internal-slot": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.4.tgz", - "integrity": "sha512-tA8URYccNzMo94s5MQZgH8NB/XTa6HsOo0MLfXTKKEnHVVdegzaQoFZ7Jp44bdvLvY2waT5dc+j5ICEswhi7UQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", "dev": true, "requires": { - "get-intrinsic": "^1.1.3", + "get-intrinsic": "^1.2.0", "has": "^1.0.3", "side-channel": "^1.0.4" } @@ -23177,6 +21841,17 @@ "has-tostringtag": "^1.0.0" } }, + "is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + } + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -23230,14 +21905,6 @@ "dev": true, "requires": { "ci-info": "^3.2.0" - }, - "dependencies": { - "ci-info": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", - "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==", - "dev": true - } } }, "is-date-object": { @@ -23249,12 +21916,6 @@ "has-tostringtag": "^1.0.0" } }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", - "dev": true - }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -23334,9 +21995,9 @@ "dev": true }, "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", "dev": true }, "is-port-reachable": { @@ -23465,9 +22126,9 @@ "dev": true }, "js-sdsl": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", - "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==", "dev": true }, "js-sha3": { @@ -23504,12 +22165,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", @@ -23727,6 +22382,15 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "requires": { + "is-utf8": "^0.2.0" + } } } }, @@ -23740,14 +22404,6 @@ "js-yaml": "^3.13.0", "pify": "^4.0.1", "strip-bom": "^3.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true - } } }, "locate-path": { @@ -23789,6 +22445,12 @@ "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", "dev": true }, + "lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true + }, "lodash.zip": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", @@ -23805,6 +22467,15 @@ "is-unicode-supported": "^0.1.0" }, "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -23814,6 +22485,36 @@ "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -23933,84 +22634,11 @@ "yargs-parser": "^18.1.3" }, "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - } - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, "type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -24063,16 +22691,6 @@ "picomatch": "^2.3.1" } }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, "mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -24094,12 +22712,6 @@ "mime-db": "1.52.0" } }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, "mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -24143,9 +22755,9 @@ } }, "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, "minimist-options": { @@ -24157,14 +22769,6 @@ "arrify": "^1.0.1", "is-plain-obj": "^1.1.0", "kind-of": "^6.0.3" - }, - "dependencies": { - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true - } } }, "minipass": { @@ -24187,9 +22791,9 @@ } }, "mixme": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.4.tgz", - "integrity": "sha512-3KYa4m4Vlqx98GPdOHghxSdNtTvcP8E0kkaJ5Dlh+h2DRzF7zpuVVcA8B0QpKd11YJeP9QQ7ASkKzOeu195Wzw==", + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.9.tgz", + "integrity": "sha512-VC5fg6ySUscaWUpI4gxCBTQMH2RdUpNrk+MsbpCYtIvf9SBJdiUey4qE7BXviJsJR4nDQxCZ+3yaYNW3guz/Pw==", "dev": true }, "mkdirp": { @@ -24277,6 +22881,12 @@ "wrap-ansi": "^7.0.0" } }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -24312,6 +22922,12 @@ } } }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -24362,15 +22978,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, "p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -24423,6 +23030,12 @@ "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } + }, + "yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true } } }, @@ -24486,12 +23099,6 @@ } } }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", - "dev": true - }, "nano-base32": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/nano-base32/-/nano-base32-1.0.1.tgz", @@ -24511,9 +23118,9 @@ "dev": true }, "napi-macros": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.0.0.tgz", - "integrity": "sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz", + "integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==", "dev": true }, "natural-compare": { @@ -24540,12 +23147,6 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", "dev": true }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, "no-case": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", @@ -24589,18 +23190,18 @@ } }, "node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", "dev": true, "requires": { "whatwg-url": "^5.0.0" } }, "node-gyp-build": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz", - "integrity": "sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", "dev": true }, "node-interval-tree": { @@ -24613,9 +23214,9 @@ } }, "nofilter": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-1.0.4.tgz", - "integrity": "sha512-N8lidFp+fCz+TD51+haYdbDGrcBWwuHX40F5+z0qkUjMJ5Tp+rdSuAkMJ9N9eoolDlEVTf6u5icM+cNKkKW2mA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", + "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", "dev": true }, "nopt": { @@ -24705,9 +23306,9 @@ "dev": true }, "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", "dev": true }, "object-keys": { @@ -24716,16 +23317,29 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, "object.getownpropertydescriptors": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", - "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz", + "integrity": "sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==", "dev": true, "requires": { "array.prototype.reduce": "^1.0.5", "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" + "define-properties": "^1.2.0", + "es-abstract": "^1.21.2", + "safe-array-concat": "^1.0.0" } }, "obliterator": { @@ -24761,15 +23375,6 @@ "wrappy": "1" } }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -24829,12 +23434,12 @@ } }, "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "requires": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" } }, "p-locate": { @@ -24844,6 +23449,17 @@ "dev": true, "requires": { "p-limit": "^2.2.0" + }, + "dependencies": { + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + } } }, "p-map": { @@ -24885,19 +23501,6 @@ "callsites": "^3.0.0" } }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, "parse-cache-control": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", @@ -24911,13 +23514,15 @@ "dev": true }, "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "requires": { + "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" } }, "parse5": { @@ -24976,12 +23581,6 @@ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -25067,6 +23666,12 @@ "find-up": "^4.0.0" } }, + "pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true + }, "preferred-pm": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/preferred-pm/-/preferred-pm-3.0.3.tgz", @@ -25098,15 +23703,6 @@ "p-locate": "^5.0.0" } }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, "p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -25125,45 +23721,30 @@ "dev": true }, "prettier": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.1.tgz", - "integrity": "sha512-lqGoSJBQNJidqCHE80vqZJHWHRFoNYsSpP9AjFhlhi9ODCJA541svILes/+/1GM3VaL/abZi7cpFzOpdR9UPKg==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true }, "prettier-plugin-solidity": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.1.tgz", - "integrity": "sha512-uD24KO26tAHF+zMN2nt1OUzfknzza5AgxjogQQrMLZc7j8xiQrDoNWNeOlfFC0YLTwo12CLD10b9niLyP6AqXg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/prettier-plugin-solidity/-/prettier-plugin-solidity-1.1.3.tgz", + "integrity": "sha512-fQ9yucPi2sBbA2U2Xjh6m4isUTJ7S7QLc/XDDsktqqxYfTwdYKJ0EnnywXHwCGAaYbQNK+HIYPL1OemxuMsgeg==", "dev": true, "requires": { - "@solidity-parser/parser": "^0.14.5", + "@solidity-parser/parser": "^0.16.0", "semver": "^7.3.8", "solidity-comments-extractor": "^0.0.7" }, "dependencies": { - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "@solidity-parser/parser": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz", + "integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==", "dev": true, "requires": { - "yallist": "^4.0.0" + "antlr4ts": "^0.5.0-alpha.4" } - }, - "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true } } }, @@ -25179,12 +23760,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, "promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", @@ -25194,6 +23769,17 @@ "asap": "~2.0.6" } }, + "proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -25216,20 +23802,6 @@ "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", "dev": true }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -25253,9 +23825,9 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", "dev": true, "requires": { "side-channel": "^1.0.4" @@ -25279,9 +23851,9 @@ "dev": true }, "quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true }, "randombytes": { @@ -25293,16 +23865,6 @@ "safe-buffer": "^5.1.0" } }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -25310,9 +23872,9 @@ "dev": true }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dev": true, "requires": { "bytes": "3.1.2", @@ -25322,63 +23884,41 @@ } }, "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", "dev": true, "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" }, "dependencies": { - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", "dev": true } } }, "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", "dev": true, "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" }, "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true } } }, @@ -25392,20 +23932,12 @@ "js-yaml": "^3.6.1", "pify": "^4.0.1", "strip-bom": "^3.0.0" - }, - "dependencies": { - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true - } } }, "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "requires": { "inherits": "^2.0.3", @@ -25468,22 +24000,16 @@ "dev": true }, "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" } }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, "req-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/req-cwd/-/req-cwd-2.0.0.tgz", @@ -25606,9 +24132,9 @@ "dev": true }, "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true }, "responselike": { @@ -25628,15 +24154,11 @@ } } }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true }, "reusify": { "version": "1.0.4", @@ -25702,12 +24224,6 @@ } } }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true - }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -25732,13 +24248,24 @@ "integrity": "sha512-4VlvkRUuCJvr2J6Y0ImW7NvTCriMi7ErOAqWk1y69vAdoNIzCF3yPmgeNzx+RQTLEDFq5sHfscn1MwHxP9hNfA==", "dev": true }, - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "safe-array-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.0.tgz", + "integrity": "sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==", "dev": true, "requires": { - "tslib": "^1.9.0" + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } } }, "safe-buffer": { @@ -25786,12 +24313,6 @@ "wordwrap": "^1.0.0" }, "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", - "dev": true - }, "esprima": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", @@ -25861,9 +24382,9 @@ } }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -26135,38 +24656,44 @@ "dev": true }, "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, "dependencies": { "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true } } @@ -26191,11 +24718,14 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } }, "cliui": { "version": "6.0.0", @@ -26208,10 +24738,19 @@ "wrap-ansi": "^6.2.0" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "is-fullwidth-code-point": { @@ -26275,16 +24814,6 @@ "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } } } }, @@ -26333,11 +24862,15 @@ "wrap-ansi": "^2.0.0" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "dev": true, + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } }, "fs-extra": { "version": "0.30.0", @@ -26390,6 +24923,53 @@ "graceful-fs": "^4.1.6" } }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "dev": true, + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "dev": true, + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + } + }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -26488,304 +25068,99 @@ } }, "solhint": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.3.7.tgz", - "integrity": "sha512-NjjjVmXI3ehKkb3aNtRJWw55SUVJ8HMKKodwe0HnejA+k0d2kmhw7jvpa+MCTbcEgt8IWSwx0Hu6aCo/iYOZzQ==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/solhint/-/solhint-3.4.1.tgz", + "integrity": "sha512-pzZn2RlZhws1XwvLPVSsxfHrwsteFf5eySOhpAytzXwKQYbTCJV6z8EevYDiSVKMpWrvbKpEtJ055CuEmzp4Xg==", "dev": true, "requires": { - "@solidity-parser/parser": "^0.14.1", - "ajv": "^6.6.1", - "antlr4": "4.7.1", - "ast-parents": "0.0.1", - "chalk": "^2.4.2", - "commander": "2.18.0", - "cosmiconfig": "^5.0.7", - "eslint": "^5.6.0", - "fast-diff": "^1.1.2", - "glob": "^7.1.3", - "ignore": "^4.0.6", - "js-yaml": "^3.12.0", - "lodash": "^4.17.11", - "prettier": "^1.14.3", - "semver": "^6.3.0" + "@solidity-parser/parser": "^0.16.0", + "ajv": "^6.12.6", + "antlr4": "^4.11.0", + "ast-parents": "^0.0.1", + "chalk": "^4.1.2", + "commander": "^10.0.0", + "cosmiconfig": "^8.0.0", + "fast-diff": "^1.2.0", + "glob": "^8.0.3", + "ignore": "^5.2.4", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "pluralize": "^8.0.0", + "prettier": "^2.8.3", + "semver": "^6.3.0", + "strip-ansi": "^6.0.1", + "table": "^6.8.1", + "text-table": "^0.2.0" }, "dependencies": { - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "@solidity-parser/parser": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.16.0.tgz", + "integrity": "sha512-ESipEcHyRHg4Np4SqBCfcXwyxxna1DgFVz69bgpLV8vzl/NP1DtcKsJ4dJZXWQhY/Z4J2LeKBiOkOVZn9ct33Q==", + "dev": true, + "requires": { + "antlr4ts": "^0.5.0-alpha.4" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "color-convert": "^1.9.0" + "color-convert": "^2.0.1" } }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "requires": { - "color-name": "1.1.3" + "color-name": "~1.1.4" } }, "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "eslint": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz", - "integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.9.1", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^4.0.3", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^5.0.1", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob": "^7.1.2", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.2.2", - "js-yaml": "^3.13.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.11", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "path-is-inside": "^1.0.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^5.5.1", - "strip-ansi": "^4.0.0", - "strip-json-comments": "^2.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", - "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - }, - "espree": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz", - "integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==", - "dev": true, - "requires": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - } - }, - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true - }, - "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true, - "optional": true - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", - "dev": true - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" + "argparse": "^2.0.1" } }, "semver": { @@ -26794,60 +25169,30 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "shebang-regex": "^1.0.0" + "ansi-regex": "^5.0.1" } }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true - }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { - "has-flag": "^3.0.0" - } - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" + "has-flag": "^4.0.0" } } } }, "solidity-ast": { - "version": "0.4.40", - "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.40.tgz", - "integrity": "sha512-M8uLBT2jgFB7B0iVAC5a2l71J8vim7aEm03AZkaHbDqyrl1pE+i5PriMEw6WlwGfHp3/Ym7cn9BqvVLQgRk+Yw==", + "version": "0.4.49", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.49.tgz", + "integrity": "sha512-Pr5sCAj1SFqzwFZw1HPKSq0PehlQNdM8GwKyAVYh2DOn7/cCK8LUKD1HeHnKtTgBW7hi9h4nnnan7hpAg5RhWQ==", "dev": true }, "solidity-comments": { @@ -26984,32 +25329,12 @@ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", "dev": true }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, "chokidar": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz", @@ -27037,21 +25362,6 @@ "wrap-ansi": "^5.1.0" } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -27061,12 +25371,6 @@ "ms": "^2.1.1" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -27079,12 +25383,6 @@ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, "find-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", @@ -27122,24 +25420,34 @@ "optional": true }, "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true + "globby": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.2.tgz", + "integrity": "sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==", + "dev": true, + "requires": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + } }, "js-yaml": { "version": "3.13.1", @@ -27170,15 +25478,6 @@ "chalk": "^2.4.2" } }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, "mkdirp": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", @@ -27220,13 +25519,27 @@ "yargs-unparser": "1.6.0" }, "dependencies": { - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" } } } @@ -27249,6 +25562,15 @@ "object-keys": "^1.0.11" } }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, "p-locate": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", @@ -27300,9 +25622,9 @@ "dev": true }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", + "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", "dev": true, "requires": { "has-flag": "^3.0.0" @@ -27376,9 +25698,9 @@ } }, "solidity-docgen": { - "version": "0.6.0-beta.34", - "resolved": "https://registry.npmjs.org/solidity-docgen/-/solidity-docgen-0.6.0-beta.34.tgz", - "integrity": "sha512-igdGrkg8gT1jn+B2NwzjEtSf+7NTrSi/jz88zO7MZWgETmcWbXaxgAsQP4BQeC4YFeH0Pie1NsLP7+9qDgvFtA==", + "version": "0.6.0-beta.35", + "resolved": "https://registry.npmjs.org/solidity-docgen/-/solidity-docgen-0.6.0-beta.35.tgz", + "integrity": "sha512-9QdwK1THk/MWIdq1PEW/6dvtND0pUqpFTsbKwwU9YQIMYuRhH1lek9SsgnsGGYtdJ0VTrXXcVT30q20a8Y610A==", "dev": true, "requires": { "handlebars": "^4.7.7", @@ -27477,9 +25799,9 @@ } }, "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -27503,9 +25825,9 @@ } }, "spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "sprintf-js": { @@ -27608,6 +25930,17 @@ "strip-ansi": "^4.0.0" } }, + "string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + } + }, "string.prototype.trimend": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", @@ -27640,13 +25973,10 @@ } }, "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true }, "strip-hex-prefix": { "version": "1.0.0", @@ -27670,12 +26000,12 @@ "dev": true }, "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "has-flag": "^3.0.0" } }, "swap-case": { @@ -27773,6 +26103,12 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true + }, + "quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true } } }, @@ -27797,47 +26133,72 @@ } }, "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz", + "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==", "dev": true, "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true }, "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.1" } } } @@ -27902,12 +26263,6 @@ } } }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -27959,9 +26314,9 @@ }, "dependencies": { "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", "dev": true } } @@ -27997,18 +26352,18 @@ "dev": true }, "tty-table": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.1.6.tgz", - "integrity": "sha512-kRj5CBzOrakV4VRRY5kUWbNYvo/FpOsz65DzI5op9P+cHov3+IqPbo1JE1ZnQGkHdZgNFDsrEjrfqqy/Ply9fw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tty-table/-/tty-table-4.2.1.tgz", + "integrity": "sha512-xz0uKo+KakCQ+Dxj1D/tKn2FSyreSYWzdkL/BYhgN6oMW808g8QRMuh1atAV9fjTPbWBjfbkKQpI/5rEcnAc7g==", "dev": true, "requires": { "chalk": "^4.1.2", - "csv": "^5.5.0", - "kleur": "^4.1.4", + "csv": "^5.5.3", + "kleur": "^4.1.5", "smartwrap": "^2.0.2", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "wcwidth": "^1.0.1", - "yargs": "^17.1.1" + "yargs": "^17.7.1" }, "dependencies": { "ansi-regex": { @@ -28017,6 +26372,15 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -28027,6 +26391,27 @@ "supports-color": "^7.1.0" } }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -28035,6 +26420,15 @@ "requires": { "ansi-regex": "^5.0.1" } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } } } }, @@ -28096,6 +26490,17 @@ "mime-types": "~2.1.24" } }, + "typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + } + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -28143,9 +26548,9 @@ "dev": true }, "undici": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.14.0.tgz", - "integrity": "sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==", + "version": "5.22.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.1.tgz", + "integrity": "sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw==", "dev": true, "requires": { "busboy": "^1.6.0" @@ -28282,60 +26687,46 @@ } }, "web3": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3/-/web3-1.8.1.tgz", - "integrity": "sha512-tAqFsQhGv340C9OgRJIuoScN7f7wa1tUvsnnDUMt9YE6J4gcm7TV2Uwv+KERnzvV+xgdeuULYpsioRRNKrUvoQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3/-/web3-1.10.0.tgz", + "integrity": "sha512-YfKY9wSkGcM8seO+daR89oVTcbu18NsVfvOngzqMYGUU0pPSQmE57qQDvQzUeoIOHAnXEBNzrhjQJmm8ER0rng==", "dev": true, "requires": { - "web3-bzz": "1.8.1", - "web3-core": "1.8.1", - "web3-eth": "1.8.1", - "web3-eth-personal": "1.8.1", - "web3-net": "1.8.1", - "web3-shh": "1.8.1", - "web3-utils": "1.8.1" + "web3-bzz": "1.10.0", + "web3-core": "1.10.0", + "web3-eth": "1.10.0", + "web3-eth-personal": "1.10.0", + "web3-net": "1.10.0", + "web3-shh": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-bzz": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.8.1.tgz", - "integrity": "sha512-dJJHS84nvpoxv6ijTMkdUSlRr5beCXNtx4UZcrFLHBva8dT63QEtKdLyDt2AyMJJdVzTCk78uir/6XtVWrdS6w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-bzz/-/web3-bzz-1.10.0.tgz", + "integrity": "sha512-o9IR59io3pDUsXTsps5pO5hW1D5zBmg46iNc2t4j2DkaYHNdDLwk2IP9ukoM2wg47QILfPEJYzhTfkS/CcX0KA==", "dev": true, "requires": { "@types/node": "^12.12.6", "got": "12.1.0", "swarm-js": "^0.1.40" - }, - "dependencies": { - "@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - } } }, "web3-core": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.8.1.tgz", - "integrity": "sha512-LbRZlJH2N6nS3n3Eo9Y++25IvzMY7WvYnp4NM/Ajhh97dAdglYs6rToQ2DbL2RLvTYmTew4O/y9WmOk4nq9COw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core/-/web3-core-1.10.0.tgz", + "integrity": "sha512-fWySwqy2hn3TL89w5TM8wXF1Z2Q6frQTKHWmP0ppRQorEK8NcHJRfeMiv/mQlSKoTS1F6n/nv2uyZsixFycjYQ==", "dev": true, "requires": { - "@types/bn.js": "^5.1.0", + "@types/bn.js": "^5.1.1", "@types/node": "^12.12.6", "bignumber.js": "^9.0.0", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-requestmanager": "1.8.1", - "web3-utils": "1.8.1" + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-requestmanager": "1.10.0", + "web3-utils": "1.10.0" }, "dependencies": { - "@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - }, "bignumber.js": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.1.tgz", @@ -28345,107 +26736,106 @@ } }, "web3-core-helpers": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.8.1.tgz", - "integrity": "sha512-ClzNO6T1S1gifC+BThw0+GTfcsjLEY8T1qUp6Ly2+w4PntAdNtKahxWKApWJ0l9idqot/fFIDXwO3Euu7I0Xqw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-helpers/-/web3-core-helpers-1.10.0.tgz", + "integrity": "sha512-pIxAzFDS5vnbXvfvLSpaA1tfRykAe9adw43YCKsEYQwH0gCLL0kMLkaCX3q+Q8EVmAh+e1jWL/nl9U0de1+++g==", "dev": true, "requires": { - "web3-eth-iban": "1.8.1", - "web3-utils": "1.8.1" + "web3-eth-iban": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-core-method": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.8.1.tgz", - "integrity": "sha512-oYGRodktfs86NrnFwaWTbv2S38JnpPslFwSSARwFv4W9cjbGUW3LDeA5MKD/dRY+ssZ5OaekeMsUCLoGhX68yA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-method/-/web3-core-method-1.10.0.tgz", + "integrity": "sha512-4R700jTLAMKDMhQ+nsVfIXvH6IGJlJzGisIfMKWAIswH31h5AZz7uDUW2YctI+HrYd+5uOAlS4OJeeT9bIpvkA==", "dev": true, "requires": { "@ethersproject/transactions": "^5.6.2", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-utils": "1.8.1" + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-core-promievent": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.8.1.tgz", - "integrity": "sha512-9mxqHlgB0MrZI4oUIRFkuoJMNj3E7btjrMv3sMer/Z9rYR1PfoSc1aAokw4rxKIcAh+ylVtd/acaB2HKB7aRPg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-promievent/-/web3-core-promievent-1.10.0.tgz", + "integrity": "sha512-68N7k5LWL5R38xRaKFrTFT2pm2jBNFaM4GioS00YjAKXRQ3KjmhijOMG3TICz6Aa5+6GDWYelDNx21YAeZ4YTg==", "dev": true, "requires": { "eventemitter3": "4.0.4" } }, "web3-core-requestmanager": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.8.1.tgz", - "integrity": "sha512-x+VC2YPPwZ1khvqA6TA69LvfFCOZXsoUVOxmTx/vIN22PrY9KzKhxcE7pBSiGhmab1jtmRYXUbcQSVpAXqL8cw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-requestmanager/-/web3-core-requestmanager-1.10.0.tgz", + "integrity": "sha512-3z/JKE++Os62APml4dvBM+GAuId4h3L9ckUrj7ebEtS2AR0ixyQPbrBodgL91Sv7j7cQ3Y+hllaluqjguxvSaQ==", "dev": true, "requires": { - "util": "^0.12.0", - "web3-core-helpers": "1.8.1", - "web3-providers-http": "1.8.1", - "web3-providers-ipc": "1.8.1", - "web3-providers-ws": "1.8.1" + "util": "^0.12.5", + "web3-core-helpers": "1.10.0", + "web3-providers-http": "1.10.0", + "web3-providers-ipc": "1.10.0", + "web3-providers-ws": "1.10.0" } }, "web3-core-subscriptions": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.8.1.tgz", - "integrity": "sha512-bmCMq5OeA3E2vZUh8Js1HcJbhwtsE+yeMqGC4oIZB3XsL5SLqyKLB/pU+qUYqQ9o4GdcrFTDPhPg1bgvf7p1Pw==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-core-subscriptions/-/web3-core-subscriptions-1.10.0.tgz", + "integrity": "sha512-HGm1PbDqsxejI075gxBc5OSkwymilRWZufIy9zEpnWKNmfbuv5FfHgW1/chtJP6aP3Uq2vHkvTDl3smQBb8l+g==", "dev": true, "requires": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.10.0" } }, "web3-eth": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.8.1.tgz", - "integrity": "sha512-LgyzbhFqiFRd8M8sBXoFN4ztzOnkeckl3H/9lH5ek7AdoRMhBg7tYpYRP3E5qkhd/q+yiZmcUgy1AF6NHrC1wg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth/-/web3-eth-1.10.0.tgz", + "integrity": "sha512-Z5vT6slNMLPKuwRyKGbqeGYC87OAy8bOblaqRTgg94CXcn/mmqU7iPIlG4506YdcdK3x6cfEDG7B6w+jRxypKA==", "dev": true, "requires": { - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-eth-accounts": "1.8.1", - "web3-eth-contract": "1.8.1", - "web3-eth-ens": "1.8.1", - "web3-eth-iban": "1.8.1", - "web3-eth-personal": "1.8.1", - "web3-net": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-eth-accounts": "1.10.0", + "web3-eth-contract": "1.10.0", + "web3-eth-ens": "1.10.0", + "web3-eth-iban": "1.10.0", + "web3-eth-personal": "1.10.0", + "web3-net": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-eth-abi": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.8.1.tgz", - "integrity": "sha512-0mZvCRTIG0UhDhJwNQJgJxu4b4DyIpuMA0GTfqxqeuqzX4Q/ZvmoNurw0ExTfXaGPP82UUmmdkRi6FdZOx+C6w==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-abi/-/web3-eth-abi-1.10.0.tgz", + "integrity": "sha512-cwS+qRBWpJ43aI9L3JS88QYPfFcSJJ3XapxOQ4j40v6mk7ATpA8CVK1vGTzpihNlOfMVRBkR95oAj7oL6aiDOg==", "dev": true, "requires": { "@ethersproject/abi": "^5.6.3", - "web3-utils": "1.8.1" + "web3-utils": "1.10.0" } }, "web3-eth-accounts": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.8.1.tgz", - "integrity": "sha512-mgzxSYgN54/NsOFBO1Fq1KkXp1S5KlBvI/DlgvajU72rupoFMq6Cu6Yp9GUaZ/w2ij9PzEJuFJk174XwtfMCmg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-accounts/-/web3-eth-accounts-1.10.0.tgz", + "integrity": "sha512-wiq39Uc3mOI8rw24wE2n15hboLE0E9BsQLdlmsL4Zua9diDS6B5abXG0XhFcoNsXIGMWXVZz4TOq3u4EdpXF/Q==", "dev": true, "requires": { "@ethereumjs/common": "2.5.0", "@ethereumjs/tx": "3.3.2", - "crypto-browserify": "3.12.0", "eth-lib": "0.2.8", - "ethereumjs-util": "^7.0.10", + "ethereumjs-util": "^7.1.5", "scrypt-js": "^3.0.1", "uuid": "^9.0.0", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-utils": "1.10.0" }, "dependencies": { "eth-lib": { @@ -28468,45 +26858,45 @@ } }, "web3-eth-contract": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.8.1.tgz", - "integrity": "sha512-1wphnl+/xwCE2io44JKnN+ti3oa47BKRiVzvWd42icwRbcpFfRxH9QH+aQX3u8VZIISNH7dAkTWpGIIJgGFTmg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-contract/-/web3-eth-contract-1.10.0.tgz", + "integrity": "sha512-MIC5FOzP/+2evDksQQ/dpcXhSqa/2hFNytdl/x61IeWxhh6vlFeSjq0YVTAyIzdjwnL7nEmZpjfI6y6/Ufhy7w==", "dev": true, "requires": { - "@types/bn.js": "^5.1.0", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-utils": "1.8.1" + "@types/bn.js": "^5.1.1", + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-eth-ens": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.8.1.tgz", - "integrity": "sha512-FT8xTI9uN8RxeBQa/W8pLa2aoFh4+EE34w7W2271LICKzla1dtLyb6XSdn48vsUcPmhWsTVk9mO9RTU0l4LGQQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-ens/-/web3-eth-ens-1.10.0.tgz", + "integrity": "sha512-3hpGgzX3qjgxNAmqdrC2YUQMTfnZbs4GeLEmy8aCWziVwogbuqQZ+Gzdfrym45eOZodk+lmXyLuAdqkNlvkc1g==", "dev": true, "requires": { "content-hash": "^2.5.2", "eth-ens-namehash": "2.0.8", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-promievent": "1.8.1", - "web3-eth-abi": "1.8.1", - "web3-eth-contract": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-promievent": "1.10.0", + "web3-eth-abi": "1.10.0", + "web3-eth-contract": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-eth-iban": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.8.1.tgz", - "integrity": "sha512-DomoQBfvIdtM08RyMGkMVBOH0vpOIxSSQ+jukWk/EkMLGMWJtXw/K2c2uHAeq3L/VPWNB7zXV2DUEGV/lNE2Dg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-iban/-/web3-eth-iban-1.10.0.tgz", + "integrity": "sha512-0l+SP3IGhInw7Q20LY3IVafYEuufo4Dn75jAHT7c2aDJsIolvf2Lc6ugHkBajlwUneGfbRQs/ccYPQ9JeMUbrg==", "dev": true, "requires": { "bn.js": "^5.2.1", - "web3-utils": "1.8.1" + "web3-utils": "1.10.0" }, "dependencies": { "bn.js": { @@ -28518,87 +26908,79 @@ } }, "web3-eth-personal": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.8.1.tgz", - "integrity": "sha512-myIYMvj7SDIoV9vE5BkVdon3pya1WinaXItugoii2VoTcQNPOtBxmYVH+XS5ErzCJlnxzphpQrkywyY64bbbCA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-eth-personal/-/web3-eth-personal-1.10.0.tgz", + "integrity": "sha512-anseKn98w/d703eWq52uNuZi7GhQeVjTC5/svrBWEKob0WZ5kPdo+EZoFN0sp5a5ubbrk/E0xSl1/M5yORMtpg==", "dev": true, "requires": { "@types/node": "^12.12.6", - "web3-core": "1.8.1", - "web3-core-helpers": "1.8.1", - "web3-core-method": "1.8.1", - "web3-net": "1.8.1", - "web3-utils": "1.8.1" - }, - "dependencies": { - "@types/node": { - "version": "12.20.55", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", - "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", - "dev": true - } + "web3-core": "1.10.0", + "web3-core-helpers": "1.10.0", + "web3-core-method": "1.10.0", + "web3-net": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-net": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.8.1.tgz", - "integrity": "sha512-LyEJAwogdFo0UAXZqoSJGFjopdt+kLw0P00FSZn2yszbgcoI7EwC+nXiOsEe12xz4LqpYLOtbR7+gxgiTVjjHQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-net/-/web3-net-1.10.0.tgz", + "integrity": "sha512-NLH/N3IshYWASpxk4/18Ge6n60GEvWBVeM8inx2dmZJVmRI6SJIlUxbL8jySgiTn3MMZlhbdvrGo8fpUW7a1GA==", "dev": true, "requires": { - "web3-core": "1.8.1", - "web3-core-method": "1.8.1", - "web3-utils": "1.8.1" + "web3-core": "1.10.0", + "web3-core-method": "1.10.0", + "web3-utils": "1.10.0" } }, "web3-providers-http": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.8.1.tgz", - "integrity": "sha512-1Zyts4O9W/UNEPkp+jyL19Jc3D15S4yp8xuLTjVhcUEAlHo24NDWEKxtZGUuHk4HrKL2gp8OlsDbJ7MM+ESDgg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-http/-/web3-providers-http-1.10.0.tgz", + "integrity": "sha512-eNr965YB8a9mLiNrkjAWNAPXgmQWfpBfkkn7tpEFlghfww0u3I0tktMZiaToJVcL2+Xq+81cxbkpeWJ5XQDwOA==", "dev": true, "requires": { "abortcontroller-polyfill": "^1.7.3", "cross-fetch": "^3.1.4", "es6-promise": "^4.2.8", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.10.0" } }, "web3-providers-ipc": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.8.1.tgz", - "integrity": "sha512-nw/W5nclvi+P2z2dYkLWReKLnocStflWqFl+qjtv0xn3MrUTyXMzSF0+61i77+16xFsTgzo4wS/NWIOVkR0EFA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-ipc/-/web3-providers-ipc-1.10.0.tgz", + "integrity": "sha512-OfXG1aWN8L1OUqppshzq8YISkWrYHaATW9H8eh0p89TlWMc1KZOL9vttBuaBEi96D/n0eYDn2trzt22bqHWfXA==", "dev": true, "requires": { "oboe": "2.1.5", - "web3-core-helpers": "1.8.1" + "web3-core-helpers": "1.10.0" } }, "web3-providers-ws": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.8.1.tgz", - "integrity": "sha512-TNefIDAMpdx57+YdWpYZ/xdofS0P+FfKaDYXhn24ie/tH9G+AB+UBSOKnjN0KSadcRSCMBwGPRiEmNHPavZdsA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-providers-ws/-/web3-providers-ws-1.10.0.tgz", + "integrity": "sha512-sK0fNcglW36yD5xjnjtSGBnEtf59cbw4vZzJ+CmOWIKGIR96mP5l684g0WD0Eo+f4NQc2anWWXG74lRc9OVMCQ==", "dev": true, "requires": { "eventemitter3": "4.0.4", - "web3-core-helpers": "1.8.1", + "web3-core-helpers": "1.10.0", "websocket": "^1.0.32" } }, "web3-shh": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.8.1.tgz", - "integrity": "sha512-sqHgarnfcY2Qt3PYS4R6YveHrDy7hmL09yeLLHHCI+RKirmjLVqV0rc5LJWUtlbYI+kDoa5gbgde489M9ZAC0g==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-shh/-/web3-shh-1.10.0.tgz", + "integrity": "sha512-uNUUuNsO2AjX41GJARV9zJibs11eq6HtOe6Wr0FtRUcj8SN6nHeYIzwstAvJ4fXA53gRqFMTxdntHEt9aXVjpg==", "dev": true, "requires": { - "web3-core": "1.8.1", - "web3-core-method": "1.8.1", - "web3-core-subscriptions": "1.8.1", - "web3-net": "1.8.1" + "web3-core": "1.10.0", + "web3-core-method": "1.10.0", + "web3-core-subscriptions": "1.10.0", + "web3-net": "1.10.0" } }, "web3-utils": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.8.1.tgz", - "integrity": "sha512-LgnM9p6V7rHHUGfpMZod+NST8cRfGzJ1BTXAyNo7A9cJX9LczBfSRxJp+U/GInYe9mby40t3v22AJdlELibnsQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.10.0.tgz", + "integrity": "sha512-kSaCM0uMcZTNUSmn5vMEhlo02RObGNRRCkdX0V9UTAU0+lrvn0HSaudyCo6CQzuXUsnuY2ERJGCGPfeWmv19Rg==", "dev": true, "requires": { "bn.js": "^5.2.1", @@ -28688,9 +27070,9 @@ } }, "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "dev": true }, "which-pm": { @@ -28767,6 +27149,30 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -28801,15 +27207,6 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, "ws": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", @@ -28884,9 +27281,9 @@ "dev": true }, "yargs": { - "version": "17.6.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", - "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, "requires": { "cliui": "^8.0.1", @@ -28939,10 +27336,22 @@ } }, "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + } + } }, "yargs-unparser": { "version": "2.0.0", @@ -28961,6 +27370,18 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true + }, + "decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true + }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true } } }, diff --git a/package.json b/package.json index 72978ffec..37e8f8710 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openzeppelin-solidity", "description": "Secure Smart Contract library for Solidity", - "version": "4.8.0", + "version": "4.9.2", "files": [ "/contracts/**/*.sol", "/build/contracts/*.json", @@ -17,7 +17,7 @@ "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", "lint:js": "prettier --loglevel warn --ignore-path .gitignore '**/*.{js,ts}' --check && eslint --ignore-path .gitignore .", "lint:js:fix": "prettier --loglevel warn --ignore-path .gitignore '**/*.{js,ts}' --write && eslint --ignore-path .gitignore . --fix", - "lint:sol": "prettier --loglevel warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write && solhint '{contracts,test}/**/*.sol'", + "lint:sol": "prettier --loglevel warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'", "lint:sol:fix": "prettier --loglevel warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write", "clean": "hardhat clean && rimraf build contracts/build", "prepare": "scripts/prepare.sh", @@ -29,7 +29,7 @@ "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" + "slither": "npm run clean && slither ." }, "repository": { "type": "git", @@ -59,6 +59,7 @@ "@nomiclabs/hardhat-web3": "^2.0.0", "@openzeppelin/docs-utils": "^0.1.3", "@openzeppelin/test-helpers": "^0.5.13", + "@openzeppelin/upgrades-core": "^1.20.6", "array.prototype.at": "^1.1.1", "chai": "^4.2.0", "eslint": "^8.30.0", @@ -69,7 +70,7 @@ "glob": "^8.0.3", "graphlib": "^2.1.8", "hardhat": "^2.9.1", - "hardhat-exposed": "^0.3.0", + "hardhat-exposed": "^0.3.3", "hardhat-gas-reporter": "^1.0.4", "hardhat-ignore-warnings": "^0.2.0", "keccak256": "^1.0.2", @@ -77,6 +78,7 @@ "lodash.zip": "^4.2.0", "merkletreejs": "^0.2.13", "micromatch": "^4.0.2", + "p-limit": "^3.1.0", "prettier": "^2.8.1", "prettier-plugin-solidity": "^1.1.0", "rimraf": "^3.0.2", @@ -85,6 +87,7 @@ "solidity-ast": "^0.4.25", "solidity-coverage": "^0.8.0", "solidity-docgen": "^0.6.0-beta.29", + "undici": "^5.22.1", "web3": "^1.3.0", "yargs": "^17.0.0" } diff --git a/remappings.txt b/remappings.txt new file mode 100644 index 000000000..2479e3d26 --- /dev/null +++ b/remappings.txt @@ -0,0 +1 @@ +openzeppelin/=contracts/ diff --git a/renovate.json b/renovate.json index 875bf6ec4..c0b97d8d7 100644 --- a/renovate.json +++ b/renovate.json @@ -1,4 +1,4 @@ { "extends": ["github>OpenZeppelin/configs"], - "labels": ["ignore-changelog"] + "labels": ["ignore-changeset"] } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..da3e95766 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +certora-cli==3.6.4 diff --git a/scripts/checks/compare-layout.js b/scripts/checks/compare-layout.js new file mode 100644 index 000000000..4368b77fb --- /dev/null +++ b/scripts/checks/compare-layout.js @@ -0,0 +1,20 @@ +const fs = require('fs'); +const { getStorageUpgradeReport } = require('@openzeppelin/upgrades-core/dist/storage'); + +const { ref, head } = require('yargs').argv; + +const oldLayout = JSON.parse(fs.readFileSync(ref)); +const newLayout = JSON.parse(fs.readFileSync(head)); + +for (const name in oldLayout) { + if (name in newLayout) { + const report = getStorageUpgradeReport(oldLayout[name], newLayout[name], {}); + if (!report.ok) { + console.log(`Storage layout incompatilibity found in ${name}:`); + console.log(report.explain()); + process.exitCode = 1; + } + } else { + console.log(`WARNING: ${name} is missing from the current branch`); + } +} diff --git a/scripts/checks/compareGasReports.js b/scripts/checks/compareGasReports.js index ca99dea0d..160c8cc52 100755 --- a/scripts/checks/compareGasReports.js +++ b/scripts/checks/compareGasReports.js @@ -10,6 +10,14 @@ const { argv } = require('yargs') choices: ['shell', 'markdown'], default: 'shell', }, + hideEqual: { + type: 'boolean', + default: true, + }, + strictTesting: { + type: 'boolean', + default: false, + }, }); // Deduce base tx cost from the percentage denominator @@ -40,7 +48,7 @@ class Report { } // Compare two reports - static compare(update, ref, opts = { hideEqual: true }) { + static compare(update, ref, opts = { hideEqual: true, strictTesting: false }) { if (JSON.stringify(update.config.metadata) !== JSON.stringify(ref.config.metadata)) { throw new Error('Reports produced with non matching metadata'); } @@ -70,7 +78,9 @@ class Report { const methods = Object.keys(update.info.methods) .filter(key => ref.info.methods[key]) .filter(key => update.info.methods[key].numberOfCalls > 0) - .filter(key => update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls) + .filter( + key => !opts.strictTesting || update.info.methods[key].numberOfCalls === ref.info.methods[key].numberOfCalls, + ) .map(key => ({ contract: ref.info.methods[key].contract, method: ref.info.methods[key].fnSig, @@ -220,7 +230,7 @@ function formatCmpMarkdown(rows) { } // MAIN -const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1])); +const report = Report.compare(Report.load(argv._[0]), Report.load(argv._[1]), argv); switch (argv.style) { case 'markdown': diff --git a/scripts/checks/extract-layout.js b/scripts/checks/extract-layout.js new file mode 100644 index 000000000..d0c9cf36b --- /dev/null +++ b/scripts/checks/extract-layout.js @@ -0,0 +1,40 @@ +const fs = require('fs'); +const { findAll } = require('solidity-ast/utils'); +const { astDereferencer } = require('@openzeppelin/upgrades-core/dist/ast-dereferencer'); +const { solcInputOutputDecoder } = require('@openzeppelin/upgrades-core/dist/src-decoder'); +const { extractStorageLayout } = require('@openzeppelin/upgrades-core/dist/storage/extract'); + +const { _ } = require('yargs').argv; + +const skipPath = ['contracts/mocks/', 'contracts-exposed/']; +const skipKind = ['interface', 'library']; + +function extractLayouts(path) { + const layout = {}; + const { input, output } = JSON.parse(fs.readFileSync(path)); + + const decoder = solcInputOutputDecoder(input, output); + const deref = astDereferencer(output); + + for (const src in output.contracts) { + if (skipPath.some(prefix => src.startsWith(prefix))) { + continue; + } + + for (const contractDef of findAll('ContractDefinition', output.sources[src].ast)) { + if (skipKind.includes(contractDef.contractKind)) { + continue; + } + + layout[contractDef.name] = extractStorageLayout( + contractDef, + decoder, + deref, + output.contracts[src][contractDef.name].storageLayout, + ); + } + } + return layout; +} + +console.log(JSON.stringify(Object.assign(..._.map(extractLayouts)))); diff --git a/scripts/checks/inheritance-ordering.js b/scripts/checks/inheritance-ordering.js index 45c707e6f..72aa37ef7 100755 --- a/scripts/checks/inheritance-ordering.js +++ b/scripts/checks/inheritance-ordering.js @@ -13,7 +13,7 @@ for (const artifact of artifacts) { const linearized = []; for (const source in solcOutput.contracts) { - if (source.includes('/mocks/')) { + if (['contracts-exposed/', 'contracts/mocks/'].some(pattern => source.startsWith(pattern))) { continue; } diff --git a/scripts/generate/run.js b/scripts/generate/run.js index a34823223..53589455a 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -13,15 +13,10 @@ function getVersion(path) { } } -for (const [file, template] of Object.entries({ - 'utils/math/SafeCast.sol': './templates/SafeCast.js', - 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', - 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', - 'utils/Checkpoints.sol': './templates/Checkpoints.js', -})) { +function generateFromTemplate(file, template, outputPrefix = '') { const script = path.relative(path.join(__dirname, '../..'), __filename); const input = path.join(path.dirname(script), template); - const output = `./contracts/${file}`; + const output = path.join(outputPrefix, file); const version = getVersion(output); const content = format( '// SPDX-License-Identifier: MIT', @@ -34,3 +29,21 @@ for (const [file, template] of Object.entries({ fs.writeFileSync(output, content); cp.execFileSync('prettier', ['--write', output]); } + +// Contracts +for (const [file, template] of Object.entries({ + 'utils/math/SafeCast.sol': './templates/SafeCast.js', + 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', + 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', + 'utils/structs/Checkpoints.sol': './templates/Checkpoints.js', + 'utils/StorageSlot.sol': './templates/StorageSlot.js', +})) { + generateFromTemplate(file, template, './contracts/'); +} + +// Tests +for (const [file, template] of Object.entries({ + 'utils/structs/Checkpoints.t.sol': './templates/Checkpoints.t.js', +})) { + generateFromTemplate(file, template, './test/'); +} diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index c4673d245..d28134ce7 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -1,12 +1,12 @@ const format = require('../format-lines'); +const { OPTS } = require('./Checkpoints.opts.js'); -const VALUE_SIZES = [224, 160]; - +// TEMPLATE const header = `\ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import "./math/Math.sol"; -import "./math/SafeCast.sol"; +import "../math/Math.sol"; +import "../math/SafeCast.sol"; /** * @dev This library defines the \`History\` struct, for checkpointing values as they change at different points in @@ -19,7 +19,14 @@ import "./math/SafeCast.sol"; */ `; -const types = opts => `\ +const errors = `\ + /** + * @dev A value was attempted to be inserted on a past checkpoint. + */ + error CheckpointUnorderedInsertion(); +`; + +const template = opts => `\ struct ${opts.historyTypeName} { ${opts.checkpointTypeName}[] ${opts.checkpointFieldName}; } @@ -28,10 +35,7 @@ struct ${opts.checkpointTypeName} { ${opts.keyTypeName} ${opts.keyFieldName}; ${opts.valueTypeName} ${opts.valueFieldName}; } -`; -/* eslint-disable max-len */ -const operations = opts => `\ /** * @dev Pushes a (\`key\`, \`value\`) pair into a ${opts.historyTypeName} so that it is stored as the checkpoint. * @@ -46,7 +50,7 @@ function push( } /** - * @dev Returns the value in the oldest checkpoint with key greater or equal than the search key, or zero if there is none. + * @dev Returns the value in the first (oldest) checkpoint with key greater or equal than the search key, or zero if there is none. */ function lowerLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) { uint256 len = self.${opts.checkpointFieldName}.length; @@ -55,40 +59,20 @@ function lowerLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} k } /** - * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. */ function upperLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) { uint256 len = self.${opts.checkpointFieldName}.length; uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len); return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName}; } -`; - -const legacyOperations = opts => `\ -/** - * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one - * before it is returned, or zero otherwise. Because the number returned corresponds to that at the end of the - * block, the requested block number must be in the past, excluding the current block. - */ -function getAtBlock(${opts.historyTypeName} storage self, uint256 blockNumber) internal view returns (uint256) { - require(blockNumber < block.number, "Checkpoints: block not yet mined"); - uint32 key = SafeCast.toUint32(blockNumber); - - uint256 len = self.${opts.checkpointFieldName}.length; - uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, len); - return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName}; -} /** - * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one - * before it is returned, or zero otherwise. Similar to {upperLookup} but optimized for the case when the searched - * checkpoint is probably "recent", defined as being among the last sqrt(N) checkpoints where N is the number of - * checkpoints. + * @dev Returns the value in the last (most recent) checkpoint with key lower or equal than the search key, or zero if there is none. + * + * NOTE: This is a variant of {upperLookup} that is optimised to find "recent" checkpoint (checkpoints with high keys). */ -function getAtProbablyRecentBlock(${opts.historyTypeName} storage self, uint256 blockNumber) internal view returns (uint256) { - require(blockNumber < block.number, "Checkpoints: block not yet mined"); - uint32 key = SafeCast.toUint32(blockNumber); - +function upperLookupRecent(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) { uint256 len = self.${opts.checkpointFieldName}.length; uint256 low = 0; @@ -96,7 +80,7 @@ function getAtProbablyRecentBlock(${opts.historyTypeName} storage self, uint256 if (len > 5) { uint256 mid = len - Math.sqrt(len); - if (key < _unsafeAccess(self.${opts.checkpointFieldName}, mid)._blockNumber) { + if (key < _unsafeAccess(self.${opts.checkpointFieldName}, mid)._key) { high = mid; } else { low = mid + 1; @@ -108,38 +92,6 @@ function getAtProbablyRecentBlock(${opts.historyTypeName} storage self, uint256 return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName}; } -/** - * @dev Returns checkpoint at given position. - */ -function getAtPosition(History storage self, uint32 pos) internal view returns (Checkpoint memory) { - return self._checkpoints[pos]; -} - -/** - * @dev Pushes a value onto a History so that it is stored as the checkpoint for the current block. - * - * Returns previous value and new value. - */ -function push(${opts.historyTypeName} storage self, uint256 value) internal returns (uint256, uint256) { - return _insert(self.${opts.checkpointFieldName}, SafeCast.toUint32(block.number), SafeCast.toUint224(value)); -} - -/** - * @dev Pushes a value onto a History, by updating the latest value using binary operation \`op\`. The new value will - * be set to \`op(latest, delta)\`. - * - * Returns previous value and new value. - */ -function push( - ${opts.historyTypeName} storage self, - function(uint256, uint256) view returns (uint256) op, - uint256 delta -) internal returns (uint256, uint256) { - return push(self, op(latest(self), delta)); -} -`; - -const common = opts => `\ /** * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints. */ @@ -177,6 +129,13 @@ function length(${opts.historyTypeName} storage self) internal view returns (uin return self.${opts.checkpointFieldName}.length; } +/** + * @dev Returns checkpoint at given position. + */ +function at(${opts.historyTypeName} storage self, uint32 pos) internal view returns (${opts.checkpointTypeName} memory) { + return self.${opts.checkpointFieldName}[pos]; +} + /** * @dev Pushes a (\`key\`, \`value\`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, * or by updating the last one. @@ -193,7 +152,9 @@ function _insert( ${opts.checkpointTypeName} memory last = _unsafeAccess(self, pos - 1); // Checkpoint keys must be non-decreasing. - require(last.${opts.keyFieldName} <= key, "Checkpoint: decreasing keys"); + if(last.${opts.keyFieldName} > key) { + revert CheckpointUnorderedInsertion(); + } // Update or push new checkpoint if (last.${opts.keyFieldName} == key) { @@ -209,7 +170,7 @@ function _insert( } /** - * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or \`high\` if there is none. + * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or \`high\` if there is none. * \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`. * * WARNING: \`high\` should not be greater than the array's length. @@ -232,7 +193,7 @@ function _upperBinaryLookup( } /** - * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or \`high\` if there is none. + * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or \`high\` if there is none. * \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`. * * WARNING: \`high\` should not be greater than the array's length. @@ -270,37 +231,11 @@ function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos) `; /* eslint-enable max-len */ -// OPTIONS -const defaultOpts = size => ({ - historyTypeName: `Trace${size}`, - checkpointTypeName: `Checkpoint${size}`, - checkpointFieldName: '_checkpoints', - keyTypeName: `uint${256 - size}`, - keyFieldName: '_key', - valueTypeName: `uint${size}`, - valueFieldName: '_value', -}); - -const OPTS = VALUE_SIZES.map(size => defaultOpts(size)); - -const LEGACY_OPTS = { - ...defaultOpts(224), - historyTypeName: 'History', - checkpointTypeName: 'Checkpoint', - keyFieldName: '_blockNumber', -}; - // GENERATE module.exports = format( header.trimEnd(), 'library Checkpoints {', - [ - // Legacy types & functions - types(LEGACY_OPTS), - legacyOperations(LEGACY_OPTS), - common(LEGACY_OPTS), - // New flavors - ...OPTS.flatMap(opts => [types(opts), operations(opts), common(opts)]), - ], + errors, + OPTS.flatMap(opts => template(opts)), '}', ); diff --git a/scripts/generate/templates/Checkpoints.opts.js b/scripts/generate/templates/Checkpoints.opts.js new file mode 100644 index 000000000..b8be23104 --- /dev/null +++ b/scripts/generate/templates/Checkpoints.opts.js @@ -0,0 +1,17 @@ +// OPTIONS +const VALUE_SIZES = [224, 160]; + +const defaultOpts = size => ({ + historyTypeName: `Trace${size}`, + checkpointTypeName: `Checkpoint${size}`, + checkpointFieldName: '_checkpoints', + keyTypeName: `uint${256 - size}`, + keyFieldName: '_key', + valueTypeName: `uint${size}`, + valueFieldName: '_value', +}); + +module.exports = { + VALUE_SIZES, + OPTS: VALUE_SIZES.map(size => defaultOpts(size)), +}; diff --git a/scripts/generate/templates/Checkpoints.t.js b/scripts/generate/templates/Checkpoints.t.js new file mode 100644 index 000000000..c5b225f38 --- /dev/null +++ b/scripts/generate/templates/Checkpoints.t.js @@ -0,0 +1,146 @@ +const format = require('../format-lines'); +const { capitalize } = require('../../helpers'); +const { OPTS } = require('./Checkpoints.opts.js'); + +// TEMPLATE +const header = `\ +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../../contracts/utils/math/SafeCast.sol"; +import "../../../contracts/utils/structs/Checkpoints.sol"; +`; + +/* eslint-disable max-len */ +const template = opts => `\ +using Checkpoints for Checkpoints.${opts.historyTypeName}; + +// Maximum gap between keys used during the fuzzing tests: the \`_prepareKeys\` function with make sure that +// key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range. +uint8 internal constant _KEY_MAX_GAP = 64; + +Checkpoints.${opts.historyTypeName} internal _ckpts; + +// helpers +function _bound${capitalize(opts.keyTypeName)}( + ${opts.keyTypeName} x, + ${opts.keyTypeName} min, + ${opts.keyTypeName} max +) internal view returns (${opts.keyTypeName}) { + return SafeCast.to${capitalize(opts.keyTypeName)}(bound(uint256(x), uint256(min), uint256(max))); +} + +function _prepareKeys( + ${opts.keyTypeName}[] memory keys, + ${opts.keyTypeName} maxSpread +) internal view { + ${opts.keyTypeName} lastKey = 0; + for (uint256 i = 0; i < keys.length; ++i) { + ${opts.keyTypeName} key = _bound${capitalize(opts.keyTypeName)}(keys[i], lastKey, lastKey + maxSpread); + keys[i] = key; + lastKey = key; + } +} + +function _assertLatestCheckpoint( + bool exist, + ${opts.keyTypeName} key, + ${opts.valueTypeName} value +) internal { + (bool _exist, ${opts.keyTypeName} _key, ${opts.valueTypeName} _value) = _ckpts.latestCheckpoint(); + assertEq(_exist, exist); + assertEq(_key, key); + assertEq(_value, value); +} + +// tests +function testPush( + ${opts.keyTypeName}[] memory keys, + ${opts.valueTypeName}[] memory values, + ${opts.keyTypeName} pastKey +) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + // initial state + assertEq(_ckpts.length(), 0); + assertEq(_ckpts.latest(), 0); + _assertLatestCheckpoint(false, 0, 0); + + uint256 duplicates = 0; + for (uint256 i = 0; i < keys.length; ++i) { + ${opts.keyTypeName} key = keys[i]; + ${opts.valueTypeName} value = values[i % values.length]; + if (i > 0 && key == keys[i-1]) ++duplicates; + + // push + _ckpts.push(key, value); + + // check length & latest + assertEq(_ckpts.length(), i + 1 - duplicates); + assertEq(_ckpts.latest(), value); + _assertLatestCheckpoint(true, key, value); + } + + if (keys.length > 0) { + ${opts.keyTypeName} lastKey = keys[keys.length - 1]; + if (lastKey > 0) { + pastKey = _bound${capitalize(opts.keyTypeName)}(pastKey, 0, lastKey - 1); + + vm.expectRevert(); + this.push(pastKey, values[keys.length % values.length]); + } + } +} + +// used to test reverts +function push(${opts.keyTypeName} key, ${opts.valueTypeName} value) external { + _ckpts.push(key, value); +} + +function testLookup( + ${opts.keyTypeName}[] memory keys, + ${opts.valueTypeName}[] memory values, + ${opts.keyTypeName} lookup +) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + ${opts.keyTypeName} lastKey = keys.length == 0 ? 0 : keys[keys.length - 1]; + lookup = _bound${capitalize(opts.keyTypeName)}(lookup, 0, lastKey + _KEY_MAX_GAP); + + ${opts.valueTypeName} upper = 0; + ${opts.valueTypeName} lower = 0; + ${opts.keyTypeName} lowerKey = type(${opts.keyTypeName}).max; + for (uint256 i = 0; i < keys.length; ++i) { + ${opts.keyTypeName} key = keys[i]; + ${opts.valueTypeName} value = values[i % values.length]; + + // push + _ckpts.push(key, value); + + // track expected result of lookups + if (key <= lookup) { + upper = value; + } + // find the first key that is not smaller than the lookup key + if (key >= lookup && (i == 0 || keys[i-1] < lookup)) { + lowerKey = key; + } + if (key == lowerKey) { + lower = value; + } + } + + // check lookup + assertEq(_ckpts.lowerLookup(lookup), lower); + assertEq(_ckpts.upperLookup(lookup), upper); + assertEq(_ckpts.upperLookupRecent(lookup), upper); +} +`; + +// GENERATE +module.exports = format( + header, + ...OPTS.flatMap(opts => [`contract Checkpoints${opts.historyTypeName}Test is Test {`, [template(opts)], '}']), +); diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index dbd502a0b..8899f4819 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -10,7 +10,7 @@ const TYPES = [ /* eslint-disable max-len */ const header = `\ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "./EnumerableSet.sol"; @@ -25,7 +25,7 @@ import "./EnumerableSet.sol"; * (O(1)). * - Entries are enumerated in O(n). No guarantees are made on the ordering. * - * \`\`\` + * \`\`\`solidity * contract Example { * // Add the library methods * using EnumerableMap for EnumerableMap.UintToAddressMap; @@ -66,6 +66,11 @@ const defaultMap = () => `\ // This means that we can only create new EnumerableMaps for types that fit // in bytes32. +/** + * @dev Query for a nonexistent map key. + */ +error EnumerableMapNonexistentKey(bytes32 key); + struct Bytes32ToBytes32Map { // Storage of keys EnumerableSet.Bytes32Set _keys; @@ -149,23 +154,9 @@ function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view retu */ function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) { bytes32 value = map._values[key]; - require(value != 0 || contains(map, key), "EnumerableMap: nonexistent key"); - return value; -} - -/** - * @dev Same as {get}, with a custom error message when \`key\` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ -function get( - Bytes32ToBytes32Map storage map, - bytes32 key, - string memory errorMessage -) internal view returns (bytes32) { - bytes32 value = map._values[key]; - require(value != 0 || contains(map, key), errorMessage); + if(value == 0 && !contains(map, key)) { + revert EnumerableMapNonexistentKey(key); + } return value; } @@ -261,20 +252,6 @@ function get(${name} storage map, ${keyType} key) internal view returns (${value return ${fromBytes32(valueType, `get(map._inner, ${toBytes32(keyType, 'key')})`)}; } -/** - * @dev Same as {get}, with a custom error message when \`key\` is not in the map. - * - * CAUTION: This function is deprecated because it requires allocating memory for the error - * message unnecessarily. For custom revert reasons use {tryGet}. - */ -function get( - ${name} storage map, - ${keyType} key, - string memory errorMessage -) internal view returns (${valueType}) { - return ${fromBytes32(valueType, `get(map._inner, ${toBytes32(keyType, 'key')}, errorMessage)`)}; -} - /** * @dev Return the an array containing all the keys * diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index a3af2c304..1edab9162 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -9,7 +9,7 @@ const TYPES = [ /* eslint-disable max-len */ const header = `\ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Library for managing @@ -22,7 +22,7 @@ pragma solidity ^0.8.0; * (O(1)). * - Elements are enumerated in O(n). No guarantees are made on the ordering. * - * \`\`\` + * \`\`\`solidity * contract Example { * // Add the library methods * using EnumerableSet for EnumerableSet.AddressSet; diff --git a/scripts/generate/templates/SafeCast.js b/scripts/generate/templates/SafeCast.js index 3e2b148fd..6a4a80c2b 100644 --- a/scripts/generate/templates/SafeCast.js +++ b/scripts/generate/templates/SafeCast.js @@ -61,7 +61,7 @@ const version = (selector, length) => { }; const header = `\ -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; /** * @dev Wrappers over Solidity's uintXX/intXX casting operators with added overflow @@ -74,12 +74,31 @@ pragma solidity ^0.8.0; * * Using this library instead of the unchecked operations eliminates an entire * class of bugs, so it's recommended to use it always. - * - * Can be combined with {SafeMath} and {SignedSafeMath} to extend it to smaller types, by performing - * all math on \`uint256\` and \`int256\` and then downcasting. */ `; +const errors = `\ + /** + * @dev Value doesn't fit in an uint of \`bits\` size. + */ + error SafeCastOverflowedUintDowncast(uint8 bits, uint256 value); + + /** + * @dev An int value doesn't fit in an uint of \`bits\` size. + */ + error SafeCastOverflowedIntToUint(int256 value); + + /** + * @dev Value doesn't fit in an int of \`bits\` size. + */ + error SafeCastOverflowedIntDowncast(uint8 bits, int256 value); + + /** + * @dev An uint value doesn't fit in an int of \`bits\` size. + */ + error SafeCastOverflowedUintToInt(uint256 value); +`; + const toUintDownCast = length => `\ /** * @dev Returns the downcasted uint${length} from uint256, reverting on @@ -94,7 +113,9 @@ const toUintDownCast = length => `\ * _Available since v${version('toUint(uint)', length)}._ */ function toUint${length}(uint256 value) internal pure returns (uint${length}) { - require(value <= type(uint${length}).max, "SafeCast: value doesn't fit in ${length} bits"); + if (value > type(uint${length}).max) { + revert SafeCastOverflowedUintDowncast(${length}, value); + } return uint${length}(value); } `; @@ -116,7 +137,9 @@ const toIntDownCast = length => `\ */ function toInt${length}(int256 value) internal pure returns (int${length} downcasted) { downcasted = int${length}(value); - require(downcasted == value, "SafeCast: value doesn't fit in ${length} bits"); + if (downcasted != value) { + revert SafeCastOverflowedIntDowncast(${length}, value); + } } `; /* eslint-enable max-len */ @@ -133,7 +156,9 @@ const toInt = length => `\ */ function toInt${length}(uint${length} value) internal pure returns (int${length}) { // Note: Unsafe cast below is okay because \`type(int${length}).max\` is guaranteed to be positive - require(value <= uint${length}(type(int${length}).max), "SafeCast: value doesn't fit in an int${length}"); + if (value > uint${length}(type(int${length}).max)) { + revert SafeCastOverflowedUintToInt(value); + } return int${length}(value); } `; @@ -149,7 +174,9 @@ const toUint = length => `\ * _Available since v${version('toUint(int)', length)}._ */ function toUint${length}(int${length} value) internal pure returns (uint${length}) { - require(value >= 0, "SafeCast: value must be positive"); + if (value < 0) { + revert SafeCastOverflowedIntToUint(value); + } return uint${length}(value); } `; @@ -158,6 +185,7 @@ function toUint${length}(int${length} value) internal pure returns (uint${length module.exports = format( header.trimEnd(), 'library SafeCast {', + errors, [...LENGTHS.map(toUintDownCast), toUint(256), ...LENGTHS.map(toIntDownCast), toInt(256)], '}', ); diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js new file mode 100644 index 000000000..3e2263a0c --- /dev/null +++ b/scripts/generate/templates/StorageSlot.js @@ -0,0 +1,87 @@ +const format = require('../format-lines'); +const { capitalize, unique } = require('../../helpers'); + +const TYPES = [ + { type: 'address', isValueType: true, version: '4.1' }, + { type: 'bool', isValueType: true, name: 'Boolean', version: '4.1' }, + { type: 'bytes32', isValueType: true, version: '4.1' }, + { type: 'uint256', isValueType: true, version: '4.1' }, + { type: 'string', isValueType: false, version: '4.9' }, + { type: 'bytes', isValueType: false, version: '4.9' }, +].map(type => Object.assign(type, { struct: (type.name ?? capitalize(type.type)) + 'Slot' })); + +const VERSIONS = unique(TYPES.map(t => t.version)).map( + version => + `_Available since v${version} for ${TYPES.filter(t => t.version == version) + .map(t => `\`${t.type}\``) + .join(', ')}._`, +); + +const header = `\ +pragma solidity ^0.8.19; + +/** + * @dev Library for reading and writing primitive types to specific storage slots. + * + * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * The functions in this library return Slot structs that contain a \`value\` member that can be used to read or write. + * + * Example usage to set ERC1967 implementation slot: + * \`\`\`solidity + * contract ERC1967 { + * bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + * + * function _getImplementation() internal view returns (address) { + * return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value; + * } + * + * function _setImplementation(address newImplementation) internal { + * require(newImplementation.code.length > 0); + * StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation; + * } + * } + * \`\`\` + * +${VERSIONS.map(s => ` * ${s}`).join('\n')} + */ +`; + +const struct = type => `\ +struct ${type.struct} { + ${type.type} value; +} +`; + +const get = type => `\ +/** + * @dev Returns an \`${type.struct}\` with member \`value\` located at \`slot\`. + */ +function get${type.struct}(bytes32 slot) internal pure returns (${type.struct} storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := slot + } +} +`; + +const getStorage = type => `\ +/** + * @dev Returns an \`${type.struct}\` representation of the ${type.type} storage pointer \`store\`. + */ +function get${type.struct}(${type.type} storage store) internal pure returns (${type.struct} storage r) { + /// @solidity memory-safe-assembly + assembly { + r.slot := store.slot + } +} +`; + +// GENERATE +module.exports = format( + header.trimEnd(), + 'library StorageSlot {', + [...TYPES.map(struct), ...TYPES.flatMap(type => [get(type), type.isValueType ? '' : getStorage(type)])], + '}', +); diff --git a/scripts/helpers.js b/scripts/helpers.js index 26d0a2baa..fb9aad4fc 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -24,9 +24,14 @@ function zip(...args) { .map((_, i) => args.map(arg => arg[i])); } +function capitalize(str) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + module.exports = { chunk, range, unique, zip, + capitalize, }; diff --git a/scripts/migrate-imports.js b/scripts/migrate-imports.js deleted file mode 100755 index f60441903..000000000 --- a/scripts/migrate-imports.js +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env node - -const { promises: fs } = require('fs'); -const path = require('path'); - -const pathUpdates = { - // 'access/AccessControl.sol': undefined, - // 'access/Ownable.sol': undefined, - 'access/TimelockController.sol': 'governance/TimelockController.sol', - 'cryptography/ECDSA.sol': 'utils/cryptography/ECDSA.sol', - 'cryptography/MerkleProof.sol': 'utils/cryptography/MerkleProof.sol', - 'drafts/EIP712.sol': 'utils/cryptography/EIP712.sol', - 'drafts/ERC20Permit.sol': 'token/ERC20/extensions/ERC20Permit.sol', - 'drafts/IERC20Permit.sol': 'token/ERC20/extensions/IERC20Permit.sol', - 'GSN/Context.sol': 'utils/Context.sol', - // 'GSN/GSNRecipientERC20Fee.sol': undefined, - // 'GSN/GSNRecipientSignature.sol': undefined, - // 'GSN/GSNRecipient.sol': undefined, - // 'GSN/IRelayHub.sol': undefined, - // 'GSN/IRelayRecipient.sol': undefined, - 'introspection/ERC165Checker.sol': 'utils/introspection/ERC165Checker.sol', - 'introspection/ERC165.sol': 'utils/introspection/ERC165.sol', - 'introspection/ERC1820Implementer.sol': 'utils/introspection/ERC1820Implementer.sol', - 'introspection/IERC165.sol': 'utils/introspection/IERC165.sol', - 'introspection/IERC1820Implementer.sol': 'utils/introspection/IERC1820Implementer.sol', - 'introspection/IERC1820Registry.sol': 'utils/introspection/IERC1820Registry.sol', - 'math/Math.sol': 'utils/math/Math.sol', - 'math/SafeMath.sol': 'utils/math/SafeMath.sol', - 'math/SignedSafeMath.sol': 'utils/math/SignedSafeMath.sol', - 'payment/escrow/ConditionalEscrow.sol': 'utils/escrow/ConditionalEscrow.sol', - 'payment/escrow/Escrow.sol': 'utils/escrow/Escrow.sol', - 'payment/escrow/RefundEscrow.sol': 'utils/escrow/RefundEscrow.sol', - 'payment/PaymentSplitter.sol': 'finance/PaymentSplitter.sol', - 'utils/PaymentSplitter.sol': 'finance/PaymentSplitter.sol', - 'payment/PullPayment.sol': 'security/PullPayment.sol', - 'presets/ERC1155PresetMinterPauser.sol': 'token/ERC1155/presets/ERC1155PresetMinterPauser.sol', - 'presets/ERC20PresetFixedSupply.sol': 'token/ERC20/presets/ERC20PresetFixedSupply.sol', - 'presets/ERC20PresetMinterPauser.sol': 'token/ERC20/presets/ERC20PresetMinterPauser.sol', - 'presets/ERC721PresetMinterPauserAutoId.sol': 'token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol', - 'presets/ERC777PresetFixedSupply.sol': 'token/ERC777/presets/ERC777PresetFixedSupply.sol', - 'proxy/BeaconProxy.sol': 'proxy/beacon/BeaconProxy.sol', - // 'proxy/Clones.sol': undefined, - 'proxy/IBeacon.sol': 'proxy/beacon/IBeacon.sol', - 'proxy/Initializable.sol': 'proxy/utils/Initializable.sol', - 'utils/Initializable.sol': 'proxy/utils/Initializable.sol', - 'proxy/ProxyAdmin.sol': 'proxy/transparent/ProxyAdmin.sol', - // 'proxy/Proxy.sol': undefined, - 'proxy/TransparentUpgradeableProxy.sol': 'proxy/transparent/TransparentUpgradeableProxy.sol', - 'proxy/UpgradeableBeacon.sol': 'proxy/beacon/UpgradeableBeacon.sol', - 'proxy/UpgradeableProxy.sol': 'proxy/ERC1967/ERC1967Proxy.sol', - 'token/ERC1155/ERC1155Burnable.sol': 'token/ERC1155/extensions/ERC1155Burnable.sol', - 'token/ERC1155/ERC1155Holder.sol': 'token/ERC1155/utils/ERC1155Holder.sol', - 'token/ERC1155/ERC1155Pausable.sol': 'token/ERC1155/extensions/ERC1155Pausable.sol', - 'token/ERC1155/ERC1155Receiver.sol': 'token/ERC1155/utils/ERC1155Receiver.sol', - // 'token/ERC1155/ERC1155.sol': undefined, - 'token/ERC1155/IERC1155MetadataURI.sol': 'token/ERC1155/extensions/IERC1155MetadataURI.sol', - // 'token/ERC1155/IERC1155Receiver.sol': undefined, - // 'token/ERC1155/IERC1155.sol': undefined, - 'token/ERC20/ERC20Burnable.sol': 'token/ERC20/extensions/ERC20Burnable.sol', - 'token/ERC20/ERC20Capped.sol': 'token/ERC20/extensions/ERC20Capped.sol', - 'token/ERC20/ERC20Pausable.sol': 'token/ERC20/extensions/ERC20Pausable.sol', - 'token/ERC20/ERC20Snapshot.sol': 'token/ERC20/extensions/ERC20Snapshot.sol', - // 'token/ERC20/ERC20.sol': undefined, - // 'token/ERC20/IERC20.sol': undefined, - 'token/ERC20/SafeERC20.sol': 'token/ERC20/utils/SafeERC20.sol', - 'token/ERC20/TokenTimelock.sol': 'token/ERC20/utils/TokenTimelock.sol', - 'token/ERC721/ERC721Burnable.sol': 'token/ERC721/extensions/ERC721Burnable.sol', - 'token/ERC721/ERC721Holder.sol': 'token/ERC721/utils/ERC721Holder.sol', - 'token/ERC721/ERC721Pausable.sol': 'token/ERC721/extensions/ERC721Pausable.sol', - // 'token/ERC721/ERC721.sol': undefined, - 'token/ERC721/IERC721Enumerable.sol': 'token/ERC721/extensions/IERC721Enumerable.sol', - 'token/ERC721/IERC721Metadata.sol': 'token/ERC721/extensions/IERC721Metadata.sol', - // 'token/ERC721/IERC721Receiver.sol': undefined, - // 'token/ERC721/IERC721.sol': undefined, - // 'token/ERC777/ERC777.sol': undefined, - // 'token/ERC777/IERC777Recipient.sol': undefined, - // 'token/ERC777/IERC777Sender.sol': undefined, - // 'token/ERC777/IERC777.sol': undefined, - // 'utils/Address.sol': undefined, - // 'utils/Arrays.sol': undefined, - // 'utils/Context.sol': undefined, - // 'utils/Counters.sol': undefined, - // 'utils/Create2.sol': undefined, - 'utils/EnumerableMap.sol': 'utils/structs/EnumerableMap.sol', - 'utils/EnumerableSet.sol': 'utils/structs/EnumerableSet.sol', - 'utils/Pausable.sol': 'security/Pausable.sol', - 'utils/ReentrancyGuard.sol': 'security/ReentrancyGuard.sol', - 'utils/SafeCast.sol': 'utils/math/SafeCast.sol', - // 'utils/Strings.sol': undefined, - 'utils/cryptography/draft-EIP712.sol': 'utils/cryptography/EIP712.sol', - 'token/ERC20/extensions/draft-ERC20Permit.sol': 'token/ERC20/extensions/ERC20Permit.sol', - 'token/ERC20/extensions/draft-IERC20Permit.sol': 'token/ERC20/extensions/IERC20Permit.sol', -}; - -async function main(paths = ['contracts']) { - const files = await listFilesRecursively(paths, /\.sol$/); - - const updatedFiles = []; - for (const file of files) { - if (await updateFile(file, updateImportPaths)) { - updatedFiles.push(file); - } - } - - if (updatedFiles.length > 0) { - console.log(`${updatedFiles.length} file(s) were updated`); - for (const c of updatedFiles) { - console.log('-', c); - } - } else { - console.log('No files were updated'); - } -} - -async function listFilesRecursively(paths, filter) { - const queue = paths; - const files = []; - - while (queue.length > 0) { - const top = queue.shift(); - const stat = await fs.stat(top); - if (stat.isFile()) { - if (top.match(filter)) { - files.push(top); - } - } else if (stat.isDirectory()) { - for (const name of await fs.readdir(top)) { - queue.push(path.join(top, name)); - } - } - } - - return files; -} - -async function updateFile(file, update) { - const content = await fs.readFile(file, 'utf8'); - const updatedContent = update(content); - if (updatedContent !== content) { - await fs.writeFile(file, updatedContent); - return true; - } else { - return false; - } -} - -function updateImportPaths(source) { - for (const [oldPath, newPath] of Object.entries(pathUpdates)) { - source = source.replace( - path.join('@openzeppelin/contracts', oldPath), - path.join('@openzeppelin/contracts', newPath), - ); - source = source.replace( - path.join('@openzeppelin/contracts-upgradeable', getUpgradeablePath(oldPath)), - path.join('@openzeppelin/contracts-upgradeable', getUpgradeablePath(newPath)), - ); - } - - return source; -} - -function getUpgradeablePath(file) { - const { dir, name, ext } = path.parse(file); - const upgradeableName = name + 'Upgradeable'; - return path.format({ dir, ext, name: upgradeableName }); -} - -module.exports = { - pathUpdates, - updateImportPaths, - getUpgradeablePath, -}; - -if (require.main === module) { - const args = process.argv.length > 2 ? process.argv.slice(2) : undefined; - main(args).catch(e => { - console.error(e); - process.exit(1); - }); -} diff --git a/scripts/prepare-docs.sh b/scripts/prepare-docs.sh index 4fc0c957a..d1317b092 100755 --- a/scripts/prepare-docs.sh +++ b/scripts/prepare-docs.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash set -euo pipefail +shopt -s globstar OUTDIR="$(node -p 'require("./docs/config.js").outputDir')" @@ -12,4 +13,14 @@ rm -rf "$OUTDIR" hardhat docgen +# copy examples and adjust imports +examples_source_dir="contracts/mocks/docs" +examples_target_dir="docs/modules/api/examples" + +for f in "$examples_source_dir"/**/*.sol; do + name="${f/#"$examples_source_dir/"/}" + mkdir -p "$examples_target_dir/$(dirname "$name")" + sed -Ee '/^import/s|"(\.\./)+|"@openzeppelin/contracts/|' "$f" > "$examples_target_dir/$name" +done + node scripts/gen-nav.js "$OUTDIR" > "$OUTDIR/../nav.adoc" diff --git a/scripts/release/update-comment.js b/scripts/release/update-comment.js index eb9f937ca..9d6df2694 100755 --- a/scripts/release/update-comment.js +++ b/scripts/release/update-comment.js @@ -16,7 +16,7 @@ const { version } = require('../../package.json'); const [tag] = run('git', 'tag') .split(/\r?\n/) .filter(semver.coerce) // check version can be processed - .filter(v => semver.lt(semver.coerce(v), version)) // only consider older tags, ignore current prereleases + .filter(v => semver.satisfies(v, `< ${version}`)) // ignores prereleases unless currently a prerelease .sort(semver.rcompare); // Ordering tag → HEAD is important here. diff --git a/scripts/release/workflow/integrity-check.sh b/scripts/release/workflow/integrity-check.sh new file mode 100644 index 000000000..86e99f929 --- /dev/null +++ b/scripts/release/workflow/integrity-check.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -euo pipefail + +CHECKSUMS="$RUNNER_TEMP/checksums.txt" + +# Extract tarball content into a tmp directory +tar xf "$TARBALL" -C "$RUNNER_TEMP" + +# Move to extracted directory +cd "$RUNNER_TEMP/package" + +# Checksum all Solidity files +find . -type f -name "*.sol" | xargs shasum > "$CHECKSUMS" + +# Back to directory with git contents +cd "$GITHUB_WORKSPACE/contracts" + +# Check against tarball contents +shasum -c "$CHECKSUMS" diff --git a/scripts/release/workflow/pack.sh b/scripts/release/workflow/pack.sh index 798417d3d..ce30712f8 100644 --- a/scripts/release/workflow/pack.sh +++ b/scripts/release/workflow/pack.sh @@ -20,6 +20,7 @@ dist_tag() { cd contracts TARBALL="$(npm pack | tee /dev/stderr | tail -1)" +echo "tarball_name=$TARBALL" >> $GITHUB_OUTPUT echo "tarball=$(pwd)/$TARBALL" >> $GITHUB_OUTPUT echo "tag=$(dist_tag)" >> $GITHUB_OUTPUT cd .. diff --git a/scripts/release/workflow/prepare-release-merge.sh b/scripts/release/workflow/prepare-release-merge.sh deleted file mode 100644 index 8be96922c..000000000 --- a/scripts/release/workflow/prepare-release-merge.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# Define merge branch name -MERGE_BRANCH=merge/$GITHUB_REF_NAME - -# Create the branch and force to start from ref -git checkout -B "$MERGE_BRANCH" "$GITHUB_REF_NAME" - -# Get deleted changesets in this branch that might conflict with master -readarray -t DELETED_CHANGESETS < <(git diff origin/master --name-only -- '.changeset/*.md') - -# Merge master, which will take those files cherry-picked. Auto-resolve conflicts favoring master. -git merge origin/master -m "Merge master to $GITHUB_REF_NAME" -X theirs - -# Remove the originally deleted changesets to correctly sync with master -rm -f "${DELETED_CHANGESETS[@]}" - -git add .changeset/ - -# Allow empty here since there may be no changes if `rm -f` failed for all changesets -git commit --allow-empty -m "Sync changesets with master" -git push -f origin "$MERGE_BRANCH" diff --git a/scripts/release/workflow/publish.sh b/scripts/release/workflow/publish.sh index f9e2802d9..e490e5d00 100644 --- a/scripts/release/workflow/publish.sh +++ b/scripts/release/workflow/publish.sh @@ -2,19 +2,25 @@ set -euo pipefail +PACKAGE_JSON_NAME="$(tar xfO "$TARBALL" package/package.json | jq -r .name)" +PACKAGE_JSON_VERSION="$(tar xfO "$TARBALL" package/package.json | jq -r .version)" + # Intentionally escape $ to avoid interpolation and writing the token to disk echo "//registry.npmjs.org/:_authToken=\${NPM_TOKEN}" > .npmrc # Actual publish npm publish "$TARBALL" --tag "$TAG" +# Clean up tags delete_tag() { - PACKAGE_JSON_NAME="$(tar xfO "$TARBALL" package/package.json | jq -r .name)" npm dist-tag rm "$PACKAGE_JSON_NAME" "$1" } if [ "$TAG" = tmp ]; then delete_tag "$TAG" -elif ["$TAG" = latest ]; then - delete_tag next +elif [ "$TAG" = latest ]; then + # Delete the next tag if it exists and is a prerelease for what is currently being published + if npm dist-tag ls "$PACKAGE_JSON_NAME" | grep -q "next: $PACKAGE_JSON_VERSION"; then + delete_tag next + fi fi diff --git a/scripts/release/workflow/start.sh b/scripts/release/workflow/start.sh index 6e5d78b82..7683ec5bc 100644 --- a/scripts/release/workflow/start.sh +++ b/scripts/release/workflow/start.sh @@ -29,4 +29,7 @@ git add . git commit -m "Start release candidate" # Push branch -git push origin "$RELEASE_BRANCH" +if ! git push origin "$RELEASE_BRANCH"; then + echo "::error file=scripts/release/start.sh::Can't push $RELEASE_BRANCH. Did you forget to run this workflow from $RELEASE_BRANCH?" + exit 1 +fi diff --git a/scripts/release/workflow/state.js b/scripts/release/workflow/state.js index 4d905e260..914e8de02 100644 --- a/scripts/release/workflow/state.js +++ b/scripts/release/workflow/state.js @@ -1,7 +1,8 @@ const { readPreState } = require('@changesets/pre'); const { default: readChangesets } = require('@changesets/read'); const { join } = require('path'); -const { version } = require(join(__dirname, '../../../package.json')); +const { fetch } = require('undici'); +const { version, name: packageName } = require(join(__dirname, '../../../contracts/package.json')); module.exports = async ({ github, context, core }) => { const state = await getState({ github, context, core }); @@ -34,8 +35,8 @@ function shouldRunChangesets({ isReleaseBranch, isPush, isWorkflowDispatch, botR return (isReleaseBranch && isPush) || (isReleaseBranch && isWorkflowDispatch && botRun); } -function shouldRunPublish({ isReleaseBranch, isPush, hasPendingChangesets }) { - return isReleaseBranch && isPush && !hasPendingChangesets; +function shouldRunPublish({ isReleaseBranch, isPush, hasPendingChangesets, isPublishedOnNpm }) { + return isReleaseBranch && isPush && !hasPendingChangesets && !isPublishedOnNpm; } function shouldRunMerge({ @@ -46,7 +47,7 @@ function shouldRunMerge({ hasPendingChangesets, prBackExists, }) { - return isReleaseBranch && isPush && !prerelease && isCurrentFinalVersion && !hasPendingChangesets && prBackExists; + return isReleaseBranch && isPush && !prerelease && isCurrentFinalVersion && !hasPendingChangesets && !prBackExists; } async function getState({ github, context, core }) { @@ -78,7 +79,9 @@ async function getState({ github, context, core }) { state: 'open', }); - state.prBackExists = prs.length === 0; + state.prBackExists = prs.length !== 0; + + state.isPublishedOnNpm = await isPublishedOnNpm(packageName, version); // Log every state value in debug mode if (core.isDebug()) for (const [key, value] of Object.entries(state)) core.debug(`${key}: ${value}`); @@ -102,3 +105,8 @@ async function readChangesetState(cwd = process.cwd()) { changesets, }; } + +async function isPublishedOnNpm(package, version) { + const res = await fetch(`https://registry.npmjs.com/${package}/${version}`); + return res.ok; +} diff --git a/scripts/upgradeable/README.md b/scripts/upgradeable/README.md new file mode 100644 index 000000000..2309f9e10 --- /dev/null +++ b/scripts/upgradeable/README.md @@ -0,0 +1,21 @@ +The upgradeable variant of OpenZeppelin Contracts is automatically generated from the original Solidity code. We call this process "transpilation" and it is implemented by our [Upgradeability Transpiler](https://github.com/OpenZeppelin/openzeppelin-transpiler/). + +When the `master` branch or `release-v*` branches are updated, the code is transpiled and pushed to [OpenZeppelin/openzeppelin-contracts-upgradeable](https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable) by the `upgradeable.yml` workflow. + +## `transpile.sh` + +Applies patches and invokes the transpiler with the command line flags we need for our requirements (for example, excluding certain files). + +## `transpile-onto.sh` + +``` +bash scripts/upgradeable/transpile-onto.sh [] +``` + +Transpiles the contents of the current git branch and commits the result as a new commit on branch ``. If branch `` doesn't exist, it will copy the commit history of `[]` (this is used in GitHub Actions, but is usually not necessary locally). + +## `patch-apply.sh` & `patch-save.sh` + +Some of the upgradeable contract variants require ad-hoc changes that are not implemented by the transpiler. These changes are implemented by patches stored in `upgradeable.patch` in this directory. `patch-apply.sh` applies these patches. + +If the patches fail to apply due to changes in the repo, the conflicts have to be resolved manually. Once fixed, `patch-save.sh` will take the changes staged in Git and update `upgradeable.patch` to match. diff --git a/scripts/upgradeable/patch-apply.sh b/scripts/upgradeable/patch-apply.sh new file mode 100755 index 000000000..d9e17589b --- /dev/null +++ b/scripts/upgradeable/patch-apply.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" +PATCH="$DIRNAME/upgradeable.patch" + +error() { + echo Error: "$*" >&2 + exit 1 +} + +if ! git diff-files --quiet ":!$PATCH" || ! git diff-index --quiet HEAD ":!$PATCH"; then + error "Repository must have no staged or unstaged changes" +fi + +if ! git apply -3 "$PATCH"; then + error "Fix conflicts and run $DIRNAME/patch-save.sh" +fi diff --git a/scripts/upgradeable/patch-save.sh b/scripts/upgradeable/patch-save.sh new file mode 100755 index 000000000..111e6f157 --- /dev/null +++ b/scripts/upgradeable/patch-save.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -euo pipefail + +DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" +PATCH="$DIRNAME/upgradeable.patch" + +error() { + echo Error: "$*" >&2 + exit 1 +} + +if ! git diff-files --quiet ":!$PATCH"; then + error "Unstaged changes. Stage to include in patch or temporarily stash." +fi + +git diff-index --cached --patch --output="$PATCH" HEAD +git restore --staged --worktree ":!$PATCH" diff --git a/scripts/upgradeable/transpile-onto.sh b/scripts/upgradeable/transpile-onto.sh new file mode 100644 index 000000000..a966a9b9a --- /dev/null +++ b/scripts/upgradeable/transpile-onto.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "usage: bash $0 []" >&2 + exit 1 +fi + +set -x + +target="$1" +base="${2-}" + +bash scripts/upgradeable/transpile.sh + +commit="$(git rev-parse --short HEAD)" +branch="$(git rev-parse --abbrev-ref HEAD)" + +git add contracts + +# detach from the current branch to avoid making changes to it +git checkout --quiet --detach + +# switch to the target branch, creating it if necessary +if git rev-parse -q --verify "$target"; then + # if the branch exists, make it the current HEAD without checking out its contents + git reset --soft "$target" + git checkout "$target" +else + # if the branch doesn't exist, create it as an orphan and check it out + git checkout --orphan "$target" + if [ -n "$base" ] && git rev-parse -q --verify "$base"; then + # if base was specified and it exists, set it as the branch history + git reset --soft "$base" + fi +fi + +# commit if there are changes to commit +if ! git diff --quiet --cached; then + git commit -m "Transpile $commit" +fi + +git checkout "$branch" diff --git a/scripts/upgradeable/transpile.sh b/scripts/upgradeable/transpile.sh new file mode 100644 index 000000000..fbffe844e --- /dev/null +++ b/scripts/upgradeable/transpile.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -euo pipefail -x + +DIRNAME="$(dirname -- "${BASH_SOURCE[0]}")" + +bash "$DIRNAME/patch-apply.sh" + +npm run clean +npm run compile + +build_info=($(jq -r '.input.sources | keys | if any(test("^contracts/mocks/.*\\bunreachable\\b")) then empty else input_filename end' artifacts/build-info/*)) +build_info_num=${#build_info[@]} + +if [ $build_info_num -ne 1 ]; then + echo "found $build_info_num relevant build info files but expected just 1" + exit 1 +fi + +# -D: delete original and excluded files +# -b: use this build info file +# -i: use included Initializable +# -x: exclude proxy-related contracts with a few exceptions +# -p: emit public initializer +npx @openzeppelin/upgrade-safe-transpiler@latest -D \ + -b "$build_info" \ + -i contracts/proxy/utils/Initializable.sol \ + -x 'contracts-exposed/**/*' \ + -x 'contracts/proxy/**/*' \ + -x '!contracts/proxy/Clones.sol' \ + -x '!contracts/proxy/ERC1967/ERC1967Storage.sol' \ + -x '!contracts/proxy/ERC1967/ERC1967Utils.sol' \ + -x '!contracts/proxy/utils/UUPSUpgradeable.sol' \ + -x '!contracts/proxy/beacon/IBeacon.sol' \ + -p 'contracts/**/presets/**/*' diff --git a/scripts/upgradeable/upgradeable.patch b/scripts/upgradeable/upgradeable.patch new file mode 100644 index 000000000..ecd0a3711 --- /dev/null +++ b/scripts/upgradeable/upgradeable.patch @@ -0,0 +1,406 @@ +diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md +deleted file mode 100644 +index 2797a088..00000000 +--- a/.github/ISSUE_TEMPLATE/bug_report.md ++++ /dev/null +@@ -1,21 +0,0 @@ +---- +-name: Bug report +-about: Report a bug in OpenZeppelin Contracts +- +---- +- +- +- +- +- +-**💻 Environment** +- +- +- +-**📝 Details** +- +- +- +-**🔢 Code to reproduce bug** +- +- +diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml +index 4018cef2..d343a53d 100644 +--- a/.github/ISSUE_TEMPLATE/config.yml ++++ b/.github/ISSUE_TEMPLATE/config.yml +@@ -1,4 +1,8 @@ ++blank_issues_enabled: false + contact_links: ++ - name: Bug Reports & Feature Requests ++ url: https://github.com/OpenZeppelin/openzeppelin-contracts/issues/new/choose ++ about: Visit the OpenZeppelin Contracts repository + - name: Questions & Support Requests + url: https://forum.openzeppelin.com/c/support/contracts/18 + about: Ask in the OpenZeppelin Forum +diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md +deleted file mode 100644 +index ff596b0c..00000000 +--- a/.github/ISSUE_TEMPLATE/feature_request.md ++++ /dev/null +@@ -1,14 +0,0 @@ +---- +-name: Feature request +-about: Suggest an idea for OpenZeppelin Contracts +- +---- +- +-**🧐 Motivation** +- +- +-**📝 Details** +- +- +- +- +diff --git a/README.md b/README.md +index 9d1c405b..c264e29c 100644 +--- a/README.md ++++ b/README.md +@@ -19,6 +19,9 @@ + + :building_construction: **Want to scale your decentralized application?** Check out [OpenZeppelin Defender](https://openzeppelin.com/defender) — a secure platform for automating and monitoring your operations. + ++> **Note** ++> You are looking at the upgradeable variant of OpenZeppelin Contracts. Be sure to review the documentation on [Using OpenZeppelin Contracts with Upgrades](https://docs.openzeppelin.com/contracts/4.x/upgradeable). ++ + ## Overview + + ### Installation +@@ -26,7 +29,7 @@ + #### Hardhat, Truffle (npm) + + ``` +-$ npm install @openzeppelin/contracts ++$ npm install @openzeppelin/contracts-upgradeable + ``` + + OpenZeppelin Contracts features a [stable API](https://docs.openzeppelin.com/contracts/releases-stability#api-stability), which means that your contracts won't break unexpectedly when upgrading to a newer minor version. +@@ -38,7 +41,7 @@ OpenZeppelin Contracts features a [stable API](https://docs.openzeppelin.com/con + > **Warning** Foundry installs the latest version initially, but subsequent `forge update` commands will use the `master` branch. + + ``` +-$ forge install OpenZeppelin/openzeppelin-contracts ++$ forge install OpenZeppelin/openzeppelin-contracts-upgradeable + ``` + + ### Usage +@@ -48,10 +51,11 @@ Once installed, you can use the contracts in the library by importing them: + ```solidity + pragma solidity ^0.8.19; + +-import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; ++import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; + +-contract MyCollectible is ERC721 { +- constructor() ERC721("MyCollectible", "MCO") { ++contract MyCollectible is ERC721Upgradeable { ++ function initialize() initializer public { ++ __ERC721_init("MyCollectible", "MCO"); + } + } + ``` +diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol +index ebdf0a33..8888803e 100644 +--- a/contracts/finance/VestingWallet.sol ++++ b/contracts/finance/VestingWallet.sol +@@ -18,6 +18,8 @@ import "../utils/Context.sol"; + * + * By setting the duration to 0, one can configure this contract to behave like an asset timelock that hold tokens for + * a beneficiary until a specified time. ++ * ++ * @custom:storage-size 52 + */ + contract VestingWallet is Context { + event EtherReleased(uint256 amount); +diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol +index 5d8318f4..ef3cde55 100644 +--- a/contracts/governance/extensions/GovernorVotes.sol ++++ b/contracts/governance/extensions/GovernorVotes.sol +@@ -10,6 +10,8 @@ import "../../interfaces/IERC5805.sol"; + * @dev Extension of {Governor} for voting weight extraction from an {ERC20Votes} token, or since v4.5 an {ERC721Votes} token. + * + * _Available since v4.3._ ++ * ++ * @custom:storage-size 51 + */ + abstract contract GovernorVotes is Governor { + IERC5805 public immutable token; +diff --git a/contracts/package.json b/contracts/package.json +index df141192..1cf90ad1 100644 +--- a/contracts/package.json ++++ b/contracts/package.json +@@ -1,5 +1,5 @@ + { +- "name": "@openzeppelin/contracts", ++ "name": "@openzeppelin/contracts-upgradeable", + "description": "Secure Smart Contract library for Solidity", + "version": "4.9.2", + "files": [ +@@ -13,7 +13,7 @@ + }, + "repository": { + "type": "git", +- "url": "https://github.com/OpenZeppelin/openzeppelin-contracts.git" ++ "url": "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git" + }, + "keywords": [ + "solidity", +diff --git a/contracts/token/ERC20/extensions/ERC20Capped.sol b/contracts/token/ERC20/extensions/ERC20Capped.sol +index 41e9ce5c..1d910dfa 100644 +--- a/contracts/token/ERC20/extensions/ERC20Capped.sol ++++ b/contracts/token/ERC20/extensions/ERC20Capped.sol +@@ -7,6 +7,8 @@ import "../ERC20.sol"; + + /** + * @dev Extension of {ERC20} that adds a cap to the supply of tokens. ++ * ++ * @custom:storage-size 51 + */ + abstract contract ERC20Capped is ERC20 { + uint256 private immutable _cap; +diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol +index 4378eb7c..1da9e731 100644 +--- a/contracts/token/ERC20/extensions/ERC20Permit.sol ++++ b/contracts/token/ERC20/extensions/ERC20Permit.sol +@@ -18,6 +18,8 @@ import "../../../utils/Nonces.sol"; + * need to send a transaction, and thus is not required to hold Ether at all. + * + * _Available since v3.4._ ++ * ++ * @custom:storage-size 51 + */ + abstract contract ERC20Permit is ERC20, IERC20Permit, EIP712, Nonces { + // solhint-disable-next-line var-name-mixedcase +diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol +index 389965e9..66436b14 100644 +--- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol ++++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol +@@ -14,6 +14,8 @@ import "../utils/SafeERC20.sol"; + * wrapping of an existing "basic" ERC20 into a governance token. + * + * _Available since v4.2._ ++ * ++ * @custom:storage-size 51 + */ + abstract contract ERC20Wrapper is ERC20 { + IERC20 private immutable _underlying; +diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol +index 2628014f..7d5193c8 100644 +--- a/contracts/utils/cryptography/EIP712.sol ++++ b/contracts/utils/cryptography/EIP712.sol +@@ -4,7 +4,6 @@ + pragma solidity ^0.8.19; + + import "./ECDSA.sol"; +-import "../ShortStrings.sol"; + import "../../interfaces/IERC5267.sol"; + + /** +@@ -30,27 +29,19 @@ import "../../interfaces/IERC5267.sol"; + * + * _Available since v3.4._ + * +- * @custom:oz-upgrades-unsafe-allow state-variable-immutable state-variable-assignment ++ * @custom:storage-size 52 + */ + abstract contract EIP712 is IERC5267 { +- using ShortStrings for *; +- + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + +- // Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to +- // invalidate the cached domain separator if the chain id changes. +- bytes32 private immutable _cachedDomainSeparator; +- uint256 private immutable _cachedChainId; +- address private immutable _cachedThis; +- ++ /// @custom:oz-renamed-from _HASHED_NAME + bytes32 private immutable _hashedName; ++ /// @custom:oz-renamed-from _HASHED_VERSION + bytes32 private immutable _hashedVersion; + +- ShortString private immutable _name; +- ShortString private immutable _version; +- string private _nameFallback; +- string private _versionFallback; ++ string private _name; ++ string private _version; + + /** + * @dev Initializes the domain separator and parameter caches. +@@ -65,29 +56,23 @@ abstract contract EIP712 is IERC5267 { + * contract upgrade]. + */ + constructor(string memory name, string memory version) { +- _name = name.toShortStringWithFallback(_nameFallback); +- _version = version.toShortStringWithFallback(_versionFallback); +- _hashedName = keccak256(bytes(name)); +- _hashedVersion = keccak256(bytes(version)); +- +- _cachedChainId = block.chainid; +- _cachedDomainSeparator = _buildDomainSeparator(); +- _cachedThis = address(this); ++ _name = name; ++ _version = version; ++ ++ // Reset prior values in storage if upgrading ++ _hashedName = 0; ++ _hashedVersion = 0; + } + + /** + * @dev Returns the domain separator for the current chain. + */ + function _domainSeparatorV4() internal view returns (bytes32) { +- if (address(this) == _cachedThis && block.chainid == _cachedChainId) { +- return _cachedDomainSeparator; +- } else { +- return _buildDomainSeparator(); +- } ++ return _buildDomainSeparator(); + } + + function _buildDomainSeparator() private view returns (bytes32) { +- return keccak256(abi.encode(_TYPE_HASH, _hashedName, _hashedVersion, block.chainid, address(this))); ++ return keccak256(abi.encode(_TYPE_HASH, _EIP712NameHash(), _EIP712VersionHash(), block.chainid, address(this))); + } + + /** +@@ -128,6 +113,10 @@ abstract contract EIP712 is IERC5267 { + uint256[] memory extensions + ) + { ++ // If the hashed name and version in storage are non-zero, the contract hasn't been properly initialized ++ // and the EIP712 domain is not reliable, as it will be missing name and version. ++ require(_hashedName == 0 && _hashedVersion == 0, "EIP712: Uninitialized"); ++ + return ( + hex"0f", // 01111 + _EIP712Name(), +@@ -142,26 +131,62 @@ abstract contract EIP712 is IERC5267 { + /** + * @dev The name parameter for the EIP712 domain. + * +- * NOTE: By default this function reads _name which is an immutable value. +- * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). +- * +- * _Available since v5.0._ ++ * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs ++ * are a concern. + */ +- // solhint-disable-next-line func-name-mixedcase +- function _EIP712Name() internal view returns (string memory) { +- return _name.toStringWithFallback(_nameFallback); ++ function _EIP712Name() internal view virtual returns (string memory) { ++ return _name; + } + + /** + * @dev The version parameter for the EIP712 domain. + * +- * NOTE: By default this function reads _version which is an immutable value. +- * It only reads from storage if necessary (in case the value is too large to fit in a ShortString). ++ * NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs ++ * are a concern. ++ */ ++ function _EIP712Version() internal view virtual returns (string memory) { ++ return _version; ++ } ++ ++ /** ++ * @dev The hash of the name parameter for the EIP712 domain. ++ * ++ * NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Name` instead. ++ */ ++ function _EIP712NameHash() internal view returns (bytes32) { ++ string memory name = _EIP712Name(); ++ if (bytes(name).length > 0) { ++ return keccak256(bytes(name)); ++ } else { ++ // If the name is empty, the contract may have been upgraded without initializing the new storage. ++ // We return the name hash in storage if non-zero, otherwise we assume the name is empty by design. ++ bytes32 hashedName = _hashedName; ++ if (hashedName != 0) { ++ return hashedName; ++ } else { ++ return keccak256(""); ++ } ++ } ++ } ++ ++ /** ++ * @dev The hash of the version parameter for the EIP712 domain. + * +- * _Available since v5.0._ ++ * NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Version` instead. + */ +- // solhint-disable-next-line func-name-mixedcase +- function _EIP712Version() internal view returns (string memory) { +- return _version.toStringWithFallback(_versionFallback); ++ function _EIP712VersionHash() internal view returns (bytes32) { ++ string memory version = _EIP712Version(); ++ if (bytes(version).length > 0) { ++ return keccak256(bytes(version)); ++ } else { ++ // If the version is empty, the contract may have been upgraded without initializing the new storage. ++ // We return the version hash in storage if non-zero, otherwise we assume the version is empty by design. ++ bytes32 hashedVersion = _hashedVersion; ++ if (hashedVersion != 0) { ++ return hashedVersion; ++ } else { ++ return keccak256(""); ++ } ++ } + } + } +diff --git a/package.json b/package.json +index 37e8f871..d098669f 100644 +--- a/package.json ++++ b/package.json +@@ -33,7 +33,7 @@ + }, + "repository": { + "type": "git", +- "url": "https://github.com/OpenZeppelin/openzeppelin-contracts.git" ++ "url": "https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable.git" + }, + "keywords": [ + "solidity", +diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js +index 7ea535b7..32e3a370 100644 +--- a/test/utils/cryptography/EIP712.test.js ++++ b/test/utils/cryptography/EIP712.test.js +@@ -47,26 +47,6 @@ contract('EIP712', function (accounts) { + const rebuildDomain = await getDomain(this.eip712); + expect(mapValues(rebuildDomain, String)).to.be.deep.equal(mapValues(this.domain, String)); + }); +- +- if (shortOrLong === 'short') { +- // Long strings are in storage, and the proxy will not be properly initialized unless +- // the upgradeable contract variant is used and the initializer is invoked. +- +- it('adjusts when behind proxy', async function () { +- const factory = await Clones.new(); +- const cloneReceipt = await factory.$clone(this.eip712.address); +- const cloneAddress = cloneReceipt.logs.find(({ event }) => event === 'return$clone').args.instance; +- const clone = new EIP712Verifier(cloneAddress); +- +- const cloneDomain = { ...this.domain, verifyingContract: clone.address }; +- +- const reportedDomain = await getDomain(clone); +- expect(mapValues(reportedDomain, String)).to.be.deep.equal(mapValues(cloneDomain, String)); +- +- const expectedSeparator = await domainSeparator(cloneDomain); +- expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator); +- }); +- } + }); + + it('hash digest', async function () { diff --git a/slither.config.json b/slither.config.json index 80ecda71b..069da1f3a 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,5 +1,5 @@ { - "detectors_to_run": "reentrancy-eth,reentrancy-no-eth,reentrancy-unlimited-gas", - "filter_paths": "contracts/mocks", + "detectors_to_run": "arbitrary-send-erc20,array-by-reference,incorrect-shift,name-reused,rtlo,suicidal,uninitialized-state,uninitialized-storage,arbitrary-send-erc20-permit,controlled-array-length,controlled-delegatecall,delegatecall-loop,msg-value-loop,reentrancy-eth,unchecked-transfer,weak-prng,domain-separator-collision,erc20-interface,erc721-interface,locked-ether,mapping-deletion,shadowing-abstract,tautology,write-after-write,boolean-cst,reentrancy-no-eth,reused-constructor,tx-origin,unchecked-lowlevel,unchecked-send,variable-scope,void-cst,events-access,events-maths,incorrect-unary,boolean-equal,cyclomatic-complexity,deprecated-standards,erc20-indexed,function-init-state,pragma,unused-state,reentrancy-unlimited-gas,constable-states,immutable-states,var-read-using-this", + "filter_paths": "contracts/mocks,contracts-exposed", "compile_force_framework": "hardhat" } diff --git a/test/access/AccessControl.behavior.js b/test/access/AccessControl.behavior.js index a93560983..b1729c5d6 100644 --- a/test/access/AccessControl.behavior.js +++ b/test/access/AccessControl.behavior.js @@ -1,13 +1,19 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent, constants, BN } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); const { expect } = require('chai'); +const { time } = require('@nomicfoundation/hardhat-network-helpers'); + const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); +const { network } = require('hardhat'); +const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants'); const DEFAULT_ADMIN_ROLE = '0x0000000000000000000000000000000000000000000000000000000000000000'; const ROLE = web3.utils.soliditySha3('ROLE'); const OTHER_ROLE = web3.utils.soliditySha3('OTHER_ROLE'); +const ZERO = web3.utils.toBN(0); -function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, otherAdmin) { +function shouldBehaveLikeAccessControl(admin, authorized, other, otherAdmin) { shouldSupportInterfaces(['AccessControl']); describe('default admin', function () { @@ -30,9 +36,10 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it('non-admin cannot grant role to other accounts', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.grantRole(ROLE, authorized, { from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -64,9 +71,10 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it('non-admin cannot revoke role', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.revokeRole(ROLE, authorized, { from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${DEFAULT_ADMIN_ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], ); }); @@ -98,9 +106,10 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it('only the sender can renounce their roles', async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.renounceRole(ROLE, authorized, { from: admin }), - `${errorPrefix}: can only renounce roles for self`, + 'AccessControlBadConfirmation', + [], ); }); @@ -141,16 +150,18 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it("a role's previous admins no longer grant roles", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.grantRole(ROLE, authorized, { from: admin }), - `${errorPrefix}: account ${admin.toLowerCase()} is missing role ${OTHER_ROLE}`, + 'AccessControlUnauthorizedAccount', + [admin.toLowerCase(), OTHER_ROLE], ); }); it("a role's previous admins no longer revoke roles", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.revokeRole(ROLE, authorized, { from: admin }), - `${errorPrefix}: account ${admin.toLowerCase()} is missing role ${OTHER_ROLE}`, + 'AccessControlUnauthorizedAccount', + [admin.toLowerCase(), OTHER_ROLE], ); }); }); @@ -165,22 +176,24 @@ function shouldBehaveLikeAccessControl(errorPrefix, admin, authorized, other, ot }); it("revert if sender doesn't have role #1", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.methods['$_checkRole(bytes32)'](ROLE, { from: other }), - `${errorPrefix}: account ${other.toLowerCase()} is missing role ${ROLE}`, + 'AccessControlUnauthorizedAccount', + [other, ROLE], ); }); it("revert if sender doesn't have role #2", async function () { - await expectRevert( + await expectRevertCustomError( this.accessControl.methods['$_checkRole(bytes32)'](OTHER_ROLE, { from: authorized }), - `${errorPrefix}: account ${authorized.toLowerCase()} is missing role ${OTHER_ROLE}`, + 'AccessControlUnauthorizedAccount', + [authorized.toLowerCase(), OTHER_ROLE], ); }); }); } -function shouldBehaveLikeAccessControlEnumerable(errorPrefix, admin, authorized, other, otherAdmin, otherAuthorized) { +function shouldBehaveLikeAccessControlEnumerable(admin, authorized, other, otherAdmin, otherAuthorized) { shouldSupportInterfaces(['AccessControlEnumerable']); describe('enumerating', function () { @@ -210,8 +223,657 @@ function shouldBehaveLikeAccessControlEnumerable(errorPrefix, admin, authorized, }); } +function shouldBehaveLikeAccessControlDefaultAdminRules(delay, defaultAdmin, newDefaultAdmin, other) { + shouldSupportInterfaces(['AccessControlDefaultAdminRules']); + + for (const getter of ['owner', 'defaultAdmin']) { + describe(`${getter}()`, function () { + it('has a default set to the initial default admin', async function () { + const value = await this.accessControl[getter](); + expect(value).to.equal(defaultAdmin); + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, value)).to.be.true; + }); + + it('changes if the default admin changes', async function () { + // Starts an admin transfer + await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + + // Wait for acceptance + const acceptSchedule = web3.utils.toBN(await time.latest()).add(delay); + await time.setNextBlockTimestamp(acceptSchedule.addn(1)); + await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }); + + const value = await this.accessControl[getter](); + expect(value).to.equal(newDefaultAdmin); + }); + }); + } + + describe('pendingDefaultAdmin()', function () { + it('returns 0 if no pending default admin transfer', async function () { + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.eq(ZERO_ADDRESS); + expect(schedule).to.be.bignumber.eq(ZERO); + }); + + describe('when there is a scheduled default admin transfer', function () { + beforeEach('begins admin transfer', async function () { + await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + }); + + for (const [fromSchedule, tag] of [ + [-1, 'before'], + [0, 'exactly when'], + [1, 'after'], + ]) { + it(`returns pending admin and schedule ${tag} it passes if not accepted`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdmin(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + await network.provider.send('evm_mine'); // Mine a block to force the timestamp + + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.eq(newDefaultAdmin); + expect(schedule).to.be.bignumber.eq(firstSchedule); + }); + } + + it('returns 0 after schedule passes and the transfer was accepted', async function () { + // Wait after schedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdmin(); + await time.setNextBlockTimestamp(firstSchedule.addn(1)); + + // Accepts + await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }); + + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.eq(ZERO_ADDRESS); + expect(schedule).to.be.bignumber.eq(ZERO); + }); + }); + }); + + describe('defaultAdminDelay()', function () { + it('returns the current delay', async function () { + expect(await this.accessControl.defaultAdminDelay()).to.be.bignumber.eq(delay); + }); + + describe('when there is a scheduled delay change', function () { + const newDelay = web3.utils.toBN(0xdead); // Any change + + beforeEach('begins delay change', async function () { + await this.accessControl.changeDefaultAdminDelay(newDelay, { from: defaultAdmin }); + }); + + for (const [fromSchedule, tag, expectedDelay, delayTag] of [ + [-1, 'before', delay, 'old'], + [0, 'exactly when', delay, 'old'], + [1, 'after', newDelay, 'new'], + ]) { + it(`returns ${delayTag} delay ${tag} delay schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(schedule.toNumber() + fromSchedule); + await network.provider.send('evm_mine'); // Mine a block to force the timestamp + + const currentDelay = await this.accessControl.defaultAdminDelay(); + expect(currentDelay).to.be.bignumber.eq(expectedDelay); + }); + } + }); + }); + + describe('pendingDefaultAdminDelay()', function () { + it('returns 0 if not set', async function () { + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(ZERO); + expect(schedule).to.be.bignumber.eq(ZERO); + }); + + describe('when there is a scheduled delay change', function () { + const newDelay = web3.utils.toBN(0xdead); // Any change + + beforeEach('begins admin transfer', async function () { + await this.accessControl.changeDefaultAdminDelay(newDelay, { from: defaultAdmin }); + }); + + for (const [fromSchedule, tag, expectedDelay, delayTag, expectZeroSchedule] of [ + [-1, 'before', newDelay, 'new'], + [0, 'exactly when', newDelay, 'new'], + [1, 'after', ZERO, 'zero', true], + ]) { + it(`returns ${delayTag} delay ${tag} delay schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + await network.provider.send('evm_mine'); // Mine a block to force the timestamp + + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(expectedDelay); + expect(schedule).to.be.bignumber.eq(expectZeroSchedule ? ZERO : firstSchedule); + }); + } + }); + }); + + describe('defaultAdminDelayIncreaseWait()', function () { + it('should return 5 days (default)', async function () { + expect(await this.accessControl.defaultAdminDelayIncreaseWait()).to.be.bignumber.eq( + web3.utils.toBN(time.duration.days(5)), + ); + }); + }); + + it('should revert if granting default admin role', async function () { + await expectRevertCustomError( + this.accessControl.grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), + 'AccessControlEnforcedDefaultAdminRules', + [], + ); + }); + + it('should revert if revoking default admin role', async function () { + await expectRevertCustomError( + this.accessControl.revokeRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), + 'AccessControlEnforcedDefaultAdminRules', + [], + ); + }); + + it("should revert if defaultAdmin's admin is changed", async function () { + await expectRevertCustomError( + this.accessControl.$_setRoleAdmin(DEFAULT_ADMIN_ROLE, defaultAdmin), + 'AccessControlEnforcedDefaultAdminRules', + [], + ); + }); + + it('should not grant the default admin role twice', async function () { + await expectRevertCustomError( + this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, defaultAdmin), + 'AccessControlEnforcedDefaultAdminRules', + [], + ); + }); + + describe('begins a default admin transfer', function () { + let receipt; + let acceptSchedule; + + it('reverts if called by non default admin accounts', async function () { + await expectRevertCustomError( + this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: other }), + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], + ); + }); + + describe('when there is no pending delay nor pending admin transfer', function () { + beforeEach('begins admin transfer', async function () { + receipt = await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + acceptSchedule = web3.utils.toBN(await time.latest()).add(delay); + }); + + it('should set pending default admin and schedule', async function () { + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(newDefaultAdmin); + expect(schedule).to.be.bignumber.equal(acceptSchedule); + expectEvent(receipt, 'DefaultAdminTransferScheduled', { + newAdmin, + acceptSchedule, + }); + }); + }); + + describe('when there is a pending admin transfer', function () { + beforeEach('sets a pending default admin transfer', async function () { + await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + acceptSchedule = web3.utils.toBN(await time.latest()).add(delay); + }); + + for (const [fromSchedule, tag] of [ + [-1, 'before'], + [0, 'exactly when'], + [1, 'after'], + ]) { + it(`should be able to begin a transfer again ${tag} acceptSchedule passes`, async function () { + // Wait until schedule + fromSchedule + await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule); + + // defaultAdmin changes its mind and begin again to another address + const receipt = await this.accessControl.beginDefaultAdminTransfer(other, { from: defaultAdmin }); + const newSchedule = web3.utils.toBN(await time.latest()).add(delay); + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(other); + expect(schedule).to.be.bignumber.equal(newSchedule); + + // Cancellation is always emitted since it was never accepted + expectEvent(receipt, 'DefaultAdminTransferCanceled'); + }); + } + + it('should not emit a cancellation event if the new default admin accepted', async function () { + // Wait until the acceptSchedule has passed + await time.setNextBlockTimestamp(acceptSchedule.addn(1)); + + // Accept and restart + await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }); + const receipt = await this.accessControl.beginDefaultAdminTransfer(other, { from: newDefaultAdmin }); + + expectEvent.notEmitted(receipt, 'DefaultAdminTransferCanceled'); + }); + }); + + describe('when there is a pending delay', function () { + const newDelay = web3.utils.toBN(time.duration.hours(3)); + + beforeEach('schedule a delay change', async function () { + await this.accessControl.changeDefaultAdminDelay(newDelay, { from: defaultAdmin }); + const pendingDefaultAdminDelay = await this.accessControl.pendingDefaultAdminDelay(); + acceptSchedule = pendingDefaultAdminDelay.schedule; + }); + + for (const [fromSchedule, schedulePassed, expectedDelay, delayTag] of [ + [-1, 'before', delay, 'old'], + [0, 'exactly when', delay, 'old'], + [1, 'after', newDelay, 'new'], + ]) { + it(`should set the ${delayTag} delay and apply it to next default admin transfer schedule ${schedulePassed} acceptSchedule passed`, async function () { + // Wait until the expected fromSchedule time + await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule); + + // Start the new default admin transfer and get its schedule + const receipt = await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + const expectedAcceptSchedule = web3.utils.toBN(await time.latest()).add(expectedDelay); + + // Check that the schedule corresponds with the new delay + const { newAdmin, schedule: transferSchedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(newDefaultAdmin); + expect(transferSchedule).to.be.bignumber.equal(expectedAcceptSchedule); + + expectEvent(receipt, 'DefaultAdminTransferScheduled', { + newAdmin, + acceptSchedule: expectedAcceptSchedule, + }); + }); + } + }); + }); + + describe('accepts transfer admin', function () { + let acceptSchedule; + + beforeEach(async function () { + await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + acceptSchedule = web3.utils.toBN(await time.latest()).add(delay); + }); + + it('should revert if caller is not pending default admin', async function () { + await time.setNextBlockTimestamp(acceptSchedule.addn(1)); + await expectRevertCustomError( + this.accessControl.acceptDefaultAdminTransfer({ from: other }), + 'AccessControlInvalidDefaultAdmin', + [other], + ); + }); + + describe('when caller is pending default admin and delay has passed', function () { + beforeEach(async function () { + await time.setNextBlockTimestamp(acceptSchedule.addn(1)); + }); + + it('accepts a transfer and changes default admin', async function () { + const receipt = await this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }); + + // Storage changes + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.false; + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, newDefaultAdmin)).to.be.true; + expect(await this.accessControl.owner()).to.equal(newDefaultAdmin); + + // Emit events + expectEvent(receipt, 'RoleRevoked', { + role: DEFAULT_ADMIN_ROLE, + account: defaultAdmin, + }); + expectEvent(receipt, 'RoleGranted', { + role: DEFAULT_ADMIN_ROLE, + account: newDefaultAdmin, + }); + + // Resets pending default admin and schedule + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(constants.ZERO_ADDRESS); + expect(schedule).to.be.bignumber.equal(ZERO); + }); + }); + + describe('schedule not passed', function () { + for (const [fromSchedule, tag] of [ + [-1, 'less'], + [0, 'equal'], + ]) { + it(`should revert if block.timestamp is ${tag} to schedule`, async function () { + await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule); + await expectRevertCustomError( + this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), + 'AccessControlEnforcedDefaultAdminDelay', + [acceptSchedule], + ); + }); + } + }); + }); + + describe('cancels a default admin transfer', function () { + it('reverts if called by non default admin accounts', async function () { + await expectRevertCustomError( + this.accessControl.cancelDefaultAdminTransfer({ from: other }), + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], + ); + }); + + describe('when there is a pending default admin transfer', function () { + let acceptSchedule; + + beforeEach(async function () { + await this.accessControl.beginDefaultAdminTransfer(newDefaultAdmin, { from: defaultAdmin }); + acceptSchedule = web3.utils.toBN(await time.latest()).add(delay); + }); + + for (const [fromSchedule, tag] of [ + [-1, 'before'], + [0, 'exactly when'], + [1, 'after'], + ]) { + it(`resets pending default admin and schedule ${tag} transfer schedule passes`, async function () { + // Advance until passed delay + await time.setNextBlockTimestamp(acceptSchedule.toNumber() + fromSchedule); + + const receipt = await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin }); + + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(constants.ZERO_ADDRESS); + expect(schedule).to.be.bignumber.equal(ZERO); + + expectEvent(receipt, 'DefaultAdminTransferCanceled'); + }); + } + + it('should revert if the previous default admin tries to accept', async function () { + await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin }); + + // Advance until passed delay + await time.setNextBlockTimestamp(acceptSchedule.addn(1)); + + // Previous pending default admin should not be able to accept after cancellation. + await expectRevertCustomError( + this.accessControl.acceptDefaultAdminTransfer({ from: newDefaultAdmin }), + 'AccessControlInvalidDefaultAdmin', + [newDefaultAdmin], + ); + }); + }); + + describe('when there is no pending default admin transfer', async function () { + it('should succeed without changes', async function () { + const receipt = await this.accessControl.cancelDefaultAdminTransfer({ from: defaultAdmin }); + + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(constants.ZERO_ADDRESS); + expect(schedule).to.be.bignumber.equal(ZERO); + + expectEvent.notEmitted(receipt, 'DefaultAdminTransferCanceled'); + }); + }); + }); + + describe('renounces admin', function () { + let expectedSchedule; + let delayPassed; + let delayNotPassed; + + beforeEach(async function () { + await this.accessControl.beginDefaultAdminTransfer(constants.ZERO_ADDRESS, { from: defaultAdmin }); + expectedSchedule = web3.utils.toBN(await time.latest()).add(delay); + delayNotPassed = expectedSchedule; + delayPassed = expectedSchedule.addn(1); + }); + + it('reverts if caller is not default admin', async function () { + await time.setNextBlockTimestamp(delayPassed); + await expectRevertCustomError( + this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from: defaultAdmin }), + 'AccessControlBadConfirmation', + [], + ); + }); + + it("renouncing the admin role when not an admin doesn't affect the schedule", async function () { + await time.setNextBlockTimestamp(delayPassed); + await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from: other }); + + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.equal(constants.ZERO_ADDRESS); + expect(schedule).to.be.bignumber.equal(expectedSchedule); + }); + + it('keeps defaultAdmin consistent with hasRole if another non-defaultAdmin user renounces the DEFAULT_ADMIN_ROLE', async function () { + await time.setNextBlockTimestamp(delayPassed); + + // This passes because it's a noop + await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, other, { from: other }); + + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.true; + expect(await this.accessControl.defaultAdmin()).to.be.equal(defaultAdmin); + }); + + it('renounces role', async function () { + await time.setNextBlockTimestamp(delayPassed); + const receipt = await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }); + + expect(await this.accessControl.hasRole(DEFAULT_ADMIN_ROLE, defaultAdmin)).to.be.false; + expect(await this.accessControl.defaultAdmin()).to.be.equal(constants.ZERO_ADDRESS); + expectEvent(receipt, 'RoleRevoked', { + role: DEFAULT_ADMIN_ROLE, + account: defaultAdmin, + }); + expect(await this.accessControl.owner()).to.equal(constants.ZERO_ADDRESS); + const { newAdmin, schedule } = await this.accessControl.pendingDefaultAdmin(); + expect(newAdmin).to.eq(ZERO_ADDRESS); + expect(schedule).to.be.bignumber.eq(ZERO); + }); + + it('allows to recover access using the internal _grantRole', async function () { + await time.setNextBlockTimestamp(delayPassed); + await this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }); + + const grantRoleReceipt = await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, other); + expectEvent(grantRoleReceipt, 'RoleGranted', { + role: DEFAULT_ADMIN_ROLE, + account: other, + }); + }); + + describe('schedule not passed', function () { + for (const [fromSchedule, tag] of [ + [-1, 'less'], + [0, 'equal'], + ]) { + it(`reverts if block.timestamp is ${tag} to schedule`, async function () { + await time.setNextBlockTimestamp(delayNotPassed.toNumber() + fromSchedule); + await expectRevertCustomError( + this.accessControl.renounceRole(DEFAULT_ADMIN_ROLE, defaultAdmin, { from: defaultAdmin }), + 'AccessControlEnforcedDefaultAdminDelay', + [expectedSchedule], + ); + }); + } + }); + }); + + describe('changes delay', function () { + it('reverts if called by non default admin accounts', async function () { + await expectRevertCustomError( + this.accessControl.changeDefaultAdminDelay(time.duration.hours(4), { + from: other, + }), + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], + ); + }); + + for (const [newDefaultAdminDelay, delayChangeType] of [ + [web3.utils.toBN(delay).subn(time.duration.hours(1)), 'decreased'], + [web3.utils.toBN(delay).addn(time.duration.hours(1)), 'increased'], + [web3.utils.toBN(delay).addn(time.duration.days(5)), 'increased to more than 5 days'], + ]) { + describe(`when the delay is ${delayChangeType}`, function () { + it('begins the delay change to the new delay', async function () { + // Begins the change + const receipt = await this.accessControl.changeDefaultAdminDelay(newDefaultAdminDelay, { + from: defaultAdmin, + }); + + // Calculate expected values + const cap = await this.accessControl.defaultAdminDelayIncreaseWait(); + const changeDelay = newDefaultAdminDelay.lte(delay) + ? delay.sub(newDefaultAdminDelay) + : BN.min(newDefaultAdminDelay, cap); + const timestamp = web3.utils.toBN(await time.latest()); + const effectSchedule = timestamp.add(changeDelay); + + // Assert + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(newDefaultAdminDelay); + expect(schedule).to.be.bignumber.eq(effectSchedule); + expectEvent(receipt, 'DefaultAdminDelayChangeScheduled', { + newDelay, + effectSchedule, + }); + }); + + describe('scheduling again', function () { + beforeEach('schedule once', async function () { + await this.accessControl.changeDefaultAdminDelay(newDefaultAdminDelay, { from: defaultAdmin }); + }); + + for (const [fromSchedule, tag] of [ + [-1, 'before'], + [0, 'exactly when'], + [1, 'after'], + ]) { + const passed = fromSchedule > 0; + + it(`succeeds ${tag} the delay schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + + // Default admin changes its mind and begins another delay change + const anotherNewDefaultAdminDelay = newDefaultAdminDelay.addn(time.duration.hours(2)); + const receipt = await this.accessControl.changeDefaultAdminDelay(anotherNewDefaultAdminDelay, { + from: defaultAdmin, + }); + + // Calculate expected values + const cap = await this.accessControl.defaultAdminDelayIncreaseWait(); + const timestamp = web3.utils.toBN(await time.latest()); + const effectSchedule = timestamp.add(BN.min(cap, anotherNewDefaultAdminDelay)); + + // Assert + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(anotherNewDefaultAdminDelay); + expect(schedule).to.be.bignumber.eq(effectSchedule); + expectEvent(receipt, 'DefaultAdminDelayChangeScheduled', { + newDelay, + effectSchedule, + }); + }); + + const emit = passed ? 'not emit' : 'emit'; + it(`should ${emit} a cancellation event ${tag} the delay schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + + // Default admin changes its mind and begins another delay change + const anotherNewDefaultAdminDelay = newDefaultAdminDelay.addn(time.duration.hours(2)); + const receipt = await this.accessControl.changeDefaultAdminDelay(anotherNewDefaultAdminDelay, { + from: defaultAdmin, + }); + + const eventMatcher = passed ? expectEvent.notEmitted : expectEvent; + eventMatcher(receipt, 'DefaultAdminDelayChangeCanceled'); + }); + } + }); + }); + } + }); + + describe('rollbacks a delay change', function () { + it('reverts if called by non default admin accounts', async function () { + await expectRevertCustomError( + this.accessControl.rollbackDefaultAdminDelay({ from: other }), + 'AccessControlUnauthorizedAccount', + [other, DEFAULT_ADMIN_ROLE], + ); + }); + + describe('when there is a pending delay', function () { + beforeEach('set pending delay', async function () { + await this.accessControl.changeDefaultAdminDelay(time.duration.hours(12), { from: defaultAdmin }); + }); + + for (const [fromSchedule, tag] of [ + [-1, 'before'], + [0, 'exactly when'], + [1, 'after'], + ]) { + const passed = fromSchedule > 0; + + it(`resets pending delay and schedule ${tag} delay change schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + + await this.accessControl.rollbackDefaultAdminDelay({ from: defaultAdmin }); + + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(ZERO); + expect(schedule).to.be.bignumber.eq(ZERO); + }); + + const emit = passed ? 'not emit' : 'emit'; + it(`should ${emit} a cancellation event ${tag} the delay schedule passes`, async function () { + // Wait until schedule + fromSchedule + const { schedule: firstSchedule } = await this.accessControl.pendingDefaultAdminDelay(); + await time.setNextBlockTimestamp(firstSchedule.toNumber() + fromSchedule); + + const receipt = await this.accessControl.rollbackDefaultAdminDelay({ from: defaultAdmin }); + + const eventMatcher = passed ? expectEvent.notEmitted : expectEvent; + eventMatcher(receipt, 'DefaultAdminDelayChangeCanceled'); + }); + } + }); + + describe('when there is no pending delay', function () { + it('succeeds without changes', async function () { + await this.accessControl.rollbackDefaultAdminDelay({ from: defaultAdmin }); + + const { newDelay, schedule } = await this.accessControl.pendingDefaultAdminDelay(); + expect(newDelay).to.be.bignumber.eq(ZERO); + expect(schedule).to.be.bignumber.eq(ZERO); + }); + }); + }); +} + module.exports = { DEFAULT_ADMIN_ROLE, shouldBehaveLikeAccessControl, shouldBehaveLikeAccessControlEnumerable, + shouldBehaveLikeAccessControlDefaultAdminRules, }; diff --git a/test/access/AccessControl.test.js b/test/access/AccessControl.test.js index 90efad3d0..14463b505 100644 --- a/test/access/AccessControl.test.js +++ b/test/access/AccessControl.test.js @@ -8,5 +8,5 @@ contract('AccessControl', function (accounts) { await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, accounts[0]); }); - shouldBehaveLikeAccessControl('AccessControl', ...accounts); + shouldBehaveLikeAccessControl(...accounts); }); diff --git a/test/access/AccessControlCrossChain.test.js b/test/access/AccessControlCrossChain.test.js deleted file mode 100644 index d5a41076b..000000000 --- a/test/access/AccessControlCrossChain.test.js +++ /dev/null @@ -1,49 +0,0 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); -const { BridgeHelper } = require('../helpers/crosschain'); - -const { DEFAULT_ADMIN_ROLE, shouldBehaveLikeAccessControl } = require('./AccessControl.behavior.js'); - -const crossChainRoleAlias = role => - web3.utils.leftPad( - web3.utils.toHex(web3.utils.toBN(role).xor(web3.utils.toBN(web3.utils.soliditySha3('CROSSCHAIN_ALIAS')))), - 64, - ); - -const AccessControlCrossChainMock = artifacts.require('$AccessControlCrossChainMock'); - -const ROLE = web3.utils.soliditySha3('ROLE'); - -contract('AccessControl', function (accounts) { - before(async function () { - this.bridge = await BridgeHelper.deploy(); - }); - - beforeEach(async function () { - this.accessControl = await AccessControlCrossChainMock.new({ from: accounts[0] }); - await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, accounts[0]); - }); - - shouldBehaveLikeAccessControl('AccessControl', ...accounts); - - describe('CrossChain enabled', function () { - beforeEach(async function () { - await this.accessControl.grantRole(ROLE, accounts[0], { from: accounts[0] }); - await this.accessControl.grantRole(crossChainRoleAlias(ROLE), accounts[1], { from: accounts[0] }); - }); - - it('check alliassing', async function () { - expect(await this.accessControl.$_crossChainRoleAlias(ROLE)).to.be.bignumber.equal(crossChainRoleAlias(ROLE)); - }); - - it('Crosschain calls not authorized to non-aliased addresses', async function () { - await expectRevert( - this.bridge.call(accounts[0], this.accessControl, '$_checkRole(bytes32)', [ROLE]), - `AccessControl: account ${accounts[0].toLowerCase()} is missing role ${crossChainRoleAlias(ROLE)}`, - ); - }); - - it('Crosschain calls not authorized to non-aliased addresses', async function () { - await this.bridge.call(accounts[1], this.accessControl, '$_checkRole(bytes32)', [ROLE]); - }); - }); -}); diff --git a/test/access/AccessControlDefaultAdminRules.test.js b/test/access/AccessControlDefaultAdminRules.test.js new file mode 100644 index 000000000..b8eae3220 --- /dev/null +++ b/test/access/AccessControlDefaultAdminRules.test.js @@ -0,0 +1,26 @@ +const { time, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { + shouldBehaveLikeAccessControl, + shouldBehaveLikeAccessControlDefaultAdminRules, +} = require('./AccessControl.behavior.js'); + +const AccessControlDefaultAdminRules = artifacts.require('$AccessControlDefaultAdminRules'); + +contract('AccessControlDefaultAdminRules', function (accounts) { + const delay = web3.utils.toBN(time.duration.hours(10)); + + beforeEach(async function () { + this.accessControl = await AccessControlDefaultAdminRules.new(delay, accounts[0], { from: accounts[0] }); + }); + + it('initial admin not zero', async function () { + await expectRevert( + AccessControlDefaultAdminRules.new(delay, constants.ZERO_ADDRESS), + 'AccessControlInvalidDefaultAdmin', + [constants.ZERO_ADDRESS], + ); + }); + + shouldBehaveLikeAccessControl(...accounts); + shouldBehaveLikeAccessControlDefaultAdminRules(delay, ...accounts); +}); diff --git a/test/access/AccessControlEnumerable.test.js b/test/access/AccessControlEnumerable.test.js index 2aa59f4c0..0e1879700 100644 --- a/test/access/AccessControlEnumerable.test.js +++ b/test/access/AccessControlEnumerable.test.js @@ -12,6 +12,6 @@ contract('AccessControl', function (accounts) { await this.accessControl.$_grantRole(DEFAULT_ADMIN_ROLE, accounts[0]); }); - shouldBehaveLikeAccessControl('AccessControl', ...accounts); - shouldBehaveLikeAccessControlEnumerable('AccessControl', ...accounts); + shouldBehaveLikeAccessControl(...accounts); + shouldBehaveLikeAccessControlEnumerable(...accounts); }); diff --git a/test/access/Ownable.test.js b/test/access/Ownable.test.js index dc308f98f..079d694d7 100644 --- a/test/access/Ownable.test.js +++ b/test/access/Ownable.test.js @@ -1,4 +1,6 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); + const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); @@ -9,7 +11,7 @@ contract('Ownable', function (accounts) { const [owner, other] = accounts; beforeEach(async function () { - this.ownable = await Ownable.new({ from: owner }); + this.ownable = await Ownable.new(owner); }); it('has an owner', async function () { @@ -25,19 +27,24 @@ contract('Ownable', function (accounts) { }); it('prevents non-owners from transferring', async function () { - await expectRevert(this.ownable.transferOwnership(other, { from: other }), 'Ownable: caller is not the owner'); + await expectRevertCustomError( + this.ownable.transferOwnership(other, { from: other }), + 'OwnableUnauthorizedAccount', + [other], + ); }); it('guards ownership against stuck state', async function () { - await expectRevert( + await expectRevertCustomError( this.ownable.transferOwnership(ZERO_ADDRESS, { from: owner }), - 'Ownable: new owner is the zero address', + 'OwnableInvalidOwner', + [ZERO_ADDRESS], ); }); }); describe('renounce ownership', function () { - it('loses owner after renouncement', async function () { + it('loses ownership after renouncement', async function () { const receipt = await this.ownable.renounceOwnership({ from: owner }); expectEvent(receipt, 'OwnershipTransferred'); @@ -45,7 +52,17 @@ contract('Ownable', function (accounts) { }); it('prevents non-owners from renouncement', async function () { - await expectRevert(this.ownable.renounceOwnership({ from: other }), 'Ownable: caller is not the owner'); + await expectRevertCustomError(this.ownable.renounceOwnership({ from: other }), 'OwnableUnauthorizedAccount', [ + other, + ]); + }); + + it('allows to recover access using the internal _transferOwnership', async function () { + await this.ownable.renounceOwnership({ from: owner }); + const receipt = await this.ownable.$_transferOwnership(other); + expectEvent(receipt, 'OwnershipTransferred'); + + expect(await this.ownable.owner()).to.equal(other); }); }); }); diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index 64d432762..bdbac48fa 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -1,6 +1,7 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); const Ownable2Step = artifacts.require('$Ownable2Step'); @@ -8,7 +9,7 @@ contract('Ownable2Step', function (accounts) { const [owner, accountA, accountB] = accounts; beforeEach(async function () { - this.ownable2Step = await Ownable2Step.new({ from: owner }); + this.ownable2Step = await Ownable2Step.new(owner); }); describe('transfer ownership', function () { @@ -27,6 +28,17 @@ contract('Ownable2Step', function (accounts) { expect(await this.ownable2Step.pendingOwner()).to.not.equal(accountA); }); + it('guards transfer against invalid user', async function () { + await this.ownable2Step.transferOwnership(accountA, { from: owner }); + await expectRevertCustomError( + this.ownable2Step.acceptOwnership({ from: accountB }), + 'OwnableUnauthorizedAccount', + [accountB], + ); + }); + }); + + describe('renouncing ownership', async function () { it('changes owner after renouncing ownership', async function () { await this.ownable2Step.renounceOwnership({ from: owner }); // If renounceOwnership is removed from parent an alternative is needed ... @@ -40,18 +52,19 @@ contract('Ownable2Step', function (accounts) { expect(await this.ownable2Step.pendingOwner()).to.equal(accountA); await this.ownable2Step.renounceOwnership({ from: owner }); expect(await this.ownable2Step.pendingOwner()).to.equal(ZERO_ADDRESS); - await expectRevert( + await expectRevertCustomError( this.ownable2Step.acceptOwnership({ from: accountA }), - 'Ownable2Step: caller is not the new owner', + 'OwnableUnauthorizedAccount', + [accountA], ); }); - it('guards transfer against invalid user', async function () { - await this.ownable2Step.transferOwnership(accountA, { from: owner }); - await expectRevert( - this.ownable2Step.acceptOwnership({ from: accountB }), - 'Ownable2Step: caller is not the new owner', - ); + it('allows to recover access using the internal _transferOwnership', async function () { + await this.ownable2Step.renounceOwnership({ from: owner }); + const receipt = await this.ownable2Step.$_transferOwnership(accountA); + expectEvent(receipt, 'OwnershipTransferred'); + + expect(await this.ownable2Step.owner()).to.equal(accountA); }); }); }); diff --git a/test/crosschain/CrossChainEnabled.test.js b/test/crosschain/CrossChainEnabled.test.js deleted file mode 100644 index 9e7d26308..000000000 --- a/test/crosschain/CrossChainEnabled.test.js +++ /dev/null @@ -1,78 +0,0 @@ -const { BridgeHelper } = require('../helpers/crosschain'); -const { expectRevertCustomError } = require('../helpers/customError'); - -function randomAddress() { - return web3.utils.toChecksumAddress(web3.utils.randomHex(20)); -} - -const CrossChainEnabledAMBMock = artifacts.require('CrossChainEnabledAMBMock'); -const CrossChainEnabledArbitrumL1Mock = artifacts.require('CrossChainEnabledArbitrumL1Mock'); -const CrossChainEnabledArbitrumL2Mock = artifacts.require('CrossChainEnabledArbitrumL2Mock'); -const CrossChainEnabledOptimismMock = artifacts.require('CrossChainEnabledOptimismMock'); -const CrossChainEnabledPolygonChildMock = artifacts.require('CrossChainEnabledPolygonChildMock'); - -function shouldBehaveLikeReceiver(sender = randomAddress()) { - it('should reject same-chain calls', async function () { - await expectRevertCustomError(this.receiver.crossChainRestricted(), 'NotCrossChainCall()'); - - await expectRevertCustomError(this.receiver.crossChainOwnerRestricted(), 'NotCrossChainCall()'); - }); - - it('should restrict to cross-chain call from a invalid sender', async function () { - await expectRevertCustomError( - this.bridge.call(sender, this.receiver, 'crossChainOwnerRestricted()'), - `InvalidCrossChainSender("${sender}", "${await this.receiver.owner()}")`, - ); - }); - - it('should grant access to cross-chain call from the owner', async function () { - await this.bridge.call(await this.receiver.owner(), this.receiver, 'crossChainOwnerRestricted()'); - }); -} - -contract('CrossChainEnabled', function () { - describe('AMB', function () { - beforeEach(async function () { - this.bridge = await BridgeHelper.deploy('AMB'); - this.receiver = await CrossChainEnabledAMBMock.new(this.bridge.address); - }); - - shouldBehaveLikeReceiver(); - }); - - describe('Arbitrum-L1', function () { - beforeEach(async function () { - this.bridge = await BridgeHelper.deploy('Arbitrum-L1'); - this.receiver = await CrossChainEnabledArbitrumL1Mock.new(this.bridge.address); - }); - - shouldBehaveLikeReceiver(); - }); - - describe('Arbitrum-L2', function () { - beforeEach(async function () { - this.bridge = await BridgeHelper.deploy('Arbitrum-L2'); - this.receiver = await CrossChainEnabledArbitrumL2Mock.new(); - }); - - shouldBehaveLikeReceiver(); - }); - - describe('Optimism', function () { - beforeEach(async function () { - this.bridge = await BridgeHelper.deploy('Optimism'); - this.receiver = await CrossChainEnabledOptimismMock.new(this.bridge.address); - }); - - shouldBehaveLikeReceiver(); - }); - - describe('Polygon-Child', function () { - beforeEach(async function () { - this.bridge = await BridgeHelper.deploy('Polygon-Child'); - this.receiver = await CrossChainEnabledPolygonChildMock.new(this.bridge.address); - }); - - shouldBehaveLikeReceiver(); - }); -}); diff --git a/test/finance/PaymentSplitter.test.js b/test/finance/PaymentSplitter.test.js deleted file mode 100644 index 1408c9f51..000000000 --- a/test/finance/PaymentSplitter.test.js +++ /dev/null @@ -1,217 +0,0 @@ -const { balance, constants, ether, expectEvent, send, expectRevert } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - -const { expect } = require('chai'); - -const PaymentSplitter = artifacts.require('PaymentSplitter'); -const ERC20 = artifacts.require('$ERC20'); - -contract('PaymentSplitter', function (accounts) { - const [owner, payee1, payee2, payee3, nonpayee1, payer1] = accounts; - - const amount = ether('1'); - - it('rejects an empty set of payees', async function () { - await expectRevert(PaymentSplitter.new([], []), 'PaymentSplitter: no payees'); - }); - - it('rejects more payees than shares', async function () { - await expectRevert( - PaymentSplitter.new([payee1, payee2, payee3], [20, 30]), - 'PaymentSplitter: payees and shares length mismatch', - ); - }); - - it('rejects more shares than payees', async function () { - await expectRevert( - PaymentSplitter.new([payee1, payee2], [20, 30, 40]), - 'PaymentSplitter: payees and shares length mismatch', - ); - }); - - it('rejects null payees', async function () { - await expectRevert( - PaymentSplitter.new([payee1, ZERO_ADDRESS], [20, 30]), - 'PaymentSplitter: account is the zero address', - ); - }); - - it('rejects zero-valued shares', async function () { - await expectRevert(PaymentSplitter.new([payee1, payee2], [20, 0]), 'PaymentSplitter: shares are 0'); - }); - - it('rejects repeated payees', async function () { - await expectRevert(PaymentSplitter.new([payee1, payee1], [20, 30]), 'PaymentSplitter: account already has shares'); - }); - - context('once deployed', function () { - beforeEach(async function () { - this.payees = [payee1, payee2, payee3]; - this.shares = [20, 10, 70]; - - this.contract = await PaymentSplitter.new(this.payees, this.shares); - this.token = await ERC20.new('MyToken', 'MT'); - await this.token.$_mint(owner, ether('1000')); - }); - - it('has total shares', async function () { - expect(await this.contract.totalShares()).to.be.bignumber.equal('100'); - }); - - it('has payees', async function () { - await Promise.all( - this.payees.map(async (payee, index) => { - expect(await this.contract.payee(index)).to.equal(payee); - expect(await this.contract.released(payee)).to.be.bignumber.equal('0'); - expect(await this.contract.releasable(payee)).to.be.bignumber.equal('0'); - }), - ); - }); - - describe('accepts payments', function () { - it('Ether', async function () { - await send.ether(owner, this.contract.address, amount); - - expect(await balance.current(this.contract.address)).to.be.bignumber.equal(amount); - }); - - it('Token', async function () { - await this.token.transfer(this.contract.address, amount, { from: owner }); - - expect(await this.token.balanceOf(this.contract.address)).to.be.bignumber.equal(amount); - }); - }); - - describe('shares', function () { - it('stores shares if address is payee', async function () { - expect(await this.contract.shares(payee1)).to.be.bignumber.not.equal('0'); - }); - - it('does not store shares if address is not payee', async function () { - expect(await this.contract.shares(nonpayee1)).to.be.bignumber.equal('0'); - }); - }); - - 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'); - }); - it('reverts if non-payee want to claim', async function () { - await send.ether(payer1, this.contract.address, amount); - await expectRevert(this.contract.release(nonpayee1), 'PaymentSplitter: account has no shares'); - }); - }); - - 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', - ); - }); - it('reverts if non-payee want to claim', async function () { - await this.token.transfer(this.contract.address, amount, { from: owner }); - await expectRevert( - this.contract.release(this.token.address, nonpayee1), - 'PaymentSplitter: account has no shares', - ); - }); - }); - }); - - 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); - - // receive funds - const initBalance = await balance.current(this.contract.address); - expect(initBalance).to.be.bignumber.equal(amount); - - // distribute to payees - - const tracker1 = await balance.tracker(payee1); - const receipt1 = await this.contract.release(payee1); - const profit1 = await tracker1.delta(); - expect(profit1).to.be.bignumber.equal(ether('0.20')); - expectEvent(receipt1, 'PaymentReleased', { to: payee1, amount: profit1 }); - - const tracker2 = await balance.tracker(payee2); - const receipt2 = await this.contract.release(payee2); - const profit2 = await tracker2.delta(); - expect(profit2).to.be.bignumber.equal(ether('0.10')); - expectEvent(receipt2, 'PaymentReleased', { to: payee2, amount: profit2 }); - - const tracker3 = await balance.tracker(payee3); - const receipt3 = await this.contract.release(payee3); - const profit3 = await tracker3.delta(); - expect(profit3).to.be.bignumber.equal(ether('0.70')); - expectEvent(receipt3, 'PaymentReleased', { to: payee3, amount: profit3 }); - - // end balance should be zero - expect(await balance.current(this.contract.address)).to.be.bignumber.equal('0'); - - // check correct funds released accounting - expect(await this.contract.totalReleased()).to.be.bignumber.equal(initBalance); - }); - - it('Token', async function () { - expect(await this.token.balanceOf(payee1)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOf(payee2)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOf(payee3)).to.be.bignumber.equal('0'); - - await this.token.transfer(this.contract.address, amount, { from: owner }); - - expectEvent(await this.contract.release(this.token.address, payee1), 'ERC20PaymentReleased', { - token: this.token.address, - to: payee1, - amount: ether('0.20'), - }); - - await this.token.transfer(this.contract.address, amount, { from: owner }); - - expectEvent(await this.contract.release(this.token.address, payee1), 'ERC20PaymentReleased', { - token: this.token.address, - to: payee1, - amount: ether('0.20'), - }); - - expectEvent(await this.contract.release(this.token.address, payee2), 'ERC20PaymentReleased', { - token: this.token.address, - to: payee2, - amount: ether('0.20'), - }); - - expectEvent(await this.contract.release(this.token.address, payee3), 'ERC20PaymentReleased', { - token: this.token.address, - to: payee3, - amount: ether('1.40'), - }); - - expect(await this.token.balanceOf(payee1)).to.be.bignumber.equal(ether('0.40')); - expect(await this.token.balanceOf(payee2)).to.be.bignumber.equal(ether('0.20')); - expect(await this.token.balanceOf(payee3)).to.be.bignumber.equal(ether('1.40')); - }); - }); - }); -}); diff --git a/test/finance/VestingWallet.test.js b/test/finance/VestingWallet.test.js index a110b2cd9..91ca04da0 100644 --- a/test/finance/VestingWallet.test.js +++ b/test/finance/VestingWallet.test.js @@ -1,7 +1,8 @@ -const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, time } = require('@openzeppelin/test-helpers'); const { web3 } = require('@openzeppelin/test-helpers/src/setup'); const { expect } = require('chai'); const { BNmin } = require('../helpers/math'); +const { expectRevertCustomError } = require('../helpers/customError'); const VestingWallet = artifacts.require('VestingWallet'); const ERC20 = artifacts.require('$ERC20'); @@ -20,9 +21,10 @@ contract('VestingWallet', function (accounts) { }); it('rejects zero address for beneficiary', async function () { - await expectRevert( + await expectRevertCustomError( VestingWallet.new(constants.ZERO_ADDRESS, this.start, duration), - 'VestingWallet: beneficiary is zero address', + 'VestingWalletInvalidBeneficiary', + [constants.ZERO_ADDRESS], ); }); @@ -30,6 +32,7 @@ contract('VestingWallet', function (accounts) { expect(await this.mock.beneficiary()).to.be.equal(beneficiary); expect(await this.mock.start()).to.be.bignumber.equal(this.start); expect(await this.mock.duration()).to.be.bignumber.equal(duration); + expect(await this.mock.end()).to.be.bignumber.equal(this.start.add(duration)); }); describe('vesting schedule', function () { diff --git a/test/governance/Governor.t.sol b/test/governance/Governor.t.sol new file mode 100644 index 000000000..43c4c5ddd --- /dev/null +++ b/test/governance/Governor.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../contracts/utils/Strings.sol"; +import "../../contracts/governance/Governor.sol"; + +contract GovernorInternalTest is Test, Governor { + constructor() Governor("") {} + + function testValidDescriptionForProposer(string memory description, address proposer, bool includeProposer) public { + if (includeProposer) { + description = string.concat(description, "#proposer=", Strings.toHexString(proposer)); + } + assertTrue(_isValidDescriptionForProposer(proposer, description)); + } + + function testInvalidDescriptionForProposer( + string memory description, + address commitProposer, + address actualProposer + ) public { + vm.assume(commitProposer != actualProposer); + description = string.concat(description, "#proposer=", Strings.toHexString(commitProposer)); + assertFalse(_isValidDescriptionForProposer(actualProposer, description)); + } + + // We don't need to truly implement implement the missing functions because we are just testing + // internal helpers. + + function clock() public pure override returns (uint48) {} + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public pure override returns (string memory) {} + + // solhint-disable-next-line func-name-mixedcase + function COUNTING_MODE() public pure virtual override returns (string memory) {} + + function votingDelay() public pure virtual override returns (uint256) {} + + function votingPeriod() public pure virtual override returns (uint256) {} + + function quorum(uint256) public pure virtual override returns (uint256) {} + + function hasVoted(uint256, address) public pure virtual override returns (bool) {} + + function _quorumReached(uint256) internal pure virtual override returns (bool) {} + + function _voteSucceeded(uint256) internal pure virtual override returns (bool) {} + + function _getVotes(address, uint256, bytes memory) internal pure virtual override returns (uint256) {} + + function _countVote(uint256, address, uint8, uint256, bytes memory) internal virtual override {} +} diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index a66020ed9..909c38686 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -1,632 +1,865 @@ -const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const { fromRpcSig } = require('ethereumjs-util'); const Enums = require('../helpers/enums'); -const { getChainId } = require('../helpers/chainid'); -const { EIP712Domain } = require('../helpers/eip712'); -const { GovernorHelper } = require('../helpers/governance'); +const { getDomain, domainType } = require('../helpers/eip712'); +const { GovernorHelper, proposalStatesToBitMap } = require('../helpers/governance'); +const { clockFromReceipt } = require('../helpers/time'); +const { expectRevertCustomError } = require('../helpers/customError'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); +const { shouldBehaveLikeEIP6372 } = require('./utils/EIP6372.behavior'); +const { ZERO_BYTES32 } = require('@openzeppelin/test-helpers/src/constants'); -const Token = artifacts.require('$ERC20Votes'); const Governor = artifacts.require('$GovernorMock'); const CallReceiver = artifacts.require('CallReceiverMock'); const ERC721 = artifacts.require('$ERC721'); const ERC1155 = artifacts.require('$ERC1155'); +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, + { Token: artifacts.require('$ERC20VotesLegacyMock'), mode: 'blocknumber' }, +]; + contract('Governor', function (accounts) { const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; - const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); const name = 'OZ-Governor'; const version = '1'; const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - this.chainId = await getChainId(); - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new( - name, // name - votingDelay, // initialVotingDelay - votingPeriod, // initialVotingPeriod - 0, // initialProposalThreshold - this.token.address, // tokenAddress - 10, // quorumNumeratorValue - ); - this.receiver = await CallReceiver.new(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.chainId = await web3.eth.getChainId(); + try { + this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); + } catch { + // ERC20VotesLegacyMock has a different construction that uses version='1' by default. + this.token = await Token.new(tokenName, tokenSymbol, tokenName); + } + this.mock = await Governor.new( + name, // name + votingDelay, // initialVotingDelay + votingPeriod, // initialVotingPeriod + 0, // initialProposalThreshold + this.token.address, // tokenAddress + 10, // quorumNumeratorValue + ); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - value, - }, - ], - '', - ); - }); - - shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor', 'GovernorWithParams']); - - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=for,abstain'); - }); - - it('nominal workflow', async function () { - // Before - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); - - // Run proposal - const txPropose = await this.helper.propose({ from: proposer }); - - expectEvent(txPropose, 'ProposalCreated', { - proposalId: this.proposal.id, - proposer, - targets: this.proposal.targets, - // values: this.proposal.values, - signatures: this.proposal.signatures, - calldatas: this.proposal.data, - startBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay), - endBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod), - description: this.proposal.description, - }); - - await this.helper.waitForSnapshot(); - - expectEvent( - await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }), - 'VoteCast', - { - voter: voter1, - support: Enums.VoteType.For, - reason: 'This is nice', - weight: web3.utils.toWei('10'), - }, - ); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', { - voter: voter2, - support: Enums.VoteType.For, - weight: web3.utils.toWei('7'), - }); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', { - voter: voter3, - support: Enums.VoteType.Against, - weight: web3.utils.toWei('5'), - }); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', { - voter: voter4, - support: Enums.VoteType.Abstain, - weight: web3.utils.toWei('2'), - }); - - await this.helper.waitForDeadline(); - - const txExecute = await this.helper.execute(); - - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - - // After - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); - }); - - it('vote with signature', async function () { - const voterBySig = Wallet.generate(); - const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); - - const signature = async message => { - return fromRpcSig( - ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { - data: { - types: { - EIP712Domain, - Ballot: [ - { name: 'proposalId', type: 'uint256' }, - { name: 'support', type: 'uint8' }, - ], + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + value, }, - domain: { name, version, chainId: this.chainId, verifyingContract: this.mock.address }, - primaryType: 'Ballot', - message, + ], + '', + ); + }); + + shouldSupportInterfaces(['ERC165', 'ERC1155Receiver', 'Governor', 'GovernorWithParams', 'GovernorCancel']); + shouldBehaveLikeEIP6372(mode); + + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=for,abstain'); + }); + + it('nominal workflow', async function () { + // Before + expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(constants.ZERO_ADDRESS); + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); + + // Run proposal + const txPropose = await this.helper.propose({ from: proposer }); + + expectEvent(txPropose, 'ProposalCreated', { + proposalId: this.proposal.id, + proposer, + targets: this.proposal.targets, + // values: this.proposal.values, + signatures: this.proposal.signatures, + calldatas: this.proposal.data, + voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay), + voteEnd: web3.utils + .toBN(await clockFromReceipt[mode](txPropose.receipt)) + .add(votingDelay) + .add(votingPeriod), + description: this.proposal.description, + }); + + await this.helper.waitForSnapshot(); + + expectEvent( + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }), + 'VoteCast', + { + voter: voter1, + support: Enums.VoteType.For, + reason: 'This is nice', + weight: web3.utils.toWei('10'), }, - }), - ); - }; - - await this.token.delegate(voterBySigAddress, { from: voter1 }); - - // Run proposal - await this.helper.propose(); - await this.helper.waitForSnapshot(); - expectEvent(await this.helper.vote({ support: Enums.VoteType.For, signature }), 'VoteCast', { - voter: voterBySigAddress, - support: Enums.VoteType.For, - }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - // After - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true); - }); - - it('send ethers', async function () { - this.proposal = this.helper.setProposal( - [ - { - target: empty, - value, - }, - ], - '', - ); - - // Before - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); - expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0'); - - // Run proposal - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - // After - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value); - }); - - describe('should revert', function () { - describe('on propose', function () { - it('if proposal already exists', async function () { - await this.helper.propose(); - await expectRevert(this.helper.propose(), 'Governor: proposal already exists'); - }); - }); - - describe('on vote', function () { - it('if proposal does not exist', async function () { - await expectRevert( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: unknown proposal id', ); - }); - it('if voting has not started', async function () { - await this.helper.propose(); - await expectRevert( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', - ); - }); + expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', { + voter: voter2, + support: Enums.VoteType.For, + weight: web3.utils.toWei('7'), + }); - it('if support value is invalid', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await expectRevert( - this.helper.vote({ support: new BN('255') }), - 'GovernorVotingSimple: invalid value for enum VoteType', - ); - }); + expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', { + voter: voter3, + support: Enums.VoteType.Against, + weight: web3.utils.toWei('5'), + }); - it('if vote was already casted', async function () { - await this.helper.propose(); - 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 }), - 'GovernorVotingSimple: vote already cast', - ); - }); + expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', { + voter: voter4, + support: Enums.VoteType.Abstain, + weight: web3.utils.toWei('2'), + }); - it('if voting is over', async function () { - await this.helper.propose(); await this.helper.waitForDeadline(); - await expectRevert( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', - ); - }); - }); - describe('on execute', function () { - it('if proposal does not exist', async function () { - await expectRevert(this.helper.execute(), 'Governor: unknown proposal id'); + const txExecute = await this.helper.execute(); + + expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + + // After + expect(await this.mock.proposalProposer(this.proposal.id)).to.be.equal(proposer); + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); }); - it('if quorum is not reached', async function () { + it('vote with signature', async function () { + const voterBySig = Wallet.generate(); + const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); + + const signature = (contract, message) => + getDomain(contract) + .then(domain => ({ + primaryType: 'Ballot', + types: { + EIP712Domain: domainType(domain), + Ballot: [ + { name: 'proposalId', type: 'uint256' }, + { name: 'support', type: 'uint8' }, + ], + }, + domain, + message, + })) + .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data })) + .then(fromRpcSig); + + await this.token.delegate(voterBySigAddress, { from: voter1 }); + + // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 }); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); + expectEvent(await this.helper.vote({ support: Enums.VoteType.For, signature }), 'VoteCast', { + voter: voterBySigAddress, + support: Enums.VoteType.For, + }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + // After + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voterBySigAddress)).to.be.equal(true); }); - it('if score not reached', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); + it('send ethers', async function () { + const empty = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); - it('if voting is not over', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - it('if receiver revert without reason', async function () { this.proposal = this.helper.setProposal( [ { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(), + target: empty, + value, }, ], '', ); - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'Governor: call reverted without message'); - }); + // Before + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(value); + expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal('0'); - it('if receiver revert with reason', async function () { - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting'); - }); - - it('if proposal was already executed', async function () { + // Run proposal await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); await this.helper.waitForDeadline(); await this.helper.execute(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - }); - }); - describe('state', function () { - it('Unset', async function () { - await expectRevert(this.mock.state(this.proposal.id), 'Governor: unknown proposal id'); - }); - - it('Pending & Active', async function () { - await this.helper.propose(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); - await this.helper.waitForSnapshot(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); - await this.helper.waitForSnapshot(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - }); - - it('Defeated', async function () { - await this.helper.propose(); - await this.helper.waitForDeadline(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - await this.helper.waitForDeadline(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); - }); - - it('Succeeded', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - await this.helper.waitForDeadline(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); - }); - - it('Executed', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); - }); - }); - - describe('cancel', function () { - it('before proposal', async function () { - await expectRevert(this.helper.cancel(), 'Governor: unknown proposal id'); - }); - - it('after proposal', async function () { - await this.helper.propose(); - - await this.helper.cancel(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - - await this.helper.waitForSnapshot(); - await expectRevert( - this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), - 'Governor: vote not currently active', - ); - }); - - it('after vote', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - - await this.helper.cancel(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - - await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - it('after deadline', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - await this.helper.cancel(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - it('after execution', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - await expectRevert(this.helper.cancel(), 'Governor: proposal not active'); - }); - }); - - describe('proposal length', function () { - it('empty', async function () { - this.helper.setProposal([], ''); - await expectRevert(this.helper.propose(), 'Governor: empty proposal'); - }); - - it('mismatch #1', async function () { - this.helper.setProposal( - { - targets: [], - values: [web3.utils.toWei('0')], - data: [this.receiver.contract.methods.mockFunction().encodeABI()], - }, - '', - ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); - }); - - it('mismatch #2', async function () { - this.helper.setProposal( - { - targets: [this.receiver.address], - values: [], - data: [this.receiver.contract.methods.mockFunction().encodeABI()], - }, - '', - ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); - }); - - it('mismatch #3', async function () { - this.helper.setProposal( - { - targets: [this.receiver.address], - values: [web3.utils.toWei('0')], - data: [], - }, - '', - ); - await expectRevert(this.helper.propose(), 'Governor: invalid proposal length'); - }); - }); - - describe('onlyGovernance updates', function () { - it('setVotingDelay is protected', async function () { - await expectRevert(this.mock.setVotingDelay('0'), 'Governor: onlyGovernance'); - }); - - it('setVotingPeriod is protected', async function () { - await expectRevert(this.mock.setVotingPeriod('32'), 'Governor: onlyGovernance'); - }); - - it('setProposalThreshold is protected', async function () { - await expectRevert(this.mock.setProposalThreshold('1000000000000000000'), 'Governor: onlyGovernance'); - }); - - it('can setVotingDelay through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.setVotingDelay('0').encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' }); - - expect(await this.mock.votingDelay()).to.be.bignumber.equal('0'); - }); - - it('can setVotingPeriod through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' }); - - expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32'); - }); - - it('cannot setVotingPeriod to 0 through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.setVotingPeriod('0').encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - await expectRevert(this.helper.execute(), 'GovernorSettings: voting period too low'); - }); - - it('can setProposalThreshold to 0 through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.execute(), 'ProposalThresholdSet', { - oldProposalThreshold: '0', - newProposalThreshold: '1000000000000000000', + // After + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(empty)).to.be.bignumber.equal(value); }); - expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000'); - }); - }); + describe('should revert', function () { + describe('on propose', function () { + it('if proposal already exists', async function () { + await this.helper.propose(); + await expectRevertCustomError(this.helper.propose(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Pending, + ZERO_BYTES32, + ]); + }); + }); - describe('safe receive', function () { - describe('ERC721', function () { - const name = 'Non Fungible Token'; - const symbol = 'NFT'; - const tokenId = new BN(1); + describe('on vote', function () { + it('if proposal does not exist', async function () { + await expectRevertCustomError( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'GovernorNonexistentProposal', + [this.proposal.id], + ); + }); - beforeEach(async function () { - this.token = await ERC721.new(name, symbol); - await this.token.$_mint(owner, tokenId); + it('if voting has not started', async function () { + await this.helper.propose(); + await expectRevertCustomError( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'GovernorUnexpectedProposalState', + [this.proposal.id, Enums.ProposalState.Pending, proposalStatesToBitMap([Enums.ProposalState.Active])], + ); + }); + + it('if support value is invalid', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await expectRevertCustomError( + this.helper.vote({ support: web3.utils.toBN('255') }), + 'GovernorInvalidVoteType', + [], + ); + }); + + it('if vote was already casted', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await expectRevertCustomError( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'GovernorAlreadyCastVote', + [voter1], + ); + }); + + it('if voting is over', async function () { + await this.helper.propose(); + await this.helper.waitForDeadline(); + await expectRevertCustomError( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'GovernorUnexpectedProposalState', + [this.proposal.id, Enums.ProposalState.Defeated, proposalStatesToBitMap([Enums.ProposalState.Active])], + ); + }); + }); + + describe('on execute', function () { + it('if proposal does not exist', async function () { + await expectRevertCustomError(this.helper.execute(), 'GovernorNonexistentProposal', [this.proposal.id]); + }); + + it('if quorum is not reached', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter3 }); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + + it('if score not reached', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + + it('if voting is not over', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + + it('if receiver revert without reason', async function () { + this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunctionRevertsNoReason().encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await expectRevertCustomError(this.helper.execute(), 'FailedInnerCall', []); + }); + + it('if receiver revert with reason', async function () { + this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunctionRevertsReason().encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await expectRevert(this.helper.execute(), 'CallReceiverMock: reverting'); + }); + + it('if proposal was already executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + }); }); - it('can receive an ERC721 safeTransfer', async function () { - await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }); + describe('state', function () { + it('Unset', async function () { + await expectRevertCustomError(this.mock.state(this.proposal.id), 'GovernorNonexistentProposal', [ + this.proposal.id, + ]); + }); + + it('Pending & Active', async function () { + await this.helper.propose(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); + await this.helper.waitForSnapshot(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Pending); + await this.helper.waitForSnapshot(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + }); + + it('Defeated', async function () { + await this.helper.propose(); + await this.helper.waitForDeadline(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); + }); + + it('Succeeded', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + }); + + it('Executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Executed); + }); + }); + + describe('cancel', function () { + describe('internal', function () { + it('before proposal', async function () { + await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorNonexistentProposal', [ + this.proposal.id, + ]); + }); + + it('after proposal', async function () { + await this.helper.propose(); + + await this.helper.cancel('internal'); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await this.helper.waitForSnapshot(); + await expectRevertCustomError( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'GovernorUnexpectedProposalState', + [this.proposal.id, Enums.ProposalState.Canceled, proposalStatesToBitMap([Enums.ProposalState.Active])], + ); + }); + + it('after vote', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + + await this.helper.cancel('internal'); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await this.helper.waitForDeadline(); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + + it('after deadline', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + await this.helper.cancel('internal'); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + + it('after execution', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + await expectRevertCustomError(this.helper.cancel('internal'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap( + [Enums.ProposalState.Canceled, Enums.ProposalState.Expired, Enums.ProposalState.Executed], + { inverted: true }, + ), + ]); + }); + }); + + describe('public', function () { + it('before proposal', async function () { + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorNonexistentProposal', [ + this.proposal.id, + ]); + }); + + it('after proposal', async function () { + await this.helper.propose(); + + await this.helper.cancel('external'); + }); + + it('after proposal - restricted to proposer', async function () { + await this.helper.propose(); + + await expectRevertCustomError(this.helper.cancel('external', { from: owner }), 'GovernorOnlyProposer', [ + owner, + ]); + }); + + it('after vote started', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(1); // snapshot + 1 block + + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); + }); + + it('after vote', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Active, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); + }); + + it('after deadline', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Succeeded, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); + }); + + it('after execution', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Pending]), + ]); + }); + }); + }); + + describe('proposal length', function () { + it('empty', async function () { + this.helper.setProposal([], ''); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 0, 0]); + }); + + it('mismatch #1', async function () { + this.helper.setProposal( + { + targets: [], + values: [web3.utils.toWei('0')], + data: [this.receiver.contract.methods.mockFunction().encodeABI()], + }, + '', + ); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [0, 1, 1]); + }); + + it('mismatch #2', async function () { + this.helper.setProposal( + { + targets: [this.receiver.address], + values: [], + data: [this.receiver.contract.methods.mockFunction().encodeABI()], + }, + '', + ); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 1, 0]); + }); + + it('mismatch #3', async function () { + this.helper.setProposal( + { + targets: [this.receiver.address], + values: [web3.utils.toWei('0')], + data: [], + }, + '', + ); + await expectRevertCustomError(this.helper.propose(), 'GovernorInvalidProposalLength', [1, 0, 1]); + }); + }); + + describe('frontrun protection using description suffix', function () { + describe('without protection', function () { + describe('without suffix', function () { + it('proposer can propose', async function () { + expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); + }); + + it('someone else can propose', async function () { + expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); + }); + }); + + describe('with different suffix', function () { + beforeEach(async function () { + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + value, + }, + ], + `#wrong-suffix=${proposer}`, + ); + }); + + it('proposer can propose', async function () { + expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); + }); + + it('someone else can propose', async function () { + expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); + }); + }); + + describe('with proposer suffix but bad address part', function () { + beforeEach(async function () { + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + value, + }, + ], + `#proposer=0x3C44CdDdB6a900fa2b585dd299e03d12FA429XYZ`, // XYZ are not a valid hex char + ); + }); + + it('propose can propose', async function () { + expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); + }); + + it('someone else can propose', async function () { + expectEvent(await this.helper.propose({ from: voter1 }), 'ProposalCreated'); + }); + }); + }); + + describe('with protection via proposer suffix', function () { + beforeEach(async function () { + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + value, + }, + ], + `#proposer=${proposer}`, + ); + }); + + it('proposer can propose', async function () { + expectEvent(await this.helper.propose({ from: proposer }), 'ProposalCreated'); + }); + + it('someone else cannot propose', async function () { + await expectRevert(this.helper.propose({ from: voter1 }), 'Governor: proposer restricted'); + }); + }); + }); + + describe('onlyGovernance updates', function () { + it('setVotingDelay is protected', async function () { + await expectRevertCustomError(this.mock.setVotingDelay('0', { from: owner }), 'GovernorOnlyExecutor', [ + owner, + ]); + }); + + it('setVotingPeriod is protected', async function () { + await expectRevertCustomError(this.mock.setVotingPeriod('32', { from: owner }), 'GovernorOnlyExecutor', [ + owner, + ]); + }); + + it('setProposalThreshold is protected', async function () { + await expectRevertCustomError( + this.mock.setProposalThreshold('1000000000000000000', { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); + }); + + it('can setVotingDelay through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.setVotingDelay('0').encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent(await this.helper.execute(), 'VotingDelaySet', { oldVotingDelay: '4', newVotingDelay: '0' }); + + expect(await this.mock.votingDelay()).to.be.bignumber.equal('0'); + }); + + it('can setVotingPeriod through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.setVotingPeriod('32').encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent(await this.helper.execute(), 'VotingPeriodSet', { oldVotingPeriod: '16', newVotingPeriod: '32' }); + + expect(await this.mock.votingPeriod()).to.be.bignumber.equal('32'); + }); + + it('cannot setVotingPeriod to 0 through governance', async function () { + const votingPeriod = 0; + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.setVotingPeriod(votingPeriod).encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidVotingPeriod', [votingPeriod]); + }); + + it('can setProposalThreshold to 0 through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.setProposalThreshold('1000000000000000000').encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent(await this.helper.execute(), 'ProposalThresholdSet', { + oldProposalThreshold: '0', + newProposalThreshold: '1000000000000000000', + }); + + expect(await this.mock.proposalThreshold()).to.be.bignumber.equal('1000000000000000000'); + }); + }); + + describe('safe receive', function () { + describe('ERC721', function () { + const name = 'Non Fungible Token'; + const symbol = 'NFT'; + const tokenId = web3.utils.toBN(1); + + beforeEach(async function () { + this.token = await ERC721.new(name, symbol); + await this.token.$_mint(owner, tokenId); + }); + + it('can receive an ERC721 safeTransfer', async function () { + await this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }); + }); + }); + + describe('ERC1155', function () { + const uri = 'https://token-cdn-domain/{id}.json'; + const tokenIds = { + 1: web3.utils.toBN(1000), + 2: web3.utils.toBN(2000), + 3: web3.utils.toBN(3000), + }; + + beforeEach(async function () { + this.token = await ERC1155.new(uri); + await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + }); + + it('can receive ERC1155 safeTransfer', async function () { + await this.token.safeTransferFrom( + owner, + this.mock.address, + ...Object.entries(tokenIds)[0], // id + amount + '0x', + { from: owner }, + ); + }); + + it('can receive ERC1155 safeBatchTransfer', async function () { + await this.token.safeBatchTransferFrom( + owner, + this.mock.address, + Object.keys(tokenIds), + Object.values(tokenIds), + '0x', + { from: owner }, + ); + }); + }); }); }); - - describe('ERC1155', function () { - const uri = 'https://token-cdn-domain/{id}.json'; - const tokenIds = { - 1: new BN(1000), - 2: new BN(2000), - 3: new BN(3000), - }; - - beforeEach(async function () { - this.token = await ERC1155.new(uri); - await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); - }); - - it('can receive ERC1155 safeTransfer', async function () { - await this.token.safeTransferFrom( - owner, - this.mock.address, - ...Object.entries(tokenIds)[0], // id + amount - '0x', - { from: owner }, - ); - }); - - it('can receive ERC1155 safeBatchTransfer', async function () { - await this.token.safeBatchTransferFrom( - owner, - this.mock.address, - Object.keys(tokenIds), - Object.values(tokenIds), - '0x', - { from: owner }, - ); - }); - }); - }); + } }); diff --git a/test/governance/TimelockController.test.js b/test/governance/TimelockController.test.js index 607f8bc83..d8fcdce6c 100644 --- a/test/governance/TimelockController.test.js +++ b/test/governance/TimelockController.test.js @@ -4,12 +4,15 @@ const { ZERO_ADDRESS, ZERO_BYTES32 } = constants; const { expect } = require('chai'); const { shouldSupportInterfaces } = require('../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../helpers/customError'); +const { OperationState } = require('../helpers/enums'); const TimelockController = artifacts.require('TimelockController'); const CallReceiverMock = artifacts.require('CallReceiverMock'); const Implementation2 = artifacts.require('Implementation2'); const ERC721 = artifacts.require('$ERC721'); const ERC1155 = artifacts.require('$ERC1155'); +const TimelockReentrant = artifacts.require('$TimelockReentrant'); const MINDELAY = time.duration.days(1); @@ -158,6 +161,11 @@ contract('TimelockController', function (accounts) { delay: MINDELAY, }); + expectEvent(receipt, 'CallSalt', { + id: this.operation.id, + salt: this.operation.salt, + }); + const block = await web3.eth.getBlock(receipt.receipt.blockHash); expect(await this.mock.getTimestamp(this.operation.id)).to.be.bignumber.equal( @@ -176,7 +184,7 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); - await expectRevert( + await expectRevertCustomError( this.mock.schedule( this.operation.target, this.operation.value, @@ -186,12 +194,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: operation already scheduled', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Unset], ); }); it('prevent non-proposer from committing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.schedule( this.operation.target, this.operation.value, @@ -201,12 +210,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${PROPOSER_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, PROPOSER_ROLE], ); }); it('enforce minimum delay', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.schedule( this.operation.target, this.operation.value, @@ -216,9 +226,23 @@ contract('TimelockController', function (accounts) { MINDELAY - 1, { from: proposer }, ), - 'TimelockController: insufficient delay', + 'TimelockInsufficientDelay', + [MINDELAY, MINDELAY - 1], ); }); + + it('schedule operation with salt zero', async function () { + const { receipt } = await this.mock.schedule( + this.operation.target, + this.operation.value, + this.operation.data, + this.operation.predecessor, + ZERO_BYTES32, + MINDELAY, + { from: proposer }, + ); + expectEvent.notEmitted(receipt, 'CallSalt'); + }); }); describe('execute', function () { @@ -233,7 +257,7 @@ contract('TimelockController', function (accounts) { }); it('revert if operation is not scheduled', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -242,7 +266,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -260,7 +285,7 @@ contract('TimelockController', function (accounts) { }); it('revert if execution comes too early 1/2', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -269,7 +294,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -277,7 +303,7 @@ contract('TimelockController', function (accounts) { const timestamp = await this.mock.getTimestamp(this.operation.id); await time.increaseTo(timestamp - 5); // -1 is too tight, test sometime fails - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -286,7 +312,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -315,7 +342,7 @@ contract('TimelockController', function (accounts) { }); it('prevent non-executor from revealing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation.target, this.operation.value, @@ -324,9 +351,87 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${EXECUTOR_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, EXECUTOR_ROLE], ); }); + + it('prevents reentrancy execution', async function () { + // Create operation + const reentrant = await TimelockReentrant.new(); + const reentrantOperation = genOperation( + reentrant.address, + 0, + reentrant.contract.methods.reenter().encodeABI(), + ZERO_BYTES32, + salt, + ); + + // Schedule so it can be executed + await this.mock.schedule( + reentrantOperation.target, + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + MINDELAY, + { from: proposer }, + ); + + // Advance on time to make the operation executable + const timestamp = await this.mock.getTimestamp(reentrantOperation.id); + await time.increaseTo(timestamp); + + // Grant executor role to the reentrant contract + await this.mock.grantRole(EXECUTOR_ROLE, reentrant.address, { from: admin }); + + // Prepare reenter + const data = this.mock.contract.methods + .execute( + reentrantOperation.target, + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + ) + .encodeABI(); + await reentrant.enableRentrancy(this.mock.address, data); + + // Expect to fail + await expectRevertCustomError( + this.mock.execute( + reentrantOperation.target, + reentrantOperation.value, + reentrantOperation.data, + reentrantOperation.predecessor, + reentrantOperation.salt, + { from: executor }, + ), + 'TimelockUnexpectedOperationState', + [reentrantOperation.id, OperationState.Ready], + ); + + // Disable reentrancy + await reentrant.disableReentrancy(); + const nonReentrantOperation = reentrantOperation; // Not anymore + + // Try again successfully + const receipt = await this.mock.execute( + nonReentrantOperation.target, + nonReentrantOperation.value, + nonReentrantOperation.data, + nonReentrantOperation.predecessor, + nonReentrantOperation.salt, + { from: executor }, + ); + expectEvent(receipt, 'CallExecuted', { + id: nonReentrantOperation.id, + index: web3.utils.toBN(0), + target: nonReentrantOperation.target, + value: web3.utils.toBN(nonReentrantOperation.value), + data: nonReentrantOperation.data, + }); + }); }); }); }); @@ -364,6 +469,11 @@ contract('TimelockController', function (accounts) { predecessor: this.operation.predecessor, delay: MINDELAY, }); + + expectEvent(receipt, 'CallSalt', { + id: this.operation.id, + salt: this.operation.salt, + }); } const block = await web3.eth.getBlock(receipt.receipt.blockHash); @@ -384,7 +494,7 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -394,12 +504,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: operation already scheduled', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Unset], ); }); it('length of batch parameter must match #1', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, [], @@ -409,12 +520,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, this.operation.payloads.length, 0], ); }); it('length of batch parameter must match #1', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -424,12 +536,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: proposer }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, 0, this.operation.payloads.length], ); }); it('prevent non-proposer from committing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -439,12 +552,13 @@ contract('TimelockController', function (accounts) { MINDELAY, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${PROPOSER_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, PROPOSER_ROLE], ); }); it('enforce minimum delay', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.scheduleBatch( this.operation.targets, this.operation.values, @@ -454,7 +568,8 @@ contract('TimelockController', function (accounts) { MINDELAY - 1, { from: proposer }, ), - 'TimelockController: insufficient delay', + 'TimelockInsufficientDelay', + [MINDELAY, MINDELAY - 1], ); }); }); @@ -471,7 +586,7 @@ contract('TimelockController', function (accounts) { }); it('revert if operation is not scheduled', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -480,7 +595,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -498,7 +614,7 @@ contract('TimelockController', function (accounts) { }); it('revert if execution comes too early 1/2', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -507,7 +623,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -515,7 +632,7 @@ contract('TimelockController', function (accounts) { const timestamp = await this.mock.getTimestamp(this.operation.id); await time.increaseTo(timestamp - 5); // -1 is to tight, test sometime fails - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -524,7 +641,8 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: operation is not ready', + 'TimelockUnexpectedOperationState', + [this.operation.id, OperationState.Ready], ); }); @@ -555,7 +673,7 @@ contract('TimelockController', function (accounts) { }); it('prevent non-executor from revealing', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -564,12 +682,13 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: other }, ), - `AccessControl: account ${other.toLowerCase()} is missing role ${EXECUTOR_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, EXECUTOR_ROLE], ); }); it('length mismatch #1', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( [], this.operation.values, @@ -578,12 +697,13 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [0, this.operation.payloads.length, this.operation.values.length], ); }); it('length mismatch #2', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, [], @@ -592,12 +712,13 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, this.operation.payloads.length, 0], ); }); it('length mismatch #3', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( this.operation.targets, this.operation.values, @@ -606,9 +727,89 @@ contract('TimelockController', function (accounts) { this.operation.salt, { from: executor }, ), - 'TimelockController: length mismatch', + 'TimelockInvalidOperationLength', + [this.operation.targets.length, 0, this.operation.values.length], ); }); + + it('prevents reentrancy execution', async function () { + // Create operation + const reentrant = await TimelockReentrant.new(); + const reentrantBatchOperation = genOperationBatch( + [reentrant.address], + [0], + [reentrant.contract.methods.reenter().encodeABI()], + ZERO_BYTES32, + salt, + ); + + // Schedule so it can be executed + await this.mock.scheduleBatch( + reentrantBatchOperation.targets, + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + MINDELAY, + { from: proposer }, + ); + + // Advance on time to make the operation executable + const timestamp = await this.mock.getTimestamp(reentrantBatchOperation.id); + await time.increaseTo(timestamp); + + // Grant executor role to the reentrant contract + await this.mock.grantRole(EXECUTOR_ROLE, reentrant.address, { from: admin }); + + // Prepare reenter + const data = this.mock.contract.methods + .executeBatch( + reentrantBatchOperation.targets, + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + ) + .encodeABI(); + await reentrant.enableRentrancy(this.mock.address, data); + + // Expect to fail + await expectRevertCustomError( + this.mock.executeBatch( + reentrantBatchOperation.targets, + reentrantBatchOperation.values, + reentrantBatchOperation.payloads, + reentrantBatchOperation.predecessor, + reentrantBatchOperation.salt, + { from: executor }, + ), + 'TimelockUnexpectedOperationState', + [reentrantBatchOperation.id, OperationState.Ready], + ); + + // Disable reentrancy + await reentrant.disableReentrancy(); + const nonReentrantBatchOperation = reentrantBatchOperation; // Not anymore + + // Try again successfully + const receipt = await this.mock.executeBatch( + nonReentrantBatchOperation.targets, + nonReentrantBatchOperation.values, + nonReentrantBatchOperation.payloads, + nonReentrantBatchOperation.predecessor, + nonReentrantBatchOperation.salt, + { from: executor }, + ); + for (const i in nonReentrantBatchOperation.targets) { + expectEvent(receipt, 'CallExecuted', { + id: nonReentrantBatchOperation.id, + index: web3.utils.toBN(i), + target: nonReentrantBatchOperation.targets[i], + value: web3.utils.toBN(nonReentrantBatchOperation.values[i]), + data: nonReentrantBatchOperation.payloads[i], + }); + } + }); }); }); @@ -618,7 +819,7 @@ contract('TimelockController', function (accounts) { [0, 0, 0], [ this.callreceivermock.contract.methods.mockFunction().encodeABI(), - this.callreceivermock.contract.methods.mockFunctionThrows().encodeABI(), + this.callreceivermock.contract.methods.mockFunctionRevertsNoReason().encodeABI(), this.callreceivermock.contract.methods.mockFunction().encodeABI(), ], ZERO_BYTES32, @@ -635,7 +836,7 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + await expectRevertCustomError( this.mock.executeBatch( operation.targets, operation.values, @@ -644,7 +845,8 @@ contract('TimelockController', function (accounts) { operation.salt, { from: executor }, ), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); }); }); @@ -676,16 +878,18 @@ contract('TimelockController', function (accounts) { }); it('cannot cancel invalid operation', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.cancel(constants.ZERO_BYTES32, { from: canceller }), - 'TimelockController: operation cannot be cancelled', + 'TimelockUnexpectedOperationState', + [constants.ZERO_BYTES32, OperationState.Pending], ); }); it('prevent non-canceller from canceling', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.cancel(this.operation.id, { from: other }), - `AccessControl: account ${other.toLowerCase()} is missing role ${CANCELLER_ROLE}`, + `AccessControlUnauthorizedAccount`, + [other, CANCELLER_ROLE], ); }); }); @@ -693,7 +897,7 @@ contract('TimelockController', function (accounts) { describe('maintenance', function () { it('prevent unauthorized maintenance', async function () { - await expectRevert(this.mock.updateDelay(0, { from: other }), 'TimelockController: caller must be timelock'); + await expectRevertCustomError(this.mock.updateDelay(0, { from: other }), 'TimelockUnauthorizedCaller', [other]); }); it('timelock scheduled maintenance', async function () { @@ -768,7 +972,7 @@ contract('TimelockController', function (accounts) { }); it('cannot execute before dependency', async function () { - await expectRevert( + await expectRevertCustomError( this.mock.execute( this.operation2.target, this.operation2.value, @@ -777,7 +981,8 @@ contract('TimelockController', function (accounts) { this.operation2.salt, { from: executor }, ), - 'TimelockController: missing dependency', + 'TimelockUnexecutedPredecessor', + [this.operation1.id], ); }); @@ -854,11 +1059,12 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); }); @@ -881,11 +1087,11 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + // Targeted function reverts with a panic code (0x1) + the timelock bubble the panic code + await expectRevert.unspecified( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), - 'TimelockController: underlying transaction reverted', ); }); @@ -908,12 +1114,13 @@ contract('TimelockController', function (accounts) { { from: proposer }, ); await time.increase(MINDELAY); - await expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, - gas: '70000', + gas: '100000', }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); }); @@ -976,11 +1183,12 @@ contract('TimelockController', function (accounts) { expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - await expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); @@ -1010,11 +1218,12 @@ contract('TimelockController', function (accounts) { expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); expect(await web3.eth.getBalance(this.callreceivermock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - await expectRevert( + await expectRevertCustomError( this.mock.execute(operation.target, operation.value, operation.data, operation.predecessor, operation.salt, { from: executor, }), - 'TimelockController: underlying transaction reverted', + 'FailedInnerCall', + [], ); expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); diff --git a/test/governance/compatibility/GovernorCompatibilityBravo.test.js b/test/governance/compatibility/GovernorCompatibilityBravo.test.js index 38e6a8d4a..4182dfb4e 100644 --- a/test/governance/compatibility/GovernorCompatibilityBravo.test.js +++ b/test/governance/compatibility/GovernorCompatibilityBravo.test.js @@ -1,14 +1,17 @@ -const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const RLP = require('rlp'); const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); +const { clockFromReceipt } = require('../../helpers/time'); +const { expectRevertCustomError } = require('../../helpers/customError'); -const Token = artifacts.require('$ERC20VotesComp'); const Timelock = artifacts.require('CompTimelock'); const Governor = artifacts.require('$GovernorCompatibilityBravoMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const { shouldBehaveLikeEIP6372 } = require('../utils/EIP6372.behavior'); + function makeContractAddress(creator, nonce) { return web3.utils.toChecksumAddress( web3.utils @@ -18,6 +21,11 @@ function makeContractAddress(creator, nonce) { ); } +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorCompatibilityBravo', function (accounts) { const [owner, proposer, voter1, voter2, voter3, voter4, other] = accounts; @@ -26,218 +34,273 @@ contract('GovernorCompatibilityBravo', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const proposalThreshold = web3.utils.toWei('10'); const value = web3.utils.toWei('1'); - beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); + const votes = { + [owner]: tokenSupply, + [proposer]: proposalThreshold, + [voter1]: web3.utils.toWei('10'), + [voter2]: web3.utils.toWei('7'), + [voter3]: web3.utils.toWei('5'), + [voter4]: web3.utils.toWei('2'), + [other]: 0, + }; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + const [deployer] = await web3.eth.getAccounts(); - // Need to predict governance address to set it as timelock admin with a delayed transfer - const nonce = await web3.eth.getTransactionCount(deployer); - const predictGovernor = makeContractAddress(deployer, nonce + 1); + this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.timelock = await Timelock.new(predictGovernor, 2 * 86400); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - proposalThreshold, - this.timelock.address, - this.token.address, - ); - this.receiver = await CallReceiver.new(); + // Need to predict governance address to set it as timelock admin with a delayed transfer + const nonce = await web3.eth.getTransactionCount(deployer); + const predictGovernor = makeContractAddress(deployer, nonce + 1); - this.helper = new GovernorHelper(this.mock); + this.timelock = await Timelock.new(predictGovernor, 2 * 86400); + this.mock = await Governor.new( + name, + votingDelay, + votingPeriod, + proposalThreshold, + this.timelock.address, + this.token.address, + ); + this.receiver = await CallReceiver.new(); - await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); + this.helper = new GovernorHelper(this.mock, mode); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: proposer, value: proposalThreshold }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - signature: 'mockFunction()', - }, - ], - '', - ); - }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: proposer, value: votes[proposer] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter1, value: votes[voter1] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: votes[voter2] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: votes[voter3] }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: votes[voter4] }, { from: owner }); - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - expect(await this.mock.quorumVotes()).to.be.bignumber.equal('0'); - expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=bravo'); - }); - - it('nominal workflow', async function () { - // Before - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(value); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); - - // Run proposal - const txPropose = await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - // After - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); - - const proposal = await this.mock.proposals(this.proposal.id); - expect(proposal.id).to.be.bignumber.equal(this.proposal.id); - expect(proposal.proposer).to.be.equal(proposer); - expect(proposal.eta).to.be.bignumber.equal(await this.mock.proposalEta(this.proposal.id)); - expect(proposal.startBlock).to.be.bignumber.equal(await this.mock.proposalSnapshot(this.proposal.id)); - expect(proposal.endBlock).to.be.bignumber.equal(await this.mock.proposalDeadline(this.proposal.id)); - expect(proposal.canceled).to.be.equal(false); - expect(proposal.executed).to.be.equal(true); - - const action = await this.mock.getActions(this.proposal.id); - expect(action.targets).to.be.deep.equal(this.proposal.targets); - // expect(action.values).to.be.deep.equal(this.proposal.values); - expect(action.signatures).to.be.deep.equal(this.proposal.signatures); - expect(action.calldatas).to.be.deep.equal(this.proposal.data); - - const voteReceipt1 = await this.mock.getReceipt(this.proposal.id, voter1); - expect(voteReceipt1.hasVoted).to.be.equal(true); - expect(voteReceipt1.support).to.be.bignumber.equal(Enums.VoteType.For); - expect(voteReceipt1.votes).to.be.bignumber.equal(web3.utils.toWei('10')); - - const voteReceipt2 = await this.mock.getReceipt(this.proposal.id, voter2); - expect(voteReceipt2.hasVoted).to.be.equal(true); - expect(voteReceipt2.support).to.be.bignumber.equal(Enums.VoteType.For); - expect(voteReceipt2.votes).to.be.bignumber.equal(web3.utils.toWei('7')); - - const voteReceipt3 = await this.mock.getReceipt(this.proposal.id, voter3); - expect(voteReceipt3.hasVoted).to.be.equal(true); - expect(voteReceipt3.support).to.be.bignumber.equal(Enums.VoteType.Against); - expect(voteReceipt3.votes).to.be.bignumber.equal(web3.utils.toWei('5')); - - const voteReceipt4 = await this.mock.getReceipt(this.proposal.id, voter4); - expect(voteReceipt4.hasVoted).to.be.equal(true); - expect(voteReceipt4.support).to.be.bignumber.equal(Enums.VoteType.Abstain); - expect(voteReceipt4.votes).to.be.bignumber.equal(web3.utils.toWei('2')); - - expectEvent(txPropose, 'ProposalCreated', { - proposalId: this.proposal.id, - proposer, - targets: this.proposal.targets, - // values: this.proposal.values, - signatures: this.proposal.signatures.map(() => ''), // this event doesn't contain the proposal detail - calldatas: this.proposal.fulldata, - startBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay), - endBlock: new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod), - description: this.proposal.description, - }); - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - }); - - it('double voting is forbidden', 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( - [ - { target, data: this.receiver.contract.methods.mockFunction().encodeABI() }, - { target, data: this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI() }, - { target, signature: 'mockFunctionNonPayable()' }, - { - target, - signature: 'mockFunctionWithArgs(uint256,uint256)', - data: web3.eth.abi.encodeParameters(['uint256', 'uint256'], [18, 43]), - }, - ], - '', - ); - - await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', { a: '17', b: '42' }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', { a: '18', b: '43' }); - }); - - describe('should revert', function () { - describe('on propose', function () { - it('if proposal does not meet proposalThreshold', async function () { - await expectRevert(this.helper.propose({ from: other }), 'Governor: proposer votes below proposal threshold'); - }); - }); - - describe('on vote', function () { - it('if vote type is invalide', async function () { - await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - await expectRevert( - this.helper.vote({ support: 5 }, { from: voter1 }), - 'GovernorCompatibilityBravo: invalid vote type', + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + signature: 'mockFunction()', + }, + ], + '', ); }); - }); - }); - describe('cancel', function () { - it('proposer can cancel', async function () { - await this.helper.propose({ from: proposer }); - await this.helper.cancel({ from: proposer }); - }); + shouldBehaveLikeEIP6372(mode); - it('anyone can cancel if proposer drop below threshold', async function () { - await this.helper.propose({ from: proposer }); - await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer }); - await this.helper.cancel(); - }); + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.quorumVotes()).to.be.bignumber.equal('0'); + expect(await this.mock.COUNTING_MODE()).to.be.equal('support=bravo&quorum=bravo'); + }); - it('cannot cancel is proposer is still above threshold', async function () { - await this.helper.propose({ from: proposer }); - await expectRevert(this.helper.cancel(), 'GovernorBravo: proposer above threshold'); + it('nominal workflow', async function () { + // Before + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(false); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(value); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal('0'); + + // Run proposal + const txPropose = await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + // After + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); + + const proposal = await this.mock.proposals(this.proposal.id); + expect(proposal.id).to.be.bignumber.equal(this.proposal.id); + expect(proposal.proposer).to.be.equal(proposer); + expect(proposal.eta).to.be.bignumber.equal(await this.mock.proposalEta(this.proposal.id)); + expect(proposal.startBlock).to.be.bignumber.equal(await this.mock.proposalSnapshot(this.proposal.id)); + expect(proposal.endBlock).to.be.bignumber.equal(await this.mock.proposalDeadline(this.proposal.id)); + expect(proposal.canceled).to.be.equal(false); + expect(proposal.executed).to.be.equal(true); + + const action = await this.mock.getActions(this.proposal.id); + expect(action.targets).to.be.deep.equal(this.proposal.targets); + // expect(action.values).to.be.deep.equal(this.proposal.values); + expect(action.signatures).to.be.deep.equal(this.proposal.signatures); + expect(action.calldatas).to.be.deep.equal(this.proposal.data); + + const voteReceipt1 = await this.mock.getReceipt(this.proposal.id, voter1); + expect(voteReceipt1.hasVoted).to.be.equal(true); + expect(voteReceipt1.support).to.be.bignumber.equal(Enums.VoteType.For); + expect(voteReceipt1.votes).to.be.bignumber.equal(web3.utils.toWei('10')); + + const voteReceipt2 = await this.mock.getReceipt(this.proposal.id, voter2); + expect(voteReceipt2.hasVoted).to.be.equal(true); + expect(voteReceipt2.support).to.be.bignumber.equal(Enums.VoteType.For); + expect(voteReceipt2.votes).to.be.bignumber.equal(web3.utils.toWei('7')); + + const voteReceipt3 = await this.mock.getReceipt(this.proposal.id, voter3); + expect(voteReceipt3.hasVoted).to.be.equal(true); + expect(voteReceipt3.support).to.be.bignumber.equal(Enums.VoteType.Against); + expect(voteReceipt3.votes).to.be.bignumber.equal(web3.utils.toWei('5')); + + const voteReceipt4 = await this.mock.getReceipt(this.proposal.id, voter4); + expect(voteReceipt4.hasVoted).to.be.equal(true); + expect(voteReceipt4.support).to.be.bignumber.equal(Enums.VoteType.Abstain); + expect(voteReceipt4.votes).to.be.bignumber.equal(web3.utils.toWei('2')); + + expectEvent(txPropose, 'ProposalCreated', { + proposalId: this.proposal.id, + proposer, + targets: this.proposal.targets, + // values: this.proposal.values, + signatures: this.proposal.signatures.map(() => ''), // this event doesn't contain the proposal detail + calldatas: this.proposal.fulldata, + voteStart: web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay), + voteEnd: web3.utils + .toBN(await clockFromReceipt[mode](txPropose.receipt)) + .add(votingDelay) + .add(votingPeriod), + description: this.proposal.description, + }); + expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + }); + + it('double voting is forbidden', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await expectRevertCustomError( + this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), + 'GovernorAlreadyCastVote', + [voter1], + ); + }); + + it('with function selector and arguments', async function () { + const target = this.receiver.address; + this.helper.setProposal( + [ + { target, data: this.receiver.contract.methods.mockFunction().encodeABI() }, + { target, data: this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI() }, + { target, signature: 'mockFunctionNonPayable()' }, + { + target, + signature: 'mockFunctionWithArgs(uint256,uint256)', + data: web3.eth.abi.encodeParameters(['uint256', 'uint256'], [18, 43]), + }, + ], + '', + ); + + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', { + a: '17', + b: '42', + }); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalledWithArgs', { + a: '18', + b: '43', + }); + }); + + it('with inconsistent array size for selector and arguments', async function () { + const target = this.receiver.address; + const signatures = ['mockFunction()']; // One signature + const data = ['0x', this.receiver.contract.methods.mockFunctionWithArgs(17, 42).encodeABI()]; // Two data entries + this.helper.setProposal( + { + targets: [target, target], + values: [0, 0], + signatures, + data, + }, + '', + ); + + await expectRevertCustomError(this.helper.propose({ from: proposer }), 'GovernorInvalidSignaturesLength', [ + signatures.length, + data.length, + ]); + }); + + describe('should revert', function () { + describe('on propose', function () { + it('if proposal does not meet proposalThreshold', async function () { + await expectRevertCustomError(this.helper.propose({ from: other }), 'GovernorInsufficientProposerVotes', [ + other, + votes[other], + proposalThreshold, + ]); + }); + }); + + describe('on vote', function () { + it('if vote type is invalid', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await expectRevertCustomError( + this.helper.vote({ support: 5 }, { from: voter1 }), + 'GovernorInvalidVoteType', + [], + ); + }); + }); + }); + + describe('cancel', function () { + it('proposer can cancel', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.cancel('external', { from: proposer }); + }); + + it('anyone can cancel if proposer drop below threshold', async function () { + await this.helper.propose({ from: proposer }); + await this.token.transfer(voter1, web3.utils.toWei('1'), { from: proposer }); + await this.helper.cancel('external'); + }); + + it('cannot cancel is proposer is still above threshold', async function () { + await this.helper.propose({ from: proposer }); + await expectRevertCustomError(this.helper.cancel('external'), 'GovernorInsufficientProposerVotes', [ + proposer, + votes[proposer], + proposalThreshold, + ]); + }); + }); }); - }); + } }); diff --git a/test/governance/extensions/GovernorComp.test.js b/test/governance/extensions/GovernorComp.test.js deleted file mode 100644 index 7fcf14ead..000000000 --- a/test/governance/extensions/GovernorComp.test.js +++ /dev/null @@ -1,82 +0,0 @@ -const { BN } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); - -const Enums = require('../../helpers/enums'); -const { GovernorHelper } = require('../../helpers/governance'); - -const Token = artifacts.require('$ERC20VotesComp'); -const Governor = artifacts.require('$GovernorCompMock'); -const CallReceiver = artifacts.require('CallReceiverMock'); - -contract('GovernorComp', function (accounts) { - const [owner, voter1, voter2, voter3, voter4] = accounts; - - const name = 'OZ-Governor'; - const version = '1'; - const tokenName = 'MockToken'; - const tokenSymbol = 'MTKN'; - const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); - const value = web3.utils.toWei('1'); - - beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock); - - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); - - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - }); - - it('voting with comp token', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); - - await this.mock.proposalVotes(this.proposal.id).then(results => { - expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); - expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); - expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); - }); - }); -}); diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 4d84f2046..22265cc25 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -1,13 +1,17 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); -const Token = artifacts.require('$ERC721Votes'); const Governor = artifacts.require('$GovernorVoteMocks'); const CallReceiver = artifacts.require('CallReceiverMock'); +const TOKENS = [ + { Token: artifacts.require('$ERC721Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC721VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorERC721', function (accounts) { const [owner, voter1, voter2, voter3, voter4] = accounts; @@ -15,94 +19,98 @@ contract('GovernorERC721', function (accounts) { const version = '1'; const tokenName = 'MockNFToken'; const tokenSymbol = 'MTKN'; - const NFT0 = new BN(0); - const NFT1 = new BN(1); - const NFT2 = new BN(2); - const NFT3 = new BN(3); - const NFT4 = new BN(4); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const NFT0 = web3.utils.toBN(0); + const NFT1 = web3.utils.toBN(1); + const NFT2 = web3.utils.toBN(2); + const NFT3 = web3.utils.toBN(3); + const NFT4 = web3.utils.toBN(4); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - await Promise.all([NFT0, NFT1, NFT2, NFT3, NFT4].map(tokenId => this.token.$_mint(owner, tokenId))); - await this.helper.delegate({ token: this.token, to: voter1, tokenId: NFT0 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT1 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT2 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, tokenId: NFT3 }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, tokenId: NFT4 }, { from: owner }); + await Promise.all([NFT0, NFT1, NFT2, NFT3, NFT4].map(tokenId => this.token.$_mint(owner, tokenId))); + await this.helper.delegate({ token: this.token, to: voter1, tokenId: NFT0 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT1 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, tokenId: NFT2 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, tokenId: NFT3 }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, tokenId: NFT4 }, { from: owner }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], + '', + ); + }); - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - }); + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + }); - it('voting with ERC721 token', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); + it('voting with ERC721 token', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); - expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), 'VoteCast', { - voter: voter1, - support: Enums.VoteType.For, - weight: '1', + expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }), 'VoteCast', { + voter: voter1, + support: Enums.VoteType.For, + weight: '1', + }); + + expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', { + voter: voter2, + support: Enums.VoteType.For, + weight: '2', + }); + + expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', { + voter: voter3, + support: Enums.VoteType.Against, + weight: '1', + }); + + expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', { + voter: voter4, + support: Enums.VoteType.Abstain, + weight: '1', + }); + + await this.helper.waitForDeadline(); + await this.helper.execute(); + + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); + + await this.mock.proposalVotes(this.proposal.id).then(results => { + expect(results.forVotes).to.be.bignumber.equal('3'); + expect(results.againstVotes).to.be.bignumber.equal('1'); + expect(results.abstainVotes).to.be.bignumber.equal('1'); + }); + }); }); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }), 'VoteCast', { - voter: voter2, - support: Enums.VoteType.For, - weight: '2', - }); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }), 'VoteCast', { - voter: voter3, - support: Enums.VoteType.Against, - weight: '1', - }); - - expectEvent(await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }), 'VoteCast', { - voter: voter4, - support: Enums.VoteType.Abstain, - weight: '1', - }); - - await this.helper.waitForDeadline(); - await this.helper.execute(); - - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); - - await this.mock.proposalVotes(this.proposal.id).then(results => { - expect(results.forVotes).to.be.bignumber.equal('3'); - expect(results.againstVotes).to.be.bignumber.equal('1'); - expect(results.abstainVotes).to.be.bignumber.equal('1'); - }); - }); + } }); diff --git a/test/governance/extensions/GovernorPreventLateQuorum.test.js b/test/governance/extensions/GovernorPreventLateQuorum.test.js index 387654b52..17ae05a73 100644 --- a/test/governance/extensions/GovernorPreventLateQuorum.test.js +++ b/test/governance/extensions/GovernorPreventLateQuorum.test.js @@ -1,13 +1,19 @@ -const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); const { GovernorHelper } = require('../../helpers/governance'); +const { clockFromReceipt } = require('../../helpers/time'); +const { expectRevertCustomError } = require('../../helpers/customError'); -const Token = artifacts.require('$ERC20VotesComp'); const Governor = artifacts.require('$GovernorPreventLateQuorumMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorPreventLateQuorum', function (accounts) { const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; @@ -16,158 +22,174 @@ contract('GovernorPreventLateQuorum', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); - const lateQuorumVoteExtension = new BN(8); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); + const lateQuorumVoteExtension = web3.utils.toBN(8); const quorum = web3.utils.toWei('1'); const value = web3.utils.toWei('1'); - beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new( - name, - votingDelay, - votingPeriod, - 0, - this.token.address, - lateQuorumVoteExtension, - quorum, - ); - this.receiver = await CallReceiver.new(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); + this.mock = await Governor.new( + name, + votingDelay, + votingPeriod, + 0, + this.token.address, + lateQuorumVoteExtension, + quorum, + ); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); - - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal(quorum); - expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(lateQuorumVoteExtension); - }); - - it('nominal workflow unaffected', async function () { - const txPropose = await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); - - await this.mock.proposalVotes(this.proposal.id).then(results => { - expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); - expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); - expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); - }); - - const startBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay); - const endBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod); - expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); - expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock); - - expectEvent(txPropose, 'ProposalCreated', { - proposalId: this.proposal.id, - proposer, - targets: this.proposal.targets, - // values: this.proposal.values.map(value => new BN(value)), - signatures: this.proposal.signatures, - calldatas: this.proposal.data, - startBlock, - endBlock, - description: this.proposal.description, - }); - }); - - it('Delay is extended to prevent last minute take-over', async function () { - const txPropose = await this.helper.propose({ from: proposer }); - - // compute original schedule - const startBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay); - const endBlock = new BN(txPropose.receipt.blockNumber).add(votingDelay).add(votingPeriod); - expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); - expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock); - - // wait for the last minute to vote - await this.helper.waitForDeadline(-1); - const txVote = await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - - // cannot execute yet - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - - // compute new extended schedule - const extendedDeadline = new BN(txVote.receipt.blockNumber).add(lateQuorumVoteExtension); - expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); - expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(extendedDeadline); - - // still possible to vote - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); - - await this.helper.waitForDeadline(); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); - await this.helper.waitForDeadline(+1); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); - - // check extension event - expectEvent(txVote, 'ProposalExtended', { proposalId: this.proposal.id, extendedDeadline }); - }); - - describe('onlyGovernance updates', function () { - it('setLateQuorumVoteExtension is protected', async function () { - await expectRevert(this.mock.setLateQuorumVoteExtension(0), 'Governor: onlyGovernance'); - }); - - it('can setLateQuorumVoteExtension through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.setLateQuorumVoteExtension('0').encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.execute(), 'LateQuorumVoteExtensionSet', { - oldVoteExtension: lateQuorumVoteExtension, - newVoteExtension: '0', + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], + '', + ); }); - expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal('0'); + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal(quorum); + expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal(lateQuorumVoteExtension); + }); + + it('nominal workflow unaffected', async function () { + const txPropose = await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter3)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter4)).to.be.equal(true); + + await this.mock.proposalVotes(this.proposal.id).then(results => { + expect(results.forVotes).to.be.bignumber.equal(web3.utils.toWei('17')); + expect(results.againstVotes).to.be.bignumber.equal(web3.utils.toWei('5')); + expect(results.abstainVotes).to.be.bignumber.equal(web3.utils.toWei('2')); + }); + + const voteStart = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay); + const voteEnd = web3.utils + .toBN(await clockFromReceipt[mode](txPropose.receipt)) + .add(votingDelay) + .add(votingPeriod); + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(voteStart); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(voteEnd); + + expectEvent(txPropose, 'ProposalCreated', { + proposalId: this.proposal.id, + proposer, + targets: this.proposal.targets, + // values: this.proposal.values.map(value => web3.utils.toBN(value)), + signatures: this.proposal.signatures, + calldatas: this.proposal.data, + voteStart, + voteEnd, + description: this.proposal.description, + }); + }); + + it('Delay is extended to prevent last minute take-over', async function () { + const txPropose = await this.helper.propose({ from: proposer }); + + // compute original schedule + const startBlock = web3.utils.toBN(await clockFromReceipt[mode](txPropose.receipt)).add(votingDelay); + const endBlock = web3.utils + .toBN(await clockFromReceipt[mode](txPropose.receipt)) + .add(votingDelay) + .add(votingPeriod); + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(endBlock); + + // wait for the last minute to vote + await this.helper.waitForDeadline(-1); + const txVote = await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + + // cannot execute yet + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + + // compute new extended schedule + const extendedDeadline = web3.utils + .toBN(await clockFromReceipt[mode](txVote.receipt)) + .add(lateQuorumVoteExtension); + expect(await this.mock.proposalSnapshot(this.proposal.id)).to.be.bignumber.equal(startBlock); + expect(await this.mock.proposalDeadline(this.proposal.id)).to.be.bignumber.equal(extendedDeadline); + + // still possible to vote + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter1 }); + + await this.helper.waitForDeadline(); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Active); + await this.helper.waitForDeadline(+1); + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Defeated); + + // check extension event + expectEvent(txVote, 'ProposalExtended', { proposalId: this.proposal.id, extendedDeadline }); + }); + + describe('onlyGovernance updates', function () { + it('setLateQuorumVoteExtension is protected', async function () { + await expectRevertCustomError( + this.mock.setLateQuorumVoteExtension(0, { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); + }); + + it('can setLateQuorumVoteExtension through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.setLateQuorumVoteExtension('0').encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent(await this.helper.execute(), 'LateQuorumVoteExtensionSet', { + oldVoteExtension: lateQuorumVoteExtension, + newVoteExtension: '0', + }); + + expect(await this.mock.lateQuorumVoteExtension()).to.be.bignumber.equal('0'); + }); + }); }); - }); + } }); diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index 9cca9797c..c406baf8d 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -1,16 +1,18 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const RLP = require('rlp'); const Enums = require('../../helpers/enums'); -const { GovernorHelper } = require('../../helpers/governance'); +const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); +const { expectRevertCustomError } = require('../../helpers/customError'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); -const Token = artifacts.require('$ERC20Votes'); const Timelock = artifacts.require('CompTimelock'); const Governor = artifacts.require('$GovernorTimelockCompoundMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const ERC721 = artifacts.require('$ERC721'); +const ERC1155 = artifacts.require('$ERC1155'); function makeContractAddress(creator, nonce) { return web3.utils.toChecksumAddress( @@ -21,6 +23,11 @@ function makeContractAddress(creator, nonce) { ); } +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorTimelockCompound', function (accounts) { const [owner, voter1, voter2, voter3, voter4, other] = accounts; @@ -29,306 +36,409 @@ contract('GovernorTimelockCompound', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); - - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - - // Need to predict governance address to set it as timelock admin with a delayed transfer - const nonce = await web3.eth.getTransactionCount(deployer); - const predictGovernor = makeContractAddress(deployer, nonce + 1); - - this.timelock = await Timelock.new(predictGovernor, 2 * 86400); - this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.timelock.address, this.token.address, 0); - this.receiver = await CallReceiver.new(); - - this.helper = new GovernorHelper(this.mock); - - await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); - - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); - - shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']); - - it("doesn't accept ether transfers", async function () { - await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); - }); - - it('post deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - - expect(await this.mock.timelock()).to.be.equal(this.timelock.address); - expect(await this.timelock.admin()).to.be.equal(this.mock.address); - }); - - it('nominal', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - const txQueue = await this.helper.queue(); - const eta = await this.mock.proposalEta(this.proposal.id); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'QueueTransaction', { eta }); - - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.timelock, 'ExecuteTransaction', { eta }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - }); - - describe('should revert', function () { - describe('on queue', function () { - it('if already queued', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); - }); - - it('if proposal contains duplicate calls', async function () { - const action = { - target: this.token.address, - data: this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI(), - }; - this.helper.setProposal([action, action], ''); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await expectRevert(this.helper.queue(), 'GovernorTimelockCompound: identical proposal action already queued'); - await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued'); - }); - }); - - describe('on execute', function () { - it('if not queued', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(+1); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); - - await expectRevert(this.helper.execute(), 'GovernorTimelockCompound: proposal not yet queued'); - }); - - it('if too early', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); - - await expectRevert( - this.helper.execute(), - "Timelock::executeTransaction: Transaction hasn't surpassed time lock", - ); - }); - - it('if too late', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(+30 * 86400); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Expired); - - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - it('if already executed', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - await this.helper.execute(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - }); - }); - - describe('cancel', function () { - it('cancel before queue prevents scheduling', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.cancel(), 'ProposalCanceled', { proposalId: this.proposal.id }); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); - }); - - it('cancel after queue prevents executing', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expectEvent(await this.helper.cancel(), 'ProposalCanceled', { proposalId: this.proposal.id }); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - }); - - describe('onlyGovernance', function () { - describe('relay', function () { + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { beforeEach(async function () { - await this.token.$_mint(this.mock.address, 1); - }); + const [deployer] = await web3.eth.getAccounts(); - it('is protected', async function () { - await expectRevert( - this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()), - 'Governor: onlyGovernance', + this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); + + // Need to predict governance address to set it as timelock admin with a delayed transfer + const nonce = await web3.eth.getTransactionCount(deployer); + const predictGovernor = makeContractAddress(deployer, nonce + 1); + + this.timelock = await Timelock.new(predictGovernor, 2 * 86400); + this.mock = await Governor.new( + name, + votingDelay, + votingPeriod, + 0, + this.timelock.address, + this.token.address, + 0, ); - }); + this.receiver = await CallReceiver.new(); - it('can be executed through governance', async function () { - this.helper.setProposal( + this.helper = new GovernorHelper(this.mock, mode); + + await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); + + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + + // default proposal + this.proposal = this.helper.setProposal( [ { - target: this.mock.address, - data: this.mock.contract.methods - .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) - .encodeABI(), + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), }, ], '', ); + }); - expect(await this.token.balanceOf(this.mock.address), 1); - expect(await this.token.balanceOf(other), 0); + shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']); + it("doesn't accept ether transfers", async function () { + await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); + }); + + it('post deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + + expect(await this.mock.timelock()).to.be.equal(this.timelock.address); + expect(await this.timelock.admin()).to.be.equal(this.mock.address); + }); + + it('nominal', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); await this.helper.waitForDeadline(); - await this.helper.queue(); + const txQueue = await this.helper.queue(); + const eta = await this.mock.proposalEta(this.proposal.id); await this.helper.waitForEta(); const txExecute = await this.helper.execute(); - expect(await this.token.balanceOf(this.mock.address), 0); - expect(await this.token.balanceOf(other), 1); + expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txQueue.tx, this.timelock, 'QueueTransaction', { eta }); - expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { - from: this.mock.address, - to: other, - value: '1', + expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txExecute.tx, this.timelock, 'ExecuteTransaction', { eta }); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + }); + + describe('should revert', function () { + describe('on queue', function () { + it('if already queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Queued, + proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ]); + }); + + it('if proposal contains duplicate calls', async function () { + const action = { + target: this.token.address, + data: this.token.contract.methods.approve(this.receiver.address, constants.MAX_UINT256).encodeABI(), + }; + const { id } = this.helper.setProposal([action, action], ''); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await expectRevertCustomError(this.helper.queue(), 'GovernorAlreadyQueuedProposal', [id]); + await expectRevertCustomError(this.helper.execute(), 'GovernorNotQueuedProposal', [id]); + }); + }); + + describe('on execute', function () { + it('if not queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(+1); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + + await expectRevertCustomError(this.helper.execute(), 'GovernorNotQueuedProposal', [this.proposal.id]); + }); + + it('if too early', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + await expectRevert( + this.helper.execute(), + "Timelock::executeTransaction: Transaction hasn't surpassed time lock", + ); + }); + + it('if too late', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(+30 * 86400); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Expired); + + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Expired, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + + it('if already executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + }); + + describe('on safe receive', function () { + describe('ERC721', function () { + const name = 'Non Fungible Token'; + const symbol = 'NFT'; + const tokenId = web3.utils.toBN(1); + + beforeEach(async function () { + this.token = await ERC721.new(name, symbol); + await this.token.$_mint(owner, tokenId); + }); + + it("can't receive an ERC721 safeTransfer", async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }), + 'GovernorDisabledDeposit', + [], + ); + }); + }); + + describe('ERC1155', function () { + const uri = 'https://token-cdn-domain/{id}.json'; + const tokenIds = { + 1: web3.utils.toBN(1000), + 2: web3.utils.toBN(2000), + 3: web3.utils.toBN(3000), + }; + + beforeEach(async function () { + this.token = await ERC1155.new(uri); + await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + }); + + it("can't receive ERC1155 safeTransfer", async function () { + await expectRevertCustomError( + this.token.safeTransferFrom( + owner, + this.mock.address, + ...Object.entries(tokenIds)[0], // id + amount + '0x', + { from: owner }, + ), + 'GovernorDisabledDeposit', + [], + ); + }); + + it("can't receive ERC1155 safeBatchTransfer", async function () { + await expectRevertCustomError( + this.token.safeBatchTransferFrom( + owner, + this.mock.address, + Object.keys(tokenIds), + Object.values(tokenIds), + '0x', + { from: owner }, + ), + 'GovernorDisabledDeposit', + [], + ); + }); + }); + }); + }); + + describe('cancel', function () { + it('cancel before queue prevents scheduling', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ]); + }); + + it('cancel after queue prevents executing', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + }); + + describe('onlyGovernance', function () { + describe('relay', function () { + beforeEach(async function () { + await this.token.$_mint(this.mock.address, 1); + }); + + it('is protected', async function () { + await expectRevertCustomError( + this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI(), { + from: owner, + }), + 'GovernorOnlyExecutor', + [owner], + ); + }); + + it('can be executed through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods + .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) + .encodeABI(), + }, + ], + '', + ); + + expect(await this.token.balanceOf(this.mock.address), 1); + expect(await this.token.balanceOf(other), 0); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expect(await this.token.balanceOf(this.mock.address), 0); + expect(await this.token.balanceOf(other), 1); + + await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { + from: this.mock.address, + to: other, + value: '1', + }); + }); + }); + + describe('updateTimelock', function () { + beforeEach(async function () { + this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400); + }); + + it('is protected', async function () { + await expectRevertCustomError( + this.mock.updateTimelock(this.newTimelock.address, { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); + }); + + it('can be executed through governance to', async function () { + this.helper.setProposal( + [ + { + target: this.timelock.address, + data: this.timelock.contract.methods.setPendingAdmin(owner).encodeABI(), + }, + { + target: this.mock.address, + data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expectEvent(txExecute, 'TimelockChange', { + oldTimelock: this.timelock.address, + newTimelock: this.newTimelock.address, + }); + + expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + }); + }); + + it('can transfer timelock to new governor', async function () { + const newGovernor = await Governor.new(name, 8, 32, 0, this.timelock.address, this.token.address, 0); + this.helper.setProposal( + [ + { + target: this.timelock.address, + data: this.timelock.contract.methods.setPendingAdmin(newGovernor.address).encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + await expectEvent.inTransaction(txExecute.tx, this.timelock, 'NewPendingAdmin', { + newPendingAdmin: newGovernor.address, + }); + + await newGovernor.__acceptAdmin(); + expect(await this.timelock.admin()).to.be.bignumber.equal(newGovernor.address); }); }); }); - - describe('updateTimelock', function () { - beforeEach(async function () { - this.newTimelock = await Timelock.new(this.mock.address, 7 * 86400); - }); - - it('is protected', async function () { - await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance'); - }); - - it('can be executed through governance to', async function () { - this.helper.setProposal( - [ - { - target: this.timelock.address, - data: this.timelock.contract.methods.setPendingAdmin(owner).encodeABI(), - }, - { - target: this.mock.address, - data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expectEvent(txExecute, 'TimelockChange', { - oldTimelock: this.timelock.address, - newTimelock: this.newTimelock.address, - }); - - expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); - }); - }); - - it('can transfer timelock to new governor', async function () { - const newGovernor = await Governor.new(name, 8, 32, 0, this.timelock.address, this.token.address, 0); - this.helper.setProposal( - [ - { - target: this.timelock.address, - data: this.timelock.contract.methods.setPendingAdmin(newGovernor.address).encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - await expectEvent.inTransaction(txExecute.tx, this.timelock, 'NewPendingAdmin', { - newPendingAdmin: newGovernor.address, - }); - - await newGovernor.__acceptAdmin(); - expect(await this.timelock.admin()).to.be.bignumber.equal(newGovernor.address); - }); - }); + } }); diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index b5e2e7f11..3265dfa68 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -1,15 +1,22 @@ -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); -const { GovernorHelper } = require('../../helpers/governance'); +const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); +const { expectRevertCustomError } = require('../../helpers/customError'); const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); -const Token = artifacts.require('$ERC20Votes'); const Timelock = artifacts.require('TimelockController'); const Governor = artifacts.require('$GovernorTimelockControlMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const ERC721 = artifacts.require('$ERC721'); +const ERC1155 = artifacts.require('$ERC1155'); + +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, +]; contract('GovernorTimelockControl', function (accounts) { const [owner, voter1, voter2, voter3, voter4, other] = accounts; @@ -24,270 +31,466 @@ contract('GovernorTimelockControl', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - const [deployer] = await web3.eth.getAccounts(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + const [deployer] = await web3.eth.getAccounts(); - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.timelock = await Timelock.new(3600, [], [], deployer); - this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.timelock.address, this.token.address, 0); - this.receiver = await CallReceiver.new(); + this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); + this.timelock = await Timelock.new(3600, [], [], deployer); + this.mock = await Governor.new( + name, + votingDelay, + votingPeriod, + 0, + this.timelock.address, + this.token.address, + 0, + ); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - this.DEFAULT_ADMIN_ROLE = await this.timelock.DEFAULT_ADMIN_ROLE(); - this.PROPOSER_ROLE = await this.timelock.PROPOSER_ROLE(); - this.EXECUTOR_ROLE = await this.timelock.EXECUTOR_ROLE(); - this.CANCELLER_ROLE = await this.timelock.CANCELLER_ROLE(); + this.PROPOSER_ROLE = await this.timelock.PROPOSER_ROLE(); + this.EXECUTOR_ROLE = await this.timelock.EXECUTOR_ROLE(); + this.CANCELLER_ROLE = await this.timelock.CANCELLER_ROLE(); - await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.timelock.address, value }); - // normal setup: governor is proposer, everyone is executor, timelock is its own admin - await this.timelock.grantRole(PROPOSER_ROLE, this.mock.address); - await this.timelock.grantRole(PROPOSER_ROLE, owner); - await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address); - await this.timelock.grantRole(CANCELLER_ROLE, owner); - await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS); - await this.timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer); + // normal setup: governor is proposer, everyone is executor, timelock is its own admin + await this.timelock.grantRole(PROPOSER_ROLE, this.mock.address); + await this.timelock.grantRole(PROPOSER_ROLE, owner); + await this.timelock.grantRole(CANCELLER_ROLE, this.mock.address); + await this.timelock.grantRole(CANCELLER_ROLE, owner); + await this.timelock.grantRole(EXECUTOR_ROLE, constants.ZERO_ADDRESS); + await this.timelock.revokeRole(DEFAULT_ADMIN_ROLE, deployer); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - this.proposal.timelockid = await this.timelock.hashOperationBatch( - ...this.proposal.shortProposal.slice(0, 3), - '0x0', - this.proposal.shortProposal[3], - ); - }); - - shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']); - - it("doesn't accept ether transfers", async function () { - await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); - }); - - it('post deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - - expect(await this.mock.timelock()).to.be.equal(this.timelock.address); - }); - - it('nominal', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - const txQueue = await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid }); - - expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); - await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid }); - await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); - }); - - describe('should revert', function () { - describe('on queue', function () { - it('if already queued', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); - }); - }); - - describe('on execute', function () { - it('if not queued', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(+1); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); - - await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready'); - }); - - it('if too early', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); - - await expectRevert(this.helper.execute(), 'TimelockController: operation is not ready'); - }); - - it('if already executed', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - await this.helper.execute(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - it('if already executed by another proposer', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - - await this.timelock.executeBatch( + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], + '', + ); + this.proposal.timelockid = await this.timelock.hashOperationBatch( ...this.proposal.shortProposal.slice(0, 3), '0x0', this.proposal.shortProposal[3], ); - - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - }); - }); - - describe('cancel', function () { - it('cancel before queue prevents scheduling', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.cancel(), 'ProposalCanceled', { proposalId: this.proposal.id }); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.queue(), 'Governor: proposal not successful'); - }); - - it('cancel after queue prevents executing', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expectEvent(await this.helper.cancel(), 'ProposalCanceled', { proposalId: this.proposal.id }); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - it('cancel on timelock is reflected on governor', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); - - expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', { - id: this.proposal.timelockid, }); - expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); - }); - }); + shouldSupportInterfaces(['ERC165', 'Governor', 'GovernorWithParams', 'GovernorTimelock']); - describe('onlyGovernance', function () { - describe('relay', function () { - beforeEach(async function () { - await this.token.$_mint(this.mock.address, 1); + it("doesn't accept ether transfers", async function () { + await expectRevert.unspecified(web3.eth.sendTransaction({ from: owner, to: this.mock.address, value: 1 })); }); - it('is protected', async function () { - await expectRevert( - this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()), - 'Governor: onlyGovernance', - ); + it('post deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + + expect(await this.mock.timelock()).to.be.equal(this.timelock.address); }); - it('can be executed through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods - .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) - .encodeABI(), - }, - ], - '', - ); - - expect(await this.token.balanceOf(this.mock.address), 1); - expect(await this.token.balanceOf(other), 0); - + it('nominal', async function () { await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); await this.helper.waitForDeadline(); - await this.helper.queue(); + const txQueue = await this.helper.queue(); await this.helper.waitForEta(); const txExecute = await this.helper.execute(); - expect(await this.token.balanceOf(this.mock.address), 0); - expect(await this.token.balanceOf(other), 1); + expectEvent(txQueue, 'ProposalQueued', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallScheduled', { id: this.proposal.timelockid }); + await expectEvent.inTransaction(txQueue.tx, this.timelock, 'CallSalt', { + id: this.proposal.timelockid, + }); - expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { - from: this.mock.address, - to: other, - value: '1', + expectEvent(txExecute, 'ProposalExecuted', { proposalId: this.proposal.id }); + await expectEvent.inTransaction(txExecute.tx, this.timelock, 'CallExecuted', { id: this.proposal.timelockid }); + await expectEvent.inTransaction(txExecute.tx, this.receiver, 'MockFunctionCalled'); + }); + + describe('should revert', function () { + describe('on queue', function () { + it('if already queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Queued, + proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ]); + }); + }); + + describe('on execute', function () { + it('if not queued', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(+1); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Succeeded); + + await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ + this.proposal.timelockid, + Enums.OperationState.Ready, + ]); + }); + + it('if too early', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + await expectRevertCustomError(this.helper.execute(), 'TimelockUnexpectedOperationState', [ + this.proposal.timelockid, + Enums.OperationState.Ready, + ]); + }); + + it('if already executed', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + + it('if already executed by another proposer', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + + await this.timelock.executeBatch( + ...this.proposal.shortProposal.slice(0, 3), + '0x0', + this.proposal.shortProposal[3], + ); + + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Executed, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); }); }); - it('is payable and can transfer eth to EOA', async function () { - const t2g = web3.utils.toBN(128); // timelock to governor - const g2o = web3.utils.toBN(100); // governor to eoa (other) + describe('cancel', function () { + it('cancel before queue prevents scheduling', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevertCustomError(this.helper.queue(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded]), + ]); + }); + + it('cancel after queue prevents executing', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expectEvent(await this.helper.cancel('internal'), 'ProposalCanceled', { proposalId: this.proposal.id }); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Canceled, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); + + it('cancel on timelock is reflected on governor', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Queued); + + expectEvent(await this.timelock.cancel(this.proposal.timelockid, { from: owner }), 'Cancelled', { + id: this.proposal.timelockid, + }); + + expect(await this.mock.state(this.proposal.id)).to.be.bignumber.equal(Enums.ProposalState.Canceled); + }); + }); + + describe('onlyGovernance', function () { + describe('relay', function () { + beforeEach(async function () { + await this.token.$_mint(this.mock.address, 1); + }); + + it('is protected', async function () { + await expectRevertCustomError( + this.mock.relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI(), { + from: owner, + }), + 'GovernorOnlyExecutor', + [owner], + ); + }); + + it('can be executed through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods + .relay(this.token.address, 0, this.token.contract.methods.transfer(other, 1).encodeABI()) + .encodeABI(), + }, + ], + '', + ); + + expect(await this.token.balanceOf(this.mock.address), 1); + expect(await this.token.balanceOf(other), 0); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expect(await this.token.balanceOf(this.mock.address), 0); + expect(await this.token.balanceOf(other), 1); + + await expectEvent.inTransaction(txExecute.tx, this.token, 'Transfer', { + from: this.mock.address, + to: other, + value: '1', + }); + }); + + it('is payable and can transfer eth to EOA', async function () { + const t2g = web3.utils.toBN(128); // timelock to governor + const g2o = web3.utils.toBN(100); // governor to eoa (other) + + this.helper.setProposal( + [ + { + target: this.mock.address, + value: t2g, + data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(), + }, + ], + '', + ); + + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); + const timelockBalance = await web3.eth.getBalance(this.timelock.address).then(web3.utils.toBN); + const otherBalance = await web3.eth.getBalance(other).then(web3.utils.toBN); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + await this.helper.execute(); + + expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(timelockBalance.sub(t2g)); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(t2g.sub(g2o)); + expect(await web3.eth.getBalance(other)).to.be.bignumber.equal(otherBalance.add(g2o)); + }); + + it('protected against other proposers', async function () { + const target = this.mock.address; + const value = web3.utils.toWei('0'); + const data = this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(); + const predecessor = constants.ZERO_BYTES32; + const salt = constants.ZERO_BYTES32; + const delay = 3600; + + await this.timelock.schedule(target, value, data, predecessor, salt, delay, { from: owner }); + + await time.increase(3600); + + await expectRevertCustomError( + this.timelock.execute(target, value, data, predecessor, salt, { from: owner }), + 'QueueEmpty', // Bubbled up from Governor + [], + ); + }); + }); + + describe('updateTimelock', function () { + beforeEach(async function () { + this.newTimelock = await Timelock.new( + 3600, + [this.mock.address], + [this.mock.address], + constants.ZERO_ADDRESS, + ); + }); + + it('is protected', async function () { + await expectRevertCustomError( + this.mock.updateTimelock(this.newTimelock.address, { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); + }); + + it('can be executed through governance to', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.queue(); + await this.helper.waitForEta(); + const txExecute = await this.helper.execute(); + + expectEvent(txExecute, 'TimelockChange', { + oldTimelock: this.timelock.address, + newTimelock: this.newTimelock.address, + }); + + expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); + }); + }); + + describe('on safe receive', function () { + describe('ERC721', function () { + const name = 'Non Fungible Token'; + const symbol = 'NFT'; + const tokenId = web3.utils.toBN(1); + + beforeEach(async function () { + this.token = await ERC721.new(name, symbol); + await this.token.$_mint(owner, tokenId); + }); + + it("can't receive an ERC721 safeTransfer", async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(owner, this.mock.address, tokenId, { from: owner }), + 'GovernorDisabledDeposit', + [], + ); + }); + }); + + describe('ERC1155', function () { + const uri = 'https://token-cdn-domain/{id}.json'; + const tokenIds = { + 1: web3.utils.toBN(1000), + 2: web3.utils.toBN(2000), + 3: web3.utils.toBN(3000), + }; + + beforeEach(async function () { + this.token = await ERC1155.new(uri); + await this.token.$_mintBatch(owner, Object.keys(tokenIds), Object.values(tokenIds), '0x'); + }); + + it("can't receive ERC1155 safeTransfer", async function () { + await expectRevertCustomError( + this.token.safeTransferFrom( + owner, + this.mock.address, + ...Object.entries(tokenIds)[0], // id + amount + '0x', + { from: owner }, + ), + 'GovernorDisabledDeposit', + [], + ); + }); + + it("can't receive ERC1155 safeBatchTransfer", async function () { + await expectRevertCustomError( + this.token.safeBatchTransferFrom( + owner, + this.mock.address, + Object.keys(tokenIds), + Object.values(tokenIds), + '0x', + { from: owner }, + ), + 'GovernorDisabledDeposit', + [], + ); + }); + }); + }); + }); + + it('clear queue of pending governor calls', async function () { this.helper.setProposal( [ { target: this.mock.address, - value: t2g, - data: this.mock.contract.methods.relay(other, g2o, '0x').encodeABI(), + data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(), }, ], '', ); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(web3.utils.toBN(0)); - const timelockBalance = await web3.eth.getBalance(this.timelock.address).then(web3.utils.toBN); - const otherBalance = await web3.eth.getBalance(other).then(web3.utils.toBN); - await this.helper.propose(); await this.helper.waitForSnapshot(); await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); @@ -296,97 +499,10 @@ contract('GovernorTimelockControl', function (accounts) { await this.helper.waitForEta(); await this.helper.execute(); - expect(await web3.eth.getBalance(this.timelock.address)).to.be.bignumber.equal(timelockBalance.sub(t2g)); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal(t2g.sub(g2o)); - expect(await web3.eth.getBalance(other)).to.be.bignumber.equal(otherBalance.add(g2o)); - }); - - it('protected against other proposers', async function () { - await this.timelock.schedule( - this.mock.address, - web3.utils.toWei('0'), - this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(), - constants.ZERO_BYTES32, - constants.ZERO_BYTES32, - 3600, - { from: owner }, - ); - - await time.increase(3600); - - await expectRevert( - this.timelock.execute( - this.mock.address, - web3.utils.toWei('0'), - this.mock.contract.methods.relay(constants.ZERO_ADDRESS, 0, '0x').encodeABI(), - constants.ZERO_BYTES32, - constants.ZERO_BYTES32, - { from: owner }, - ), - 'TimelockController: underlying transaction reverted', - ); + // This path clears _governanceCall as part of the afterExecute call, + // but we have not way to check that the cleanup actually happened other + // then coverage reports. }); }); - - describe('updateTimelock', function () { - beforeEach(async function () { - this.newTimelock = await Timelock.new(3600, [this.mock.address], [this.mock.address], constants.ZERO_ADDRESS); - }); - - it('is protected', async function () { - await expectRevert(this.mock.updateTimelock(this.newTimelock.address), 'Governor: onlyGovernance'); - }); - - it('can be executed through governance to', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.updateTimelock(this.newTimelock.address).encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - const txExecute = await this.helper.execute(); - - expectEvent(txExecute, 'TimelockChange', { - oldTimelock: this.timelock.address, - newTimelock: this.newTimelock.address, - }); - - expect(await this.mock.timelock()).to.be.bignumber.equal(this.newTimelock.address); - }); - }); - }); - - it('clear queue of pending governor calls', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.nonGovernanceFunction().encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.queue(); - await this.helper.waitForEta(); - await this.helper.execute(); - - // This path clears _governanceCall as part of the afterExecute call, - // but we have not way to check that the cleanup actually happened other - // then coverage reports. - }); + } }); diff --git a/test/governance/extensions/GovernorVotesQuorumFraction.test.js b/test/governance/extensions/GovernorVotesQuorumFraction.test.js index a20695ac0..ece9c78d6 100644 --- a/test/governance/extensions/GovernorVotesQuorumFraction.test.js +++ b/test/governance/extensions/GovernorVotesQuorumFraction.test.js @@ -1,13 +1,19 @@ -const { BN, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { expectEvent, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const Enums = require('../../helpers/enums'); -const { GovernorHelper } = require('../../helpers/governance'); +const { GovernorHelper, proposalStatesToBitMap } = require('../../helpers/governance'); +const { clock } = require('../../helpers/time'); +const { expectRevertCustomError } = require('../../helpers/customError'); -const Token = artifacts.require('$ERC20Votes'); const Governor = artifacts.require('$GovernorMock'); const CallReceiver = artifacts.require('CallReceiverMock'); +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorVotesQuorumFraction', function (accounts) { const [owner, voter1, voter2, voter3, voter4] = accounts; @@ -15,129 +21,147 @@ contract('GovernorVotesQuorumFraction', function (accounts) { const version = '1'; const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; - const tokenSupply = new BN(web3.utils.toWei('100')); - const ratio = new BN(8); // percents - const newRatio = new BN(6); // percents - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const tokenSupply = web3.utils.toBN(web3.utils.toWei('100')); + const ratio = web3.utils.toBN(8); // percents + const newRatio = web3.utils.toBN(6); // percents + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - this.owner = owner; - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.token.address, ratio); - this.receiver = await CallReceiver.new(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.owner = owner; + this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); + this.mock = await Governor.new(name, votingDelay, votingPeriod, 0, this.token.address, ratio); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); - - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); - expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(ratio); - expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); - expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1)))).to.be.bignumber.equal( - tokenSupply.mul(ratio).divn(100), - ); - }); - - it('quroum reached', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - }); - - it('quroum not reached', async function () { - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.waitForDeadline(); - await expectRevert(this.helper.execute(), 'Governor: proposal not successful'); - }); - - describe('onlyGovernance updates', function () { - it('updateQuorumNumerator is protected', async function () { - await expectRevert(this.mock.updateQuorumNumerator(newRatio), 'Governor: onlyGovernance'); - }); - - it('can updateQuorumNumerator through governance', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - expectEvent(await this.helper.execute(), 'QuorumNumeratorUpdated', { - oldQuorumNumerator: ratio, - newQuorumNumerator: newRatio, + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), + }, + ], + '', + ); }); - expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(newRatio); - expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + expect(await this.mock.quorum(0)).to.be.bignumber.equal('0'); + expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(ratio); + expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); + expect(await clock[mode]().then(timepoint => this.mock.quorum(timepoint - 1))).to.be.bignumber.equal( + tokenSupply.mul(ratio).divn(100), + ); + }); - // it takes one block for the new quorum to take effect - expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1)))).to.be.bignumber.equal( - tokenSupply.mul(ratio).divn(100), - ); + it('quroum reached', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + }); - await time.advanceBlock(); + it('quroum not reached', async function () { + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.waitForDeadline(); + await expectRevertCustomError(this.helper.execute(), 'GovernorUnexpectedProposalState', [ + this.proposal.id, + Enums.ProposalState.Defeated, + proposalStatesToBitMap([Enums.ProposalState.Succeeded, Enums.ProposalState.Queued]), + ]); + }); - expect(await time.latestBlock().then(blockNumber => this.mock.quorum(blockNumber.subn(1)))).to.be.bignumber.equal( - tokenSupply.mul(newRatio).divn(100), - ); + describe('onlyGovernance updates', function () { + it('updateQuorumNumerator is protected', async function () { + await expectRevertCustomError( + this.mock.updateQuorumNumerator(newRatio, { from: owner }), + 'GovernorOnlyExecutor', + [owner], + ); + }); + + it('can updateQuorumNumerator through governance', async function () { + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.updateQuorumNumerator(newRatio).encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + expectEvent(await this.helper.execute(), 'QuorumNumeratorUpdated', { + oldQuorumNumerator: ratio, + newQuorumNumerator: newRatio, + }); + + expect(await this.mock.quorumNumerator()).to.be.bignumber.equal(newRatio); + expect(await this.mock.quorumDenominator()).to.be.bignumber.equal('100'); + + // it takes one block for the new quorum to take effect + expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal( + tokenSupply.mul(ratio).divn(100), + ); + + await time.advanceBlock(); + + expect(await clock[mode]().then(blockNumber => this.mock.quorum(blockNumber - 1))).to.be.bignumber.equal( + tokenSupply.mul(newRatio).divn(100), + ); + }); + + it('cannot updateQuorumNumerator over the maximum', async function () { + const quorumNumerator = 101; + this.helper.setProposal( + [ + { + target: this.mock.address, + data: this.mock.contract.methods.updateQuorumNumerator(quorumNumerator).encodeABI(), + }, + ], + '', + ); + + await this.helper.propose(); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); + await this.helper.waitForDeadline(); + + const quorumDenominator = await this.mock.quorumDenominator(); + + await expectRevertCustomError(this.helper.execute(), 'GovernorInvalidQuorumFraction', [ + quorumNumerator, + quorumDenominator, + ]); + }); + }); }); - - it('cannot updateQuorumNumerator over the maximum', async function () { - this.helper.setProposal( - [ - { - target: this.mock.address, - data: this.mock.contract.methods.updateQuorumNumerator('101').encodeABI(), - }, - ], - '', - ); - - await this.helper.propose(); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter1 }); - await this.helper.waitForDeadline(); - - await expectRevert(this.helper.execute(), 'GovernorVotesQuorumFraction: quorumNumerator over quorumDenominator'); - }); - }); + } }); diff --git a/test/governance/extensions/GovernorWithParams.test.js b/test/governance/extensions/GovernorWithParams.test.js index 8680c7c19..00936c505 100644 --- a/test/governance/extensions/GovernorWithParams.test.js +++ b/test/governance/extensions/GovernorWithParams.test.js @@ -1,25 +1,28 @@ -const { BN, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; const { fromRpcSig } = require('ethereumjs-util'); const Enums = require('../../helpers/enums'); -const { getChainId } = require('../../helpers/chainid'); -const { EIP712Domain } = require('../../helpers/eip712'); +const { getDomain, domainType } = require('../../helpers/eip712'); const { GovernorHelper } = require('../../helpers/governance'); -const Token = artifacts.require('$ERC20VotesComp'); const Governor = artifacts.require('$GovernorWithParamsMock'); const CallReceiver = artifacts.require('CallReceiverMock'); const rawParams = { - uintParam: new BN('42'), + uintParam: web3.utils.toBN('42'), strParam: 'These are my params', }; const encodedParams = web3.eth.abi.encodeParameters(['uint256', 'string'], Object.values(rawParams)); +const TOKENS = [ + { Token: artifacts.require('$ERC20Votes'), mode: 'blocknumber' }, + { Token: artifacts.require('$ERC20VotesTimestampMock'), mode: 'timestamp' }, +]; + contract('GovernorWithParams', function (accounts) { const [owner, proposer, voter1, voter2, voter3, voter4] = accounts; @@ -28,143 +31,145 @@ contract('GovernorWithParams', function (accounts) { const tokenName = 'MockToken'; const tokenSymbol = 'MTKN'; const tokenSupply = web3.utils.toWei('100'); - const votingDelay = new BN(4); - const votingPeriod = new BN(16); + const votingDelay = web3.utils.toBN(4); + const votingPeriod = web3.utils.toBN(16); const value = web3.utils.toWei('1'); - beforeEach(async function () { - this.chainId = await getChainId(); - this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); - this.mock = await Governor.new(name, this.token.address); - this.receiver = await CallReceiver.new(); + for (const { mode, Token } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.chainId = await web3.eth.getChainId(); + this.token = await Token.new(tokenName, tokenSymbol, tokenName, version); + this.mock = await Governor.new(name, this.token.address); + this.receiver = await CallReceiver.new(); - this.helper = new GovernorHelper(this.mock); + this.helper = new GovernorHelper(this.mock, mode); - await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); + await web3.eth.sendTransaction({ from: owner, to: this.mock.address, value }); - await this.token.$_mint(owner, tokenSupply); - await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); - await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); + await this.token.$_mint(owner, tokenSupply); + await this.helper.delegate({ token: this.token, to: voter1, value: web3.utils.toWei('10') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter2, value: web3.utils.toWei('7') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter3, value: web3.utils.toWei('5') }, { from: owner }); + await this.helper.delegate({ token: this.token, to: voter4, value: web3.utils.toWei('2') }, { from: owner }); - // default proposal - this.proposal = this.helper.setProposal( - [ - { - target: this.receiver.address, - value, - data: this.receiver.contract.methods.mockFunction().encodeABI(), - }, - ], - '', - ); - }); - - it('deployment check', async function () { - expect(await this.mock.name()).to.be.equal(name); - expect(await this.mock.token()).to.be.equal(this.token.address); - expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); - expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); - }); - - it('nominal is unaffected', async function () { - await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); - await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); - await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); - await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); - await this.helper.waitForDeadline(); - await this.helper.execute(); - - expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); - expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); - expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); - expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); - expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); - }); - - it('Voting with params is properly supported', async function () { - await this.helper.propose({ from: proposer }); - await this.helper.waitForSnapshot(); - - const weight = new BN(web3.utils.toWei('7')).sub(rawParams.uintParam); - - const tx = await this.helper.vote( - { - support: Enums.VoteType.For, - reason: 'no particular reason', - params: encodedParams, - }, - { from: voter2 }, - ); - - expectEvent(tx, 'CountParams', { ...rawParams }); - expectEvent(tx, 'VoteCastWithParams', { - voter: voter2, - proposalId: this.proposal.id, - support: Enums.VoteType.For, - weight, - reason: 'no particular reason', - params: encodedParams, - }); - - const votes = await this.mock.proposalVotes(this.proposal.id); - expect(votes.forVotes).to.be.bignumber.equal(weight); - }); - - it('Voting with params by signature is properly supported', async function () { - const voterBySig = Wallet.generate(); - const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); - - const signature = async message => { - return fromRpcSig( - ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { - data: { - types: { - EIP712Domain, - ExtendedBallot: [ - { name: 'proposalId', type: 'uint256' }, - { name: 'support', type: 'uint8' }, - { name: 'reason', type: 'string' }, - { name: 'params', type: 'bytes' }, - ], + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.address, + value, + data: this.receiver.contract.methods.mockFunction().encodeABI(), }, - domain: { name, version, chainId: this.chainId, verifyingContract: this.mock.address }, - primaryType: 'ExtendedBallot', - message, + ], + '', + ); + }); + + it('deployment check', async function () { + expect(await this.mock.name()).to.be.equal(name); + expect(await this.mock.token()).to.be.equal(this.token.address); + expect(await this.mock.votingDelay()).to.be.bignumber.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.be.bignumber.equal(votingPeriod); + }); + + it('nominal is unaffected', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + await this.helper.vote({ support: Enums.VoteType.For, reason: 'This is nice' }, { from: voter1 }); + await this.helper.vote({ support: Enums.VoteType.For }, { from: voter2 }); + await this.helper.vote({ support: Enums.VoteType.Against }, { from: voter3 }); + await this.helper.vote({ support: Enums.VoteType.Abstain }, { from: voter4 }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + expect(await this.mock.hasVoted(this.proposal.id, owner)).to.be.equal(false); + expect(await this.mock.hasVoted(this.proposal.id, voter1)).to.be.equal(true); + expect(await this.mock.hasVoted(this.proposal.id, voter2)).to.be.equal(true); + expect(await web3.eth.getBalance(this.mock.address)).to.be.bignumber.equal('0'); + expect(await web3.eth.getBalance(this.receiver.address)).to.be.bignumber.equal(value); + }); + + it('Voting with params is properly supported', async function () { + await this.helper.propose({ from: proposer }); + await this.helper.waitForSnapshot(); + + const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam); + + const tx = await this.helper.vote( + { + support: Enums.VoteType.For, + reason: 'no particular reason', + params: encodedParams, }, - }), - ); - }; + { from: voter2 }, + ); - await this.token.delegate(voterBySigAddress, { from: voter2 }); + expectEvent(tx, 'CountParams', { ...rawParams }); + expectEvent(tx, 'VoteCastWithParams', { + voter: voter2, + proposalId: this.proposal.id, + support: Enums.VoteType.For, + weight, + reason: 'no particular reason', + params: encodedParams, + }); - // Run proposal - await this.helper.propose(); - await this.helper.waitForSnapshot(); + const votes = await this.mock.proposalVotes(this.proposal.id); + expect(votes.forVotes).to.be.bignumber.equal(weight); + }); - const weight = new BN(web3.utils.toWei('7')).sub(rawParams.uintParam); + it('Voting with params by signature is properly supported', async function () { + const voterBySig = Wallet.generate(); + const voterBySigAddress = web3.utils.toChecksumAddress(voterBySig.getAddressString()); - const tx = await this.helper.vote({ - support: Enums.VoteType.For, - reason: 'no particular reason', - params: encodedParams, - signature, + const signature = (contract, message) => + getDomain(contract) + .then(domain => ({ + primaryType: 'ExtendedBallot', + types: { + EIP712Domain: domainType(domain), + ExtendedBallot: [ + { name: 'proposalId', type: 'uint256' }, + { name: 'support', type: 'uint8' }, + { name: 'reason', type: 'string' }, + { name: 'params', type: 'bytes' }, + ], + }, + domain, + message, + })) + .then(data => ethSigUtil.signTypedMessage(voterBySig.getPrivateKey(), { data })) + .then(fromRpcSig); + + await this.token.delegate(voterBySigAddress, { from: voter2 }); + + // Run proposal + await this.helper.propose(); + await this.helper.waitForSnapshot(); + + const weight = web3.utils.toBN(web3.utils.toWei('7')).sub(rawParams.uintParam); + + const tx = await this.helper.vote({ + support: Enums.VoteType.For, + reason: 'no particular reason', + params: encodedParams, + signature, + }); + + expectEvent(tx, 'CountParams', { ...rawParams }); + expectEvent(tx, 'VoteCastWithParams', { + voter: voterBySigAddress, + proposalId: this.proposal.id, + support: Enums.VoteType.For, + weight, + reason: 'no particular reason', + params: encodedParams, + }); + + const votes = await this.mock.proposalVotes(this.proposal.id); + expect(votes.forVotes).to.be.bignumber.equal(weight); + }); }); - - expectEvent(tx, 'CountParams', { ...rawParams }); - expectEvent(tx, 'VoteCastWithParams', { - voter: voterBySigAddress, - proposalId: this.proposal.id, - support: Enums.VoteType.For, - weight, - reason: 'no particular reason', - params: encodedParams, - }); - - const votes = await this.mock.proposalVotes(this.proposal.id); - expect(votes.forVotes).to.be.bignumber.equal(weight); - }); + } }); diff --git a/test/governance/utils/EIP6372.behavior.js b/test/governance/utils/EIP6372.behavior.js new file mode 100644 index 000000000..022ec3568 --- /dev/null +++ b/test/governance/utils/EIP6372.behavior.js @@ -0,0 +1,23 @@ +const { clock } = require('../../helpers/time'); + +function shouldBehaveLikeEIP6372(mode = 'blocknumber') { + describe('should implement EIP6372', function () { + beforeEach(async function () { + this.mock = this.mock ?? this.token ?? this.votes; + }); + + it('clock is correct', async function () { + expect(await this.mock.clock()).to.be.bignumber.equal(await clock[mode]().then(web3.utils.toBN)); + }); + + it('CLOCK_MODE is correct', async function () { + const params = new URLSearchParams(await this.mock.CLOCK_MODE()); + expect(params.get('mode')).to.be.equal(mode); + expect(params.get('from')).to.be.equal(mode == 'blocknumber' ? 'default' : null); + }); + }); +} + +module.exports = { + shouldBehaveLikeEIP6372, +}; diff --git a/test/governance/utils/Votes.behavior.js b/test/governance/utils/Votes.behavior.js index 7cc8f133e..20ebdba4f 100644 --- a/test/governance/utils/Votes.behavior.js +++ b/test/governance/utils/Votes.behavior.js @@ -1,4 +1,4 @@ -const { constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, time } = require('@openzeppelin/test-helpers'); const { MAX_UINT256, ZERO_ADDRESS } = constants; @@ -6,8 +6,10 @@ const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const { EIP712Domain, domainSeparator } = require('../../helpers/eip712'); -const { web3 } = require('hardhat'); +const { shouldBehaveLikeEIP6372 } = require('./EIP6372.behavior'); +const { getDomain, domainType } = require('../../helpers/eip712'); +const { clockFromReceipt } = require('../../helpers/time'); +const { expectRevertCustomError } = require('../../helpers/customError'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -15,9 +17,19 @@ const Delegation = [ { name: 'expiry', type: 'uint256' }, ]; -const version = '1'; +const buildAndSignDelegation = (contract, message, pk) => + getDomain(contract) + .then(domain => ({ + primaryType: 'Delegation', + types: { EIP712Domain: domainType(domain), Delegation }, + domain, + message, + })) + .then(data => fromRpcSig(ethSigUtil.signTypedMessage(pk, { data }))); + +function shouldBehaveLikeVotes(accounts, tokens, { mode = 'blocknumber', fungible = true }) { + shouldBehaveLikeEIP6372(mode); -function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { const getWeight = token => web3.utils.toBN(fungible ? token : 1); describe('run votes workflow', function () { @@ -25,18 +37,7 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { expect(await this.votes.nonces(accounts[0])).to.be.bignumber.equal('0'); }); - it('domain separator', async function () { - expect(await this.votes.DOMAIN_SEPARATOR()).to.equal( - await domainSeparator({ - name: this.name, - version, - chainId: this.chainId, - verifyingContract: this.votes.address, - }), - ); - }); - - describe('delegation', function () { + describe('delegation with signature', function () { const token = tokens[0]; it('delegation without tokens', async function () { @@ -60,6 +61,8 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { expect(await this.votes.delegates(accounts[1])).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.votes.delegate(accounts[1], { from: accounts[1] }); + const timepoint = await clockFromReceipt[mode](receipt); + expectEvent(receipt, 'DelegateChanged', { delegator: accounts[1], fromDelegate: ZERO_ADDRESS, @@ -73,9 +76,9 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { expect(await this.votes.delegates(accounts[1])).to.be.equal(accounts[1]); expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal(weight); - expect(await this.votes.getPastVotes(accounts[1], receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(accounts[1], timepoint - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.votes.getPastVotes(accounts[1], receipt.blockNumber)).to.be.bignumber.equal(weight); + expect(await this.votes.getPastVotes(accounts[1], timepoint)).to.be.bignumber.equal(weight); }); it('delegation update', async function () { @@ -88,6 +91,8 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { expect(await this.votes.getVotes(accounts[2])).to.be.bignumber.equal('0'); const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] }); + const timepoint = await clockFromReceipt[mode](receipt); + expectEvent(receipt, 'DelegateChanged', { delegator: accounts[1], fromDelegate: accounts[1], @@ -108,11 +113,11 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { expect(await this.votes.getVotes(accounts[1])).to.be.bignumber.equal('0'); expect(await this.votes.getVotes(accounts[2])).to.be.bignumber.equal(weight); - expect(await this.votes.getPastVotes(accounts[1], receipt.blockNumber - 1)).to.be.bignumber.equal(weight); - expect(await this.votes.getPastVotes(accounts[2], receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(accounts[1], timepoint - 1)).to.be.bignumber.equal(weight); + expect(await this.votes.getPastVotes(accounts[2], timepoint - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.votes.getPastVotes(accounts[1], receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastVotes(accounts[2], receipt.blockNumber)).to.be.bignumber.equal(weight); + expect(await this.votes.getPastVotes(accounts[1], timepoint)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(accounts[2], timepoint)).to.be.bignumber.equal(weight); }); describe('with signature', function () { @@ -121,33 +126,25 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { const nonce = 0; delegator.address = web3.utils.toChecksumAddress(delegator.getAddressString()); - const buildData = (chainId, verifyingContract, name, message) => ({ - data: { - primaryType: 'Delegation', - types: { EIP712Domain, Delegation }, - domain: { name, version, chainId, verifyingContract }, - message, - }, - }); - it('accept signed delegation', async function () { await this.votes.$_mint(delegator.address, token); const weight = getWeight(token); - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, this.name, { - delegatee, - nonce, - expiry: MAX_UINT256, - }), - ), + const { v, r, s } = await buildAndSignDelegation( + this.votes, + { + delegatee, + nonce, + expiry: MAX_UINT256, + }, + delegator.getPrivateKey(), ); expect(await this.votes.delegates(delegator.address)).to.be.equal(ZERO_ADDRESS); const { receipt } = await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s); + const timepoint = await clockFromReceipt[mode](receipt); + expectEvent(receipt, 'DelegateChanged', { delegator: delegator.address, fromDelegate: ZERO_ADDRESS, @@ -162,38 +159,40 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { expect(await this.votes.delegates(delegator.address)).to.be.equal(delegatee); expect(await this.votes.getVotes(delegator.address)).to.be.bignumber.equal('0'); expect(await this.votes.getVotes(delegatee)).to.be.bignumber.equal(weight); - expect(await this.votes.getPastVotes(delegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(delegatee, timepoint - 1)).to.be.bignumber.equal('0'); await time.advanceBlock(); - expect(await this.votes.getPastVotes(delegatee, receipt.blockNumber)).to.be.bignumber.equal(weight); + expect(await this.votes.getPastVotes(delegatee, timepoint)).to.be.bignumber.equal(weight); }); it('rejects reused signature', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, this.name, { - delegatee, - nonce, - expiry: MAX_UINT256, - }), - ), + const { v, r, s } = await buildAndSignDelegation( + this.votes, + { + delegatee, + nonce, + expiry: MAX_UINT256, + }, + delegator.getPrivateKey(), ); await this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s); - await expectRevert(this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s), 'Votes: invalid nonce'); + await expectRevertCustomError( + this.votes.delegateBySig(delegatee, nonce, MAX_UINT256, v, r, s), + 'InvalidAccountNonce', + [delegator.address, nonce + 1], + ); }); it('rejects bad delegatee', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, this.name, { - delegatee, - nonce, - expiry: MAX_UINT256, - }), - ), + const { v, r, s } = await buildAndSignDelegation( + this.votes, + { + delegatee, + nonce, + expiry: MAX_UINT256, + }, + delegator.getPrivateKey(), ); const receipt = await this.votes.delegateBySig(other, nonce, MAX_UINT256, v, r, s); @@ -204,36 +203,40 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { }); it('rejects bad nonce', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, this.name, { - delegatee, - nonce: nonce + 1, - expiry: MAX_UINT256, - }), - ), + const { v, r, s } = await buildAndSignDelegation( + this.votes, + { + delegatee, + nonce: nonce + 1, + expiry: MAX_UINT256, + }, + delegator.getPrivateKey(), ); - await expectRevert( + + await expectRevertCustomError( this.votes.delegateBySig(delegatee, nonce + 1, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', + 'InvalidAccountNonce', + [delegator.address, 0], ); }); it('rejects expired permit', async function () { const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.votes.address, this.name, { - delegatee, - nonce, - expiry, - }), - ), + const { v, r, s } = await buildAndSignDelegation( + this.votes, + { + delegatee, + nonce, + expiry, + }, + delegator.getPrivateKey(), ); - await expectRevert(this.votes.delegateBySig(delegatee, nonce, expiry, v, r, s), 'Votes: signature expired'); + await expectRevertCustomError( + this.votes.delegateBySig(delegatee, nonce, expiry, v, r, s), + 'VotesExpiredSignature', + [expiry], + ); }); }); }); @@ -244,7 +247,12 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { }); it('reverts if block number >= current block', async function () { - await expectRevert(this.votes.getPastTotalSupply(5e10), 'block not yet mined'); + const timepoint = 5e10; + const clock = await this.votes.clock(); + await expectRevertCustomError(this.votes.getPastTotalSupply(timepoint), 'ERC5805FutureLookup', [ + timepoint, + clock, + ]); }); it('returns 0 if there are no checkpoints', async function () { @@ -252,36 +260,50 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { }); it('returns the correct checkpointed total supply', async function () { - const blockNumber = Number(await time.latestBlock()); + const weight = tokens.map(token => getWeight(token)); - await this.votes.$_mint(accounts[1], tokens[0]); // mint 0 + // t0 = mint #0 + const t0 = await this.votes.$_mint(accounts[1], tokens[0]); await time.advanceBlock(); - await this.votes.$_mint(accounts[1], tokens[1]); // mint 1 + // t1 = mint #1 + const t1 = await this.votes.$_mint(accounts[1], tokens[1]); await time.advanceBlock(); - await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[1]); // burn 1 + // t2 = burn #1 + const t2 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[1]); await time.advanceBlock(); - await this.votes.$_mint(accounts[1], tokens[2]); // mint 2 + // t3 = mint #2 + const t3 = await this.votes.$_mint(accounts[1], tokens[2]); await time.advanceBlock(); - await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[0]); // burn 0 + // t4 = burn #0 + const t4 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[0]); await time.advanceBlock(); - await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[2]); // burn 2 + // t5 = burn #2 + const t5 = await this.votes.$_burn(...(fungible ? [accounts[1]] : []), tokens[2]); await time.advanceBlock(); - const weight = tokens.map(getWeight); + t0.timepoint = await clockFromReceipt[mode](t0.receipt); + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + t5.timepoint = await clockFromReceipt[mode](t5.receipt); - expect(await this.votes.getPastTotalSupply(blockNumber)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastTotalSupply(blockNumber + 1)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(blockNumber + 2)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(blockNumber + 3)).to.be.bignumber.equal(weight[0].add(weight[1])); - expect(await this.votes.getPastTotalSupply(blockNumber + 4)).to.be.bignumber.equal(weight[0].add(weight[1])); - expect(await this.votes.getPastTotalSupply(blockNumber + 5)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(blockNumber + 6)).to.be.bignumber.equal(weight[0]); - expect(await this.votes.getPastTotalSupply(blockNumber + 7)).to.be.bignumber.equal(weight[0].add(weight[2])); - expect(await this.votes.getPastTotalSupply(blockNumber + 8)).to.be.bignumber.equal(weight[0].add(weight[2])); - expect(await this.votes.getPastTotalSupply(blockNumber + 9)).to.be.bignumber.equal(weight[2]); - expect(await this.votes.getPastTotalSupply(blockNumber + 10)).to.be.bignumber.equal(weight[2]); - expect(await this.votes.getPastTotalSupply(blockNumber + 11)).to.be.bignumber.equal('0'); - await expectRevert(this.votes.getPastTotalSupply(blockNumber + 12), 'Checkpoints: block not yet mined'); + expect(await this.votes.getPastTotalSupply(t0.timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastTotalSupply(t0.timepoint)).to.be.bignumber.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t0.timepoint + 1)).to.be.bignumber.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal(weight[0].add(weight[1])); + expect(await this.votes.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal(weight[0].add(weight[1])); + expect(await this.votes.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal(weight[0]); + expect(await this.votes.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal(weight[0].add(weight[2])); + expect(await this.votes.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal(weight[0].add(weight[2])); + expect(await this.votes.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal(weight[2]); + expect(await this.votes.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal(weight[2]); + expect(await this.votes.getPastTotalSupply(t5.timepoint)).to.be.bignumber.equal('0'); + await expectRevertCustomError(this.votes.getPastTotalSupply(t5.timepoint + 1), 'ERC5805FutureLookup', [ + t5.timepoint + 1, // timepoint + t5.timepoint + 1, // clock + ]); }); }); @@ -296,7 +318,12 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { describe('getPastVotes', function () { it('reverts if block number >= current block', async function () { - await expectRevert(this.votes.getPastVotes(accounts[2], 5e10), 'block not yet mined'); + const clock = await this.votes.clock(); + const timepoint = 5e10; // far in the future + await expectRevertCustomError(this.votes.getPastVotes(accounts[2], timepoint), 'ERC5805FutureLookup', [ + timepoint, + clock, + ]); }); it('returns 0 if there are no checkpoints', async function () { @@ -304,21 +331,24 @@ function shouldBehaveLikeVotes(accounts, tokens, fungible = true) { }); it('returns the latest block if >= last checkpoint block', async function () { - const tx = await this.votes.delegate(accounts[2], { from: accounts[1] }); + const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] }); + const timepoint = await clockFromReceipt[mode](receipt); await time.advanceBlock(); await time.advanceBlock(); + const latest = await this.votes.getVotes(accounts[2]); - expect(await this.votes.getPastVotes(accounts[2], tx.receipt.blockNumber)).to.be.bignumber.equal(latest); - expect(await this.votes.getPastVotes(accounts[2], tx.receipt.blockNumber + 1)).to.be.bignumber.equal(latest); + expect(await this.votes.getPastVotes(accounts[2], timepoint)).to.be.bignumber.equal(latest); + expect(await this.votes.getPastVotes(accounts[2], timepoint + 1)).to.be.bignumber.equal(latest); }); it('returns zero if < first checkpoint block', async function () { await time.advanceBlock(); - const tx = await this.votes.delegate(accounts[2], { from: accounts[1] }); + const { receipt } = await this.votes.delegate(accounts[2], { from: accounts[1] }); + const timepoint = await clockFromReceipt[mode](receipt); await time.advanceBlock(); await time.advanceBlock(); - expect(await this.votes.getPastVotes(accounts[2], tx.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(accounts[2], timepoint - 1)).to.be.bignumber.equal('0'); }); }); }); diff --git a/test/governance/utils/Votes.test.js b/test/governance/utils/Votes.test.js index c0c574df4..b2b80f9fe 100644 --- a/test/governance/utils/Votes.test.js +++ b/test/governance/utils/Votes.test.js @@ -1,13 +1,17 @@ -const { constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -const { getChainId } = require('../../helpers/chainid'); +const { clockFromReceipt } = require('../../helpers/time'); const { BNsum } = require('../../helpers/math'); +const { expectRevertCustomError } = require('../../helpers/customError'); require('array.prototype.at/auto'); const { shouldBehaveLikeVotes } = require('./Votes.behavior'); -const Votes = artifacts.require('$VotesMock'); +const MODES = { + blocknumber: artifacts.require('$VotesMock'), + timestamp: artifacts.require('$VotesTimestampMock'), +}; contract('Votes', function (accounts) { const [account1, account2, account3] = accounts; @@ -17,70 +21,72 @@ contract('Votes', function (accounts) { [account3]: web3.utils.toBN('20'), }; - beforeEach(async function () { - this.name = 'My Vote'; - this.votes = await Votes.new(this.name, '1'); - }); + const name = 'My Vote'; + const version = '1'; - it('starts with zero votes', async function () { - expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('0'); - }); + for (const [mode, artifact] of Object.entries(MODES)) { + describe(`vote with ${mode}`, function () { + beforeEach(async function () { + this.votes = await artifact.new(name, version); + }); - describe('performs voting operations', function () { - beforeEach(async function () { - this.txs = []; - for (const [account, amount] of Object.entries(amounts)) { - this.txs.push(await this.votes.$_mint(account, amount)); - } + shouldBehaveLikeVotes(accounts, Object.values(amounts), { mode, fungible: true }); + + it('starts with zero votes', async function () { + expect(await this.votes.getTotalSupply()).to.be.bignumber.equal('0'); + }); + + describe('performs voting operations', function () { + beforeEach(async function () { + this.txs = []; + for (const [account, amount] of Object.entries(amounts)) { + this.txs.push(await this.votes.$_mint(account, amount)); + } + }); + + it('reverts if block number >= current block', async function () { + const lastTxTimepoint = await clockFromReceipt[mode](this.txs.at(-1).receipt); + const clock = await this.votes.clock(); + await expectRevertCustomError(this.votes.getPastTotalSupply(lastTxTimepoint + 1), 'ERC5805FutureLookup', [ + lastTxTimepoint + 1, + clock, + ]); + }); + + it('delegates', async function () { + expect(await this.votes.getVotes(account1)).to.be.bignumber.equal('0'); + expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); + expect(await this.votes.delegates(account1)).to.be.equal(constants.ZERO_ADDRESS); + expect(await this.votes.delegates(account2)).to.be.equal(constants.ZERO_ADDRESS); + + await this.votes.delegate(account1, account1); + + expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account1]); + expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); + expect(await this.votes.delegates(account1)).to.be.equal(account1); + expect(await this.votes.delegates(account2)).to.be.equal(constants.ZERO_ADDRESS); + + await this.votes.delegate(account2, account1); + + expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account1].add(amounts[account2])); + expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); + expect(await this.votes.delegates(account1)).to.be.equal(account1); + expect(await this.votes.delegates(account2)).to.be.equal(account1); + }); + + it('cross delegates', async function () { + await this.votes.delegate(account1, account2); + await this.votes.delegate(account2, account1); + + expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account2]); + expect(await this.votes.getVotes(account2)).to.be.bignumber.equal(amounts[account1]); + }); + + it('returns total amount of votes', async function () { + const totalSupply = BNsum(...Object.values(amounts)); + expect(await this.votes.getTotalSupply()).to.be.bignumber.equal(totalSupply); + }); + }); }); - - it('reverts if block number >= current block', async function () { - await expectRevert( - this.votes.getPastTotalSupply(this.txs.at(-1).receipt.blockNumber + 1), - 'Checkpoints: block not yet mined', - ); - }); - - it('delegates', async function () { - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal('0'); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); - expect(await this.votes.delegates(account1)).to.be.equal(constants.ZERO_ADDRESS); - expect(await this.votes.delegates(account2)).to.be.equal(constants.ZERO_ADDRESS); - - await this.votes.delegate(account1, account1); - - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account1]); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); - expect(await this.votes.delegates(account1)).to.be.equal(account1); - expect(await this.votes.delegates(account2)).to.be.equal(constants.ZERO_ADDRESS); - - await this.votes.delegate(account2, account1); - - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account1].add(amounts[account2])); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal('0'); - expect(await this.votes.delegates(account1)).to.be.equal(account1); - expect(await this.votes.delegates(account2)).to.be.equal(account1); - }); - - it('cross delegates', async function () { - await this.votes.delegate(account1, account2); - await this.votes.delegate(account2, account1); - - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(amounts[account2]); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal(amounts[account1]); - }); - - it('returns total amount of votes', async function () { - const totalSupply = BNsum(...Object.values(amounts)); - expect(await this.votes.getTotalSupply()).to.be.bignumber.equal(totalSupply); - }); - }); - - describe('performs voting workflow', function () { - beforeEach(async function () { - this.chainId = await getChainId(); - }); - - shouldBehaveLikeVotes(accounts, Object.values(amounts)); - }); + } }); diff --git a/test/helpers/crosschain.js b/test/helpers/crosschain.js deleted file mode 100644 index 9e6ff9610..000000000 --- a/test/helpers/crosschain.js +++ /dev/null @@ -1,61 +0,0 @@ -const { promisify } = require('util'); - -const BridgeAMBMock = artifacts.require('BridgeAMBMock'); -const BridgeArbitrumL1Mock = artifacts.require('BridgeArbitrumL1Mock'); -const BridgeArbitrumL2Mock = artifacts.require('BridgeArbitrumL2Mock'); -const BridgeOptimismMock = artifacts.require('BridgeOptimismMock'); -const BridgePolygonChildMock = artifacts.require('BridgePolygonChildMock'); - -class BridgeHelper { - static async deploy(type) { - return new BridgeHelper(await deployBridge(type)); - } - - constructor(bridge) { - this.bridge = bridge; - this.address = bridge.address; - } - - call(from, target, selector = undefined, args = []) { - return this.bridge.relayAs( - target.address || target, - selector ? target.contract.methods[selector](...args).encodeABI() : '0x', - from, - ); - } -} - -async function deployBridge(type = 'Arbitrum-L2') { - switch (type) { - case 'AMB': - return BridgeAMBMock.new(); - - case 'Arbitrum-L1': - return BridgeArbitrumL1Mock.new(); - - case 'Arbitrum-L2': { - const instance = await BridgeArbitrumL2Mock.new(); - const code = await web3.eth.getCode(instance.address); - await promisify(web3.currentProvider.send.bind(web3.currentProvider))({ - jsonrpc: '2.0', - method: 'hardhat_setCode', - params: ['0x0000000000000000000000000000000000000064', code], - id: new Date().getTime(), - }); - return BridgeArbitrumL2Mock.at('0x0000000000000000000000000000000000000064'); - } - - case 'Optimism': - return BridgeOptimismMock.new(); - - case 'Polygon-Child': - return BridgePolygonChildMock.new(); - - default: - throw new Error(`CrossChain: ${type} is not supported`); - } -} - -module.exports = { - BridgeHelper, -}; diff --git a/test/helpers/customError.js b/test/helpers/customError.js index 3cfcd7277..e38170b78 100644 --- a/test/helpers/customError.js +++ b/test/helpers/customError.js @@ -1,22 +1,44 @@ -const { config } = require('hardhat'); - -const optimizationsEnabled = config.solidity.compilers.some(c => c.settings.optimizer.enabled); +const { expect } = require('chai'); /** Revert handler that supports custom errors. */ -async function expectRevertCustomError(promise, reason) { - try { - await promise; - expect.fail("Expected promise to throw but it didn't"); - } catch (revert) { - if (reason) { - if (optimizationsEnabled) { - // Optimizations currently mess with Hardhat's decoding of custom errors - expect(revert.message).to.include.oneOf([reason, 'unrecognized return data or custom error']); - } else { - expect(revert.message).to.include(reason); - } - } +async function expectRevertCustomError(promise, expectedErrorName, args) { + if (!Array.isArray(args)) { + expect.fail('Expected 3rd array parameter for error arguments'); } + + await promise.then( + () => expect.fail("Expected promise to throw but it didn't"), + ({ message }) => { + // The revert message for custom errors looks like: + // VM Exception while processing transaction: + // reverted with custom error 'InvalidAccountNonce("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 0)' + + // We trim out anything inside the single quotes as comma-separated values + const [, error] = message.match(/'(.*)'/); + + // Attempt to parse as an error + const match = error.match(/(?\w+)\((?.*)\)/); + if (!match) { + expect.fail(`Couldn't parse "${error}" as a custom error`); + } + // Extract the error name and parameters + const errorName = match.groups.name; + const argMatches = [...match.groups.args.matchAll(/-?\w+/g)]; + + // Assert error name + expect(errorName).to.be.equal( + expectedErrorName, + `Unexpected custom error name (with found args: [${argMatches.map(([a]) => a)}])`, + ); + + // Coerce to string for comparison since `arg` can be either a number or hex. + const sanitizedExpected = args.map(arg => arg.toString().toLowerCase()); + const sanitizedActual = argMatches.map(([arg]) => arg.toString().toLowerCase()); + + // Assert argument equality + expect(sanitizedActual).to.have.members(sanitizedExpected, `Unexpected ${errorName} arguments`); + }, + ); } module.exports = { diff --git a/test/helpers/eip712.js b/test/helpers/eip712.js index 9851df662..b12a6233e 100644 --- a/test/helpers/eip712.js +++ b/test/helpers/eip712.js @@ -6,6 +6,7 @@ const EIP712Domain = [ { name: 'version', type: 'string' }, { name: 'chainId', type: 'uint256' }, { name: 'verifyingContract', type: 'address' }, + { name: 'salt', type: 'bytes32' }, ]; const Permit = [ @@ -24,25 +25,43 @@ function hexStringToBuffer(hexstr) { return Buffer.from(hexstr.replace(/^0x/, ''), 'hex'); } -async function domainSeparator({ name, version, chainId, verifyingContract }) { +async function getDomain(contract) { + const { fields, name, version, chainId, verifyingContract, salt, extensions } = await contract.eip712Domain(); + + if (extensions.length > 0) { + throw Error('Extensions not implemented'); + } + + const domain = { name, version, chainId, verifyingContract, salt }; + for (const [i, { name }] of EIP712Domain.entries()) { + if (!(fields & (1 << i))) { + delete domain[name]; + } + } + + return domain; +} + +function domainType(domain) { + return EIP712Domain.filter(({ name }) => domain[name] !== undefined); +} + +function domainSeparator(domain) { return bufferToHexString( - ethSigUtil.TypedDataUtils.hashStruct( - 'EIP712Domain', - { name, version, chainId, verifyingContract }, - { EIP712Domain }, - ), + ethSigUtil.TypedDataUtils.hashStruct('EIP712Domain', domain, { EIP712Domain: domainType(domain) }), ); } -async function hashTypedData(domain, structHash) { - return domainSeparator(domain).then(separator => - bufferToHexString(keccak256(Buffer.concat(['0x1901', separator, structHash].map(str => hexStringToBuffer(str))))), +function hashTypedData(domain, structHash) { + return bufferToHexString( + keccak256(Buffer.concat(['0x1901', domainSeparator(domain), structHash].map(str => hexStringToBuffer(str)))), ); } module.exports = { - EIP712Domain, Permit, + getDomain, + domainType, domainSeparator, hashTypedData, }; diff --git a/test/helpers/enums.js b/test/helpers/enums.js index cc650abf4..d4a4fdbd0 100644 --- a/test/helpers/enums.js +++ b/test/helpers/enums.js @@ -7,4 +7,5 @@ module.exports = { ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'), VoteType: Enum('Against', 'For', 'Abstain'), Rounding: Enum('Down', 'Up', 'Zero'), + OperationState: Enum('Unset', 'Pending', 'Ready', 'Done'), }; diff --git a/test/helpers/governance.js b/test/helpers/governance.js index ff341aa12..665c21605 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -1,4 +1,5 @@ -const { time } = require('@openzeppelin/test-helpers'); +const { forward } = require('../helpers/time'); +const { ProposalState } = require('./enums'); function zip(...args) { return Array(Math.max(...args.map(array => array.length))) @@ -15,8 +16,9 @@ function concatOpts(args, opts = null) { } class GovernorHelper { - constructor(governor) { + constructor(governor, mode = 'blocknumber') { this.governor = governor; + this.mode = mode; } delegate(delegation = {}, opts = null) { @@ -62,14 +64,25 @@ class GovernorHelper { ); } - cancel(opts = null) { + cancel(visibility = 'external', opts = null) { const proposal = this.currentProposal; - return proposal.useCompatibilityInterface - ? this.governor.methods['cancel(uint256)'](...concatOpts([proposal.id], opts)) - : this.governor.methods['$_cancel(address[],uint256[],bytes[],bytes32)']( + switch (visibility) { + case 'external': + if (proposal.useCompatibilityInterface) { + return this.governor.methods['cancel(uint256)'](...concatOpts([proposal.id], opts)); + } else { + return this.governor.methods['cancel(address[],uint256[],bytes[],bytes32)']( + ...concatOpts(proposal.shortProposal, opts), + ); + } + case 'internal': + return this.governor.methods['$_cancel(address[],uint256[],bytes[],bytes32)']( ...concatOpts(proposal.shortProposal, opts), ); + default: + throw new Error(`unsuported visibility "${visibility}"`); + } } vote(vote = {}, opts = null) { @@ -79,7 +92,7 @@ class GovernorHelper { ? // if signature, and either params or reason → vote.params || vote.reason ? vote - .signature({ + .signature(this.governor, { proposalId: proposal.id, support: vote.support, reason: vote.reason || '', @@ -91,7 +104,7 @@ class GovernorHelper { ), ) : vote - .signature({ + .signature(this.governor, { proposalId: proposal.id, support: vote.support, }) @@ -109,23 +122,22 @@ class GovernorHelper { : this.governor.castVote(...concatOpts([proposal.id, vote.support], opts)); } - waitForSnapshot(offset = 0) { + async waitForSnapshot(offset = 0) { const proposal = this.currentProposal; - return this.governor - .proposalSnapshot(proposal.id) - .then(blockNumber => time.advanceBlockTo(blockNumber.addn(offset))); + const timepoint = await this.governor.proposalSnapshot(proposal.id); + return forward[this.mode](timepoint.addn(offset)); } - waitForDeadline(offset = 0) { + async waitForDeadline(offset = 0) { const proposal = this.currentProposal; - return this.governor - .proposalDeadline(proposal.id) - .then(blockNumber => time.advanceBlockTo(blockNumber.addn(offset))); + const timepoint = await this.governor.proposalDeadline(proposal.id); + return forward[this.mode](timepoint.addn(offset)); } - waitForEta(offset = 0) { + async waitForEta(offset = 0) { const proposal = this.currentProposal; - return this.governor.proposalEta(proposal.id).then(timestamp => time.increaseTo(timestamp.addn(offset))); + const timestamp = await this.governor.proposalEta(proposal.id); + return forward.timestamp(timestamp.addn(offset)); } /** @@ -185,6 +197,44 @@ class GovernorHelper { } } +/** + * Encodes a list ProposalStates into a bytes32 representation where each bit enabled corresponds to + * the underlying position in the `ProposalState` enum. For example: + * + * 0x000...10000 + * ^^^^^^------ ... + * ^----- Succeeded + * ^---- Defeated + * ^--- Canceled + * ^-- Active + * ^- Pending + */ +function proposalStatesToBitMap(proposalStates, options = {}) { + if (!Array.isArray(proposalStates)) { + proposalStates = [proposalStates]; + } + const statesCount = Object.keys(ProposalState).length; + let result = 0; + + const uniqueProposalStates = new Set(proposalStates.map(bn => bn.toNumber())); // Remove duplicates + for (const state of uniqueProposalStates) { + if (state < 0 || state >= statesCount) { + expect.fail(`ProposalState ${state} out of possible states (0...${statesCount}-1)`); + } else { + result |= 1 << state; + } + } + + if (options.inverted) { + const mask = 2 ** statesCount - 1; + result = result ^ mask; + } + + const hex = web3.utils.numberToHex(result); + return web3.utils.padLeft(hex, 64); +} + module.exports = { GovernorHelper, + proposalStatesToBitMap, }; diff --git a/test/helpers/sign.js b/test/helpers/sign.js index 417ef591d..d537116bb 100644 --- a/test/helpers/sign.js +++ b/test/helpers/sign.js @@ -4,6 +4,21 @@ function toEthSignedMessageHash(messageHex) { return web3.utils.sha3(Buffer.concat([prefix, messageBuffer])); } +/** + * Create a signed data with intended validator according to the version 0 of EIP-191 + * @param validatorAddress The address of the validator + * @param dataHex The data to be concatenated with the prefix and signed + */ +function toDataWithIntendedValidatorHash(validatorAddress, dataHex) { + const validatorBuffer = Buffer.from(web3.utils.hexToBytes(validatorAddress)); + const dataBuffer = Buffer.from(web3.utils.hexToBytes(dataHex)); + const preambleBuffer = Buffer.from('\x19'); + const versionBuffer = Buffer.from('\x00'); + const ethMessage = Buffer.concat([preambleBuffer, versionBuffer, validatorBuffer, dataBuffer]); + + return web3.utils.sha3(ethMessage); +} + /** * Create a signer between a contract and a signer for a voucher of method, args, and redeemer * Note that `method` is the web3 method, not the truffle-contract method @@ -43,5 +58,6 @@ const getSignFor = module.exports = { toEthSignedMessageHash, + toDataWithIntendedValidatorHash, getSignFor, }; diff --git a/test/helpers/time.js b/test/helpers/time.js new file mode 100644 index 000000000..30df8dc32 --- /dev/null +++ b/test/helpers/time.js @@ -0,0 +1,17 @@ +const ozHelpers = require('@openzeppelin/test-helpers'); +const helpers = require('@nomicfoundation/hardhat-network-helpers'); + +module.exports = { + clock: { + blocknumber: () => helpers.time.latestBlock(), + timestamp: () => helpers.time.latest(), + }, + clockFromReceipt: { + blocknumber: receipt => Promise.resolve(receipt.blockNumber), + timestamp: receipt => web3.eth.getBlock(receipt.blockNumber).then(block => block.timestamp), + }, + forward: { + blocknumber: ozHelpers.time.advanceBlockTo, + timestamp: helpers.time.increaseTo, + }, +}; diff --git a/test/metatx/ERC2771Context.test.js b/test/metatx/ERC2771Context.test.js index 788777726..6c298d3d9 100644 --- a/test/metatx/ERC2771Context.test.js +++ b/test/metatx/ERC2771Context.test.js @@ -1,6 +1,6 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const { EIP712Domain } = require('../helpers/eip712'); +const { getDomain, domainType } = require('../helpers/eip712'); const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); @@ -10,24 +10,15 @@ const MinimalForwarder = artifacts.require('MinimalForwarder'); const ContextMockCaller = artifacts.require('ContextMockCaller'); const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior'); -const { getChainId } = require('../helpers/chainid'); - -const name = 'MinimalForwarder'; -const version = '0.0.1'; contract('ERC2771Context', function (accounts) { beforeEach(async function () { this.forwarder = await MinimalForwarder.new(); this.recipient = await ERC2771ContextMock.new(this.forwarder.address); - this.domain = { - name, - version, - chainId: await getChainId(), - verifyingContract: this.forwarder.address, - }; + this.domain = await getDomain(this.forwarder); this.types = { - EIP712Domain, + EIP712Domain: domainType(this.domain), ForwardRequest: [ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, diff --git a/test/metatx/MinimalForwarder.test.js b/test/metatx/MinimalForwarder.test.js index 24de1719d..c775c5e44 100644 --- a/test/metatx/MinimalForwarder.test.js +++ b/test/metatx/MinimalForwarder.test.js @@ -1,29 +1,21 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const { EIP712Domain } = require('../helpers/eip712'); +const { getDomain, domainType } = require('../helpers/eip712'); +const { expectRevertCustomError } = require('../helpers/customError'); -const { expectRevert, constants } = require('@openzeppelin/test-helpers'); +const { constants, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -const { getChainId } = require('../helpers/chainid'); - const MinimalForwarder = artifacts.require('MinimalForwarder'); const CallReceiverMock = artifacts.require('CallReceiverMock'); -const name = 'MinimalForwarder'; -const version = '0.0.1'; - contract('MinimalForwarder', function (accounts) { beforeEach(async function () { this.forwarder = await MinimalForwarder.new(); - this.domain = { - name, - version, - chainId: await getChainId(), - verifyingContract: this.forwarder.address, - }; + + this.domain = await getDomain(this.forwarder); this.types = { - EIP712Domain, + EIP712Domain: domainType(this.domain), ForwardRequest: [ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, @@ -36,6 +28,14 @@ contract('MinimalForwarder', function (accounts) { }); context('with message', function () { + const tamperedValues = { + from: accounts[0], + to: accounts[0], + value: web3.utils.toWei('1'), + nonce: 1234, + data: '0x1742', + }; + beforeEach(async function () { this.wallet = Wallet.generate(); this.sender = web3.utils.toChecksumAddress(this.wallet.getAddressString()); @@ -47,14 +47,15 @@ contract('MinimalForwarder', function (accounts) { nonce: Number(await this.forwarder.getNonce(this.sender)), data: '0x', }; - this.sign = () => + this.forgeData = req => ({ + types: this.types, + domain: this.domain, + primaryType: 'ForwardRequest', + message: { ...this.req, ...req }, + }); + this.sign = req => ethSigUtil.signTypedMessage(this.wallet.getPrivateKey(), { - data: { - types: this.types, - domain: this.domain, - primaryType: 'ForwardRequest', - message: this.req, - }, + data: this.forgeData(req), }); }); @@ -73,31 +74,29 @@ contract('MinimalForwarder', function (accounts) { }); }); - context('invalid signature', function () { - it('tampered from', async function () { - expect(await this.forwarder.verify({ ...this.req, from: accounts[0] }, this.sign())).to.be.equal(false); - }); - it('tampered to', async function () { - expect(await this.forwarder.verify({ ...this.req, to: accounts[0] }, this.sign())).to.be.equal(false); - }); - it('tampered value', async function () { - expect(await this.forwarder.verify({ ...this.req, value: web3.utils.toWei('1') }, this.sign())).to.be.equal( - false, - ); - }); - it('tampered nonce', async function () { - expect(await this.forwarder.verify({ ...this.req, nonce: this.req.nonce + 1 }, this.sign())).to.be.equal( - false, - ); - }); - it('tampered data', async function () { - expect(await this.forwarder.verify({ ...this.req, data: '0x1742' }, this.sign())).to.be.equal(false); - }); - it('tampered signature', async function () { + context('with tampered values', function () { + for (const [key, value] of Object.entries(tamperedValues)) { + it(`returns false with tampered ${key}`, async function () { + expect(await this.forwarder.verify(this.forgeData({ [key]: value }).message, this.sign())).to.be.equal( + false, + ); + }); + } + + it('returns false with tampered signature', async function () { const tamperedsign = web3.utils.hexToBytes(this.sign()); tamperedsign[42] ^= 0xff; expect(await this.forwarder.verify(this.req, web3.utils.bytesToHex(tamperedsign))).to.be.equal(false); }); + + it('returns false with valid signature for non-current nonce', async function () { + const req = { + ...this.req, + nonce: this.req.nonce + 1, + }; + const sig = this.sign(req); + expect(await this.forwarder.verify(req, sig)).to.be.equal(false); + }); }); }); @@ -118,44 +117,38 @@ contract('MinimalForwarder', function (accounts) { }); }); - context('invalid signature', function () { - it('tampered from', async function () { - await expectRevert( - this.forwarder.execute({ ...this.req, from: accounts[0] }, this.sign()), - 'MinimalForwarder: signature does not match request', + context('with tampered values', function () { + for (const [key, value] of Object.entries(tamperedValues)) { + it(`reverts with tampered ${key}`, async function () { + const sig = this.sign(); + const data = this.forgeData({ [key]: value }); + await expectRevertCustomError(this.forwarder.execute(data.message, sig), 'MinimalForwarderInvalidSigner', [ + ethSigUtil.recoverTypedSignature({ data, sig }), + data.message.from, + ]); + }); + } + + it('reverts with tampered signature', async function () { + const tamperedSig = web3.utils.hexToBytes(this.sign()); + tamperedSig[42] ^= 0xff; + await expectRevertCustomError( + this.forwarder.execute(this.req, web3.utils.bytesToHex(tamperedSig)), + 'MinimalForwarderInvalidSigner', + [ethSigUtil.recoverTypedSignature({ data: this.forgeData(), sig: tamperedSig }), this.req.from], ); }); - it('tampered to', async function () { - await expectRevert( - this.forwarder.execute({ ...this.req, to: accounts[0] }, this.sign()), - 'MinimalForwarder: signature does not match request', - ); - }); - it('tampered value', async function () { - await expectRevert( - this.forwarder.execute({ ...this.req, value: web3.utils.toWei('1') }, this.sign()), - 'MinimalForwarder: signature does not match request', - ); - }); - it('tampered nonce', async function () { - await expectRevert( - this.forwarder.execute({ ...this.req, nonce: this.req.nonce + 1 }, this.sign()), - 'MinimalForwarder: signature does not match request', - ); - }); - it('tampered data', async function () { - await expectRevert( - this.forwarder.execute({ ...this.req, data: '0x1742' }, this.sign()), - 'MinimalForwarder: signature does not match request', - ); - }); - it('tampered signature', async function () { - const tamperedsign = web3.utils.hexToBytes(this.sign()); - tamperedsign[42] ^= 0xff; - await expectRevert( - this.forwarder.execute(this.req, web3.utils.bytesToHex(tamperedsign)), - 'MinimalForwarder: signature does not match request', - ); + + it('reverts with valid signature for non-current nonce', async function () { + const req = { + ...this.req, + nonce: this.req.nonce + 1, + }; + const sig = this.sign(req); + await expectRevertCustomError(this.forwarder.execute(req, sig), 'MinimalForwarderInvalidNonce', [ + this.req.from, + this.req.nonce, + ]); }); }); diff --git a/test/proxy/Clones.test.js b/test/proxy/Clones.test.js index 947b2ed95..2edd1999c 100644 --- a/test/proxy/Clones.test.js +++ b/test/proxy/Clones.test.js @@ -1,7 +1,9 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { computeCreate2Address } = require('../helpers/create2'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); + const shouldBehaveLikeClone = require('./Clones.behaviour'); const Clones = artifacts.require('$Clones'); @@ -36,7 +38,7 @@ contract('Clones', function (accounts) { // deploy once expectEvent(await factory.$cloneDeterministic(implementation, salt), 'return$cloneDeterministic'); // deploy twice - await expectRevert(factory.$cloneDeterministic(implementation, salt), 'ERC1167: create2 failed'); + await expectRevertCustomError(factory.$cloneDeterministic(implementation, salt), 'ERC1167FailedCreateClone', []); }); it('address prediction', async function () { diff --git a/test/proxy/beacon/BeaconProxy.test.js b/test/proxy/beacon/BeaconProxy.test.js index 968f00be8..63d982397 100644 --- a/test/proxy/beacon/BeaconProxy.test.js +++ b/test/proxy/beacon/BeaconProxy.test.js @@ -1,6 +1,8 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); const { getSlot, BeaconSlot } = require('../../helpers/erc1967'); +const { expectRevertCustomError } = require('../../helpers/customError'); + const { expect } = require('chai'); const UpgradeableBeacon = artifacts.require('UpgradeableBeacon'); @@ -11,11 +13,11 @@ const BadBeaconNoImpl = artifacts.require('BadBeaconNoImpl'); const BadBeaconNotContract = artifacts.require('BadBeaconNotContract'); contract('BeaconProxy', function (accounts) { - const [anotherAccount] = accounts; + const [upgradeableBeaconAdmin, anotherAccount] = accounts; describe('bad beacon is not accepted', async function () { it('non-contract beacon', async function () { - await expectRevert(BeaconProxy.new(anotherAccount, '0x'), 'ERC1967: new beacon is not a contract'); + await expectRevertCustomError(BeaconProxy.new(anotherAccount, '0x'), 'ERC1967InvalidBeacon', [anotherAccount]); }); it('non-compliant beacon', async function () { @@ -25,7 +27,10 @@ contract('BeaconProxy', function (accounts) { it('non-contract implementation', async function () { const beacon = await BadBeaconNotContract.new(); - await expectRevert(BeaconProxy.new(beacon.address, '0x'), 'ERC1967: beacon implementation is not a contract'); + const implementation = await beacon.implementation(); + await expectRevertCustomError(BeaconProxy.new(beacon.address, '0x'), 'ERC1967InvalidImplementation', [ + implementation, + ]); }); }); @@ -49,7 +54,7 @@ contract('BeaconProxy', function (accounts) { }); beforeEach('deploy beacon', async function () { - this.beacon = await UpgradeableBeacon.new(this.implementationV0.address); + this.beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin); }); it('no initialization', async function () { @@ -81,7 +86,7 @@ contract('BeaconProxy', function (accounts) { }); it('upgrade a proxy by upgrading its beacon', async function () { - const beacon = await UpgradeableBeacon.new(this.implementationV0.address); + const beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin); const value = '10'; const data = this.implementationV0.contract.methods.initializeNonPayableWithValue(value).encodeABI(); @@ -96,7 +101,7 @@ contract('BeaconProxy', function (accounts) { expect(await dummy.version()).to.eq('V1'); // upgrade beacon - await beacon.upgradeTo(this.implementationV1.address); + await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin }); // test upgraded version expect(await dummy.version()).to.eq('V2'); @@ -106,7 +111,7 @@ contract('BeaconProxy', function (accounts) { const value1 = '10'; const value2 = '42'; - const beacon = await UpgradeableBeacon.new(this.implementationV0.address); + const beacon = await UpgradeableBeacon.new(this.implementationV0.address, upgradeableBeaconAdmin); const proxy1InitializeData = this.implementationV0.contract.methods .initializeNonPayableWithValue(value1) @@ -130,7 +135,7 @@ contract('BeaconProxy', function (accounts) { expect(await dummy2.version()).to.eq('V1'); // upgrade beacon - await beacon.upgradeTo(this.implementationV1.address); + await beacon.upgradeTo(this.implementationV1.address, { from: upgradeableBeaconAdmin }); // test upgraded version expect(await dummy1.version()).to.eq('V2'); diff --git a/test/proxy/beacon/UpgradeableBeacon.test.js b/test/proxy/beacon/UpgradeableBeacon.test.js index d65f3e0a5..4c58f1740 100644 --- a/test/proxy/beacon/UpgradeableBeacon.test.js +++ b/test/proxy/beacon/UpgradeableBeacon.test.js @@ -1,6 +1,8 @@ -const { expectRevert, expectEvent } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); + const UpgradeableBeacon = artifacts.require('UpgradeableBeacon'); const Implementation1 = artifacts.require('Implementation1'); const Implementation2 = artifacts.require('Implementation2'); @@ -9,13 +11,13 @@ contract('UpgradeableBeacon', function (accounts) { const [owner, other] = accounts; it('cannot be created with non-contract implementation', async function () { - await expectRevert(UpgradeableBeacon.new(accounts[0]), 'UpgradeableBeacon: implementation is not a contract'); + await expectRevertCustomError(UpgradeableBeacon.new(other, owner), 'BeaconInvalidImplementation', [other]); }); context('once deployed', async function () { beforeEach('deploying beacon', async function () { this.v1 = await Implementation1.new(); - this.beacon = await UpgradeableBeacon.new(this.v1.address, { from: owner }); + this.beacon = await UpgradeableBeacon.new(this.v1.address, owner); }); it('returns implementation', async function () { @@ -30,15 +32,16 @@ contract('UpgradeableBeacon', function (accounts) { }); it('cannot be upgraded to a non-contract', async function () { - await expectRevert( - this.beacon.upgradeTo(other, { from: owner }), - 'UpgradeableBeacon: implementation is not a contract', - ); + await expectRevertCustomError(this.beacon.upgradeTo(other, { from: owner }), 'BeaconInvalidImplementation', [ + other, + ]); }); it('cannot be upgraded by other account', async function () { const v2 = await Implementation2.new(); - await expectRevert(this.beacon.upgradeTo(v2.address, { from: other }), 'Ownable: caller is not the owner'); + await expectRevertCustomError(this.beacon.upgradeTo(v2.address, { from: other }), 'OwnableUnauthorizedAccount', [ + other, + ]); }); }); }); diff --git a/test/proxy/transparent/ProxyAdmin.test.js b/test/proxy/transparent/ProxyAdmin.test.js index 4d49274d2..23f7ce9b2 100644 --- a/test/proxy/transparent/ProxyAdmin.test.js +++ b/test/proxy/transparent/ProxyAdmin.test.js @@ -1,13 +1,16 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); -const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); const { expect } = require('chai'); const ImplV1 = artifacts.require('DummyImplementation'); const ImplV2 = artifacts.require('DummyImplementationV2'); const ProxyAdmin = artifacts.require('ProxyAdmin'); const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy'); +const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy'); + +const { getAddressInSlot, ImplementationSlot } = require('../../helpers/erc1967'); +const { expectRevertCustomError } = require('../../helpers/customError'); contract('ProxyAdmin', function (accounts) { - const [proxyAdminOwner, newAdmin, anotherAccount] = accounts; + const [proxyAdminOwner, anotherAccount] = accounts; before('set implementations', async function () { this.implementationV1 = await ImplV1.new(); @@ -16,41 +19,26 @@ contract('ProxyAdmin', function (accounts) { beforeEach(async function () { const initializeData = Buffer.from(''); - this.proxyAdmin = await ProxyAdmin.new({ from: proxyAdminOwner }); - this.proxy = await TransparentUpgradeableProxy.new( + this.proxyAdmin = await ProxyAdmin.new(proxyAdminOwner); + const proxy = await TransparentUpgradeableProxy.new( this.implementationV1.address, this.proxyAdmin.address, initializeData, - { from: proxyAdminOwner }, ); + this.proxy = await ITransparentUpgradeableProxy.at(proxy.address); }); it('has an owner', async function () { expect(await this.proxyAdmin.owner()).to.equal(proxyAdminOwner); }); - describe('#changeProxyAdmin', function () { - it('fails to change proxy admin if its not the proxy owner', async function () { - await expectRevert( - this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: anotherAccount }), - 'caller is not the owner', - ); - }); - - it('changes proxy admin', async function () { - await this.proxyAdmin.changeProxyAdmin(this.proxy.address, newAdmin, { from: proxyAdminOwner }); - - const newProxyAdmin = await getAddressInSlot(this.proxy, AdminSlot); - expect(newProxyAdmin).to.be.eq(newAdmin); - }); - }); - describe('#upgrade', function () { context('with unauthorized account', function () { it('fails to upgrade', async function () { - await expectRevert( + await expectRevertCustomError( this.proxyAdmin.upgrade(this.proxy.address, this.implementationV2.address, { from: anotherAccount }), - 'caller is not the owner', + 'OwnableUnauthorizedAccount', + [anotherAccount], ); }); }); @@ -60,7 +48,7 @@ contract('ProxyAdmin', function (accounts) { await this.proxyAdmin.upgrade(this.proxy.address, this.implementationV2.address, { from: proxyAdminOwner }); const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot); - expect(implementationAddress).to.be.eq(this.implementationV2.address); + expect(implementationAddress).to.be.equal(this.implementationV2.address); }); }); }); @@ -69,11 +57,12 @@ contract('ProxyAdmin', function (accounts) { context('with unauthorized account', function () { it('fails to upgrade', async function () { const callData = new ImplV1('').contract.methods.initializeNonPayableWithValue(1337).encodeABI(); - await expectRevert( + await expectRevertCustomError( this.proxyAdmin.upgradeAndCall(this.proxy.address, this.implementationV2.address, callData, { from: anotherAccount, }), - 'caller is not the owner', + 'OwnableUnauthorizedAccount', + [anotherAccount], ); }); }); @@ -97,7 +86,7 @@ contract('ProxyAdmin', function (accounts) { from: proxyAdminOwner, }); const implementationAddress = await getAddressInSlot(this.proxy, ImplementationSlot); - expect(implementationAddress).to.be.eq(this.implementationV2.address); + expect(implementationAddress).to.be.equal(this.implementationV2.address); }); }); }); diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js index 1e3b42b6e..1a03b84db 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js @@ -1,10 +1,11 @@ const { BN, expectRevert, expectEvent, constants } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; const { getAddressInSlot, ImplementationSlot, AdminSlot } = require('../../helpers/erc1967'); +const { expectRevertCustomError } = require('../../helpers/customError'); const { expect } = require('chai'); +const { web3 } = require('hardhat'); -const Proxy = artifacts.require('Proxy'); const Implementation1 = artifacts.require('Implementation1'); const Implementation2 = artifacts.require('Implementation2'); const Implementation3 = artifacts.require('Implementation3'); @@ -46,6 +47,32 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); }); + describe('proxy admin', function () { + it('emits AdminChanged event during construction', async function () { + expectEvent.inConstruction(this.proxy, 'AdminChanged', { + previousAdmin: ZERO_ADDRESS, + newAdmin: proxyAdminAddress, + }); + }); + + it('sets the admin in the storage', async function () { + expect(await getAddressInSlot(this.proxy, AdminSlot)).to.be.equal(proxyAdminAddress); + }); + + it('can overwrite the admin by the implementation', async function () { + const dummy = new DummyImplementation(this.proxyAddress); + await dummy.unsafeOverrideAdmin(anotherAccount); + const ERC1967AdminSlotValue = await getAddressInSlot(this.proxy, AdminSlot); + expect(ERC1967AdminSlotValue).to.be.equal(anotherAccount); + + // Still allows previous admin to execute admin operations + expect(ERC1967AdminSlotValue).to.not.equal(proxyAdminAddress); + expectEvent(await this.proxy.upgradeTo(this.implementationV1, { from: proxyAdminAddress }), 'Upgraded', { + implementation: this.implementationV1, + }); + }); + }); + describe('upgradeTo', function () { describe('when the sender is the admin', function () { const from = proxyAdminAddress; @@ -67,10 +94,9 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx describe('when the given implementation is the zero address', function () { it('reverts', async function () { - await expectRevert( - this.proxy.upgradeTo(ZERO_ADDRESS, { from }), - 'ERC1967: new implementation is not a contract', - ); + await expectRevertCustomError(this.proxy.upgradeTo(ZERO_ADDRESS, { from }), 'ERC1967InvalidImplementation', [ + ZERO_ADDRESS, + ]); }); }); }); @@ -121,13 +147,11 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx expect(balance.toString()).to.be.bignumber.equal(value.toString()); }); - it.skip('uses the storage of the proxy', async function () { + it('uses the storage of the proxy', async function () { // storage layout should look as follows: - // - 0: Initializable storage - // - 1-50: Initailizable reserved storage (50 slots) - // - 51: initializerRan - // - 52: x - const storedValue = await Proxy.at(this.proxyAddress).getStorageAt(52); + // - 0: Initializable storage ++ initializerRan ++ onlyInitializingRan + // - 1: x + const storedValue = await web3.eth.getStorageAt(this.proxyAddress, 1); expect(parseInt(storedValue)).to.eq(42); }); }); @@ -260,60 +284,51 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy(createProx }); }); - describe('changeAdmin', function () { - describe('when the new proposed admin is not the zero address', function () { - const newAdmin = anotherAccount; - - describe('when the sender is the admin', function () { - beforeEach('transferring', async function () { - this.receipt = await this.proxy.changeAdmin(newAdmin, { from: proxyAdminAddress }); - }); - - it('assigns new proxy admin', async function () { - const newProxyAdmin = await getAddressInSlot(this.proxy, AdminSlot); - expect(newProxyAdmin).to.be.equal(anotherAccount); - }); - - it('emits an event', function () { - expectEvent(this.receipt, 'AdminChanged', { - previousAdmin: proxyAdminAddress, - newAdmin: newAdmin, - }); - }); - }); - - describe('when the sender is not the admin', function () { - it('reverts', async function () { - await expectRevert.unspecified(this.proxy.changeAdmin(newAdmin, { from: anotherAccount })); - }); - }); - }); - - describe('when the new proposed admin is the zero address', function () { - it('reverts', async function () { - await expectRevert( - this.proxy.changeAdmin(ZERO_ADDRESS, { from: proxyAdminAddress }), - 'ERC1967: new admin is the zero address', - ); - }); - }); - }); - describe('transparent proxy', function () { beforeEach('creating proxy', async function () { const initializeData = Buffer.from(''); - this.impl = await ClashingImplementation.new(); - this.proxy = await createProxy(this.impl.address, proxyAdminAddress, initializeData, { from: proxyAdminOwner }); - + this.clashingImplV0 = (await ClashingImplementation.new()).address; + this.clashingImplV1 = (await ClashingImplementation.new()).address; + this.proxy = await createProxy(this.clashingImplV0, proxyAdminAddress, initializeData, { + from: proxyAdminOwner, + }); this.clashing = new ClashingImplementation(this.proxy.address); }); it('proxy admin cannot call delegated functions', async function () { - await expectRevert( + await expectRevertCustomError( this.clashing.delegatedFunction({ from: proxyAdminAddress }), - 'TransparentUpgradeableProxy: admin cannot fallback to proxy target', + 'ProxyDeniedAdminAccess', + [], ); }); + + describe('when function names clash', function () { + it('executes the proxy function if the sender is the admin', async function () { + const receipt = await this.proxy.upgradeTo(this.clashingImplV1, { from: proxyAdminAddress, value: 0 }); + expectEvent(receipt, 'Upgraded', { implementation: this.clashingImplV1 }); + }); + + it('delegates the call to implementation when sender is not the admin', async function () { + const receipt = await this.proxy.upgradeTo(this.clashingImplV1, { from: anotherAccount, value: 0 }); + expectEvent.notEmitted(receipt, 'Upgraded'); + expectEvent.inTransaction(receipt.tx, this.clashing, 'ClashingImplementationCall'); + }); + + it('requires 0 value calling upgradeTo by proxy admin', async function () { + await expectRevertCustomError( + this.proxy.upgradeTo(this.clashingImplV1, { from: proxyAdminAddress, value: 1 }), + 'ProxyNonPayableFunction', + [], + ); + }); + + it('allows calling with value if sender is not the admin', async function () { + const receipt = await this.proxy.upgradeTo(this.clashingImplV1, { from: anotherAccount, value: 1 }); + expectEvent.notEmitted(receipt, 'Upgraded'); + expectEvent.inTransaction(receipt.tx, this.clashing, 'ClashingImplementationCall'); + }); + }); }); describe('regression', () => { diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.test.js b/test/proxy/transparent/TransparentUpgradeableProxy.test.js index 86dd55d32..d60a31a21 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.test.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.test.js @@ -2,12 +2,14 @@ const shouldBehaveLikeProxy = require('../Proxy.behaviour'); const shouldBehaveLikeTransparentUpgradeableProxy = require('./TransparentUpgradeableProxy.behaviour'); const TransparentUpgradeableProxy = artifacts.require('TransparentUpgradeableProxy'); +const ITransparentUpgradeableProxy = artifacts.require('ITransparentUpgradeableProxy'); contract('TransparentUpgradeableProxy', function (accounts) { const [proxyAdminAddress, proxyAdminOwner] = accounts; const createProxy = async function (logic, admin, initData, opts) { - return TransparentUpgradeableProxy.new(logic, admin, initData, opts); + const { address } = await TransparentUpgradeableProxy.new(logic, admin, initData, opts); + return ITransparentUpgradeableProxy.at(address); }; shouldBehaveLikeProxy(createProxy, proxyAdminAddress, proxyAdminOwner); diff --git a/test/proxy/utils/Initializable.test.js b/test/proxy/utils/Initializable.test.js index 39c820b9d..e3e0fc02f 100644 --- a/test/proxy/utils/Initializable.test.js +++ b/test/proxy/utils/Initializable.test.js @@ -1,5 +1,6 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); const InitializableMock = artifacts.require('InitializableMock'); const ConstructorInitializableMock = artifacts.require('ConstructorInitializableMock'); @@ -40,13 +41,13 @@ contract('Initializable', function () { }); it('initializer does not run again', async function () { - await expectRevert(this.contract.initialize(), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.initialize(), 'AlreadyInitialized', []); }); }); describe('nested under an initializer', function () { it('initializer modifier reverts', async function () { - await expectRevert(this.contract.initializerNested(), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.initializerNested(), 'AlreadyInitialized', []); }); it('onlyInitializing modifier succeeds', async function () { @@ -56,7 +57,7 @@ contract('Initializable', function () { }); it('cannot call onlyInitializable function outside the scope of an initializable function', async function () { - await expectRevert(this.contract.initializeOnlyInitializing(), 'Initializable: contract is not initializing'); + await expectRevertCustomError(this.contract.initializeOnlyInitializing(), 'NotInitializing', []); }); }); @@ -98,9 +99,9 @@ contract('Initializable', function () { it('cannot nest reinitializers', async function () { expect(await this.contract.counter()).to.be.bignumber.equal('0'); - await expectRevert(this.contract.nestedReinitialize(2, 2), 'Initializable: contract is already initialized'); - await expectRevert(this.contract.nestedReinitialize(2, 3), 'Initializable: contract is already initialized'); - await expectRevert(this.contract.nestedReinitialize(3, 2), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.nestedReinitialize(2, 2), 'AlreadyInitialized', []); + await expectRevertCustomError(this.contract.nestedReinitialize(2, 3), 'AlreadyInitialized', []); + await expectRevertCustomError(this.contract.nestedReinitialize(3, 2), 'AlreadyInitialized', []); }); it('can chain reinitializers', async function () { @@ -119,18 +120,18 @@ contract('Initializable', function () { describe('contract locking', function () { it('prevents initialization', async function () { await this.contract.disableInitializers(); - await expectRevert(this.contract.initialize(), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.initialize(), 'AlreadyInitialized', []); }); it('prevents re-initialization', async function () { await this.contract.disableInitializers(); - await expectRevert(this.contract.reinitialize(255), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.reinitialize(255), 'AlreadyInitialized', []); }); it('can lock contract after initialization', async function () { await this.contract.initialize(); await this.contract.disableInitializers(); - await expectRevert(this.contract.reinitialize(255), 'Initializable: contract is already initialized'); + await expectRevertCustomError(this.contract.reinitialize(255), 'AlreadyInitialized', []); }); }); }); @@ -205,8 +206,8 @@ contract('Initializable', function () { 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'); + await expectRevertCustomError(DisableBad1.new(), 'AlreadyInitialized', []); + await expectRevertCustomError(DisableBad2.new(), 'AlreadyInitialized', []); }); it('old and new patterns in good sequence', async function () { diff --git a/test/proxy/utils/UUPSUpgradeable.test.js b/test/proxy/utils/UUPSUpgradeable.test.js index b0c1b3f6f..6a8104248 100644 --- a/test/proxy/utils/UUPSUpgradeable.test.js +++ b/test/proxy/utils/UUPSUpgradeable.test.js @@ -1,12 +1,13 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const { web3 } = require('@openzeppelin/test-helpers/src/setup'); -const { getSlot, ImplementationSlot } = require('../../helpers/erc1967'); +const { expectEvent } = require('@openzeppelin/test-helpers'); +const { getAddressInSlot, ImplementationSlot } = require('../../helpers/erc1967'); +const { expectRevertCustomError } = require('../../helpers/customError'); const ERC1967Proxy = artifacts.require('ERC1967Proxy'); const UUPSUpgradeableMock = artifacts.require('UUPSUpgradeableMock'); const UUPSUpgradeableUnsafeMock = artifacts.require('UUPSUpgradeableUnsafeMock'); -const UUPSUpgradeableLegacyMock = artifacts.require('UUPSUpgradeableLegacyMock'); const NonUpgradeableMock = artifacts.require('NonUpgradeableMock'); +const UUPSUnsupportedProxiableUUID = artifacts.require('UUPSUnsupportedProxiableUUID'); +const Clones = artifacts.require('$Clones'); contract('UUPSUpgradeable', function () { before(async function () { @@ -14,6 +15,9 @@ contract('UUPSUpgradeable', function () { this.implUpgradeOk = await UUPSUpgradeableMock.new(); this.implUpgradeUnsafe = await UUPSUpgradeableUnsafeMock.new(); this.implUpgradeNonUUPS = await NonUpgradeableMock.new(); + this.implUnsupportedUUID = await UUPSUnsupportedProxiableUUID.new(); + // Used for testing non ERC1967 compliant proxies (clones are proxies that don't use the ERC1967 implementation slot) + this.cloneFactory = await Clones.new(); }); beforeEach(async function () { @@ -25,6 +29,7 @@ contract('UUPSUpgradeable', function () { const { receipt } = await this.instance.upgradeTo(this.implUpgradeOk.address); expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1); expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address }); + expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeOk.address); }); it('upgrade to upgradeable implementation with call', async function () { @@ -36,20 +41,76 @@ contract('UUPSUpgradeable', function () { ); expect(receipt.logs.filter(({ event }) => event === 'Upgraded').length).to.be.equal(1); expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeOk.address }); + expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeOk.address); expect(await this.instance.current()).to.be.bignumber.equal('1'); }); + it('calling upgradeTo on the implementation reverts', async function () { + await expectRevertCustomError( + this.implInitial.upgradeTo(this.implUpgradeOk.address), + 'UUPSUnauthorizedCallContext', + [], + ); + }); + + it('calling upgradeToAndCall on the implementation reverts', async function () { + await expectRevertCustomError( + this.implInitial.upgradeToAndCall( + this.implUpgradeOk.address, + this.implUpgradeOk.contract.methods.increment().encodeABI(), + ), + 'UUPSUnauthorizedCallContext', + [], + ); + }); + + it('calling upgradeTo from a contract that is not an ERC1967 proxy (with the right implementation) reverts', async function () { + const receipt = await this.cloneFactory.$clone(this.implUpgradeOk.address); + const instance = await UUPSUpgradeableMock.at( + receipt.logs.find(({ event }) => event === 'return$clone').args.instance, + ); + + await expectRevertCustomError( + instance.upgradeTo(this.implUpgradeUnsafe.address), + 'UUPSUnauthorizedCallContext', + [], + ); + }); + + it('calling upgradeToAndCall from a contract that is not an ERC1967 proxy (with the right implementation) reverts', async function () { + const receipt = await this.cloneFactory.$clone(this.implUpgradeOk.address); + const instance = await UUPSUpgradeableMock.at( + receipt.logs.find(({ event }) => event === 'return$clone').args.instance, + ); + + await expectRevertCustomError( + instance.upgradeToAndCall(this.implUpgradeUnsafe.address, '0x'), + 'UUPSUnauthorizedCallContext', + [], + ); + }); + + it('rejects upgrading to an unsupported UUID', async function () { + await expectRevertCustomError( + this.instance.upgradeTo(this.implUnsupportedUUID.address), + 'UUPSUnsupportedProxiableUUID', + [web3.utils.keccak256('invalid UUID')], + ); + }); + it('upgrade to and unsafe upgradeable implementation', async function () { const { receipt } = await this.instance.upgradeTo(this.implUpgradeUnsafe.address); expectEvent(receipt, 'Upgraded', { implementation: this.implUpgradeUnsafe.address }); + expect(await getAddressInSlot(this.instance, ImplementationSlot)).to.be.equal(this.implUpgradeUnsafe.address); }); // delegate to a non existing upgradeTo function causes a low level revert it('reject upgrade to non uups implementation', async function () { - await expectRevert( + await expectRevertCustomError( this.instance.upgradeTo(this.implUpgradeNonUUPS.address), - 'ERC1967Upgrade: new implementation is not UUPS', + 'ERC1967InvalidImplementation', + [this.implUpgradeNonUUPS.address], ); }); @@ -57,29 +118,8 @@ contract('UUPSUpgradeable', function () { const { address } = await ERC1967Proxy.new(this.implInitial.address, '0x'); const otherInstance = await UUPSUpgradeableMock.at(address); - await expectRevert( - this.instance.upgradeTo(otherInstance.address), - 'ERC1967Upgrade: new implementation is not UUPS', - ); - }); - - it('can upgrade from legacy implementations', async function () { - const legacyImpl = await UUPSUpgradeableLegacyMock.new(); - const legacyInstance = await ERC1967Proxy.new(legacyImpl.address, '0x').then(({ address }) => - UUPSUpgradeableLegacyMock.at(address), - ); - - const receipt = await legacyInstance.upgradeTo(this.implInitial.address); - - const UpgradedEvents = receipt.logs.filter( - ({ address, event }) => address === legacyInstance.address && event === 'Upgraded', - ); - expect(UpgradedEvents.length).to.be.equal(1); - - expectEvent(receipt, 'Upgraded', { implementation: this.implInitial.address }); - - const implementationSlot = await getSlot(legacyInstance, ImplementationSlot); - const implementationAddress = web3.utils.toChecksumAddress(implementationSlot.substr(-40)); - expect(implementationAddress).to.be.equal(this.implInitial.address); + await expectRevertCustomError(this.instance.upgradeTo(otherInstance.address), 'ERC1967InvalidImplementation', [ + otherInstance.address, + ]); }); }); diff --git a/test/security/Pausable.test.js b/test/security/Pausable.test.js index 5cca11e47..e60a62c74 100644 --- a/test/security/Pausable.test.js +++ b/test/security/Pausable.test.js @@ -1,7 +1,8 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); - +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); + const PausableMock = artifacts.require('PausableMock'); contract('Pausable', function (accounts) { @@ -24,7 +25,7 @@ contract('Pausable', function (accounts) { }); it('cannot take drastic measure in non-pause', async function () { - await expectRevert(this.pausable.drasticMeasure(), 'Pausable: not paused'); + await expectRevertCustomError(this.pausable.drasticMeasure(), 'ExpectedPause', []); expect(await this.pausable.drasticMeasureTaken()).to.equal(false); }); @@ -38,7 +39,7 @@ contract('Pausable', function (accounts) { }); it('cannot perform normal process in pause', async function () { - await expectRevert(this.pausable.normalProcess(), 'Pausable: paused'); + await expectRevertCustomError(this.pausable.normalProcess(), 'EnforcedPause', []); }); it('can take a drastic measure in a pause', async function () { @@ -47,7 +48,7 @@ contract('Pausable', function (accounts) { }); it('reverts when re-pausing', async function () { - await expectRevert(this.pausable.pause(), 'Pausable: paused'); + await expectRevertCustomError(this.pausable.pause(), 'EnforcedPause', []); }); describe('unpausing', function () { @@ -72,11 +73,11 @@ contract('Pausable', function (accounts) { }); it('should prevent drastic measure', async function () { - await expectRevert(this.pausable.drasticMeasure(), 'Pausable: not paused'); + await expectRevertCustomError(this.pausable.drasticMeasure(), 'ExpectedPause', []); }); it('reverts when re-unpausing', async function () { - await expectRevert(this.pausable.unpause(), 'Pausable: not paused'); + await expectRevertCustomError(this.pausable.unpause(), 'ExpectedPause', []); }); }); }); diff --git a/test/security/PullPayment.test.js b/test/security/PullPayment.test.js deleted file mode 100644 index 5bf72bbe6..000000000 --- a/test/security/PullPayment.test.js +++ /dev/null @@ -1,51 +0,0 @@ -const { balance, ether } = require('@openzeppelin/test-helpers'); - -const { expect } = require('chai'); - -const PullPaymentMock = artifacts.require('PullPaymentMock'); - -contract('PullPayment', function (accounts) { - const [payer, payee1, payee2] = accounts; - - const amount = ether('17'); - - beforeEach(async function () { - this.contract = await PullPaymentMock.new({ value: amount }); - }); - - describe('payments', function () { - it('can record an async payment correctly', async function () { - await this.contract.callTransfer(payee1, 100, { from: payer }); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('100'); - }); - - it('can add multiple balances on one account', async function () { - await this.contract.callTransfer(payee1, 200, { from: payer }); - await this.contract.callTransfer(payee1, 300, { from: payer }); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('500'); - }); - - it('can add balances on multiple accounts', async function () { - await this.contract.callTransfer(payee1, 200, { from: payer }); - await this.contract.callTransfer(payee2, 300, { from: payer }); - - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('200'); - - expect(await this.contract.payments(payee2)).to.be.bignumber.equal('300'); - }); - }); - - describe('withdrawPayments', function () { - it('can withdraw payment', async function () { - const balanceTracker = await balance.tracker(payee1); - - await this.contract.callTransfer(payee1, amount, { from: payer }); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal(amount); - - await this.contract.withdrawPayments(payee1); - - expect(await balanceTracker.delta()).to.be.bignumber.equal(amount); - expect(await this.contract.payments(payee1)).to.be.bignumber.equal('0'); - }); - }); -}); diff --git a/test/security/ReentrancyGuard.test.js b/test/security/ReentrancyGuard.test.js index 1a80bc860..15355c098 100644 --- a/test/security/ReentrancyGuard.test.js +++ b/test/security/ReentrancyGuard.test.js @@ -1,7 +1,8 @@ const { expectRevert } = require('@openzeppelin/test-helpers'); - const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); + const ReentrancyMock = artifacts.require('ReentrancyMock'); const ReentrancyAttack = artifacts.require('ReentrancyAttack'); @@ -19,7 +20,7 @@ contract('ReentrancyGuard', function () { it('does not allow remote callback', async function () { const attacker = await ReentrancyAttack.new(); - await expectRevert(this.reentrancyMock.countAndCall(attacker.address), 'ReentrancyAttack: failed call'); + await expectRevert(this.reentrancyMock.countAndCall(attacker.address), 'ReentrancyAttack: failed call', []); }); it('_reentrancyGuardEntered should be true when guarded', async function () { @@ -34,10 +35,10 @@ contract('ReentrancyGuard', function () { // I put them here as documentation, and to monitor any changes // in the side-effects. it('does not allow local recursion', async function () { - await expectRevert(this.reentrancyMock.countLocalRecursive(10), 'ReentrancyGuard: reentrant call'); + await expectRevertCustomError(this.reentrancyMock.countLocalRecursive(10), 'ReentrancyGuardReentrantCall', []); }); it('does not allow indirect local recursion', async function () { - await expectRevert(this.reentrancyMock.countThisRecursive(10), 'ReentrancyMock: failed call'); + await expectRevert(this.reentrancyMock.countThisRecursive(10), 'ReentrancyMock: failed call', []); }); }); diff --git a/test/token/ERC1155/ERC1155.behavior.js b/test/token/ERC1155/ERC1155.behavior.js index 96d448a9e..4bf4a7319 100644 --- a/test/token/ERC1155/ERC1155.behavior.js +++ b/test/token/ERC1155/ERC1155.behavior.js @@ -1,11 +1,13 @@ const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); const { ZERO_ADDRESS } = constants; -const { expect } = require('chai'); - const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../../helpers/customError'); +const { Enum } = require('../../helpers/enums'); const ERC1155ReceiverMock = artifacts.require('ERC1155ReceiverMock'); +const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, multiTokenHolder, recipient, proxy]) { const firstTokenId = new BN(1); @@ -20,11 +22,8 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m describe('like an ERC1155', function () { describe('balanceOf', function () { - it('reverts when queried about the zero address', async function () { - await expectRevert( - this.token.balanceOf(ZERO_ADDRESS, firstTokenId), - 'ERC1155: address zero is not a valid owner', - ); + it('should return 0 when queried about the zero address', async function () { + expect(await this.token.balanceOf(ZERO_ADDRESS, firstTokenId)).to.be.bignumber.equal('0'); }); context("when accounts don't own tokens", function () { @@ -59,31 +58,30 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m describe('balanceOfBatch', function () { it("reverts when input arrays don't match up", async function () { - await expectRevert( - this.token.balanceOfBatch( - [firstTokenHolder, secondTokenHolder, firstTokenHolder, secondTokenHolder], - [firstTokenId, secondTokenId, unknownTokenId], - ), - 'ERC1155: accounts and ids length mismatch', - ); + const accounts1 = [firstTokenHolder, secondTokenHolder, firstTokenHolder, secondTokenHolder]; + const ids1 = [firstTokenId, secondTokenId, unknownTokenId]; + await expectRevertCustomError(this.token.balanceOfBatch(accounts1, ids1), 'ERC1155InvalidArrayLength', [ + accounts1.length, + ids1.length, + ]); - await expectRevert( - this.token.balanceOfBatch( - [firstTokenHolder, secondTokenHolder], - [firstTokenId, secondTokenId, unknownTokenId], - ), - 'ERC1155: accounts and ids length mismatch', - ); + const accounts2 = [firstTokenHolder, secondTokenHolder]; + const ids2 = [firstTokenId, secondTokenId, unknownTokenId]; + await expectRevertCustomError(this.token.balanceOfBatch(accounts2, ids2), 'ERC1155InvalidArrayLength', [ + accounts2.length, + ids2.length, + ]); }); - it('reverts when one of the addresses is the zero address', async function () { - await expectRevert( - this.token.balanceOfBatch( - [firstTokenHolder, secondTokenHolder, ZERO_ADDRESS], - [firstTokenId, secondTokenId, unknownTokenId], - ), - 'ERC1155: address zero is not a valid owner', + it('should return 0 as the balance when one of the addresses is the zero address', async function () { + const result = await this.token.balanceOfBatch( + [firstTokenHolder, secondTokenHolder, ZERO_ADDRESS], + [firstTokenId, secondTokenId, unknownTokenId], ); + expect(result).to.be.an('array'); + expect(result[0]).to.be.a.bignumber.equal('0'); + expect(result[1]).to.be.a.bignumber.equal('0'); + expect(result[2]).to.be.a.bignumber.equal('0'); }); context("when accounts don't own tokens", function () { @@ -154,9 +152,10 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts if attempting to approve self as an operator', async function () { - await expectRevert( + await expectRevertCustomError( this.token.setApprovalForAll(multiTokenHolder, true, { from: multiTokenHolder }), - 'ERC1155: setting approval status for self', + 'ERC1155InvalidOperator', + [multiTokenHolder], ); }); }); @@ -172,20 +171,22 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts when transferring more than balance', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount.addn(1), '0x', { from: multiTokenHolder, }), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [multiTokenHolder, firstAmount, firstAmount.addn(1), firstTokenId], ); }); it('reverts when transferring to zero address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, ZERO_ADDRESS, firstTokenId, firstAmount, '0x', { from: multiTokenHolder, }), - 'ERC1155: transfer to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], ); }); @@ -249,11 +250,12 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, recipient, firstTokenId, firstAmount, '0x', { from: proxy, }), - 'ERC1155: caller is not token owner or approved', + 'ERC1155MissingApprovalForAll', + [proxy, multiTokenHolder], ); }); }); @@ -295,9 +297,8 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - false, RECEIVER_BATCH_MAGIC_VALUE, - false, + RevertType.None, ); }); @@ -369,36 +370,96 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m context('to a receiver contract returning unexpected value', function () { beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new('0x00c0ffee', false, RECEIVER_BATCH_MAGIC_VALUE, false); + this.receiver = await ERC1155ReceiverMock.new('0x00c0ffee', RECEIVER_BATCH_MAGIC_VALUE, RevertType.None); }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { from: multiTokenHolder, }), - 'ERC1155: ERC1155Receiver rejected tokens', + 'ERC1155InvalidReceiver', + [this.receiver.address], ); }); }); context('to a receiver contract that reverts', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - true, - RECEIVER_BATCH_MAGIC_VALUE, - false, - ); + context('with a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithMessage, + ); + }); + + it('reverts', async function () { + await expectRevert( + this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + 'ERC1155ReceiverMock: reverting on receive', + ); + }); }); - it('reverts', async function () { - await expectRevert( - this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { - from: multiTokenHolder, - }), - 'ERC1155ReceiverMock: reverting on receive', - ); + context('without a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithoutMessage, + ); + }); + + it('reverts', async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + 'ERC1155InvalidReceiver', + [this.receiver.address], + ); + }); + }); + + context('with a custom error', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithCustomError, + ); + }); + + it('reverts', async function () { + await expectRevertCustomError( + this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + 'CustomError', + [RECEIVER_SINGLE_MAGIC_VALUE], + ); + }); + }); + + context('with a panic', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.Panic, + ); + }); + + it('reverts', async function () { + await expectRevert.unspecified( + this.token.safeTransferFrom(multiTokenHolder, this.receiver.address, firstTokenId, firstAmount, '0x', { + from: multiTokenHolder, + }), + ); + }); }); }); @@ -425,7 +486,7 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts when transferring amount more than any of balances', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, recipient, @@ -434,38 +495,36 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: multiTokenHolder }, ), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [multiTokenHolder, secondAmount, secondAmount.addn(1), secondTokenId], ); }); it("reverts when ids array length doesn't match amounts array length", async function () { - await expectRevert( - this.token.safeBatchTransferFrom( - multiTokenHolder, - recipient, - [firstTokenId], - [firstAmount, secondAmount], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155: ids and amounts length mismatch', + const ids1 = [firstTokenId]; + const amounts1 = [firstAmount, secondAmount]; + + await expectRevertCustomError( + this.token.safeBatchTransferFrom(multiTokenHolder, recipient, ids1, amounts1, '0x', { + from: multiTokenHolder, + }), + 'ERC1155InvalidArrayLength', + [ids1.length, amounts1.length], ); - await expectRevert( - this.token.safeBatchTransferFrom( - multiTokenHolder, - recipient, - [firstTokenId, secondTokenId], - [firstAmount], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155: ids and amounts length mismatch', + const ids2 = [firstTokenId, secondTokenId]; + const amounts2 = [firstAmount]; + await expectRevertCustomError( + this.token.safeBatchTransferFrom(multiTokenHolder, recipient, ids2, amounts2, '0x', { + from: multiTokenHolder, + }), + 'ERC1155InvalidArrayLength', + [ids2.length, amounts2.length], ); }); it('reverts when transferring to zero address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, ZERO_ADDRESS, @@ -474,7 +533,16 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: multiTokenHolder }, ), - 'ERC1155: transfer to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], + ); + }); + + it('reverts when transferring from zero address', async function () { + await expectRevertCustomError( + this.token.$_safeBatchTransferFrom(ZERO_ADDRESS, multiTokenHolder, [firstTokenId], [firstAmount], '0x'), + 'ERC1155InvalidSender', + [ZERO_ADDRESS], ); }); @@ -532,7 +600,7 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, recipient, @@ -541,7 +609,8 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: proxy }, ), - 'ERC1155: caller is not token owner or approved', + 'ERC1155MissingApprovalForAll', + [proxy, multiTokenHolder], ); }); }); @@ -580,9 +649,8 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - false, RECEIVER_BATCH_MAGIC_VALUE, - false, + RevertType.None, ); }); @@ -656,14 +724,13 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m beforeEach(async function () { this.receiver = await ERC1155ReceiverMock.new( RECEIVER_SINGLE_MAGIC_VALUE, - false, RECEIVER_SINGLE_MAGIC_VALUE, - false, + RevertType.None, ); }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom( multiTokenHolder, this.receiver.address, @@ -672,71 +739,107 @@ function shouldBehaveLikeERC1155([minter, firstTokenHolder, secondTokenHolder, m '0x', { from: multiTokenHolder }, ), - 'ERC1155: ERC1155Receiver rejected tokens', + 'ERC1155InvalidReceiver', + [this.receiver.address], ); }); }); context('to a receiver contract that reverts', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - false, - RECEIVER_BATCH_MAGIC_VALUE, - true, - ); + context('with a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithMessage, + ); + }); + + it('reverts', async function () { + await expectRevert( + this.token.safeBatchTransferFrom( + multiTokenHolder, + this.receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + 'ERC1155ReceiverMock: reverting on batch receive', + ); + }); }); - it('reverts', async function () { - await expectRevert( - this.token.safeBatchTransferFrom( - multiTokenHolder, - this.receiver.address, - [firstTokenId, secondTokenId], - [firstAmount, secondAmount], - '0x', - { from: multiTokenHolder }, - ), - 'ERC1155ReceiverMock: reverting on batch receive', - ); - }); - }); + context('without a revert string', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithoutMessage, + ); + }); - context('to a receiver contract that reverts only on single transfers', function () { - beforeEach(async function () { - this.receiver = await ERC1155ReceiverMock.new( - RECEIVER_SINGLE_MAGIC_VALUE, - true, - RECEIVER_BATCH_MAGIC_VALUE, - false, - ); - - this.toWhom = this.receiver.address; - this.transferReceipt = await this.token.safeBatchTransferFrom( - multiTokenHolder, - this.receiver.address, - [firstTokenId, secondTokenId], - [firstAmount, secondAmount], - '0x', - { from: multiTokenHolder }, - ); - this.transferLogs = this.transferReceipt; + it('reverts', async function () { + await expectRevertCustomError( + this.token.safeBatchTransferFrom( + multiTokenHolder, + this.receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + 'ERC1155InvalidReceiver', + [this.receiver.address], + ); + }); }); - batchTransferWasSuccessful.call(this, { - operator: multiTokenHolder, - from: multiTokenHolder, - ids: [firstTokenId, secondTokenId], - values: [firstAmount, secondAmount], + context('with a custom error', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.RevertWithCustomError, + ); + }); + + it('reverts', async function () { + await expectRevertCustomError( + this.token.safeBatchTransferFrom( + multiTokenHolder, + this.receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + 'CustomError', + [RECEIVER_SINGLE_MAGIC_VALUE], + ); + }); }); - it('calls onERC1155BatchReceived', async function () { - await expectEvent.inTransaction(this.transferReceipt.tx, ERC1155ReceiverMock, 'BatchReceived', { - operator: multiTokenHolder, - from: multiTokenHolder, - // ids: [firstTokenId, secondTokenId], - // values: [firstAmount, secondAmount], - data: null, + context('with a panic', function () { + beforeEach(async function () { + this.receiver = await ERC1155ReceiverMock.new( + RECEIVER_SINGLE_MAGIC_VALUE, + RECEIVER_BATCH_MAGIC_VALUE, + RevertType.Panic, + ); + }); + + it('reverts', async function () { + await expectRevert.unspecified( + this.token.safeBatchTransferFrom( + multiTokenHolder, + this.receiver.address, + [firstTokenId, secondTokenId], + [firstAmount, secondAmount], + '0x', + { from: multiTokenHolder }, + ), + ); }); }); }); diff --git a/test/token/ERC1155/ERC1155.test.js b/test/token/ERC1155/ERC1155.test.js index 48197eeb5..23555dd54 100644 --- a/test/token/ERC1155/ERC1155.test.js +++ b/test/token/ERC1155/ERC1155.test.js @@ -1,8 +1,10 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); + const { shouldBehaveLikeERC1155 } = require('./ERC1155.behavior'); const ERC1155Mock = artifacts.require('$ERC1155'); @@ -30,9 +32,10 @@ contract('ERC1155', function (accounts) { describe('_mint', function () { it('reverts with a zero destination address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mint(ZERO_ADDRESS, tokenId, mintAmount, data), - 'ERC1155: mint to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], ); }); @@ -59,21 +62,24 @@ contract('ERC1155', function (accounts) { describe('_mintBatch', function () { it('reverts with a zero destination address', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(ZERO_ADDRESS, tokenBatchIds, mintAmounts, data), - 'ERC1155: mint to the zero address', + 'ERC1155InvalidReceiver', + [ZERO_ADDRESS], ); }); it('reverts if length of inputs do not match', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds, mintAmounts.slice(1), data), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length, mintAmounts.length - 1], ); - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(tokenBatchHolder, tokenBatchIds.slice(1), mintAmounts, data), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length - 1, mintAmounts.length], ); }); @@ -107,22 +113,26 @@ contract('ERC1155', function (accounts) { describe('_burn', function () { it("reverts when burning the zero account's tokens", async function () { - await expectRevert(this.token.$_burn(ZERO_ADDRESS, tokenId, mintAmount), 'ERC1155: burn from the zero address'); + await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, tokenId, mintAmount), 'ERC1155InvalidSender', [ + ZERO_ADDRESS, + ]); }); it('reverts when burning a non-existent token id', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burn(tokenHolder, tokenId, mintAmount), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [tokenHolder, 0, mintAmount, tokenId], ); }); it('reverts when burning more than available tokens', async function () { await this.token.$_mint(tokenHolder, tokenId, mintAmount, data, { from: operator }); - await expectRevert( + await expectRevertCustomError( this.token.$_burn(tokenHolder, tokenId, mintAmount.addn(1)), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [tokenHolder, mintAmount, mintAmount.addn(1), tokenId], ); }); @@ -150,28 +160,32 @@ contract('ERC1155', function (accounts) { describe('_burnBatch', function () { it("reverts when burning the zero account's tokens", async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(ZERO_ADDRESS, tokenBatchIds, burnAmounts), - 'ERC1155: burn from the zero address', + 'ERC1155InvalidSender', + [ZERO_ADDRESS], ); }); it('reverts if length of inputs do not match', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts.slice(1)), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length, burnAmounts.length - 1], ); - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds.slice(1), burnAmounts), - 'ERC1155: ids and amounts length mismatch', + 'ERC1155InvalidArrayLength', + [tokenBatchIds.length - 1, burnAmounts.length], ); }); it('reverts when burning a non-existent token id', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(tokenBatchHolder, tokenBatchIds, burnAmounts), - 'ERC1155: insufficient balance for transfer', + 'ERC1155InsufficientBalance', + [tokenBatchHolder, 0, tokenBatchIds[0], burnAmounts[0]], ); }); diff --git a/test/token/ERC1155/extensions/ERC1155Burnable.test.js b/test/token/ERC1155/extensions/ERC1155Burnable.test.js index f80d9935a..65a2f95f4 100644 --- a/test/token/ERC1155/extensions/ERC1155Burnable.test.js +++ b/test/token/ERC1155/extensions/ERC1155Burnable.test.js @@ -1,7 +1,9 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); + const ERC1155Burnable = artifacts.require('$ERC1155Burnable'); contract('ERC1155Burnable', function (accounts) { @@ -34,9 +36,10 @@ contract('ERC1155Burnable', function (accounts) { }); it("unapproved accounts cannot burn the holder's tokens", async function () { - await expectRevert( + await expectRevertCustomError( this.token.burn(holder, tokenIds[0], amounts[0].subn(1), { from: other }), - 'ERC1155: caller is not token owner or approved', + 'ERC1155MissingApprovalForAll', + [other, holder], ); }); }); @@ -58,9 +61,10 @@ contract('ERC1155Burnable', function (accounts) { }); it("unapproved accounts cannot burn the holder's tokens", async function () { - await expectRevert( + await expectRevertCustomError( this.token.burnBatch(holder, tokenIds, [amounts[0].subn(1), amounts[1].subn(2)], { from: other }), - 'ERC1155: caller is not token owner or approved', + 'ERC1155MissingApprovalForAll', + [other, holder], ); }); }); diff --git a/test/token/ERC1155/extensions/ERC1155Pausable.test.js b/test/token/ERC1155/extensions/ERC1155Pausable.test.js index f4d5cedec..b0ac54bdb 100644 --- a/test/token/ERC1155/extensions/ERC1155Pausable.test.js +++ b/test/token/ERC1155/extensions/ERC1155Pausable.test.js @@ -1,6 +1,7 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC1155Pausable = artifacts.require('$ERC1155Pausable'); @@ -28,60 +29,64 @@ contract('ERC1155Pausable', function (accounts) { }); it('reverts when trying to safeTransferFrom from holder', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(holder, receiver, firstTokenId, firstTokenAmount, '0x', { from: holder }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeTransferFrom from operator', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(holder, receiver, firstTokenId, firstTokenAmount, '0x', { from: operator }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeBatchTransferFrom from holder', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom(holder, receiver, [firstTokenId], [firstTokenAmount], '0x', { from: holder }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeBatchTransferFrom from operator', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeBatchTransferFrom(holder, receiver, [firstTokenId], [firstTokenAmount], '0x', { from: operator, }), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to mint', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mint(holder, secondTokenId, secondTokenAmount, '0x'), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to mintBatch', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_mintBatch(holder, [secondTokenId], [secondTokenAmount], '0x'), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to burn', async function () { - await expectRevert( - this.token.$_burn(holder, firstTokenId, firstTokenAmount), - 'ERC1155Pausable: token transfer while paused', - ); + await expectRevertCustomError(this.token.$_burn(holder, firstTokenId, firstTokenAmount), 'EnforcedPause', []); }); it('reverts when trying to burnBatch', async function () { - await expectRevert( + await expectRevertCustomError( this.token.$_burnBatch(holder, [firstTokenId], [firstTokenAmount]), - 'ERC1155Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); diff --git a/test/token/ERC1155/utils/ERC1155Holder.test.js b/test/token/ERC1155/utils/ERC1155Holder.test.js index 864e89b50..8d8541640 100644 --- a/test/token/ERC1155/utils/ERC1155Holder.test.js +++ b/test/token/ERC1155/utils/ERC1155Holder.test.js @@ -1,6 +1,6 @@ const { BN } = require('@openzeppelin/test-helpers'); -const ERC1155Holder = artifacts.require('ERC1155Holder'); +const ERC1155Holder = artifacts.require('$ERC1155Holder'); const ERC1155 = artifacts.require('$ERC1155'); const { expect } = require('chai'); diff --git a/test/token/ERC20/ERC20.behavior.js b/test/token/ERC20/ERC20.behavior.js index 41e47f065..bb2efda89 100644 --- a/test/token/ERC20/ERC20.behavior.js +++ b/test/token/ERC20/ERC20.behavior.js @@ -1,8 +1,13 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { ZERO_ADDRESS, MAX_UINT256 } = constants; -function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipient, anotherAccount) { +const { expectRevertCustomError } = require('../../helpers/customError'); + +function shouldBehaveLikeERC20(initialSupply, accounts, opts = {}) { + const [initialHolder, recipient, anotherAccount] = accounts; + const { forcedApproval } = opts; + describe('total supply', function () { it('returns the total amount of tokens', async function () { expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply); @@ -24,7 +29,7 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); describe('transfer', function () { - shouldBehaveLikeERC20Transfer(errorPrefix, initialHolder, recipient, initialSupply, function (from, to, value) { + shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, value) { return this.token.transfer(to, value, { from }); }); }); @@ -68,13 +73,22 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); }); - it('emits an approval event', async function () { - expectEvent(await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'Approval', { - owner: tokenOwner, - spender: spender, - value: await this.token.allowance(tokenOwner, spender), + if (forcedApproval) { + it('emits an approval event', async function () { + expectEvent(await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'Approval', { + owner: tokenOwner, + spender: spender, + value: await this.token.allowance(tokenOwner, spender), + }); }); - }); + } else { + it('does not emit an approval event', async function () { + expectEvent.notEmitted( + await this.token.transferFrom(tokenOwner, to, amount, { from: spender }), + 'Approval', + ); + }); + } }); describe('when the token owner does not have enough balance', function () { @@ -85,9 +99,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: transfer amount exceeds balance`, + 'ERC20InsufficientBalance', + [tokenOwner, amount - 1, amount], ); }); }); @@ -104,9 +119,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi const amount = initialSupply; it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: insufficient allowance`, + 'ERC20InsufficientAllowance', + [spender, allowance, amount], ); }); }); @@ -119,9 +135,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: transfer amount exceeds balance`, + 'ERC20InsufficientBalance', + [tokenOwner, amount - 1, amount], ); }); }); @@ -153,9 +170,10 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi }); it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(tokenOwner, to, amount, { from: spender }), - `${errorPrefix}: transfer to the zero address`, + 'ERC20InvalidReceiver', + [ZERO_ADDRESS], ); }); }); @@ -167,31 +185,33 @@ function shouldBehaveLikeERC20(errorPrefix, initialSupply, initialHolder, recipi const to = recipient; it('reverts', async function () { - await expectRevert(this.token.transferFrom(tokenOwner, to, amount, { from: spender }), 'from the zero address'); + await expectRevertCustomError( + this.token.transferFrom(tokenOwner, to, amount, { from: spender }), + 'ERC20InvalidApprover', + [ZERO_ADDRESS], + ); }); }); }); describe('approve', function () { - shouldBehaveLikeERC20Approve( - errorPrefix, - initialHolder, - recipient, - initialSupply, - function (owner, spender, amount) { - return this.token.approve(spender, amount, { from: owner }); - }, - ); + shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, amount) { + return this.token.approve(spender, amount, { from: owner }); + }); }); } -function shouldBehaveLikeERC20Transfer(errorPrefix, from, to, balance, transfer) { +function shouldBehaveLikeERC20Transfer(from, to, balance, transfer) { describe('when the recipient is not the zero address', function () { describe('when the sender does not have enough balance', function () { const amount = balance.addn(1); it('reverts', async function () { - await expectRevert(transfer.call(this, from, to, amount), `${errorPrefix}: transfer amount exceeds balance`); + await expectRevertCustomError(transfer.call(this, from, to, amount), 'ERC20InsufficientBalance', [ + from, + balance, + amount, + ]); }); }); @@ -230,15 +250,14 @@ function shouldBehaveLikeERC20Transfer(errorPrefix, from, to, balance, transfer) describe('when the recipient is the zero address', function () { it('reverts', async function () { - await expectRevert( - transfer.call(this, from, ZERO_ADDRESS, balance), - `${errorPrefix}: transfer to the zero address`, - ); + await expectRevertCustomError(transfer.call(this, from, ZERO_ADDRESS, balance), 'ERC20InvalidReceiver', [ + ZERO_ADDRESS, + ]); }); }); } -function shouldBehaveLikeERC20Approve(errorPrefix, owner, spender, supply, approve) { +function shouldBehaveLikeERC20Approve(owner, spender, supply, approve) { describe('when the spender is not the zero address', function () { describe('when the sender has enough balance', function () { const amount = supply; @@ -307,10 +326,9 @@ function shouldBehaveLikeERC20Approve(errorPrefix, owner, spender, supply, appro describe('when the spender is the zero address', function () { it('reverts', async function () { - await expectRevert( - approve.call(this, owner, ZERO_ADDRESS, supply), - `${errorPrefix}: approve to the zero address`, - ); + await expectRevertCustomError(approve.call(this, owner, ZERO_ADDRESS, supply), `ERC20InvalidSpender`, [ + ZERO_ADDRESS, + ]); }); }); } diff --git a/test/token/ERC20/ERC20.test.js b/test/token/ERC20/ERC20.test.js index c29197578..ef6d82f2b 100644 --- a/test/token/ERC20/ERC20.test.js +++ b/test/token/ERC20/ERC20.test.js @@ -7,350 +7,359 @@ const { shouldBehaveLikeERC20Transfer, shouldBehaveLikeERC20Approve, } = require('./ERC20.behavior'); +const { expectRevertCustomError } = require('../../helpers/customError'); -const ERC20 = artifacts.require('$ERC20'); -const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); +const TOKENS = [ + { Token: artifacts.require('$ERC20') }, + { Token: artifacts.require('$ERC20ApprovalMock'), forcedApproval: true }, +]; contract('ERC20', function (accounts) { - const [initialHolder, recipient, anotherAccount] = accounts; + const [initialHolder, recipient] = accounts; const name = 'My Token'; const symbol = 'MTKN'; - const initialSupply = new BN(100); - beforeEach(async function () { - this.token = await ERC20.new(name, symbol); - await this.token.$_mint(initialHolder, initialSupply); - }); + for (const { Token, forcedApproval } of TOKENS) { + describe(`using ${Token._json.contractName}`, function () { + beforeEach(async function () { + this.token = await Token.new(name, symbol); + await this.token.$_mint(initialHolder, initialSupply); + }); - it('has a name', async function () { - expect(await this.token.name()).to.equal(name); - }); + shouldBehaveLikeERC20(initialSupply, accounts, { forcedApproval }); - it('has a symbol', async function () { - expect(await this.token.symbol()).to.equal(symbol); - }); + it('has a name', async function () { + expect(await this.token.name()).to.equal(name); + }); - it('has 18 decimals', async function () { - expect(await this.token.decimals()).to.be.bignumber.equal('18'); - }); + it('has a symbol', async function () { + expect(await this.token.symbol()).to.equal(symbol); + }); - describe('set decimals', function () { - const decimals = new BN(6); + it('has 18 decimals', async function () { + expect(await this.token.decimals()).to.be.bignumber.equal('18'); + }); - it('can set decimals during construction', async function () { - const token = await ERC20Decimals.new(name, symbol, decimals); - expect(await token.decimals()).to.be.bignumber.equal(decimals); - }); - }); + describe('decrease allowance', function () { + describe('when the spender is not the zero address', function () { + const spender = recipient; - shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount); + function shouldDecreaseApproval(amount) { + describe('when there was no approved amount before', function () { + it('reverts', async function () { + const allowance = await this.token.allowance(initialHolder, spender); + await expectRevertCustomError( + this.token.decreaseAllowance(spender, amount, { from: initialHolder }), + 'ERC20FailedDecreaseAllowance', + [spender, allowance, amount], + ); + }); + }); - describe('decrease allowance', function () { - describe('when the spender is not the zero address', function () { - const spender = recipient; + describe('when the spender had an approved amount', function () { + const approvedAmount = amount; + + beforeEach(async function () { + await this.token.approve(spender, approvedAmount, { from: initialHolder }); + }); + + it('emits an approval event', async function () { + expectEvent( + await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }), + 'Approval', + { owner: initialHolder, spender: spender, value: new BN(0) }, + ); + }); + + it('decreases the spender allowance subtracting the requested amount', async function () { + await this.token.decreaseAllowance(spender, approvedAmount.subn(1), { from: initialHolder }); + + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('1'); + }); + + it('sets the allowance to zero when all allowance is removed', async function () { + await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }); + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('0'); + }); + + it('reverts when more than the full allowance is removed', async function () { + await expectRevertCustomError( + this.token.decreaseAllowance(spender, approvedAmount.addn(1), { from: initialHolder }), + 'ERC20FailedDecreaseAllowance', + [spender, approvedAmount, approvedAmount.addn(1)], + ); + }); + }); + } + + describe('when the sender has enough balance', function () { + const amount = initialSupply; + + shouldDecreaseApproval(amount); + }); + + describe('when the sender does not have enough balance', function () { + const amount = initialSupply.addn(1); + + shouldDecreaseApproval(amount); + }); + }); + + describe('when the spender is the zero address', function () { + const amount = initialSupply; + const spender = ZERO_ADDRESS; - function shouldDecreaseApproval(amount) { - describe('when there was no approved amount before', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.decreaseAllowance(spender, amount, { from: initialHolder }), - 'ERC20: decreased allowance below zero', + 'ERC20FailedDecreaseAllowance', + [spender, 0, amount], ); }); }); + }); - describe('when the spender had an approved amount', function () { - const approvedAmount = amount; - - beforeEach(async function () { - await this.token.approve(spender, approvedAmount, { from: initialHolder }); - }); - - it('emits an approval event', async function () { - expectEvent( - await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }), - 'Approval', - { owner: initialHolder, spender: spender, value: new BN(0) }, - ); - }); - - it('decreases the spender allowance subtracting the requested amount', async function () { - await this.token.decreaseAllowance(spender, approvedAmount.subn(1), { from: initialHolder }); - - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('1'); - }); - - it('sets the allowance to zero when all allowance is removed', async function () { - await this.token.decreaseAllowance(spender, approvedAmount, { from: initialHolder }); - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal('0'); - }); - - it('reverts when more than the full allowance is removed', async function () { - await expectRevert( - this.token.decreaseAllowance(spender, approvedAmount.addn(1), { from: initialHolder }), - 'ERC20: decreased allowance below zero', - ); - }); - }); - } - - describe('when the sender has enough balance', function () { + describe('increase allowance', function () { const amount = initialSupply; - shouldDecreaseApproval(amount); - }); + describe('when the spender is not the zero address', function () { + const spender = recipient; - describe('when the sender does not have enough balance', function () { - const amount = initialSupply.addn(1); + describe('when the sender has enough balance', function () { + it('emits an approval event', async function () { + expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { + owner: initialHolder, + spender: spender, + value: amount, + }); + }); - shouldDecreaseApproval(amount); - }); - }); + describe('when there was no approved amount before', function () { + it('approves the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - describe('when the spender is the zero address', function () { - const amount = initialSupply; - const spender = ZERO_ADDRESS; + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); + }); + }); - it('reverts', async function () { - await expectRevert( - this.token.decreaseAllowance(spender, amount, { from: initialHolder }), - 'ERC20: decreased allowance below zero', - ); - }); - }); - }); + describe('when the spender had an approved amount', function () { + beforeEach(async function () { + await this.token.approve(spender, new BN(1), { from: initialHolder }); + }); - describe('increase allowance', function () { - const amount = initialSupply; + it('increases the spender allowance adding the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - describe('when the spender is not the zero address', function () { - const spender = recipient; + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); + }); + }); + }); - describe('when the sender has enough balance', function () { - it('emits an approval event', async function () { - expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { - owner: initialHolder, - spender: spender, - value: amount, + describe('when the sender does not have enough balance', function () { + const amount = initialSupply.addn(1); + + it('emits an approval event', async function () { + expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { + owner: initialHolder, + spender: spender, + value: amount, + }); + }); + + describe('when there was no approved amount before', function () { + it('approves the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); + }); + }); + + describe('when the spender had an approved amount', function () { + beforeEach(async function () { + await this.token.approve(spender, new BN(1), { from: initialHolder }); + }); + + it('increases the spender allowance adding the requested amount', async function () { + await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + + expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); + }); + }); }); }); - describe('when there was no approved amount before', function () { - it('approves the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); + describe('when the spender is the zero address', function () { + const spender = ZERO_ADDRESS; - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); - }); - }); - - describe('when the spender had an approved amount', function () { - beforeEach(async function () { - await this.token.approve(spender, new BN(1), { from: initialHolder }); - }); - - it('increases the spender allowance adding the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); + it('reverts', async function () { + await expectRevertCustomError( + this.token.increaseAllowance(spender, amount, { from: initialHolder }), + 'ERC20InvalidSpender', + [ZERO_ADDRESS], + ); }); }); }); - describe('when the sender does not have enough balance', function () { - const amount = initialSupply.addn(1); - - it('emits an approval event', async function () { - expectEvent(await this.token.increaseAllowance(spender, amount, { from: initialHolder }), 'Approval', { - owner: initialHolder, - spender: spender, - value: amount, - }); + describe('_mint', function () { + const amount = new BN(50); + it('rejects a null account', async function () { + await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, amount), 'ERC20InvalidReceiver', [ + ZERO_ADDRESS, + ]); }); - describe('when there was no approved amount before', function () { - it('approves the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount); - }); + it('rejects overflow', async function () { + const maxUint256 = new BN('2').pow(new BN(256)).subn(1); + await expectRevert( + this.token.$_mint(recipient, maxUint256), + 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)', + ); }); - describe('when the spender had an approved amount', function () { - beforeEach(async function () { - await this.token.approve(spender, new BN(1), { from: initialHolder }); + describe('for a non zero account', function () { + beforeEach('minting', async function () { + this.receipt = await this.token.$_mint(recipient, amount); }); - it('increases the spender allowance adding the requested amount', async function () { - await this.token.increaseAllowance(spender, amount, { from: initialHolder }); - - expect(await this.token.allowance(initialHolder, spender)).to.be.bignumber.equal(amount.addn(1)); - }); - }); - }); - }); - - describe('when the spender is the zero address', function () { - const spender = ZERO_ADDRESS; - - it('reverts', async function () { - await expectRevert( - this.token.increaseAllowance(spender, amount, { from: initialHolder }), - 'ERC20: approve to the zero address', - ); - }); - }); - }); - - describe('_mint', function () { - const amount = new BN(50); - it('rejects a null account', async function () { - await expectRevert(this.token.$_mint(ZERO_ADDRESS, amount), 'ERC20: mint to the zero address'); - }); - - it('rejects overflow', async function () { - const maxUint256 = new BN('2').pow(new BN(256)).subn(1); - await expectRevert( - this.token.$_mint(recipient, maxUint256), - 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)', - ); - }); - - describe('for a non zero account', function () { - beforeEach('minting', async function () { - this.receipt = await this.token.$_mint(recipient, amount); - }); - - it('increments totalSupply', async function () { - const expectedSupply = initialSupply.add(amount); - expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); - }); - - it('increments recipient balance', async function () { - expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount); - }); - - it('emits Transfer event', async function () { - const event = expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: recipient }); - - expect(event.args.value).to.be.bignumber.equal(amount); - }); - }); - }); - - describe('_burn', function () { - it('rejects a null account', async function () { - await expectRevert(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20: burn from the zero address'); - }); - - describe('for a non zero account', function () { - it('rejects burning more than balance', async function () { - await expectRevert( - this.token.$_burn(initialHolder, initialSupply.addn(1)), - 'ERC20: transfer amount exceeds balance', - ); - }); - - const describeBurn = function (description, amount) { - describe(description, function () { - beforeEach('burning', async function () { - this.receipt = await this.token.$_burn(initialHolder, amount); - }); - - it('decrements totalSupply', async function () { - const expectedSupply = initialSupply.sub(amount); + it('increments totalSupply', async function () { + const expectedSupply = initialSupply.add(amount); expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); }); - it('decrements initialHolder balance', async function () { - const expectedBalance = initialSupply.sub(amount); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(expectedBalance); + it('increments recipient balance', async function () { + expect(await this.token.balanceOf(recipient)).to.be.bignumber.equal(amount); }); it('emits Transfer event', async function () { - const event = expectEvent(this.receipt, 'Transfer', { from: initialHolder, to: ZERO_ADDRESS }); + const event = expectEvent(this.receipt, 'Transfer', { from: ZERO_ADDRESS, to: recipient }); expect(event.args.value).to.be.bignumber.equal(amount); }); }); - }; - - describeBurn('for entire balance', initialSupply); - describeBurn('for less amount than balance', initialSupply.subn(1)); - }); - }); - - describe('_update', function () { - const amount = new BN(1); - - it('from is the zero address', async function () { - const balanceBefore = await this.token.balanceOf(initialHolder); - const totalSupply = await this.token.totalSupply(); - - expectEvent(await this.token.$_update(ZERO_ADDRESS, initialHolder, amount), 'Transfer', { - from: ZERO_ADDRESS, - to: initialHolder, - value: amount, }); - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.add(amount)); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.add(amount)); - }); - it('to is the zero address', async function () { - const balanceBefore = await this.token.balanceOf(initialHolder); - const totalSupply = await this.token.totalSupply(); + describe('_burn', function () { + it('rejects a null account', async function () { + await expectRevertCustomError(this.token.$_burn(ZERO_ADDRESS, new BN(1)), 'ERC20InvalidSender', [ + ZERO_ADDRESS, + ]); + }); - expectEvent(await this.token.$_update(initialHolder, ZERO_ADDRESS, amount), 'Transfer', { - from: initialHolder, - to: ZERO_ADDRESS, - value: amount, + describe('for a non zero account', function () { + it('rejects burning more than balance', async function () { + await expectRevertCustomError( + this.token.$_burn(initialHolder, initialSupply.addn(1)), + 'ERC20InsufficientBalance', + [initialHolder, initialSupply, initialSupply.addn(1)], + ); + }); + + const describeBurn = function (description, amount) { + describe(description, function () { + beforeEach('burning', async function () { + this.receipt = await this.token.$_burn(initialHolder, amount); + }); + + it('decrements totalSupply', async function () { + const expectedSupply = initialSupply.sub(amount); + expect(await this.token.totalSupply()).to.be.bignumber.equal(expectedSupply); + }); + + it('decrements initialHolder balance', async function () { + const expectedBalance = initialSupply.sub(amount); + expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(expectedBalance); + }); + + it('emits Transfer event', async function () { + const event = expectEvent(this.receipt, 'Transfer', { from: initialHolder, to: ZERO_ADDRESS }); + + expect(event.args.value).to.be.bignumber.equal(amount); + }); + }); + }; + + describeBurn('for entire balance', initialSupply); + describeBurn('for less amount than balance', initialSupply.subn(1)); + }); }); - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.sub(amount)); - expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.sub(amount)); - }); - it('from and to are the zero address', async function () { - const totalSupply = await this.token.totalSupply(); + describe('_update', function () { + const amount = new BN(1); - await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount); + it('from is the zero address', async function () { + const balanceBefore = await this.token.balanceOf(initialHolder); + const totalSupply = await this.token.totalSupply(); - expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply); - expectEvent(await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount), 'Transfer', { - from: ZERO_ADDRESS, - to: ZERO_ADDRESS, - value: amount, + expectEvent(await this.token.$_update(ZERO_ADDRESS, initialHolder, amount), 'Transfer', { + from: ZERO_ADDRESS, + to: initialHolder, + value: amount, + }); + expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.add(amount)); + expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.add(amount)); + }); + + it('to is the zero address', async function () { + const balanceBefore = await this.token.balanceOf(initialHolder); + const totalSupply = await this.token.totalSupply(); + + expectEvent(await this.token.$_update(initialHolder, ZERO_ADDRESS, amount), 'Transfer', { + from: initialHolder, + to: ZERO_ADDRESS, + value: amount, + }); + expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply.sub(amount)); + expect(await this.token.balanceOf(initialHolder)).to.be.bignumber.equal(balanceBefore.sub(amount)); + }); + + it('from and to are the zero address', async function () { + const totalSupply = await this.token.totalSupply(); + + await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount); + + expect(await this.token.totalSupply()).to.be.bignumber.equal(totalSupply); + expectEvent(await this.token.$_update(ZERO_ADDRESS, ZERO_ADDRESS, amount), 'Transfer', { + from: ZERO_ADDRESS, + to: ZERO_ADDRESS, + value: amount, + }); + }); + }); + + describe('_transfer', function () { + shouldBehaveLikeERC20Transfer(initialHolder, recipient, initialSupply, function (from, to, amount) { + return this.token.$_transfer(from, to, amount); + }); + + describe('when the sender is the zero address', function () { + it('reverts', async function () { + await expectRevertCustomError( + this.token.$_transfer(ZERO_ADDRESS, recipient, initialSupply), + 'ERC20InvalidSender', + [ZERO_ADDRESS], + ); + }); + }); + }); + + describe('_approve', function () { + shouldBehaveLikeERC20Approve(initialHolder, recipient, initialSupply, function (owner, spender, amount) { + return this.token.$_approve(owner, spender, amount); + }); + + describe('when the owner is the zero address', function () { + it('reverts', async function () { + await expectRevertCustomError( + this.token.$_approve(ZERO_ADDRESS, recipient, initialSupply), + 'ERC20InvalidApprover', + [ZERO_ADDRESS], + ); + }); + }); }); }); - }); - - describe('_transfer', function () { - shouldBehaveLikeERC20Transfer('ERC20', initialHolder, recipient, initialSupply, function (from, to, amount) { - return this.token.$_transfer(from, to, amount); - }); - - describe('when the sender is the zero address', function () { - it('reverts', async function () { - await expectRevert( - this.token.$_transfer(ZERO_ADDRESS, recipient, initialSupply), - 'ERC20: transfer from the zero address', - ); - }); - }); - }); - - describe('_approve', function () { - shouldBehaveLikeERC20Approve('ERC20', initialHolder, recipient, initialSupply, function (owner, spender, amount) { - return this.token.$_approve(owner, spender, amount); - }); - - describe('when the owner is the zero address', function () { - it('reverts', async function () { - await expectRevert( - this.token.$_approve(ZERO_ADDRESS, recipient, initialSupply), - 'ERC20: approve from the zero address', - ); - }); - }); - }); + } }); diff --git a/test/token/ERC20/extensions/ERC20Burnable.behavior.js b/test/token/ERC20/extensions/ERC20Burnable.behavior.js index 448dda4ab..848e54b79 100644 --- a/test/token/ERC20/extensions/ERC20Burnable.behavior.js +++ b/test/token/ERC20/extensions/ERC20Burnable.behavior.js @@ -1,7 +1,8 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { ZERO_ADDRESS } = constants; const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { describe('burn', function () { @@ -37,7 +38,11 @@ function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { const amount = initialBalance.addn(1); it('reverts', async function () { - await expectRevert(this.token.burn(amount, { from: owner }), 'ERC20: transfer amount exceeds balance'); + await expectRevertCustomError(this.token.burn(amount, { from: owner }), 'ERC20InsufficientBalance', [ + owner, + initialBalance, + amount, + ]); }); }); }); @@ -83,9 +88,10 @@ function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { it('reverts', async function () { await this.token.approve(burner, amount, { from: owner }); - await expectRevert( + await expectRevertCustomError( this.token.burnFrom(owner, amount, { from: burner }), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [owner, initialBalance, amount], ); }); }); @@ -95,9 +101,10 @@ function shouldBehaveLikeERC20Burnable(owner, initialBalance, [burner]) { it('reverts', async function () { await this.token.approve(burner, allowance, { from: owner }); - await expectRevert( + await expectRevertCustomError( this.token.burnFrom(owner, allowance.addn(1), { from: burner }), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [burner, allowance, allowance.addn(1)], ); }); }); diff --git a/test/token/ERC20/extensions/ERC20Capped.behavior.js b/test/token/ERC20/extensions/ERC20Capped.behavior.js index 97bad1db1..c40e4fcc4 100644 --- a/test/token/ERC20/extensions/ERC20Capped.behavior.js +++ b/test/token/ERC20/extensions/ERC20Capped.behavior.js @@ -1,6 +1,5 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); - const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); function shouldBehaveLikeERC20Capped(accounts, cap) { describe('capped token', function () { @@ -17,12 +16,12 @@ function shouldBehaveLikeERC20Capped(accounts, cap) { it('fails to mint if the amount exceeds the cap', async function () { await this.token.$_mint(user, cap.subn(1)); - await expectRevert(this.token.$_mint(user, 2), 'ERC20Capped: cap exceeded'); + await expectRevertCustomError(this.token.$_mint(user, 2), 'ERC20ExceededCap', [cap.addn(1), cap]); }); it('fails to mint after cap is reached', async function () { await this.token.$_mint(user, cap); - await expectRevert(this.token.$_mint(user, 1), 'ERC20Capped: cap exceeded'); + await expectRevertCustomError(this.token.$_mint(user, 1), 'ERC20ExceededCap', [cap.addn(1), cap]); }); }); } diff --git a/test/token/ERC20/extensions/ERC20Capped.test.js b/test/token/ERC20/extensions/ERC20Capped.test.js index a86d38c1a..1f4a2bee3 100644 --- a/test/token/ERC20/extensions/ERC20Capped.test.js +++ b/test/token/ERC20/extensions/ERC20Capped.test.js @@ -1,5 +1,6 @@ -const { ether, expectRevert } = require('@openzeppelin/test-helpers'); +const { ether } = require('@openzeppelin/test-helpers'); const { shouldBehaveLikeERC20Capped } = require('./ERC20Capped.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC20Capped = artifacts.require('$ERC20Capped'); @@ -10,7 +11,7 @@ contract('ERC20Capped', function (accounts) { const symbol = 'MTKN'; it('requires a non-zero cap', async function () { - await expectRevert(ERC20Capped.new(name, symbol, 0), 'ERC20Capped: cap is 0'); + await expectRevertCustomError(ERC20Capped.new(name, symbol, 0), 'ERC20InvalidCap', [0]); }); context('once deployed', async function () { diff --git a/test/token/ERC20/extensions/ERC20FlashMint.test.js b/test/token/ERC20/extensions/ERC20FlashMint.test.js index ee9bedd26..a646704e2 100644 --- a/test/token/ERC20/extensions/ERC20FlashMint.test.js +++ b/test/token/ERC20/extensions/ERC20FlashMint.test.js @@ -2,6 +2,7 @@ const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const { MAX_UINT256, ZERO_ADDRESS } = constants; const ERC20FlashMintMock = artifacts.require('$ERC20FlashMintMock'); @@ -37,7 +38,9 @@ contract('ERC20FlashMint', function (accounts) { }); it('token mismatch', async function () { - await expectRevert(this.token.flashFee(ZERO_ADDRESS, loanAmount), 'ERC20FlashMint: wrong token'); + await expectRevertCustomError(this.token.flashFee(ZERO_ADDRESS, loanAmount), 'ERC3156UnsupportedToken', [ + ZERO_ADDRESS, + ]); }); }); @@ -79,26 +82,29 @@ contract('ERC20FlashMint', function (accounts) { it('missing return value', async function () { const receiver = await ERC3156FlashBorrowerMock.new(false, true); - await expectRevert( + await expectRevertCustomError( this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'), - 'ERC20FlashMint: invalid return value', + 'ERC3156InvalidReceiver', + [receiver.address], ); }); it('missing approval', async function () { const receiver = await ERC3156FlashBorrowerMock.new(true, false); - await expectRevert( + await expectRevertCustomError( this.token.flashLoan(receiver.address, this.token.address, loanAmount, '0x'), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [this.token.address, 0, loanAmount], ); }); it('unavailable funds', async function () { const receiver = await ERC3156FlashBorrowerMock.new(true, true); const data = this.token.contract.methods.transfer(other, 10).encodeABI(); - await expectRevert( + await expectRevertCustomError( this.token.flashLoan(receiver.address, this.token.address, loanAmount, data), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [receiver.address, loanAmount - 10, loanAmount], ); }); diff --git a/test/token/ERC20/extensions/ERC20Pausable.test.js b/test/token/ERC20/extensions/ERC20Pausable.test.js index ead442b99..72bfc7769 100644 --- a/test/token/ERC20/extensions/ERC20Pausable.test.js +++ b/test/token/ERC20/extensions/ERC20Pausable.test.js @@ -1,6 +1,7 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC20Pausable = artifacts.require('$ERC20Pausable'); @@ -39,9 +40,10 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to transfer when paused', async function () { await this.token.$_pause(); - await expectRevert( + await expectRevertCustomError( this.token.transfer(recipient, initialSupply, { from: holder }), - 'ERC20Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); }); @@ -73,9 +75,10 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to transfer from when paused', async function () { await this.token.$_pause(); - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(holder, recipient, allowance, { from: anotherAccount }), - 'ERC20Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); }); @@ -101,7 +104,7 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to mint when paused', async function () { await this.token.$_pause(); - await expectRevert(this.token.$_mint(recipient, amount), 'ERC20Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_mint(recipient, amount), 'EnforcedPause', []); }); }); @@ -126,7 +129,7 @@ contract('ERC20Pausable', function (accounts) { it('reverts when trying to burn when paused', async function () { await this.token.$_pause(); - await expectRevert(this.token.$_burn(holder, amount), 'ERC20Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_burn(holder, amount), 'EnforcedPause', []); }); }); }); diff --git a/test/token/ERC20/extensions/ERC20Permit.test.js b/test/token/ERC20/extensions/ERC20Permit.test.js new file mode 100644 index 000000000..388716d53 --- /dev/null +++ b/test/token/ERC20/extensions/ERC20Permit.test.js @@ -0,0 +1,118 @@ +/* eslint-disable */ + +const { BN, constants, time } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); +const { MAX_UINT256 } = constants; + +const { fromRpcSig } = require('ethereumjs-util'); +const ethSigUtil = require('eth-sig-util'); +const Wallet = require('ethereumjs-wallet').default; + +const ERC20Permit = artifacts.require('$ERC20Permit'); + +const { Permit, getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); +const { getChainId } = require('../../../helpers/chainid'); +const { expectRevertCustomError } = require('../../../helpers/customError'); + +contract('ERC20Permit', function (accounts) { + const [initialHolder, spender] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + + const initialSupply = new BN(100); + + beforeEach(async function () { + this.chainId = await getChainId(); + + this.token = await ERC20Permit.new(name, symbol, name); + await this.token.$_mint(initialHolder, initialSupply); + }); + + it('initial nonce is 0', async function () { + expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); + }); + + it('domain separator', async function () { + expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator)); + }); + + describe('permit', function () { + const wallet = Wallet.generate(); + + const owner = wallet.getAddressString(); + const value = new BN(42); + const nonce = 0; + const maxDeadline = MAX_UINT256; + + const buildData = (contract, deadline = maxDeadline) => + getDomain(contract).then(domain => ({ + primaryType: 'Permit', + types: { EIP712Domain: domainType(domain), Permit }, + domain, + message: { owner, spender, value, nonce, deadline }, + })); + + it('accepts owner signature', async function () { + const { v, r, s } = await buildData(this.token) + .then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data })) + .then(fromRpcSig); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); + expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value); + }); + + it('rejects reused signature', async function () { + const sig = await buildData(this.token).then(data => + ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }), + ); + const { r, s, v } = fromRpcSig(sig); + + await this.token.permit(owner, spender, value, maxDeadline, v, r, s); + + const domain = await getDomain(this.token); + const typedMessage = { + primaryType: 'Permit', + types: { EIP712Domain: domainType(domain), Permit }, + domain, + message: { owner, spender, value, nonce: nonce + 1, deadline: maxDeadline }, + }; + + await expectRevertCustomError( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC2612InvalidSigner', + [ethSigUtil.recoverTypedSignature({ data: typedMessage, sig }), owner], + ); + }); + + it('rejects other signature', async function () { + const otherWallet = Wallet.generate(); + + const { v, r, s } = await buildData(this.token) + .then(data => ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data })) + .then(fromRpcSig); + + await expectRevertCustomError( + this.token.permit(owner, spender, value, maxDeadline, v, r, s), + 'ERC2612InvalidSigner', + [await otherWallet.getAddressString(), owner], + ); + }); + + it('rejects expired permit', async function () { + const deadline = (await time.latest()) - time.duration.weeks(1); + + const { v, r, s } = await buildData(this.token, deadline) + .then(data => ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data })) + .then(fromRpcSig); + + await expectRevertCustomError( + this.token.permit(owner, spender, value, deadline, v, r, s), + 'ERC2612ExpiredSignature', + [deadline], + ); + }); + }); +}); diff --git a/test/token/ERC20/extensions/ERC20Snapshot.test.js b/test/token/ERC20/extensions/ERC20Snapshot.test.js deleted file mode 100644 index fb0bb31d3..000000000 --- a/test/token/ERC20/extensions/ERC20Snapshot.test.js +++ /dev/null @@ -1,207 +0,0 @@ -const { BN, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const ERC20Snapshot = artifacts.require('$ERC20Snapshot'); - -const { expect } = require('chai'); - -contract('ERC20Snapshot', function (accounts) { - const [initialHolder, recipient, other] = accounts; - - const initialSupply = new BN(100); - - const name = 'My Token'; - const symbol = 'MTKN'; - - beforeEach(async function () { - this.token = await ERC20Snapshot.new(name, symbol); - await this.token.$_mint(initialHolder, initialSupply); - }); - - describe('snapshot', function () { - it('emits a snapshot event', async function () { - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot'); - }); - - it('creates increasing snapshots ids, starting from 1', async function () { - for (const id of ['1', '2', '3', '4', '5']) { - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id }); - } - }); - }); - - describe('totalSupplyAt', function () { - it('reverts with a snapshot id of 0', async function () { - await expectRevert(this.token.totalSupplyAt(0), 'ERC20Snapshot: id is 0'); - }); - - it('reverts with a not-yet-created snapshot id', async function () { - await expectRevert(this.token.totalSupplyAt(1), 'ERC20Snapshot: nonexistent id'); - }); - - context('with initial snapshot', function () { - beforeEach(async function () { - this.initialSnapshotId = new BN('1'); - - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id: this.initialSnapshotId }); - }); - - context('with no supply changes after the snapshot', function () { - it('returns the current total supply', async function () { - expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply); - }); - }); - - context('with supply changes after the snapshot', function () { - beforeEach(async function () { - await this.token.$_mint(other, new BN('50')); - await this.token.$_burn(initialHolder, new BN('20')); - }); - - it('returns the total supply before the changes', async function () { - expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply); - }); - - context('with a second snapshot after supply changes', function () { - beforeEach(async function () { - this.secondSnapshotId = new BN('2'); - - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id: this.secondSnapshotId }); - }); - - it('snapshots return the supply before and after the changes', async function () { - expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply); - - expect(await this.token.totalSupplyAt(this.secondSnapshotId)).to.be.bignumber.equal( - await this.token.totalSupply(), - ); - }); - }); - - context('with multiple snapshots after supply changes', function () { - beforeEach(async function () { - this.secondSnapshotIds = ['2', '3', '4']; - - for (const id of this.secondSnapshotIds) { - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id }); - } - }); - - it('all posterior snapshots return the supply after the changes', async function () { - expect(await this.token.totalSupplyAt(this.initialSnapshotId)).to.be.bignumber.equal(initialSupply); - - const currentSupply = await this.token.totalSupply(); - - for (const id of this.secondSnapshotIds) { - expect(await this.token.totalSupplyAt(id)).to.be.bignumber.equal(currentSupply); - } - }); - }); - }); - }); - }); - - describe('balanceOfAt', function () { - it('reverts with a snapshot id of 0', async function () { - await expectRevert(this.token.balanceOfAt(other, 0), 'ERC20Snapshot: id is 0'); - }); - - it('reverts with a not-yet-created snapshot id', async function () { - await expectRevert(this.token.balanceOfAt(other, 1), 'ERC20Snapshot: nonexistent id'); - }); - - context('with initial snapshot', function () { - beforeEach(async function () { - this.initialSnapshotId = new BN('1'); - - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id: this.initialSnapshotId }); - }); - - context('with no balance changes after the snapshot', function () { - it('returns the current balance for all accounts', async function () { - expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal( - initialSupply, - ); - expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0'); - }); - }); - - context('with balance changes after the snapshot', function () { - beforeEach(async function () { - await this.token.transfer(recipient, new BN('10'), { from: initialHolder }); - await this.token.$_mint(other, new BN('50')); - await this.token.$_burn(initialHolder, new BN('20')); - }); - - it('returns the balances before the changes', async function () { - expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal( - initialSupply, - ); - expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0'); - }); - - context('with a second snapshot after supply changes', function () { - beforeEach(async function () { - this.secondSnapshotId = new BN('2'); - - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id: this.secondSnapshotId }); - }); - - it('snapshots return the balances before and after the changes', async function () { - expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal( - initialSupply, - ); - expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0'); - - expect(await this.token.balanceOfAt(initialHolder, this.secondSnapshotId)).to.be.bignumber.equal( - await this.token.balanceOf(initialHolder), - ); - expect(await this.token.balanceOfAt(recipient, this.secondSnapshotId)).to.be.bignumber.equal( - await this.token.balanceOf(recipient), - ); - expect(await this.token.balanceOfAt(other, this.secondSnapshotId)).to.be.bignumber.equal( - await this.token.balanceOf(other), - ); - }); - }); - - context('with multiple snapshots after supply changes', function () { - beforeEach(async function () { - this.secondSnapshotIds = ['2', '3', '4']; - - for (const id of this.secondSnapshotIds) { - const receipt = await this.token.$_snapshot(); - expectEvent(receipt, 'Snapshot', { id }); - } - }); - - it('all posterior snapshots return the supply after the changes', async function () { - expect(await this.token.balanceOfAt(initialHolder, this.initialSnapshotId)).to.be.bignumber.equal( - initialSupply, - ); - expect(await this.token.balanceOfAt(recipient, this.initialSnapshotId)).to.be.bignumber.equal('0'); - expect(await this.token.balanceOfAt(other, this.initialSnapshotId)).to.be.bignumber.equal('0'); - - for (const id of this.secondSnapshotIds) { - expect(await this.token.balanceOfAt(initialHolder, id)).to.be.bignumber.equal( - await this.token.balanceOf(initialHolder), - ); - expect(await this.token.balanceOfAt(recipient, id)).to.be.bignumber.equal( - await this.token.balanceOf(recipient), - ); - expect(await this.token.balanceOfAt(other, id)).to.be.bignumber.equal(await this.token.balanceOf(other)); - } - }); - }); - }); - }); - }); -}); diff --git a/test/token/ERC20/extensions/ERC20Votes.test.js b/test/token/ERC20/extensions/ERC20Votes.test.js index 8d010ec6b..714a98adc 100644 --- a/test/token/ERC20/extensions/ERC20Votes.test.js +++ b/test/token/ERC20/extensions/ERC20Votes.test.js @@ -1,6 +1,6 @@ /* eslint-disable */ -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { MAX_UINT256, ZERO_ADDRESS } = constants; @@ -9,11 +9,10 @@ const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const ERC20Votes = artifacts.require('$ERC20Votes'); - const { batchInBlock } = require('../../../helpers/txpool'); -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); -const { getChainId } = require('../../../helpers/chainid'); +const { getDomain, domainType, domainSeparator } = require('../../../helpers/eip712'); +const { clock, clockFromReceipt } = require('../../../helpers/time'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const Delegation = [ { name: 'delegatee', type: 'address' }, @@ -21,6 +20,11 @@ const Delegation = [ { name: 'expiry', type: 'uint256' }, ]; +const MODES = { + blocknumber: artifacts.require('$ERC20Votes'), + timestamp: artifacts.require('$ERC20VotesTimestampMock'), +}; + contract('ERC20Votes', function (accounts) { const [holder, recipient, holderDelegatee, other1, other2] = accounts; @@ -29,557 +33,561 @@ contract('ERC20Votes', function (accounts) { const version = '1'; const supply = new BN('10000000000000000000000000'); - beforeEach(async function () { - this.chainId = await getChainId(); - this.token = await ERC20Votes.new(name, symbol, name, version); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect(await this.token.DOMAIN_SEPARATOR()).to.equal( - await domainSeparator({ name, version, chainId: this.chainId, verifyingContract: this.token.address }), - ); - }); - - it('minting restriction', async function () { - const amount = new BN('2').pow(new BN('224')); - await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes'); - }); - - it('recent checkpoints', async function () { - await this.token.delegate(holder, { from: holder }); - for (let i = 0; i < 6; i++) { - await this.token.$_mint(holder, 1); - } - const block = await web3.eth.getBlockNumber(); - // recent - expect(await this.token.getPastVotes(holder, block - 1)).to.be.bignumber.equal('5'); - // non-recent - expect(await this.token.getPastVotes(holder, block - 6)).to.be.bignumber.equal('0'); - }); - - describe('set delegation', function () { - describe('call', function () { - it('delegation with balance', async function () { - await this.token.$_mint(holder, supply); - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '0', - newBalance: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - - it('delegation without balance', async function () { - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - }); - }); - - describe('with signature', function () { - const delegator = Wallet.generate(); - const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); - const nonce = 0; - - const buildData = (chainId, verifyingContract, message) => ({ - data: { - primaryType: 'Delegation', - types: { EIP712Domain, Delegation }, - domain: { name, version, chainId, verifyingContract }, - message, - }, - }); - + for (const [mode, artifact] of Object.entries(MODES)) { + describe(`vote with ${mode}`, function () { beforeEach(async function () { - await this.token.$_mint(delegatorAddress, supply); + this.token = await artifact.new(name, symbol, name, version); + this.votes = this.token; }); - it('accept signed delegation', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); + // includes EIP6372 behavior check + shouldBehaveLikeVotes(accounts, [1, 17, 42], { mode, fungible: true }); - expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); + it('initial nonce is 0', async function () { + expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); + }); - const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - expectEvent(receipt, 'DelegateChanged', { - delegator: delegatorAddress, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatorAddress, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatorAddress, - previousBalance: '0', - newBalance: supply, + it('minting restriction', async function () { + const amount = new BN('2').pow(new BN('224')); + await expectRevertCustomError(this.token.$_mint(holder, amount), 'ERC20ExceededSafeSupply', [ + amount, + amount.subn(1), + ]); + }); + + it('recent checkpoints', async function () { + await this.token.delegate(holder, { from: holder }); + for (let i = 0; i < 6; i++) { + await this.token.$_mint(holder, 1); + } + const timepoint = await clock[mode](); + expect(await this.token.numCheckpoints(holder)).to.be.bignumber.equal('6'); + // recent + expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('5'); + // non-recent + expect(await this.token.getPastVotes(holder, timepoint - 6)).to.be.bignumber.equal('0'); + }); + + describe('set delegation', function () { + describe('call', function () { + it('delegation with balance', async function () { + await this.token.$_mint(holder, supply); + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); + + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(supply); + }); + + it('delegation without balance', async function () { + expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); + + const { receipt } = await this.token.delegate(holder, { from: holder }); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: ZERO_ADDRESS, + toDelegate: holder, + }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + expect(await this.token.delegates(holder)).to.be.equal(holder); + }); }); - expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + describe('with signature', function () { + const delegator = Wallet.generate(); + const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); + const nonce = 0; - expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); + const buildData = (contract, message) => + getDomain(contract).then(domain => ({ + primaryType: 'Delegation', + types: { EIP712Domain: domainType(domain), Delegation }, + domain, + message, + })); - it('rejects reused signature', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { + beforeEach(async function () { + await this.token.$_mint(delegatorAddress, supply); + }); + + it('accept signed delegation', async function () { + const { v, r, s } = await buildData(this.token, { delegatee: delegatorAddress, nonce, expiry: MAX_UINT256, - }), - ), - ); + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', - ); - }); + const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); + const timepoint = await clockFromReceipt[mode](receipt); - it('rejects bad delegatee', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { + expectEvent(receipt, 'DelegateChanged', { + delegator: delegatorAddress, + fromDelegate: ZERO_ADDRESS, + toDelegate: delegatorAddress, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: delegatorAddress, + previousBalance: '0', + newBalance: supply, + }); + + expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); + + expect(await this.token.getVotes(delegatorAddress)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(delegatorAddress, timepoint - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(delegatorAddress, timepoint)).to.be.bignumber.equal(supply); + }); + + it('rejects reused signature', async function () { + const { v, r, s } = await buildData(this.token, { delegatee: delegatorAddress, nonce, expiry: MAX_UINT256, - }), - ), - ); + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); - const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegatorAddress); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(holderDelegatee); - }); + await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - it('rejects bad nonce', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { + await expectRevertCustomError( + this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), + 'InvalidAccountNonce', + [delegatorAddress, nonce + 1], + ); + }); + + it('rejects bad delegatee', async function () { + const { v, r, s } = await buildData(this.token, { delegatee: delegatorAddress, nonce, expiry: MAX_UINT256, - }), - ), - ); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', - ); - }); + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { + const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); + const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); + expect(args.delegator).to.not.be.equal(delegatorAddress); + expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); + expect(args.toDelegate).to.be.equal(holderDelegatee); + }); + + it('rejects bad nonce', async function () { + const sig = await buildData(this.token, { + delegatee: delegatorAddress, + nonce, + expiry: MAX_UINT256, + }).then(data => ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data })); + const { r, s, v } = fromRpcSig(sig); + + const domain = await getDomain(this.token); + const typedMessage = { + primaryType: 'Delegation', + types: { EIP712Domain: domainType(domain), Delegation }, + domain, + message: { delegatee: delegatorAddress, nonce: nonce + 1, expiry: MAX_UINT256 }, + }; + + await expectRevertCustomError( + this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), + 'InvalidAccountNonce', + [ethSigUtil.recoverTypedSignature({ data: typedMessage, sig }), nonce], + ); + }); + + it('rejects expired permit', async function () { + const expiry = (await time.latest()) - time.duration.weeks(1); + const { v, r, s } = await buildData(this.token, { delegatee: delegatorAddress, nonce, expiry, - }), - ), - ); + }).then(data => fromRpcSig(ethSigUtil.signTypedMessage(delegator.getPrivateKey(), { data }))); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'Votes: signature expired', - ); - }); - }); - }); - - describe('change delegation', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - await this.token.delegate(holder, { from: holder }); - }); - - it('call', async function () { - expect(await this.token.delegates(holder)).to.be.equal(holder); - - const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: holder, - toDelegate: holderDelegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holderDelegatee, - previousBalance: '0', - newBalance: supply, + await expectRevertCustomError( + this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), + 'VotesExpiredSignature', + [expiry], + ); + }); + }); }); - expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); + describe('change delegation', function () { + beforeEach(async function () { + await this.token.$_mint(holder, supply); + await this.token.delegate(holder, { from: holder }); + }); - expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - }); + it('call', async function () { + expect(await this.token.delegates(holder)).to.be.equal(holder); - describe('transfers', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - }); + const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); - it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + expectEvent(receipt, 'DelegateChanged', { + delegator: holder, + fromDelegate: holder, + toDelegate: holderDelegatee, + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: '0', + }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holderDelegatee, + previousBalance: '0', + newBalance: supply, + }); - this.holderVotes = '0'; - this.recipientVotes = '0'; - }); + expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); - it('sender delegation', async function () { - await this.token.delegate(holder, { from: holder }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply.subn(1), + expect(await this.token.getVotes(holder)).to.be.bignumber.equal('0'); + expect(await this.token.getVotes(holderDelegatee)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holder, timepoint - 1)).to.be.bignumber.equal(supply); + expect(await this.token.getPastVotes(holderDelegatee, timepoint - 1)).to.be.bignumber.equal('0'); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(holderDelegatee, timepoint)).to.be.bignumber.equal(supply); + }); }); - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); + describe('transfers', function () { + beforeEach(async function () { + await this.token.$_mint(holder, supply); + }); - this.holderVotes = supply.subn(1); - this.recipientVotes = '0'; - }); + it('no delegation', async function () { + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - it('receiver delegation', async function () { - await this.token.delegate(recipient, { from: recipient }); + this.holderVotes = '0'; + this.recipientVotes = '0'; + }); - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); + it('sender delegation', async function () { + await this.token.delegate(holder, { from: holder }); - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: supply.subn(1), + }); - this.holderVotes = '0'; - this.recipientVotes = '1'; - }); + const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); + expect( + receipt.logs + .filter(({ event }) => event == 'DelegateVotesChanged') + .every(({ logIndex }) => transferLogIndex < logIndex), + ).to.be.equal(true); - it('full delegation', async function () { - await this.token.delegate(holder, { from: holder }); - await this.token.delegate(recipient, { from: recipient }); + this.holderVotes = supply.subn(1); + this.recipientVotes = '0'; + }); - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply.subn(1), - }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); + it('receiver delegation', async function () { + await this.token.delegate(recipient, { from: recipient }); - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); - this.holderVotes = supply.subn(1); - this.recipientVotes = '1'; - }); + const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); + expect( + receipt.logs + .filter(({ event }) => event == 'DelegateVotesChanged') + .every(({ logIndex }) => transferLogIndex < logIndex), + ).to.be.equal(true); - afterEach(async function () { - expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); + this.holderVotes = '0'; + this.recipientVotes = '1'; + }); - // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" - const blockNumber = await time.latestBlock(); - await time.advanceBlock(); - expect(await this.token.getPastVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getPastVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); - }); - }); + it('full delegation', async function () { + await this.token.delegate(holder, { from: holder }); + await this.token.delegate(recipient, { from: recipient }); - // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. - describe('Compound test suite', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - }); + const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); + expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); + expectEvent(receipt, 'DelegateVotesChanged', { + delegate: holder, + previousBalance: supply, + newBalance: supply.subn(1), + }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); - describe('numCheckpoints', function () { - it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); + expect( + receipt.logs + .filter(({ event }) => event == 'DelegateVotesChanged') + .every(({ logIndex }) => transferLogIndex < logIndex), + ).to.be.equal(true); - const t1 = await this.token.delegate(other1, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + this.holderVotes = supply.subn(1); + this.recipientVotes = '1'; + }); - const t2 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + afterEach(async function () { + expect(await this.token.getVotes(holder)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); - const t3 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '100']); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.receipt.blockNumber.toString(), '90']); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.receipt.blockNumber.toString(), '80']); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']); - - await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const timepoint = await clock[mode](); + await time.advanceBlock(); + expect(await this.token.getPastVotes(holder, timepoint)).to.be.bignumber.equal(this.holderVotes); + expect(await this.token.getPastVotes(recipient, timepoint)).to.be.bignumber.equal(this.recipientVotes); + }); }); - it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. + describe('Compound test suite', function () { + beforeEach(async function () { + await this.token.$_mint(holder, supply); + }); - const [t1, t2, t3] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - ]); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '80']); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check + describe('balanceOf', function () { + it('grants to initial account', async function () { + expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); + }); + }); - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']); - }); - }); + describe('numCheckpoints', function () { + it('returns the number of checkpoints for a delegate', async function () { + await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - describe('balanceOf', function () { - it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); - }); - }); + const t1 = await this.token.delegate(other1, { from: recipient }); + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - describe('getPastVotes', function () { - it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPastVotes(other1, 5e10), 'Checkpoints: block not yet mined'); + const t2 = await this.token.transfer(other2, 10, { from: recipient }); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + + const t3 = await this.token.transfer(other2, 10, { from: recipient }); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); + + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '100']); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.timepoint.toString(), '90']); + expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.timepoint.toString(), '80']); + expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.timepoint.toString(), '100']); + + await time.advanceBlock(); + expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal('100'); + expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal('90'); + expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal('80'); + expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal('100'); + }); + + it('does not add more than one checkpoint in a block', async function () { + await this.token.transfer(recipient, '100', { from: holder }); + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); + + const [t1, t2, t3] = await batchInBlock([ + () => this.token.delegate(other1, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 200000 }), + () => this.token.transfer(other2, 10, { from: recipient, gas: 200000 }), + ]); + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); + expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.timepoint.toString(), '80']); + + const t4 = await this.token.transfer(recipient, 20, { from: holder }); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + + expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); + expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.timepoint.toString(), '100']); + }); + }); + + describe('getPastVotes', function () { + it('reverts if block number >= current block', async function () { + const clock = await this.token.clock(); + await expectRevertCustomError(this.token.getPastVotes(other1, 5e10), 'ERC5805FutureLookup', [5e10, clock]); + }); + + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); + }); + + it('returns the latest block if >= last checkpoint block', async function () { + const { receipt } = await this.token.delegate(other1, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, timepoint)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const { receipt } = await this.token.delegate(other1, { from: holder }); + const timepoint = await clockFromReceipt[mode](receipt); + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.token.getPastVotes(other1, timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.delegate(other1, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.transfer(other2, 10, { from: holder }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.transfer(holder, 20, { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + + expect(await this.token.getPastVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPastVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal( + '9999999999999999999999990', + ); + expect(await this.token.getPastVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999990', + ); + expect(await this.token.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal( + '9999999999999999999999980', + ); + expect(await this.token.getPastVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999980', + ); + expect(await this.token.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPastVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + }); }); - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastVotes(other1, 0)).to.be.bignumber.equal('0'); - }); + describe('getPastTotalSupply', function () { + beforeEach(async function () { + await this.token.delegate(holder, { from: holder }); + }); - it('returns the latest block if >= last checkpoint block', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); + it('reverts if block number >= current block', async function () { + const clock = await this.token.clock(); + await expectRevertCustomError(this.token.getPastTotalSupply(5e10), 'ERC5805FutureLookup', [5e10, clock]); + }); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); + it('returns 0 if there are no checkpoints', async function () { + expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); + }); - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); + it('returns the latest block if >= last checkpoint block', async function () { + const { receipt } = await this.token.$_mint(holder, supply); + const timepoint = await clockFromReceipt[mode](receipt); + await time.advanceBlock(); + await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); + expect(await this.token.getPastTotalSupply(timepoint)).to.be.bignumber.equal(supply); + expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal(supply); + }); - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); + it('returns zero if < first checkpoint block', async function () { + await time.advanceBlock(); + const { receipt } = await this.token.$_mint(holder, supply); + const timepoint = await clockFromReceipt[mode](receipt); + await time.advanceBlock(); + await time.advanceBlock(); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); + expect(await this.token.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + const t1 = await this.token.$_mint(holder, supply); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.token.$_burn(holder, 10); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.token.$_burn(holder, 10); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.token.$_mint(holder, 20); + await time.advanceBlock(); + await time.advanceBlock(); + + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + + expect(await this.token.getPastTotalSupply(t1.timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.token.getPastTotalSupply(t1.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t1.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + expect(await this.token.getPastTotalSupply(t2.timepoint)).to.be.bignumber.equal('9999999999999999999999990'); + expect(await this.token.getPastTotalSupply(t2.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999990', + ); + expect(await this.token.getPastTotalSupply(t3.timepoint)).to.be.bignumber.equal('9999999999999999999999980'); + expect(await this.token.getPastTotalSupply(t3.timepoint + 1)).to.be.bignumber.equal( + '9999999999999999999999980', + ); + expect(await this.token.getPastTotalSupply(t4.timepoint)).to.be.bignumber.equal('10000000000000000000000000'); + expect(await this.token.getPastTotalSupply(t4.timepoint + 1)).to.be.bignumber.equal( + '10000000000000000000000000', + ); + }); }); }); - }); - - describe('getPastTotalSupply', function () { - beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); - }); - - it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPastTotalSupply(5e10), 'Checkpoints: block not yet mined'); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.$_mint(holder, supply); - - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.$_mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.$_mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.$_mint(holder, 20); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - }); - - describe('Voting workflow', function () { - beforeEach(async function () { - this.name = name; - this.votes = this.token; - }); - - shouldBehaveLikeVotes(accounts, [1, 17, 42]); - }); + } }); diff --git a/test/token/ERC20/extensions/ERC20VotesComp.test.js b/test/token/ERC20/extensions/ERC20VotesComp.test.js deleted file mode 100644 index ef4a22be2..000000000 --- a/test/token/ERC20/extensions/ERC20VotesComp.test.js +++ /dev/null @@ -1,552 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256, ZERO_ADDRESS } = constants; - -const { batchInBlock } = require('../../../helpers/txpool'); -const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior'); -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC20VotesComp = artifacts.require('$ERC20VotesComp'); - -const { EIP712Domain, domainSeparator } = require('../../../helpers/eip712'); -const { getChainId } = require('../../../helpers/chainid'); - -const Delegation = [ - { name: 'delegatee', type: 'address' }, - { name: 'nonce', type: 'uint256' }, - { name: 'expiry', type: 'uint256' }, -]; - -contract('ERC20VotesComp', function (accounts) { - const [holder, recipient, holderDelegatee, other1, other2] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - const supply = new BN('10000000000000000000000000'); - - beforeEach(async function () { - this.chainId = await getChainId(); - this.token = await ERC20VotesComp.new(name, symbol, name, version); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(holder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect(await this.token.DOMAIN_SEPARATOR()).to.equal( - await domainSeparator({ name, version, chainId: this.chainId, verifyingContract: this.token.address }), - ); - }); - - it('minting restriction', async function () { - const amount = new BN('2').pow(new BN('96')); - await expectRevert(this.token.$_mint(holder, amount), 'ERC20Votes: total supply risks overflowing votes'); - }); - - describe('set delegation', function () { - describe('call', function () { - it('delegation with balance', async function () { - await this.token.$_mint(holder, supply); - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: '0', - newBalance: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - - expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(holder, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - - it('delegation without balance', async function () { - expect(await this.token.delegates(holder)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegate(holder, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: ZERO_ADDRESS, - toDelegate: holder, - }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - expect(await this.token.delegates(holder)).to.be.equal(holder); - }); - }); - - describe('with signature', function () { - const delegator = Wallet.generate(); - const delegatorAddress = web3.utils.toChecksumAddress(delegator.getAddressString()); - const nonce = 0; - - const buildData = (chainId, verifyingContract, message) => ({ - data: { - primaryType: 'Delegation', - types: { EIP712Domain, Delegation }, - domain: { name, version, chainId, verifyingContract }, - message, - }, - }); - - beforeEach(async function () { - await this.token.$_mint(delegatorAddress, supply); - }); - - it('accept signed delegation', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(ZERO_ADDRESS); - - const { receipt } = await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - expectEvent(receipt, 'DelegateChanged', { - delegator: delegatorAddress, - fromDelegate: ZERO_ADDRESS, - toDelegate: delegatorAddress, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: delegatorAddress, - previousBalance: '0', - newBalance: supply, - }); - - expect(await this.token.delegates(delegatorAddress)).to.be.equal(delegatorAddress); - - expect(await this.token.getCurrentVotes(delegatorAddress)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(delegatorAddress, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(delegatorAddress, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - - it('rejects reused signature', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); - - await this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s); - - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', - ); - }); - - it('rejects bad delegatee', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); - - const receipt = await this.token.delegateBySig(holderDelegatee, nonce, MAX_UINT256, v, r, s); - const { args } = receipt.logs.find(({ event }) => event == 'DelegateChanged'); - expect(args.delegator).to.not.be.equal(delegatorAddress); - expect(args.fromDelegate).to.be.equal(ZERO_ADDRESS); - expect(args.toDelegate).to.be.equal(holderDelegatee); - }); - - it('rejects bad nonce', async function () { - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry: MAX_UINT256, - }), - ), - ); - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce + 1, MAX_UINT256, v, r, s), - 'Votes: invalid nonce', - ); - }); - - it('rejects expired permit', async function () { - const expiry = (await time.latest()) - time.duration.weeks(1); - const { v, r, s } = fromRpcSig( - ethSigUtil.signTypedMessage( - delegator.getPrivateKey(), - buildData(this.chainId, this.token.address, { - delegatee: delegatorAddress, - nonce, - expiry, - }), - ), - ); - - await expectRevert( - this.token.delegateBySig(delegatorAddress, nonce, expiry, v, r, s), - 'Votes: signature expired', - ); - }); - }); - }); - - describe('change delegation', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - await this.token.delegate(holder, { from: holder }); - }); - - it('call', async function () { - expect(await this.token.delegates(holder)).to.be.equal(holder); - - const { receipt } = await this.token.delegate(holderDelegatee, { from: holder }); - expectEvent(receipt, 'DelegateChanged', { - delegator: holder, - fromDelegate: holder, - toDelegate: holderDelegatee, - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: '0', - }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holderDelegatee, - previousBalance: '0', - newBalance: supply, - }); - - expect(await this.token.delegates(holder)).to.be.equal(holderDelegatee); - - expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal('0'); - expect(await this.token.getCurrentVotes(holderDelegatee)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(holder, receipt.blockNumber - 1)).to.be.bignumber.equal(supply); - expect(await this.token.getPriorVotes(holderDelegatee, receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(holder, receipt.blockNumber)).to.be.bignumber.equal('0'); - expect(await this.token.getPriorVotes(holderDelegatee, receipt.blockNumber)).to.be.bignumber.equal(supply); - }); - }); - - describe('transfers', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - }); - - it('no delegation', async function () { - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - this.holderVotes = '0'; - this.recipientVotes = '0'; - }); - - it('sender delegation', async function () { - await this.token.delegate(holder, { from: holder }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply.subn(1), - }); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '0'; - }); - - it('receiver delegation', async function () { - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); - - this.holderVotes = '0'; - this.recipientVotes = '1'; - }); - - it('full delegation', async function () { - await this.token.delegate(holder, { from: holder }); - await this.token.delegate(recipient, { from: recipient }); - - const { receipt } = await this.token.transfer(recipient, 1, { from: holder }); - expectEvent(receipt, 'Transfer', { from: holder, to: recipient, value: '1' }); - expectEvent(receipt, 'DelegateVotesChanged', { - delegate: holder, - previousBalance: supply, - newBalance: supply.subn(1), - }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: recipient, previousBalance: '0', newBalance: '1' }); - - this.holderVotes = supply.subn(1); - this.recipientVotes = '1'; - }); - - afterEach(async function () { - expect(await this.token.getCurrentVotes(holder)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getCurrentVotes(recipient)).to.be.bignumber.equal(this.recipientVotes); - - // need to advance 2 blocks to see the effect of a transfer on "getPriorVotes" - const blockNumber = await time.latestBlock(); - await time.advanceBlock(); - expect(await this.token.getPriorVotes(holder, blockNumber)).to.be.bignumber.equal(this.holderVotes); - expect(await this.token.getPriorVotes(recipient, blockNumber)).to.be.bignumber.equal(this.recipientVotes); - }); - }); - - // The following tests are a adaptation of https://github.com/compound-finance/compound-protocol/blob/master/tests/Governance/CompTest.js. - describe('Compound test suite', function () { - beforeEach(async function () { - await this.token.$_mint(holder, supply); - }); - - describe('balanceOf', function () { - it('grants to initial account', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('10000000000000000000000000'); - }); - }); - - describe('numCheckpoints', function () { - it('returns the number of checkpoints for a delegate', async function () { - await this.token.transfer(recipient, '100', { from: holder }); //give an account a few tokens for readability - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const t1 = await this.token.delegate(other1, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - - const t2 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - - const t3 = await this.token.transfer(other2, 10, { from: recipient }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('3'); - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('4'); - - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '100']); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t2.receipt.blockNumber.toString(), '90']); - expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([t3.receipt.blockNumber.toString(), '80']); - expect(await this.token.checkpoints(other1, 3)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']); - - await time.advanceBlock(); - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal('100'); - expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('90'); - expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('80'); - expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('100'); - }); - - it('does not add more than one checkpoint in a block', async function () { - await this.token.transfer(recipient, '100', { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('0'); - - const [t1, t2, t3] = await batchInBlock([ - () => this.token.delegate(other1, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - () => this.token.transfer(other2, 10, { from: recipient, gas: 100000 }), - ]); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('1'); - expect(await this.token.checkpoints(other1, 0)).to.be.deep.equal([t1.receipt.blockNumber.toString(), '80']); - // expectReve(await this.token.checkpoints(other1, 1)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - // expect(await this.token.checkpoints(other1, 2)).to.be.deep.equal([ '0', '0' ]); // Reverts due to array overflow check - - const t4 = await this.token.transfer(recipient, 20, { from: holder }); - expect(await this.token.numCheckpoints(other1)).to.be.bignumber.equal('2'); - expect(await this.token.checkpoints(other1, 1)).to.be.deep.equal([t4.receipt.blockNumber.toString(), '100']); - }); - }); - - describe('getPriorVotes', function () { - it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPriorVotes(other1, 5e10), 'Checkpoints: block not yet mined'); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPriorVotes(other1, 0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.delegate(other1, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.transfer(other2, 10, { from: holder }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.transfer(holder, 20, { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPriorVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPriorVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPriorVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - }); - }); - - describe('getPastTotalSupply', function () { - beforeEach(async function () { - await this.token.delegate(holder, { from: holder }); - }); - - it('reverts if block number >= current block', async function () { - await expectRevert(this.token.getPastTotalSupply(5e10), 'Checkpoints: block not yet mined'); - }); - - it('returns 0 if there are no checkpoints', async function () { - expect(await this.token.getPastTotalSupply(0)).to.be.bignumber.equal('0'); - }); - - it('returns the latest block if >= last checkpoint block', async function () { - t1 = await this.token.$_mint(holder, supply); - - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal(supply); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal(supply); - }); - - it('returns zero if < first checkpoint block', async function () { - await time.advanceBlock(); - const t1 = await this.token.$_mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - const t1 = await this.token.$_mint(holder, supply); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.token.$_burn(holder, 10); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.token.$_mint(holder, 20); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t1.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastTotalSupply(t2.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999990', - ); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastTotalSupply(t3.receipt.blockNumber + 1)).to.be.bignumber.equal( - '9999999999999999999999980', - ); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - expect(await this.token.getPastTotalSupply(t4.receipt.blockNumber + 1)).to.be.bignumber.equal( - '10000000000000000000000000', - ); - }); - }); - - describe('Voting workflow', function () { - beforeEach(async function () { - this.name = name; - this.votes = this.token; - }); - - shouldBehaveLikeVotes(accounts, [1, 17, 42]); - }); -}); diff --git a/test/token/ERC20/extensions/ERC20Wrapper.test.js b/test/token/ERC20/extensions/ERC20Wrapper.test.js index 774a9cbda..94415d088 100644 --- a/test/token/ERC20/extensions/ERC20Wrapper.test.js +++ b/test/token/ERC20/extensions/ERC20Wrapper.test.js @@ -1,15 +1,16 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { ZERO_ADDRESS, MAX_UINT256 } = constants; const { shouldBehaveLikeERC20 } = require('../ERC20.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const NotAnERC20 = artifacts.require('CallReceiverMock'); const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); const ERC20Wrapper = artifacts.require('$ERC20Wrapper'); -contract('ERC20', function (accounts) { - const [initialHolder, recipient, anotherAccount] = accounts; +contract('ERC20Wrapper', function (accounts) { + const [initialHolder, receiver] = accounts; const name = 'My Token'; const symbol = 'MTKN'; @@ -66,23 +67,25 @@ contract('ERC20', function (accounts) { }); it('missing approval', async function () { - await expectRevert( + await expectRevertCustomError( this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }), - 'ERC20: insufficient allowance', + 'ERC20InsufficientAllowance', + [this.token.address, 0, initialSupply], ); }); it('missing balance', async function () { await this.underlying.approve(this.token.address, MAX_UINT256, { from: initialHolder }); - await expectRevert( + await expectRevertCustomError( this.token.depositFor(initialHolder, MAX_UINT256, { from: initialHolder }), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [initialHolder, initialSupply, MAX_UINT256], ); }); it('to other account', async function () { await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder }); - const { tx } = await this.token.depositFor(anotherAccount, initialSupply, { from: initialHolder }); + const { tx } = await this.token.depositFor(receiver, initialSupply, { from: initialHolder }); await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { from: initialHolder, to: this.token.address, @@ -90,7 +93,7 @@ contract('ERC20', function (accounts) { }); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: ZERO_ADDRESS, - to: anotherAccount, + to: receiver, value: initialSupply, }); }); @@ -103,9 +106,10 @@ contract('ERC20', function (accounts) { }); it('missing balance', async function () { - await expectRevert( + await expectRevertCustomError( this.token.withdrawTo(initialHolder, MAX_UINT256, { from: initialHolder }), - 'ERC20: transfer amount exceeds balance', + 'ERC20InsufficientBalance', + [initialHolder, initialSupply, MAX_UINT256], ); }); @@ -140,10 +144,10 @@ contract('ERC20', function (accounts) { }); it('to other account', async function () { - const { tx } = await this.token.withdrawTo(anotherAccount, initialSupply, { from: initialHolder }); + const { tx } = await this.token.withdrawTo(receiver, initialSupply, { from: initialHolder }); await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { from: this.token.address, - to: anotherAccount, + to: receiver, value: initialSupply, }); await expectEvent.inTransaction(tx, this.token, 'Transfer', { @@ -159,10 +163,10 @@ contract('ERC20', function (accounts) { await this.underlying.approve(this.token.address, initialSupply, { from: initialHolder }); await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }); - const { tx } = await this.token.$_recover(anotherAccount); + const { tx } = await this.token.$_recover(receiver); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: ZERO_ADDRESS, - to: anotherAccount, + to: receiver, value: '0', }); }); @@ -170,10 +174,10 @@ contract('ERC20', function (accounts) { it('something to recover', async function () { await this.underlying.transfer(this.token.address, initialSupply, { from: initialHolder }); - const { tx } = await this.token.$_recover(anotherAccount); + const { tx } = await this.token.$_recover(receiver); await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: ZERO_ADDRESS, - to: anotherAccount, + to: receiver, value: initialSupply, }); }); @@ -185,6 +189,6 @@ contract('ERC20', function (accounts) { await this.token.depositFor(initialHolder, initialSupply, { from: initialHolder }); }); - shouldBehaveLikeERC20('ERC20', initialSupply, initialHolder, recipient, anotherAccount); + shouldBehaveLikeERC20(initialSupply, accounts); }); }); diff --git a/test/token/ERC20/extensions/ERC4626.t.sol b/test/token/ERC20/extensions/ERC4626.t.sol index 3d7dd4a3e..da01c7d81 100644 --- a/test/token/ERC20/extensions/ERC4626.t.sol +++ b/test/token/ERC20/extensions/ERC4626.t.sol @@ -1,36 +1,42 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; -import "erc4626-tests/ERC4626.test.sol"; +import {ERC4626Test} from "erc4626-tests/ERC4626.test.sol"; -import {SafeCast} from "../../../../contracts/utils/math/SafeCast.sol"; -import {ERC20Mock} from "../../../../contracts/mocks/ERC20Mock.sol"; -import {ERC4626Mock} from "../../../../contracts/mocks/ERC4626Mock.sol"; +import {SafeCast} from "openzeppelin/utils/math/SafeCast.sol"; +import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol"; +import {ERC4626} from "openzeppelin/token/ERC20/extensions/ERC4626.sol"; + +import {ERC20Mock} from "openzeppelin/mocks/token/ERC20Mock.sol"; +import {ERC4626Mock} from "openzeppelin/mocks/token/ERC4626Mock.sol"; +import {ERC4626OffsetMock} from "openzeppelin/mocks/token/ERC4626OffsetMock.sol"; + +contract ERC4626VaultOffsetMock is ERC4626OffsetMock { + constructor( + ERC20 underlying_, + uint8 offset_ + ) ERC20("My Token Vault", "MTKNV") ERC4626(underlying_) ERC4626OffsetMock(offset_) {} +} contract ERC4626StdTest is ERC4626Test { + ERC20 private _underlying = new ERC20Mock(); + function setUp() public override { - _underlying_ = address(new ERC20Mock()); + _underlying_ = address(_underlying); _vault_ = address(new ERC4626Mock(_underlying_)); _delta_ = 0; - _vaultMayBeEmpty = false; + _vaultMayBeEmpty = true; _unlimitedAmount = true; } - // solhint-disable-next-line func-name-mixedcase - function test_RT_mint_withdraw(ERC4626Test.Init memory init, uint256 shares) public override { - // There is an edge case where we currently behave different than the property tests, - // when all assets are lost to negative yield. - - // Sum all initially deposited assets. - int256 initAssets = 0; - for (uint256 i = 0; i < init.share.length; i++) { - vm.assume(init.share[i] <= uint256(type(int256).max - initAssets)); - initAssets += SafeCast.toInt256(init.share[i]); - } - - // Reject tests where the yield loses all assets from the vault. - vm.assume(init.yield > -initAssets); - - super.test_RT_mint_withdraw(init, shares); + /** + * @dev Check the case where calculated `decimals` value overflows the `uint8` type. + */ + function testFuzzDecimalsOverflow(uint8 offset) public { + /// @dev Remember that the `_underlying` exhibits a `decimals` value of 18. + offset = uint8(bound(uint256(offset), 238, uint256(type(uint8).max))); + ERC4626VaultOffsetMock erc4626VaultOffsetMock = new ERC4626VaultOffsetMock(_underlying, offset); + vm.expectRevert(); + erc4626VaultOffsetMock.decimals(); } } diff --git a/test/token/ERC20/extensions/ERC4626.test.js b/test/token/ERC20/extensions/ERC4626.test.js index f85ffd56d..99d6009e4 100644 --- a/test/token/ERC20/extensions/ERC4626.test.js +++ b/test/token/ERC20/extensions/ERC4626.test.js @@ -1,34 +1,23 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { Enum } = require('../../../helpers/enums'); +const { expectRevertCustomError } = require('../../../helpers/customError'); + const ERC20Decimals = artifacts.require('$ERC20DecimalsMock'); const ERC4626 = artifacts.require('$ERC4626'); -const ERC4626Decimals = artifacts.require('$ERC4626DecimalsMock'); - -const parseToken = token => new BN(token).mul(new BN('1000000000000')); -const parseShare = share => new BN(share).mul(new BN('1000000000000000000')); +const ERC4626LimitsMock = artifacts.require('$ERC4626LimitsMock'); +const ERC4626OffsetMock = artifacts.require('$ERC4626OffsetMock'); +const ERC4626FeesMock = artifacts.require('$ERC4626FeesMock'); +const ERC20ExcessDecimalsMock = artifacts.require('ERC20ExcessDecimalsMock'); +const ERC20Reentrant = artifacts.require('$ERC20Reentrant'); contract('ERC4626', function (accounts) { const [holder, recipient, spender, other, user1, user2] = accounts; const name = 'My Token'; const symbol = 'MTKN'; - - beforeEach(async function () { - this.token = await ERC20Decimals.new(name, symbol, 12); - this.vault = await ERC4626Decimals.new(name + ' Vault', symbol + 'V', this.token.address, 18); - - 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.decimals()).to.be.bignumber.equal('18'); - expect(await this.vault.asset()).to.be.equal(this.token.address); - }); + const decimals = web3.utils.toBN(18); it('inherit decimals if from asset', async function () { for (const decimals of [0, 9, 12, 18, 36].map(web3.utils.toBN)) { @@ -38,371 +27,839 @@ contract('ERC4626', function (accounts) { } }); - describe('empty vault: no assets & no shares', function () { - it('status', async function () { - expect(await this.vault.totalAssets()).to.be.bignumber.equal('0'); + it('asset has not yet been created', async function () { + const vault = await ERC4626.new('', '', other); + expect(await vault.decimals()).to.be.bignumber.equal(decimals); + }); + + it('underlying excess decimals', async function () { + const token = await ERC20ExcessDecimalsMock.new(); + const vault = await ERC4626.new('', '', token.address); + expect(await vault.decimals()).to.be.bignumber.equal(decimals); + }); + + it('decimals overflow', async function () { + for (const offset of [243, 250, 255].map(web3.utils.toBN)) { + const token = await ERC20Decimals.new('', '', decimals); + const vault = await ERC4626OffsetMock.new(name + ' Vault', symbol + 'V', token.address, offset); + await expectRevert( + vault.decimals(), + 'reverted with panic code 0x11 (Arithmetic operation underflowed or overflowed outside of an unchecked block)', + ); + } + }); + + describe('reentrancy', async function () { + const reenterType = Enum('No', 'Before', 'After'); + + const amount = web3.utils.toBN(1000000000000000000); + const reenterAmount = web3.utils.toBN(1000000000); + let token; + let vault; + + beforeEach(async function () { + token = await ERC20Reentrant.new(); + // Use offset 1 so the rate is not 1:1 and we can't possibly confuse assets and shares + vault = await ERC4626OffsetMock.new('', '', token.address, 1); + // Funds and approval for tests + await token.$_mint(holder, amount); + await token.$_mint(other, amount); + await token.$_approve(holder, vault.address, constants.MAX_UINT256); + await token.$_approve(other, vault.address, constants.MAX_UINT256); + await token.$_approve(token.address, vault.address, constants.MAX_UINT256); }); - 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)); + // During a `_deposit`, the vault does `transferFrom(depositor, vault, assets)` -> `_mint(receiver, shares)` + // such that a reentrancy BEFORE the transfer guarantees the price is kept the same. + // If the order of transfer -> mint is changed to mint -> transfer, the reentrancy could be triggered on an + // intermediate state in which the ratio of assets/shares has been decreased (more shares than assets). + it('correct share price is observed during reentrancy before deposit', async function () { + // mint token for deposit + await token.$_mint(token.address, reenterAmount); - const { tx } = await this.vault.deposit(parseToken(1), recipient, { from: holder }); + // Schedules a reentrancy from the token contract + await token.scheduleReenter( + reenterType.Before, + vault.address, + vault.contract.methods.deposit(reenterAmount, holder).encodeABI(), + ); - expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: parseToken(1), + // Initial share price + const sharesForDeposit = await vault.previewDeposit(amount, { from: holder }); + const sharesForReenter = await vault.previewDeposit(reenterAmount, { from: holder }); + + // Do deposit normally, triggering the _beforeTokenTransfer hook + const receipt = await vault.deposit(amount, holder, { from: holder }); + + // Main deposit event + await expectEvent(receipt, 'Deposit', { + sender: holder, + owner: holder, + assets: amount, + shares: sharesForDeposit, + }); + // Reentrant deposit event → uses the same price + await expectEvent(receipt, 'Deposit', { + sender: token.address, + owner: holder, + assets: reenterAmount, + shares: sharesForReenter, }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(1), - }); + // Assert prices is kept + const sharesAfter = await vault.previewDeposit(amount, { from: holder }); + expect(sharesForDeposit).to.be.bignumber.eq(sharesAfter); }); - 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)); + // During a `_withdraw`, the vault does `_burn(owner, shares)` -> `transfer(receiver, assets)` + // such that a reentrancy AFTER the transfer guarantees the price is kept the same. + // If the order of burn -> transfer is changed to transfer -> burn, the reentrancy could be triggered on an + // intermediate state in which the ratio of shares/assets has been decreased (more assets than shares). + it('correct share price is observed during reentrancy after withdraw', async function () { + // Deposit into the vault: holder gets `amount` share, token.address gets `reenterAmount` shares + await vault.deposit(amount, holder, { from: holder }); + await vault.deposit(reenterAmount, token.address, { from: other }); - const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); + // Schedules a reentrancy from the token contract + await token.scheduleReenter( + reenterType.After, + vault.address, + vault.contract.methods.withdraw(reenterAmount, holder, token.address).encodeABI(), + ); - expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: parseToken(1), + // Initial share price + const sharesForWithdraw = await vault.previewWithdraw(amount, { from: holder }); + const sharesForReenter = await vault.previewWithdraw(reenterAmount, { from: holder }); + + // Do withdraw normally, triggering the _afterTokenTransfer hook + const receipt = await vault.withdraw(amount, holder, holder, { from: holder }); + + // Main withdraw event + await expectEvent(receipt, 'Withdraw', { + sender: holder, + receiver: holder, + owner: holder, + assets: amount, + shares: sharesForWithdraw, + }); + // Reentrant withdraw event → uses the same price + await expectEvent(receipt, 'Withdraw', { + sender: token.address, + receiver: holder, + owner: token.address, + assets: reenterAmount, + shares: sharesForReenter, }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(1), - }); + // Assert price is kept + const sharesAfter = await vault.previewWithdraw(amount, { from: holder }); + expect(sharesForWithdraw).to.be.bignumber.eq(sharesAfter); }); - 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'); + // Donate newly minted tokens to the vault during the reentracy causes the share price to increase. + // Still, the deposit that trigger the reentracy is not affected and get the previewed price. + // Further deposits will get a different price (getting fewer shares for the same amount of assets) + it('share price change during reentracy does not affect deposit', async function () { + // Schedules a reentrancy from the token contract that mess up the share price + await token.scheduleReenter( + reenterType.Before, + token.address, + token.contract.methods.$_mint(vault.address, reenterAmount).encodeABI(), + ); - const { tx } = await this.vault.withdraw('0', recipient, holder, { from: holder }); + // Price before + const sharesBefore = await vault.previewDeposit(amount); - expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', + // Deposit, triggering the _beforeTokenTransfer hook + const receipt = await vault.deposit(amount, holder, { from: holder }); + + // Price is as previewed + await expectEvent(receipt, 'Deposit', { + sender: holder, + owner: holder, + assets: amount, + shares: sharesBefore, }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: '0', - }); + // Price was modified during reentrancy + const sharesAfter = await vault.previewDeposit(amount); + expect(sharesAfter).to.be.bignumber.lt(sharesBefore); }); - 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'); + // Burn some tokens from the vault during the reentracy causes the share price to drop. + // Still, the withdraw that trigger the reentracy is not affected and get the previewed price. + // Further withdraw will get a different price (needing more shares for the same amount of assets) + it('share price change during reentracy does not affect withdraw', async function () { + await vault.deposit(amount, other, { from: other }); + await vault.deposit(amount, holder, { from: holder }); - const { tx } = await this.vault.redeem('0', recipient, holder, { from: holder }); + // Schedules a reentrancy from the token contract that mess up the share price + await token.scheduleReenter( + reenterType.After, + token.address, + token.contract.methods.$_burn(vault.address, reenterAmount).encodeABI(), + ); - expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: this.vault.address, - to: recipient, - value: '0', + // Price before + const sharesBefore = await vault.previewWithdraw(amount); + + // Withdraw, triggering the _afterTokenTransfer hook + const receipt = await vault.withdraw(amount, holder, holder, { from: holder }); + + // Price is as previewed + await expectEvent(receipt, 'Withdraw', { + sender: holder, + receiver: holder, + owner: holder, + assets: amount, + shares: sharesBefore, }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: holder, - to: constants.ZERO_ADDRESS, - value: '0', - }); + // Price was modified during reentrancy + const sharesAfter = await vault.previewWithdraw(amount); + expect(sharesAfter).to.be.bignumber.gt(sharesBefore); }); }); - describe('partially empty vault: assets & no shares', function () { + describe('limits', async function () { beforeEach(async function () { - await this.token.$_mint(this.vault.address, parseToken(1)); // 1 token + this.token = await ERC20Decimals.new(name, symbol, decimals); + this.vault = await ERC4626LimitsMock.new(name + ' Vault', symbol + 'V', this.token.address); }); - it('status', async function () { - expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1)); + it('reverts on deposit() above max deposit', async function () { + const maxDeposit = await this.vault.maxDeposit(holder); + await expectRevertCustomError(this.vault.deposit(maxDeposit.addn(1), recipient), 'ERC4626ExceededMaxDeposit', [ + recipient, + maxDeposit.addn(1), + maxDeposit, + ]); }); - 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('reverts on mint() above max mint', async function () { + const maxMint = await this.vault.maxMint(holder); + await expectRevertCustomError(this.vault.mint(maxMint.addn(1), recipient), 'ERC4626ExceededMaxMint', [ + recipient, + maxMint.addn(1), + maxMint, + ]); }); - 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('reverts on withdraw() above max withdraw', async function () { + const maxWithdraw = await this.vault.maxWithdraw(holder); + await expectRevertCustomError( + this.vault.withdraw(maxWithdraw.addn(1), recipient, holder), + 'ERC4626ExceededMaxWithdraw', + [holder, maxWithdraw.addn(1), maxWithdraw], + ); }); - 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', - }); + it('reverts on redeem() above max redeem', async function () { + const maxRedeem = await this.vault.maxRedeem(holder); + await expectRevertCustomError( + this.vault.redeem(maxRedeem.addn(1), recipient, holder), + 'ERC4626ExceededMaxRedeem', + [holder, maxRedeem.addn(1), maxRedeem], + ); }); }); - describe('partially empty vault: shares & no assets', function () { - beforeEach(async function () { - await this.vault.$_mint(holder, parseShare(1)); // 1 share - }); + for (const offset of [0, 6, 18].map(web3.utils.toBN)) { + const parseToken = token => web3.utils.toBN(10).pow(decimals).muln(token); + const parseShare = share => web3.utils.toBN(10).pow(decimals.add(offset)).muln(share); - it('status', async function () { - expect(await this.vault.totalAssets()).to.be.bignumber.equal('0'); - }); + const virtualAssets = web3.utils.toBN(1); + const virtualShares = web3.utils.toBN(10).pow(offset); - it('deposit', async function () { - expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal('0'); + describe(`offset: ${offset}`, function () { + beforeEach(async function () { + this.token = await ERC20Decimals.new(name, symbol, decimals); + this.vault = await ERC4626OffsetMock.new(name + ' Vault', symbol + 'V', this.token.address, offset); - // 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, + await this.token.$_mint(holder, constants.MAX_INT256); // 50% of maximum + await this.token.approve(this.vault.address, constants.MAX_UINT256, { from: holder }); + await this.vault.approve(spender, constants.MAX_UINT256, { from: holder }); }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: 0, + 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.decimals()).to.be.bignumber.equal(decimals.add(offset)); + expect(await this.vault.asset()).to.be.equal(this.token.address); }); - // Cannot deposit more than 0 - await expectRevert.unspecified(this.vault.previewDeposit(parseToken(1))); - await expectRevert( - this.vault.deposit(parseToken(1), recipient, { from: holder }), - 'ERC4626: deposit more than max', - ); + 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 }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: parseToken(1), + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: parseShare(1), + }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: parseToken(1), + shares: 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 }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: parseToken(1), + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: parseShare(1), + }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: parseToken(1), + shares: 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 }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: '0', + shares: '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 }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: '0', + shares: '0', + }); + }); + }); + + describe('inflation attack: offset price by direct deposit of assets', function () { + beforeEach(async function () { + // Donate 1 token to the vault to offset the price + await this.token.$_mint(this.vault.address, parseToken(1)); + }); + + it('status', async function () { + expect(await this.vault.totalSupply()).to.be.bignumber.equal('0'); + expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1)); + }); + + /** + * | offset | deposited assets | redeemable assets | + * |--------|----------------------|----------------------| + * | 0 | 1.000000000000000000 | 0. | + * | 6 | 1.000000000000000000 | 0.999999000000000000 | + * | 18 | 1.000000000000000000 | 0.999999999999999999 | + * + * Attack is possible, but made difficult by the offset. For the attack to be successful + * the attacker needs to frontrun a deposit 10**offset times bigger than what the victim + * was trying to deposit + */ + it('deposit', async function () { + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); + + const depositAssets = parseToken(1); + const expectedShares = depositAssets.mul(effectiveShares).div(effectiveAssets); + + expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256); + expect(await this.vault.previewDeposit(depositAssets)).to.be.bignumber.equal(expectedShares); + + const { tx } = await this.vault.deposit(depositAssets, recipient, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: depositAssets, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: expectedShares, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: depositAssets, + shares: expectedShares, + }); + }); + + /** + * | offset | deposited assets | redeemable assets | + * |--------|----------------------|----------------------| + * | 0 | 1000000000000000001. | 1000000000000000001. | + * | 6 | 1000000000000000001. | 1000000000000000001. | + * | 18 | 1000000000000000001. | 1000000000000000001. | + * + * Using mint protects against inflation attack, but makes minting shares very expensive. + * The ER20 allowance for the underlying asset is needed to protect the user from (too) + * large deposits. + */ + it('mint', async function () { + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); + + const mintShares = parseShare(1); + const expectedAssets = mintShares.mul(effectiveAssets).div(effectiveShares); + + expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); + expect(await this.vault.previewMint(mintShares)).to.be.bignumber.equal(expectedAssets); + + const { tx } = await this.vault.mint(mintShares, recipient, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: expectedAssets, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: mintShares, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: expectedAssets, + shares: mintShares, + }); + }); + + 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 }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: '0', + shares: '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 }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: '0', + }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: '0', + shares: '0', + }); + }); + }); + + describe('full vault: assets & shares', function () { + beforeEach(async function () { + // Add 1 token of underlying asset and 100 shares to the vault + await this.token.$_mint(this.vault.address, parseToken(1)); + await this.vault.$_mint(holder, parseShare(100)); + }); + + it('status', async function () { + expect(await this.vault.totalSupply()).to.be.bignumber.equal(parseShare(100)); + expect(await this.vault.totalAssets()).to.be.bignumber.equal(parseToken(1)); + }); + + /** + * | offset | deposited assets | redeemable assets | + * |--------|--------------------- |----------------------| + * | 0 | 1.000000000000000000 | 0.999999999999999999 | + * | 6 | 1.000000000000000000 | 0.999999999999999999 | + * | 18 | 1.000000000000000000 | 0.999999999999999999 | + * + * Virtual shares & assets captures part of the value + */ + it('deposit', async function () { + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); + + const depositAssets = parseToken(1); + const expectedShares = depositAssets.mul(effectiveShares).div(effectiveAssets); + + expect(await this.vault.maxDeposit(holder)).to.be.bignumber.equal(constants.MAX_UINT256); + expect(await this.vault.previewDeposit(depositAssets)).to.be.bignumber.equal(expectedShares); + + const { tx } = await this.vault.deposit(depositAssets, recipient, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: depositAssets, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: expectedShares, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: depositAssets, + shares: expectedShares, + }); + }); + + /** + * | offset | deposited assets | redeemable assets | + * |--------|--------------------- |----------------------| + * | 0 | 0.010000000000000001 | 0.010000000000000000 | + * | 6 | 0.010000000000000001 | 0.010000000000000000 | + * | 18 | 0.010000000000000001 | 0.010000000000000000 | + * + * Virtual shares & assets captures part of the value + */ + it('mint', async function () { + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); + + const mintShares = parseShare(1); + const expectedAssets = mintShares.mul(effectiveAssets).div(effectiveShares).addn(1); // add for the rounding + + expect(await this.vault.maxMint(holder)).to.be.bignumber.equal(constants.MAX_UINT256); + expect(await this.vault.previewMint(mintShares)).to.be.bignumber.equal(expectedAssets); + + const { tx } = await this.vault.mint(mintShares, recipient, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: expectedAssets, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: mintShares, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: expectedAssets, + shares: mintShares, + }); + }); + + it('withdraw', async function () { + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); + + const withdrawAssets = parseToken(1); + const expectedShares = withdrawAssets.mul(effectiveShares).div(effectiveAssets).addn(1); // add for the rounding + + expect(await this.vault.maxWithdraw(holder)).to.be.bignumber.equal(withdrawAssets); + expect(await this.vault.previewWithdraw(withdrawAssets)).to.be.bignumber.equal(expectedShares); + + const { tx } = await this.vault.withdraw(withdrawAssets, recipient, holder, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: withdrawAssets, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: expectedShares, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: withdrawAssets, + shares: expectedShares, + }); + }); + + it('withdraw with approval', async function () { + const assets = await this.vault.previewWithdraw(parseToken(1)); + await expectRevertCustomError( + this.vault.withdraw(parseToken(1), recipient, holder, { from: other }), + 'ERC20InsufficientAllowance', + [other, 0, assets], + ); + + await this.vault.withdraw(parseToken(1), recipient, holder, { from: spender }); + }); + + it('redeem', async function () { + const effectiveAssets = await this.vault.totalAssets().then(x => x.add(virtualAssets)); + const effectiveShares = await this.vault.totalSupply().then(x => x.add(virtualShares)); + + const redeemShares = parseShare(100); + const expectedAssets = redeemShares.mul(effectiveAssets).div(effectiveShares); + + expect(await this.vault.maxRedeem(holder)).to.be.bignumber.equal(redeemShares); + expect(await this.vault.previewRedeem(redeemShares)).to.be.bignumber.equal(expectedAssets); + + const { tx } = await this.vault.redeem(redeemShares, recipient, holder, { from: holder }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: expectedAssets, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: redeemShares, + }); + + await expectEvent.inTransaction(tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: expectedAssets, + shares: redeemShares, + }); + }); + + it('redeem with approval', async function () { + await expectRevertCustomError( + this.vault.redeem(parseShare(100), recipient, holder, { from: other }), + 'ERC20InsufficientAllowance', + [other, 0, parseShare(100)], + ); + + await this.vault.redeem(parseShare(100), recipient, holder, { from: spender }); + }); + }); }); + } - 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'); + describe('ERC4626Fees', function () { + const feeBasePoint = web3.utils.toBN(5e3); + const amountWithoutFees = web3.utils.toBN(10000); + const fees = amountWithoutFees.mul(feeBasePoint).divn(1e5); + const amountWithFees = amountWithoutFees.add(fees); - const { tx } = await this.vault.mint(parseShare(1), recipient, { from: holder }); + describe('input fees', function () { + beforeEach(async function () { + this.token = await ERC20Decimals.new(name, symbol, 18); + this.vault = await ERC4626FeesMock.new( + name + ' Vault', + symbol + 'V', + this.token.address, + feeBasePoint, + other, + 0, + constants.ZERO_ADDRESS, + ); - expectEvent.inTransaction(tx, this.token, 'Transfer', { - from: holder, - to: this.vault.address, - value: '0', + await this.token.$_mint(holder, constants.MAX_INT256); + await this.token.approve(this.vault.address, constants.MAX_INT256, { from: holder }); }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(1), + it('deposit', async function () { + expect(await this.vault.previewDeposit(amountWithFees)).to.be.bignumber.equal(amountWithoutFees); + ({ tx: this.tx } = await this.vault.deposit(amountWithFees, recipient, { from: holder })); + }); + + it('mint', async function () { + expect(await this.vault.previewMint(amountWithoutFees)).to.be.bignumber.equal(amountWithFees); + ({ tx: this.tx } = await this.vault.mint(amountWithoutFees, recipient, { from: holder })); + }); + + afterEach(async function () { + // get total + await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { + from: holder, + to: this.vault.address, + value: amountWithFees, + }); + + // redirect fees + await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { + from: this.vault.address, + to: other, + value: fees, + }); + + // mint shares + await expectEvent.inTransaction(this.tx, this.vault, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: recipient, + value: amountWithoutFees, + }); + + // deposit event + await expectEvent.inTransaction(this.tx, this.vault, 'Deposit', { + sender: holder, + owner: recipient, + assets: amountWithFees, + shares: amountWithoutFees, + }); }); }); - 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')); + describe('output fees', function () { + beforeEach(async function () { + this.token = await ERC20Decimals.new(name, symbol, 18); + this.vault = await ERC4626FeesMock.new( + name + ' Vault', + symbol + 'V', + this.token.address, + 0, + constants.ZERO_ADDRESS, + 5e3, // 5% + other, + ); - 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', + await this.token.$_mint(this.vault.address, constants.MAX_INT256); + await this.vault.$_mint(holder, constants.MAX_INT256); }); - 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', + it('redeem', async function () { + expect(await this.vault.previewRedeem(amountWithFees)).to.be.bignumber.equal(amountWithoutFees); + ({ tx: this.tx } = await this.vault.redeem(amountWithFees, recipient, holder, { from: holder })); }); - 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.$_mint(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), + it('withdraw', async function () { + expect(await this.vault.previewWithdraw(amountWithoutFees)).to.be.bignumber.equal(amountWithFees); + ({ tx: this.tx } = await this.vault.withdraw(amountWithoutFees, recipient, holder, { from: holder })); }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { - from: constants.ZERO_ADDRESS, - to: recipient, - value: parseShare(100), + afterEach(async function () { + // withdraw principal + await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { + from: this.vault.address, + to: recipient, + value: amountWithoutFees, + }); + + // redirect fees + await expectEvent.inTransaction(this.tx, this.token, 'Transfer', { + from: this.vault.address, + to: other, + value: fees, + }); + + // mint shares + await expectEvent.inTransaction(this.tx, this.vault, 'Transfer', { + from: holder, + to: constants.ZERO_ADDRESS, + value: amountWithFees, + }); + + // withdraw event + await expectEvent.inTransaction(this.tx, this.vault, 'Withdraw', { + sender: holder, + receiver: recipient, + owner: holder, + assets: amountWithoutFees, + shares: amountWithFees, + }); }); }); - - 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: @@ -420,12 +877,12 @@ contract('ERC4626', function (accounts) { // 1. Alice mints 2000 shares (costs 2000 tokens) { const { tx } = await this.vault.mint(2000, user1, { from: user1 }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: user1, to: this.vault.address, value: '2000', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: user1, value: '2000', @@ -436,6 +893,9 @@ contract('ERC4626', function (accounts) { 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.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '2000', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('2000'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('2000'); } @@ -443,12 +903,12 @@ contract('ERC4626', function (accounts) { // 2. Bob deposits 4000 tokens (mints 4000 shares) { const { tx } = await this.vault.mint(4000, user2, { from: user2 }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: user2, to: this.vault.address, value: '4000', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: user2, value: '4000', @@ -459,6 +919,9 @@ contract('ERC4626', function (accounts) { 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.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '6000', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('6000'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('6000'); } @@ -468,20 +931,23 @@ contract('ERC4626', function (accounts) { 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.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('2999'); // used to be 3000, but virtual assets/shares captures part of the yield + expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('5999'); // used to be 6000, but virtual assets/shares captures part of the yield + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).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', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: user1, to: this.vault.address, value: '2000', }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: user1, value: '1333', @@ -491,6 +957,9 @@ contract('ERC4626', function (accounts) { 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.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '7333', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('7333'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('11000'); } @@ -500,12 +969,12 @@ contract('ERC4626', function (accounts) { // NOTE: Alices's vault assets got rounded up { const { tx } = await this.vault.mint(2000, user2, { from: user2 }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: user2, to: this.vault.address, - value: '3001', + value: '3000', // used to be 3001 }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: constants.ZERO_ADDRESS, to: user2, value: '2000', @@ -513,10 +982,13 @@ contract('ERC4626', function (accounts) { 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(user1))).to.be.bignumber.equal('4999'); // used to be 5000 expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('9000'); + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '9333', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('14001'); + expect(await this.vault.totalAssets()).to.be.bignumber.equal('14000'); // used to be 14001 } // 6. Vault mutates by +3000 tokens @@ -525,29 +997,35 @@ contract('ERC4626', function (accounts) { 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.convertToAssets(await this.vault.balanceOf(user1))).to.be.bignumber.equal('6070'); // used to be 6071 + expect(await this.vault.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('10928'); // used to be 10929 + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '9333', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('9333'); - expect(await this.vault.totalAssets()).to.be.bignumber.equal('17001'); + expect(await this.vault.totalAssets()).to.be.bignumber.equal('17000'); // used to be 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', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: user1, to: constants.ZERO_ADDRESS, value: '1333', }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: user1, - value: '2428', + value: '2427', // used to be 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.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '8000', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('8000'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('14573'); } @@ -555,12 +1033,12 @@ contract('ERC4626', function (accounts) { // 8. Bob withdraws 2929 assets (1608 shares) { const { tx } = await this.vault.withdraw(2929, user2, user2, { from: user2 }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: user2, to: constants.ZERO_ADDRESS, value: '1608', }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: user2, value: '2929', @@ -570,6 +1048,9 @@ contract('ERC4626', function (accounts) { 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.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '6392', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('6392'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('11644'); } @@ -578,12 +1059,12 @@ contract('ERC4626', function (accounts) { // 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', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: user1, to: constants.ZERO_ADDRESS, value: '2000', }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: user1, value: '3643', @@ -592,7 +1073,10 @@ contract('ERC4626', function (accounts) { 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.convertToAssets(await this.vault.balanceOf(user2))).to.be.bignumber.equal('8000'); // used to be 8001 + expect(await this.vault.convertToShares(await this.token.balanceOf(this.vault.address))).to.be.bignumber.equal( + '4392', + ); expect(await this.vault.totalSupply()).to.be.bignumber.equal('4392'); expect(await this.vault.totalAssets()).to.be.bignumber.equal('8001'); } @@ -600,23 +1084,26 @@ contract('ERC4626', function (accounts) { // 10. Bob redeem 4392 shares (8001 tokens) { const { tx } = await this.vault.redeem(4392, user2, user2, { from: user2 }); - expectEvent.inTransaction(tx, this.vault, 'Transfer', { + await expectEvent.inTransaction(tx, this.vault, 'Transfer', { from: user2, to: constants.ZERO_ADDRESS, value: '4392', }); - expectEvent.inTransaction(tx, this.token, 'Transfer', { + await expectEvent.inTransaction(tx, this.token, 'Transfer', { from: this.vault.address, to: user2, - value: '8001', + value: '8000', // used to be 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.convertToShares(await this.token.balanceOf(this.vault.address))).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'); + expect(await this.vault.totalAssets()).to.be.bignumber.equal('1'); // used to be 0 } }); }); diff --git a/test/token/ERC20/extensions/draft-ERC20Permit.test.js b/test/token/ERC20/extensions/draft-ERC20Permit.test.js deleted file mode 100644 index eb6737826..000000000 --- a/test/token/ERC20/extensions/draft-ERC20Permit.test.js +++ /dev/null @@ -1,103 +0,0 @@ -/* eslint-disable */ - -const { BN, constants, expectRevert, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); -const { MAX_UINT256 } = constants; - -const { fromRpcSig } = require('ethereumjs-util'); -const ethSigUtil = require('eth-sig-util'); -const Wallet = require('ethereumjs-wallet').default; - -const ERC20Permit = artifacts.require('$ERC20Permit'); - -const { EIP712Domain, Permit, domainSeparator } = require('../../../helpers/eip712'); -const { getChainId } = require('../../../helpers/chainid'); - -contract('ERC20Permit', function (accounts) { - const [initialHolder, spender] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - const version = '1'; - - const initialSupply = new BN(100); - - beforeEach(async function () { - this.chainId = await getChainId(); - - this.token = await ERC20Permit.new(name, symbol, name); - await this.token.$_mint(initialHolder, initialSupply); - }); - - it('initial nonce is 0', async function () { - expect(await this.token.nonces(initialHolder)).to.be.bignumber.equal('0'); - }); - - it('domain separator', async function () { - expect(await this.token.DOMAIN_SEPARATOR()).to.equal( - await domainSeparator({ name, version, chainId: this.chainId, verifyingContract: this.token.address }), - ); - }); - - describe('permit', function () { - const wallet = Wallet.generate(); - - const owner = wallet.getAddressString(); - const value = new BN(42); - const nonce = 0; - const maxDeadline = MAX_UINT256; - - const buildData = (chainId, verifyingContract, deadline = maxDeadline) => ({ - primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name, version, chainId, verifyingContract }, - message: { owner, spender, value, nonce, deadline }, - }); - - it('accepts owner signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); - expect(await this.token.allowance(owner, spender)).to.be.bignumber.equal(value); - }); - - it('rejects reused signature', async function () { - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await this.token.permit(owner, spender, value, maxDeadline, v, r, s); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC20Permit: invalid signature', - ); - }); - - it('rejects other signature', async function () { - const otherWallet = Wallet.generate(); - const data = buildData(this.chainId, this.token.address); - const signature = ethSigUtil.signTypedMessage(otherWallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert( - this.token.permit(owner, spender, value, maxDeadline, v, r, s), - 'ERC20Permit: invalid signature', - ); - }); - - it('rejects expired permit', async function () { - const deadline = (await time.latest()) - time.duration.weeks(1); - - const data = buildData(this.chainId, this.token.address, deadline); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); - const { v, r, s } = fromRpcSig(signature); - - await expectRevert(this.token.permit(owner, spender, value, deadline, v, r, s), 'ERC20Permit: expired deadline'); - }); - }); -}); diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 878989cb1..eb6e26755 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -1,20 +1,24 @@ -const { constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); const SafeERC20 = artifacts.require('$SafeERC20'); -const ERC20ReturnFalseMock = artifacts.require('ERC20ReturnFalseMock'); -const ERC20ReturnTrueMock = artifacts.require('ERC20ReturnTrueMock'); -const ERC20NoReturnMock = artifacts.require('ERC20NoReturnMock'); -const ERC20PermitNoRevertMock = artifacts.require('ERC20PermitNoRevertMock'); +const ERC20ReturnFalseMock = artifacts.require('$ERC20ReturnFalseMock'); +const ERC20ReturnTrueMock = artifacts.require('$ERC20'); // default implementation returns true +const ERC20NoReturnMock = artifacts.require('$ERC20NoReturnMock'); +const ERC20PermitNoRevertMock = artifacts.require('$ERC20PermitNoRevertMock'); +const ERC20ForceApproveMock = artifacts.require('$ERC20ForceApproveMock'); -const { EIP712Domain, Permit } = require('../../../helpers/eip712'); -const { getChainId } = require('../../../helpers/chainid'); +const { getDomain, domainType, Permit } = require('../../../helpers/eip712'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const { fromRpcSig } = require('ethereumjs-util'); const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; +const name = 'ERC20Mock'; +const symbol = 'ERC20Mock'; + contract('SafeERC20', function (accounts) { - const [hasNoCode] = accounts; + const [hasNoCode, receiver, spender] = accounts; before(async function () { this.mock = await SafeERC20.new(); @@ -25,31 +29,97 @@ contract('SafeERC20', function (accounts) { this.token = { address: hasNoCode }; }); - shouldRevertOnAllCalls('Address: call to non-contract'); + it('reverts on transfer', async function () { + await expectRevertCustomError(this.mock.$safeTransfer(this.token.address, receiver, 0), 'AddressEmptyCode', [ + this.token.address, + ]); + }); + + it('reverts on transferFrom', async function () { + await expectRevertCustomError( + this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), + 'AddressEmptyCode', + [this.token.address], + ); + }); + + it('reverts on increaseAllowance', async function () { + // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason) + await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, spender, 0)); + }); + + it('reverts on decreaseAllowance', async function () { + // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason) + await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, spender, 0)); + }); + + it('reverts on forceApprove', async function () { + await expectRevertCustomError(this.mock.$forceApprove(this.token.address, spender, 0), 'AddressEmptyCode', [ + this.token.address, + ]); + }); }); describe('with token that returns false on all calls', function () { beforeEach(async function () { - this.token = await ERC20ReturnFalseMock.new(); + this.token = await ERC20ReturnFalseMock.new(name, symbol); }); - shouldRevertOnAllCalls('SafeERC20: ERC20 operation did not succeed'); + it('reverts on transfer', async function () { + await expectRevertCustomError( + this.mock.$safeTransfer(this.token.address, receiver, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on transferFrom', async function () { + await expectRevertCustomError( + this.mock.$safeTransferFrom(this.token.address, this.mock.address, receiver, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on increaseAllowance', async function () { + await expectRevertCustomError( + this.mock.$safeIncreaseAllowance(this.token.address, spender, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on decreaseAllowance', async function () { + await expectRevertCustomError( + this.mock.$safeDecreaseAllowance(this.token.address, spender, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); + + it('reverts on forceApprove', async function () { + await expectRevertCustomError( + this.mock.$forceApprove(this.token.address, spender, 0), + 'SafeERC20FailedOperation', + [this.token.address], + ); + }); }); describe('with token that returns true on all calls', function () { beforeEach(async function () { - this.token = await ERC20ReturnTrueMock.new(); + this.token = await ERC20ReturnTrueMock.new(name, symbol); }); - shouldOnlyRevertOnErrors(); + shouldOnlyRevertOnErrors(accounts); }); describe('with token that returns no boolean values', function () { beforeEach(async function () { - this.token = await ERC20NoReturnMock.new(); + this.token = await ERC20NoReturnMock.new(name, symbol); }); - shouldOnlyRevertOnErrors(); + shouldOnlyRevertOnErrors(accounts); }); describe("with token that doesn't revert on invalid permit", function () { @@ -58,16 +128,15 @@ contract('SafeERC20', function (accounts) { const spender = hasNoCode; beforeEach(async function () { - const chainId = await getChainId(); + this.token = await ERC20PermitNoRevertMock.new(name, symbol, name); - this.token = await ERC20PermitNoRevertMock.new(); - - this.data = { + this.data = await getDomain(this.token).then(domain => ({ primaryType: 'Permit', - types: { EIP712Domain, Permit }, - domain: { name: 'ERC20PermitNoRevertMock', version: '1', chainId, verifyingContract: this.token.address }, + types: { EIP712Domain: domainType(domain), Permit }, + domain, message: { owner, spender, value: '42', nonce: '0', deadline: constants.MAX_UINT256 }, - }; + })); + this.signature = fromRpcSig(ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data: this.data })); }); @@ -116,7 +185,7 @@ contract('SafeERC20', function (accounts) { ); expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); // invalid call revert when called through the SafeERC20 library - await expectRevert( + await expectRevertCustomError( this.mock.$safePermit( this.token.address, this.data.message.owner, @@ -127,7 +196,8 @@ contract('SafeERC20', function (accounts) { this.signature.r, this.signature.s, ), - 'SafeERC20: permit did not succeed', + 'SafeERC20FailedOperation', + [this.token.address], ); expect(await this.token.nonces(owner)).to.be.bignumber.equal('1'); }); @@ -152,7 +222,7 @@ contract('SafeERC20', function (accounts) { ); // invalid call revert when called through the SafeERC20 library - await expectRevert( + await expectRevertCustomError( this.mock.$safePermit( this.token.address, this.data.message.owner, @@ -163,102 +233,129 @@ contract('SafeERC20', function (accounts) { invalidSignature.r, invalidSignature.s, ), - 'SafeERC20: permit did not succeed', + 'SafeERC20FailedOperation', + [this.token.address], ); }); }); + + describe('with usdt approval beaviour', function () { + const spender = hasNoCode; + + beforeEach(async function () { + this.token = await ERC20ForceApproveMock.new(name, symbol); + }); + + describe('with initial approval', function () { + beforeEach(async function () { + await this.token.$_approve(this.mock.address, spender, 100); + }); + + it('safeIncreaseAllowance works', async function () { + await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10); + expect(this.token.allowance(this.mock.address, spender, 90)); + }); + + it('safeDecreaseAllowance works', async function () { + await this.mock.$safeDecreaseAllowance(this.token.address, spender, 10); + expect(this.token.allowance(this.mock.address, spender, 110)); + }); + + it('forceApprove works', async function () { + await this.mock.$forceApprove(this.token.address, spender, 200); + expect(this.token.allowance(this.mock.address, spender, 200)); + }); + }); + }); }); -function shouldRevertOnAllCalls(reason) { - it('reverts on transfer', async function () { - await expectRevert(this.mock.$safeTransfer(this.token.address, constants.ZERO_ADDRESS, 0), reason); - }); +function shouldOnlyRevertOnErrors([owner, receiver, spender]) { + describe('transfers', function () { + beforeEach(async function () { + await this.token.$_mint(owner, 100); + await this.token.$_mint(this.mock.address, 100); + await this.token.approve(this.mock.address, constants.MAX_UINT256, { from: owner }); + }); - it('reverts on transferFrom', async function () { - await expectRevert( - this.mock.$safeTransferFrom(this.token.address, this.mock.address, constants.ZERO_ADDRESS, 0), - reason, - ); - }); + it("doesn't revert on transfer", async function () { + const { tx } = await this.mock.$safeTransfer(this.token.address, receiver, 10); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: this.mock.address, + to: receiver, + value: '10', + }); + }); - it('reverts on approve', async function () { - await expectRevert(this.mock.$safeApprove(this.token.address, constants.ZERO_ADDRESS, 0), reason); - }); - - it('reverts on increaseAllowance', async function () { - // [TODO] make sure it's reverting for the right reason - await expectRevert.unspecified(this.mock.$safeIncreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 0)); - }); - - it('reverts on decreaseAllowance', async function () { - // [TODO] make sure it's reverting for the right reason - await expectRevert.unspecified(this.mock.$safeDecreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 0)); - }); -} - -function shouldOnlyRevertOnErrors() { - it("doesn't revert on transfer", async function () { - await this.mock.$safeTransfer(this.token.address, constants.ZERO_ADDRESS, 0); - }); - - it("doesn't revert on transferFrom", async function () { - await this.mock.$safeTransferFrom(this.token.address, this.mock.address, constants.ZERO_ADDRESS, 0); + it("doesn't revert on transferFrom", async function () { + const { tx } = await this.mock.$safeTransferFrom(this.token.address, owner, receiver, 10); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: owner, + to: receiver, + value: '10', + }); + }); }); describe('approvals', function () { context('with zero allowance', function () { beforeEach(async function () { - await this.token.setAllowance(this.mock.address, 0); + await this.token.$_approve(this.mock.address, spender, 0); }); - it("doesn't revert when approving a non-zero allowance", async function () { - await this.mock.$safeApprove(this.token.address, constants.ZERO_ADDRESS, 100); + it("doesn't revert when force approving a non-zero allowance", async function () { + await this.mock.$forceApprove(this.token.address, spender, 100); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('100'); }); - it("doesn't revert when approving a zero allowance", async function () { - await this.mock.$safeApprove(this.token.address, constants.ZERO_ADDRESS, 0); + it("doesn't revert when force approving a zero allowance", async function () { + await this.mock.$forceApprove(this.token.address, spender, 0); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0'); }); it("doesn't revert when increasing the allowance", async function () { - await this.mock.$safeIncreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 10); + await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('10'); }); it('reverts when decreasing the allowance', async function () { - await expectRevert( - this.mock.$safeDecreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 10), - 'SafeERC20: decreased allowance below zero', + await expectRevertCustomError( + this.mock.$safeDecreaseAllowance(this.token.address, spender, 10), + 'SafeERC20FailedDecreaseAllowance', + [spender, 0, 10], ); }); }); context('with non-zero allowance', function () { beforeEach(async function () { - await this.token.setAllowance(this.mock.address, 100); + await this.token.$_approve(this.mock.address, spender, 100); }); - it('reverts when approving a non-zero allowance', async function () { - await expectRevert( - this.mock.$safeApprove(this.token.address, constants.ZERO_ADDRESS, 20), - 'SafeERC20: approve from non-zero to non-zero allowance', - ); + it("doesn't revert when force approving a non-zero allowance", async function () { + await this.mock.$forceApprove(this.token.address, spender, 20); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('20'); }); - it("doesn't revert when approving a zero allowance", async function () { - await this.mock.$safeApprove(this.token.address, constants.ZERO_ADDRESS, 0); + it("doesn't revert when force approving a zero allowance", async function () { + await this.mock.$forceApprove(this.token.address, spender, 0); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('0'); }); it("doesn't revert when increasing the allowance", async function () { - await this.mock.$safeIncreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 10); + await this.mock.$safeIncreaseAllowance(this.token.address, spender, 10); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('110'); }); it("doesn't revert when decreasing the allowance to a positive value", async function () { - await this.mock.$safeDecreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 50); + await this.mock.$safeDecreaseAllowance(this.token.address, spender, 50); + expect(await this.token.allowance(this.mock.address, spender)).to.be.bignumber.equal('50'); }); it('reverts when decreasing the allowance to a negative value', async function () { - await expectRevert( - this.mock.$safeDecreaseAllowance(this.token.address, constants.ZERO_ADDRESS, 200), - 'SafeERC20: decreased allowance below zero', + await expectRevertCustomError( + this.mock.$safeDecreaseAllowance(this.token.address, spender, 200), + 'SafeERC20FailedDecreaseAllowance', + [spender, 100, 200], ); }); }); diff --git a/test/token/ERC20/utils/TokenTimelock.test.js b/test/token/ERC20/utils/TokenTimelock.test.js deleted file mode 100644 index 22e8071eb..000000000 --- a/test/token/ERC20/utils/TokenTimelock.test.js +++ /dev/null @@ -1,71 +0,0 @@ -const { BN, expectRevert, time } = require('@openzeppelin/test-helpers'); - -const { expect } = require('chai'); - -const ERC20 = artifacts.require('$ERC20'); -const TokenTimelock = artifacts.require('TokenTimelock'); - -contract('TokenTimelock', function (accounts) { - const [beneficiary] = accounts; - - const name = 'My Token'; - const symbol = 'MTKN'; - - const amount = new BN(100); - - context('with token', function () { - beforeEach(async function () { - this.token = await ERC20.new(name, symbol); - }); - - it('rejects a release time in the past', async function () { - const pastReleaseTime = (await time.latest()).sub(time.duration.years(1)); - await expectRevert( - TokenTimelock.new(this.token.address, beneficiary, pastReleaseTime), - 'TokenTimelock: release time is before current time', - ); - }); - - context('once deployed', function () { - beforeEach(async function () { - this.releaseTime = (await time.latest()).add(time.duration.years(1)); - this.timelock = await TokenTimelock.new(this.token.address, beneficiary, this.releaseTime); - await this.token.$_mint(this.timelock.address, amount); - }); - - it('can get state', async function () { - expect(await this.timelock.token()).to.equal(this.token.address); - expect(await this.timelock.beneficiary()).to.equal(beneficiary); - expect(await this.timelock.releaseTime()).to.be.bignumber.equal(this.releaseTime); - }); - - it('cannot be released before time limit', async function () { - await expectRevert(this.timelock.release(), 'TokenTimelock: current time is before release time'); - }); - - it('cannot be released just before time limit', async function () { - await time.increaseTo(this.releaseTime.sub(time.duration.seconds(3))); - await expectRevert(this.timelock.release(), 'TokenTimelock: current time is before release time'); - }); - - it('can be released just after limit', async function () { - await time.increaseTo(this.releaseTime.add(time.duration.seconds(1))); - await this.timelock.release(); - expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount); - }); - - it('can be released after time limit', async function () { - await time.increaseTo(this.releaseTime.add(time.duration.years(1))); - await this.timelock.release(); - expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount); - }); - - it('cannot be released twice', async function () { - await time.increaseTo(this.releaseTime.add(time.duration.years(1))); - await this.timelock.release(); - await expectRevert(this.timelock.release(), 'TokenTimelock: no tokens to release'); - expect(await this.token.balanceOf(beneficiary)).to.be.bignumber.equal(amount); - }); - }); - }); -}); diff --git a/test/token/ERC721/ERC721.behavior.js b/test/token/ERC721/ERC721.behavior.js index c09479572..75700f6ab 100644 --- a/test/token/ERC721/ERC721.behavior.js +++ b/test/token/ERC721/ERC721.behavior.js @@ -3,13 +3,13 @@ const { expect } = require('chai'); const { ZERO_ADDRESS } = constants; const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../../helpers/customError'); +const { Enum } = require('../../helpers/enums'); const ERC721ReceiverMock = artifacts.require('ERC721ReceiverMock'); +const NonERC721ReceiverMock = artifacts.require('CallReceiverMock'); -const Error = ['None', 'RevertWithMessage', 'RevertWithoutMessage', 'Panic'].reduce( - (acc, entry, idx) => Object.assign({ [entry]: idx }, acc), - {}, -); +const RevertType = Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'); const firstTokenId = new BN('5042'); const secondTokenId = new BN('79217'); @@ -19,7 +19,7 @@ const baseURI = 'https://api.example.com/v1/'; const RECEIVER_MAGIC_VALUE = '0x150b7a02'; -function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherApproved, operator, other) { +function shouldBehaveLikeERC721(owner, newOwner, approved, anotherApproved, operator, other) { shouldSupportInterfaces(['ERC165', 'ERC721']); context('with minted tokens', function () { @@ -44,7 +44,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when querying the zero address', function () { it('throws', async function () { - await expectRevert(this.token.balanceOf(ZERO_ADDRESS), 'ERC721: address zero is not a valid owner'); + await expectRevertCustomError(this.token.balanceOf(ZERO_ADDRESS), 'ERC721InvalidOwner', [ZERO_ADDRESS]); }); }); }); @@ -62,7 +62,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA const tokenId = nonExistentTokenId; it('reverts', async function () { - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); }); }); }); @@ -171,36 +171,40 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the address of the previous owner is incorrect', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, other, other, tokenId, { from: owner }), - 'ERC721: transfer from incorrect owner', + 'ERC721IncorrectOwner', + [other, tokenId, owner], ); }); }); context('when the sender is not authorized for the token id', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, owner, other, tokenId, { from: other }), - 'ERC721: caller is not token owner or approved', + 'ERC721InsufficientApproval', + [other, tokenId], ); }); }); context('when the given token ID does not exist', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, owner, other, nonExistentTokenId, { from: owner }), - 'ERC721: invalid token ID', + 'ERC721NonexistentToken', + [nonExistentTokenId], ); }); }); context('when the address to transfer the token to is the zero address', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFunction.call(this, owner, ZERO_ADDRESS, tokenId, { from: owner }), - 'ERC721: transfer to the zero address', + 'ERC721InvalidReceiver', + [ZERO_ADDRESS], ); }); }); @@ -228,7 +232,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a valid receiver contract', function () { beforeEach(async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.None); + this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); this.toWhom = this.receiver.address; }); @@ -258,9 +262,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('with an invalid token id', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( transferFun.call(this, owner, this.receiver.address, nonExistentTokenId, { from: owner }), - 'ERC721: invalid token ID', + 'ERC721NonexistentToken', + [nonExistentTokenId], ); }); }); @@ -277,17 +282,18 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a receiver contract returning unexpected value', function () { it('reverts', async function () { - const invalidReceiver = await ERC721ReceiverMock.new('0x42', Error.None); - await expectRevert( + const invalidReceiver = await ERC721ReceiverMock.new('0x42', RevertType.None); + await expectRevertCustomError( this.token.safeTransferFrom(owner, invalidReceiver.address, tokenId, { from: owner }), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [invalidReceiver.address], ); }); }); describe('to a receiver contract that reverts with message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithMessage); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); await expectRevert( this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), 'ERC721ReceiverMock: reverting', @@ -297,17 +303,35 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a receiver contract that reverts without message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithoutMessage); - await expectRevert( + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithoutMessage, + ); + await expectRevertCustomError( this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [revertingReceiver.address], + ); + }); + }); + + describe('to a receiver contract that reverts with custom error', function () { + it('reverts', async function () { + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithCustomError, + ); + await expectRevertCustomError( + this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), + 'CustomError', + [RECEIVER_MAGIC_VALUE], ); }); }); describe('to a receiver contract that panics', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.Panic); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); await expectRevert.unspecified( this.token.safeTransferFrom(owner, revertingReceiver.address, tokenId, { from: owner }), ); @@ -316,10 +340,11 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('to a contract that does not implement the required function', function () { it('reverts', async function () { - const nonReceiver = this.token; - await expectRevert( + const nonReceiver = await NonERC721ReceiverMock.new(); + await expectRevertCustomError( this.token.safeTransferFrom(owner, nonReceiver.address, tokenId, { from: owner }), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [nonReceiver.address], ); }); }); @@ -333,7 +358,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('via safeMint', function () { // regular minting is tested in ERC721Mintable.test.js and others it('calls onERC721Received — with data', async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.None); + this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); const receipt = await this.token.$_safeMint(this.receiver.address, tokenId, data); await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { @@ -344,7 +369,7 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA }); it('calls onERC721Received — without data', async function () { - this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.None); + this.receiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.None); const receipt = await this.token.$_safeMint(this.receiver.address, tokenId); await expectEvent.inTransaction(receipt.tx, ERC721ReceiverMock, 'Received', { @@ -355,17 +380,18 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('to a receiver contract returning unexpected value', function () { it('reverts', async function () { - const invalidReceiver = await ERC721ReceiverMock.new('0x42', Error.None); - await expectRevert( + const invalidReceiver = await ERC721ReceiverMock.new('0x42', RevertType.None); + await expectRevertCustomError( this.token.$_safeMint(invalidReceiver.address, tokenId), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [invalidReceiver.address], ); }); }); context('to a receiver contract that reverts with message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithMessage); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.RevertWithMessage); await expectRevert( this.token.$_safeMint(revertingReceiver.address, tokenId), 'ERC721ReceiverMock: reverting', @@ -375,27 +401,44 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('to a receiver contract that reverts without message', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.RevertWithoutMessage); - await expectRevert( - this.token.$_safeMint(revertingReceiver.address, tokenId), - 'ERC721: transfer to non ERC721Receiver implementer', + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithoutMessage, ); + await expectRevertCustomError( + this.token.$_safeMint(revertingReceiver.address, tokenId), + 'ERC721InvalidReceiver', + [revertingReceiver.address], + ); + }); + }); + + context('to a receiver contract that reverts with custom error', function () { + it('reverts', async function () { + const revertingReceiver = await ERC721ReceiverMock.new( + RECEIVER_MAGIC_VALUE, + RevertType.RevertWithCustomError, + ); + await expectRevertCustomError(this.token.$_safeMint(revertingReceiver.address, tokenId), 'CustomError', [ + RECEIVER_MAGIC_VALUE, + ]); }); }); context('to a receiver contract that panics', function () { it('reverts', async function () { - const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, Error.Panic); + const revertingReceiver = await ERC721ReceiverMock.new(RECEIVER_MAGIC_VALUE, RevertType.Panic); await expectRevert.unspecified(this.token.$_safeMint(revertingReceiver.address, tokenId)); }); }); context('to a contract that does not implement the required function', function () { it('reverts', async function () { - const nonReceiver = this.token; - await expectRevert( + const nonReceiver = await NonERC721ReceiverMock.new(); + await expectRevertCustomError( this.token.$_safeMint(nonReceiver.address, tokenId), - 'ERC721: transfer to non ERC721Receiver implementer', + 'ERC721InvalidReceiver', + [nonReceiver.address], ); }); }); @@ -483,15 +526,18 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the address that receives the approval is the owner', function () { it('reverts', async function () { - await expectRevert(this.token.approve(owner, tokenId, { from: owner }), 'ERC721: approval to current owner'); + await expectRevertCustomError(this.token.approve(owner, tokenId, { from: owner }), 'ERC721InvalidOperator', [ + owner, + ]); }); }); context('when the sender does not own the given token ID', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.approve(approved, tokenId, { from: other }), - 'ERC721: approve caller is not token owner or approved', + 'ERC721InvalidApprover', + [other], ); }); }); @@ -499,9 +545,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the sender is approved for the given token ID', function () { it('reverts', async function () { await this.token.approve(approved, tokenId, { from: owner }); - await expectRevert( + await expectRevertCustomError( this.token.approve(anotherApproved, tokenId, { from: approved }), - 'ERC721: approve caller is not token owner or approved for all', + 'ERC721InvalidApprover', + [approved], ); }); }); @@ -518,9 +565,10 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the given token ID does not exist', function () { it('reverts', async function () { - await expectRevert( + await expectRevertCustomError( this.token.approve(approved, nonExistentTokenId, { from: operator }), - 'ERC721: invalid token ID', + 'ERC721NonexistentToken', + [nonExistentTokenId], ); }); }); @@ -599,7 +647,11 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA context('when the operator is the owner', function () { it('reverts', async function () { - await expectRevert(this.token.setApprovalForAll(owner, true, { from: owner }), 'ERC721: approve to caller'); + await expectRevertCustomError( + this.token.setApprovalForAll(owner, true, { from: owner }), + 'ERC721InvalidOperator', + [owner], + ); }); }); }); @@ -607,7 +659,9 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('getApproved', async function () { context('when token is not minted', async function () { it('reverts', async function () { - await expectRevert(this.token.getApproved(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.getApproved(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); }); @@ -631,7 +685,9 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA describe('_mint(address, uint256)', function () { it('reverts with a null destination address', async function () { - await expectRevert(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721: mint to the zero address'); + await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721InvalidReceiver', [ + ZERO_ADDRESS, + ]); }); context('with minted token', async function () { @@ -649,14 +705,16 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA }); it('reverts when adding a token id that already exists', async function () { - await expectRevert(this.token.$_mint(owner, firstTokenId), 'ERC721: token already minted'); + await expectRevertCustomError(this.token.$_mint(owner, firstTokenId), 'ERC721InvalidSender', [ZERO_ADDRESS]); }); }); }); describe('_burn', function () { it('reverts when burning a non-existent token id', async function () { - await expectRevert(this.token.$_burn(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.$_burn(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); context('with minted tokens', function () { @@ -676,18 +734,18 @@ function shouldBehaveLikeERC721(errorPrefix, owner, newOwner, approved, anotherA it('deletes the token', async function () { expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); - await expectRevert(this.token.ownerOf(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); it('reverts when burning a token id that has been deleted', async function () { - await expectRevert(this.token.$_burn(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.$_burn(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); }); }); }); } -function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved, anotherApproved, operator, other) { +function shouldBehaveLikeERC721Enumerable(owner, newOwner, approved, anotherApproved, operator, other) { shouldSupportInterfaces(['ERC721Enumerable']); context('with minted tokens', function () { @@ -712,13 +770,13 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved describe('when the index is greater than or equal to the total tokens owned by the given address', function () { it('reverts', async function () { - await expectRevert(this.token.tokenOfOwnerByIndex(owner, 2), 'ERC721Enumerable: owner index out of bounds'); + await expectRevertCustomError(this.token.tokenOfOwnerByIndex(owner, 2), 'ERC721OutOfBoundsIndex', [owner, 2]); }); }); describe('when the given address does not own any token', function () { it('reverts', async function () { - await expectRevert(this.token.tokenOfOwnerByIndex(other, 0), 'ERC721Enumerable: owner index out of bounds'); + await expectRevertCustomError(this.token.tokenOfOwnerByIndex(other, 0), 'ERC721OutOfBoundsIndex', [other, 0]); }); }); @@ -739,7 +797,7 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved it('returns empty collection for original owner', async function () { expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('0'); - await expectRevert(this.token.tokenOfOwnerByIndex(owner, 0), 'ERC721Enumerable: owner index out of bounds'); + await expectRevertCustomError(this.token.tokenOfOwnerByIndex(owner, 0), 'ERC721OutOfBoundsIndex', [owner, 0]); }); }); }); @@ -754,7 +812,7 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved }); it('reverts if index is greater than supply', async function () { - await expectRevert(this.token.tokenByIndex(2), 'ERC721Enumerable: global index out of bounds'); + await expectRevertCustomError(this.token.tokenByIndex(2), 'ERC721OutOfBoundsIndex', [ZERO_ADDRESS, 2]); }); [firstTokenId, secondTokenId].forEach(function (tokenId) { @@ -780,7 +838,9 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved describe('_mint(address, uint256)', function () { it('reverts with a null destination address', async function () { - await expectRevert(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721: mint to the zero address'); + await expectRevertCustomError(this.token.$_mint(ZERO_ADDRESS, firstTokenId), 'ERC721InvalidReceiver', [ + ZERO_ADDRESS, + ]); }); context('with minted token', async function () { @@ -800,7 +860,7 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved describe('_burn', function () { it('reverts when burning a non-existent token id', async function () { - await expectRevert(this.token.$_burn(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.$_burn(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); context('with minted tokens', function () { @@ -825,14 +885,14 @@ function shouldBehaveLikeERC721Enumerable(errorPrefix, owner, newOwner, approved it('burns all tokens', async function () { await this.token.$_burn(secondTokenId, { from: owner }); expect(await this.token.totalSupply()).to.be.bignumber.equal('0'); - await expectRevert(this.token.tokenByIndex(0), 'ERC721Enumerable: global index out of bounds'); + await expectRevertCustomError(this.token.tokenByIndex(0), 'ERC721OutOfBoundsIndex', [ZERO_ADDRESS, 0]); }); }); }); }); } -function shouldBehaveLikeERC721Metadata(errorPrefix, name, symbol, owner) { +function shouldBehaveLikeERC721Metadata(name, symbol, owner) { shouldSupportInterfaces(['ERC721Metadata']); describe('metadata', function () { @@ -854,7 +914,9 @@ function shouldBehaveLikeERC721Metadata(errorPrefix, name, symbol, owner) { }); it('reverts when queried for non existent token id', async function () { - await expectRevert(this.token.tokenURI(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); describe('base URI', function () { diff --git a/test/token/ERC721/ERC721.test.js b/test/token/ERC721/ERC721.test.js index 312430cb9..372dd5069 100644 --- a/test/token/ERC721/ERC721.test.js +++ b/test/token/ERC721/ERC721.test.js @@ -10,6 +10,6 @@ contract('ERC721', function (accounts) { this.token = await ERC721.new(name, symbol); }); - shouldBehaveLikeERC721('ERC721', ...accounts); - shouldBehaveLikeERC721Metadata('ERC721', name, symbol, ...accounts); + shouldBehaveLikeERC721(...accounts); + shouldBehaveLikeERC721Metadata(name, symbol, ...accounts); }); diff --git a/test/token/ERC721/ERC721Enumerable.test.js b/test/token/ERC721/ERC721Enumerable.test.js index b32f22dd6..31c28d177 100644 --- a/test/token/ERC721/ERC721Enumerable.test.js +++ b/test/token/ERC721/ERC721Enumerable.test.js @@ -14,7 +14,7 @@ contract('ERC721Enumerable', function (accounts) { this.token = await ERC721Enumerable.new(name, symbol); }); - shouldBehaveLikeERC721('ERC721', ...accounts); - shouldBehaveLikeERC721Metadata('ERC721', name, symbol, ...accounts); - shouldBehaveLikeERC721Enumerable('ERC721', ...accounts); + shouldBehaveLikeERC721(...accounts); + shouldBehaveLikeERC721Metadata(name, symbol, ...accounts); + shouldBehaveLikeERC721Enumerable(...accounts); }); diff --git a/test/token/ERC721/extensions/ERC721Burnable.test.js b/test/token/ERC721/extensions/ERC721Burnable.test.js index 6a4bc6dbc..df059e090 100644 --- a/test/token/ERC721/extensions/ERC721Burnable.test.js +++ b/test/token/ERC721/extensions/ERC721Burnable.test.js @@ -1,11 +1,12 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC721Burnable = artifacts.require('$ERC721Burnable'); contract('ERC721Burnable', function (accounts) { - const [owner, approved] = accounts; + const [owner, approved, another] = accounts; const firstTokenId = new BN(1); const secondTokenId = new BN(2); @@ -34,7 +35,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: invalid token ID'); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); expect(await this.token.balanceOf(owner)).to.be.bignumber.equal('1'); }); @@ -55,14 +56,25 @@ contract('ERC721Burnable', function (accounts) { context('getApproved', function () { it('reverts', async function () { - await expectRevert(this.token.getApproved(tokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.getApproved(tokenId), 'ERC721NonexistentToken', [tokenId]); }); }); }); + describe('when there is no previous approval burned', function () { + it('reverts', async function () { + await expectRevertCustomError(this.token.burn(tokenId, { from: another }), 'ERC721InsufficientApproval', [ + another, + tokenId, + ]); + }); + }); + 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: invalid token ID'); + await expectRevertCustomError(this.token.burn(unknownTokenId, { from: owner }), 'ERC721NonexistentToken', [ + unknownTokenId, + ]); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Consecutive.t.sol b/test/token/ERC721/extensions/ERC721Consecutive.t.sol new file mode 100644 index 000000000..896a5ef57 --- /dev/null +++ b/test/token/ERC721/extensions/ERC721Consecutive.t.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +// solhint-disable func-name-mixedcase + +import "../../../../contracts/token/ERC721/extensions/ERC721Consecutive.sol"; +import "forge-std/Test.sol"; + +function toSingleton(address account) pure returns (address[] memory) { + address[] memory accounts = new address[](1); + accounts[0] = account; + return accounts; +} + +contract ERC721ConsecutiveTarget is StdUtils, ERC721Consecutive { + uint96 private immutable _offset; + uint256 public totalMinted = 0; + + constructor(address[] memory receivers, uint256[] memory batches, uint256 startingId) ERC721("", "") { + _offset = uint96(startingId); + for (uint256 i = 0; i < batches.length; i++) { + address receiver = receivers[i % receivers.length]; + uint96 batchSize = uint96(bound(batches[i], 0, _maxBatchSize())); + _mintConsecutive(receiver, batchSize); + totalMinted += batchSize; + } + } + + function burn(uint256 tokenId) public { + _burn(tokenId); + } + + function _firstConsecutiveId() internal view virtual override returns (uint96) { + return _offset; + } +} + +contract ERC721ConsecutiveTest is Test { + function test_balance(address receiver, uint256[] calldata batches, uint96 startingId) public { + vm.assume(receiver != address(0)); + + uint256 startingTokenId = bound(startingId, 0, 5000); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId); + + assertEq(token.balanceOf(receiver), token.totalMinted()); + } + + function test_ownership( + address receiver, + uint256[] calldata batches, + uint256[2] calldata unboundedTokenId, + uint96 startingId + ) public { + vm.assume(receiver != address(0)); + + uint256 startingTokenId = bound(startingId, 0, 5000); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId); + + if (token.totalMinted() > 0) { + uint256 validTokenId = bound( + unboundedTokenId[0], + startingTokenId, + startingTokenId + token.totalMinted() - 1 + ); + assertEq(token.ownerOf(validTokenId), receiver); + } + + uint256 invalidTokenId = bound( + unboundedTokenId[1], + startingTokenId + token.totalMinted(), + startingTokenId + token.totalMinted() + 1 + ); + vm.expectRevert(); + token.ownerOf(invalidTokenId); + } + + function test_burn( + address receiver, + uint256[] calldata batches, + uint256 unboundedTokenId, + uint96 startingId + ) public { + vm.assume(receiver != address(0)); + + uint256 startingTokenId = bound(startingId, 0, 5000); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId); + + // only test if we minted at least one token + uint256 supply = token.totalMinted(); + vm.assume(supply > 0); + + // burn a token in [0; supply[ + uint256 tokenId = bound(unboundedTokenId, startingTokenId, startingTokenId + supply - 1); + token.burn(tokenId); + + // balance should have decreased + assertEq(token.balanceOf(receiver), supply - 1); + + // token should be burnt + vm.expectRevert(); + token.ownerOf(tokenId); + } + + function test_transfer( + address[2] calldata accounts, + uint256[2] calldata unboundedBatches, + uint256[2] calldata unboundedTokenId, + uint96 startingId + ) public { + vm.assume(accounts[0] != address(0)); + vm.assume(accounts[1] != address(0)); + vm.assume(accounts[0] != accounts[1]); + + uint256 startingTokenId = bound(startingId, 1, 5000); + + address[] memory receivers = new address[](2); + receivers[0] = accounts[0]; + receivers[1] = accounts[1]; + + // We assume _maxBatchSize is 5000 (the default). This test will break otherwise. + uint256[] memory batches = new uint256[](2); + batches[0] = bound(unboundedBatches[0], startingTokenId, 5000); + batches[1] = bound(unboundedBatches[1], startingTokenId, 5000); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(receivers, batches, startingTokenId); + + uint256 tokenId0 = bound(unboundedTokenId[0], startingTokenId, batches[0]); + uint256 tokenId1 = bound(unboundedTokenId[1], startingTokenId, batches[1]) + batches[0]; + + assertEq(token.ownerOf(tokenId0), accounts[0]); + assertEq(token.ownerOf(tokenId1), accounts[1]); + assertEq(token.balanceOf(accounts[0]), batches[0]); + assertEq(token.balanceOf(accounts[1]), batches[1]); + + vm.prank(accounts[0]); + token.transferFrom(accounts[0], accounts[1], tokenId0); + + assertEq(token.ownerOf(tokenId0), accounts[1]); + assertEq(token.ownerOf(tokenId1), accounts[1]); + assertEq(token.balanceOf(accounts[0]), batches[0] - 1); + assertEq(token.balanceOf(accounts[1]), batches[1] + 1); + + vm.prank(accounts[1]); + token.transferFrom(accounts[1], accounts[0], tokenId1); + + assertEq(token.ownerOf(tokenId0), accounts[1]); + assertEq(token.ownerOf(tokenId1), accounts[0]); + assertEq(token.balanceOf(accounts[0]), batches[0]); + assertEq(token.balanceOf(accounts[1]), batches[1]); + } + + function test_start_consecutive_id( + address receiver, + uint256[2] calldata unboundedBatches, + uint256[2] calldata unboundedTokenId, + uint96 startingId + ) public { + vm.assume(receiver != address(0)); + + uint256 startingTokenId = bound(startingId, 1, 5000); + + // We assume _maxBatchSize is 5000 (the default). This test will break otherwise. + uint256[] memory batches = new uint256[](2); + batches[0] = bound(unboundedBatches[0], startingTokenId, 5000); + batches[1] = bound(unboundedBatches[1], startingTokenId, 5000); + + ERC721ConsecutiveTarget token = new ERC721ConsecutiveTarget(toSingleton(receiver), batches, startingTokenId); + + uint256 tokenId0 = bound(unboundedTokenId[0], startingTokenId, batches[0]); + uint256 tokenId1 = bound(unboundedTokenId[1], startingTokenId, batches[1]); + + assertEq(token.ownerOf(tokenId0), receiver); + assertEq(token.ownerOf(tokenId1), receiver); + assertEq(token.balanceOf(receiver), batches[0] + batches[1]); + } +} diff --git a/test/token/ERC721/extensions/ERC721Consecutive.test.js b/test/token/ERC721/extensions/ERC721Consecutive.test.js index 47eb820cd..55c26dffe 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.test.js +++ b/test/token/ERC721/extensions/ERC721Consecutive.test.js @@ -1,6 +1,8 @@ -const { constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { constants, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { sum } = require('../../../helpers/math'); +const { expectRevertCustomError } = require('../../../helpers/customError'); +const { ZERO_ADDRESS } = require('@openzeppelin/test-helpers/src/constants'); const ERC721ConsecutiveMock = artifacts.require('$ERC721ConsecutiveMock'); const ERC721ConsecutiveEnumerableMock = artifacts.require('$ERC721ConsecutiveEnumerableMock'); @@ -13,190 +15,221 @@ contract('ERC721Consecutive', function (accounts) { const symbol = 'NFT'; const batches = [ { receiver: user1, amount: 0 }, - { receiver: user1, amount: 3 }, + { receiver: user1, amount: 1 }, + { receiver: user1, amount: 2 }, { receiver: user2, amount: 5 }, { receiver: user3, amount: 0 }, { receiver: user1, amount: 7 }, ]; const delegates = [user1, user3]; - describe('with valid batches', function () { - beforeEach(async function () { - this.token = await ERC721ConsecutiveMock.new( - name, - symbol, - delegates, - batches.map(({ receiver }) => receiver), - batches.map(({ amount }) => amount), - ); - }); - - describe('minting during construction', function () { - it('events are emitted at construction', async function () { - let first = 0; - - for (const batch of batches) { - if (batch.amount > 0) { - await expectEvent.inConstruction(this.token, 'ConsecutiveTransfer', { - fromTokenId: web3.utils.toBN(first), - toTokenId: web3.utils.toBN(first + batch.amount - 1), - fromAddress: constants.ZERO_ADDRESS, - toAddress: batch.receiver, - }); - } else { - // expectEvent.notEmitted.inConstruction only looks at event name, and doesn't check the parameters - } - first += batch.amount; - } - }); - - it('ownership is set', async function () { - const owners = batches.flatMap(({ receiver, amount }) => Array(amount).fill(receiver)); - - for (const tokenId in owners) { - expect(await this.token.ownerOf(tokenId)).to.be.equal(owners[tokenId]); - } - }); - - it('balance & voting power are set', async function () { - for (const account of accounts) { - const balance = sum(...batches.filter(({ receiver }) => receiver === account).map(({ amount }) => amount)); - - expect(await this.token.balanceOf(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); - - // If not delegated at construction, check before + do delegation - if (!delegates.includes(account)) { - expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(0)); - - await this.token.delegate(account, { from: account }); - } - - // At this point all accounts should have delegated - expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); - } - }); - }); - - describe('minting after construction', function () { - it('consecutive minting is not possible after construction', async function () { - await expectRevert( - this.token.$_mintConsecutive(user1, 10), - 'ERC721Consecutive: batch minting restricted to constructor', + for (const offset of [0, 1, 42]) { + describe(`with offset ${offset}`, function () { + beforeEach(async function () { + this.token = await ERC721ConsecutiveMock.new( + name, + symbol, + offset, + delegates, + batches.map(({ receiver }) => receiver), + batches.map(({ amount }) => amount), ); }); - it('simple minting is possible after construction', async function () { - const tokenId = sum(...batches.map(b => b.amount)); + describe('minting during construction', function () { + it('events are emitted at construction', async function () { + let first = offset; - expect(await this.token.$_exists(tokenId)).to.be.equal(false); + for (const batch of batches) { + if (batch.amount > 0) { + await expectEvent.inConstruction(this.token, 'ConsecutiveTransfer', { + fromTokenId: web3.utils.toBN(first), + toTokenId: web3.utils.toBN(first + batch.amount - 1), + fromAddress: constants.ZERO_ADDRESS, + toAddress: batch.receiver, + }); + } else { + // expectEvent.notEmitted.inConstruction only looks at event name, and doesn't check the parameters + } + first += batch.amount; + } + }); - expectEvent(await this.token.$_mint(user1, tokenId), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user1, - tokenId: tokenId.toString(), + it('ownership is set', async function () { + const owners = [ + ...Array(offset).fill(constants.ZERO_ADDRESS), + ...batches.flatMap(({ receiver, amount }) => Array(amount).fill(receiver)), + ]; + + for (const tokenId in owners) { + if (owners[tokenId] != constants.ZERO_ADDRESS) { + expect(await this.token.ownerOf(tokenId)).to.be.equal(owners[tokenId]); + } + } + }); + + it('balance & voting power are set', async function () { + for (const account of accounts) { + const balance = sum(...batches.filter(({ receiver }) => receiver === account).map(({ amount }) => amount)); + + expect(await this.token.balanceOf(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); + + // If not delegated at construction, check before + do delegation + if (!delegates.includes(account)) { + expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(0)); + + await this.token.delegate(account, { from: account }); + } + + // At this point all accounts should have delegated + expect(await this.token.getVotes(account)).to.be.bignumber.equal(web3.utils.toBN(balance)); + } + }); + + it('reverts on consecutive minting to the zero address', async function () { + await expectRevertCustomError( + ERC721ConsecutiveMock.new(name, symbol, offset, delegates, [ZERO_ADDRESS], [10]), + 'ERC721InvalidReceiver', + [ZERO_ADDRESS], + ); }); }); - it('cannot mint a token that has been batched minted', async function () { - const tokenId = sum(...batches.map(b => b.amount)) - 1; + describe('minting after construction', function () { + it('consecutive minting is not possible after construction', async function () { + await expectRevertCustomError(this.token.$_mintConsecutive(user1, 10), 'ERC721ForbiddenBatchMint', []); + }); - expect(await this.token.$_exists(tokenId)).to.be.equal(true); + it('simple minting is possible after construction', async function () { + const tokenId = sum(...batches.map(b => b.amount)) + offset; - await expectRevert(this.token.$_mint(user1, tokenId), 'ERC721: token already minted'); + expect(await this.token.$_exists(tokenId)).to.be.equal(false); + + expectEvent(await this.token.$_mint(user1, tokenId), 'Transfer', { + from: constants.ZERO_ADDRESS, + to: user1, + tokenId: tokenId.toString(), + }); + }); + + it('cannot mint a token that has been batched minted', async function () { + const tokenId = sum(...batches.map(b => b.amount)) + offset - 1; + + expect(await this.token.$_exists(tokenId)).to.be.equal(true); + + await expectRevertCustomError(this.token.$_mint(user1, tokenId), 'ERC721InvalidSender', [ZERO_ADDRESS]); + }); + }); + + describe('ERC721 behavior', function () { + const tokenId = web3.utils.toBN(offset + 1); + + it('core takes over ownership on transfer', async function () { + await this.token.transferFrom(user1, receiver, tokenId, { from: user1 }); + + expect(await this.token.ownerOf(tokenId)).to.be.equal(receiver); + }); + + it('tokens can be burned and re-minted #1', async function () { + expectEvent(await this.token.$_burn(tokenId, { from: user1 }), 'Transfer', { + from: user1, + to: constants.ZERO_ADDRESS, + tokenId, + }); + + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + + expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { + from: constants.ZERO_ADDRESS, + to: user2, + tokenId, + }); + + expect(await this.token.ownerOf(tokenId)).to.be.equal(user2); + }); + + it('tokens can be burned and re-minted #2', async function () { + const tokenId = web3.utils.toBN(sum(...batches.map(({ amount }) => amount)) + offset); + + expect(await this.token.$_exists(tokenId)).to.be.equal(false); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + + // mint + await this.token.$_mint(user1, tokenId); + + expect(await this.token.$_exists(tokenId)).to.be.equal(true); + expect(await this.token.ownerOf(tokenId), user1); + + // burn + expectEvent(await this.token.$_burn(tokenId, { from: user1 }), 'Transfer', { + from: user1, + to: constants.ZERO_ADDRESS, + tokenId, + }); + + expect(await this.token.$_exists(tokenId)).to.be.equal(false); + await expectRevertCustomError(this.token.ownerOf(tokenId), 'ERC721NonexistentToken', [tokenId]); + + // re-mint + expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { + from: constants.ZERO_ADDRESS, + to: user2, + tokenId, + }); + + expect(await this.token.$_exists(tokenId)).to.be.equal(true); + expect(await this.token.ownerOf(tokenId), user2); + }); + + it('reverts burning batches of size != 1', async function () { + const tokenId = batches[0].amount + offset; + const receiver = batches[0].receiver; + + await expectRevertCustomError( + this.token.$_afterTokenTransfer(receiver, ZERO_ADDRESS, tokenId, 2), + 'ERC721ForbiddenBatchBurn', + [], + ); + }); }); }); - - describe('ERC721 behavior', function () { - it('core takes over ownership on transfer', async function () { - await this.token.transferFrom(user1, receiver, 1, { from: user1 }); - - expect(await this.token.ownerOf(1)).to.be.equal(receiver); - }); - - it('tokens can be burned and re-minted #1', async function () { - expectEvent(await this.token.$_burn(1, { from: user1 }), 'Transfer', { - from: user1, - to: constants.ZERO_ADDRESS, - tokenId: '1', - }); - - await expectRevert(this.token.ownerOf(1), 'ERC721: invalid token ID'); - - expectEvent(await this.token.$_mint(user2, 1), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user2, - tokenId: '1', - }); - - expect(await this.token.ownerOf(1)).to.be.equal(user2); - }); - - it('tokens can be burned and re-minted #2', async function () { - const tokenId = web3.utils.toBN(sum(...batches.map(({ amount }) => amount))); - - expect(await this.token.$_exists(tokenId)).to.be.equal(false); - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); - - // mint - await this.token.$_mint(user1, tokenId); - - expect(await this.token.$_exists(tokenId)).to.be.equal(true); - expect(await this.token.ownerOf(tokenId), user1); - - // burn - expectEvent(await this.token.$_burn(tokenId, { from: user1 }), 'Transfer', { - from: user1, - to: constants.ZERO_ADDRESS, - tokenId, - }); - - expect(await this.token.$_exists(tokenId)).to.be.equal(false); - await expectRevert(this.token.ownerOf(tokenId), 'ERC721: invalid token ID'); - - // re-mint - expectEvent(await this.token.$_mint(user2, tokenId), 'Transfer', { - from: constants.ZERO_ADDRESS, - to: user2, - tokenId, - }); - - expect(await this.token.$_exists(tokenId)).to.be.equal(true); - expect(await this.token.ownerOf(tokenId), user2); - }); - }); - }); + } describe('invalid use', function () { it('cannot mint a batch larger than 5000', async function () { - await expectRevert( - ERC721ConsecutiveMock.new(name, symbol, [], [user1], ['5001']), - 'ERC721Consecutive: batch too large', + await expectRevertCustomError( + ERC721ConsecutiveMock.new(name, symbol, 0, [], [user1], ['5001']), + 'ERC721ExceededMaxBatchMint', + [5000, 5001], ); }); it('cannot use single minting during construction', async function () { - await expectRevert( + await expectRevertCustomError( ERC721ConsecutiveNoConstructorMintMock.new(name, symbol), - "ERC721Consecutive: can't mint during construction", + 'ERC721ForbiddenMint', + [], ); }); it('cannot use single minting during construction', async function () { - await expectRevert( + await expectRevertCustomError( ERC721ConsecutiveNoConstructorMintMock.new(name, symbol), - "ERC721Consecutive: can't mint during construction", + 'ERC721ForbiddenMint', + [], ); }); it('consecutive mint not compatible with enumerability', async function () { - await expectRevert( + await expectRevertCustomError( ERC721ConsecutiveEnumerableMock.new( name, symbol, batches.map(({ receiver }) => receiver), batches.map(({ amount }) => amount), ), - 'ERC721Enumerable: consecutive transfers not supported', + 'ERC721EnumerableForbiddenBatchMint', + [], ); }); }); diff --git a/test/token/ERC721/extensions/ERC721Pausable.test.js b/test/token/ERC721/extensions/ERC721Pausable.test.js index c7fc8233f..ec99dea96 100644 --- a/test/token/ERC721/extensions/ERC721Pausable.test.js +++ b/test/token/ERC721/extensions/ERC721Pausable.test.js @@ -1,6 +1,7 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../../helpers/customError'); const ERC721Pausable = artifacts.require('$ERC721Pausable'); @@ -26,34 +27,37 @@ contract('ERC721Pausable', function (accounts) { }); it('reverts when trying to transferFrom', async function () { - await expectRevert( + await expectRevertCustomError( this.token.transferFrom(owner, receiver, firstTokenId, { from: owner }), - 'ERC721Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeTransferFrom', async function () { - await expectRevert( + await expectRevertCustomError( this.token.safeTransferFrom(owner, receiver, firstTokenId, { from: owner }), - 'ERC721Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to safeTransferFrom with data', async function () { - await expectRevert( + await expectRevertCustomError( this.token.methods['safeTransferFrom(address,address,uint256,bytes)'](owner, receiver, firstTokenId, mockData, { from: owner, }), - 'ERC721Pausable: token transfer while paused', + 'EnforcedPause', + [], ); }); it('reverts when trying to mint', async function () { - await expectRevert(this.token.$_mint(receiver, secondTokenId), 'ERC721Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_mint(receiver, secondTokenId), 'EnforcedPause', []); }); it('reverts when trying to burn', async function () { - await expectRevert(this.token.$_burn(firstTokenId), 'ERC721Pausable: token transfer while paused'); + await expectRevertCustomError(this.token.$_burn(firstTokenId), 'EnforcedPause', []); }); describe('getApproved', function () { diff --git a/test/token/ERC721/extensions/ERC721URIStorage.test.js b/test/token/ERC721/extensions/ERC721URIStorage.test.js index c0274c669..34738cae1 100644 --- a/test/token/ERC721/extensions/ERC721URIStorage.test.js +++ b/test/token/ERC721/extensions/ERC721URIStorage.test.js @@ -1,7 +1,9 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); - +const { BN, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); + const ERC721URIStorageMock = artifacts.require('$ERC721URIStorageMock'); contract('ERC721URIStorage', function (accounts) { @@ -17,6 +19,8 @@ contract('ERC721URIStorage', function (accounts) { this.token = await ERC721URIStorageMock.new(name, symbol); }); + shouldSupportInterfaces(['0x49064906']); + describe('token URI', function () { beforeEach(async function () { await this.token.$_mint(owner, firstTokenId); @@ -30,7 +34,9 @@ contract('ERC721URIStorage', function (accounts) { }); it('reverts when queried for non existent token id', async function () { - await expectRevert(this.token.tokenURI(nonExistentTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(nonExistentTokenId), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); it('can be set for a token id', async function () { @@ -38,11 +44,16 @@ contract('ERC721URIStorage', function (accounts) { expect(await this.token.tokenURI(firstTokenId)).to.be.equal(sampleUri); }); + it('setting the uri emits an event', async function () { + expectEvent(await this.token.$_setTokenURI(firstTokenId, sampleUri), 'MetadataUpdate', { + _tokenId: firstTokenId, + }); + }); + it('reverts when setting for non existent token id', async function () { - await expectRevert( - this.token.$_setTokenURI(nonExistentTokenId, sampleUri), - 'ERC721URIStorage: URI set of nonexistent token', - ); + await expectRevertCustomError(this.token.$_setTokenURI(nonExistentTokenId, sampleUri), 'ERC721NonexistentToken', [ + nonExistentTokenId, + ]); }); it('base URI can be set', async function () { @@ -76,7 +87,7 @@ contract('ERC721URIStorage', function (accounts) { await this.token.$_burn(firstTokenId, { from: owner }); expect(await this.token.$_exists(firstTokenId)).to.equal(false); - await expectRevert(this.token.tokenURI(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); it('tokens with URI can be burnt ', async function () { @@ -85,7 +96,7 @@ contract('ERC721URIStorage', function (accounts) { await this.token.$_burn(firstTokenId, { from: owner }); expect(await this.token.$_exists(firstTokenId)).to.equal(false); - await expectRevert(this.token.tokenURI(firstTokenId), 'ERC721: invalid token ID'); + await expectRevertCustomError(this.token.tokenURI(firstTokenId), 'ERC721NonexistentToken', [firstTokenId]); }); }); }); diff --git a/test/token/ERC721/extensions/ERC721Votes.test.js b/test/token/ERC721/extensions/ERC721Votes.test.js index 07fda5d22..caa44ea8b 100644 --- a/test/token/ERC721/extensions/ERC721Votes.test.js +++ b/test/token/ERC721/extensions/ERC721Votes.test.js @@ -3,173 +3,181 @@ const { expectEvent, time } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); -const { getChainId } = require('../../../helpers/chainid'); +const { clock, clockFromReceipt } = require('../../../helpers/time'); const { shouldBehaveLikeVotes } = require('../../../governance/utils/Votes.behavior'); -const ERC721Votes = artifacts.require('$ERC721Votes'); +const MODES = { + blocknumber: artifacts.require('$ERC721Votes'), + // no timestamp mode for ERC721Votes yet +}; contract('ERC721Votes', function (accounts) { - const [account1, account2, account1Delegatee, other1, other2] = accounts; + const [account1, account2, other1, other2] = accounts; + const name = 'My Vote'; const symbol = 'MTKN'; + const version = '1'; const tokens = ['10000000000000000000000000', '10', '20', '30'].map(n => web3.utils.toBN(n)); - beforeEach(async function () { - this.chainId = await getChainId(); + for (const [mode, artifact] of Object.entries(MODES)) { + describe(`vote with ${mode}`, function () { + beforeEach(async function () { + this.votes = await artifact.new(name, symbol, name, version); + }); - this.votes = await ERC721Votes.new(name, symbol, name, '1'); - }); + // includes EIP6372 behavior check + shouldBehaveLikeVotes(accounts, tokens, { mode, fungible: false }); - describe('balanceOf', function () { - beforeEach(async function () { - await this.votes.$_mint(account1, tokens[0]); - await this.votes.$_mint(account1, tokens[1]); - await this.votes.$_mint(account1, tokens[2]); - await this.votes.$_mint(account1, tokens[3]); + describe('balanceOf', function () { + beforeEach(async function () { + await this.votes.$_mint(account1, tokens[0]); + await this.votes.$_mint(account1, tokens[1]); + await this.votes.$_mint(account1, tokens[2]); + await this.votes.$_mint(account1, tokens[3]); + }); + + it('grants to initial account', async function () { + expect(await this.votes.balanceOf(account1)).to.be.bignumber.equal('4'); + }); + }); + + describe('transfers', function () { + beforeEach(async function () { + await this.votes.$_mint(account1, tokens[0]); + }); + + it('no delegation', async function () { + const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); + expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); + expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); + + this.account1Votes = '0'; + this.account2Votes = '0'; + }); + + it('sender delegation', async function () { + await this.votes.delegate(account1, { from: account1 }); + + const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); + expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousBalance: '1', newBalance: '0' }); + + const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); + expect( + receipt.logs + .filter(({ event }) => event == 'DelegateVotesChanged') + .every(({ logIndex }) => transferLogIndex < logIndex), + ).to.be.equal(true); + + this.account1Votes = '0'; + this.account2Votes = '0'; + }); + + it('receiver delegation', async function () { + await this.votes.delegate(account2, { from: account2 }); + + const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); + expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, previousBalance: '0', newBalance: '1' }); + + const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); + expect( + receipt.logs + .filter(({ event }) => event == 'DelegateVotesChanged') + .every(({ logIndex }) => transferLogIndex < logIndex), + ).to.be.equal(true); + + this.account1Votes = '0'; + this.account2Votes = '1'; + }); + + it('full delegation', async function () { + await this.votes.delegate(account1, { from: account1 }); + await this.votes.delegate(account2, { from: account2 }); + + const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); + expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousBalance: '1', newBalance: '0' }); + expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, previousBalance: '0', newBalance: '1' }); + + const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); + expect( + receipt.logs + .filter(({ event }) => event == 'DelegateVotesChanged') + .every(({ logIndex }) => transferLogIndex < logIndex), + ).to.be.equal(true); + + this.account1Votes = '0'; + this.account2Votes = '1'; + }); + + it('returns the same total supply on transfers', async function () { + await this.votes.delegate(account1, { from: account1 }); + + const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); + const timepoint = await clockFromReceipt[mode](receipt); + + await time.advanceBlock(); + await time.advanceBlock(); + + expect(await this.votes.getPastTotalSupply(timepoint - 1)).to.be.bignumber.equal('1'); + expect(await this.votes.getPastTotalSupply(timepoint + 1)).to.be.bignumber.equal('1'); + + this.account1Votes = '0'; + this.account2Votes = '0'; + }); + + it('generally returns the voting balance at the appropriate checkpoint', async function () { + await this.votes.$_mint(account1, tokens[1]); + await this.votes.$_mint(account1, tokens[2]); + await this.votes.$_mint(account1, tokens[3]); + + const total = await this.votes.balanceOf(account1); + + const t1 = await this.votes.delegate(other1, { from: account1 }); + await time.advanceBlock(); + await time.advanceBlock(); + const t2 = await this.votes.transferFrom(account1, other2, tokens[0], { from: account1 }); + await time.advanceBlock(); + await time.advanceBlock(); + const t3 = await this.votes.transferFrom(account1, other2, tokens[2], { from: account1 }); + await time.advanceBlock(); + await time.advanceBlock(); + const t4 = await this.votes.transferFrom(other2, account1, tokens[2], { from: other2 }); + await time.advanceBlock(); + await time.advanceBlock(); + + t1.timepoint = await clockFromReceipt[mode](t1.receipt); + t2.timepoint = await clockFromReceipt[mode](t2.receipt); + t3.timepoint = await clockFromReceipt[mode](t3.receipt); + t4.timepoint = await clockFromReceipt[mode](t4.receipt); + + expect(await this.votes.getPastVotes(other1, t1.timepoint - 1)).to.be.bignumber.equal('0'); + expect(await this.votes.getPastVotes(other1, t1.timepoint)).to.be.bignumber.equal(total); + expect(await this.votes.getPastVotes(other1, t1.timepoint + 1)).to.be.bignumber.equal(total); + expect(await this.votes.getPastVotes(other1, t2.timepoint)).to.be.bignumber.equal('3'); + expect(await this.votes.getPastVotes(other1, t2.timepoint + 1)).to.be.bignumber.equal('3'); + expect(await this.votes.getPastVotes(other1, t3.timepoint)).to.be.bignumber.equal('2'); + expect(await this.votes.getPastVotes(other1, t3.timepoint + 1)).to.be.bignumber.equal('2'); + expect(await this.votes.getPastVotes(other1, t4.timepoint)).to.be.bignumber.equal('3'); + expect(await this.votes.getPastVotes(other1, t4.timepoint + 1)).to.be.bignumber.equal('3'); + + this.account1Votes = '0'; + this.account2Votes = '0'; + }); + + afterEach(async function () { + expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(this.account1Votes); + expect(await this.votes.getVotes(account2)).to.be.bignumber.equal(this.account2Votes); + + // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" + const timepoint = await clock[mode](); + await time.advanceBlock(); + expect(await this.votes.getPastVotes(account1, timepoint)).to.be.bignumber.equal(this.account1Votes); + expect(await this.votes.getPastVotes(account2, timepoint)).to.be.bignumber.equal(this.account2Votes); + }); + }); }); - - it('grants to initial account', async function () { - expect(await this.votes.balanceOf(account1)).to.be.bignumber.equal('4'); - }); - }); - - describe('transfers', function () { - beforeEach(async function () { - await this.votes.$_mint(account1, tokens[0]); - }); - - it('no delegation', async function () { - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent.notEmitted(receipt, 'DelegateVotesChanged'); - - this.account1Votes = '0'; - this.account2Votes = '0'; - }); - - it('sender delegation', async function () { - await this.votes.delegate(account1, { from: account1 }); - - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousBalance: '1', newBalance: '0' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.account1Votes = '0'; - this.account2Votes = '0'; - }); - - it('receiver delegation', async function () { - await this.votes.delegate(account2, { from: account2 }); - - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, previousBalance: '0', newBalance: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.account1Votes = '0'; - this.account2Votes = '1'; - }); - - it('full delegation', async function () { - await this.votes.delegate(account1, { from: account1 }); - await this.votes.delegate(account2, { from: account2 }); - - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - expectEvent(receipt, 'Transfer', { from: account1, to: account2, tokenId: tokens[0] }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account1, previousBalance: '1', newBalance: '0' }); - expectEvent(receipt, 'DelegateVotesChanged', { delegate: account2, previousBalance: '0', newBalance: '1' }); - - const { logIndex: transferLogIndex } = receipt.logs.find(({ event }) => event == 'Transfer'); - expect( - receipt.logs - .filter(({ event }) => event == 'DelegateVotesChanged') - .every(({ logIndex }) => transferLogIndex < logIndex), - ).to.be.equal(true); - - this.account1Votes = '0'; - this.account2Votes = '1'; - }); - - it('returns the same total supply on transfers', async function () { - await this.votes.delegate(account1, { from: account1 }); - - const { receipt } = await this.votes.transferFrom(account1, account2, tokens[0], { from: account1 }); - - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.votes.getPastTotalSupply(receipt.blockNumber - 1)).to.be.bignumber.equal('1'); - expect(await this.votes.getPastTotalSupply(receipt.blockNumber + 1)).to.be.bignumber.equal('1'); - - this.account1Votes = '0'; - this.account2Votes = '0'; - }); - - it('generally returns the voting balance at the appropriate checkpoint', async function () { - await this.votes.$_mint(account1, tokens[1]); - await this.votes.$_mint(account1, tokens[2]); - await this.votes.$_mint(account1, tokens[3]); - - const total = await this.votes.balanceOf(account1); - - const t1 = await this.votes.delegate(other1, { from: account1 }); - await time.advanceBlock(); - await time.advanceBlock(); - const t2 = await this.votes.transferFrom(account1, other2, tokens[0], { from: account1 }); - await time.advanceBlock(); - await time.advanceBlock(); - const t3 = await this.votes.transferFrom(account1, other2, tokens[2], { from: account1 }); - await time.advanceBlock(); - await time.advanceBlock(); - const t4 = await this.votes.transferFrom(other2, account1, tokens[2], { from: other2 }); - await time.advanceBlock(); - await time.advanceBlock(); - - expect(await this.votes.getPastVotes(other1, t1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await this.votes.getPastVotes(other1, t1.receipt.blockNumber)).to.be.bignumber.equal(total); - expect(await this.votes.getPastVotes(other1, t1.receipt.blockNumber + 1)).to.be.bignumber.equal(total); - expect(await this.votes.getPastVotes(other1, t2.receipt.blockNumber)).to.be.bignumber.equal('3'); - expect(await this.votes.getPastVotes(other1, t2.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); - expect(await this.votes.getPastVotes(other1, t3.receipt.blockNumber)).to.be.bignumber.equal('2'); - expect(await this.votes.getPastVotes(other1, t3.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); - expect(await this.votes.getPastVotes(other1, t4.receipt.blockNumber)).to.be.bignumber.equal('3'); - expect(await this.votes.getPastVotes(other1, t4.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); - - this.account1Votes = '0'; - this.account2Votes = '0'; - }); - - afterEach(async function () { - expect(await this.votes.getVotes(account1)).to.be.bignumber.equal(this.account1Votes); - expect(await this.votes.getVotes(account2)).to.be.bignumber.equal(this.account2Votes); - - // need to advance 2 blocks to see the effect of a transfer on "getPastVotes" - const blockNumber = await time.latestBlock(); - await time.advanceBlock(); - expect(await this.votes.getPastVotes(account1, blockNumber)).to.be.bignumber.equal(this.account1Votes); - expect(await this.votes.getPastVotes(account2, blockNumber)).to.be.bignumber.equal(this.account2Votes); - }); - }); - - describe('Voting workflow', function () { - beforeEach(async function () { - this.name = name; - }); - - shouldBehaveLikeVotes(accounts, tokens, false); - }); + } }); diff --git a/test/token/ERC721/extensions/ERC721Wrapper.test.js b/test/token/ERC721/extensions/ERC721Wrapper.test.js new file mode 100644 index 000000000..683997744 --- /dev/null +++ b/test/token/ERC721/extensions/ERC721Wrapper.test.js @@ -0,0 +1,289 @@ +const { BN, expectEvent, constants } = require('@openzeppelin/test-helpers'); +const { expect } = require('chai'); + +const { shouldBehaveLikeERC721 } = require('../ERC721.behavior'); +const { expectRevertCustomError } = require('../../../helpers/customError'); + +const ERC721 = artifacts.require('$ERC721'); +const ERC721Wrapper = artifacts.require('$ERC721Wrapper'); + +contract('ERC721Wrapper', function (accounts) { + const [initialHolder, anotherAccount, approvedAccount] = accounts; + + const name = 'My Token'; + const symbol = 'MTKN'; + const firstTokenId = new BN(1); + const secondTokenId = new BN(2); + + beforeEach(async function () { + this.underlying = await ERC721.new(name, symbol); + this.token = await ERC721Wrapper.new(`Wrapped ${name}`, `W${symbol}`, this.underlying.address); + + await this.underlying.$_safeMint(initialHolder, firstTokenId); + await this.underlying.$_safeMint(initialHolder, secondTokenId); + }); + + it('has a name', async function () { + expect(await this.token.name()).to.equal(`Wrapped ${name}`); + }); + + it('has a symbol', async function () { + expect(await this.token.symbol()).to.equal(`W${symbol}`); + }); + + it('has underlying', async function () { + expect(await this.token.underlying()).to.be.bignumber.equal(this.underlying.address); + }); + + describe('depositFor', function () { + it('works with token approval', async function () { + await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); + + const { tx } = await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: initialHolder, + to: this.token.address, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: initialHolder, + tokenId: firstTokenId, + }); + }); + + it('works with approval for all', async function () { + await this.underlying.setApprovalForAll(this.token.address, true, { from: initialHolder }); + + const { tx } = await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: initialHolder, + to: this.token.address, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: initialHolder, + tokenId: firstTokenId, + }); + }); + + it('works sending to another account', async function () { + await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); + + const { tx } = await this.token.depositFor(anotherAccount, [firstTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: initialHolder, + to: this.token.address, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: anotherAccount, + tokenId: firstTokenId, + }); + }); + + it('works with multiple tokens', async function () { + await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); + await this.underlying.approve(this.token.address, secondTokenId, { from: initialHolder }); + + const { tx } = await this.token.depositFor(initialHolder, [firstTokenId, secondTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: initialHolder, + to: this.token.address, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: initialHolder, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: initialHolder, + to: this.token.address, + tokenId: secondTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: initialHolder, + tokenId: secondTokenId, + }); + }); + + it('reverts with missing approval', async function () { + await expectRevertCustomError( + this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }), + 'ERC721InsufficientApproval', + [this.token.address, firstTokenId], + ); + }); + }); + + describe('withdrawTo', function () { + beforeEach(async function () { + await this.underlying.approve(this.token.address, firstTokenId, { from: initialHolder }); + await this.token.depositFor(initialHolder, [firstTokenId], { from: initialHolder }); + }); + + it('works for an owner', async function () { + const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: initialHolder, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: firstTokenId, + }); + }); + + it('works for an approved', async function () { + await this.token.approve(approvedAccount, firstTokenId, { from: initialHolder }); + + const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: approvedAccount }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: initialHolder, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: firstTokenId, + }); + }); + + it('works for an approved for all', async function () { + await this.token.setApprovalForAll(approvedAccount, true, { from: initialHolder }); + + const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId], { from: approvedAccount }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: initialHolder, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: firstTokenId, + }); + }); + + it("doesn't work for a non-owner nor approved", async function () { + await expectRevertCustomError( + this.token.withdrawTo(initialHolder, [firstTokenId], { from: anotherAccount }), + 'ERC721InsufficientApproval', + [anotherAccount, firstTokenId], + ); + }); + + it('works with multiple tokens', async function () { + await this.underlying.approve(this.token.address, secondTokenId, { from: initialHolder }); + await this.token.depositFor(initialHolder, [secondTokenId], { from: initialHolder }); + + const { tx } = await this.token.withdrawTo(initialHolder, [firstTokenId, secondTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: initialHolder, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: initialHolder, + tokenId: secondTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: secondTokenId, + }); + }); + + it('works to another account', async function () { + const { tx } = await this.token.withdrawTo(anotherAccount, [firstTokenId], { from: initialHolder }); + + await expectEvent.inTransaction(tx, this.underlying, 'Transfer', { + from: this.token.address, + to: anotherAccount, + tokenId: firstTokenId, + }); + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: initialHolder, + to: constants.ZERO_ADDRESS, + tokenId: firstTokenId, + }); + }); + }); + + describe('onERC721Received', function () { + it('only allows calls from underlying', async function () { + await expectRevertCustomError( + this.token.onERC721Received( + initialHolder, + this.token.address, + firstTokenId, + anotherAccount, // Correct data + { from: anotherAccount }, + ), + 'ERC721UnsupportedToken', + [anotherAccount], + ); + }); + + it('mints a token to from', async function () { + const { tx } = await this.underlying.safeTransferFrom(initialHolder, this.token.address, firstTokenId, { + from: initialHolder, + }); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: initialHolder, + tokenId: firstTokenId, + }); + }); + }); + + describe('_recover', function () { + it('works if there is something to recover', async function () { + // Should use `transferFrom` to avoid `onERC721Received` minting + await this.underlying.transferFrom(initialHolder, this.token.address, firstTokenId, { from: initialHolder }); + + const { tx } = await this.token.$_recover(anotherAccount, firstTokenId); + + await expectEvent.inTransaction(tx, this.token, 'Transfer', { + from: constants.ZERO_ADDRESS, + to: anotherAccount, + tokenId: firstTokenId, + }); + }); + + it('reverts if there is nothing to recover', async function () { + const owner = await this.underlying.ownerOf(firstTokenId); + await expectRevertCustomError(this.token.$_recover(initialHolder, firstTokenId), 'ERC721IncorrectOwner', [ + this.token.address, + firstTokenId, + owner, + ]); + }); + }); + + describe('ERC712 behavior', function () { + shouldBehaveLikeERC721(...accounts); + }); +}); diff --git a/test/token/ERC721/utils/ERC721Holder.test.js b/test/token/ERC721/utils/ERC721Holder.test.js index 0fd822280..4aa2b7948 100644 --- a/test/token/ERC721/utils/ERC721Holder.test.js +++ b/test/token/ERC721/utils/ERC721Holder.test.js @@ -1,6 +1,6 @@ const { expect } = require('chai'); -const ERC721Holder = artifacts.require('ERC721Holder'); +const ERC721Holder = artifacts.require('$ERC721Holder'); const ERC721 = artifacts.require('$ERC721'); contract('ERC721Holder', function (accounts) { diff --git a/test/token/ERC777/ERC777.behavior.js b/test/token/ERC777/ERC777.behavior.js deleted file mode 100644 index b1585bc91..000000000 --- a/test/token/ERC777/ERC777.behavior.js +++ /dev/null @@ -1,597 +0,0 @@ -const { BN, constants, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - -const { expect } = require('chai'); - -const ERC777SenderRecipientMock = artifacts.require('ERC777SenderRecipientMock'); - -function shouldBehaveLikeERC777DirectSendBurn(holder, recipient, data) { - shouldBehaveLikeERC777DirectSend(holder, recipient, data); - shouldBehaveLikeERC777DirectBurn(holder, data); -} - -function shouldBehaveLikeERC777OperatorSendBurn(holder, recipient, operator, data, operatorData) { - shouldBehaveLikeERC777OperatorSend(holder, recipient, operator, data, operatorData); - shouldBehaveLikeERC777OperatorBurn(holder, operator, data, operatorData); -} - -function shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, recipient, operator, data, operatorData) { - shouldBehaveLikeERC777UnauthorizedOperatorSend(holder, recipient, operator, data, operatorData); - shouldBehaveLikeERC777UnauthorizedOperatorBurn(holder, operator, data, operatorData); -} - -function shouldBehaveLikeERC777DirectSend(holder, recipient, data) { - describe('direct send', function () { - context('when the sender has tokens', function () { - shouldDirectSendTokens(holder, recipient, new BN('0'), data); - shouldDirectSendTokens(holder, recipient, new BN('1'), data); - - it('reverts when sending more than the balance', async function () { - const balance = await this.token.balanceOf(holder); - await expectRevert.unspecified(this.token.send(recipient, balance.addn(1), data, { from: holder })); - }); - - it('reverts when sending to the zero address', async function () { - await expectRevert.unspecified(this.token.send(ZERO_ADDRESS, new BN('1'), data, { from: holder })); - }); - }); - - context('when the sender has no tokens', function () { - removeBalance(holder); - - shouldDirectSendTokens(holder, recipient, new BN('0'), data); - - it('reverts when sending a non-zero amount', async function () { - await expectRevert.unspecified(this.token.send(recipient, new BN('1'), data, { from: holder })); - }); - }); - }); -} - -function shouldBehaveLikeERC777OperatorSend(holder, recipient, operator, data, operatorData) { - describe('operator send', function () { - context('when the sender has tokens', async function () { - shouldOperatorSendTokens(holder, operator, recipient, new BN('0'), data, operatorData); - shouldOperatorSendTokens(holder, operator, recipient, new BN('1'), data, operatorData); - - it('reverts when sending more than the balance', async function () { - const balance = await this.token.balanceOf(holder); - await expectRevert.unspecified( - this.token.operatorSend(holder, recipient, balance.addn(1), data, operatorData, { from: operator }), - ); - }); - - it('reverts when sending to the zero address', async function () { - await expectRevert.unspecified( - this.token.operatorSend(holder, ZERO_ADDRESS, new BN('1'), data, operatorData, { from: operator }), - ); - }); - }); - - context('when the sender has no tokens', function () { - removeBalance(holder); - - shouldOperatorSendTokens(holder, operator, recipient, new BN('0'), data, operatorData); - - it('reverts when sending a non-zero amount', async function () { - await expectRevert.unspecified( - this.token.operatorSend(holder, recipient, new BN('1'), data, operatorData, { from: operator }), - ); - }); - - it('reverts when sending from the zero address', async function () { - // This is not yet reflected in the spec - await expectRevert.unspecified( - this.token.operatorSend(ZERO_ADDRESS, recipient, new BN('0'), data, operatorData, { from: operator }), - ); - }); - }); - }); -} - -function shouldBehaveLikeERC777UnauthorizedOperatorSend(holder, recipient, operator, data, operatorData) { - describe('operator send', function () { - it('reverts', async function () { - await expectRevert.unspecified(this.token.operatorSend(holder, recipient, new BN('0'), data, operatorData)); - }); - }); -} - -function shouldBehaveLikeERC777DirectBurn(holder, data) { - describe('direct burn', function () { - context('when the sender has tokens', function () { - shouldDirectBurnTokens(holder, new BN('0'), data); - shouldDirectBurnTokens(holder, new BN('1'), data); - - it('reverts when burning more than the balance', async function () { - const balance = await this.token.balanceOf(holder); - await expectRevert.unspecified(this.token.burn(balance.addn(1), data, { from: holder })); - }); - }); - - context('when the sender has no tokens', function () { - removeBalance(holder); - - shouldDirectBurnTokens(holder, new BN('0'), data); - - it('reverts when burning a non-zero amount', async function () { - await expectRevert.unspecified(this.token.burn(new BN('1'), data, { from: holder })); - }); - }); - }); -} - -function shouldBehaveLikeERC777OperatorBurn(holder, operator, data, operatorData) { - describe('operator burn', function () { - context('when the sender has tokens', async function () { - shouldOperatorBurnTokens(holder, operator, new BN('0'), data, operatorData); - shouldOperatorBurnTokens(holder, operator, new BN('1'), data, operatorData); - - it('reverts when burning more than the balance', async function () { - const balance = await this.token.balanceOf(holder); - await expectRevert.unspecified( - this.token.operatorBurn(holder, balance.addn(1), data, operatorData, { from: operator }), - ); - }); - }); - - context('when the sender has no tokens', function () { - removeBalance(holder); - - shouldOperatorBurnTokens(holder, operator, new BN('0'), data, operatorData); - - it('reverts when burning a non-zero amount', async function () { - await expectRevert.unspecified( - this.token.operatorBurn(holder, new BN('1'), data, operatorData, { from: operator }), - ); - }); - - it('reverts when burning from the zero address', async function () { - // This is not yet reflected in the spec - await expectRevert.unspecified( - this.token.operatorBurn(ZERO_ADDRESS, new BN('0'), data, operatorData, { from: operator }), - ); - }); - }); - }); -} - -function shouldBehaveLikeERC777UnauthorizedOperatorBurn(holder, operator, data, operatorData) { - describe('operator burn', function () { - it('reverts', async function () { - await expectRevert.unspecified(this.token.operatorBurn(holder, new BN('0'), data, operatorData)); - }); - }); -} - -function shouldDirectSendTokens(from, to, amount, data) { - shouldSendTokens(from, null, to, amount, data, null); -} - -function shouldOperatorSendTokens(from, operator, to, amount, data, operatorData) { - shouldSendTokens(from, operator, to, amount, data, operatorData); -} - -function shouldSendTokens(from, operator, to, amount, data, operatorData) { - const operatorCall = operator !== null; - - it(`${operatorCall ? 'operator ' : ''}can send an amount of ${amount}`, async function () { - const initialTotalSupply = await this.token.totalSupply(); - const initialFromBalance = await this.token.balanceOf(from); - const initialToBalance = await this.token.balanceOf(to); - - let receipt; - if (!operatorCall) { - receipt = await this.token.send(to, amount, data, { from }); - expectEvent(receipt, 'Sent', { - operator: from, - from, - to, - amount, - data, - operatorData: null, - }); - } else { - receipt = await this.token.operatorSend(from, to, amount, data, operatorData, { from: operator }); - expectEvent(receipt, 'Sent', { - operator, - from, - to, - amount, - data, - operatorData, - }); - } - - expectEvent(receipt, 'Transfer', { - from, - to, - value: amount, - }); - - const finalTotalSupply = await this.token.totalSupply(); - const finalFromBalance = await this.token.balanceOf(from); - const finalToBalance = await this.token.balanceOf(to); - - expect(finalTotalSupply).to.be.bignumber.equal(initialTotalSupply); - expect(finalToBalance.sub(initialToBalance)).to.be.bignumber.equal(amount); - expect(finalFromBalance.sub(initialFromBalance)).to.be.bignumber.equal(amount.neg()); - }); -} - -function shouldDirectBurnTokens(from, amount, data) { - shouldBurnTokens(from, null, amount, data, null); -} - -function shouldOperatorBurnTokens(from, operator, amount, data, operatorData) { - shouldBurnTokens(from, operator, amount, data, operatorData); -} - -function shouldBurnTokens(from, operator, amount, data, operatorData) { - const operatorCall = operator !== null; - - it(`${operatorCall ? 'operator ' : ''}can burn an amount of ${amount}`, async function () { - const initialTotalSupply = await this.token.totalSupply(); - const initialFromBalance = await this.token.balanceOf(from); - - let receipt; - if (!operatorCall) { - receipt = await this.token.burn(amount, data, { from }); - expectEvent(receipt, 'Burned', { - operator: from, - from, - amount, - data, - operatorData: null, - }); - } else { - receipt = await this.token.operatorBurn(from, amount, data, operatorData, { from: operator }); - expectEvent(receipt, 'Burned', { - operator, - from, - amount, - data, - operatorData, - }); - } - - expectEvent(receipt, 'Transfer', { - from, - to: ZERO_ADDRESS, - value: amount, - }); - - const finalTotalSupply = await this.token.totalSupply(); - const finalFromBalance = await this.token.balanceOf(from); - - expect(finalTotalSupply.sub(initialTotalSupply)).to.be.bignumber.equal(amount.neg()); - expect(finalFromBalance.sub(initialFromBalance)).to.be.bignumber.equal(amount.neg()); - }); -} - -function shouldBehaveLikeERC777InternalMint(recipient, operator, amount, data, operatorData) { - shouldInternalMintTokens(operator, recipient, new BN('0'), data, operatorData); - shouldInternalMintTokens(operator, recipient, amount, data, operatorData); - - it('reverts when minting tokens for the zero address', async function () { - await expectRevert.unspecified( - this.token.$_mint(ZERO_ADDRESS, amount, data, operatorData, true, { from: operator }), - ); - }); -} - -function shouldInternalMintTokens(operator, to, amount, data, operatorData) { - it(`can (internal) mint an amount of ${amount}`, async function () { - const initialTotalSupply = await this.token.totalSupply(); - const initialToBalance = await this.token.balanceOf(to); - - const receipt = await this.token.$_mint(to, amount, data, operatorData, true, { from: operator }); - - expectEvent(receipt, 'Minted', { - operator, - to, - amount, - data, - operatorData, - }); - - expectEvent(receipt, 'Transfer', { - from: ZERO_ADDRESS, - to, - value: amount, - }); - - const finalTotalSupply = await this.token.totalSupply(); - const finalToBalance = await this.token.balanceOf(to); - - expect(finalTotalSupply.sub(initialTotalSupply)).to.be.bignumber.equal(amount); - expect(finalToBalance.sub(initialToBalance)).to.be.bignumber.equal(amount); - }); -} - -function shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData) { - context('when TokensRecipient reverts', function () { - beforeEach(async function () { - await this.tokensRecipientImplementer.setShouldRevertReceive(true); - }); - - it('send reverts', async function () { - await expectRevert.unspecified(sendFromHolder(this.token, this.sender, this.recipient, amount, data)); - }); - - it('operatorSend reverts', async function () { - await expectRevert.unspecified( - this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator }), - ); - }); - - it('mint (internal) reverts', async function () { - await expectRevert.unspecified( - this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }), - ); - }); - }); - - context('when TokensRecipient does not revert', function () { - beforeEach(async function () { - await this.tokensRecipientImplementer.setShouldRevertSend(false); - }); - - it('TokensRecipient receives send data and is called after state mutation', async function () { - const { tx } = await sendFromHolder(this.token, this.sender, this.recipient, amount, data); - - const postSenderBalance = await this.token.balanceOf(this.sender); - const postRecipientBalance = await this.token.balanceOf(this.recipient); - - await assertTokensReceivedCalled( - this.token, - tx, - this.sender, - this.sender, - this.recipient, - amount, - data, - null, - postSenderBalance, - postRecipientBalance, - ); - }); - - it('TokensRecipient receives operatorSend data and is called after state mutation', async function () { - const { tx } = await this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { - from: operator, - }); - - const postSenderBalance = await this.token.balanceOf(this.sender); - const postRecipientBalance = await this.token.balanceOf(this.recipient); - - await assertTokensReceivedCalled( - this.token, - tx, - operator, - this.sender, - this.recipient, - amount, - data, - operatorData, - postSenderBalance, - postRecipientBalance, - ); - }); - - it('TokensRecipient receives mint (internal) data and is called after state mutation', async function () { - const { tx } = await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }); - - const postRecipientBalance = await this.token.balanceOf(this.recipient); - - await assertTokensReceivedCalled( - this.token, - tx, - operator, - ZERO_ADDRESS, - this.recipient, - amount, - data, - operatorData, - new BN('0'), - postRecipientBalance, - ); - }); - }); -} - -function shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData) { - context('when TokensSender reverts', function () { - beforeEach(async function () { - await this.tokensSenderImplementer.setShouldRevertSend(true); - }); - - it('send reverts', async function () { - await expectRevert.unspecified(sendFromHolder(this.token, this.sender, this.recipient, amount, data)); - }); - - it('operatorSend reverts', async function () { - await expectRevert.unspecified( - this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator }), - ); - }); - - it('burn reverts', async function () { - await expectRevert.unspecified(burnFromHolder(this.token, this.sender, amount, data)); - }); - - it('operatorBurn reverts', async function () { - await expectRevert.unspecified( - this.token.operatorBurn(this.sender, amount, data, operatorData, { from: operator }), - ); - }); - }); - - context('when TokensSender does not revert', function () { - beforeEach(async function () { - await this.tokensSenderImplementer.setShouldRevertSend(false); - }); - - it('TokensSender receives send data and is called before state mutation', async function () { - const preSenderBalance = await this.token.balanceOf(this.sender); - const preRecipientBalance = await this.token.balanceOf(this.recipient); - - const { tx } = await sendFromHolder(this.token, this.sender, this.recipient, amount, data); - - await assertTokensToSendCalled( - this.token, - tx, - this.sender, - this.sender, - this.recipient, - amount, - data, - null, - preSenderBalance, - preRecipientBalance, - ); - }); - - it('TokensSender receives operatorSend data and is called before state mutation', async function () { - const preSenderBalance = await this.token.balanceOf(this.sender); - const preRecipientBalance = await this.token.balanceOf(this.recipient); - - const { tx } = await this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { - from: operator, - }); - - await assertTokensToSendCalled( - this.token, - tx, - operator, - this.sender, - this.recipient, - amount, - data, - operatorData, - preSenderBalance, - preRecipientBalance, - ); - }); - - it('TokensSender receives burn data and is called before state mutation', async function () { - const preSenderBalance = await this.token.balanceOf(this.sender); - - const { tx } = await burnFromHolder(this.token, this.sender, amount, data, { from: this.sender }); - - await assertTokensToSendCalled( - this.token, - tx, - this.sender, - this.sender, - ZERO_ADDRESS, - amount, - data, - null, - preSenderBalance, - ); - }); - - it('TokensSender receives operatorBurn data and is called before state mutation', async function () { - const preSenderBalance = await this.token.balanceOf(this.sender); - - const { tx } = await this.token.operatorBurn(this.sender, amount, data, operatorData, { from: operator }); - - await assertTokensToSendCalled( - this.token, - tx, - operator, - this.sender, - ZERO_ADDRESS, - amount, - data, - operatorData, - preSenderBalance, - ); - }); - }); -} - -function removeBalance(holder) { - beforeEach(async function () { - await this.token.burn(await this.token.balanceOf(holder), '0x', { from: holder }); - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal('0'); - }); -} - -async function assertTokensReceivedCalled( - token, - txHash, - operator, - from, - to, - amount, - data, - operatorData, - fromBalance, - toBalance = '0', -) { - await expectEvent.inTransaction(txHash, ERC777SenderRecipientMock, 'TokensReceivedCalled', { - operator, - from, - to, - amount, - data, - operatorData, - token: token.address, - fromBalance, - toBalance, - }); -} - -async function assertTokensToSendCalled( - token, - txHash, - operator, - from, - to, - amount, - data, - operatorData, - fromBalance, - toBalance = '0', -) { - await expectEvent.inTransaction(txHash, ERC777SenderRecipientMock, 'TokensToSendCalled', { - operator, - from, - to, - amount, - data, - operatorData, - token: token.address, - fromBalance, - toBalance, - }); -} - -async function sendFromHolder(token, holder, to, amount, data) { - if ((await web3.eth.getCode(holder)).length <= '0x'.length) { - return token.send(to, amount, data, { from: holder }); - } else { - // assume holder is ERC777SenderRecipientMock contract - return (await ERC777SenderRecipientMock.at(holder)).send(token.address, to, amount, data); - } -} - -async function burnFromHolder(token, holder, amount, data) { - if ((await web3.eth.getCode(holder)).length <= '0x'.length) { - return token.burn(amount, data, { from: holder }); - } else { - // assume holder is ERC777SenderRecipientMock contract - return (await ERC777SenderRecipientMock.at(holder)).burn(token.address, amount, data); - } -} - -module.exports = { - shouldBehaveLikeERC777DirectSendBurn, - shouldBehaveLikeERC777OperatorSendBurn, - shouldBehaveLikeERC777UnauthorizedOperatorSendBurn, - shouldBehaveLikeERC777InternalMint, - shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook, - shouldBehaveLikeERC777SendBurnWithSendHook, -}; diff --git a/test/token/ERC777/ERC777.test.js b/test/token/ERC777/ERC777.test.js deleted file mode 100644 index 44bc25351..000000000 --- a/test/token/ERC777/ERC777.test.js +++ /dev/null @@ -1,556 +0,0 @@ -const { BN, constants, expectEvent, expectRevert, singletons } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - -const { expect } = require('chai'); - -const { - shouldBehaveLikeERC777DirectSendBurn, - shouldBehaveLikeERC777OperatorSendBurn, - shouldBehaveLikeERC777UnauthorizedOperatorSendBurn, - shouldBehaveLikeERC777InternalMint, - shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook, - shouldBehaveLikeERC777SendBurnWithSendHook, -} = require('./ERC777.behavior'); - -const { shouldBehaveLikeERC20, shouldBehaveLikeERC20Approve } = require('../ERC20/ERC20.behavior'); - -const ERC777 = artifacts.require('$ERC777Mock'); -const ERC777SenderRecipientMock = artifacts.require('$ERC777SenderRecipientMock'); - -contract('ERC777', function (accounts) { - const [registryFunder, holder, defaultOperatorA, defaultOperatorB, newOperator, anyone] = accounts; - - const initialSupply = new BN('10000'); - const name = 'ERC777Test'; - const symbol = '777T'; - const data = web3.utils.sha3('OZ777TestData'); - const operatorData = web3.utils.sha3('OZ777TestOperatorData'); - - const defaultOperators = [defaultOperatorA, defaultOperatorB]; - - beforeEach(async function () { - this.erc1820 = await singletons.ERC1820Registry(registryFunder); - }); - - context('with default operators', function () { - beforeEach(async function () { - this.token = await ERC777.new(name, symbol, defaultOperators); - await this.token.$_mint(holder, initialSupply, '0x', '0x'); - }); - - describe('as an ERC20 token', function () { - shouldBehaveLikeERC20('ERC777', initialSupply, holder, anyone, defaultOperatorA); - - describe('_approve', function () { - shouldBehaveLikeERC20Approve('ERC777', holder, anyone, initialSupply, function (owner, spender, amount) { - return this.token.$_approve(owner, spender, amount); - }); - - describe('when the owner is the zero address', function () { - it('reverts', async function () { - await expectRevert( - this.token.$_approve(ZERO_ADDRESS, anyone, initialSupply), - 'ERC777: approve from the zero address', - ); - }); - }); - }); - }); - - it('does not emit AuthorizedOperator events for default operators', async function () { - await expectEvent.notEmitted.inConstruction(this.token, 'AuthorizedOperator'); - }); - - describe('basic information', function () { - it('returns the name', async function () { - expect(await this.token.name()).to.equal(name); - }); - - it('returns the symbol', async function () { - expect(await this.token.symbol()).to.equal(symbol); - }); - - it('returns a granularity of 1', async function () { - expect(await this.token.granularity()).to.be.bignumber.equal('1'); - }); - - it('returns the default operators', async function () { - expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators); - }); - - it('default operators are operators for all accounts', async function () { - for (const operator of defaultOperators) { - expect(await this.token.isOperatorFor(operator, anyone)).to.equal(true); - } - }); - - it('returns the total supply', async function () { - expect(await this.token.totalSupply()).to.be.bignumber.equal(initialSupply); - }); - - it('returns 18 when decimals is called', async function () { - expect(await this.token.decimals()).to.be.bignumber.equal('18'); - }); - - it('the ERC777Token interface is registered in the registry', async function () { - expect( - await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC777Token')), - ).to.equal(this.token.address); - }); - - it('the ERC20Token interface is registered in the registry', async function () { - expect( - await this.erc1820.getInterfaceImplementer(this.token.address, web3.utils.soliditySha3('ERC20Token')), - ).to.equal(this.token.address); - }); - }); - - describe('balanceOf', function () { - context('for an account with no tokens', function () { - it('returns zero', async function () { - expect(await this.token.balanceOf(anyone)).to.be.bignumber.equal('0'); - }); - }); - - context('for an account with tokens', function () { - it('returns their balance', async function () { - expect(await this.token.balanceOf(holder)).to.be.bignumber.equal(initialSupply); - }); - }); - }); - - context('with no ERC777TokensSender and no ERC777TokensRecipient implementers', function () { - describe('send/burn', function () { - shouldBehaveLikeERC777DirectSendBurn(holder, anyone, data); - - context('with self operator', function () { - shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, holder, data, operatorData); - }); - - context('with first default operator', function () { - shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorA, data, operatorData); - }); - - context('with second default operator', function () { - shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, defaultOperatorB, data, operatorData); - }); - - context('before authorizing a new operator', function () { - shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData); - }); - - context('with new authorized operator', function () { - beforeEach(async function () { - await this.token.authorizeOperator(newOperator, { from: holder }); - }); - - shouldBehaveLikeERC777OperatorSendBurn(holder, anyone, newOperator, data, operatorData); - - context('with revoked operator', function () { - beforeEach(async function () { - await this.token.revokeOperator(newOperator, { from: holder }); - }); - - shouldBehaveLikeERC777UnauthorizedOperatorSendBurn(holder, anyone, newOperator, data, operatorData); - }); - }); - }); - - describe('mint (internal)', function () { - const to = anyone; - const amount = new BN('5'); - - context('with default operator', function () { - const operator = defaultOperatorA; - - shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData); - }); - - context('with non operator', function () { - const operator = newOperator; - - shouldBehaveLikeERC777InternalMint(to, operator, amount, data, operatorData); - }); - }); - - describe('mint (internal extended)', function () { - const amount = new BN('5'); - - context('to anyone', function () { - beforeEach(async function () { - this.recipient = anyone; - }); - - context('with default operator', function () { - const operator = defaultOperatorA; - - it('without requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator }); - }); - - it('with requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }); - }); - }); - - context('with non operator', function () { - const operator = newOperator; - - it('without requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator }); - }); - - it('with requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }); - }); - }); - }); - - context('to non ERC777TokensRecipient implementer', function () { - beforeEach(async function () { - this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new(); - this.recipient = this.tokensRecipientImplementer.address; - }); - - context('with default operator', function () { - const operator = defaultOperatorA; - - it('without requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator }); - }); - - it('with requireReceptionAck', async function () { - await expectRevert( - this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }), - 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient', - ); - }); - }); - - context('with non operator', function () { - const operator = newOperator; - - it('without requireReceptionAck', async function () { - await this.token.$_mint(this.recipient, amount, data, operatorData, false, { from: operator }); - }); - - it('with requireReceptionAck', async function () { - await expectRevert( - this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }), - 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient', - ); - }); - }); - }); - }); - }); - - describe('operator management', function () { - it('accounts are their own operator', async function () { - expect(await this.token.isOperatorFor(holder, holder)).to.equal(true); - }); - - it('reverts when self-authorizing', async function () { - await expectRevert( - this.token.authorizeOperator(holder, { from: holder }), - 'ERC777: authorizing self as operator', - ); - }); - - it('reverts when self-revoking', async function () { - await expectRevert(this.token.revokeOperator(holder, { from: holder }), 'ERC777: revoking self as operator'); - }); - - it('non-operators can be revoked', async function () { - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false); - - const receipt = await this.token.revokeOperator(newOperator, { from: holder }); - expectEvent(receipt, 'RevokedOperator', { operator: newOperator, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false); - }); - - it('non-operators can be authorized', async function () { - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false); - - const receipt = await this.token.authorizeOperator(newOperator, { from: holder }); - expectEvent(receipt, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(true); - }); - - describe('new operators', function () { - beforeEach(async function () { - await this.token.authorizeOperator(newOperator, { from: holder }); - }); - - it('are not added to the default operators list', async function () { - expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators); - }); - - it('can be re-authorized', async function () { - const receipt = await this.token.authorizeOperator(newOperator, { from: holder }); - expectEvent(receipt, 'AuthorizedOperator', { operator: newOperator, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(true); - }); - - it('can be revoked', async function () { - const receipt = await this.token.revokeOperator(newOperator, { from: holder }); - expectEvent(receipt, 'RevokedOperator', { operator: newOperator, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(newOperator, holder)).to.equal(false); - }); - }); - - describe('default operators', function () { - it('can be re-authorized', async function () { - const receipt = await this.token.authorizeOperator(defaultOperatorA, { from: holder }); - expectEvent(receipt, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(true); - }); - - it('can be revoked', async function () { - const receipt = await this.token.revokeOperator(defaultOperatorA, { from: holder }); - expectEvent(receipt, 'RevokedOperator', { operator: defaultOperatorA, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(false); - }); - - it('cannot be revoked for themselves', async function () { - await expectRevert( - this.token.revokeOperator(defaultOperatorA, { from: defaultOperatorA }), - 'ERC777: revoking self as operator', - ); - }); - - context('with revoked default operator', function () { - beforeEach(async function () { - await this.token.revokeOperator(defaultOperatorA, { from: holder }); - }); - - it('default operator is not revoked for other holders', async function () { - expect(await this.token.isOperatorFor(defaultOperatorA, anyone)).to.equal(true); - }); - - it('other default operators are not revoked', async function () { - expect(await this.token.isOperatorFor(defaultOperatorB, holder)).to.equal(true); - }); - - it('default operators list is not modified', async function () { - expect(await this.token.defaultOperators()).to.deep.equal(defaultOperators); - }); - - it('revoked default operator can be re-authorized', async function () { - const receipt = await this.token.authorizeOperator(defaultOperatorA, { from: holder }); - expectEvent(receipt, 'AuthorizedOperator', { operator: defaultOperatorA, tokenHolder: holder }); - - expect(await this.token.isOperatorFor(defaultOperatorA, holder)).to.equal(true); - }); - }); - }); - }); - - describe('send and receive hooks', function () { - const amount = new BN('1'); - const operator = defaultOperatorA; - // sender and recipient are stored inside 'this', since in some tests their addresses are determined dynamically - - describe('tokensReceived', function () { - beforeEach(function () { - this.sender = holder; - }); - - context('with no ERC777TokensRecipient implementer', function () { - context('with contract recipient', function () { - beforeEach(async function () { - this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new(); - this.recipient = this.tokensRecipientImplementer.address; - - // Note that tokensRecipientImplementer doesn't implement the recipient interface for the recipient - }); - - it('send reverts', async function () { - await expectRevert( - this.token.send(this.recipient, amount, data, { from: holder }), - 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient', - ); - }); - - it('operatorSend reverts', async function () { - await expectRevert( - this.token.operatorSend(this.sender, this.recipient, amount, data, operatorData, { from: operator }), - 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient', - ); - }); - - it('mint (internal) reverts', async function () { - await expectRevert( - this.token.$_mint(this.recipient, amount, data, operatorData, true, { from: operator }), - 'ERC777: token recipient contract has no implementer for ERC777TokensRecipient', - ); - }); - - it('(ERC20) transfer succeeds', async function () { - await this.token.transfer(this.recipient, amount, { from: holder }); - }); - - it('(ERC20) transferFrom succeeds', async function () { - const approved = anyone; - await this.token.approve(approved, amount, { from: this.sender }); - await this.token.transferFrom(this.sender, this.recipient, amount, { from: approved }); - }); - }); - }); - - context('with ERC777TokensRecipient implementer', function () { - context('with contract as implementer for an externally owned account', function () { - beforeEach(async function () { - this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new(); - this.recipient = anyone; - - await this.tokensRecipientImplementer.recipientFor(this.recipient); - - await this.erc1820.setInterfaceImplementer( - this.recipient, - web3.utils.soliditySha3('ERC777TokensRecipient'), - this.tokensRecipientImplementer.address, - { from: this.recipient }, - ); - }); - - shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData); - }); - - context('with contract as implementer for another contract', function () { - beforeEach(async function () { - this.recipientContract = await ERC777SenderRecipientMock.new(); - this.recipient = this.recipientContract.address; - - this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new(); - await this.tokensRecipientImplementer.recipientFor(this.recipient); - await this.recipientContract.registerRecipient(this.tokensRecipientImplementer.address); - }); - - shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData); - }); - - context('with contract as implementer for itself', function () { - beforeEach(async function () { - this.tokensRecipientImplementer = await ERC777SenderRecipientMock.new(); - this.recipient = this.tokensRecipientImplementer.address; - - await this.tokensRecipientImplementer.recipientFor(this.recipient); - }); - - shouldBehaveLikeERC777SendBurnMintInternalWithReceiveHook(operator, amount, data, operatorData); - }); - }); - }); - - describe('tokensToSend', function () { - beforeEach(function () { - this.recipient = anyone; - }); - - context('with a contract as implementer for an externally owned account', function () { - beforeEach(async function () { - this.tokensSenderImplementer = await ERC777SenderRecipientMock.new(); - this.sender = holder; - - await this.tokensSenderImplementer.senderFor(this.sender); - - await this.erc1820.setInterfaceImplementer( - this.sender, - web3.utils.soliditySha3('ERC777TokensSender'), - this.tokensSenderImplementer.address, - { from: this.sender }, - ); - }); - - shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData); - }); - - context('with contract as implementer for another contract', function () { - beforeEach(async function () { - this.senderContract = await ERC777SenderRecipientMock.new(); - this.sender = this.senderContract.address; - - this.tokensSenderImplementer = await ERC777SenderRecipientMock.new(); - await this.tokensSenderImplementer.senderFor(this.sender); - await this.senderContract.registerSender(this.tokensSenderImplementer.address); - - // For the contract to be able to receive tokens (that it can later send), it must also implement the - // recipient interface. - - await this.senderContract.recipientFor(this.sender); - await this.token.send(this.sender, amount, data, { from: holder }); - }); - - shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData); - }); - - context('with a contract as implementer for itself', function () { - beforeEach(async function () { - this.tokensSenderImplementer = await ERC777SenderRecipientMock.new(); - this.sender = this.tokensSenderImplementer.address; - - await this.tokensSenderImplementer.senderFor(this.sender); - - // For the contract to be able to receive tokens (that it can later send), it must also implement the - // recipient interface. - - await this.tokensSenderImplementer.recipientFor(this.sender); - await this.token.send(this.sender, amount, data, { from: holder }); - }); - - shouldBehaveLikeERC777SendBurnWithSendHook(operator, amount, data, operatorData); - }); - }); - }); - }); - - context('with no default operators', function () { - beforeEach(async function () { - this.token = await ERC777.new(name, symbol, []); - }); - - it('default operators list is empty', async function () { - expect(await this.token.defaultOperators()).to.deep.equal([]); - }); - }); - - describe('relative order of hooks', function () { - beforeEach(async function () { - await singletons.ERC1820Registry(registryFunder); - this.sender = await ERC777SenderRecipientMock.new(); - await this.sender.registerRecipient(this.sender.address); - await this.sender.registerSender(this.sender.address); - this.token = await ERC777.new(name, symbol, []); - await this.token.$_mint(this.sender.address, 1, '0x', '0x'); - }); - - it('send', async function () { - const { receipt } = await this.sender.send(this.token.address, anyone, 1, '0x'); - - const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer'); - expect(internalBeforeHook).to.be.gte(0); - const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled'); - expect(externalSendHook).to.be.gte(0); - - expect(externalSendHook).to.be.lt(internalBeforeHook); - }); - - it('burn', async function () { - const { receipt } = await this.sender.burn(this.token.address, 1, '0x'); - - const internalBeforeHook = receipt.logs.findIndex(l => l.event === 'BeforeTokenTransfer'); - expect(internalBeforeHook).to.be.gte(0); - const externalSendHook = receipt.logs.findIndex(l => l.event === 'TokensToSendCalled'); - expect(externalSendHook).to.be.gte(0); - - expect(externalSendHook).to.be.lt(internalBeforeHook); - }); - }); -}); diff --git a/test/token/common/ERC2981.behavior.js b/test/token/common/ERC2981.behavior.js index 5d0f67715..15efa239f 100644 --- a/test/token/common/ERC2981.behavior.js +++ b/test/token/common/ERC2981.behavior.js @@ -1,8 +1,9 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { ZERO_ADDRESS } = constants; const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); +const { expectRevertCustomError } = require('../../helpers/customError'); function shouldBehaveLikeERC2981() { const royaltyFraction = new BN('10'); @@ -60,11 +61,18 @@ function shouldBehaveLikeERC2981() { }); it('reverts if invalid parameters', async function () { - await expectRevert(this.token.$_setDefaultRoyalty(ZERO_ADDRESS, royaltyFraction), 'ERC2981: invalid receiver'); + const royaltyDenominator = await this.token.$_feeDenominator(); + await expectRevertCustomError( + this.token.$_setDefaultRoyalty(ZERO_ADDRESS, royaltyFraction), + 'ERC2981InvalidDefaultRoyaltyReceiver', + [ZERO_ADDRESS], + ); - await expectRevert( - this.token.$_setDefaultRoyalty(this.account1, new BN('11000')), - 'ERC2981: royalty fee will exceed salePrice', + const anotherRoyaltyFraction = new BN('11000'); + await expectRevertCustomError( + this.token.$_setDefaultRoyalty(this.account1, anotherRoyaltyFraction), + 'ERC2981InvalidDefaultRoyalty', + [anotherRoyaltyFraction, royaltyDenominator], ); }); }); @@ -104,14 +112,18 @@ function shouldBehaveLikeERC2981() { }); it('reverts if invalid parameters', async function () { - await expectRevert( + const royaltyDenominator = await this.token.$_feeDenominator(); + await expectRevertCustomError( this.token.$_setTokenRoyalty(this.tokenId1, ZERO_ADDRESS, royaltyFraction), - 'ERC2981: Invalid parameters', + 'ERC2981InvalidTokenRoyaltyReceiver', + [this.tokenId1.toString(), ZERO_ADDRESS], ); - await expectRevert( - this.token.$_setTokenRoyalty(this.tokenId1, this.account1, new BN('11000')), - 'ERC2981: royalty fee will exceed salePrice', + const anotherRoyaltyFraction = new BN('11000'); + await expectRevertCustomError( + this.token.$_setTokenRoyalty(this.tokenId1, this.account1, anotherRoyaltyFraction), + 'ERC2981InvalidTokenRoyalty', + [this.tokenId1.toString(), anotherRoyaltyFraction, royaltyDenominator], ); }); diff --git a/test/utils/Address.test.js b/test/utils/Address.test.js index 2f8115c16..beded18e1 100644 --- a/test/utils/Address.test.js +++ b/test/utils/Address.test.js @@ -1,7 +1,9 @@ const { balance, constants, ether, expectRevert, send, expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); const Address = artifacts.require('$Address'); +const AddressFnPointerMock = artifacts.require('$AddressFnPointerMock'); const EtherReceiver = artifacts.require('EtherReceiverMock'); const CallReceiverMock = artifacts.require('CallReceiverMock'); @@ -10,6 +12,7 @@ contract('Address', function (accounts) { beforeEach(async function () { this.mock = await Address.new(); + this.mockFnPointer = await AddressFnPointerMock.new(); }); describe('sendValue', function () { @@ -25,7 +28,9 @@ contract('Address', function (accounts) { }); it('reverts when sending non-zero amounts', async function () { - await expectRevert(this.mock.$sendValue(other, 1), 'Address: insufficient balance'); + await expectRevertCustomError(this.mock.$sendValue(other, 1), 'AddressInsufficientBalance', [ + this.mock.address, + ]); }); }); @@ -52,7 +57,9 @@ contract('Address', function (accounts) { }); it('reverts when sending more than the balance', async function () { - await expectRevert(this.mock.$sendValue(recipient, funds.addn(1)), 'Address: insufficient balance'); + await expectRevertCustomError(this.mock.$sendValue(recipient, funds.addn(1)), 'AddressInsufficientBalance', [ + this.mock.address, + ]); }); context('with contract recipient', function () { @@ -71,10 +78,7 @@ contract('Address', function (accounts) { it('reverts on recipient revert', async function () { await this.target.setAcceptEther(false); - await expectRevert( - this.mock.$sendValue(this.target.address, funds), - 'Address: unable to send value, recipient may have reverted', - ); + await expectRevertCustomError(this.mock.$sendValue(this.target.address, funds), 'FailedInnerCall', []); }); }); }); @@ -91,18 +95,27 @@ contract('Address', function (accounts) { const receipt = await this.mock.$functionCall(this.target.address, abiEncodedCall); - expectEvent(receipt, 'return$functionCall_address_bytes', { + expectEvent(receipt, 'return$functionCall', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); }); + it('calls the requested empty return function', async function () { + const abiEncodedCall = this.target.contract.methods.mockFunctionEmptyReturn().encodeABI(); + + const receipt = await this.mock.$functionCall(this.target.address, abiEncodedCall); + + await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); + }); + it('reverts when the called function reverts with no reason', async function () { const abiEncodedCall = this.target.contract.methods.mockFunctionRevertsNoReason().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCall(this.target.address, abiEncodedCall), - 'Address: low-level call failed', + 'FailedInnerCall', + [], ); }); @@ -115,9 +128,10 @@ contract('Address', function (accounts) { it('reverts when the called function runs out of gas', async function () { const abiEncodedCall = this.target.contract.methods.mockFunctionOutOfGas().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCall(this.target.address, abiEncodedCall, { gas: '120000' }), - 'Address: low-level call failed', + 'FailedInnerCall', + [], ); }); @@ -127,6 +141,14 @@ contract('Address', function (accounts) { await expectRevert.unspecified(this.mock.$functionCall(this.target.address, abiEncodedCall)); }); + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCall(this.target.address, '0x12345678'), + 'CustomRevert', + [], + ); + }); + it('reverts when function does not exist', async function () { const abiEncodedCall = web3.eth.abi.encodeFunctionCall( { @@ -137,9 +159,10 @@ contract('Address', function (accounts) { [], ); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCall(this.target.address, abiEncodedCall), - 'Address: low-level call failed', + 'FailedInnerCall', + [], ); }); }); @@ -149,7 +172,9 @@ contract('Address', function (accounts) { const [recipient] = accounts; const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert(this.mock.$functionCall(recipient, abiEncodedCall), 'Address: call to non-contract'); + await expectRevertCustomError(this.mock.$functionCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ + recipient, + ]); }); }); }); @@ -164,7 +189,7 @@ contract('Address', function (accounts) { const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, 0); - expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', { + expectEvent(receipt, 'return$functionCallWithValue', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); @@ -177,9 +202,10 @@ contract('Address', function (accounts) { it('reverts if insufficient sender balance', async function () { const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount), - 'Address: insufficient balance for call', + 'AddressInsufficientBalance', + [this.mock.address], ); }); @@ -191,7 +217,7 @@ contract('Address', function (accounts) { await send.ether(other, this.mock.address, amount); const receipt = await this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount); - expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', { + expectEvent(receipt, 'return$functionCallWithValue', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); @@ -210,7 +236,7 @@ contract('Address', function (accounts) { from: other, value: amount, }); - expectEvent(receipt, 'return$functionCallWithValue_address_bytes_uint256', { + expectEvent(receipt, 'return$functionCallWithValue', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']), }); await expectEvent.inTransaction(receipt.tx, CallReceiverMock, 'MockFunctionCalled'); @@ -222,9 +248,18 @@ contract('Address', function (accounts) { const abiEncodedCall = this.target.contract.methods.mockFunctionNonPayable().encodeABI(); await send.ether(other, this.mock.address, amount); - await expectRevert( + await expectRevertCustomError( this.mock.$functionCallWithValue(this.target.address, abiEncodedCall, amount), - 'Address: low-level call with value failed', + 'FailedInnerCall', + [], + ); + }); + + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCallWithValue(this.target.address, '0x12345678', 0), + 'CustomRevert', + [], ); }); }); @@ -246,9 +281,10 @@ contract('Address', function (accounts) { it('reverts on a non-static function', async function () { const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert( + await expectRevertCustomError( this.mock.$functionStaticCall(this.target.address, abiEncodedCall), - 'Address: low-level static call failed', + 'FailedInnerCall', + [], ); }); @@ -265,7 +301,17 @@ contract('Address', function (accounts) { const [recipient] = accounts; const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert(this.mock.$functionStaticCall(recipient, abiEncodedCall), 'Address: call to non-contract'); + await expectRevertCustomError(this.mock.$functionStaticCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ + recipient, + ]); + }); + + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCallWithValue(this.target.address, '0x12345678', 0), + 'CustomRevert', + [], + ); }); }); @@ -285,7 +331,7 @@ contract('Address', function (accounts) { expectEvent( await this.mock.$functionDelegateCall(this.target.address, abiEncodedCall), - 'return$functionDelegateCall_address_bytes', + 'return$functionDelegateCall', { ret0: web3.eth.abi.encodeParameters(['string'], ['0x1234']) }, ); @@ -305,7 +351,32 @@ contract('Address', function (accounts) { const [recipient] = accounts; const abiEncodedCall = this.target.contract.methods.mockFunction().encodeABI(); - await expectRevert(this.mock.$functionDelegateCall(recipient, abiEncodedCall), 'Address: call to non-contract'); + await expectRevertCustomError(this.mock.$functionDelegateCall(recipient, abiEncodedCall), 'AddressEmptyCode', [ + recipient, + ]); + }); + + it('bubbles up error if specified', async function () { + await expectRevertCustomError( + this.mockFnPointer.functionCallWithValue(this.target.address, '0x12345678', 0), + 'CustomRevert', + [], + ); + }); + }); + + describe('verifyCallResult', function () { + it('returns returndata on success', async function () { + const returndata = '0x123abc'; + expect(await this.mockFnPointer.verifyCallResult(true, returndata)).to.equal(returndata); + }); + + it('reverts with return data and error', async function () { + await expectRevertCustomError(this.mockFnPointer.verifyCallResult(false, '0x'), 'CustomRevert', []); + }); + + it('reverts expecting error if provided onRevert is a non-reverting function', async function () { + await expectRevertCustomError(this.mockFnPointer.verifyCallResultVoid(false, '0x'), 'FailedInnerCall', []); }); }); }); diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js deleted file mode 100644 index d43d469cc..000000000 --- a/test/utils/Checkpoints.test.js +++ /dev/null @@ -1,203 +0,0 @@ -const { expectRevert, time } = require('@openzeppelin/test-helpers'); - -const { expect } = require('chai'); - -const { batchInBlock } = require('../helpers/txpool'); - -const $Checkpoints = artifacts.require('$Checkpoints'); - -const first = array => (array.length ? array[0] : undefined); -const last = array => (array.length ? array[array.length - 1] : undefined); - -contract('Checkpoints', function () { - beforeEach(async function () { - this.mock = await $Checkpoints.new(); - }); - - describe('History checkpoints', function () { - const latest = (self, ...args) => self.methods['$latest_Checkpoints_History(uint256)'](0, ...args); - const latestCheckpoint = (self, ...args) => - self.methods['$latestCheckpoint_Checkpoints_History(uint256)'](0, ...args); - const push = (self, ...args) => self.methods['$push(uint256,uint256)'](0, ...args); - const getAtBlock = (self, ...args) => self.methods['$getAtBlock(uint256,uint256)'](0, ...args); - const getAtRecentBlock = (self, ...args) => self.methods['$getAtProbablyRecentBlock(uint256,uint256)'](0, ...args); - const getLength = (self, ...args) => self.methods['$length_Checkpoints_History(uint256)'](0, ...args); - - describe('without checkpoints', function () { - it('returns zero as latest value', async function () { - expect(await latest(this.mock)).to.be.bignumber.equal('0'); - - const ckpt = await latestCheckpoint(this.mock); - expect(ckpt[0]).to.be.equal(false); - expect(ckpt[1]).to.be.bignumber.equal('0'); - expect(ckpt[2]).to.be.bignumber.equal('0'); - }); - - it('returns zero as past value', async function () { - await time.advanceBlock(); - expect(await getAtBlock(this.mock, (await web3.eth.getBlockNumber()) - 1)).to.be.bignumber.equal('0'); - expect(await getAtRecentBlock(this.mock, (await web3.eth.getBlockNumber()) - 1)).to.be.bignumber.equal('0'); - }); - }); - - describe('with checkpoints', function () { - beforeEach('pushing checkpoints', async function () { - this.tx1 = await push(this.mock, 1); - this.tx2 = await push(this.mock, 2); - await time.advanceBlock(); - this.tx3 = await push(this.mock, 3); - await time.advanceBlock(); - await time.advanceBlock(); - }); - - it('returns latest value', async function () { - expect(await latest(this.mock)).to.be.bignumber.equal('3'); - - const ckpt = await latestCheckpoint(this.mock); - expect(ckpt[0]).to.be.equal(true); - expect(ckpt[1]).to.be.bignumber.equal(web3.utils.toBN(this.tx3.receipt.blockNumber)); - expect(ckpt[2]).to.be.bignumber.equal(web3.utils.toBN('3')); - }); - - for (const getAtBlockVariant of [getAtBlock, getAtRecentBlock]) { - describe(`lookup: ${getAtBlockVariant}`, function () { - it('returns past values', async function () { - expect(await getAtBlockVariant(this.mock, this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0'); - expect(await getAtBlockVariant(this.mock, this.tx1.receipt.blockNumber)).to.be.bignumber.equal('1'); - expect(await getAtBlockVariant(this.mock, this.tx2.receipt.blockNumber)).to.be.bignumber.equal('2'); - // Block with no new checkpoints - expect(await getAtBlockVariant(this.mock, this.tx2.receipt.blockNumber + 1)).to.be.bignumber.equal('2'); - expect(await getAtBlockVariant(this.mock, this.tx3.receipt.blockNumber)).to.be.bignumber.equal('3'); - expect(await getAtBlockVariant(this.mock, this.tx3.receipt.blockNumber + 1)).to.be.bignumber.equal('3'); - }); - it('reverts if block number >= current block', async function () { - await expectRevert( - getAtBlockVariant(this.mock, await web3.eth.getBlockNumber()), - 'Checkpoints: block not yet mined', - ); - - await expectRevert( - getAtBlockVariant(this.mock, (await web3.eth.getBlockNumber()) + 1), - 'Checkpoints: block not yet mined', - ); - }); - }); - } - - it('multiple checkpoints in the same block', async function () { - const lengthBefore = await getLength(this.mock); - - await batchInBlock([ - () => push(this.mock, 8, { gas: 100000 }), - () => push(this.mock, 9, { gas: 100000 }), - () => push(this.mock, 10, { gas: 100000 }), - ]); - - expect(await getLength(this.mock)).to.be.bignumber.equal(lengthBefore.addn(1)); - expect(await latest(this.mock)).to.be.bignumber.equal('10'); - }); - - it('more than 5 checkpoints', async function () { - for (let i = 4; i <= 6; i++) { - await push(this.mock, i); - } - expect(await getLength(this.mock)).to.be.bignumber.equal('6'); - const block = await web3.eth.getBlockNumber(); - // recent - expect(await getAtRecentBlock(this.mock, block - 1)).to.be.bignumber.equal('5'); - // non-recent - expect(await getAtRecentBlock(this.mock, block - 9)).to.be.bignumber.equal('0'); - }); - }); - }); - - for (const length of [160, 224]) { - describe(`Trace${length}`, function () { - const latest = (self, ...args) => self.methods[`$latest_Checkpoints_Trace${length}(uint256)`](0, ...args); - const latestCheckpoint = (self, ...args) => - self.methods[`$latestCheckpoint_Checkpoints_Trace${length}(uint256)`](0, ...args); - const push = (self, ...args) => self.methods[`$push(uint256,uint${256 - length},uint${length})`](0, ...args); - const upperLookup = (self, ...args) => self.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args); - const lowerLookup = (self, ...args) => self.methods[`$lowerLookup(uint256,uint${256 - length})`](0, ...args); - const getLength = (self, ...args) => self.methods[`$length_Checkpoints_Trace${length}(uint256)`](0, ...args); - - describe('without checkpoints', function () { - it('returns zero as latest value', async function () { - expect(await latest(this.mock)).to.be.bignumber.equal('0'); - - const ckpt = await latestCheckpoint(this.mock); - expect(ckpt[0]).to.be.equal(false); - expect(ckpt[1]).to.be.bignumber.equal('0'); - expect(ckpt[2]).to.be.bignumber.equal('0'); - }); - - it('lookup returns 0', async function () { - expect(await lowerLookup(this.mock, 0)).to.be.bignumber.equal('0'); - expect(await upperLookup(this.mock, 0)).to.be.bignumber.equal('0'); - }); - }); - - describe('with checkpoints', function () { - beforeEach('pushing checkpoints', async function () { - this.checkpoints = [ - { key: '2', value: '17' }, - { key: '3', value: '42' }, - { key: '5', value: '101' }, - { key: '7', value: '23' }, - { key: '11', value: '99' }, - ]; - for (const { key, value } of this.checkpoints) { - await push(this.mock, key, value); - } - }); - - it('length', async function () { - expect(await getLength(this.mock)).to.be.bignumber.equal(this.checkpoints.length.toString()); - }); - - it('returns latest value', async function () { - expect(await latest(this.mock)).to.be.bignumber.equal(last(this.checkpoints).value); - - const ckpt = await latestCheckpoint(this.mock); - expect(ckpt[0]).to.be.equal(true); - expect(ckpt[1]).to.be.bignumber.equal(last(this.checkpoints).key); - expect(ckpt[2]).to.be.bignumber.equal(last(this.checkpoints).value); - }); - - it('cannot push values in the past', async function () { - await expectRevert(push(this.mock, last(this.checkpoints).key - 1, '0'), 'Checkpoint: decreasing keys'); - }); - - it('can update last value', async function () { - const newValue = '42'; - - // check length before the update - expect(await getLength(this.mock)).to.be.bignumber.equal(this.checkpoints.length.toString()); - - // update last key - await push(this.mock, last(this.checkpoints).key, newValue); - expect(await latest(this.mock)).to.be.bignumber.equal(newValue); - - // check that length did not change - expect(await getLength(this.mock)).to.be.bignumber.equal(this.checkpoints.length.toString()); - }); - - it('lower lookup', async function () { - for (let i = 0; i < 14; ++i) { - const value = first(this.checkpoints.filter(x => i <= x.key))?.value || '0'; - - expect(await lowerLookup(this.mock, i)).to.be.bignumber.equal(value); - } - }); - - it('upper lookup', async function () { - for (let i = 0; i < 14; ++i) { - const value = last(this.checkpoints.filter(x => i >= x.key))?.value || '0'; - - expect(await upperLookup(this.mock, i)).to.be.bignumber.equal(value); - } - }); - }); - }); - } -}); diff --git a/test/utils/Counters.test.js b/test/utils/Counters.test.js deleted file mode 100644 index 6fb89922d..000000000 --- a/test/utils/Counters.test.js +++ /dev/null @@ -1,84 +0,0 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); - -const { expect } = require('chai'); - -const Counters = artifacts.require('$Counters'); - -contract('Counters', function () { - beforeEach(async function () { - this.counter = await Counters.new(); - }); - - it('starts at zero', async function () { - expect(await this.counter.$current(0)).to.be.bignumber.equal('0'); - }); - - describe('increment', function () { - context('starting from 0', function () { - it('increments the current value by one', async function () { - await this.counter.$increment(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('1'); - }); - - it('can be called multiple times', async function () { - await this.counter.$increment(0); - await this.counter.$increment(0); - await this.counter.$increment(0); - - expect(await this.counter.$current(0)).to.be.bignumber.equal('3'); - }); - }); - }); - - describe('decrement', function () { - beforeEach(async function () { - await this.counter.$increment(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('1'); - }); - context('starting from 1', function () { - it('decrements the current value by one', async function () { - await this.counter.$decrement(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('0'); - }); - - it('reverts if the current value is 0', async function () { - await this.counter.$decrement(0); - await expectRevert(this.counter.$decrement(0), 'Counter: decrement overflow'); - }); - }); - context('after incremented to 3', function () { - it('can be called multiple times', async function () { - await this.counter.$increment(0); - await this.counter.$increment(0); - - expect(await this.counter.$current(0)).to.be.bignumber.equal('3'); - - await this.counter.$decrement(0); - await this.counter.$decrement(0); - await this.counter.$decrement(0); - - expect(await this.counter.$current(0)).to.be.bignumber.equal('0'); - }); - }); - }); - - describe('reset', function () { - context('null counter', function () { - it('does not throw', async function () { - await this.counter.$reset(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('0'); - }); - }); - - context('non null counter', function () { - beforeEach(async function () { - await this.counter.$increment(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('1'); - }); - it('reset to 0', async function () { - await this.counter.$reset(0); - expect(await this.counter.$current(0)).to.be.bignumber.equal('0'); - }); - }); - }); -}); diff --git a/test/utils/Create2.test.js b/test/utils/Create2.test.js index 2fc27dc15..f88d5504c 100644 --- a/test/utils/Create2.test.js +++ b/test/utils/Create2.test.js @@ -1,10 +1,14 @@ const { balance, ether, expectEvent, expectRevert, send } = require('@openzeppelin/test-helpers'); const { computeCreate2Address } = require('../helpers/create2'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); const Create2 = artifacts.require('$Create2'); const VestingWallet = artifacts.require('VestingWallet'); -const ERC1820Implementer = artifacts.require('$ERC1820Implementer'); +// This should be a contract that: +// - has no constructor arguments +// - has no immutable variable populated during construction +const ConstructorLessContract = Create2; contract('Create2', function (accounts) { const [deployerAccount, other] = accounts; @@ -38,14 +42,14 @@ contract('Create2', function (accounts) { }); describe('deploy', function () { - it('deploys a ERC1820Implementer from inline assembly code', async function () { - const offChainComputed = computeCreate2Address(saltHex, ERC1820Implementer.bytecode, this.factory.address); + it('deploys a contract without constructor', async function () { + const offChainComputed = computeCreate2Address(saltHex, ConstructorLessContract.bytecode, this.factory.address); - expectEvent(await this.factory.$deploy(0, saltHex, ERC1820Implementer.bytecode), 'return$deploy', { + expectEvent(await this.factory.$deploy(0, saltHex, ConstructorLessContract.bytecode), 'return$deploy', { addr: offChainComputed, }); - expect(ERC1820Implementer.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2)); + expect(ConstructorLessContract.bytecode).to.include((await web3.eth.getCode(offChainComputed)).slice(2)); }); it('deploys a contract with constructor arguments', async function () { @@ -75,15 +79,22 @@ contract('Create2', function (accounts) { it('fails deploying a contract in an existent address', async function () { expectEvent(await this.factory.$deploy(0, saltHex, constructorByteCode), 'return$deploy'); - await expectRevert(this.factory.$deploy(0, saltHex, constructorByteCode), 'Create2: Failed on deploy'); + // TODO: Make sure it actually throws "Create2FailedDeployment". + // For some unknown reason, the revert reason sometimes return: + // `revert with unrecognized return data or custom error` + await expectRevert.unspecified(this.factory.$deploy(0, saltHex, constructorByteCode)); }); it('fails deploying a contract if the bytecode length is zero', async function () { - await expectRevert(this.factory.$deploy(0, saltHex, '0x'), 'Create2: bytecode length is zero'); + await expectRevertCustomError(this.factory.$deploy(0, saltHex, '0x'), 'Create2EmptyBytecode', []); }); it('fails deploying a contract if factory contract does not have sufficient balance', async function () { - await expectRevert(this.factory.$deploy(1, saltHex, constructorByteCode), 'Create2: insufficient balance'); + await expectRevertCustomError( + this.factory.$deploy(1, saltHex, constructorByteCode), + 'Create2InsufficientBalance', + [0, 1], + ); }); }); }); diff --git a/test/utils/Multicall.test.js b/test/utils/Multicall.test.js index cfb800769..65443cd0a 100644 --- a/test/utils/Multicall.test.js +++ b/test/utils/Multicall.test.js @@ -1,4 +1,5 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); const ERC20MulticallMock = artifacts.require('$ERC20MulticallMock'); @@ -50,7 +51,7 @@ contract('Multicall', function (accounts) { { from: deployer }, ); - await expectRevert(call, 'ERC20: transfer amount exceeds balance'); + await expectRevertCustomError(call, 'ERC20InsufficientBalance', [deployer, 0, amount]); expect(await this.multicallToken.balanceOf(alice)).to.be.bignumber.equal(new BN('0')); }); @@ -63,6 +64,6 @@ contract('Multicall', function (accounts) { { from: deployer }, ); - await expectRevert(call, 'ERC20: transfer amount exceeds balance'); + await expectRevertCustomError(call, 'ERC20InsufficientBalance', [deployer, 0, amount]); }); }); diff --git a/test/utils/Nonces.test.js b/test/utils/Nonces.test.js index a09cddb1f..361eeeeec 100644 --- a/test/utils/Nonces.test.js +++ b/test/utils/Nonces.test.js @@ -1,4 +1,5 @@ const expectEvent = require('@openzeppelin/test-helpers/src/expectEvent'); +const { expectRevertCustomError } = require('../helpers/customError'); require('@openzeppelin/test-helpers'); @@ -15,22 +16,57 @@ contract('Nonces', function (accounts) { expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); }); - it('increment a nonce', async function () { - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); + describe('_useNonce', function () { + it('increments a nonce', async function () { + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); - const { receipt } = await this.nonces.$_useNonce(sender); - expectEvent(receipt, 'return$_useNonce', { current: '0' }); + const { receipt } = await this.nonces.$_useNonce(sender); + expectEvent(receipt, 'return$_useNonce', ['0']); - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + }); + + it("increments only sender's nonce", async function () { + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); + expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + + await this.nonces.$_useNonce(sender); + + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + }); }); - it('nonce is specific to address argument', async function () { - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('0'); - expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + describe('_useCheckedNonce', function () { + it('increments a nonce', async function () { + const currentNonce = await this.nonces.nonces(sender); + expect(currentNonce).to.be.bignumber.equal('0'); - await this.nonces.$_useNonce(sender); + const { receipt } = await this.nonces.$_useCheckedNonce(sender, currentNonce); + expectEvent(receipt, 'return$_useCheckedNonce', [currentNonce]); - expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); - expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + }); + + it("increments only sender's nonce", async function () { + const currentNonce = await this.nonces.nonces(sender); + + expect(currentNonce).to.be.bignumber.equal('0'); + expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + + await this.nonces.$_useCheckedNonce(sender, currentNonce); + + expect(await this.nonces.nonces(sender)).to.be.bignumber.equal('1'); + expect(await this.nonces.nonces(other)).to.be.bignumber.equal('0'); + }); + + it('reverts when nonce is not the expected', async function () { + const currentNonce = await this.nonces.nonces(sender); + await expectRevertCustomError( + this.nonces.$_useCheckedNonce(sender, currentNonce.addn(1)), + 'InvalidAccountNonce', + [sender, currentNonce], + ); + }); }); }); diff --git a/test/utils/ShortStrings.t.sol b/test/utils/ShortStrings.t.sol new file mode 100644 index 000000000..b70793bd7 --- /dev/null +++ b/test/utils/ShortStrings.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; + +import "../../contracts/utils/ShortStrings.sol"; + +contract ShortStringsTest is Test { + string _fallback; + + function testRoundtripShort(string memory input) external { + vm.assume(_isShort(input)); + ShortString short = ShortStrings.toShortString(input); + string memory output = ShortStrings.toString(short); + assertEq(input, output); + } + + function testRoundtripWithFallback(string memory input, string memory fallbackInitial) external { + _fallback = fallbackInitial; // Make sure that the initial value has no effect + ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback); + string memory output = ShortStrings.toStringWithFallback(short, _fallback); + assertEq(input, output); + } + + function testRevertLong(string memory input) external { + vm.assume(!_isShort(input)); + vm.expectRevert(abi.encodeWithSelector(ShortStrings.StringTooLong.selector, input)); + this.toShortString(input); + } + + function testLengthShort(string memory input) external { + vm.assume(_isShort(input)); + uint256 inputLength = bytes(input).length; + ShortString short = ShortStrings.toShortString(input); + uint256 shortLength = ShortStrings.byteLength(short); + assertEq(inputLength, shortLength); + } + + function testLengthWithFallback(string memory input, string memory fallbackInitial) external { + _fallback = fallbackInitial; + uint256 inputLength = bytes(input).length; + ShortString short = ShortStrings.toShortStringWithFallback(input, _fallback); + uint256 shortLength = ShortStrings.byteLengthWithFallback(short, _fallback); + assertEq(inputLength, shortLength); + } + + function toShortString(string memory input) external pure returns (ShortString) { + return ShortStrings.toShortString(input); + } + + function _isShort(string memory input) internal pure returns (bool) { + return bytes(input).length < 32; + } +} diff --git a/test/utils/ShortStrings.test.js b/test/utils/ShortStrings.test.js new file mode 100644 index 000000000..189281d38 --- /dev/null +++ b/test/utils/ShortStrings.test.js @@ -0,0 +1,55 @@ +const { expect } = require('chai'); +const { expectRevertCustomError } = require('../helpers/customError'); + +const ShortStrings = artifacts.require('$ShortStrings'); + +function length(sstr) { + return parseInt(sstr.slice(64), 16); +} + +function decode(sstr) { + return web3.utils.toUtf8(sstr).slice(0, length(sstr)); +} + +contract('ShortStrings', function () { + before(async function () { + this.mock = await ShortStrings.new(); + }); + + for (const str of [0, 1, 16, 31, 32, 64, 1024].map(length => 'a'.repeat(length))) { + describe(`with string length ${str.length}`, function () { + it('encode / decode', async function () { + if (str.length < 32) { + const encoded = await this.mock.$toShortString(str); + expect(decode(encoded)).to.be.equal(str); + + const length = await this.mock.$byteLength(encoded); + expect(length.toNumber()).to.be.equal(str.length); + + const decoded = await this.mock.$toString(encoded); + expect(decoded).to.be.equal(str); + } else { + await expectRevertCustomError(this.mock.$toShortString(str), 'StringTooLong', [str]); + } + }); + + it('set / get with fallback', async function () { + const { logs } = await this.mock.$toShortStringWithFallback(str, 0); + const { ret0 } = logs.find(({ event }) => event == 'return$toShortStringWithFallback').args; + + const promise = this.mock.$toString(ret0); + if (str.length < 32) { + expect(await promise).to.be.equal(str); + } else { + await expectRevertCustomError(promise, 'InvalidShortString', []); + } + + const length = await this.mock.$byteLengthWithFallback(ret0, 0); + expect(length.toNumber()).to.be.equal(str.length); + + const recovered = await this.mock.$toStringWithFallback(ret0, 0); + expect(recovered).to.be.equal(str); + }); + }); + } +}); diff --git a/test/utils/StorageSlot.test.js b/test/utils/StorageSlot.test.js index 9d428875f..846512ed2 100644 --- a/test/utils/StorageSlot.test.js +++ b/test/utils/StorageSlot.test.js @@ -107,4 +107,104 @@ contract('StorageSlot', function (accounts) { }); }); }); + + describe('string storage slot', function () { + beforeEach(async function () { + this.value = 'lorem ipsum'; + }); + + it('set', async function () { + await this.store.setString(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setString(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.getString(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.getString(otherSlot)).to.be.equal(''); + }); + }); + }); + + describe('string storage pointer', function () { + beforeEach(async function () { + this.value = 'lorem ipsum'; + }); + + it('set', async function () { + await this.store.setStringStorage(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setStringStorage(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.stringMap(slot)).to.be.equal(this.value); + expect(await this.store.getStringStorage(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.stringMap(otherSlot)).to.be.equal(''); + expect(await this.store.getStringStorage(otherSlot)).to.be.equal(''); + }); + }); + }); + + describe('bytes storage slot', function () { + beforeEach(async function () { + this.value = web3.utils.randomHex(128); + }); + + it('set', async function () { + await this.store.setBytes(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setBytes(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.getBytes(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.getBytes(otherSlot)).to.be.equal(null); + }); + }); + }); + + describe('bytes storage pointer', function () { + beforeEach(async function () { + this.value = web3.utils.randomHex(128); + }); + + it('set', async function () { + await this.store.setBytesStorage(slot, this.value); + }); + + describe('get', function () { + beforeEach(async function () { + await this.store.setBytesStorage(slot, this.value); + }); + + it('from right slot', async function () { + expect(await this.store.bytesMap(slot)).to.be.equal(this.value); + expect(await this.store.getBytesStorage(slot)).to.be.equal(this.value); + }); + + it('from other slot', async function () { + expect(await this.store.bytesMap(otherSlot)).to.be.equal(null); + expect(await this.store.getBytesStorage(otherSlot)).to.be.equal(null); + }); + }); + }); }); diff --git a/test/utils/Strings.test.js b/test/utils/Strings.test.js index 6658871a0..2435fc71c 100644 --- a/test/utils/Strings.test.js +++ b/test/utils/Strings.test.js @@ -1,4 +1,5 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN, constants } = require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../helpers/customError'); const { expect } = require('chai'); @@ -47,22 +48,22 @@ contract('Strings', function () { describe('int256', function () { it('converts MAX_INT256', async function () { const value = constants.MAX_INT256; - expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value.toString(10)); + expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value.toString(10)); }); it('converts MIN_INT256', async function () { const value = constants.MIN_INT256; - expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value.toString(10)); + expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value.toString(10)); }); for (const value of values) { it(`convert ${value}`, async function () { - expect(await this.strings.methods['$toString(int256)'](value)).to.equal(value); + expect(await this.strings.methods['$toStringSigned(int256)'](value)).to.equal(value); }); it(`convert negative ${value}`, async function () { const negated = new BN(value).neg(); - expect(await this.strings.methods['$toString(int256)'](negated)).to.equal(negated.toString(10)); + expect(await this.strings.methods['$toStringSigned(int256)'](negated)).to.equal(negated.toString(10)); }); } }); @@ -92,9 +93,11 @@ contract('Strings', function () { }); it('converts a positive number (short)', async function () { - await expectRevert( - this.strings.methods['$toHexString(uint256,uint256)'](0x4132, 1), - 'Strings: hex length insufficient', + const length = 1; + await expectRevertCustomError( + this.strings.methods['$toHexString(uint256,uint256)'](0x4132, length), + `StringsInsufficientHexLength`, + [0x4132, length], ); }); diff --git a/test/utils/TimersBlockNumberImpl.test.js b/test/utils/TimersBlockNumberImpl.test.js deleted file mode 100644 index ee0d54bfb..000000000 --- a/test/utils/TimersBlockNumberImpl.test.js +++ /dev/null @@ -1,55 +0,0 @@ -const { BN, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); - -const TimersBlockNumberImpl = artifacts.require('TimersBlockNumberImpl'); - -contract('TimersBlockNumber', function () { - beforeEach(async function () { - this.instance = await TimersBlockNumberImpl.new(); - this.now = await web3.eth.getBlock('latest').then(({ number }) => number); - }); - - it('unset', async function () { - expect(await this.instance.getDeadline()).to.be.bignumber.equal('0'); - expect(await this.instance.isUnset()).to.be.equal(true); - expect(await this.instance.isStarted()).to.be.equal(false); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('pending', async function () { - await this.instance.setDeadline(this.now + 3); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now + 3)); - expect(await this.instance.isUnset()).to.be.equal(false); - expect(await this.instance.isStarted()).to.be.equal(true); - expect(await this.instance.isPending()).to.be.equal(true); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('expired', async function () { - await this.instance.setDeadline(this.now - 3); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now - 3)); - expect(await this.instance.isUnset()).to.be.equal(false); - expect(await this.instance.isStarted()).to.be.equal(true); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(true); - }); - - it('reset', async function () { - await this.instance.reset(); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(0)); - expect(await this.instance.isUnset()).to.be.equal(true); - expect(await this.instance.isStarted()).to.be.equal(false); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('fast forward', async function () { - await this.instance.setDeadline(this.now + 3); - expect(await this.instance.isPending()).to.be.equal(true); - expect(await this.instance.isExpired()).to.be.equal(false); - await time.advanceBlockTo(this.now + 3); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(true); - }); -}); diff --git a/test/utils/TimersTimestamp.test.js b/test/utils/TimersTimestamp.test.js deleted file mode 100644 index fc32de7d0..000000000 --- a/test/utils/TimersTimestamp.test.js +++ /dev/null @@ -1,55 +0,0 @@ -const { BN, time } = require('@openzeppelin/test-helpers'); -const { expect } = require('chai'); - -const TimersTimestampImpl = artifacts.require('TimersTimestampImpl'); - -contract('TimersTimestamp', function () { - beforeEach(async function () { - this.instance = await TimersTimestampImpl.new(); - this.now = await web3.eth.getBlock('latest').then(({ timestamp }) => timestamp); - }); - - it('unset', async function () { - expect(await this.instance.getDeadline()).to.be.bignumber.equal('0'); - expect(await this.instance.isUnset()).to.be.equal(true); - expect(await this.instance.isStarted()).to.be.equal(false); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('pending', async function () { - await this.instance.setDeadline(this.now + 100); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now + 100)); - expect(await this.instance.isUnset()).to.be.equal(false); - expect(await this.instance.isStarted()).to.be.equal(true); - expect(await this.instance.isPending()).to.be.equal(true); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('expired', async function () { - await this.instance.setDeadline(this.now - 100); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(this.now - 100)); - expect(await this.instance.isUnset()).to.be.equal(false); - expect(await this.instance.isStarted()).to.be.equal(true); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(true); - }); - - it('reset', async function () { - await this.instance.reset(); - expect(await this.instance.getDeadline()).to.be.bignumber.equal(new BN(0)); - expect(await this.instance.isUnset()).to.be.equal(true); - expect(await this.instance.isStarted()).to.be.equal(false); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(false); - }); - - it('fast forward', async function () { - await this.instance.setDeadline(this.now + 100); - expect(await this.instance.isPending()).to.be.equal(true); - expect(await this.instance.isExpired()).to.be.equal(false); - await time.increaseTo(this.now + 100); - expect(await this.instance.isPending()).to.be.equal(false); - expect(await this.instance.isExpired()).to.be.equal(true); - }); -}); diff --git a/test/utils/cryptography/ECDSA.test.js b/test/utils/cryptography/ECDSA.test.js index 3b19cde60..3fd112a18 100644 --- a/test/utils/cryptography/ECDSA.test.js +++ b/test/utils/cryptography/ECDSA.test.js @@ -1,5 +1,6 @@ -const { expectRevert } = require('@openzeppelin/test-helpers'); -const { toEthSignedMessageHash } = require('../../helpers/sign'); +require('@openzeppelin/test-helpers'); +const { expectRevertCustomError } = require('../../helpers/customError'); +const { toEthSignedMessageHash, toDataWithIntendedValidatorHash } = require('../../helpers/sign'); const { expect } = require('chai'); @@ -8,6 +9,7 @@ const ECDSA = artifacts.require('$ECDSA'); const TEST_MESSAGE = web3.utils.sha3('OpenZeppelin'); const WRONG_MESSAGE = web3.utils.sha3('Nope'); const NON_HASH_MESSAGE = '0x' + Buffer.from('abcd').toString('hex'); +const RANDOM_ADDRESS = web3.utils.toChecksumAddress(web3.utils.randomHex(20)); function to2098Format(signature) { const long = web3.utils.hexToBytes(signature); @@ -50,17 +52,18 @@ contract('ECDSA', function (accounts) { context('recover with invalid signature', function () { it('with short signature', async function () { - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, '0x1234'), 'ECDSA: invalid signature length'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, '0x1234'), 'ECDSAInvalidSignatureLength', [2]); }); it('with long signature', async function () { - await expectRevert( + await expectRevertCustomError( // eslint-disable-next-line max-len this.ecdsa.$recover( TEST_MESSAGE, '0x01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789', ), - 'ECDSA: invalid signature length', + 'ECDSAInvalidSignatureLength', + [85], ); }); }); @@ -92,7 +95,7 @@ contract('ECDSA', function (accounts) { // eslint-disable-next-line max-len const signature = '0x332ce75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c'; - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); }); }); @@ -140,11 +143,12 @@ contract('ECDSA', function (accounts) { it('reverts wrong v values', async function () { for (const v of ['00', '01']) { const signature = signatureWithoutV + v; - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); - await expectRevert( + await expectRevertCustomError( this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), - 'ECDSA: invalid signature', + 'ECDSAInvalidSignature', + [], ); } }); @@ -152,9 +156,10 @@ contract('ECDSA', function (accounts) { it('rejects short EIP2098 format', async function () { const v = '1b'; // 27 = 1b. const signature = signatureWithoutV + v; - await expectRevert( + await expectRevertCustomError( this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)), - 'ECDSA: invalid signature length', + 'ECDSAInvalidSignatureLength', + [64], ); }); }); @@ -202,11 +207,12 @@ contract('ECDSA', function (accounts) { it('reverts invalid v values', async function () { for (const v of ['00', '01']) { const signature = signatureWithoutV + v; - await expectRevert(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSA: invalid signature'); + await expectRevertCustomError(this.ecdsa.$recover(TEST_MESSAGE, signature), 'ECDSAInvalidSignature', []); - await expectRevert( + await expectRevertCustomError( this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(signature)), - 'ECDSA: invalid signature', + 'ECDSAInvalidSignature', + [], ); } }); @@ -214,9 +220,10 @@ contract('ECDSA', function (accounts) { it('rejects short EIP2098 format', async function () { const v = '1c'; // 27 = 1b. const signature = signatureWithoutV + v; - await expectRevert( + await expectRevertCustomError( this.ecdsa.$recover(TEST_MESSAGE, to2098Format(signature)), - 'ECDSA: invalid signature length', + 'ECDSAInvalidSignatureLength', + [64], ); }); }); @@ -226,10 +233,12 @@ contract('ECDSA', function (accounts) { // eslint-disable-next-line max-len const highSSignature = '0xe742ff452d41413616a5bf43fe15dd88294e983d3d36206c2712f39083d638bde0a0fc89be718fbc1033e1d30d78be1c68081562ed2e97af876f286f3453231d1b'; - await expectRevert(this.ecdsa.$recover(message, highSSignature), "ECDSA: invalid signature 's' value"); - await expectRevert( - this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, ...split(highSSignature)), - "ECDSA: invalid signature 's' value", + const [r, v, s] = split(highSSignature); + await expectRevertCustomError(this.ecdsa.$recover(message, highSSignature), 'ECDSAInvalidSignatureS', [s]); + await expectRevertCustomError( + this.ecdsa.methods['$recover(bytes32,uint8,bytes32,bytes32)'](TEST_MESSAGE, r, v, s), + 'ECDSAInvalidSignatureS', + [s], ); expect(() => to2098Format(highSSignature)).to.throw("invalid signature 's' value"); }); @@ -248,4 +257,12 @@ contract('ECDSA', function (accounts) { ); }); }); + + context('toDataWithIntendedValidatorHash', function () { + it('returns the hash correctly', async function () { + expect( + await this.ecdsa.methods['$toDataWithIntendedValidatorHash(address,bytes)'](RANDOM_ADDRESS, NON_HASH_MESSAGE), + ).to.equal(toDataWithIntendedValidatorHash(RANDOM_ADDRESS, NON_HASH_MESSAGE)); + }); + }); }); diff --git a/test/utils/cryptography/EIP712.test.js b/test/utils/cryptography/EIP712.test.js index 8c20b91ba..7ea535b7f 100644 --- a/test/utils/cryptography/EIP712.test.js +++ b/test/utils/cryptography/EIP712.test.js @@ -1,63 +1,111 @@ const ethSigUtil = require('eth-sig-util'); const Wallet = require('ethereumjs-wallet').default; -const { EIP712Domain, domainSeparator, hashTypedData } = require('../../helpers/eip712'); +const { getDomain, domainType, domainSeparator, hashTypedData } = require('../../helpers/eip712'); const { getChainId } = require('../../helpers/chainid'); +const { mapValues } = require('../../helpers/map-values'); const EIP712Verifier = artifacts.require('$EIP712Verifier'); +const Clones = artifacts.require('$Clones'); contract('EIP712', function (accounts) { const [mailTo] = accounts; - const name = 'A Name'; - const version = '1'; + const shortName = 'A Name'; + const shortVersion = '1'; - beforeEach('deploying', async function () { - this.eip712 = await EIP712Verifier.new(name, version); + const longName = 'A'.repeat(40); + const longVersion = 'B'.repeat(40); - this.domain = { - name, - version, - chainId: await getChainId(), - verifyingContract: this.eip712.address, - }; - }); + const cases = [ + ['short', shortName, shortVersion], + ['long', longName, longVersion], + ]; - it('domain separator', async function () { - const expected = await domainSeparator(this.domain); + for (const [shortOrLong, name, version] of cases) { + describe(`with ${shortOrLong} name and version`, function () { + beforeEach('deploying', async function () { + this.eip712 = await EIP712Verifier.new(name, version); - expect(await this.eip712.$_domainSeparatorV4()).to.equal(expected); - }); + this.domain = { + name, + version, + chainId: await getChainId(), + verifyingContract: this.eip712.address, + }; + this.domainType = domainType(this.domain); + }); - it('hash digest', async function () { - const structhash = web3.utils.randomHex(32); - const expected = await hashTypedData(this.domain, structhash); + describe('domain separator', function () { + it('is internally available', async function () { + const expected = await domainSeparator(this.domain); - expect(await this.eip712.$_hashTypedDataV4(structhash)).to.be.equal(expected); - }); + expect(await this.eip712.$_domainSeparatorV4()).to.equal(expected); + }); - it('digest', async function () { - const message = { - to: mailTo, - contents: 'very interesting', - }; + it("can be rebuilt using EIP-5267's eip712Domain", async function () { + const rebuildDomain = await getDomain(this.eip712); + expect(mapValues(rebuildDomain, String)).to.be.deep.equal(mapValues(this.domain, String)); + }); - const data = { - types: { - EIP712Domain, - Mail: [ - { name: 'to', type: 'address' }, - { name: 'contents', type: 'string' }, - ], - }, - domain: this.domain, - primaryType: 'Mail', - message, - }; + if (shortOrLong === 'short') { + // Long strings are in storage, and the proxy will not be properly initialized unless + // the upgradeable contract variant is used and the initializer is invoked. - const wallet = Wallet.generate(); - const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + it('adjusts when behind proxy', async function () { + const factory = await Clones.new(); + const cloneReceipt = await factory.$clone(this.eip712.address); + const cloneAddress = cloneReceipt.logs.find(({ event }) => event === 'return$clone').args.instance; + const clone = new EIP712Verifier(cloneAddress); - await this.eip712.verify(signature, wallet.getAddressString(), message.to, message.contents); - }); + const cloneDomain = { ...this.domain, verifyingContract: clone.address }; + + const reportedDomain = await getDomain(clone); + expect(mapValues(reportedDomain, String)).to.be.deep.equal(mapValues(cloneDomain, String)); + + const expectedSeparator = await domainSeparator(cloneDomain); + expect(await clone.$_domainSeparatorV4()).to.equal(expectedSeparator); + }); + } + }); + + it('hash digest', async function () { + const structhash = web3.utils.randomHex(32); + expect(await this.eip712.$_hashTypedDataV4(structhash)).to.be.equal(hashTypedData(this.domain, structhash)); + }); + + it('digest', async function () { + const message = { + to: mailTo, + contents: 'very interesting', + }; + + const data = { + types: { + EIP712Domain: this.domainType, + Mail: [ + { name: 'to', type: 'address' }, + { name: 'contents', type: 'string' }, + ], + }, + domain: this.domain, + primaryType: 'Mail', + message, + }; + + const wallet = Wallet.generate(); + const signature = ethSigUtil.signTypedMessage(wallet.getPrivateKey(), { data }); + + await this.eip712.verify(signature, wallet.getAddressString(), message.to, message.contents); + }); + + it('name', async function () { + expect(await this.eip712.$_EIP712Name()).to.be.equal(name); + }); + + it('version', async function () { + expect(await this.eip712.$_EIP712Version()).to.be.equal(version); + }); + }); + } }); diff --git a/test/utils/cryptography/MerkleProof.test.js b/test/utils/cryptography/MerkleProof.test.js index 62157b56a..5b87bc525 100644 --- a/test/utils/cryptography/MerkleProof.test.js +++ b/test/utils/cryptography/MerkleProof.test.js @@ -1,10 +1,10 @@ -require('@openzeppelin/test-helpers'); - const { expectRevert } = require('@openzeppelin/test-helpers'); + const { MerkleTree } = require('merkletreejs'); const keccak256 = require('keccak256'); const { expect } = require('chai'); +const { expectRevertCustomError } = require('../../helpers/customError'); const MerkleProof = artifacts.require('$MerkleProof'); @@ -106,23 +106,25 @@ contract('MerkleProof', function () { const root = merkleTree.getRoot(); - await expectRevert( + await expectRevertCustomError( this.merkleProof.$multiProofVerify( [leaves[1], fill, merkleTree.layers[1][1]], [false, false, false], root, [leaves[0], badLeaf], // A, E ), - 'MerkleProof: invalid multiproof', + 'MerkleProofInvalidMultiproof', + [], ); - await expectRevert( + await expectRevertCustomError( this.merkleProof.$multiProofVerifyCalldata( [leaves[1], fill, merkleTree.layers[1][1]], [false, false, false], root, [leaves[0], badLeaf], // A, E ), - 'MerkleProof: invalid multiproof', + 'MerkleProofInvalidMultiproof', + [], ); }); @@ -176,5 +178,30 @@ contract('MerkleProof', function () { expect(await this.merkleProof.$multiProofVerify([root], [], root, [])).to.equal(true); expect(await this.merkleProof.$multiProofVerifyCalldata([root], [], root, [])).to.equal(true); }); + + it('reverts processing manipulated proofs with a zero-value node at depth 1', async function () { + // Create a merkle tree that contains a zero leaf at depth 1 + const leaves = [keccak256('real leaf'), Buffer.alloc(32, 0)]; + const merkleTree = new MerkleTree(leaves, keccak256, { sortPairs: true }); + + const root = merkleTree.getRoot(); + + // Now we can pass any **malicious** fake leaves as valid! + const maliciousLeaves = ['malicious', 'leaves'].map(keccak256).sort(Buffer.compare); + const maliciousProof = [leaves[0], leaves[0]]; + const maliciousProofFlags = [true, true, false]; + + await expectRevertCustomError( + this.merkleProof.$multiProofVerify(maliciousProof, maliciousProofFlags, root, maliciousLeaves), + 'MerkleProofInvalidMultiproof', + [], + ); + + await expectRevertCustomError( + this.merkleProof.$multiProofVerifyCalldata(maliciousProof, maliciousProofFlags, root, maliciousLeaves), + 'MerkleProofInvalidMultiproof', + [], + ); + }); }); }); diff --git a/test/utils/cryptography/SignatureChecker.test.js b/test/utils/cryptography/SignatureChecker.test.js index 11054c3e1..ba8b100d1 100644 --- a/test/utils/cryptography/SignatureChecker.test.js +++ b/test/utils/cryptography/SignatureChecker.test.js @@ -40,44 +40,48 @@ contract('SignatureChecker (ERC1271)', function (accounts) { }); context('ERC1271 wallet', function () { - it('with matching signer and signature', async function () { - expect( - await this.signaturechecker.$isValidSignatureNow( - this.wallet.address, - toEthSignedMessageHash(TEST_MESSAGE), - this.signature, - ), - ).to.equal(true); - }); + for (const signature of ['isValidERC1271SignatureNow', 'isValidSignatureNow']) { + context(signature, function () { + it('with matching signer and signature', async function () { + expect( + await this.signaturechecker[`$${signature}`]( + this.wallet.address, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + ), + ).to.equal(true); + }); - it('with invalid signer', async function () { - expect( - await this.signaturechecker.$isValidSignatureNow( - this.signaturechecker.address, - toEthSignedMessageHash(TEST_MESSAGE), - this.signature, - ), - ).to.equal(false); - }); + it('with invalid signer', async function () { + expect( + await this.signaturechecker[`$${signature}`]( + this.signaturechecker.address, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + ), + ).to.equal(false); + }); - it('with invalid signature', async function () { - expect( - await this.signaturechecker.$isValidSignatureNow( - this.wallet.address, - toEthSignedMessageHash(WRONG_MESSAGE), - this.signature, - ), - ).to.equal(false); - }); + it('with invalid signature', async function () { + expect( + await this.signaturechecker[`$${signature}`]( + this.wallet.address, + toEthSignedMessageHash(WRONG_MESSAGE), + this.signature, + ), + ).to.equal(false); + }); - it('with malicious wallet', async function () { - expect( - await this.signaturechecker.$isValidSignatureNow( - this.malicious.address, - toEthSignedMessageHash(TEST_MESSAGE), - this.signature, - ), - ).to.equal(false); - }); + it('with malicious wallet', async function () { + expect( + await this.signaturechecker[`$${signature}`]( + this.malicious.address, + toEthSignedMessageHash(TEST_MESSAGE), + this.signature, + ), + ).to.equal(false); + }); + }); + } }); }); diff --git a/test/utils/escrow/ConditionalEscrow.test.js b/test/utils/escrow/ConditionalEscrow.test.js deleted file mode 100644 index 9e17a8bce..000000000 --- a/test/utils/escrow/ConditionalEscrow.test.js +++ /dev/null @@ -1,37 +0,0 @@ -const { ether, expectRevert } = require('@openzeppelin/test-helpers'); -const { shouldBehaveLikeEscrow } = require('./Escrow.behavior'); - -const ConditionalEscrowMock = artifacts.require('ConditionalEscrowMock'); - -contract('ConditionalEscrow', function (accounts) { - const [owner, payee, ...otherAccounts] = accounts; - - beforeEach(async function () { - this.escrow = await ConditionalEscrowMock.new({ from: owner }); - }); - - context('when withdrawal is allowed', function () { - beforeEach(async function () { - await Promise.all(otherAccounts.map(payee => this.escrow.setAllowed(payee, true))); - }); - - shouldBehaveLikeEscrow(owner, otherAccounts); - }); - - context('when withdrawal is disallowed', function () { - const amount = ether('23'); - - beforeEach(async function () { - await this.escrow.setAllowed(payee, false); - }); - - it('reverts on withdrawals', async function () { - await this.escrow.deposit(payee, { from: owner, value: amount }); - - await expectRevert( - this.escrow.withdraw(payee, { from: owner }), - 'ConditionalEscrow: payee is not allowed to withdraw', - ); - }); - }); -}); diff --git a/test/utils/escrow/Escrow.behavior.js b/test/utils/escrow/Escrow.behavior.js deleted file mode 100644 index f2059ed62..000000000 --- a/test/utils/escrow/Escrow.behavior.js +++ /dev/null @@ -1,90 +0,0 @@ -const { balance, ether, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); - -const { expect } = require('chai'); - -function shouldBehaveLikeEscrow(owner, [payee1, payee2]) { - const amount = ether('42'); - - describe('as an escrow', function () { - describe('deposits', function () { - it('can accept a single deposit', async function () { - await this.escrow.deposit(payee1, { from: owner, value: amount }); - - expect(await balance.current(this.escrow.address)).to.be.bignumber.equal(amount); - - expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal(amount); - }); - - it('can accept an empty deposit', async function () { - await this.escrow.deposit(payee1, { from: owner, value: 0 }); - }); - - it('only the owner can deposit', async function () { - await expectRevert(this.escrow.deposit(payee1, { from: payee2 }), 'Ownable: caller is not the owner'); - }); - - it('emits a deposited event', async function () { - const receipt = await this.escrow.deposit(payee1, { from: owner, value: amount }); - expectEvent(receipt, 'Deposited', { - payee: payee1, - weiAmount: amount, - }); - }); - - it('can add multiple deposits on a single account', async function () { - await this.escrow.deposit(payee1, { from: owner, value: amount }); - await this.escrow.deposit(payee1, { from: owner, value: amount.muln(2) }); - - expect(await balance.current(this.escrow.address)).to.be.bignumber.equal(amount.muln(3)); - - expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal(amount.muln(3)); - }); - - it('can track deposits to multiple accounts', async function () { - await this.escrow.deposit(payee1, { from: owner, value: amount }); - await this.escrow.deposit(payee2, { from: owner, value: amount.muln(2) }); - - expect(await balance.current(this.escrow.address)).to.be.bignumber.equal(amount.muln(3)); - - expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal(amount); - - expect(await this.escrow.depositsOf(payee2)).to.be.bignumber.equal(amount.muln(2)); - }); - }); - - describe('withdrawals', async function () { - it('can withdraw payments', async function () { - const balanceTracker = await balance.tracker(payee1); - - await this.escrow.deposit(payee1, { from: owner, value: amount }); - await this.escrow.withdraw(payee1, { from: owner }); - - expect(await balanceTracker.delta()).to.be.bignumber.equal(amount); - - expect(await balance.current(this.escrow.address)).to.be.bignumber.equal('0'); - expect(await this.escrow.depositsOf(payee1)).to.be.bignumber.equal('0'); - }); - - it('can do an empty withdrawal', async function () { - await this.escrow.withdraw(payee1, { from: owner }); - }); - - it('only the owner can withdraw', async function () { - await expectRevert(this.escrow.withdraw(payee1, { from: payee1 }), 'Ownable: caller is not the owner'); - }); - - it('emits a withdrawn event', async function () { - await this.escrow.deposit(payee1, { from: owner, value: amount }); - const receipt = await this.escrow.withdraw(payee1, { from: owner }); - expectEvent(receipt, 'Withdrawn', { - payee: payee1, - weiAmount: amount, - }); - }); - }); - }); -} - -module.exports = { - shouldBehaveLikeEscrow, -}; diff --git a/test/utils/escrow/Escrow.test.js b/test/utils/escrow/Escrow.test.js deleted file mode 100644 index 47627904b..000000000 --- a/test/utils/escrow/Escrow.test.js +++ /dev/null @@ -1,14 +0,0 @@ -require('@openzeppelin/test-helpers'); -const { shouldBehaveLikeEscrow } = require('./Escrow.behavior'); - -const Escrow = artifacts.require('Escrow'); - -contract('Escrow', function (accounts) { - const [owner, ...otherAccounts] = accounts; - - beforeEach(async function () { - this.escrow = await Escrow.new({ from: owner }); - }); - - shouldBehaveLikeEscrow(owner, otherAccounts); -}); diff --git a/test/utils/escrow/RefundEscrow.test.js b/test/utils/escrow/RefundEscrow.test.js deleted file mode 100644 index c5344db16..000000000 --- a/test/utils/escrow/RefundEscrow.test.js +++ /dev/null @@ -1,143 +0,0 @@ -const { balance, constants, ether, expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); -const { ZERO_ADDRESS } = constants; - -const { expect } = require('chai'); - -const RefundEscrow = artifacts.require('RefundEscrow'); - -contract('RefundEscrow', function (accounts) { - const [owner, beneficiary, refundee1, refundee2] = accounts; - - const amount = ether('54'); - const refundees = [refundee1, refundee2]; - - it('requires a non-null beneficiary', async function () { - await expectRevert( - RefundEscrow.new(ZERO_ADDRESS, { from: owner }), - 'RefundEscrow: beneficiary is the zero address', - ); - }); - - context('once deployed', function () { - beforeEach(async function () { - this.escrow = await RefundEscrow.new(beneficiary, { from: owner }); - }); - - context('active state', function () { - it('has beneficiary and state', async function () { - expect(await this.escrow.beneficiary()).to.equal(beneficiary); - expect(await this.escrow.state()).to.be.bignumber.equal('0'); - }); - - it('accepts deposits', async function () { - await this.escrow.deposit(refundee1, { from: owner, value: amount }); - - expect(await this.escrow.depositsOf(refundee1)).to.be.bignumber.equal(amount); - }); - - it('does not refund refundees', async function () { - await this.escrow.deposit(refundee1, { from: owner, value: amount }); - await expectRevert(this.escrow.withdraw(refundee1), 'ConditionalEscrow: payee is not allowed to withdraw'); - }); - - it('does not allow beneficiary withdrawal', async function () { - await this.escrow.deposit(refundee1, { from: owner, value: amount }); - await expectRevert( - this.escrow.beneficiaryWithdraw(), - 'RefundEscrow: beneficiary can only withdraw while closed', - ); - }); - }); - - it('only the owner can enter closed state', async function () { - await expectRevert(this.escrow.close({ from: beneficiary }), 'Ownable: caller is not the owner'); - - const receipt = await this.escrow.close({ from: owner }); - expectEvent(receipt, 'RefundsClosed'); - }); - - context('closed state', function () { - beforeEach(async function () { - await Promise.all(refundees.map(refundee => this.escrow.deposit(refundee, { from: owner, value: amount }))); - - await this.escrow.close({ from: owner }); - }); - - it('rejects deposits', async function () { - await expectRevert( - this.escrow.deposit(refundee1, { from: owner, value: amount }), - 'RefundEscrow: can only deposit while active', - ); - }); - - it('does not refund refundees', async function () { - await expectRevert(this.escrow.withdraw(refundee1), 'ConditionalEscrow: payee is not allowed to withdraw'); - }); - - it('allows beneficiary withdrawal', async function () { - const balanceTracker = await balance.tracker(beneficiary); - await this.escrow.beneficiaryWithdraw(); - expect(await balanceTracker.delta()).to.be.bignumber.equal(amount.muln(refundees.length)); - }); - - it('prevents entering the refund state', async function () { - await expectRevert( - this.escrow.enableRefunds({ from: owner }), - 'RefundEscrow: can only enable refunds while active', - ); - }); - - it('prevents re-entering the closed state', async function () { - await expectRevert(this.escrow.close({ from: owner }), 'RefundEscrow: can only close while active'); - }); - }); - - it('only the owner can enter refund state', async function () { - await expectRevert(this.escrow.enableRefunds({ from: beneficiary }), 'Ownable: caller is not the owner'); - - const receipt = await this.escrow.enableRefunds({ from: owner }); - expectEvent(receipt, 'RefundsEnabled'); - }); - - context('refund state', function () { - beforeEach(async function () { - await Promise.all(refundees.map(refundee => this.escrow.deposit(refundee, { from: owner, value: amount }))); - - await this.escrow.enableRefunds({ from: owner }); - }); - - it('rejects deposits', async function () { - await expectRevert( - this.escrow.deposit(refundee1, { from: owner, value: amount }), - 'RefundEscrow: can only deposit while active', - ); - }); - - it('refunds refundees', async function () { - for (const refundee of [refundee1, refundee2]) { - const balanceTracker = await balance.tracker(refundee); - await this.escrow.withdraw(refundee, { from: owner }); - expect(await balanceTracker.delta()).to.be.bignumber.equal(amount); - } - }); - - it('does not allow beneficiary withdrawal', async function () { - await expectRevert( - this.escrow.beneficiaryWithdraw(), - 'RefundEscrow: beneficiary can only withdraw while closed', - ); - }); - - it('prevents entering the closed state', async function () { - await expectRevert(this.escrow.close({ from: owner }), 'RefundEscrow: can only close while active'); - }); - - it('prevents re-entering the refund state', async function () { - await expectRevert( - this.escrow.enableRefunds({ from: owner }), - 'RefundEscrow: can only enable refunds while active', - ); - }); - }); - }); -}); diff --git a/test/utils/introspection/ERC1820Implementer.test.js b/test/utils/introspection/ERC1820Implementer.test.js deleted file mode 100644 index 83d0a6534..000000000 --- a/test/utils/introspection/ERC1820Implementer.test.js +++ /dev/null @@ -1,71 +0,0 @@ -const { expectRevert, singletons } = require('@openzeppelin/test-helpers'); -const { bufferToHex, keccakFromString } = require('ethereumjs-util'); - -const { expect } = require('chai'); - -const ERC1820Implementer = artifacts.require('$ERC1820Implementer'); - -contract('ERC1820Implementer', function (accounts) { - const [registryFunder, implementee, other] = accounts; - - const ERC1820_ACCEPT_MAGIC = bufferToHex(keccakFromString('ERC1820_ACCEPT_MAGIC')); - - beforeEach(async function () { - this.implementer = await ERC1820Implementer.new(); - this.registry = await singletons.ERC1820Registry(registryFunder); - - this.interfaceA = bufferToHex(keccakFromString('interfaceA')); - this.interfaceB = bufferToHex(keccakFromString('interfaceB')); - }); - - context('with no registered interfaces', function () { - it('returns false when interface implementation is queried', async function () { - expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceA, implementee)).to.not.equal( - ERC1820_ACCEPT_MAGIC, - ); - }); - - it('reverts when attempting to set as implementer in the registry', async function () { - await expectRevert( - this.registry.setInterfaceImplementer(implementee, this.interfaceA, this.implementer.address, { - from: implementee, - }), - 'Does not implement the interface', - ); - }); - }); - - context('with registered interfaces', function () { - beforeEach(async function () { - await this.implementer.$_registerInterfaceForAddress(this.interfaceA, implementee); - }); - - it('returns true when interface implementation is queried', async function () { - expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceA, implementee)).to.equal( - ERC1820_ACCEPT_MAGIC, - ); - }); - - it('returns false when interface implementation for non-supported interfaces is queried', async function () { - expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceB, implementee)).to.not.equal( - ERC1820_ACCEPT_MAGIC, - ); - }); - - it('returns false when interface implementation for non-supported addresses is queried', async function () { - expect(await this.implementer.canImplementInterfaceForAddress(this.interfaceA, other)).to.not.equal( - ERC1820_ACCEPT_MAGIC, - ); - }); - - it('can be set as an implementer for supported interfaces in the registry', async function () { - await this.registry.setInterfaceImplementer(implementee, this.interfaceA, this.implementer.address, { - from: implementee, - }); - - expect(await this.registry.getInterfaceImplementer(implementee, this.interfaceA)).to.equal( - this.implementer.address, - ); - }); - }); -}); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index ba5aca2fc..201a55f47 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -37,6 +37,18 @@ const INTERFACES = { 'renounceRole(bytes32,address)', ], AccessControlEnumerable: ['getRoleMember(bytes32,uint256)', 'getRoleMemberCount(bytes32)'], + AccessControlDefaultAdminRules: [ + 'defaultAdminDelay()', + 'pendingDefaultAdminDelay()', + 'defaultAdmin()', + 'pendingDefaultAdmin()', + 'defaultAdminDelayIncreaseWait()', + 'changeDefaultAdminDelay(uint48)', + 'rollbackDefaultAdminDelay()', + 'beginDefaultAdminTransfer(address)', + 'acceptDefaultAdminTransfer()', + 'cancelDefaultAdminTransfer()', + ], Governor: [ 'name()', 'version()', @@ -78,6 +90,7 @@ const INTERFACES = { 'castVoteBySig(uint256,uint8,uint8,bytes32,bytes32)', 'castVoteWithReasonAndParamsBySig(uint256,uint8,string,bytes,uint8,bytes32,bytes32)', ], + GovernorCancel: ['proposalProposer(uint256)', 'cancel(address[],uint256[],bytes[],bytes32)'], GovernorTimelock: ['timelock()', 'proposalEta(uint256)', 'queue(address[],uint256[],bytes[],bytes32)'], ERC2981: ['royaltyInfo(uint256,uint256)'], }; @@ -100,23 +113,28 @@ function shouldSupportInterfaces(interfaces = []) { it('supportsInterface uses less than 30k gas', async function () { for (const k of interfaces) { - const interfaceId = INTERFACE_IDS[k]; + const interfaceId = INTERFACE_IDS[k] ?? k; expect(await this.contractUnderTest.supportsInterface.estimateGas(interfaceId)).to.be.lte(30000); } }); it('all interfaces are reported as supported', async function () { for (const k of interfaces) { - const interfaceId = INTERFACE_IDS[k]; - expect(await this.contractUnderTest.supportsInterface(interfaceId)).to.equal(true); + const interfaceId = INTERFACE_IDS[k] ?? k; + expect(await this.contractUnderTest.supportsInterface(interfaceId)).to.equal(true, `does not support ${k}`); } }); it('all interface functions are in ABI', async function () { for (const k of interfaces) { + // skip interfaces for which we don't have a function list + if (INTERFACES[k] === undefined) continue; for (const fnName of INTERFACES[k]) { const fnSig = FN_SIGNATURES[fnName]; - expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal(1); + expect(this.contractUnderTest.abi.filter(fn => fn.signature === fnSig).length).to.equal( + 1, + `did not find ${fnName}`, + ); } } }); diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 5542baf9d..61f558e0e 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "../../../contracts/utils/math/Math.sol"; -import "../../../contracts/utils/math/SafeMath.sol"; contract MathTest is Test { // CEILDIV @@ -47,7 +46,7 @@ contract MathTest is Test { } function _squareBigger(uint256 value, uint256 ref) private pure returns (bool) { - (bool noOverflow, uint256 square) = SafeMath.tryMul(value, value); + (bool noOverflow, uint256 square) = Math.tryMul(value, value); return !noOverflow || square > ref; } @@ -177,7 +176,7 @@ contract MathTest is Test { } // Helpers - function _asRounding(uint8 r) private returns (Math.Rounding) { + function _asRounding(uint8 r) private pure returns (Math.Rounding) { vm.assume(r < uint8(type(Math.Rounding).max)); return Math.Rounding(r); } diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 6b97a646f..afd822b17 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -2,9 +2,25 @@ const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { MAX_UINT256 } = constants; const { Rounding } = require('../../helpers/enums.js'); +const { expectRevertCustomError } = require('../../helpers/customError.js'); const Math = artifacts.require('$Math'); +function expectStruct(value, expected) { + for (const key in expected) { + if (BN.isBN(value[key])) { + expect(value[key]).to.be.bignumber.equal(expected[key]); + } else { + expect(value[key]).to.be.equal(expected[key]); + } + } +} + +async function testCommutativeIterable(fn, lhs, rhs, expected, ...extra) { + expectStruct(await fn(lhs, rhs, ...extra), expected); + expectStruct(await fn(rhs, lhs, ...extra), expected); +} + contract('Math', function () { const min = new BN('1234'); const max = new BN('5678'); @@ -15,6 +31,130 @@ contract('Math', function () { this.math = await Math.new(); }); + describe('tryAdd', function () { + it('adds correctly', async function () { + const a = new BN('5678'); + const b = new BN('1234'); + + await testCommutativeIterable(this.math.$tryAdd, a, b, [true, a.add(b)]); + }); + + it('reverts on addition overflow', async function () { + const a = MAX_UINT256; + const b = new BN('1'); + + await testCommutativeIterable(this.math.$tryAdd, a, b, [false, '0']); + }); + }); + + describe('trySub', function () { + it('subtracts correctly', async function () { + const a = new BN('5678'); + const b = new BN('1234'); + + expectStruct(await this.math.$trySub(a, b), [true, a.sub(b)]); + }); + + it('reverts if subtraction result would be negative', async function () { + const a = new BN('1234'); + const b = new BN('5678'); + + expectStruct(await this.math.$trySub(a, b), [false, '0']); + }); + }); + + describe('tryMul', function () { + it('multiplies correctly', async function () { + const a = new BN('1234'); + const b = new BN('5678'); + + await testCommutativeIterable(this.math.$tryMul, a, b, [true, a.mul(b)]); + }); + + it('multiplies by zero correctly', async function () { + const a = new BN('0'); + const b = new BN('5678'); + + await testCommutativeIterable(this.math.$tryMul, a, b, [true, a.mul(b)]); + }); + + it('reverts on multiplication overflow', async function () { + const a = MAX_UINT256; + const b = new BN('2'); + + await testCommutativeIterable(this.math.$tryMul, a, b, [false, '0']); + }); + }); + + describe('tryDiv', function () { + it('divides correctly', async function () { + const a = new BN('5678'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryDiv(a, b), [true, a.div(b)]); + }); + + it('divides zero correctly', async function () { + const a = new BN('0'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryDiv(a, b), [true, a.div(b)]); + }); + + it('returns complete number result on non-even division', async function () { + const a = new BN('7000'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryDiv(a, b), [true, a.div(b)]); + }); + + it('reverts on division by zero', async function () { + const a = new BN('5678'); + const b = new BN('0'); + + expectStruct(await this.math.$tryDiv(a, b), [false, '0']); + }); + }); + + describe('tryMod', function () { + describe('modulos correctly', async function () { + it('when the dividend is smaller than the divisor', async function () { + const a = new BN('284'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + }); + + it('when the dividend is equal to the divisor', async function () { + const a = new BN('5678'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + }); + + it('when the dividend is larger than the divisor', async function () { + const a = new BN('7000'); + const b = new BN('5678'); + + expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + }); + + it('when the dividend is a multiple of the divisor', async function () { + const a = new BN('17034'); // 17034 == 5678 * 3 + const b = new BN('5678'); + + expectStruct(await this.math.$tryMod(a, b), [true, a.mod(b)]); + }); + }); + + it('reverts with a 0 divisor', async function () { + const a = new BN('5678'); + const b = new BN('0'); + + expectStruct(await this.math.$tryMod(a, b), [false, '0']); + }); + }); + describe('max', function () { it('is correctly detected in first argument position', async function () { expect(await this.math.$max(max, min)).to.be.bignumber.equal(max); @@ -65,6 +205,19 @@ contract('Math', function () { }); describe('ceilDiv', function () { + it('reverts on zero division', async function () { + const a = new BN('2'); + const b = new BN('0'); + // It's unspecified because it's a low level 0 division error + await expectRevert.unspecified(this.math.$ceilDiv(a, b)); + }); + + it('does not round up a zero result', async function () { + const a = new BN('0'); + const b = new BN('2'); + expect(await this.math.$ceilDiv(a, b)).to.be.bignumber.equal('0'); + }); + it('does not round up on exact division', async function () { const a = new BN('10'); const b = new BN('5'); @@ -94,6 +247,10 @@ contract('Math', function () { await expectRevert.unspecified(this.math.$mulDiv(1, 1, 0, Rounding.Down)); }); + it('reverts with result higher than 2 ^ 256', async function () { + await expectRevertCustomError(this.math.$mulDiv(5, MAX_UINT256, 2, Rounding.Down), 'MathOverflowedMulDiv', []); + }); + 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'); diff --git a/test/utils/math/SafeCast.test.js b/test/utils/math/SafeCast.test.js index 63223f5d1..4b8ec5a72 100644 --- a/test/utils/math/SafeCast.test.js +++ b/test/utils/math/SafeCast.test.js @@ -1,6 +1,7 @@ -const { BN, expectRevert } = require('@openzeppelin/test-helpers'); +const { BN } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const { range } = require('../../../scripts/helpers'); +const { expectRevertCustomError } = require('../../helpers/customError'); const SafeCast = artifacts.require('$SafeCast'); @@ -26,16 +27,18 @@ contract('SafeCast', async function () { }); it(`reverts when downcasting 2^${bits} (${maxValue.addn(1)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toUint${bits}`](maxValue.addn(1)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedUintDowncast`, + [bits, maxValue.addn(1)], ); }); it(`reverts when downcasting 2^${bits} + 1 (${maxValue.addn(2)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toUint${bits}`](maxValue.addn(2)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedUintDowncast`, + [bits, maxValue.addn(2)], ); }); }); @@ -60,11 +63,11 @@ contract('SafeCast', async function () { }); it('reverts when casting -1', async function () { - await expectRevert(this.safeCast.$toUint256(-1), 'SafeCast: value must be positive'); + await expectRevertCustomError(this.safeCast.$toUint256(-1), `SafeCastOverflowedIntToUint`, [-1]); }); it(`reverts when casting INT256_MIN (${minInt256})`, async function () { - await expectRevert(this.safeCast.$toUint256(minInt256), 'SafeCast: value must be positive'); + await expectRevertCustomError(this.safeCast.$toUint256(minInt256), `SafeCastOverflowedIntToUint`, [minInt256]); }); }); @@ -94,30 +97,34 @@ contract('SafeCast', async function () { }); it(`reverts when downcasting -2^${bits - 1} - 1 (${minValue.subn(1)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](minValue.subn(1)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, minValue.subn(1)], ); }); it(`reverts when downcasting -2^${bits - 1} - 2 (${minValue.subn(2)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](minValue.subn(2)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, minValue.subn(2)], ); }); it(`reverts when downcasting 2^${bits - 1} (${maxValue.addn(1)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](maxValue.addn(1)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, maxValue.addn(1)], ); }); it(`reverts when downcasting 2^${bits - 1} + 1 (${maxValue.addn(2)})`, async function () { - await expectRevert( + await expectRevertCustomError( this.safeCast[`$toInt${bits}`](maxValue.addn(2)), - `SafeCast: value doesn't fit in ${bits} bits`, + `SafeCastOverflowedIntDowncast`, + [bits, maxValue.addn(2)], ); }); }); @@ -142,11 +149,13 @@ contract('SafeCast', async function () { }); it(`reverts when casting INT256_MAX + 1 (${maxInt256.addn(1)})`, async function () { - await expectRevert(this.safeCast.$toInt256(maxInt256.addn(1)), "SafeCast: value doesn't fit in an int256"); + await expectRevertCustomError(this.safeCast.$toInt256(maxInt256.addn(1)), 'SafeCastOverflowedUintToInt', [ + maxInt256.addn(1), + ]); }); it(`reverts when casting UINT256_MAX (${maxUint256})`, async function () { - await expectRevert(this.safeCast.$toInt256(maxUint256), "SafeCast: value doesn't fit in an int256"); + await expectRevertCustomError(this.safeCast.$toInt256(maxUint256), 'SafeCastOverflowedUintToInt', [maxUint256]); }); }); }); diff --git a/test/utils/math/SafeMath.test.js b/test/utils/math/SafeMath.test.js deleted file mode 100644 index 9598ac57c..000000000 --- a/test/utils/math/SafeMath.test.js +++ /dev/null @@ -1,433 +0,0 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); -const { MAX_UINT256 } = constants; - -const { expect } = require('chai'); - -const SafeMath = artifacts.require('$SafeMath'); -const SafeMathMemoryCheck = artifacts.require('$SafeMathMemoryCheck'); - -function expectStruct(value, expected) { - for (const key in expected) { - if (BN.isBN(value[key])) { - expect(value[key]).to.be.bignumber.equal(expected[key]); - } else { - expect(value[key]).to.be.equal(expected[key]); - } - } -} - -contract('SafeMath', function () { - beforeEach(async function () { - this.safeMath = await SafeMath.new(); - }); - - async function testCommutative(fn, lhs, rhs, expected, ...extra) { - expect(await fn(lhs, rhs, ...extra)).to.be.bignumber.equal(expected); - expect(await fn(rhs, lhs, ...extra)).to.be.bignumber.equal(expected); - } - - async function testFailsCommutative(fn, lhs, rhs, reason, ...extra) { - if (reason === undefined) { - await expectRevert.unspecified(fn(lhs, rhs, ...extra)); - await expectRevert.unspecified(fn(rhs, lhs, ...extra)); - } else { - await expectRevert(fn(lhs, rhs, ...extra), reason); - await expectRevert(fn(rhs, lhs, ...extra), reason); - } - } - - async function testCommutativeIterable(fn, lhs, rhs, expected, ...extra) { - expectStruct(await fn(lhs, rhs, ...extra), expected); - expectStruct(await fn(rhs, lhs, ...extra), expected); - } - - describe('with flag', function () { - describe('add', function () { - it('adds correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - testCommutativeIterable(this.safeMath.$tryAdd, a, b, [true, a.add(b)]); - }); - - it('reverts on addition overflow', async function () { - const a = MAX_UINT256; - const b = new BN('1'); - - testCommutativeIterable(this.safeMath.$tryAdd, a, b, [false, '0']); - }); - }); - - describe('sub', function () { - it('subtracts correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - expectStruct(await this.safeMath.$trySub(a, b), [true, a.sub(b)]); - }); - - it('reverts if subtraction result would be negative', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$trySub(a, b), [false, '0']); - }); - }); - - describe('mul', function () { - it('multiplies correctly', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - testCommutativeIterable(this.safeMath.$tryMul, a, b, [true, a.mul(b)]); - }); - - it('multiplies by zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - testCommutativeIterable(this.safeMath.$tryMul, a, b, [true, a.mul(b)]); - }); - - it('reverts on multiplication overflow', async function () { - const a = MAX_UINT256; - const b = new BN('2'); - - testCommutativeIterable(this.safeMath.$tryMul, a, b, [false, '0']); - }); - }); - - describe('div', function () { - it('divides correctly', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryDiv(a, b), [true, a.div(b)]); - }); - - it('divides zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryDiv(a, b), [true, a.div(b)]); - }); - - it('returns complete number result on non-even division', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryDiv(a, b), [true, a.div(b)]); - }); - - it('reverts on division by zero', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - expectStruct(await this.safeMath.$tryDiv(a, b), [false, '0']); - }); - }); - - describe('mod', function () { - describe('modulos correctly', async function () { - it('when the dividend is smaller than the divisor', async function () { - const a = new BN('284'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]); - }); - - it('when the dividend is equal to the divisor', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]); - }); - - it('when the dividend is larger than the divisor', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]); - }); - - it('when the dividend is a multiple of the divisor', async function () { - const a = new BN('17034'); // 17034 == 5678 * 3 - const b = new BN('5678'); - - expectStruct(await this.safeMath.$tryMod(a, b), [true, a.mod(b)]); - }); - }); - - it('reverts with a 0 divisor', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - expectStruct(await this.safeMath.$tryMod(a, b), [false, '0']); - }); - }); - }); - - describe('with default revert message', function () { - describe('add', function () { - it('adds correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - await testCommutative(this.safeMath.$add, a, b, a.add(b)); - }); - - it('reverts on addition overflow', async function () { - const a = MAX_UINT256; - const b = new BN('1'); - - await testFailsCommutative(this.safeMath.$add, a, b, undefined); - }); - }); - - describe('sub', function () { - it('subtracts correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - expect(await this.safeMath.$sub(a, b)).to.be.bignumber.equal(a.sub(b)); - }); - - it('reverts if subtraction result would be negative', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - await expectRevert.unspecified(this.safeMath.$sub(a, b)); - }); - }); - - describe('mul', function () { - it('multiplies correctly', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - await testCommutative(this.safeMath.$mul, a, b, a.mul(b)); - }); - - it('multiplies by zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - await testCommutative(this.safeMath.$mul, a, b, '0'); - }); - - it('reverts on multiplication overflow', async function () { - const a = MAX_UINT256; - const b = new BN('2'); - - await testFailsCommutative(this.safeMath.$mul, a, b, undefined); - }); - }); - - describe('div', function () { - it('divides correctly', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal(a.div(b)); - }); - - it('divides zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('0'); - }); - - it('returns complete number result on non-even division', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('1'); - }); - - it('reverts on division by zero', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - await expectRevert.unspecified(this.safeMath.$div(a, b)); - }); - }); - - describe('mod', function () { - describe('modulos correctly', async function () { - it('when the dividend is smaller than the divisor', async function () { - const a = new BN('284'); - const b = new BN('5678'); - - expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is equal to the divisor', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is larger than the divisor', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is a multiple of the divisor', async function () { - const a = new BN('17034'); // 17034 == 5678 * 3 - const b = new BN('5678'); - - expect(await this.safeMath.$mod(a, b)).to.be.bignumber.equal(a.mod(b)); - }); - }); - - it('reverts with a 0 divisor', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - await expectRevert.unspecified(this.safeMath.$mod(a, b)); - }); - }); - }); - - describe('with custom revert message', function () { - describe('sub', function () { - it('subtracts correctly', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - expect( - await this.safeMath.methods['$sub(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.sub(b)); - }); - - it('reverts if subtraction result would be negative', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - await expectRevert( - this.safeMath.methods['$sub(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - 'MyErrorMessage', - ); - }); - }); - - describe('div', function () { - it('divides correctly', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.div(b)); - }); - - it('divides zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal('0'); - }); - - it('returns complete number result on non-even division', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal('1'); - }); - - it('reverts on division by zero', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - await expectRevert( - this.safeMath.methods['$div(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - 'MyErrorMessage', - ); - }); - }); - - describe('mod', function () { - describe('modulos correctly', async function () { - it('when the dividend is smaller than the divisor', async function () { - const a = new BN('284'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is equal to the divisor', async function () { - const a = new BN('5678'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is larger than the divisor', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.mod(b)); - }); - - it('when the dividend is a multiple of the divisor', async function () { - const a = new BN('17034'); // 17034 == 5678 * 3 - const b = new BN('5678'); - - expect( - await this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - ).to.be.bignumber.equal(a.mod(b)); - }); - }); - - it('reverts with a 0 divisor', async function () { - const a = new BN('5678'); - const b = new BN('0'); - - await expectRevert( - this.safeMath.methods['$mod(uint256,uint256,string)'](a, b, 'MyErrorMessage'), - 'MyErrorMessage', - ); - }); - }); - }); - - describe('memory leakage', function () { - beforeEach(async function () { - this.safeMathMemoryCheck = await SafeMathMemoryCheck.new(); - }); - - it('add', async function () { - expect(await this.safeMathMemoryCheck.$addMemoryCheck()).to.be.bignumber.equal('0'); - }); - - it('sub', async function () { - expect(await this.safeMathMemoryCheck.$subMemoryCheck()).to.be.bignumber.equal('0'); - }); - - it('mul', async function () { - expect(await this.safeMathMemoryCheck.$mulMemoryCheck()).to.be.bignumber.equal('0'); - }); - - it('div', async function () { - expect(await this.safeMathMemoryCheck.$divMemoryCheck()).to.be.bignumber.equal('0'); - }); - - it('mod', async function () { - expect(await this.safeMathMemoryCheck.$modMemoryCheck()).to.be.bignumber.equal('0'); - }); - }); -}); diff --git a/test/utils/math/SignedSafeMath.test.js b/test/utils/math/SignedSafeMath.test.js deleted file mode 100644 index 3b6530cf5..000000000 --- a/test/utils/math/SignedSafeMath.test.js +++ /dev/null @@ -1,152 +0,0 @@ -const { BN, constants, expectRevert } = require('@openzeppelin/test-helpers'); -const { MAX_INT256, MIN_INT256 } = constants; - -const { expect } = require('chai'); - -const SignedSafeMath = artifacts.require('$SignedSafeMath'); - -contract('SignedSafeMath', function () { - beforeEach(async function () { - this.safeMath = await SignedSafeMath.new(); - }); - - async function testCommutative(fn, lhs, rhs, expected) { - expect(await fn(lhs, rhs)).to.be.bignumber.equal(expected); - expect(await fn(rhs, lhs)).to.be.bignumber.equal(expected); - } - - async function testFailsCommutative(fn, lhs, rhs) { - await expectRevert.unspecified(fn(lhs, rhs)); - await expectRevert.unspecified(fn(rhs, lhs)); - } - - describe('add', function () { - it('adds correctly if it does not overflow and the result is positive', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - await testCommutative(this.safeMath.$add, a, b, a.add(b)); - }); - - it('adds correctly if it does not overflow and the result is negative', async function () { - const a = MAX_INT256; - const b = MIN_INT256; - - await testCommutative(this.safeMath.$add, a, b, a.add(b)); - }); - - it('reverts on positive addition overflow', async function () { - const a = MAX_INT256; - const b = new BN('1'); - - await testFailsCommutative(this.safeMath.$add, a, b); - }); - - it('reverts on negative addition overflow', async function () { - const a = MIN_INT256; - const b = new BN('-1'); - - await testFailsCommutative(this.safeMath.$add, a, b); - }); - }); - - describe('sub', function () { - it('subtracts correctly if it does not overflow and the result is positive', async function () { - const a = new BN('5678'); - const b = new BN('1234'); - - const result = await this.safeMath.$sub(a, b); - expect(result).to.be.bignumber.equal(a.sub(b)); - }); - - it('subtracts correctly if it does not overflow and the result is negative', async function () { - const a = new BN('1234'); - const b = new BN('5678'); - - const result = await this.safeMath.$sub(a, b); - expect(result).to.be.bignumber.equal(a.sub(b)); - }); - - it('reverts on positive subtraction overflow', async function () { - const a = MAX_INT256; - const b = new BN('-1'); - - await expectRevert.unspecified(this.safeMath.$sub(a, b)); - }); - - it('reverts on negative subtraction overflow', async function () { - const a = MIN_INT256; - const b = new BN('1'); - - await expectRevert.unspecified(this.safeMath.$sub(a, b)); - }); - }); - - describe('mul', function () { - it('multiplies correctly', async function () { - const a = new BN('5678'); - const b = new BN('-1234'); - - await testCommutative(this.safeMath.$mul, a, b, a.mul(b)); - }); - - it('multiplies by zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - await testCommutative(this.safeMath.$mul, a, b, '0'); - }); - - it('reverts on multiplication overflow, positive operands', async function () { - const a = MAX_INT256; - const b = new BN('2'); - - await testFailsCommutative(this.safeMath.$mul, a, b); - }); - - it('reverts when minimum integer is multiplied by -1', async function () { - const a = MIN_INT256; - const b = new BN('-1'); - - await testFailsCommutative(this.safeMath.$mul, a, b); - }); - }); - - describe('div', function () { - it('divides correctly', async function () { - const a = new BN('-5678'); - const b = new BN('5678'); - - const result = await this.safeMath.$div(a, b); - expect(result).to.be.bignumber.equal(a.div(b)); - }); - - it('divides zero correctly', async function () { - const a = new BN('0'); - const b = new BN('5678'); - - expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('0'); - }); - - it('returns complete number result on non-even division', async function () { - const a = new BN('7000'); - const b = new BN('5678'); - - expect(await this.safeMath.$div(a, b)).to.be.bignumber.equal('1'); - }); - - it('reverts on division by zero', async function () { - const a = new BN('-5678'); - const b = new BN('0'); - - await expectRevert.unspecified(this.safeMath.$div(a, b)); - }); - - it('reverts on overflow, negative second', async function () { - const a = new BN(MIN_INT256); - const b = new BN('-1'); - - await expectRevert.unspecified(this.safeMath.$div(a, b)); - }); - }); -}); diff --git a/test/utils/structs/Checkpoints.t.sol b/test/utils/structs/Checkpoints.t.sol new file mode 100644 index 000000000..1fb6dcb4e --- /dev/null +++ b/test/utils/structs/Checkpoints.t.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/Checkpoints.t.js. + +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "../../../contracts/utils/math/SafeCast.sol"; +import "../../../contracts/utils/structs/Checkpoints.sol"; + +contract CheckpointsTrace224Test is Test { + using Checkpoints for Checkpoints.Trace224; + + // Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that + // key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range. + uint8 internal constant _KEY_MAX_GAP = 64; + + Checkpoints.Trace224 internal _ckpts; + + // helpers + function _boundUint32(uint32 x, uint32 min, uint32 max) internal view returns (uint32) { + return SafeCast.toUint32(bound(uint256(x), uint256(min), uint256(max))); + } + + function _prepareKeys(uint32[] memory keys, uint32 maxSpread) internal view { + uint32 lastKey = 0; + for (uint256 i = 0; i < keys.length; ++i) { + uint32 key = _boundUint32(keys[i], lastKey, lastKey + maxSpread); + keys[i] = key; + lastKey = key; + } + } + + function _assertLatestCheckpoint(bool exist, uint32 key, uint224 value) internal { + (bool _exist, uint32 _key, uint224 _value) = _ckpts.latestCheckpoint(); + assertEq(_exist, exist); + assertEq(_key, key); + assertEq(_value, value); + } + + // tests + function testPush(uint32[] memory keys, uint224[] memory values, uint32 pastKey) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + // initial state + assertEq(_ckpts.length(), 0); + assertEq(_ckpts.latest(), 0); + _assertLatestCheckpoint(false, 0, 0); + + uint256 duplicates = 0; + for (uint256 i = 0; i < keys.length; ++i) { + uint32 key = keys[i]; + uint224 value = values[i % values.length]; + if (i > 0 && key == keys[i - 1]) ++duplicates; + + // push + _ckpts.push(key, value); + + // check length & latest + assertEq(_ckpts.length(), i + 1 - duplicates); + assertEq(_ckpts.latest(), value); + _assertLatestCheckpoint(true, key, value); + } + + if (keys.length > 0) { + uint32 lastKey = keys[keys.length - 1]; + if (lastKey > 0) { + pastKey = _boundUint32(pastKey, 0, lastKey - 1); + + vm.expectRevert(); + this.push(pastKey, values[keys.length % values.length]); + } + } + } + + // used to test reverts + function push(uint32 key, uint224 value) external { + _ckpts.push(key, value); + } + + function testLookup(uint32[] memory keys, uint224[] memory values, uint32 lookup) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + uint32 lastKey = keys.length == 0 ? 0 : keys[keys.length - 1]; + lookup = _boundUint32(lookup, 0, lastKey + _KEY_MAX_GAP); + + uint224 upper = 0; + uint224 lower = 0; + uint32 lowerKey = type(uint32).max; + for (uint256 i = 0; i < keys.length; ++i) { + uint32 key = keys[i]; + uint224 value = values[i % values.length]; + + // push + _ckpts.push(key, value); + + // track expected result of lookups + if (key <= lookup) { + upper = value; + } + // find the first key that is not smaller than the lookup key + if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) { + lowerKey = key; + } + if (key == lowerKey) { + lower = value; + } + } + + // check lookup + assertEq(_ckpts.lowerLookup(lookup), lower); + assertEq(_ckpts.upperLookup(lookup), upper); + assertEq(_ckpts.upperLookupRecent(lookup), upper); + } +} + +contract CheckpointsTrace160Test is Test { + using Checkpoints for Checkpoints.Trace160; + + // Maximum gap between keys used during the fuzzing tests: the `_prepareKeys` function with make sure that + // key#n+1 is in the [key#n, key#n + _KEY_MAX_GAP] range. + uint8 internal constant _KEY_MAX_GAP = 64; + + Checkpoints.Trace160 internal _ckpts; + + // helpers + function _boundUint96(uint96 x, uint96 min, uint96 max) internal view returns (uint96) { + return SafeCast.toUint96(bound(uint256(x), uint256(min), uint256(max))); + } + + function _prepareKeys(uint96[] memory keys, uint96 maxSpread) internal view { + uint96 lastKey = 0; + for (uint256 i = 0; i < keys.length; ++i) { + uint96 key = _boundUint96(keys[i], lastKey, lastKey + maxSpread); + keys[i] = key; + lastKey = key; + } + } + + function _assertLatestCheckpoint(bool exist, uint96 key, uint160 value) internal { + (bool _exist, uint96 _key, uint160 _value) = _ckpts.latestCheckpoint(); + assertEq(_exist, exist); + assertEq(_key, key); + assertEq(_value, value); + } + + // tests + function testPush(uint96[] memory keys, uint160[] memory values, uint96 pastKey) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + // initial state + assertEq(_ckpts.length(), 0); + assertEq(_ckpts.latest(), 0); + _assertLatestCheckpoint(false, 0, 0); + + uint256 duplicates = 0; + for (uint256 i = 0; i < keys.length; ++i) { + uint96 key = keys[i]; + uint160 value = values[i % values.length]; + if (i > 0 && key == keys[i - 1]) ++duplicates; + + // push + _ckpts.push(key, value); + + // check length & latest + assertEq(_ckpts.length(), i + 1 - duplicates); + assertEq(_ckpts.latest(), value); + _assertLatestCheckpoint(true, key, value); + } + + if (keys.length > 0) { + uint96 lastKey = keys[keys.length - 1]; + if (lastKey > 0) { + pastKey = _boundUint96(pastKey, 0, lastKey - 1); + + vm.expectRevert(); + this.push(pastKey, values[keys.length % values.length]); + } + } + } + + // used to test reverts + function push(uint96 key, uint160 value) external { + _ckpts.push(key, value); + } + + function testLookup(uint96[] memory keys, uint160[] memory values, uint96 lookup) public { + vm.assume(values.length > 0 && values.length <= keys.length); + _prepareKeys(keys, _KEY_MAX_GAP); + + uint96 lastKey = keys.length == 0 ? 0 : keys[keys.length - 1]; + lookup = _boundUint96(lookup, 0, lastKey + _KEY_MAX_GAP); + + uint160 upper = 0; + uint160 lower = 0; + uint96 lowerKey = type(uint96).max; + for (uint256 i = 0; i < keys.length; ++i) { + uint96 key = keys[i]; + uint160 value = values[i % values.length]; + + // push + _ckpts.push(key, value); + + // track expected result of lookups + if (key <= lookup) { + upper = value; + } + // find the first key that is not smaller than the lookup key + if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) { + lowerKey = key; + } + if (key == lowerKey) { + lower = value; + } + } + + // check lookup + assertEq(_ckpts.lowerLookup(lookup), lower); + assertEq(_ckpts.upperLookup(lookup), upper); + assertEq(_ckpts.upperLookupRecent(lookup), upper); + } +} diff --git a/test/utils/structs/Checkpoints.test.js b/test/utils/structs/Checkpoints.test.js new file mode 100644 index 000000000..936ac565a --- /dev/null +++ b/test/utils/structs/Checkpoints.test.js @@ -0,0 +1,158 @@ +require('@openzeppelin/test-helpers'); + +const { expect } = require('chai'); + +const { VALUE_SIZES } = require('../../../scripts/generate/templates/Checkpoints.opts.js'); +const { expectRevertCustomError } = require('../../helpers/customError.js'); +const { expectRevert } = require('@openzeppelin/test-helpers'); + +const $Checkpoints = artifacts.require('$Checkpoints'); + +// The library name may be 'Checkpoints' or 'CheckpointsUpgradeable' +const libraryName = $Checkpoints._json.contractName.replace(/^\$/, ''); + +const first = array => (array.length ? array[0] : undefined); +const last = array => (array.length ? array[array.length - 1] : undefined); + +contract('Checkpoints', function () { + beforeEach(async function () { + this.mock = await $Checkpoints.new(); + }); + + for (const length of VALUE_SIZES) { + describe(`Trace${length}`, function () { + beforeEach(async function () { + this.methods = { + at: (...args) => this.mock.methods[`$at_${libraryName}_Trace${length}(uint256,uint32)`](0, ...args), + latest: (...args) => this.mock.methods[`$latest_${libraryName}_Trace${length}(uint256)`](0, ...args), + latestCheckpoint: (...args) => + this.mock.methods[`$latestCheckpoint_${libraryName}_Trace${length}(uint256)`](0, ...args), + length: (...args) => this.mock.methods[`$length_${libraryName}_Trace${length}(uint256)`](0, ...args), + push: (...args) => this.mock.methods[`$push(uint256,uint${256 - length},uint${length})`](0, ...args), + lowerLookup: (...args) => this.mock.methods[`$lowerLookup(uint256,uint${256 - length})`](0, ...args), + upperLookup: (...args) => this.mock.methods[`$upperLookup(uint256,uint${256 - length})`](0, ...args), + upperLookupRecent: (...args) => + this.mock.methods[`$upperLookupRecent(uint256,uint${256 - length})`](0, ...args), + }; + }); + + describe('without checkpoints', function () { + it('at zero reverts', async function () { + // Reverts with array out of bound access, which is unspecified + await expectRevert.unspecified(this.methods.at(0)); + }); + + it('returns zero as latest value', async function () { + expect(await this.methods.latest()).to.be.bignumber.equal('0'); + + const ckpt = await this.methods.latestCheckpoint(); + expect(ckpt[0]).to.be.equal(false); + expect(ckpt[1]).to.be.bignumber.equal('0'); + expect(ckpt[2]).to.be.bignumber.equal('0'); + }); + + it('lookup returns 0', async function () { + expect(await this.methods.lowerLookup(0)).to.be.bignumber.equal('0'); + expect(await this.methods.upperLookup(0)).to.be.bignumber.equal('0'); + expect(await this.methods.upperLookupRecent(0)).to.be.bignumber.equal('0'); + }); + }); + + describe('with checkpoints', function () { + beforeEach('pushing checkpoints', async function () { + this.checkpoints = [ + { key: '2', value: '17' }, + { key: '3', value: '42' }, + { key: '5', value: '101' }, + { key: '7', value: '23' }, + { key: '11', value: '99' }, + ]; + for (const { key, value } of this.checkpoints) { + await this.methods.push(key, value); + } + }); + + it('at keys', async function () { + for (const [index, { key, value }] of this.checkpoints.entries()) { + const at = await this.methods.at(index); + expect(at._value).to.be.bignumber.equal(value); + expect(at._key).to.be.bignumber.equal(key); + } + }); + + it('length', async function () { + expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString()); + }); + + it('returns latest value', async function () { + expect(await this.methods.latest()).to.be.bignumber.equal(last(this.checkpoints).value); + + const ckpt = await this.methods.latestCheckpoint(); + expect(ckpt[0]).to.be.equal(true); + expect(ckpt[1]).to.be.bignumber.equal(last(this.checkpoints).key); + expect(ckpt[2]).to.be.bignumber.equal(last(this.checkpoints).value); + }); + + it('cannot push values in the past', async function () { + await expectRevertCustomError( + this.methods.push(last(this.checkpoints).key - 1, '0'), + 'CheckpointUnorderedInsertion', + [], + ); + }); + + it('can update last value', async function () { + const newValue = '42'; + + // check length before the update + expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString()); + + // update last key + await this.methods.push(last(this.checkpoints).key, newValue); + expect(await this.methods.latest()).to.be.bignumber.equal(newValue); + + // check that length did not change + expect(await this.methods.length()).to.be.bignumber.equal(this.checkpoints.length.toString()); + }); + + it('lower lookup', async function () { + for (let i = 0; i < 14; ++i) { + const value = first(this.checkpoints.filter(x => i <= x.key))?.value || '0'; + + expect(await this.methods.lowerLookup(i)).to.be.bignumber.equal(value); + } + }); + + it('upper lookup & upperLookupRecent', async function () { + for (let i = 0; i < 14; ++i) { + const value = last(this.checkpoints.filter(x => i >= x.key))?.value || '0'; + + expect(await this.methods.upperLookup(i)).to.be.bignumber.equal(value); + expect(await this.methods.upperLookupRecent(i)).to.be.bignumber.equal(value); + } + }); + + it('upperLookupRecent with more than 5 checkpoints', async function () { + const moreCheckpoints = [ + { key: '12', value: '22' }, + { key: '13', value: '131' }, + { key: '17', value: '45' }, + { key: '19', value: '31452' }, + { key: '21', value: '0' }, + ]; + const allCheckpoints = [].concat(this.checkpoints, moreCheckpoints); + + for (const { key, value } of moreCheckpoints) { + await this.methods.push(key, value); + } + + for (let i = 0; i < 25; ++i) { + const value = last(allCheckpoints.filter(x => i >= x.key))?.value || '0'; + expect(await this.methods.upperLookup(i)).to.be.bignumber.equal(value); + expect(await this.methods.upperLookupRecent(i)).to.be.bignumber.equal(value); + } + }); + }); + }); + } +}); diff --git a/test/utils/structs/DoubleEndedQueue.test.js b/test/utils/structs/DoubleEndedQueue.test.js index 2fbb8dc25..cbf37d76b 100644 --- a/test/utils/structs/DoubleEndedQueue.test.js +++ b/test/utils/structs/DoubleEndedQueue.test.js @@ -30,10 +30,10 @@ contract('DoubleEndedQueue', function () { }); it('reverts on accesses', async function () { - await expectRevertCustomError(this.deque.$popBack(0), 'Empty()'); - await expectRevertCustomError(this.deque.$popFront(0), 'Empty()'); - await expectRevertCustomError(this.deque.$back(0), 'Empty()'); - await expectRevertCustomError(this.deque.$front(0), 'Empty()'); + await expectRevertCustomError(this.deque.$popBack(0), 'QueueEmpty', []); + await expectRevertCustomError(this.deque.$popFront(0), 'QueueEmpty', []); + await expectRevertCustomError(this.deque.$back(0), 'QueueEmpty', []); + await expectRevertCustomError(this.deque.$front(0), 'QueueEmpty', []); }); }); @@ -54,7 +54,7 @@ contract('DoubleEndedQueue', function () { }); it('out of bounds access', async function () { - await expectRevertCustomError(this.deque.$at(0, this.content.length), 'OutOfBounds()'); + await expectRevertCustomError(this.deque.$at(0, this.content.length), 'QueueOutOfBounds', []); }); describe('push', function () { diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index b4b5ee372..67b19e39a 100644 --- a/test/utils/structs/EnumerableMap.behavior.js +++ b/test/utils/structs/EnumerableMap.behavior.js @@ -1,7 +1,8 @@ -const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers'); +const { expectEvent } = require('@openzeppelin/test-helpers'); const { expect } = require('chai'); const zip = require('lodash.zip'); +const { expectRevertCustomError } = require('../../helpers/customError'); function shouldBehaveLikeMap(keys, values, zeroValue, methods, events) { const [keyA, keyB, keyC] = keys; @@ -150,18 +151,10 @@ function shouldBehaveLikeMap(keys, values, zeroValue, methods, events) { expect(await methods.get(this.map, keyA).then(r => r.toString())).to.be.equal(valueA.toString()); }); it('missing value', async function () { - await expectRevert(methods.get(this.map, keyB), 'EnumerableMap: nonexistent key'); - }); - }); - - describe('get with message', function () { - it('existing value', async function () { - expect(await methods.getWithMessage(this.map, keyA, 'custom error string').then(r => r.toString())).to.be.equal( - valueA.toString(), - ); - }); - it('missing value', async function () { - await expectRevert(methods.getWithMessage(this.map, keyB, 'custom error string'), 'custom error string'); + const key = web3.utils.toHex(keyB); + await expectRevertCustomError(methods.get(this.map, keyB), 'EnumerableMapNonexistentKey', [ + key.length == 66 ? key : web3.utils.padLeft(key, 64, '0'), + ]); }); }); diff --git a/test/utils/structs/EnumerableMap.test.js b/test/utils/structs/EnumerableMap.test.js index 548e73775..8861fa731 100644 --- a/test/utils/structs/EnumerableMap.test.js +++ b/test/utils/structs/EnumerableMap.test.js @@ -14,6 +14,9 @@ const getMethods = ms => { ); }; +// Get the name of the library. In the transpiled code it will be EnumerableMapUpgradeable. +const library = EnumerableMap._json.contractName.replace(/^\$/, ''); + contract('EnumerableMap', function (accounts) { const [accountA, accountB, accountC] = accounts; @@ -38,17 +41,16 @@ contract('EnumerableMap', function (accounts) { getMethods({ set: '$set(uint256,address,uint256)', get: '$get(uint256,address)', - getWithMessage: '$get(uint256,address,string)', tryGet: '$tryGet(uint256,address)', remove: '$remove(uint256,address)', - length: '$length_EnumerableMap_AddressToUintMap(uint256)', - at: '$at_EnumerableMap_AddressToUintMap(uint256,uint256)', + length: `$length_${library}_AddressToUintMap(uint256)`, + at: `$at_${library}_AddressToUintMap(uint256,uint256)`, contains: '$contains(uint256,address)', - keys: '$keys_EnumerableMap_AddressToUintMap(uint256)', + keys: `$keys_${library}_AddressToUintMap(uint256)`, }), { - setReturn: 'return$set_EnumerableMap_AddressToUintMap_address_uint256', - removeReturn: 'return$remove_EnumerableMap_AddressToUintMap_address', + setReturn: `return$set_${library}_AddressToUintMap_address_uint256`, + removeReturn: `return$remove_${library}_AddressToUintMap_address`, }, ); }); @@ -61,18 +63,17 @@ contract('EnumerableMap', function (accounts) { constants.ZERO_ADDRESS, getMethods({ set: '$set(uint256,uint256,address)', - get: '$get_EnumerableMap_UintToAddressMap(uint256,uint256)', - getWithMessage: '$get_EnumerableMap_UintToAddressMap(uint256,uint256,string)', - tryGet: '$tryGet_EnumerableMap_UintToAddressMap(uint256,uint256)', - remove: '$remove_EnumerableMap_UintToAddressMap(uint256,uint256)', - length: '$length_EnumerableMap_UintToAddressMap(uint256)', - at: '$at_EnumerableMap_UintToAddressMap(uint256,uint256)', - contains: '$contains_EnumerableMap_UintToAddressMap(uint256,uint256)', - keys: '$keys_EnumerableMap_UintToAddressMap(uint256)', + get: `$get_${library}_UintToAddressMap(uint256,uint256)`, + tryGet: `$tryGet_${library}_UintToAddressMap(uint256,uint256)`, + remove: `$remove_${library}_UintToAddressMap(uint256,uint256)`, + length: `$length_${library}_UintToAddressMap(uint256)`, + at: `$at_${library}_UintToAddressMap(uint256,uint256)`, + contains: `$contains_${library}_UintToAddressMap(uint256,uint256)`, + keys: `$keys_${library}_UintToAddressMap(uint256)`, }), { - setReturn: 'return$set_EnumerableMap_UintToAddressMap_uint256_address', - removeReturn: 'return$remove_EnumerableMap_UintToAddressMap_uint256', + setReturn: `return$set_${library}_UintToAddressMap_uint256_address`, + removeReturn: `return$remove_${library}_UintToAddressMap_uint256`, }, ); }); @@ -85,18 +86,17 @@ contract('EnumerableMap', function (accounts) { constants.ZERO_BYTES32, getMethods({ set: '$set(uint256,bytes32,bytes32)', - get: '$get_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - getWithMessage: '$get_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32,string)', - tryGet: '$tryGet_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - remove: '$remove_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - length: '$length_EnumerableMap_Bytes32ToBytes32Map(uint256)', - at: '$at_EnumerableMap_Bytes32ToBytes32Map(uint256,uint256)', - contains: '$contains_EnumerableMap_Bytes32ToBytes32Map(uint256,bytes32)', - keys: '$keys_EnumerableMap_Bytes32ToBytes32Map(uint256)', + get: `$get_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, + tryGet: `$tryGet_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, + remove: `$remove_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, + length: `$length_${library}_Bytes32ToBytes32Map(uint256)`, + at: `$at_${library}_Bytes32ToBytes32Map(uint256,uint256)`, + contains: `$contains_${library}_Bytes32ToBytes32Map(uint256,bytes32)`, + keys: `$keys_${library}_Bytes32ToBytes32Map(uint256)`, }), { - setReturn: 'return$set_EnumerableMap_Bytes32ToBytes32Map_bytes32_bytes32', - removeReturn: 'return$remove_EnumerableMap_Bytes32ToBytes32Map_bytes32', + setReturn: `return$set_${library}_Bytes32ToBytes32Map_bytes32_bytes32`, + removeReturn: `return$remove_${library}_Bytes32ToBytes32Map_bytes32`, }, ); }); @@ -109,18 +109,17 @@ contract('EnumerableMap', function (accounts) { new BN('0'), getMethods({ set: '$set(uint256,uint256,uint256)', - get: '$get_EnumerableMap_UintToUintMap(uint256,uint256)', - getWithMessage: '$get_EnumerableMap_UintToUintMap(uint256,uint256,string)', - tryGet: '$tryGet_EnumerableMap_UintToUintMap(uint256,uint256)', - remove: '$remove_EnumerableMap_UintToUintMap(uint256,uint256)', - length: '$length_EnumerableMap_UintToUintMap(uint256)', - at: '$at_EnumerableMap_UintToUintMap(uint256,uint256)', - contains: '$contains_EnumerableMap_UintToUintMap(uint256,uint256)', - keys: '$keys_EnumerableMap_UintToUintMap(uint256)', + get: `$get_${library}_UintToUintMap(uint256,uint256)`, + tryGet: `$tryGet_${library}_UintToUintMap(uint256,uint256)`, + remove: `$remove_${library}_UintToUintMap(uint256,uint256)`, + length: `$length_${library}_UintToUintMap(uint256)`, + at: `$at_${library}_UintToUintMap(uint256,uint256)`, + contains: `$contains_${library}_UintToUintMap(uint256,uint256)`, + keys: `$keys_${library}_UintToUintMap(uint256)`, }), { - setReturn: 'return$set_EnumerableMap_UintToUintMap_uint256_uint256', - removeReturn: 'return$remove_EnumerableMap_UintToUintMap_uint256', + setReturn: `return$set_${library}_UintToUintMap_uint256_uint256`, + removeReturn: `return$remove_${library}_UintToUintMap_uint256`, }, ); }); @@ -133,18 +132,17 @@ contract('EnumerableMap', function (accounts) { new BN('0'), getMethods({ set: '$set(uint256,bytes32,uint256)', - get: '$get_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - getWithMessage: '$get_EnumerableMap_Bytes32ToUintMap(uint256,bytes32,string)', - tryGet: '$tryGet_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - remove: '$remove_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - length: '$length_EnumerableMap_Bytes32ToUintMap(uint256)', - at: '$at_EnumerableMap_Bytes32ToUintMap(uint256,uint256)', - contains: '$contains_EnumerableMap_Bytes32ToUintMap(uint256,bytes32)', - keys: '$keys_EnumerableMap_Bytes32ToUintMap(uint256)', + get: `$get_${library}_Bytes32ToUintMap(uint256,bytes32)`, + tryGet: `$tryGet_${library}_Bytes32ToUintMap(uint256,bytes32)`, + remove: `$remove_${library}_Bytes32ToUintMap(uint256,bytes32)`, + length: `$length_${library}_Bytes32ToUintMap(uint256)`, + at: `$at_${library}_Bytes32ToUintMap(uint256,uint256)`, + contains: `$contains_${library}_Bytes32ToUintMap(uint256,bytes32)`, + keys: `$keys_${library}_Bytes32ToUintMap(uint256)`, }), { - setReturn: 'return$set_EnumerableMap_Bytes32ToUintMap_bytes32_uint256', - removeReturn: 'return$remove_EnumerableMap_Bytes32ToUintMap_bytes32', + setReturn: `return$set_${library}_Bytes32ToUintMap_bytes32_uint256`, + removeReturn: `return$remove_${library}_Bytes32ToUintMap_bytes32`, }, ); }); diff --git a/test/utils/structs/EnumerableSet.test.js b/test/utils/structs/EnumerableSet.test.js index 862cb6d9e..3ba9d7ff7 100644 --- a/test/utils/structs/EnumerableSet.test.js +++ b/test/utils/structs/EnumerableSet.test.js @@ -12,6 +12,9 @@ const getMethods = ms => { ); }; +// Get the name of the library. In the transpiled code it will be EnumerableSetUpgradeable. +const library = EnumerableSet._json.contractName.replace(/^\$/, ''); + contract('EnumerableSet', function (accounts) { beforeEach(async function () { this.set = await EnumerableSet.new(); @@ -25,13 +28,13 @@ contract('EnumerableSet', function (accounts) { add: '$add(uint256,bytes32)', remove: '$remove(uint256,bytes32)', contains: '$contains(uint256,bytes32)', - length: '$length_EnumerableSet_Bytes32Set(uint256)', - at: '$at_EnumerableSet_Bytes32Set(uint256,uint256)', - values: '$values_EnumerableSet_Bytes32Set(uint256)', + length: `$length_${library}_Bytes32Set(uint256)`, + at: `$at_${library}_Bytes32Set(uint256,uint256)`, + values: `$values_${library}_Bytes32Set(uint256)`, }), { - addReturn: 'return$add_EnumerableSet_Bytes32Set_bytes32', - removeReturn: 'return$remove_EnumerableSet_Bytes32Set_bytes32', + addReturn: `return$add_${library}_Bytes32Set_bytes32`, + removeReturn: `return$remove_${library}_Bytes32Set_bytes32`, }, ); }); @@ -44,13 +47,13 @@ contract('EnumerableSet', function (accounts) { add: '$add(uint256,address)', remove: '$remove(uint256,address)', contains: '$contains(uint256,address)', - length: '$length_EnumerableSet_AddressSet(uint256)', - at: '$at_EnumerableSet_AddressSet(uint256,uint256)', - values: '$values_EnumerableSet_AddressSet(uint256)', + length: `$length_${library}_AddressSet(uint256)`, + at: `$at_${library}_AddressSet(uint256,uint256)`, + values: `$values_${library}_AddressSet(uint256)`, }), { - addReturn: 'return$add_EnumerableSet_AddressSet_address', - removeReturn: 'return$remove_EnumerableSet_AddressSet_address', + addReturn: `return$add_${library}_AddressSet_address`, + removeReturn: `return$remove_${library}_AddressSet_address`, }, ); }); @@ -63,13 +66,13 @@ contract('EnumerableSet', function (accounts) { add: '$add(uint256,uint256)', remove: '$remove(uint256,uint256)', contains: '$contains(uint256,uint256)', - length: '$length_EnumerableSet_UintSet(uint256)', - at: '$at_EnumerableSet_UintSet(uint256,uint256)', - values: '$values_EnumerableSet_UintSet(uint256)', + length: `$length_${library}_UintSet(uint256)`, + at: `$at_${library}_UintSet(uint256,uint256)`, + values: `$values_${library}_UintSet(uint256)`, }), { - addReturn: 'return$add_EnumerableSet_UintSet_uint256', - removeReturn: 'return$remove_EnumerableSet_UintSet_uint256', + addReturn: `return$add_${library}_UintSet_uint256`, + removeReturn: `return$remove_${library}_UintSet_uint256`, }, ); });