diff --git a/.changeset/chilled-walls-develop.md b/.changeset/chilled-walls-develop.md deleted file mode 100644 index 4108feb612d..00000000000 --- a/.changeset/chilled-walls-develop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Clones`: Add version of `clone` and `cloneDeterministic` that support sending value at creation. diff --git a/.changeset/chilly-humans-warn.md b/.changeset/chilly-humans-warn.md deleted file mode 100644 index 1301dfec416..00000000000 --- a/.changeset/chilly-humans-warn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`ProxyAdmin`: Fixed documentation for `UPGRADE_INTERFACE_VERSION` getter. diff --git a/.changeset/cold-cheetahs-check.md b/.changeset/cold-cheetahs-check.md deleted file mode 100644 index 0697dcdf7b4..00000000000 --- a/.changeset/cold-cheetahs-check.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`CircularBuffer`: Add a data structure that stores the last `N` values pushed to it. diff --git a/.changeset/cool-mangos-compare.md b/.changeset/cool-mangos-compare.md deleted file mode 100644 index 470ee089456..00000000000 --- a/.changeset/cool-mangos-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Math`: add an `invMod` function to get the modular multiplicative inverse of a number in Z/nZ. diff --git a/.changeset/curvy-crabs-repeat.md b/.changeset/curvy-crabs-repeat.md deleted file mode 100644 index db3ef275bb3..00000000000 --- a/.changeset/curvy-crabs-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`RSA`: Library to verify signatures according to RFC 8017 Signature Verification Operation diff --git a/.changeset/dirty-cobras-smile.md b/.changeset/dirty-cobras-smile.md deleted file mode 100644 index d71194cfc21..00000000000 --- a/.changeset/dirty-cobras-smile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Arrays`: add a `sort` functions for `address[]`, `bytes32[]` and `uint256[]` memory arrays. diff --git a/.changeset/eight-eyes-burn.md b/.changeset/eight-eyes-burn.md deleted file mode 100644 index 908c90c7bbf..00000000000 --- a/.changeset/eight-eyes-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`GovernorCountingFractional`: Add a governor counting module that allows distributing voting power amongst 3 options (For, Against, Abstain). diff --git a/.changeset/eighty-hounds-promise.md b/.changeset/eighty-hounds-promise.md new file mode 100644 index 00000000000..3727a6515f0 --- /dev/null +++ b/.changeset/eighty-hounds-promise.md @@ -0,0 +1,5 @@ +--- +'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/eleven-planets-relax.md b/.changeset/eleven-planets-relax.md deleted file mode 100644 index a1f1bbf1c4e..00000000000 --- a/.changeset/eleven-planets-relax.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`TransparentUpgradeableProxy`: Make internal `_proxyAdmin()` getter have `view` visibility. diff --git a/.changeset/flat-turtles-repeat.md b/.changeset/flat-turtles-repeat.md deleted file mode 100644 index 6b627201ac9..00000000000 --- a/.changeset/flat-turtles-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Arrays`: deprecate `findUpperBound` in favor of the new `lowerBound`. diff --git a/.changeset/fluffy-buses-jump.md b/.changeset/fluffy-buses-jump.md deleted file mode 100644 index 0525a4d8e43..00000000000 --- a/.changeset/fluffy-buses-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Comparator`: A library of comparator functions, useful for customizing the behavior of the Heap structure. diff --git a/.changeset/fluffy-steaks-exist.md b/.changeset/fluffy-steaks-exist.md deleted file mode 100644 index b625e243481..00000000000 --- a/.changeset/fluffy-steaks-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`Create2`, `Clones`: Mask `computeAddress` and `cloneDeterministic` outputs to produce a clean value for an `address` type (i.e. only use 20 bytes) diff --git a/.changeset/forty-dodos-visit.md b/.changeset/forty-dodos-visit.md deleted file mode 100644 index 7d5ae747335..00000000000 --- a/.changeset/forty-dodos-visit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Strings`: Added a utility function for converting an address to checksummed string. diff --git a/.changeset/friendly-nails-push.md b/.changeset/friendly-nails-push.md deleted file mode 100644 index 157bf05561a..00000000000 --- a/.changeset/friendly-nails-push.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC1363`: Add implementation of the token payable standard allowing execution of contract code after transfers and approvals. diff --git a/.changeset/gentle-bulldogs-turn.md b/.changeset/gentle-bulldogs-turn.md deleted file mode 100644 index 12bc87a2dfb..00000000000 --- a/.changeset/gentle-bulldogs-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`DoubleEndedQueue`: Custom errors replaced with native panic codes. diff --git a/.changeset/gorgeous-badgers-vanish.md b/.changeset/gorgeous-badgers-vanish.md deleted file mode 100644 index ce75ed6ebae..00000000000 --- a/.changeset/gorgeous-badgers-vanish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`SlotDerivation`: Add a library of methods for derivating common storage slots. diff --git a/.changeset/great-lions-hear.md b/.changeset/great-lions-hear.md new file mode 100644 index 00000000000..2be5de253a8 --- /dev/null +++ b/.changeset/great-lions-hear.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`VotesExtended`: Create an extension of `Votes` which checkpoints balances and delegates. diff --git a/.changeset/great-pianos-work.md b/.changeset/great-pianos-work.md deleted file mode 100644 index da54483e47e..00000000000 --- a/.changeset/great-pianos-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Heap`: A data structure that implements a heap-based priority queue. diff --git a/.changeset/healthy-books-shout.md b/.changeset/healthy-books-shout.md new file mode 100644 index 00000000000..274e7a48868 --- /dev/null +++ b/.changeset/healthy-books-shout.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`CAIP2` and `CAIP10`: Add libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers. diff --git a/.changeset/heavy-baboons-give.md b/.changeset/heavy-baboons-give.md deleted file mode 100644 index 5852748f81c..00000000000 --- a/.changeset/heavy-baboons-give.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Packing`: Added a new utility for packing, extracting and replacing bytesXX values. diff --git a/.changeset/hot-shrimps-wait.md b/.changeset/hot-shrimps-wait.md new file mode 100644 index 00000000000..e4e96a981ad --- /dev/null +++ b/.changeset/hot-shrimps-wait.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Packing`: Add variants for packing `bytes10` and `bytes22` diff --git a/.changeset/kind-planets-cough.md b/.changeset/kind-planets-cough.md deleted file mode 100644 index 988e24c4ad2..00000000000 --- a/.changeset/kind-planets-cough.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`StorageSlot`: Add primitives for operating on the transient storage space using a typed-slot representation. diff --git a/.changeset/light-news-listen.md b/.changeset/light-news-listen.md deleted file mode 100644 index 1572d908139..00000000000 --- a/.changeset/light-news-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`AccessManager`: Allow the `onlyAuthorized` modifier to restrict functions added to the manager. diff --git a/.changeset/lovely-dodos-lay.md b/.changeset/lovely-dodos-lay.md new file mode 100644 index 00000000000..da225132630 --- /dev/null +++ b/.changeset/lovely-dodos-lay.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`NoncesKeyed`: Add a variant of `Nonces` that implements the ERC-4337 entrypoint nonce system. diff --git a/.changeset/lucky-crews-eat.md b/.changeset/lucky-crews-eat.md deleted file mode 100644 index 48592b5eaf1..00000000000 --- a/.changeset/lucky-crews-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Votes`: Set `_moveDelegateVotes` visibility to internal instead of private. diff --git a/.changeset/nervous-eyes-teach.md b/.changeset/nervous-eyes-teach.md deleted file mode 100644 index f85bc66d8f7..00000000000 --- a/.changeset/nervous-eyes-teach.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Create2`: Bubbles up returndata from a deployed contract that reverted during construction. diff --git a/.changeset/nervous-pans-grow.md b/.changeset/nervous-pans-grow.md deleted file mode 100644 index b86a075c678..00000000000 --- a/.changeset/nervous-pans-grow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': patch ---- - -`SafeCast`: Add `toUint(bool)` for operating on `bool` values as `uint256`. diff --git a/.changeset/nice-paws-pull.md b/.changeset/nice-paws-pull.md deleted file mode 100644 index 11f48d51f82..00000000000 --- a/.changeset/nice-paws-pull.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`SafeERC20`: Add "relaxed" function for interacting with ERC-1363 functions in a way that is compatible with EOAs. diff --git a/.changeset/odd-files-protect.md b/.changeset/odd-files-protect.md deleted file mode 100644 index 8b334acfd11..00000000000 --- a/.changeset/odd-files-protect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Hashes`: A library with commonly used hash functions. diff --git a/.changeset/odd-lobsters-wash.md b/.changeset/odd-lobsters-wash.md deleted file mode 100644 index 578f7a42ea7..00000000000 --- a/.changeset/odd-lobsters-wash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`P256`: Library for verification and public key recovery of P256 (aka secp256r1) signatures. diff --git a/.changeset/pink-wasps-hammer.md b/.changeset/pink-wasps-hammer.md new file mode 100644 index 00000000000..69f63d3ca19 --- /dev/null +++ b/.changeset/pink-wasps-hammer.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`GovernorCountingOverridable`: Add a governor counting module that enables token holders to override the vote of their delegate. diff --git a/.changeset/poor-chefs-cheat.md b/.changeset/poor-chefs-cheat.md deleted file mode 100644 index 39db3d5139c..00000000000 --- a/.changeset/poor-chefs-cheat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC721Utils` and `ERC1155Utils`: Add reusable libraries with functions to perform acceptance checks on `IERC721Receiver` and `IERC1155Receiver` implementers. diff --git a/.changeset/proud-planes-arrive.md b/.changeset/proud-planes-arrive.md new file mode 100644 index 00000000000..60c831bd690 --- /dev/null +++ b/.changeset/proud-planes-arrive.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Bytes`: Add a library of common operations that operate on `bytes` objects. diff --git a/.changeset/serious-carrots-provide.md b/.changeset/serious-carrots-provide.md deleted file mode 100644 index 60a16580d8a..00000000000 --- a/.changeset/serious-carrots-provide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ERC20TemporaryApproval`: Add an ERC-20 extension that implements temporary approval using transient storage, based on ERC7674 (draft). diff --git a/.changeset/seven-donkeys-tap.md b/.changeset/seven-donkeys-tap.md new file mode 100644 index 00000000000..25d2305b9b8 --- /dev/null +++ b/.changeset/seven-donkeys-tap.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +Update some pragma directives to ensure that all file requirements match that of the files they import. diff --git a/.changeset/seven-insects-taste.md b/.changeset/seven-insects-taste.md new file mode 100644 index 00000000000..bfa8737d7de --- /dev/null +++ b/.changeset/seven-insects-taste.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': patch +--- + +`ERC7579Utils`: Add ABI decoding checks on calldata bounds within `decodeBatch` diff --git a/.changeset/shiny-poets-whisper.md b/.changeset/shiny-poets-whisper.md deleted file mode 100644 index 92497033acf..00000000000 --- a/.changeset/shiny-poets-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Math`: Add `modExp` function that exposes the `EIP-198` precompile. Includes `uint256` and `bytes memory` versions. diff --git a/.changeset/silver-swans-promise.md b/.changeset/silver-swans-promise.md deleted file mode 100644 index 1d2ff2e9ef8..00000000000 --- a/.changeset/silver-swans-promise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Panic`: Add a library for reverting with panic codes. diff --git a/.changeset/small-seahorses-bathe.md b/.changeset/small-seahorses-bathe.md new file mode 100644 index 00000000000..7b5ec794f38 --- /dev/null +++ b/.changeset/small-seahorses-bathe.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC7579Utils`: Add a reusable library to interact with ERC-7579 modular accounts diff --git a/.changeset/smart-bugs-switch.md b/.changeset/smart-bugs-switch.md deleted file mode 100644 index 8a001ae58a1..00000000000 --- a/.changeset/smart-bugs-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Math`: Custom errors replaced with native panic codes. diff --git a/.changeset/spotty-falcons-explain.md b/.changeset/spotty-falcons-explain.md deleted file mode 100644 index 28cb95190c1..00000000000 --- a/.changeset/spotty-falcons-explain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Math`, `SignedMath`: Add a branchless `ternary` function that computes`cond ? a : b` in constant gas cost. diff --git a/.changeset/spotty-queens-own.md b/.changeset/spotty-queens-own.md deleted file mode 100644 index 98fb2fbc0ed..00000000000 --- a/.changeset/spotty-queens-own.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`MerkleProof`: Add variations of `verify`, `processProof`, `multiProofVerify` and `processMultiProof` (and equivalent calldata version) with support for custom hashing functions. diff --git a/.changeset/strong-singers-talk.md b/.changeset/strong-singers-talk.md deleted file mode 100644 index 7897980cbae..00000000000 --- a/.changeset/strong-singers-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Errors`: New library of common custom errors. diff --git a/.changeset/thick-pumpkins-report.md b/.changeset/thick-pumpkins-report.md deleted file mode 100644 index f17a208950c..00000000000 --- a/.changeset/thick-pumpkins-report.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Arrays`: add new functions `lowerBound`, `upperBound`, `lowerBoundMemory` and `upperBoundMemory` for lookups in sorted arrays with potential duplicates. diff --git a/.changeset/thin-walls-drop.md b/.changeset/thin-walls-drop.md deleted file mode 100644 index 80260020256..00000000000 --- a/.changeset/thin-walls-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`AccessManager`, `VestingWallet`, `TimelockController` and `ERC2771Forwarder`: Added a public `initializer` function in their corresponding upgradeable variants. diff --git a/.changeset/twenty-feet-grin.md b/.changeset/twenty-feet-grin.md deleted file mode 100644 index 69b4fe63b2e..00000000000 --- a/.changeset/twenty-feet-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`Base64`: Add `encodeURL` following section 5 of RFC4648 for URL encoding diff --git a/.changeset/violet-moons-tell.md b/.changeset/violet-moons-tell.md deleted file mode 100644 index be215e1934b..00000000000 --- a/.changeset/violet-moons-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`AccessControlEnumerable`: Add a `getRoleMembers` method to return all accounts that have `role`. diff --git a/.changeset/warm-sheep-cover.md b/.changeset/warm-sheep-cover.md deleted file mode 100644 index f0a2ebaa256..00000000000 --- a/.changeset/warm-sheep-cover.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`MerkleTree`: A data structure that allows inserting elements into a merkle tree and updating its root hash. diff --git a/.changeset/weak-roses-bathe.md b/.changeset/weak-roses-bathe.md new file mode 100644 index 00000000000..416b2e746d3 --- /dev/null +++ b/.changeset/weak-roses-bathe.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`ERC4337Utils`: Add a reusable library to manipulate user operations and interact with ERC-4337 contracts diff --git a/.changeset/wise-bobcats-speak.md b/.changeset/wise-bobcats-speak.md deleted file mode 100644 index 6ecd9695723..00000000000 --- a/.changeset/wise-bobcats-speak.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`VestingWalletCliff`: Add an extension of the `VestingWallet` contract with an added cliff. diff --git a/.changeset/witty-chicken-smile.md b/.changeset/witty-chicken-smile.md deleted file mode 100644 index 6fae3e744ad..00000000000 --- a/.changeset/witty-chicken-smile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`ReentrancyGuardTransient`: Added a variant of `ReentrancyGuard` that uses transient storage. diff --git a/.changeset/yellow-deers-walk.md b/.changeset/yellow-deers-walk.md deleted file mode 100644 index ad370b36e58..00000000000 --- a/.changeset/yellow-deers-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`EnumerableMap`: add `UintToBytes32Map`, `AddressToAddressMap`, `AddressToBytes32Map` and `Bytes32ToAddressMap`. diff --git a/.changeset/yellow-moles-hammer.md b/.changeset/yellow-moles-hammer.md deleted file mode 100644 index b13971a28a9..00000000000 --- a/.changeset/yellow-moles-hammer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'openzeppelin-solidity': minor ---- - -`SignatureChecker`: refactor `isValidSignatureNow` to avoid validating ECDSA signatures if there is code deployed at the signer's address. diff --git a/.codecov.yml b/.codecov.yml index 5bee9146ab9..4cec4ef7d5f 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -13,3 +13,4 @@ coverage: ignore: - "test" - "contracts/mocks" + - "contracts/vendor" diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index a5418c5e4e1..00000000000 --- a/.eslintrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "root": true, - "extends" : [ - "eslint:recommended", - "prettier", - ], - "env": { - "es2022": true, - "browser": true, - "node": true, - "mocha": true, - }, - "globals" : { - "artifacts": "readonly", - "contract": "readonly", - "web3": "readonly", - "extendEnvironment": "readonly", - "expect": "readonly", - } -} diff --git a/.githooks/pre-push b/.githooks/pre-push deleted file mode 100755 index f028ce58e0b..00000000000 --- a/.githooks/pre-push +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if [ "${CI:-"false"}" != "true" ]; then - npm run test:generation - npm run lint -fi diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 9d338bb642f..18a38b3c5c0 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -41,6 +41,8 @@ jobs: run: npm run test - name: Check linearisation of the inheritance graph run: npm run test:inheritance + - name: Check pragma consistency between files + run: npm run test:pragma - name: Check proceduraly generated contracts are up-to-date run: npm run test:generation - name: Compare gas costs @@ -68,6 +70,8 @@ jobs: run: npm run test - name: Check linearisation of the inheritance graph run: npm run test:inheritance + - name: Check pragma consistency between files + run: npm run test:pragma - name: Check storage layout uses: ./.github/actions/storage-layout continue-on-error: ${{ contains(github.event.pull_request.labels.*.name, 'breaking change') }} @@ -129,4 +133,4 @@ jobs: with: check_hidden: true check_filenames: true - skip: package-lock.json,*.pdf + skip: package-lock.json,*.pdf,vendor diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 00000000000..4738b05554b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +npm run test:generation +npx lint-staged diff --git a/.solcover.js b/.solcover.js index e0dea5e2c9b..f079998cff3 100644 --- a/.solcover.js +++ b/.solcover.js @@ -10,4 +10,12 @@ module.exports = { fgrep: '[skip-on-coverage]', invert: true, }, + // Work around stack too deep for coverage + configureYulOptimizer: true, + solcOptimizerDetails: { + yul: true, + yulDetails: { + optimizerSteps: '', + }, + }, }; diff --git a/CHANGELOG.md b/CHANGELOG.md index f327a1cb446..cc52db338ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,14 @@ # Changelog + +## 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 `_countVotes` 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 `_countVotes` function. +- `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. -### Custom error 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: @@ -18,6 +21,82 @@ This version comes with changes to the custom error identifiers. Contracts previ - `SafeERC20`: Replace generic `Error(string)` with `SafeERC20FailedOperation` if the returned data can't be decoded as `bool`. - `SafeERC20`: Replace generic `SafeERC20FailedOperation` with the revert message from the contract call if it fails. +### Changes by category + +#### General + +- `AccessManager`, `VestingWallet`, `TimelockController` and `ERC2771Forwarder`: Added a public `initializer` function in their corresponding upgradeable variants. ([#5008](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5008)) + +#### Access + +- `AccessControlEnumerable`: Add a `getRoleMembers` method to return all accounts that have `role`. ([#4546](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4546)) +- `AccessManager`: Allow the `onlyAuthorized` modifier to restrict functions added to the manager. ([#5014](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5014)) + +#### Finance + +- `VestingWalletCliff`: Add an extension of the `VestingWallet` contract with an added cliff. ([#4870](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4870)) + +#### Governance + +- `GovernorCountingFractional`: Add a governor counting module that allows distributing voting power amongst 3 options (For, Against, Abstain). ([#5045](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5045)) +- `Votes`: Set `_moveDelegateVotes` visibility to internal instead of private. ([#5007](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5007)) + +#### Proxy + +- `Clones`: Add version of `clone` and `cloneDeterministic` that support sending value at creation. ([#4936](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4936)) +- `TransparentUpgradeableProxy`: Make internal `_proxyAdmin()` getter have `view` visibility. ([#4688](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4688)) +- `ProxyAdmin`: Fixed documentation for `UPGRADE_INTERFACE_VERSION` getter. ([#5031](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5031)) + +#### Tokens + +- `ERC1363`: Add implementation of the token payable standard allowing execution of contract code after transfers and approvals. ([#4631](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4631)) +- `ERC20TemporaryApproval`: Add an ERC-20 extension that implements temporary approval using transient storage, based on ERC7674 (draft). ([#5071](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5071)) +- `SafeERC20`: Add "relaxed" function for interacting with ERC-1363 functions in a way that is compatible with EOAs. ([#4631](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4631)) +- `SafeERC20`: Document risks of `safeIncreaseAllowance` and `safeDecreaseAllowance` when associated with ERC-7674. ([#5262](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5262)) +- `ERC721Utils` and `ERC1155Utils`: Add reusable libraries with functions to perform acceptance checks on `IERC721Receiver` and `IERC1155Receiver` implementers. ([#4845](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4845)) +- `ERC1363Utils`: Add helper similar to the existing ERC721Utils and ERC1155Utils. ([#5133](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5133)) + +#### Utils + +- `Arrays`: add a `sort` functions for `address[]`, `bytes32[]` and `uint256[]` memory arrays. ([#4846](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4846)) +- `Arrays`: add new functions `lowerBound`, `upperBound`, `lowerBoundMemory` and `upperBoundMemory` for lookups in sorted arrays with potential duplicates. ([#4842](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4842)) +- `Arrays`: deprecate `findUpperBound` in favor of the new `lowerBound`. ([#4842](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4842)) +- `Base64`: Add `encodeURL` following section 5 of RFC4648 for URL encoding ([#4822](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4822)) +- `Comparator`: A library of comparator functions, useful for customizing the behavior of the Heap structure. ([#5084](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5084)) +- `Create2`: Bubbles up returndata from a deployed contract that reverted during construction. ([#5052](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5052)) +- `Create2`, `Clones`: Mask `computeAddress` and `cloneDeterministic` outputs to produce a clean value for an `address` type (i.e. only use 20 bytes) ([#4941](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4941)) +- `Errors`: New library of common custom errors. ([#4936](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4936)) +- `Hashes`: A library with commonly used hash functions. ([#3617](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3617)) +- `Packing`: Added a new utility for packing, extracting and replacing bytesXX values. ([#4992](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4992)) +- `Panic`: Add a library for reverting with panic codes. ([#3298](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3298)) +- `ReentrancyGuardTransient`: Added a variant of `ReentrancyGuard` that uses transient storage. ([#4988](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4988)) +- `Strings`: Added a utility function for converting an address to checksummed string. ([#5067](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5067)) +- `SlotDerivation`: Add a library of methods for derivating common storage slots. ([#4975](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4975)) +- `TransientSlot`: Add primitives for operating on the transient storage space using a typed-slot representation. ([#4980](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4980)) + +##### Cryptography + +- `SignatureChecker`: refactor `isValidSignatureNow` to avoid validating ECDSA signatures if there is code deployed at the signer's address. ([#4951](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4951)) +- `MerkleProof`: Add variations of `verify`, `processProof`, `multiProofVerify` and `processMultiProof` (and equivalent calldata version) with support for custom hashing functions. ([#4887](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4887)) +- `P256`: Library for verification and public key recovery of P256 (aka secp256r1) signatures. ([#4881](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4881)) +- `RSA`: Library to verify signatures according to RFC 8017 Signature Verification Operation ([#4952](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4952)) + +#### Math + +- `Math`: add an `invMod` function to get the modular multiplicative inverse of a number in Z/nZ. ([#4839](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4839)) +- `Math`: Add `modExp` function that exposes the `EIP-198` precompile. Includes `uint256` and `bytes memory` versions. ([#3298](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3298)) +- `Math`: Custom errors replaced with native panic codes. ([#3298](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3298)) +- `Math`, `SignedMath`: Add a branchless `ternary` function that computes`cond ? a : b` in constant gas cost. ([#4976](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4976)) +- `SafeCast`: Add `toUint(bool)` for operating on `bool` values as `uint256`. ([#4878](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4878)) + +#### Structures + +- `CircularBuffer`: Add a data structure that stores the last `N` values pushed to it. ([#4913](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4913)) +- `DoubleEndedQueue`: Custom errors replaced with native panic codes. ([#4872](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4872)) +- `EnumerableMap`: add `UintToBytes32Map`, `AddressToAddressMap`, `AddressToBytes32Map` and `Bytes32ToAddressMap`. ([#4843](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4843)) +- `Heap`: A data structure that implements a heap-based priority queue. ([#5084](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/5084)) +- `MerkleTree`: A data structure that allows inserting elements into a merkle tree and updating its root hash. ([#3617](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3617)) + ## 5.0.2 (2024-02-29) - `Base64`: Fix issue where dirty memory located just after the input buffer is affecting the result. ([#4926](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/4926)) diff --git a/GUIDELINES.md b/GUIDELINES.md index a1418d6528a..2c21e956b1e 100644 --- a/GUIDELINES.md +++ b/GUIDELINES.md @@ -55,7 +55,7 @@ External contributions must be reviewed separately by multiple maintainers. Automation should be used as much as possible to reduce the possibility of human error and forgetfulness. -Automations that make use of sensitive credentials must use secure secret management, and must be strengthened against attacks such as [those on GitHub Actions worklows](https://github.com/nikitastupin/pwnhub). +Automations that make use of sensitive credentials must use secure secret management, and must be strengthened against attacks such as [those on GitHub Actions workflows](https://github.com/nikitastupin/pwnhub). Some other examples of automation are: diff --git a/audits/2024-10-v5.1.pdf b/audits/2024-10-v5.1.pdf new file mode 100644 index 00000000000..4d85dc7ba9e Binary files /dev/null and b/audits/2024-10-v5.1.pdf differ diff --git a/audits/2024-12-v5.2.pdf b/audits/2024-12-v5.2.pdf new file mode 100644 index 00000000000..ef138f7ec61 Binary files /dev/null and b/audits/2024-12-v5.2.pdf differ diff --git a/audits/README.md b/audits/README.md index 369d06402e6..4e8c94653cd 100644 --- a/audits/README.md +++ b/audits/README.md @@ -1,12 +1,14 @@ # Audits -| Date | Version | Commit | Auditor | Scope | Links | -| ------------ | ------- | --------- | ------------ | -------------------- | ----------------------------------------------------------- | -| October 2023 | v5.0.0 | `b5a3e69` | OpenZeppelin | v5.0 Changes | [🔗](./2023-10-v5.0.pdf) | -| May 2023 | v4.9.0 | `91df66c` | OpenZeppelin | v4.9 Changes | [🔗](./2023-05-v4.9.pdf) | -| October 2022 | v4.8.0 | `14f98db` | OpenZeppelin | ERC4626, Checkpoints | [🔗](./2022-10-ERC4626.pdf) [🔗](./2022-10-Checkpoints.pdf) | -| October 2018 | v2.0.0 | `dac5bcc` | LevelK | Everything | [🔗](./2018-10.pdf) | -| March 2017 | v1.0.4 | `9c5975a` | New Alchemy | Everything | [🔗](./2017-03.md) | +| Date | Version | Commit | Auditor | Scope | Links | +| ------------- | ------- | -------------------------------------------------------------------------------- | ------------ | -------------------- | ----------------------------------------------------------- | +| December 2024 | v5.2.0 | [`98d28f9`](https://github.com/openzeppelin/openzeppelin-contracts/tree/98d28f9) | OpenZeppelin | v5.2 Changes | [🔗](./2024-12-v5.2.pdf) | +| October 2024 | v5.1.0 | [`aba9ff6`](https://github.com/openzeppelin/openzeppelin-contracts/tree/aba9ff6) | OpenZeppelin | v5.1 Changes | [🔗](./2024-10-v5.1.pdf) | +| October 2023 | v5.0.0 | [`b5a3e69`](https://github.com/openzeppelin/openzeppelin-contracts/tree/b5a3e69) | OpenZeppelin | v5.0 Changes | [🔗](./2023-10-v5.0.pdf) | +| May 2023 | v4.9.0 | [`91df66c`](https://github.com/openzeppelin/openzeppelin-contracts/tree/91df66c) | OpenZeppelin | v4.9 Changes | [🔗](./2023-05-v4.9.pdf) | +| October 2022 | v4.8.0 | [`14f98db`](https://github.com/openzeppelin/openzeppelin-contracts/tree/14f98db) | OpenZeppelin | ERC4626, Checkpoints | [🔗](./2022-10-ERC4626.pdf) [🔗](./2022-10-Checkpoints.pdf) | +| October 2018 | v2.0.0 | [`dac5bcc`](https://github.com/openzeppelin/openzeppelin-contracts/tree/dac5bcc) | LevelK | Everything | [🔗](./2018-10.pdf) | +| March 2017 | v1.0.4 | [`9c5975a`](https://github.com/openzeppelin/openzeppelin-contracts/tree/9c5975a) | New Alchemy | Everything | [🔗](./2017-03.md) | # Formal Verification diff --git a/certora/run.js b/certora/run.js index 7b65534ea1d..a5ad9eece3b 100755 --- a/certora/run.js +++ b/certora/run.js @@ -7,11 +7,16 @@ // node certora/run.js AccessControl // node certora/run.js AccessControlHarness:AccessControl -const proc = require('child_process'); -const { PassThrough } = require('stream'); -const events = require('events'); - -const argv = require('yargs') +import { spawn } from 'child_process'; +import { PassThrough } from 'stream'; +import { once } from 'events'; +import path from 'path'; +import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; +import pLimit from 'p-limit'; +import fs from 'fs/promises'; + +const argv = yargs(hideBin(process.argv)) .env('') .options({ all: { @@ -21,7 +26,7 @@ const argv = require('yargs') spec: { alias: 's', type: 'string', - default: __dirname + '/specs.json', + default: path.resolve(import.meta.dirname, 'specs.json'), }, parallel: { alias: 'p', @@ -38,18 +43,20 @@ const argv = require('yargs') type: 'array', default: [], }, - }).argv; + }) + .parse(); function match(entry, request) { const [reqSpec, reqContract] = request.split(':').reverse(); return entry.spec == reqSpec && (!reqContract || entry.contract == reqContract); } -const specs = require(argv.spec).filter(s => argv.all || argv._.some(r => match(s, r))); -const limit = require('p-limit')(argv.parallel); +const specs = JSON.parse(fs.readFileSync(argv.spec, 'utf8')).filter(s => argv.all || argv._.some(r => match(s, r))); + +const limit = pLimit(argv.parallel); if (argv._.length == 0 && !argv.all) { - console.error(`Warning: No specs requested. Did you forgot to toggle '--all'?`); + console.error(`Warning: No specs requested. Did you forget to toggle '--all'?`); } for (const r of argv._) { @@ -64,12 +71,13 @@ if (process.exitCode) { } for (const { spec, contract, files, options = [] } of specs) { - limit( - runCertora, - spec, - contract, - files, - [...options, ...argv.options].flatMap(opt => opt.split(' ')), + limit(() => + runCertora( + spec, + contract, + files, + [...options, ...argv.options].flatMap(opt => opt.split(' ')), + ), ); } @@ -79,7 +87,7 @@ async function runCertora(spec, contract, files, options = []) { if (argv.verbose) { console.log('Running:', args.join(' ')); } - const child = proc.spawn('certoraRun', args); + const child = spawn('certoraRun', args); const stream = new PassThrough(); const output = collect(stream); @@ -103,7 +111,7 @@ async function runCertora(spec, contract, files, options = []) { }); // wait for process end - const [code, signal] = await events.once(child, 'exit'); + const [code, signal] = await once(child, 'exit'); // error if (code || signal) { diff --git a/contracts/access/IAccessControl.sol b/contracts/access/IAccessControl.sol index 67ea537ed8d..4c16a6ef75c 100644 --- a/contracts/access/IAccessControl.sol +++ b/contracts/access/IAccessControl.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (access/IAccessControl.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (access/IAccessControl.sol) pragma solidity ^0.8.20; diff --git a/contracts/access/Ownable2Step.sol b/contracts/access/Ownable2Step.sol index 46765c4daa8..3a0747ce7cd 100644 --- a/contracts/access/Ownable2Step.sol +++ b/contracts/access/Ownable2Step.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (access/Ownable2Step.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (access/Ownable2Step.sol) pragma solidity ^0.8.20; @@ -37,6 +37,8 @@ abstract contract Ownable2Step is Ownable { /** * @dev Starts the ownership transfer of the contract to a new account. Replaces the pending transfer if there is one. * Can only be called by the current owner. + * + * Setting `newOwner` to the zero address is allowed; this can be used to cancel an initiated ownership transfer. */ function transferOwnership(address newOwner) public virtual override onlyOwner { _pendingOwner = newOwner; diff --git a/contracts/access/extensions/AccessControlEnumerable.sol b/contracts/access/extensions/AccessControlEnumerable.sol index 222490c9b30..b1980e364b0 100644 --- a/contracts/access/extensions/AccessControlEnumerable.sol +++ b/contracts/access/extensions/AccessControlEnumerable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (access/extensions/AccessControlEnumerable.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/AccessControlEnumerable.sol) pragma solidity ^0.8.20; diff --git a/contracts/access/extensions/IAccessControlDefaultAdminRules.sol b/contracts/access/extensions/IAccessControlDefaultAdminRules.sol index b74355aad31..3740749c4b8 100644 --- a/contracts/access/extensions/IAccessControlDefaultAdminRules.sol +++ b/contracts/access/extensions/IAccessControlDefaultAdminRules.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (access/extensions/IAccessControlDefaultAdminRules.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/IAccessControlDefaultAdminRules.sol) pragma solidity ^0.8.20; diff --git a/contracts/access/extensions/IAccessControlEnumerable.sol b/contracts/access/extensions/IAccessControlEnumerable.sol index 11429686355..bb9bac86ee1 100644 --- a/contracts/access/extensions/IAccessControlEnumerable.sol +++ b/contracts/access/extensions/IAccessControlEnumerable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (access/extensions/IAccessControlEnumerable.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (access/extensions/IAccessControlEnumerable.sol) pragma solidity ^0.8.20; diff --git a/contracts/access/manager/AccessManaged.sol b/contracts/access/manager/AccessManaged.sol index 41b21b0b79d..352a54def2e 100644 --- a/contracts/access/manager/AccessManaged.sol +++ b/contracts/access/manager/AccessManaged.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (access/manager/AccessManaged.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (access/manager/AccessManaged.sol) pragma solidity ^0.8.20; diff --git a/contracts/access/manager/AccessManager.sol b/contracts/access/manager/AccessManager.sol index a6e2d7aed0e..051080554c6 100644 --- a/contracts/access/manager/AccessManager.sol +++ b/contracts/access/manager/AccessManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (access/manager/AccessManager.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (access/manager/AccessManager.sol) pragma solidity ^0.8.20; diff --git a/contracts/access/manager/IAccessManager.sol b/contracts/access/manager/IAccessManager.sol index 2811281a319..ebcd1d614a1 100644 --- a/contracts/access/manager/IAccessManager.sol +++ b/contracts/access/manager/IAccessManager.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (access/manager/IAccessManager.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (access/manager/IAccessManager.sol) pragma solidity ^0.8.20; diff --git a/contracts/account/README.adoc b/contracts/account/README.adoc new file mode 100644 index 00000000000..d2eb9db5ee9 --- /dev/null +++ b/contracts/account/README.adoc @@ -0,0 +1,12 @@ += Account + +[.readme-notice] +NOTE: This document is better viewed at https://docs.openzeppelin.com/contracts/api/account + +This directory includes contracts to build accounts for ERC-4337. + +== Utilities + +{{ERC4337Utils}} + +{{ERC7579Utils}} diff --git a/contracts/account/utils/draft-ERC4337Utils.sol b/contracts/account/utils/draft-ERC4337Utils.sol new file mode 100644 index 00000000000..78100b0c96e --- /dev/null +++ b/contracts/account/utils/draft-ERC4337Utils.sol @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "../../interfaces/draft-IERC4337.sol"; +import {Math} from "../../utils/math/Math.sol"; +import {Packing} from "../../utils/Packing.sol"; + +/** + * @dev Library with common ERC-4337 utility functions. + * + * See https://eips.ethereum.org/EIPS/eip-4337[ERC-4337]. + */ +library ERC4337Utils { + using Packing for *; + + /// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) return this value on success. + uint256 internal constant SIG_VALIDATION_SUCCESS = 0; + + /// @dev For simulation purposes, validateUserOp (and validatePaymasterUserOp) must return this value in case of signature failure, instead of revert. + uint256 internal constant SIG_VALIDATION_FAILED = 1; + + /// @dev Parses the validation data into its components. See {packValidationData}. + function parseValidationData( + uint256 validationData + ) internal pure returns (address aggregator, uint48 validAfter, uint48 validUntil) { + validAfter = uint48(bytes32(validationData).extract_32_6(0)); + validUntil = uint48(bytes32(validationData).extract_32_6(6)); + aggregator = address(bytes32(validationData).extract_32_20(12)); + if (validUntil == 0) validUntil = type(uint48).max; + } + + /// @dev Packs the validation data into a single uint256. See {parseValidationData}. + function packValidationData( + address aggregator, + uint48 validAfter, + uint48 validUntil + ) internal pure returns (uint256) { + return uint256(bytes6(validAfter).pack_6_6(bytes6(validUntil)).pack_12_20(bytes20(aggregator))); + } + + /// @dev Same as {packValidationData}, but with a boolean signature success flag. + function packValidationData(bool sigSuccess, uint48 validAfter, uint48 validUntil) internal pure returns (uint256) { + return + packValidationData( + address(uint160(Math.ternary(sigSuccess, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED))), + validAfter, + validUntil + ); + } + + /** + * @dev Combines two validation data into a single one. + * + * The `aggregator` is set to {SIG_VALIDATION_SUCCESS} if both are successful, while + * the `validAfter` is the maximum and the `validUntil` is the minimum of both. + */ + function combineValidationData(uint256 validationData1, uint256 validationData2) internal pure returns (uint256) { + (address aggregator1, uint48 validAfter1, uint48 validUntil1) = parseValidationData(validationData1); + (address aggregator2, uint48 validAfter2, uint48 validUntil2) = parseValidationData(validationData2); + + bool success = aggregator1 == address(uint160(SIG_VALIDATION_SUCCESS)) && + aggregator2 == address(uint160(SIG_VALIDATION_SUCCESS)); + uint48 validAfter = uint48(Math.max(validAfter1, validAfter2)); + uint48 validUntil = uint48(Math.min(validUntil1, validUntil2)); + return packValidationData(success, validAfter, validUntil); + } + + /// @dev Returns the aggregator of the `validationData` and whether it is out of time range. + function getValidationData(uint256 validationData) internal view returns (address aggregator, bool outOfTimeRange) { + (address aggregator_, uint48 validAfter, uint48 validUntil) = parseValidationData(validationData); + return (aggregator_, block.timestamp < validAfter || validUntil < block.timestamp); + } + + /// @dev Computes the hash of a user operation for a given entrypoint and chainid. + function hash( + PackedUserOperation calldata self, + address entrypoint, + uint256 chainid + ) internal pure returns (bytes32) { + bytes32 result = keccak256( + abi.encode( + keccak256( + abi.encode( + self.sender, + self.nonce, + keccak256(self.initCode), + keccak256(self.callData), + self.accountGasLimits, + self.preVerificationGas, + self.gasFees, + keccak256(self.paymasterAndData) + ) + ), + entrypoint, + chainid + ) + ); + return result; + } + + /// @dev Returns `factory` from the {PackedUserOperation}, or address(0) if the initCode is empty or not properly formatted. + function factory(PackedUserOperation calldata self) internal pure returns (address) { + return self.initCode.length < 20 ? address(0) : address(bytes20(self.initCode[0:20])); + } + + /// @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:]; + } + + /// @dev Returns `verificationGasLimit` from the {PackedUserOperation}. + function verificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(self.accountGasLimits.extract_32_16(0)); + } + + /// @dev Returns `accountGasLimits` from the {PackedUserOperation}. + function callGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(self.accountGasLimits.extract_32_16(16)); + } + + /// @dev Returns the first section of `gasFees` from the {PackedUserOperation}. + function maxPriorityFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(self.gasFees.extract_32_16(0)); + } + + /// @dev Returns the second section of `gasFees` from the {PackedUserOperation}. + function maxFeePerGas(PackedUserOperation calldata self) internal pure returns (uint256) { + return uint128(self.gasFees.extract_32_16(16)); + } + + /// @dev Returns the total gas price for the {PackedUserOperation} (ie. `maxFeePerGas` or `maxPriorityFeePerGas + basefee`). + function gasPrice(PackedUserOperation calldata self) internal view returns (uint256) { + unchecked { + // Following values are "per gas" + uint256 maxPriorityFee = maxPriorityFeePerGas(self); + uint256 maxFee = maxFeePerGas(self); + return Math.min(maxFee, maxPriorityFee + block.basefee); + } + } + + /// @dev Returns the first section of `paymasterAndData` from the {PackedUserOperation}. + function paymaster(PackedUserOperation calldata self) internal pure returns (address) { + return self.paymasterAndData.length < 52 ? address(0) : address(bytes20(self.paymasterAndData[0:20])); + } + + /// @dev Returns the second section of `paymasterAndData` from the {PackedUserOperation}. + function paymasterVerificationGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return self.paymasterAndData.length < 52 ? 0 : uint128(bytes16(self.paymasterAndData[20:36])); + } + + /// @dev Returns the third section of `paymasterAndData` from the {PackedUserOperation}. + function paymasterPostOpGasLimit(PackedUserOperation calldata self) internal pure returns (uint256) { + return self.paymasterAndData.length < 52 ? 0 : uint128(bytes16(self.paymasterAndData[36:52])); + } + + /// @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 + } + } +} diff --git a/contracts/account/utils/draft-ERC7579Utils.sol b/contracts/account/utils/draft-ERC7579Utils.sol new file mode 100644 index 00000000000..8be094e649d --- /dev/null +++ b/contracts/account/utils/draft-ERC7579Utils.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Execution} from "../../interfaces/draft-IERC7579.sol"; +import {Packing} from "../../utils/Packing.sol"; +import {Address} from "../../utils/Address.sol"; + +type Mode is bytes32; +type CallType is bytes1; +type ExecType is bytes1; +type ModeSelector is bytes4; +type ModePayload is bytes22; + +/** + * @dev Library with common ERC-7579 utility functions. + * + * See https://eips.ethereum.org/EIPS/eip-7579[ERC-7579]. + */ +// slither-disable-next-line unused-state +library ERC7579Utils { + using Packing for *; + + /// @dev A single `call` execution. + CallType internal constant CALLTYPE_SINGLE = CallType.wrap(0x00); + + /// @dev A batch of `call` executions. + CallType internal constant CALLTYPE_BATCH = CallType.wrap(0x01); + + /// @dev A `delegatecall` execution. + CallType internal constant CALLTYPE_DELEGATECALL = CallType.wrap(0xFF); + + /// @dev Default execution type that reverts on failure. + ExecType internal constant EXECTYPE_DEFAULT = ExecType.wrap(0x00); + + /// @dev Execution type that does not revert on failure. + ExecType internal constant EXECTYPE_TRY = ExecType.wrap(0x01); + + /** + * @dev Emits when an {EXECTYPE_TRY} execution fails. + * @param batchExecutionIndex The index of the failed call in the execution batch. + * @param returndata The returned data from the failed call. + */ + event ERC7579TryExecuteFail(uint256 batchExecutionIndex, bytes returndata); + + /// @dev The provided {CallType} is not supported. + error ERC7579UnsupportedCallType(CallType callType); + + /// @dev The provided {ExecType} is not supported. + error ERC7579UnsupportedExecType(ExecType execType); + + /// @dev The provided module doesn't match the provided module type. + error ERC7579MismatchedModuleTypeId(uint256 moduleTypeId, address module); + + /// @dev The module is not installed. + error ERC7579UninstalledModule(uint256 moduleTypeId, address module); + + /// @dev The module is already installed. + error ERC7579AlreadyInstalledModule(uint256 moduleTypeId, address module); + + /// @dev The module type is not supported. + error ERC7579UnsupportedModuleType(uint256 moduleTypeId); + + /// @dev Input calldata not properly formatted and possibly malicious. + error ERC7579DecodingError(); + + /// @dev Executes a single call. + function execSingle( + bytes calldata executionCalldata, + ExecType execType + ) internal returns (bytes[] memory returnData) { + (address target, uint256 value, bytes calldata callData) = decodeSingle(executionCalldata); + returnData = new bytes[](1); + returnData[0] = _call(0, execType, target, value, callData); + } + + /// @dev Executes a batch of calls. + function execBatch( + bytes calldata executionCalldata, + ExecType execType + ) internal returns (bytes[] memory returnData) { + Execution[] calldata executionBatch = decodeBatch(executionCalldata); + returnData = new bytes[](executionBatch.length); + for (uint256 i = 0; i < executionBatch.length; ++i) { + returnData[i] = _call( + i, + execType, + executionBatch[i].target, + executionBatch[i].value, + executionBatch[i].callData + ); + } + } + + /// @dev Executes a delegate call. + function execDelegateCall( + bytes calldata executionCalldata, + ExecType execType + ) internal returns (bytes[] memory returnData) { + (address target, bytes calldata callData) = decodeDelegate(executionCalldata); + returnData = new bytes[](1); + returnData[0] = _delegatecall(0, execType, target, callData); + } + + /// @dev Encodes the mode with the provided parameters. See {decodeMode}. + function encodeMode( + CallType callType, + ExecType execType, + ModeSelector selector, + ModePayload payload + ) internal pure returns (Mode mode) { + return + Mode.wrap( + CallType + .unwrap(callType) + .pack_1_1(ExecType.unwrap(execType)) + .pack_2_4(bytes4(0)) + .pack_6_4(ModeSelector.unwrap(selector)) + .pack_10_22(ModePayload.unwrap(payload)) + ); + } + + /// @dev Decodes the mode into its parameters. See {encodeMode}. + function decodeMode( + Mode mode + ) internal pure returns (CallType callType, ExecType execType, ModeSelector selector, ModePayload payload) { + return ( + CallType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 0)), + ExecType.wrap(Packing.extract_32_1(Mode.unwrap(mode), 1)), + ModeSelector.wrap(Packing.extract_32_4(Mode.unwrap(mode), 6)), + ModePayload.wrap(Packing.extract_32_22(Mode.unwrap(mode), 10)) + ); + } + + /// @dev Encodes a single call execution. See {decodeSingle}. + function encodeSingle( + address target, + uint256 value, + bytes calldata callData + ) internal pure returns (bytes memory executionCalldata) { + return abi.encodePacked(target, value, callData); + } + + /// @dev Decodes a single call execution. See {encodeSingle}. + function decodeSingle( + bytes calldata executionCalldata + ) internal pure returns (address target, uint256 value, bytes calldata callData) { + target = address(bytes20(executionCalldata[0:20])); + value = uint256(bytes32(executionCalldata[20:52])); + callData = executionCalldata[52:]; + } + + /// @dev Encodes a delegate call execution. See {decodeDelegate}. + function encodeDelegate( + address target, + bytes calldata callData + ) internal pure returns (bytes memory executionCalldata) { + return abi.encodePacked(target, callData); + } + + /// @dev Decodes a delegate call execution. See {encodeDelegate}. + function decodeDelegate( + bytes calldata executionCalldata + ) internal pure returns (address target, bytes calldata callData) { + target = address(bytes20(executionCalldata[0:20])); + callData = executionCalldata[20:]; + } + + /// @dev Encodes a batch of executions. See {decodeBatch}. + function encodeBatch(Execution[] memory executionBatch) internal pure returns (bytes memory executionCalldata) { + return abi.encode(executionBatch); + } + + /// @dev Decodes a batch of executions. See {encodeBatch}. + /// + /// NOTE: This function runs some checks and will throw a {ERC7579DecodingError} if the input is not properly formatted. + function decodeBatch(bytes calldata executionCalldata) internal pure returns (Execution[] calldata executionBatch) { + unchecked { + uint256 bufferLength = executionCalldata.length; + + // Check executionCalldata is not empty. + if (bufferLength < 32) revert ERC7579DecodingError(); + + // Get the offset of the array (pointer to the array length). + uint256 arrayLengthOffset = uint256(bytes32(executionCalldata[0:32])); + + // The array length (at arrayLengthOffset) should be 32 bytes long. We check that this is within the + // buffer bounds. Since we know bufferLength is at least 32, we can subtract with no overflow risk. + if (arrayLengthOffset > bufferLength - 32) revert ERC7579DecodingError(); + + // Get the array length. arrayLengthOffset + 32 is bounded by bufferLength so it does not overflow. + uint256 arrayLength = uint256(bytes32(executionCalldata[arrayLengthOffset:arrayLengthOffset + 32])); + + // Check that the buffer is long enough to store the array elements as "offset pointer": + // - each element of the array is an "offset pointer" to the data. + // - each "offset pointer" (to an array element) takes 32 bytes. + // - validity of the calldata at that location is checked when the array element is accessed, so we only + // need to check that the buffer is large enough to hold the pointers. + // + // Since we know bufferLength is at least arrayLengthOffset + 32, we can subtract with no overflow risk. + // Solidity limits length of such arrays to 2**64-1, this guarantees `arrayLength * 32` does not overflow. + if (arrayLength > type(uint64).max || bufferLength - arrayLengthOffset - 32 < arrayLength * 32) + revert ERC7579DecodingError(); + + assembly ("memory-safe") { + executionBatch.offset := add(add(executionCalldata.offset, arrayLengthOffset), 32) + executionBatch.length := arrayLength + } + } + } + + /// @dev Executes a `call` to the target with the provided {ExecType}. + function _call( + uint256 index, + ExecType execType, + address target, + uint256 value, + bytes calldata data + ) private returns (bytes memory) { + (bool success, bytes memory returndata) = target.call{value: value}(data); + return _validateExecutionMode(index, execType, success, returndata); + } + + /// @dev Executes a `delegatecall` to the target with the provided {ExecType}. + function _delegatecall( + uint256 index, + ExecType execType, + address target, + bytes calldata data + ) private returns (bytes memory) { + (bool success, bytes memory returndata) = target.delegatecall(data); + return _validateExecutionMode(index, execType, success, returndata); + } + + /// @dev Validates the execution mode and returns the returndata. + function _validateExecutionMode( + uint256 index, + ExecType execType, + bool success, + bytes memory returndata + ) private returns (bytes memory) { + if (execType == ERC7579Utils.EXECTYPE_DEFAULT) { + Address.verifyCallResult(success, returndata); + } else if (execType == ERC7579Utils.EXECTYPE_TRY) { + if (!success) emit ERC7579TryExecuteFail(index, returndata); + } else { + revert ERC7579UnsupportedExecType(execType); + } + return returndata; + } +} + +// Operators +using {eqCallType as ==} for CallType global; +using {eqExecType as ==} for ExecType global; +using {eqModeSelector as ==} for ModeSelector global; +using {eqModePayload as ==} for ModePayload global; + +/// @dev Compares two `CallType` values for equality. +function eqCallType(CallType a, CallType b) pure returns (bool) { + return CallType.unwrap(a) == CallType.unwrap(b); +} + +/// @dev Compares two `ExecType` values for equality. +function eqExecType(ExecType a, ExecType b) pure returns (bool) { + return ExecType.unwrap(a) == ExecType.unwrap(b); +} + +/// @dev Compares two `ModeSelector` values for equality. +function eqModeSelector(ModeSelector a, ModeSelector b) pure returns (bool) { + return ModeSelector.unwrap(a) == ModeSelector.unwrap(b); +} + +/// @dev Compares two `ModePayload` values for equality. +function eqModePayload(ModePayload a, ModePayload b) pure returns (bool) { + return ModePayload.unwrap(a) == ModePayload.unwrap(b); +} diff --git a/contracts/finance/VestingWallet.sol b/contracts/finance/VestingWallet.sol index f472b66068b..b03c4968430 100644 --- a/contracts/finance/VestingWallet.sol +++ b/contracts/finance/VestingWallet.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (finance/VestingWallet.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (finance/VestingWallet.sol) pragma solidity ^0.8.20; import {IERC20} from "../token/ERC20/IERC20.sol"; @@ -26,6 +26,11 @@ import {Ownable} from "../access/Ownable.sol"; * * NOTE: When using this contract with any token whose balance is adjusted automatically (i.e. a rebase token), make * sure to account the supply/balance adjustment in the vesting schedule to ensure the vested amount is as intended. + * + * NOTE: Chains with support for native ERC20s may allow the vesting wallet to withdraw the underlying asset as both an + * ERC20 and as native currency. For example, if chain C supports token A and the wallet gets deposited 100 A, then + * at 50% of the vesting period, the beneficiary can withdraw 50 A as ERC20 and 25 A as native currency (totaling 75 A). + * Consider disabling one of the withdrawal methods. */ contract VestingWallet is Context, Ownable { event EtherReleased(uint256 amount); @@ -37,8 +42,8 @@ contract VestingWallet is Context, Ownable { uint64 private immutable _duration; /** - * @dev Sets the sender as the initial owner, the beneficiary as the pending owner, the start timestamp and the - * vesting duration of the vesting wallet. + * @dev Sets the beneficiary (owner), the start timestamp and the vesting duration (in seconds) of the vesting + * wallet. */ constructor(address beneficiary, uint64 startTimestamp, uint64 durationSeconds) payable Ownable(beneficiary) { _start = startTimestamp; diff --git a/contracts/finance/VestingWalletCliff.sol b/contracts/finance/VestingWalletCliff.sol index d98be640bc7..dd1da6580bd 100644 --- a/contracts/finance/VestingWalletCliff.sol +++ b/contracts/finance/VestingWalletCliff.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (finance/VestingWalletCliff.sol) pragma solidity ^0.8.20; @@ -7,6 +8,8 @@ import {VestingWallet} from "./VestingWallet.sol"; /** * @dev Extension of {VestingWallet} that adds a cliff to the vesting schedule. + * + * _Available since v5.1._ */ abstract contract VestingWalletCliff is VestingWallet { using SafeCast for *; @@ -17,8 +20,8 @@ abstract contract VestingWalletCliff is VestingWallet { error InvalidCliffDuration(uint64 cliffSeconds, uint64 durationSeconds); /** - * @dev Sets the sender as the initial owner, the beneficiary as the pending owner, the start timestamp, the - * vesting duration and the duration of the cliff of the vesting wallet. + * @dev Set the duration of the cliff, in seconds. The cliff starts vesting schedule (see {VestingWallet}'s + * constructor) and ends `cliffSeconds` later. */ constructor(uint64 cliffSeconds) { if (cliffSeconds > duration()) { diff --git a/contracts/governance/Governor.sol b/contracts/governance/Governor.sol index 453f2029088..390411556b0 100644 --- a/contracts/governance/Governor.sol +++ b/contracts/governance/Governor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (governance/Governor.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (governance/Governor.sol) pragma solidity ^0.8.20; @@ -13,6 +13,7 @@ import {DoubleEndedQueue} from "../utils/structs/DoubleEndedQueue.sol"; import {Address} from "../utils/Address.sol"; import {Context} from "../utils/Context.sol"; import {Nonces} from "../utils/Nonces.sol"; +import {Strings} from "../utils/Strings.sol"; import {IGovernor, IERC6372} from "./IGovernor.sol"; /** @@ -259,6 +260,13 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 bytes memory params ) internal virtual returns (uint256); + /** + * @dev Hook that should be called every time the tally for a proposal is updated. + * + * Note: This function must run successfully. Reverts will result in the bricking of governance + */ + function _tallyUpdated(uint256 proposalId) internal virtual {} + /** * @dev Default additional encoded parameters used by castVote methods that don't include them * @@ -648,6 +656,8 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 emit VoteCastWithParams(account, proposalId, support, votedWeight, reason, params); } + _tallyUpdated(proposalId); + return votedWeight; } @@ -731,7 +741,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 * * If requirements are not met, reverts with a {GovernorUnexpectedProposalState} error. */ - function _validateStateBitmap(uint256 proposalId, bytes32 allowedStates) private view returns (ProposalState) { + function _validateStateBitmap(uint256 proposalId, bytes32 allowedStates) internal view returns (ProposalState) { ProposalState currentState = state(proposalId); if (_encodeStateBitmap(currentState) & allowedStates == bytes32(0)) { revert GovernorUnexpectedProposalState(proposalId, currentState, allowedStates); @@ -760,67 +770,25 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 address proposer, string memory description ) internal view virtual returns (bool) { - uint256 len = bytes(description).length; - - // Length is too short to contain a valid proposer suffix - if (len < 52) { - return true; - } - - // Extract what would be the `#proposer=0x` marker beginning the suffix - bytes12 marker; - assembly ("memory-safe") { - // - Start of the string contents in memory = description + 32 - // - First character of the marker = len - 52 - // - Length of "#proposer=0x0000000000000000000000000000000000000000" = 52 - // - We read the memory word starting at the first character of the marker: - // - (description + 32) + (len - 52) = description + (len - 20) - // - Note: Solidity will ignore anything past the first 12 bytes - marker := mload(add(description, sub(len, 20))) - } - - // If the marker is not found, there is no proposer suffix to check - if (marker != bytes12("#proposer=0x")) { - return true; - } + unchecked { + uint256 length = bytes(description).length; - // Parse the 40 characters following the marker as uint160 - uint160 recovered = 0; - for (uint256 i = len - 40; i < len; ++i) { - (bool isHex, uint8 value) = _tryHexToUint(bytes(description)[i]); - // If any of the characters is not a hex digit, ignore the suffix entirely - if (!isHex) { + // Length is too short to contain a valid proposer suffix + if (length < 52) { return true; } - recovered = (recovered << 4) | value; - } - return recovered == uint160(proposer); - } + // Extract what would be the `#proposer=` marker beginning the suffix + bytes10 marker = bytes10(_unsafeReadBytesOffset(bytes(description), length - 52)); - /** - * @dev Try to parse a character from a string as a hex value. Returns `(true, value)` if the char is in - * `[0-9a-fA-F]` and `(false, 0)` otherwise. Value is guaranteed to be in the range `0 <= value < 16` - */ - function _tryHexToUint(bytes1 char) private pure returns (bool, uint8) { - uint8 c = uint8(char); - unchecked { - // Case 0-9 - if (47 < c && c < 58) { - return (true, c - 48); - } - // Case A-F - else if (64 < c && c < 71) { - return (true, c - 55); - } - // Case a-f - else if (96 < c && c < 103) { - return (true, c - 87); - } - // Else: not a hex char - else { - return (false, 0); + // If the marker is not found, there is no proposer suffix to check + if (marker != bytes10("#proposer=")) { + return true; } + + // Check that the last 42 characters (after the marker) are a properly formatted address. + (bool success, address recovered) = Strings.tryParseAddress(description, length - 42, length); + return !success || recovered == proposer; } } @@ -849,4 +817,17 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72 * @inheritdoc IGovernor */ function quorum(uint256 timepoint) public view virtual returns (uint256); + + /** + * @dev Reads a bytes32 from a bytes array without bounds checking. + * + * NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the + * assembly block as such would prevent some optimizations. + */ + function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) { + // This is not memory safe in the general case, but all calls to this private function are within bounds. + assembly ("memory-safe") { + value := mload(add(buffer, add(0x20, offset))) + } + } } diff --git a/contracts/governance/IGovernor.sol b/contracts/governance/IGovernor.sol index 67abfdbbae4..28f8aaac044 100644 --- a/contracts/governance/IGovernor.sol +++ b/contracts/governance/IGovernor.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (governance/IGovernor.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (governance/IGovernor.sol) pragma solidity ^0.8.20; diff --git a/contracts/governance/README.adoc b/contracts/governance/README.adoc index 0f556b90d69..d390d1be82d 100644 --- a/contracts/governance/README.adoc +++ b/contracts/governance/README.adoc @@ -30,6 +30,8 @@ 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. + 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. * {GovernorTimelockAccess}: Connects with an instance of an {AccessManager}. This allows restrictions (and delays) enforced by the manager to be considered by the Governor and integrated into the AccessManager's "schedule + execute" workflow. @@ -66,6 +68,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{GovernorCountingFractional}} +{{GovernorCountingOverride}} + {{GovernorVotes}} {{GovernorVotesQuorumFraction}} @@ -88,6 +92,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you {{Votes}} +{{VotesExtended}} + == Timelock In a governance system, the {TimelockController} contract is in charge of introducing a delay between a proposal and its execution. It can be used with or without a {Governor}. diff --git a/contracts/governance/extensions/GovernorCountingFractional.sol b/contracts/governance/extensions/GovernorCountingFractional.sol index 5a553fbdb44..d2231bb9529 100644 --- a/contracts/governance/extensions/GovernorCountingFractional.sol +++ b/contracts/governance/extensions/GovernorCountingFractional.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (governance/extensions/GovernorCountingFractional.sol) pragma solidity ^0.8.20; @@ -27,6 +28,8 @@ import {Math} from "../../utils/math/Math.sol"; * * Voting privately from a shielded pool using zero knowledge proofs. * * Based on ScopeLift's GovernorCountingFractional[https://github.com/ScopeLift/flexible-voting/blob/e5de2efd1368387b840931f19f3c184c85842761/src/GovernorCountingFractional.sol] + * + * _Available since v5.1._ */ abstract contract GovernorCountingFractional is Governor { using Math for *; @@ -43,7 +46,7 @@ abstract contract GovernorCountingFractional is Governor { /** * @dev Mapping from proposal ID to vote tallies for that proposal. */ - mapping(uint256 => ProposalVote) private _proposalVotes; + mapping(uint256 proposalId => ProposalVote) private _proposalVotes; /** * @dev A fractional vote params uses more votes than are available for that user. @@ -145,7 +148,7 @@ abstract contract GovernorCountingFractional is Governor { uint256 againstVotes = 0; uint256 forVotes = 0; uint256 abstainVotes = 0; - uint256 usedWeight; + uint256 usedWeight = 0; // For clarity of event indexing, fractional voting must be clearly advertised in the "support" field. // diff --git a/contracts/governance/extensions/GovernorCountingOverridable.sol b/contracts/governance/extensions/GovernorCountingOverridable.sol new file mode 100644 index 00000000000..db375a93f47 --- /dev/null +++ b/contracts/governance/extensions/GovernorCountingOverridable.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {SignatureChecker} from "../../utils/cryptography/SignatureChecker.sol"; +import {SafeCast} from "../../utils/math/SafeCast.sol"; +import {VotesExtended} from "../utils/VotesExtended.sol"; +import {GovernorVotes} from "./GovernorVotes.sol"; + +/** + * @dev Extension of {Governor} which enables delegators to override the vote of their delegates. This module requires a + * token that inherits {VotesExtended}. + */ +abstract contract GovernorCountingOverridable is GovernorVotes { + bytes32 public constant OVERRIDE_BALLOT_TYPEHASH = + keccak256("OverrideBallot(uint256 proposalId,uint8 support,address voter,uint256 nonce,string reason)"); + + /** + * @dev Supported vote types. Matches Governor Bravo ordering. + */ + enum VoteType { + Against, + For, + Abstain + } + + struct VoteReceipt { + uint8 casted; // 0 if vote was not casted. Otherwise: support + 1 + bool hasOverriden; + uint208 overridenWeight; + } + + struct ProposalVote { + uint256[3] votes; + mapping(address voter => VoteReceipt) voteReceipt; + } + + /// @dev The votes casted by `delegate` were reduced by `weight` after an override vote was casted by the original token holder + event VoteReduced(address indexed delegate, uint256 proposalId, uint8 support, uint256 weight); + + /// @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); + + mapping(uint256 proposalId => ProposalVote) private _proposalVotes; + + /** + * @dev See {IGovernor-COUNTING_MODE}. + */ + // solhint-disable-next-line func-name-mixedcase + function COUNTING_MODE() public pure virtual override returns (string memory) { + return "support=bravo,override&quorum=for,abstain&overridable=true"; + } + + /** + * @dev See {IGovernor-hasVoted}. + * + * NOTE: Calling {castVote} (or similar) casts a vote using the voting power that is delegated to the voter. + * Conversely, calling {castOverrideVote} (or similar) uses the voting power of the account itself, from its asset + * balances. Casting an "override vote" does not count as voting and won't be reflected by this getter. Consider + * using {hasVotedOverride} to check if an account has casted an "override vote" for a given proposal id. + */ + function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) { + return _proposalVotes[proposalId].voteReceipt[account].casted != 0; + } + + /** + * @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; + } + + /** + * @dev Accessor to the internal vote counts. + */ + function proposalVotes( + uint256 proposalId + ) public view virtual returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) { + uint256[3] storage votes = _proposalVotes[proposalId].votes; + return (votes[uint8(VoteType.Against)], votes[uint8(VoteType.For)], votes[uint8(VoteType.Abstain)]); + } + + /** + * @dev See {Governor-_quorumReached}. + */ + function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) { + uint256[3] storage votes = _proposalVotes[proposalId].votes; + return quorum(proposalSnapshot(proposalId)) <= votes[uint8(VoteType.For)] + votes[uint8(VoteType.Abstain)]; + } + + /** + * @dev See {Governor-_voteSucceeded}. In this module, the forVotes must be strictly over the againstVotes. + */ + function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) { + uint256[3] storage votes = _proposalVotes[proposalId].votes; + return votes[uint8(VoteType.For)] > votes[uint8(VoteType.Against)]; + } + + /** + * @dev See {Governor-_countVote}. In this module, the support follows the `VoteType` enum (from Governor Bravo). + * + * NOTE: called by {Governor-_castVote} which emits the {IGovernor-VoteCast} (or {IGovernor-VoteCastWithParams}) + * event. + */ + function _countVote( + uint256 proposalId, + address account, + uint8 support, + uint256 totalWeight, + bytes memory /*params*/ + ) internal virtual override returns (uint256) { + ProposalVote storage proposalVote = _proposalVotes[proposalId]; + + if (support > uint8(VoteType.Abstain)) { + revert GovernorInvalidVoteType(); + } + + if (proposalVote.voteReceipt[account].casted != 0) { + revert GovernorAlreadyCastVote(account); + } + + totalWeight -= proposalVote.voteReceipt[account].overridenWeight; + proposalVote.votes[support] += totalWeight; + proposalVote.voteReceipt[account].casted = support + 1; + + return totalWeight; + } + + /** + * @dev Variant of {Governor-_countVote} that deals with vote overrides. + * + * NOTE: See {hasVoted} for more details about the difference between {castVote} and {castOverrideVote}. + */ + function _countOverride(uint256 proposalId, address account, uint8 support) internal virtual returns (uint256) { + ProposalVote storage proposalVote = _proposalVotes[proposalId]; + + if (support > uint8(VoteType.Abstain)) { + revert GovernorInvalidVoteType(); + } + + if (proposalVote.voteReceipt[account].hasOverriden) { + revert GovernorAlreadyOverridenVote(account); + } + + uint256 snapshot = proposalSnapshot(proposalId); + uint256 overridenWeight = 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; + if (delegateCasted == 0) { + proposalVote.voteReceipt[delegate].overridenWeight += SafeCast.toUint208(overridenWeight); + } else { + uint8 delegateSupport = delegateCasted - 1; + proposalVote.votes[delegateSupport] -= overridenWeight; + emit VoteReduced(delegate, proposalId, delegateSupport, overridenWeight); + } + + return overridenWeight; + } + + /// @dev Variant of {Governor-_castVote} that deals with vote overrides. Returns the overridden weight. + function _castOverride( + uint256 proposalId, + address account, + uint8 support, + string calldata reason + ) internal virtual returns (uint256) { + _validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Active)); + + uint256 overridenWeight = _countOverride(proposalId, account, support); + + emit OverrideVoteCast(account, proposalId, support, overridenWeight, reason); + + _tallyUpdated(proposalId); + + return overridenWeight; + } + + /// @dev Public function for casting an override vote. Returns the overridden weight. + function castOverrideVote( + uint256 proposalId, + uint8 support, + string calldata reason + ) public virtual returns (uint256) { + address voter = _msgSender(); + return _castOverride(proposalId, voter, support, reason); + } + + /// @dev Public function for casting an override vote using a voter's signature. Returns the overridden weight. + function castOverrideVoteBySig( + uint256 proposalId, + uint8 support, + address voter, + string calldata reason, + bytes calldata signature + ) public virtual returns (uint256) { + bool valid = SignatureChecker.isValidSignatureNow( + voter, + _hashTypedDataV4( + keccak256( + abi.encode( + OVERRIDE_BALLOT_TYPEHASH, + proposalId, + support, + voter, + _useNonce(voter), + keccak256(bytes(reason)) + ) + ) + ), + signature + ); + + if (!valid) { + revert GovernorInvalidSignature(voter); + } + + return _castOverride(proposalId, voter, support, reason); + } +} diff --git a/contracts/governance/extensions/GovernorCountingSimple.sol b/contracts/governance/extensions/GovernorCountingSimple.sol index def29e34e61..0b89b24381d 100644 --- a/contracts/governance/extensions/GovernorCountingSimple.sol +++ b/contracts/governance/extensions/GovernorCountingSimple.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (governance/extensions/GovernorCountingSimple.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (governance/extensions/GovernorCountingSimple.sol) pragma solidity ^0.8.20; diff --git a/contracts/governance/extensions/GovernorPreventLateQuorum.sol b/contracts/governance/extensions/GovernorPreventLateQuorum.sol index ff80af648db..eb93add8099 100644 --- a/contracts/governance/extensions/GovernorPreventLateQuorum.sol +++ b/contracts/governance/extensions/GovernorPreventLateQuorum.sol @@ -44,20 +44,12 @@ abstract contract GovernorPreventLateQuorum is Governor { } /** - * @dev Casts a vote and detects if it caused quorum to be reached, potentially extending the voting period. See - * {Governor-_castVote}. + * @dev Vote tally updated and detects if it caused quorum to be reached, potentially extending the voting period. * * May emit a {ProposalExtended} event. */ - function _castVote( - uint256 proposalId, - address account, - uint8 support, - string memory reason, - bytes memory params - ) internal virtual override returns (uint256) { - uint256 result = super._castVote(proposalId, account, support, reason, params); - + function _tallyUpdated(uint256 proposalId) internal virtual override { + super._tallyUpdated(proposalId); if (_extendedDeadlines[proposalId] == 0 && _quorumReached(proposalId)) { uint48 extendedDeadline = clock() + lateQuorumVoteExtension(); @@ -67,8 +59,6 @@ abstract contract GovernorPreventLateQuorum is Governor { _extendedDeadlines[proposalId] = extendedDeadline; } - - return result; } /** diff --git a/contracts/governance/extensions/GovernorStorage.sol b/contracts/governance/extensions/GovernorStorage.sol index 2547b553bc8..22db099924a 100644 --- a/contracts/governance/extensions/GovernorStorage.sol +++ b/contracts/governance/extensions/GovernorStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (governance/extensions/GovernorStorage.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (governance/extensions/GovernorStorage.sol) pragma solidity ^0.8.20; @@ -88,7 +88,12 @@ abstract contract GovernorStorage is Governor { */ function proposalDetails( uint256 proposalId - ) public view virtual returns (address[] memory, uint256[] memory, bytes[] memory, bytes32) { + ) + public + view + virtual + returns (address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash) + { // here, using memory is more efficient than storage ProposalDetails memory details = _proposalDetails[proposalId]; if (details.descriptionHash == 0) { @@ -102,14 +107,19 @@ abstract contract GovernorStorage is Governor { */ function proposalDetailsAt( uint256 index - ) public view virtual returns (uint256, address[] memory, uint256[] memory, bytes[] memory, bytes32) { - uint256 proposalId = _proposalIds[index]; - ( + ) + public + view + virtual + returns ( + uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash - ) = proposalDetails(proposalId); - return (proposalId, targets, values, calldatas, descriptionHash); + ) + { + proposalId = _proposalIds[index]; + (targets, values, calldatas, descriptionHash) = proposalDetails(proposalId); } } diff --git a/contracts/governance/extensions/GovernorTimelockAccess.sol b/contracts/governance/extensions/GovernorTimelockAccess.sol index 6e2c5b84e3d..5b8429b8320 100644 --- a/contracts/governance/extensions/GovernorTimelockAccess.sol +++ b/contracts/governance/extensions/GovernorTimelockAccess.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (governance/extensions/GovernorTimelockAccess.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (governance/extensions/GovernorTimelockAccess.sol) pragma solidity ^0.8.20; diff --git a/contracts/governance/extensions/GovernorTimelockCompound.sol b/contracts/governance/extensions/GovernorTimelockCompound.sol index 77cf369ff49..f98edc8362e 100644 --- a/contracts/governance/extensions/GovernorTimelockCompound.sol +++ b/contracts/governance/extensions/GovernorTimelockCompound.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (governance/extensions/GovernorTimelockCompound.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (governance/extensions/GovernorTimelockCompound.sol) pragma solidity ^0.8.20; @@ -10,7 +10,7 @@ import {SafeCast} from "../../utils/math/SafeCast.sol"; /** * @dev Extension of {Governor} that binds the execution process to a Compound Timelock. This adds a delay, enforced by - * the external timelock to all successful proposal (in addition to the voting duration). The {Governor} needs to be + * the external timelock to all successful proposals (in addition to the voting duration). The {Governor} needs to be * the admin of the timelock for any operation to be performed. A public, unrestricted, * {GovernorTimelockCompound-__acceptAdmin} is available to accept ownership of the timelock. * diff --git a/contracts/governance/extensions/GovernorTimelockControl.sol b/contracts/governance/extensions/GovernorTimelockControl.sol index 52d4b429598..ba0953d1608 100644 --- a/contracts/governance/extensions/GovernorTimelockControl.sol +++ b/contracts/governance/extensions/GovernorTimelockControl.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (governance/extensions/GovernorTimelockControl.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (governance/extensions/GovernorTimelockControl.sol) pragma solidity ^0.8.20; diff --git a/contracts/governance/extensions/GovernorVotes.sol b/contracts/governance/extensions/GovernorVotes.sol index 16cd934355e..9aeaf1214a1 100644 --- a/contracts/governance/extensions/GovernorVotes.sol +++ b/contracts/governance/extensions/GovernorVotes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (governance/extensions/GovernorVotes.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (governance/extensions/GovernorVotes.sol) pragma solidity ^0.8.20; diff --git a/contracts/governance/utils/Votes.sol b/contracts/governance/utils/Votes.sol index 2b4def2e0cb..976579719de 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.0.0) (governance/utils/Votes.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (governance/utils/Votes.sol) pragma solidity ^0.8.20; import {IERC5805} from "../../interfaces/IERC5805.sol"; @@ -71,6 +71,15 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { return "mode=blocknumber&from=default"; } + /** + * @dev Validate that a timepoint is in the past, and return it as a uint48. + */ + function _validateTimepoint(uint256 timepoint) internal view returns (uint48) { + uint48 currentTimepoint = clock(); + if (timepoint >= currentTimepoint) revert ERC5805FutureLookup(timepoint, currentTimepoint); + return SafeCast.toUint48(timepoint); + } + /** * @dev Returns the current amount of votes that `account` has. */ @@ -87,11 +96,7 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ function getPastVotes(address account, uint256 timepoint) public view virtual returns (uint256) { - uint48 currentTimepoint = clock(); - if (timepoint >= currentTimepoint) { - revert ERC5805FutureLookup(timepoint, currentTimepoint); - } - return _delegateCheckpoints[account].upperLookupRecent(SafeCast.toUint48(timepoint)); + return _delegateCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint)); } /** @@ -107,11 +112,7 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. */ function getPastTotalSupply(uint256 timepoint) public view virtual returns (uint256) { - uint48 currentTimepoint = clock(); - if (timepoint >= currentTimepoint) { - revert ERC5805FutureLookup(timepoint, currentTimepoint); - } - return _totalCheckpoints.upperLookupRecent(SafeCast.toUint48(timepoint)); + return _totalCheckpoints.upperLookupRecent(_validateTimepoint(timepoint)); } /** @@ -232,7 +233,7 @@ abstract contract Votes is Context, EIP712, Nonces, IERC5805 { Checkpoints.Trace208 storage store, function(uint208, uint208) view returns (uint208) op, uint208 delta - ) private returns (uint208, uint208) { + ) private returns (uint208 oldValue, uint208 newValue) { return store.push(clock(), op(store.latest(), delta)); } diff --git a/contracts/governance/utils/VotesExtended.sol b/contracts/governance/utils/VotesExtended.sol new file mode 100644 index 00000000000..70b0d92fb75 --- /dev/null +++ b/contracts/governance/utils/VotesExtended.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Checkpoints} from "../../utils/structs/Checkpoints.sol"; +import {Votes} from "./Votes.sol"; +import {SafeCast} from "../../utils/math/SafeCast.sol"; + +/** + * @dev Extension of {Votes} that adds checkpoints for delegations and balances. + * + * WARNING: While this contract extends {Votes}, valid uses of {Votes} may not be compatible with + * {VotesExtended} without additional considerations. This implementation of {_transferVotingUnits} must + * run AFTER the voting weight movement is registered, such that it is reflected on {_getVotingUnits}. + * + * Said differently, {VotesExtended} MUST be integrated in a way that calls {_transferVotingUnits} AFTER the + * asset transfer is registered and balances are updated: + * + * ```solidity + * contract VotingToken is Token, VotesExtended { + * function transfer(address from, address to, uint256 tokenId) public override { + * super.transfer(from, to, tokenId); // <- Perform the transfer first ... + * _transferVotingUnits(from, to, 1); // <- ... then call _transferVotingUnits. + * } + * + * function _getVotingUnits(address account) internal view override returns (uint256) { + * return balanceOf(account); + * } + * } + * ``` + * + * {ERC20Votes} and {ERC721Votes} follow this pattern and are thus safe to use with {VotesExtended}. + */ +abstract contract VotesExtended is Votes { + using Checkpoints for Checkpoints.Trace160; + using Checkpoints for Checkpoints.Trace208; + + mapping(address delegator => Checkpoints.Trace160) private _userDelegationCheckpoints; + mapping(address account => Checkpoints.Trace208) private _userVotingUnitsCheckpoints; + + /** + * @dev Returns the delegate of an `account` at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. + * + * Requirements: + * + * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. + */ + function getPastDelegate(address account, uint256 timepoint) public view virtual returns (address) { + return address(_userDelegationCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint))); + } + + /** + * @dev Returns the `balanceOf` of an `account` at a specific moment in the past. If the `clock()` is + * configured to use block numbers, this will return the value at the end of the corresponding block. + * + * Requirements: + * + * - `timepoint` must be in the past. If operating using block numbers, the block must be already mined. + */ + function getPastBalanceOf(address account, uint256 timepoint) public view virtual returns (uint256) { + return _userVotingUnitsCheckpoints[account].upperLookupRecent(_validateTimepoint(timepoint)); + } + + /// @inheritdoc Votes + function _delegate(address account, address delegatee) internal virtual override { + super._delegate(account, delegatee); + + _userDelegationCheckpoints[account].push(clock(), uint160(delegatee)); + } + + /// @inheritdoc Votes + function _transferVotingUnits(address from, address to, uint256 amount) internal virtual override { + super._transferVotingUnits(from, to, amount); + if (from != to) { + if (from != address(0)) { + _userVotingUnitsCheckpoints[from].push(clock(), SafeCast.toUint208(_getVotingUnits(from))); + } + if (to != address(0)) { + _userVotingUnitsCheckpoints[to].push(clock(), SafeCast.toUint208(_getVotingUnits(to))); + } + } + } +} diff --git a/contracts/interfaces/IERC1271.sol b/contracts/interfaces/IERC1271.sol index 5f5b326336e..8c239942ac8 100644 --- a/contracts/interfaces/IERC1271.sol +++ b/contracts/interfaces/IERC1271.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1271.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1271.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC1363.sol b/contracts/interfaces/IERC1363.sol index a5246da37ca..02de2285997 100644 --- a/contracts/interfaces/IERC1363.sol +++ b/contracts/interfaces/IERC1363.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1363.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC1363Receiver.sol b/contracts/interfaces/IERC1363Receiver.sol index 279178849d6..02c065861cc 100644 --- a/contracts/interfaces/IERC1363Receiver.sol +++ b/contracts/interfaces/IERC1363Receiver.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1363Receiver.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363Receiver.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC1363Spender.sol b/contracts/interfaces/IERC1363Spender.sol index b4bf3f42c3d..13af938f066 100644 --- a/contracts/interfaces/IERC1363Spender.sol +++ b/contracts/interfaces/IERC1363Spender.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1363Spender.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1363Spender.sol) pragma solidity ^0.8.20; /** - * @title ERC1363Spender + * @title IERC1363Spender * @dev Interface for any contract that wants to support `approveAndCall` * from ERC-1363 token contracts. */ diff --git a/contracts/interfaces/IERC1820Implementer.sol b/contracts/interfaces/IERC1820Implementer.sol index 9cf941a3343..95289c65cf4 100644 --- a/contracts/interfaces/IERC1820Implementer.sol +++ b/contracts/interfaces/IERC1820Implementer.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1820Implementer.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1820Implementer.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC1820Registry.sol b/contracts/interfaces/IERC1820Registry.sol index b8f3d73998c..fa70466114d 100644 --- a/contracts/interfaces/IERC1820Registry.sol +++ b/contracts/interfaces/IERC1820Registry.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC1820Registry.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC1820Registry.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC2981.sol b/contracts/interfaces/IERC2981.sol index 22b951dd84e..db5eb5cd446 100644 --- a/contracts/interfaces/IERC2981.sol +++ b/contracts/interfaces/IERC2981.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC2981.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC2981.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC3156FlashBorrower.sol b/contracts/interfaces/IERC3156FlashBorrower.sol index 4fd10e7cfea..daafb17ee05 100644 --- a/contracts/interfaces/IERC3156FlashBorrower.sol +++ b/contracts/interfaces/IERC3156FlashBorrower.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC3156FlashBorrower.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC3156FlashBorrower.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC3156FlashLender.sol b/contracts/interfaces/IERC3156FlashLender.sol index 47208ac3112..7b1b071d4df 100644 --- a/contracts/interfaces/IERC3156FlashLender.sol +++ b/contracts/interfaces/IERC3156FlashLender.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC3156FlashLender.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC3156FlashLender.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC4626.sol b/contracts/interfaces/IERC4626.sol index 9a24507a7fd..8ebadd72fcf 100644 --- a/contracts/interfaces/IERC4626.sol +++ b/contracts/interfaces/IERC4626.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC4626.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC4626.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC4906.sol b/contracts/interfaces/IERC4906.sol index cdcace31fcb..6ecd061347b 100644 --- a/contracts/interfaces/IERC4906.sol +++ b/contracts/interfaces/IERC4906.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC4906.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC4906.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC777.sol b/contracts/interfaces/IERC777.sol index 31f05aa6421..1e672330a41 100644 --- a/contracts/interfaces/IERC777.sol +++ b/contracts/interfaces/IERC777.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC777.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC777.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC777Recipient.sol b/contracts/interfaces/IERC777Recipient.sol index 1619e112679..c377de971fc 100644 --- a/contracts/interfaces/IERC777Recipient.sol +++ b/contracts/interfaces/IERC777Recipient.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC777Recipient.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC777Recipient.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/IERC777Sender.sol b/contracts/interfaces/IERC777Sender.sol index f47a7832399..0ec8c278484 100644 --- a/contracts/interfaces/IERC777Sender.sol +++ b/contracts/interfaces/IERC777Sender.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/IERC777Sender.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/IERC777Sender.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/draft-IERC1822.sol b/contracts/interfaces/draft-IERC1822.sol index ad047dae600..f846ea6bf0e 100644 --- a/contracts/interfaces/draft-IERC1822.sol +++ b/contracts/interfaces/draft-IERC1822.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/draft-IERC1822.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/draft-IERC1822.sol) pragma solidity ^0.8.20; diff --git a/contracts/interfaces/draft-IERC4337.sol b/contracts/interfaces/draft-IERC4337.sol new file mode 100644 index 00000000000..67ab146c053 --- /dev/null +++ b/contracts/interfaces/draft-IERC4337.sol @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev A https://github.com/ethereum/ercs/blob/master/ERCS/erc-4337.md#useroperation[user operation] is composed of the following elements: + * - `sender` (`address`): The account making the operation + * - `nonce` (`uint256`): Anti-replay parameter (see “Semi-abstracted Nonce Support” ) + * - `factory` (`address`): account factory, only for new accounts + * - `factoryData` (`bytes`): data for account factory (only if account factory exists) + * - `callData` (`bytes`): The data to pass to the sender during the main execution call + * - `callGasLimit` (`uint256`): The amount of gas to allocate the main execution call + * - `verificationGasLimit` (`uint256`): The amount of gas to allocate for the verification step + * - `preVerificationGas` (`uint256`): Extra gas to pay the bundler + * - `maxFeePerGas` (`uint256`): Maximum fee per gas (similar to EIP-1559 max_fee_per_gas) + * - `maxPriorityFeePerGas` (`uint256`): Maximum priority fee per gas (similar to EIP-1559 max_priority_fee_per_gas) + * - `paymaster` (`address`): Address of paymaster contract, (or empty, if account pays for itself) + * - `paymasterVerificationGasLimit` (`uint256`): The amount of gas to allocate for the paymaster validation code + * - `paymasterPostOpGasLimit` (`uint256`): The amount of gas to allocate for the paymaster post-operation code + * - `paymasterData` (`bytes`): Data for paymaster (only if paymaster exists) + * - `signature` (`bytes`): Data passed into the account to verify authorization + * + * When passed to on-chain contacts, the following packed version is used. + * - `sender` (`address`) + * - `nonce` (`uint256`) + * - `initCode` (`bytes`): concatenation of factory address and factoryData (or empty) + * - `callData` (`bytes`) + * - `accountGasLimits` (`bytes32`): concatenation of verificationGas (16 bytes) and callGas (16 bytes) + * - `preVerificationGas` (`uint256`) + * - `gasFees` (`bytes32`): concatenation of maxPriorityFeePerGas (16 bytes) and maxFeePerGas (16 bytes) + * - `paymasterAndData` (`bytes`): concatenation of paymaster fields (or empty) + * - `signature` (`bytes`) + */ +struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; // `abi.encodePacked(factory, factoryData)` + bytes callData; + bytes32 accountGasLimits; // `abi.encodePacked(verificationGasLimit, callGasLimit)` 16 bytes each + uint256 preVerificationGas; + bytes32 gasFees; // `abi.encodePacked(maxPriorityFeePerGas, maxFeePerGas)` 16 bytes each + bytes paymasterAndData; // `abi.encodePacked(paymaster, paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData)` (20 bytes, 16 bytes, 16 bytes, dynamic) + bytes signature; +} + +/** + * @dev Aggregates and validates multiple signatures for a batch of user operations. + * + * A contract could implement this interface with custom validation schemes that allow signature aggregation, + * enabling significant optimizations and gas savings for execution and transaction data cost. + * + * Bundlers and clients whitelist supported aggregators. + * + * See https://eips.ethereum.org/EIPS/eip-7766[ERC-7766] + */ +interface IAggregator { + /** + * @dev Validates the signature for a user operation. + * Returns an alternative signature that should be used during bundling. + */ + function validateUserOpSignature( + PackedUserOperation calldata userOp + ) external view returns (bytes memory sigForUserOp); + + /** + * @dev Returns an aggregated signature for a batch of user operation's signatures. + */ + function aggregateSignatures( + PackedUserOperation[] calldata userOps + ) external view returns (bytes memory aggregatesSignature); + + /** + * @dev Validates that the aggregated signature is valid for the user operations. + * + * Requirements: + * + * - The aggregated signature MUST match the given list of operations. + */ + function validateSignatures(PackedUserOperation[] calldata userOps, bytes calldata signature) external view; +} + +/** + * @dev Handle nonce management for accounts. + * + * Nonces are used in accounts as a replay protection mechanism and to ensure the order of user operations. + * To avoid limiting the number of operations an account can perform, the interface allows using parallel + * nonces by using a `key` parameter. + * + * See https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337 semi-abstracted nonce support]. + */ +interface IEntryPointNonces { + /** + * @dev Returns the nonce for a `sender` account and a `key`. + * + * Nonces for a certain `key` are always increasing. + */ + function getNonce(address sender, uint192 key) external view returns (uint256 nonce); +} + +/** + * @dev Handle stake management for entities (i.e. accounts, paymasters, factories). + * + * The EntryPoint must implement the following API to let entities like paymasters have a stake, + * and thus have more flexibility in their storage access + * (see https://eips.ethereum.org/EIPS/eip-4337#reputation-scoring-and-throttlingbanning-for-global-entities[reputation, throttling and banning.]) + */ +interface IEntryPointStake { + /** + * @dev Returns the balance of the account. + */ + function balanceOf(address account) external view returns (uint256); + + /** + * @dev Deposits `msg.value` to the account. + */ + function depositTo(address account) external payable; + + /** + * @dev Withdraws `withdrawAmount` from the account to `withdrawAddress`. + */ + function withdrawTo(address payable withdrawAddress, uint256 withdrawAmount) external; + + /** + * @dev Adds stake to the account with an unstake delay of `unstakeDelaySec`. + */ + function addStake(uint32 unstakeDelaySec) external payable; + + /** + * @dev Unlocks the stake of the account. + */ + function unlockStake() external; + + /** + * @dev Withdraws the stake of the account to `withdrawAddress`. + */ + function withdrawStake(address payable withdrawAddress) external; +} + +/** + * @dev Entry point for user operations. + * + * User operations are validated and executed by this contract. + */ +interface IEntryPoint is IEntryPointNonces, IEntryPointStake { + /** + * @dev A user operation at `opIndex` failed with `reason`. + */ + error FailedOp(uint256 opIndex, string reason); + + /** + * @dev A user operation at `opIndex` failed with `reason` and `inner` returned data. + */ + error FailedOpWithRevert(uint256 opIndex, string reason, bytes inner); + + /** + * @dev Batch of aggregated user operations per aggregator. + */ + struct UserOpsPerAggregator { + PackedUserOperation[] userOps; + IAggregator aggregator; + bytes signature; + } + + /** + * @dev Executes a batch of user operations. + * @param beneficiary Address to which gas is refunded up completing the execution. + */ + function handleOps(PackedUserOperation[] calldata ops, address payable beneficiary) external; + + /** + * @dev Executes a batch of aggregated user operations per aggregator. + * @param beneficiary Address to which gas is refunded up completing the execution. + */ + function handleAggregatedOps( + UserOpsPerAggregator[] calldata opsPerAggregator, + address payable beneficiary + ) external; +} + +/** + * @dev Base interface for an ERC-4337 account. + */ +interface IAccount { + /** + * @dev Validates a user operation. + * + * * MUST validate the caller is a trusted EntryPoint + * * MUST validate that the signature is a valid signature of the userOpHash, and SHOULD + * return SIG_VALIDATION_FAILED (and not revert) on signature mismatch. Any other error MUST revert. + * * MUST pay the entryPoint (caller) at least the “missingAccountFunds” (which might + * be zero, in case the current account’s deposit is high enough) + * + * Returns an encoded packed validation data that is composed of the following elements: + * + * - `authorizer` (`address`): 0 for success, 1 for failure, otherwise the address of an authorizer contract + * - `validUntil` (`uint48`): The UserOp is valid only up to this time. Zero for “infinite”. + * - `validAfter` (`uint48`): The UserOp is valid only after this time. + */ + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external returns (uint256 validationData); +} + +/** + * @dev Support for executing user operations by prepending the {executeUserOp} function selector + * to the UserOperation's `callData`. + */ +interface IAccountExecute { + /** + * @dev Executes a user operation. + */ + function executeUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external; +} + +/** + * @dev Interface for a paymaster contract that agrees to pay for the gas costs of a user operation. + * + * NOTE: A paymaster must hold a stake to cover the required entrypoint stake and also the gas for the transaction. + */ +interface IPaymaster { + enum PostOpMode { + opSucceeded, + opReverted, + postOpReverted + } + + /** + * @dev Validates whether the paymaster is willing to pay for the user operation. See + * {IAccount-validateUserOp} for additional information on the return value. + * + * NOTE: Bundlers will reject this method if it modifies the state, unless it's whitelisted. + */ + function validatePaymasterUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 maxCost + ) external returns (bytes memory context, uint256 validationData); + + /** + * @dev Verifies the sender is the entrypoint. + * @param actualGasCost the actual amount paid (by account or paymaster) for this UserOperation + * @param actualUserOpFeePerGas total gas used by this UserOperation (including preVerification, creation, validation and execution) + */ + function postOp( + PostOpMode mode, + bytes calldata context, + uint256 actualGasCost, + uint256 actualUserOpFeePerGas + ) external; +} diff --git a/contracts/interfaces/draft-IERC6093.sol b/contracts/interfaces/draft-IERC6093.sol index 75fd75643e6..3227fd624fc 100644 --- a/contracts/interfaces/draft-IERC6093.sol +++ b/contracts/interfaces/draft-IERC6093.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (interfaces/draft-IERC6093.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/draft-IERC6093.sol) pragma solidity ^0.8.20; /** diff --git a/contracts/interfaces/draft-IERC7579.sol b/contracts/interfaces/draft-IERC7579.sol new file mode 100644 index 00000000000..f97e33dc045 --- /dev/null +++ b/contracts/interfaces/draft-IERC7579.sol @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {PackedUserOperation} from "./draft-IERC4337.sol"; + +uint256 constant VALIDATION_SUCCESS = 0; +uint256 constant VALIDATION_FAILED = 1; +uint256 constant MODULE_TYPE_VALIDATOR = 1; +uint256 constant MODULE_TYPE_EXECUTOR = 2; +uint256 constant MODULE_TYPE_FALLBACK = 3; +uint256 constant MODULE_TYPE_HOOK = 4; + +/// @dev Minimal configuration interface for ERC-7579 modules +interface IERC7579Module { + /** + * @dev This function is called by the smart account during installation of the module + * @param data arbitrary data that may be required on the module during `onInstall` initialization + * + * MUST revert on error (e.g. if module is already enabled) + */ + function onInstall(bytes calldata data) external; + + /** + * @dev This function is called by the smart account during uninstallation of the module + * @param data arbitrary data that may be required on the module during `onUninstall` de-initialization + * + * MUST revert on error + */ + function onUninstall(bytes calldata data) external; + + /** + * @dev Returns boolean value if module is a certain type + * @param moduleTypeId the module type ID according the ERC-7579 spec + * + * MUST return true if the module is of the given type and false otherwise + */ + function isModuleType(uint256 moduleTypeId) external view returns (bool); +} + +/** + * @dev ERC-7579 Validation module (type 1). + * + * A module that implements logic to validate user operations and signatures. + */ +interface IERC7579Validator is IERC7579Module { + /** + * @dev Validates a UserOperation + * @param userOp the ERC-4337 PackedUserOperation + * @param userOpHash the hash of the ERC-4337 PackedUserOperation + * + * MUST validate that the signature is a valid signature of the userOpHash + * SHOULD return ERC-4337's SIG_VALIDATION_FAILED (and not revert) on signature mismatch + * See {IAccount-validateUserOp} for additional information on the return value + */ + function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash) external returns (uint256); + + /** + * @dev Validates a signature using ERC-1271 + * @param sender the address that sent the ERC-1271 request to the smart account + * @param hash the hash of the ERC-1271 request + * @param signature the signature of the ERC-1271 request + * + * MUST return the ERC-1271 `MAGIC_VALUE` if the signature is valid + * MUST NOT modify state + */ + function isValidSignatureWithSender( + address sender, + bytes32 hash, + bytes calldata signature + ) external view returns (bytes4); +} + +/** + * @dev ERC-7579 Hooks module (type 4). + * + * A module that implements logic to execute before and after the account executes a user operation, + * either individually or batched. + */ +interface IERC7579Hook is IERC7579Module { + /** + * @dev Called by the smart account before execution + * @param msgSender the address that called the smart account + * @param value the value that was sent to the smart account + * @param msgData the data that was sent to the smart account + * + * MAY return arbitrary data in the `hookData` return value + */ + function preCheck( + address msgSender, + uint256 value, + bytes calldata msgData + ) external returns (bytes memory hookData); + + /** + * @dev Called by the smart account after execution + * @param hookData the data that was returned by the `preCheck` function + * + * MAY validate the `hookData` to validate transaction context of the `preCheck` function + */ + function postCheck(bytes calldata hookData) external; +} + +struct Execution { + address target; + uint256 value; + bytes callData; +} + +/** + * @dev ERC-7579 Execution. + * + * Accounts should implement this interface so that the Entrypoint and ERC-7579 modules can execute operations. + */ +interface IERC7579Execution { + /** + * @dev Executes a transaction on behalf of the account. + * @param mode The encoded execution mode of the transaction. See ModeLib.sol for details + * @param executionCalldata The encoded execution call data + * + * MUST ensure adequate authorization control: e.g. onlyEntryPointOrSelf if used with ERC-4337 + * If a mode is requested that is not supported by the Account, it MUST revert + */ + function execute(bytes32 mode, bytes calldata executionCalldata) external; + + /** + * @dev Executes a transaction on behalf of the account. + * This function is intended to be called by Executor Modules + * @param mode The encoded execution mode of the transaction. See ModeLib.sol for details + * @param executionCalldata The encoded execution call data + * @return returnData An array with the returned data of each executed subcall + * + * MUST ensure adequate authorization control: i.e. onlyExecutorModule + * If a mode is requested that is not supported by the Account, it MUST revert + */ + function executeFromExecutor( + bytes32 mode, + bytes calldata executionCalldata + ) external returns (bytes[] memory returnData); +} + +/** + * @dev ERC-7579 Account Config. + * + * Accounts should implement this interface to expose information that identifies the account, supported modules and capabilities. + */ +interface IERC7579AccountConfig { + /** + * @dev Returns the account id of the smart account + * @return accountImplementationId the account id of the smart account + * + * MUST return a non-empty string + * The accountId SHOULD be structured like so: + * "vendorname.accountname.semver" + * The id SHOULD be unique across all smart accounts + */ + function accountId() external view returns (string memory accountImplementationId); + + /** + * @dev Function to check if the account supports a certain execution mode (see above) + * @param encodedMode the encoded mode + * + * MUST return true if the account supports the mode and false otherwise + */ + function supportsExecutionMode(bytes32 encodedMode) external view returns (bool); + + /** + * @dev Function to check if the account supports a certain module typeId + * @param moduleTypeId the module type ID according to the ERC-7579 spec + * + * MUST return true if the account supports the module type and false otherwise + */ + function supportsModule(uint256 moduleTypeId) external view returns (bool); +} + +/** + * @dev ERC-7579 Module Config. + * + * Accounts should implement this interface to allow installing and uninstalling modules. + */ +interface IERC7579ModuleConfig { + event ModuleInstalled(uint256 moduleTypeId, address module); + event ModuleUninstalled(uint256 moduleTypeId, address module); + + /** + * @dev Installs a Module of a certain type on the smart account + * @param moduleTypeId the module type ID according to the ERC-7579 spec + * @param module the module address + * @param initData arbitrary data that may be required on the module during `onInstall` + * initialization. + * + * MUST implement authorization control + * MUST call `onInstall` on the module with the `initData` parameter if provided + * MUST emit ModuleInstalled event + * MUST revert if the module is already installed or the initialization on the module failed + */ + function installModule(uint256 moduleTypeId, address module, bytes calldata initData) external; + + /** + * @dev Uninstalls a Module of a certain type on the smart account + * @param moduleTypeId the module type ID according the ERC-7579 spec + * @param module the module address + * @param deInitData arbitrary data that may be required on the module during `onInstall` + * initialization. + * + * MUST implement authorization control + * MUST call `onUninstall` on the module with the `deInitData` parameter if provided + * MUST emit ModuleUninstalled event + * MUST revert if the module is not installed or the deInitialization on the module failed + */ + function uninstallModule(uint256 moduleTypeId, address module, bytes calldata deInitData) external; + + /** + * @dev Returns whether a module is installed on the smart account + * @param moduleTypeId the module type ID according the ERC-7579 spec + * @param module the module address + * @param additionalContext arbitrary data that may be required to determine if the module is installed + * + * MUST return true if the module is installed and false otherwise + */ + function isModuleInstalled( + uint256 moduleTypeId, + address module, + bytes calldata additionalContext + ) external view returns (bool); +} diff --git a/contracts/interfaces/draft-IERC7674.sol b/contracts/interfaces/draft-IERC7674.sol index 949ec806e84..be3c413ec36 100644 --- a/contracts/interfaces/draft-IERC7674.sol +++ b/contracts/interfaces/draft-IERC7674.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (interfaces/draft-IERC7674.sol) pragma solidity ^0.8.20; diff --git a/contracts/metatx/ERC2771Context.sol b/contracts/metatx/ERC2771Context.sol index d448b24b128..794bfb3e5ef 100644 --- a/contracts/metatx/ERC2771Context.sol +++ b/contracts/metatx/ERC2771Context.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.1) (metatx/ERC2771Context.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (metatx/ERC2771Context.sol) pragma solidity ^0.8.20; diff --git a/contracts/metatx/ERC2771Forwarder.sol b/contracts/metatx/ERC2771Forwarder.sol index b66e7894b2c..4a069874ecb 100644 --- a/contracts/metatx/ERC2771Forwarder.sol +++ b/contracts/metatx/ERC2771Forwarder.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (metatx/ERC2771Forwarder.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (metatx/ERC2771Forwarder.sol) pragma solidity ^0.8.20; @@ -218,7 +218,7 @@ contract ERC2771Forwarder is EIP712, Nonces { */ function _recoverForwardRequestSigner( ForwardRequestData calldata request - ) internal view virtual returns (bool, address) { + ) internal view virtual returns (bool isValid, address signer) { (address recovered, ECDSA.RecoverError err, ) = _hashTypedDataV4( keccak256( abi.encode( diff --git a/contracts/mocks/DummyImplementation.sol b/contracts/mocks/DummyImplementation.sol index 4925c89df4b..0f1147407f5 100644 --- a/contracts/mocks/DummyImplementation.sol +++ b/contracts/mocks/DummyImplementation.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol"; import {StorageSlot} from "../utils/StorageSlot.sol"; diff --git a/contracts/mocks/MerkleTreeMock.sol b/contracts/mocks/MerkleTreeMock.sol index 2454efa2f91..dcde6b65884 100644 --- a/contracts/mocks/MerkleTreeMock.sol +++ b/contracts/mocks/MerkleTreeMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {MerkleTree} from "../utils/structs/MerkleTree.sol"; diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 846c77d98e8..0bc89e4ceb7 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.24; // We keep these imports and a dummy contract just to we can run the test suite after transpilation. @@ -9,6 +9,9 @@ import {Arrays} from "../utils/Arrays.sol"; import {AuthorityUtils} from "../access/manager/AuthorityUtils.sol"; import {Base64} from "../utils/Base64.sol"; import {BitMaps} from "../utils/structs/BitMaps.sol"; +import {Bytes} from "../utils/Bytes.sol"; +import {CAIP2} from "../utils/CAIP2.sol"; +import {CAIP10} from "../utils/CAIP10.sol"; import {Checkpoints} from "../utils/structs/Checkpoints.sol"; import {CircularBuffer} from "../utils/structs/CircularBuffer.sol"; import {Clones} from "../proxy/Clones.sol"; @@ -22,10 +25,14 @@ import {ERC165} from "../utils/introspection/ERC165.sol"; import {ERC165Checker} from "../utils/introspection/ERC165Checker.sol"; import {ERC1967Utils} from "../proxy/ERC1967/ERC1967Utils.sol"; import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol"; +import {ERC4337Utils} from "../account/utils/draft-ERC4337Utils.sol"; +import {ERC7579Utils} from "../account/utils/draft-ERC7579Utils.sol"; import {Heap} from "../utils/structs/Heap.sol"; import {Math} from "../utils/math/Math.sol"; import {MerkleProof} from "../utils/cryptography/MerkleProof.sol"; import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol"; +import {Nonces} from "../utils/Nonces.sol"; +import {NoncesKeyed} from "../utils/NoncesKeyed.sol"; import {P256} from "../utils/cryptography/P256.sol"; import {Panic} from "../utils/Panic.sol"; import {Packing} from "../utils/Packing.sol"; diff --git a/contracts/mocks/StorageSlotMock.sol b/contracts/mocks/StorageSlotMock.sol index e69bd36a249..ec176e21fb5 100644 --- a/contracts/mocks/StorageSlotMock.sol +++ b/contracts/mocks/StorageSlotMock.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // This file was procedurally generated from scripts/generate/templates/StorageSlotMock.js. -pragma solidity ^0.8.24; +pragma solidity ^0.8.20; import {Multicall} from "../utils/Multicall.sol"; import {StorageSlot} from "../utils/StorageSlot.sol"; @@ -84,54 +84,4 @@ contract StorageSlotMock is Multicall { function getBytesStorage(uint256 key) public view returns (bytes memory) { return bytesMap[key].getBytesSlot().value; } - - event AddressValue(bytes32 slot, address value); - - function tloadAddress(bytes32 slot) public { - emit AddressValue(slot, slot.asAddress().tload()); - } - - function tstore(bytes32 slot, address value) public { - slot.asAddress().tstore(value); - } - - event BooleanValue(bytes32 slot, bool value); - - function tloadBoolean(bytes32 slot) public { - emit BooleanValue(slot, slot.asBoolean().tload()); - } - - function tstore(bytes32 slot, bool value) public { - slot.asBoolean().tstore(value); - } - - event Bytes32Value(bytes32 slot, bytes32 value); - - function tloadBytes32(bytes32 slot) public { - emit Bytes32Value(slot, slot.asBytes32().tload()); - } - - function tstore(bytes32 slot, bytes32 value) public { - slot.asBytes32().tstore(value); - } - - event Uint256Value(bytes32 slot, uint256 value); - - function tloadUint256(bytes32 slot) public { - emit Uint256Value(slot, slot.asUint256().tload()); - } - - function tstore(bytes32 slot, uint256 value) public { - slot.asUint256().tstore(value); - } - - event Int256Value(bytes32 slot, int256 value); - - function tloadInt256(bytes32 slot) public { - emit Int256Value(slot, slot.asInt256().tload()); - } - - function tstore(bytes32 slot, int256 value) public { - slot.asInt256().tstore(value); - } } diff --git a/contracts/mocks/TransientSlotMock.sol b/contracts/mocks/TransientSlotMock.sol new file mode 100644 index 00000000000..6b18fa52f29 --- /dev/null +++ b/contracts/mocks/TransientSlotMock.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +// This file was procedurally generated from scripts/generate/templates/TransientSlotMock.js. + +pragma solidity ^0.8.24; + +import {Multicall} from "../utils/Multicall.sol"; +import {TransientSlot} from "../utils/TransientSlot.sol"; + +contract TransientSlotMock is Multicall { + using TransientSlot for *; + + event AddressValue(bytes32 slot, address value); + + function tloadAddress(bytes32 slot) public { + emit AddressValue(slot, slot.asAddress().tload()); + } + + function tstore(bytes32 slot, address value) public { + slot.asAddress().tstore(value); + } + + event BooleanValue(bytes32 slot, bool value); + + function tloadBoolean(bytes32 slot) public { + emit BooleanValue(slot, slot.asBoolean().tload()); + } + + function tstore(bytes32 slot, bool value) public { + slot.asBoolean().tstore(value); + } + + event Bytes32Value(bytes32 slot, bytes32 value); + + function tloadBytes32(bytes32 slot) public { + emit Bytes32Value(slot, slot.asBytes32().tload()); + } + + function tstore(bytes32 slot, bytes32 value) public { + slot.asBytes32().tstore(value); + } + + event Uint256Value(bytes32 slot, uint256 value); + + function tloadUint256(bytes32 slot) public { + emit Uint256Value(slot, slot.asUint256().tload()); + } + + function tstore(bytes32 slot, uint256 value) public { + slot.asUint256().tstore(value); + } + + event Int256Value(bytes32 slot, int256 value); + + function tloadInt256(bytes32 slot) public { + emit Int256Value(slot, slot.asInt256().tload()); + } + + function tstore(bytes32 slot, int256 value) public { + slot.asInt256().tstore(value); + } +} diff --git a/contracts/mocks/VotesExtendedMock.sol b/contracts/mocks/VotesExtendedMock.sol new file mode 100644 index 00000000000..9c456190e97 --- /dev/null +++ b/contracts/mocks/VotesExtendedMock.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {VotesExtended} from "../governance/utils/VotesExtended.sol"; + +abstract contract VotesExtendedMock is VotesExtended { + mapping(address voter => uint256) private _votingUnits; + + function getTotalSupply() public view returns (uint256) { + return _getTotalSupply(); + } + + function delegate(address account, address newDelegation) public { + return _delegate(account, newDelegation); + } + + function _getVotingUnits(address account) internal view override returns (uint256) { + return _votingUnits[account]; + } + + function _mint(address account, uint256 votes) internal { + _votingUnits[account] += votes; + _transferVotingUnits(address(0), account, votes); + } + + function _burn(address account, uint256 votes) internal { + _votingUnits[account] += votes; + _transferVotingUnits(account, address(0), votes); + } +} + +abstract contract VotesExtendedTimestampMock is VotesExtendedMock { + function clock() public view override returns (uint48) { + return uint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + return "mode=timestamp"; + } +} diff --git a/contracts/mocks/account/utils/ERC7579UtilsMock.sol b/contracts/mocks/account/utils/ERC7579UtilsMock.sol new file mode 100644 index 00000000000..e0a1e1a50ea --- /dev/null +++ b/contracts/mocks/account/utils/ERC7579UtilsMock.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {CallType, ExecType, ModeSelector, ModePayload} from "../../../account/utils/draft-ERC7579Utils.sol"; + +contract ERC7579UtilsGlobalMock { + function eqCallTypeGlobal(CallType callType1, CallType callType2) internal pure returns (bool) { + return callType1 == callType2; + } + + function eqExecTypeGlobal(ExecType execType1, ExecType execType2) internal pure returns (bool) { + return execType1 == execType2; + } + + function eqModeSelectorGlobal(ModeSelector modeSelector1, ModeSelector modeSelector2) internal pure returns (bool) { + return modeSelector1 == modeSelector2; + } + + function eqModePayloadGlobal(ModePayload modePayload1, ModePayload modePayload2) internal pure returns (bool) { + return modePayload1 == modePayload2; + } +} diff --git a/contracts/mocks/docs/access-control/AccessControlUnrevokableAdmin.sol b/contracts/mocks/docs/access-control/AccessControlNonRevokableAdmin.sol similarity index 100% rename from contracts/mocks/docs/access-control/AccessControlUnrevokableAdmin.sol rename to contracts/mocks/docs/access-control/AccessControlNonRevokableAdmin.sol diff --git a/contracts/mocks/governance/GovernorCountingOverridableMock.sol b/contracts/mocks/governance/GovernorCountingOverridableMock.sol new file mode 100644 index 00000000000..bae09d933ad --- /dev/null +++ b/contracts/mocks/governance/GovernorCountingOverridableMock.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Governor} from "../../governance/Governor.sol"; +import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol"; +import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol"; +import {GovernorCountingOverridable, VotesExtended} from "../../governance/extensions/GovernorCountingOverridable.sol"; + +abstract contract GovernorCountingOverridableMock is + GovernorSettings, + GovernorVotesQuorumFraction, + GovernorCountingOverridable +{ + function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { + return super.proposalThreshold(); + } +} diff --git a/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol b/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol index fde0863ce86..176976f9199 100644 --- a/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol +++ b/contracts/mocks/governance/GovernorPreventLateQuorumMock.sol @@ -34,13 +34,7 @@ abstract contract GovernorPreventLateQuorumMock is return super.proposalThreshold(); } - function _castVote( - uint256 proposalId, - address account, - uint8 support, - string memory reason, - bytes memory params - ) internal override(Governor, GovernorPreventLateQuorum) returns (uint256) { - return super._castVote(proposalId, account, support, reason, params); + function _tallyUpdated(uint256 proposalId) internal override(Governor, GovernorPreventLateQuorum) { + super._tallyUpdated(proposalId); } } diff --git a/contracts/mocks/governance/GovernorStorageMock.sol b/contracts/mocks/governance/GovernorStorageMock.sol index 88c6bf906bf..26e0e10b57a 100644 --- a/contracts/mocks/governance/GovernorStorageMock.sol +++ b/contracts/mocks/governance/GovernorStorageMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.20; import {IGovernor, Governor} from "../../governance/Governor.sol"; import {GovernorTimelockControl} from "../../governance/extensions/GovernorTimelockControl.sol"; diff --git a/contracts/mocks/proxy/UUPSUpgradeableMock.sol b/contracts/mocks/proxy/UUPSUpgradeableMock.sol index a5f2d4a25d8..8c5641e6ca3 100644 --- a/contracts/mocks/proxy/UUPSUpgradeableMock.sol +++ b/contracts/mocks/proxy/UUPSUpgradeableMock.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {UUPSUpgradeable} from "../../proxy/utils/UUPSUpgradeable.sol"; import {ERC1967Utils} from "../../proxy/ERC1967/ERC1967Utils.sol"; diff --git a/contracts/mocks/token/ERC20VotesAdditionalCheckpointsMock.sol b/contracts/mocks/token/ERC20VotesAdditionalCheckpointsMock.sol new file mode 100644 index 00000000000..39b3c654b17 --- /dev/null +++ b/contracts/mocks/token/ERC20VotesAdditionalCheckpointsMock.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {ERC20Votes} from "../../token/ERC20/extensions/ERC20Votes.sol"; +import {VotesExtended, Votes} from "../../governance/utils/VotesExtended.sol"; +import {SafeCast} from "../../utils/math/SafeCast.sol"; + +abstract contract ERC20VotesExtendedMock is ERC20Votes, VotesExtended { + function _delegate(address account, address delegatee) internal virtual override(Votes, VotesExtended) { + return super._delegate(account, delegatee); + } + + function _transferVotingUnits( + address from, + address to, + uint256 amount + ) internal virtual override(Votes, VotesExtended) { + return super._transferVotingUnits(from, to, amount); + } +} + +abstract contract ERC20VotesExtendedTimestampMock is ERC20VotesExtendedMock { + function clock() public view virtual override returns (uint48) { + return SafeCast.toUint48(block.timestamp); + } + + // solhint-disable-next-line func-name-mixedcase + function CLOCK_MODE() public view virtual override returns (string memory) { + return "mode=timestamp"; + } +} diff --git a/contracts/package.json b/contracts/package.json index 845e8c4035d..e0ed163d05a 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.0.2", + "version": "5.1.0", "files": [ "**/*.sol", "/build/contracts/*.json", diff --git a/contracts/proxy/Clones.sol b/contracts/proxy/Clones.sol index 454d7fb97d2..5bd45a0864b 100644 --- a/contracts/proxy/Clones.sol +++ b/contracts/proxy/Clones.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (proxy/Clones.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (proxy/Clones.sol) pragma solidity ^0.8.20; @@ -57,7 +57,7 @@ library Clones { * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`. * * This function uses the create2 opcode and a `salt` to deterministically deploy - * the clone. Using the same `implementation` and `salt` multiple time will revert, since + * the clone. Using the same `implementation` and `salt` multiple times will revert, since * the clones cannot be deployed twice at the same address. */ function cloneDeterministic(address implementation, bytes32 salt) internal returns (address instance) { @@ -163,8 +163,8 @@ library Clones { * access the arguments within the implementation, use {fetchCloneArgs}. * * This function uses the create2 opcode and a `salt` to deterministically deploy the clone. Using the same - * `implementation` and `salt` multiple time will revert, since the clones cannot be deployed twice at the same - * address. + * `implementation`, `args` and `salt` multiple times will revert, since the clones cannot be deployed twice + * at the same address. */ function cloneDeterministicWithImmutableArgs( address implementation, @@ -227,9 +227,9 @@ library Clones { * function should only be used to check addresses that are known to be clones. */ function fetchCloneArgs(address instance) internal view returns (bytes memory) { - bytes memory result = new bytes(instance.code.length - 0x2d); // revert if length is too short + bytes memory result = new bytes(instance.code.length - 45); // revert if length is too short assembly ("memory-safe") { - extcodecopy(instance, add(result, 0x20), 0x2d, mload(result)) + extcodecopy(instance, add(result, 32), 45, mload(result)) } return result; } @@ -248,11 +248,11 @@ library Clones { address implementation, bytes memory args ) private pure returns (bytes memory) { - if (args.length > 0x5fd3) revert CloneArgumentsTooLong(); + if (args.length > 24531) revert CloneArgumentsTooLong(); return abi.encodePacked( hex"61", - uint16(args.length + 0x2d), + uint16(args.length + 45), hex"3d81600a3d39f3363d3d373d3d3d363d73", implementation, hex"5af43d82803e903d91602b57fd5bf3", diff --git a/contracts/proxy/ERC1967/ERC1967Proxy.sol b/contracts/proxy/ERC1967/ERC1967Proxy.sol index 8f6b717a56f..cad9eb5ab7e 100644 --- a/contracts/proxy/ERC1967/ERC1967Proxy.sol +++ b/contracts/proxy/ERC1967/ERC1967Proxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (proxy/ERC1967/ERC1967Proxy.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (proxy/ERC1967/ERC1967Proxy.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {Proxy} from "../Proxy.sol"; import {ERC1967Utils} from "./ERC1967Utils.sol"; diff --git a/contracts/proxy/ERC1967/ERC1967Utils.sol b/contracts/proxy/ERC1967/ERC1967Utils.sol index cf555098af9..287bb6beee2 100644 --- a/contracts/proxy/ERC1967/ERC1967Utils.sol +++ b/contracts/proxy/ERC1967/ERC1967Utils.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (proxy/ERC1967/ERC1967Utils.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (proxy/ERC1967/ERC1967Utils.sol) -pragma solidity ^0.8.21; +pragma solidity ^0.8.22; import {IBeacon} from "../beacon/IBeacon.sol"; import {IERC1967} from "../../interfaces/IERC1967.sol"; diff --git a/contracts/proxy/beacon/BeaconProxy.sol b/contracts/proxy/beacon/BeaconProxy.sol index 9b3f627b19a..e38b9d891cf 100644 --- a/contracts/proxy/beacon/BeaconProxy.sol +++ b/contracts/proxy/beacon/BeaconProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (proxy/beacon/BeaconProxy.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (proxy/beacon/BeaconProxy.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {IBeacon} from "./IBeacon.sol"; import {Proxy} from "../Proxy.sol"; diff --git a/contracts/proxy/transparent/ProxyAdmin.sol b/contracts/proxy/transparent/ProxyAdmin.sol index 7fbdca6fd7e..2a60edfe987 100644 --- a/contracts/proxy/transparent/ProxyAdmin.sol +++ b/contracts/proxy/transparent/ProxyAdmin.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (proxy/transparent/ProxyAdmin.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (proxy/transparent/ProxyAdmin.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {ITransparentUpgradeableProxy} from "./TransparentUpgradeableProxy.sol"; import {Ownable} from "../../access/Ownable.sol"; diff --git a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol index 6344eb9fa5f..7342d9f8f0a 100644 --- a/contracts/proxy/transparent/TransparentUpgradeableProxy.sol +++ b/contracts/proxy/transparent/TransparentUpgradeableProxy.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (proxy/transparent/TransparentUpgradeableProxy.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (proxy/transparent/TransparentUpgradeableProxy.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {ERC1967Utils} from "../ERC1967/ERC1967Utils.sol"; import {ERC1967Proxy} from "../ERC1967/ERC1967Proxy.sol"; @@ -15,12 +15,7 @@ import {ProxyAdmin} from "./ProxyAdmin.sol"; * include them in the ABI so this interface must be used to interact with it. */ interface ITransparentUpgradeableProxy is IERC1967 { - /** - * @dev Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call - * encoded in `data`. - * - * See {UUPSUpgradeable-upgradeToAndCall} - */ + /// @dev See {UUPSUpgradeable-upgradeToAndCall} function upgradeToAndCall(address newImplementation, bytes calldata data) external payable; } diff --git a/contracts/proxy/utils/UUPSUpgradeable.sol b/contracts/proxy/utils/UUPSUpgradeable.sol index 20eb1f72604..745c56fa5d2 100644 --- a/contracts/proxy/utils/UUPSUpgradeable.sol +++ b/contracts/proxy/utils/UUPSUpgradeable.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (proxy/utils/UUPSUpgradeable.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (proxy/utils/UUPSUpgradeable.sol) -pragma solidity ^0.8.20; +pragma solidity ^0.8.22; import {IERC1822Proxiable} from "../../interfaces/draft-IERC1822.sol"; import {ERC1967Utils} from "../ERC1967/ERC1967Utils.sol"; diff --git a/contracts/token/ERC1155/ERC1155.sol b/contracts/token/ERC1155/ERC1155.sol index b0e7e547ab2..3e0a91f8bcc 100644 --- a/contracts/token/ERC1155/ERC1155.sol +++ b/contracts/token/ERC1155/ERC1155.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/ERC1155.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC1155/ERC1155.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC1155/IERC1155.sol b/contracts/token/ERC1155/IERC1155.sol index 5a1805fadee..0da320fbefb 100644 --- a/contracts/token/ERC1155/IERC1155.sol +++ b/contracts/token/ERC1155/IERC1155.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.1) (token/ERC1155/IERC1155.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC1155/IERC1155.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC1155/IERC1155Receiver.sol b/contracts/token/ERC1155/IERC1155Receiver.sol index 36ad4c75206..7d9bc239719 100644 --- a/contracts/token/ERC1155/IERC1155Receiver.sol +++ b/contracts/token/ERC1155/IERC1155Receiver.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/IERC1155Receiver.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC1155/IERC1155Receiver.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol index e99cf2aa95d..a0de999f0cf 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Pausable.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Pausable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/extensions/ERC1155Pausable.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC1155/extensions/ERC1155Pausable.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC1155/extensions/ERC1155Supply.sol b/contracts/token/ERC1155/extensions/ERC1155Supply.sol index 33c5b103a95..00dd082a383 100644 --- a/contracts/token/ERC1155/extensions/ERC1155Supply.sol +++ b/contracts/token/ERC1155/extensions/ERC1155Supply.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/extensions/ERC1155Supply.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC1155/extensions/ERC1155Supply.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol index e8436e830d7..5abf319d327 100644 --- a/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol +++ b/contracts/token/ERC1155/extensions/ERC1155URIStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/extensions/ERC1155URIStorage.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC1155/extensions/ERC1155URIStorage.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol b/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol index ea07897b9f2..b413f4304d3 100644 --- a/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol +++ b/contracts/token/ERC1155/extensions/IERC1155MetadataURI.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/extensions/IERC1155MetadataURI.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC1155/extensions/IERC1155MetadataURI.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC1155/utils/ERC1155Holder.sol b/contracts/token/ERC1155/utils/ERC1155Holder.sol index 35be58c5238..7ad5943aceb 100644 --- a/contracts/token/ERC1155/utils/ERC1155Holder.sol +++ b/contracts/token/ERC1155/utils/ERC1155Holder.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC1155/utils/ERC1155Holder.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC1155/utils/ERC1155Holder.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC1155/utils/ERC1155Utils.sol b/contracts/token/ERC1155/utils/ERC1155Utils.sol index 1439330e41c..371cd86ba46 100644 --- a/contracts/token/ERC1155/utils/ERC1155Utils.sol +++ b/contracts/token/ERC1155/utils/ERC1155Utils.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC1155/utils/ERC1155Utils.sol) pragma solidity ^0.8.20; @@ -9,6 +10,8 @@ import {IERC1155Errors} from "../../../interfaces/draft-IERC6093.sol"; * @dev Library that provide common ERC-1155 utility functions. * * See https://eips.ethereum.org/EIPS/eip-1155[ERC-1155]. + * + * _Available since v5.1._ */ library ERC1155Utils { /** diff --git a/contracts/token/ERC20/ERC20.sol b/contracts/token/ERC20/ERC20.sol index 9885eaac996..6a9865e6aef 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.0.0) (token/ERC20/ERC20.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/ERC20.sol) pragma solidity ^0.8.20; @@ -300,7 +300,7 @@ abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { */ function _spendAllowance(address owner, address spender, uint256 value) internal virtual { uint256 currentAllowance = allowance(owner, spender); - if (currentAllowance != type(uint256).max) { + if (currentAllowance < type(uint256).max) { if (currentAllowance < value) { revert ERC20InsufficientAllowance(spender, currentAllowance, value); } diff --git a/contracts/token/ERC20/IERC20.sol b/contracts/token/ERC20/IERC20.sol index d8907216425..7d1019563f5 100644 --- a/contracts/token/ERC20/IERC20.sol +++ b/contracts/token/ERC20/IERC20.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/IERC20.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC20/extensions/ERC1363.sol b/contracts/token/ERC20/extensions/ERC1363.sol index 253e443d6b1..952582a8404 100644 --- a/contracts/token/ERC20/extensions/ERC1363.sol +++ b/contracts/token/ERC20/extensions/ERC1363.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC1363.sol) pragma solidity ^0.8.20; @@ -12,6 +13,8 @@ import {ERC1363Utils} from "../utils/ERC1363Utils.sol"; * @dev Extension of {ERC20} tokens that adds support for code execution after transfers and approvals * on recipient contracts. Calls after transfers are enabled through the {ERC1363-transferAndCall} and * {ERC1363-transferFromAndCall} methods while calls after approvals can be made with {ERC1363-approveAndCall} + * + * _Available since v5.1._ */ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** @@ -45,7 +48,8 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @dev Moves a `value` amount of tokens from the caller's account to `to` - * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. Returns a flag that indicates + * if the call succeeded. * * Requirements: * @@ -72,7 +76,8 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @dev Moves a `value` amount of tokens from `from` to `to` using the allowance mechanism - * and then calls {IERC1363Receiver-onTransferReceived} on `to`. + * and then calls {IERC1363Receiver-onTransferReceived} on `to`. Returns a flag that indicates + * if the call succeeded. * * Requirements: * @@ -105,6 +110,7 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 { /** * @dev Sets a `value` amount of tokens as the allowance of `spender` over the * caller's tokens and then calls {IERC1363Spender-onApprovalReceived} on `spender`. + * Returns a flag that indicates if the call succeeded. * * Requirements: * diff --git a/contracts/token/ERC20/extensions/ERC20FlashMint.sol b/contracts/token/ERC20/extensions/ERC20FlashMint.sol index fd8ff0b3ece..4d3a31f6df2 100644 --- a/contracts/token/ERC20/extensions/ERC20FlashMint.sol +++ b/contracts/token/ERC20/extensions/ERC20FlashMint.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC20FlashMint.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20FlashMint.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC20/extensions/ERC20Pausable.sol b/contracts/token/ERC20/extensions/ERC20Pausable.sol index 9b56f053566..2f6d86c4a53 100644 --- a/contracts/token/ERC20/extensions/ERC20Pausable.sol +++ b/contracts/token/ERC20/extensions/ERC20Pausable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC20Pausable.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Pausable.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC20/extensions/ERC20Permit.sol b/contracts/token/ERC20/extensions/ERC20Permit.sol index 22b19dc0bb7..3d36561a85f 100644 --- a/contracts/token/ERC20/extensions/ERC20Permit.sol +++ b/contracts/token/ERC20/extensions/ERC20Permit.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC20Permit.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Permit.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC20/extensions/ERC20Votes.sol b/contracts/token/ERC20/extensions/ERC20Votes.sol index 7680293a7cb..c15e7f56885 100644 --- a/contracts/token/ERC20/extensions/ERC20Votes.sol +++ b/contracts/token/ERC20/extensions/ERC20Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC20Votes.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Votes.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC20/extensions/ERC20Wrapper.sol b/contracts/token/ERC20/extensions/ERC20Wrapper.sol index eecd6ab7a63..9cc5aaf5f68 100644 --- a/contracts/token/ERC20/extensions/ERC20Wrapper.sol +++ b/contracts/token/ERC20/extensions/ERC20Wrapper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC20Wrapper.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC20Wrapper.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index c71b14ad48c..ec9a255076c 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/ERC4626.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/ERC4626.sol) pragma solidity ^0.8.20; @@ -83,7 +83,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { /** * @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way. */ - function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool, uint8) { + function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool ok, uint8 assetDecimals) { (bool success, bytes memory encodedDecimals) = address(asset_).staticcall( abi.encodeCall(IERC20Metadata.decimals, ()) ); diff --git a/contracts/token/ERC20/extensions/IERC20Metadata.sol b/contracts/token/ERC20/extensions/IERC20Metadata.sol index e8c020b1fcd..3c067ef4012 100644 --- a/contracts/token/ERC20/extensions/IERC20Metadata.sol +++ b/contracts/token/ERC20/extensions/IERC20Metadata.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Metadata.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Metadata.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC20/extensions/IERC20Permit.sol b/contracts/token/ERC20/extensions/IERC20Permit.sol index a8ad26ede5a..fc374368fd2 100644 --- a/contracts/token/ERC20/extensions/IERC20Permit.sol +++ b/contracts/token/ERC20/extensions/IERC20Permit.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Permit.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/IERC20Permit.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC20/extensions/draft-ERC20TemporaryApproval.sol b/contracts/token/ERC20/extensions/draft-ERC20TemporaryApproval.sol index fa1e098a7cc..d30521b49bb 100644 --- a/contracts/token/ERC20/extensions/draft-ERC20TemporaryApproval.sol +++ b/contracts/token/ERC20/extensions/draft-ERC20TemporaryApproval.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/extensions/draft-ERC20TemporaryApproval.sol) pragma solidity ^0.8.24; @@ -6,17 +7,19 @@ import {IERC20, ERC20} from "../ERC20.sol"; import {IERC7674} from "../../../interfaces/draft-IERC7674.sol"; import {Math} from "../../../utils/math/Math.sol"; import {SlotDerivation} from "../../../utils/SlotDerivation.sol"; -import {StorageSlot} from "../../../utils/StorageSlot.sol"; +import {TransientSlot} from "../../../utils/TransientSlot.sol"; /** * @dev Extension of {ERC20} that adds support for temporary allowances following ERC-7674. * * WARNING: This is a draft contract. The corresponding ERC is still subject to changes. + * + * _Available since v5.1._ */ abstract contract ERC20TemporaryApproval is ERC20, IERC7674 { using SlotDerivation for bytes32; - using StorageSlot for bytes32; - using StorageSlot for StorageSlot.Uint256SlotType; + using TransientSlot for bytes32; + using TransientSlot for TransientSlot.Uint256Slot; // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC20_TEMPORARY_APPROVAL_STORAGE")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant ERC20_TEMPORARY_APPROVAL_STORAGE = @@ -110,10 +113,7 @@ abstract contract ERC20TemporaryApproval is ERC20, IERC7674 { } } - function _temporaryAllowanceSlot( - address owner, - address spender - ) private pure returns (StorageSlot.Uint256SlotType) { + function _temporaryAllowanceSlot(address owner, address spender) private pure returns (TransientSlot.Uint256Slot) { return ERC20_TEMPORARY_APPROVAL_STORAGE.deriveMapping(owner).deriveMapping(spender).asUint256(); } } diff --git a/contracts/token/ERC20/utils/ERC1363Utils.sol b/contracts/token/ERC20/utils/ERC1363Utils.sol index f5c931361f7..6ba26901eb0 100644 --- a/contracts/token/ERC20/utils/ERC1363Utils.sol +++ b/contracts/token/ERC20/utils/ERC1363Utils.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/utils/ERC1363Utils.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index ed41fb042c9..a77907b4c33 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -1,11 +1,10 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/utils/SafeERC20.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC20/utils/SafeERC20.sol) pragma solidity ^0.8.20; import {IERC20} from "../IERC20.sol"; import {IERC1363} from "../../../interfaces/IERC1363.sol"; -import {Address} from "../../../utils/Address.sol"; /** * @title SafeERC20 @@ -46,6 +45,11 @@ library SafeERC20 { /** * @dev Increase the calling contract's allowance toward `spender` by `value`. If `token` returns no value, * non-reverting calls are assumed to be successful. + * + * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client" + * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using + * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract + * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior. */ function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal { uint256 oldAllowance = token.allowance(address(this), spender); @@ -55,6 +59,11 @@ library SafeERC20 { /** * @dev Decrease the calling contract's allowance toward `spender` by `requestedDecrease`. If `token` returns no * value, non-reverting calls are assumed to be successful. + * + * IMPORTANT: If the token implements ERC-7674 (ERC-20 with temporary allowance), and if the "client" + * smart contract uses ERC-7674 to set temporary allowances, then the "client" smart contract should avoid using + * this function. Performing a {safeIncreaseAllowance} or {safeDecreaseAllowance} operation on a token contract + * that has a non-zero temporary allowance (for that particular owner-spender) will result in unexpected behavior. */ function safeDecreaseAllowance(IERC20 token, address spender, uint256 requestedDecrease) internal { unchecked { @@ -70,6 +79,10 @@ library SafeERC20 { * @dev Set the calling contract's allowance toward `spender` to `value`. If `token` returns no value, * non-reverting calls are assumed to be successful. Meant to be used with tokens that require the approval * to be set to zero before setting it to a non-zero value, such as USDT. + * + * NOTE: If the token implements ERC-7674, this function will not modify any temporary allowance. This function + * only sets the "standard" allowance. Any temporary allowance will remain active, in addition to the value being + * set here. */ function forceApprove(IERC20 token, address spender, uint256 value) internal { bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value)); diff --git a/contracts/token/ERC721/ERC721.sol b/contracts/token/ERC721/ERC721.sol index df7ff50575d..6aebc37309f 100644 --- a/contracts/token/ERC721/ERC721.sol +++ b/contracts/token/ERC721/ERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/ERC721.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/ERC721.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC721/IERC721.sol b/contracts/token/ERC721/IERC721.sol index d6ab6a47d2a..da393014753 100644 --- a/contracts/token/ERC721/IERC721.sol +++ b/contracts/token/ERC721/IERC721.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/IERC721.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/IERC721.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC721/IERC721Receiver.sol b/contracts/token/ERC721/IERC721Receiver.sol index a53d0777479..d472eec338a 100644 --- a/contracts/token/ERC721/IERC721Receiver.sol +++ b/contracts/token/ERC721/IERC721Receiver.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/IERC721Receiver.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/IERC721Receiver.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC721/README.adoc b/contracts/token/ERC721/README.adoc index 5554720d699..b5aa6579808 100644 --- a/contracts/token/ERC721/README.adoc +++ b/contracts/token/ERC721/README.adoc @@ -22,7 +22,7 @@ OpenZeppelin Contracts provides implementations of all four interfaces: Additionally there are a few of other extensions: -* {ERC721Consecutive}: An implementation of https://eips.ethereum.org/EIPS/eip-2309[ERC-2309] for minting batchs of tokens during construction, in accordance with ERC-721. +* {ERC721Consecutive}: An implementation of https://eips.ethereum.org/EIPS/eip-2309[ERC-2309] for minting batches of tokens during construction, in accordance with ERC-721. * {ERC721URIStorage}: A more flexible but more expensive way of storing metadata. * {ERC721Votes}: Support for voting and vote delegation. * {ERC721Royalty}: A way to signal royalty information following ERC-2981. diff --git a/contracts/token/ERC721/extensions/ERC721Burnable.sol b/contracts/token/ERC721/extensions/ERC721Burnable.sol index 65cfc744cba..c6d22455761 100644 --- a/contracts/token/ERC721/extensions/ERC721Burnable.sol +++ b/contracts/token/ERC721/extensions/ERC721Burnable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/extensions/ERC721Burnable.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/extensions/ERC721Burnable.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC721/extensions/ERC721Consecutive.sol b/contracts/token/ERC721/extensions/ERC721Consecutive.sol index 8508a79f4fc..6b849d77438 100644 --- a/contracts/token/ERC721/extensions/ERC721Consecutive.sol +++ b/contracts/token/ERC721/extensions/ERC721Consecutive.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/extensions/ERC721Consecutive.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/extensions/ERC721Consecutive.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC721/extensions/ERC721Enumerable.sol b/contracts/token/ERC721/extensions/ERC721Enumerable.sol index 1bd4375290d..43aa81e6e09 100644 --- a/contracts/token/ERC721/extensions/ERC721Enumerable.sol +++ b/contracts/token/ERC721/extensions/ERC721Enumerable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/extensions/ERC721Enumerable.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/extensions/ERC721Enumerable.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC721/extensions/ERC721Pausable.sol b/contracts/token/ERC721/extensions/ERC721Pausable.sol index 81619c7f5dc..9a75623c62a 100644 --- a/contracts/token/ERC721/extensions/ERC721Pausable.sol +++ b/contracts/token/ERC721/extensions/ERC721Pausable.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/extensions/ERC721Pausable.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/extensions/ERC721Pausable.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC721/extensions/ERC721Royalty.sol b/contracts/token/ERC721/extensions/ERC721Royalty.sol index 1e0b25af43b..cfce1786c7a 100644 --- a/contracts/token/ERC721/extensions/ERC721Royalty.sol +++ b/contracts/token/ERC721/extensions/ERC721Royalty.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/extensions/ERC721Royalty.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/extensions/ERC721Royalty.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC721/extensions/ERC721URIStorage.sol b/contracts/token/ERC721/extensions/ERC721URIStorage.sol index 562f815c0b8..d8b4d8d1f91 100644 --- a/contracts/token/ERC721/extensions/ERC721URIStorage.sol +++ b/contracts/token/ERC721/extensions/ERC721URIStorage.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/extensions/ERC721URIStorage.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/extensions/ERC721URIStorage.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC721/extensions/ERC721Votes.sol b/contracts/token/ERC721/extensions/ERC721Votes.sol index 4962cb00c19..f71195ce7c8 100644 --- a/contracts/token/ERC721/extensions/ERC721Votes.sol +++ b/contracts/token/ERC721/extensions/ERC721Votes.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/extensions/ERC721Votes.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/extensions/ERC721Votes.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC721/extensions/ERC721Wrapper.sol b/contracts/token/ERC721/extensions/ERC721Wrapper.sol index 0a8acacb87b..111136bbe77 100644 --- a/contracts/token/ERC721/extensions/ERC721Wrapper.sol +++ b/contracts/token/ERC721/extensions/ERC721Wrapper.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/extensions/ERC721Wrapper.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/extensions/ERC721Wrapper.sol) pragma solidity ^0.8.20; diff --git a/contracts/token/ERC721/utils/ERC721Utils.sol b/contracts/token/ERC721/utils/ERC721Utils.sol index 712ac16a70a..2fd091afd67 100644 --- a/contracts/token/ERC721/utils/ERC721Utils.sol +++ b/contracts/token/ERC721/utils/ERC721Utils.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (token/ERC721/utils/ERC721Utils.sol) pragma solidity ^0.8.20; @@ -9,6 +10,8 @@ import {IERC721Errors} from "../../../interfaces/draft-IERC6093.sol"; * @dev Library that provide common ERC-721 utility functions. * * See https://eips.ethereum.org/EIPS/eip-721[ERC-721]. + * + * _Available since v5.1._ */ library ERC721Utils { /** diff --git a/contracts/token/common/ERC2981.sol b/contracts/token/common/ERC2981.sol index 70e8f469144..8335e56efab 100644 --- a/contracts/token/common/ERC2981.sol +++ b/contracts/token/common/ERC2981.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (token/common/ERC2981.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (token/common/ERC2981.sol) pragma solidity ^0.8.20; @@ -58,7 +58,10 @@ abstract contract ERC2981 is IERC2981, ERC165 { /** * @inheritdoc IERC2981 */ - function royaltyInfo(uint256 tokenId, uint256 salePrice) public view virtual returns (address, uint256) { + function royaltyInfo( + uint256 tokenId, + uint256 salePrice + ) public view virtual returns (address receiver, uint256 amount) { RoyaltyInfo storage _royaltyInfo = _tokenRoyaltyInfo[tokenId]; address royaltyReceiver = _royaltyInfo.receiver; uint96 royaltyFraction = _royaltyInfo.royaltyFraction; diff --git a/contracts/utils/Address.sol b/contracts/utils/Address.sol index 40f01a93dca..a1c8af296ce 100644 --- a/contracts/utils/Address.sol +++ b/contracts/utils/Address.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/Address.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/Address.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index 432c8602800..bd3401703d4 100644 --- a/contracts/utils/Arrays.sol +++ b/contracts/utils/Arrays.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/Arrays.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/Arrays.sol) // This file was procedurally generated from scripts/generate/templates/Arrays.js. pragma solidity ^0.8.20; diff --git a/contracts/utils/Base64.sol b/contracts/utils/Base64.sol index ef9431b5578..8b7c5c5ed06 100644 --- a/contracts/utils/Base64.sol +++ b/contracts/utils/Base64.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.2) (utils/Base64.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/Base64.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol new file mode 100644 index 00000000000..cf0cb8fcd7f --- /dev/null +++ b/contracts/utils/Bytes.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {Math} from "./math/Math.sol"; + +/** + * @dev Bytes operations. + */ +library Bytes { + /** + * @dev Forward search for `s` in `buffer` + * * If `s` is present in the buffer, returns the index of the first instance + * * If `s` is not present in the buffer, returns type(uint256).max + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf[Javascript's `Array.indexOf`] + */ + function indexOf(bytes memory buffer, bytes1 s) internal pure returns (uint256) { + return indexOf(buffer, s, 0); + } + + /** + * @dev Forward search for `s` in `buffer` starting at position `pos` + * * If `s` is present in the buffer (at or after `pos`), returns the index of the next instance + * * If `s` is not present in the buffer (at or after `pos`), returns type(uint256).max + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf[Javascript's `Array.indexOf`] + */ + function indexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) { + uint256 length = buffer.length; + for (uint256 i = pos; i < length; ++i) { + if (bytes1(_unsafeReadBytesOffset(buffer, i)) == s) { + return i; + } + } + return type(uint256).max; + } + + /** + * @dev Backward search for `s` in `buffer` + * * If `s` is present in the buffer, returns the index of the last instance + * * If `s` is not present in the buffer, returns type(uint256).max + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf[Javascript's `Array.lastIndexOf`] + */ + function lastIndexOf(bytes memory buffer, bytes1 s) internal pure returns (uint256) { + return lastIndexOf(buffer, s, type(uint256).max); + } + + /** + * @dev Backward search for `s` in `buffer` starting at position `pos` + * * If `s` is present in the buffer (at or before `pos`), returns the index of the previous instance + * * If `s` is not present in the buffer (at or before `pos`), returns type(uint256).max + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf[Javascript's `Array.lastIndexOf`] + */ + function lastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) { + unchecked { + uint256 length = buffer.length; + // NOTE here we cannot do `i = Math.min(pos + 1, length)` because `pos + 1` could overflow + for (uint256 i = Math.min(pos, length - 1) + 1; i > 0; --i) { + if (bytes1(_unsafeReadBytesOffset(buffer, i - 1)) == s) { + return i - 1; + } + } + return type(uint256).max; + } + } + + /** + * @dev Copies the content of `buffer`, from `start` (included) to the end of `buffer` into a new bytes object in + * memory. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`] + */ + function slice(bytes memory buffer, uint256 start) internal pure returns (bytes memory) { + return slice(buffer, start, buffer.length); + } + + /** + * @dev Copies the content of `buffer`, from `start` (included) to `end` (excluded) into a new bytes object in + * memory. + * + * NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice[Javascript's `Array.slice`] + */ + function slice(bytes memory buffer, uint256 start, uint256 end) internal pure returns (bytes memory) { + // sanitize + uint256 length = buffer.length; + end = Math.min(end, length); + start = Math.min(start, end); + + // allocate and copy + bytes memory result = new bytes(end - start); + assembly ("memory-safe") { + mcopy(add(result, 0x20), add(buffer, add(start, 0x20)), sub(end, start)) + } + + return result; + } + + /** + * @dev Reads a bytes32 from a bytes array without bounds checking. + * + * NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the + * assembly block as such would prevent some optimizations. + */ + function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) { + // This is not memory safe in the general case, but all calls to this private function are within bounds. + assembly ("memory-safe") { + value := mload(add(buffer, add(0x20, offset))) + } + } +} diff --git a/contracts/utils/CAIP10.sol b/contracts/utils/CAIP10.sol new file mode 100644 index 00000000000..f0424601735 --- /dev/null +++ b/contracts/utils/CAIP10.sol @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {Bytes} from "./Bytes.sol"; +import {Strings} from "./Strings.sol"; +import {CAIP2} from "./CAIP2.sol"; + +/** + * @dev Helper library to format and parse CAIP-10 identifiers + * + * https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md[CAIP-10] defines account identifiers as: + * account_id: chain_id + ":" + account_address + * chain_id: [-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32} (See {CAIP2}) + * account_address: [-.%a-zA-Z0-9]{1,128} + * + * WARNING: According to [CAIP-10's canonicalization section](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md#canonicalization), + * the implementation remains at the developer's discretion. Please note that case variations may introduce ambiguity. + * For example, when building hashes to identify accounts or data associated to them, multiple representations of the + * same account would derive to different hashes. For EVM chains, we recommend using checksummed addresses for the + * "account_address" part. They can be generated onchain using {Strings-toChecksumHexString}. + */ +library CAIP10 { + using Strings for address; + using Bytes for bytes; + + /// @dev Return the CAIP-10 identifier for an account on the current (local) chain. + function local(address account) internal view returns (string memory) { + return format(CAIP2.local(), account.toChecksumHexString()); + } + + /** + * @dev Return the CAIP-10 identifier for a given caip2 chain and account. + * + * NOTE: This function does not verify that the inputs are properly formatted. + */ + function format(string memory caip2, string memory account) internal pure returns (string memory) { + return string.concat(caip2, ":", account); + } + + /** + * @dev Parse a CAIP-10 identifier into its components. + * + * NOTE: This function does not verify that the CAIP-10 input is properly formatted. The `caip2` return can be + * parsed using the {CAIP2} library. + */ + function parse(string memory caip10) internal pure returns (string memory caip2, string memory account) { + bytes memory buffer = bytes(caip10); + + uint256 pos = buffer.lastIndexOf(":"); + return (string(buffer.slice(0, pos)), string(buffer.slice(pos + 1))); + } +} diff --git a/contracts/utils/CAIP2.sol b/contracts/utils/CAIP2.sol new file mode 100644 index 00000000000..a7a69e6a873 --- /dev/null +++ b/contracts/utils/CAIP2.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.24; + +import {Bytes} from "./Bytes.sol"; +import {Strings} from "./Strings.sol"; + +/** + * @dev Helper library to format and parse CAIP-2 identifiers + * + * https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md[CAIP-2] defines chain identifiers as: + * chain_id: namespace + ":" + reference + * namespace: [-a-z0-9]{3,8} + * reference: [-_a-zA-Z0-9]{1,32} + * + * WARNING: In some cases, multiple CAIP-2 identifiers may all be valid representation of a single chain. + * For EVM chains, it is recommended to use `eip155:xxx` as the canonical representation (where `xxx` is + * the EIP-155 chain id). Consider the possible ambiguity when processing CAIP-2 identifiers or when using them + * in the context of hashes. + */ +library CAIP2 { + using Strings for uint256; + using Bytes for bytes; + + /// @dev Return the CAIP-2 identifier for the current (local) chain. + function local() internal view returns (string memory) { + return format("eip155", block.chainid.toString()); + } + + /** + * @dev Return the CAIP-2 identifier for a given namespace and reference. + * + * NOTE: This function does not verify that the inputs are properly formatted. + */ + function format(string memory namespace, string memory ref) internal pure returns (string memory) { + return string.concat(namespace, ":", ref); + } + + /** + * @dev Parse a CAIP-2 identifier into its components. + * + * NOTE: This function does not verify that the CAIP-2 input is properly formatted. + */ + function parse(string memory caip2) internal pure returns (string memory namespace, string memory ref) { + bytes memory buffer = bytes(caip2); + + uint256 pos = buffer.indexOf(":"); + return (string(buffer.slice(0, pos)), string(buffer.slice(pos + 1))); + } +} diff --git a/contracts/utils/Comparators.sol b/contracts/utils/Comparators.sol index 3a63aa0e8ee..a8c5e73dfea 100644 --- a/contracts/utils/Comparators.sol +++ b/contracts/utils/Comparators.sol @@ -1,7 +1,13 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/Comparators.sol) pragma solidity ^0.8.20; +/** + * @dev Provides a set of functions to compare values. + * + * _Available since v5.1._ + */ library Comparators { function lt(uint256 a, uint256 b) internal pure returns (bool) { return a < b; diff --git a/contracts/utils/Create2.sol b/contracts/utils/Create2.sol index 121eb0c742a..ffd39d9a46c 100644 --- a/contracts/utils/Create2.sol +++ b/contracts/utils/Create2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/Create2.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/Create2.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/Errors.sol b/contracts/utils/Errors.sol index 633d8c20286..442fc18920f 100644 --- a/contracts/utils/Errors.sol +++ b/contracts/utils/Errors.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/Errors.sol) pragma solidity ^0.8.20; @@ -7,6 +8,8 @@ pragma solidity ^0.8.20; * * IMPORTANT: Backwards compatibility is not guaranteed in future versions of the library. * It is recommended to avoid relying on the error API for critical functionality. + * + * _Available since v5.1._ */ library Errors { /** diff --git a/contracts/utils/NoncesKeyed.sol b/contracts/utils/NoncesKeyed.sol new file mode 100644 index 00000000000..31cd0704e15 --- /dev/null +++ b/contracts/utils/NoncesKeyed.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Nonces} from "./Nonces.sol"; + +/** + * @dev Alternative to {Nonces}, that supports key-ed nonces. + * + * Follows the https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337's semi-abstracted nonce system]. + * + * NOTE: This contract inherits from {Nonces} and reuses its storage for the first nonce key (i.e. `0`). This + * makes upgrading from {Nonces} to {NoncesKeyed} safe when using their upgradeable versions (e.g. `NoncesKeyedUpgradeable`). + * Doing so will NOT reset the current state of nonces, avoiding replay attacks where a nonce is reused after the upgrade. + */ +abstract contract NoncesKeyed is Nonces { + mapping(address owner => mapping(uint192 key => uint64)) private _nonces; + + /// @dev Returns the next unused nonce for an address and key. Result contains the key prefix. + function nonces(address owner, uint192 key) public view virtual returns (uint256) { + return key == 0 ? nonces(owner) : _pack(key, _nonces[owner][key]); + } + + /** + * @dev Consumes the next unused nonce for an address and key. + * + * Returns the current value without the key prefix. Consumed nonce is increased, so calling this function twice + * with the same arguments will return different (sequential) results. + */ + function _useNonce(address owner, uint192 key) internal virtual returns (uint256) { + // For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be + // decremented or reset. This guarantees that the nonce never overflows. + unchecked { + // It is important to do x++ and not ++x here. + return key == 0 ? _useNonce(owner) : _pack(key, _nonces[owner][key]++); + } + } + + /** + * @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`. + * + * This version takes the key and the nonce in a single uint256 parameter: + * - use the first 24 bytes for the key + * - use the last 8 bytes for the nonce + */ + function _useCheckedNonce(address owner, uint256 keyNonce) internal virtual override { + (uint192 key, ) = _unpack(keyNonce); + if (key == 0) { + super._useCheckedNonce(owner, keyNonce); + } else { + uint256 current = _useNonce(owner, key); + if (keyNonce != current) revert InvalidAccountNonce(owner, current); + } + } + + /** + * @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`. + * + * This version takes the key and the nonce as two different parameters. + */ + function _useCheckedNonce(address owner, uint192 key, uint64 nonce) internal virtual { + _useCheckedNonce(owner, _pack(key, nonce)); + } + + /// @dev Pack key and nonce into a keyNonce + function _pack(uint192 key, uint64 nonce) private pure returns (uint256) { + return (uint256(key) << 64) | nonce; + } + + /// @dev Unpack a keyNonce into its key and nonce components + function _unpack(uint256 keyNonce) private pure returns (uint192 key, uint64 nonce) { + return (uint192(keyNonce >> 64), uint64(keyNonce)); + } +} diff --git a/contracts/utils/Packing.sol b/contracts/utils/Packing.sol index 8a8e3fee86f..f38e64a3bf6 100644 --- a/contracts/utils/Packing.sol +++ b/contracts/utils/Packing.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/Packing.sol) // This file was procedurally generated from scripts/generate/templates/Packing.js. pragma solidity ^0.8.20; @@ -28,6 +29,8 @@ pragma solidity ^0.8.20; * } * } * ``` + * + * _Available since v5.1._ */ // solhint-disable func-name-mixedcase library Packing { @@ -65,6 +68,38 @@ library Packing { } } + function pack_2_8(bytes2 left, bytes8 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + left := and(left, shl(240, not(0))) + right := and(right, shl(192, not(0))) + result := or(left, shr(16, right)) + } + } + + function pack_2_10(bytes2 left, bytes10 right) internal pure returns (bytes12 result) { + assembly ("memory-safe") { + left := and(left, shl(240, not(0))) + right := and(right, shl(176, not(0))) + result := or(left, shr(16, right)) + } + } + + function pack_2_20(bytes2 left, bytes20 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(240, not(0))) + right := and(right, shl(96, not(0))) + result := or(left, shr(16, right)) + } + } + + function pack_2_22(bytes2 left, bytes22 right) internal pure returns (bytes24 result) { + assembly ("memory-safe") { + left := and(left, shl(240, not(0))) + right := and(right, shl(80, not(0))) + result := or(left, shr(16, right)) + } + } + function pack_4_2(bytes4 left, bytes2 right) internal pure returns (bytes6 result) { assembly ("memory-safe") { left := and(left, shl(224, not(0))) @@ -81,6 +116,14 @@ library Packing { } } + function pack_4_6(bytes4 left, bytes6 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + left := and(left, shl(224, not(0))) + right := and(right, shl(208, not(0))) + result := or(left, shr(32, right)) + } + } + function pack_4_8(bytes4 left, bytes8 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { left := and(left, shl(224, not(0))) @@ -137,6 +180,14 @@ library Packing { } } + function pack_6_4(bytes6 left, bytes4 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + left := and(left, shl(208, not(0))) + right := and(right, shl(224, not(0))) + result := or(left, shr(48, right)) + } + } + function pack_6_6(bytes6 left, bytes6 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { left := and(left, shl(208, not(0))) @@ -145,6 +196,38 @@ library Packing { } } + function pack_6_10(bytes6 left, bytes10 right) internal pure returns (bytes16 result) { + assembly ("memory-safe") { + left := and(left, shl(208, not(0))) + right := and(right, shl(176, not(0))) + result := or(left, shr(48, right)) + } + } + + function pack_6_16(bytes6 left, bytes16 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(208, not(0))) + right := and(right, shl(128, not(0))) + result := or(left, shr(48, right)) + } + } + + function pack_6_22(bytes6 left, bytes22 right) internal pure returns (bytes28 result) { + assembly ("memory-safe") { + left := and(left, shl(208, not(0))) + right := and(right, shl(80, not(0))) + result := or(left, shr(48, right)) + } + } + + function pack_8_2(bytes8 left, bytes2 right) internal pure returns (bytes10 result) { + assembly ("memory-safe") { + left := and(left, shl(192, not(0))) + right := and(right, shl(240, not(0))) + result := or(left, shr(64, right)) + } + } + function pack_8_4(bytes8 left, bytes4 right) internal pure returns (bytes12 result) { assembly ("memory-safe") { left := and(left, shl(192, not(0))) @@ -193,6 +276,46 @@ library Packing { } } + function pack_10_2(bytes10 left, bytes2 right) internal pure returns (bytes12 result) { + assembly ("memory-safe") { + left := and(left, shl(176, not(0))) + right := and(right, shl(240, not(0))) + result := or(left, shr(80, right)) + } + } + + function pack_10_6(bytes10 left, bytes6 right) internal pure returns (bytes16 result) { + assembly ("memory-safe") { + left := and(left, shl(176, not(0))) + right := and(right, shl(208, not(0))) + result := or(left, shr(80, right)) + } + } + + function pack_10_10(bytes10 left, bytes10 right) internal pure returns (bytes20 result) { + assembly ("memory-safe") { + left := and(left, shl(176, not(0))) + right := and(right, shl(176, not(0))) + result := or(left, shr(80, right)) + } + } + + function pack_10_12(bytes10 left, bytes12 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(176, not(0))) + right := and(right, shl(160, not(0))) + result := or(left, shr(80, right)) + } + } + + function pack_10_22(bytes10 left, bytes22 right) internal pure returns (bytes32 result) { + assembly ("memory-safe") { + left := and(left, shl(176, not(0))) + right := and(right, shl(80, not(0))) + result := or(left, shr(80, right)) + } + } + function pack_12_4(bytes12 left, bytes4 right) internal pure returns (bytes16 result) { assembly ("memory-safe") { left := and(left, shl(160, not(0))) @@ -209,6 +332,14 @@ library Packing { } } + function pack_12_10(bytes12 left, bytes10 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(160, not(0))) + right := and(right, shl(176, not(0))) + result := or(left, shr(96, right)) + } + } + function pack_12_12(bytes12 left, bytes12 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { left := and(left, shl(160, not(0))) @@ -241,6 +372,14 @@ library Packing { } } + function pack_16_6(bytes16 left, bytes6 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(128, not(0))) + right := and(right, shl(208, not(0))) + result := or(left, shr(128, right)) + } + } + function pack_16_8(bytes16 left, bytes8 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { left := and(left, shl(128, not(0))) @@ -265,6 +404,14 @@ library Packing { } } + function pack_20_2(bytes20 left, bytes2 right) internal pure returns (bytes22 result) { + assembly ("memory-safe") { + left := and(left, shl(96, not(0))) + right := and(right, shl(240, not(0))) + result := or(left, shr(160, right)) + } + } + function pack_20_4(bytes20 left, bytes4 right) internal pure returns (bytes24 result) { assembly ("memory-safe") { left := and(left, shl(96, not(0))) @@ -289,6 +436,30 @@ library Packing { } } + function pack_22_2(bytes22 left, bytes2 right) internal pure returns (bytes24 result) { + assembly ("memory-safe") { + left := and(left, shl(80, not(0))) + right := and(right, shl(240, not(0))) + result := or(left, shr(176, right)) + } + } + + function pack_22_6(bytes22 left, bytes6 right) internal pure returns (bytes28 result) { + assembly ("memory-safe") { + left := and(left, shl(80, not(0))) + right := and(right, shl(208, not(0))) + result := or(left, shr(176, right)) + } + } + + function pack_22_10(bytes22 left, bytes10 right) internal pure returns (bytes32 result) { + assembly ("memory-safe") { + left := and(left, shl(80, not(0))) + right := and(right, shl(176, not(0))) + result := or(left, shr(176, right)) + } + } + function pack_24_4(bytes24 left, bytes4 right) internal pure returns (bytes28 result) { assembly ("memory-safe") { left := and(left, shl(64, not(0))) @@ -463,6 +634,81 @@ library Packing { } } + function extract_10_1(bytes10 self, uint8 offset) internal pure returns (bytes1 result) { + if (offset > 9) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(248, not(0))) + } + } + + function replace_10_1(bytes10 self, bytes1 value, uint8 offset) internal pure returns (bytes10 result) { + bytes1 oldValue = extract_10_1(self, offset); + assembly ("memory-safe") { + value := and(value, shl(248, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_2(bytes10 self, uint8 offset) internal pure returns (bytes2 result) { + if (offset > 8) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(240, not(0))) + } + } + + function replace_10_2(bytes10 self, bytes2 value, uint8 offset) internal pure returns (bytes10 result) { + bytes2 oldValue = extract_10_2(self, offset); + assembly ("memory-safe") { + value := and(value, shl(240, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_4(bytes10 self, uint8 offset) internal pure returns (bytes4 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(224, not(0))) + } + } + + function replace_10_4(bytes10 self, bytes4 value, uint8 offset) internal pure returns (bytes10 result) { + bytes4 oldValue = extract_10_4(self, offset); + assembly ("memory-safe") { + value := and(value, shl(224, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_6(bytes10 self, uint8 offset) internal pure returns (bytes6 result) { + if (offset > 4) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(208, not(0))) + } + } + + function replace_10_6(bytes10 self, bytes6 value, uint8 offset) internal pure returns (bytes10 result) { + bytes6 oldValue = extract_10_6(self, offset); + assembly ("memory-safe") { + value := and(value, shl(208, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_10_8(bytes10 self, uint8 offset) internal pure returns (bytes8 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(192, not(0))) + } + } + + function replace_10_8(bytes10 self, bytes8 value, uint8 offset) internal pure returns (bytes10 result) { + bytes8 oldValue = extract_10_8(self, offset); + assembly ("memory-safe") { + value := and(value, shl(192, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_12_1(bytes12 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 11) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -538,6 +784,21 @@ library Packing { } } + function extract_12_10(bytes12 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_12_10(bytes12 self, bytes10 value, uint8 offset) internal pure returns (bytes12 result) { + bytes10 oldValue = extract_12_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_16_1(bytes16 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 15) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -613,6 +874,21 @@ library Packing { } } + function extract_16_10(bytes16 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_16_10(bytes16 self, bytes10 value, uint8 offset) internal pure returns (bytes16 result) { + bytes10 oldValue = extract_16_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_16_12(bytes16 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 4) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -703,6 +979,21 @@ library Packing { } } + function extract_20_10(bytes20 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 10) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_20_10(bytes20 self, bytes10 value, uint8 offset) internal pure returns (bytes20 result) { + bytes10 oldValue = extract_20_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_20_12(bytes20 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 8) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -733,6 +1024,141 @@ library Packing { } } + function extract_22_1(bytes22 self, uint8 offset) internal pure returns (bytes1 result) { + if (offset > 21) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(248, not(0))) + } + } + + function replace_22_1(bytes22 self, bytes1 value, uint8 offset) internal pure returns (bytes22 result) { + bytes1 oldValue = extract_22_1(self, offset); + assembly ("memory-safe") { + value := and(value, shl(248, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_2(bytes22 self, uint8 offset) internal pure returns (bytes2 result) { + if (offset > 20) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(240, not(0))) + } + } + + function replace_22_2(bytes22 self, bytes2 value, uint8 offset) internal pure returns (bytes22 result) { + bytes2 oldValue = extract_22_2(self, offset); + assembly ("memory-safe") { + value := and(value, shl(240, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_4(bytes22 self, uint8 offset) internal pure returns (bytes4 result) { + if (offset > 18) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(224, not(0))) + } + } + + function replace_22_4(bytes22 self, bytes4 value, uint8 offset) internal pure returns (bytes22 result) { + bytes4 oldValue = extract_22_4(self, offset); + assembly ("memory-safe") { + value := and(value, shl(224, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_6(bytes22 self, uint8 offset) internal pure returns (bytes6 result) { + if (offset > 16) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(208, not(0))) + } + } + + function replace_22_6(bytes22 self, bytes6 value, uint8 offset) internal pure returns (bytes22 result) { + bytes6 oldValue = extract_22_6(self, offset); + assembly ("memory-safe") { + value := and(value, shl(208, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_8(bytes22 self, uint8 offset) internal pure returns (bytes8 result) { + if (offset > 14) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(192, not(0))) + } + } + + function replace_22_8(bytes22 self, bytes8 value, uint8 offset) internal pure returns (bytes22 result) { + bytes8 oldValue = extract_22_8(self, offset); + assembly ("memory-safe") { + value := and(value, shl(192, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_10(bytes22 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 12) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_22_10(bytes22 self, bytes10 value, uint8 offset) internal pure returns (bytes22 result) { + bytes10 oldValue = extract_22_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_12(bytes22 self, uint8 offset) internal pure returns (bytes12 result) { + if (offset > 10) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(160, not(0))) + } + } + + function replace_22_12(bytes22 self, bytes12 value, uint8 offset) internal pure returns (bytes22 result) { + bytes12 oldValue = extract_22_12(self, offset); + assembly ("memory-safe") { + value := and(value, shl(160, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_16(bytes22 self, uint8 offset) internal pure returns (bytes16 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(128, not(0))) + } + } + + function replace_22_16(bytes22 self, bytes16 value, uint8 offset) internal pure returns (bytes22 result) { + bytes16 oldValue = extract_22_16(self, offset); + assembly ("memory-safe") { + value := and(value, shl(128, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + + function extract_22_20(bytes22 self, uint8 offset) internal pure returns (bytes20 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(96, not(0))) + } + } + + function replace_22_20(bytes22 self, bytes20 value, uint8 offset) internal pure returns (bytes22 result) { + bytes20 oldValue = extract_22_20(self, offset); + assembly ("memory-safe") { + value := and(value, shl(96, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_24_1(bytes24 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 23) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -808,6 +1234,21 @@ library Packing { } } + function extract_24_10(bytes24 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 14) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_24_10(bytes24 self, bytes10 value, uint8 offset) internal pure returns (bytes24 result) { + bytes10 oldValue = extract_24_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_24_12(bytes24 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 12) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -853,6 +1294,21 @@ library Packing { } } + function extract_24_22(bytes24 self, uint8 offset) internal pure returns (bytes22 result) { + if (offset > 2) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(80, not(0))) + } + } + + function replace_24_22(bytes24 self, bytes22 value, uint8 offset) internal pure returns (bytes24 result) { + bytes22 oldValue = extract_24_22(self, offset); + assembly ("memory-safe") { + value := and(value, shl(80, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_28_1(bytes28 self, uint8 offset) internal pure returns (bytes1 result) { if (offset > 27) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -928,6 +1384,21 @@ library Packing { } } + function extract_28_10(bytes28 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 18) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_28_10(bytes28 self, bytes10 value, uint8 offset) internal pure returns (bytes28 result) { + bytes10 oldValue = extract_28_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_28_12(bytes28 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 16) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -973,6 +1444,21 @@ library Packing { } } + function extract_28_22(bytes28 self, uint8 offset) internal pure returns (bytes22 result) { + if (offset > 6) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(80, not(0))) + } + } + + function replace_28_22(bytes28 self, bytes22 value, uint8 offset) internal pure returns (bytes28 result) { + bytes22 oldValue = extract_28_22(self, offset); + assembly ("memory-safe") { + value := and(value, shl(80, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_28_24(bytes28 self, uint8 offset) internal pure returns (bytes24 result) { if (offset > 4) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -1063,6 +1549,21 @@ library Packing { } } + function extract_32_10(bytes32 self, uint8 offset) internal pure returns (bytes10 result) { + if (offset > 22) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(176, not(0))) + } + } + + function replace_32_10(bytes32 self, bytes10 value, uint8 offset) internal pure returns (bytes32 result) { + bytes10 oldValue = extract_32_10(self, offset); + assembly ("memory-safe") { + value := and(value, shl(176, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_32_12(bytes32 self, uint8 offset) internal pure returns (bytes12 result) { if (offset > 20) revert OutOfRangeAccess(); assembly ("memory-safe") { @@ -1108,6 +1609,21 @@ library Packing { } } + function extract_32_22(bytes32 self, uint8 offset) internal pure returns (bytes22 result) { + if (offset > 10) revert OutOfRangeAccess(); + assembly ("memory-safe") { + result := and(shl(mul(8, offset), self), shl(80, not(0))) + } + } + + function replace_32_22(bytes32 self, bytes22 value, uint8 offset) internal pure returns (bytes32 result) { + bytes22 oldValue = extract_32_22(self, offset); + assembly ("memory-safe") { + value := and(value, shl(80, not(0))) + result := xor(self, shr(mul(8, offset), xor(oldValue, value))) + } + } + function extract_32_24(bytes32 self, uint8 offset) internal pure returns (bytes24 result) { if (offset > 8) revert OutOfRangeAccess(); assembly ("memory-safe") { diff --git a/contracts/utils/Panic.sol b/contracts/utils/Panic.sol index 8769bd9a786..e168824d34b 100644 --- a/contracts/utils/Panic.sol +++ b/contracts/utils/Panic.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/Panic.sol) pragma solidity ^0.8.20; @@ -18,6 +19,8 @@ pragma solidity ^0.8.20; * ``` * * Follows the list from https://github.com/ethereum/solidity/blob/v0.8.24/libsolutil/ErrorCodes.h[libsolutil]. + * + * _Available since v5.1._ */ // slither-disable-next-line unused-state library Panic { diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 0ef3e5387c8..8f08a86987f 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -18,6 +18,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {ReentrancyGuardTransient}: Variant of {ReentrancyGuard} that uses transient storage (https://eips.ethereum.org/EIPS/eip-1153[EIP-1153]). * {Pausable}: A common emergency response mechanism that can pause functionality while a remediation is pending. * {Nonces}: Utility for tracking and verifying address nonces that only increment. + * {NoncesKeyed}: Alternative to {Nonces}, that support key-ed nonces following https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337 speciciations]. * {ERC165}, {ERC165Checker}: Utilities for inspecting interfaces supported by contracts. * {BitMaps}: A simple library to manage boolean value mapped to a numerical index in an efficient way. * {EnumerableMap}: A type like Solidity's https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`], but with key-value _enumeration_: this will let you know how many entries a mapping has, and iterate over them (which is not possible with `mapping`). @@ -31,15 +32,18 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Address}: Collection of functions for overloading Solidity's https://docs.soliditylang.org/en/latest/types.html#address[`address`] type. * {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. * {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. * {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. Also include primitives for reading from and writing to transient storage (only value types are currently supported). + * {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). * {Multicall}: Abstract contract with a utility to allow batching together multiple calls in a single transaction. Useful for allowing EOAs to perform multiple operations at once. * {Context}: A utility for abstracting the sender and calldata in the current execution context. * {Packing}: A library for packing and unpacking multiple values into bytes32 * {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes]. - * {Comparators}: A library that contains comparator functions to use with with the {Heap} library. + * {Comparators}: A library that contains comparator functions to use with the {Heap} library. + * {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers. [NOTE] ==== @@ -58,6 +62,10 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable {{ECDSA}} +{{P256}} + +{{RSA}} + {{EIP712}} {{MessageHashUtils}} @@ -78,6 +86,8 @@ Because Solidity does not support generic types, {EnumerableMap} and {Enumerable {{Nonces}} +{{NoncesKeyed}} + == Introspection This set of interfaces and contracts deal with https://en.wikipedia.org/wiki/Type_introspection[type introspection] of contracts, that is, examining which functions can be called on them. This is usually referred to as a contract's _interface_. @@ -126,6 +136,8 @@ Ethereum contracts have no native concept of an interface, so applications must {{StorageSlot}} +{{TransientSlot}} + {{Multicall}} {{Context}} diff --git a/contracts/utils/ReentrancyGuard.sol b/contracts/utils/ReentrancyGuard.sol index 081851170e2..a95fb512f31 100644 --- a/contracts/utils/ReentrancyGuard.sol +++ b/contracts/utils/ReentrancyGuard.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/ReentrancyGuard.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuard.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/ReentrancyGuardTransient.sol b/contracts/utils/ReentrancyGuardTransient.sol index 0389b86205d..1a62e29dafa 100644 --- a/contracts/utils/ReentrancyGuardTransient.sol +++ b/contracts/utils/ReentrancyGuardTransient.sol @@ -1,16 +1,19 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/ReentrancyGuardTransient.sol) pragma solidity ^0.8.24; -import {StorageSlot} from "./StorageSlot.sol"; +import {TransientSlot} from "./TransientSlot.sol"; /** * @dev Variant of {ReentrancyGuard} that uses transient storage. * * NOTE: This variant only works on networks where EIP-1153 is available. + * + * _Available since v5.1._ */ abstract contract ReentrancyGuardTransient { - using StorageSlot for *; + using TransientSlot for *; // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ReentrancyGuard")) - 1)) & ~bytes32(uint256(0xff)) bytes32 private constant REENTRANCY_GUARD_STORAGE = diff --git a/contracts/utils/ShortStrings.sol b/contracts/utils/ShortStrings.sol index 78fc5cd963d..fb8bde51668 100644 --- a/contracts/utils/ShortStrings.sol +++ b/contracts/utils/ShortStrings.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/ShortStrings.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/ShortStrings.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/SlotDerivation.sol b/contracts/utils/SlotDerivation.sol index 62c28a55fa6..62d1545c650 100644 --- a/contracts/utils/SlotDerivation.sol +++ b/contracts/utils/SlotDerivation.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/SlotDerivation.sol) // This file was procedurally generated from scripts/generate/templates/SlotDerivation.js. pragma solidity ^0.8.20; @@ -34,6 +35,8 @@ pragma solidity ^0.8.20; * * NOTE: This library provides a way to manipulate storage locations in a non-standard way. Tooling for checking * upgrade safety will ignore the slots accessed through this library. + * + * _Available since v5.1._ */ library SlotDerivation { /** @@ -70,7 +73,7 @@ library SlotDerivation { */ function deriveMapping(bytes32 slot, address key) internal pure returns (bytes32 result) { assembly ("memory-safe") { - mstore(0x00, key) + mstore(0x00, and(key, shr(96, not(0)))) mstore(0x20, slot) result := keccak256(0x00, 0x40) } @@ -81,7 +84,7 @@ library SlotDerivation { */ function deriveMapping(bytes32 slot, bool key) internal pure returns (bytes32 result) { assembly ("memory-safe") { - mstore(0x00, key) + mstore(0x00, iszero(iszero(key))) mstore(0x20, slot) result := keccak256(0x00, 0x40) } diff --git a/contracts/utils/StorageSlot.sol b/contracts/utils/StorageSlot.sol index 2e4f736d8d1..aebb10524a2 100644 --- a/contracts/utils/StorageSlot.sol +++ b/contracts/utils/StorageSlot.sol @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/StorageSlot.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol) // This file was procedurally generated from scripts/generate/templates/StorageSlot.js. -pragma solidity ^0.8.24; +pragma solidity ^0.8.20; /** * @dev Library for reading and writing primitive types to specific storage slots. @@ -29,24 +29,6 @@ pragma solidity ^0.8.24; * } * ``` * - * Since version 5.1, this library also support writing and reading value types to and from transient storage. - * - * * Example using transient storage: - * ```solidity - * contract Lock { - * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot. - * bytes32 internal constant _LOCK_SLOT = 0xf4678858b2b588224636b8522b729e7722d32fc491da849ed75b3fdf3c84f542; - * - * modifier locked() { - * require(!_LOCK_SLOT.asBoolean().tload()); - * - * _LOCK_SLOT.asBoolean().tstore(true); - * _; - * _LOCK_SLOT.asBoolean().tstore(false); - * } - * } - * ``` - * * TIP: Consider using this library along with {SlotDerivation}. */ library StorageSlot { @@ -158,154 +140,4 @@ library StorageSlot { r.slot := store.slot } } - - /** - * @dev UDVT that represent a slot holding a address. - */ - type AddressSlotType is bytes32; - - /** - * @dev Cast an arbitrary slot to a AddressSlotType. - */ - function asAddress(bytes32 slot) internal pure returns (AddressSlotType) { - return AddressSlotType.wrap(slot); - } - - /** - * @dev UDVT that represent a slot holding a bool. - */ - type BooleanSlotType is bytes32; - - /** - * @dev Cast an arbitrary slot to a BooleanSlotType. - */ - function asBoolean(bytes32 slot) internal pure returns (BooleanSlotType) { - return BooleanSlotType.wrap(slot); - } - - /** - * @dev UDVT that represent a slot holding a bytes32. - */ - type Bytes32SlotType is bytes32; - - /** - * @dev Cast an arbitrary slot to a Bytes32SlotType. - */ - function asBytes32(bytes32 slot) internal pure returns (Bytes32SlotType) { - return Bytes32SlotType.wrap(slot); - } - - /** - * @dev UDVT that represent a slot holding a uint256. - */ - type Uint256SlotType is bytes32; - - /** - * @dev Cast an arbitrary slot to a Uint256SlotType. - */ - function asUint256(bytes32 slot) internal pure returns (Uint256SlotType) { - return Uint256SlotType.wrap(slot); - } - - /** - * @dev UDVT that represent a slot holding a int256. - */ - type Int256SlotType is bytes32; - - /** - * @dev Cast an arbitrary slot to a Int256SlotType. - */ - function asInt256(bytes32 slot) internal pure returns (Int256SlotType) { - return Int256SlotType.wrap(slot); - } - - /** - * @dev Load the value held at location `slot` in transient storage. - */ - function tload(AddressSlotType slot) internal view returns (address value) { - assembly ("memory-safe") { - value := tload(slot) - } - } - - /** - * @dev Store `value` at location `slot` in transient storage. - */ - function tstore(AddressSlotType slot, address value) internal { - assembly ("memory-safe") { - tstore(slot, value) - } - } - - /** - * @dev Load the value held at location `slot` in transient storage. - */ - function tload(BooleanSlotType slot) internal view returns (bool value) { - assembly ("memory-safe") { - value := tload(slot) - } - } - - /** - * @dev Store `value` at location `slot` in transient storage. - */ - function tstore(BooleanSlotType slot, bool value) internal { - assembly ("memory-safe") { - tstore(slot, value) - } - } - - /** - * @dev Load the value held at location `slot` in transient storage. - */ - function tload(Bytes32SlotType slot) internal view returns (bytes32 value) { - assembly ("memory-safe") { - value := tload(slot) - } - } - - /** - * @dev Store `value` at location `slot` in transient storage. - */ - function tstore(Bytes32SlotType slot, bytes32 value) internal { - assembly ("memory-safe") { - tstore(slot, value) - } - } - - /** - * @dev Load the value held at location `slot` in transient storage. - */ - function tload(Uint256SlotType slot) internal view returns (uint256 value) { - assembly ("memory-safe") { - value := tload(slot) - } - } - - /** - * @dev Store `value` at location `slot` in transient storage. - */ - function tstore(Uint256SlotType slot, uint256 value) internal { - assembly ("memory-safe") { - tstore(slot, value) - } - } - - /** - * @dev Load the value held at location `slot` in transient storage. - */ - function tload(Int256SlotType slot) internal view returns (int256 value) { - assembly ("memory-safe") { - value := tload(slot) - } - } - - /** - * @dev Store `value` at location `slot` in transient storage. - */ - function tstore(Int256SlotType slot, int256 value) internal { - assembly ("memory-safe") { - tstore(slot, value) - } - } } diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 5448060b70e..f9465eaf04d 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -1,15 +1,18 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/Strings.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/Strings.sol) pragma solidity ^0.8.20; import {Math} from "./math/Math.sol"; +import {SafeCast} from "./math/SafeCast.sol"; import {SignedMath} from "./math/SignedMath.sol"; /** * @dev String operations. */ library Strings { + using SafeCast for *; + bytes16 private constant HEX_DIGITS = "0123456789abcdef"; uint8 private constant ADDRESS_LENGTH = 20; @@ -18,6 +21,16 @@ library Strings { */ error StringsInsufficientHexLength(uint256 value, uint256 length); + /** + * @dev The string being parsed contains characters that are not in scope of the given base. + */ + error StringsInvalidChar(); + + /** + * @dev The string being parsed is not a properly formatted address. + */ + error StringsInvalidAddressFormat(); + /** * @dev Converts a `uint256` to its ASCII `string` decimal representation. */ @@ -113,4 +126,316 @@ library Strings { function equal(string memory a, string memory b) internal pure returns (bool) { return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); } + + /** + * @dev Parse a decimal string and returns the value as a `uint256`. + * + * Requirements: + * - The string must be formatted as `[0-9]*` + * - The result must fit into an `uint256` type + */ + function parseUint(string memory input) internal pure returns (uint256) { + return parseUint(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseUint} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `[0-9]*` + * - The result must fit into an `uint256` type + */ + function parseUint(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) { + (bool success, uint256 value) = tryParseUint(input, begin, end); + if (!success) revert StringsInvalidChar(); + return value; + } + + /** + * @dev Variant of {parseUint-string} that returns false if the parsing fails because of an invalid character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseUint(string memory input) internal pure returns (bool success, uint256 value) { + return _tryParseUintUncheckedBounds(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseUint-string-uint256-uint256} that returns false if the parsing fails because of an invalid + * character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseUint( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, uint256 value) { + if (end > bytes(input).length || begin > end) return (false, 0); + return _tryParseUintUncheckedBounds(input, begin, end); + } + + /** + * @dev Implementation of {tryParseUint} that does not check bounds. Caller should make sure that + * `begin <= end <= input.length`. Other inputs would result in undefined behavior. + */ + function _tryParseUintUncheckedBounds( + string memory input, + uint256 begin, + uint256 end + ) private pure returns (bool success, uint256 value) { + bytes memory buffer = bytes(input); + + uint256 result = 0; + for (uint256 i = begin; i < end; ++i) { + uint8 chr = _tryParseChr(bytes1(_unsafeReadBytesOffset(buffer, i))); + if (chr > 9) return (false, 0); + result *= 10; + result += chr; + } + return (true, result); + } + + /** + * @dev Parse a decimal string and returns the value as a `int256`. + * + * Requirements: + * - The string must be formatted as `[-+]?[0-9]*` + * - The result must fit in an `int256` type. + */ + function parseInt(string memory input) internal pure returns (int256) { + return parseInt(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseInt-string} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `[-+]?[0-9]*` + * - The result must fit in an `int256` type. + */ + function parseInt(string memory input, uint256 begin, uint256 end) internal pure returns (int256) { + (bool success, int256 value) = tryParseInt(input, begin, end); + if (!success) revert StringsInvalidChar(); + return value; + } + + /** + * @dev Variant of {parseInt-string} that returns false if the parsing fails because of an invalid character or if + * the result does not fit in a `int256`. + * + * NOTE: This function will revert if the absolute value of the result does not fit in a `uint256`. + */ + function tryParseInt(string memory input) internal pure returns (bool success, int256 value) { + return _tryParseIntUncheckedBounds(input, 0, bytes(input).length); + } + + uint256 private constant ABS_MIN_INT256 = 2 ** 255; + + /** + * @dev Variant of {parseInt-string-uint256-uint256} that returns false if the parsing fails because of an invalid + * character or if the result does not fit in a `int256`. + * + * NOTE: This function will revert if the absolute value of the result does not fit in a `uint256`. + */ + function tryParseInt( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, int256 value) { + if (end > bytes(input).length || begin > end) return (false, 0); + return _tryParseIntUncheckedBounds(input, begin, end); + } + + /** + * @dev Implementation of {tryParseInt} that does not check bounds. Caller should make sure that + * `begin <= end <= input.length`. Other inputs would result in undefined behavior. + */ + function _tryParseIntUncheckedBounds( + string memory input, + uint256 begin, + uint256 end + ) private pure returns (bool success, int256 value) { + bytes memory buffer = bytes(input); + + // Check presence of a negative sign. + bytes1 sign = begin == end ? bytes1(0) : bytes1(_unsafeReadBytesOffset(buffer, begin)); // don't do out-of-bound (possibly unsafe) read if sub-string is empty + bool positiveSign = sign == bytes1("+"); + bool negativeSign = sign == bytes1("-"); + uint256 offset = (positiveSign || negativeSign).toUint(); + + (bool absSuccess, uint256 absValue) = tryParseUint(input, begin + offset, end); + + if (absSuccess && absValue < ABS_MIN_INT256) { + return (true, negativeSign ? -int256(absValue) : int256(absValue)); + } else if (absSuccess && negativeSign && absValue == ABS_MIN_INT256) { + return (true, type(int256).min); + } else return (false, 0); + } + + /** + * @dev Parse a hexadecimal string (with or without "0x" prefix), and returns the value as a `uint256`. + * + * Requirements: + * - The string must be formatted as `(0x)?[0-9a-fA-F]*` + * - The result must fit in an `uint256` type. + */ + function parseHexUint(string memory input) internal pure returns (uint256) { + return parseHexUint(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseHexUint} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `(0x)?[0-9a-fA-F]*` + * - The result must fit in an `uint256` type. + */ + function parseHexUint(string memory input, uint256 begin, uint256 end) internal pure returns (uint256) { + (bool success, uint256 value) = tryParseHexUint(input, begin, end); + if (!success) revert StringsInvalidChar(); + return value; + } + + /** + * @dev Variant of {parseHexUint-string} that returns false if the parsing fails because of an invalid character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseHexUint(string memory input) internal pure returns (bool success, uint256 value) { + return _tryParseHexUintUncheckedBounds(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseHexUint-string-uint256-uint256} that returns false if the parsing fails because of an + * invalid character. + * + * NOTE: This function will revert if the result does not fit in a `uint256`. + */ + function tryParseHexUint( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, uint256 value) { + if (end > bytes(input).length || begin > end) return (false, 0); + return _tryParseHexUintUncheckedBounds(input, begin, end); + } + + /** + * @dev Implementation of {tryParseHexUint} that does not check bounds. Caller should make sure that + * `begin <= end <= input.length`. Other inputs would result in undefined behavior. + */ + function _tryParseHexUintUncheckedBounds( + string memory input, + uint256 begin, + uint256 end + ) private pure returns (bool success, uint256 value) { + bytes memory buffer = bytes(input); + + // skip 0x prefix if present + bool hasPrefix = (end > begin + 1) && bytes2(_unsafeReadBytesOffset(buffer, begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty + uint256 offset = hasPrefix.toUint() * 2; + + uint256 result = 0; + for (uint256 i = begin + offset; i < end; ++i) { + uint8 chr = _tryParseChr(bytes1(_unsafeReadBytesOffset(buffer, i))); + if (chr > 15) return (false, 0); + 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. + result += chr; + } + } + return (true, result); + } + + /** + * @dev Parse a hexadecimal string (with or without "0x" prefix), and returns the value as an `address`. + * + * Requirements: + * - The string must be formatted as `(0x)?[0-9a-fA-F]{40}` + */ + function parseAddress(string memory input) internal pure returns (address) { + return parseAddress(input, 0, bytes(input).length); + } + + /** + * @dev Variant of {parseAddress} that parses a substring of `input` located between position `begin` (included) and + * `end` (excluded). + * + * Requirements: + * - The substring must be formatted as `(0x)?[0-9a-fA-F]{40}` + */ + function parseAddress(string memory input, uint256 begin, uint256 end) internal pure returns (address) { + (bool success, address value) = tryParseAddress(input, begin, end); + if (!success) revert StringsInvalidAddressFormat(); + return value; + } + + /** + * @dev Variant of {parseAddress-string} that returns false if the parsing fails because the input is not a properly + * formatted address. See {parseAddress} requirements. + */ + function tryParseAddress(string memory input) internal pure returns (bool success, address value) { + return tryParseAddress(input, 0, bytes(input).length); + } + + /** + * @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. + */ + function tryParseAddress( + string memory input, + uint256 begin, + uint256 end + ) internal pure returns (bool success, address value) { + if (end > bytes(input).length || begin > end) return (false, address(0)); + + bool hasPrefix = (end > begin + 1) && bytes2(_unsafeReadBytesOffset(bytes(input), begin)) == bytes2("0x"); // don't do out-of-bound (possibly unsafe) read if sub-string is empty + uint256 expectedLength = 40 + hasPrefix.toUint() * 2; + + // check that input is the correct length + if (end - begin == expectedLength) { + // length guarantees that this does not overflow, and value is at most type(uint160).max + (bool s, uint256 v) = _tryParseHexUintUncheckedBounds(input, begin, end); + return (s, address(uint160(v))); + } else { + return (false, address(0)); + } + } + + function _tryParseChr(bytes1 chr) private pure returns (uint8) { + uint8 value = uint8(chr); + + // Try to parse `chr`: + // - Case 1: [0-9] + // - Case 2: [a-f] + // - Case 3: [A-F] + // - otherwise not supported + unchecked { + if (value > 47 && value < 58) value -= 48; + else if (value > 96 && value < 103) value -= 87; + else if (value > 64 && value < 71) value -= 55; + else return type(uint8).max; + } + + return value; + } + + /** + * @dev Reads a bytes32 from a bytes array without bounds checking. + * + * NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the + * assembly block as such would prevent some optimizations. + */ + function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) { + // This is not memory safe in the general case, but all calls to this private function are within bounds. + assembly ("memory-safe") { + value := mload(add(buffer, add(0x20, offset))) + } + } } diff --git a/contracts/utils/TransientSlot.sol b/contracts/utils/TransientSlot.sol new file mode 100644 index 00000000000..25c57dd3f96 --- /dev/null +++ b/contracts/utils/TransientSlot.sol @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/TransientSlot.sol) +// This file was procedurally generated from scripts/generate/templates/TransientSlot.js. + +pragma solidity ^0.8.24; + +/** + * @dev Library for reading and writing value-types to specific transient storage slots. + * + * Transient slots are often used to store temporary values that are removed after the current transaction. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * * Example reading and writing values using transient storage: + * ```solidity + * contract Lock { + * using TransientSlot for *; + * + * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot. + * bytes32 internal constant _LOCK_SLOT = 0xf4678858b2b588224636b8522b729e7722d32fc491da849ed75b3fdf3c84f542; + * + * modifier locked() { + * require(!_LOCK_SLOT.asBoolean().tload()); + * + * _LOCK_SLOT.asBoolean().tstore(true); + * _; + * _LOCK_SLOT.asBoolean().tstore(false); + * } + * } + * ``` + * + * TIP: Consider using this library along with {SlotDerivation}. + */ +library TransientSlot { + /** + * @dev UDVT that represent a slot holding a address. + */ + type AddressSlot is bytes32; + + /** + * @dev Cast an arbitrary slot to a AddressSlot. + */ + function asAddress(bytes32 slot) internal pure returns (AddressSlot) { + return AddressSlot.wrap(slot); + } + + /** + * @dev UDVT that represent a slot holding a bool. + */ + type BooleanSlot is bytes32; + + /** + * @dev Cast an arbitrary slot to a BooleanSlot. + */ + function asBoolean(bytes32 slot) internal pure returns (BooleanSlot) { + return BooleanSlot.wrap(slot); + } + + /** + * @dev UDVT that represent a slot holding a bytes32. + */ + type Bytes32Slot is bytes32; + + /** + * @dev Cast an arbitrary slot to a Bytes32Slot. + */ + function asBytes32(bytes32 slot) internal pure returns (Bytes32Slot) { + return Bytes32Slot.wrap(slot); + } + + /** + * @dev UDVT that represent a slot holding a uint256. + */ + type Uint256Slot is bytes32; + + /** + * @dev Cast an arbitrary slot to a Uint256Slot. + */ + function asUint256(bytes32 slot) internal pure returns (Uint256Slot) { + return Uint256Slot.wrap(slot); + } + + /** + * @dev UDVT that represent a slot holding a int256. + */ + type Int256Slot is bytes32; + + /** + * @dev Cast an arbitrary slot to a Int256Slot. + */ + function asInt256(bytes32 slot) internal pure returns (Int256Slot) { + return Int256Slot.wrap(slot); + } + + /** + * @dev Load the value held at location `slot` in transient storage. + */ + function tload(AddressSlot slot) internal view returns (address value) { + assembly ("memory-safe") { + value := tload(slot) + } + } + + /** + * @dev Store `value` at location `slot` in transient storage. + */ + function tstore(AddressSlot slot, address value) internal { + assembly ("memory-safe") { + tstore(slot, value) + } + } + + /** + * @dev Load the value held at location `slot` in transient storage. + */ + function tload(BooleanSlot slot) internal view returns (bool value) { + assembly ("memory-safe") { + value := tload(slot) + } + } + + /** + * @dev Store `value` at location `slot` in transient storage. + */ + function tstore(BooleanSlot slot, bool value) internal { + assembly ("memory-safe") { + tstore(slot, value) + } + } + + /** + * @dev Load the value held at location `slot` in transient storage. + */ + function tload(Bytes32Slot slot) internal view returns (bytes32 value) { + assembly ("memory-safe") { + value := tload(slot) + } + } + + /** + * @dev Store `value` at location `slot` in transient storage. + */ + function tstore(Bytes32Slot slot, bytes32 value) internal { + assembly ("memory-safe") { + tstore(slot, value) + } + } + + /** + * @dev Load the value held at location `slot` in transient storage. + */ + function tload(Uint256Slot slot) internal view returns (uint256 value) { + assembly ("memory-safe") { + value := tload(slot) + } + } + + /** + * @dev Store `value` at location `slot` in transient storage. + */ + function tstore(Uint256Slot slot, uint256 value) internal { + assembly ("memory-safe") { + tstore(slot, value) + } + } + + /** + * @dev Load the value held at location `slot` in transient storage. + */ + function tload(Int256Slot slot) internal view returns (int256 value) { + assembly ("memory-safe") { + value := tload(slot) + } + } + + /** + * @dev Store `value` at location `slot` in transient storage. + */ + function tstore(Int256Slot slot, int256 value) internal { + assembly ("memory-safe") { + tstore(slot, value) + } + } +} diff --git a/contracts/utils/cryptography/ECDSA.sol b/contracts/utils/cryptography/ECDSA.sol index 3736171bf9f..6493f56338a 100644 --- a/contracts/utils/cryptography/ECDSA.sol +++ b/contracts/utils/cryptography/ECDSA.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/ECDSA.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/ECDSA.sol) pragma solidity ^0.8.20; @@ -53,7 +53,10 @@ library ECDSA { * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] */ - function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError, bytes32) { + function tryRecover( + bytes32 hash, + bytes memory signature + ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) { if (signature.length == 65) { bytes32 r; bytes32 s; @@ -96,7 +99,11 @@ library ECDSA { * * See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures] */ - function tryRecover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address, RecoverError, bytes32) { + function tryRecover( + bytes32 hash, + bytes32 r, + bytes32 vs + ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) { unchecked { bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); // We do not check for an overflow here since the shift operation results in 0 or 1. @@ -123,7 +130,7 @@ library ECDSA { uint8 v, bytes32 r, bytes32 s - ) internal pure returns (address, RecoverError, bytes32) { + ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) { // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most diff --git a/contracts/utils/cryptography/EIP712.sol b/contracts/utils/cryptography/EIP712.sol index 77c4c8990fe..f15a67bd9b5 100644 --- a/contracts/utils/cryptography/EIP712.sol +++ b/contracts/utils/cryptography/EIP712.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/EIP712.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/EIP712.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/cryptography/Hashes.sol b/contracts/utils/cryptography/Hashes.sol index c78bc80fd79..893883164fb 100644 --- a/contracts/utils/cryptography/Hashes.sol +++ b/contracts/utils/cryptography/Hashes.sol @@ -1,9 +1,12 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/Hashes.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; /** * @dev Library of standard hash functions. + * + * _Available since v5.1._ */ library Hashes { /** diff --git a/contracts/utils/cryptography/MerkleProof.sol b/contracts/utils/cryptography/MerkleProof.sol index a8658013592..19b09e2af65 100644 --- a/contracts/utils/cryptography/MerkleProof.sol +++ b/contracts/utils/cryptography/MerkleProof.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MerkleProof.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/MerkleProof.sol) // This file was procedurally generated from scripts/generate/templates/MerkleProof.js. pragma solidity ^0.8.20; diff --git a/contracts/utils/cryptography/MessageHashUtils.sol b/contracts/utils/cryptography/MessageHashUtils.sol index 35746ce72ce..e1cbccb65ec 100644 --- a/contracts/utils/cryptography/MessageHashUtils.sol +++ b/contracts/utils/cryptography/MessageHashUtils.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MessageHashUtils.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/MessageHashUtils.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/cryptography/P256.sol b/contracts/utils/cryptography/P256.sol index 69717bdc51a..510eb55e700 100644 --- a/contracts/utils/cryptography/P256.sol +++ b/contracts/utils/cryptography/P256.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/P256.sol) pragma solidity ^0.8.20; import {Math} from "../math/Math.sol"; @@ -11,9 +12,11 @@ import {Errors} from "../Errors.sol"; * and cryptographic standards. Some notable examples include Apple's Secure Enclave and Android's Keystore * as well as authentication protocols like FIDO2. * - * Based on the original https://github.com/itsobvioustech/aa-passkeys-wallet/blob/main/src/Secp256r1.sol[implementation of itsobvioustech]. - * Heavily inspired in https://github.com/maxrobot/elliptic-solidity/blob/master/contracts/Secp256r1.sol[maxrobot] and - * https://github.com/tdrerup/elliptic-curve-solidity/blob/master/contracts/curves/EllipticCurve.sol[tdrerup] implementations. + * Based on the original https://github.com/itsobvioustech/aa-passkeys-wallet/blob/d3d423f28a4d8dfcb203c7fa0c47f42592a7378e/src/Secp256r1.sol[implementation of itsobvioustech] (GNU General Public License v3.0). + * Heavily inspired in https://github.com/maxrobot/elliptic-solidity/blob/c4bb1b6e8ae89534d8db3a6b3a6b52219100520f/contracts/Secp256r1.sol[maxrobot] and + * https://github.com/tdrerup/elliptic-curve-solidity/blob/59a9c25957d4d190eff53b6610731d81a077a15e/contracts/curves/EllipticCurve.sol[tdrerup] implementations. + * + * _Available since v5.1._ */ library P256 { struct JPoint { @@ -120,7 +123,7 @@ library P256 { * IMPORTANT: This function disallows signatures where the `s` value is above `N/2` to prevent malleability. * To flip the `s` value, compute `s = N - s` and `v = 1 - v` if (`v = 0 | 1`). */ - function recovery(bytes32 h, uint8 v, bytes32 r, bytes32 s) internal view returns (bytes32, bytes32) { + function recovery(bytes32 h, uint8 v, bytes32 r, bytes32 s) internal view returns (bytes32 x, bytes32 y) { if (!_isProperSignature(r, s) || v > 1) { return (0, 0); } @@ -130,19 +133,19 @@ library P256 { uint256 ry2 = addmod(mulmod(addmod(mulmod(rx, rx, p), A, p), rx, p), B, p); // weierstrass equation y² = x³ + a.x + b uint256 ry = Math.modExp(ry2, P1DIV4, p); // This formula for sqrt work because P ≡ 3 (mod 4) if (mulmod(ry, ry, p) != ry2) return (0, 0); // Sanity check - if (ry % 2 != v % 2) ry = p - ry; + if (ry % 2 != v) ry = p - ry; JPoint[16] memory points = _preComputeJacobianPoints(rx, ry); uint256 w = Math.invModPrime(uint256(r), N); uint256 u1 = mulmod(N - (uint256(h) % N), w, N); uint256 u2 = mulmod(uint256(s), w, N); - (uint256 x, uint256 y) = _jMultShamir(points, u1, u2); - return (bytes32(x), bytes32(y)); + (uint256 xU, uint256 yU) = _jMultShamir(points, u1, u2); + return (bytes32(xU), bytes32(yU)); } /** * @dev Checks if (x, y) are valid coordinates of a point on the curve. - * In particular this function checks that x <= P and y <= P. + * In particular this function checks that x < P and y < P. */ function isValidPublicKey(bytes32 x, bytes32 y) internal pure returns (bool result) { assembly ("memory-safe") { @@ -183,6 +186,13 @@ library P256 { /** * @dev Point addition on the jacobian coordinates * Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-1998-cmo-2 + * + * Note that: + * + * - `addition-add-1998-cmo-2` doesn't support identical input points. This version is modified to use + * the `h` and `r` values computed by `addition-add-1998-cmo-2` to detect identical inputs, and fallback to + * `doubling-dbl-1998-cmo-2` if needed. + * - if one of the points is at infinity (i.e. `z=0`), the result is undefined. */ function _jAdd( JPoint memory p1, @@ -195,25 +205,53 @@ library P256 { let z1 := mload(add(p1, 0x40)) let zz1 := mulmod(z1, z1, p) // zz1 = z1² let s1 := mulmod(mload(add(p1, 0x20)), mulmod(mulmod(z2, z2, p), z2, p), p) // s1 = y1*z2³ - let r := addmod(mulmod(y2, mulmod(zz1, z1, p), p), sub(p, s1), p) // r = s2-s1 = y2*z1³-s1 + let r := addmod(mulmod(y2, mulmod(zz1, z1, p), p), sub(p, s1), p) // r = s2-s1 = y2*z1³-s1 = y2*z1³-y1*z2³ let u1 := mulmod(mload(p1), mulmod(z2, z2, p), p) // u1 = x1*z2² - let h := addmod(mulmod(x2, zz1, p), sub(p, u1), p) // h = u2-u1 = x2*z1²-u1 - let hh := mulmod(h, h, p) // h² + let h := addmod(mulmod(x2, zz1, p), sub(p, u1), p) // h = u2-u1 = x2*z1²-u1 = x2*z1²-x1*z2² + + // detect edge cases where inputs are identical + switch and(iszero(r), iszero(h)) + // case 0: points are different + case 0 { + let hh := mulmod(h, h, p) // h² + + // x' = r²-h³-2*u1*h² + rx := addmod( + addmod(mulmod(r, r, p), sub(p, mulmod(h, hh, p)), p), + sub(p, mulmod(2, mulmod(u1, hh, p), p)), + p + ) + // y' = r*(u1*h²-x')-s1*h³ + ry := addmod( + mulmod(r, addmod(mulmod(u1, hh, p), sub(p, rx), p), p), + sub(p, mulmod(s1, mulmod(h, hh, p), p)), + p + ) + // z' = h*z1*z2 + rz := mulmod(h, mulmod(z1, z2, p), p) + } + // case 1: points are equal + case 1 { + let x := x2 + let y := y2 + let z := z2 + let yy := mulmod(y, y, p) + let zz := mulmod(z, z, p) + let m := addmod(mulmod(3, mulmod(x, x, p), p), mulmod(A, mulmod(zz, zz, p), p), p) // m = 3*x²+a*z⁴ + let s := mulmod(4, mulmod(x, yy, p), p) // s = 4*x*y² + + // x' = t = m²-2*s + rx := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) - // x' = r²-h³-2*u1*h² - rx := addmod( - addmod(mulmod(r, r, p), sub(p, mulmod(h, hh, p)), p), - sub(p, mulmod(2, mulmod(u1, hh, p), p)), - p - ) - // y' = r*(u1*h²-x')-s1*h³ - ry := addmod( - mulmod(r, addmod(mulmod(u1, hh, p), sub(p, rx), p), p), - sub(p, mulmod(s1, mulmod(h, hh, p), p)), - p - ) - // z' = h*z1*z2 - rz := mulmod(h, mulmod(z1, z2, p), p) + // y' = m*(s-t)-8*y⁴ = m*(s-x')-8*y⁴ + // cut the computation to avoid stack too deep + let rytmp1 := sub(p, mulmod(8, mulmod(yy, yy, p), p)) // -8*y⁴ + let rytmp2 := addmod(s, sub(p, rx), p) // s-x' + ry := addmod(mulmod(m, rytmp2, p), rytmp1, p) // m*(s-x')-8*y⁴ + + // z' = 2*y*z + rz := mulmod(2, mulmod(y, z, p), p) + } } } @@ -226,8 +264,8 @@ library P256 { let p := P let yy := mulmod(y, y, p) let zz := mulmod(z, z, p) - let s := mulmod(4, mulmod(x, yy, p), p) // s = 4*x*y² let m := addmod(mulmod(3, mulmod(x, x, p), p), mulmod(A, mulmod(zz, zz, p), p), p) // m = 3*x²+a*z⁴ + let s := mulmod(4, mulmod(x, yy, p), p) // s = 4*x*y² // x' = t = m²-2*s rx := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) @@ -239,15 +277,20 @@ library P256 { } /** - * @dev Compute P·u1 + Q·u2 using the precomputed points for P and Q (see {_preComputeJacobianPoints}). + * @dev Compute G·u1 + P·u2 using the precomputed points for G and P (see {_preComputeJacobianPoints}). * * Uses Strauss Shamir trick for EC multiplication * https://stackoverflow.com/questions/50993471/ec-scalar-multiplication-with-strauss-shamir-method - * we optimise on this a bit to do with 2 bits at a time rather than a single bit - * the individual points for a single pass are precomputed - * overall this reduces the number of additions while keeping the same number of doublings + * + * We optimize this for 2 bits at a time rather than a single bit. The individual points for a single pass are + * precomputed. Overall this reduces the number of additions while keeping the same number of + * doublings */ - function _jMultShamir(JPoint[16] memory points, uint256 u1, uint256 u2) private view returns (uint256, uint256) { + function _jMultShamir( + JPoint[16] memory points, + uint256 u1, + uint256 u2 + ) private view returns (uint256 rx, uint256 ry) { uint256 x = 0; uint256 y = 0; uint256 z = 0; @@ -257,9 +300,14 @@ library P256 { (x, y, z) = _jDouble(x, y, z); (x, y, z) = _jDouble(x, y, z); } - // Read 2 bits of u1, and 2 bits of u2. Combining the two give a lookup index in the table. + // Read 2 bits of u1, and 2 bits of u2. Combining the two gives the lookup index in the table. uint256 pos = ((u1 >> 252) & 0xc) | ((u2 >> 254) & 0x3); - if (pos > 0) { + // Points that have z = 0 are points at infinity. They are the additive 0 of the group + // - if the lookup point is a 0, we can skip it + // - otherwise: + // - if the current point (x, y, z) is 0, we use the lookup point as our new value (0+P=P) + // - if the current point (x, y, z) is not 0, both points are valid and we can use `_jAdd` + if (points[pos].z != 0) { if (z == 0) { (x, y, z) = (points[pos].x, points[pos].y, points[pos].z); } else { @@ -285,6 +333,11 @@ library P256 { * │ 8 │ 2g 2g+p 2g+2p 2g+3p │ * │ 12 │ 3g 3g+p 3g+2p 3g+3p │ * └────┴─────────────────────┘ + * + * Note that `_jAdd` (and thus `_jAddPoint`) does not handle the case where one of the inputs is a point at + * infinity (z = 0). However, we know that since `N ≡ 1 mod 2` and `N ≡ 1 mod 3`, there is no point P such that + * 2P = 0 or 3P = 0. This guarantees that g, 2g, 3g, p, 2p, 3p are all non-zero, and that all `_jAddPoint` calls + * have valid inputs. */ function _preComputeJacobianPoints(uint256 px, uint256 py) private pure returns (JPoint[16] memory points) { points[0x00] = JPoint(0, 0, 0); // 0,0 @@ -292,17 +345,17 @@ library P256 { points[0x04] = JPoint(GX, GY, 1); // 0,1 (g) points[0x02] = _jDoublePoint(points[0x01]); // 2,0 (2p) points[0x08] = _jDoublePoint(points[0x04]); // 0,2 (2g) - points[0x03] = _jAddPoint(points[0x01], points[0x02]); // 3,0 (3p) + points[0x03] = _jAddPoint(points[0x01], points[0x02]); // 3,0 (p+2p = 3p) points[0x05] = _jAddPoint(points[0x01], points[0x04]); // 1,1 (p+g) points[0x06] = _jAddPoint(points[0x02], points[0x04]); // 2,1 (2p+g) points[0x07] = _jAddPoint(points[0x03], points[0x04]); // 3,1 (3p+g) points[0x09] = _jAddPoint(points[0x01], points[0x08]); // 1,2 (p+2g) points[0x0a] = _jAddPoint(points[0x02], points[0x08]); // 2,2 (2p+2g) points[0x0b] = _jAddPoint(points[0x03], points[0x08]); // 3,2 (3p+2g) - points[0x0c] = _jAddPoint(points[0x04], points[0x08]); // 0,3 (g+2g) + points[0x0c] = _jAddPoint(points[0x04], points[0x08]); // 0,3 (g+2g = 3g) points[0x0d] = _jAddPoint(points[0x01], points[0x0c]); // 1,3 (p+3g) points[0x0e] = _jAddPoint(points[0x02], points[0x0c]); // 2,3 (2p+3g) - points[0x0f] = _jAddPoint(points[0x03], points[0x0C]); // 3,3 (3p+3g) + points[0x0f] = _jAddPoint(points[0x03], points[0x0c]); // 3,3 (3p+3g) } function _jAddPoint(JPoint memory p1, JPoint memory p2) private pure returns (JPoint memory) { diff --git a/contracts/utils/cryptography/RSA.sol b/contracts/utils/cryptography/RSA.sol index 1b5860c3b2c..4e04ce5cc56 100644 --- a/contracts/utils/cryptography/RSA.sol +++ b/contracts/utils/cryptography/RSA.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/RSA.sol) pragma solidity ^0.8.20; import {Math} from "../math/Math.sol"; @@ -10,11 +11,13 @@ import {Math} from "../math/Math.sol"; * The padding follows the EMSA-PKCS1-v1_5-ENCODE encoding definition as per section 9.2 of the RFC. This padding makes * RSA semantically secure for signing messages. * - * Inspired by https://github.com/adria0/SolRsaVerify[Adrià Massanet's work] + * Inspired by https://github.com/adria0/SolRsaVerify/blob/79c6182cabb9102ea69d4a2e996816091d5f1cd1[Adrià Massanet's work] (GNU General Public License v3.0). + * + * _Available since v5.1._ */ library RSA { /** - * @dev Same as {pkcs1} but using SHA256 to calculate the digest of `data`. + * @dev Same as {pkcs1Sha256} but using SHA256 to calculate the digest of `data`. */ function pkcs1Sha256( bytes memory data, @@ -22,30 +25,36 @@ library RSA { bytes memory e, bytes memory n ) internal view returns (bool) { - return pkcs1(sha256(data), s, e, n); + return pkcs1Sha256(sha256(data), s, e, n); } /** * @dev Verifies a PKCSv1.5 signature given a digest according to the verification - * method described in https://datatracker.ietf.org/doc/html/rfc8017#section-8.2.2[section 8.2.2 of RFC8017]. + * method described in https://datatracker.ietf.org/doc/html/rfc8017#section-8.2.2[section 8.2.2 of RFC8017] with + * support for explicit or implicit NULL parameters in the DigestInfo (no other optional parameters are supported). + * + * IMPORTANT: For security reason, this function requires the signature and modulus to have a length of at least + * 2048 bits. If you use a smaller key, consider replacing it with a larger, more secure, one. * - * IMPORTANT: Although this function allows for it, using n of length 1024 bits is considered unsafe. - * Consider using at least 2048 bits. + * WARNING: This verification algorithm doesn't prevent replayability. If called multiple times with the same + * digest, public key and (valid signature), it will return true every time. Consider including an onchain nonce + * or unique identifier in the message to prevent replay attacks. * - * WARNING: PKCS#1 v1.5 allows for replayability given the message may contain arbitrary optional parameters in the - * DigestInfo. Consider using an onchain nonce or unique identifier to include in the message to prevent replay attacks. + * WARNING: This verification algorithm supports any exponent. NIST recommends using `65537` (or higher). + * That is the default value many libraries use, such as OpenSSL. Developers may choose to reject public keys + * using a low exponent out of security concerns. * * @param digest the digest to verify * @param s is a buffer containing the signature * @param e is the exponent of the public key * @param n is the modulus of the public key */ - function pkcs1(bytes32 digest, bytes memory s, bytes memory e, bytes memory n) internal view returns (bool) { + function pkcs1Sha256(bytes32 digest, bytes memory s, bytes memory e, bytes memory n) internal view returns (bool) { unchecked { // cache and check length uint256 length = n.length; if ( - length < 0x40 || // PKCS#1 padding is slightly less than 0x40 bytes at the bare minimum + length < 0x100 || // Enforce 2048 bits minimum length != s.length // signature must have the same length as the finite field ) { return false; @@ -78,7 +87,7 @@ library RSA { // - PS is padding filled with 0xFF // - DigestInfo ::= SEQUENCE { // digestAlgorithm AlgorithmIdentifier, - // [optional algorithm parameters] + // [optional algorithm parameters] -- not currently supported // digest OCTET STRING // } @@ -94,13 +103,13 @@ library RSA { // it should be at 32 (digest) + 2 bytes from the end. To those 34 bytes, we add the // OID (9 bytes) and its length (2 bytes) to get the position of the DigestInfo sequence, // which is expected to have a length of 0x31 when the NULL param is present or 0x2f if not. - if (bytes1(_unsafeReadBytes32(buffer, length - 50)) == 0x31) { + if (bytes1(_unsafeReadBytes32(buffer, length - 0x32)) == 0x31) { offset = 0x34; // 00 (1 byte) | SEQUENCE length (0x31) = 3031 (2 bytes) | SEQUENCE length (0x0d) = 300d (2 bytes) | OBJECT_IDENTIFIER length (0x09) = 0609 (2 bytes) // SHA256 OID = 608648016503040201 (9 bytes) | NULL = 0500 (2 bytes) (explicit) | OCTET_STRING length (0x20) = 0420 (2 bytes) params = 0x003031300d060960864801650304020105000420000000000000000000000000; mask = 0xffffffffffffffffffffffffffffffffffffffff000000000000000000000000; // (20 bytes) - } else if (bytes1(_unsafeReadBytes32(buffer, length - 48)) == 0x2F) { + } else if (bytes1(_unsafeReadBytes32(buffer, length - 0x30)) == 0x2F) { offset = 0x32; // 00 (1 byte) | SEQUENCE length (0x2f) = 302f (2 bytes) | SEQUENCE length (0x0b) = 300b (2 bytes) | OBJECT_IDENTIFIER length (0x09) = 0609 (2 bytes) // SHA256 OID = 608648016503040201 (9 bytes) | NULL = | OCTET_STRING length (0x20) = 0420 (2 bytes) @@ -111,7 +120,7 @@ library RSA { return false; } - // Length is at least 0x40 and offset is at most 0x34, so this is safe. There is always some padding. + // Length is at least 0x100 and offset is at most 0x34, so this is safe. There is always some padding. uint256 paddingEnd = length - offset; // The padding has variable (arbitrary) length, so we check it byte per byte in a loop. @@ -137,7 +146,7 @@ library RSA { /// @dev Reads a bytes32 from a bytes array without bounds checking. function _unsafeReadBytes32(bytes memory array, uint256 offset) private pure returns (bytes32 result) { // Memory safeness is guaranteed as long as the provided `array` is a Solidity-allocated bytes array - // and `offset` is within bounds. This is the case for all calls to this private function from {pkcs1}. + // and `offset` is within bounds. This is the case for all calls to this private function from {pkcs1Sha256}. assembly ("memory-safe") { result := mload(add(add(array, 0x20), offset)) } diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 9aaa2e0716c..554f00d89d1 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/SignatureChecker.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/SignatureChecker.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/introspection/ERC165.sol b/contracts/utils/introspection/ERC165.sol index 664b39fc19d..9fbce0447e7 100644 --- a/contracts/utils/introspection/ERC165.sol +++ b/contracts/utils/introspection/ERC165.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/ERC165.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/ERC165.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/introspection/ERC165Checker.sol b/contracts/utils/introspection/ERC165Checker.sol index a0ac72c21bf..8650f5503cc 100644 --- a/contracts/utils/introspection/ERC165Checker.sol +++ b/contracts/utils/introspection/ERC165Checker.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/ERC165Checker.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/ERC165Checker.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/introspection/IERC165.sol b/contracts/utils/introspection/IERC165.sol index bfbdf5dda39..719ec358659 100644 --- a/contracts/utils/introspection/IERC165.sol +++ b/contracts/utils/introspection/IERC165.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/introspection/IERC165.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/introspection/IERC165.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 9e277f3f50d..2acd0754060 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/Math.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/Math.sol) pragma solidity ^0.8.20; @@ -537,41 +537,45 @@ library Math { * @dev Return the log in base 2 of a positive value rounded towards zero. * Returns 0 if given 0. */ - function log2(uint256 value) internal pure returns (uint256) { - uint256 result = 0; - uint256 exp; - unchecked { - exp = 128 * SafeCast.toUint(value > (1 << 128) - 1); - value >>= exp; - result += exp; - - exp = 64 * SafeCast.toUint(value > (1 << 64) - 1); - value >>= exp; - result += exp; - - exp = 32 * SafeCast.toUint(value > (1 << 32) - 1); - value >>= exp; - result += exp; - - exp = 16 * SafeCast.toUint(value > (1 << 16) - 1); - value >>= exp; - result += exp; - - exp = 8 * SafeCast.toUint(value > (1 << 8) - 1); - value >>= exp; - result += exp; - - exp = 4 * SafeCast.toUint(value > (1 << 4) - 1); - value >>= exp; - result += exp; - - exp = 2 * SafeCast.toUint(value > (1 << 2) - 1); - value >>= exp; - result += exp; - - result += SafeCast.toUint(value > 1); + function log2(uint256 x) internal pure returns (uint256 r) { + // If value has upper 128 bits set, log2 result is at least 128 + r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7; + // If upper 64 bits of 128-bit half set, add 64 to result + r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6; + // If upper 32 bits of 64-bit half set, add 32 to result + r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5; + // If upper 16 bits of 32-bit half set, add 16 to result + r |= SafeCast.toUint((x >> r) > 0xffff) << 4; + // If upper 8 bits of 16-bit half set, add 8 to result + r |= SafeCast.toUint((x >> r) > 0xff) << 3; + // If upper 4 bits of 8-bit half set, add 4 to result + r |= SafeCast.toUint((x >> r) > 0xf) << 2; + + // Shifts value right by the current result and use it as an index into this lookup table: + // + // | x (4 bits) | index | table[index] = MSB position | + // |------------|---------|-----------------------------| + // | 0000 | 0 | table[0] = 0 | + // | 0001 | 1 | table[1] = 0 | + // | 0010 | 2 | table[2] = 1 | + // | 0011 | 3 | table[3] = 1 | + // | 0100 | 4 | table[4] = 2 | + // | 0101 | 5 | table[5] = 2 | + // | 0110 | 6 | table[6] = 2 | + // | 0111 | 7 | table[7] = 2 | + // | 1000 | 8 | table[8] = 3 | + // | 1001 | 9 | table[9] = 3 | + // | 1010 | 10 | table[10] = 3 | + // | 1011 | 11 | table[11] = 3 | + // | 1100 | 12 | table[12] = 3 | + // | 1101 | 13 | table[13] = 3 | + // | 1110 | 14 | table[14] = 3 | + // | 1111 | 15 | table[15] = 3 | + // + // The lookup table is represented as a 32-byte value with the MSB positions for 0-15 in the last 16 bytes. + assembly ("memory-safe") { + r := or(r, byte(shr(r, x), 0x0000010102020202030303030303030300000000000000000000000000000000)) } - return result; } /** @@ -640,29 +644,17 @@ library Math { * * Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string. */ - function log256(uint256 value) internal pure returns (uint256) { - uint256 result = 0; - uint256 isGt; - unchecked { - isGt = SafeCast.toUint(value > (1 << 128) - 1); - value >>= isGt * 128; - result += isGt * 16; - - isGt = SafeCast.toUint(value > (1 << 64) - 1); - value >>= isGt * 64; - result += isGt * 8; - - isGt = SafeCast.toUint(value > (1 << 32) - 1); - value >>= isGt * 32; - result += isGt * 4; - - isGt = SafeCast.toUint(value > (1 << 16) - 1); - value >>= isGt * 16; - result += isGt * 2; - - result += SafeCast.toUint(value > (1 << 8) - 1); - } - return result; + function log256(uint256 x) internal pure returns (uint256 r) { + // If value has upper 128 bits set, log2 result is at least 128 + r = SafeCast.toUint(x > 0xffffffffffffffffffffffffffffffff) << 7; + // If upper 64 bits of 128-bit half set, add 64 to result + r |= SafeCast.toUint((x >> r) > 0xffffffffffffffff) << 6; + // If upper 32 bits of 64-bit half set, add 32 to result + r |= SafeCast.toUint((x >> r) > 0xffffffff) << 5; + // If upper 16 bits of 32-bit half set, add 16 to result + r |= SafeCast.toUint((x >> r) > 0xffff) << 4; + // Add 1 if upper 8 bits of 16-bit half set, and divide accumulated result by 8 + return (r >> 3) | SafeCast.toUint((x >> r) > 0xff); } /** diff --git a/contracts/utils/math/SafeCast.sol b/contracts/utils/math/SafeCast.sol index 36832006ebf..b345ede1e69 100644 --- a/contracts/utils/math/SafeCast.sol +++ b/contracts/utils/math/SafeCast.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SafeCast.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SafeCast.sol) // This file was procedurally generated from scripts/generate/templates/SafeCast.js. pragma solidity ^0.8.20; diff --git a/contracts/utils/math/SignedMath.sol b/contracts/utils/math/SignedMath.sol index 502f4ecb03f..7c97aa4c22d 100644 --- a/contracts/utils/math/SignedMath.sol +++ b/contracts/utils/math/SignedMath.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/math/SignedMath.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/math/SignedMath.sol) pragma solidity ^0.8.20; diff --git a/contracts/utils/structs/Checkpoints.sol b/contracts/utils/structs/Checkpoints.sol index 8d1575e162e..8d8f130319d 100644 --- a/contracts/utils/structs/Checkpoints.sol +++ b/contracts/utils/structs/Checkpoints.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/Checkpoints.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/structs/Checkpoints.sol) // This file was procedurally generated from scripts/generate/templates/Checkpoints.js. pragma solidity ^0.8.20; @@ -36,7 +36,11 @@ library Checkpoints { * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint32).max` key set will disable the * library. */ - function push(Trace224 storage self, uint32 key, uint224 value) internal returns (uint224, uint224) { + function push( + Trace224 storage self, + uint32 key, + uint224 value + ) internal returns (uint224 oldValue, uint224 newValue) { return _insert(self._checkpoints, key, value); } @@ -127,7 +131,11 @@ library Checkpoints { * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, * or by updating the last one. */ - function _insert(Checkpoint224[] storage self, uint32 key, uint224 value) private returns (uint224, uint224) { + function _insert( + Checkpoint224[] storage self, + uint32 key, + uint224 value + ) private returns (uint224 oldValue, uint224 newValue) { uint256 pos = self.length; if (pos > 0) { @@ -231,7 +239,11 @@ library Checkpoints { * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint48).max` key set will disable the * library. */ - function push(Trace208 storage self, uint48 key, uint208 value) internal returns (uint208, uint208) { + function push( + Trace208 storage self, + uint48 key, + uint208 value + ) internal returns (uint208 oldValue, uint208 newValue) { return _insert(self._checkpoints, key, value); } @@ -322,7 +334,11 @@ library Checkpoints { * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, * or by updating the last one. */ - function _insert(Checkpoint208[] storage self, uint48 key, uint208 value) private returns (uint208, uint208) { + function _insert( + Checkpoint208[] storage self, + uint48 key, + uint208 value + ) private returns (uint208 oldValue, uint208 newValue) { uint256 pos = self.length; if (pos > 0) { @@ -426,7 +442,11 @@ library Checkpoints { * IMPORTANT: Never accept `key` as a user input, since an arbitrary `type(uint96).max` key set will disable the * library. */ - function push(Trace160 storage self, uint96 key, uint160 value) internal returns (uint160, uint160) { + function push( + Trace160 storage self, + uint96 key, + uint160 value + ) internal returns (uint160 oldValue, uint160 newValue) { return _insert(self._checkpoints, key, value); } @@ -517,7 +537,11 @@ library Checkpoints { * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, * or by updating the last one. */ - function _insert(Checkpoint160[] storage self, uint96 key, uint160 value) private returns (uint160, uint160) { + function _insert( + Checkpoint160[] storage self, + uint96 key, + uint160 value + ) private returns (uint160 oldValue, uint160 newValue) { uint256 pos = self.length; if (pos > 0) { diff --git a/contracts/utils/structs/CircularBuffer.sol b/contracts/utils/structs/CircularBuffer.sol index ae4e082595a..ae0a4a88f68 100644 --- a/contracts/utils/structs/CircularBuffer.sol +++ b/contracts/utils/structs/CircularBuffer.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/structs/CircularBuffer.sol) pragma solidity ^0.8.20; import {Math} from "../math/Math.sol"; @@ -34,8 +35,15 @@ import {Panic} from "../Panic.sol"; * CircularBuffer.Bytes32CircularBuffer private myBuffer; * } * ``` + * + * _Available since v5.1._ */ library CircularBuffer { + /** + * @dev Error emitted when trying to setup a buffer with a size of 0. + */ + error InvalidBufferSize(); + /** * @dev Counts the number of items that have been pushed to the buffer. The residuo modulo _data.length indicates * where the next value should be stored. @@ -44,8 +52,9 @@ library CircularBuffer { * directly. Use the functions provided below instead. Modifying the struct manually may violate assumptions and * lead to unexpected behavior. * - * The last item is at data[(index - 1) % data.length] and the last item is at data[index % data.length]. This - * range can wrap around. + * In a full buffer: + * - The most recently pushed item (last) is at data[(index - 1) % data.length] + * - The oldest item (first) is at data[index % data.length] */ struct Bytes32CircularBuffer { uint256 _count; @@ -61,6 +70,7 @@ library CircularBuffer { * Consider a large buffer size may render the function unusable. */ function setup(Bytes32CircularBuffer storage self, uint256 size) internal { + if (size == 0) revert InvalidBufferSize(); clear(self); Arrays.unsafeSetLength(self._data, size); } diff --git a/contracts/utils/structs/DoubleEndedQueue.sol b/contracts/utils/structs/DoubleEndedQueue.sol index 48f0d68c1eb..f243243bbd7 100644 --- a/contracts/utils/structs/DoubleEndedQueue.sol +++ b/contracts/utils/structs/DoubleEndedQueue.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/DoubleEndedQueue.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/structs/DoubleEndedQueue.sol) pragma solidity ^0.8.20; import {Panic} from "../Panic.sol"; diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index e61182e1fbd..4e12acec142 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/contracts/utils/structs/EnumerableMap.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableMap.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/structs/EnumerableMap.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableMap.js. pragma solidity ^0.8.20; @@ -114,21 +114,21 @@ library EnumerableMap { * * - `index` must be strictly less than {length}. */ - function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32, bytes32) { - bytes32 key = map._keys.at(index); - return (key, map._values[key]); + function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32 key, bytes32 value) { + bytes32 atKey = map._keys.at(index); + return (atKey, map._values[atKey]); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ - function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool, bytes32) { - bytes32 value = map._values[key]; - if (value == bytes32(0)) { + function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool exists, bytes32 value) { + bytes32 val = map._values[key]; + if (val == bytes32(0)) { return (contains(map, key), bytes32(0)); } else { - return (true, value); + return (true, val); } } @@ -208,18 +208,18 @@ library EnumerableMap { * * - `index` must be strictly less than {length}. */ - function at(UintToUintMap storage map, uint256 index) internal view returns (uint256, uint256) { - (bytes32 key, bytes32 value) = at(map._inner, index); - return (uint256(key), uint256(value)); + function at(UintToUintMap storage map, uint256 index) internal view returns (uint256 key, uint256 value) { + (bytes32 atKey, bytes32 val) = at(map._inner, index); + return (uint256(atKey), uint256(val)); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ - function tryGet(UintToUintMap storage map, uint256 key) internal view returns (bool, uint256) { - (bool success, bytes32 value) = tryGet(map._inner, bytes32(key)); - return (success, uint256(value)); + function tryGet(UintToUintMap storage map, uint256 key) internal view returns (bool exists, uint256 value) { + (bool success, bytes32 val) = tryGet(map._inner, bytes32(key)); + return (success, uint256(val)); } /** @@ -301,18 +301,18 @@ library EnumerableMap { * * - `index` must be strictly less than {length}. */ - function at(UintToAddressMap storage map, uint256 index) internal view returns (uint256, address) { - (bytes32 key, bytes32 value) = at(map._inner, index); - return (uint256(key), address(uint160(uint256(value)))); + function at(UintToAddressMap storage map, uint256 index) internal view returns (uint256 key, address value) { + (bytes32 atKey, bytes32 val) = at(map._inner, index); + return (uint256(atKey), address(uint160(uint256(val)))); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ - function tryGet(UintToAddressMap storage map, uint256 key) internal view returns (bool, address) { - (bool success, bytes32 value) = tryGet(map._inner, bytes32(key)); - return (success, address(uint160(uint256(value)))); + function tryGet(UintToAddressMap storage map, uint256 key) internal view returns (bool exists, address value) { + (bool success, bytes32 val) = tryGet(map._inner, bytes32(key)); + return (success, address(uint160(uint256(val)))); } /** @@ -394,18 +394,18 @@ library EnumerableMap { * * - `index` must be strictly less than {length}. */ - function at(UintToBytes32Map storage map, uint256 index) internal view returns (uint256, bytes32) { - (bytes32 key, bytes32 value) = at(map._inner, index); - return (uint256(key), value); + function at(UintToBytes32Map storage map, uint256 index) internal view returns (uint256 key, bytes32 value) { + (bytes32 atKey, bytes32 val) = at(map._inner, index); + return (uint256(atKey), val); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ - function tryGet(UintToBytes32Map storage map, uint256 key) internal view returns (bool, bytes32) { - (bool success, bytes32 value) = tryGet(map._inner, bytes32(key)); - return (success, value); + function tryGet(UintToBytes32Map storage map, uint256 key) internal view returns (bool exists, bytes32 value) { + (bool success, bytes32 val) = tryGet(map._inner, bytes32(key)); + return (success, val); } /** @@ -487,18 +487,18 @@ library EnumerableMap { * * - `index` must be strictly less than {length}. */ - function at(AddressToUintMap storage map, uint256 index) internal view returns (address, uint256) { - (bytes32 key, bytes32 value) = at(map._inner, index); - return (address(uint160(uint256(key))), uint256(value)); + function at(AddressToUintMap storage map, uint256 index) internal view returns (address key, uint256 value) { + (bytes32 atKey, bytes32 val) = at(map._inner, index); + return (address(uint160(uint256(atKey))), uint256(val)); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ - function tryGet(AddressToUintMap storage map, address key) internal view returns (bool, uint256) { - (bool success, bytes32 value) = tryGet(map._inner, bytes32(uint256(uint160(key)))); - return (success, uint256(value)); + function tryGet(AddressToUintMap storage map, address key) internal view returns (bool exists, uint256 value) { + (bool success, bytes32 val) = tryGet(map._inner, bytes32(uint256(uint160(key)))); + return (success, uint256(val)); } /** @@ -580,18 +580,18 @@ library EnumerableMap { * * - `index` must be strictly less than {length}. */ - function at(AddressToAddressMap storage map, uint256 index) internal view returns (address, address) { - (bytes32 key, bytes32 value) = at(map._inner, index); - return (address(uint160(uint256(key))), address(uint160(uint256(value)))); + function at(AddressToAddressMap storage map, uint256 index) internal view returns (address key, address value) { + (bytes32 atKey, bytes32 val) = at(map._inner, index); + return (address(uint160(uint256(atKey))), address(uint160(uint256(val)))); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ - function tryGet(AddressToAddressMap storage map, address key) internal view returns (bool, address) { - (bool success, bytes32 value) = tryGet(map._inner, bytes32(uint256(uint160(key)))); - return (success, address(uint160(uint256(value)))); + function tryGet(AddressToAddressMap storage map, address key) internal view returns (bool exists, address value) { + (bool success, bytes32 val) = tryGet(map._inner, bytes32(uint256(uint160(key)))); + return (success, address(uint160(uint256(val)))); } /** @@ -673,18 +673,18 @@ library EnumerableMap { * * - `index` must be strictly less than {length}. */ - function at(AddressToBytes32Map storage map, uint256 index) internal view returns (address, bytes32) { - (bytes32 key, bytes32 value) = at(map._inner, index); - return (address(uint160(uint256(key))), value); + function at(AddressToBytes32Map storage map, uint256 index) internal view returns (address key, bytes32 value) { + (bytes32 atKey, bytes32 val) = at(map._inner, index); + return (address(uint160(uint256(atKey))), val); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ - function tryGet(AddressToBytes32Map storage map, address key) internal view returns (bool, bytes32) { - (bool success, bytes32 value) = tryGet(map._inner, bytes32(uint256(uint160(key)))); - return (success, value); + function tryGet(AddressToBytes32Map storage map, address key) internal view returns (bool exists, bytes32 value) { + (bool success, bytes32 val) = tryGet(map._inner, bytes32(uint256(uint160(key)))); + return (success, val); } /** @@ -766,18 +766,18 @@ library EnumerableMap { * * - `index` must be strictly less than {length}. */ - function at(Bytes32ToUintMap storage map, uint256 index) internal view returns (bytes32, uint256) { - (bytes32 key, bytes32 value) = at(map._inner, index); - return (key, uint256(value)); + function at(Bytes32ToUintMap storage map, uint256 index) internal view returns (bytes32 key, uint256 value) { + (bytes32 atKey, bytes32 val) = at(map._inner, index); + return (atKey, uint256(val)); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ - function tryGet(Bytes32ToUintMap storage map, bytes32 key) internal view returns (bool, uint256) { - (bool success, bytes32 value) = tryGet(map._inner, key); - return (success, uint256(value)); + function tryGet(Bytes32ToUintMap storage map, bytes32 key) internal view returns (bool exists, uint256 value) { + (bool success, bytes32 val) = tryGet(map._inner, key); + return (success, uint256(val)); } /** @@ -859,18 +859,18 @@ library EnumerableMap { * * - `index` must be strictly less than {length}. */ - function at(Bytes32ToAddressMap storage map, uint256 index) internal view returns (bytes32, address) { - (bytes32 key, bytes32 value) = at(map._inner, index); - return (key, address(uint160(uint256(value)))); + function at(Bytes32ToAddressMap storage map, uint256 index) internal view returns (bytes32 key, address value) { + (bytes32 atKey, bytes32 val) = at(map._inner, index); + return (atKey, address(uint160(uint256(val)))); } /** * @dev Tries to returns the value associated with `key`. O(1). * Does not revert if `key` is not in the map. */ - function tryGet(Bytes32ToAddressMap storage map, bytes32 key) internal view returns (bool, address) { - (bool success, bytes32 value) = tryGet(map._inner, key); - return (success, address(uint160(uint256(value)))); + function tryGet(Bytes32ToAddressMap storage map, bytes32 key) internal view returns (bool exists, address value) { + (bool success, bytes32 val) = tryGet(map._inner, key); + return (success, address(uint160(uint256(val)))); } /** diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 90fcfa9d77b..065202e8204 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/contracts/utils/structs/EnumerableSet.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/structs/EnumerableSet.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/structs/EnumerableSet.sol) // This file was procedurally generated from scripts/generate/templates/EnumerableSet.js. pragma solidity ^0.8.20; diff --git a/contracts/utils/structs/Heap.sol b/contracts/utils/structs/Heap.sol index 24d97292fdf..c97bb432a33 100644 --- a/contracts/utils/structs/Heap.sol +++ b/contracts/utils/structs/Heap.sol @@ -1,33 +1,26 @@ // SPDX-License-Identifier: MIT -// This file was procedurally generated from scripts/generate/templates/Heap.js. +// OpenZeppelin Contracts (last updated v5.1.0) (utils/structs/Heap.sol) pragma solidity ^0.8.20; import {Math} from "../math/Math.sol"; import {SafeCast} from "../math/SafeCast.sol"; import {Comparators} from "../Comparators.sol"; +import {Arrays} from "../Arrays.sol"; import {Panic} from "../Panic.sol"; +import {StorageSlot} from "../StorageSlot.sol"; /** * @dev Library for managing https://en.wikipedia.org/wiki/Binary_heap[binary heap] that can be used as * https://en.wikipedia.org/wiki/Priority_queue[priority queue]. * - * Heaps are represented as an array of Node objects. This array stores two overlapping structures: - * * A tree structure where the first element (index 0) is the root, and where the node at index i is the child of the - * node at index (i-1)/2 and the father of nodes at index 2*i+1 and 2*i+2. Each node stores the index (in the array) - * where the corresponding value is stored. - * * A list of payloads values where each index contains a value and a lookup index. The type of the value depends on - * the variant being used. The lookup is the index of the node (in the tree) that points to this value. - * - * Some invariants: - * ``` - * i == heap.data[heap.data[i].index].lookup // for all indices i - * i == heap.data[heap.data[i].lookup].index // for all indices i - * ``` + * Heaps are represented as a tree of values where the first element (index 0) is the root, and where the node at + * index i is the child of the node at index (i-1)/2 and the parent of nodes at index 2*i+1 and 2*i+2. Each node + * stores an element of the heap. * * The structure is ordered so that each node is bigger than its parent. An immediate consequence is that the * highest priority value is the one at the root. This value can be looked up in constant time (O(1)) at - * `heap.data[heap.data[0].index].value` + * `heap.tree[0]` * * The structure is designed to perform the following operations with the corresponding complexities: * @@ -37,35 +30,33 @@ import {Panic} from "../Panic.sol"; * * replace (replace the highest priority value with a new value): O(log(n)) * * length (get the number of elements): O(1) * * clear (remove all elements): O(1) + * + * IMPORTANT: This library allows for the use of custom comparator functions. Given that manipulating + * memory can lead to unexpected behavior. Consider verifying that the comparator does not manipulate + * the Heap's state directly and that it follows the Solidity memory safety rules. + * + * _Available since v5.1._ */ library Heap { + using Arrays for *; using Math for *; using SafeCast for *; /** * @dev Binary heap that supports values of type uint256. * - * Each element of that structure uses 2 storage slots. + * Each element of that structure uses one storage slot. */ struct Uint256Heap { - Uint256HeapNode[] data; - } - - /** - * @dev Internal node type for Uint256Heap. Stores a value of type uint256. - */ - struct Uint256HeapNode { - uint256 value; - uint64 index; // position -> value - uint64 lookup; // value -> position + uint256[] tree; } /** * @dev Lookup the root element of the heap. */ function peek(Uint256Heap storage self) internal view returns (uint256) { - // self.data[0] will `ARRAY_ACCESS_OUT_OF_BOUNDS` panic if heap is empty. - return _unsafeNodeAccess(self, self.data[0].index).value; + // self.tree[0] will `ARRAY_ACCESS_OUT_OF_BOUNDS` panic if heap is empty. + return self.tree[0]; } /** @@ -89,44 +80,19 @@ library Heap { function(uint256, uint256) view returns (bool) comp ) internal returns (uint256) { unchecked { - uint64 size = length(self); + uint256 size = length(self); if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - uint64 last = size - 1; - - // get root location (in the data array) and value - Uint256HeapNode storage rootNode = _unsafeNodeAccess(self, 0); - uint64 rootIdx = rootNode.index; - Uint256HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); - Uint256HeapNode storage lastNode = _unsafeNodeAccess(self, last); - uint256 rootDataValue = rootData.value; - - // if root is not the last element of the data array (that will get popped), reorder the data array. - if (rootIdx != last) { - // get details about the value stored in the last element of the array (that will get popped) - uint64 lastDataIdx = lastNode.lookup; - uint256 lastDataValue = lastNode.value; - // copy these values to the location of the root (that is safe, and that we no longer use) - rootData.value = lastDataValue; - rootData.lookup = lastDataIdx; - // update the tree node that used to point to that last element (value now located where the root was) - _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; - } - - // get last leaf location (in the data array) and value - uint64 lastIdx = lastNode.index; - uint256 lastValue = _unsafeNodeAccess(self, lastIdx).value; + // cache + uint256 rootValue = self.tree.unsafeAccess(0).value; + uint256 lastValue = self.tree.unsafeAccess(size - 1).value; - // move the last leaf to the root, pop last leaf ... - rootNode.index = lastIdx; - _unsafeNodeAccess(self, lastIdx).lookup = 0; - self.data.pop(); + // swap last leaf with root, shrink tree and re-heapify + self.tree.pop(); + self.tree.unsafeAccess(0).value = lastValue; + _siftDown(self, size - 1, 0, lastValue, comp); - // ... and heapify - _siftDown(self, last, 0, lastValue, comp); - - // return root value - return rootDataValue; + return rootValue; } } @@ -151,10 +117,10 @@ library Heap { uint256 value, function(uint256, uint256) view returns (bool) comp ) internal { - uint64 size = length(self); - if (size == type(uint64).max) Panic.panic(Panic.RESOURCE_ERROR); + uint256 size = length(self); - self.data.push(Uint256HeapNode({index: size, lookup: size, value: value})); + // push new item and re-heapify + self.tree.push(value); _siftUp(self, size, value, comp); } @@ -181,396 +147,110 @@ library Heap { uint256 newValue, function(uint256, uint256) view returns (bool) comp ) internal returns (uint256) { - uint64 size = length(self); + uint256 size = length(self); if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - // position of the node that holds the data for the root - uint64 rootIdx = _unsafeNodeAccess(self, 0).index; - // storage pointer to the node that holds the data for the root - Uint256HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); - - // cache old value and replace it - uint256 oldValue = rootData.value; - rootData.value = newValue; + // cache + uint256 oldValue = self.tree.unsafeAccess(0).value; - // re-heapify + // replace and re-heapify + self.tree.unsafeAccess(0).value = newValue; _siftDown(self, size, 0, newValue, comp); - // return old root value return oldValue; } /** * @dev Returns the number of elements in the heap. */ - function length(Uint256Heap storage self) internal view returns (uint64) { - return self.data.length.toUint64(); + function length(Uint256Heap storage self) internal view returns (uint256) { + return self.tree.length; } /** * @dev Removes all elements in the heap. */ function clear(Uint256Heap storage self) internal { - Uint256HeapNode[] storage data = self.data; - assembly ("memory-safe") { - sstore(data.slot, 0) - } + self.tree.unsafeSetLength(0); } /** * @dev Swap node `i` and `j` in the tree. */ - function _swap(Uint256Heap storage self, uint64 i, uint64 j) private { - Uint256HeapNode storage ni = _unsafeNodeAccess(self, i); - Uint256HeapNode storage nj = _unsafeNodeAccess(self, j); - uint64 ii = ni.index; - uint64 jj = nj.index; - // update pointers to the data (swap the value) - ni.index = jj; - nj.index = ii; - // update lookup pointers for consistency - _unsafeNodeAccess(self, ii).lookup = j; - _unsafeNodeAccess(self, jj).lookup = i; + function _swap(Uint256Heap storage self, uint256 i, uint256 j) private { + StorageSlot.Uint256Slot storage ni = self.tree.unsafeAccess(i); + StorageSlot.Uint256Slot storage nj = self.tree.unsafeAccess(j); + (ni.value, nj.value) = (nj.value, ni.value); } /** - * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a + * @dev Perform heap maintenance on `self`, starting at `index` (with the `value`), using `comp` as a * comparator, and moving toward the leaves of the underlying tree. * - * NOTE: This is a private function that is called in a trusted context with already cached parameters. `length` - * and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These + * NOTE: This is a private function that is called in a trusted context with already cached parameters. `size` + * and `value` could be extracted from `self` and `index`, but that would require redundant storage read. These * parameters are not verified. It is the caller role to make sure the parameters are correct. */ function _siftDown( Uint256Heap storage self, - uint64 size, - uint64 pos, + uint256 size, + uint256 index, uint256 value, function(uint256, uint256) view returns (bool) comp ) private { - uint256 left = 2 * pos + 1; // this could overflow uint64 - uint256 right = 2 * pos + 2; // this could overflow uint64 - - if (right < size) { - // the check guarantees that `left` and `right` are both valid uint64 - uint64 lIndex = uint64(left); - uint64 rIndex = uint64(right); - uint256 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; - uint256 rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, rIndex).index).value; - if (comp(lValue, value) || comp(rValue, value)) { - uint64 index = uint64(comp(lValue, rValue).ternary(lIndex, rIndex)); - _swap(self, pos, index); - _siftDown(self, size, index, value, comp); - } - } else if (left < size) { - // the check guarantees that `left` is a valid uint64 - uint64 lIndex = uint64(left); - uint256 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; - if (comp(lValue, value)) { - _swap(self, pos, lIndex); - _siftDown(self, size, lIndex, value, comp); + unchecked { + // Check if there is a risk of overflow when computing the indices of the child nodes. If that is the case, + // there cannot be child nodes in the tree, so sifting is done. + if (index >= type(uint256).max / 2) return; + + // Compute the indices of the potential child nodes + uint256 lIndex = 2 * index + 1; + uint256 rIndex = 2 * index + 2; + + // Three cases: + // 1. Both children exist: sifting may continue on one of the branch (selection required) + // 2. Only left child exist: sifting may continue on the left branch (no selection required) + // 3. Neither child exist: sifting is done + if (rIndex < size) { + uint256 lValue = self.tree.unsafeAccess(lIndex).value; + uint256 rValue = self.tree.unsafeAccess(rIndex).value; + if (comp(lValue, value) || comp(rValue, value)) { + uint256 cIndex = comp(lValue, rValue).ternary(lIndex, rIndex); + _swap(self, index, cIndex); + _siftDown(self, size, cIndex, value, comp); + } + } else if (lIndex < size) { + uint256 lValue = self.tree.unsafeAccess(lIndex).value; + if (comp(lValue, value)) { + _swap(self, index, lIndex); + _siftDown(self, size, lIndex, value, comp); + } } } } /** - * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a + * @dev Perform heap maintenance on `self`, starting at `index` (with the `value`), using `comp` as a * comparator, and moving toward the root of the underlying tree. * * NOTE: This is a private function that is called in a trusted context with already cached parameters. `value` - * could be extracted from `self` and `pos`, but that would require redundant storage read. These parameters are not + * could be extracted from `self` and `index`, but that would require redundant storage read. These parameters are not * verified. It is the caller role to make sure the parameters are correct. */ function _siftUp( Uint256Heap storage self, - uint64 pos, + uint256 index, uint256 value, function(uint256, uint256) view returns (bool) comp ) private { unchecked { - while (pos > 0) { - uint64 parent = (pos - 1) / 2; - uint256 parentValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, parent).index).value; + while (index > 0) { + uint256 parentIndex = (index - 1) / 2; + uint256 parentValue = self.tree.unsafeAccess(parentIndex).value; if (comp(parentValue, value)) break; - _swap(self, pos, parent); - pos = parent; - } - } - } - - function _unsafeNodeAccess( - Uint256Heap storage self, - uint64 pos - ) private pure returns (Uint256HeapNode storage result) { - assembly ("memory-safe") { - mstore(0x00, self.slot) - result.slot := add(keccak256(0x00, 0x20), mul(pos, 2)) - } - } - - /** - * @dev Binary heap that supports values of type uint208. - * - * Each element of that structure uses 1 storage slots. - */ - struct Uint208Heap { - Uint208HeapNode[] data; - } - - /** - * @dev Internal node type for Uint208Heap. Stores a value of type uint208. - */ - struct Uint208HeapNode { - uint208 value; - uint24 index; // position -> value - uint24 lookup; // value -> position - } - - /** - * @dev Lookup the root element of the heap. - */ - function peek(Uint208Heap storage self) internal view returns (uint208) { - // self.data[0] will `ARRAY_ACCESS_OUT_OF_BOUNDS` panic if heap is empty. - return _unsafeNodeAccess(self, self.data[0].index).value; - } - - /** - * @dev Remove (and return) the root element for the heap using the default comparator. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ - function pop(Uint208Heap storage self) internal returns (uint208) { - return pop(self, Comparators.lt); - } - - /** - * @dev Remove (and return) the root element for the heap using the provided comparator. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ - function pop( - Uint208Heap storage self, - function(uint256, uint256) view returns (bool) comp - ) internal returns (uint208) { - unchecked { - uint24 size = length(self); - if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - - uint24 last = size - 1; - - // get root location (in the data array) and value - Uint208HeapNode storage rootNode = _unsafeNodeAccess(self, 0); - uint24 rootIdx = rootNode.index; - Uint208HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); - Uint208HeapNode storage lastNode = _unsafeNodeAccess(self, last); - uint208 rootDataValue = rootData.value; - - // if root is not the last element of the data array (that will get popped), reorder the data array. - if (rootIdx != last) { - // get details about the value stored in the last element of the array (that will get popped) - uint24 lastDataIdx = lastNode.lookup; - uint208 lastDataValue = lastNode.value; - // copy these values to the location of the root (that is safe, and that we no longer use) - rootData.value = lastDataValue; - rootData.lookup = lastDataIdx; - // update the tree node that used to point to that last element (value now located where the root was) - _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; + _swap(self, index, parentIndex); + index = parentIndex; } - - // get last leaf location (in the data array) and value - uint24 lastIdx = lastNode.index; - uint208 lastValue = _unsafeNodeAccess(self, lastIdx).value; - - // move the last leaf to the root, pop last leaf ... - rootNode.index = lastIdx; - _unsafeNodeAccess(self, lastIdx).lookup = 0; - self.data.pop(); - - // ... and heapify - _siftDown(self, last, 0, lastValue, comp); - - // return root value - return rootDataValue; - } - } - - /** - * @dev Insert a new element in the heap using the default comparator. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ - function insert(Uint208Heap storage self, uint208 value) internal { - insert(self, value, Comparators.lt); - } - - /** - * @dev Insert a new element in the heap using the provided comparator. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ - function insert( - Uint208Heap storage self, - uint208 value, - function(uint256, uint256) view returns (bool) comp - ) internal { - uint24 size = length(self); - if (size == type(uint24).max) Panic.panic(Panic.RESOURCE_ERROR); - - self.data.push(Uint208HeapNode({index: size, lookup: size, value: value})); - _siftUp(self, size, value, comp); - } - - /** - * @dev Return the root element for the heap, and replace it with a new value, using the default comparator. - * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ - function replace(Uint208Heap storage self, uint208 newValue) internal returns (uint208) { - return replace(self, newValue, Comparators.lt); - } - - /** - * @dev Return the root element for the heap, and replace it with a new value, using the provided comparator. - * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ - function replace( - Uint208Heap storage self, - uint208 newValue, - function(uint256, uint256) view returns (bool) comp - ) internal returns (uint208) { - uint24 size = length(self); - if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - - // position of the node that holds the data for the root - uint24 rootIdx = _unsafeNodeAccess(self, 0).index; - // storage pointer to the node that holds the data for the root - Uint208HeapNode storage rootData = _unsafeNodeAccess(self, rootIdx); - - // cache old value and replace it - uint208 oldValue = rootData.value; - rootData.value = newValue; - - // re-heapify - _siftDown(self, size, 0, newValue, comp); - - // return old root value - return oldValue; - } - - /** - * @dev Returns the number of elements in the heap. - */ - function length(Uint208Heap storage self) internal view returns (uint24) { - return self.data.length.toUint24(); - } - - /** - * @dev Removes all elements in the heap. - */ - function clear(Uint208Heap storage self) internal { - Uint208HeapNode[] storage data = self.data; - assembly ("memory-safe") { - sstore(data.slot, 0) - } - } - - /** - * @dev Swap node `i` and `j` in the tree. - */ - function _swap(Uint208Heap storage self, uint24 i, uint24 j) private { - Uint208HeapNode storage ni = _unsafeNodeAccess(self, i); - Uint208HeapNode storage nj = _unsafeNodeAccess(self, j); - uint24 ii = ni.index; - uint24 jj = nj.index; - // update pointers to the data (swap the value) - ni.index = jj; - nj.index = ii; - // update lookup pointers for consistency - _unsafeNodeAccess(self, ii).lookup = j; - _unsafeNodeAccess(self, jj).lookup = i; - } - - /** - * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a - * comparator, and moving toward the leaves of the underlying tree. - * - * NOTE: This is a private function that is called in a trusted context with already cached parameters. `length` - * and `value` could be extracted from `self` and `pos`, but that would require redundant storage read. These - * parameters are not verified. It is the caller role to make sure the parameters are correct. - */ - function _siftDown( - Uint208Heap storage self, - uint24 size, - uint24 pos, - uint208 value, - function(uint256, uint256) view returns (bool) comp - ) private { - uint256 left = 2 * pos + 1; // this could overflow uint24 - uint256 right = 2 * pos + 2; // this could overflow uint24 - - if (right < size) { - // the check guarantees that `left` and `right` are both valid uint24 - uint24 lIndex = uint24(left); - uint24 rIndex = uint24(right); - uint208 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; - uint208 rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, rIndex).index).value; - if (comp(lValue, value) || comp(rValue, value)) { - uint24 index = uint24(comp(lValue, rValue).ternary(lIndex, rIndex)); - _swap(self, pos, index); - _siftDown(self, size, index, value, comp); - } - } else if (left < size) { - // the check guarantees that `left` is a valid uint24 - uint24 lIndex = uint24(left); - uint208 lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; - if (comp(lValue, value)) { - _swap(self, pos, lIndex); - _siftDown(self, size, lIndex, value, comp); - } - } - } - - /** - * @dev Perform heap maintenance on `self`, starting at position `pos` (with the `value`), using `comp` as a - * comparator, and moving toward the root of the underlying tree. - * - * NOTE: This is a private function that is called in a trusted context with already cached parameters. `value` - * could be extracted from `self` and `pos`, but that would require redundant storage read. These parameters are not - * verified. It is the caller role to make sure the parameters are correct. - */ - function _siftUp( - Uint208Heap storage self, - uint24 pos, - uint208 value, - function(uint256, uint256) view returns (bool) comp - ) private { - unchecked { - while (pos > 0) { - uint24 parent = (pos - 1) / 2; - uint208 parentValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, parent).index).value; - if (comp(parentValue, value)) break; - _swap(self, pos, parent); - pos = parent; - } - } - } - - function _unsafeNodeAccess( - Uint208Heap storage self, - uint24 pos - ) private pure returns (Uint208HeapNode storage result) { - assembly ("memory-safe") { - mstore(0x00, self.slot) - result.slot := add(keccak256(0x00, 0x20), pos) } } } diff --git a/contracts/utils/structs/MerkleTree.sol b/contracts/utils/structs/MerkleTree.sol index 93f59ace585..56f5bc67237 100644 --- a/contracts/utils/structs/MerkleTree.sol +++ b/contracts/utils/structs/MerkleTree.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/structs/MerkleTree.sol) -pragma solidity ^0.8.0; +pragma solidity ^0.8.20; import {Hashes} from "../cryptography/Hashes.sol"; import {Arrays} from "../Arrays.sol"; @@ -49,7 +50,7 @@ library MerkleTree { /** * @dev Initialize a {Bytes32PushTree} using {Hashes-commutativeKeccak256} to hash internal nodes. - * The capacity of the tree (i.e. number of leaves) is set to `2**levels`. + * The capacity of the tree (i.e. number of leaves) is set to `2**treeDepth`. * * Calling this function on MerkleTree that was already setup and used will reset it to a blank state. * @@ -59,8 +60,8 @@ library MerkleTree { * IMPORTANT: The zero value should be carefully chosen since it will be stored in the tree representing * empty leaves. It should be a value that is not expected to be part of the tree. */ - function setup(Bytes32PushTree storage self, uint8 levels, bytes32 zero) internal returns (bytes32 initialRoot) { - return setup(self, levels, zero, Hashes.commutativeKeccak256); + function setup(Bytes32PushTree storage self, uint8 treeDepth, bytes32 zero) internal returns (bytes32 initialRoot) { + return setup(self, treeDepth, zero, Hashes.commutativeKeccak256); } /** @@ -70,21 +71,24 @@ library MerkleTree { * should be pushed to it using the custom push function, which should be the same one as used during the setup. * * IMPORTANT: Providing a custom hashing function is a security-sensitive operation since it may - * compromise the soundness of the tree. Consider using functions from {Hashes}. + * compromise the soundness of the tree. + * + * NOTE: Consider verifying that the hashing function does not manipulate the memory state directly and that it + * follows the Solidity memory safety rules. Otherwise, it may lead to unexpected behavior. */ function setup( Bytes32PushTree storage self, - uint8 levels, + uint8 treeDepth, bytes32 zero, function(bytes32, bytes32) view returns (bytes32) fnHash ) internal returns (bytes32 initialRoot) { // Store depth in the dynamic array - Arrays.unsafeSetLength(self._sides, levels); - Arrays.unsafeSetLength(self._zeros, levels); + Arrays.unsafeSetLength(self._sides, treeDepth); + Arrays.unsafeSetLength(self._zeros, treeDepth); // Build each root of zero-filled subtrees bytes32 currentZero = zero; - for (uint32 i = 0; i < levels; ++i) { + for (uint32 i = 0; i < treeDepth; ++i) { Arrays.unsafeAccess(self._zeros, i).value = currentZero; currentZero = fnHash(currentZero, currentZero); } @@ -126,20 +130,20 @@ library MerkleTree { function(bytes32, bytes32) view returns (bytes32) fnHash ) internal returns (uint256 index, bytes32 newRoot) { // Cache read - uint256 levels = self._zeros.length; + uint256 treeDepth = depth(self); // Get leaf index index = self._nextLeafIndex++; // Check if tree is full. - if (index >= 1 << levels) { + if (index >= 1 << treeDepth) { Panic.panic(Panic.RESOURCE_ERROR); } // Rebuild branch from leaf to root uint256 currentIndex = index; bytes32 currentLevelHash = leaf; - for (uint32 i = 0; i < levels; i++) { + for (uint32 i = 0; i < treeDepth; i++) { // Reaching the parent node, is currentLevelHash the left child? bool isLeft = currentIndex % 2 == 0; diff --git a/contracts/utils/types/Time.sol b/contracts/utils/types/Time.sol index 9faef31f054..a495932de39 100644 --- a/contracts/utils/types/Time.sol +++ b/contracts/utils/types/Time.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v5.0.0) (utils/types/Time.sol) +// OpenZeppelin Contracts (last updated v5.1.0) (utils/types/Time.sol) pragma solidity ^0.8.20; @@ -71,8 +71,11 @@ library Time { * @dev Get the value at a given timepoint plus the pending value and effect timepoint if there is a scheduled * change after this timepoint. If the effect timepoint is 0, then the pending value should not be considered. */ - function _getFullAt(Delay self, uint48 timepoint) private pure returns (uint32, uint32, uint48) { - (uint32 valueBefore, uint32 valueAfter, uint48 effect) = self.unpack(); + function _getFullAt( + Delay self, + uint48 timepoint + ) private pure returns (uint32 valueBefore, uint32 valueAfter, uint48 effect) { + (valueBefore, valueAfter, effect) = self.unpack(); return effect <= timepoint ? (valueAfter, 0, 0) : (valueBefore, valueAfter, effect); } @@ -80,7 +83,7 @@ library Time { * @dev Get the current value plus the pending value and effect timepoint if there is a scheduled change. If the * effect timepoint is 0, then the pending value should not be considered. */ - function getFull(Delay self) internal view returns (uint32, uint32, uint48) { + function getFull(Delay self) internal view returns (uint32 valueBefore, uint32 valueAfter, uint48 effect) { return _getFullAt(self, timestamp()); } diff --git a/docs/modules/ROOT/pages/erc1155.adoc b/docs/modules/ROOT/pages/erc1155.adoc index 5bfb49accd4..2c31db8b86c 100644 --- a/docs/modules/ROOT/pages/erc1155.adoc +++ b/docs/modules/ROOT/pages/erc1155.adoc @@ -106,9 +106,9 @@ A key difference when using xref:api:token/ERC1155.adoc#IERC1155-safeTransferFro ERC1155InvalidReceiver("
") ---- -This is a good thing! It means that the recipient contract has not registered itself as aware of the ERC-1155 protocol, so transfers to it are disabled to *prevent tokens from being locked forever*. As an example, https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d[the Golem contract currently holds over 350k `GNT` tokens], worth multiple tens of thousands of dollars, and lacks methods to get them out of there. This has happened to virtually every ERC20-backed project, usually due to user error. +This is a good thing! It means that the recipient contract has not registered itself as aware of the ERC-1155 protocol, so transfers to it are disabled to *prevent tokens from being locked forever*. As an example, https://etherscan.io/token/0xa74476443119A942dE498590Fe1f2454d7D4aC0d?a=0xa74476443119A942dE498590Fe1f2454d7D4aC0d[the Golem contract currently holds over 350k `GNT` tokens], and lacks methods to get them out of there. This has happened to virtually every ERC20-backed project, usually due to user error. -In order for our contract to receive ERC-1155 tokens we can inherit from the convenience contract xref:api:token/ERC1155.adoc#ERC1155Holder[`ERC1155Holder`] which handles the registering for us. Though we need to remember to implement functionality to allow tokens to be transferred out of our contract: +In order for our contract to receive ERC-1155 tokens we can inherit from the convenience contract xref:api:token/ERC1155.adoc#ERC1155Holder[`ERC1155Holder`] which handles the registering for us. However, we need to remember to implement functionality to allow tokens to be transferred out of our contract: [source,solidity] ---- diff --git a/docs/modules/ROOT/pages/erc4626.adoc b/docs/modules/ROOT/pages/erc4626.adoc index 977d8160632..c219595dd5c 100644 --- a/docs/modules/ROOT/pages/erc4626.adoc +++ b/docs/modules/ROOT/pages/erc4626.adoc @@ -29,7 +29,7 @@ image::erc4626-rate-loglogext.png[More exchange rates in logarithmic scale] === The attack -When depositing tokens, the number of shares a user gets is rounded towards zero. This rounding takes away value from the user in favor or the vault (i.e. in favor of all the current share holders). This rounding is often negligible because of the amount at stake. If you deposit 1e9 shares worth of tokens, the rounding will have you lose at most 0.0000001% of your deposit. However if you deposit 10 shares worth of tokens, you could lose 10% of your deposit. Even worse, if you deposit <1 share worth of tokens, then you get 0 shares, and you basically made a donation. +When depositing tokens, the number of shares a user gets is rounded towards zero. This rounding takes away value from the user in favor of the vault (i.e. in favor of all the current shareholders). This rounding is often negligible because of the amount at stake. If you deposit 1e9 shares worth of tokens, the rounding will have you lose at most 0.0000001% of your deposit. However if you deposit 10 shares worth of tokens, you could lose 10% of your deposit. Even worse, if you deposit <1 share worth of tokens, then you get 0 shares, and you basically made a donation. For a given amount of assets, the more shares you receive the safer you are. If you want to limit your losses to at most 1%, you need to receive at least 100 shares. @@ -47,7 +47,7 @@ The idea of an inflation attack is that an attacker can donate assets to the vau image::erc4626-attack.png[Inflation attack without protection] -Figure 6 shows how an attacker can manipulate the rate of an empty vault. First the attacker must deposit a small amount of tokens (1 token) and follow up with a donation of 1e5 tokens directly to the vault to move the exchange rate "right". This puts the vault in a state where any deposit smaller than 1e5 would be completely lost to the vault. Given that the attacker is the only share holder (from their donation), the attacker would steal all the tokens deposited. +Figure 6 shows how an attacker can manipulate the rate of an empty vault. First the attacker must deposit a small amount of tokens (1 token) and follow up with a donation of 1e5 tokens directly to the vault to move the exchange rate "right". This puts the vault in a state where any deposit smaller than 1e5 would be completely lost to the vault. Given that the attacker is the only shareholder (from their donation), the attacker would steal all the tokens deposited. An attacker would typically wait for a user to do the first deposit into the vault, and would frontrun that operation with the attack described above. The risk is low, and the size of the "donation" required to manipulate the vault is equivalent to the size of the deposit that is being attacked. diff --git a/docs/modules/ROOT/pages/extending-contracts.adoc b/docs/modules/ROOT/pages/extending-contracts.adoc index 1cdc0d7814f..8ff4101f220 100644 --- a/docs/modules/ROOT/pages/extending-contracts.adoc +++ b/docs/modules/ROOT/pages/extending-contracts.adoc @@ -46,6 +46,6 @@ NOTE: The same rule is implemented and extended in xref:api:access.adoc#AccessCo The maintainers of OpenZeppelin Contracts are mainly concerned with the correctness and security of the code as published in the library, and the combinations of base contracts with the official extensions from the library. -Custom overrides, and those of hooks in particular, may break some important assumptions and introduce vulnerabilities in otherwise secure code. While we try to ensure the contracts remain secure in the face of a wide range of potential customizations, this is done in a best-effort manner. While we try to document all important assumptions, this should not be relied upon. Custom overrides should be carefully reviewed and checked against the source code of the contract they are customizing so as to fully understand their impact and guarantee their security. +Custom overrides, especially to hooks, can disrupt important assumptions and may introduce security risks in the code that was previously secure. While we try to ensure the contracts remain secure in the face of a wide range of potential customizations, this is done in a best-effort manner. While we try to document all important assumptions, this should not be relied upon. Custom overrides should be carefully reviewed and checked against the source code of the contract they are customizing to fully understand their impact and guarantee their security. The way functions interact internally should not be assumed to stay stable across releases of the library. For example, a function that is used in one context in a particular release may not be used in the same context in the next release. Contracts that override functions should revalidate their assumptions when updating the version of OpenZeppelin Contracts they are built on. diff --git a/docs/modules/ROOT/pages/governance.adoc b/docs/modules/ROOT/pages/governance.adoc index 19f23d78d92..5f8e77555aa 100644 --- a/docs/modules/ROOT/pages/governance.adoc +++ b/docs/modules/ROOT/pages/governance.adoc @@ -74,7 +74,7 @@ votingPeriod: How long does a proposal remain open to votes. These parameters are specified in the unit defined in the token's clock. Assuming the token uses block numbers, and assuming block time of around 12 seconds, we will have set votingDelay = 1 day = 7200 blocks, and votingPeriod = 1 week = 50400 blocks. -We can optionally set a proposal threshold as well. This restricts proposal creation to accounts who have enough voting power. +We can optionally set a proposal threshold as well. This restricts proposal creation to accounts that have enough voting power. ```solidity include::api:example$governance/MyGovernor.sol[] diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index b8afec4eabd..bb519907d7a 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -32,7 +32,7 @@ WARNING: Getting signature verification right is not trivial: make sure you full ==== P256 Signatures (secp256r1) -P256, also known as secp256r1, is one of the most used signature schemes. P256 signatures are standardized by the National Institute of Standards and Technology (NIST) and it's widely available in consumer hardware and software. +P256, also known as secp256r1, is one of the most used signature schemes. P256 signatures are standardized by the National Institute of Standards and Technology (NIST) and they are widely available in consumer hardware and software. These signatures are different from regular Ethereum Signatures (secp256k1) in that they use a different elliptic curve to perform operations but have similar security guarantees. @@ -51,7 +51,7 @@ function _verify( } ---- -By default, the `verify` function will try calling the (https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md)[RIP-7212] precompile at address `0x100` and will fallback to an implementation in Solidity if not available. We encourage you to use `verifyNative` if you know the precompile is available on the chain you're working on and on any other chain on which you intend to use the same bytecode in the future. In case of any doubts regarding the implementation roadmap of the native precompile `P256` of potential future target chains, please consider using `verifySolidity`. +By default, the `verify` function will try calling the https://github.com/ethereum/RIPs/blob/master/RIPS/rip-7212.md[RIP-7212] precompile at address `0x100` and will fallback to an implementation in Solidity if not available. We encourage you to use `verifyNative` if you know the precompile is available on the chain you're working on and on any other chain on which you intend to use the same bytecode in the future. In case of any doubts regarding the implementation roadmap of the native precompile `P256` of potential future target chains, please consider using `verifySolidity`. [source,solidity] ---- @@ -73,7 +73,7 @@ IMPORTANT: The P256 library only allows for `s` values in the lower order of the ==== RSA -RSA a public-key cryptosystem that was popularized by corporate and governmental public key infrastructures (https://en.wikipedia.org/wiki/Public_key_infrastructure[PKIs]) and https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions[DNSSEC]. +RSA is a public-key cryptosystem that was popularized by corporate and governmental public key infrastructures (https://en.wikipedia.org/wiki/Public_key_infrastructure[PKIs]) and https://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions[DNSSEC]. This cryptosystem consists of using a private key that's the product of 2 large prime numbers. The message is signed by applying a modular exponentiation to its hash (commonly SHA256), where both the exponent and modulus compose the public key of the signer. @@ -91,7 +91,7 @@ function _verify( bytes memory e, bytes memory n ) internal pure returns (bool) { - return data.pkcs1(signature, e, n); + return data.pkcs1Sha256(signature, e, n); } ---- @@ -99,7 +99,7 @@ IMPORTANT: Always use keys of at least 2048 bits. Additionally, be aware that PK === Verifying Merkle Proofs -Developers can build a Merkle Tree off-chain, which allows for verifying that an element (leaf) is part of a set by using a Merkle Proof. This technique is widely used for creating whitelists (e.g. for airdrops) and other advanced use cases. +Developers can build a Merkle Tree off-chain, which allows for verifying that an element (leaf) is part of a set by using a Merkle Proof. This technique is widely used for creating whitelists (e.g., for airdrops) and other advanced use cases. TIP: OpenZeppelin Contracts provides a https://github.com/OpenZeppelin/merkle-tree[JavaScript library] for building trees off-chain and generating proofs. @@ -149,7 +149,7 @@ contract MyContract { [[math]] == Math -Although Solidity already provides math operators (i.e. `+`, `-`, etc.), Contracts includes xref:api:utils.adoc#Math[`Math`]; a set of utilities for dealing with mathematical operators, with support for extra operations (eg. xref:api:utils.adoc#Math-average-uint256-uint256-[`average`]) and xref:api:utils.adoc#SignedMath[`SignedMath`]; a library specialized in signed math operations. +Although Solidity already provides math operators (i.e. `+`, `-`, etc.), Contracts includes xref:api:utils.adoc#Math[`Math`]; a set of utilities for dealing with mathematical operators, with support for extra operations (e.g., xref:api:utils.adoc#Math-average-uint256-uint256-[`average`]) and xref:api:utils.adoc#SignedMath[`SignedMath`]; a library specialized in signed math operations. Include these contracts with `using Math for uint256` or `using SignedMath for int256` and then use their functions in your code: @@ -272,7 +272,7 @@ function replace(Uint256Heap storage self, uint256 newValue) internal returns (u === Packing -The storage in the EVM is shaped in chunks of 32 bytes, each of this chunks is known as _slot_, and can hold multiple values together as long as these values don't exceed its size. These properties of the storage allow for a technique known as _packing_, that consists of placing values together on a single storage slot to reduce the costs associated to reading and writing to multiple slots instead of just one. +The storage in the EVM is shaped in chunks of 32 bytes, each of this chunks is known as a _slot_, and can hold multiple values together as long as these values don't exceed its size. These properties of the storage allow for a technique known as _packing_, that consists of placing values together on a single storage slot to reduce the costs associated to reading and writing to multiple slots instead of just one. Commonly, developers pack values using structs that place values together so they fit better in storage. However, this approach requires to load such struct from either calldata or memory. Although sometimes necessary, it may be useful to pack values in a single slot and treat it as a packed value without involving calldata or memory. @@ -319,7 +319,7 @@ function _setImplementation(address newImplementation) internal { } ---- -The xref:api:utils.adoc#StorageSlot[`StorageSlot`] library also supports transient storage through user defined value types (https://docs.soliditylang.org/en/latest/types.html#user-defined-value-types[UDVTs]), which enables the same value types as in Solidity. +The xref:api:utils.adoc#TransientSlot[`TransientSlot`] library supports transient storage through user defined value types (https://docs.soliditylang.org/en/latest/types.html#user-defined-value-types[UDVTs]), which enables the same value types as in Solidity. [source,solidity] ---- diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000000..00fcc95bb4d --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,26 @@ +import js from '@eslint/js'; +import { includeIgnoreFile } from '@eslint/compat'; +import prettier from 'eslint-config-prettier'; +import globals from 'globals'; +import path from 'path'; + +export default [ + js.configs.recommended, + prettier, + { + languageOptions: { + ecmaVersion: 2022, + globals: { + ...globals.browser, + ...globals.mocha, + ...globals.node, + artifacts: 'readonly', + contract: 'readonly', + web3: 'readonly', + extendEnvironment: 'readonly', + expect: 'readonly', + }, + }, + }, + includeIgnoreFile(path.resolve(import.meta.dirname, '.gitignore')), +]; diff --git a/foundry.toml b/foundry.toml index 3f60b7cbbf1..78dd0781224 100644 --- a/foundry.toml +++ b/foundry.toml @@ -8,6 +8,7 @@ out = 'out' libs = ['node_modules', 'lib'] test = 'test' cache_path = 'cache_forge' +fs_permissions = [{ access = "read", path = "./test/bin" }] [fuzz] runs = 5000 diff --git a/fv-requirements.txt b/fv-requirements.txt index 6dda424ed2b..608b4de24a3 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.1.14 +halmos==0.2.0 diff --git a/lib/forge-std b/lib/forge-std index ae570fec082..1eea5bae12a 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit ae570fec082bfe1c1f45b0acca4a2b4f84d345ce +Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262 diff --git a/package-lock.json b/package-lock.json index dc147499814..2b7f5083416 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,19 @@ { "name": "openzeppelin-solidity", - "version": "5.0.2", + "version": "5.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openzeppelin-solidity", - "version": "5.0.2", + "version": "5.1.0", "license": "MIT", "devDependencies": { "@changesets/changelog-github": "^0.5.0", "@changesets/cli": "^2.26.0", "@changesets/pre": "^2.0.0", "@changesets/read": "^0.6.0", + "@eslint/compat": "^1.2.1", "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", "@nomicfoundation/hardhat-ethers": "^3.0.4", "@nomicfoundation/hardhat-network-helpers": "^1.0.3", @@ -21,18 +22,21 @@ "@openzeppelin/upgrade-safe-transpiler": "^0.3.32", "@openzeppelin/upgrades-core": "^1.20.6", "chai": "^4.2.0", - "eslint": "^8.30.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.0.0", - "ethers": "^6.7.1", + "ethers": "^6.13.4", "glob": "^11.0.0", + "globals": "^15.3.0", "graphlib": "^2.1.8", "hardhat": "^2.22.2", "hardhat-exposed": "^0.3.15", - "hardhat-gas-reporter": "^2.0.0", + "hardhat-gas-reporter": "^2.1.0", "hardhat-ignore-warnings": "^0.2.11", + "husky": "^9.1.7", + "lint-staged": "^15.2.10", "lodash.startcase": "^4.4.0", "micromatch": "^4.0.2", - "p-limit": "^3.1.0", + "p-limit": "^6.0.0", "prettier": "^3.0.0", "prettier-plugin-solidity": "^1.1.0", "rimraf": "^6.0.0", @@ -42,7 +46,7 @@ "solidity-ast": "^0.4.50", "solidity-coverage": "^0.8.5", "solidity-docgen": "^0.6.0-beta.29", - "undici": "^6.11.1", + "undici": "^7.0.0", "yargs": "^17.0.0" } }, @@ -56,10 +60,11 @@ } }, "node_modules/@adraffy/ens-normalize": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.9.2.tgz", - "integrity": "sha512-0h+FrQDqe2Wn+IIGFkTCd4aAwTJ+7834Ek1COohCyV26AXhwQ7WQaz+4F/nLOeVl/3BtWHOHLPsq46V8YB46Eg==", - "dev": true + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "dev": true, + "license": "MIT" }, "node_modules/@babel/code-frame": { "version": "7.22.13", @@ -571,24 +576,69 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", - "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/compat": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.2.1.tgz", + "integrity": "sha512-JbHG2TWuCeNzh87fXo+/46Z1LEo9DBA9T188d0fZgGxAD+cNyS6sx9fdiyxjGPBMyQVRlCutTByZ6a5+YMkF7g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz", + "integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -596,7 +646,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -608,6 +658,18 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -621,12 +683,36 @@ } }, "node_modules/@eslint/js": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", - "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", + "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.1.tgz", + "integrity": "sha512-HFZ4Mp26nbWk9d/BpvP0YNL6W4UoZF0VFcTw/aPPA8RpOxeFQgK+ClABGgAUXs9Y/RGX/l1vOmrqz1MQt9MNuw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@ethereumjs/rlp": { @@ -1111,18 +1197,28 @@ "pnpm": "7.5.1" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -1138,11 +1234,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -2450,6 +2554,13 @@ "@types/chai": "*" } }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -2475,6 +2586,13 @@ "ci-info": "^3.1.0" } }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2494,12 +2612,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", - "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", "dev": true, + "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/normalize-package-data": { @@ -2560,10 +2679,11 @@ } }, "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -2576,6 +2696,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -3470,6 +3591,65 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3531,6 +3711,12 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3970,18 +4156,6 @@ "node": ">=8" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dotenv": { "version": "8.6.0", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", @@ -4040,6 +4214,18 @@ "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -4277,57 +4463,64 @@ } }, "node_modules/eslint": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", - "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", + "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.49.0", - "@humanwhocodes/config-array": "^0.11.11", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.7.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.13.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -4343,16 +4536,17 @@ } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -4375,6 +4569,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4385,17 +4580,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/eslint/node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4412,6 +4602,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4423,13 +4614,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -4437,11 +4630,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -4458,6 +4665,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -4470,27 +4678,17 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -4501,11 +4699,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -4521,6 +4736,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4528,18 +4744,45 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -4575,6 +4818,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -4689,9 +4933,9 @@ "dev": true }, "node_modules/ethers": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.7.1.tgz", - "integrity": "sha512-qX5kxIFMfg1i+epfgb0xF4WM7IqapIIu50pOJ17aebkxxa4BacW5jFrQRmCJpDEg2ZK2oNtR5QjrQ1WDBF29dA==", + "version": "6.13.4", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.4.tgz", + "integrity": "sha512-21YtnZVg4/zKkCQPjrDj38B1r4nQvTZLopUGMLQ1ePU2zV/joCfDC3t3iKQjWRzjjjbzR+mdAIoikeBRNkdllA==", "dev": true, "funding": [ { @@ -4703,60 +4947,71 @@ "url": "https://www.buymeacoffee.com/ricmoo" } ], + "license": "MIT", "dependencies": { - "@adraffy/ens-normalize": "1.9.2", - "@noble/hashes": "1.1.2", - "@noble/secp256k1": "1.7.1", - "@types/node": "18.15.13", + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", "aes-js": "4.0.0-beta.5", - "tslib": "2.4.0", - "ws": "8.5.0" + "tslib": "2.7.0", + "ws": "8.17.1" }, "engines": { "node": ">=14.0.0" } }, - "node_modules/ethers/node_modules/@noble/hashes": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz", - "integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==", + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] - }, - "node_modules/ethers/node_modules/@types/node": { - "version": "18.15.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", - "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==", - "dev": true + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, - "node_modules/ethers/node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", "dev": true }, "node_modules/ethers/node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", - "dev": true + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true, + "license": "0BSD" }, "node_modules/ethers/node_modules/ws": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -4801,6 +5056,12 @@ "npm": ">=3" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -4811,6 +5072,53 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/extendable-error": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.7.tgz", @@ -4881,15 +5189,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -4937,59 +5246,25 @@ } }, "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", - "dev": true, - "dependencies": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/flat-cache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.6", @@ -5156,6 +5431,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -5326,15 +5613,13 @@ } }, "node_modules/globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5424,12 +5709,6 @@ "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "dev": true }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/graphlib": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", @@ -5562,6 +5841,7 @@ "resolved": "https://registry.npmjs.org/hardhat-gas-reporter/-/hardhat-gas-reporter-2.1.0.tgz", "integrity": "sha512-d/WU/qHhBFnbweAm2fAAjcaaE0M7BKZ4r+/bqcFlfP6um28BXtlv2FrJ6oyQUGSFD0ttbmB7sH4ZFDzkYw5GzA==", "dev": true, + "license": "MIT", "dependencies": { "@ethersproject/abi": "^5.7.0", "@ethersproject/bytes": "^5.7.0", @@ -6353,6 +6633,30 @@ "integrity": "sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==", "dev": true }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", + "dev": true, + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6585,6 +6889,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6643,15 +6959,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", @@ -6698,6 +7005,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -6917,10 +7236,11 @@ } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -6980,12 +7300,168 @@ "node": ">= 0.8.0" } }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/lint-staged": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", + "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", + "dev": true, + "dependencies": { + "chalk": "~5.3.0", + "commander": "~12.1.0", + "debug": "~4.3.6", + "execa": "~8.0.1", + "lilconfig": "~3.1.2", + "listr2": "~8.2.4", + "micromatch": "~4.0.8", + "pidtree": "~0.6.0", + "string-argv": "~0.3.2", + "yaml": "~2.5.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/lint-staged/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lint-staged/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/load-yaml-file": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", @@ -7123,10 +7599,131 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/loupe": { @@ -7243,6 +7840,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7259,12 +7862,12 @@ "dev": true }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -7292,6 +7895,30 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", @@ -7598,6 +8225,22 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/mocha/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mocha/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -7669,6 +8312,19 @@ "node": ">=10" } }, + "node_modules/mocha/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -7820,6 +8476,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/number-to-bn": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/number-to-bn/-/number-to-bn-1.7.0.tgz", @@ -7891,6 +8574,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", @@ -7960,15 +8658,16 @@ } }, "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.0.0.tgz", + "integrity": "sha512-Dx+NzOuILWwjJE9OYtEKuQRy0i3c5QVAmDsVrvvRSgyNnPuB27D2DyEjl6QTNyeePaAHjaPk+ya0yA0Frld8RA==", "dev": true, + "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "yocto-queue": "^1.1.1" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -8176,6 +8875,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -8252,6 +8963,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/preferred-pm/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/preferred-pm/node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -8267,6 +8994,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/preferred-pm/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -8688,6 +9428,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -8707,6 +9490,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, "node_modules/rimraf": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.0.tgz", @@ -8935,20 +9724,41 @@ "dev": true }, "node_modules/secp256k1": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.3.tgz", - "integrity": "sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-4.0.4.tgz", + "integrity": "sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==", "dev": true, "hasInstallScript": true, "dependencies": { - "elliptic": "^6.5.4", - "node-addon-api": "^2.0.0", + "elliptic": "^6.5.7", + "node-addon-api": "^5.0.0", "node-gyp-build": "^4.2.0" }, "engines": { - "node": ">=10.0.0" + "node": ">=18.0.0" + } + }, + "node_modules/secp256k1/node_modules/elliptic": { + "version": "6.5.7", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz", + "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==", + "dev": true, + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/secp256k1/node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", + "dev": true + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -10024,6 +10834,32 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", @@ -10048,6 +10884,39 @@ "node": ">=8" } }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", @@ -10127,6 +10996,18 @@ "node": ">=4" } }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-hex-prefix": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-hex-prefix/-/strip-hex-prefix-1.0.0.tgz", @@ -10613,19 +11494,21 @@ } }, "node_modules/undici": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.12.0.tgz", - "integrity": "sha512-d87yk8lqSFUYtR5fTFe2frpkMIrUEz+lgoJmhcL+J3StVl+8fj8ytE4lLnJOTPCE12YbumNGzf4LYsQyusdV5g==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.0.0.tgz", + "integrity": "sha512-c4xi3kWnQJrb7h2q8aJYKvUzmz7boCgz1cUCC6OwdeM5Tr2P0hDuthr2iut4ggqsz+Cnh20U/LoTzbKIdDS/Nw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=18.0" + "node": ">=20.18.1" } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/universalify": { "version": "0.1.2", @@ -11144,10 +12027,11 @@ "dev": true }, "node_modules/ws": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", - "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.3.0" }, @@ -11173,6 +12057,18 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -11293,12 +12189,13 @@ } }, "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -11307,7 +12204,10 @@ "scripts/solhint-custom": { "name": "solhint-plugin-openzeppelin", "version": "0.0.0", - "dev": true + "dev": true, + "dependencies": { + "minimatch": "^3.1.2" + } } } } diff --git a/package.json b/package.json index f7c0f519737..d616c5583cd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openzeppelin-solidity", "description": "Secure Smart Contract library for Solidity", - "version": "5.0.2", + "version": "5.1.0", "private": true, "files": [ "/contracts/**/*.sol", @@ -13,12 +13,12 @@ "coverage": "scripts/checks/coverage.sh", "docs": "npm run prepare-docs && oz-docs", "docs:watch": "oz-docs watch contracts docs/templates docs/config.js", - "prepare": "scripts/prepare.sh", + "prepare": "husky", "prepare-docs": "scripts/prepare-docs.sh", "lint": "npm run lint:js && npm run lint:sol", "lint:fix": "npm run lint:js:fix && npm run lint:sol:fix", - "lint:js": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --check && eslint --ignore-path .gitignore .", - "lint:js:fix": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --write && eslint --ignore-path .gitignore . --fix", + "lint:js": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --check && eslint .", + "lint:js:fix": "prettier --log-level warn --ignore-path .gitignore '**/*.{js,ts}' --write && eslint . --fix", "lint:sol": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --check && solhint '{contracts,test}/**/*.sol'", "lint:sol:fix": "prettier --log-level warn --ignore-path .gitignore '{contracts,test}/**/*.sol' --write", "clean": "hardhat clean && rimraf build contracts/build", @@ -26,8 +26,9 @@ "generate": "scripts/generate/run.js", "version": "scripts/release/version.sh", "test": "hardhat test", - "test:inheritance": "scripts/checks/inheritance-ordering.js artifacts/build-info/*", "test:generation": "scripts/checks/generation.sh", + "test:inheritance": "scripts/checks/inheritance-ordering.js artifacts/build-info/*", + "test:pragma": "scripts/checks/pragma-consistency.js artifacts/build-info/*", "gas-report": "env ENABLE_GAS_REPORT=true npm run test", "slither": "npm run clean && slither ." }, @@ -54,6 +55,7 @@ "@changesets/cli": "^2.26.0", "@changesets/pre": "^2.0.0", "@changesets/read": "^0.6.0", + "@eslint/compat": "^1.2.1", "@nomicfoundation/hardhat-chai-matchers": "^2.0.6", "@nomicfoundation/hardhat-ethers": "^3.0.4", "@nomicfoundation/hardhat-network-helpers": "^1.0.3", @@ -62,18 +64,21 @@ "@openzeppelin/upgrade-safe-transpiler": "^0.3.32", "@openzeppelin/upgrades-core": "^1.20.6", "chai": "^4.2.0", - "eslint": "^8.30.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.0.0", - "ethers": "^6.7.1", + "ethers": "^6.13.4", "glob": "^11.0.0", + "globals": "^15.3.0", "graphlib": "^2.1.8", "hardhat": "^2.22.2", "hardhat-exposed": "^0.3.15", - "hardhat-gas-reporter": "^2.0.0", + "hardhat-gas-reporter": "^2.1.0", "hardhat-ignore-warnings": "^0.2.11", + "husky": "^9.1.7", + "lint-staged": "^15.2.10", "lodash.startcase": "^4.4.0", "micromatch": "^4.0.2", - "p-limit": "^3.1.0", + "p-limit": "^6.0.0", "prettier": "^3.0.0", "prettier-plugin-solidity": "^1.1.0", "rimraf": "^6.0.0", @@ -83,7 +88,17 @@ "solidity-ast": "^0.4.50", "solidity-coverage": "^0.8.5", "solidity-docgen": "^0.6.0-beta.29", - "undici": "^6.11.1", + "undici": "^7.0.0", "yargs": "^17.0.0" + }, + "lint-staged": { + "*.{js,ts}": [ + "prettier --log-level warn --ignore-path .gitignore --check", + "eslint" + ], + "*.sol": [ + "prettier --log-level warn --ignore-path .gitignore --check", + "solhint" + ] } } diff --git a/scripts/checks/inheritance-ordering.js b/scripts/checks/inheritance-ordering.js index 72aa37ef7b3..fbeac9ea7c4 100755 --- a/scripts/checks/inheritance-ordering.js +++ b/scripts/checks/inheritance-ordering.js @@ -2,9 +2,13 @@ const path = require('path'); const graphlib = require('graphlib'); +const match = require('micromatch'); const { findAll } = require('solidity-ast/utils'); const { _: artifacts } = require('yargs').argv; +// files to skip +const skipPatterns = ['contracts-exposed/**', 'contracts/mocks/**']; + for (const artifact of artifacts) { const { output: solcOutput } = require(path.resolve(__dirname, '../..', artifact)); @@ -13,10 +17,7 @@ for (const artifact of artifacts) { const linearized = []; for (const source in solcOutput.contracts) { - if (['contracts-exposed/', 'contracts/mocks/'].some(pattern => source.startsWith(pattern))) { - continue; - } - + if (match.any(source, skipPatterns)) continue; for (const contractDef of findAll('ContractDefinition', solcOutput.sources[source].ast)) { names[contractDef.id] = contractDef.name; linearized.push(contractDef.linearizedBaseContracts); @@ -30,7 +31,7 @@ for (const artifact of artifacts) { } /// graphlib.alg.findCycles will not find minimal cycles. - /// We are only interested int cycles of lengths 2 (needs proof) + /// We are only interested in cycles of lengths 2 (needs proof) graph.nodes().forEach((x, i, nodes) => nodes .slice(i + 1) diff --git a/scripts/checks/pragma-consistency.js b/scripts/checks/pragma-consistency.js new file mode 100755 index 00000000000..f2f3c548f59 --- /dev/null +++ b/scripts/checks/pragma-consistency.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node + +const path = require('path'); +const semver = require('semver'); +const match = require('micromatch'); +const { findAll } = require('solidity-ast/utils'); +const { _: artifacts } = require('yargs').argv; + +// files to skip +const skipPatterns = ['contracts-exposed/**', 'contracts/mocks/WithInit.sol']; + +for (const artifact of artifacts) { + const { output: solcOutput } = require(path.resolve(__dirname, '../..', artifact)); + + const pragma = {}; + + // Extract pragma directive for all files + for (const source in solcOutput.contracts) { + if (match.any(source, skipPatterns)) continue; + for (const { literals } of findAll('PragmaDirective', solcOutput.sources[source].ast)) { + // There should only be one. + const [first, ...rest] = literals; + if (first === 'solidity') pragma[source] = rest.join(''); + } + } + + // Compare the pragma directive of the file, to that of the files it imports + for (const source in solcOutput.contracts) { + if (match.any(source, skipPatterns)) continue; + // minimum version of the compiler that matches source's pragma + const minVersion = semver.minVersion(pragma[source]); + // loop over all imports in source + for (const { absolutePath } of findAll('ImportDirective', solcOutput.sources[source].ast)) { + // So files that only import without declaring anything cause issues, because they don't shop in in "pragma" + if (!pragma[absolutePath]) continue; + // Check that the minVersion for source satisfies the requirements of the imported files + if (!semver.satisfies(minVersion, pragma[absolutePath])) { + console.log( + `- ${source} uses ${pragma[source]} but depends on ${absolutePath} that requires ${pragma[absolutePath]}`, + ); + process.exitCode = 1; + } + } + } +} + +if (!process.exitCode) { + console.log('Pragma directives are consistent.'); +} diff --git a/scripts/generate/helpers/sanitize.js b/scripts/generate/helpers/sanitize.js new file mode 100644 index 00000000000..e680ec1bf46 --- /dev/null +++ b/scripts/generate/helpers/sanitize.js @@ -0,0 +1,5 @@ +module.exports = { + address: expr => `and(${expr}, shr(96, not(0)))`, + bool: expr => `iszero(iszero(${expr}))`, + bytes: (expr, size) => `and(${expr}, shl(${256 - 8 * size}, not(0)))`, +}; diff --git a/scripts/generate/run.js b/scripts/generate/run.js index d49105c5a85..6779c93f44b 100755 --- a/scripts/generate/run.js +++ b/scripts/generate/run.js @@ -8,7 +8,7 @@ const format = require('./format-lines'); function getVersion(path) { try { return fs.readFileSync(path, 'utf8').match(/\/\/ OpenZeppelin Contracts \(last updated v[^)]+\)/)[0]; - } catch (err) { + } catch { return null; } } @@ -37,12 +37,13 @@ for (const [file, template] of Object.entries({ 'utils/structs/Checkpoints.sol': './templates/Checkpoints.js', 'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js', 'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js', - 'utils/structs/Heap.sol': './templates/Heap.js', 'utils/SlotDerivation.sol': './templates/SlotDerivation.js', 'utils/StorageSlot.sol': './templates/StorageSlot.js', + 'utils/TransientSlot.sol': './templates/TransientSlot.js', 'utils/Arrays.sol': './templates/Arrays.js', 'utils/Packing.sol': './templates/Packing.js', 'mocks/StorageSlotMock.sol': './templates/StorageSlotMock.js', + 'mocks/TransientSlotMock.sol': './templates/TransientSlotMock.js', })) { generateFromTemplate(file, template, './contracts/'); } @@ -50,7 +51,6 @@ for (const [file, template] of Object.entries({ // Tests for (const [file, template] of Object.entries({ 'utils/structs/Checkpoints.t.sol': './templates/Checkpoints.t.js', - 'utils/structs/Heap.t.sol': './templates/Heap.t.js', 'utils/Packing.t.sol': './templates/Packing.t.js', 'utils/SlotDerivation.t.sol': './templates/SlotDerivation.t.js', })) { diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js index 1a4d1a7a5cd..7ec4a72532a 100644 --- a/scripts/generate/templates/Checkpoints.js +++ b/scripts/generate/templates/Checkpoints.js @@ -41,7 +41,11 @@ struct ${opts.checkpointTypeName} { * IMPORTANT: Never accept \`key\` as a user input, since an arbitrary \`type(${opts.keyTypeName}).max\` key set will disable the * library. */ -function push(${opts.historyTypeName} storage self, ${opts.keyTypeName} key, ${opts.valueTypeName} value) internal returns (${opts.valueTypeName}, ${opts.valueTypeName}) { +function push( + ${opts.historyTypeName} storage self, + ${opts.keyTypeName} key, + ${opts.valueTypeName} value +) internal returns (${opts.valueTypeName} oldValue, ${opts.valueTypeName} newValue) { return _insert(self.${opts.checkpointFieldName}, key, value); } @@ -132,7 +136,11 @@ function at(${opts.historyTypeName} storage self, uint32 pos) internal view retu * @dev Pushes a (\`key\`, \`value\`) pair into an ordered list of checkpoints, either by inserting a new checkpoint, * or by updating the last one. */ -function _insert(${opts.checkpointTypeName}[] storage self, ${opts.keyTypeName} key, ${opts.valueTypeName} value) private returns (${opts.valueTypeName}, ${opts.valueTypeName}) { +function _insert( + ${opts.checkpointTypeName}[] storage self, + ${opts.keyTypeName} key, + ${opts.valueTypeName} value +) private returns (${opts.valueTypeName} oldValue, ${opts.valueTypeName} newValue) { uint256 pos = self.length; if (pos > 0) { @@ -219,7 +227,6 @@ function _unsafeAccess( } } `; -/* eslint-enable max-len */ // GENERATE module.exports = format( diff --git a/scripts/generate/templates/Checkpoints.t.js b/scripts/generate/templates/Checkpoints.t.js index dd564e59bdd..8e178adfac6 100644 --- a/scripts/generate/templates/Checkpoints.t.js +++ b/scripts/generate/templates/Checkpoints.t.js @@ -11,7 +11,6 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol"; `; -/* eslint-disable max-len */ const template = opts => `\ using Checkpoints for Checkpoints.${opts.historyTypeName}; @@ -37,7 +36,7 @@ function _prepareKeys(${opts.keyTypeName}[] memory keys, ${opts.keyTypeName} max } } -function _assertLatestCheckpoint(bool exist, ${opts.keyTypeName} key, ${opts.valueTypeName} value) internal { +function _assertLatestCheckpoint(bool exist, ${opts.keyTypeName} key, ${opts.valueTypeName} value) internal view { (bool _exist, ${opts.keyTypeName} _key, ${opts.valueTypeName} _value) = _ckpts.latestCheckpoint(); assertEq(_exist, exist); assertEq(_key, key); diff --git a/scripts/generate/templates/EnumerableMap.js b/scripts/generate/templates/EnumerableMap.js index 656d8463dec..c9cad6c1bc8 100644 --- a/scripts/generate/templates/EnumerableMap.js +++ b/scripts/generate/templates/EnumerableMap.js @@ -2,7 +2,6 @@ const format = require('../format-lines'); const { fromBytes32, toBytes32 } = require('./conversion'); const { TYPES } = require('./EnumerableMap.opts'); -/* eslint-disable max-len */ const header = `\ pragma solidity ^0.8.20; @@ -52,7 +51,6 @@ import {EnumerableSet} from "./EnumerableSet.sol"; * ==== */ `; -/* eslint-enable max-len */ const defaultMap = `\ // To implement this library for multiple types with as little code repetition as possible, we write it in @@ -117,21 +115,21 @@ function length(Bytes32ToBytes32Map storage map) internal view returns (uint256) * * - \`index\` must be strictly less than {length}. */ -function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32, bytes32) { - bytes32 key = map._keys.at(index); - return (key, map._values[key]); +function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32 key, bytes32 value) { + bytes32 atKey = map._keys.at(index); + return (atKey, map._values[atKey]); } /** * @dev Tries to returns the value associated with \`key\`. O(1). * Does not revert if \`key\` is not in the map. */ -function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool, bytes32) { - bytes32 value = map._values[key]; - if (value == bytes32(0)) { +function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool exists, bytes32 value) { + bytes32 val = map._values[key]; + if (val == bytes32(0)) { return (contains(map, key), bytes32(0)); } else { - return (true, value); + return (true, val); } } @@ -213,18 +211,18 @@ function length(${name} storage map) internal view returns (uint256) { * * - \`index\` must be strictly less than {length}. */ -function at(${name} storage map, uint256 index) internal view returns (${keyType}, ${valueType}) { - (bytes32 key, bytes32 value) = at(map._inner, index); - return (${fromBytes32(keyType, 'key')}, ${fromBytes32(valueType, 'value')}); +function at(${name} storage map, uint256 index) internal view returns (${keyType} key, ${valueType} value) { + (bytes32 atKey, bytes32 val) = at(map._inner, index); + return (${fromBytes32(keyType, 'atKey')}, ${fromBytes32(valueType, 'val')}); } /** * @dev Tries to returns the value associated with \`key\`. O(1). * Does not revert if \`key\` is not in the map. */ -function tryGet(${name} storage map, ${keyType} key) internal view returns (bool, ${valueType}) { - (bool success, bytes32 value) = tryGet(map._inner, ${toBytes32(keyType, 'key')}); - return (success, ${fromBytes32(valueType, 'value')}); +function tryGet(${name} storage map, ${keyType} key) internal view returns (bool exists, ${valueType} value) { + (bool success, bytes32 val) = tryGet(map._inner, ${toBytes32(keyType, 'key')}); + return (success, ${fromBytes32(valueType, 'val')}); } /** diff --git a/scripts/generate/templates/EnumerableSet.js b/scripts/generate/templates/EnumerableSet.js index 351466b1313..02eccd0df11 100644 --- a/scripts/generate/templates/EnumerableSet.js +++ b/scripts/generate/templates/EnumerableSet.js @@ -2,7 +2,6 @@ const format = require('../format-lines'); const { fromBytes32, toBytes32 } = require('./conversion'); const { TYPES } = require('./EnumerableSet.opts'); -/* eslint-disable max-len */ const header = `\ pragma solidity ^0.8.20; @@ -41,7 +40,6 @@ pragma solidity ^0.8.20; * ==== */ `; -/* eslint-enable max-len */ const defaultSet = `\ // To implement this library for multiple types with as little code diff --git a/scripts/generate/templates/Heap.js b/scripts/generate/templates/Heap.js deleted file mode 100644 index aebc42e9ed6..00000000000 --- a/scripts/generate/templates/Heap.js +++ /dev/null @@ -1,327 +0,0 @@ -const format = require('../format-lines'); -const { TYPES } = require('./Heap.opts'); -const { capitalize } = require('../../helpers'); - -/* eslint-disable max-len */ -const header = `\ -pragma solidity ^0.8.20; - -import {Math} from "../math/Math.sol"; -import {SafeCast} from "../math/SafeCast.sol"; -import {Comparators} from "../Comparators.sol"; -import {Panic} from "../Panic.sol"; - -/** - * @dev Library for managing https://en.wikipedia.org/wiki/Binary_heap[binary heap] that can be used as - * https://en.wikipedia.org/wiki/Priority_queue[priority queue]. - * - * Heaps are represented as an array of Node objects. This array stores two overlapping structures: - * * A tree structure where the first element (index 0) is the root, and where the node at index i is the child of the - * node at index (i-1)/2 and the father of nodes at index 2*i+1 and 2*i+2. Each node stores the index (in the array) - * where the corresponding value is stored. - * * A list of payloads values where each index contains a value and a lookup index. The type of the value depends on - * the variant being used. The lookup is the index of the node (in the tree) that points to this value. - * - * Some invariants: - * \`\`\` - * i == heap.data[heap.data[i].index].lookup // for all indices i - * i == heap.data[heap.data[i].lookup].index // for all indices i - * \`\`\` - * - * The structure is ordered so that each node is bigger than its parent. An immediate consequence is that the - * highest priority value is the one at the root. This value can be looked up in constant time (O(1)) at - * \`heap.data[heap.data[0].index].value\` - * - * The structure is designed to perform the following operations with the corresponding complexities: - * - * * peek (get the highest priority value): O(1) - * * insert (insert a value): O(log(n)) - * * pop (remove the highest priority value): O(log(n)) - * * replace (replace the highest priority value with a new value): O(log(n)) - * * length (get the number of elements): O(1) - * * clear (remove all elements): O(1) - */ -`; - -const generate = ({ struct, node, valueType, indexType, blockSize }) => `\ -/** - * @dev Binary heap that supports values of type ${valueType}. - * - * Each element of that structure uses ${blockSize} storage slots. - */ -struct ${struct} { - ${node}[] data; -} - -/** - * @dev Internal node type for ${struct}. Stores a value of type ${valueType}. - */ -struct ${node} { - ${valueType} value; - ${indexType} index; // position -> value - ${indexType} lookup; // value -> position -} - -/** - * @dev Lookup the root element of the heap. - */ -function peek(${struct} storage self) internal view returns (${valueType}) { - // self.data[0] will \`ARRAY_ACCESS_OUT_OF_BOUNDS\` panic if heap is empty. - return _unsafeNodeAccess(self, self.data[0].index).value; -} - -/** - * @dev Remove (and return) the root element for the heap using the default comparator. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ -function pop(${struct} storage self) internal returns (${valueType}) { - return pop(self, Comparators.lt); -} - -/** - * @dev Remove (and return) the root element for the heap using the provided comparator. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ -function pop( - ${struct} storage self, - function(uint256, uint256) view returns (bool) comp -) internal returns (${valueType}) { - unchecked { - ${indexType} size = length(self); - if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - - ${indexType} last = size - 1; - - // get root location (in the data array) and value - ${node} storage rootNode = _unsafeNodeAccess(self, 0); - ${indexType} rootIdx = rootNode.index; - ${node} storage rootData = _unsafeNodeAccess(self, rootIdx); - ${node} storage lastNode = _unsafeNodeAccess(self, last); - ${valueType} rootDataValue = rootData.value; - - // if root is not the last element of the data array (that will get popped), reorder the data array. - if (rootIdx != last) { - // get details about the value stored in the last element of the array (that will get popped) - ${indexType} lastDataIdx = lastNode.lookup; - ${valueType} lastDataValue = lastNode.value; - // copy these values to the location of the root (that is safe, and that we no longer use) - rootData.value = lastDataValue; - rootData.lookup = lastDataIdx; - // update the tree node that used to point to that last element (value now located where the root was) - _unsafeNodeAccess(self, lastDataIdx).index = rootIdx; - } - - // get last leaf location (in the data array) and value - ${indexType} lastIdx = lastNode.index; - ${valueType} lastValue = _unsafeNodeAccess(self, lastIdx).value; - - // move the last leaf to the root, pop last leaf ... - rootNode.index = lastIdx; - _unsafeNodeAccess(self, lastIdx).lookup = 0; - self.data.pop(); - - // ... and heapify - _siftDown(self, last, 0, lastValue, comp); - - // return root value - return rootDataValue; - } -} - -/** - * @dev Insert a new element in the heap using the default comparator. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ -function insert(${struct} storage self, ${valueType} value) internal { - insert(self, value, Comparators.lt); -} - -/** - * @dev Insert a new element in the heap using the provided comparator. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ -function insert( - ${struct} storage self, - ${valueType} value, - function(uint256, uint256) view returns (bool) comp -) internal { - ${indexType} size = length(self); - if (size == type(${indexType}).max) Panic.panic(Panic.RESOURCE_ERROR); - - self.data.push(${struct}Node({index: size, lookup: size, value: value})); - _siftUp(self, size, value, comp); -} - -/** - * @dev Return the root element for the heap, and replace it with a new value, using the default comparator. - * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ -function replace(${struct} storage self, ${valueType} newValue) internal returns (${valueType}) { - return replace(self, newValue, Comparators.lt); -} - -/** - * @dev Return the root element for the heap, and replace it with a new value, using the provided comparator. - * This is equivalent to using {pop} and {insert}, but requires only one rebalancing operation. - * - * NOTE: All inserting and removal from a heap should always be done using the same comparator. Mixing comparator - * during the lifecycle of a heap will result in undefined behavior. - */ -function replace( - ${struct} storage self, - ${valueType} newValue, - function(uint256, uint256) view returns (bool) comp -) internal returns (${valueType}) { - ${indexType} size = length(self); - if (size == 0) Panic.panic(Panic.EMPTY_ARRAY_POP); - - // position of the node that holds the data for the root - ${indexType} rootIdx = _unsafeNodeAccess(self, 0).index; - // storage pointer to the node that holds the data for the root - ${node} storage rootData = _unsafeNodeAccess(self, rootIdx); - - // cache old value and replace it - ${valueType} oldValue = rootData.value; - rootData.value = newValue; - - // re-heapify - _siftDown(self, size, 0, newValue, comp); - - // return old root value - return oldValue; -} - -/** - * @dev Returns the number of elements in the heap. - */ -function length(${struct} storage self) internal view returns (${indexType}) { - return self.data.length.to${capitalize(indexType)}(); -} - -/** - * @dev Removes all elements in the heap. - */ -function clear(${struct} storage self) internal { - ${struct}Node[] storage data = self.data; - assembly ("memory-safe") { - sstore(data.slot, 0) - } -} - -/** - * @dev Swap node \`i\` and \`j\` in the tree. - */ -function _swap(${struct} storage self, ${indexType} i, ${indexType} j) private { - ${node} storage ni = _unsafeNodeAccess(self, i); - ${node} storage nj = _unsafeNodeAccess(self, j); - ${indexType} ii = ni.index; - ${indexType} jj = nj.index; - // update pointers to the data (swap the value) - ni.index = jj; - nj.index = ii; - // update lookup pointers for consistency - _unsafeNodeAccess(self, ii).lookup = j; - _unsafeNodeAccess(self, jj).lookup = i; -} - -/** - * @dev Perform heap maintenance on \`self\`, starting at position \`pos\` (with the \`value\`), using \`comp\` as a - * comparator, and moving toward the leaves of the underlying tree. - * - * NOTE: This is a private function that is called in a trusted context with already cached parameters. \`length\` - * and \`value\` could be extracted from \`self\` and \`pos\`, but that would require redundant storage read. These - * parameters are not verified. It is the caller role to make sure the parameters are correct. - */ -function _siftDown( - ${struct} storage self, - ${indexType} size, - ${indexType} pos, - ${valueType} value, - function(uint256, uint256) view returns (bool) comp -) private { - uint256 left = 2 * pos + 1; // this could overflow ${indexType} - uint256 right = 2 * pos + 2; // this could overflow ${indexType} - - if (right < size) { - // the check guarantees that \`left\` and \`right\` are both valid ${indexType} - ${indexType} lIndex = ${indexType}(left); - ${indexType} rIndex = ${indexType}(right); - ${valueType} lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; - ${valueType} rValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, rIndex).index).value; - if (comp(lValue, value) || comp(rValue, value)) { - ${indexType} index = ${indexType}(comp(lValue, rValue).ternary(lIndex, rIndex)); - _swap(self, pos, index); - _siftDown(self, size, index, value, comp); - } - } else if (left < size) { - // the check guarantees that \`left\` is a valid ${indexType} - ${indexType} lIndex = ${indexType}(left); - ${valueType} lValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, lIndex).index).value; - if (comp(lValue, value)) { - _swap(self, pos, lIndex); - _siftDown(self, size, lIndex, value, comp); - } - } -} - -/** - * @dev Perform heap maintenance on \`self\`, starting at position \`pos\` (with the \`value\`), using \`comp\` as a - * comparator, and moving toward the root of the underlying tree. - * - * NOTE: This is a private function that is called in a trusted context with already cached parameters. \`value\` - * could be extracted from \`self\` and \`pos\`, but that would require redundant storage read. These parameters are not - * verified. It is the caller role to make sure the parameters are correct. - */ -function _siftUp( - ${struct} storage self, - ${indexType} pos, - ${valueType} value, - function(uint256, uint256) view returns (bool) comp -) private { - unchecked { - while (pos > 0) { - ${indexType} parent = (pos - 1) / 2; - ${valueType} parentValue = _unsafeNodeAccess(self, _unsafeNodeAccess(self, parent).index).value; - if (comp(parentValue, value)) break; - _swap(self, pos, parent); - pos = parent; - } - } -} - -function _unsafeNodeAccess( - ${struct} storage self, - ${indexType} pos -) private pure returns (${node} storage result) { - assembly ("memory-safe") { - mstore(0x00, self.slot) - result.slot := add(keccak256(0x00, 0x20), ${blockSize == 1 ? 'pos' : `mul(pos, ${blockSize})`}) - } -} -`; - -// GENERATE -module.exports = format( - header.trimEnd(), - 'library Heap {', - format( - [].concat( - 'using Math for *;', - 'using SafeCast for *;', - '', - TYPES.map(type => generate(type)), - ), - ).trimEnd(), - '}', -); diff --git a/scripts/generate/templates/Heap.opts.js b/scripts/generate/templates/Heap.opts.js deleted file mode 100644 index 8b8be0afdfa..00000000000 --- a/scripts/generate/templates/Heap.opts.js +++ /dev/null @@ -1,13 +0,0 @@ -const makeType = (valueSize, indexSize) => ({ - struct: `Uint${valueSize}Heap`, - node: `Uint${valueSize}HeapNode`, - valueSize, - valueType: `uint${valueSize}`, - indexSize, - indexType: `uint${indexSize}`, - blockSize: Math.ceil((valueSize + 2 * indexSize) / 256), -}); - -module.exports = { - TYPES: [makeType(256, 64), makeType(208, 24)], -}; diff --git a/scripts/generate/templates/Heap.t.js b/scripts/generate/templates/Heap.t.js deleted file mode 100644 index 04b3152ba3a..00000000000 --- a/scripts/generate/templates/Heap.t.js +++ /dev/null @@ -1,89 +0,0 @@ -const format = require('../format-lines'); -const { TYPES } = require('./Heap.opts'); - -/* eslint-disable max-len */ -const header = `\ -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; -import {Heap} from "@openzeppelin/contracts/utils/structs/Heap.sol"; -import {Comparators} from "@openzeppelin/contracts/utils/Comparators.sol"; -`; - -const generate = ({ struct, valueType }) => `\ -contract ${struct}Test is Test { - using Heap for Heap.${struct}; - - Heap.${struct} internal heap; - - function _validateHeap(function(uint256, uint256) view returns (bool) comp) internal { - for (uint32 i = 0; i < heap.length(); ++i) { - // lookups - assertEq(i, heap.data[heap.data[i].index].lookup); - assertEq(i, heap.data[heap.data[i].lookup].index); - - // ordering: each node has a value bigger then its parent - if (i > 0) - assertFalse(comp(heap.data[heap.data[i].index].value, heap.data[heap.data[(i - 1) / 2].index].value)); - } - } - - function testFuzz(${valueType}[] calldata input) public { - vm.assume(input.length < 0x20); - assertEq(heap.length(), 0); - - uint256 min = type(uint256).max; - for (uint256 i = 0; i < input.length; ++i) { - heap.insert(input[i]); - assertEq(heap.length(), i + 1); - _validateHeap(Comparators.lt); - - min = Math.min(min, input[i]); - assertEq(heap.peek(), min); - } - - uint256 max = 0; - for (uint256 i = 0; i < input.length; ++i) { - ${valueType} top = heap.peek(); - ${valueType} pop = heap.pop(); - assertEq(heap.length(), input.length - i - 1); - _validateHeap(Comparators.lt); - - assertEq(pop, top); - assertGe(pop, max); - max = pop; - } - } - - function testFuzzGt(${valueType}[] calldata input) public { - vm.assume(input.length < 0x20); - assertEq(heap.length(), 0); - - uint256 max = 0; - for (uint256 i = 0; i < input.length; ++i) { - heap.insert(input[i], Comparators.gt); - assertEq(heap.length(), i + 1); - _validateHeap(Comparators.gt); - - max = Math.max(max, input[i]); - assertEq(heap.peek(), max); - } - - uint256 min = type(uint256).max; - for (uint256 i = 0; i < input.length; ++i) { - ${valueType} top = heap.peek(); - ${valueType} pop = heap.pop(Comparators.gt); - assertEq(heap.length(), input.length - i - 1); - _validateHeap(Comparators.gt); - - assertEq(pop, top); - assertLe(pop, min); - min = pop; - } - } -} -`; - -// GENERATE -module.exports = format(header, ...TYPES.map(type => generate(type))); diff --git a/scripts/generate/templates/MerkleProof.js b/scripts/generate/templates/MerkleProof.js index 6711a87e28c..890b2febac4 100644 --- a/scripts/generate/templates/MerkleProof.js +++ b/scripts/generate/templates/MerkleProof.js @@ -43,7 +43,6 @@ const errors = `\ error MerkleProofInvalidMultiproof(); `; -/* eslint-disable max-len */ const templateProof = ({ suffix, location, visibility, hash }) => `\ /** * @dev Returns true if a \`leaf\` can be proved to be a part of a Merkle tree @@ -172,7 +171,6 @@ function processMultiProof${suffix}(${formatArgsMultiline( } } `; -/* eslint-enable max-len */ // GENERATE module.exports = format( diff --git a/scripts/generate/templates/Packing.js b/scripts/generate/templates/Packing.js index b9422ef4833..9f3b7716a6a 100644 --- a/scripts/generate/templates/Packing.js +++ b/scripts/generate/templates/Packing.js @@ -1,4 +1,5 @@ const format = require('../format-lines'); +const sanitize = require('../helpers/sanitize'); const { product } = require('../../helpers'); const { SIZES } = require('./Packing.opts'); @@ -31,6 +32,8 @@ pragma solidity ^0.8.20; * } * } * \`\`\` + * + * _Available since v5.1._ */ // solhint-disable func-name-mixedcase `; @@ -44,8 +47,8 @@ function pack_${left}_${right}(bytes${left} left, bytes${right} right) internal left + right } result) { assembly ("memory-safe") { - left := and(left, shl(${256 - 8 * left}, not(0))) - right := and(right, shl(${256 - 8 * right}, not(0))) + left := ${sanitize.bytes('left', left)} + right := ${sanitize.bytes('right', right)} result := or(left, shr(${8 * left}, right)) } } @@ -55,7 +58,7 @@ const extract = (outer, inner) => `\ function extract_${outer}_${inner}(bytes${outer} self, uint8 offset) internal pure returns (bytes${inner} result) { if (offset > ${outer - inner}) revert OutOfRangeAccess(); assembly ("memory-safe") { - result := and(shl(mul(8, offset), self), shl(${256 - 8 * inner}, not(0))) + result := ${sanitize.bytes('shl(mul(8, offset), self)', inner)} } } `; @@ -64,7 +67,7 @@ const replace = (outer, inner) => `\ function replace_${outer}_${inner}(bytes${outer} self, bytes${inner} value, uint8 offset) internal pure returns (bytes${outer} result) { bytes${inner} oldValue = extract_${outer}_${inner}(self, offset); assembly ("memory-safe") { - value := and(value, shl(${256 - 8 * inner}, not(0))) + value := ${sanitize.bytes('value', inner)} result := xor(self, shr(mul(8, offset), xor(oldValue, value))) } } diff --git a/scripts/generate/templates/Packing.opts.js b/scripts/generate/templates/Packing.opts.js index de9ab77ff53..893ad6297cf 100644 --- a/scripts/generate/templates/Packing.opts.js +++ b/scripts/generate/templates/Packing.opts.js @@ -1,3 +1,3 @@ module.exports = { - SIZES: [1, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32], + SIZES: [1, 2, 4, 6, 8, 10, 12, 16, 20, 22, 24, 28, 32], }; diff --git a/scripts/generate/templates/Packing.t.js b/scripts/generate/templates/Packing.t.js index 56e9c0cc7c4..1feec28f5a5 100644 --- a/scripts/generate/templates/Packing.t.js +++ b/scripts/generate/templates/Packing.t.js @@ -11,14 +11,14 @@ import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; `; const testPack = (left, right) => `\ -function testPack(bytes${left} left, bytes${right} right) external { +function testPack(bytes${left} left, bytes${right} right) external pure { assertEq(left, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${left}(0)); assertEq(right, Packing.pack_${left}_${right}(left, right).extract_${left + right}_${right}(${left})); } `; const testReplace = (outer, inner) => `\ -function testReplace(bytes${outer} container, bytes${inner} newValue, uint8 offset) external { +function testReplace(bytes${outer} container, bytes${inner} newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, ${outer - inner})); bytes${inner} oldValue = container.extract_${outer}_${inner}(offset); diff --git a/scripts/generate/templates/SafeCast.js b/scripts/generate/templates/SafeCast.js index a3b32e3f00a..21000cf4a62 100644 --- a/scripts/generate/templates/SafeCast.js +++ b/scripts/generate/templates/SafeCast.js @@ -61,7 +61,6 @@ function toUint${length}(uint256 value) internal pure returns (uint${length}) { } `; -/* eslint-disable max-len */ const toIntDownCast = length => `\ /** * @dev Returns the downcasted int${length} from int256, reverting on @@ -81,7 +80,6 @@ function toInt${length}(int256 value) internal pure returns (int${length} downca } } `; -/* eslint-enable max-len */ const toInt = length => `\ /** diff --git a/scripts/generate/templates/Slot.opts.js b/scripts/generate/templates/Slot.opts.js index aed1f988845..3eca2bcf08f 100644 --- a/scripts/generate/templates/Slot.opts.js +++ b/scripts/generate/templates/Slot.opts.js @@ -10,4 +10,6 @@ const TYPES = [ { type: 'bytes', isValueType: false }, ].map(type => Object.assign(type, { name: type.name ?? capitalize(type.type) })); +Object.assign(TYPES, Object.fromEntries(TYPES.map(entry => [entry.type, entry]))); + module.exports = { TYPES }; diff --git a/scripts/generate/templates/SlotDerivation.js b/scripts/generate/templates/SlotDerivation.js index 5311fb3c8d1..ec4d244b931 100644 --- a/scripts/generate/templates/SlotDerivation.js +++ b/scripts/generate/templates/SlotDerivation.js @@ -1,4 +1,5 @@ const format = require('../format-lines'); +const sanitize = require('../helpers/sanitize'); const { TYPES } = require('./Slot.opts'); const header = `\ @@ -35,6 +36,8 @@ pragma solidity ^0.8.20; * * NOTE: This library provides a way to manipulate storage locations in a non-standard way. Tooling for checking * upgrade safety will ignore the slots accessed through this library. + * + * _Available since v5.1._ */ `; @@ -77,7 +80,7 @@ const mapping = ({ type }) => `\ */ function deriveMapping(bytes32 slot, ${type} key) internal pure returns (bytes32 result) { assembly ("memory-safe") { - mstore(0x00, key) + mstore(0x00, ${(sanitize[type] ?? (x => x))('key')}) mstore(0x20, slot) result := keccak256(0x00, 0x40) } diff --git a/scripts/generate/templates/SlotDerivation.t.js b/scripts/generate/templates/SlotDerivation.t.js index dc7b07ff45a..824af079a14 100644 --- a/scripts/generate/templates/SlotDerivation.t.js +++ b/scripts/generate/templates/SlotDerivation.t.js @@ -45,7 +45,7 @@ function _assertDeriveArray(uint256 length, uint256 offset) public { const mapping = ({ type, name }) => `\ mapping(${type} => bytes) private _${type}Mapping; -function testSymbolicDeriveMapping${name}(${type} key) public { +function testSymbolicDeriveMapping${name}(${type} key) public view { bytes32 baseSlot; assembly { baseSlot := _${type}Mapping.slot @@ -61,18 +61,30 @@ function testSymbolicDeriveMapping${name}(${type} key) public { } `; +const mappingDirty = ({ type, name }) => `\ +function testSymbolicDeriveMapping${name}Dirty(bytes32 dirtyKey) public { + ${type} key; + assembly { + key := dirtyKey + } + + // run the "normal" test using a potentially dirty value + testSymbolicDeriveMapping${name}(key); +} +`; + const boundedMapping = ({ type, name }) => `\ mapping(${type} => bytes) private _${type}Mapping; -function testDeriveMapping${name}(${type} memory key) public { +function testDeriveMapping${name}(${type} memory key) public view { _assertDeriveMapping${name}(key); } -function symbolicDeriveMapping${name}() public { +function symbolicDeriveMapping${name}() public view { _assertDeriveMapping${name}(svm.create${name}(256, "DeriveMapping${name}Input")); } -function _assertDeriveMapping${name}(${type} memory key) internal { +function _assertDeriveMapping${name}(${type} memory key) internal view { bytes32 baseSlot; assembly { baseSlot := _${type}Mapping.slot @@ -107,6 +119,8 @@ module.exports = format( })), ), ).map(type => (type.isValueType ? mapping(type) : boundedMapping(type))), + mappingDirty(TYPES.bool), + mappingDirty(TYPES.address), ), ).trimEnd(), '}', diff --git a/scripts/generate/templates/StorageSlot.js b/scripts/generate/templates/StorageSlot.js index 7a00f5e225c..53287b81fd9 100644 --- a/scripts/generate/templates/StorageSlot.js +++ b/scripts/generate/templates/StorageSlot.js @@ -2,7 +2,7 @@ const format = require('../format-lines'); const { TYPES } = require('./Slot.opts'); const header = `\ -pragma solidity ^0.8.24; +pragma solidity ^0.8.20; /** * @dev Library for reading and writing primitive types to specific storage slots. @@ -29,24 +29,6 @@ pragma solidity ^0.8.24; * } * \`\`\` * - * Since version 5.1, this library also support writing and reading value types to and from transient storage. - * - * * Example using transient storage: - * \`\`\`solidity - * contract Lock { - * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot. - * bytes32 internal constant _LOCK_SLOT = 0xf4678858b2b588224636b8522b729e7722d32fc491da849ed75b3fdf3c84f542; - * - * modifier locked() { - * require(!_LOCK_SLOT.asBoolean().tload()); - * - * _LOCK_SLOT.asBoolean().tstore(true); - * _; - * _LOCK_SLOT.asBoolean().tstore(false); - * } - * } - * \`\`\` - * * TIP: Consider using this library along with {SlotDerivation}. */ `; @@ -81,40 +63,6 @@ function get${name}Slot(${type} storage store) internal pure returns (${name}Slo } `; -const udvt = ({ type, name }) => `\ -/** - * @dev UDVT that represent a slot holding a ${type}. - */ -type ${name}SlotType is bytes32; - -/** - * @dev Cast an arbitrary slot to a ${name}SlotType. - */ -function as${name}(bytes32 slot) internal pure returns (${name}SlotType) { - return ${name}SlotType.wrap(slot); -} -`; - -const transient = ({ type, name }) => `\ -/** - * @dev Load the value held at location \`slot\` in transient storage. - */ -function tload(${name}SlotType slot) internal view returns (${type} value) { - assembly ("memory-safe") { - value := tload(slot) - } -} - -/** - * @dev Store \`value\` at location \`slot\` in transient storage. - */ -function tstore(${name}SlotType slot, ${type} value) internal { - assembly ("memory-safe") { - tstore(slot, value) - } -} -`; - // GENERATE module.exports = format( header.trimEnd(), @@ -123,8 +71,6 @@ module.exports = format( [].concat( TYPES.map(type => struct(type)), TYPES.flatMap(type => [get(type), !type.isValueType && getStorage(type)].filter(Boolean)), - TYPES.filter(type => type.isValueType).map(type => udvt(type)), - TYPES.filter(type => type.isValueType).map(type => transient(type)), ), ).trimEnd(), '}', diff --git a/scripts/generate/templates/StorageSlotMock.js b/scripts/generate/templates/StorageSlotMock.js index 623a6759247..c6d326a5e26 100644 --- a/scripts/generate/templates/StorageSlotMock.js +++ b/scripts/generate/templates/StorageSlotMock.js @@ -2,7 +2,7 @@ const format = require('../format-lines'); const { TYPES } = require('./Slot.opts'); const header = `\ -pragma solidity ^0.8.24; +pragma solidity ^0.8.20; import {Multicall} from "../utils/Multicall.sol"; import {StorageSlot} from "../utils/StorageSlot.sol"; @@ -40,18 +40,6 @@ function get${name}Storage(uint256 key) public view returns (${type} memory) { } `; -const transient = ({ type, name }) => `\ -event ${name}Value(bytes32 slot, ${type} value); - -function tload${name}(bytes32 slot) public { - emit ${name}Value(slot, slot.as${name}().tload()); -} - -function tstore(bytes32 slot, ${type} value) public { - slot.as${name}().tstore(value); -} -`; - // GENERATE module.exports = format( header, @@ -63,7 +51,6 @@ module.exports = format( TYPES.filter(type => type.isValueType).map(type => storageSetValueType(type)), TYPES.filter(type => type.isValueType).map(type => storageGetValueType(type)), TYPES.filter(type => !type.isValueType).map(type => storageSetNonValueType(type)), - TYPES.filter(type => type.isValueType).map(type => transient(type)), ), ).trimEnd(), '}', diff --git a/scripts/generate/templates/TransientSlot.js b/scripts/generate/templates/TransientSlot.js new file mode 100644 index 00000000000..8e291bc13da --- /dev/null +++ b/scripts/generate/templates/TransientSlot.js @@ -0,0 +1,80 @@ +const format = require('../format-lines'); +const { TYPES } = require('./Slot.opts'); + +const header = `\ +pragma solidity ^0.8.24; + +/** + * @dev Library for reading and writing value-types to specific transient storage slots. + * + * Transient slots are often used to store temporary values that are removed after the current transaction. + * This library helps with reading and writing to such slots without the need for inline assembly. + * + * * Example reading and writing values using transient storage: + * \`\`\`solidity + * contract Lock { + * using TransientSlot for *; + * + * // Define the slot. Alternatively, use the SlotDerivation library to derive the slot. + * bytes32 internal constant _LOCK_SLOT = 0xf4678858b2b588224636b8522b729e7722d32fc491da849ed75b3fdf3c84f542; + * + * modifier locked() { + * require(!_LOCK_SLOT.asBoolean().tload()); + * + * _LOCK_SLOT.asBoolean().tstore(true); + * _; + * _LOCK_SLOT.asBoolean().tstore(false); + * } + * } + * \`\`\` + * + * TIP: Consider using this library along with {SlotDerivation}. + */ +`; + +const udvt = ({ type, name }) => `\ +/** + * @dev UDVT that represent a slot holding a ${type}. + */ +type ${name}Slot is bytes32; + +/** + * @dev Cast an arbitrary slot to a ${name}Slot. + */ +function as${name}(bytes32 slot) internal pure returns (${name}Slot) { + return ${name}Slot.wrap(slot); +} +`; + +const transient = ({ type, name }) => `\ +/** + * @dev Load the value held at location \`slot\` in transient storage. + */ +function tload(${name}Slot slot) internal view returns (${type} value) { + assembly ("memory-safe") { + value := tload(slot) + } +} + +/** + * @dev Store \`value\` at location \`slot\` in transient storage. + */ +function tstore(${name}Slot slot, ${type} value) internal { + assembly ("memory-safe") { + tstore(slot, value) + } +} +`; + +// GENERATE +module.exports = format( + header.trimEnd(), + 'library TransientSlot {', + format( + [].concat( + TYPES.filter(type => type.isValueType).map(type => udvt(type)), + TYPES.filter(type => type.isValueType).map(type => transient(type)), + ), + ).trimEnd(), + '}', +); diff --git a/scripts/generate/templates/TransientSlotMock.js b/scripts/generate/templates/TransientSlotMock.js new file mode 100644 index 00000000000..4807b0cc1ff --- /dev/null +++ b/scripts/generate/templates/TransientSlotMock.js @@ -0,0 +1,35 @@ +const format = require('../format-lines'); +const { TYPES } = require('./Slot.opts'); + +const header = `\ +pragma solidity ^0.8.24; + +import {Multicall} from "../utils/Multicall.sol"; +import {TransientSlot} from "../utils/TransientSlot.sol"; +`; + +const transient = ({ type, name }) => `\ +event ${name}Value(bytes32 slot, ${type} value); + +function tload${name}(bytes32 slot) public { + emit ${name}Value(slot, slot.as${name}().tload()); +} + +function tstore(bytes32 slot, ${type} value) public { + slot.as${name}().tstore(value); +} +`; + +// GENERATE +module.exports = format( + header, + 'contract TransientSlotMock is Multicall {', + format( + [].concat( + 'using TransientSlot for *;', + '', + TYPES.filter(type => type.isValueType).map(type => transient(type)), + ), + ).trimEnd(), + '}', +); diff --git a/scripts/prepare.sh b/scripts/prepare.sh deleted file mode 100755 index a7d74227d26..00000000000 --- a/scripts/prepare.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if git status &>/dev/null; then git config core.hooksPath .githooks; fi diff --git a/scripts/release/workflow/state.js b/scripts/release/workflow/state.js index 914e8de0222..002f7774d47 100644 --- a/scripts/release/workflow/state.js +++ b/scripts/release/workflow/state.js @@ -106,7 +106,7 @@ async function readChangesetState(cwd = process.cwd()) { }; } -async function isPublishedOnNpm(package, version) { - const res = await fetch(`https://registry.npmjs.com/${package}/${version}`); +async function isPublishedOnNpm(packageName, version) { + const res = await fetch(`https://registry.npmjs.com/${packageName}/${version}`); return res.ok; } diff --git a/scripts/solhint-custom/package.json b/scripts/solhint-custom/package.json index 075eb929dea..ce9690d74ef 100644 --- a/scripts/solhint-custom/package.json +++ b/scripts/solhint-custom/package.json @@ -1,5 +1,8 @@ { "name": "solhint-plugin-openzeppelin", "version": "0.0.0", - "private": true + "private": true, + "dependencies": { + "minimatch": "^3.1.2" + } } diff --git a/scripts/update-docs-branch.js b/scripts/update-docs-branch.js index 324ba0c67ca..cf61daad835 100644 --- a/scripts/update-docs-branch.js +++ b/scripts/update-docs-branch.js @@ -6,7 +6,7 @@ const run = cmd => { const tryRead = cmd => { try { return read(cmd); - } catch (e) { + } catch { return undefined; } }; diff --git a/slither.config.json b/slither.config.json index 069da1f3a21..fa52f4dd1dd 100644 --- a/slither.config.json +++ b/slither.config.json @@ -1,5 +1,5 @@ { "detectors_to_run": "arbitrary-send-erc20,array-by-reference,incorrect-shift,name-reused,rtlo,suicidal,uninitialized-state,uninitialized-storage,arbitrary-send-erc20-permit,controlled-array-length,controlled-delegatecall,delegatecall-loop,msg-value-loop,reentrancy-eth,unchecked-transfer,weak-prng,domain-separator-collision,erc20-interface,erc721-interface,locked-ether,mapping-deletion,shadowing-abstract,tautology,write-after-write,boolean-cst,reentrancy-no-eth,reused-constructor,tx-origin,unchecked-lowlevel,unchecked-send,variable-scope,void-cst,events-access,events-maths,incorrect-unary,boolean-equal,cyclomatic-complexity,deprecated-standards,erc20-indexed,function-init-state,pragma,unused-state,reentrancy-unlimited-gas,constable-states,immutable-states,var-read-using-this", - "filter_paths": "contracts/mocks,contracts-exposed", + "filter_paths": "contracts/mocks,contracts/vendor,contracts-exposed", "compile_force_framework": "hardhat" } diff --git a/solhint.config.js b/solhint.config.js index f0bd7994f97..47c1cebfe25 100644 --- a/solhint.config.js +++ b/solhint.config.js @@ -1,4 +1,4 @@ -const customRules = require('./scripts/solhint-custom'); +const customRules = require('solhint-plugin-openzeppelin'); const rules = [ 'avoid-tx-origin', diff --git a/test/TESTING.md b/test/TESTING.md index a5ee9323ff4..321c7e592d6 100644 --- a/test/TESTING.md +++ b/test/TESTING.md @@ -1,3 +1,3 @@ ## Testing -Unit test are critical to OpenZeppelin Contracts. They help ensure code quality and mitigate against security vulnerabilities. The directory structure within the `/test` directory corresponds to the `/contracts` directory. +Unit tests are critical to OpenZeppelin Contracts. They help ensure code quality and mitigate against security vulnerabilities. The directory structure within the `/test` directory corresponds to the `/contracts` directory. diff --git a/test/access/Ownable2Step.test.js b/test/access/Ownable2Step.test.js index 4c49e217015..5620a249132 100644 --- a/test/access/Ownable2Step.test.js +++ b/test/access/Ownable2Step.test.js @@ -81,5 +81,22 @@ describe('Ownable2Step', function () { expect(await this.ownable2Step.owner()).to.equal(this.accountA); }); + + it('allows the owner to cancel an initiated ownership transfer by setting newOwner to zero address', async function () { + // initiate ownership transfer to accountA + await this.ownable2Step.connect(this.owner).transferOwnership(this.accountA); + expect(await this.ownable2Step.pendingOwner()).to.equal(this.accountA); + + // cancel the ownership transfer by setting newOwner to zero address + await expect(this.ownable2Step.connect(this.owner).transferOwnership(ethers.ZeroAddress)) + .to.emit(this.ownable2Step, 'OwnershipTransferStarted') + .withArgs(this.owner, ethers.ZeroAddress); + expect(await this.ownable2Step.pendingOwner()).to.equal(ethers.ZeroAddress); + + // verify that accountA cannot accept ownership anymore + await expect(this.ownable2Step.connect(this.accountA).acceptOwnership()) + .to.be.revertedWithCustomError(this.ownable2Step, 'OwnableUnauthorizedAccount') + .withArgs(this.accountA); + }); }); }); diff --git a/test/account/utils/draft-ERC4337Utils.test.js b/test/account/utils/draft-ERC4337Utils.test.js new file mode 100644 index 00000000000..7c292910dfb --- /dev/null +++ b/test/account/utils/draft-ERC4337Utils.test.js @@ -0,0 +1,288 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { packValidationData, UserOperation } = require('../../helpers/erc4337'); +const { deployEntrypoint } = require('../../helpers/erc4337-entrypoint'); +const { MAX_UINT48 } = require('../../helpers/constants'); +const ADDRESS_ONE = '0x0000000000000000000000000000000000000001'; + +const fixture = async () => { + const { entrypoint } = await deployEntrypoint(); + const [authorizer, sender, factory, paymaster] = await ethers.getSigners(); + const utils = await ethers.deployContract('$ERC4337Utils'); + const SIG_VALIDATION_SUCCESS = await utils.$SIG_VALIDATION_SUCCESS(); + const SIG_VALIDATION_FAILED = await utils.$SIG_VALIDATION_FAILED(); + return { utils, authorizer, sender, entrypoint, factory, paymaster, SIG_VALIDATION_SUCCESS, SIG_VALIDATION_FAILED }; +}; + +describe('ERC4337Utils', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('parseValidationData', function () { + it('parses the validation data', async function () { + const authorizer = this.authorizer; + const validUntil = 0x12345678n; + const validAfter = 0x9abcdef0n; + const validationData = packValidationData(validAfter, validUntil, authorizer); + + expect(this.utils.$parseValidationData(validationData)).to.eventually.deep.equal([ + authorizer.address, + validAfter, + validUntil, + ]); + }); + + it('returns an type(uint48).max if until is 0', async function () { + const authorizer = this.authorizer; + const validAfter = 0x12345678n; + const validationData = packValidationData(validAfter, 0, authorizer); + + expect(this.utils.$parseValidationData(validationData)).to.eventually.deep.equal([ + authorizer.address, + validAfter, + MAX_UINT48, + ]); + }); + + it('parse canonical values', async function () { + expect(this.utils.$parseValidationData(this.SIG_VALIDATION_SUCCESS)).to.eventually.deep.equal([ + ethers.ZeroAddress, + 0n, + MAX_UINT48, + ]); + + expect(this.utils.$parseValidationData(this.SIG_VALIDATION_FAILED)).to.eventually.deep.equal([ + ADDRESS_ONE, + 0n, + MAX_UINT48, + ]); + }); + }); + + describe('packValidationData', function () { + it('packs the validation data', async function () { + const authorizer = this.authorizer; + const validUntil = 0x12345678n; + const validAfter = 0x9abcdef0n; + const validationData = packValidationData(validAfter, validUntil, authorizer); + + expect( + this.utils.$packValidationData(ethers.Typed.address(authorizer), validAfter, validUntil), + ).to.eventually.equal(validationData); + }); + + it('packs the validation data (bool)', async function () { + const success = false; + const validUntil = 0x12345678n; + const validAfter = 0x9abcdef0n; + const validationData = packValidationData(validAfter, validUntil, false); + + expect(this.utils.$packValidationData(ethers.Typed.bool(success), validAfter, validUntil)).to.eventually.equal( + validationData, + ); + }); + + it('packing reproduced canonical values', async function () { + expect(this.utils.$packValidationData(ethers.Typed.address(ethers.ZeroAddress), 0n, 0n)).to.eventually.equal( + this.SIG_VALIDATION_SUCCESS, + ); + expect(this.utils.$packValidationData(ethers.Typed.bool(true), 0n, 0n)).to.eventually.equal( + this.SIG_VALIDATION_SUCCESS, + ); + expect(this.utils.$packValidationData(ethers.Typed.address(ADDRESS_ONE), 0n, 0n)).to.eventually.equal( + this.SIG_VALIDATION_FAILED, + ); + expect(this.utils.$packValidationData(ethers.Typed.bool(false), 0n, 0n)).to.eventually.equal( + this.SIG_VALIDATION_FAILED, + ); + }); + }); + + describe('combineValidationData', function () { + const validUntil1 = 0x12345678n; + const validAfter1 = 0x9abcdef0n; + const validUntil2 = 0x87654321n; + const validAfter2 = 0xabcdef90n; + + it('combines the validation data', async function () { + const validationData1 = packValidationData(validAfter1, validUntil1, ethers.ZeroAddress); + const validationData2 = packValidationData(validAfter2, validUntil2, ethers.ZeroAddress); + const expected = packValidationData(validAfter2, validUntil1, true); + + // check symmetry + expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected); + expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected); + }); + + for (const [authorizer1, authorizer2] of [ + [ethers.ZeroAddress, '0xbf023313b891fd6000544b79e353323aa94a4f29'], + ['0xbf023313b891fd6000544b79e353323aa94a4f29', ethers.ZeroAddress], + ]) { + it('returns SIG_VALIDATION_FAILURE if one of the authorizers is not address(0)', async function () { + const validationData1 = packValidationData(validAfter1, validUntil1, authorizer1); + const validationData2 = packValidationData(validAfter2, validUntil2, authorizer2); + const expected = packValidationData(validAfter2, validUntil1, false); + + // check symmetry + expect(this.utils.$combineValidationData(validationData1, validationData2)).to.eventually.equal(expected); + expect(this.utils.$combineValidationData(validationData2, validationData1)).to.eventually.equal(expected); + }); + } + }); + + describe('getValidationData', function () { + it('returns the validation data with valid validity range', async function () { + const aggregator = this.authorizer; + const validAfter = 0; + const validUntil = MAX_UINT48; + const validationData = packValidationData(validAfter, validUntil, aggregator); + + expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, false]); + }); + + it('returns the validation data with invalid validity range (expired)', async function () { + const aggregator = this.authorizer; + const validAfter = 0; + const validUntil = 1; + const validationData = packValidationData(validAfter, validUntil, aggregator); + + expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, true]); + }); + + it('returns the validation data with invalid validity range (not yet valid)', async function () { + const aggregator = this.authorizer; + const validAfter = MAX_UINT48; + const validUntil = MAX_UINT48; + const validationData = packValidationData(validAfter, validUntil, aggregator); + + expect(this.utils.$getValidationData(validationData)).to.eventually.deep.equal([aggregator.address, true]); + }); + + it('returns address(0) and false for validationData = 0', function () { + expect(this.utils.$getValidationData(0n)).to.eventually.deep.equal([ethers.ZeroAddress, false]); + }); + }); + + describe('hash', function () { + it('returns the operation hash with specified entrypoint and chainId', async function () { + const userOp = new UserOperation({ sender: this.sender, nonce: 1 }); + const chainId = await ethers.provider.getNetwork().then(({ chainId }) => chainId); + const otherChainId = 0xdeadbeef; + + // check that helper matches entrypoint logic + expect(this.entrypoint.getUserOpHash(userOp.packed)).to.eventually.equal(userOp.hash(this.entrypoint, chainId)); + + // check library against helper + expect(this.utils.$hash(userOp.packed, this.entrypoint, chainId)).to.eventually.equal( + userOp.hash(this.entrypoint, chainId), + ); + expect(this.utils.$hash(userOp.packed, this.entrypoint, otherChainId)).to.eventually.equal( + userOp.hash(this.entrypoint, otherChainId), + ); + }); + }); + + describe('userOp values', function () { + describe('intiCode', function () { + beforeEach(async function () { + this.userOp = new UserOperation({ + sender: this.sender, + nonce: 1, + verificationGas: 0x12345678n, + factory: this.factory, + factoryData: '0x123456', + }); + + this.emptyUserOp = new UserOperation({ + sender: this.sender, + nonce: 1, + }); + }); + + it('returns factory', async function () { + expect(this.utils.$factory(this.userOp.packed)).to.eventually.equal(this.factory); + expect(this.utils.$factory(this.emptyUserOp.packed)).to.eventually.equal(ethers.ZeroAddress); + }); + + it('returns factoryData', async function () { + expect(this.utils.$factoryData(this.userOp.packed)).to.eventually.equal('0x123456'); + expect(this.utils.$factoryData(this.emptyUserOp.packed)).to.eventually.equal('0x'); + }); + }); + + it('returns verificationGasLimit', async function () { + const userOp = new UserOperation({ sender: this.sender, nonce: 1, verificationGas: 0x12345678n }); + expect(this.utils.$verificationGasLimit(userOp.packed)).to.eventually.equal(userOp.verificationGas); + }); + + it('returns callGasLimit', async function () { + const userOp = new UserOperation({ sender: this.sender, nonce: 1, callGas: 0x12345678n }); + expect(this.utils.$callGasLimit(userOp.packed)).to.eventually.equal(userOp.callGas); + }); + + it('returns maxPriorityFeePerGas', async function () { + const userOp = new UserOperation({ sender: this.sender, nonce: 1, maxPriorityFee: 0x12345678n }); + expect(this.utils.$maxPriorityFeePerGas(userOp.packed)).to.eventually.equal(userOp.maxPriorityFee); + }); + + it('returns maxFeePerGas', async function () { + const userOp = new UserOperation({ sender: this.sender, nonce: 1, maxFeePerGas: 0x12345678n }); + expect(this.utils.$maxFeePerGas(userOp.packed)).to.eventually.equal(userOp.maxFeePerGas); + }); + + it('returns gasPrice', async function () { + const userOp = new UserOperation({ + sender: this.sender, + nonce: 1, + maxPriorityFee: 0x12345678n, + maxFeePerGas: 0x87654321n, + }); + expect(this.utils.$gasPrice(userOp.packed)).to.eventually.equal(userOp.maxPriorityFee); + }); + + describe('paymasterAndData', function () { + beforeEach(async function () { + this.userOp = new UserOperation({ + sender: this.sender, + nonce: 1, + paymaster: this.paymaster, + paymasterVerificationGasLimit: 0x12345678n, + paymasterPostOpGasLimit: 0x87654321n, + paymasterData: '0xbeefcafe', + }); + + this.emptyUserOp = new UserOperation({ + sender: this.sender, + nonce: 1, + }); + }); + + it('returns paymaster', async function () { + expect(this.utils.$paymaster(this.userOp.packed)).to.eventually.equal(this.userOp.paymaster); + expect(this.utils.$paymaster(this.emptyUserOp.packed)).to.eventually.equal(ethers.ZeroAddress); + }); + + it('returns verificationGasLimit', async function () { + expect(this.utils.$paymasterVerificationGasLimit(this.userOp.packed)).to.eventually.equal( + this.userOp.paymasterVerificationGasLimit, + ); + expect(this.utils.$paymasterVerificationGasLimit(this.emptyUserOp.packed)).to.eventually.equal(0n); + }); + + it('returns postOpGasLimit', async function () { + expect(this.utils.$paymasterPostOpGasLimit(this.userOp.packed)).to.eventually.equal( + this.userOp.paymasterPostOpGasLimit, + ); + expect(this.utils.$paymasterPostOpGasLimit(this.emptyUserOp.packed)).to.eventually.equal(0n); + }); + + it('returns data', async function () { + expect(this.utils.$paymasterData(this.userOp.packed)).to.eventually.equal(this.userOp.paymasterData); + expect(this.utils.$paymasterData(this.emptyUserOp.packed)).to.eventually.equal('0x'); + }); + }); + }); +}); diff --git a/test/account/utils/draft-ERC7579Utils.t.sol b/test/account/utils/draft-ERC7579Utils.t.sol new file mode 100644 index 00000000000..fdd4edf5958 --- /dev/null +++ b/test/account/utils/draft-ERC7579Utils.t.sol @@ -0,0 +1,421 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.24; + +// Parts of this test file are adapted from Adam Egyed (@adamegyed) proof of concept available at: +// https://github.com/adamegyed/erc7579-execute-vulnerability/tree/4589a30ff139e143d6c57183ac62b5c029217a90 +// +// solhint-disable no-console + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; +import {PackedUserOperation, IAccount, IEntryPoint} from "@openzeppelin/contracts/interfaces/draft-IERC4337.sol"; +import {ERC4337Utils} from "@openzeppelin/contracts/account/utils/draft-ERC4337Utils.sol"; +import {ERC7579Utils, Mode, CallType, ExecType, ModeSelector, ModePayload, Execution} from "@openzeppelin/contracts/account/utils/draft-ERC7579Utils.sol"; +import {Test, Vm, console} from "forge-std/Test.sol"; + +contract SampleAccount is IAccount, Ownable { + using ECDSA for *; + using MessageHashUtils for *; + using ERC4337Utils for *; + using ERC7579Utils for *; + + IEntryPoint internal constant ENTRY_POINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); + + event Log(bool duringValidation, Execution[] calls); + + error UnsupportedCallType(CallType callType); + + constructor(address initialOwner) Ownable(initialOwner) {} + + function validateUserOp( + PackedUserOperation calldata userOp, + bytes32 userOpHash, + uint256 missingAccountFunds + ) external override returns (uint256 validationData) { + require(msg.sender == address(ENTRY_POINT), "only from EP"); + // Check signature + if (userOpHash.toEthSignedMessageHash().recover(userOp.signature) != owner()) { + revert OwnableUnauthorizedAccount(_msgSender()); + } + + // If this is an execute call with a batch operation, log the call details from the calldata + if (bytes4(userOp.callData[0x00:0x04]) == this.execute.selector) { + (CallType callType, , , ) = Mode.wrap(bytes32(userOp.callData[0x04:0x24])).decodeMode(); + + if (callType == ERC7579Utils.CALLTYPE_BATCH) { + // Remove the selector + bytes calldata params = userOp.callData[0x04:]; + + // Use the same vulnerable assignment technique here, but assert afterwards that the checks aren't + // broken here by comparing to the result of `abi.decode(...)`. + bytes calldata executionCalldata; + assembly ("memory-safe") { + let dataptr := add(params.offset, calldataload(add(params.offset, 0x20))) + executionCalldata.offset := add(dataptr, 32) + executionCalldata.length := calldataload(dataptr) + } + // Check that this decoding step is done correctly. + (, bytes memory executionCalldataMemory) = abi.decode(params, (bytes32, bytes)); + + require( + keccak256(executionCalldata) == keccak256(executionCalldataMemory), + "decoding during validation failed" + ); + // Now, we know that we have `bytes calldata executionCalldata` as would be decoded by the solidity + // builtin decoder for the `execute` function. + + // This is where the vulnerability from ExecutionLib results in a different result between validation + // andexecution. + + emit Log(true, executionCalldata.decodeBatch()); + } + } + + if (missingAccountFunds > 0) { + (bool success, ) = payable(msg.sender).call{value: missingAccountFunds}(""); + success; // Silence warning. The entrypoint should validate the result. + } + + return ERC4337Utils.SIG_VALIDATION_SUCCESS; + } + + function execute(Mode mode, bytes calldata executionCalldata) external payable { + require(msg.sender == address(this) || msg.sender == address(ENTRY_POINT), "not auth"); + + (CallType callType, ExecType execType, , ) = mode.decodeMode(); + + // check if calltype is batch or single + if (callType == ERC7579Utils.CALLTYPE_SINGLE) { + executionCalldata.execSingle(execType); + } else if (callType == ERC7579Utils.CALLTYPE_BATCH) { + executionCalldata.execBatch(execType); + + emit Log(false, executionCalldata.decodeBatch()); + } else if (callType == ERC7579Utils.CALLTYPE_DELEGATECALL) { + executionCalldata.execDelegateCall(execType); + } else { + revert UnsupportedCallType(callType); + } + } +} + +contract ERC7579UtilsTest is Test { + using MessageHashUtils for *; + using ERC4337Utils for *; + using ERC7579Utils for *; + + IEntryPoint private constant ENTRYPOINT = IEntryPoint(payable(0x0000000071727De22E5E9d8BAf0edAc6f37da032)); + address private _owner; + uint256 private _ownerKey; + address private _account; + address private _beneficiary; + address private _recipient1; + address private _recipient2; + + constructor() { + vm.etch(0x0000000071727De22E5E9d8BAf0edAc6f37da032, vm.readFileBinary("test/bin/EntryPoint070.bytecode")); + vm.etch(0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C, vm.readFileBinary("test/bin/SenderCreator070.bytecode")); + + // signing key + (_owner, _ownerKey) = makeAddrAndKey("owner"); + + // ERC-4337 account + _account = address(new SampleAccount(_owner)); + vm.deal(_account, 1 ether); + + // other + _beneficiary = makeAddr("beneficiary"); + _recipient1 = makeAddr("recipient1"); + _recipient2 = makeAddr("recipient2"); + } + + function testExecuteBatchDecodeCorrectly() public { + Execution[] memory calls = new Execution[](2); + calls[0] = Execution({target: _recipient1, value: 1 wei, callData: ""}); + calls[1] = Execution({target: _recipient2, value: 1 wei, callData: ""}); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = PackedUserOperation({ + sender: _account, + nonce: 0, + initCode: "", + callData: abi.encodeCall( + SampleAccount.execute, + ( + ERC7579Utils.encodeMode( + ERC7579Utils.CALLTYPE_BATCH, + ERC7579Utils.EXECTYPE_DEFAULT, + ModeSelector.wrap(0x00), + ModePayload.wrap(0x00) + ), + ERC7579Utils.encodeBatch(calls) + ) + ), + accountGasLimits: _packGas(500_000, 500_000), + preVerificationGas: 0, + gasFees: _packGas(1, 1), + paymasterAndData: "", + signature: "" + }); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + _ownerKey, + this.hashUserOperation(userOps[0]).toEthSignedMessageHash() + ); + userOps[0].signature = abi.encodePacked(r, s, v); + + vm.recordLogs(); + ENTRYPOINT.handleOps(userOps, payable(_beneficiary)); + + assertEq(_recipient1.balance, 1 wei); + assertEq(_recipient2.balance, 1 wei); + + _collectAndPrintLogs(false); + } + + function testExecuteBatchDecodeEmpty() public { + bytes memory fakeCalls = abi.encodePacked( + uint256(1), // Length of execution[] + uint256(0x20), // offset + uint256(uint160(_recipient1)), // target + uint256(1), // value: 1 wei + uint256(0x60), // offset of data + uint256(0) // length of + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = PackedUserOperation({ + sender: _account, + nonce: 0, + initCode: "", + callData: abi.encodeCall( + SampleAccount.execute, + ( + ERC7579Utils.encodeMode( + ERC7579Utils.CALLTYPE_BATCH, + ERC7579Utils.EXECTYPE_DEFAULT, + ModeSelector.wrap(0x00), + ModePayload.wrap(0x00) + ), + abi.encodePacked( + uint256(0x70) // fake offset pointing to paymasterAndData + ) + ) + ), + accountGasLimits: _packGas(500_000, 500_000), + preVerificationGas: 0, + gasFees: _packGas(1, 1), + paymasterAndData: abi.encodePacked(address(0), fakeCalls), + signature: "" + }); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + _ownerKey, + this.hashUserOperation(userOps[0]).toEthSignedMessageHash() + ); + userOps[0].signature = abi.encodePacked(r, s, v); + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector) + ) + ); + ENTRYPOINT.handleOps(userOps, payable(_beneficiary)); + + _collectAndPrintLogs(false); + } + + function testExecuteBatchDecodeDifferent() public { + bytes memory execCallData = abi.encodePacked( + uint256(0x20), // offset pointing to the next segment + uint256(5), // Length of execution[] + uint256(0), // offset of calls[0], and target (!!) + uint256(0x20), // offset of calls[1], and value (!!) + uint256(0), // offset of calls[2], and rel offset of data (!!) + uint256(0) // offset of calls[3]. + // There is one more to read by the array length, but it's not present here. This will be + // paymasterAndData.length during validation, pointing to an all-zero call. + // During execution, the offset will be 0, pointing to a call with value. + ); + + PackedUserOperation[] memory userOps = new PackedUserOperation[](1); + userOps[0] = PackedUserOperation({ + sender: _account, + nonce: 0, + initCode: "", + callData: abi.encodePacked( + SampleAccount.execute.selector, + ERC7579Utils.encodeMode( + ERC7579Utils.CALLTYPE_BATCH, + ERC7579Utils.EXECTYPE_DEFAULT, + ModeSelector.wrap(0x00), + ModePayload.wrap(0x00) + ), + uint256(0x5c), // offset pointing to the next segment + uint224(type(uint224).max), // Padding to align the `bytes` types + // type(uint256).max, // unknown padding + uint256(execCallData.length), // Length of the data + execCallData + ), + accountGasLimits: _packGas(500_000, 500_000), + preVerificationGas: 0, + gasFees: _packGas(1, 1), + paymasterAndData: abi.encodePacked(uint256(0), uint256(0)), // padding length to create an offset + signature: "" + }); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign( + _ownerKey, + this.hashUserOperation(userOps[0]).toEthSignedMessageHash() + ); + userOps[0].signature = abi.encodePacked(r, s, v); + + vm.expectRevert( + abi.encodeWithSelector( + IEntryPoint.FailedOpWithRevert.selector, + 0, + "AA23 reverted", + abi.encodeWithSelector(ERC7579Utils.ERC7579DecodingError.selector) + ) + ); + ENTRYPOINT.handleOps(userOps, payable(_beneficiary)); + + _collectAndPrintLogs(true); + } + + function testDecodeBatch() public { + // BAD: buffer empty + vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector); + this.callDecodeBatch(""); + + // BAD: buffer too short + vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector); + this.callDecodeBatch(abi.encodePacked(uint128(0))); + + // GOOD + this.callDecodeBatch(abi.encode(0)); + // Note: Solidity also supports this even though it's odd. Offset 0 means array is at the same location, which + // is interpreted as an array of length 0, which doesn't require any more data + // solhint-disable-next-line var-name-mixedcase + uint256[] memory _1 = abi.decode(abi.encode(0), (uint256[])); + _1; + + // BAD: offset is out of bounds + vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector); + this.callDecodeBatch(abi.encode(1)); + + // GOOD + this.callDecodeBatch(abi.encode(32, 0)); + + // BAD: reported array length extends beyond bounds + vm.expectRevert(ERC7579Utils.ERC7579DecodingError.selector); + this.callDecodeBatch(abi.encode(32, 1)); + + // GOOD + this.callDecodeBatch(abi.encode(32, 1, 0)); + + // GOOD + // + // 0000000000000000000000000000000000000000000000000000000000000020 (32) offset + // 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length + // 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset + // 000000000000000000000000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (recipient) target for element #0 + // 000000000000000000000000000000000000000000000000000000000000002a (42) value for element #0 + // 0000000000000000000000000000000000000000000000000000000000000060 (96) offset to calldata for element #0 + // 000000000000000000000000000000000000000000000000000000000000000c (12) length of the calldata for element #0 + // 48656c6c6f20576f726c64210000000000000000000000000000000000000000 (..) buffer for the calldata for element #0 + assertEq( + bytes("Hello World!"), + this.callDecodeBatchAndGetFirstBytes( + abi.encode(32, 1, 32, _recipient1, 42, 96, 12, bytes12("Hello World!")) + ) + ); + + // This is invalid, the first element of the array points is out of bounds + // but we allow it past initial validation, because solidity will validate later when the bytes field is accessed + // + // 0000000000000000000000000000000000000000000000000000000000000020 (32) offset + // 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length + // 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset + // + bytes memory invalid = abi.encode(32, 1, 32); + this.callDecodeBatch(invalid); + vm.expectRevert(); + this.callDecodeBatchAndGetFirst(invalid); + + // this is invalid: the bytes field of the first element of the array is out of bounds + // but we allow it past initial validation, because solidity will validate later when the bytes field is accessed + // + // 0000000000000000000000000000000000000000000000000000000000000020 (32) offset + // 0000000000000000000000000000000000000000000000000000000000000001 ( 1) array length + // 0000000000000000000000000000000000000000000000000000000000000020 (32) element 0 offset + // 000000000000000000000000xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (recipient) target for element #0 + // 000000000000000000000000000000000000000000000000000000000000002a (42) value for element #0 + // 0000000000000000000000000000000000000000000000000000000000000060 (96) offset to calldata for element #0 + // + bytes memory invalidDeeply = abi.encode(32, 1, 32, _recipient1, 42, 96); + this.callDecodeBatch(invalidDeeply); + // Note that this is ok because we don't return the value. Returning it would introduce a check that would fails. + this.callDecodeBatchAndGetFirst(invalidDeeply); + vm.expectRevert(); + this.callDecodeBatchAndGetFirstBytes(invalidDeeply); + } + + function callDecodeBatch(bytes calldata executionCalldata) public pure { + ERC7579Utils.decodeBatch(executionCalldata); + } + + function callDecodeBatchAndGetFirst(bytes calldata executionCalldata) public pure { + ERC7579Utils.decodeBatch(executionCalldata)[0]; + } + + function callDecodeBatchAndGetFirstBytes(bytes calldata executionCalldata) public pure returns (bytes calldata) { + return ERC7579Utils.decodeBatch(executionCalldata)[0].callData; + } + + function hashUserOperation(PackedUserOperation calldata useroperation) public view returns (bytes32) { + return useroperation.hash(address(ENTRYPOINT), block.chainid); + } + + function _collectAndPrintLogs(bool includeTotalValue) internal { + Vm.Log[] memory logs = vm.getRecordedLogs(); + for (uint256 i = 0; i < logs.length; i++) { + if (logs[i].emitter == _account) { + _printDecodedCalls(logs[i].data, includeTotalValue); + } + } + } + + function _printDecodedCalls(bytes memory logData, bool includeTotalValue) internal pure { + (bool duringValidation, Execution[] memory calls) = abi.decode(logData, (bool, Execution[])); + + console.log( + string.concat( + "Batch execute contents, as read during ", + duringValidation ? "validation" : "execution", + ": " + ) + ); + console.log(" Execution[] length: %s", calls.length); + + uint256 totalValue = 0; + for (uint256 i = 0; i < calls.length; ++i) { + console.log(string.concat(" calls[", vm.toString(i), "].target = ", vm.toString(calls[i].target))); + console.log(string.concat(" calls[", vm.toString(i), "].value = ", vm.toString(calls[i].value))); + console.log(string.concat(" calls[", vm.toString(i), "].data = ", vm.toString(calls[i].callData))); + totalValue += calls[i].value; + } + + if (includeTotalValue) { + console.log(" Total value: %s", totalValue); + } + } + + function _packGas(uint256 upper, uint256 lower) internal pure returns (bytes32) { + return bytes32(uint256((upper << 128) | uint128(lower))); + } +} diff --git a/test/account/utils/draft-ERC7579Utils.test.js b/test/account/utils/draft-ERC7579Utils.test.js new file mode 100644 index 00000000000..7419c667b69 --- /dev/null +++ b/test/account/utils/draft-ERC7579Utils.test.js @@ -0,0 +1,353 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { + EXEC_TYPE_DEFAULT, + EXEC_TYPE_TRY, + encodeSingle, + encodeBatch, + encodeDelegate, + CALL_TYPE_CALL, + CALL_TYPE_BATCH, + encodeMode, +} = require('../../helpers/erc7579'); +const { selector } = require('../../helpers/methods'); + +const coder = ethers.AbiCoder.defaultAbiCoder(); + +const fixture = async () => { + const [sender] = await ethers.getSigners(); + const utils = await ethers.deployContract('$ERC7579Utils', { value: ethers.parseEther('1') }); + const utilsGlobal = await ethers.deployContract('$ERC7579UtilsGlobalMock'); + const target = await ethers.deployContract('CallReceiverMock'); + const anotherTarget = await ethers.deployContract('CallReceiverMock'); + return { utils, utilsGlobal, target, anotherTarget, sender }; +}; + +describe('ERC7579Utils', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('execSingle', function () { + it('calls the target with value', async function () { + const value = 0x012; + const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction')); + + await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT)).to.emit(this.target, 'MockFunctionCalled'); + + expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value); + }); + + it('calls the target with value and args', async function () { + const value = 0x432; + const data = encodeSingle( + this.target, + value, + this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']), + ); + + await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT)) + .to.emit(this.target, 'MockFunctionCalledWithArgs') + .withArgs(42, '0x1234'); + + expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value); + }); + + it('reverts when target reverts in default ExecType', async function () { + const value = 0x012; + const data = encodeSingle( + this.target, + value, + this.target.interface.encodeFunctionData('mockFunctionRevertsReason'), + ); + + await expect(this.utils.$execSingle(data, EXEC_TYPE_DEFAULT)).to.be.revertedWith('CallReceiverMock: reverting'); + }); + + it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () { + const value = 0x012; + const data = encodeSingle( + this.target, + value, + this.target.interface.encodeFunctionData('mockFunctionRevertsReason'), + ); + + await expect(this.utils.$execSingle(data, EXEC_TYPE_TRY)) + .to.emit(this.utils, 'ERC7579TryExecuteFail') + .withArgs( + CALL_TYPE_CALL, + ethers.solidityPacked( + ['bytes4', 'bytes'], + [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])], + ), + ); + }); + + it('reverts with an invalid exec type', async function () { + const value = 0x012; + const data = encodeSingle(this.target, value, this.target.interface.encodeFunctionData('mockFunction')); + + await expect(this.utils.$execSingle(data, '0x03')) + .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType') + .withArgs('0x03'); + }); + }); + + describe('execBatch', function () { + it('calls the targets with value', async function () { + const value1 = 0x012; + const value2 = 0x234; + const data = encodeBatch( + [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')], + [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')], + ); + + await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT)) + .to.emit(this.target, 'MockFunctionCalled') + .to.emit(this.anotherTarget, 'MockFunctionCalled'); + + expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1); + expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(value2); + }); + + it('calls the targets with value and args', async function () { + const value1 = 0x012; + const value2 = 0x234; + const data = encodeBatch( + [this.target, value1, this.target.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234'])], + [ + this.anotherTarget, + value2, + this.anotherTarget.interface.encodeFunctionData('mockFunctionWithArgs', [42, '0x1234']), + ], + ); + + await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT)) + .to.emit(this.target, 'MockFunctionCalledWithArgs') + .to.emit(this.anotherTarget, 'MockFunctionCalledWithArgs'); + + expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1); + expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(value2); + }); + + it('reverts when any target reverts in default ExecType', async function () { + const value1 = 0x012; + const value2 = 0x234; + const data = encodeBatch( + [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')], + [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')], + ); + + await expect(this.utils.$execBatch(data, EXEC_TYPE_DEFAULT)).to.be.revertedWith('CallReceiverMock: reverting'); + }); + + it('emits ERC7579TryExecuteFail event when any target reverts in try ExecType', async function () { + const value1 = 0x012; + const value2 = 0x234; + const data = encodeBatch( + [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')], + [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunctionRevertsReason')], + ); + + await expect(this.utils.$execBatch(data, EXEC_TYPE_TRY)) + .to.emit(this.utils, 'ERC7579TryExecuteFail') + .withArgs( + CALL_TYPE_BATCH, + ethers.solidityPacked( + ['bytes4', 'bytes'], + [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])], + ), + ); + + // Check balances + expect(ethers.provider.getBalance(this.target)).to.eventually.equal(value1); + expect(ethers.provider.getBalance(this.anotherTarget)).to.eventually.equal(0); + }); + + it('reverts with an invalid exec type', async function () { + const value1 = 0x012; + const value2 = 0x234; + const data = encodeBatch( + [this.target, value1, this.target.interface.encodeFunctionData('mockFunction')], + [this.anotherTarget, value2, this.anotherTarget.interface.encodeFunctionData('mockFunction')], + ); + + await expect(this.utils.$execBatch(data, '0x03')) + .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType') + .withArgs('0x03'); + }); + }); + + describe('execDelegateCall', function () { + it('delegate calls the target', async function () { + const slot = ethers.hexlify(ethers.randomBytes(32)); + const value = ethers.hexlify(ethers.randomBytes(32)); + const data = encodeDelegate( + this.target, + this.target.interface.encodeFunctionData('mockFunctionWritesStorage', [slot, value]), + ); + + expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(ethers.ZeroHash); + await this.utils.$execDelegateCall(data, EXEC_TYPE_DEFAULT); + expect(ethers.provider.getStorage(this.utils.target, slot)).to.eventually.equal(value); + }); + + it('reverts when target reverts in default ExecType', async function () { + const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')); + await expect(this.utils.$execDelegateCall(data, EXEC_TYPE_DEFAULT)).to.be.revertedWith( + 'CallReceiverMock: reverting', + ); + }); + + it('emits ERC7579TryExecuteFail event when target reverts in try ExecType', async function () { + const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunctionRevertsReason')); + await expect(this.utils.$execDelegateCall(data, EXEC_TYPE_TRY)) + .to.emit(this.utils, 'ERC7579TryExecuteFail') + .withArgs( + CALL_TYPE_CALL, + ethers.solidityPacked( + ['bytes4', 'bytes'], + [selector('Error(string)'), coder.encode(['string'], ['CallReceiverMock: reverting'])], + ), + ); + }); + + it('reverts with an invalid exec type', async function () { + const data = encodeDelegate(this.target, this.target.interface.encodeFunctionData('mockFunction')); + await expect(this.utils.$execDelegateCall(data, '0x03')) + .to.be.revertedWithCustomError(this.utils, 'ERC7579UnsupportedExecType') + .withArgs('0x03'); + }); + }); + + it('encodes Mode', async function () { + const callType = CALL_TYPE_BATCH; + const execType = EXEC_TYPE_TRY; + const selector = '0x12345678'; + const payload = ethers.toBeHex(0, 22); + + expect(this.utils.$encodeMode(callType, execType, selector, payload)).to.eventually.equal( + encodeMode({ + callType, + execType, + selector, + payload, + }), + ); + }); + + it('decodes Mode', async function () { + const callType = CALL_TYPE_BATCH; + const execType = EXEC_TYPE_TRY; + const selector = '0x12345678'; + const payload = ethers.toBeHex(0, 22); + + expect( + this.utils.$decodeMode( + encodeMode({ + callType, + execType, + selector, + payload, + }), + ), + ).to.eventually.deep.equal([callType, execType, selector, payload]); + }); + + it('encodes single', async function () { + const target = this.target; + const value = 0x123; + const data = '0x12345678'; + + expect(this.utils.$encodeSingle(target, value, data)).to.eventually.equal(encodeSingle(target, value, data)); + }); + + it('decodes single', async function () { + const target = this.target; + const value = 0x123; + const data = '0x12345678'; + + expect(this.utils.$decodeSingle(encodeSingle(target, value, data))).to.eventually.deep.equal([ + target.target, + value, + data, + ]); + }); + + it('encodes batch', async function () { + const entries = [ + [this.target, 0x123, '0x12345678'], + [this.anotherTarget, 0x456, '0x12345678'], + ]; + + expect(this.utils.$encodeBatch(entries)).to.eventually.equal(encodeBatch(...entries)); + }); + + it('decodes batch', async function () { + const entries = [ + [this.target.target, 0x123, '0x12345678'], + [this.anotherTarget.target, 0x456, '0x12345678'], + ]; + + expect(this.utils.$decodeBatch(encodeBatch(...entries))).to.eventually.deep.equal(entries); + }); + + it('encodes delegate', async function () { + const target = this.target; + const data = '0x12345678'; + + expect(this.utils.$encodeDelegate(target, data)).to.eventually.equal(encodeDelegate(target, data)); + }); + + it('decodes delegate', async function () { + const target = this.target; + const data = '0x12345678'; + + expect(this.utils.$decodeDelegate(encodeDelegate(target, data))).to.eventually.deep.equal([target.target, data]); + }); + + describe('global', function () { + describe('eqCallTypeGlobal', function () { + it('returns true if both call types are equal', async function () { + expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_BATCH, CALL_TYPE_BATCH)).to.eventually.be.true; + }); + + it('returns false if both call types are different', async function () { + expect(this.utilsGlobal.$eqCallTypeGlobal(CALL_TYPE_CALL, CALL_TYPE_BATCH)).to.eventually.be.false; + }); + }); + + describe('eqExecTypeGlobal', function () { + it('returns true if both exec types are equal', async function () { + expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_TRY, EXEC_TYPE_TRY)).to.eventually.be.true; + }); + + it('returns false if both exec types are different', async function () { + expect(this.utilsGlobal.$eqExecTypeGlobal(EXEC_TYPE_DEFAULT, EXEC_TYPE_TRY)).to.eventually.be.false; + }); + }); + + describe('eqModeSelectorGlobal', function () { + it('returns true if both selectors are equal', async function () { + expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x12345678')).to.eventually.be.true; + }); + + it('returns false if both selectors are different', async function () { + expect(this.utilsGlobal.$eqModeSelectorGlobal('0x12345678', '0x87654321')).to.eventually.be.false; + }); + }); + + describe('eqModePayloadGlobal', function () { + it('returns true if both payloads are equal', async function () { + expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(0, 22))).to.eventually.be + .true; + }); + + it('returns false if both payloads are different', async function () { + expect(this.utilsGlobal.$eqModePayloadGlobal(ethers.toBeHex(0, 22), ethers.toBeHex(1, 22))).to.eventually.be + .false; + }); + }); + }); +}); diff --git a/test/bin/EntryPoint070.abi b/test/bin/EntryPoint070.abi new file mode 100644 index 00000000000..3f3b1d6e5ff --- /dev/null +++ b/test/bin/EntryPoint070.abi @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"bool","name":"success","type":"bool"},{"internalType":"bytes","name":"ret","type":"bytes"}],"name":"DelegateAndRevert","type":"error"},{"inputs":[{"internalType":"uint256","name":"opIndex","type":"uint256"},{"internalType":"string","name":"reason","type":"string"}],"name":"FailedOp","type":"error"},{"inputs":[{"internalType":"uint256","name":"opIndex","type":"uint256"},{"internalType":"string","name":"reason","type":"string"},{"internalType":"bytes","name":"inner","type":"bytes"}],"name":"FailedOpWithRevert","type":"error"},{"inputs":[{"internalType":"bytes","name":"returnData","type":"bytes"}],"name":"PostOpReverted","type":"error"},{"inputs":[],"name":"ReentrancyGuardReentrantCall","type":"error"},{"inputs":[{"internalType":"address","name":"sender","type":"address"}],"name":"SenderAddressResult","type":"error"},{"inputs":[{"internalType":"address","name":"aggregator","type":"address"}],"name":"SignatureValidationFailed","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"address","name":"factory","type":"address"},{"indexed":false,"internalType":"address","name":"paymaster","type":"address"}],"name":"AccountDeployed","type":"event"},{"anonymous":false,"inputs":[],"name":"BeforeExecution","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"totalDeposit","type":"uint256"}],"name":"Deposited","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"nonce","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"revertReason","type":"bytes"}],"name":"PostOpRevertReason","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"aggregator","type":"address"}],"name":"SignatureAggregatorChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"totalStaked","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"unstakeDelaySec","type":"uint256"}],"name":"StakeLocked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"uint256","name":"withdrawTime","type":"uint256"}],"name":"StakeUnlocked","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"address","name":"withdrawAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"StakeWithdrawn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"paymaster","type":"address"},{"indexed":false,"internalType":"uint256","name":"nonce","type":"uint256"},{"indexed":false,"internalType":"bool","name":"success","type":"bool"},{"indexed":false,"internalType":"uint256","name":"actualGasCost","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"actualGasUsed","type":"uint256"}],"name":"UserOperationEvent","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"nonce","type":"uint256"}],"name":"UserOperationPrefundTooLow","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":false,"internalType":"uint256","name":"nonce","type":"uint256"},{"indexed":false,"internalType":"bytes","name":"revertReason","type":"bytes"}],"name":"UserOperationRevertReason","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"account","type":"address"},{"indexed":false,"internalType":"address","name":"withdrawAddress","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"Withdrawn","type":"event"},{"inputs":[{"internalType":"uint32","name":"unstakeDelaySec","type":"uint32"}],"name":"addStake","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"target","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"delegateAndRevert","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"depositTo","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"deposits","outputs":[{"internalType":"uint256","name":"deposit","type":"uint256"},{"internalType":"bool","name":"staked","type":"bool"},{"internalType":"uint112","name":"stake","type":"uint112"},{"internalType":"uint32","name":"unstakeDelaySec","type":"uint32"},{"internalType":"uint48","name":"withdrawTime","type":"uint48"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"getDepositInfo","outputs":[{"components":[{"internalType":"uint256","name":"deposit","type":"uint256"},{"internalType":"bool","name":"staked","type":"bool"},{"internalType":"uint112","name":"stake","type":"uint112"},{"internalType":"uint32","name":"unstakeDelaySec","type":"uint32"},{"internalType":"uint48","name":"withdrawTime","type":"uint48"}],"internalType":"struct IStakeManager.DepositInfo","name":"info","type":"tuple"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint192","name":"key","type":"uint192"}],"name":"getNonce","outputs":[{"internalType":"uint256","name":"nonce","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes","name":"initCode","type":"bytes"}],"name":"getSenderAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"bytes32","name":"accountGasLimits","type":"bytes32"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"bytes32","name":"gasFees","type":"bytes32"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct PackedUserOperation","name":"userOp","type":"tuple"}],"name":"getUserOpHash","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"components":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"bytes32","name":"accountGasLimits","type":"bytes32"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"bytes32","name":"gasFees","type":"bytes32"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct PackedUserOperation[]","name":"userOps","type":"tuple[]"},{"internalType":"contract IAggregator","name":"aggregator","type":"address"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct IEntryPoint.UserOpsPerAggregator[]","name":"opsPerAggregator","type":"tuple[]"},{"internalType":"address payable","name":"beneficiary","type":"address"}],"name":"handleAggregatedOps","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"bytes","name":"initCode","type":"bytes"},{"internalType":"bytes","name":"callData","type":"bytes"},{"internalType":"bytes32","name":"accountGasLimits","type":"bytes32"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"bytes32","name":"gasFees","type":"bytes32"},{"internalType":"bytes","name":"paymasterAndData","type":"bytes"},{"internalType":"bytes","name":"signature","type":"bytes"}],"internalType":"struct PackedUserOperation[]","name":"ops","type":"tuple[]"},{"internalType":"address payable","name":"beneficiary","type":"address"}],"name":"handleOps","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint192","name":"key","type":"uint192"}],"name":"incrementNonce","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"bytes","name":"callData","type":"bytes"},{"components":[{"components":[{"internalType":"address","name":"sender","type":"address"},{"internalType":"uint256","name":"nonce","type":"uint256"},{"internalType":"uint256","name":"verificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"callGasLimit","type":"uint256"},{"internalType":"uint256","name":"paymasterVerificationGasLimit","type":"uint256"},{"internalType":"uint256","name":"paymasterPostOpGasLimit","type":"uint256"},{"internalType":"uint256","name":"preVerificationGas","type":"uint256"},{"internalType":"address","name":"paymaster","type":"address"},{"internalType":"uint256","name":"maxFeePerGas","type":"uint256"},{"internalType":"uint256","name":"maxPriorityFeePerGas","type":"uint256"}],"internalType":"struct EntryPoint.MemoryUserOp","name":"mUserOp","type":"tuple"},{"internalType":"bytes32","name":"userOpHash","type":"bytes32"},{"internalType":"uint256","name":"prefund","type":"uint256"},{"internalType":"uint256","name":"contextOffset","type":"uint256"},{"internalType":"uint256","name":"preOpGas","type":"uint256"}],"internalType":"struct EntryPoint.UserOpInfo","name":"opInfo","type":"tuple"},{"internalType":"bytes","name":"context","type":"bytes"}],"name":"innerHandleOp","outputs":[{"internalType":"uint256","name":"actualGasCost","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"uint192","name":"","type":"uint192"}],"name":"nonceSequenceNumber","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes4","name":"interfaceId","type":"bytes4"}],"name":"supportsInterface","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"unlockStake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"withdrawAddress","type":"address"}],"name":"withdrawStake","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"withdrawAddress","type":"address"},{"internalType":"uint256","name":"withdrawAmount","type":"uint256"}],"name":"withdrawTo","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}] \ No newline at end of file diff --git a/test/bin/EntryPoint070.bytecode b/test/bin/EntryPoint070.bytecode new file mode 100644 index 00000000000..fce261af5dc Binary files /dev/null and b/test/bin/EntryPoint070.bytecode differ diff --git a/test/bin/SenderCreator070.abi b/test/bin/SenderCreator070.abi new file mode 100644 index 00000000000..0a1f0e4fbac --- /dev/null +++ b/test/bin/SenderCreator070.abi @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"bytes","name":"initCode","type":"bytes"}],"name":"createSender","outputs":[{"internalType":"address","name":"sender","type":"address"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/test/bin/SenderCreator070.bytecode b/test/bin/SenderCreator070.bytecode new file mode 100644 index 00000000000..8344c202819 Binary files /dev/null and b/test/bin/SenderCreator070.bytecode differ diff --git a/test/governance/Governor.t.sol b/test/governance/Governor.t.sol index 958461abb95..66b684d26e4 100644 --- a/test/governance/Governor.t.sol +++ b/test/governance/Governor.t.sol @@ -9,7 +9,11 @@ import {Governor} from "@openzeppelin/contracts/governance/Governor.sol"; contract GovernorInternalTest is Test, Governor { constructor() Governor("") {} - function testValidDescriptionForProposer(string memory description, address proposer, bool includeProposer) public { + function testValidDescriptionForProposer( + string memory description, + address proposer, + bool includeProposer + ) public view { if (includeProposer) { description = string.concat(description, "#proposer=", Strings.toHexString(proposer)); } @@ -20,7 +24,7 @@ contract GovernorInternalTest is Test, Governor { string memory description, address commitProposer, address actualProposer - ) public { + ) public view { vm.assume(commitProposer != actualProposer); description = string.concat(description, "#proposer=", Strings.toHexString(commitProposer)); assertFalse(_isValidDescriptionForProposer(actualProposer, description)); diff --git a/test/governance/extensions/GovernorCountingFractional.test.js b/test/governance/extensions/GovernorCountingFractional.test.js index 393dbad79d5..a46de210b93 100644 --- a/test/governance/extensions/GovernorCountingFractional.test.js +++ b/test/governance/extensions/GovernorCountingFractional.test.js @@ -27,7 +27,7 @@ describe('GovernorCountingFractional', function () { const [owner, proposer, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); const receiver = await ethers.deployContract('CallReceiverMock'); - const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]); const mock = await ethers.deployContract('$GovernorFractionalMock', [ name, // name votingDelay, // initialVotingDelay diff --git a/test/governance/extensions/GovernorCountingOverridable.test.js b/test/governance/extensions/GovernorCountingOverridable.test.js new file mode 100644 index 00000000000..32ee47439f3 --- /dev/null +++ b/test/governance/extensions/GovernorCountingOverridable.test.js @@ -0,0 +1,346 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers'); + +const { GovernorHelper } = require('../../helpers/governance'); +const { getDomain, OverrideBallot } = require('../../helpers/eip712'); +const { VoteType } = require('../../helpers/enums'); + +const TOKENS = [ + { Token: '$ERC20VotesExtendedMock', mode: 'blocknumber' }, + { Token: '$ERC20VotesExtendedTimestampMock', mode: 'timestamp' }, +]; + +const name = 'Override 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'); + +const signBallot = account => (contract, message) => + getDomain(contract).then(domain => account.signTypedData(domain, { OverrideBallot }, message)); + +describe('GovernorCountingOverridable', function () { + for (const { Token, mode } of TOKENS) { + const fixture = async () => { + const [owner, proposer, 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('$GovernorCountingOverridableMock', [ + name, // name + votingDelay, // initialVotingDelay + votingPeriod, // initialVotingPeriod + 0n, // initialProposalThreshold + token, // tokenAddress + 10n, // quorumNumeratorValue + ]); + + 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, 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 () { + expect(await this.mock.name()).to.equal(name); + expect(await this.mock.token()).to.equal(this.token); + expect(await this.mock.votingDelay()).to.equal(votingDelay); + expect(await this.mock.votingPeriod()).to.equal(votingPeriod); + expect(await this.mock.COUNTING_MODE()).to.equal('support=bravo,override&quorum=for,abstain&overridable=true'); + }); + + it('nominal is unaffected', async function () { + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + await this.helper.connect(this.voter1).vote({ support: VoteType.For, reason: 'This is nice' }); + await this.helper.connect(this.voter2).vote({ support: VoteType.For }); + await this.helper.connect(this.voter3).vote({ support: VoteType.Against }); + await this.helper.connect(this.voter4).vote({ support: VoteType.Abstain }); + await this.helper.waitForDeadline(); + await this.helper.execute(); + + expect(await this.mock.hasVoted(this.proposal.id, this.owner)).to.be.false; + expect(await this.mock.hasVoted(this.proposal.id, this.voter1)).to.be.true; + expect(await this.mock.hasVoted(this.proposal.id, this.voter2)).to.be.true; + expect(await ethers.provider.getBalance(this.mock)).to.equal(0n); + expect(await ethers.provider.getBalance(this.receiver)).to.equal(value); + }); + + describe('cast override vote', async function () { + beforeEach(async function () { + // user 1 -(delegate 10 tokens)-> user 2 + // user 2 -(delegate 7 tokens)-> user 2 + // user 3 -(delegate 5 tokens)-> user 1 + // user 4 -(delegate 2 tokens)-> user 2 + await this.token.connect(this.voter1).delegate(this.voter2); + await this.token.connect(this.voter3).delegate(this.voter1); + await this.token.connect(this.voter4).delegate(this.voter2); + await mine(); + + await this.helper.connect(this.proposer).propose(); + await this.helper.waitForSnapshot(); + }); + + it('override after delegate vote', async function () { + expect(await this.mock.hasVoted(this.helper.id, this.voter1)).to.be.false; + expect(await this.mock.hasVoted(this.helper.id, this.voter2)).to.be.false; + expect(await this.mock.hasVoted(this.helper.id, this.voter3)).to.be.false; + expect(await this.mock.hasVoted(this.helper.id, this.voter4)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter1)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter2)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter3)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter4)).to.be.false; + + // user 2 votes + + await expect(this.helper.connect(this.voter2).vote({ support: VoteType.For })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter2, this.helper.id, VoteType.For, ethers.parseEther('19'), ''); // 10 + 7 + 2 + + expect(await this.mock.proposalVotes(this.helper.id)).to.deep.eq( + [0, 19, 0].map(x => ethers.parseEther(x.toString())), + ); + expect(await this.mock.hasVoted(this.helper.id, this.voter2)).to.be.true; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter2)).to.be.false; + + // user 1 overrides after user 2 votes + + const reason = "disagree with user 2's decision"; + await expect(this.mock.connect(this.voter1).castOverrideVote(this.helper.id, VoteType.Against, reason)) + .to.emit(this.mock, 'OverrideVoteCast') + .withArgs(this.voter1, this.helper.id, VoteType.Against, ethers.parseEther('10'), reason) + .to.emit(this.mock, 'VoteReduced') + .withArgs(this.voter2, this.helper.id, VoteType.For, ethers.parseEther('10')); + + expect(await this.mock.proposalVotes(this.helper.id)).to.deep.eq( + [10, 9, 0].map(x => ethers.parseEther(x.toString())), + ); + expect(await this.mock.hasVoted(this.helper.id, this.voter1)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter1)).to.be.true; + }); + + it('override before delegate vote', async function () { + expect(await this.mock.hasVoted(this.helper.id, this.voter1)).to.be.false; + expect(await this.mock.hasVoted(this.helper.id, this.voter2)).to.be.false; + expect(await this.mock.hasVoted(this.helper.id, this.voter3)).to.be.false; + expect(await this.mock.hasVoted(this.helper.id, this.voter4)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter1)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter2)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter3)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter4)).to.be.false; + + // user 1 overrides before user 2 votes + + const reason = 'voter 2 is not voting'; + await expect(this.mock.connect(this.voter1).castOverrideVote(this.helper.id, VoteType.Against, reason)) + .to.emit(this.mock, 'OverrideVoteCast') + .withArgs(this.voter1, this.helper.id, VoteType.Against, ethers.parseEther('10'), reason) + .to.not.emit(this.mock, 'VoteReduced'); + + expect(await this.mock.proposalVotes(this.helper.id)).to.deep.eq( + [10, 0, 0].map(x => ethers.parseEther(x.toString())), + ); + expect(await this.mock.hasVoted(this.helper.id, this.voter1)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter1)).to.be.true; + + // user 2 votes + + await expect(this.helper.connect(this.voter2).vote({ support: VoteType.For })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter2, this.helper.id, VoteType.For, ethers.parseEther('9'), ''); // 7 + 2 + + expect(await this.mock.proposalVotes(this.helper.id)).to.deep.eq( + [10, 9, 0].map(x => ethers.parseEther(x.toString())), + ); + expect(await this.mock.hasVoted(this.helper.id, this.voter2)).to.be.true; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter2)).to.be.false; + }); + + it('override before and after delegate vote', async function () { + expect(await this.mock.hasVoted(this.helper.id, this.voter1)).to.be.false; + expect(await this.mock.hasVoted(this.helper.id, this.voter2)).to.be.false; + expect(await this.mock.hasVoted(this.helper.id, this.voter3)).to.be.false; + expect(await this.mock.hasVoted(this.helper.id, this.voter4)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter1)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter2)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter3)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter4)).to.be.false; + + // user 1 overrides before user 2 votes + + const reason = 'voter 2 is not voting'; + await expect(this.mock.connect(this.voter1).castOverrideVote(this.helper.id, VoteType.Against, reason)) + .to.emit(this.mock, 'OverrideVoteCast') + .withArgs(this.voter1, this.helper.id, VoteType.Against, ethers.parseEther('10'), reason) + .to.not.emit(this.mock, 'VoteReduced'); + + expect(await this.mock.proposalVotes(this.helper.id)).to.deep.eq( + [10, 0, 0].map(x => ethers.parseEther(x.toString())), + ); + expect(await this.mock.hasVoted(this.helper.id, this.voter1)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter1)).to.be.true; + + // user 2 votes + + await expect(this.helper.connect(this.voter2).vote({ support: VoteType.For })) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter2, this.helper.id, VoteType.For, ethers.parseEther('9'), ''); // 7 + 2 + + expect(await this.mock.proposalVotes(this.helper.id)).to.deep.eq( + [10, 9, 0].map(x => ethers.parseEther(x.toString())), + ); + expect(await this.mock.hasVoted(this.helper.id, this.voter2)).to.be.true; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter2)).to.be.false; + + // User 4 overrides after user 2 votes + + const reason2 = "disagree with user 2's decision"; + await expect(this.mock.connect(this.voter4).castOverrideVote(this.helper.id, VoteType.Abstain, reason2)) + .to.emit(this.mock, 'OverrideVoteCast') + .withArgs(this.voter4, this.helper.id, VoteType.Abstain, ethers.parseEther('2'), reason2) + .to.emit(this.mock, 'VoteReduced') + .withArgs(this.voter2, this.helper.id, VoteType.For, ethers.parseEther('2')); + + expect(await this.mock.proposalVotes(this.helper.id)).to.deep.eq( + [10, 7, 2].map(x => ethers.parseEther(x.toString())), + ); + expect(await this.mock.hasVoted(this.helper.id, this.voter4)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter4)).to.be.true; + }); + + it('vote (with delegated balance) and override (with self balance) are independent', async function () { + expect(await this.mock.proposalVotes(this.helper.id)).to.deep.eq( + [0, 0, 0].map(x => ethers.parseEther(x.toString())), + ); + expect(await this.mock.hasVoted(this.helper.id, this.voter1)).to.be.false; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter1)).to.be.false; + + // user 1 votes with delegated weight from user 3 + await expect(this.mock.connect(this.voter1).castVote(this.helper.id, VoteType.For)) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter1, this.helper.id, VoteType.For, ethers.parseEther('5'), ''); + + // user 1 cast an override vote with its own balance (delegated to user 2) + await expect(this.mock.connect(this.voter1).castOverrideVote(this.helper.id, VoteType.Against, '')) + .to.emit(this.mock, 'OverrideVoteCast') + .withArgs(this.voter1, this.helper.id, VoteType.Against, ethers.parseEther('10'), ''); + + expect(await this.mock.proposalVotes(this.helper.id)).to.deep.eq( + [10, 5, 0].map(x => ethers.parseEther(x.toString())), + ); + expect(await this.mock.hasVoted(this.helper.id, this.voter1)).to.be.true; + expect(await this.mock.hasVotedOverride(this.helper.id, this.voter1)).to.be.true; + }); + + it('can not override vote twice', async function () { + await expect(this.mock.connect(this.voter1).castOverrideVote(this.helper.id, VoteType.Against, '')) + .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') + .withArgs(this.voter1.address); + }); + + it('can not vote twice', async function () { + await expect(this.mock.connect(this.voter1).castVote(this.helper.id, VoteType.Against)) + .to.emit(this.mock, 'VoteCast') + .withArgs(this.voter1, this.helper.id, VoteType.Against, ethers.parseEther('5'), ''); + await expect(this.mock.connect(this.voter1).castVote(this.helper.id, VoteType.Abstain)) + .to.be.revertedWithCustomError(this.mock, 'GovernorAlreadyCastVote') + .withArgs(this.voter1.address); + }); + + describe('invalid vote type', function () { + it('override vote', async function () { + await expect( + this.mock.connect(this.voter1).castOverrideVote(this.helper.id, 3, ''), + ).to.be.revertedWithCustomError(this.mock, 'GovernorInvalidVoteType'); + }); + + it('traditional vote', async function () { + await expect(this.mock.connect(this.voter1).castVote(this.helper.id, 3)).to.be.revertedWithCustomError( + this.mock, + 'GovernorInvalidVoteType', + ); + }); + }); + + describe('by signature', function () { + it('EOA signature', async function () { + const nonce = await this.mock.nonces(this.voter1); + + await expect( + this.helper.overrideVote({ + support: VoteType.For, + voter: this.voter1.address, + nonce, + signature: signBallot(this.voter1), + }), + ) + .to.emit(this.mock, 'OverrideVoteCast') + .withArgs(this.voter1, this.helper.id, VoteType.For, ethers.parseEther('10'), ''); + + expect(await this.mock.hasVotedOverride(this.proposal.id, this.voter1)).to.be.true; + }); + + it('revert if signature does not match signer', async function () { + const nonce = await this.mock.nonces(this.voter1); + + const voteParams = { + support: VoteType.For, + voter: this.voter2.address, + nonce, + signature: signBallot(this.voter1), + }; + + await expect(this.helper.overrideVote(voteParams)) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature') + .withArgs(voteParams.voter); + }); + + it('revert if vote nonce is incorrect', async function () { + const nonce = await this.mock.nonces(this.voter1); + + const voteParams = { + support: VoteType.For, + voter: this.voter1.address, + nonce: nonce + 1n, + signature: signBallot(this.voter1), + }; + + await expect(this.helper.overrideVote(voteParams)) + .to.be.revertedWithCustomError(this.mock, 'GovernorInvalidSignature') + .withArgs(voteParams.voter); + }); + }); + }); + }); + } +}); diff --git a/test/governance/extensions/GovernorERC721.test.js b/test/governance/extensions/GovernorERC721.test.js index 1ae5508d7da..15910b8fe1f 100644 --- a/test/governance/extensions/GovernorERC721.test.js +++ b/test/governance/extensions/GovernorERC721.test.js @@ -29,7 +29,7 @@ describe('GovernorERC721', function () { const [owner, voter1, voter2, voter3, voter4] = await ethers.getSigners(); const receiver = await ethers.deployContract('CallReceiverMock'); - const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]); const mock = await ethers.deployContract('$GovernorMock', [ name, // name votingDelay, // initialVotingDelay diff --git a/test/governance/extensions/GovernorPreventLateQuorum.test.js b/test/governance/extensions/GovernorPreventLateQuorum.test.js index aac0e689815..761087aa945 100644 --- a/test/governance/extensions/GovernorPreventLateQuorum.test.js +++ b/test/governance/extensions/GovernorPreventLateQuorum.test.js @@ -28,7 +28,7 @@ describe('GovernorPreventLateQuorum', function () { const [owner, proposer, voter1, voter2, voter3, voter4] = await ethers.getSigners(); const receiver = await ethers.deployContract('CallReceiverMock'); - const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]); const mock = await ethers.deployContract('$GovernorPreventLateQuorumMock', [ name, // name votingDelay, // initialVotingDelay diff --git a/test/governance/extensions/GovernorStorage.test.js b/test/governance/extensions/GovernorStorage.test.js index ef56fa53e44..f079405b5a5 100644 --- a/test/governance/extensions/GovernorStorage.test.js +++ b/test/governance/extensions/GovernorStorage.test.js @@ -33,7 +33,7 @@ describe('GovernorStorage', function () { const [deployer, owner, proposer, voter1, voter2, voter3, voter4] = await ethers.getSigners(); const receiver = await ethers.deployContract('CallReceiverMock'); - const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]); const timelock = await ethers.deployContract('TimelockController', [delay, [], [], deployer]); const mock = await ethers.deployContract('$GovernorStorageMock', [ name, diff --git a/test/governance/extensions/GovernorTimelockAccess.test.js b/test/governance/extensions/GovernorTimelockAccess.test.js index c3d3b32684e..5eea6478abd 100644 --- a/test/governance/extensions/GovernorTimelockAccess.test.js +++ b/test/governance/extensions/GovernorTimelockAccess.test.js @@ -40,7 +40,7 @@ describe('GovernorTimelockAccess', function () { const manager = await ethers.deployContract('$AccessManager', [admin]); const receiver = await ethers.deployContract('$AccessManagedTarget', [manager]); - const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]); const mock = await ethers.deployContract('$GovernorTimelockAccessMock', [ name, votingDelay, diff --git a/test/governance/extensions/GovernorTimelockCompound.test.js b/test/governance/extensions/GovernorTimelockCompound.test.js index 545bf359d30..cd82481d500 100644 --- a/test/governance/extensions/GovernorTimelockCompound.test.js +++ b/test/governance/extensions/GovernorTimelockCompound.test.js @@ -28,7 +28,7 @@ describe('GovernorTimelockCompound', function () { const [deployer, owner, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); const receiver = await ethers.deployContract('CallReceiverMock'); - const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]); const predictGovernor = await deployer .getNonce() .then(nonce => ethers.getCreateAddress({ from: deployer.address, nonce: nonce + 1 })); diff --git a/test/governance/extensions/GovernorTimelockControl.test.js b/test/governance/extensions/GovernorTimelockControl.test.js index c1156a50993..507c7e27832 100644 --- a/test/governance/extensions/GovernorTimelockControl.test.js +++ b/test/governance/extensions/GovernorTimelockControl.test.js @@ -34,7 +34,7 @@ describe('GovernorTimelockControl', function () { const [deployer, owner, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); const receiver = await ethers.deployContract('CallReceiverMock'); - const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]); const timelock = await ethers.deployContract('TimelockController', [delay, [], [], deployer]); const mock = await ethers.deployContract('$GovernorTimelockControlMock', [ name, diff --git a/test/governance/extensions/GovernorVotesQuorumFraction.test.js b/test/governance/extensions/GovernorVotesQuorumFraction.test.js index 368e396f92b..99afd393173 100644 --- a/test/governance/extensions/GovernorVotesQuorumFraction.test.js +++ b/test/governance/extensions/GovernorVotesQuorumFraction.test.js @@ -29,7 +29,7 @@ describe('GovernorVotesQuorumFraction', function () { const receiver = await ethers.deployContract('CallReceiverMock'); - const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]); const mock = await ethers.deployContract('$GovernorMock', [name, votingDelay, votingPeriod, 0n, token, ratio]); await owner.sendTransaction({ to: mock, value }); diff --git a/test/governance/extensions/GovernorWithParams.test.js b/test/governance/extensions/GovernorWithParams.test.js index 37e15f5c2dd..db19bc61683 100644 --- a/test/governance/extensions/GovernorWithParams.test.js +++ b/test/governance/extensions/GovernorWithParams.test.js @@ -31,7 +31,7 @@ describe('GovernorWithParams', function () { const [owner, proposer, voter1, voter2, voter3, voter4, other] = await ethers.getSigners(); const receiver = await ethers.deployContract('CallReceiverMock'); - const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, version]); + const token = await ethers.deployContract(Token, [tokenName, tokenSymbol, tokenName, version]); const mock = await ethers.deployContract('$GovernorWithParamsMock', [name, token]); await owner.sendTransaction({ to: mock, value }); diff --git a/test/governance/utils/ERC6372.behavior.js b/test/governance/utils/ERC6372.behavior.js index abcae43c7f3..32f27b59a7c 100644 --- a/test/governance/utils/ERC6372.behavior.js +++ b/test/governance/utils/ERC6372.behavior.js @@ -1,21 +1,24 @@ const { expect } = require('chai'); - const time = require('../../helpers/time'); function shouldBehaveLikeERC6372(mode = 'blocknumber') { - describe('should implement ERC-6372', function () { + describe(`ERC-6372 behavior in ${mode} mode`, function () { beforeEach(async function () { this.mock = this.mock ?? this.token ?? this.votes; }); - it('clock is correct', async function () { - expect(await this.mock.clock()).to.equal(await time.clock[mode]()); + it('should have a correct clock value', async function () { + const currentClock = await this.mock.clock(); + const expectedClock = await time.clock[mode](); + expect(currentClock).to.equal(expectedClock, `Clock mismatch in ${mode} mode`); }); - it('CLOCK_MODE is correct', async function () { - const params = new URLSearchParams(await this.mock.CLOCK_MODE()); - expect(params.get('mode')).to.equal(mode); - expect(params.get('from')).to.equal(mode == 'blocknumber' ? 'default' : null); + it('should have the correct CLOCK_MODE parameters', async function () { + const clockModeParams = new URLSearchParams(await this.mock.CLOCK_MODE()); + const expectedFromValue = mode === 'blocknumber' ? 'default' : null; + + expect(clockModeParams.get('mode')).to.equal(mode, `Expected mode to be ${mode}`); + expect(clockModeParams.get('from')).to.equal(expectedFromValue, `Expected 'from' to be ${expectedFromValue}`); }); }); } diff --git a/test/governance/utils/VotesExtended.test.js b/test/governance/utils/VotesExtended.test.js new file mode 100644 index 00000000000..4a66ef2748c --- /dev/null +++ b/test/governance/utils/VotesExtended.test.js @@ -0,0 +1,152 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture, mine } = require('@nomicfoundation/hardhat-network-helpers'); + +const { sum } = require('../../helpers/math'); +const { zip } = require('../../helpers/iterate'); +const time = require('../../helpers/time'); + +const { shouldBehaveLikeVotes } = require('./Votes.behavior'); + +const MODES = { + blocknumber: '$VotesExtendedMock', + timestamp: '$VotesExtendedTimestampMock', +}; + +const AMOUNTS = [ethers.parseEther('10000000'), 10n, 20n]; + +describe('VotesExtended', function () { + for (const [mode, artifact] of Object.entries(MODES)) { + const fixture = async () => { + const accounts = await ethers.getSigners(); + + const amounts = Object.fromEntries( + zip( + accounts.slice(0, AMOUNTS.length).map(({ address }) => address), + AMOUNTS, + ), + ); + + const name = 'Override Votes'; + const version = '1'; + const votes = await ethers.deployContract(artifact, [name, version]); + + return { accounts, amounts, votes, name, version }; + }; + + describe(`vote with ${mode}`, function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeVotes(AMOUNTS, { mode, fungible: true }); + + it('starts with zero votes', async function () { + expect(await this.votes.getTotalSupply()).to.equal(0n); + }); + + describe('performs voting operations', function () { + beforeEach(async function () { + this.txs = []; + for (const [account, amount] of Object.entries(this.amounts)) { + this.txs.push(await this.votes.$_mint(account, amount)); + } + }); + + it('reverts if block number >= current block', async function () { + const lastTxTimepoint = await time.clockFromReceipt[mode](this.txs.at(-1)); + const clock = await this.votes.clock(); + await expect(this.votes.getPastTotalSupply(lastTxTimepoint)) + .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup') + .withArgs(lastTxTimepoint, clock); + }); + + it('delegates', async function () { + expect(await this.votes.getVotes(this.accounts[0])).to.equal(0n); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n); + expect(await this.votes.delegates(this.accounts[0])).to.equal(ethers.ZeroAddress); + expect(await this.votes.delegates(this.accounts[1])).to.equal(ethers.ZeroAddress); + + await this.votes.delegate(this.accounts[0], ethers.Typed.address(this.accounts[0])); + + expect(await this.votes.getVotes(this.accounts[0])).to.equal(this.amounts[this.accounts[0].address]); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n); + expect(await this.votes.delegates(this.accounts[0])).to.equal(this.accounts[0]); + expect(await this.votes.delegates(this.accounts[1])).to.equal(ethers.ZeroAddress); + + await this.votes.delegate(this.accounts[1], ethers.Typed.address(this.accounts[0])); + + expect(await this.votes.getVotes(this.accounts[0])).to.equal( + this.amounts[this.accounts[0].address] + this.amounts[this.accounts[1].address], + ); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(0n); + expect(await this.votes.delegates(this.accounts[0])).to.equal(this.accounts[0]); + expect(await this.votes.delegates(this.accounts[1])).to.equal(this.accounts[0]); + }); + + it('cross delegates', async function () { + await this.votes.delegate(this.accounts[0], ethers.Typed.address(this.accounts[1])); + await this.votes.delegate(this.accounts[1], ethers.Typed.address(this.accounts[0])); + + expect(await this.votes.getVotes(this.accounts[0])).to.equal(this.amounts[this.accounts[1].address]); + expect(await this.votes.getVotes(this.accounts[1])).to.equal(this.amounts[this.accounts[0].address]); + }); + + it('returns total amount of votes', async function () { + const totalSupply = sum(...Object.values(this.amounts)); + expect(await this.votes.getTotalSupply()).to.equal(totalSupply); + }); + }); + }); + + describe(`checkpoint delegates with ${mode}`, function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + it('checkpoint delegates', async function () { + const tx = await this.votes.delegate(this.accounts[0], ethers.Typed.address(this.accounts[1])); + const timepoint = await time.clockFromReceipt[mode](tx); + await mine(2); + + expect(await this.votes.getPastDelegate(this.accounts[0], timepoint - 1n)).to.equal(ethers.ZeroAddress); + expect(await this.votes.getPastDelegate(this.accounts[0], timepoint)).to.equal(this.accounts[1].address); + expect(await this.votes.getPastDelegate(this.accounts[0], timepoint + 1n)).to.equal(this.accounts[1].address); + }); + + it('reverts if current timepoint <= timepoint', async function () { + const tx = await this.votes.delegate(this.accounts[0], ethers.Typed.address(this.accounts[1])); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(this.votes.getPastDelegate(this.accounts[0], timepoint + 1n)) + .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup') + .withArgs(timepoint + 1n, timepoint); + }); + }); + + describe(`checkpoint balances with ${mode}`, function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + it('checkpoint balances', async function () { + const tx = await this.votes.$_mint(this.accounts[0].address, 100n); + const timepoint = await time.clockFromReceipt[mode](tx); + await mine(2); + + expect(await this.votes.getPastBalanceOf(this.accounts[0].address, timepoint - 1n)).to.equal(0n); + expect(await this.votes.getPastBalanceOf(this.accounts[0].address, timepoint)).to.equal(100n); + expect(await this.votes.getPastBalanceOf(this.accounts[0].address, timepoint + 1n)).to.equal(100n); + }); + + it('reverts if current timepoint <= timepoint', async function () { + const tx = await this.votes.$_mint(this.accounts[0].address, 100n); + const timepoint = await time.clockFromReceipt[mode](tx); + + await expect(this.votes.getPastBalanceOf(this.accounts[0], timepoint + 1n)) + .to.be.revertedWithCustomError(this.votes, 'ERC5805FutureLookup') + .withArgs(timepoint + 1n, timepoint); + }); + }); + } +}); diff --git a/test/helpers/chains.js b/test/helpers/chains.js new file mode 100644 index 00000000000..3711a81258e --- /dev/null +++ b/test/helpers/chains.js @@ -0,0 +1,109 @@ +// NOTE: this file defines some examples of CAIP-2 and CAIP-10 identifiers. +// The following listing does not pretend to be exhaustive or even accurate. It SHOULD NOT be used in production. + +const { ethers } = require('hardhat'); +const { mapValues } = require('./iterate'); + +// EVM (https://axelarscan.io/resources/chains?type=evm) +const ethereum = { + Ethereum: '1', + optimism: '10', + binance: '56', + Polygon: '137', + Fantom: '250', + fraxtal: '252', + filecoin: '314', + Moonbeam: '1284', + centrifuge: '2031', + kava: '2222', + mantle: '5000', + base: '8453', + immutable: '13371', + arbitrum: '42161', + celo: '42220', + Avalanche: '43114', + linea: '59144', + blast: '81457', + scroll: '534352', + aurora: '1313161554', +}; + +// Cosmos (https://axelarscan.io/resources/chains?type=cosmos) +const cosmos = { + Axelarnet: 'axelar-dojo-1', + osmosis: 'osmosis-1', + cosmoshub: 'cosmoshub-4', + juno: 'juno-1', + 'e-money': 'emoney-3', + injective: 'injective-1', + crescent: 'crescent-1', + kujira: 'kaiyo-1', + 'secret-snip': 'secret-4', + secret: 'secret-4', + sei: 'pacific-1', + stargaze: 'stargaze-1', + assetmantle: 'mantle-1', + fetch: 'fetchhub-4', + ki: 'kichain-2', + evmos: 'evmos_9001-2', + aura: 'xstaxy-1', + comdex: 'comdex-1', + persistence: 'core-1', + regen: 'regen-1', + umee: 'umee-1', + agoric: 'agoric-3', + xpla: 'dimension_37-1', + acre: 'acre_9052-1', + stride: 'stride-1', + carbon: 'carbon-1', + sommelier: 'sommelier-3', + neutron: 'neutron-1', + rebus: 'reb_1111-1', + archway: 'archway-1', + provenance: 'pio-mainnet-1', + ixo: 'ixo-5', + migaloo: 'migaloo-1', + teritori: 'teritori-1', + haqq: 'haqq_11235-1', + celestia: 'celestia', + ojo: 'agamotto', + chihuahua: 'chihuahua-1', + saga: 'ssc-1', + dymension: 'dymension_1100-1', + fxcore: 'fxcore', + c4e: 'perun-1', + bitsong: 'bitsong-2b', + nolus: 'pirin-1', + lava: 'lava-mainnet-1', + 'terra-2': 'phoenix-1', + terra: 'columbus-5', +}; + +const makeCAIP = ({ namespace, reference, account }) => ({ + namespace, + reference, + account, + caip2: `${namespace}:${reference}`, + caip10: `${namespace}:${reference}:${account}`, + toCaip10: other => `${namespace}:${reference}:${ethers.getAddress(other.target ?? other.address ?? other)}`, +}); + +module.exports = { + CHAINS: mapValues( + Object.assign( + mapValues(ethereum, reference => ({ + namespace: 'eip155', + reference, + account: ethers.Wallet.createRandom().address, + })), + mapValues(cosmos, reference => ({ + namespace: 'cosmos', + reference, + account: ethers.encodeBase58(ethers.randomBytes(32)), + })), + ), + makeCAIP, + ), + getLocalCAIP: account => + ethers.provider.getNetwork().then(({ chainId }) => makeCAIP({ namespace: 'eip155', reference: chainId, account })), +}; diff --git a/test/helpers/eip712-types.js b/test/helpers/eip712-types.js index b2b6ccf837b..d04969e442b 100644 --- a/test/helpers/eip712-types.js +++ b/test/helpers/eip712-types.js @@ -32,6 +32,13 @@ module.exports = mapValues( reason: 'string', params: 'bytes', }, + OverrideBallot: { + proposalId: 'uint256', + support: 'uint8', + voter: 'address', + nonce: 'uint256', + reason: 'string', + }, Delegation: { delegatee: 'address', nonce: 'uint256', diff --git a/test/helpers/erc4337-entrypoint.js b/test/helpers/erc4337-entrypoint.js new file mode 100644 index 00000000000..aba49f4c458 --- /dev/null +++ b/test/helpers/erc4337-entrypoint.js @@ -0,0 +1,31 @@ +const { ethers } = require('hardhat'); +const { setCode } = require('@nomicfoundation/hardhat-network-helpers'); +const fs = require('fs'); +const path = require('path'); + +const INSTANCES = { + entrypoint: { + address: '0x0000000071727De22E5E9d8BAf0edAc6f37da032', + abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../bin/EntryPoint070.abi'), 'utf-8')), + bytecode: fs.readFileSync(path.resolve(__dirname, '../bin/EntryPoint070.bytecode'), 'hex'), + }, + sendercreator: { + address: '0xEFC2c1444eBCC4Db75e7613d20C6a62fF67A167C', + abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../bin/SenderCreator070.abi'), 'utf-8')), + bytecode: fs.readFileSync(path.resolve(__dirname, '../bin/SenderCreator070.bytecode'), 'hex'), + }, +}; + +function deployEntrypoint() { + return Promise.all( + Object.entries(INSTANCES).map(([name, { address, abi, bytecode }]) => + setCode(address, '0x' + bytecode.replace(/0x/, '')) + .then(() => ethers.getContractAt(abi, address)) + .then(instance => ({ [name]: instance })), + ), + ).then(namedInstances => Object.assign(...namedInstances)); +} + +module.exports = { + deployEntrypoint, +}; diff --git a/test/helpers/erc4337.js b/test/helpers/erc4337.js new file mode 100644 index 00000000000..50a3b4a043b --- /dev/null +++ b/test/helpers/erc4337.js @@ -0,0 +1,111 @@ +const { ethers } = require('hardhat'); + +const SIG_VALIDATION_SUCCESS = '0x0000000000000000000000000000000000000000'; +const SIG_VALIDATION_FAILURE = '0x0000000000000000000000000000000000000001'; + +function getAddress(account) { + return account.target ?? account.address ?? account; +} + +function pack(left, right) { + return ethers.solidityPacked(['uint128', 'uint128'], [left, right]); +} + +function packValidationData(validAfter, validUntil, authorizer) { + return ethers.solidityPacked( + ['uint48', 'uint48', 'address'], + [ + validAfter, + validUntil, + typeof authorizer == 'boolean' + ? authorizer + ? SIG_VALIDATION_SUCCESS + : SIG_VALIDATION_FAILURE + : getAddress(authorizer), + ], + ); +} + +function packInitCode(factory, factoryData) { + return ethers.solidityPacked(['address', 'bytes'], [getAddress(factory), factoryData]); +} + +function packPaymasterAndData(paymaster, paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData) { + return ethers.solidityPacked( + ['address', 'uint128', 'uint128', 'bytes'], + [getAddress(paymaster), paymasterVerificationGasLimit, paymasterPostOpGasLimit, paymasterData], + ); +} + +/// Represent one user operation +class UserOperation { + constructor(params) { + this.sender = getAddress(params.sender); + this.nonce = params.nonce; + this.factory = params.factory ?? undefined; + this.factoryData = params.factoryData ?? '0x'; + this.callData = params.callData ?? '0x'; + this.verificationGas = params.verificationGas ?? 10_000_000n; + this.callGas = params.callGas ?? 100_000n; + this.preVerificationGas = params.preVerificationGas ?? 100_000n; + this.maxPriorityFee = params.maxPriorityFee ?? 100_000n; + this.maxFeePerGas = params.maxFeePerGas ?? 100_000n; + this.paymaster = params.paymaster ?? undefined; + this.paymasterVerificationGasLimit = params.paymasterVerificationGasLimit ?? 0n; + this.paymasterPostOpGasLimit = params.paymasterPostOpGasLimit ?? 0n; + this.paymasterData = params.paymasterData ?? '0x'; + this.signature = params.signature ?? '0x'; + } + + get packed() { + return { + sender: this.sender, + nonce: this.nonce, + initCode: this.factory ? packInitCode(this.factory, this.factoryData) : '0x', + callData: this.callData, + accountGasLimits: pack(this.verificationGas, this.callGas), + preVerificationGas: this.preVerificationGas, + gasFees: pack(this.maxPriorityFee, this.maxFeePerGas), + paymasterAndData: this.paymaster + ? packPaymasterAndData( + this.paymaster, + this.paymasterVerificationGasLimit, + this.paymasterPostOpGasLimit, + this.paymasterData, + ) + : '0x', + signature: this.signature, + }; + } + + hash(entrypoint, chainId) { + const p = this.packed; + const h = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ['address', 'uint256', 'bytes32', 'bytes32', 'uint256', 'uint256', 'uint256', 'uint256'], + [ + p.sender, + p.nonce, + ethers.keccak256(p.initCode), + ethers.keccak256(p.callData), + p.accountGasLimits, + p.preVerificationGas, + p.gasFees, + ethers.keccak256(p.paymasterAndData), + ], + ), + ); + return ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode(['bytes32', 'address', 'uint256'], [h, getAddress(entrypoint), chainId]), + ); + } +} + +module.exports = { + SIG_VALIDATION_SUCCESS, + SIG_VALIDATION_FAILURE, + packValidationData, + packInitCode, + packPaymasterAndData, + UserOperation, +}; diff --git a/test/helpers/erc7579.js b/test/helpers/erc7579.js new file mode 100644 index 00000000000..6c3b4759b73 --- /dev/null +++ b/test/helpers/erc7579.js @@ -0,0 +1,58 @@ +const { ethers } = require('hardhat'); + +const MODULE_TYPE_VALIDATOR = 1; +const MODULE_TYPE_EXECUTOR = 2; +const MODULE_TYPE_FALLBACK = 3; +const MODULE_TYPE_HOOK = 4; + +const EXEC_TYPE_DEFAULT = '0x00'; +const EXEC_TYPE_TRY = '0x01'; + +const CALL_TYPE_CALL = '0x00'; +const CALL_TYPE_BATCH = '0x01'; +const CALL_TYPE_DELEGATE = '0xff'; + +const encodeMode = ({ + callType = '0x00', + execType = '0x00', + selector = '0x00000000', + payload = '0x00000000000000000000000000000000000000000000', +} = {}) => + ethers.solidityPacked( + ['bytes1', 'bytes1', 'bytes4', 'bytes4', 'bytes22'], + [callType, execType, '0x00000000', selector, payload], + ); + +const encodeSingle = (target, value = 0n, data = '0x') => + ethers.solidityPacked(['address', 'uint256', 'bytes'], [target.target ?? target.address ?? target, value, data]); + +const encodeBatch = (...entries) => + ethers.AbiCoder.defaultAbiCoder().encode( + ['(address,uint256,bytes)[]'], + [ + entries.map(entry => + Array.isArray(entry) + ? [entry[0].target ?? entry[0].address ?? entry[0], entry[1] ?? 0n, entry[2] ?? '0x'] + : [entry.target.target ?? entry.target.address ?? entry.target, entry.value ?? 0n, entry.data ?? '0x'], + ), + ], + ); + +const encodeDelegate = (target, data = '0x') => + ethers.solidityPacked(['address', 'bytes'], [target.target ?? target.address ?? target, data]); + +module.exports = { + MODULE_TYPE_VALIDATOR, + MODULE_TYPE_EXECUTOR, + MODULE_TYPE_FALLBACK, + MODULE_TYPE_HOOK, + EXEC_TYPE_DEFAULT, + EXEC_TYPE_TRY, + CALL_TYPE_CALL, + CALL_TYPE_BATCH, + CALL_TYPE_DELEGATE, + encodeMode, + encodeSingle, + encodeBatch, + encodeDelegate, +}; diff --git a/test/helpers/governance.js b/test/helpers/governance.js index dce5927b730..540967af49d 100644 --- a/test/helpers/governance.js +++ b/test/helpers/governance.js @@ -128,6 +128,23 @@ class GovernorHelper { return await this.governor[method](...args); } + async overrideVote(vote = {}) { + let method = 'castOverrideVote'; + let args = [this.id, vote.support]; + + vote.reason = vote.reason ?? ''; + + if (vote.signature) { + let message = this.forgeMessage(vote); + message.reason = message.reason ?? ''; + const sign = await vote.signature(this.governor, message); + method = 'castOverrideVoteBySig'; + args.push(vote.voter, vote.reason ?? '', sign); + } + + return await this.governor[method](...args); + } + /// Clock helpers async waitForSnapshot(offset = 0n) { const timepoint = await this.governor.proposalSnapshot(this.id); diff --git a/test/helpers/iterate.js b/test/helpers/iterate.js index ef4526e133f..8c8e9649dea 100644 --- a/test/helpers/iterate.js +++ b/test/helpers/iterate.js @@ -13,11 +13,11 @@ module.exports = { // Range from start to end in increment // Example: range(17,42,7) → [17,24,31,38] range: (start, stop = undefined, step = 1) => { - if (!stop) { + if (stop == undefined) { stop = start; start = 0; } - return start < stop ? Array.from({ length: Math.ceil((stop - start) / step) }, (_, i) => start + i * step) : []; + return start < stop ? Array.from({ length: (stop - start + step - 1) / step }, (_, i) => start + i * step) : []; }, // Unique elements, with an optional getter function @@ -30,7 +30,12 @@ module.exports = { // ================================================ Object helpers ================================================= - // Create a new object by mapping the values through a function, keeping the keys + // Create a new object by mapping the values through a function, keeping the keys. Second function can be used to pre-filter entries // Example: mapValues({a:1,b:2,c:3}, x => x**2) → {a:1,b:4,c:9} - mapValues: (obj, fn) => Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, fn(v)])), + mapValues: (obj, fn, fn2 = () => true) => + Object.fromEntries( + Object.entries(obj) + .filter(fn2) + .map(([k, v]) => [k, fn(v)]), + ), }; diff --git a/test/helpers/storage.js b/test/helpers/storage.js index a75a3060d4b..466cbb10c56 100644 --- a/test/helpers/storage.js +++ b/test/helpers/storage.js @@ -26,7 +26,7 @@ const upgradeableSlot = (contractName, offset) => { // Try to get the artifact paths, will throw if it doesn't exist artifacts._getArtifactPathSync(`${contractName}Upgradeable`); return offset + ethers.toBigInt(erc7201Slot(erc7201format(contractName))); - } catch (_) { + } catch { return offset; } }; diff --git a/test/metatx/ERC2771Context.test.js b/test/metatx/ERC2771Context.test.js index 15da61dad1e..93354d0fc8b 100644 --- a/test/metatx/ERC2771Context.test.js +++ b/test/metatx/ERC2771Context.test.js @@ -11,7 +11,7 @@ const { shouldBehaveLikeRegularContext } = require('../utils/Context.behavior'); async function fixture() { const [sender, other] = await ethers.getSigners(); - const forwarder = await ethers.deployContract('ERC2771Forwarder', []); + const forwarder = await ethers.deployContract('ERC2771Forwarder', ['ERC2771Forwarder']); const forwarderAsSigner = await impersonate(forwarder.target); const context = await ethers.deployContract('ERC2771ContextMock', [forwarder]); const domain = await getDomain(forwarder); diff --git a/test/metatx/ERC2771Forwarder.t.sol b/test/metatx/ERC2771Forwarder.t.sol index d69b4750a39..e6baac6f030 100644 --- a/test/metatx/ERC2771Forwarder.t.sol +++ b/test/metatx/ERC2771Forwarder.t.sol @@ -5,21 +5,24 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {ERC2771Forwarder} from "@openzeppelin/contracts/metatx/ERC2771Forwarder.sol"; import {CallReceiverMockTrustingForwarder, CallReceiverMock} from "@openzeppelin/contracts/mocks/CallReceiverMock.sol"; - -struct ForwardRequest { - address from; - address to; - uint256 value; - uint256 gas; - uint256 nonce; - uint48 deadline; - bytes data; +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; + +enum TamperType { + FROM, + TO, + VALUE, + DATA, + SIGNATURE } contract ERC2771ForwarderMock is ERC2771Forwarder { constructor(string memory name) ERC2771Forwarder(name) {} - function structHash(ForwardRequest calldata request) external view returns (bytes32) { + function forwardRequestStructHash( + ERC2771Forwarder.ForwardRequestData calldata request, + uint256 nonce + ) external view returns (bytes32) { return _hashTypedDataV4( keccak256( @@ -29,7 +32,7 @@ contract ERC2771ForwarderMock is ERC2771Forwarder { request.to, request.value, request.gas, - request.nonce, + nonce, request.deadline, keccak256(request.data) ) @@ -39,127 +42,238 @@ contract ERC2771ForwarderMock is ERC2771Forwarder { } contract ERC2771ForwarderTest is Test { + using ECDSA for bytes32; + ERC2771ForwarderMock internal _erc2771Forwarder; CallReceiverMockTrustingForwarder internal _receiver; - uint256 internal _signerPrivateKey; - uint256 internal _relayerPrivateKey; - - address internal _signer; - address internal _relayer; + uint256 internal _signerPrivateKey = 0xA11CE; + address internal _signer = vm.addr(_signerPrivateKey); uint256 internal constant _MAX_ETHER = 10_000_000; // To avoid overflow function setUp() public { _erc2771Forwarder = new ERC2771ForwarderMock("ERC2771Forwarder"); _receiver = new CallReceiverMockTrustingForwarder(address(_erc2771Forwarder)); + } - _signerPrivateKey = 0xA11CE; - _relayerPrivateKey = 0xB0B; - - _signer = vm.addr(_signerPrivateKey); - _relayer = vm.addr(_relayerPrivateKey); + // Forge a new ForwardRequestData + function _forgeRequestData() private view returns (ERC2771Forwarder.ForwardRequestData memory) { + return + _forgeRequestData({ + value: 0, + deadline: uint48(block.timestamp + 1), + data: abi.encodeCall(CallReceiverMock.mockFunction, ()) + }); } function _forgeRequestData( uint256 value, - uint256 nonce, uint48 deadline, bytes memory data ) private view returns (ERC2771Forwarder.ForwardRequestData memory) { - ForwardRequest memory request = ForwardRequest({ - from: _signer, - to: address(_receiver), - value: value, - gas: 30000, - nonce: nonce, - deadline: deadline, - data: data - }); - - bytes32 digest = _erc2771Forwarder.structHash(request); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPrivateKey, digest); - bytes memory signature = abi.encodePacked(r, s, v); - return ERC2771Forwarder.ForwardRequestData({ - from: request.from, - to: request.to, - value: request.value, - gas: request.gas, - deadline: request.deadline, - data: request.data, - signature: signature + from: _signer, + to: address(_receiver), + value: value, + gas: 30000, + deadline: deadline, + data: data, + signature: "" }); } - function testExecuteAvoidsETHStuck(uint256 initialBalance, uint256 value, bool targetReverts) public { - initialBalance = bound(initialBalance, 0, _MAX_ETHER); - value = bound(value, 0, _MAX_ETHER); + // Sign a ForwardRequestData (in place) for a given nonce. Also returns it for convenience. + function _signRequestData( + ERC2771Forwarder.ForwardRequestData memory request, + uint256 nonce + ) private view returns (ERC2771Forwarder.ForwardRequestData memory) { + bytes32 digest = _erc2771Forwarder.forwardRequestStructHash(request, nonce); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_signerPrivateKey, digest); + request.signature = abi.encodePacked(r, s, v); + return request; + } - vm.deal(address(_erc2771Forwarder), initialBalance); + // Tamper a ForwardRequestData (in place). Also returns it for convenience. + function _tamperRequestData( + ERC2771Forwarder.ForwardRequestData memory request, + TamperType tamper + ) private returns (ERC2771Forwarder.ForwardRequestData memory) { + if (tamper == TamperType.FROM) request.from = vm.randomAddress(); + else if (tamper == TamperType.TO) request.to = vm.randomAddress(); + else if (tamper == TamperType.VALUE) request.value = vm.randomUint(); + else if (tamper == TamperType.DATA) request.data = vm.randomBytes(4); + else if (tamper == TamperType.SIGNATURE) request.signature = vm.randomBytes(65); + + return request; + } - uint256 nonce = _erc2771Forwarder.nonces(_signer); + // Predict the revert error for a tampered request, and expect it is emitted. + function _tamperedExpectRevert( + ERC2771Forwarder.ForwardRequestData memory request, + TamperType tamper, + uint256 nonce + ) private returns (ERC2771Forwarder.ForwardRequestData memory) { + if (tamper == TamperType.FROM) nonce = _erc2771Forwarder.nonces(request.from); + + // predict revert + if (tamper == TamperType.TO) { + vm.expectRevert( + abi.encodeWithSelector( + ERC2771Forwarder.ERC2771UntrustfulTarget.selector, + request.to, + address(_erc2771Forwarder) + ) + ); + } else { + (address recovered, , ) = _erc2771Forwarder.forwardRequestStructHash(request, nonce).tryRecover( + request.signature + ); + vm.expectRevert( + abi.encodeWithSelector(ERC2771Forwarder.ERC2771ForwarderInvalidSigner.selector, recovered, request.from) + ); + } + return request; + } - vm.deal(address(this), value); + function testExecuteAvoidsETHStuck(uint256 initialBalance, uint256 value, bool targetReverts) public { + initialBalance = bound(initialBalance, 0, _MAX_ETHER); + value = bound(value, 0, _MAX_ETHER); - ERC2771Forwarder.ForwardRequestData memory requestData = _forgeRequestData({ + // create and sign request + ERC2771Forwarder.ForwardRequestData memory request = _forgeRequestData({ value: value, - nonce: nonce, deadline: uint48(block.timestamp + 1), data: targetReverts ? abi.encodeCall(CallReceiverMock.mockFunctionRevertsNoReason, ()) : abi.encodeCall(CallReceiverMock.mockFunction, ()) }); + _signRequestData(request, _erc2771Forwarder.nonces(_signer)); - if (targetReverts) { - vm.expectRevert(); - } + vm.deal(address(_erc2771Forwarder), initialBalance); + vm.deal(address(this), request.value); + + if (targetReverts) vm.expectRevert(); + _erc2771Forwarder.execute{value: value}(request); - _erc2771Forwarder.execute{value: value}(requestData); assertEq(address(_erc2771Forwarder).balance, initialBalance); } function testExecuteBatchAvoidsETHStuck(uint256 initialBalance, uint256 batchSize, uint256 value) public { + uint256 seed = uint256(keccak256(abi.encodePacked(initialBalance, batchSize, value))); + batchSize = bound(batchSize, 1, 10); initialBalance = bound(initialBalance, 0, _MAX_ETHER); value = bound(value, 0, _MAX_ETHER); + address refundReceiver = address(0xebe); + uint256 refundExpected = 0; + uint256 nonce = _erc2771Forwarder.nonces(_signer); + + // create an sign array or 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; + + requests[i] = _forgeRequestData({ + value: value, + deadline: uint48(block.timestamp + 1), + data: failure + ? abi.encodeCall(CallReceiverMock.mockFunctionRevertsNoReason, ()) + : abi.encodeCall(CallReceiverMock.mockFunction, ()) + }); + _signRequestData(requests[i], nonce + i); + + refundExpected += SafeCast.toUint(failure) * value; + } + + // distribute ether vm.deal(address(_erc2771Forwarder), initialBalance); + vm.deal(address(this), value * batchSize); + + // execute batch + _erc2771Forwarder.executeBatch{value: value * batchSize}(requests, payable(refundReceiver)); + + // check balances + assertEq(address(_erc2771Forwarder).balance, initialBalance); + assertEq(refundReceiver.balance, refundExpected); + } + + function testVerifyTamperedValues(uint8 _tamper) public { + TamperType tamper = _asTamper(_tamper); + + // create request, sign, tamper + ERC2771Forwarder.ForwardRequestData memory request = _forgeRequestData(); + _signRequestData(request, 0); + _tamperRequestData(request, tamper); + + // should not pass verification + assertFalse(_erc2771Forwarder.verify(request)); + } + + function testExecuteTamperedValues(uint8 _tamper) public { + TamperType tamper = _asTamper(_tamper); + + // create request, sign, tamper, expect execution revert + ERC2771Forwarder.ForwardRequestData memory request = _forgeRequestData(); + _signRequestData(request, 0); + _tamperRequestData(request, tamper); + _tamperedExpectRevert(request, tamper, 0); + + vm.deal(address(this), request.value); + _erc2771Forwarder.execute{value: request.value}(request); + } + + function testExecuteBatchTamperedValuesZeroReceiver(uint8 _tamper) public { + TamperType tamper = _asTamper(_tamper); uint256 nonce = _erc2771Forwarder.nonces(_signer); - ERC2771Forwarder.ForwardRequestData[] memory batchRequestDatas = new ERC2771Forwarder.ForwardRequestData[]( - batchSize - ); + // create an sign array or requests + ERC2771Forwarder.ForwardRequestData[] memory requests = new ERC2771Forwarder.ForwardRequestData[](3); + for (uint256 i = 0; i < requests.length; ++i) { + requests[i] = _forgeRequestData({ + value: 0, + deadline: uint48(block.timestamp + 1), + data: abi.encodeCall(CallReceiverMock.mockFunction, ()) + }); + _signRequestData(requests[i], nonce + i); + } - uint256 expectedRefund; + // tamper with request[1] and expect execution revert + _tamperRequestData(requests[1], tamper); + _tamperedExpectRevert(requests[1], tamper, nonce + 1); - for (uint256 i = 0; i < batchSize; ++i) { - bytes memory data; - bool succeed = uint256(keccak256(abi.encodePacked(initialBalance, i))) % 2 == 0; + vm.deal(address(this), requests[1].value); + _erc2771Forwarder.executeBatch{value: requests[1].value}(requests, payable(address(0))); + } - if (succeed) { - data = abi.encodeCall(CallReceiverMock.mockFunction, ()); - } else { - expectedRefund += value; - data = abi.encodeCall(CallReceiverMock.mockFunctionRevertsNoReason, ()); - } + function testExecuteBatchTamperedValues(uint8 _tamper) public { + TamperType tamper = _asTamper(_tamper); + uint256 nonce = _erc2771Forwarder.nonces(_signer); - batchRequestDatas[i] = _forgeRequestData({ - value: value, - nonce: nonce + i, + // create an sign array or requests + ERC2771Forwarder.ForwardRequestData[] memory requests = new ERC2771Forwarder.ForwardRequestData[](3); + for (uint256 i = 0; i < requests.length; ++i) { + requests[i] = _forgeRequestData({ + value: 0, deadline: uint48(block.timestamp + 1), - data: data + data: abi.encodeCall(CallReceiverMock.mockFunction, ()) }); + _signRequestData(requests[i], nonce + i); } - address payable refundReceiver = payable(address(0xebe)); - uint256 totalValue = value * batchSize; + // tamper with request[1] + _tamperRequestData(requests[1], tamper); - vm.deal(address(this), totalValue); - _erc2771Forwarder.executeBatch{value: totalValue}(batchRequestDatas, refundReceiver); + // should not revert + vm.expectCall(address(_receiver), abi.encodeCall(CallReceiverMock.mockFunction, ()), 1); - assertEq(address(_erc2771Forwarder).balance, initialBalance); - assertEq(refundReceiver.balance, expectedRefund); + vm.deal(address(this), requests[1].value); + _erc2771Forwarder.executeBatch{value: requests[1].value}(requests, payable(address(0xebe))); + } + + function _asTamper(uint8 _tamper) private pure returns (TamperType) { + return TamperType(bound(_tamper, uint8(TamperType.FROM), uint8(TamperType.SIGNATURE))); } } diff --git a/test/metatx/ERC2771Forwarder.test.js b/test/metatx/ERC2771Forwarder.test.js index 8653ad708cb..bf6cfd10c4b 100644 --- a/test/metatx/ERC2771Forwarder.test.js +++ b/test/metatx/ERC2771Forwarder.test.js @@ -52,19 +52,6 @@ async function fixture() { }; } -// values or function to tamper with a signed request. -const tamperedValues = { - from: ethers.Wallet.createRandom().address, - to: ethers.Wallet.createRandom().address, - value: ethers.parseEther('0.5'), - data: '0x1742', - signature: s => { - const t = ethers.toBeArray(s); - t[42] ^= 0xff; - return ethers.hexlify(t); - }, -}; - describe('ERC2771Forwarder', function () { beforeEach(async function () { Object.assign(this, await loadFixture(fixture)); @@ -81,15 +68,6 @@ describe('ERC2771Forwarder', function () { }); describe('with tampered values', function () { - for (const [key, value] of Object.entries(tamperedValues)) { - it(`returns false with tampered ${key}`, async function () { - const request = await this.forgeRequest(); - request[key] = typeof value == 'function' ? value(request[key]) : value; - - expect(await this.forwarder.verify(request)).to.be.false; - }); - } - it('returns false with valid signature for non-current nonce', async function () { const request = await this.forgeRequest({ nonce: 1337n }); expect(await this.forwarder.verify(request)).to.be.false; @@ -127,24 +105,6 @@ describe('ERC2771Forwarder', function () { }); describe('with tampered request', function () { - for (const [key, value] of Object.entries(tamperedValues)) { - it(`reverts with tampered ${key}`, async function () { - const request = await this.forgeRequest(); - request[key] = typeof value == 'function' ? value(request[key]) : value; - - const promise = this.forwarder.execute(request, { value: key == 'value' ? value : 0 }); - if (key != 'to') { - await expect(promise) - .to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderInvalidSigner') - .withArgs(ethers.verifyTypedData(this.domain, this.types, request, request.signature), request.from); - } else { - await expect(promise) - .to.be.revertedWithCustomError(this.forwarder, 'ERC2771UntrustfulTarget') - .withArgs(request.to, this.forwarder); - } - }); - } - it('reverts with valid signature for non-current nonce', async function () { const request = await this.forgeRequest(); @@ -284,26 +244,6 @@ describe('ERC2771Forwarder', function () { this.refundReceiver = ethers.ZeroAddress; }); - for (const [key, value] of Object.entries(tamperedValues)) { - it(`reverts with at least one tampered request ${key}`, async function () { - this.requests[idx][key] = typeof value == 'function' ? value(this.requests[idx][key]) : value; - - const promise = this.forwarder.executeBatch(this.requests, this.refundReceiver, { value: this.value }); - if (key != 'to') { - await expect(promise) - .to.be.revertedWithCustomError(this.forwarder, 'ERC2771ForwarderInvalidSigner') - .withArgs( - ethers.verifyTypedData(this.domain, this.types, this.requests[idx], this.requests[idx].signature), - this.requests[idx].from, - ); - } else { - await expect(promise) - .to.be.revertedWithCustomError(this.forwarder, 'ERC2771UntrustfulTarget') - .withArgs(this.requests[idx].to, this.forwarder); - } - }); - } - it('reverts with at least one valid signature for non-current nonce', async function () { // Execute first a request await this.forwarder.execute(this.requests[idx], { value: this.requests[idx].value }); @@ -340,23 +280,6 @@ describe('ERC2771Forwarder', function () { this.initialTamperedRequestNonce = await this.forwarder.nonces(this.requests[idx].from); }); - for (const [key, value] of Object.entries(tamperedValues)) { - it(`ignores a request with tampered ${key} and refunds its value`, async function () { - this.requests[idx][key] = typeof value == 'function' ? value(this.requests[idx][key]) : value; - - const events = await this.forwarder - .executeBatch(this.requests, this.refundReceiver, { value: requestsValue(this.requests) }) - .then(tx => tx.wait()) - .then(receipt => - receipt.logs.filter( - log => log?.fragment?.type == 'event' && log?.fragment?.name == 'ExecutedForwardRequest', - ), - ); - - expect(events).to.have.lengthOf(this.requests.length - 1); - }); - } - it('ignores a request with a valid signature for non-current nonce', async function () { // Execute first a request await this.forwarder.execute(this.requests[idx], { value: this.requests[idx].value }); diff --git a/test/proxy/Clones.t.sol b/test/proxy/Clones.t.sol index e589ba90678..5da6d56d5a6 100644 --- a/test/proxy/Clones.t.sol +++ b/test/proxy/Clones.t.sol @@ -10,7 +10,7 @@ contract ClonesTest is Test { return 42; } - function testSymbolicPredictDeterministicAddressSpillage(address implementation, bytes32 salt) public { + function testSymbolicPredictDeterministicAddressSpillage(address implementation, bytes32 salt) public view { address predicted = Clones.predictDeterministicAddress(implementation, salt); bytes32 spillage; assembly ("memory-safe") { @@ -23,7 +23,7 @@ contract ClonesTest is Test { address implementation, bytes32 salt, bytes memory args - ) public { + ) public view { vm.assume(args.length < 0xbfd3); address predicted = Clones.predictDeterministicAddressWithImmutableArgs(implementation, args, salt); @@ -59,7 +59,7 @@ contract ClonesTest is Test { assertEq(ClonesTest(cloneDirty).getNumber(), this.getNumber()); } - function testPredictDeterministicAddressDirty(bytes32 salt) external { + function testPredictDeterministicAddressDirty(bytes32 salt) external view { address predictClean = Clones.predictDeterministicAddress(address(this), salt); address predictDirty = Clones.predictDeterministicAddress(_dirty(address(this)), salt); diff --git a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js index d90bd56e2dd..8e1d62eaad3 100644 --- a/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js +++ b/test/proxy/transparent/TransparentUpgradeableProxy.behaviour.js @@ -243,10 +243,10 @@ module.exports = function shouldBehaveLikeTransparentUpgradeableProxy() { }); it('proxy admin cannot call delegated functions', async function () { - const interface = await ethers.getContractFactory('TransparentUpgradeableProxy'); + const factory = await ethers.getContractFactory('TransparentUpgradeableProxy'); await expect(this.instance.connect(this.proxyAdminAsSigner).delegatedFunction()).to.be.revertedWithCustomError( - interface, + factory, 'ProxyDeniedAdminAccess', ); }); diff --git a/test/token/ERC721/extensions/ERC721Consecutive.test.js b/test/token/ERC721/extensions/ERC721Consecutive.test.js index d2eda944ccd..f62d6dc5b12 100644 --- a/test/token/ERC721/extensions/ERC721Consecutive.test.js +++ b/test/token/ERC721/extensions/ERC721Consecutive.test.js @@ -202,35 +202,35 @@ describe('ERC721Consecutive', function () { const receiver = ethers.Wallet.createRandom(); it('cannot mint a batch larger than 5000', async function () { - const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveMock'); + const factory = await ethers.getContractFactory('$ERC721ConsecutiveMock'); await expect(ethers.deployContract('$ERC721ConsecutiveMock', [name, symbol, 0, [], [receiver], [5001n]])) - .to.be.revertedWithCustomError({ interface }, 'ERC721ExceededMaxBatchMint') + .to.be.revertedWithCustomError(factory, 'ERC721ExceededMaxBatchMint') .withArgs(5001n, 5000n); }); it('cannot use single minting during construction', async function () { - const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveNoConstructorMintMock'); + const factory = await ethers.getContractFactory('$ERC721ConsecutiveNoConstructorMintMock'); await expect( ethers.deployContract('$ERC721ConsecutiveNoConstructorMintMock', [name, symbol]), - ).to.be.revertedWithCustomError({ interface }, 'ERC721ForbiddenMint'); + ).to.be.revertedWithCustomError(factory, 'ERC721ForbiddenMint'); }); it('cannot use single minting during construction', async function () { - const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveNoConstructorMintMock'); + const factory = await ethers.getContractFactory('$ERC721ConsecutiveNoConstructorMintMock'); await expect( ethers.deployContract('$ERC721ConsecutiveNoConstructorMintMock', [name, symbol]), - ).to.be.revertedWithCustomError({ interface }, 'ERC721ForbiddenMint'); + ).to.be.revertedWithCustomError(factory, 'ERC721ForbiddenMint'); }); it('consecutive mint not compatible with enumerability', async function () { - const { interface } = await ethers.getContractFactory('$ERC721ConsecutiveEnumerableMock'); + const factory = await ethers.getContractFactory('$ERC721ConsecutiveEnumerableMock'); await expect( ethers.deployContract('$ERC721ConsecutiveEnumerableMock', [name, symbol, [receiver], [100n]]), - ).to.be.revertedWithCustomError({ interface }, 'ERC721EnumerableForbiddenBatchMint'); + ).to.be.revertedWithCustomError(factory, 'ERC721EnumerableForbiddenBatchMint'); }); }); }); diff --git a/test/utils/Address.test.js b/test/utils/Address.test.js index 21775397ab2..8307a923e69 100644 --- a/test/utils/Address.test.js +++ b/test/utils/Address.test.js @@ -126,8 +126,9 @@ describe('Address', function () { }); it('reverts when function does not exist', async function () { - const interface = new ethers.Interface(['function mockFunctionDoesNotExist()']); - const call = interface.encodeFunctionData('mockFunctionDoesNotExist'); + const call = new ethers.Interface(['function mockFunctionDoesNotExist()']).encodeFunctionData( + 'mockFunctionDoesNotExist', + ); await expect(this.mock.$functionCall(this.target, call)).to.be.revertedWithCustomError(this.mock, 'FailedCall'); }); diff --git a/test/utils/Arrays.t.sol b/test/utils/Arrays.t.sol index 09c7b66b6d6..e45d29c919d 100644 --- a/test/utils/Arrays.t.sol +++ b/test/utils/Arrays.t.sol @@ -7,12 +7,12 @@ import {SymTest} from "halmos-cheatcodes/SymTest.sol"; import {Arrays} from "@openzeppelin/contracts/utils/Arrays.sol"; contract ArraysTest is Test, SymTest { - function testSort(uint256[] memory values) public { + function testSort(uint256[] memory values) public pure { Arrays.sort(values); _assertSort(values); } - function symbolicSort() public { + function symbolicSort() public pure { uint256[] memory values = new uint256[](3); for (uint256 i = 0; i < 3; i++) { values[i] = svm.createUint256("arrayElement"); @@ -23,7 +23,7 @@ contract ArraysTest is Test, SymTest { /// Asserts - function _assertSort(uint256[] memory values) internal { + function _assertSort(uint256[] memory values) internal pure { for (uint256 i = 1; i < values.length; ++i) { assertLe(values[i - 1], values[i]); } diff --git a/test/utils/Base64.t.sol b/test/utils/Base64.t.sol index 021ae03af09..b8aa7ac6121 100644 --- a/test/utils/Base64.t.sol +++ b/test/utils/Base64.t.sol @@ -6,11 +6,11 @@ import {Test} from "forge-std/Test.sol"; import {Base64} from "@openzeppelin/contracts/utils/Base64.sol"; contract Base64Test is Test { - function testEncode(bytes memory input) external { + function testEncode(bytes memory input) external pure { assertEq(Base64.encode(input), vm.toBase64(input)); } - function testEncodeURL(bytes memory input) external { + function testEncodeURL(bytes memory input) external pure { assertEq(Base64.encodeURL(input), _removePadding(vm.toBase64URL(input))); } diff --git a/test/utils/Bytes.test.js b/test/utils/Bytes.test.js new file mode 100644 index 00000000000..52a1ae95e77 --- /dev/null +++ b/test/utils/Bytes.test.js @@ -0,0 +1,88 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const mock = await ethers.deployContract('$Bytes'); + return { mock }; +} + +const lorem = ethers.toUtf8Bytes( + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', +); +const present = lorem.at(1); +const absent = 255; + +describe('Bytes', function () { + before(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('indexOf', function () { + it('first', async function () { + expect(await this.mock.$indexOf(lorem, ethers.toBeHex(present))).to.equal(lorem.indexOf(present)); + }); + + it('from index', async function () { + for (const start in Array(lorem.length + 10).fill()) { + const index = lorem.indexOf(present, start); + const result = index === -1 ? ethers.MaxUint256 : index; + expect(await this.mock.$indexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start))).to.equal(result); + } + }); + + it('absent', async function () { + expect(await this.mock.$indexOf(lorem, ethers.toBeHex(absent))).to.equal(ethers.MaxUint256); + }); + }); + + describe('lastIndexOf', function () { + it('first', async function () { + expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(present))).to.equal(lorem.lastIndexOf(present)); + }); + + it('from index', async function () { + for (const start in Array(lorem.length + 10).fill()) { + const index = lorem.lastIndexOf(present, start); + const result = index === -1 ? ethers.MaxUint256 : index; + expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(present), ethers.Typed.uint256(start))).to.equal( + result, + ); + } + }); + + it('absent', async function () { + expect(await this.mock.$lastIndexOf(lorem, ethers.toBeHex(absent))).to.equal(ethers.MaxUint256); + }); + }); + + describe('slice', function () { + describe('slice(bytes, uint256)', function () { + for (const [descr, start] of Object.entries({ + 'start = 0': 0, + 'start within bound': 10, + 'start out of bound': 1000, + })) { + it(descr, async function () { + const result = ethers.hexlify(lorem.slice(start)); + expect(await this.mock.$slice(lorem, start)).to.equal(result); + }); + } + }); + + describe('slice(bytes, uint256, uint256)', function () { + for (const [descr, [start, end]] of Object.entries({ + 'start = 0': [0, 42], + 'start and end within bound': [17, 42], + 'end out of bound': [42, 1000], + 'start = end': [17, 17], + 'start > end': [42, 17], + })) { + it(descr, async function () { + const result = ethers.hexlify(lorem.slice(start, end)); + expect(await this.mock.$slice(lorem, start, ethers.Typed.uint256(end))).to.equal(result); + }); + } + }); + }); +}); diff --git a/test/utils/CAIP.test.js b/test/utils/CAIP.test.js new file mode 100644 index 00000000000..cd5995cade0 --- /dev/null +++ b/test/utils/CAIP.test.js @@ -0,0 +1,53 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +const { CHAINS, getLocalCAIP } = require('../helpers/chains'); + +async function fixture() { + const caip2 = await ethers.deployContract('$CAIP2'); + const caip10 = await ethers.deployContract('$CAIP10'); + return { caip2, caip10 }; +} + +describe('CAIP utilities', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('CAIP-2', function () { + it('local()', async function () { + const { caip2 } = await getLocalCAIP(); + expect(await this.caip2.$local()).to.equal(caip2); + }); + + for (const { namespace, reference, caip2 } of Object.values(CHAINS)) + it(`format(${namespace}, ${reference})`, async function () { + expect(await this.caip2.$format(namespace, reference)).to.equal(caip2); + }); + + for (const { namespace, reference, caip2 } of Object.values(CHAINS)) + it(`parse(${caip2})`, async function () { + expect(await this.caip2.$parse(caip2)).to.deep.equal([namespace, reference]); + }); + }); + + describe('CAIP-10', function () { + const { address: account } = ethers.Wallet.createRandom(); + + it(`local(${account})`, async function () { + const { caip10 } = await getLocalCAIP(account); + expect(await this.caip10.$local(ethers.Typed.address(account))).to.equal(caip10); + }); + + for (const { account, caip2, caip10 } of Object.values(CHAINS)) + it(`format(${caip2}, ${account})`, async function () { + expect(await this.caip10.$format(ethers.Typed.string(caip2), ethers.Typed.string(account))).to.equal(caip10); + }); + + for (const { account, caip2, caip10 } of Object.values(CHAINS)) + it(`parse(${caip10})`, async function () { + expect(await this.caip10.$parse(caip10)).to.deep.equal([caip2, account]); + }); + }); +}); diff --git a/test/utils/Create2.t.sol b/test/utils/Create2.t.sol index 6cc037a3b3b..b73db9f9a25 100644 --- a/test/utils/Create2.t.sol +++ b/test/utils/Create2.t.sol @@ -6,7 +6,7 @@ import {Test} from "forge-std/Test.sol"; import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; contract Create2Test is Test { - function testSymbolicComputeAddressSpillage(bytes32 salt, bytes32 bytecodeHash, address deployer) public { + function testSymbolicComputeAddressSpillage(bytes32 salt, bytes32 bytecodeHash, address deployer) public pure { address predicted = Create2.computeAddress(salt, bytecodeHash, deployer); bytes32 spillage; assembly ("memory-safe") { diff --git a/test/utils/Create2.test.js b/test/utils/Create2.test.js index 152fdbdf4c1..99c47a0e34a 100644 --- a/test/utils/Create2.test.js +++ b/test/utils/Create2.test.js @@ -14,13 +14,13 @@ async function fixture() { // We use a vesting wallet, with 3 constructor arguments. const constructorByteCode = await ethers .getContractFactory('VestingWallet') - .then(({ bytecode, interface }) => ethers.concat([bytecode, interface.encodeDeploy([other.address, 0n, 0n])])); + .then(factory => ethers.concat([factory.bytecode, factory.interface.encodeDeploy([other.address, 0n, 0n])])); // Bytecode for deploying a contract that has no constructor log. // Here we use the Create2 helper factory. const constructorLessBytecode = await ethers .getContractFactory('$Create2') - .then(({ bytecode, interface }) => ethers.concat([bytecode, interface.encodeDeploy([])])); + .then(factory => ethers.concat([factory.bytecode, factory.interface.encodeDeploy([])])); const mockFactory = await ethers.getContractFactory('ConstructorMock'); diff --git a/test/utils/Nonces.behavior.js b/test/utils/Nonces.behavior.js new file mode 100644 index 00000000000..32201f690d6 --- /dev/null +++ b/test/utils/Nonces.behavior.js @@ -0,0 +1,189 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); + +function shouldBehaveLikeNonces() { + describe('should behave like Nonces', function () { + const sender = ethers.Wallet.createRandom(); + const other = ethers.Wallet.createRandom(); + + it('gets a nonce', async function () { + expect(this.mock.nonces(sender)).to.eventually.equal(0n); + }); + + describe('_useNonce', function () { + it('increments a nonce', async function () { + expect(this.mock.nonces(sender)).to.eventually.equal(0n); + + const eventName = ['return$_useNonce', 'return$_useNonce_address'].find(name => + this.mock.interface.getEvent(name), + ); + + await expect(this.mock.$_useNonce(sender)).to.emit(this.mock, eventName).withArgs(0n); + + expect(this.mock.nonces(sender)).to.eventually.equal(1n); + }); + + it("increments only sender's nonce", async function () { + expect(this.mock.nonces(sender)).to.eventually.equal(0n); + expect(this.mock.nonces(other)).to.eventually.equal(0n); + + await this.mock.$_useNonce(sender); + + expect(this.mock.nonces(sender)).to.eventually.equal(1n); + expect(this.mock.nonces(other)).to.eventually.equal(0n); + }); + }); + + describe('_useCheckedNonce', function () { + it('increments a nonce', async function () { + // current nonce is 0n + expect(this.mock.nonces(sender)).to.eventually.equal(0n); + + await this.mock.$_useCheckedNonce(sender, 0n); + + expect(this.mock.nonces(sender)).to.eventually.equal(1n); + }); + + it("increments only sender's nonce", async function () { + // current nonce is 0n + expect(this.mock.nonces(sender)).to.eventually.equal(0n); + expect(this.mock.nonces(other)).to.eventually.equal(0n); + + await this.mock.$_useCheckedNonce(sender, 0n); + + expect(this.mock.nonces(sender)).to.eventually.equal(1n); + expect(this.mock.nonces(other)).to.eventually.equal(0n); + }); + + it('reverts when nonce is not the expected', async function () { + const currentNonce = await this.mock.nonces(sender); + + await expect(this.mock.$_useCheckedNonce(sender, currentNonce + 1n)) + .to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce') + .withArgs(sender, currentNonce); + }); + }); + }); +} + +function shouldBehaveLikeNoncesKeyed() { + describe('should support nonces with keys', function () { + const sender = ethers.Wallet.createRandom(); + + const keyOffset = key => key << 64n; + + it('gets a nonce', async function () { + expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n); + expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n); + }); + + describe('_useNonce', function () { + it('default variant uses key 0', async function () { + expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n); + expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n); + + await expect(this.mock.$_useNonce(sender)).to.emit(this.mock, 'return$_useNonce_address').withArgs(0n); + + await expect(this.mock.$_useNonce(sender, ethers.Typed.uint192(0n))) + .to.emit(this.mock, 'return$_useNonce_address_uint192') + .withArgs(keyOffset(0n) + 1n); + + expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 2n); + expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n); + }); + + it('use nonce at another key', async function () { + expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n); + expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 0n); + + await expect(this.mock.$_useNonce(sender, ethers.Typed.uint192(17n))) + .to.emit(this.mock, 'return$_useNonce_address_uint192') + .withArgs(keyOffset(17n) + 0n); + + await expect(this.mock.$_useNonce(sender, ethers.Typed.uint192(17n))) + .to.emit(this.mock, 'return$_useNonce_address_uint192') + .withArgs(keyOffset(17n) + 1n); + + expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(keyOffset(0n) + 0n); + expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(keyOffset(17n) + 2n); + }); + }); + + describe('_useCheckedNonce(address, uint256)', function () { + it('default variant uses key 0', async function () { + const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(0n)); + + await this.mock.$_useCheckedNonce(sender, currentNonce); + + expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(currentNonce + 1n); + }); + + it('use nonce at another key', async function () { + const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(17n)); + + await this.mock.$_useCheckedNonce(sender, currentNonce); + + expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(currentNonce + 1n); + }); + + it('reverts when nonce is not the expected', async function () { + const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(42n)); + + // use and increment + await this.mock.$_useCheckedNonce(sender, currentNonce); + + // reuse same nonce + await expect(this.mock.$_useCheckedNonce(sender, currentNonce)) + .to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce') + .withArgs(sender, currentNonce + 1n); + + // use "future" nonce too early + await expect(this.mock.$_useCheckedNonce(sender, currentNonce + 10n)) + .to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce') + .withArgs(sender, currentNonce + 1n); + }); + }); + + describe('_useCheckedNonce(address, uint192, uint64)', function () { + const MASK = 0xffffffffffffffffn; + + it('default variant uses key 0', async function () { + const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(0n)); + + await this.mock.$_useCheckedNonce(sender, ethers.Typed.uint192(0n), currentNonce); + + expect(this.mock.nonces(sender, ethers.Typed.uint192(0n))).to.eventually.equal(currentNonce + 1n); + }); + + it('use nonce at another key', async function () { + const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(17n)); + + await this.mock.$_useCheckedNonce(sender, ethers.Typed.uint192(17n), currentNonce & MASK); + + expect(this.mock.nonces(sender, ethers.Typed.uint192(17n))).to.eventually.equal(currentNonce + 1n); + }); + + it('reverts when nonce is not the expected', async function () { + const currentNonce = await this.mock.nonces(sender, ethers.Typed.uint192(42n)); + + // use and increment + await this.mock.$_useCheckedNonce(sender, ethers.Typed.uint192(42n), currentNonce & MASK); + + // reuse same nonce + await expect(this.mock.$_useCheckedNonce(sender, ethers.Typed.uint192(42n), currentNonce & MASK)) + .to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce') + .withArgs(sender, currentNonce + 1n); + + // use "future" nonce too early + await expect(this.mock.$_useCheckedNonce(sender, ethers.Typed.uint192(42n), (currentNonce & MASK) + 10n)) + .to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce') + .withArgs(sender, currentNonce + 1n); + }); + }); + }); +} + +module.exports = { + shouldBehaveLikeNonces, + shouldBehaveLikeNoncesKeyed, +}; diff --git a/test/utils/Nonces.test.js b/test/utils/Nonces.test.js index 2cb4798dea6..85aa7358a00 100644 --- a/test/utils/Nonces.test.js +++ b/test/utils/Nonces.test.js @@ -1,13 +1,10 @@ const { ethers } = require('hardhat'); -const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { shouldBehaveLikeNonces } = require('./Nonces.behavior'); async function fixture() { - const [sender, other] = await ethers.getSigners(); - const mock = await ethers.deployContract('$Nonces'); - - return { sender, other, mock }; + return { mock }; } describe('Nonces', function () { @@ -15,61 +12,5 @@ describe('Nonces', function () { Object.assign(this, await loadFixture(fixture)); }); - it('gets a nonce', async function () { - expect(await this.mock.nonces(this.sender)).to.equal(0n); - }); - - describe('_useNonce', function () { - it('increments a nonce', async function () { - expect(await this.mock.nonces(this.sender)).to.equal(0n); - - await expect(await this.mock.$_useNonce(this.sender)) - .to.emit(this.mock, 'return$_useNonce') - .withArgs(0n); - - expect(await this.mock.nonces(this.sender)).to.equal(1n); - }); - - it("increments only sender's nonce", async function () { - expect(await this.mock.nonces(this.sender)).to.equal(0n); - expect(await this.mock.nonces(this.other)).to.equal(0n); - - await this.mock.$_useNonce(this.sender); - - expect(await this.mock.nonces(this.sender)).to.equal(1n); - expect(await this.mock.nonces(this.other)).to.equal(0n); - }); - }); - - describe('_useCheckedNonce', function () { - it('increments a nonce', async function () { - const currentNonce = await this.mock.nonces(this.sender); - - expect(currentNonce).to.equal(0n); - - await this.mock.$_useCheckedNonce(this.sender, currentNonce); - - expect(await this.mock.nonces(this.sender)).to.equal(1n); - }); - - it("increments only sender's nonce", async function () { - const currentNonce = await this.mock.nonces(this.sender); - - expect(currentNonce).to.equal(0n); - expect(await this.mock.nonces(this.other)).to.equal(0n); - - await this.mock.$_useCheckedNonce(this.sender, currentNonce); - - expect(await this.mock.nonces(this.sender)).to.equal(1n); - expect(await this.mock.nonces(this.other)).to.equal(0n); - }); - - it('reverts when nonce is not the expected', async function () { - const currentNonce = await this.mock.nonces(this.sender); - - await expect(this.mock.$_useCheckedNonce(this.sender, currentNonce + 1n)) - .to.be.revertedWithCustomError(this.mock, 'InvalidAccountNonce') - .withArgs(this.sender, currentNonce); - }); - }); + shouldBehaveLikeNonces(); }); diff --git a/test/utils/NoncesKeyed.test.js b/test/utils/NoncesKeyed.test.js new file mode 100644 index 00000000000..c46948ee402 --- /dev/null +++ b/test/utils/NoncesKeyed.test.js @@ -0,0 +1,17 @@ +const { ethers } = require('hardhat'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { shouldBehaveLikeNonces, shouldBehaveLikeNoncesKeyed } = require('./Nonces.behavior'); + +async function fixture() { + const mock = await ethers.deployContract('$NoncesKeyed'); + return { mock }; +} + +describe('NoncesKeyed', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + shouldBehaveLikeNonces(); + shouldBehaveLikeNoncesKeyed(); +}); diff --git a/test/utils/Packing.t.sol b/test/utils/Packing.t.sol index 9531f1bffbb..40f052c80ff 100644 --- a/test/utils/Packing.t.sol +++ b/test/utils/Packing.t.sol @@ -9,182 +9,287 @@ import {Packing} from "@openzeppelin/contracts/utils/Packing.sol"; contract PackingTest is Test { using Packing for *; - function testPack(bytes1 left, bytes1 right) external { + function testPack(bytes1 left, bytes1 right) external pure { assertEq(left, Packing.pack_1_1(left, right).extract_2_1(0)); assertEq(right, Packing.pack_1_1(left, right).extract_2_1(1)); } - function testPack(bytes2 left, bytes2 right) external { + function testPack(bytes2 left, bytes2 right) external pure { assertEq(left, Packing.pack_2_2(left, right).extract_4_2(0)); assertEq(right, Packing.pack_2_2(left, right).extract_4_2(2)); } - function testPack(bytes2 left, bytes4 right) external { + function testPack(bytes2 left, bytes4 right) external pure { assertEq(left, Packing.pack_2_4(left, right).extract_6_2(0)); assertEq(right, Packing.pack_2_4(left, right).extract_6_4(2)); } - function testPack(bytes2 left, bytes6 right) external { + function testPack(bytes2 left, bytes6 right) external pure { assertEq(left, Packing.pack_2_6(left, right).extract_8_2(0)); assertEq(right, Packing.pack_2_6(left, right).extract_8_6(2)); } - function testPack(bytes4 left, bytes2 right) external { + function testPack(bytes2 left, bytes8 right) external pure { + assertEq(left, Packing.pack_2_8(left, right).extract_10_2(0)); + assertEq(right, Packing.pack_2_8(left, right).extract_10_8(2)); + } + + function testPack(bytes2 left, bytes10 right) external pure { + assertEq(left, Packing.pack_2_10(left, right).extract_12_2(0)); + assertEq(right, Packing.pack_2_10(left, right).extract_12_10(2)); + } + + function testPack(bytes2 left, bytes20 right) external pure { + assertEq(left, Packing.pack_2_20(left, right).extract_22_2(0)); + assertEq(right, Packing.pack_2_20(left, right).extract_22_20(2)); + } + + function testPack(bytes2 left, bytes22 right) external pure { + assertEq(left, Packing.pack_2_22(left, right).extract_24_2(0)); + assertEq(right, Packing.pack_2_22(left, right).extract_24_22(2)); + } + + function testPack(bytes4 left, bytes2 right) external pure { assertEq(left, Packing.pack_4_2(left, right).extract_6_4(0)); assertEq(right, Packing.pack_4_2(left, right).extract_6_2(4)); } - function testPack(bytes4 left, bytes4 right) external { + function testPack(bytes4 left, bytes4 right) external pure { assertEq(left, Packing.pack_4_4(left, right).extract_8_4(0)); assertEq(right, Packing.pack_4_4(left, right).extract_8_4(4)); } - function testPack(bytes4 left, bytes8 right) external { + function testPack(bytes4 left, bytes6 right) external pure { + assertEq(left, Packing.pack_4_6(left, right).extract_10_4(0)); + assertEq(right, Packing.pack_4_6(left, right).extract_10_6(4)); + } + + function testPack(bytes4 left, bytes8 right) external pure { assertEq(left, Packing.pack_4_8(left, right).extract_12_4(0)); assertEq(right, Packing.pack_4_8(left, right).extract_12_8(4)); } - function testPack(bytes4 left, bytes12 right) external { + function testPack(bytes4 left, bytes12 right) external pure { assertEq(left, Packing.pack_4_12(left, right).extract_16_4(0)); assertEq(right, Packing.pack_4_12(left, right).extract_16_12(4)); } - function testPack(bytes4 left, bytes16 right) external { + function testPack(bytes4 left, bytes16 right) external pure { assertEq(left, Packing.pack_4_16(left, right).extract_20_4(0)); assertEq(right, Packing.pack_4_16(left, right).extract_20_16(4)); } - function testPack(bytes4 left, bytes20 right) external { + function testPack(bytes4 left, bytes20 right) external pure { assertEq(left, Packing.pack_4_20(left, right).extract_24_4(0)); assertEq(right, Packing.pack_4_20(left, right).extract_24_20(4)); } - function testPack(bytes4 left, bytes24 right) external { + function testPack(bytes4 left, bytes24 right) external pure { assertEq(left, Packing.pack_4_24(left, right).extract_28_4(0)); assertEq(right, Packing.pack_4_24(left, right).extract_28_24(4)); } - function testPack(bytes4 left, bytes28 right) external { + function testPack(bytes4 left, bytes28 right) external pure { assertEq(left, Packing.pack_4_28(left, right).extract_32_4(0)); assertEq(right, Packing.pack_4_28(left, right).extract_32_28(4)); } - function testPack(bytes6 left, bytes2 right) external { + function testPack(bytes6 left, bytes2 right) external pure { assertEq(left, Packing.pack_6_2(left, right).extract_8_6(0)); assertEq(right, Packing.pack_6_2(left, right).extract_8_2(6)); } - function testPack(bytes6 left, bytes6 right) external { + function testPack(bytes6 left, bytes4 right) external pure { + assertEq(left, Packing.pack_6_4(left, right).extract_10_6(0)); + assertEq(right, Packing.pack_6_4(left, right).extract_10_4(6)); + } + + function testPack(bytes6 left, bytes6 right) external pure { assertEq(left, Packing.pack_6_6(left, right).extract_12_6(0)); assertEq(right, Packing.pack_6_6(left, right).extract_12_6(6)); } - function testPack(bytes8 left, bytes4 right) external { + function testPack(bytes6 left, bytes10 right) external pure { + assertEq(left, Packing.pack_6_10(left, right).extract_16_6(0)); + assertEq(right, Packing.pack_6_10(left, right).extract_16_10(6)); + } + + function testPack(bytes6 left, bytes16 right) external pure { + assertEq(left, Packing.pack_6_16(left, right).extract_22_6(0)); + assertEq(right, Packing.pack_6_16(left, right).extract_22_16(6)); + } + + function testPack(bytes6 left, bytes22 right) external pure { + assertEq(left, Packing.pack_6_22(left, right).extract_28_6(0)); + assertEq(right, Packing.pack_6_22(left, right).extract_28_22(6)); + } + + function testPack(bytes8 left, bytes2 right) external pure { + assertEq(left, Packing.pack_8_2(left, right).extract_10_8(0)); + assertEq(right, Packing.pack_8_2(left, right).extract_10_2(8)); + } + + function testPack(bytes8 left, bytes4 right) external pure { assertEq(left, Packing.pack_8_4(left, right).extract_12_8(0)); assertEq(right, Packing.pack_8_4(left, right).extract_12_4(8)); } - function testPack(bytes8 left, bytes8 right) external { + function testPack(bytes8 left, bytes8 right) external pure { assertEq(left, Packing.pack_8_8(left, right).extract_16_8(0)); assertEq(right, Packing.pack_8_8(left, right).extract_16_8(8)); } - function testPack(bytes8 left, bytes12 right) external { + function testPack(bytes8 left, bytes12 right) external pure { assertEq(left, Packing.pack_8_12(left, right).extract_20_8(0)); assertEq(right, Packing.pack_8_12(left, right).extract_20_12(8)); } - function testPack(bytes8 left, bytes16 right) external { + function testPack(bytes8 left, bytes16 right) external pure { assertEq(left, Packing.pack_8_16(left, right).extract_24_8(0)); assertEq(right, Packing.pack_8_16(left, right).extract_24_16(8)); } - function testPack(bytes8 left, bytes20 right) external { + function testPack(bytes8 left, bytes20 right) external pure { assertEq(left, Packing.pack_8_20(left, right).extract_28_8(0)); assertEq(right, Packing.pack_8_20(left, right).extract_28_20(8)); } - function testPack(bytes8 left, bytes24 right) external { + function testPack(bytes8 left, bytes24 right) external pure { assertEq(left, Packing.pack_8_24(left, right).extract_32_8(0)); assertEq(right, Packing.pack_8_24(left, right).extract_32_24(8)); } - function testPack(bytes12 left, bytes4 right) external { + function testPack(bytes10 left, bytes2 right) external pure { + assertEq(left, Packing.pack_10_2(left, right).extract_12_10(0)); + assertEq(right, Packing.pack_10_2(left, right).extract_12_2(10)); + } + + function testPack(bytes10 left, bytes6 right) external pure { + assertEq(left, Packing.pack_10_6(left, right).extract_16_10(0)); + assertEq(right, Packing.pack_10_6(left, right).extract_16_6(10)); + } + + function testPack(bytes10 left, bytes10 right) external pure { + assertEq(left, Packing.pack_10_10(left, right).extract_20_10(0)); + assertEq(right, Packing.pack_10_10(left, right).extract_20_10(10)); + } + + function testPack(bytes10 left, bytes12 right) external pure { + assertEq(left, Packing.pack_10_12(left, right).extract_22_10(0)); + assertEq(right, Packing.pack_10_12(left, right).extract_22_12(10)); + } + + function testPack(bytes10 left, bytes22 right) external pure { + assertEq(left, Packing.pack_10_22(left, right).extract_32_10(0)); + assertEq(right, Packing.pack_10_22(left, right).extract_32_22(10)); + } + + function testPack(bytes12 left, bytes4 right) external pure { assertEq(left, Packing.pack_12_4(left, right).extract_16_12(0)); assertEq(right, Packing.pack_12_4(left, right).extract_16_4(12)); } - function testPack(bytes12 left, bytes8 right) external { + function testPack(bytes12 left, bytes8 right) external pure { assertEq(left, Packing.pack_12_8(left, right).extract_20_12(0)); assertEq(right, Packing.pack_12_8(left, right).extract_20_8(12)); } - function testPack(bytes12 left, bytes12 right) external { + function testPack(bytes12 left, bytes10 right) external pure { + assertEq(left, Packing.pack_12_10(left, right).extract_22_12(0)); + assertEq(right, Packing.pack_12_10(left, right).extract_22_10(12)); + } + + function testPack(bytes12 left, bytes12 right) external pure { assertEq(left, Packing.pack_12_12(left, right).extract_24_12(0)); assertEq(right, Packing.pack_12_12(left, right).extract_24_12(12)); } - function testPack(bytes12 left, bytes16 right) external { + function testPack(bytes12 left, bytes16 right) external pure { assertEq(left, Packing.pack_12_16(left, right).extract_28_12(0)); assertEq(right, Packing.pack_12_16(left, right).extract_28_16(12)); } - function testPack(bytes12 left, bytes20 right) external { + function testPack(bytes12 left, bytes20 right) external pure { assertEq(left, Packing.pack_12_20(left, right).extract_32_12(0)); assertEq(right, Packing.pack_12_20(left, right).extract_32_20(12)); } - function testPack(bytes16 left, bytes4 right) external { + function testPack(bytes16 left, bytes4 right) external pure { assertEq(left, Packing.pack_16_4(left, right).extract_20_16(0)); assertEq(right, Packing.pack_16_4(left, right).extract_20_4(16)); } - function testPack(bytes16 left, bytes8 right) external { + function testPack(bytes16 left, bytes6 right) external pure { + assertEq(left, Packing.pack_16_6(left, right).extract_22_16(0)); + assertEq(right, Packing.pack_16_6(left, right).extract_22_6(16)); + } + + function testPack(bytes16 left, bytes8 right) external pure { assertEq(left, Packing.pack_16_8(left, right).extract_24_16(0)); assertEq(right, Packing.pack_16_8(left, right).extract_24_8(16)); } - function testPack(bytes16 left, bytes12 right) external { + function testPack(bytes16 left, bytes12 right) external pure { assertEq(left, Packing.pack_16_12(left, right).extract_28_16(0)); assertEq(right, Packing.pack_16_12(left, right).extract_28_12(16)); } - function testPack(bytes16 left, bytes16 right) external { + function testPack(bytes16 left, bytes16 right) external pure { assertEq(left, Packing.pack_16_16(left, right).extract_32_16(0)); assertEq(right, Packing.pack_16_16(left, right).extract_32_16(16)); } - function testPack(bytes20 left, bytes4 right) external { + function testPack(bytes20 left, bytes2 right) external pure { + assertEq(left, Packing.pack_20_2(left, right).extract_22_20(0)); + assertEq(right, Packing.pack_20_2(left, right).extract_22_2(20)); + } + + function testPack(bytes20 left, bytes4 right) external pure { assertEq(left, Packing.pack_20_4(left, right).extract_24_20(0)); assertEq(right, Packing.pack_20_4(left, right).extract_24_4(20)); } - function testPack(bytes20 left, bytes8 right) external { + function testPack(bytes20 left, bytes8 right) external pure { assertEq(left, Packing.pack_20_8(left, right).extract_28_20(0)); assertEq(right, Packing.pack_20_8(left, right).extract_28_8(20)); } - function testPack(bytes20 left, bytes12 right) external { + function testPack(bytes20 left, bytes12 right) external pure { assertEq(left, Packing.pack_20_12(left, right).extract_32_20(0)); assertEq(right, Packing.pack_20_12(left, right).extract_32_12(20)); } - function testPack(bytes24 left, bytes4 right) external { + function testPack(bytes22 left, bytes2 right) external pure { + assertEq(left, Packing.pack_22_2(left, right).extract_24_22(0)); + assertEq(right, Packing.pack_22_2(left, right).extract_24_2(22)); + } + + function testPack(bytes22 left, bytes6 right) external pure { + assertEq(left, Packing.pack_22_6(left, right).extract_28_22(0)); + assertEq(right, Packing.pack_22_6(left, right).extract_28_6(22)); + } + + function testPack(bytes22 left, bytes10 right) external pure { + assertEq(left, Packing.pack_22_10(left, right).extract_32_22(0)); + assertEq(right, Packing.pack_22_10(left, right).extract_32_10(22)); + } + + function testPack(bytes24 left, bytes4 right) external pure { assertEq(left, Packing.pack_24_4(left, right).extract_28_24(0)); assertEq(right, Packing.pack_24_4(left, right).extract_28_4(24)); } - function testPack(bytes24 left, bytes8 right) external { + function testPack(bytes24 left, bytes8 right) external pure { assertEq(left, Packing.pack_24_8(left, right).extract_32_24(0)); assertEq(right, Packing.pack_24_8(left, right).extract_32_8(24)); } - function testPack(bytes28 left, bytes4 right) external { + function testPack(bytes28 left, bytes4 right) external pure { assertEq(left, Packing.pack_28_4(left, right).extract_32_28(0)); assertEq(right, Packing.pack_28_4(left, right).extract_32_4(28)); } - function testReplace(bytes2 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes2 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 1)); bytes1 oldValue = container.extract_2_1(offset); @@ -193,7 +298,7 @@ contract PackingTest is Test { assertEq(container, container.replace_2_1(newValue, offset).replace_2_1(oldValue, offset)); } - function testReplace(bytes4 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes4 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 3)); bytes1 oldValue = container.extract_4_1(offset); @@ -202,7 +307,7 @@ contract PackingTest is Test { assertEq(container, container.replace_4_1(newValue, offset).replace_4_1(oldValue, offset)); } - function testReplace(bytes4 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes4 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 2)); bytes2 oldValue = container.extract_4_2(offset); @@ -211,7 +316,7 @@ contract PackingTest is Test { assertEq(container, container.replace_4_2(newValue, offset).replace_4_2(oldValue, offset)); } - function testReplace(bytes6 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes6 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 5)); bytes1 oldValue = container.extract_6_1(offset); @@ -220,7 +325,7 @@ contract PackingTest is Test { assertEq(container, container.replace_6_1(newValue, offset).replace_6_1(oldValue, offset)); } - function testReplace(bytes6 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes6 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes2 oldValue = container.extract_6_2(offset); @@ -229,7 +334,7 @@ contract PackingTest is Test { assertEq(container, container.replace_6_2(newValue, offset).replace_6_2(oldValue, offset)); } - function testReplace(bytes6 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes6 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 2)); bytes4 oldValue = container.extract_6_4(offset); @@ -238,7 +343,7 @@ contract PackingTest is Test { assertEq(container, container.replace_6_4(newValue, offset).replace_6_4(oldValue, offset)); } - function testReplace(bytes8 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes8 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 7)); bytes1 oldValue = container.extract_8_1(offset); @@ -247,7 +352,7 @@ contract PackingTest is Test { assertEq(container, container.replace_8_1(newValue, offset).replace_8_1(oldValue, offset)); } - function testReplace(bytes8 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes8 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 6)); bytes2 oldValue = container.extract_8_2(offset); @@ -256,7 +361,7 @@ contract PackingTest is Test { assertEq(container, container.replace_8_2(newValue, offset).replace_8_2(oldValue, offset)); } - function testReplace(bytes8 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes8 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes4 oldValue = container.extract_8_4(offset); @@ -265,7 +370,7 @@ contract PackingTest is Test { assertEq(container, container.replace_8_4(newValue, offset).replace_8_4(oldValue, offset)); } - function testReplace(bytes8 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes8 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 2)); bytes6 oldValue = container.extract_8_6(offset); @@ -274,7 +379,52 @@ contract PackingTest is Test { assertEq(container, container.replace_8_6(newValue, offset).replace_8_6(oldValue, offset)); } - function testReplace(bytes12 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes10 container, bytes1 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 9)); + + bytes1 oldValue = container.extract_10_1(offset); + + assertEq(newValue, container.replace_10_1(newValue, offset).extract_10_1(offset)); + assertEq(container, container.replace_10_1(newValue, offset).replace_10_1(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes2 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 8)); + + bytes2 oldValue = container.extract_10_2(offset); + + assertEq(newValue, container.replace_10_2(newValue, offset).extract_10_2(offset)); + assertEq(container, container.replace_10_2(newValue, offset).replace_10_2(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes4 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 6)); + + bytes4 oldValue = container.extract_10_4(offset); + + assertEq(newValue, container.replace_10_4(newValue, offset).extract_10_4(offset)); + assertEq(container, container.replace_10_4(newValue, offset).replace_10_4(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes6 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 4)); + + bytes6 oldValue = container.extract_10_6(offset); + + assertEq(newValue, container.replace_10_6(newValue, offset).extract_10_6(offset)); + assertEq(container, container.replace_10_6(newValue, offset).replace_10_6(oldValue, offset)); + } + + function testReplace(bytes10 container, bytes8 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 2)); + + bytes8 oldValue = container.extract_10_8(offset); + + assertEq(newValue, container.replace_10_8(newValue, offset).extract_10_8(offset)); + assertEq(container, container.replace_10_8(newValue, offset).replace_10_8(oldValue, offset)); + } + + function testReplace(bytes12 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 11)); bytes1 oldValue = container.extract_12_1(offset); @@ -283,7 +433,7 @@ contract PackingTest is Test { assertEq(container, container.replace_12_1(newValue, offset).replace_12_1(oldValue, offset)); } - function testReplace(bytes12 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes12 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 10)); bytes2 oldValue = container.extract_12_2(offset); @@ -292,7 +442,7 @@ contract PackingTest is Test { assertEq(container, container.replace_12_2(newValue, offset).replace_12_2(oldValue, offset)); } - function testReplace(bytes12 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes12 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes4 oldValue = container.extract_12_4(offset); @@ -301,7 +451,7 @@ contract PackingTest is Test { assertEq(container, container.replace_12_4(newValue, offset).replace_12_4(oldValue, offset)); } - function testReplace(bytes12 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes12 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 6)); bytes6 oldValue = container.extract_12_6(offset); @@ -310,7 +460,7 @@ contract PackingTest is Test { assertEq(container, container.replace_12_6(newValue, offset).replace_12_6(oldValue, offset)); } - function testReplace(bytes12 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes12 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes8 oldValue = container.extract_12_8(offset); @@ -319,7 +469,16 @@ contract PackingTest is Test { assertEq(container, container.replace_12_8(newValue, offset).replace_12_8(oldValue, offset)); } - function testReplace(bytes16 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes12 container, bytes10 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 2)); + + bytes10 oldValue = container.extract_12_10(offset); + + assertEq(newValue, container.replace_12_10(newValue, offset).extract_12_10(offset)); + assertEq(container, container.replace_12_10(newValue, offset).replace_12_10(oldValue, offset)); + } + + function testReplace(bytes16 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 15)); bytes1 oldValue = container.extract_16_1(offset); @@ -328,7 +487,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_1(newValue, offset).replace_16_1(oldValue, offset)); } - function testReplace(bytes16 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 14)); bytes2 oldValue = container.extract_16_2(offset); @@ -337,7 +496,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_2(newValue, offset).replace_16_2(oldValue, offset)); } - function testReplace(bytes16 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 12)); bytes4 oldValue = container.extract_16_4(offset); @@ -346,7 +505,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_4(newValue, offset).replace_16_4(oldValue, offset)); } - function testReplace(bytes16 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 10)); bytes6 oldValue = container.extract_16_6(offset); @@ -355,7 +514,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_6(newValue, offset).replace_16_6(oldValue, offset)); } - function testReplace(bytes16 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes8 oldValue = container.extract_16_8(offset); @@ -364,7 +523,16 @@ contract PackingTest is Test { assertEq(container, container.replace_16_8(newValue, offset).replace_16_8(oldValue, offset)); } - function testReplace(bytes16 container, bytes12 newValue, uint8 offset) external { + function testReplace(bytes16 container, bytes10 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 6)); + + bytes10 oldValue = container.extract_16_10(offset); + + assertEq(newValue, container.replace_16_10(newValue, offset).extract_16_10(offset)); + assertEq(container, container.replace_16_10(newValue, offset).replace_16_10(oldValue, offset)); + } + + function testReplace(bytes16 container, bytes12 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes12 oldValue = container.extract_16_12(offset); @@ -373,7 +541,7 @@ contract PackingTest is Test { assertEq(container, container.replace_16_12(newValue, offset).replace_16_12(oldValue, offset)); } - function testReplace(bytes20 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 19)); bytes1 oldValue = container.extract_20_1(offset); @@ -382,7 +550,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_1(newValue, offset).replace_20_1(oldValue, offset)); } - function testReplace(bytes20 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 18)); bytes2 oldValue = container.extract_20_2(offset); @@ -391,7 +559,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_2(newValue, offset).replace_20_2(oldValue, offset)); } - function testReplace(bytes20 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 16)); bytes4 oldValue = container.extract_20_4(offset); @@ -400,7 +568,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_4(newValue, offset).replace_20_4(oldValue, offset)); } - function testReplace(bytes20 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 14)); bytes6 oldValue = container.extract_20_6(offset); @@ -409,7 +577,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_6(newValue, offset).replace_20_6(oldValue, offset)); } - function testReplace(bytes20 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 12)); bytes8 oldValue = container.extract_20_8(offset); @@ -418,7 +586,16 @@ contract PackingTest is Test { assertEq(container, container.replace_20_8(newValue, offset).replace_20_8(oldValue, offset)); } - function testReplace(bytes20 container, bytes12 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes10 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 10)); + + bytes10 oldValue = container.extract_20_10(offset); + + assertEq(newValue, container.replace_20_10(newValue, offset).extract_20_10(offset)); + assertEq(container, container.replace_20_10(newValue, offset).replace_20_10(oldValue, offset)); + } + + function testReplace(bytes20 container, bytes12 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes12 oldValue = container.extract_20_12(offset); @@ -427,7 +604,7 @@ contract PackingTest is Test { assertEq(container, container.replace_20_12(newValue, offset).replace_20_12(oldValue, offset)); } - function testReplace(bytes20 container, bytes16 newValue, uint8 offset) external { + function testReplace(bytes20 container, bytes16 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes16 oldValue = container.extract_20_16(offset); @@ -436,7 +613,88 @@ contract PackingTest is Test { assertEq(container, container.replace_20_16(newValue, offset).replace_20_16(oldValue, offset)); } - function testReplace(bytes24 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes22 container, bytes1 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 21)); + + bytes1 oldValue = container.extract_22_1(offset); + + assertEq(newValue, container.replace_22_1(newValue, offset).extract_22_1(offset)); + assertEq(container, container.replace_22_1(newValue, offset).replace_22_1(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes2 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 20)); + + bytes2 oldValue = container.extract_22_2(offset); + + assertEq(newValue, container.replace_22_2(newValue, offset).extract_22_2(offset)); + assertEq(container, container.replace_22_2(newValue, offset).replace_22_2(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes4 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 18)); + + bytes4 oldValue = container.extract_22_4(offset); + + assertEq(newValue, container.replace_22_4(newValue, offset).extract_22_4(offset)); + assertEq(container, container.replace_22_4(newValue, offset).replace_22_4(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes6 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 16)); + + bytes6 oldValue = container.extract_22_6(offset); + + assertEq(newValue, container.replace_22_6(newValue, offset).extract_22_6(offset)); + assertEq(container, container.replace_22_6(newValue, offset).replace_22_6(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes8 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 14)); + + bytes8 oldValue = container.extract_22_8(offset); + + assertEq(newValue, container.replace_22_8(newValue, offset).extract_22_8(offset)); + assertEq(container, container.replace_22_8(newValue, offset).replace_22_8(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes10 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 12)); + + bytes10 oldValue = container.extract_22_10(offset); + + assertEq(newValue, container.replace_22_10(newValue, offset).extract_22_10(offset)); + assertEq(container, container.replace_22_10(newValue, offset).replace_22_10(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes12 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 10)); + + bytes12 oldValue = container.extract_22_12(offset); + + assertEq(newValue, container.replace_22_12(newValue, offset).extract_22_12(offset)); + assertEq(container, container.replace_22_12(newValue, offset).replace_22_12(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes16 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 6)); + + bytes16 oldValue = container.extract_22_16(offset); + + assertEq(newValue, container.replace_22_16(newValue, offset).extract_22_16(offset)); + assertEq(container, container.replace_22_16(newValue, offset).replace_22_16(oldValue, offset)); + } + + function testReplace(bytes22 container, bytes20 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 2)); + + bytes20 oldValue = container.extract_22_20(offset); + + assertEq(newValue, container.replace_22_20(newValue, offset).extract_22_20(offset)); + assertEq(container, container.replace_22_20(newValue, offset).replace_22_20(oldValue, offset)); + } + + function testReplace(bytes24 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 23)); bytes1 oldValue = container.extract_24_1(offset); @@ -445,7 +703,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_1(newValue, offset).replace_24_1(oldValue, offset)); } - function testReplace(bytes24 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 22)); bytes2 oldValue = container.extract_24_2(offset); @@ -454,7 +712,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_2(newValue, offset).replace_24_2(oldValue, offset)); } - function testReplace(bytes24 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 20)); bytes4 oldValue = container.extract_24_4(offset); @@ -463,7 +721,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_4(newValue, offset).replace_24_4(oldValue, offset)); } - function testReplace(bytes24 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 18)); bytes6 oldValue = container.extract_24_6(offset); @@ -472,7 +730,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_6(newValue, offset).replace_24_6(oldValue, offset)); } - function testReplace(bytes24 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 16)); bytes8 oldValue = container.extract_24_8(offset); @@ -481,7 +739,16 @@ contract PackingTest is Test { assertEq(container, container.replace_24_8(newValue, offset).replace_24_8(oldValue, offset)); } - function testReplace(bytes24 container, bytes12 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes10 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 14)); + + bytes10 oldValue = container.extract_24_10(offset); + + assertEq(newValue, container.replace_24_10(newValue, offset).extract_24_10(offset)); + assertEq(container, container.replace_24_10(newValue, offset).replace_24_10(oldValue, offset)); + } + + function testReplace(bytes24 container, bytes12 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 12)); bytes12 oldValue = container.extract_24_12(offset); @@ -490,7 +757,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_12(newValue, offset).replace_24_12(oldValue, offset)); } - function testReplace(bytes24 container, bytes16 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes16 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes16 oldValue = container.extract_24_16(offset); @@ -499,7 +766,7 @@ contract PackingTest is Test { assertEq(container, container.replace_24_16(newValue, offset).replace_24_16(oldValue, offset)); } - function testReplace(bytes24 container, bytes20 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes20 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes20 oldValue = container.extract_24_20(offset); @@ -508,7 +775,16 @@ contract PackingTest is Test { assertEq(container, container.replace_24_20(newValue, offset).replace_24_20(oldValue, offset)); } - function testReplace(bytes28 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes24 container, bytes22 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 2)); + + bytes22 oldValue = container.extract_24_22(offset); + + assertEq(newValue, container.replace_24_22(newValue, offset).extract_24_22(offset)); + assertEq(container, container.replace_24_22(newValue, offset).replace_24_22(oldValue, offset)); + } + + function testReplace(bytes28 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 27)); bytes1 oldValue = container.extract_28_1(offset); @@ -517,7 +793,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_1(newValue, offset).replace_28_1(oldValue, offset)); } - function testReplace(bytes28 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 26)); bytes2 oldValue = container.extract_28_2(offset); @@ -526,7 +802,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_2(newValue, offset).replace_28_2(oldValue, offset)); } - function testReplace(bytes28 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 24)); bytes4 oldValue = container.extract_28_4(offset); @@ -535,7 +811,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_4(newValue, offset).replace_28_4(oldValue, offset)); } - function testReplace(bytes28 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 22)); bytes6 oldValue = container.extract_28_6(offset); @@ -544,7 +820,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_6(newValue, offset).replace_28_6(oldValue, offset)); } - function testReplace(bytes28 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 20)); bytes8 oldValue = container.extract_28_8(offset); @@ -553,7 +829,16 @@ contract PackingTest is Test { assertEq(container, container.replace_28_8(newValue, offset).replace_28_8(oldValue, offset)); } - function testReplace(bytes28 container, bytes12 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes10 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 18)); + + bytes10 oldValue = container.extract_28_10(offset); + + assertEq(newValue, container.replace_28_10(newValue, offset).extract_28_10(offset)); + assertEq(container, container.replace_28_10(newValue, offset).replace_28_10(oldValue, offset)); + } + + function testReplace(bytes28 container, bytes12 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 16)); bytes12 oldValue = container.extract_28_12(offset); @@ -562,7 +847,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_12(newValue, offset).replace_28_12(oldValue, offset)); } - function testReplace(bytes28 container, bytes16 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes16 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 12)); bytes16 oldValue = container.extract_28_16(offset); @@ -571,7 +856,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_16(newValue, offset).replace_28_16(oldValue, offset)); } - function testReplace(bytes28 container, bytes20 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes20 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes20 oldValue = container.extract_28_20(offset); @@ -580,7 +865,16 @@ contract PackingTest is Test { assertEq(container, container.replace_28_20(newValue, offset).replace_28_20(oldValue, offset)); } - function testReplace(bytes28 container, bytes24 newValue, uint8 offset) external { + function testReplace(bytes28 container, bytes22 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 6)); + + bytes22 oldValue = container.extract_28_22(offset); + + assertEq(newValue, container.replace_28_22(newValue, offset).extract_28_22(offset)); + assertEq(container, container.replace_28_22(newValue, offset).replace_28_22(oldValue, offset)); + } + + function testReplace(bytes28 container, bytes24 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes24 oldValue = container.extract_28_24(offset); @@ -589,7 +883,7 @@ contract PackingTest is Test { assertEq(container, container.replace_28_24(newValue, offset).replace_28_24(oldValue, offset)); } - function testReplace(bytes32 container, bytes1 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes1 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 31)); bytes1 oldValue = container.extract_32_1(offset); @@ -598,7 +892,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_1(newValue, offset).replace_32_1(oldValue, offset)); } - function testReplace(bytes32 container, bytes2 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes2 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 30)); bytes2 oldValue = container.extract_32_2(offset); @@ -607,7 +901,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_2(newValue, offset).replace_32_2(oldValue, offset)); } - function testReplace(bytes32 container, bytes4 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes4 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 28)); bytes4 oldValue = container.extract_32_4(offset); @@ -616,7 +910,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_4(newValue, offset).replace_32_4(oldValue, offset)); } - function testReplace(bytes32 container, bytes6 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes6 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 26)); bytes6 oldValue = container.extract_32_6(offset); @@ -625,7 +919,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_6(newValue, offset).replace_32_6(oldValue, offset)); } - function testReplace(bytes32 container, bytes8 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes8 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 24)); bytes8 oldValue = container.extract_32_8(offset); @@ -634,7 +928,16 @@ contract PackingTest is Test { assertEq(container, container.replace_32_8(newValue, offset).replace_32_8(oldValue, offset)); } - function testReplace(bytes32 container, bytes12 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes10 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 22)); + + bytes10 oldValue = container.extract_32_10(offset); + + assertEq(newValue, container.replace_32_10(newValue, offset).extract_32_10(offset)); + assertEq(container, container.replace_32_10(newValue, offset).replace_32_10(oldValue, offset)); + } + + function testReplace(bytes32 container, bytes12 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 20)); bytes12 oldValue = container.extract_32_12(offset); @@ -643,7 +946,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_12(newValue, offset).replace_32_12(oldValue, offset)); } - function testReplace(bytes32 container, bytes16 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes16 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 16)); bytes16 oldValue = container.extract_32_16(offset); @@ -652,7 +955,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_16(newValue, offset).replace_32_16(oldValue, offset)); } - function testReplace(bytes32 container, bytes20 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes20 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 12)); bytes20 oldValue = container.extract_32_20(offset); @@ -661,7 +964,16 @@ contract PackingTest is Test { assertEq(container, container.replace_32_20(newValue, offset).replace_32_20(oldValue, offset)); } - function testReplace(bytes32 container, bytes24 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes22 newValue, uint8 offset) external pure { + offset = uint8(bound(offset, 0, 10)); + + bytes22 oldValue = container.extract_32_22(offset); + + assertEq(newValue, container.replace_32_22(newValue, offset).extract_32_22(offset)); + assertEq(container, container.replace_32_22(newValue, offset).replace_32_22(oldValue, offset)); + } + + function testReplace(bytes32 container, bytes24 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 8)); bytes24 oldValue = container.extract_32_24(offset); @@ -670,7 +982,7 @@ contract PackingTest is Test { assertEq(container, container.replace_32_24(newValue, offset).replace_32_24(oldValue, offset)); } - function testReplace(bytes32 container, bytes28 newValue, uint8 offset) external { + function testReplace(bytes32 container, bytes28 newValue, uint8 offset) external pure { offset = uint8(bound(offset, 0, 4)); bytes28 oldValue = container.extract_32_28(offset); diff --git a/test/utils/ShortStrings.t.sol b/test/utils/ShortStrings.t.sol index 4aeafd721a8..80313bf7cd1 100644 --- a/test/utils/ShortStrings.t.sol +++ b/test/utils/ShortStrings.t.sol @@ -10,12 +10,12 @@ import {ShortStrings, ShortString} from "@openzeppelin/contracts/utils/ShortStri contract ShortStringsTest is Test, SymTest { string _fallback; - function testRoundtripShort(string memory input) external { + function testRoundtripShort(string memory input) external pure { vm.assume(_isShort(input)); _assertRoundtripShort(input); } - function symbolicRoundtripShort() external { + function symbolicRoundtripShort() external pure { string memory input = svm.createString(31, "RoundtripShortInput"); _assertRoundtripShort(input); } @@ -41,12 +41,12 @@ contract ShortStringsTest is Test, SymTest { _assertRevertLong(input); } - function testLengthShort(string memory input) external { + function testLengthShort(string memory input) external pure { vm.assume(_isShort(input)); _assertLengthShort(input); } - function symbolicLengthShort() external { + function symbolicLengthShort() external pure { string memory input = svm.createString(31, "LengthShortInput"); _assertLengthShort(input); } @@ -66,7 +66,7 @@ contract ShortStringsTest is Test, SymTest { /// Assertions - function _assertRoundtripShort(string memory input) internal { + function _assertRoundtripShort(string memory input) internal pure { ShortString short = ShortStrings.toShortString(input); string memory output = ShortStrings.toString(short); assertEq(input, output); @@ -84,7 +84,7 @@ contract ShortStringsTest is Test, SymTest { this.toShortString(input); } - function _assertLengthShort(string memory input) internal { + function _assertLengthShort(string memory input) internal pure { ShortString short = ShortStrings.toShortString(input); uint256 shortLength = ShortStrings.byteLength(short); uint256 inputLength = bytes(input).length; diff --git a/test/utils/SlotDerivation.t.sol b/test/utils/SlotDerivation.t.sol index 300a58b5f14..a0846e33212 100644 --- a/test/utils/SlotDerivation.t.sol +++ b/test/utils/SlotDerivation.t.sol @@ -42,7 +42,7 @@ contract SlotDerivationTest is Test, SymTest { mapping(address => bytes) private _addressMapping; - function testSymbolicDeriveMappingAddress(address key) public { + function testSymbolicDeriveMappingAddress(address key) public view { bytes32 baseSlot; assembly { baseSlot := _addressMapping.slot @@ -59,7 +59,7 @@ contract SlotDerivationTest is Test, SymTest { mapping(bool => bytes) private _boolMapping; - function testSymbolicDeriveMappingBoolean(bool key) public { + function testSymbolicDeriveMappingBoolean(bool key) public view { bytes32 baseSlot; assembly { baseSlot := _boolMapping.slot @@ -76,7 +76,7 @@ contract SlotDerivationTest is Test, SymTest { mapping(bytes32 => bytes) private _bytes32Mapping; - function testSymbolicDeriveMappingBytes32(bytes32 key) public { + function testSymbolicDeriveMappingBytes32(bytes32 key) public view { bytes32 baseSlot; assembly { baseSlot := _bytes32Mapping.slot @@ -93,7 +93,7 @@ contract SlotDerivationTest is Test, SymTest { mapping(bytes4 => bytes) private _bytes4Mapping; - function testSymbolicDeriveMappingBytes4(bytes4 key) public { + function testSymbolicDeriveMappingBytes4(bytes4 key) public view { bytes32 baseSlot; assembly { baseSlot := _bytes4Mapping.slot @@ -110,7 +110,7 @@ contract SlotDerivationTest is Test, SymTest { mapping(uint256 => bytes) private _uint256Mapping; - function testSymbolicDeriveMappingUint256(uint256 key) public { + function testSymbolicDeriveMappingUint256(uint256 key) public view { bytes32 baseSlot; assembly { baseSlot := _uint256Mapping.slot @@ -127,7 +127,7 @@ contract SlotDerivationTest is Test, SymTest { mapping(uint32 => bytes) private _uint32Mapping; - function testSymbolicDeriveMappingUint32(uint32 key) public { + function testSymbolicDeriveMappingUint32(uint32 key) public view { bytes32 baseSlot; assembly { baseSlot := _uint32Mapping.slot @@ -144,7 +144,7 @@ contract SlotDerivationTest is Test, SymTest { mapping(int256 => bytes) private _int256Mapping; - function testSymbolicDeriveMappingInt256(int256 key) public { + function testSymbolicDeriveMappingInt256(int256 key) public view { bytes32 baseSlot; assembly { baseSlot := _int256Mapping.slot @@ -161,7 +161,7 @@ contract SlotDerivationTest is Test, SymTest { mapping(int32 => bytes) private _int32Mapping; - function testSymbolicDeriveMappingInt32(int32 key) public { + function testSymbolicDeriveMappingInt32(int32 key) public view { bytes32 baseSlot; assembly { baseSlot := _int32Mapping.slot @@ -178,15 +178,15 @@ contract SlotDerivationTest is Test, SymTest { mapping(string => bytes) private _stringMapping; - function testDeriveMappingString(string memory key) public { + function testDeriveMappingString(string memory key) public view { _assertDeriveMappingString(key); } - function symbolicDeriveMappingString() public { + function symbolicDeriveMappingString() public view { _assertDeriveMappingString(svm.createString(256, "DeriveMappingStringInput")); } - function _assertDeriveMappingString(string memory key) internal { + function _assertDeriveMappingString(string memory key) internal view { bytes32 baseSlot; assembly { baseSlot := _stringMapping.slot @@ -203,15 +203,15 @@ contract SlotDerivationTest is Test, SymTest { mapping(bytes => bytes) private _bytesMapping; - function testDeriveMappingBytes(bytes memory key) public { + function testDeriveMappingBytes(bytes memory key) public view { _assertDeriveMappingBytes(key); } - function symbolicDeriveMappingBytes() public { + function symbolicDeriveMappingBytes() public view { _assertDeriveMappingBytes(svm.createBytes(256, "DeriveMappingBytesInput")); } - function _assertDeriveMappingBytes(bytes memory key) internal { + function _assertDeriveMappingBytes(bytes memory key) internal view { bytes32 baseSlot; assembly { baseSlot := _bytesMapping.slot @@ -225,4 +225,24 @@ contract SlotDerivationTest is Test, SymTest { assertEq(baseSlot.deriveMapping(key), derivedSlot); } + + function testSymbolicDeriveMappingBooleanDirty(bytes32 dirtyKey) public { + bool key; + assembly { + key := dirtyKey + } + + // run the "normal" test using a potentially dirty value + testSymbolicDeriveMappingBoolean(key); + } + + function testSymbolicDeriveMappingAddressDirty(bytes32 dirtyKey) public { + address key; + assembly { + key := dirtyKey + } + + // run the "normal" test using a potentially dirty value + testSymbolicDeriveMappingAddress(key); + } } diff --git a/test/utils/StorageSlot.test.js b/test/utils/StorageSlot.test.js index 35e83e29e63..ddcf305d1a7 100644 --- a/test/utils/StorageSlot.test.js +++ b/test/utils/StorageSlot.test.js @@ -70,37 +70,4 @@ describe('StorageSlot', function () { }); }); } - - for (const { name, type, value, zero } of TYPES.filter(type => type.isValueType)) { - describe(`${type} transient slot`, function () { - const load = `tload${name}(bytes32)`; - const store = `tstore(bytes32,${type})`; - const event = `${name}Value`; - - it('load', async function () { - await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero); - }); - - it('store and load (2 txs)', async function () { - await this.mock[store](slot, value); - await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero); - }); - - it('store and load (batched)', async function () { - await expect( - this.mock.multicall([ - this.mock.interface.encodeFunctionData(store, [slot, value]), - this.mock.interface.encodeFunctionData(load, [slot]), - this.mock.interface.encodeFunctionData(load, [otherSlot]), - ]), - ) - .to.emit(this.mock, event) - .withArgs(slot, value) - .to.emit(this.mock, event) - .withArgs(otherSlot, zero); - - await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero); - }); - }); - } }); diff --git a/test/utils/Strings.t.sol b/test/utils/Strings.t.sol new file mode 100644 index 00000000000..fe3c90bd115 --- /dev/null +++ b/test/utils/Strings.t.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +contract StringsTest is Test { + using Strings for *; + + function testParse(uint256 value) external pure { + assertEq(value, value.toString().parseUint()); + } + + function testParseSigned(int256 value) external pure { + assertEq(value, value.toStringSigned().parseInt()); + } + + function testParseHex(uint256 value) external pure { + assertEq(value, value.toHexString().parseHexUint()); + } + + function testParseChecksumHex(address value) external pure { + assertEq(value, value.toChecksumHexString().parseAddress()); + } + + function testTryParseHexUintExtendedEnd(string memory random) external pure { + uint256 length = bytes(random).length; + assembly ("memory-safe") { + mstore(add(add(random, 0x20), length), 0x3030303030303030303030303030303030303030303030303030303030303030) + } + + (bool success, ) = random.tryParseHexUint(1, length + 1); + assertFalse(success); + } + + function testTryParseAddressExtendedEnd(address random, uint256 begin) external pure { + begin = bound(begin, 3, 43); + string memory input = random.toHexString(); + uint256 length = bytes(input).length; + + assembly ("memory-safe") { + mstore(add(add(input, 0x20), length), 0x3030303030303030303030303030303030303030303030303030303030303030) + } + + (bool success, ) = input.tryParseAddress(begin, begin + 40); + assertFalse(success); + } +} diff --git a/test/utils/Strings.test.js b/test/utils/Strings.test.js index 6353fd886db..0b2d87190d3 100644 --- a/test/utils/Strings.test.js +++ b/test/utils/Strings.test.js @@ -1,6 +1,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); async function fixture() { const mock = await ethers.deployContract('$Strings'); @@ -38,11 +39,15 @@ describe('Strings', function () { it('converts MAX_UINT256', async function () { const value = ethers.MaxUint256; expect(await this.mock.$toString(value)).to.equal(value.toString(10)); + expect(await this.mock.$parseUint(value.toString(10))).to.equal(value); + expect(await this.mock.$tryParseUint(value.toString(10))).to.deep.equal([true, value]); }); for (const value of values) { it(`converts ${value}`, async function () { - expect(await this.mock.$toString(value)).to.equal(value); + expect(await this.mock.$toString(value)).to.equal(value.toString(10)); + expect(await this.mock.$parseUint(value.toString(10))).to.equal(value); + expect(await this.mock.$tryParseUint(value.toString(10))).to.deep.equal([true, value]); }); } }); @@ -51,21 +56,29 @@ describe('Strings', function () { it('converts MAX_INT256', async function () { const value = ethers.MaxInt256; expect(await this.mock.$toStringSigned(value)).to.equal(value.toString(10)); + expect(await this.mock.$parseInt(value.toString(10))).to.equal(value); + expect(await this.mock.$tryParseInt(value.toString(10))).to.deep.equal([true, value]); }); it('converts MIN_INT256', async function () { const value = ethers.MinInt256; expect(await this.mock.$toStringSigned(value)).to.equal(value.toString(10)); + expect(await this.mock.$parseInt(value.toString(10))).to.equal(value); + expect(await this.mock.$tryParseInt(value.toString(10))).to.deep.equal([true, value]); }); for (const value of values) { it(`convert ${value}`, async function () { - expect(await this.mock.$toStringSigned(value)).to.equal(value); + expect(await this.mock.$toStringSigned(value)).to.equal(value.toString(10)); + expect(await this.mock.$parseInt(value.toString(10))).to.equal(value); + expect(await this.mock.$tryParseInt(value.toString(10))).to.deep.equal([true, value]); }); it(`convert negative ${value}`, async function () { const negated = -value; expect(await this.mock.$toStringSigned(negated)).to.equal(negated.toString(10)); + expect(await this.mock.$parseInt(negated.toString(10))).to.equal(negated); + expect(await this.mock.$tryParseInt(negated.toString(10))).to.deep.equal([true, negated]); }); } }); @@ -73,17 +86,36 @@ describe('Strings', function () { describe('toHexString', function () { it('converts 0', async function () { - expect(await this.mock.getFunction('$toHexString(uint256)')(0n)).to.equal('0x00'); + const value = 0n; + const string = ethers.toBeHex(value); // 0x00 + + expect(await this.mock.getFunction('$toHexString(uint256)')(value)).to.equal(string); + expect(await this.mock.$parseHexUint(string)).to.equal(value); + expect(await this.mock.$parseHexUint(string.replace(/0x/, ''))).to.equal(value); + expect(await this.mock.$tryParseHexUint(string)).to.deep.equal([true, value]); + expect(await this.mock.$tryParseHexUint(string.replace(/0x/, ''))).to.deep.equal([true, value]); }); it('converts a positive number', async function () { - expect(await this.mock.getFunction('$toHexString(uint256)')(0x4132n)).to.equal('0x4132'); + const value = 0x4132n; + const string = ethers.toBeHex(value); + + expect(await this.mock.getFunction('$toHexString(uint256)')(value)).to.equal(string); + expect(await this.mock.$parseHexUint(string)).to.equal(value); + expect(await this.mock.$parseHexUint(string.replace(/0x/, ''))).to.equal(value); + expect(await this.mock.$tryParseHexUint(string)).to.deep.equal([true, value]); + expect(await this.mock.$tryParseHexUint(string.replace(/0x/, ''))).to.deep.equal([true, value]); }); it('converts MAX_UINT256', async function () { - expect(await this.mock.getFunction('$toHexString(uint256)')(ethers.MaxUint256)).to.equal( - `0x${ethers.MaxUint256.toString(16)}`, - ); + const value = ethers.MaxUint256; + const string = ethers.toBeHex(value); + + expect(await this.mock.getFunction('$toHexString(uint256)')(value)).to.equal(string); + expect(await this.mock.$parseHexUint(string)).to.equal(value); + expect(await this.mock.$parseHexUint(string.replace(/0x/, ''))).to.equal(value); + expect(await this.mock.$tryParseHexUint(string)).to.deep.equal([true, value]); + expect(await this.mock.$tryParseHexUint(string.replace(/0x/, ''))).to.deep.equal([true, value]); }); }); @@ -97,13 +129,13 @@ describe('Strings', function () { it('converts a positive number (short)', async function () { const length = 1n; await expect(this.mock.getFunction('$toHexString(uint256,uint256)')(0x4132n, length)) - .to.be.revertedWithCustomError(this.mock, `StringsInsufficientHexLength`) + .to.be.revertedWithCustomError(this.mock, 'StringsInsufficientHexLength') .withArgs(0x4132, length); }); it('converts MAX_UINT256', async function () { expect(await this.mock.getFunction('$toHexString(uint256,uint256)')(ethers.MaxUint256, 32n)).to.equal( - `0x${ethers.MaxUint256.toString(16)}`, + ethers.toBeHex(ethers.MaxUint256), ); }); }); @@ -139,9 +171,16 @@ describe('Strings', function () { describe('toChecksumHexString', function () { for (const addr of addresses) { it(`converts ${addr}`, async function () { - expect(await this.mock.getFunction('$toChecksumHexString(address)')(addr)).to.equal( - ethers.getAddress(addr.toLowerCase()), - ); + expect(await this.mock.$toChecksumHexString(addr)).to.equal(ethers.getAddress(addr)); + }); + } + }); + + describe('parseAddress', function () { + for (const addr of addresses) { + it(`converts ${addr}`, async function () { + expect(await this.mock.$parseAddress(addr)).to.equal(ethers.getAddress(addr)); + expect(await this.mock.$tryParseAddress(addr)).to.deep.equal([true, ethers.getAddress(addr)]); }); } }); @@ -177,4 +216,127 @@ describe('Strings', function () { expect(await this.mock.$equal(str1, str2)).to.be.true; }); }); + + describe('Edge cases: invalid parsing', function () { + it('parseUint overflow', async function () { + await expect(this.mock.$parseUint((ethers.MaxUint256 + 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$tryParseUint((ethers.MaxUint256 + 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + }); + + it('parseUint invalid character', async function () { + await expect(this.mock.$parseUint('0x1')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseUint('1f')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseUint('-10')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseUint('1.0')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseUint('1 000')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + expect(await this.mock.$tryParseUint('0x1')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseUint('1f')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseUint('-10')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseUint('1.0')).deep.equal([false, 0n]); + expect(await this.mock.$tryParseUint('1 000')).deep.equal([false, 0n]); + }); + + it('parseUint invalid range', async function () { + expect(this.mock.$parseUint('12', 3, 2)).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + expect(await this.mock.$tryParseUint('12', 3, 2)).to.deep.equal([false, 0n]); + }); + + it('parseInt overflow', async function () { + await expect(this.mock.$parseInt((ethers.MaxUint256 + 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$parseInt((-ethers.MaxUint256 - 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$tryParseInt((ethers.MaxUint256 + 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$tryParseInt((-ethers.MaxUint256 - 1n).toString(10))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$parseInt((ethers.MaxInt256 + 1n).toString(10))).to.be.revertedWithCustomError( + this.mock, + 'StringsInvalidChar', + ); + await expect(this.mock.$parseInt((ethers.MinInt256 - 1n).toString(10))).to.be.revertedWithCustomError( + this.mock, + 'StringsInvalidChar', + ); + expect(await this.mock.$tryParseInt((ethers.MaxInt256 + 1n).toString(10))).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseInt((ethers.MinInt256 - 1n).toString(10))).to.deep.equal([false, 0n]); + }); + + it('parseInt invalid character', async function () { + await expect(this.mock.$parseInt('0x1')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseInt('1f')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseInt('1.0')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseInt('1 000')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + expect(await this.mock.$tryParseInt('0x1')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseInt('1f')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseInt('1.0')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseInt('1 000')).to.deep.equal([false, 0n]); + }); + + it('parseInt invalid range', async function () { + expect(this.mock.$parseInt('-12', 3, 2)).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + expect(await this.mock.$tryParseInt('-12', 3, 2)).to.deep.equal([false, 0n]); + }); + + it('parseHexUint overflow', async function () { + await expect(this.mock.$parseHexUint((ethers.MaxUint256 + 1n).toString(16))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + await expect(this.mock.$tryParseHexUint((ethers.MaxUint256 + 1n).toString(16))).to.be.revertedWithPanic( + PANIC_CODES.ARITHMETIC_OVERFLOW, + ); + }); + + it('parseHexUint invalid character', async function () { + await expect(this.mock.$parseHexUint('0123456789abcdefg')).to.be.revertedWithCustomError( + this.mock, + 'StringsInvalidChar', + ); + await expect(this.mock.$parseHexUint('-1')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseHexUint('-f')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseHexUint('-0xf')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseHexUint('1.0')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + await expect(this.mock.$parseHexUint('1 000')).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + expect(await this.mock.$tryParseHexUint('0123456789abcdefg')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseHexUint('-1')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseHexUint('-f')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseHexUint('-0xf')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseHexUint('1.0')).to.deep.equal([false, 0n]); + expect(await this.mock.$tryParseHexUint('1 000')).to.deep.equal([false, 0n]); + }); + + it('parseHexUint invalid begin and end', async function () { + expect(this.mock.$parseHexUint('0x', 3, 2)).to.be.revertedWithCustomError(this.mock, 'StringsInvalidChar'); + expect(await this.mock.$tryParseHexUint('0x', 3, 2)).to.deep.equal([false, 0n]); + }); + + it('parseAddress invalid format', async function () { + for (const addr of [ + '0x736a507fB2881d6bB62dcA54673CF5295dC07833', // valid + '0x736a507fB2881d6-B62dcA54673CF5295dC07833', // invalid char + '0x0736a507fB2881d6bB62dcA54673CF5295dC07833', // tooLong + '0x36a507fB2881d6bB62dcA54673CF5295dC07833', // tooShort + '736a507fB2881d6bB62dcA54673CF5295dC07833', // missingPrefix - supported + ]) { + if (ethers.isAddress(addr)) { + expect(await this.mock.$parseAddress(addr)).to.equal(ethers.getAddress(addr)); + expect(await this.mock.$tryParseAddress(addr)).to.deep.equal([true, ethers.getAddress(addr)]); + } else { + await expect(this.mock.$parseAddress(addr)).to.be.revertedWithCustomError( + this.mock, + 'StringsInvalidAddressFormat', + ); + expect(await this.mock.$tryParseAddress(addr)).to.deep.equal([false, ethers.ZeroAddress]); + } + } + }); + }); }); diff --git a/test/utils/TransientSlot.test.js b/test/utils/TransientSlot.test.js new file mode 100644 index 00000000000..7b70be375d4 --- /dev/null +++ b/test/utils/TransientSlot.test.js @@ -0,0 +1,59 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { generators } = require('../helpers/random'); + +const slot = ethers.id('some.storage.slot'); +const otherSlot = ethers.id('some.other.storage.slot'); + +// Non-value types are not supported by the `TransientSlot` library. +const TYPES = [ + { name: 'Boolean', type: 'bool', value: true, zero: false }, + { name: 'Address', type: 'address', value: generators.address(), zero: generators.address.zero }, + { name: 'Bytes32', type: 'bytes32', value: generators.bytes32(), zero: generators.bytes32.zero }, + { name: 'Uint256', type: 'uint256', value: generators.uint256(), zero: generators.uint256.zero }, + { name: 'Int256', type: 'int256', value: generators.int256(), zero: generators.int256.zero }, +]; + +async function fixture() { + return { mock: await ethers.deployContract('TransientSlotMock') }; +} + +describe('TransientSlot', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + for (const { name, type, value, zero } of TYPES) { + describe(`${type} transient slot`, function () { + const load = `tload${name}(bytes32)`; + const store = `tstore(bytes32,${type})`; + const event = `${name}Value`; + + it('load', async function () { + await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero); + }); + + it('store and load (2 txs)', async function () { + await this.mock[store](slot, value); + await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero); + }); + + it('store and load (batched)', async function () { + await expect( + this.mock.multicall([ + this.mock.interface.encodeFunctionData(store, [slot, value]), + this.mock.interface.encodeFunctionData(load, [slot]), + this.mock.interface.encodeFunctionData(load, [otherSlot]), + ]), + ) + .to.emit(this.mock, event) + .withArgs(slot, value) + .to.emit(this.mock, event) + .withArgs(otherSlot, zero); + + await expect(this.mock[load](slot)).to.emit(this.mock, event).withArgs(slot, zero); + }); + }); + } +}); diff --git a/test/utils/cryptography/ECDSA.test.js b/test/utils/cryptography/ECDSA.test.js index 6b24bdbceaa..0f2879a86c4 100644 --- a/test/utils/cryptography/ECDSA.test.js +++ b/test/utils/cryptography/ECDSA.test.js @@ -26,7 +26,6 @@ describe('ECDSA', function () { it('with long signature', async function () { await expect( - // eslint-disable-next-line max-len this.mock.$recover( TEST_MESSAGE, '0x01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789', @@ -61,7 +60,6 @@ describe('ECDSA', function () { }); it('reverts with invalid signature', async function () { - // eslint-disable-next-line max-len const signature = '0x332ce75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e01c'; await expect(this.mock.$recover(TEST_MESSAGE, signature)).to.be.revertedWithCustomError( @@ -73,7 +71,7 @@ describe('ECDSA', function () { describe('with v=27 signature', function () { const signer = '0x2cc1166f6212628A0deEf2B33BEFB2187D35b86c'; - // eslint-disable-next-line max-len + const signatureWithoutV = '0x5d99b6f7f6d1f73d1a26497f2b1c89b24c0993913f86e9a2d02cd69887d9c94f3c880358579d811b21dd1b7fd9bb01c1d81d10e69f0384e675c32b39643be892'; @@ -133,7 +131,7 @@ describe('ECDSA', function () { describe('with v=28 signature', function () { const signer = '0x1E318623aB09Fe6de3C9b8672098464Aeda9100E'; - // eslint-disable-next-line max-len + const signatureWithoutV = '0x331fe75a821c982f9127538858900d87d3ec1f9f737338ad67cad133fa48feff48e6fa0c18abc62e42820f05943e47af3e9fbe306ce74d64094bdf1691ee53e0'; @@ -193,7 +191,7 @@ describe('ECDSA', function () { it('reverts with high-s value signature', async function () { const message = '0xb94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9'; - // eslint-disable-next-line max-len + const highSSignature = '0xe742ff452d41413616a5bf43fe15dd88294e983d3d36206c2712f39083d638bde0a0fc89be718fbc1033e1d30d78be1c68081562ed2e97af876f286f3453231d1b'; diff --git a/test/utils/cryptography/P256.t.sol b/test/utils/cryptography/P256.t.sol index 1391afd76ef..d70cdf1bf2d 100644 --- a/test/utils/cryptography/P256.t.sol +++ b/test/utils/cryptography/P256.t.sol @@ -9,26 +9,30 @@ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; contract P256Test is Test { /// forge-config: default.fuzz.runs = 512 - function testVerify(uint256 seed, bytes32 digest) public { - uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.N - 1); + function testVerify(bytes32 digest, uint256 seed) public view { + uint256 privateKey = _asPrivateKey(seed); - (bytes32 x, bytes32 y) = P256PublicKey.getPublicKey(privateKey); + (uint256 x, uint256 y) = vm.publicKeyP256(privateKey); (bytes32 r, bytes32 s) = vm.signP256(privateKey, digest); s = _ensureLowerS(s); - assertTrue(P256.verify(digest, r, s, x, y)); - assertTrue(P256.verifySolidity(digest, r, s, x, y)); + assertTrue(P256.verify(digest, r, s, bytes32(x), bytes32(y))); + assertTrue(P256.verifySolidity(digest, r, s, bytes32(x), bytes32(y))); } /// forge-config: default.fuzz.runs = 512 - function testRecover(uint256 seed, bytes32 digest) public { - uint256 privateKey = bound(uint256(keccak256(abi.encode(seed))), 1, P256.N - 1); + function testRecover(bytes32 digest, uint256 seed) public view { + uint256 privateKey = _asPrivateKey(seed); - (bytes32 x, bytes32 y) = P256PublicKey.getPublicKey(privateKey); + (uint256 x, uint256 y) = vm.publicKeyP256(privateKey); (bytes32 r, bytes32 s) = vm.signP256(privateKey, digest); s = _ensureLowerS(s); (bytes32 qx0, bytes32 qy0) = P256.recovery(digest, 0, r, s); (bytes32 qx1, bytes32 qy1) = P256.recovery(digest, 1, r, s); - assertTrue((qx0 == x && qy0 == y) || (qx1 == x && qy1 == y)); + assertTrue((qx0 == bytes32(x) && qy0 == bytes32(y)) || (qx1 == bytes32(x) && qy1 == bytes32(y))); + } + + function _asPrivateKey(uint256 seed) private pure returns (uint256) { + return bound(seed, 1, P256.N - 1); } function _ensureLowerS(bytes32 s) private pure returns (bytes32) { @@ -38,98 +42,3 @@ contract P256Test is Test { } } } - -/** - * @dev Library to derive P256 public key from private key - * Should be removed if Foundry adds this functionality - * See https://github.com/foundry-rs/foundry/issues/7908 - */ -library P256PublicKey { - function getPublicKey(uint256 privateKey) internal view returns (bytes32, bytes32) { - (uint256 x, uint256 y, uint256 z) = _jMult(P256.GX, P256.GY, 1, privateKey); - return _affineFromJacobian(x, y, z); - } - - function _jMult( - uint256 x, - uint256 y, - uint256 z, - uint256 k - ) private pure returns (uint256 rx, uint256 ry, uint256 rz) { - unchecked { - for (uint256 i = 0; i < 256; ++i) { - if (rz > 0) { - (rx, ry, rz) = _jDouble(rx, ry, rz); - } - if (k >> 255 > 0) { - if (rz == 0) { - (rx, ry, rz) = (x, y, z); - } else { - (rx, ry, rz) = _jAdd(rx, ry, rz, x, y, z); - } - } - k <<= 1; - } - } - } - - /// From P256.sol - - function _affineFromJacobian(uint256 jx, uint256 jy, uint256 jz) private view returns (bytes32 ax, bytes32 ay) { - if (jz == 0) return (0, 0); - uint256 zinv = Math.invModPrime(jz, P256.P); - uint256 zzinv = mulmod(zinv, zinv, P256.P); - uint256 zzzinv = mulmod(zzinv, zinv, P256.P); - ax = bytes32(mulmod(jx, zzinv, P256.P)); - ay = bytes32(mulmod(jy, zzzinv, P256.P)); - } - - function _jDouble(uint256 x, uint256 y, uint256 z) private pure returns (uint256 rx, uint256 ry, uint256 rz) { - uint256 p = P256.P; - uint256 a = P256.A; - assembly ("memory-safe") { - let yy := mulmod(y, y, p) - let zz := mulmod(z, z, p) - let s := mulmod(4, mulmod(x, yy, p), p) // s = 4*x*y² - let m := addmod(mulmod(3, mulmod(x, x, p), p), mulmod(a, mulmod(zz, zz, p), p), p) // m = 3*x²+a*z⁴ - let t := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) // t = m²-2*s - - // x' = t - rx := t - // y' = m*(s-t)-8*y⁴ - ry := addmod(mulmod(m, addmod(s, sub(p, t), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p) - // z' = 2*y*z - rz := mulmod(2, mulmod(y, z, p), p) - } - } - - function _jAdd( - uint256 x1, - uint256 y1, - uint256 z1, - uint256 x2, - uint256 y2, - uint256 z2 - ) private pure returns (uint256 rx, uint256 ry, uint256 rz) { - uint256 p = P256.P; - assembly ("memory-safe") { - let zz1 := mulmod(z1, z1, p) // zz1 = z1² - let zz2 := mulmod(z2, z2, p) // zz2 = z2² - let u1 := mulmod(x1, zz2, p) // u1 = x1*z2² - let u2 := mulmod(x2, zz1, p) // u2 = x2*z1² - let s1 := mulmod(y1, mulmod(zz2, z2, p), p) // s1 = y1*z2³ - let s2 := mulmod(y2, mulmod(zz1, z1, p), p) // s2 = y2*z1³ - let h := addmod(u2, sub(p, u1), p) // h = u2-u1 - let hh := mulmod(h, h, p) // h² - let hhh := mulmod(h, hh, p) // h³ - let r := addmod(s2, sub(p, s1), p) // r = s2-s1 - - // x' = r²-h³-2*u1*h² - rx := addmod(addmod(mulmod(r, r, p), sub(p, hhh), p), sub(p, mulmod(2, mulmod(u1, hh, p), p)), p) - // y' = r*(u1*h²-x')-s1*h³ - ry := addmod(mulmod(r, addmod(mulmod(u1, hh, p), sub(p, rx), p), p), sub(p, mulmod(s1, hhh, p)), p) - // z' = h*z1*z2 - rz := mulmod(h, mulmod(z1, z2, p), p) - } - } -} diff --git a/test/utils/cryptography/RSA.test.js b/test/utils/cryptography/RSA.test.js index d717811f1dc..bdf33911fe0 100644 --- a/test/utils/cryptography/RSA.test.js +++ b/test/utils/cryptography/RSA.test.js @@ -1,6 +1,7 @@ const { ethers } = require('hardhat'); const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); +const { bytes, bytes32 } = ethers.Typed; const parse = require('./RSA.helper'); @@ -24,7 +25,7 @@ describe('RSA', function () { // const { sha224, sha256 } = require('@noble/hashes/sha256'); // const { sha384, sha512 } = require('@noble/hashes/sha512'); - if (test.SHAAlg === 'SHA256') { + if (test.SHAAlg === 'SHA256' && length >= 0x100) { const result = test.Result === 'P'; it(`signature length ${length} ${test.extra} ${result ? 'works' : 'fails'}`, async function () { @@ -33,65 +34,69 @@ describe('RSA', function () { const exp = '0x' + test.e; const mod = '0x' + test.n; - expect(await this.mock.$pkcs1(ethers.sha256(data), sig, exp, mod)).to.equal(result); - expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.equal(result); + expect(await this.mock.$pkcs1Sha256(bytes32(ethers.sha256(data)), sig, exp, mod)).to.equal(result); + expect(await this.mock.$pkcs1Sha256(bytes(data), sig, exp, mod)).to.equal(result); }); } } }); describe('others tests', function () { - it('openssl', async function () { - const data = ethers.toUtf8Bytes('hello world'); - const sig = - '0x079bed733b48d69bdb03076cb17d9809072a5a765460bc72072d687dba492afe951d75b814f561f253ee5cc0f3d703b6eab5b5df635b03a5437c0a5c179309812f5b5c97650361c645bc99f806054de21eb187bc0a704ed38d3d4c2871a117c19b6da7e9a3d808481c46b22652d15b899ad3792da5419e50ee38759560002388'; - const exp = - '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001'; - const mod = - '0xdf3edde009b96bc5b03b48bd73fe70a3ad20eaf624d0dc1ba121a45cc739893741b7cf82acf1c91573ec8266538997c6699760148de57e54983191eca0176f518e547b85fe0bb7d9e150df19eee734cf5338219c7f8f7b13b39f5384179f62c135e544cb70be7505751f34568e06981095aeec4f3a887639718a3e11d48c240d'; - expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.true; - }); + // > openssl genrsa -out private.pem 2048 + // > openssl rsa -in private.pem -outform der -pubout -out public.pem + // > openssl asn1parse -in public.pem -inform DER -strparse 19 + // > echo -n 'hello world!' | openssl dgst -sha256 -sign private.pem | xxd -p | tr -d \\n + const openssl = { + descr: 'openssl', + data: ethers.toUtf8Bytes('hello world!'), + sig: '0x2ff4349940bf0db9bce422e316ac47e3d24b0a869acb05c9c46f74e17491177698b150f2a5996a6bf7d7c73e05af91ad78632115a7d95b823c462596486e56e8473b75a270ca4760cd83f244d5d3af81d2c7d188879abbc2992b22d51e22ffb725f0828c852ee44f81def383e0f92ebfa3c6d97ca5e52a4254f9a886680e3fb394c2a8a955849313dce2cb416f8a67974effd9a17d229146ce10a98684fb3d46a1e53ddaf831cdd2beed895532533c554ae087b2738a5c4cf0802e8062b2a599fd76d67b92eabffa8a92b24e08fbc866217502a4a3d9f6157e491bede3c1048fa8f2d804f66128e8a883018b0ec33a59e1086bf71ae5dc193d9815ca82892dbc', + exp: '0x010001', + mod: '0xDC1CE5F7B202464CD320B4F9E44FEE0A358BE7022AB155A5BDEE45B1AED3C5A19645D898E294CBA96EAD6929FD8FB4B23E9ADB4D3143A736232C32A8617A77B89F7D8399B9BE37F8349D111067F71D2F20237B9F1A7C1CF44819F9FA5AA030F563DCFB1CC59FFAA86BA2ABEE28D949FED0DF34071B7558950079E28CD9BBA4CAC2F0F86D7BBFB13363C792B5A70C9B279F0B43A264A7CB1A7C7C41FC6EC1D1C1125A6BECE3207AE582F74CE896B9AC18DB00C8985B70145217B831CC313FC06581E186BF70A2EEE2C3C065B5C91A89B2C099B4924CDBF5707D161BD83AC8D9FCA309AC75D63EACF21027C2C9C9F05994331CBDFDD24F9BC6C8B58D8F1824540B', + result: true, + }; // According to RFC4055, pg.5 and RFC8017, pg. 64, for SHA-1, and the SHA-2 family, // the algorithm parameter has to be NULL and both explicit NULL parameter and implicit // NULL parameter (ie, absent NULL parameter) are considered to be legal and equivalent. - it('rfc8017 implicit null parameter', async function () { - const data = ethers.toUtf8Bytes('hello world!'); - const sig = - '0xa0073057133ff3758e7e111b4d7441f1d8cbe4b2dd5ee4316a14264290dee5ed7f175716639bd9bb43a14e4f9fcb9e84dedd35e2205caac04828b2c053f68176d971ea88534dd2eeec903043c3469fc69c206b2a8694fd262488441ed8852280c3d4994e9d42bd1d575c7024095f1a20665925c2175e089c0d731471f6cc145404edf5559fd2276e45e448086f71c78d0cc6628fad394a34e51e8c10bc39bfe09ed2f5f742cc68bee899d0a41e4c75b7b80afd1c321d89ccd9fe8197c44624d91cc935dfa48de3c201099b5b417be748aef29248527e8bbb173cab76b48478d4177b338fe1f1244e64d7d23f07add560d5ad50b68d6649a49d7bc3db686daaa7'; - const exp = '0x03'; - const mod = - '0xe932ac92252f585b3a80a4dd76a897c8b7652952fe788f6ec8dd640587a1ee5647670a8ad4c2be0f9fa6e49c605adf77b5174230af7bd50e5d6d6d6d28ccf0a886a514cc72e51d209cc772a52ef419f6a953f3135929588ebe9b351fca61ced78f346fe00dbb6306e5c2a4c6dfc3779af85ab417371cf34d8387b9b30ae46d7a5ff5a655b8d8455f1b94ae736989d60a6f2fd5cadbffbd504c5a756a2e6bb5cecc13bca7503f6df8b52ace5c410997e98809db4dc30d943de4e812a47553dce54844a78e36401d13f77dc650619fed88d8b3926e3d8e319c80c744779ac5d6abe252896950917476ece5e8fc27d5f053d6018d91b502c4787558a002b9283da7'; - expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.true; - }); + const rfc4055 = { + descr: 'rfc8017 implicit null parameter', + data: ethers.toUtf8Bytes('hello world!'), + sig: '0xa0073057133ff3758e7e111b4d7441f1d8cbe4b2dd5ee4316a14264290dee5ed7f175716639bd9bb43a14e4f9fcb9e84dedd35e2205caac04828b2c053f68176d971ea88534dd2eeec903043c3469fc69c206b2a8694fd262488441ed8852280c3d4994e9d42bd1d575c7024095f1a20665925c2175e089c0d731471f6cc145404edf5559fd2276e45e448086f71c78d0cc6628fad394a34e51e8c10bc39bfe09ed2f5f742cc68bee899d0a41e4c75b7b80afd1c321d89ccd9fe8197c44624d91cc935dfa48de3c201099b5b417be748aef29248527e8bbb173cab76b48478d4177b338fe1f1244e64d7d23f07add560d5ad50b68d6649a49d7bc3db686daaa7', + exp: '0x03', + mod: '0xe932ac92252f585b3a80a4dd76a897c8b7652952fe788f6ec8dd640587a1ee5647670a8ad4c2be0f9fa6e49c605adf77b5174230af7bd50e5d6d6d6d28ccf0a886a514cc72e51d209cc772a52ef419f6a953f3135929588ebe9b351fca61ced78f346fe00dbb6306e5c2a4c6dfc3779af85ab417371cf34d8387b9b30ae46d7a5ff5a655b8d8455f1b94ae736989d60a6f2fd5cadbffbd504c5a756a2e6bb5cecc13bca7503f6df8b52ace5c410997e98809db4dc30d943de4e812a47553dce54844a78e36401d13f77dc650619fed88d8b3926e3d8e319c80c744779ac5d6abe252896950917476ece5e8fc27d5f053d6018d91b502c4787558a002b9283da7', + result: true, + }; - it('returns false for a very short n', async function () { - const data = ethers.toUtf8Bytes('hello world!'); - const sig = '0x0102'; - const exp = '0x03'; - const mod = '0x0405'; - expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.false; - }); + const shortN = { + descr: 'returns false for a very short n', + data: ethers.toUtf8Bytes('hello world!'), + sig: '0x0102', + exp: '0x03', + mod: '0x0405', + result: false, + }; - it('returns false for a signature with different length to n', async function () { - const data = ethers.toUtf8Bytes('hello world!'); - const sig = '0x00112233'; - const exp = '0x03'; - const mod = - '0xe932ac92252f585b3a80a4dd76a897c8b7652952fe788f6ec8dd640587a1ee5647670a8ad4c2be0f9fa6e49c605adf77b5174230af7bd50e5d6d6d6d28ccf0a886a514cc72e51d209cc772a52ef419f6a953f3135929588ebe9b351fca61ced78f346fe00dbb6306e5c2a4c6dfc3779af85ab417371cf34d8387b9b30ae46d7a5ff5a655b8d8455f1b94ae736989d60a6f2fd5cadbffbd504c5a756a2e6bb5cecc13bca7503f6df8b52ace5c410997e98809db4dc30d943de4e812a47553dce54844a78e36401d13f77dc650619fed88d8b3926e3d8e319c80c744779ac5d6abe252896950917476ece5e8fc27d5f053d6018d91b502c4787558a002b9283da7'; - expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.false; - }); + const differentLength = { + descr: 'returns false for a signature with different length to n', + data: ethers.toUtf8Bytes('hello world!'), + sig: '0x00112233', + exp: '0x03', + mod: '0xe932ac92252f585b3a80a4dd76a897c8b7652952fe788f6ec8dd640587a1ee5647670a8ad4c2be0f9fa6e49c605adf77b5174230af7bd50e5d6d6d6d28ccf0a886a514cc72e51d209cc772a52ef419f6a953f3135929588ebe9b351fca61ced78f346fe00dbb6306e5c2a4c6dfc3779af85ab417371cf34d8387b9b30ae46d7a5ff5a655b8d8455f1b94ae736989d60a6f2fd5cadbffbd504c5a756a2e6bb5cecc13bca7503f6df8b52ace5c410997e98809db4dc30d943de4e812a47553dce54844a78e36401d13f77dc650619fed88d8b3926e3d8e319c80c744779ac5d6abe252896950917476ece5e8fc27d5f053d6018d91b502c4787558a002b9283da7', + result: false, + }; - it('returns false if s >= n', async function () { - // this is the openssl example where sig has been replaced by sig + mod - const data = ethers.toUtf8Bytes('hello world'); - const sig = - '0xe6dacb53450242618b3e502a257c08acb44b456c7931988da84f0cda8182b435d6d5453ac1e72b07c7dadf2747609b7d544d15f3f14081f9dbad9c48b7aa78d2bdafd81d630f19a0270d7911f4ec82b171e9a95889ffc9e740dc9fac89407a82d152ecb514967d4d9165e67ce0d7f39a3082657cdfca148a5fc2b3a7348c4795'; - const exp = - '0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010001'; - const mod = - '0xdf3edde009b96bc5b03b48bd73fe70a3ad20eaf624d0dc1ba121a45cc739893741b7cf82acf1c91573ec8266538997c6699760148de57e54983191eca0176f518e547b85fe0bb7d9e150df19eee734cf5338219c7f8f7b13b39f5384179f62c135e544cb70be7505751f34568e06981095aeec4f3a887639718a3e11d48c240d'; - expect(await this.mock.$pkcs1Sha256(data, sig, exp, mod)).to.be.false; - }); + // this is the openssl example where sig has been replaced by sig + mod + const sTooLarge = { + ...openssl, + descr: 'returns false if s >= n', + sig: ethers.toBeHex(ethers.toBigInt(openssl.sig) + ethers.toBigInt(openssl.mod)), + result: false, + }; + + for (const { descr, data, sig, exp, mod, result } of [openssl, rfc4055, shortN, differentLength, sTooLarge]) { + it(descr, async function () { + expect(await this.mock.$pkcs1Sha256(bytes(data), sig, exp, mod)).to.equal(result); + }); + } }); }); diff --git a/test/utils/introspection/SupportsInterface.behavior.js b/test/utils/introspection/SupportsInterface.behavior.js index c2bd1a479b2..51948279c50 100644 --- a/test/utils/introspection/SupportsInterface.behavior.js +++ b/test/utils/introspection/SupportsInterface.behavior.js @@ -92,8 +92,16 @@ const SIGNATURES = { const INTERFACE_IDS = mapValues(SIGNATURES, interfaceId); -function shouldSupportInterfaces(interfaces = []) { +function shouldSupportInterfaces(interfaces = [], signatures = SIGNATURES) { + // case where only signatures are provided + if (!Array.isArray(interfaces)) { + signatures = interfaces; + interfaces = Object.keys(interfaces); + } + interfaces.unshift('ERC165'); + signatures.ERC165 = SIGNATURES.ERC165; + const interfaceIds = mapValues(signatures, interfaceId, ([name]) => interfaces.includes(name)); describe('ERC165', function () { beforeEach(function () { @@ -103,14 +111,14 @@ function shouldSupportInterfaces(interfaces = []) { describe('when the interfaceId is supported', function () { it('uses less than 30k gas', async function () { for (const k of interfaces) { - const interface = INTERFACE_IDS[k] ?? k; - expect(await this.contractUnderTest.supportsInterface.estimateGas(interface)).to.lte(30_000n); + const interfaceId = interfaceIds[k] ?? k; + expect(await this.contractUnderTest.supportsInterface.estimateGas(interfaceId)).to.lte(30_000n); } }); it('returns true', async function () { for (const k of interfaces) { - const interfaceId = INTERFACE_IDS[k] ?? k; + const interfaceId = interfaceIds[k] ?? k; expect(await this.contractUnderTest.supportsInterface(interfaceId), `does not support ${k}`).to.be.true; } }); @@ -129,10 +137,10 @@ function shouldSupportInterfaces(interfaces = []) { it('all interface functions are in ABI', async function () { for (const k of interfaces) { // skip interfaces for which we don't have a function list - if (SIGNATURES[k] === undefined) continue; + if (signatures[k] === undefined) continue; // Check the presence of each function in the contract's interface - for (const fnSig of SIGNATURES[k]) { + for (const fnSig of signatures[k]) { expect(this.contractUnderTest.interface.hasFunction(fnSig), `did not find ${fnSig}`).to.be.true; } } @@ -141,5 +149,7 @@ function shouldSupportInterfaces(interfaces = []) { } module.exports = { + SIGNATURES, + INTERFACE_IDS, shouldSupportInterfaces, }; diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 3d4932eea7a..6c122d66c3f 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -7,18 +7,18 @@ import {Test, stdError} from "forge-std/Test.sol"; import {Math} from "@openzeppelin/contracts/utils/math/Math.sol"; contract MathTest is Test { - function testSymbolicTernary(bool f, uint256 a, uint256 b) public { + function testSymbolicTernary(bool f, uint256 a, uint256 b) public pure { assertEq(Math.ternary(f, a, b), f ? a : b); } // MIN & MAX - function testSymbolicMinMax(uint256 a, uint256 b) public { + function testSymbolicMinMax(uint256 a, uint256 b) public pure { assertEq(Math.min(a, b), a < b ? a : b); assertEq(Math.max(a, b), a > b ? a : b); } // CEILDIV - function testCeilDiv(uint256 a, uint256 b) public { + function testCeilDiv(uint256 a, uint256 b) public pure { vm.assume(b > 0); uint256 result = Math.ceilDiv(a, b); @@ -35,7 +35,7 @@ contract MathTest is Test { } // SQRT - function testSqrt(uint256 input, uint8 r) public { + function testSqrt(uint256 input, uint8 r) public pure { Math.Rounding rounding = _asRounding(r); uint256 result = Math.sqrt(input, rounding); @@ -66,31 +66,31 @@ contract MathTest is Test { } // INV - function testInvMod(uint256 value, uint256 p) public { + function testInvMod(uint256 value, uint256 p) public pure { _testInvMod(value, p, true); } - function testInvMod2(uint256 seed) public { + function testInvMod2(uint256 seed) public pure { uint256 p = 2; // prime _testInvMod(bound(seed, 1, p - 1), p, false); } - function testInvMod17(uint256 seed) public { + function testInvMod17(uint256 seed) public pure { uint256 p = 17; // prime _testInvMod(bound(seed, 1, p - 1), p, false); } - function testInvMod65537(uint256 seed) public { + function testInvMod65537(uint256 seed) public pure { uint256 p = 65537; // prime _testInvMod(bound(seed, 1, p - 1), p, false); } - function testInvModP256(uint256 seed) public { + function testInvModP256(uint256 seed) public pure { uint256 p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff; // prime _testInvMod(bound(seed, 1, p - 1), p, false); } - function _testInvMod(uint256 value, uint256 p, bool allowZero) private { + function _testInvMod(uint256 value, uint256 p, bool allowZero) private pure { uint256 inverse = Math.invMod(value, p); if (inverse != 0) { assertEq(mulmod(value, inverse, p), 1); @@ -101,7 +101,7 @@ contract MathTest is Test { } // LOG2 - function testLog2(uint256 input, uint8 r) public { + function testLog2(uint256 input, uint8 r) public pure { Math.Rounding rounding = _asRounding(r); uint256 result = Math.log2(input, rounding); @@ -128,7 +128,7 @@ contract MathTest is Test { } // LOG10 - function testLog10(uint256 input, uint8 r) public { + function testLog10(uint256 input, uint8 r) public pure { Math.Rounding rounding = _asRounding(r); uint256 result = Math.log10(input, rounding); @@ -155,7 +155,7 @@ contract MathTest is Test { } // LOG256 - function testLog256(uint256 input, uint8 r) public { + function testLog256(uint256 input, uint8 r) public pure { Math.Rounding rounding = _asRounding(r); uint256 result = Math.log256(input, rounding); @@ -182,7 +182,7 @@ contract MathTest is Test { } // MULDIV - function testMulDiv(uint256 x, uint256 y, uint256 d) public { + function testMulDiv(uint256 x, uint256 y, uint256 d) public pure { // Full precision for x * y (uint256 xyHi, uint256 xyLo) = _mulHighLow(x, y); @@ -225,7 +225,7 @@ contract MathTest is Test { assertEq(result, _nativeModExp(b, e, m)); } - function testTryModExp(uint256 b, uint256 e, uint256 m) public { + function testTryModExp(uint256 b, uint256 e, uint256 m) public view { (bool success, uint256 result) = Math.tryModExp(b, e, m); assertEq(success, m != 0); if (success) { @@ -247,7 +247,7 @@ contract MathTest is Test { assertEq(res, _nativeModExp(b, e, m)); } - function testTryModExpMemory(uint256 b, uint256 e, uint256 m) public { + function testTryModExpMemory(uint256 b, uint256 e, uint256 m) public view { (bool success, bytes memory result) = Math.tryModExp( abi.encodePacked(b), abi.encodePacked(e), diff --git a/test/utils/math/SignedMath.t.sol b/test/utils/math/SignedMath.t.sol index bbad109b71e..98a40a13998 100644 --- a/test/utils/math/SignedMath.t.sol +++ b/test/utils/math/SignedMath.t.sol @@ -8,18 +8,18 @@ import {Math} from "../../../contracts/utils/math/Math.sol"; import {SignedMath} from "../../../contracts/utils/math/SignedMath.sol"; contract SignedMathTest is Test { - function testSymbolicTernary(bool f, int256 a, int256 b) public { + function testSymbolicTernary(bool f, int256 a, int256 b) public pure { assertEq(SignedMath.ternary(f, a, b), f ? a : b); } // MIN & MAX - function testSymbolicMinMax(int256 a, int256 b) public { + function testSymbolicMinMax(int256 a, int256 b) public pure { assertEq(SignedMath.min(a, b), a < b ? a : b); assertEq(SignedMath.max(a, b), a > b ? a : b); } // MIN - function testSymbolicMin(int256 a, int256 b) public { + function testSymbolicMin(int256 a, int256 b) public pure { int256 result = SignedMath.min(a, b); assertLe(result, a); @@ -28,7 +28,7 @@ contract SignedMathTest is Test { } // MAX - function testSymbolicMax(int256 a, int256 b) public { + function testSymbolicMax(int256 a, int256 b) public pure { int256 result = SignedMath.max(a, b); assertGe(result, a); @@ -38,7 +38,7 @@ contract SignedMathTest is Test { // AVERAGE // 1. simple test, not full int256 range - function testAverage1(int256 a, int256 b) public { + function testAverage1(int256 a, int256 b) public pure { a = bound(a, type(int256).min / 2, type(int256).max / 2); b = bound(b, type(int256).min / 2, type(int256).max / 2); @@ -48,7 +48,7 @@ contract SignedMathTest is Test { } // 2. more complex test, full int256 range - function testAverage2(int256 a, int256 b) public { + function testAverage2(int256 a, int256 b) public pure { (int256 result, int256 min, int256 max) = ( SignedMath.average(a, b), SignedMath.min(a, b), @@ -69,7 +69,7 @@ contract SignedMathTest is Test { } // ABS - function testSymbolicAbs(int256 a) public { + function testSymbolicAbs(int256 a) public pure { uint256 result = SignedMath.abs(a); unchecked { diff --git a/test/utils/structs/Checkpoints.t.sol b/test/utils/structs/Checkpoints.t.sol index 1f4b344c57f..548a9d47c5c 100644 --- a/test/utils/structs/Checkpoints.t.sol +++ b/test/utils/structs/Checkpoints.t.sol @@ -30,7 +30,7 @@ contract CheckpointsTrace224Test is Test { } } - function _assertLatestCheckpoint(bool exist, uint32 key, uint224 value) internal { + function _assertLatestCheckpoint(bool exist, uint32 key, uint224 value) internal view { (bool _exist, uint32 _key, uint224 _value) = _ckpts.latestCheckpoint(); assertEq(_exist, exist); assertEq(_key, key); @@ -138,7 +138,7 @@ contract CheckpointsTrace208Test is Test { } } - function _assertLatestCheckpoint(bool exist, uint48 key, uint208 value) internal { + function _assertLatestCheckpoint(bool exist, uint48 key, uint208 value) internal view { (bool _exist, uint48 _key, uint208 _value) = _ckpts.latestCheckpoint(); assertEq(_exist, exist); assertEq(_key, key); @@ -246,7 +246,7 @@ contract CheckpointsTrace160Test is Test { } } - function _assertLatestCheckpoint(bool exist, uint96 key, uint160 value) internal { + function _assertLatestCheckpoint(bool exist, uint96 key, uint160 value) internal view { (bool _exist, uint96 _key, uint160 _value) = _ckpts.latestCheckpoint(); assertEq(_exist, exist); assertEq(_key, key); diff --git a/test/utils/structs/CircularBuffer.test.js b/test/utils/structs/CircularBuffer.test.js index c3d61aaa823..e79ba6923b4 100644 --- a/test/utils/structs/CircularBuffer.test.js +++ b/test/utils/structs/CircularBuffer.test.js @@ -18,6 +18,10 @@ describe('CircularBuffer', function () { Object.assign(this, await loadFixture(fixture)); }); + it('reverts on invalid setup', async function () { + await expect(this.mock.$setup(0, 0)).to.be.revertedWithCustomError(this.mock, 'InvalidBufferSize'); + }); + it('starts empty', async function () { expect(await this.mock.$count(0)).to.equal(0n); expect(await this.mock.$length(0)).to.equal(LENGTH); diff --git a/test/utils/structs/Heap.t.sol b/test/utils/structs/Heap.t.sol index b9d0b98787c..ba7d7701209 100644 --- a/test/utils/structs/Heap.t.sol +++ b/test/utils/structs/Heap.t.sol @@ -1,5 +1,4 @@ // SPDX-License-Identifier: MIT -// This file was procedurally generated from scripts/generate/templates/Heap.t.js. pragma solidity ^0.8.20; @@ -13,15 +12,9 @@ contract Uint256HeapTest is Test { Heap.Uint256Heap internal heap; - function _validateHeap(function(uint256, uint256) view returns (bool) comp) internal { - for (uint32 i = 0; i < heap.length(); ++i) { - // lookups - assertEq(i, heap.data[heap.data[i].index].lookup); - assertEq(i, heap.data[heap.data[i].lookup].index); - - // ordering: each node has a value bigger then its parent - if (i > 0) - assertFalse(comp(heap.data[heap.data[i].index].value, heap.data[heap.data[(i - 1) / 2].index].value)); + function _validateHeap(function(uint256, uint256) view returns (bool) comp) internal view { + for (uint32 i = 1; i < heap.length(); ++i) { + assertFalse(comp(heap.tree[i], heap.tree[(i - 1) / 2])); } } @@ -79,75 +72,3 @@ contract Uint256HeapTest is Test { } } } - -contract Uint208HeapTest is Test { - using Heap for Heap.Uint208Heap; - - Heap.Uint208Heap internal heap; - - function _validateHeap(function(uint256, uint256) view returns (bool) comp) internal { - for (uint32 i = 0; i < heap.length(); ++i) { - // lookups - assertEq(i, heap.data[heap.data[i].index].lookup); - assertEq(i, heap.data[heap.data[i].lookup].index); - - // ordering: each node has a value bigger then its parent - if (i > 0) - assertFalse(comp(heap.data[heap.data[i].index].value, heap.data[heap.data[(i - 1) / 2].index].value)); - } - } - - function testFuzz(uint208[] calldata input) public { - vm.assume(input.length < 0x20); - assertEq(heap.length(), 0); - - uint256 min = type(uint256).max; - for (uint256 i = 0; i < input.length; ++i) { - heap.insert(input[i]); - assertEq(heap.length(), i + 1); - _validateHeap(Comparators.lt); - - min = Math.min(min, input[i]); - assertEq(heap.peek(), min); - } - - uint256 max = 0; - for (uint256 i = 0; i < input.length; ++i) { - uint208 top = heap.peek(); - uint208 pop = heap.pop(); - assertEq(heap.length(), input.length - i - 1); - _validateHeap(Comparators.lt); - - assertEq(pop, top); - assertGe(pop, max); - max = pop; - } - } - - function testFuzzGt(uint208[] calldata input) public { - vm.assume(input.length < 0x20); - assertEq(heap.length(), 0); - - uint256 max = 0; - for (uint256 i = 0; i < input.length; ++i) { - heap.insert(input[i], Comparators.gt); - assertEq(heap.length(), i + 1); - _validateHeap(Comparators.gt); - - max = Math.max(max, input[i]); - assertEq(heap.peek(), max); - } - - uint256 min = type(uint256).max; - for (uint256 i = 0; i < input.length; ++i) { - uint208 top = heap.peek(); - uint208 pop = heap.pop(Comparators.gt); - assertEq(heap.length(), input.length - i - 1); - _validateHeap(Comparators.gt); - - assertEq(pop, top); - assertLe(pop, min); - min = pop; - } - } -} diff --git a/test/utils/structs/Heap.test.js b/test/utils/structs/Heap.test.js index 7e95a0e7adb..6d751205c7a 100644 --- a/test/utils/structs/Heap.test.js +++ b/test/utils/structs/Heap.test.js @@ -3,8 +3,6 @@ const { expect } = require('chai'); const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); -const { TYPES } = require('../../../scripts/generate/templates/Heap.opts'); - async function fixture() { const mock = await ethers.deployContract('$Heap'); return { mock }; @@ -15,117 +13,101 @@ describe('Heap', function () { Object.assign(this, await loadFixture(fixture)); }); - for (const { struct, valueType } of TYPES) { - describe(struct, function () { - const popEvent = `return$pop_Heap_${struct}`; - const replaceEvent = `return$replace_Heap_${struct}_${valueType}`; - - beforeEach(async function () { - this.helper = { - clear: (...args) => this.mock[`$clear_Heap_${struct}`](0, ...args), - insert: (...args) => this.mock[`$insert(uint256,${valueType})`](0, ...args), - replace: (...args) => this.mock[`$replace(uint256,${valueType})`](0, ...args), - length: (...args) => this.mock[`$length_Heap_${struct}`](0, ...args), - pop: (...args) => this.mock[`$pop_Heap_${struct}`](0, ...args), - peek: (...args) => this.mock[`$peek_Heap_${struct}`](0, ...args), - }; - }); - - it('starts empty', async function () { - expect(await this.helper.length()).to.equal(0n); - }); - - it('peek, pop and replace from empty', async function () { - await expect(this.helper.peek()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); - await expect(this.helper.pop()).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); - await expect(this.helper.replace(0n)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); - }); - - it('clear', async function () { - await this.helper.insert(42n); - - expect(await this.helper.length()).to.equal(1n); - expect(await this.helper.peek()).to.equal(42n); + describe('Uint256Heap', function () { + it('starts empty', async function () { + expect(await this.mock.$length(0)).to.equal(0n); + }); - await this.helper.clear(); + it('peek, pop and replace from empty', async function () { + await expect(this.mock.$peek(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + await expect(this.mock.$replace(0, 0n)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + }); - expect(await this.helper.length()).to.equal(0n); - await expect(this.helper.peek()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); - }); + it('clear', async function () { + await this.mock.$insert(0, 42n); - it('support duplicated items', async function () { - expect(await this.helper.length()).to.equal(0n); + expect(await this.mock.$length(0)).to.equal(1n); + expect(await this.mock.$peek(0)).to.equal(42n); - // insert 5 times - await this.helper.insert(42n); - await this.helper.insert(42n); - await this.helper.insert(42n); - await this.helper.insert(42n); - await this.helper.insert(42n); + await this.mock.$clear(0); - // pop 5 times - await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(42n); - await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(42n); - await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(42n); - await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(42n); - await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(42n); + expect(await this.mock.$length(0)).to.equal(0n); + await expect(this.mock.$peek(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); - // popping a 6th time panics - await expect(this.helper.pop()).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); - }); + it('support duplicated items', async function () { + expect(await this.mock.$length(0)).to.equal(0n); + + // insert 5 times + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + await this.mock.$insert(0, 42n); + + // pop 5 times + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(42n); + + // popping a 6th time panics + await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + }); - it('insert, pop and replace', async function () { - const heap = []; - for (const { op, value } of [ - { op: 'insert', value: 712 }, // [712] - { op: 'insert', value: 20 }, // [20, 712] - { op: 'insert', value: 4337 }, // [20, 712, 4437] - { op: 'pop' }, // 20, [712, 4437] - { op: 'insert', value: 1559 }, // [712, 1559, 4437] - { op: 'insert', value: 165 }, // [165, 712, 1559, 4437] - { op: 'insert', value: 155 }, // [155, 165, 712, 1559, 4437] - { op: 'insert', value: 7702 }, // [155, 165, 712, 1559, 4437, 7702] - { op: 'pop' }, // 155, [165, 712, 1559, 4437, 7702] - { op: 'replace', value: 721 }, // 165, [712, 721, 1559, 4437, 7702] - { op: 'pop' }, // 712, [721, 1559, 4437, 7702] - { op: 'pop' }, // 721, [1559, 4437, 7702] - { op: 'pop' }, // 1559, [4437, 7702] - { op: 'pop' }, // 4437, [7702] - { op: 'pop' }, // 7702, [] - { op: 'pop' }, // panic - { op: 'replace', value: '1363' }, // panic - ]) { - switch (op) { - case 'insert': - await this.helper.insert(value); + it('insert, pop and replace', async function () { + const heap = []; + for (const { op, value } of [ + { op: 'insert', value: 712 }, // [712] + { op: 'insert', value: 20 }, // [20, 712] + { op: 'insert', value: 4337 }, // [20, 712, 4437] + { op: 'pop' }, // 20, [712, 4437] + { op: 'insert', value: 1559 }, // [712, 1559, 4437] + { op: 'insert', value: 165 }, // [165, 712, 1559, 4437] + { op: 'insert', value: 155 }, // [155, 165, 712, 1559, 4437] + { op: 'insert', value: 7702 }, // [155, 165, 712, 1559, 4437, 7702] + { op: 'pop' }, // 155, [165, 712, 1559, 4437, 7702] + { op: 'replace', value: 721 }, // 165, [712, 721, 1559, 4437, 7702] + { op: 'pop' }, // 712, [721, 1559, 4437, 7702] + { op: 'pop' }, // 721, [1559, 4437, 7702] + { op: 'pop' }, // 1559, [4437, 7702] + { op: 'pop' }, // 4437, [7702] + { op: 'pop' }, // 7702, [] + { op: 'pop' }, // panic + { op: 'replace', value: '1363' }, // panic + ]) { + switch (op) { + case 'insert': + await this.mock.$insert(0, value); + heap.push(value); + heap.sort((a, b) => a - b); + break; + case 'pop': + if (heap.length == 0) { + await expect(this.mock.$pop(0)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + } else { + await expect(this.mock.$pop(0)).to.emit(this.mock, 'return$pop').withArgs(heap.shift()); + } + break; + case 'replace': + if (heap.length == 0) { + await expect(this.mock.$replace(0, value)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); + } else { + await expect(this.mock.$replace(0, value)).to.emit(this.mock, 'return$replace').withArgs(heap.shift()); heap.push(value); heap.sort((a, b) => a - b); - break; - case 'pop': - if (heap.length == 0) { - await expect(this.helper.pop()).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); - } else { - await expect(this.helper.pop()).to.emit(this.mock, popEvent).withArgs(heap.shift()); - } - break; - case 'replace': - if (heap.length == 0) { - await expect(this.helper.replace(value)).to.be.revertedWithPanic(PANIC_CODES.POP_ON_EMPTY_ARRAY); - } else { - await expect(this.helper.replace(value)).to.emit(this.mock, replaceEvent).withArgs(heap.shift()); - heap.push(value); - heap.sort((a, b) => a - b); - } - break; - } - expect(await this.helper.length()).to.equal(heap.length); - if (heap.length == 0) { - await expect(this.helper.peek()).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); - } else { - expect(await this.helper.peek()).to.equal(heap[0]); - } + } + break; + } + expect(await this.mock.$length(0)).to.equal(heap.length); + if (heap.length == 0) { + await expect(this.mock.$peek(0)).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + } else { + expect(await this.mock.$peek(0)).to.equal(heap[0]); } - }); + } }); - } + }); });