diff --git a/.changeset/brown-seals-sing.md b/.changeset/brown-seals-sing.md new file mode 100644 index 00000000000..7f4cff3dcc0 --- /dev/null +++ b/.changeset/brown-seals-sing.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`SafeERC20`: Add `trySafeTransfer` and `trySafeTransferFrom` that do not revert and return false if the transfer is not successful. diff --git a/.changeset/brown-turkeys-marry.md b/.changeset/brown-turkeys-marry.md new file mode 100644 index 00000000000..0440f0d9464 --- /dev/null +++ b/.changeset/brown-turkeys-marry.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ER6909TokenSupply`: Add an extension of ERC6909 which tracks total supply for each token id. diff --git a/.changeset/dirty-bananas-shake.md b/.changeset/dirty-bananas-shake.md new file mode 100644 index 00000000000..4e10a427c40 --- /dev/null +++ b/.changeset/dirty-bananas-shake.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC6909ContentURI`: Add an extension of ERC6909 which adds content URI functionality. diff --git a/.changeset/eighty-hounds-promise.md b/.changeset/eighty-hounds-promise.md deleted file mode 100644 index 3727a6515f0..00000000000 --- a/.changeset/eighty-hounds-promise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Strings`: Add `parseUint`, `parseInt`, `parseHexUint` and `parseAddress` to parse strings into numbers and addresses. Also provide variants of these functions that parse substrings, and `tryXxx` variants that do not revert on invalid input. diff --git a/.changeset/famous-timers-compare.md b/.changeset/famous-timers-compare.md new file mode 100644 index 00000000000..2c8d184870b --- /dev/null +++ b/.changeset/famous-timers-compare.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC2771Forwarder`: Expose the `_isTrustedByTarget` internal function to check whether a target trusts the forwarder. diff --git a/.changeset/four-chairs-help.md b/.changeset/four-chairs-help.md deleted file mode 100644 index cbd0076075e..00000000000 --- a/.changeset/four-chairs-help.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"openzeppelin-solidity": minor ---- - -`Clones`: Add `cloneWithImmutableArgs` and `cloneDeterministicWithImmutableArgs` variants that create clones with per-instance immutable arguments. The immutable arguments can be retrieved using `fetchCloneArgs`. The corresponding `predictDeterministicWithImmutableArgs` function is also included. diff --git a/.changeset/good-cameras-rush.md b/.changeset/good-cameras-rush.md new file mode 100644 index 00000000000..ebe663c7a6c --- /dev/null +++ b/.changeset/good-cameras-rush.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`EnumerableMap`: Add `clear` function to EnumerableMaps which deletes all entries in the map. diff --git a/.changeset/good-cameras-serve.md b/.changeset/good-cameras-serve.md new file mode 100644 index 00000000000..1f1895504dd --- /dev/null +++ b/.changeset/good-cameras-serve.md @@ -0,0 +1,5 @@ +--- +"openzeppelin-solidity": minor +--- + +`Calldata`: Library with `emptyBytes` and `emptyString` functions to generate empty `bytes` and `string` calldata types. diff --git a/.changeset/gorgeous-apes-jam.md b/.changeset/gorgeous-apes-jam.md new file mode 100644 index 00000000000..14ca3522e77 --- /dev/null +++ b/.changeset/gorgeous-apes-jam.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`TimelockController`: Receive function is now virtual. diff --git a/.changeset/great-lions-hear.md b/.changeset/great-lions-hear.md deleted file mode 100644 index 2be5de253a8..00000000000 --- a/.changeset/great-lions-hear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`VotesExtended`: Create an extension of `Votes` which checkpoints balances and delegates. diff --git a/.changeset/green-drinks-report.md b/.changeset/green-drinks-report.md new file mode 100644 index 00000000000..983062cbbe7 --- /dev/null +++ b/.changeset/green-drinks-report.md @@ -0,0 +1,5 @@ +--- +"openzeppelin-solidity": minor +--- + +`Pausable`: Stop explicitly setting `paused` to `false` during construction. diff --git a/.changeset/healthy-books-shout.md b/.changeset/healthy-books-shout.md deleted file mode 100644 index 274e7a48868..00000000000 --- a/.changeset/healthy-books-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`CAIP2` and `CAIP10`: Add libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers. diff --git a/.changeset/hot-shrimps-wait.md b/.changeset/hot-shrimps-wait.md deleted file mode 100644 index e4e96a981ad..00000000000 --- a/.changeset/hot-shrimps-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Packing`: Add variants for packing `bytes10` and `bytes22` diff --git a/.changeset/lovely-dodos-lay.md b/.changeset/lovely-dodos-lay.md deleted file mode 100644 index da225132630..00000000000 --- a/.changeset/lovely-dodos-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`NoncesKeyed`: Add a variant of `Nonces` that implements the ERC-4337 entrypoint nonce system. diff --git a/.changeset/lucky-teachers-sip.md b/.changeset/lucky-teachers-sip.md new file mode 100644 index 00000000000..fab22e2665d --- /dev/null +++ b/.changeset/lucky-teachers-sip.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`EnumerableSet`: Add `Bytes32x2Set` that handles (ordered) pairs of bytes32. diff --git a/.changeset/pink-wasps-hammer.md b/.changeset/pink-wasps-hammer.md deleted file mode 100644 index 69f63d3ca19..00000000000 --- a/.changeset/pink-wasps-hammer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`GovernorCountingOverridable`: Add a governor counting module that enables token holders to override the vote of their delegate. diff --git a/.changeset/pretty-lobsters-tan.md b/.changeset/pretty-lobsters-tan.md new file mode 100644 index 00000000000..d3b8644ff5f --- /dev/null +++ b/.changeset/pretty-lobsters-tan.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`GovernorProposalGuardian`: Add a governance extension that defines a proposal guardian who can cancel proposals at any stage in their lifecycle. diff --git a/.changeset/proud-cooks-do.md b/.changeset/proud-cooks-do.md new file mode 100644 index 00000000000..e3d4331aeb2 --- /dev/null +++ b/.changeset/proud-cooks-do.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC6909Metadata`: Add an extension of ERC6909 which adds metadata functionality. diff --git a/.changeset/proud-planes-arrive.md b/.changeset/proud-planes-arrive.md deleted file mode 100644 index 60c831bd690..00000000000 --- a/.changeset/proud-planes-arrive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Bytes`: Add a library of common operations that operate on `bytes` objects. diff --git a/.changeset/seven-donkeys-tap.md b/.changeset/seven-donkeys-tap.md deleted file mode 100644 index 25d2305b9b8..00000000000 --- a/.changeset/seven-donkeys-tap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -Update some pragma directives to ensure that all file requirements match that of the files they import. diff --git a/.changeset/sixty-tips-wink.md b/.changeset/sixty-tips-wink.md new file mode 100644 index 00000000000..35c14cb890d --- /dev/null +++ b/.changeset/sixty-tips-wink.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`EnumerableSet`: Add `clear` function to EnumerableSets which deletes all values in the set. diff --git a/.changeset/small-seahorses-bathe.md b/.changeset/small-seahorses-bathe.md deleted file mode 100644 index 7b5ec794f38..00000000000 --- a/.changeset/small-seahorses-bathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC7579Utils`: Add a reusable library to interact with ERC-7579 modular accounts diff --git a/.changeset/ten-hats-begin.md b/.changeset/ten-hats-begin.md new file mode 100644 index 00000000000..bb7ab77e2ff --- /dev/null +++ b/.changeset/ten-hats-begin.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC6909`: Add a standard implementation of ERC6909. diff --git a/.changeset/ten-peas-mix.md b/.changeset/ten-peas-mix.md new file mode 100644 index 00000000000..4e7ae24b073 --- /dev/null +++ b/.changeset/ten-peas-mix.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Hashes`: Expose `efficientKeccak256` for hashing non-commutative pairs of bytes32 without allocating extra memory. diff --git a/.changeset/tricky-bats-pretend.md b/.changeset/tricky-bats-pretend.md deleted file mode 100644 index 2809d329357..00000000000 --- a/.changeset/tricky-bats-pretend.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC1363Utils`: Add helper similar to the existing `ERC721Utils` and `ERC1155Utils` diff --git a/.changeset/weak-roses-bathe.md b/.changeset/weak-roses-bathe.md deleted file mode 100644 index 416b2e746d3..00000000000 --- a/.changeset/weak-roses-bathe.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC4337Utils`: Add a reusable library to manipulate user operations and interact with ERC-4337 contracts diff --git a/.github/actions/gas-compare/action.yml b/.github/actions/gas-compare/action.yml index 23a756f3d29..e764ad5d75b 100644 --- a/.github/actions/gas-compare/action.yml +++ b/.github/actions/gas-compare/action.yml @@ -44,7 +44,8 @@ runs: shell: bash - name: Save report if: github.event_name != 'pull_request' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: gasreport + overwrite: true path: ${{ inputs.out_report }} diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index b68fec64977..3c5fc602e13 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -19,4 +19,4 @@ runs: - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 with: - version: nightly + version: stable diff --git a/.github/actions/storage-layout/action.yml b/.github/actions/storage-layout/action.yml index 573564b6758..fb68d5f6bea 100644 --- a/.github/actions/storage-layout/action.yml +++ b/.github/actions/storage-layout/action.yml @@ -50,7 +50,8 @@ runs: shell: bash - name: Save artifacts if: github.event_name != 'pull_request' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: layout + overwrite: true path: ${{ inputs.out_layout }} diff --git a/.gitignore b/.gitignore index b2b1eab1ef7..50f1bf5b7bc 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ npm-debug.log # docs artifacts docs/modules/api +build/site # only used to package @openzeppelin/contracts contracts/build/ diff --git a/CHANGELOG.md b/CHANGELOG.md index cc52db338ca..bae9cb6c96c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,62 @@ # Changelog +### Breaking Changes + +- Replace `GovernorCountingOverridable.VoteReceipt` struct parameter member names `hasOverriden` and `overridenWeight` for `hasOverridden` and `overriddenWeight` respectively. + +#### Custom error changes + +- Replace `GovernorAlreadyOverridenVote` with `GovernorAlreadyOverriddenVote`. + +## 5.2.0 (2025-01-08) + +### Breaking Changes + +#### Custom error changes + +This version comes with changes to the custom error identifiers. Contracts previously depending on the following errors should be replaced accordingly: + +- Replace `Errors.FailedCall` with a bubbled-up revert reason in `Address.sendValue`. + +### Changes by category + +#### General + +- Update some pragma directives to ensure that all file requirements match that of the files they import. ([#5273](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5273)) + +#### Account + +- `ERC4337Utils`: Add a reusable library to manipulate user operations and interact with ERC-4337 contracts ([#5274](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5274)) +- `ERC7579Utils`: Add a reusable library to interact with ERC-7579 modular accounts ([#5274](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5274)) + +#### Governance + +- `GovernorCountingOverridable`: Add a governor counting module that enables token holders to override the vote of their delegate. ([#5192](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5192)) +- `VotesExtended`: Create an extension of `Votes` which checkpoints balances and delegates. ([#5192](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5192)) + +### Proxy + +- `Clones`: Add `cloneWithImmutableArgs` and `cloneDeterministicWithImmutableArgs` variants that create clones with per-instance immutable arguments. The immutable arguments can be retrieved using `fetchCloneArgs`. The corresponding `predictDeterministicWithImmutableArgs` function is also included. ([#5109](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5109)) + +### Tokens + +- `ERC1363Utils`: Add helper similar to the existing `ERC721Utils` and `ERC1155Utils` ([#5133](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5133)) + +### Utils + +- `Address`: bubble up revert data on `sendValue` failed call ([#5418](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5418)) +- `Bytes`: Add a library of common operations that operate on `bytes` objects. ([#5252](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5252)) +- `CAIP2` and `CAIP10`: Add libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers. ([#5252](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5252)) +- `NoncesKeyed`: Add a variant of `Nonces` that implements the ERC-4337 entrypoint nonce system. ([#5272](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5272)) +- `Packing`: Add variants for packing `bytes10` and `bytes22` ([#5274](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5274)) +- `Strings`: Add `parseUint`, `parseInt`, `parseHexUint` and `parseAddress` to parse strings into numbers and addresses. Also provide variants of these functions that parse substrings, and `tryXxx` variants that do not revert on invalid input. ([#5166](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5166)) ## 5.1.0 (2024-10-17) ### Breaking changes - `ERC1967Utils`: Removed duplicate declaration of the `Upgraded`, `AdminChanged` and `BeaconUpgraded` events. These events are still available through the `IERC1967` interface located under the `contracts/interfaces/` directory. Minimum pragma version is now 0.8.21. -- `Governor`, `GovernorCountingSimple`: The `_countVote` virtual function now returns an `uint256` with the total votes casted. This change allows for more flexibility for partial and fractional voting. Upgrading users may get a compilation error that can be fixed by adding a return statement to the `_countVote` function. +- `Governor`, `GovernorCountingSimple`: The `_countVote` virtual function now returns an `uint256` with the total votes cast. This change allows for more flexibility for partial and fractional voting. Upgrading users may get a compilation error that can be fixed by adding a return statement to the `_countVote` function. #### Custom error changes diff --git a/FUNDING.json b/FUNDING.json index c67286216ce..0a362ba351f 100644 --- a/FUNDING.json +++ b/FUNDING.json @@ -3,5 +3,8 @@ "ethereum": { "ownedBy": "0xAeb37910f93486C85A1F8F994b67E8187554d664" } + }, + "opRetro": { + "projectId": "0x939241afa4c4b9e1dda6b8250baa8f04fa8b0debce738cfd324c0b18f9926d25" } } diff --git a/LICENSE b/LICENSE index b2fee8f2116..367f411ba02 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016-2024 Zeppelin Group Ltd +Copyright (c) 2016-2025 Zeppelin Group Ltd Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index fa7b4e31e55..60d0a430a1d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Coverage Status](https://codecov.io/gh/OpenZeppelin/openzeppelin-contracts/graph/badge.svg)](https://codecov.io/gh/OpenZeppelin/openzeppelin-contracts) [![GitPOAPs](https://public-api.gitpoap.io/v1/repo/OpenZeppelin/openzeppelin-contracts/badge)](https://www.gitpoap.io/gh/OpenZeppelin/openzeppelin-contracts) [![Docs](https://img.shields.io/badge/docs-%F0%9F%93%84-yellow)](https://docs.openzeppelin.com/contracts) -[![Forum](https://img.shields.io/badge/forum-%F0%9F%92%AC-yellow)](https://docs.openzeppelin.com/contracts) +[![Forum](https://img.shields.io/badge/forum-%F0%9F%92%AC-yellow)](https://forum.openzeppelin.com/) **A library for secure smart contract development.** Build on a solid foundation of community-vetted code. @@ -70,7 +70,7 @@ The guides in the [documentation site](https://docs.openzeppelin.com/contracts) * [Tokens](https://docs.openzeppelin.com/contracts/tokens): create tradeable assets or collectives, and distribute them via [Crowdsales](https://docs.openzeppelin.com/contracts/crowdsales). * [Utilities](https://docs.openzeppelin.com/contracts/utilities): generic useful tools including non-overflowing math, signature verification, and trustless paying systems. -The [full API](https://docs.openzeppelin.com/contracts/api/token/ERC20) is also thoroughly documented, and serves as a great reference when developing your smart contract application. You can also ask for help or follow Contracts's development in the [community forum](https://forum.openzeppelin.com). +The [full API](https://docs.openzeppelin.com/contracts/api/token/ERC20) is also thoroughly documented, and serves as a great reference when developing your smart contract application. You can also ask for help or follow Contracts' development in the [community forum](https://forum.openzeppelin.com). Finally, you may want to take a look at the [guides on our blog](https://blog.openzeppelin.com/), which cover several common use cases and good practices. The following articles provide great background reading, though please note that some of the referenced tools have changed, as the tooling in the ecosystem continues to rapidly evolve. diff --git a/RELEASING.md b/RELEASING.md index 06dd218e874..bc2b2d5003c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -4,7 +4,7 @@ OpenZeppelin Contracts uses a fully automated release process that takes care of ## Changesets -[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. +[Changesets](https://github.com/changesets/changesets/) are 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. ## Branching model diff --git a/SECURITY.md b/SECURITY.md index 9922c45e7a1..bea59e1f2ad 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -40,4 +40,4 @@ Note as well that the Solidity language itself only guarantees security updates ## Legal -Smart contracts are a nascent technology and carry a high level of technical risk and uncertainty. 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. Your use of the project is also governed by the terms found at www.openzeppelin.com/tos (the "Terms"). As set out in the Terms, you are solely responsible for any use of OpenZeppelin Contracts and you assume all risks associated with any such use. This Security Policy in no way evidences or represents an on-going duty by any contributor, including OpenZeppelin, to correct any flaws or alert you to all or any of the potential risks of utilizing the project. +Smart contracts are a nascent technology and carry a high level of technical risk and uncertainty. 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. Your use of the project is also governed by the terms found at www.openzeppelin.com/tos (the "Terms"). As set out in the Terms, you are solely responsible for any use of OpenZeppelin Contracts and you assume all risks associated with any such use. This Security Policy in no way evidences or represents an ongoing duty by any contributor, including OpenZeppelin, to correct any flaws or alert you to all or any of the potential risks of utilizing the project. diff --git a/audits/2017-03.md b/audits/2017-03.md index 4cd6dbfd30d..e2f6cc172c9 100644 --- a/audits/2017-03.md +++ b/audits/2017-03.md @@ -20,7 +20,7 @@ The git commit hash we evaluated is: # Disclaimer -The audit makes no statements or warrantees about utility of the code, safety of the code, suitability of the business model, regulatory regime for the business model, or any other statements about fitness of the contracts to purpose, or their bugfree status. The audit documentation is for discussion purposes only. +The audit makes no statements or warrantees about utility of the code, safety of the code, suitability of the business model, regulatory regime for the business model, or any other statements about fitness of the contracts to purpose, or their bug free status. The audit documentation is for discussion purposes only. # Executive Summary diff --git a/certora/README.md b/certora/README.md index cd85ba3d4c4..ff2ccdf7d5a 100644 --- a/certora/README.md +++ b/certora/README.md @@ -2,14 +2,14 @@ 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 is 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. +> 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 diff --git a/certora/run.js b/certora/run.js index a5ad9eece3b..91f4a6aece1 100755 --- a/certora/run.js +++ b/certora/run.js @@ -160,7 +160,7 @@ function writeEntry(spec, contract, success, url) { formatRow( spec, contract, - success ? ':x:' : ':heavy_check_mark:', + success ? ':heavy_check_mark:' : ':x:', url ? `[link](${url?.replace('/output/', '/jobStatus/')})` : 'error', url ? `[link](${url})` : 'error', ), diff --git a/certora/specs/AccessControlDefaultAdminRules.spec b/certora/specs/AccessControlDefaultAdminRules.spec index 2f5bb9d4538..5860fd564c5 100644 --- a/certora/specs/AccessControlDefaultAdminRules.spec +++ b/certora/specs/AccessControlDefaultAdminRules.spec @@ -176,7 +176,7 @@ rule renounceRoleEffect(env e, bytes32 role) { /* ┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Rule: defaultAdmin is only affected by accepting an admin transfer or renoucing │ +│ Rule: defaultAdmin is only affected by accepting an admin transfer or renouncing │ └─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ */ rule noDefaultAdminChange(env e, method f, calldataarg args) { @@ -188,7 +188,7 @@ rule noDefaultAdminChange(env e, method f, calldataarg args) { f.selector == sig:acceptDefaultAdminTransfer().selector || f.selector == sig:renounceRole(bytes32,address).selector ), - "default admin is only affected by accepting an admin transfer or renoucing"; + "default admin is only affected by accepting an admin transfer or renouncing"; } /* diff --git a/certora/specs/ERC20FlashMint.spec b/certora/specs/ERC20FlashMint.spec index 4071052ea7f..6942495b0bf 100644 --- a/certora/specs/ERC20FlashMint.spec +++ b/certora/specs/ERC20FlashMint.spec @@ -18,7 +18,7 @@ methods { */ ghost mapping(address => mathint) trackedMintAmount; ghost mapping(address => mathint) trackedBurnAmount; -ghost mapping(address => mapping(address => mathint)) trackedTransferedAmount; +ghost mapping(address => mapping(address => mathint)) trackedTransferredAmount; function specUpdate(address from, address to, uint256 amount) { if (from == 0 && to == 0) { assert(false); } // defensive @@ -28,7 +28,7 @@ function specUpdate(address from, address to, uint256 amount) { } else if (to == 0) { trackedBurnAmount[from] = amount; } else { - trackedTransferedAmount[from][to] = amount; + trackedTransferredAmount[from][to] = amount; } } @@ -51,5 +51,5 @@ rule checkMintAndBurn(env e) { assert trackedMintAmount[receiver] == to_mathint(amount); assert trackedBurnAmount[receiver] == amount + to_mathint(recipient == 0 ? fees : 0); - assert (fees > 0 && recipient != 0) => trackedTransferedAmount[receiver][recipient] == to_mathint(fees); + assert (fees > 0 && recipient != 0) => trackedTransferredAmount[receiver][recipient] == to_mathint(fees); } diff --git a/contracts/access/AccessControl.sol b/contracts/access/AccessControl.sol index 3e3341e9cfd..1c3807bc53d 100644 --- a/contracts/access/AccessControl.sol +++ b/contracts/access/AccessControl.sol @@ -191,7 +191,7 @@ abstract contract AccessControl is Context, IAccessControl, ERC165 { } /** - * @dev Attempts to revoke `role` to `account` and returns a boolean indicating if `role` was revoked. + * @dev Attempts to revoke `role` from `account` and returns a boolean indicating if `role` was revoked. * * Internal function without access restriction. * diff --git a/contracts/account/utils/draft-ERC4337Utils.sol b/contracts/account/utils/draft-ERC4337Utils.sol index 355b0720b41..b17a0db81a4 100644 --- a/contracts/account/utils/draft-ERC4337Utils.sol +++ b/contracts/account/utils/draft-ERC4337Utils.sol @@ -1,9 +1,11 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (account/utils/draft-ERC4337Utils.sol) pragma solidity ^0.8.20; -import {PackedUserOperation} from "../../interfaces/draft-IERC4337.sol"; +import {IEntryPoint, PackedUserOperation} from "../../interfaces/draft-IERC4337.sol"; import {Math} from "../../utils/math/Math.sol"; +import {Calldata} from "../../utils/Calldata.sol"; import {Packing} from "../../utils/Packing.sol"; /** @@ -14,6 +16,9 @@ import {Packing} from "../../utils/Packing.sol"; library ERC4337Utils { using Packing for *; + /// @dev Address of the entrypoint v0.7.0 + IEntryPoint internal constant ENTRYPOINT_V07 = IEntryPoint(0x0000000071727De22E5E9d8BAf0edAc6f37da032); + /// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) return this value on success. uint256 internal constant SIG_VALIDATION_SUCCESS = 0; @@ -106,7 +111,7 @@ library ERC4337Utils { /// @dev Returns `factoryData` from the {PackedUserOperation}, or empty bytes if the initCode is empty or not properly formatted. function factoryData(PackedUserOperation calldata self) internal pure returns (bytes calldata) { - return self.initCode.length < 20 ? _emptyCalldataBytes() : self.initCode[20:]; + return self.initCode.length < 20 ? Calldata.emptyBytes() : self.initCode[20:]; } /// @dev Returns `verificationGasLimit` from the {PackedUserOperation}. @@ -156,14 +161,6 @@ library ERC4337Utils { /// @dev Returns the fourth section of `paymasterAndData` from the {PackedUserOperation}. function paymasterData(PackedUserOperation calldata self) internal pure returns (bytes calldata) { - return self.paymasterAndData.length < 52 ? _emptyCalldataBytes() : self.paymasterAndData[52:]; - } - - // slither-disable-next-line write-after-write - function _emptyCalldataBytes() private pure returns (bytes calldata result) { - assembly ("memory-safe") { - result.offset := 0 - result.length := 0 - } + return self.paymasterAndData.length < 52 ? Calldata.emptyBytes() : self.paymasterAndData[52:]; } } diff --git a/contracts/account/utils/draft-ERC7579Utils.sol b/contracts/account/utils/draft-ERC7579Utils.sol index 8be094e649d..28aa64d5cf6 100644 --- a/contracts/account/utils/draft-ERC7579Utils.sol +++ b/contracts/account/utils/draft-ERC7579Utils.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (account/utils/draft-ERC7579Utils.sol) pragma solidity ^0.8.20; diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index b03c4968430..314ddd7d9fb 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (finance/VestingWallet.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (finance/VestingWallet.sol) pragma solidity ^0.8.20; import {IERC20} from "../token/ERC20/IERC20.sol"; @@ -16,7 +16,7 @@ import {Ownable} from "../access/Ownable.sol"; * 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 + * By setting the duration to 0, one can configure this contract to behave like an asset timelock that holds tokens for * a beneficiary until a specified time. * * NOTE: Since the wallet is {Ownable}, and ownership can be transferred, it is possible to sell unvested tokens. diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index f45aba096eb..4b67b199538 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (governance/Governor.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (governance/Governor.sol) pragma solidity ^0.8.20; @@ -455,7 +455,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 * performed (for example adding a vault/timelock). * * NOTE: Calling this function directly will NOT check the current state of the proposal, set the executed flag to - * true or emit the `ProposalExecuted` event. Executing a proposal should be done using {execute} or {_execute}. + * true or emit the `ProposalExecuted` event. Executing a proposal should be done using {execute}. */ function _executeOperations( uint256 /* proposalId */, @@ -484,11 +484,8 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 // changes it. The `getProposalId` duplication has a cost that is limited, and that we accept. uint256 proposalId = getProposalId(targets, values, calldatas, descriptionHash); - // public cancel restrictions (on top of existing _cancel restrictions). - _validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Pending)); - if (_msgSender() != proposalProposer(proposalId)) { - revert GovernorOnlyProposer(_msgSender()); - } + address caller = _msgSender(); + if (!_validateCancel(proposalId, caller)) revert GovernorUnableToCancel(proposalId, caller); return _cancel(targets, values, calldatas, descriptionHash); } @@ -805,6 +802,15 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 } } + /** + * @dev Check if the `caller` can cancel the proposal with the given `proposalId`. + * + * The default implementation allows the proposal proposer to cancel the proposal during the pending state. + */ + function _validateCancel(uint256 proposalId, address caller) internal view virtual returns (bool) { + return (state(proposalId) == ProposalState.Pending) && caller == proposalProposer(proposalId); + } + /** * @inheritdoc IERC6372 */ diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 36ef099a7d5..702d2beb7f5 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -39,11 +39,6 @@ interface IGovernor is IERC165, IERC6372 { */ error GovernorDisabledDeposit(); - /** - * @dev The `account` is not a proposer. - */ - error GovernorOnlyProposer(address account); - /** * @dev The `account` is not the governance executor. */ @@ -112,6 +107,11 @@ interface IGovernor is IERC165, IERC6372 { */ error GovernorInvalidSignature(address voter); + /** + * @dev The given `account` is unable to cancel the proposal with given `proposalId`. + */ + error GovernorUnableToCancel(uint256 proposalId, address account); + /** * @dev Emitted when a proposal is created. */ diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index d390d1be82d..a9d29808372 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -20,7 +20,7 @@ For a written walkthrough, check out our guide on xref:ROOT:governance.adoc[How 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. +* {GovernorVotes}: Extracts voting weight from an {IVotes} contract. * {GovernorVotesQuorumFraction}: Combines with `GovernorVotes` to set the quorum as a fraction of the total token supply. @@ -30,7 +30,7 @@ Counting modules determine valid voting options. * {GovernorCountingFractional}: A more modular voting system that allows a user to vote with only part of its voting power, and to split that weight arbitrarily between the 3 different options (Against, For and Abstain). -* {GovernorCountingOverridable}: An extended version of `GovernorCountingSimple` which allows delegatees to override their delegates while the vote is live. +* {GovernorCountingOverridable}: An extended version of `GovernorCountingSimple` which allows delegatees to override their delegates while the vote is live. Must be used in conjunction with {VotesExtended}. Timelock extensions add a delay for governance decisions to be executed. The workflow is extended to require a `queue` step before execution. With these modules, proposals are executed by the external timelock contract, thus it is the timelock that has to hold the assets that are being governed. @@ -48,6 +48,8 @@ Other extensions can customize the behavior or interface in multiple ways. * {GovernorPreventLateQuorum}: Ensures there is a minimum voting period after quorum is reached as a security protection against large voters. +* {GovernorProposalGuardian}: Adds a proposal guardian that can cancel proposals at any stage in their lifecycle--this permission is passed on to the proposers if the guardian is not set. + In addition to modules and extensions, the core contract requires a few virtual functions to be implemented to your particular specifications: * <>: Delay (in ERC-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. @@ -68,7 +70,7 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorCountingFractional}} -{{GovernorCountingOverride}} +{{GovernorCountingOverridable}} {{GovernorVotes}} @@ -88,6 +90,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorStorage}} +{{GovernorProposalGuardian}} + == Utils {{Votes}} @@ -103,7 +107,7 @@ In a governance system, the {TimelockController} contract is in charge of introd [[timelock-terminology]] ==== Terminology -* *Operation:* A transaction (or a set of transactions) that is the subject of the timelock. It has to be scheduled by a proposer and executed by an executor. The timelock enforces a minimum delay between the proposition and the execution (see xref:access-control.adoc#operation_lifecycle[operation lifecycle]). If the operation contains multiple transactions (batch mode), they are executed atomically. Operations are identified by the hash of their content. +* *Operation:* A transaction (or a set of transactions) that is the subject of the timelock. It has to be scheduled by a proposer and executed by an executor. The timelock enforces a minimum delay between the proposition and the execution. If the operation contains multiple transactions (batch mode), they are executed atomically. Operations are identified by the hash of their content. * *Operation status:* ** *Unset:* An operation that is not part of the timelock mechanism. ** *Waiting:* An operation that has been scheduled, before the timer expires. diff --git a/contracts/governance/TimelockController.sol b/contracts/governance/TimelockController.sol index 349d940fd50..d2ba1701610 100644 --- a/contracts/governance/TimelockController.sol +++ b/contracts/governance/TimelockController.sol @@ -152,7 +152,7 @@ contract TimelockController is AccessControl, ERC721Holder, ERC1155Holder { /** * @dev Contract might receive/hold ETH as part of the maintenance process. */ - receive() external payable {} + receive() external payable virtual {} /** * @dev See {IERC165-supportsInterface}. diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index d2231bb9529..958de671c4c 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -125,9 +125,9 @@ abstract contract GovernorCountingFractional is Governor { * * `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))` * - * NOTE: Consider that fractional voting restricts the number of casted vote (in each category) to 128 bits. + * NOTE: Consider that fractional voting restricts the number of casted votes (in each category) to 128 bits. * Depending on how many decimals the underlying token has, a single voter may require to split their vote into - * multiple vote operations. For precision higher than ~30 decimals, large token holders may require an + * multiple vote operations. For precision higher than ~30 decimals, large token holders may require a * potentially large number of calls to cast all their votes. The voter has the possibility to cast all the * remaining votes in a single operation using the traditional "bravo" vote. */ diff --git a/contracts/governance/extensions/GovernorCountingOverridable.sol b/contracts/governance/extensions/GovernorCountingOverridable.sol index db375a93f47..43d1401db49 100644 --- a/contracts/governance/extensions/GovernorCountingOverridable.sol +++ b/contracts/governance/extensions/GovernorCountingOverridable.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (governance/extensions/GovernorCountingOverridable.sol) pragma solidity ^0.8.20; @@ -26,8 +27,8 @@ abstract contract GovernorCountingOverridable is GovernorVotes { struct VoteReceipt { uint8 casted; // 0 if vote was not casted. Otherwise: support + 1 - bool hasOverriden; - uint208 overridenWeight; + bool hasOverridden; + uint208 overriddenWeight; } struct ProposalVote { @@ -41,7 +42,7 @@ abstract contract GovernorCountingOverridable is GovernorVotes { /// @dev A delegated vote on `proposalId` was overridden by `weight` event OverrideVoteCast(address indexed voter, uint256 proposalId, uint8 support, uint256 weight, string reason); - error GovernorAlreadyOverridenVote(address account); + error GovernorAlreadyOverriddenVote(address account); mapping(uint256 proposalId => ProposalVote) private _proposalVotes; @@ -69,7 +70,7 @@ abstract contract GovernorCountingOverridable is GovernorVotes { * @dev Check if an `account` has overridden their delegate for a proposal. */ function hasVotedOverride(uint256 proposalId, address account) public view virtual returns (bool) { - return _proposalVotes[proposalId].voteReceipt[account].hasOverriden; + return _proposalVotes[proposalId].voteReceipt[account].hasOverridden; } /** @@ -121,7 +122,7 @@ abstract contract GovernorCountingOverridable is GovernorVotes { revert GovernorAlreadyCastVote(account); } - totalWeight -= proposalVote.voteReceipt[account].overridenWeight; + totalWeight -= proposalVote.voteReceipt[account].overriddenWeight; proposalVote.votes[support] += totalWeight; proposalVote.voteReceipt[account].casted = support + 1; @@ -140,26 +141,26 @@ abstract contract GovernorCountingOverridable is GovernorVotes { revert GovernorInvalidVoteType(); } - if (proposalVote.voteReceipt[account].hasOverriden) { - revert GovernorAlreadyOverridenVote(account); + if (proposalVote.voteReceipt[account].hasOverridden) { + revert GovernorAlreadyOverriddenVote(account); } uint256 snapshot = proposalSnapshot(proposalId); - uint256 overridenWeight = VotesExtended(address(token())).getPastBalanceOf(account, snapshot); + uint256 overriddenWeight = VotesExtended(address(token())).getPastBalanceOf(account, snapshot); address delegate = VotesExtended(address(token())).getPastDelegate(account, snapshot); uint8 delegateCasted = proposalVote.voteReceipt[delegate].casted; - proposalVote.voteReceipt[account].hasOverriden = true; - proposalVote.votes[support] += overridenWeight; + proposalVote.voteReceipt[account].hasOverridden = true; + proposalVote.votes[support] += overriddenWeight; if (delegateCasted == 0) { - proposalVote.voteReceipt[delegate].overridenWeight += SafeCast.toUint208(overridenWeight); + proposalVote.voteReceipt[delegate].overriddenWeight += SafeCast.toUint208(overriddenWeight); } else { uint8 delegateSupport = delegateCasted - 1; - proposalVote.votes[delegateSupport] -= overridenWeight; - emit VoteReduced(delegate, proposalId, delegateSupport, overridenWeight); + proposalVote.votes[delegateSupport] -= overriddenWeight; + emit VoteReduced(delegate, proposalId, delegateSupport, overriddenWeight); } - return overridenWeight; + return overriddenWeight; } /// @dev Variant of {Governor-_castVote} that deals with vote overrides. Returns the overridden weight. @@ -171,13 +172,13 @@ abstract contract GovernorCountingOverridable is GovernorVotes { ) internal virtual returns (uint256) { _validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Active)); - uint256 overridenWeight = _countOverride(proposalId, account, support); + uint256 overriddenWeight = _countOverride(proposalId, account, support); - emit OverrideVoteCast(account, proposalId, support, overridenWeight, reason); + emit OverrideVoteCast(account, proposalId, support, overriddenWeight, reason); _tallyUpdated(proposalId); - return overridenWeight; + return overriddenWeight; } /// @dev Public function for casting an override vote. Returns the overridden weight. diff --git a/contracts/governance/extensions/GovernorPreventLateQuorum.sol b/contracts/governance/extensions/GovernorPreventLateQuorum.sol index eb93add8099..02b201d8a04 100644 --- a/contracts/governance/extensions/GovernorPreventLateQuorum.sol +++ b/contracts/governance/extensions/GovernorPreventLateQuorum.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (governance/extensions/GovernorPreventLateQuorum.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (governance/extensions/GovernorPreventLateQuorum.sol) pragma solidity ^0.8.20; diff --git a/contracts/governance/extensions/GovernorProposalGuardian.sol b/contracts/governance/extensions/GovernorProposalGuardian.sol new file mode 100644 index 00000000000..339024a45b7 --- /dev/null +++ b/contracts/governance/extensions/GovernorProposalGuardian.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Governor} from "../Governor.sol"; + +/** + * @dev Extension of {Governor} which adds a proposal guardian that can cancel proposals at any stage in the proposal's lifecycle. + * + * NOTE: if the proposal guardian is not configured, then proposers take this role for their proposals. + */ +abstract contract GovernorProposalGuardian is Governor { + address private _proposalGuardian; + + event ProposalGuardianSet(address oldProposalGuardian, address newProposalGuardian); + + /** + * @dev Getter that returns the address of the proposal guardian. + */ + function proposalGuardian() public view virtual returns (address) { + return _proposalGuardian; + } + + /** + * @dev Update the proposal guardian's address. This operation can only be performed through a governance proposal. + * + * Emits a {ProposalGuardianSet} event. + */ + function setProposalGuardian(address newProposalGuardian) public virtual onlyGovernance { + _setProposalGuardian(newProposalGuardian); + } + + /** + * @dev Internal setter for the proposal guardian. + * + * Emits a {ProposalGuardianSet} event. + */ + function _setProposalGuardian(address newProposalGuardian) internal virtual { + emit ProposalGuardianSet(_proposalGuardian, newProposalGuardian); + _proposalGuardian = newProposalGuardian; + } + + /** + * @dev Override {Governor-_validateCancel} to implement the extended cancellation logic. + * + * * The {proposalGuardian} can cancel any proposal at any point. + * * If no proposal guardian is set, the {IGovernor-proposalProposer} can cancel their proposals at any point. + * * In any case, permissions defined in {Governor-_validateCancel} (or another override) remains valid. + */ + function _validateCancel(uint256 proposalId, address caller) internal view virtual override returns (bool) { + address guardian = proposalGuardian(); + + return + guardian == caller || + (guardian == address(0) && caller == proposalProposer(proposalId)) || + super._validateCancel(proposalId, caller); + } +} diff --git a/contracts/governance/extensions/GovernorStorage.sol b/contracts/governance/extensions/GovernorStorage.sol index 22db099924a..23d84ac523a 100644 --- a/contracts/governance/extensions/GovernorStorage.sol +++ b/contracts/governance/extensions/GovernorStorage.sol @@ -50,7 +50,7 @@ abstract contract GovernorStorage is Governor { } /** - * @dev Version of {IGovernorTimelock-queue} with only `proposalId` as an argument. + * @dev Version of {IGovernor-queue} with only `proposalId` as an argument. */ function queue(uint256 proposalId) public virtual { // here, using storage is more efficient than memory diff --git a/contracts/governance/extensions/GovernorTimelockAccess.sol b/contracts/governance/extensions/GovernorTimelockAccess.sol index 5b8429b8320..347b0f5afbb 100644 --- a/contracts/governance/extensions/GovernorTimelockAccess.sol +++ b/contracts/governance/extensions/GovernorTimelockAccess.sol @@ -277,7 +277,7 @@ abstract contract GovernorTimelockAccess is Governor { } /** - * @dev See {IGovernor-_cancel} + * @dev See {Governor-_cancel} */ function _cancel( address[] memory targets, diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 976579719de..f5994f2bdf8 100644 --- a/contracts/governance/utils/Votes.sol +++ b/contracts/governance/utils/Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (governance/utils/Votes.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (governance/utils/Votes.sol) pragma solidity ^0.8.20; import {IERC5805} from "../../interfaces/IERC5805.sol"; diff --git a/contracts/governance/utils/VotesExtended.sol b/contracts/governance/utils/VotesExtended.sol index 70b0d92fb75..5b6732038ae 100644 --- a/contracts/governance/utils/VotesExtended.sol +++ b/contracts/governance/utils/VotesExtended.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (governance/utils/VotesExtended.sol) pragma solidity ^0.8.20; import {Checkpoints} from "../../utils/structs/Checkpoints.sol"; diff --git a/contracts/interfaces/IERC1271.sol b/contracts/interfaces/IERC1271.sol index 8c239942ac8..0111be61c55 100644 --- a/contracts/interfaces/IERC1271.sol +++ b/contracts/interfaces/IERC1271.sol @@ -11,7 +11,7 @@ interface IERC1271 { /** * @dev Should return whether the signature provided is valid for the provided data * @param hash Hash of the data to be signed - * @param signature Signature byte array associated with _data + * @param signature Signature byte array associated with `hash` */ function isValidSignature(bytes32 hash, bytes memory signature) external view returns (bytes4 magicValue); } diff --git a/contracts/interfaces/README.adoc b/contracts/interfaces/README.adoc index 61aae05d167..703cd15b5d4 100644 --- a/contracts/interfaces/README.adoc +++ b/contracts/interfaces/README.adoc @@ -40,6 +40,10 @@ are useful to interact with third party contracts that implement them. - {IERC5313} - {IERC5805} - {IERC6372} +- {IERC6909} +- {IERC6909ContentURI} +- {IERC6909Metadata} +- {IERC6909TokenSupply} - {IERC7674} == Detailed ABI @@ -74,12 +78,22 @@ are useful to interact with third party contracts that implement them. {{IERC4626}} -{{IERC5313}} +{{IERC4906}} {{IERC5267}} +{{IERC5313}} + {{IERC5805}} {{IERC6372}} +{{IERC6909}} + +{{IERC6909ContentURI}} + +{{IERC6909Metadata}} + +{{IERC6909TokenSupply}} + {{IERC7674}} diff --git a/contracts/interfaces/draft-IERC4337.sol b/contracts/interfaces/draft-IERC4337.sol index 67ab146c053..2aa526db680 100644 --- a/contracts/interfaces/draft-IERC4337.sol +++ b/contracts/interfaces/draft-IERC4337.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (interfaces/draft-IERC4337.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/draft-IERC7579.sol b/contracts/interfaces/draft-IERC7579.sol index 94088cf1818..077d9783fb9 100644 --- a/contracts/interfaces/draft-IERC7579.sol +++ b/contracts/interfaces/draft-IERC7579.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (interfaces/draft-IERC7579.sol) pragma solidity ^0.8.20; import {PackedUserOperation} from "./draft-IERC4337.sol"; diff --git a/contracts/metatx/ERC2771Forwarder.sol b/contracts/metatx/ERC2771Forwarder.sol index 4a069874ecb..107878a50a1 100644 --- a/contracts/metatx/ERC2771Forwarder.sol +++ b/contracts/metatx/ERC2771Forwarder.sol @@ -302,8 +302,11 @@ contract ERC2771Forwarder is EIP712, Nonces { * * This function performs a static call to the target contract calling the * {ERC2771Context-isTrustedForwarder} function. + * + * NOTE: Consider the execution of this forwarder is permissionless. Without this check, anyone may transfer assets + * that are owned by, or are approved to this forwarder. */ - function _isTrustedByTarget(address target) private view returns (bool) { + function _isTrustedByTarget(address target) internal view virtual returns (bool) { bytes memory encodedParams = abi.encodeCall(ERC2771Context.isTrustedForwarder, (address(this))); bool success; diff --git a/contracts/mocks/docs/token/ERC6909/ERC6909GameItems.sol b/contracts/mocks/docs/token/ERC6909/ERC6909GameItems.sol new file mode 100644 index 00000000000..611e1dd667b --- /dev/null +++ b/contracts/mocks/docs/token/ERC6909/ERC6909GameItems.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC6909Metadata} from "../../../../token/ERC6909/extensions/draft-ERC6909Metadata.sol"; + +contract ERC6909GameItems is ERC6909Metadata { + uint256 public constant GOLD = 0; + uint256 public constant SILVER = 1; + uint256 public constant THORS_HAMMER = 2; + uint256 public constant SWORD = 3; + uint256 public constant SHIELD = 4; + + constructor() { + _setDecimals(GOLD, 18); + _setDecimals(SILVER, 18); + // Default decimals is 0 + _setDecimals(SWORD, 9); + _setDecimals(SHIELD, 9); + + _mint(msg.sender, GOLD, 10 ** 18); + _mint(msg.sender, SILVER, 10_000 ** 18); + _mint(msg.sender, THORS_HAMMER, 1); + _mint(msg.sender, SWORD, 10 ** 9); + _mint(msg.sender, SHIELD, 10 ** 9); + } +} diff --git a/contracts/mocks/governance/GovernorProposalGuardianMock.sol b/contracts/mocks/governance/GovernorProposalGuardianMock.sol new file mode 100644 index 00000000000..5ed45d6c942 --- /dev/null +++ b/contracts/mocks/governance/GovernorProposalGuardianMock.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Governor} from "../../governance/Governor.sol"; +import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol"; +import {GovernorCountingSimple} from "../../governance/extensions/GovernorCountingSimple.sol"; +import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol"; +import {GovernorProposalGuardian} from "../../governance/extensions/GovernorProposalGuardian.sol"; + +abstract contract GovernorProposalGuardianMock is + GovernorSettings, + GovernorVotesQuorumFraction, + GovernorCountingSimple, + GovernorProposalGuardian +{ + function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { + return super.proposalThreshold(); + } + + function _validateCancel( + uint256 proposalId, + address caller + ) internal view override(Governor, GovernorProposalGuardian) returns (bool) { + return super._validateCancel(proposalId, caller); + } +} diff --git a/contracts/package.json b/contracts/package.json index e0ed163d05a..3682eadeb97 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@openzeppelin/contracts", "description": "Secure Smart Contract library for Solidity", - "version": "5.1.0", + "version": "5.2.0", "files": [ "**/*.sol", "/build/contracts/*.json", diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index 5bd45a0864b..6b8d9cc6fae 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (proxy/Clones.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (proxy/Clones.sol) pragma solidity ^0.8.20; diff --git a/contracts/proxy/ERC1967/ERC1967Proxy.sol b/contracts/proxy/ERC1967/ERC1967Proxy.sol index cad9eb5ab7e..eb482f6ecac 100644 --- a/contracts/proxy/ERC1967/ERC1967Proxy.sol +++ b/contracts/proxy/ERC1967/ERC1967Proxy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (proxy/ERC1967/ERC1967Proxy.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (proxy/ERC1967/ERC1967Proxy.sol) pragma solidity ^0.8.22; diff --git a/contracts/proxy/ERC1967/ERC1967Utils.sol b/contracts/proxy/ERC1967/ERC1967Utils.sol index 287bb6beee2..73fe697f357 100644 --- a/contracts/proxy/ERC1967/ERC1967Utils.sol +++ b/contracts/proxy/ERC1967/ERC1967Utils.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (proxy/ERC1967/ERC1967Utils.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (proxy/ERC1967/ERC1967Utils.sol) pragma solidity ^0.8.22; diff --git a/contracts/proxy/beacon/BeaconProxy.sol b/contracts/proxy/beacon/BeaconProxy.sol index e38b9d891cf..36558d67bca 100644 --- a/contracts/proxy/beacon/BeaconProxy.sol +++ b/contracts/proxy/beacon/BeaconProxy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (proxy/beacon/BeaconProxy.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (proxy/beacon/BeaconProxy.sol) pragma solidity ^0.8.22; diff --git a/contracts/proxy/transparent/ProxyAdmin.sol b/contracts/proxy/transparent/ProxyAdmin.sol index 2a60edfe987..eefd49a80e6 100644 --- a/contracts/proxy/transparent/ProxyAdmin.sol +++ b/contracts/proxy/transparent/ProxyAdmin.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (proxy/transparent/ProxyAdmin.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (proxy/transparent/ProxyAdmin.sol) pragma solidity ^0.8.22; diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index 7342d9f8f0a..21af0e315a0 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (proxy/transparent/TransparentUpgradeableProxy.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (proxy/transparent/TransparentUpgradeableProxy.sol) pragma solidity ^0.8.22; diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index 745c56fa5d2..8be137d8d78 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (proxy/utils/UUPSUpgradeable.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (proxy/utils/UUPSUpgradeable.sol) pragma solidity ^0.8.22; @@ -91,7 +91,6 @@ abstract contract UUPSUpgradeable is IERC1822Proxiable { /** * @dev Reverts if the execution is not performed via delegatecall or the execution * context is not of a proxy with an ERC-1967 compliant implementation pointing to self. - * See {_onlyProxy}. */ function _checkProxy() internal view virtual { if ( diff --git a/contracts/token/ERC1155/IERC1155.sol b/contracts/token/ERC1155/IERC1155.sol index 0da320fbefb..8b4fcb5bc5d 100644 --- a/contracts/token/ERC1155/IERC1155.sol +++ b/contracts/token/ERC1155/IERC1155.sol @@ -81,7 +81,7 @@ interface IERC1155 is IERC165 { * @dev Transfers a `value` amount of tokens of 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. + * to an untrusted contract, when invoking {IERC1155Receiver-onERC1155Received} on the receiver. * Ensure to follow the checks-effects-interactions pattern and consider employing * reentrancy guards when interacting with untrusted contracts. * @@ -101,7 +101,7 @@ 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. + * to an untrusted contract, when invoking {IERC1155Receiver-onERC1155BatchReceived} on the receiver. * Ensure to follow the checks-effects-interactions pattern and consider employing * reentrancy guards when interacting with untrusted contracts. * diff --git a/contracts/token/ERC1155/utils/ERC1155Utils.sol b/contracts/token/ERC1155/utils/ERC1155Utils.sol index 371cd86ba46..cac0d5a0e02 100644 --- a/contracts/token/ERC1155/utils/ERC1155Utils.sol +++ b/contracts/token/ERC1155/utils/ERC1155Utils.sol @@ -15,7 +15,7 @@ import {IERC1155Errors} from "../../../interfaces/draft-IERC6093.sol"; */ library ERC1155Utils { /** - * @dev Performs an acceptance check for the provided `operator` by calling {IERC1155-onERC1155Received} + * @dev Performs an acceptance check for the provided `operator` by calling {IERC1155Receiver-onERC1155Received} * on the `to` address. The `operator` is generally the address that initiated the token transfer (i.e. `msg.sender`). * * The acceptance call is not executed and treated as a no-op if the target address doesn't contain code (i.e. an EOA). @@ -50,7 +50,7 @@ library ERC1155Utils { } /** - * @dev Performs a batch acceptance check for the provided `operator` by calling {IERC1155-onERC1155BatchReceived} + * @dev Performs a batch acceptance check for the provided `operator` by calling {IERC1155Receiver-onERC1155BatchReceived} * on the `to` address. The `operator` is generally the address that initiated the token transfer (i.e. `msg.sender`). * * The acceptance call is not executed and treated as a no-op if the target address doesn't contain code (i.e. an EOA). diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index 6a9865e6aef..471908d6c1b 100644 --- a/contracts/token/ERC20/ERC20.sol +++ b/contracts/token/ERC20/ERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/ERC20.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (token/ERC20/ERC20.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index 952582a8404..30ffd0f0877 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC1363.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (token/ERC20/extensions/ERC1363.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index 338b71d62c5..569d54d8b05 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -34,7 +34,7 @@ import {Math} from "../../../utils/math/Math.sol"; * offset (0) makes it non-profitable even if an attacker is able to capture value from multiple user deposits, 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]. + * underlying math can be found xref:ROOT: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 diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index a77907b4c33..347e2aa8b6c 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/utils/SafeERC20.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (token/ERC20/utils/SafeERC20.sol) pragma solidity ^0.8.20; @@ -42,6 +42,20 @@ library SafeERC20 { _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); } + /** + * @dev Variant of {safeTransfer} that returns a bool instead of reverting if the operation is not successful. + */ + function trySafeTransfer(IERC20 token, address to, uint256 value) internal returns (bool) { + return _callOptionalReturnBool(token, abi.encodeCall(token.transfer, (to, value))); + } + + /** + * @dev Variant of {safeTransferFrom} that returns a bool instead of reverting if the operation is not successful. + */ + function trySafeTransferFrom(IERC20 token, address from, address to, uint256 value) internal returns (bool) { + return _callOptionalReturnBool(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. diff --git a/contracts/token/ERC6909/README.adoc b/contracts/token/ERC6909/README.adoc new file mode 100644 index 00000000000..3fc9e1e5246 --- /dev/null +++ b/contracts/token/ERC6909/README.adoc @@ -0,0 +1,27 @@ += ERC-6909 + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc6909 + +This set of interfaces and contracts are all related to the https://eips.ethereum.org/EIPS/eip-6909[ERC-6909 Minimal Multi-Token Interface]. + +The ERC consists of four interfaces which fulfill different roles--the interfaces are as follows: + +. {IERC6909}: Base interface for a vanilla ERC6909 token. +. {IERC6909ContentURI}: Extends the base interface and adds content URI (contract and token level) functionality. +. {IERC6909Metadata}: Extends the base interface and adds metadata functionality, which exposes a name, symbol, and decimals for each token id. +. {IERC6909TokenSupply}: Extends the base interface and adds total supply functionality for each token id. + +Implementations are provided for each of the 4 interfaces defined in the ERC. + +== Core + +{{ERC6909}} + +== Extensions + +{{ERC6909ContentURI}} + +{{ERC6909Metadata}} + +{{ERC6909TokenSupply}} \ No newline at end of file diff --git a/contracts/token/ERC6909/draft-ERC6909.sol b/contracts/token/ERC6909/draft-ERC6909.sol new file mode 100644 index 00000000000..e821d4b3b22 --- /dev/null +++ b/contracts/token/ERC6909/draft-ERC6909.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {IERC6909} from "../../interfaces/draft-IERC6909.sol"; +import {Context} from "../../utils/Context.sol"; +import {IERC165, ERC165} from "../../utils/introspection/ERC165.sol"; + +/** + * @dev Implementation of ERC-6909. + * See https://eips.ethereum.org/EIPS/eip-6909 + */ +contract ERC6909 is Context, ERC165, IERC6909 { + mapping(address owner => mapping(uint256 id => uint256)) private _balances; + + mapping(address owner => mapping(address operator => bool)) private _operatorApprovals; + + mapping(address owner => mapping(address spender => mapping(uint256 id => uint256))) private _allowances; + + error ERC6909InsufficientBalance(address sender, uint256 balance, uint256 needed, uint256 id); + error ERC6909InsufficientAllowance(address spender, uint256 allowance, uint256 needed, uint256 id); + error ERC6909InvalidApprover(address approver); + error ERC6909InvalidReceiver(address receiver); + error ERC6909InvalidSender(address sender); + error ERC6909InvalidSpender(address spender); + + /// @inheritdoc IERC165 + function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) { + return interfaceId == type(IERC6909).interfaceId || super.supportsInterface(interfaceId); + } + + /// @inheritdoc IERC6909 + function balanceOf(address owner, uint256 id) public view virtual override returns (uint256) { + return _balances[owner][id]; + } + + /// @inheritdoc IERC6909 + function allowance(address owner, address spender, uint256 id) public view virtual override returns (uint256) { + return _allowances[owner][spender][id]; + } + + /// @inheritdoc IERC6909 + function isOperator(address owner, address spender) public view virtual override returns (bool) { + return _operatorApprovals[owner][spender]; + } + + /// @inheritdoc IERC6909 + function approve(address spender, uint256 id, uint256 amount) public virtual override returns (bool) { + _approve(_msgSender(), spender, id, amount); + return true; + } + + /// @inheritdoc IERC6909 + function setOperator(address spender, bool approved) public virtual override returns (bool) { + _setOperator(_msgSender(), spender, approved); + return true; + } + + /// @inheritdoc IERC6909 + function transfer(address receiver, uint256 id, uint256 amount) public virtual override returns (bool) { + _transfer(_msgSender(), receiver, id, amount); + return true; + } + + /// @inheritdoc IERC6909 + function transferFrom( + address sender, + address receiver, + uint256 id, + uint256 amount + ) public virtual override returns (bool) { + address caller = _msgSender(); + if (sender != caller && !isOperator(sender, caller)) { + _spendAllowance(sender, caller, id, amount); + } + _transfer(sender, receiver, id, amount); + return true; + } + + /** + * @dev Creates `amount` of token `id` and assigns them to `account`, by transferring it from address(0). + * Relies on the `_update` mechanism + * + * Emits a {Transfer} event with `from` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _mint(address to, uint256 id, uint256 amount) internal { + if (to == address(0)) { + revert ERC6909InvalidReceiver(address(0)); + } + _update(address(0), to, id, amount); + } + + /** + * @dev Moves `amount` of token `id` from `from` to `to` without checking for approvals. + * + * This internal function is equivalent to {transfer}, and can be used to + * e.g. implement automatic token fees, slashing mechanisms, etc. + * + * Emits a {Transfer} event. + * + * NOTE: This function is not virtual, {_update} should be overridden instead. + */ + function _transfer(address from, address to, uint256 id, uint256 amount) internal { + if (from == address(0)) { + revert ERC6909InvalidSender(address(0)); + } + if (to == address(0)) { + revert ERC6909InvalidReceiver(address(0)); + } + _update(from, to, id, amount); + } + + /** + * @dev Destroys a `amount` of token `id` from `account`. + * Relies on the `_update` mechanism. + * + * Emits a {Transfer} event with `to` set to the zero address. + * + * NOTE: This function is not virtual, {_update} should be overridden instead + */ + function _burn(address from, uint256 id, uint256 amount) internal { + if (from == address(0)) { + revert ERC6909InvalidSender(address(0)); + } + _update(from, address(0), id, amount); + } + + /** + * @dev Transfers `amount` of token `id` from `from` to `to`, or alternatively mints (or burns) if `from` + * (or `to`) is the zero address. All customizations to transfers, mints, and burns should be done by overriding + * this function. + * + * Emits a {Transfer} event. + */ + function _update(address from, address to, uint256 id, uint256 amount) internal virtual { + address caller = _msgSender(); + + if (from != address(0)) { + uint256 fromBalance = _balances[from][id]; + if (fromBalance < amount) { + revert ERC6909InsufficientBalance(from, fromBalance, amount, id); + } + unchecked { + // Overflow not possible: amount <= fromBalance. + _balances[from][id] = fromBalance - amount; + } + } + if (to != address(0)) { + _balances[to][id] += amount; + } + + emit Transfer(caller, from, to, id, amount); + } + + /** + * @dev Sets `amount` as the allowance of `spender` over the `owner`'s `id` tokens. + * + * This internal function is equivalent to `approve`, and can be used to e.g. set automatic allowances for certain + * subsystems, etc. + * + * Emits an {Approval} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _approve(address owner, address spender, uint256 id, uint256 amount) internal virtual { + if (owner == address(0)) { + revert ERC6909InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC6909InvalidSpender(address(0)); + } + _allowances[owner][spender][id] = amount; + emit Approval(owner, spender, id, amount); + } + + /** + * @dev Approve `spender` to operate on all of `owner`'s tokens + * + * This internal function is equivalent to `setOperator`, and can be used to e.g. set automatic allowances for + * certain subsystems, etc. + * + * Emits an {OperatorSet} event. + * + * Requirements: + * + * - `owner` cannot be the zero address. + * - `spender` cannot be the zero address. + */ + function _setOperator(address owner, address spender, bool approved) internal virtual { + if (owner == address(0)) { + revert ERC6909InvalidApprover(address(0)); + } + if (spender == address(0)) { + revert ERC6909InvalidSpender(address(0)); + } + _operatorApprovals[owner][spender] = approved; + emit OperatorSet(owner, spender, approved); + } + + /** + * @dev Updates `owner`'s allowance for `spender` based on spent `amount`. + * + * Does not update the allowance value in case of infinite allowance. + * Revert if not enough allowance is available. + * + * Does not emit an {Approval} event. + */ + function _spendAllowance(address owner, address spender, uint256 id, uint256 amount) internal virtual { + uint256 currentAllowance = allowance(owner, spender, id); + if (currentAllowance < type(uint256).max) { + if (currentAllowance < amount) { + revert ERC6909InsufficientAllowance(spender, currentAllowance, amount, id); + } + unchecked { + _allowances[owner][spender][id] = currentAllowance - amount; + } + } + } +} diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol new file mode 100644 index 00000000000..8839947936d --- /dev/null +++ b/contracts/token/ERC6909/extensions/draft-ERC6909ContentURI.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC6909} from "../draft-ERC6909.sol"; +import {IERC6909ContentURI} from "../../../interfaces/draft-IERC6909.sol"; + +/** + * @dev Implementation of the Content URI extension defined in ERC6909. + */ +contract ERC6909ContentURI is ERC6909, IERC6909ContentURI { + string private _contractURI; + mapping(uint256 id => string) private _tokenURIs; + + /// @dev Event emitted when the contract URI is changed. See https://eips.ethereum.org/EIPS/eip-7572[ERC-7572] for details. + event ContractURIUpdated(); + + /// @dev See {IERC1155-URI} + event URI(string value, uint256 indexed id); + + /// @inheritdoc IERC6909ContentURI + function contractURI() public view virtual override returns (string memory) { + return _contractURI; + } + + /// @inheritdoc IERC6909ContentURI + function tokenURI(uint256 id) public view virtual override returns (string memory) { + return _tokenURIs[id]; + } + + /** + * @dev Sets the {contractURI} for the contract. + * + * Emits a {ContractURIUpdated} event. + */ + function _setContractURI(string memory newContractURI) internal virtual { + _contractURI = newContractURI; + + emit ContractURIUpdated(); + } + + /** + * @dev Sets the {tokenURI} for a given token of type `id`. + * + * Emits a {URI} event. + */ + function _setTokenURI(uint256 id, string memory newTokenURI) internal virtual { + _tokenURIs[id] = newTokenURI; + + emit URI(newTokenURI, id); + } +} diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol new file mode 100644 index 00000000000..4132863863a --- /dev/null +++ b/contracts/token/ERC6909/extensions/draft-ERC6909Metadata.sol @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC6909} from "../draft-ERC6909.sol"; +import {IERC6909Metadata} from "../../../interfaces/draft-IERC6909.sol"; + +/** + * @dev Implementation of the Metadata extension defined in ERC6909. Exposes the name, symbol, and decimals of each token id. + */ +contract ERC6909Metadata is ERC6909, IERC6909Metadata { + struct TokenMetadata { + string name; + string symbol; + uint8 decimals; + } + + mapping(uint256 id => TokenMetadata) private _tokenMetadata; + + /// @dev The name of the token of type `id` was updated to `newName`. + event ERC6909NameUpdated(uint256 indexed id, string newName); + + /// @dev The symbol for the token of type `id` was updated to `newSymbol`. + event ERC6909SymbolUpdated(uint256 indexed id, string newSymbol); + + /// @dev The decimals value for token of type `id` was updated to `newDecimals`. + event ERC6909DecimalsUpdated(uint256 indexed id, uint8 newDecimals); + + /// @inheritdoc IERC6909Metadata + function name(uint256 id) public view virtual override returns (string memory) { + return _tokenMetadata[id].name; + } + + /// @inheritdoc IERC6909Metadata + function symbol(uint256 id) public view virtual override returns (string memory) { + return _tokenMetadata[id].symbol; + } + + /// @inheritdoc IERC6909Metadata + function decimals(uint256 id) public view virtual override returns (uint8) { + return _tokenMetadata[id].decimals; + } + + /** + * @dev Sets the `name` for a given token of type `id`. + * + * Emits an {ERC6909NameUpdated} event. + */ + function _setName(uint256 id, string memory newName) internal virtual { + _tokenMetadata[id].name = newName; + + emit ERC6909NameUpdated(id, newName); + } + + /** + * @dev Sets the `symbol` for a given token of type `id`. + * + * Emits an {ERC6909SymbolUpdated} event. + */ + function _setSymbol(uint256 id, string memory newSymbol) internal virtual { + _tokenMetadata[id].symbol = newSymbol; + + emit ERC6909SymbolUpdated(id, newSymbol); + } + + /** + * @dev Sets the `decimals` for a given token of type `id`. + * + * Emits an {ERC6909DecimalsUpdated} event. + */ + function _setDecimals(uint256 id, uint8 newDecimals) internal virtual { + _tokenMetadata[id].decimals = newDecimals; + + emit ERC6909DecimalsUpdated(id, newDecimals); + } +} diff --git a/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol b/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol new file mode 100644 index 00000000000..476935f8fe1 --- /dev/null +++ b/contracts/token/ERC6909/extensions/draft-ERC6909TokenSupply.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {ERC6909} from "../draft-ERC6909.sol"; +import {IERC6909TokenSupply} from "../../../interfaces/draft-IERC6909.sol"; + +/** + * @dev Implementation of the Token Supply extension defined in ERC6909. + * Tracks the total supply of each token id individually. + */ +contract ERC6909TokenSupply is ERC6909, IERC6909TokenSupply { + mapping(uint256 id => uint256) private _totalSupplies; + + /// @inheritdoc IERC6909TokenSupply + function totalSupply(uint256 id) public view virtual override returns (uint256) { + return _totalSupplies[id]; + } + + /// @dev Override the `_update` function to update the total supply of each token id as necessary. + function _update(address from, address to, uint256 id, uint256 amount) internal virtual override { + super._update(from, to, id, amount); + + if (from == address(0)) { + _totalSupplies[id] += amount; + } + if (to == address(0)) { + unchecked { + // amount <= _balances[id][from] <= _totalSupplies[id] + _totalSupplies[id] -= amount; + } + } + } +} diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index b5aa6579808..22a306235c1 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -3,7 +3,7 @@ [.readme-notice] NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/token/erc721 -This set of interfaces, contracts, and utilities are all related to the https://eips.ethereum.org/EIPS/eip-721[ERC-721 Non-Fungible Token Standard]. +This set of interfaces, contracts, and utilities is all related to the https://eips.ethereum.org/EIPS/eip-721[ERC-721 Non-Fungible Token Standard]. TIP: For a walk through on how to create an ERC-721 token read our xref:ROOT:erc721.adoc[ERC-721 guide]. diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index 6b849d77438..4f265774459 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -159,7 +159,7 @@ abstract contract ERC721Consecutive is IERC2309, ERC721 { } /** - * @dev Used to offset the first token id in {_nextConsecutiveId} + * @dev Used to offset the first token id in `_nextConsecutiveId` */ function _firstConsecutiveId() internal view virtual returns (uint96) { return 0; diff --git a/contracts/token/ERC721/extensions/ERC721URIStorage.sol b/contracts/token/ERC721/extensions/ERC721URIStorage.sol index d8b4d8d1f91..45fb7e529f8 100644 --- a/contracts/token/ERC721/extensions/ERC721URIStorage.sol +++ b/contracts/token/ERC721/extensions/ERC721URIStorage.sol @@ -52,7 +52,7 @@ abstract contract ERC721URIStorage is IERC4906, ERC721 { /** * @dev Sets `_tokenURI` as the tokenURI of `tokenId`. * - * Emits {MetadataUpdate}. + * Emits {IERC4906-MetadataUpdate}. */ function _setTokenURI(uint256 tokenId, string memory _tokenURI) internal virtual { _tokenURIs[tokenId] = _tokenURI; diff --git a/contracts/token/ERC721/utils/ERC721Utils.sol b/contracts/token/ERC721/utils/ERC721Utils.sol index 2fd091afd67..c94b690c309 100644 --- a/contracts/token/ERC721/utils/ERC721Utils.sol +++ b/contracts/token/ERC721/utils/ERC721Utils.sol @@ -15,7 +15,7 @@ import {IERC721Errors} from "../../../interfaces/draft-IERC6093.sol"; */ library ERC721Utils { /** - * @dev Performs an acceptance check for the provided `operator` by calling {IERC721-onERC721Received} + * @dev Performs an acceptance check for the provided `operator` by calling {IERC721Receiver-onERC721Received} * on the `to` address. The `operator` is generally the address that initiated the token transfer (i.e. `msg.sender`). * * The acceptance call is not executed and treated as a no-op if the target address doesn't contain code (i.e. an EOA). diff --git a/contracts/token/common/ERC2981.sol b/contracts/token/common/ERC2981.sol index 8335e56efab..0c49a85275b 100644 --- a/contracts/token/common/ERC2981.sol +++ b/contracts/token/common/ERC2981.sol @@ -39,7 +39,7 @@ abstract contract ERC2981 is IERC2981, ERC165 { error ERC2981InvalidDefaultRoyaltyReceiver(address receiver); /** - * @dev The royalty set for an specific `tokenId` is invalid (eg. (numerator / denominator) >= 1). + * @dev The royalty set for a specific `tokenId` is invalid (eg. (numerator / denominator) >= 1). */ error ERC2981InvalidTokenRoyalty(uint256 tokenId, uint256 numerator, uint256 denominator); diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index d8e88749148..696f94ffa1f 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (utils/Address.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (utils/Address.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index bd3401703d4..b3cb3f42189 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -448,7 +448,7 @@ library Arrays { } /** - * @dev Helper to set the length of an dynamic array. Directly writing to `.length` is forbidden. + * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden. * * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased. */ @@ -459,7 +459,7 @@ library Arrays { } /** - * @dev Helper to set the length of an dynamic array. Directly writing to `.length` is forbidden. + * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden. * * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased. */ @@ -470,7 +470,7 @@ library Arrays { } /** - * @dev Helper to set the length of an dynamic array. Directly writing to `.length` is forbidden. + * @dev Helper to set the length of a dynamic array. Directly writing to `.length` is forbidden. * * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased. */ diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index cf0cb8fcd7f..f0708507f03 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (utils/Bytes.sol) pragma solidity ^0.8.24; diff --git a/contracts/utils/CAIP10.sol b/contracts/utils/CAIP10.sol index f0424601735..84b35da046f 100644 --- a/contracts/utils/CAIP10.sol +++ b/contracts/utils/CAIP10.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (utils/CAIP10.sol) pragma solidity ^0.8.24; diff --git a/contracts/utils/CAIP2.sol b/contracts/utils/CAIP2.sol index a7a69e6a873..d06dd6da7f2 100644 --- a/contracts/utils/CAIP2.sol +++ b/contracts/utils/CAIP2.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (utils/CAIP2.sol) pragma solidity ^0.8.24; diff --git a/contracts/utils/Calldata.sol b/contracts/utils/Calldata.sol new file mode 100644 index 00000000000..60e0b08b015 --- /dev/null +++ b/contracts/utils/Calldata.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev Helper library for manipulating objects in calldata. + */ +library Calldata { + // slither-disable-next-line write-after-write + function emptyBytes() internal pure returns (bytes calldata result) { + assembly ("memory-safe") { + result.offset := 0 + result.length := 0 + } + } + + // slither-disable-next-line write-after-write + function emptyString() internal pure returns (string calldata result) { + assembly ("memory-safe") { + result.offset := 0 + result.length := 0 + } + } +} diff --git a/contracts/utils/Multicall.sol b/contracts/utils/Multicall.sol index 0dd5b4adc37..c34c243e11c 100644 --- a/contracts/utils/Multicall.sol +++ b/contracts/utils/Multicall.sol @@ -13,10 +13,10 @@ import {Context} from "./Context.sol"; * careful about sending transactions invoking {multicall}. For example, a relay address that filters function * selectors won't filter calls nested within a {multicall} operation. * - * NOTE: Since 5.0.1 and 4.9.4, this contract identifies non-canonical contexts (i.e. `msg.sender` is not {_msgSender}). + * NOTE: Since 5.0.1 and 4.9.4, this contract identifies non-canonical contexts (i.e. `msg.sender` is not {Context-_msgSender}). * If a non-canonical context is identified, the following self `delegatecall` appends the last bytes of `msg.data` * to the subcall. This makes it safe to use with {ERC2771Context}. Contexts that don't affect the resolution of - * {_msgSender} are not propagated to subcalls. + * {Context-_msgSender} are not propagated to subcalls. */ abstract contract Multicall is Context { /** diff --git a/contracts/utils/NoncesKeyed.sol b/contracts/utils/NoncesKeyed.sol index 31cd0704e15..df9c5704bc9 100644 --- a/contracts/utils/NoncesKeyed.sol +++ b/contracts/utils/NoncesKeyed.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.2.0) (utils/NoncesKeyed.sol) pragma solidity ^0.8.20; import {Nonces} from "./Nonces.sol"; diff --git a/contracts/utils/Packing.sol b/contracts/utils/Packing.sol index f38e64a3bf6..f7c5d6fcc3c 100644 --- a/contracts/utils/Packing.sol +++ b/contracts/utils/Packing.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (utils/Packing.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (utils/Packing.sol) // This file was procedurally generated from scripts/generate/templates/Packing.js. pragma solidity ^0.8.20; diff --git a/contracts/utils/Pausable.sol b/contracts/utils/Pausable.sol index 312f1cb90fe..ffeab759f92 100644 --- a/contracts/utils/Pausable.sol +++ b/contracts/utils/Pausable.sol @@ -37,13 +37,6 @@ abstract contract Pausable is Context { */ error ExpectedPause(); - /** - * @dev Initializes the contract in unpaused state. - */ - constructor() { - _paused = false; - } - /** * @dev Modifier to make a function callable only when the contract is not paused. * diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 8f08a86987f..d841de5e314 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -33,8 +33,9 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Arrays}: Collection of functions that operate on https://docs.soliditylang.org/en/latest/types.html#arrays[`arrays`]. * {Base64}: On-chain base64 and base64URL encoding according to https://datatracker.ietf.org/doc/html/rfc4648[RFC-4648]. * {Bytes}: Common operations on bytes objects. + * {Calldata}: Helpers for manipulating calldata. * {Strings}: Common operations for strings formatting. - * {ShortString}: Library to encode (and decode) short strings into (or from) a single bytes32 slot for optimizing costs. Short strings are limited to 31 characters. + * {ShortStrings}: Library to encode (and decode) short strings into (or from) a single bytes32 slot for optimizing costs. Short strings are limited to 31 characters. * {SlotDerivation}: Methods for deriving storage slot from ERC-7201 namespaces as well as from constructions such as mapping and arrays. * {StorageSlot}: Methods for accessing specific storage slots formatted as common primitive types. * {TransientSlot}: Primitives for reading from and writing to transient storage (only value types are currently supported). @@ -128,6 +129,10 @@ Ethereum contracts have no native concept of an interface, so applications must {{Base64}} +{{Bytes}} + +{{Calldata}} + {{Strings}} {{ShortStrings}} @@ -147,3 +152,7 @@ Ethereum contracts have no native concept of an interface, so applications must {{Panic}} {{Comparators}} + +{{CAIP2}} + +{{CAIP10}} diff --git a/contracts/utils/ReentrancyGuardTransient.sol b/contracts/utils/ReentrancyGuardTransient.sol index 1a62e29dafa..08cdbee538b 100644 --- a/contracts/utils/ReentrancyGuardTransient.sol +++ b/contracts/utils/ReentrancyGuardTransient.sol @@ -38,7 +38,7 @@ abstract contract ReentrancyGuardTransient { } function _nonReentrantBefore() private { - // On the first call to nonReentrant, _status will be NOT_ENTERED + // On the first call to nonReentrant, REENTRANCY_GUARD_STORAGE.asBoolean().tload() will be false if (_reentrancyGuardEntered()) { revert ReentrancyGuardReentrantCall(); } diff --git a/contracts/utils/ShortStrings.sol b/contracts/utils/ShortStrings.sol index fb8bde51668..2d61870cbb1 100644 --- a/contracts/utils/ShortStrings.sol +++ b/contracts/utils/ShortStrings.sol @@ -95,7 +95,7 @@ library ShortStrings { } /** - * @dev Decode a string that was encoded to `ShortString` or written to storage using {setWithFallback}. + * @dev Decode a string that was encoded to `ShortString` or written to storage using {toShortStringWithFallback}. */ function toStringWithFallback(ShortString value, string storage store) internal pure returns (string memory) { if (ShortString.unwrap(value) != FALLBACK_SENTINEL) { @@ -107,7 +107,7 @@ library ShortStrings { /** * @dev Return the length of a string that was encoded to `ShortString` or written to storage using - * {setWithFallback}. + * {toShortStringWithFallback}. * * 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. diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index f9465eaf04d..9e5f1877b99 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.1.0) (utils/Strings.sol) +// OpenZeppelin Contracts (last updated v5.2.0) (utils/Strings.sol) pragma solidity ^0.8.20; @@ -139,7 +139,7 @@ library Strings { } /** - * @dev Variant of {parseUint} that parses a substring of `input` located between position `begin` (included) and + * @dev Variant of {parseUint-string} that parses a substring of `input` located between position `begin` (included) and * `end` (excluded). * * Requirements: @@ -177,7 +177,7 @@ library Strings { } /** - * @dev Implementation of {tryParseUint} that does not check bounds. Caller should make sure that + * @dev Implementation of {tryParseUint-string-uint256-uint256} that does not check bounds. Caller should make sure that * `begin <= end <= input.length`. Other inputs would result in undefined behavior. */ function _tryParseUintUncheckedBounds( @@ -250,7 +250,7 @@ library Strings { } /** - * @dev Implementation of {tryParseInt} that does not check bounds. Caller should make sure that + * @dev Implementation of {tryParseInt-string-uint256-uint256} that does not check bounds. Caller should make sure that * `begin <= end <= input.length`. Other inputs would result in undefined behavior. */ function _tryParseIntUncheckedBounds( @@ -287,7 +287,7 @@ library Strings { } /** - * @dev Variant of {parseHexUint} that parses a substring of `input` located between position `begin` (included) and + * @dev Variant of {parseHexUint-string} that parses a substring of `input` located between position `begin` (included) and * `end` (excluded). * * Requirements: @@ -325,7 +325,7 @@ library Strings { } /** - * @dev Implementation of {tryParseHexUint} that does not check bounds. Caller should make sure that + * @dev Implementation of {tryParseHexUint-string-uint256-uint256} that does not check bounds. Caller should make sure that * `begin <= end <= input.length`. Other inputs would result in undefined behavior. */ function _tryParseHexUintUncheckedBounds( @@ -346,7 +346,7 @@ library Strings { result *= 16; unchecked { // Multiplying by 16 is equivalent to a shift of 4 bits (with additional overflow check). - // This guaratees that adding a value < 16 will not cause an overflow, hence the unchecked. + // This guarantees that adding a value < 16 will not cause an overflow, hence the unchecked. result += chr; } } @@ -364,7 +364,7 @@ library Strings { } /** - * @dev Variant of {parseAddress} that parses a substring of `input` located between position `begin` (included) and + * @dev Variant of {parseAddress-string} that parses a substring of `input` located between position `begin` (included) and * `end` (excluded). * * Requirements: @@ -378,7 +378,7 @@ library Strings { /** * @dev Variant of {parseAddress-string} that returns false if the parsing fails because the input is not a properly - * formatted address. See {parseAddress} requirements. + * formatted address. See {parseAddress-string} requirements. */ function tryParseAddress(string memory input) internal pure returns (bool success, address value) { return tryParseAddress(input, 0, bytes(input).length); @@ -386,7 +386,7 @@ library Strings { /** * @dev Variant of {parseAddress-string-uint256-uint256} that returns false if the parsing fails because input is not a properly - * formatted address. See {parseAddress} requirements. + * formatted address. See {parseAddress-string-uint256-uint256} requirements. */ function tryParseAddress( string memory input, diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index f15a67bd9b5..bcb67c87a26 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -48,7 +48,9 @@ abstract contract EIP712 is IERC5267 { ShortString private immutable _name; ShortString private immutable _version; + // slither-disable-next-line constable-states string private _nameFallback; + // slither-disable-next-line constable-states string private _versionFallback; /** @@ -109,7 +111,7 @@ abstract contract EIP712 is IERC5267 { } /** - * @dev See {IERC-5267}. + * @inheritdoc IERC5267 */ function eip712Domain() public diff --git a/contracts/utils/cryptography/Hashes.sol b/contracts/utils/cryptography/Hashes.sol index 893883164fb..6b7168e8777 100644 --- a/contracts/utils/cryptography/Hashes.sol +++ b/contracts/utils/cryptography/Hashes.sol @@ -15,13 +15,13 @@ library Hashes { * NOTE: Equivalent to the `standardNodeHash` in our https://github.com/OpenZeppelin/merkle-tree[JavaScript library]. */ function commutativeKeccak256(bytes32 a, bytes32 b) internal pure returns (bytes32) { - return a < b ? _efficientKeccak256(a, b) : _efficientKeccak256(b, a); + return a < b ? efficientKeccak256(a, b) : efficientKeccak256(b, a); } /** * @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory. */ - function _efficientKeccak256(bytes32 a, bytes32 b) private pure returns (bytes32 value) { + function efficientKeccak256(bytes32 a, bytes32 b) internal pure returns (bytes32 value) { assembly ("memory-safe") { mstore(0x00, a) mstore(0x20, b) diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index 4e12acec142..d455c4d48a7 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -16,6 +16,7 @@ import {EnumerableSet} from "./EnumerableSet.sol"; * - Entries are added, removed, and checked for existence in constant time * (O(1)). * - Entries are enumerated in O(n). No guarantees are made on the ordering. + * - Map can be cleared (all entries removed) in O(n). * * ```solidity * contract Example { @@ -90,6 +91,20 @@ library EnumerableMap { return map._keys.remove(key); } + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(Bytes32ToBytes32Map storage map) internal { + uint256 len = length(map); + for (uint256 i = 0; i < len; ++i) { + delete map._values[map._keys.at(i)]; + } + map._keys.clear(); + } + /** * @dev Returns true if the key is in the map. O(1). */ @@ -185,6 +200,16 @@ library EnumerableMap { return remove(map._inner, bytes32(key)); } + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(UintToUintMap storage map) internal { + clear(map._inner); + } + /** * @dev Returns true if the key is in the map. O(1). */ @@ -278,6 +303,16 @@ library EnumerableMap { return remove(map._inner, bytes32(key)); } + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(UintToAddressMap storage map) internal { + clear(map._inner); + } + /** * @dev Returns true if the key is in the map. O(1). */ @@ -371,6 +406,16 @@ library EnumerableMap { return remove(map._inner, bytes32(key)); } + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(UintToBytes32Map storage map) internal { + clear(map._inner); + } + /** * @dev Returns true if the key is in the map. O(1). */ @@ -464,6 +509,16 @@ library EnumerableMap { return remove(map._inner, bytes32(uint256(uint160(key)))); } + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(AddressToUintMap storage map) internal { + clear(map._inner); + } + /** * @dev Returns true if the key is in the map. O(1). */ @@ -557,6 +612,16 @@ library EnumerableMap { return remove(map._inner, bytes32(uint256(uint160(key)))); } + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(AddressToAddressMap storage map) internal { + clear(map._inner); + } + /** * @dev Returns true if the key is in the map. O(1). */ @@ -650,6 +715,16 @@ library EnumerableMap { return remove(map._inner, bytes32(uint256(uint160(key)))); } + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(AddressToBytes32Map storage map) internal { + clear(map._inner); + } + /** * @dev Returns true if the key is in the map. O(1). */ @@ -743,6 +818,16 @@ library EnumerableMap { return remove(map._inner, key); } + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(Bytes32ToUintMap storage map) internal { + clear(map._inner); + } + /** * @dev Returns true if the key is in the map. O(1). */ @@ -836,6 +921,16 @@ library EnumerableMap { return remove(map._inner, key); } + /** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(Bytes32ToAddressMap storage map) internal { + clear(map._inner); + } + /** * @dev Returns true if the key is in the map. O(1). */ diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 065202e8204..7d7ba082520 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -4,6 +4,9 @@ pragma solidity ^0.8.20; +import {Arrays} from "../Arrays.sol"; +import {Hashes} from "../cryptography/Hashes.sol"; + /** * @dev Library for managing * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive @@ -14,6 +17,7 @@ pragma solidity ^0.8.20; * - Elements are added, removed, and checked for existence in constant time * (O(1)). * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * - Set can be cleared (all elements removed) in O(n). * * ```solidity * contract Example { @@ -114,6 +118,20 @@ library EnumerableSet { } } + /** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ + function _clear(Set storage set) private { + uint256 len = _length(set); + for (uint256 i = 0; i < len; ++i) { + delete set._positions[set._values[i]]; + } + Arrays.unsafeSetLength(set._values, 0); + } + /** * @dev Returns true if the value is in the set. O(1). */ @@ -180,6 +198,16 @@ library EnumerableSet { return _remove(set._inner, value); } + /** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(Bytes32Set storage set) internal { + _clear(set._inner); + } + /** * @dev Returns true if the value is in the set. O(1). */ @@ -253,6 +281,16 @@ library EnumerableSet { return _remove(set._inner, bytes32(uint256(uint160(value)))); } + /** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(AddressSet storage set) internal { + _clear(set._inner); + } + /** * @dev Returns true if the value is in the set. O(1). */ @@ -326,6 +364,16 @@ library EnumerableSet { return _remove(set._inner, bytes32(value)); } + /** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(UintSet storage set) internal { + _clear(set._inner); + } + /** * @dev Returns true if the value is in the set. O(1). */ @@ -372,4 +420,133 @@ library EnumerableSet { return result; } + + struct Bytes32x2Set { + // Storage of set values + bytes32[2][] _values; + // Position is the index of the value in the `values` array plus 1. + // Position 0 is used to mean a value is not in the self. + mapping(bytes32 valueHash => uint256) _positions; + } + + /** + * @dev Add a value to a self. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ + function add(Bytes32x2Set storage self, bytes32[2] memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[_hash(value)] = self._values.length; + return true; + } else { + return false; + } + } + + /** + * @dev Removes a value from a self. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ + function remove(Bytes32x2Set storage self, bytes32[2] memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + bytes32 valueHash = _hash(value); + uint256 position = self._positions[valueHash]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + bytes32[2] memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[_hash(lastValue)] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[valueHash]; + + return true; + } else { + return false; + } + } + + /** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ + function clear(Bytes32x2Set storage self) internal { + bytes32[2][] storage v = self._values; + + uint256 len = length(self); + for (uint256 i = 0; i < len; ++i) { + delete self._positions[_hash(v[i])]; + } + assembly ("memory-safe") { + sstore(v.slot, 0) + } + } + + /** + * @dev Returns true if the value is in the self. O(1). + */ + function contains(Bytes32x2Set storage self, bytes32[2] memory value) internal view returns (bool) { + return self._positions[_hash(value)] != 0; + } + + /** + * @dev Returns the number of values on the self. O(1). + */ + function length(Bytes32x2Set storage self) internal view returns (uint256) { + return self._values.length; + } + + /** + * @dev Returns the value stored at position `index` in the self. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - `index` must be strictly less than {length}. + */ + function at(Bytes32x2Set storage self, uint256 index) internal view returns (bytes32[2] memory) { + return self._values[index]; + } + + /** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ + function values(Bytes32x2Set storage self) internal view returns (bytes32[2][] memory) { + return self._values; + } + + function _hash(bytes32[2] memory value) private pure returns (bytes32) { + return Hashes.efficientKeccak256(value[0], value[1]); + } } diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 56f5bc67237..a52cfc91da8 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -88,7 +88,7 @@ library MerkleTree { // Build each root of zero-filled subtrees bytes32 currentZero = zero; - for (uint32 i = 0; i < treeDepth; ++i) { + for (uint256 i = 0; i < treeDepth; ++i) { Arrays.unsafeAccess(self._zeros, i).value = currentZero; currentZero = fnHash(currentZero, currentZero); } @@ -143,7 +143,7 @@ library MerkleTree { // Rebuild branch from leaf to root uint256 currentIndex = index; bytes32 currentLevelHash = leaf; - for (uint32 i = 0; i < treeDepth; i++) { + for (uint256 i = 0; i < treeDepth; i++) { // Reaching the parent node, is currentLevelHash the left child? bool isLeft = currentIndex % 2 == 0; diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index 15af8b40ea3..52f7e37b09d 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -13,6 +13,7 @@ ** xref:erc721.adoc[ERC-721] ** xref:erc1155.adoc[ERC-1155] ** xref:erc4626.adoc[ERC-4626] +** xref:erc6909.adoc[ERC-6909] * xref:governance.adoc[Governance] diff --git a/docs/modules/ROOT/pages/crowdsales.adoc b/docs/modules/ROOT/pages/crowdsales.adoc deleted file mode 100644 index 37579211d1e..00000000000 --- a/docs/modules/ROOT/pages/crowdsales.adoc +++ /dev/null @@ -1,11 +0,0 @@ -= Crowdsales - -All crowdsale-related contracts were removed from the OpenZeppelin Contracts library on the https://forum.openzeppelin.com/t/openzeppelin-contracts-v3-0-beta-release/2256[v3.0.0 release] due to both a decline in their usage and the complexity associated with migrating them to Solidity v0.6. - -They are however still available on the v2.5 release of OpenZeppelin Contracts, which you can install by running: - -```console -$ npm install @openzeppelin/contracts@v2.5 -``` - -Refer to the https://docs.openzeppelin.com/contracts/2.x/crowdsales[v2.x documentation] when working with them. diff --git a/docs/modules/ROOT/pages/drafts.adoc b/docs/modules/ROOT/pages/drafts.adoc deleted file mode 100644 index b2c1ae6282b..00000000000 --- a/docs/modules/ROOT/pages/drafts.adoc +++ /dev/null @@ -1,19 +0,0 @@ -= Drafts - -All draft contracts were either moved into a different directory or removed from the OpenZeppelin Contracts library on the https://forum.openzeppelin.com/t/openzeppelin-contracts-v3-0-beta-release/2256[v3.0.0 release]. - -* `ERC20Migrator`: removed. -* xref:api:token/ERC20.adoc#ERC20Snapshot[`ERC20Snapshot`]: moved to `token/ERC20`. -* `ERC20Detailed` and `ERC1046`: removed. -* `TokenVesting`: removed. Pending a replacement that is being discussed in https://github.com/OpenZeppelin/openzeppelin-contracts/issues/1214[`#1214`]. -* xref:api:utils.adoc#Counters[`Counters`]: moved to xref:api:utils.adoc[`utils`]. -* xref:api:utils.adoc#Strings[`Strings`]: moved to xref:api:utils.adoc[`utils`]. -* xref:api:utils.adoc#SignedSafeMath[`SignedSafeMath`]: moved to xref:api:utils.adoc[`utils`]. - -Removed contracts are still available on the v2.5 release of OpenZeppelin Contracts, which you can install by running: - -```console -$ npm install @openzeppelin/contracts@v2.5 -``` - -Refer to the xref:2.x@contracts:api:drafts.adoc[v2.x documentation] when working with them. diff --git a/docs/modules/ROOT/pages/erc20-supply.adoc b/docs/modules/ROOT/pages/erc20-supply.adoc index ae21e4a8adb..273cb32521a 100644 --- a/docs/modules/ROOT/pages/erc20-supply.adoc +++ b/docs/modules/ROOT/pages/erc20-supply.adoc @@ -68,4 +68,4 @@ include::api:example$ERC20WithAutoMinerReward.sol[] [[wrapping-up]] == Wrapping Up -We've seen how to implement a ERC-20 supply mechanism: internally through `_mint`. Hopefully this has helped you understand how to use OpenZeppelin Contracts and some of the design principles behind it, and you can apply them to your own smart contracts. +We've seen how to implement an ERC-20 supply mechanism: internally through `_mint`. Hopefully this has helped you understand how to use OpenZeppelin Contracts and some of the design principles behind it, and you can apply them to your own smart contracts. diff --git a/docs/modules/ROOT/pages/erc6909.adoc b/docs/modules/ROOT/pages/erc6909.adoc new file mode 100644 index 00000000000..164ded8ed0c --- /dev/null +++ b/docs/modules/ROOT/pages/erc6909.adoc @@ -0,0 +1,47 @@ += ERC-6909 + +ERC-6909 is a draft EIP that draws on ERC-1155 learnings since it was published in 2018. The main goals of ERC-6909 is to decrease gas costs and complexity--this is mainly accomplished by removing batching and callbacks. + +TIP: To understand the inspiration for a multi token standard, see the xref:erc1155.adoc#multi-token-standard[multi token standard] section within the EIP-1155 docs. + +== Changes from ERC-1155 + +There are three main changes from ERC-1155 which are as follows: + +. The removal of batch operations. +. The removal of transfer callbacks. +. Granularization in approvals--approvals can be set globally (as operators) or as amounts per token (inspired by ERC20). + +== Constructing an ERC-6909 Token Contract + +We'll use ERC-6909 to track multiple items in a game, each having their own unique attributes. All item types will by minted to the deployer of the contract, which we can later transfer to players. We'll also use the xref:api:token/ERC6909.adoc#ERC6909Metadata[`ERC6909Metadata`] extension to add decimals to our fungible items (the vanilla ERC-6909 implementation does not have decimals). + +For simplicity, we will mint all items in the constructor--however, minting functionality could be added to the contract to mint on demand to players. + +TIP: For an overview of minting mechanisms, check out xref:erc20-supply.adoc[Creating ERC-20 Supply]. + +Here's what a contract for tokenized items might look like: + +[source,solidity] +---- +include::api:example$token/ERC6909/ERC6909GameItems.sol[] +---- + +Note that there is no content URI functionality in the base implementation, but the xref:api:token/ERC6909.adoc#ERC6909ContentURI[`ERC6909ContentURI`] extension adds it. Additionally, the base implementation does not track total supplies, but the xref:api:token/ERC6909.adoc#ERC6909TokenSupply[`ERC6909TokenSupply`] extension tracks the total supply of each token id. + +Once the contract is deployed, we will be able to query the deployer’s balance: +[source,javascript] +---- +> gameItems.balanceOf(deployerAddress, 3) +1000000000 +---- + +We can transfer items to player accounts: +[source,javascript] +---- +> gameItems.transfer(playerAddress, 2, 1) +> gameItems.balanceOf(playerAddress, 2) +1 +> gameItems.balanceOf(deployerAddress, 2) +0 +---- diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 5f8e77555aa..c2b8218ff34 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -20,7 +20,7 @@ The ERC-20 extension to keep track of votes and vote delegation is one such case === Governor & GovernorStorage -An OpenZeppelin Governor contract is 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 and Bravo are likewise not available. It’s possible to opt in some Bravo-like behavior by inheriting from the GovernorStorage module. This module provides proposal enumerability and alternate versions of the `queue`, `execute` and `cancel` function that only take the proposal id. This module reduces the calldata needed by some operations in exchange for an increased the storage footprint. This might be a good trade-off for some L2 chains. It also provides primitives for indexer-free frontends. +An OpenZeppelin Governor contract is 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 and Bravo are likewise not available. It’s possible to opt in some Bravo-like behavior by inheriting from the GovernorStorage module. This module provides proposal enumerability and alternate versions of the `queue`, `execute` and `cancel` function that only take the proposal id. This module reduces the calldata needed by some operations in exchange for an increased storage footprint. This might be a good trade-off for some L2 chains. It also provides primitives for indexer-free frontends. Note that even with the use of this module, one important difference with Compound's GovernorBravo is 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. @@ -52,7 +52,7 @@ If your project already has a live token that does not include ERC20Votes and is include::api:example$governance/MyTokenWrapped.sol[] ``` -NOTE: The only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. ERC-721 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 only other source of voting power available in OpenZeppelin Contracts currently is xref:api:token/ERC721.adoc#ERC721Votes[`ERC721Votes`]. ERC-721 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/ERC721.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. diff --git a/docs/templates/contract.hbs b/docs/templates/contract.hbs index aaca0a3cc56..458b511b050 100644 --- a/docs/templates/contract.hbs +++ b/docs/templates/contract.hbs @@ -2,6 +2,10 @@ :{{name}}: pass:normal[xref:#{{anchor}}[`++{{name}}++`]] {{/each}} +{{#each functions}} +:{{fullname}}: pass:normal[xref:#{{anchor}}[`++{{name}}++`]] +{{/each}} + [.contract] [[{{anchor}}]] === `++{{name}}++` link:https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v{{oz-version}}/{{__item_context.file.absolutePath}}[{github-icon},role=heading-link] diff --git a/docs/templates/properties.js b/docs/templates/properties.js index 52eebac547e..8d6b2867da2 100644 --- a/docs/templates/properties.js +++ b/docs/templates/properties.js @@ -17,6 +17,22 @@ module.exports.anchor = function anchor({ item, contract }) { return res; }; +module.exports.fullname = function fullname({ item }) { + let res = ''; + res += item.name; + if ('parameters' in item) { + const signature = item.parameters.parameters.map(v => v.typeName.typeDescriptions.typeString).join(','); + res += slug('(' + signature + ')'); + } + if (isNodeType('VariableDeclaration', item)) { + res += '-' + slug(item.typeName.typeDescriptions.typeString); + } + if (res.charAt(res.length - 1) === '-') { + return res.slice(0, -1); + } + return res; +}; + module.exports.inheritance = function ({ item, build }) { if (!isNodeType('ContractDefinition', item)) { throw new Error('used inherited-items on non-contract'); diff --git a/fv-requirements.txt b/fv-requirements.txt index 80917f2886a..5aeb1225aae 100644 --- a/fv-requirements.txt +++ b/fv-requirements.txt @@ -1,4 +1,4 @@ certora-cli==4.13.1 # File uses a custom name (fv-requirements.txt) so that it isn't picked by Netlify's build # whose latest Python version is 0.3.8, incompatible with most recent versions of Halmos -halmos==0.2.3 +halmos==0.2.4 diff --git a/hardhat.config.js b/hardhat.config.js index d39d3d07323..b4b8d630fb6 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -90,7 +90,6 @@ module.exports = { 'initcode-size': 'off', }, '*': { - 'code-size': true, 'unused-param': !argv.coverage, // coverage causes unused-param warnings 'transient-storage': false, default: 'error', diff --git a/hardhat/async-test-sanity.js b/hardhat/async-test-sanity.js index c05e5bd489c..8e60f70d506 100644 --- a/hardhat/async-test-sanity.js +++ b/hardhat/async-test-sanity.js @@ -1,3 +1,10 @@ process.on('unhandledRejection', reason => { - throw new Error(reason); + // If the reason is already an Error object, throw it directly to preserve the stack trace. + if (reason instanceof Error) { + throw reason; + } else { + // If the reason is not an Error (e.g., a string, number, or other primitive), + // create a new Error object with the reason as its message. + throw new Error(`Unhandled rejection: ${reason}`); + } }); diff --git a/package.json b/package.json index 687d3d33fcd..f9e7d92058b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openzeppelin-solidity", "description": "Secure Smart Contract library for Solidity", - "version": "5.1.0", + "version": "5.2.0", "private": true, "files": [ "/contracts/**/*.sol", diff --git a/scripts/generate/templates/Arrays.js b/scripts/generate/templates/Arrays.js index 0d3676a727d..6b3a9a6a312 100644 --- a/scripts/generate/templates/Arrays.js +++ b/scripts/generate/templates/Arrays.js @@ -346,7 +346,7 @@ function unsafeMemoryAccess(${type}[] memory arr, uint256 pos) internal pure ret const unsafeSetLength = type => `\ /** - * @dev Helper to set the length of an dynamic array. Directly writing to \`.length\` is forbidden. + * @dev Helper to set the length of a dynamic array. Directly writing to \`.length\` is forbidden. * * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased. */ diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index c9cad6c1bc8..284e5ac0281 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -17,6 +17,7 @@ import {EnumerableSet} from "./EnumerableSet.sol"; * - Entries are added, removed, and checked for existence in constant time * (O(1)). * - Entries are enumerated in O(n). No guarantees are made on the ordering. + * - Map can be cleared (all entries removed) in O(n). * * \`\`\`solidity * contract Example { @@ -91,6 +92,20 @@ function remove(Bytes32ToBytes32Map storage map, bytes32 key) internal returns ( return map._keys.remove(key); } +/** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ +function clear(Bytes32ToBytes32Map storage map) internal { + uint256 len = length(map); + for (uint256 i = 0; i < len; ++i) { + delete map._values[map._keys.at(i)]; + } + map._keys.clear(); +} + /** * @dev Returns true if the key is in the map. O(1). */ @@ -188,6 +203,16 @@ function remove(${name} storage map, ${keyType} key) internal returns (bool) { return remove(map._inner, ${toBytes32(keyType, 'key')}); } +/** + * @dev Removes all the entries from a map. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block. + */ +function clear(${name} storage map) internal { + clear(map._inner); +} + /** * @dev Returns true if the key is in the map. O(1). */ diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index 02eccd0df11..e6a3d220276 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -5,6 +5,9 @@ const { TYPES } = require('./EnumerableSet.opts'); const header = `\ pragma solidity ^0.8.20; +import {Arrays} from "../Arrays.sol"; +import {Hashes} from "../cryptography/Hashes.sol"; + /** * @dev Library for managing * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive @@ -15,6 +18,7 @@ pragma solidity ^0.8.20; * - Elements are added, removed, and checked for existence in constant time * (O(1)). * - Elements are enumerated in O(n). No guarantees are made on the ordering. + * - Set can be cleared (all elements removed) in O(n). * * \`\`\`solidity * contract Example { @@ -117,6 +121,20 @@ function _remove(Set storage set, bytes32 value) private returns (bool) { } } +/** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ +function _clear(Set storage set) private { + uint256 len = _length(set); + for (uint256 i = 0; i < len; ++i) { + delete set._positions[set._values[i]]; + } + Arrays.unsafeSetLength(set._values, 0); +} + /** * @dev Returns true if the value is in the set. O(1). */ @@ -185,6 +203,16 @@ function remove(${name} storage set, ${type} value) internal returns (bool) { return _remove(set._inner, ${toBytes32(type, 'value')}); } +/** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ +function clear(${name} storage set) internal { + _clear(set._inner); +} + /** * @dev Returns true if the value is in the set. O(1). */ @@ -233,6 +261,139 @@ function values(${name} storage set) internal view returns (${type}[] memory) { } `; +const memorySet = ({ name, type }) => `\ +struct ${name} { + // Storage of set values + ${type}[] _values; + // Position is the index of the value in the \`values\` array plus 1. + // Position 0 is used to mean a value is not in the self. + mapping(bytes32 valueHash => uint256) _positions; +} + +/** + * @dev Add a value to a self. O(1). + * + * Returns true if the value was added to the set, that is if it was not + * already present. + */ +function add(${name} storage self, ${type} memory value) internal returns (bool) { + if (!contains(self, value)) { + self._values.push(value); + // The value is stored at length-1, but we add 1 to all indexes + // and use 0 as a sentinel value + self._positions[_hash(value)] = self._values.length; + return true; + } else { + return false; + } +} + +/** + * @dev Removes a value from a self. O(1). + * + * Returns true if the value was removed from the set, that is if it was + * present. + */ +function remove(${name} storage self, ${type} memory value) internal returns (bool) { + // We cache the value's position to prevent multiple reads from the same storage slot + bytes32 valueHash = _hash(value); + uint256 position = self._positions[valueHash]; + + if (position != 0) { + // Equivalent to contains(self, value) + // To delete an element from the _values array in O(1), we swap the element to delete with the last one in + // the array, and then remove the last element (sometimes called as 'swap and pop'). + // This modifies the order of the array, as noted in {at}. + + uint256 valueIndex = position - 1; + uint256 lastIndex = self._values.length - 1; + + if (valueIndex != lastIndex) { + ${type} memory lastValue = self._values[lastIndex]; + + // Move the lastValue to the index where the value to delete is + self._values[valueIndex] = lastValue; + // Update the tracked position of the lastValue (that was just moved) + self._positions[_hash(lastValue)] = position; + } + + // Delete the slot where the moved value was stored + self._values.pop(); + + // Delete the tracked position for the deleted slot + delete self._positions[valueHash]; + + return true; + } else { + return false; + } +} + +/** + * @dev Removes all the values from a set. O(n). + * + * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the + * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block. + */ +function clear(${name} storage self) internal { + ${type}[] storage v = self._values; + + uint256 len = length(self); + for (uint256 i = 0; i < len; ++i) { + delete self._positions[_hash(v[i])]; + } + assembly ("memory-safe") { + sstore(v.slot, 0) + } +} + +/** + * @dev Returns true if the value is in the self. O(1). + */ +function contains(${name} storage self, ${type} memory value) internal view returns (bool) { + return self._positions[_hash(value)] != 0; +} + +/** + * @dev Returns the number of values on the self. O(1). + */ +function length(${name} storage self) internal view returns (uint256) { + return self._values.length; +} + +/** + * @dev Returns the value stored at position \`index\` in the self. O(1). + * + * Note that there are no guarantees on the ordering of values inside the + * array, and it may change when more values are added or removed. + * + * Requirements: + * + * - \`index\` must be strictly less than {length}. + */ +function at(${name} storage self, uint256 index) internal view returns (${type} memory) { + return self._values[index]; +} + +/** + * @dev Return the entire set in an array + * + * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed + * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that + * this function has an unbounded cost, and using it as part of a state-changing function may render the function + * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block. + */ +function values(${name} storage self) internal view returns (${type}[] memory) { + return self._values; +} +`; + +const hashes = `\ +function _hash(bytes32[2] memory value) private pure returns (bytes32) { + return Hashes.efficientKeccak256(value[0], value[1]); +} +`; + // GENERATE module.exports = format( header.trimEnd(), @@ -240,7 +401,9 @@ module.exports = format( format( [].concat( defaultSet, - TYPES.map(details => customSet(details)), + TYPES.filter(({ size }) => size == undefined).map(details => customSet(details)), + TYPES.filter(({ size }) => size != undefined).map(details => memorySet(details)), + hashes, ), ).trimEnd(), '}', diff --git a/scripts/generate/templates/EnumerableSet.opts.js b/scripts/generate/templates/EnumerableSet.opts.js index 739f0acdfe4..a8173f3cf28 100644 --- a/scripts/generate/templates/EnumerableSet.opts.js +++ b/scripts/generate/templates/EnumerableSet.opts.js @@ -1,12 +1,16 @@ const { capitalize } = require('../../helpers'); -const mapType = str => (str == 'uint256' ? 'Uint' : capitalize(str)); +const mapType = ({ type, size }) => [type == 'uint256' ? 'Uint' : capitalize(type), size].filter(Boolean).join('x'); -const formatType = type => ({ - name: `${mapType(type)}Set`, - type, +const formatType = ({ type, size = undefined }) => ({ + name: `${mapType({ type, size })}Set`, + type: size != undefined ? `${type}[${size}]` : type, + base: size != undefined ? type : undefined, + size, }); -const TYPES = ['bytes32', 'address', 'uint256'].map(formatType); +const TYPES = [{ type: 'bytes32' }, { type: 'bytes32', size: 2 }, { type: 'address' }, { type: 'uint256' }].map( + formatType, +); module.exports = { TYPES, formatType }; diff --git a/scripts/set-max-old-space-size.sh b/scripts/set-max-old-space-size.sh index 525c6786418..f56b11dc3d4 100755 --- a/scripts/set-max-old-space-size.sh +++ b/scripts/set-max-old-space-size.sh @@ -1,10 +1,10 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh # This script sets the node `--max-old-space-size` to 8192 if it is not set already. # All existing `NODE_OPTIONS` are retained as is. export NODE_OPTIONS="${NODE_OPTIONS:-}" -if [[ $NODE_OPTIONS != *"--max-old-space-size"* ]]; then +if [ "${NODE_OPTIONS##*--max-old-space-size*}" = "$NODE_OPTIONS" ]; then export NODE_OPTIONS="${NODE_OPTIONS} --max-old-space-size=8192" fi diff --git a/scripts/upgradeable/upgradeable.patch b/scripts/upgradeable/upgradeable.patch index 458ecd43580..5d54ef4203e 100644 --- a/scripts/upgradeable/upgradeable.patch +++ b/scripts/upgradeable/upgradeable.patch @@ -59,7 +59,7 @@ index ff596b0c3..000000000 - - diff --git a/README.md b/README.md -index fa7b4e31e..4799b6376 100644 +index 60d0a430a..0e4f91a6d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ @@ -110,7 +110,7 @@ index fa7b4e31e..4799b6376 100644 } ``` diff --git a/contracts/package.json b/contracts/package.json -index 845e8c403..8dc181b91 100644 +index 3682eadeb..4f870d094 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,5 +1,5 @@ @@ -118,7 +118,7 @@ index 845e8c403..8dc181b91 100644 - "name": "@openzeppelin/contracts", + "name": "@openzeppelin/contracts-upgradeable", "description": "Secure Smart Contract library for Solidity", - "version": "5.0.2", + "version": "5.2.0", "files": [ @@ -13,7 +13,7 @@ }, @@ -140,7 +140,7 @@ index 845e8c403..8dc181b91 100644 + } } diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol -index 77c4c8990..602467f40 100644 +index bcb67c87a..7195c3bbd 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -4,7 +4,6 @@ @@ -151,7 +151,7 @@ index 77c4c8990..602467f40 100644 import {IERC5267} from "../../interfaces/IERC5267.sol"; /** -@@ -28,28 +27,18 @@ import {IERC5267} from "../../interfaces/IERC5267.sol"; +@@ -28,30 +27,18 @@ import {IERC5267} from "../../interfaces/IERC5267.sol"; * 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. @@ -177,14 +177,16 @@ index 77c4c8990..602467f40 100644 - ShortString private immutable _name; - ShortString private immutable _version; +- // slither-disable-next-line constable-states - string private _nameFallback; +- // slither-disable-next-line constable-states - string private _versionFallback; + string private _name; + string private _version; /** * @dev Initializes the domain separator and parameter caches. -@@ -64,29 +53,23 @@ abstract contract EIP712 is IERC5267 { +@@ -66,29 +53,23 @@ abstract contract EIP712 is IERC5267 { * contract upgrade]. */ constructor(string memory name, string memory version) { @@ -222,7 +224,7 @@ index 77c4c8990..602467f40 100644 } /** -@@ -125,6 +108,10 @@ abstract contract EIP712 is IERC5267 { +@@ -127,6 +108,10 @@ abstract contract EIP712 is IERC5267 { uint256[] memory extensions ) { @@ -233,7 +235,7 @@ index 77c4c8990..602467f40 100644 return ( hex"0f", // 01111 _EIP712Name(), -@@ -139,22 +126,62 @@ abstract contract EIP712 is IERC5267 { +@@ -141,22 +126,62 @@ abstract contract EIP712 is IERC5267 { /** * @dev The name parameter for the EIP712 domain. * @@ -307,10 +309,10 @@ index 77c4c8990..602467f40 100644 } } diff --git a/package.json b/package.json -index c4b358e10..96ab2559c 100644 +index f9e7d9205..c35020d51 100644 --- a/package.json +++ b/package.json -@@ -32,7 +32,7 @@ +@@ -34,7 +34,7 @@ }, "repository": { "type": "git", diff --git a/test/account/utils/draft-ERC4337Utils.test.js b/test/account/utils/draft-ERC4337Utils.test.js index f2569725f00..d3523477a99 100644 --- a/test/account/utils/draft-ERC4337Utils.test.js +++ b/test/account/utils/draft-ERC4337Utils.test.js @@ -20,6 +20,12 @@ describe('ERC4337Utils', function () { Object.assign(this, await loadFixture(fixture)); }); + describe('entrypoint', function () { + it('v0.7.0', async function () { + await expect(this.utils.$ENTRYPOINT_V07()).to.eventually.equal(entrypoint); + }); + }); + describe('parseValidationData', function () { it('parses the validation data', async function () { const authorizer = this.authorizer; diff --git a/test/account/utils/draft-ERC7579Utils.t.sol b/test/account/utils/draft-ERC7579Utils.t.sol index fdd4edf5958..ea5890943c5 100644 --- a/test/account/utils/draft-ERC7579Utils.t.sol +++ b/test/account/utils/draft-ERC7579Utils.t.sol @@ -20,8 +20,6 @@ contract SampleAccount is IAccount, Ownable { using ERC4337Utils for *; using ERC7579Utils for *; - IEntryPoint internal constant ENTRY_POINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); - event Log(bool duringValidation, Execution[] calls); error UnsupportedCallType(CallType callType); @@ -33,7 +31,7 @@ contract SampleAccount is IAccount, Ownable { bytes32 userOpHash, uint256 missingAccountFunds ) external override returns (uint256 validationData) { - require(msg.sender == address(ENTRY_POINT), "only from EP"); + require(msg.sender == address(ERC4337Utils.ENTRYPOINT_V07), "only from EP"); // Check signature if (userOpHash.toEthSignedMessageHash().recover(userOp.signature) != owner()) { revert OwnableUnauthorizedAccount(_msgSender()); @@ -81,7 +79,7 @@ contract SampleAccount is IAccount, Ownable { } function execute(Mode mode, bytes calldata executionCalldata) external payable { - require(msg.sender == address(this) || msg.sender == address(ENTRY_POINT), "not auth"); + require(msg.sender == address(this) || msg.sender == address(ERC4337Utils.ENTRYPOINT_V07), "not auth"); (CallType callType, ExecType execType, , ) = mode.decodeMode(); @@ -105,7 +103,6 @@ contract ERC7579UtilsTest is Test { using ERC4337Utils for *; using ERC7579Utils for *; - IEntryPoint private constant ENTRYPOINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); address private _owner; uint256 private _ownerKey; address private _account; @@ -166,7 +163,7 @@ contract ERC7579UtilsTest is Test { userOps[0].signature = abi.encodePacked(r, s, v); vm.recordLogs(); - ENTRYPOINT.handleOps(userOps, payable(_beneficiary)); + ERC4337Utils.ENTRYPOINT_V07.handleOps(userOps, payable(_beneficiary)); assertEq(_recipient1.balance, 1 wei); assertEq(_recipient2.balance, 1 wei); @@ -224,7 +221,7 @@ contract ERC7579UtilsTest is Test { abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector) ) ); - ENTRYPOINT.handleOps(userOps, payable(_beneficiary)); + ERC4337Utils.ENTRYPOINT_V07.handleOps(userOps, payable(_beneficiary)); _collectAndPrintLogs(false); } @@ -282,7 +279,7 @@ contract ERC7579UtilsTest is Test { abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector) ) ); - ENTRYPOINT.handleOps(userOps, payable(_beneficiary)); + ERC4337Utils.ENTRYPOINT_V07.handleOps(userOps, payable(_beneficiary)); _collectAndPrintLogs(true); } @@ -378,7 +375,7 @@ contract ERC7579UtilsTest is Test { } function hashUserOperation(PackedUserOperation calldata useroperation) public view returns (bytes32) { - return useroperation.hash(address(ENTRYPOINT), block.chainid); + return useroperation.hash(address(ERC4337Utils.ENTRYPOINT_V07), block.chainid); } function _collectAndPrintLogs(bool includeTotalValue) internal { diff --git a/test/governance/Governor.test.js b/test/governance/Governor.test.js index ea1c3a32457..0e4283c3bed 100644 --- a/test/governance/Governor.test.js +++ b/test/governance/Governor.test.js @@ -624,8 +624,8 @@ describe('Governor', function () { await this.helper.connect(this.proposer).propose(); await expect(this.helper.connect(this.owner).cancel('external')) - .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyProposer') - .withArgs(this.owner); + .to.be.revertedWithCustomError(this.mock, 'GovernorUnableToCancel') + .withArgs(this.proposal.id, this.owner); }); it('after vote started', async function () { @@ -633,12 +633,8 @@ describe('Governor', function () { await this.helper.waitForSnapshot(1n); // snapshot + 1 block await expect(this.helper.cancel('external')) - .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') - .withArgs( - this.proposal.id, - ProposalState.Active, - GovernorHelper.proposalStatesToBitMap([ProposalState.Pending]), - ); + .to.be.revertedWithCustomError(this.mock, 'GovernorUnableToCancel') + .withArgs(this.proposal.id, this.owner); }); it('after vote', async function () { @@ -647,12 +643,8 @@ describe('Governor', function () { await this.helper.connect(this.voter1).vote({ support: VoteType.For }); await expect(this.helper.cancel('external')) - .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') - .withArgs( - this.proposal.id, - ProposalState.Active, - GovernorHelper.proposalStatesToBitMap([ProposalState.Pending]), - ); + .to.be.revertedWithCustomError(this.mock, 'GovernorUnableToCancel') + .withArgs(this.proposal.id, this.voter1); }); it('after deadline', async function () { @@ -662,12 +654,8 @@ describe('Governor', function () { await this.helper.waitForDeadline(); await expect(this.helper.cancel('external')) - .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') - .withArgs( - this.proposal.id, - ProposalState.Succeeded, - GovernorHelper.proposalStatesToBitMap([ProposalState.Pending]), - ); + .to.be.revertedWithCustomError(this.mock, 'GovernorUnableToCancel') + .withArgs(this.proposal.id, this.voter1); }); it('after execution', async function () { @@ -678,12 +666,8 @@ describe('Governor', function () { await this.helper.execute(); await expect(this.helper.cancel('external')) - .to.be.revertedWithCustomError(this.mock, 'GovernorUnexpectedProposalState') - .withArgs( - this.proposal.id, - ProposalState.Executed, - GovernorHelper.proposalStatesToBitMap([ProposalState.Pending]), - ); + .to.be.revertedWithCustomError(this.mock, 'GovernorUnableToCancel') + .withArgs(this.proposal.id, this.voter1); }); }); }); diff --git a/test/governance/extensions/GovernorCountingOverridable.test.js b/test/governance/extensions/GovernorCountingOverridable.test.js index 32ee47439f3..fd1032b9ec2 100644 --- a/test/governance/extensions/GovernorCountingOverridable.test.js +++ b/test/governance/extensions/GovernorCountingOverridable.test.js @@ -264,7 +264,7 @@ describe('GovernorCountingOverridable', function () { .to.emit(this.mock, 'OverrideVoteCast') .withArgs(this.voter1, this.helper.id, VoteType.Against, ethers.parseEther('10'), ''); await expect(this.mock.connect(this.voter1).castOverrideVote(this.helper.id, VoteType.Abstain, '')) - .to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyOverridenVote') + .to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyOverriddenVote') .withArgs(this.voter1.address); }); diff --git a/test/governance/extensions/GovernorProposalGuardian.test.js b/test/governance/extensions/GovernorProposalGuardian.test.js new file mode 100644 index 00000000000..1741072c398 --- /dev/null +++ b/test/governance/extensions/GovernorProposalGuardian.test.js @@ -0,0 +1,132 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { impersonate } = require('../../helpers/account'); +const { GovernorHelper } = require('../../helpers/governance'); +const { ProposalState } = require('../../helpers/enums'); + +const TOKENS = [ + { Token: '$ERC20Votes', mode: 'blocknumber' }, + { Token: '$ERC20VotesTimestampMock', mode: 'timestamp' }, +]; +const name = 'Proposal Guardian Governor'; +const version = '1'; +const tokenName = 'MockToken'; +const tokenSymbol = 'MTKN'; +const tokenSupply = ethers.parseEther('100'); +const votingDelay = 4n; +const votingPeriod = 16n; +const value = ethers.parseEther('1'); + +describe('GovernorProposalGuardian', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, proposer, guardian, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); + const receiver = await ethers.deployContract('CallReceiverMock'); + + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]); + const mock = await ethers.deployContract('$GovernorProposalGuardianMock', [ + name, // name + votingDelay, // initialVotingDelay + votingPeriod, // initialVotingPeriod + 0n, // initialProposalThreshold + token, // tokenAddress + 10n, // quorumNumeratorValue + ]); + + await impersonate(mock.target); + await owner.sendTransaction({ to: mock, value }); + await token.$_mint(owner, tokenSupply); + + const helper = new GovernorHelper(mock, mode); + await helper.connect(owner).delegate({ token, to: voter1, value: ethers.parseEther('10') }); + await helper.connect(owner).delegate({ token, to: voter2, value: ethers.parseEther('7') }); + await helper.connect(owner).delegate({ token, to: voter3, value: ethers.parseEther('5') }); + await helper.connect(owner).delegate({ token, to: voter4, value: ethers.parseEther('2') }); + + return { owner, proposer, guardian, voter1, voter2, voter3, voter4, other, receiver, token, mock, helper }; + }; + + describe(`using ${Token}`, function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + + // default proposal + this.proposal = this.helper.setProposal( + [ + { + target: this.receiver.target, + value, + data: this.receiver.interface.encodeFunctionData('mockFunction'), + }, + ], + '', + ); + }); + + it('deployment check', async function () { + await expect(this.mock.name()).to.eventually.equal(name); + await expect(this.mock.token()).to.eventually.equal(this.token); + await expect(this.mock.votingDelay()).to.eventually.equal(votingDelay); + await expect(this.mock.votingPeriod()).to.eventually.equal(votingPeriod); + }); + + describe('set proposal guardian', function () { + it('from governance', async function () { + const governorSigner = await ethers.getSigner(this.mock.target); + await expect(this.mock.connect(governorSigner).setProposalGuardian(this.guardian)) + .to.emit(this.mock, 'ProposalGuardianSet') + .withArgs(ethers.ZeroAddress, this.guardian); + await expect(this.mock.proposalGuardian()).to.eventually.equal(this.guardian); + }); + + it('from non-governance', async function () { + await expect(this.mock.connect(this.other).setProposalGuardian(this.guardian)) + .to.be.revertedWithCustomError(this.mock, 'GovernorOnlyExecutor') + .withArgs(this.other); + }); + }); + + it('cancel proposal during pending state from proposer when proposal guardian is non-zero', async function () { + await this.mock.$_setProposalGuardian(this.guardian); + await this.helper.connect(this.proposer).propose(); + await expect(this.helper.connect(this.proposer).cancel()) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); + }); + + describe('cancel proposal during active state', function () { + beforeEach(async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(1n); + await expect(this.mock.state(this.proposal.id)).to.eventually.equal(ProposalState.Active); + }); + + it('from proposal guardian', async function () { + await this.mock.$_setProposalGuardian(this.guardian); + + await expect(this.helper.connect(this.guardian).cancel()) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); + }); + + it('from proposer when proposal guardian is non-zero', async function () { + await this.mock.$_setProposalGuardian(this.guardian); + + await expect(this.helper.connect(this.proposer).cancel()) + .to.be.revertedWithCustomError(this.mock, 'GovernorUnableToCancel') + .withArgs(this.proposal.id, this.proposer); + }); + + it('from proposer when proposal guardian is zero', async function () { + await this.mock.$_setProposalGuardian(ethers.ZeroAddress); + + await expect(this.helper.connect(this.proposer).cancel()) + .to.emit(this.mock, 'ProposalCanceled') + .withArgs(this.proposal.id); + }); + }); + }); + } +}); diff --git a/test/helpers/precompiles.js b/test/helpers/precompiles.js new file mode 100644 index 00000000000..fb6b7132a70 --- /dev/null +++ b/test/helpers/precompiles.js @@ -0,0 +1,12 @@ +module.exports = { + ecRecover: '0x0000000000000000000000000000000000000001', + SHA2_256: '0x0000000000000000000000000000000000000002', + RIPEMD_160: '0x0000000000000000000000000000000000000003', + identity: '0x0000000000000000000000000000000000000004', + modexp: '0x0000000000000000000000000000000000000005', + ecAdd: '0x0000000000000000000000000000000000000006', + ecMul: '0x0000000000000000000000000000000000000007', + ecPairing: '0x0000000000000000000000000000000000000008', + blake2f: '0x0000000000000000000000000000000000000009', + pointEvaluation: '0x000000000000000000000000000000000000000a', +}; diff --git a/test/helpers/time.js b/test/helpers/time.js index f6ccc3caba4..574170c137c 100644 --- a/test/helpers/time.js +++ b/test/helpers/time.js @@ -7,8 +7,11 @@ const clock = { timestamp: () => time.latest().then(ethers.toBigInt), }; const clockFromReceipt = { - blocknumber: receipt => Promise.resolve(ethers.toBigInt(receipt.blockNumber)), - timestamp: receipt => ethers.provider.getBlock(receipt.blockNumber).then(block => ethers.toBigInt(block.timestamp)), + blocknumber: receipt => Promise.resolve(receipt).then(({ blockNumber }) => ethers.toBigInt(blockNumber)), + timestamp: receipt => + Promise.resolve(receipt) + .then(({ blockNumber }) => ethers.provider.getBlock(blockNumber)) + .then(({ timestamp }) => ethers.toBigInt(timestamp)), }; const increaseBy = { blockNumber: mine, diff --git a/test/metatx/ERC2771Forwarder.t.sol b/test/metatx/ERC2771Forwarder.t.sol index e6baac6f030..605ae62097e 100644 --- a/test/metatx/ERC2771Forwarder.t.sol +++ b/test/metatx/ERC2771Forwarder.t.sol @@ -171,7 +171,7 @@ contract ERC2771ForwarderTest is Test { uint256 refundExpected = 0; uint256 nonce = _erc2771Forwarder.nonces(_signer); - // create an sign array or requests (that may fail) + // create an array of signed requests (that may fail) ERC2771Forwarder.ForwardRequestData[] memory requests = new ERC2771Forwarder.ForwardRequestData[](batchSize); for (uint256 i = 0; i < batchSize; ++i) { bool failure = (seed >> i) & 0x1 == 0x1; @@ -229,7 +229,7 @@ contract ERC2771ForwarderTest is Test { TamperType tamper = _asTamper(_tamper); uint256 nonce = _erc2771Forwarder.nonces(_signer); - // create an sign array or requests + // create an array of signed requests ERC2771Forwarder.ForwardRequestData[] memory requests = new ERC2771Forwarder.ForwardRequestData[](3); for (uint256 i = 0; i < requests.length; ++i) { requests[i] = _forgeRequestData({ @@ -252,7 +252,7 @@ contract ERC2771ForwarderTest is Test { TamperType tamper = _asTamper(_tamper); uint256 nonce = _erc2771Forwarder.nonces(_signer); - // create an sign array or requests + // create an array of signed requests ERC2771Forwarder.ForwardRequestData[] memory requests = new ERC2771Forwarder.ForwardRequestData[](3); for (uint256 i = 0; i < requests.length; ++i) { requests[i] = _forgeRequestData({ diff --git a/test/token/ERC20/utils/SafeERC20.test.js b/test/token/ERC20/utils/SafeERC20.test.js index 16b72bd6b1b..0ae94630d31 100644 --- a/test/token/ERC20/utils/SafeERC20.test.js +++ b/test/token/ERC20/utils/SafeERC20.test.js @@ -60,12 +60,24 @@ describe('SafeERC20', function () { .withArgs(this.token); }); + it('returns false on trySafeTransfer', async function () { + await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 0n)) + .to.emit(this.mock, 'return$trySafeTransfer') + .withArgs(false); + }); + it('reverts on transferFrom', async function () { await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n)) .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') .withArgs(this.token); }); + it('returns false on trySafeTransferFrom', async function () { + await expect(this.mock.$trySafeTransferFrom(this.token, this.mock, this.receiver, 0n)) + .to.emit(this.mock, 'return$trySafeTransferFrom') + .withArgs(false); + }); + it('reverts on increaseAllowance', async function () { // Call to 'token.allowance' does not return any data, resulting in a decoding error (revert without reason) await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n)).to.be.revertedWithoutReason(); @@ -94,12 +106,24 @@ describe('SafeERC20', function () { .withArgs(this.token); }); + it('returns false on trySafeTransfer', async function () { + await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 0n)) + .to.emit(this.mock, 'return$trySafeTransfer') + .withArgs(false); + }); + it('reverts on transferFrom', async function () { await expect(this.mock.$safeTransferFrom(this.token, this.mock, this.receiver, 0n)) .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') .withArgs(this.token); }); + it('returns false on trySafeTransferFrom', async function () { + await expect(this.mock.$trySafeTransferFrom(this.token, this.mock, this.receiver, 0n)) + .to.emit(this.mock, 'return$trySafeTransferFrom') + .withArgs(false); + }); + it('reverts on increaseAllowance', async function () { await expect(this.mock.$safeIncreaseAllowance(this.token, this.spender, 0n)) .to.be.revertedWithCustomError(this.mock, 'SafeERC20FailedOperation') @@ -357,11 +381,23 @@ function shouldOnlyRevertOnErrors() { .withArgs(this.mock, this.receiver, 10n); }); + it('returns true on trySafeTransfer', async function () { + await expect(this.mock.$trySafeTransfer(this.token, this.receiver, 10n)) + .to.emit(this.mock, 'return$trySafeTransfer') + .withArgs(true); + }); + it("doesn't revert on transferFrom", async function () { await expect(this.mock.$safeTransferFrom(this.token, this.owner, this.receiver, 10n)) .to.emit(this.token, 'Transfer') .withArgs(this.owner, this.receiver, 10n); }); + + it('returns true on trySafeTransferFrom', async function () { + await expect(this.mock.$trySafeTransferFrom(this.token, this.owner, this.receiver, 10n)) + .to.emit(this.mock, 'return$trySafeTransferFrom') + .withArgs(true); + }); }); describe('approvals', function () { diff --git a/test/token/ERC6909/ERC6909.behavior.js b/test/token/ERC6909/ERC6909.behavior.js new file mode 100644 index 00000000000..0d13665a263 --- /dev/null +++ b/test/token/ERC6909/ERC6909.behavior.js @@ -0,0 +1,216 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); + +const { shouldSupportInterfaces } = require('../../utils/introspection/SupportsInterface.behavior'); + +function shouldBehaveLikeERC6909() { + const firstTokenId = 1n; + const secondTokenId = 2n; + const randomTokenId = 125523n; + + const firstTokenSupply = 2000n; + const secondTokenSupply = 3000n; + const amount = 100n; + + describe('like an ERC6909', function () { + describe('balanceOf', function () { + describe("when accounts don't own tokens", function () { + it('return zero', async function () { + await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.be.equal(0n); + await expect(this.token.balanceOf(this.holder, secondTokenId)).to.eventually.be.equal(0n); + await expect(this.token.balanceOf(this.other, randomTokenId)).to.eventually.be.equal(0n); + }); + }); + + describe('when accounts own some tokens', function () { + beforeEach(async function () { + await this.token.$_mint(this.holder, firstTokenId, firstTokenSupply); + await this.token.$_mint(this.holder, secondTokenId, secondTokenSupply); + }); + + it('returns amount owned by the given address', async function () { + await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.be.equal(firstTokenSupply); + await expect(this.token.balanceOf(this.holder, secondTokenId)).to.eventually.be.equal(secondTokenSupply); + await expect(this.token.balanceOf(this.other, firstTokenId)).to.eventually.be.equal(0n); + }); + }); + }); + + describe('setOperator', function () { + it('emits an an OperatorSet event and updated the value', async function () { + await expect(this.token.connect(this.holder).setOperator(this.operator, true)) + .to.emit(this.token, 'OperatorSet') + .withArgs(this.holder, this.operator, true); + + // operator for holder + await expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.true; + + // not operator for other account + await expect(this.token.isOperator(this.other, this.operator)).to.eventually.be.false; + }); + + it('can unset the operator approval', async function () { + await this.token.connect(this.holder).setOperator(this.operator, true); + + // before + await expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.true; + + // unset + await expect(this.token.connect(this.holder).setOperator(this.operator, false)) + .to.emit(this.token, 'OperatorSet') + .withArgs(this.holder, this.operator, false); + + // after + await expect(this.token.isOperator(this.holder, this.operator)).to.eventually.be.false; + }); + + it('cannot set address(0) as an operator', async function () { + await expect(this.token.connect(this.holder).setOperator(ethers.ZeroAddress, true)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSpender') + .withArgs(ethers.ZeroAddress); + }); + }); + + describe('approve', function () { + it('emits an Approval event and updates allowance', async function () { + await expect(this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenSupply)) + .to.emit(this.token, 'Approval') + .withArgs(this.holder, this.operator, firstTokenId, firstTokenSupply); + + // approved + await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal( + firstTokenSupply, + ); + // other account is not approved + await expect(this.token.allowance(this.other, this.operator, firstTokenId)).to.eventually.be.equal(0n); + }); + + it('can unset the approval', async function () { + await expect(this.token.connect(this.holder).approve(this.operator, firstTokenId, 0n)) + .to.emit(this.token, 'Approval') + .withArgs(this.holder, this.operator, firstTokenId, 0n); + await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.be.equal(0n); + }); + + it('cannot give allowance to address(0)', async function () { + await expect(this.token.connect(this.holder).approve(ethers.ZeroAddress, firstTokenId, firstTokenSupply)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSpender') + .withArgs(ethers.ZeroAddress); + }); + }); + + describe('transfer', function () { + beforeEach(async function () { + await this.token.$_mint(this.holder, firstTokenId, firstTokenSupply); + await this.token.$_mint(this.holder, secondTokenId, secondTokenSupply); + }); + + it('transfers to the zero address are blocked', async function () { + await expect(this.token.connect(this.holder).transfer(ethers.ZeroAddress, firstTokenId, firstTokenSupply)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidReceiver') + .withArgs(ethers.ZeroAddress); + }); + + it('reverts when insufficient balance', async function () { + await expect(this.token.connect(this.holder).transfer(this.recipient, firstTokenId, firstTokenSupply + 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InsufficientBalance') + .withArgs(this.holder, firstTokenSupply, firstTokenSupply + 1n, firstTokenId); + }); + + it('emits event and transfers tokens', async function () { + await expect(this.token.connect(this.holder).transfer(this.recipient, firstTokenId, amount)) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder, this.holder, this.recipient, firstTokenId, amount); + + await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(firstTokenSupply - amount); + await expect(this.token.balanceOf(this.recipient, firstTokenId)).to.eventually.equal(amount); + }); + }); + + describe('transferFrom', function () { + beforeEach(async function () { + await this.token.$_mint(this.holder, firstTokenId, firstTokenSupply); + await this.token.$_mint(this.holder, secondTokenId, secondTokenSupply); + }); + + it('transfer from self', async function () { + await expect(this.token.connect(this.holder).transferFrom(this.holder, this.recipient, firstTokenId, amount)) + .to.emit(this.token, 'Transfer') + .withArgs(this.holder, this.holder, this.recipient, firstTokenId, amount); + + await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(firstTokenSupply - amount); + await expect(this.token.balanceOf(this.recipient, firstTokenId)).to.eventually.equal(amount); + }); + + describe('with approval', async function () { + beforeEach(async function () { + await this.token.connect(this.holder).approve(this.operator, firstTokenId, amount); + }); + + it('reverts when insufficient allowance', async function () { + await expect( + this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount + 1n), + ) + .to.be.revertedWithCustomError(this.token, 'ERC6909InsufficientAllowance') + .withArgs(this.operator, amount, amount + 1n, firstTokenId); + }); + + it('should emit transfer event and update approval (without an Approval event)', async function () { + await expect( + this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount - 1n), + ) + .to.emit(this.token, 'Transfer') + .withArgs(this.operator, this.holder, this.recipient, firstTokenId, amount - 1n) + .to.not.emit(this.token, 'Approval'); + + await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal(1n); + }); + + it("shouldn't reduce allowance when infinite", async function () { + await this.token.connect(this.holder).approve(this.operator, firstTokenId, ethers.MaxUint256); + + await this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount); + + await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal( + ethers.MaxUint256, + ); + }); + }); + }); + + describe('with operator approval', function () { + beforeEach(async function () { + await this.token.connect(this.holder).setOperator(this.operator, true); + await this.token.$_mint(this.holder, firstTokenId, firstTokenSupply); + }); + + it('operator can transfer', async function () { + await expect(this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount)) + .to.emit(this.token, 'Transfer') + .withArgs(this.operator, this.holder, this.recipient, firstTokenId, amount); + + await expect(this.token.balanceOf(this.holder, firstTokenId)).to.eventually.equal(firstTokenSupply - amount); + await expect(this.token.balanceOf(this.recipient, firstTokenId)).to.eventually.equal(amount); + }); + + it('operator transfer does not reduce allowance', async function () { + // Also give allowance + await this.token.connect(this.holder).approve(this.operator, firstTokenId, firstTokenSupply); + + await expect(this.token.connect(this.operator).transferFrom(this.holder, this.recipient, firstTokenId, amount)) + .to.emit(this.token, 'Transfer') + .withArgs(this.operator, this.holder, this.recipient, firstTokenId, amount); + + await expect(this.token.allowance(this.holder, this.operator, firstTokenId)).to.eventually.equal( + firstTokenSupply, + ); + }); + }); + + shouldSupportInterfaces(['ERC6909']); + }); +} + +module.exports = { + shouldBehaveLikeERC6909, +}; diff --git a/test/token/ERC6909/ERC6909.test.js b/test/token/ERC6909/ERC6909.test.js new file mode 100644 index 00000000000..fa41145aa69 --- /dev/null +++ b/test/token/ERC6909/ERC6909.test.js @@ -0,0 +1,104 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { shouldBehaveLikeERC6909 } = require('./ERC6909.behavior'); + +async function fixture() { + const [holder, operator, recipient, other] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC6909'); + return { token, holder, operator, recipient, other }; +} + +describe('ERC6909', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeERC6909(); + + describe('internal functions', function () { + const tokenId = 1990n; + const mintValue = 9001n; + const burnValue = 3000n; + + describe('_mint', function () { + it('reverts with a zero destination address', async function () { + await expect(this.token.$_mint(ethers.ZeroAddress, tokenId, mintValue)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidReceiver') + .withArgs(ethers.ZeroAddress); + }); + + describe('with minted tokens', function () { + beforeEach(async function () { + this.tx = await this.token.connect(this.operator).$_mint(this.holder, tokenId, mintValue); + }); + + it('emits a Transfer event from 0 address', async function () { + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.operator, ethers.ZeroAddress, this.holder, tokenId, mintValue); + }); + + it('credits the minted token value', async function () { + await expect(this.token.balanceOf(this.holder, tokenId)).to.eventually.be.equal(mintValue); + }); + }); + }); + + describe('_transfer', function () { + it('reverts when transferring from the zero address', async function () { + await expect(this.token.$_transfer(ethers.ZeroAddress, this.holder, 1n, 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSender') + .withArgs(ethers.ZeroAddress); + }); + + it('reverts when transferring to the zero address', async function () { + await expect(this.token.$_transfer(this.holder, ethers.ZeroAddress, 1n, 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidReceiver') + .withArgs(ethers.ZeroAddress); + }); + }); + + describe('_burn', function () { + it('reverts with a zero from address', async function () { + await expect(this.token.$_burn(ethers.ZeroAddress, tokenId, burnValue)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidSender') + .withArgs(ethers.ZeroAddress); + }); + + describe('with burned tokens', function () { + beforeEach(async function () { + await this.token.connect(this.operator).$_mint(this.holder, tokenId, mintValue); + this.tx = await this.token.connect(this.operator).$_burn(this.holder, tokenId, burnValue); + }); + + it('emits a Transfer event to 0 address', async function () { + await expect(this.tx) + .to.emit(this.token, 'Transfer') + .withArgs(this.operator, this.holder, ethers.ZeroAddress, tokenId, burnValue); + }); + + it('debits the burned token value', async function () { + await expect(this.token.balanceOf(this.holder, tokenId)).to.eventually.be.equal(mintValue - burnValue); + }); + }); + }); + + describe('_approve', function () { + it('reverts when the owner is the zero address', async function () { + await expect(this.token.$_approve(ethers.ZeroAddress, this.recipient, 1n, 1n)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidApprover') + .withArgs(ethers.ZeroAddress); + }); + }); + + describe('_setOperator', function () { + it('reverts when the owner is the zero address', async function () { + await expect(this.token.$_setOperator(ethers.ZeroAddress, this.operator, true)) + .to.be.revertedWithCustomError(this.token, 'ERC6909InvalidApprover') + .withArgs(ethers.ZeroAddress); + }); + }); + }); +}); diff --git a/test/token/ERC6909/extensions/ERC6909ContentURI.test.js b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js new file mode 100644 index 00000000000..3597eb78e8a --- /dev/null +++ b/test/token/ERC6909/extensions/ERC6909ContentURI.test.js @@ -0,0 +1,49 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const token = await ethers.deployContract('$ERC6909ContentURI'); + return { token }; +} + +describe('ERC6909ContentURI', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('contractURI', function () { + it('is empty string be default', async function () { + await expect(this.token.contractURI()).to.eventually.equal(''); + }); + + it('is settable by internal setter', async function () { + await this.token.$_setContractURI('https://example.com'); + await expect(this.token.contractURI()).to.eventually.equal('https://example.com'); + }); + + it('emits an event when set', async function () { + await expect(this.token.$_setContractURI('https://example.com')).to.emit(this.token, 'ContractURIUpdated'); + }); + }); + + describe('tokenURI', function () { + it('is empty string be default', async function () { + await expect(this.token.tokenURI(1n)).to.eventually.equal(''); + }); + + it('can be set by dedicated setter', async function () { + await this.token.$_setTokenURI(1n, 'https://example.com/1'); + await expect(this.token.tokenURI(1n)).to.eventually.equal('https://example.com/1'); + + // Only set for the specified token ID + await expect(this.token.tokenURI(2n)).to.eventually.equal(''); + }); + + it('emits an event when set', async function () { + await expect(this.token.$_setTokenURI(1n, 'https://example.com/1')) + .to.emit(this.token, 'URI') + .withArgs('https://example.com/1', 1n); + }); + }); +}); diff --git a/test/token/ERC6909/extensions/ERC6909Metadata.test.js b/test/token/ERC6909/extensions/ERC6909Metadata.test.js new file mode 100644 index 00000000000..e6d3dd9f32d --- /dev/null +++ b/test/token/ERC6909/extensions/ERC6909Metadata.test.js @@ -0,0 +1,58 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const token = await ethers.deployContract('$ERC6909Metadata'); + return { token }; +} + +describe('ERC6909Metadata', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('name', function () { + it('is empty string be default', async function () { + await expect(this.token.name(1n)).to.eventually.equal(''); + }); + + it('can be set by dedicated setter', async function () { + await expect(this.token.$_setName(1n, 'My Token')) + .to.emit(this.token, 'ERC6909NameUpdated') + .withArgs(1n, 'My Token'); + await expect(this.token.name(1n)).to.eventually.equal('My Token'); + + // Only set for the specified token ID + await expect(this.token.name(2n)).to.eventually.equal(''); + }); + }); + + describe('symbol', function () { + it('is empty string be default', async function () { + await expect(this.token.symbol(1n)).to.eventually.equal(''); + }); + + it('can be set by dedicated setter', async function () { + await expect(this.token.$_setSymbol(1n, 'MTK')).to.emit(this.token, 'ERC6909SymbolUpdated').withArgs(1n, 'MTK'); + await expect(this.token.symbol(1n)).to.eventually.equal('MTK'); + + // Only set for the specified token ID + await expect(this.token.symbol(2n)).to.eventually.equal(''); + }); + }); + + describe('decimals', function () { + it('is 0 by default', async function () { + await expect(this.token.decimals(1n)).to.eventually.equal(0); + }); + + it('can be set by dedicated setter', async function () { + await expect(this.token.$_setDecimals(1n, 18)).to.emit(this.token, 'ERC6909DecimalsUpdated').withArgs(1n, 18); + await expect(this.token.decimals(1n)).to.eventually.equal(18); + + // Only set for the specified token ID + await expect(this.token.decimals(2n)).to.eventually.equal(0); + }); + }); +}); diff --git a/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js b/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js new file mode 100644 index 00000000000..0b8b053d0a3 --- /dev/null +++ b/test/token/ERC6909/extensions/ERC6909TokenSupply.test.js @@ -0,0 +1,53 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { shouldBehaveLikeERC6909 } = require('../ERC6909.behavior'); + +async function fixture() { + const [holder, operator, recipient, other] = await ethers.getSigners(); + const token = await ethers.deployContract('$ERC6909TokenSupply'); + return { token, holder, operator, recipient, other }; +} + +describe('ERC6909TokenSupply', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeERC6909(); + + describe('totalSupply', function () { + it('is zero before any mint', async function () { + await expect(this.token.totalSupply(1n)).to.eventually.be.equal(0n); + }); + + it('minting tokens increases the total supply', async function () { + await this.token.$_mint(this.holder, 1n, 17n); + await expect(this.token.totalSupply(1n)).to.eventually.be.equal(17n); + }); + + describe('with tokens minted', function () { + const supply = 1000n; + + beforeEach(async function () { + await this.token.$_mint(this.holder, 1n, supply); + }); + + it('burning tokens decreases the total supply', async function () { + await this.token.$_burn(this.holder, 1n, 17n); + await expect(this.token.totalSupply(1n)).to.eventually.be.equal(supply - 17n); + }); + + it('supply unaffected by transfers', async function () { + await this.token.$_transfer(this.holder, this.recipient, 1n, 42n); + await expect(this.token.totalSupply(1n)).to.eventually.be.equal(supply); + }); + + it('supply unaffected by no-op', async function () { + await this.token.$_update(ethers.ZeroAddress, ethers.ZeroAddress, 1n, 42n); + await expect(this.token.totalSupply(1n)).to.eventually.be.equal(supply); + }); + }); + }); +}); diff --git a/test/token/ERC721/ERC721.behavior.js b/test/token/ERC721/ERC721.behavior.js index b9dd80d6d1a..433ffe00554 100644 --- a/test/token/ERC721/ERC721.behavior.js +++ b/test/token/ERC721/ERC721.behavior.js @@ -10,7 +10,6 @@ const firstTokenId = 5042n; const secondTokenId = 79217n; const nonExistentTokenId = 13n; const fourthTokenId = 4n; -const baseURI = 'https://api.example.com/v1/'; const RECEIVER_MAGIC_VALUE = '0x150b7a02'; @@ -936,31 +935,6 @@ function shouldBehaveLikeERC721Metadata(name, symbol) { .to.be.revertedWithCustomError(this.token, 'ERC721NonexistentToken') .withArgs(nonExistentTokenId); }); - - describe('base URI', function () { - beforeEach(function () { - if (!this.token.interface.hasFunction('setBaseURI')) { - this.skip(); - } - }); - - it('base URI can be set', async function () { - await this.token.setBaseURI(baseURI); - expect(await this.token.baseURI()).to.equal(baseURI); - }); - - it('base URI is added as a prefix to the token URI', async function () { - await this.token.setBaseURI(baseURI); - expect(await this.token.tokenURI(firstTokenId)).to.equal(baseURI + firstTokenId.toString()); - }); - - it('token URI can be changed by changing the base URI', async function () { - await this.token.setBaseURI(baseURI); - const newBaseURI = 'https://api.example.com/v2/'; - await this.token.setBaseURI(newBaseURI); - expect(await this.token.tokenURI(firstTokenId)).to.equal(newBaseURI + firstTokenId.toString()); - }); - }); }); }); } diff --git a/test/utils/Calldata.test.js b/test/utils/Calldata.test.js new file mode 100644 index 00000000000..7e9d3d47813 --- /dev/null +++ b/test/utils/Calldata.test.js @@ -0,0 +1,22 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const mock = await ethers.deployContract('$Calldata'); + return { mock }; +} + +describe('Calldata utilities', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + it('emptyBytes', async function () { + await expect(this.mock.$emptyBytes()).to.eventually.equal('0x'); + }); + + it('emptyString', async function () { + await expect(this.mock.$emptyString()).to.eventually.equal(''); + }); +}); diff --git a/test/utils/cryptography/SignatureChecker.test.js b/test/utils/cryptography/SignatureChecker.test.js index e6a08491a51..2b14f2f4c14 100644 --- a/test/utils/cryptography/SignatureChecker.test.js +++ b/test/utils/cryptography/SignatureChecker.test.js @@ -2,6 +2,8 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const precompile = require('../../helpers/precompiles'); + const TEST_MESSAGE = ethers.id('OpenZeppelin'); const TEST_MESSAGE_HASH = ethers.hashMessage(TEST_MESSAGE); @@ -25,15 +27,18 @@ describe('SignatureChecker (ERC1271)', function () { describe('EOA account', function () { it('with matching signer and signature', async function () { - expect(await this.mock.$isValidSignatureNow(this.signer, TEST_MESSAGE_HASH, this.signature)).to.be.true; + await expect(this.mock.$isValidSignatureNow(this.signer, TEST_MESSAGE_HASH, this.signature)).to.eventually.be + .true; }); it('with invalid signer', async function () { - expect(await this.mock.$isValidSignatureNow(this.other, TEST_MESSAGE_HASH, this.signature)).to.be.false; + await expect(this.mock.$isValidSignatureNow(this.other, TEST_MESSAGE_HASH, this.signature)).to.eventually.be + .false; }); it('with invalid signature', async function () { - expect(await this.mock.$isValidSignatureNow(this.signer, WRONG_MESSAGE_HASH, this.signature)).to.be.false; + await expect(this.mock.$isValidSignatureNow(this.signer, WRONG_MESSAGE_HASH, this.signature)).to.eventually.be + .false; }); }); @@ -41,19 +46,28 @@ describe('SignatureChecker (ERC1271)', function () { for (const fn of ['isValidERC1271SignatureNow', 'isValidSignatureNow']) { describe(fn, function () { it('with matching signer and signature', async function () { - expect(await this.mock.getFunction(`$${fn}`)(this.wallet, TEST_MESSAGE_HASH, this.signature)).to.be.true; + await expect(this.mock.getFunction(`$${fn}`)(this.wallet, TEST_MESSAGE_HASH, this.signature)).to.eventually.be + .true; }); it('with invalid signer', async function () { - expect(await this.mock.getFunction(`$${fn}`)(this.mock, TEST_MESSAGE_HASH, this.signature)).to.be.false; + await expect(this.mock.getFunction(`$${fn}`)(this.mock, TEST_MESSAGE_HASH, this.signature)).to.eventually.be + .false; + }); + + it('with identity precompile', async function () { + await expect(this.mock.getFunction(`$${fn}`)(precompile.identity, TEST_MESSAGE_HASH, this.signature)).to + .eventually.be.false; }); it('with invalid signature', async function () { - expect(await this.mock.getFunction(`$${fn}`)(this.wallet, WRONG_MESSAGE_HASH, this.signature)).to.be.false; + await expect(this.mock.getFunction(`$${fn}`)(this.wallet, WRONG_MESSAGE_HASH, this.signature)).to.eventually + .be.false; }); it('with malicious wallet', async function () { - expect(await this.mock.getFunction(`$${fn}`)(this.malicious, TEST_MESSAGE_HASH, this.signature)).to.be.false; + await expect(this.mock.getFunction(`$${fn}`)(this.malicious, TEST_MESSAGE_HASH, this.signature)).to.eventually + .be.false; }); }); } diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index bfcddee7a5e..6e716d1304b 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -90,6 +90,15 @@ const SIGNATURES = { Governor: GOVERNOR_INTERFACE, Governor_5_3: GOVERNOR_INTERFACE.concat('getProposalId(address[],uint256[],bytes[],bytes32)'), ERC2981: ['royaltyInfo(uint256,uint256)'], + ERC6909: [ + 'balanceOf(address,uint256)', + 'allowance(address,address,uint256)', + 'isOperator(address,address)', + 'transfer(address,uint256,uint256)', + 'transferFrom(address,address,uint256,uint256)', + 'approve(address,uint256,uint256)', + 'setOperator(address,bool)', + ], }; const INTERFACE_IDS = mapValues(SIGNATURES, interfaceId); diff --git a/test/utils/structs/EnumerableMap.behavior.js b/test/utils/structs/EnumerableMap.behavior.js index 37da41795dc..c80eec934ba 100644 --- a/test/utils/structs/EnumerableMap.behavior.js +++ b/test/utils/structs/EnumerableMap.behavior.js @@ -117,6 +117,49 @@ function shouldBehaveLikeMap() { }); }); + describe('clear', function () { + it('clears a single entry', async function () { + await this.methods.set(this.keyA, this.valueA); + + await this.methods.clear(); + + expect(await this.methods.contains(this.keyA)).to.be.false; + await expectMembersMatch(this.methods, [], []); + }); + + it('clears multiple entries', async function () { + await this.methods.set(this.keyA, this.valueA); + await this.methods.set(this.keyB, this.valueB); + await this.methods.set(this.keyC, this.valueC); + + await this.methods.clear(); + + expect(await this.methods.contains(this.keyA)).to.be.false; + expect(await this.methods.contains(this.keyB)).to.be.false; + expect(await this.methods.contains(this.keyC)).to.be.false; + await expectMembersMatch(this.methods, [], []); + }); + + it('does not revert on empty map', async function () { + await this.methods.clear(); + }); + + it('clear then add entry', async function () { + await this.methods.set(this.keyA, this.valueA); + await this.methods.set(this.keyB, this.valueB); + await this.methods.set(this.keyC, this.valueC); + + await this.methods.clear(); + + await this.methods.set(this.keyA, this.valueA); + + expect(await this.methods.contains(this.keyA)).to.be.true; + expect(await this.methods.contains(this.keyB)).to.be.false; + expect(await this.methods.contains(this.keyC)).to.be.false; + await expectMembersMatch(this.methods, [this.keyA], [this.valueA]); + }); + }); + describe('read', function () { beforeEach(async function () { await this.methods.set(this.keyA, this.valueA); diff --git a/test/utils/structs/EnumerableMap.test.js b/test/utils/structs/EnumerableMap.test.js index 5362e873aa3..cb4b77a651f 100644 --- a/test/utils/structs/EnumerableMap.test.js +++ b/test/utils/structs/EnumerableMap.test.js @@ -26,6 +26,7 @@ async function fixture() { get: `$get_EnumerableMap_${name}(uint256,${keyType})`, tryGet: `$tryGet_EnumerableMap_${name}(uint256,${keyType})`, remove: `$remove_EnumerableMap_${name}(uint256,${keyType})`, + clear: `$clear_EnumerableMap_${name}(uint256)`, length: `$length_EnumerableMap_${name}(uint256)`, at: `$at_EnumerableMap_${name}(uint256,uint256)`, contains: `$contains_EnumerableMap_${name}(uint256,${keyType})`, diff --git a/test/utils/structs/EnumerableSet.behavior.js b/test/utils/structs/EnumerableSet.behavior.js index d3d4f26d58b..fb932680cdd 100644 --- a/test/utils/structs/EnumerableSet.behavior.js +++ b/test/utils/structs/EnumerableSet.behavior.js @@ -47,7 +47,7 @@ function shouldBehaveLikeSet() { it('retrieves existing element', async function () { await this.methods.add(this.valueA); - expect(await this.methods.at(0)).to.equal(this.valueA); + expect(await this.methods.at(0)).to.deep.equal(this.valueA); }); }); @@ -109,6 +109,49 @@ function shouldBehaveLikeSet() { expect(await this.methods.contains(this.valueB)).to.be.false; }); }); + + describe('clear', function () { + it('clears a single value', async function () { + await this.methods.add(this.valueA); + + await this.methods.clear(); + + expect(await this.methods.contains(this.valueA)).to.be.false; + await expectMembersMatch(this.methods, []); + }); + + it('clears multiple values', async function () { + await this.methods.add(this.valueA); + await this.methods.add(this.valueB); + await this.methods.add(this.valueC); + + await this.methods.clear(); + + expect(await this.methods.contains(this.valueA)).to.be.false; + expect(await this.methods.contains(this.valueB)).to.be.false; + expect(await this.methods.contains(this.valueC)).to.be.false; + await expectMembersMatch(this.methods, []); + }); + + it('does not revert on empty set', async function () { + await this.methods.clear(); + }); + + it('clear then add value', async function () { + await this.methods.add(this.valueA); + await this.methods.add(this.valueB); + await this.methods.add(this.valueC); + + await this.methods.clear(); + + await this.methods.add(this.valueA); + + expect(await this.methods.contains(this.valueA)).to.be.true; + expect(await this.methods.contains(this.valueB)).to.be.false; + expect(await this.methods.contains(this.valueC)).to.be.false; + await expectMembersMatch(this.methods, [this.valueA]); + }); + }); } module.exports = { diff --git a/test/utils/structs/EnumerableSet.test.js b/test/utils/structs/EnumerableSet.test.js index 66d666058ea..f01810013a0 100644 --- a/test/utils/structs/EnumerableSet.test.js +++ b/test/utils/structs/EnumerableSet.test.js @@ -20,21 +20,25 @@ async function fixture() { const mock = await ethers.deployContract('$EnumerableSet'); const env = Object.fromEntries( - TYPES.map(({ name, type }) => [ + TYPES.map(({ name, type, base, size }) => [ type, { - values: Array.from({ length: 3 }, generators[type]), + values: Array.from( + { length: 3 }, + size ? () => Array.from({ length: size }, generators[base]) : generators[type], + ), methods: getMethods(mock, { add: `$add(uint256,${type})`, remove: `$remove(uint256,${type})`, + clear: `$clear_EnumerableSet_${name}(uint256)`, contains: `$contains(uint256,${type})`, length: `$length_EnumerableSet_${name}(uint256)`, at: `$at_EnumerableSet_${name}(uint256,uint256)`, values: `$values_EnumerableSet_${name}(uint256)`, }), events: { - addReturn: `return$add_EnumerableSet_${name}_${type}`, - removeReturn: `return$remove_EnumerableSet_${name}_${type}`, + addReturn: `return$add_EnumerableSet_${name}_${type.replace(/[[\]]/g, '_')}`, + removeReturn: `return$remove_EnumerableSet_${name}_${type.replace(/[[\]]/g, '_')}`, }, }, ]),