Преглед на файлове

Merge branch 'master' into update/certora-7.3.0

Ernesto García преди 1 година
родител
ревизия
2e9cd8cb8c
променени са 100 файла, в които са добавени 4236 реда и са изтрити 744 реда
  1. 5 0
      .changeset/curvy-crabs-repeat.md
  2. 5 0
      .changeset/eight-eyes-burn.md
  3. 5 0
      .changeset/fluffy-buses-jump.md
  4. 5 0
      .changeset/forty-dodos-visit.md
  5. 5 0
      .changeset/four-chairs-help.md
  6. 5 0
      .changeset/great-pianos-work.md
  7. 1 1
      .changeset/heavy-baboons-give.md
  8. 5 0
      .changeset/light-news-listen.md
  9. 5 0
      .changeset/nervous-eyes-teach.md
  10. 5 0
      .changeset/odd-lobsters-wash.md
  11. 5 0
      .changeset/serious-carrots-provide.md
  12. 5 0
      .changeset/spotty-queens-own.md
  13. 5 0
      .changeset/tricky-bats-pretend.md
  14. 3 0
      .codecov.yml
  15. 1 0
      .githooks/pre-push
  16. 1 1
      .github/workflows/checks.yml
  17. 1 1
      .github/workflows/formal-verification.yml
  18. 4 0
      CHANGELOG.md
  19. 1 1
      CODE_OF_CONDUCT.md
  20. 7 0
      GUIDELINES.md
  21. 1 1
      LICENSE
  22. 2 2
      certora/diff/access_manager_AccessManager.sol.patch
  23. 18 9
      contracts/access/manager/AccessManager.sol
  24. 5 3
      contracts/access/manager/IAccessManager.sol
  25. 8 8
      contracts/governance/Governor.sol
  26. 8 0
      contracts/governance/IGovernor.sol
  27. 5 1
      contracts/governance/README.adoc
  28. 193 0
      contracts/governance/extensions/GovernorCountingFractional.sol
  29. 7 5
      contracts/governance/extensions/GovernorCountingSimple.sol
  30. 1 1
      contracts/interfaces/IERC1363Receiver.sol
  31. 3 0
      contracts/interfaces/IERC2981.sol
  32. 3 0
      contracts/interfaces/README.adoc
  33. 16 0
      contracts/interfaces/draft-IERC7674.sol
  34. 4 6
      contracts/metatx/ERC2771Forwarder.sol
  35. 21 0
      contracts/mocks/AccessManagerMock.sol
  36. 20 0
      contracts/mocks/BatchCaller.sol
  37. 34 0
      contracts/mocks/ConstructorMock.sol
  38. 62 0
      contracts/mocks/MerkleProofCustomHashMock.sol
  39. 3 0
      contracts/mocks/Stateless.sol
  40. 6 0
      contracts/mocks/docs/ERC4626Fees.sol
  41. 14 0
      contracts/mocks/governance/GovernorFractionalMock.sol
  42. 1 1
      contracts/mocks/governance/GovernorWithParamsMock.sol
  43. 38 0
      contracts/mocks/token/ERC20GetterHelper.sol
  44. 154 18
      contracts/proxy/Clones.sol
  45. 1 1
      contracts/proxy/ERC1967/ERC1967Utils.sol
  46. 7 1
      contracts/proxy/transparent/TransparentUpgradeableProxy.sol
  47. 1 2
      contracts/token/ERC1155/ERC1155.sol
  48. 2 0
      contracts/token/ERC1155/README.adoc
  49. 7 4
      contracts/token/ERC1155/extensions/ERC1155Supply.sol
  50. 4 6
      contracts/token/ERC1155/utils/ERC1155Utils.sol
  51. 5 0
      contracts/token/ERC20/README.adoc
  52. 13 80
      contracts/token/ERC20/extensions/ERC1363.sol
  53. 119 0
      contracts/token/ERC20/extensions/draft-ERC20TemporaryApproval.sol
  54. 94 0
      contracts/token/ERC20/utils/ERC1363Utils.sol
  55. 26 14
      contracts/token/ERC20/utils/SafeERC20.sol
  56. 3 2
      contracts/token/ERC721/ERC721.sol
  57. 2 0
      contracts/token/ERC721/README.adoc
  58. 1 2
      contracts/token/ERC721/utils/ERC721Utils.sol
  59. 1 2
      contracts/utils/Address.sol
  60. 47 52
      contracts/utils/Arrays.sol
  61. 10 7
      contracts/utils/Base64.sol
  62. 13 0
      contracts/utils/Comparators.sol
  63. 8 4
      contracts/utils/Create2.sol
  64. 5 0
      contracts/utils/Errors.sol
  65. 1120 20
      contracts/utils/Packing.sol
  66. 1 2
      contracts/utils/Panic.sol
  67. 12 4
      contracts/utils/README.adoc
  68. 1 2
      contracts/utils/ShortStrings.sol
  69. 9 18
      contracts/utils/SlotDerivation.sol
  70. 25 44
      contracts/utils/StorageSlot.sol
  71. 26 4
      contracts/utils/Strings.sol
  72. 1 2
      contracts/utils/cryptography/ECDSA.sol
  73. 1 2
      contracts/utils/cryptography/Hashes.sol
  74. 329 30
      contracts/utils/cryptography/MerkleProof.sol
  75. 2 4
      contracts/utils/cryptography/MessageHashUtils.sol
  76. 317 0
      contracts/utils/cryptography/P256.sol
  77. 145 0
      contracts/utils/cryptography/RSA.sol
  78. 1 1
      contracts/utils/introspection/ERC165Checker.sol
  79. 21 8
      contracts/utils/math/Math.sol
  80. 1 2
      contracts/utils/math/SafeCast.sol
  81. 2 2
      contracts/utils/math/SignedMath.sol
  82. 15 15
      contracts/utils/structs/Checkpoints.sol
  83. 1 1
      contracts/utils/structs/CircularBuffer.sol
  84. 8 16
      contracts/utils/structs/EnumerableMap.sol
  85. 3 6
      contracts/utils/structs/EnumerableSet.sol
  86. 576 0
      contracts/utils/structs/Heap.sol
  87. 35 9
      contracts/utils/structs/MerkleTree.sol
  88. 1 1
      docs/modules/ROOT/pages/erc4626.adoc
  89. 1 1
      docs/modules/ROOT/pages/governance.adoc
  90. 158 5
      docs/modules/ROOT/pages/utilities.adoc
  91. 2 0
      foundry.toml
  92. 1 1
      fv-requirements.txt
  93. 19 21
      hardhat.config.js
  94. 168 129
      package-lock.json
  95. 5 5
      package.json
  96. 18 0
      scripts/checks/coverage.sh
  97. 9 4
      scripts/generate/run.js
  98. 84 85
      scripts/generate/templates/Arrays.js
  99. 23 36
      scripts/generate/templates/Checkpoints.js
  100. 20 28
      scripts/generate/templates/Checkpoints.t.js

+ 5 - 0
.changeset/curvy-crabs-repeat.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`RSA`: Library to verify signatures according to RFC 8017 Signature Verification Operation

+ 5 - 0
.changeset/eight-eyes-burn.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`GovernorCountingFractional`: Add a governor counting module that allows distributing voting power amongst 3 options (For, Against, Abstain).

+ 5 - 0
.changeset/fluffy-buses-jump.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Comparator`: A library of comparator functions, useful for customizing the behavior of the Heap structure.

+ 5 - 0
.changeset/forty-dodos-visit.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Strings`: Added a utility function for converting an address to checksummed string.

+ 5 - 0
.changeset/four-chairs-help.md

@@ -0,0 +1,5 @@
+---
+"openzeppelin-solidity": minor
+---
+
+`Clones`: Add `cloneWithImmutableArgs` and `cloneDeterministicWithImmutableArgs` variants that create clones with per-instance immutable arguments. The immutable arguments can be retrieved using `fetchCloneArgs`. The corresponding `predictDeterministicWithImmutableArgs` function is also included.

+ 5 - 0
.changeset/great-pianos-work.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Heap`: A data structure that implements a heap-based priority queue.

+ 1 - 1
.changeset/heavy-baboons-give.md

@@ -2,4 +2,4 @@
 'openzeppelin-solidity': minor
 ---
 
-`Packing`: Added a new utility for packing and unpacking multiple values into a single bytes32. Includes initial support for packing two `uint128` in an `Uint128x2` type.
+`Packing`: Added a new utility for packing, extracting and replacing bytesXX values.

+ 5 - 0
.changeset/light-news-listen.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`AccessManager`: Allow the `onlyAuthorized` modifier to restrict functions added to the manager.

+ 5 - 0
.changeset/nervous-eyes-teach.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Create2`: Bubbles up returndata from a deployed contract that reverted during construction.

+ 5 - 0
.changeset/odd-lobsters-wash.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`P256`: Library for verification and public key recovery of P256 (aka secp256r1) signatures.

+ 5 - 0
.changeset/serious-carrots-provide.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`ERC20TemporaryApproval`: Add an ERC-20 extension that implements temporary approval using transient storage, based on ERC7674 (draft).

+ 5 - 0
.changeset/spotty-queens-own.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`MerkleProof`: Add variations of `verify`, `processProof`, `multiProofVerify` and `processMultiProof` (and equivalent calldata version) with support for custom hashing functions.

+ 5 - 0
.changeset/tricky-bats-pretend.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`ERC1363Utils`: Add helper similar to the existing `ERC721Utils` and `ERC1155Utils`

+ 3 - 0
.codecov.yml

@@ -10,3 +10,6 @@ coverage:
     project:
       default:
         threshold: 1%
+ignore:
+  - "test"
+  - "contracts/mocks"

+ 1 - 0
.githooks/pre-push

@@ -3,5 +3,6 @@
 set -euo pipefail
 
 if [ "${CI:-"false"}" != "true" ]; then
+  npm run test:generation
   npm run lint
 fi

+ 1 - 1
.github/workflows/checks.yml

@@ -125,7 +125,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
       - name: Run CodeSpell
-        uses: codespell-project/actions-codespell@v2.0
+        uses: codespell-project/actions-codespell@v2.1
         with:
           check_hidden: true
           check_filenames: true

+ 1 - 1
.github/workflows/formal-verification.yml

@@ -10,7 +10,7 @@ on:
   workflow_dispatch: {}
 
 env:
-  PIP_VERSION: '3.10'
+  PIP_VERSION: '3.11'
   JAVA_VERSION: '11'
   SOLC_VERSION: '0.8.25'
 

+ 4 - 0
CHANGELOG.md

@@ -3,6 +3,7 @@
 ### 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.
 
 ### Custom error changes
 
@@ -13,6 +14,9 @@ This version comes with changes to the custom error identifiers. Contracts previ
 - Replace `Clones.Create2InsufficientBalance` with `Errors.InsufficientBalance`
 - Replace `Clones.ERC1167FailedCreateClone` with `Errors.FailedDeployment`
 - Replace `Clones.Create2FailedDeployment` with `Errors.FailedDeployment`
+- `SafeERC20`: Replace `Address.AddressEmptyCode` with `SafeERC20FailedOperation` if there is no code at the token's address.
+- `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.
 
 ## 5.0.2 (2024-02-29)
 

+ 1 - 1
CODE_OF_CONDUCT.md

@@ -6,7 +6,7 @@ In the interest of fostering an open and welcoming environment, we as
 contributors and maintainers pledge to making participation in our project and
 our community a harassment-free experience for everyone, regardless of age, body
 size, disability, ethnicity, sex characteristics, gender identity and expression,
-level of experience, education, socio-economic status, nationality, personal
+level of experience, education, socioeconomic status, nationality, personal
 appearance, race, religion, or sexual identity and orientation.
 
 ## Our Standards

+ 7 - 0
GUIDELINES.md

@@ -131,6 +131,13 @@ In addition to the official Solidity Style Guide we have a number of other conve
   abstract contract AccessControl is ..., {
   ```
 
+* Return values are generally not named, unless they are not immediately clear or there are multiple return values.
+
+  ```solidity
+  function expiration() public view returns (uint256) { // Good
+  function hasRole() public view returns (bool isMember, uint32 currentDelay) { // Good
+  ```
+
 * Unchecked arithmetic blocks should contain comments explaining why overflow is guaranteed not to happen. If the reason is immediately apparent from the line above the unchecked block, the comment may be omitted.
 
 * Custom errors should be declared following the [EIP-6093](https://eips.ethereum.org/EIPS/eip-6093) rationale whenever reasonable. Also, consider the following:

+ 1 - 1
LICENSE

@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2016-2024 Zeppelin Group Ltd and contributors
+Copyright (c) 2016-2024 Zeppelin Group Ltd
 
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the

+ 2 - 2
certora/diff/access_manager_AccessManager.sol.patch

@@ -64,8 +64,8 @@
       */
      function _getAdminRestrictions(
          bytes calldata data
--    ) private view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) {
-+    ) internal view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) { // private → internal for FV
+-    ) private view returns (bool adminRestricted, uint64 roleAdminId, uint32 executionDelay) {
++    ) internal view returns (bool adminRestricted, uint64 roleAdminId, uint32 executionDelay) { // private → internal for FV
          if (data.length < 4) {
              return (false, 0, 0);
          }

+ 18 - 9
contracts/access/manager/AccessManager.sol

@@ -97,7 +97,15 @@ contract AccessManager is Context, Multicall, IAccessManager {
         uint32 nonce;
     }
 
+    /**
+     * @dev The identifier of the admin role. Required to perform most configuration operations including
+     * other roles' management and target restrictions.
+     */
     uint64 public constant ADMIN_ROLE = type(uint64).min; // 0
+
+    /**
+     * @dev The identifier of the public role. Automatically granted to all addresses with no delay.
+     */
     uint64 public constant PUBLIC_ROLE = type(uint64).max; // 2**64-1
 
     mapping(address target => TargetConfig mode) private _targets;
@@ -412,9 +420,6 @@ contract AccessManager is Context, Multicall, IAccessManager {
      * Emits a {TargetClosed} event.
      */
     function _setTargetClosed(address target, bool closed) internal virtual {
-        if (target == address(this)) {
-            revert AccessManagerLockedAccount(target);
-        }
         _targets[target].closed = closed;
         emit TargetClosed(target, closed);
     }
@@ -586,7 +591,9 @@ contract AccessManager is Context, Multicall, IAccessManager {
 
     // ================================================= ADMIN LOGIC ==================================================
     /**
-     * @dev Check if the current call is authorized according to admin logic.
+     * @dev Check if the current call is authorized according to admin and roles logic.
+     *
+     * WARNING: Carefully review the considerations of {AccessManaged-restricted} since they apply to this modifier.
      */
     function _checkAuthorized() private {
         address caller = _msgSender();
@@ -611,7 +618,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
      */
     function _getAdminRestrictions(
         bytes calldata data
-    ) private view returns (bool restricted, uint64 roleAdminId, uint32 executionDelay) {
+    ) private view returns (bool adminRestricted, uint64 roleAdminId, uint32 executionDelay) {
         if (data.length < 4) {
             return (false, 0, 0);
         }
@@ -648,7 +655,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
             return (true, getRoleAdmin(roleId), 0);
         }
 
-        return (false, 0, 0);
+        return (false, getTargetFunctionRole(address(this), selector), 0);
     }
 
     // =================================================== HELPERS ====================================================
@@ -673,7 +680,7 @@ contract AccessManager is Context, Multicall, IAccessManager {
     }
 
     /**
-     * @dev A version of {canCall} that checks for admin restrictions in this contract.
+     * @dev A version of {canCall} that checks for restrictions in this contract.
      */
     function _canCallSelf(address caller, bytes calldata data) private view returns (bool immediate, uint32 delay) {
         if (data.length < 4) {
@@ -686,8 +693,10 @@ contract AccessManager is Context, Multicall, IAccessManager {
             return (_isExecuting(address(this), _checkSelector(data)), 0);
         }
 
-        (bool enabled, uint64 roleId, uint32 operationDelay) = _getAdminRestrictions(data);
-        if (!enabled) {
+        (bool adminRestricted, uint64 roleId, uint32 operationDelay) = _getAdminRestrictions(data);
+
+        // isTargetClosed apply to non-admin-restricted function
+        if (!adminRestricted && isTargetClosed(address(this))) {
             return (false, 0);
         }
 

+ 5 - 3
contracts/access/manager/IAccessManager.sol

@@ -3,7 +3,6 @@
 
 pragma solidity ^0.8.20;
 
-import {IAccessManaged} from "./IAccessManaged.sol";
 import {Time} from "../../utils/types/Time.sol";
 
 interface IAccessManager {
@@ -82,7 +81,6 @@ interface IAccessManager {
     error AccessManagerNotScheduled(bytes32 operationId);
     error AccessManagerNotReady(bytes32 operationId);
     error AccessManagerExpired(bytes32 operationId);
-    error AccessManagerLockedAccount(address account);
     error AccessManagerLockedRole(uint64 roleId);
     error AccessManagerBadConfirmation();
     error AccessManagerUnauthorizedAccount(address msgsender, uint64 roleId);
@@ -108,7 +106,7 @@ interface IAccessManager {
      * is backward compatible. Some contracts may thus ignore the second return argument. In that case they will fail
      * to identify the indirect workflow, and will consider calls that require a delay to be forbidden.
      *
-     * NOTE: This function does not report the permissions of this manager itself. These are defined by the
+     * NOTE: This function does not report the permissions of the admin functions in the manager itself. These are defined by the
      * {AccessManager} documentation.
      */
     function canCall(
@@ -134,6 +132,8 @@ interface IAccessManager {
 
     /**
      * @dev Get whether the contract is closed disabling any access. Otherwise role permissions are applied.
+     *
+     * NOTE: When the manager itself is closed, admin functions are still accessible to avoid locking the contract.
      */
     function isTargetClosed(address target) external view returns (bool);
 
@@ -308,6 +308,8 @@ interface IAccessManager {
     /**
      * @dev Set the closed flag for a contract.
      *
+     * Closing the manager itself won't disable access to admin methods to avoid locking the contract.
+     *
      * Requirements:
      *
      * - the caller must be a global admin

+ 8 - 8
contracts/governance/Governor.sol

@@ -255,9 +255,9 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
         uint256 proposalId,
         address account,
         uint8 support,
-        uint256 weight,
+        uint256 totalWeight,
         bytes memory params
-    ) internal virtual;
+    ) internal virtual returns (uint256);
 
     /**
      * @dev Default additional encoded parameters used by castVote methods that don't include them
@@ -639,16 +639,16 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
     ) internal virtual returns (uint256) {
         _validateStateBitmap(proposalId, _encodeStateBitmap(ProposalState.Active));
 
-        uint256 weight = _getVotes(account, proposalSnapshot(proposalId), params);
-        _countVote(proposalId, account, support, weight, params);
+        uint256 totalWeight = _getVotes(account, proposalSnapshot(proposalId), params);
+        uint256 votedWeight = _countVote(proposalId, account, support, totalWeight, params);
 
         if (params.length == 0) {
-            emit VoteCast(account, proposalId, support, weight, reason);
+            emit VoteCast(account, proposalId, support, votedWeight, reason);
         } else {
-            emit VoteCastWithParams(account, proposalId, support, weight, reason, params);
+            emit VoteCastWithParams(account, proposalId, support, votedWeight, reason, params);
         }
 
-        return weight;
+        return votedWeight;
     }
 
     /**
@@ -769,7 +769,7 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
 
         // Extract what would be the `#proposer=0x` marker beginning the suffix
         bytes12 marker;
-        assembly {
+        assembly ("memory-safe") {
             // - Start of the string contents in memory = description + 32
             // - First character of the marker = len - 52
             //   - Length of "#proposer=0x0000000000000000000000000000000000000000" = 52

+ 8 - 0
contracts/governance/IGovernor.sol

@@ -8,6 +8,9 @@ import {IERC6372} from "../interfaces/IERC6372.sol";
 
 /**
  * @dev Interface of the {Governor} core.
+ *
+ * NOTE: Event parameters lack the `indexed` keyword for compatibility with GovernorBravo events.
+ * Making event parameters `indexed` affects how events are decoded, potentially breaking existing indexers.
  */
 interface IGovernor is IERC165, IERC6372 {
     enum ProposalState {
@@ -83,6 +86,11 @@ interface IGovernor is IERC165, IERC6372 {
      */
     error GovernorInvalidVoteType();
 
+    /**
+     * @dev The provided params buffer is not supported by the counting module.
+     */
+    error GovernorInvalidVoteParams();
+
     /**
      * @dev Queue operation is not implemented for this governor. Execute should be called directly.
      */

+ 5 - 1
contracts/governance/README.adoc

@@ -28,6 +28,8 @@ Counting modules determine valid voting options.
 
 * {GovernorCountingSimple}: Simple voting mechanism with 3 voting options: Against, For and Abstain.
 
+* {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).
+
 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.
@@ -62,6 +64,8 @@ NOTE: Functions of the `Governor` contract do not include access control. If you
 
 {{GovernorCountingSimple}}
 
+{{GovernorCountingFractional}}
+
 {{GovernorVotes}}
 
 {{GovernorVotesQuorumFraction}}
@@ -136,7 +140,7 @@ Timelocked operations are identified by a unique id (their hash) and follow a sp
 * By calling xref:api:governance.adoc#TimelockController-schedule-address-uint256-bytes-bytes32-bytes32-uint256-[`schedule`] (or xref:api:governance.adoc#TimelockController-scheduleBatch-address---uint256---bytes---bytes32-bytes32-uint256-[`scheduleBatch`]), a proposer moves the operation from the `Unset` to the `Pending` state. This starts a timer that must be longer than the minimum delay. The timer expires at a timestamp accessible through the xref:api:governance.adoc#TimelockController-getTimestamp-bytes32-[`getTimestamp`] method.
 * Once the timer expires, the operation automatically gets the `Ready` state. At this point, it can be executed.
 * By calling xref:api:governance.adoc#TimelockController-TimelockController-execute-address-uint256-bytes-bytes32-bytes32-[`execute`] (or xref:api:governance.adoc#TimelockController-executeBatch-address---uint256---bytes---bytes32-bytes32-[`executeBatch`]), an executor triggers the operation's underlying transactions and moves it to the `Done` state. If the operation has a predecessor, it has to be in the `Done` state for this transition to succeed.
-* xref:api:governance.adoc#TimelockController-TimelockController-cancel-bytes32-[`cancel`] allows proposers to cancel any `Pending` operation. This resets the operation to the `Unset` state. It is thus possible for a proposer to re-schedule an operation that has been cancelled. In this case, the timer restarts when the operation is re-scheduled.
+* xref:api:governance.adoc#TimelockController-TimelockController-cancel-bytes32-[`cancel`] allows proposers to cancel any `Pending` operation. This resets the operation to the `Unset` state. It is thus possible for a proposer to re-schedule an operation that has been cancelled. In this case, the timer restarts when the operation is rescheduled.
 
 Operations status can be queried using the functions:
 

+ 193 - 0
contracts/governance/extensions/GovernorCountingFractional.sol

@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {Governor} from "../Governor.sol";
+import {GovernorCountingSimple} from "./GovernorCountingSimple.sol";
+import {Math} from "../../utils/math/Math.sol";
+
+/**
+ * @dev Extension of {Governor} for fractional voting.
+ *
+ * Similar to {GovernorCountingSimple}, this contract is a votes counting module for {Governor} that supports 3 options:
+ * Against, For, Abstain. Additionally, it includes a fourth option: Fractional, which allows voters to split their voting
+ * power amongst the other 3 options.
+ *
+ * Votes cast with the Fractional support must be accompanied by a `params` argument that is three packed `uint128` values
+ * representing the weight the delegate assigns to Against, For, and Abstain respectively. For those votes cast for the other
+ * 3 options, the `params` argument must be empty.
+ *
+ * This is mostly useful when the delegate is a contract that implements its own rules for voting. These delegate-contracts
+ * can cast fractional votes according to the preferences of multiple entities delegating their voting power.
+ *
+ * Some example use cases include:
+ *
+ * * Voting from tokens that are held by a DeFi pool
+ * * Voting from an L2 with tokens held by a bridge
+ * * 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]
+ */
+abstract contract GovernorCountingFractional is Governor {
+    using Math for *;
+
+    uint8 internal constant VOTE_TYPE_FRACTIONAL = 255;
+
+    struct ProposalVote {
+        uint256 againstVotes;
+        uint256 forVotes;
+        uint256 abstainVotes;
+        mapping(address voter => uint256) usedVotes;
+    }
+
+    /**
+     * @dev Mapping from proposal ID to vote tallies for that proposal.
+     */
+    mapping(uint256 => ProposalVote) private _proposalVotes;
+
+    /**
+     * @dev A fractional vote params uses more votes than are available for that user.
+     */
+    error GovernorExceedRemainingWeight(address voter, uint256 usedVotes, uint256 remainingWeight);
+
+    /**
+     * @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,fractional&quorum=for,abstain&params=fractional";
+    }
+
+    /**
+     * @dev See {IGovernor-hasVoted}.
+     */
+    function hasVoted(uint256 proposalId, address account) public view virtual override returns (bool) {
+        return usedVotes(proposalId, account) > 0;
+    }
+
+    /**
+     * @dev Get the number of votes already cast by `account` for a proposal with `proposalId`. Useful for
+     * integrations that allow delegates to cast rolling, partial votes.
+     */
+    function usedVotes(uint256 proposalId, address account) public view virtual returns (uint256) {
+        return _proposalVotes[proposalId].usedVotes[account];
+    }
+
+    /**
+     * @dev Get current distribution of votes for a given proposal.
+     */
+    function proposalVotes(
+        uint256 proposalId
+    ) public view virtual returns (uint256 againstVotes, uint256 forVotes, uint256 abstainVotes) {
+        ProposalVote storage proposalVote = _proposalVotes[proposalId];
+        return (proposalVote.againstVotes, proposalVote.forVotes, proposalVote.abstainVotes);
+    }
+
+    /**
+     * @dev See {Governor-_quorumReached}.
+     */
+    function _quorumReached(uint256 proposalId) internal view virtual override returns (bool) {
+        ProposalVote storage proposalVote = _proposalVotes[proposalId];
+        return quorum(proposalSnapshot(proposalId)) <= proposalVote.forVotes + proposalVote.abstainVotes;
+    }
+
+    /**
+     * @dev See {Governor-_voteSucceeded}. In this module, forVotes must be > againstVotes.
+     */
+    function _voteSucceeded(uint256 proposalId) internal view virtual override returns (bool) {
+        ProposalVote storage proposalVote = _proposalVotes[proposalId];
+        return proposalVote.forVotes > proposalVote.againstVotes;
+    }
+
+    /**
+     * @dev See {Governor-_countVote}. Function that records the delegate's votes.
+     *
+     * Executing this function consumes (part of) the delegate's weight on the proposal. This weight can be
+     * distributed amongst the 3 options (Against, For, Abstain) by specifying a fractional `support`.
+     *
+     * This counting module supports two vote casting modes: nominal and fractional.
+     *
+     * - Nominal: A nominal vote is cast by setting `support` to one of the 3 bravo options (Against, For, Abstain).
+     * - Fractional: A fractional vote is cast by setting `support` to `type(uint8).max` (255).
+     *
+     * Casting a nominal vote requires `params` to be empty and consumes the delegate's full remaining weight on the
+     * proposal for the specified `support` option. This is similar to the {GovernorCountingSimple} module and follows
+     * the `VoteType` enum from Governor Bravo. As a consequence, no vote weight remains unspent so no further voting
+     * is possible (for this `proposalId` and this `account`).
+     *
+     * Casting a fractional vote consumes a fraction of the delegate's remaining weight on the proposal according to the
+     * weights the delegate assigns to each support option (Against, For, Abstain respectively). The sum total of the
+     * three decoded vote weights _must_ be less than or equal to the delegate's remaining weight on the proposal (i.e.
+     * their checkpointed total weight minus votes already cast on the proposal). This format can be produced using:
+     *
+     * `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))`
+     *
+     * NOTE: Consider that fractional voting restricts the number of casted vote (in each category) to 128 bits.
+     * Depending on how many decimals the underlying token has, a single voter may require to split their vote into
+     * multiple vote operations. For precision higher than ~30 decimals, large token holders may require an
+     * potentially large number of calls to cast all their votes. The voter has the possibility to cast all the
+     * remaining votes in a single operation using the traditional "bravo" vote.
+     */
+    // slither-disable-next-line cyclomatic-complexity
+    function _countVote(
+        uint256 proposalId,
+        address account,
+        uint8 support,
+        uint256 totalWeight,
+        bytes memory params
+    ) internal virtual override returns (uint256) {
+        // Compute number of remaining votes. Returns 0 on overflow.
+        (, uint256 remainingWeight) = totalWeight.trySub(usedVotes(proposalId, account));
+        if (remainingWeight == 0) {
+            revert GovernorAlreadyCastVote(account);
+        }
+
+        uint256 againstVotes = 0;
+        uint256 forVotes = 0;
+        uint256 abstainVotes = 0;
+        uint256 usedWeight;
+
+        // For clarity of event indexing, fractional voting must be clearly advertised in the "support" field.
+        //
+        // Supported `support` value must be:
+        // - "Full" voting: `support = 0` (Against), `1` (For) or `2` (Abstain), with empty params.
+        // - "Fractional" voting: `support = 255`, with 48 bytes params.
+        if (support == uint8(GovernorCountingSimple.VoteType.Against)) {
+            if (params.length != 0) revert GovernorInvalidVoteParams();
+            usedWeight = againstVotes = remainingWeight;
+        } else if (support == uint8(GovernorCountingSimple.VoteType.For)) {
+            if (params.length != 0) revert GovernorInvalidVoteParams();
+            usedWeight = forVotes = remainingWeight;
+        } else if (support == uint8(GovernorCountingSimple.VoteType.Abstain)) {
+            if (params.length != 0) revert GovernorInvalidVoteParams();
+            usedWeight = abstainVotes = remainingWeight;
+        } else if (support == VOTE_TYPE_FRACTIONAL) {
+            // The `params` argument is expected to be three packed `uint128`:
+            // `abi.encodePacked(uint128(againstVotes), uint128(forVotes), uint128(abstainVotes))`
+            if (params.length != 0x30) revert GovernorInvalidVoteParams();
+
+            assembly ("memory-safe") {
+                againstVotes := shr(128, mload(add(params, 0x20)))
+                forVotes := shr(128, mload(add(params, 0x30)))
+                abstainVotes := shr(128, mload(add(params, 0x40)))
+                usedWeight := add(add(againstVotes, forVotes), abstainVotes) // inputs are uint128: cannot overflow
+            }
+
+            // check parsed arguments are valid
+            if (usedWeight > remainingWeight) {
+                revert GovernorExceedRemainingWeight(account, usedWeight, remainingWeight);
+            }
+        } else {
+            revert GovernorInvalidVoteType();
+        }
+
+        // update votes tracking
+        ProposalVote storage details = _proposalVotes[proposalId];
+        if (againstVotes > 0) details.againstVotes += againstVotes;
+        if (forVotes > 0) details.forVotes += forVotes;
+        if (abstainVotes > 0) details.abstainVotes += abstainVotes;
+        details.usedVotes[account] += usedWeight;
+
+        return usedWeight;
+    }
+}

+ 7 - 5
contracts/governance/extensions/GovernorCountingSimple.sol

@@ -77,9 +77,9 @@ abstract contract GovernorCountingSimple is Governor {
         uint256 proposalId,
         address account,
         uint8 support,
-        uint256 weight,
+        uint256 totalWeight,
         bytes memory // params
-    ) internal virtual override {
+    ) internal virtual override returns (uint256) {
         ProposalVote storage proposalVote = _proposalVotes[proposalId];
 
         if (proposalVote.hasVoted[account]) {
@@ -88,13 +88,15 @@ abstract contract GovernorCountingSimple is Governor {
         proposalVote.hasVoted[account] = true;
 
         if (support == uint8(VoteType.Against)) {
-            proposalVote.againstVotes += weight;
+            proposalVote.againstVotes += totalWeight;
         } else if (support == uint8(VoteType.For)) {
-            proposalVote.forVotes += weight;
+            proposalVote.forVotes += totalWeight;
         } else if (support == uint8(VoteType.Abstain)) {
-            proposalVote.abstainVotes += weight;
+            proposalVote.abstainVotes += totalWeight;
         } else {
             revert GovernorInvalidVoteType();
         }
+
+        return totalWeight;
     }
 }

+ 1 - 1
contracts/interfaces/IERC1363Receiver.sol

@@ -18,7 +18,7 @@ interface IERC1363Receiver {
      * (i.e. 0x88a7ca5c, or its own function selector).
      *
      * @param operator The address which called `transferAndCall` or `transferFromAndCall` function.
-     * @param from The address which are tokens transferred from.
+     * @param from The address which the tokens are transferred from.
      * @param value The amount of tokens transferred.
      * @param data Additional data with no specified format.
      * @return `bytes4(keccak256("onTransferReceived(address,address,uint256,bytes)"))` if transfer is allowed unless throwing.

+ 3 - 0
contracts/interfaces/IERC2981.sol

@@ -15,6 +15,9 @@ interface IERC2981 is IERC165 {
     /**
      * @dev Returns how much royalty is owed and to whom, based on a sale price that may be denominated in any unit of
      * exchange. The royalty amount is denominated and should be paid in that same unit of exchange.
+     *
+     * NOTE: ERC-2981 allows setting the royalty to 100% of the price. In that case all the price would be sent to the
+     * royalty receiver and 0 tokens to the seller. Contracts dealing with royalty should consider empty transfers.
      */
     function royaltyInfo(
         uint256 tokenId,

+ 3 - 0
contracts/interfaces/README.adoc

@@ -40,6 +40,7 @@ are useful to interact with third party contracts that implement them.
 - {IERC5313}
 - {IERC5805}
 - {IERC6372}
+- {IERC7674}
 
 == Detailed ABI
 
@@ -80,3 +81,5 @@ are useful to interact with third party contracts that implement them.
 {{IERC5805}}
 
 {{IERC6372}}
+
+{{IERC7674}}

+ 16 - 0
contracts/interfaces/draft-IERC7674.sol

@@ -0,0 +1,16 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {IERC20} from "./IERC20.sol";
+
+/**
+ * @dev Temporary Approval Extension for ERC-20 (https://github.com/ethereum/ERCs/pull/358[ERC-7674])
+ */
+interface IERC7674 is IERC20 {
+    /**
+     * @dev Set the temporary allowance, allowing `spender` to withdraw (within the same transaction) assets
+     * held by the caller.
+     */
+    function temporaryApprove(address spender, uint256 value) external returns (bool success);
+}

+ 4 - 6
contracts/metatx/ERC2771Forwarder.sol

@@ -286,7 +286,7 @@ contract ERC2771Forwarder is EIP712, Nonces {
 
             uint256 gasLeft;
 
-            assembly {
+            assembly ("memory-safe") {
                 success := call(reqGas, to, value, add(data, 0x20), mload(data), 0, 0)
                 gasLeft := gas()
             }
@@ -309,8 +309,7 @@ contract ERC2771Forwarder is EIP712, Nonces {
         bool success;
         uint256 returnSize;
         uint256 returnValue;
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             // Perform the staticcall and save the result in the scratch space.
             // | Location  | Content  | Content (Hex)                                                      |
             // |-----------|----------|--------------------------------------------------------------------|
@@ -347,7 +346,7 @@ contract ERC2771Forwarder is EIP712, Nonces {
         // We can't know X after CALL dynamic costs, but we want it to be such that X * 63 / 64 >= req.gas.
         // Let Y be the gas used in the subcall. gasleft() measured immediately after the subcall will be gasleft() = X - Y.
         // If the subcall ran out of gas, then Y = X * 63 / 64 and gasleft() = X - Y = X / 64.
-        // Under this assumption req.gas / 63 > gasleft() is true is true if and only if
+        // Under this assumption req.gas / 63 > gasleft() is true if and only if
         // req.gas / 63 > X / 64, or equivalently req.gas > X * 63 / 64.
         // This means that if the subcall runs out of gas we are able to detect that insufficient gas was passed.
         //
@@ -362,8 +361,7 @@ contract ERC2771Forwarder is EIP712, Nonces {
             // We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since
             // neither revert or assert consume all gas since Solidity 0.8.20
             // https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require
-            /// @solidity memory-safe-assembly
-            assembly {
+            assembly ("memory-safe") {
                 invalid()
             }
         }

+ 21 - 0
contracts/mocks/AccessManagerMock.sol

@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {AccessManager} from "../access/manager/AccessManager.sol";
+import {StorageSlot} from "../utils/StorageSlot.sol";
+
+contract AccessManagerMock is AccessManager {
+    event CalledRestricted(address caller);
+    event CalledUnrestricted(address caller);
+
+    constructor(address initialAdmin) AccessManager(initialAdmin) {}
+
+    function fnRestricted() public onlyAuthorized {
+        emit CalledRestricted(msg.sender);
+    }
+
+    function fnUnrestricted() public {
+        emit CalledUnrestricted(msg.sender);
+    }
+}

+ 20 - 0
contracts/mocks/BatchCaller.sol

@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {Address} from "../utils/Address.sol";
+
+contract BatchCaller {
+    struct Call {
+        address target;
+        uint256 value;
+        bytes data;
+    }
+
+    function execute(Call[] calldata calls) external returns (bytes[] memory) {
+        bytes[] memory returndata = new bytes[](calls.length);
+        for (uint256 i = 0; i < calls.length; ++i) {
+            returndata[i] = Address.functionCallWithValue(calls[i].target, calls[i].data, calls[i].value);
+        }
+        return returndata;
+    }
+}

+ 34 - 0
contracts/mocks/ConstructorMock.sol

@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+contract ConstructorMock {
+    bool foo;
+
+    enum RevertType {
+        None,
+        RevertWithoutMessage,
+        RevertWithMessage,
+        RevertWithCustomError,
+        Panic
+    }
+
+    error CustomError();
+
+    constructor(RevertType error) {
+        // After transpilation to upgradeable contract, the constructor will become an initializer
+        // To silence the `... can be restricted to view` warning, we write to state
+        foo = true;
+
+        if (error == RevertType.RevertWithoutMessage) {
+            revert();
+        } else if (error == RevertType.RevertWithMessage) {
+            revert("ConstructorMock: reverting");
+        } else if (error == RevertType.RevertWithCustomError) {
+            revert CustomError();
+        } else if (error == RevertType.Panic) {
+            uint256 a = uint256(0) / uint256(0);
+            a;
+        }
+    }
+}

+ 62 - 0
contracts/mocks/MerkleProofCustomHashMock.sol

@@ -0,0 +1,62 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";
+
+// This could be a library, but then we would have to add it to the Stateless.sol mock for upgradeable tests
+abstract contract MerkleProofCustomHashMock {
+    function customHash(bytes32 a, bytes32 b) internal pure returns (bytes32) {
+        return a < b ? sha256(abi.encode(a, b)) : sha256(abi.encode(b, a));
+    }
+
+    function verify(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal view returns (bool) {
+        return MerkleProof.verify(proof, root, leaf, customHash);
+    }
+
+    function processProof(bytes32[] calldata proof, bytes32 leaf) internal view returns (bytes32) {
+        return MerkleProof.processProof(proof, leaf, customHash);
+    }
+
+    function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal view returns (bool) {
+        return MerkleProof.verifyCalldata(proof, root, leaf, customHash);
+    }
+
+    function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal view returns (bytes32) {
+        return MerkleProof.processProofCalldata(proof, leaf, customHash);
+    }
+
+    function multiProofVerify(
+        bytes32[] calldata proof,
+        bool[] calldata proofFlags,
+        bytes32 root,
+        bytes32[] calldata leaves
+    ) internal view returns (bool) {
+        return MerkleProof.multiProofVerify(proof, proofFlags, root, leaves, customHash);
+    }
+
+    function processMultiProof(
+        bytes32[] calldata proof,
+        bool[] calldata proofFlags,
+        bytes32[] calldata leaves
+    ) internal view returns (bytes32) {
+        return MerkleProof.processMultiProof(proof, proofFlags, leaves, customHash);
+    }
+
+    function multiProofVerifyCalldata(
+        bytes32[] calldata proof,
+        bool[] calldata proofFlags,
+        bytes32 root,
+        bytes32[] calldata leaves
+    ) internal view returns (bool) {
+        return MerkleProof.multiProofVerifyCalldata(proof, proofFlags, root, leaves, customHash);
+    }
+
+    function processMultiProofCalldata(
+        bytes32[] calldata proof,
+        bool[] calldata proofFlags,
+        bytes32[] calldata leaves
+    ) internal view returns (bytes32) {
+        return MerkleProof.processMultiProofCalldata(proof, proofFlags, leaves, customHash);
+    }
+}

+ 3 - 0
contracts/mocks/Stateless.sol

@@ -22,11 +22,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 {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 {P256} from "../utils/cryptography/P256.sol";
 import {Panic} from "../utils/Panic.sol";
 import {Packing} from "../utils/Packing.sol";
+import {RSA} from "../utils/cryptography/RSA.sol";
 import {SafeCast} from "../utils/math/SafeCast.sol";
 import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol";
 import {ShortStrings} from "../utils/ShortStrings.sol";

+ 6 - 0
contracts/mocks/docs/ERC4626Fees.sol

@@ -8,6 +8,12 @@ import {SafeERC20} from "../../token/ERC20/utils/SafeERC20.sol";
 import {Math} from "../../utils/math/Math.sol";
 
 /// @dev ERC-4626 vault with entry/exit fees expressed in https://en.wikipedia.org/wiki/Basis_point[basis point (bp)].
+///
+/// NOTE: The contract charges fees in terms of assets, not shares. This means that the fees are calculated based on the
+/// amount of assets that are being deposited or withdrawn, and not based on the amount of shares that are being minted or
+/// redeemed. This is an opinionated design decision that should be taken into account when integrating this contract.
+///
+/// WARNING: This contract has not been audited and shouldn't be considered production ready. Consider using it with caution.
 abstract contract ERC4626Fees is ERC4626 {
     using Math for uint256;
 

+ 14 - 0
contracts/mocks/governance/GovernorFractionalMock.sol

@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {Governor} from "../../governance/Governor.sol";
+import {GovernorSettings} from "../../governance/extensions/GovernorSettings.sol";
+import {GovernorCountingFractional} from "../../governance/extensions/GovernorCountingFractional.sol";
+import {GovernorVotesQuorumFraction} from "../../governance/extensions/GovernorVotesQuorumFraction.sol";
+
+abstract contract GovernorFractionalMock is GovernorSettings, GovernorVotesQuorumFraction, GovernorCountingFractional {
+    function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) {
+        return super.proposalThreshold();
+    }
+}

+ 1 - 1
contracts/mocks/governance/GovernorWithParamsMock.sol

@@ -41,7 +41,7 @@ abstract contract GovernorWithParamsMock is GovernorVotes, GovernorCountingSimpl
         uint8 support,
         uint256 weight,
         bytes memory params
-    ) internal override(Governor, GovernorCountingSimple) {
+    ) internal override(Governor, GovernorCountingSimple) returns (uint256) {
         if (params.length > 0) {
             (uint256 _uintParam, string memory _strParam) = abi.decode(params, (uint256, string));
             emit CountParams(_uintParam, _strParam);

+ 38 - 0
contracts/mocks/token/ERC20GetterHelper.sol

@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {IERC20} from "../../token/ERC20/IERC20.sol";
+import {IERC20Metadata} from "../../token/ERC20/extensions/IERC20Metadata.sol";
+
+contract ERC20GetterHelper {
+    event ERC20TotalSupply(IERC20 token, uint256 totalSupply);
+    event ERC20BalanceOf(IERC20 token, address account, uint256 balanceOf);
+    event ERC20Allowance(IERC20 token, address owner, address spender, uint256 allowance);
+    event ERC20Name(IERC20Metadata token, string name);
+    event ERC20Symbol(IERC20Metadata token, string symbol);
+    event ERC20Decimals(IERC20Metadata token, uint8 decimals);
+
+    function totalSupply(IERC20 token) external {
+        emit ERC20TotalSupply(token, token.totalSupply());
+    }
+
+    function balanceOf(IERC20 token, address account) external {
+        emit ERC20BalanceOf(token, account, token.balanceOf(account));
+    }
+
+    function allowance(IERC20 token, address owner, address spender) external {
+        emit ERC20Allowance(token, owner, spender, token.allowance(owner, spender));
+    }
+
+    function name(IERC20Metadata token) external {
+        emit ERC20Name(token, token.name());
+    }
+
+    function symbol(IERC20Metadata token) external {
+        emit ERC20Symbol(token, token.symbol());
+    }
+
+    function decimals(IERC20Metadata token) external {
+        emit ERC20Decimals(token, token.decimals());
+    }
+}

+ 154 - 18
contracts/proxy/Clones.sol

@@ -3,6 +3,7 @@
 
 pragma solidity ^0.8.20;
 
+import {Create2} from "../utils/Create2.sol";
 import {Errors} from "../utils/Errors.sol";
 
 /**
@@ -17,6 +18,8 @@ import {Errors} from "../utils/Errors.sol";
  * deterministic method.
  */
 library Clones {
+    error CloneArgumentsTooLong();
+
     /**
      * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
      *
@@ -37,14 +40,12 @@ library Clones {
         if (address(this).balance < value) {
             revert Errors.InsufficientBalance(address(this).balance, value);
         }
-        /// @solidity memory-safe-assembly
-        assembly {
-            // Stores the bytecode after address
-            mstore(0x20, 0x5af43d82803e903d91602b57fd5bf3)
-            // implementation address
-            mstore(0x11, implementation)
-            // Packs the first 3 bytes of the `implementation` address with the bytecode before the address.
-            mstore(0x00, or(shr(0x88, implementation), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
+        assembly ("memory-safe") {
+            // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
+            // of the `implementation` address with the bytecode before the address.
+            mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
+            // Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
+            mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
             instance := create(value, 0x09, 0x37)
         }
         if (instance == address(0)) {
@@ -78,14 +79,12 @@ library Clones {
         if (address(this).balance < value) {
             revert Errors.InsufficientBalance(address(this).balance, value);
         }
-        /// @solidity memory-safe-assembly
-        assembly {
-            // Stores the bytecode after address
-            mstore(0x20, 0x5af43d82803e903d91602b57fd5bf3)
-            // implementation address
-            mstore(0x11, implementation)
-            // Packs the first 3 bytes of the `implementation` address with the bytecode before the address.
-            mstore(0x00, or(shr(0x88, implementation), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
+        assembly ("memory-safe") {
+            // Cleans the upper 96 bits of the `implementation` word, then packs the first 3 bytes
+            // of the `implementation` address with the bytecode before the address.
+            mstore(0x00, or(shr(0xe8, shl(0x60, implementation)), 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000))
+            // Packs the remaining 17 bytes of `implementation` with the bytecode after the address.
+            mstore(0x20, or(shl(0x78, implementation), 0x5af43d82803e903d91602b57fd5bf3))
             instance := create2(value, 0x09, 0x37, salt)
         }
         if (instance == address(0)) {
@@ -101,8 +100,7 @@ library Clones {
         bytes32 salt,
         address deployer
     ) internal pure returns (address predicted) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             let ptr := mload(0x40)
             mstore(add(ptr, 0x38), deployer)
             mstore(add(ptr, 0x24), 0x5af43d82803e903d91602b57fd5bf3ff)
@@ -123,4 +121,142 @@ library Clones {
     ) internal view returns (address predicted) {
         return predictDeterministicAddress(implementation, salt, address(this));
     }
+
+    /**
+     * @dev Deploys and returns the address of a clone that mimics the behavior of `implementation` with custom
+     * immutable arguments. These are provided through `args` and cannot be changed after deployment. To
+     * access the arguments within the implementation, use {fetchCloneArgs}.
+     *
+     * This function uses the create opcode, which should never revert.
+     */
+    function cloneWithImmutableArgs(address implementation, bytes memory args) internal returns (address instance) {
+        return cloneWithImmutableArgs(implementation, args, 0);
+    }
+
+    /**
+     * @dev Same as {xref-Clones-cloneWithImmutableArgs-address-bytes-}[cloneWithImmutableArgs], but with a `value`
+     * parameter to send native currency to the new contract.
+     *
+     * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
+     * to always have enough balance for new deployments. Consider exposing this function under a payable method.
+     */
+    function cloneWithImmutableArgs(
+        address implementation,
+        bytes memory args,
+        uint256 value
+    ) internal returns (address instance) {
+        if (address(this).balance < value) {
+            revert Errors.InsufficientBalance(address(this).balance, value);
+        }
+        bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
+        assembly ("memory-safe") {
+            instance := create(value, add(bytecode, 0x20), mload(bytecode))
+        }
+        if (instance == address(0)) {
+            revert Errors.FailedDeployment();
+        }
+    }
+
+    /**
+     * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation` with custom
+     * immutable arguments. These are provided through `args` and cannot be changed after deployment. To
+     * 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.
+     */
+    function cloneDeterministicWithImmutableArgs(
+        address implementation,
+        bytes memory args,
+        bytes32 salt
+    ) internal returns (address instance) {
+        return cloneDeterministicWithImmutableArgs(implementation, args, salt, 0);
+    }
+
+    /**
+     * @dev Same as {xref-Clones-cloneDeterministicWithImmutableArgs-address-bytes-bytes32-}[cloneDeterministicWithImmutableArgs],
+     * but with a `value` parameter to send native currency to the new contract.
+     *
+     * NOTE: Using a non-zero value at creation will require the contract using this function (e.g. a factory)
+     * to always have enough balance for new deployments. Consider exposing this function under a payable method.
+     */
+    function cloneDeterministicWithImmutableArgs(
+        address implementation,
+        bytes memory args,
+        bytes32 salt,
+        uint256 value
+    ) internal returns (address instance) {
+        bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
+        return Create2.deploy(value, salt, bytecode);
+    }
+
+    /**
+     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
+     */
+    function predictDeterministicAddressWithImmutableArgs(
+        address implementation,
+        bytes memory args,
+        bytes32 salt,
+        address deployer
+    ) internal pure returns (address predicted) {
+        bytes memory bytecode = _cloneCodeWithImmutableArgs(implementation, args);
+        return Create2.computeAddress(salt, keccak256(bytecode), deployer);
+    }
+
+    /**
+     * @dev Computes the address of a clone deployed using {Clones-cloneDeterministicWithImmutableArgs}.
+     */
+    function predictDeterministicAddressWithImmutableArgs(
+        address implementation,
+        bytes memory args,
+        bytes32 salt
+    ) internal view returns (address predicted) {
+        return predictDeterministicAddressWithImmutableArgs(implementation, args, salt, address(this));
+    }
+
+    /**
+     * @dev Get the immutable args attached to a clone.
+     *
+     * - If `instance` is a clone that was deployed using `clone` or `cloneDeterministic`, this
+     *   function will return an empty array.
+     * - If `instance` is a clone that was deployed using `cloneWithImmutableArgs` or
+     *   `cloneDeterministicWithImmutableArgs`, this function will return the args array used at
+     *   creation.
+     * - If `instance` is NOT a clone deployed using this library, the behavior is undefined. This
+     *   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
+        assembly ("memory-safe") {
+            extcodecopy(instance, add(result, 0x20), 0x2d, mload(result))
+        }
+        return result;
+    }
+
+    /**
+     * @dev Helper that prepares the initcode of the proxy with immutable args.
+     *
+     * An assembly variant of this function requires copying the `args` array, which can be efficiently done using
+     * `mcopy`. Unfortunately, that opcode is not available before cancun. A pure solidity implementation using
+     * abi.encodePacked is more expensive but also more portable and easier to review.
+     *
+     * NOTE: https://eips.ethereum.org/EIPS/eip-170[EIP-170] limits the length of the contract code to 24576 bytes.
+     * With the proxy code taking 45 bytes, that limits the length of the immutable args to 24531 bytes.
+     */
+    function _cloneCodeWithImmutableArgs(
+        address implementation,
+        bytes memory args
+    ) private pure returns (bytes memory) {
+        if (args.length > 0x5fd3) revert CloneArgumentsTooLong();
+        return
+            abi.encodePacked(
+                hex"61",
+                uint16(args.length + 0x2d),
+                hex"3d81600a3d39f3363d3d373d3d3d363d73",
+                implementation,
+                hex"5af43d82803e903d91602b57fd5bf3",
+                args
+            );
+    }
 }

+ 1 - 1
contracts/proxy/ERC1967/ERC1967Utils.sol

@@ -9,7 +9,7 @@ import {Address} from "../../utils/Address.sol";
 import {StorageSlot} from "../../utils/StorageSlot.sol";
 
 /**
- * @dev This abstract contract provides getters and event emitting update functions for
+ * @dev This library provides getters and event emitting update functions for
  * https://eips.ethereum.org/EIPS/eip-1967[ERC-1967] slots.
  */
 library ERC1967Utils {

+ 7 - 1
contracts/proxy/transparent/TransparentUpgradeableProxy.sol

@@ -15,7 +15,13 @@ import {ProxyAdmin} from "./ProxyAdmin.sol";
  * include them in the ABI so this interface must be used to interact with it.
  */
 interface ITransparentUpgradeableProxy is IERC1967 {
-    function upgradeToAndCall(address, bytes calldata) external payable;
+    /**
+     * @dev Upgrade the implementation of the proxy to `newImplementation`, and subsequently execute the function call
+     * encoded in `data`.
+     *
+     * See {UUPSUpgradeable-upgradeToAndCall}
+     */
+    function upgradeToAndCall(address newImplementation, bytes calldata data) external payable;
 }
 
 /**

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

@@ -381,8 +381,7 @@ abstract contract ERC1155 is Context, ERC165, IERC1155, IERC1155MetadataURI, IER
         uint256 element1,
         uint256 element2
     ) private pure returns (uint256[] memory array1, uint256[] memory array2) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             // Load the free memory pointer
             array1 := mload(0x40)
             // Set array length to 1

+ 2 - 0
contracts/token/ERC1155/README.adoc

@@ -39,3 +39,5 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
 == Utilities
 
 {{ERC1155Holder}}
+
+{{ERC1155Utils}}

+ 7 - 4
contracts/token/ERC1155/extensions/ERC1155Supply.sol

@@ -4,6 +4,7 @@
 pragma solidity ^0.8.20;
 
 import {ERC1155} from "../ERC1155.sol";
+import {Arrays} from "../../../utils/Arrays.sol";
 
 /**
  * @dev Extension of ERC-1155 that adds tracking of total supply per id.
@@ -19,6 +20,8 @@ import {ERC1155} from "../ERC1155.sol";
  * CAUTION: This extension should not be added in an upgrade to an already deployed contract.
  */
 abstract contract ERC1155Supply is ERC1155 {
+    using Arrays for uint256[];
+
     mapping(uint256 id => uint256) private _totalSupply;
     uint256 private _totalSupplyAll;
 
@@ -57,9 +60,9 @@ abstract contract ERC1155Supply is ERC1155 {
         if (from == address(0)) {
             uint256 totalMintValue = 0;
             for (uint256 i = 0; i < ids.length; ++i) {
-                uint256 value = values[i];
+                uint256 value = values.unsafeMemoryAccess(i);
                 // Overflow check required: The rest of the code assumes that totalSupply never overflows
-                _totalSupply[ids[i]] += value;
+                _totalSupply[ids.unsafeMemoryAccess(i)] += value;
                 totalMintValue += value;
             }
             // Overflow check required: The rest of the code assumes that totalSupplyAll never overflows
@@ -69,11 +72,11 @@ abstract contract ERC1155Supply is ERC1155 {
         if (to == address(0)) {
             uint256 totalBurnValue = 0;
             for (uint256 i = 0; i < ids.length; ++i) {
-                uint256 value = values[i];
+                uint256 value = values.unsafeMemoryAccess(i);
 
                 unchecked {
                     // Overflow not possible: values[i] <= balanceOf(from, ids[i]) <= totalSupply(ids[i])
-                    _totalSupply[ids[i]] -= value;
+                    _totalSupply[ids.unsafeMemoryAccess(i)] -= value;
                     // Overflow not possible: sum_i(values[i]) <= sum_i(totalSupply(ids[i])) <= totalSupplyAll
                     totalBurnValue += value;
                 }

+ 4 - 6
contracts/token/ERC1155/utils/ERC1155Utils.sol

@@ -15,7 +15,7 @@ library ERC1155Utils {
      * @dev Performs an acceptance check for the provided `operator` by calling {IERC1155-onERC1155Received}
      * on the `to` address. The `operator` is generally the address that initiated the token transfer (i.e. `msg.sender`).
      *
-     * The acceptance call is not executed and treated as a no-op if the target address is doesn't contain code (i.e. an EOA).
+     * The acceptance call is not executed and treated as a no-op if the target address doesn't contain code (i.e. an EOA).
      * Otherwise, the recipient must implement {IERC1155Receiver-onERC1155Received} and return the acceptance magic value to accept
      * the transfer.
      */
@@ -38,8 +38,7 @@ library ERC1155Utils {
                     // non-IERC1155Receiver implementer
                     revert IERC1155Errors.ERC1155InvalidReceiver(to);
                 } else {
-                    /// @solidity memory-safe-assembly
-                    assembly {
+                    assembly ("memory-safe") {
                         revert(add(32, reason), mload(reason))
                     }
                 }
@@ -51,7 +50,7 @@ library ERC1155Utils {
      * @dev Performs a batch acceptance check for the provided `operator` by calling {IERC1155-onERC1155BatchReceived}
      * on the `to` address. The `operator` is generally the address that initiated the token transfer (i.e. `msg.sender`).
      *
-     * The acceptance call is not executed and treated as a no-op if the target address is doesn't contain code (i.e. an EOA).
+     * The acceptance call is not executed and treated as a no-op if the target address doesn't contain code (i.e. an EOA).
      * Otherwise, the recipient must implement {IERC1155Receiver-onERC1155Received} and return the acceptance magic value to accept
      * the transfer.
      */
@@ -76,8 +75,7 @@ library ERC1155Utils {
                     // non-IERC1155Receiver implementer
                     revert IERC1155Errors.ERC1155InvalidReceiver(to);
                 } else {
-                    /// @solidity memory-safe-assembly
-                    assembly {
+                    assembly ("memory-safe") {
                         revert(add(32, reason), mload(reason))
                     }
                 }

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

@@ -22,6 +22,7 @@ Additionally there are multiple custom extensions, including:
 * {ERC20FlashMint}: token level support for flash loans through the minting and burning of ephemeral tokens (standardized as ERC-3156).
 * {ERC20Votes}: support for voting and vote delegation.
 * {ERC20Wrapper}: wrapper to create an ERC-20 backed by another ERC-20, with deposit and withdraw methods. Useful in conjunction with {ERC20Votes}.
+* {ERC20TemporaryApproval}: support for approvals lasting for only one transaction, as defined in ERC-7674.
 * {ERC1363}: support for calling the target of a transfer or approval, enabling code execution on the receiver within a single transaction.
 * {ERC4626}: tokenized vault that manages shares (represented as ERC-20) that are backed by assets (another ERC-20).
 
@@ -61,6 +62,8 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
 
 {{ERC20FlashMint}}
 
+{{ERC20TemporaryApproval}}
+
 {{ERC1363}}
 
 {{ERC4626}}
@@ -68,3 +71,5 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
 == Utilities
 
 {{SafeERC20}}
+
+{{ERC1363Utils}}

+ 13 - 80
contracts/token/ERC20/extensions/ERC1363.sol

@@ -4,10 +4,8 @@ pragma solidity ^0.8.20;
 
 import {ERC20} from "../ERC20.sol";
 import {IERC165, ERC165} from "../../../utils/introspection/ERC165.sol";
-
 import {IERC1363} from "../../../interfaces/IERC1363.sol";
-import {IERC1363Receiver} from "../../../interfaces/IERC1363Receiver.sol";
-import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol";
+import {ERC1363Utils} from "../utils/ERC1363Utils.sol";
 
 /**
  * @title ERC1363
@@ -16,30 +14,25 @@ import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol";
  * {ERC1363-transferFromAndCall} methods while calls after approvals can be made with {ERC1363-approveAndCall}
  */
 abstract contract ERC1363 is ERC20, ERC165, IERC1363 {
-    /**
-     * @dev Indicates a failure with the token `receiver`. Used in transfers.
-     * @param receiver Address to which tokens are being transferred.
-     */
-    error ERC1363InvalidReceiver(address receiver);
-
-    /**
-     * @dev Indicates a failure with the token `spender`. Used in approvals.
-     * @param spender Address that may be allowed to operate on tokens without being their owner.
-     */
-    error ERC1363InvalidSpender(address spender);
-
     /**
      * @dev Indicates a failure within the {transfer} part of a transferAndCall operation.
+     * @param receiver Address to which tokens are being transferred.
+     * @param value Amount of tokens to be transferred.
      */
-    error ERC1363TransferFailed(address to, uint256 value);
+    error ERC1363TransferFailed(address receiver, uint256 value);
 
     /**
      * @dev Indicates a failure within the {transferFrom} part of a transferFromAndCall operation.
+     * @param sender Address from which to send tokens.
+     * @param receiver Address to which tokens are being transferred.
+     * @param value Amount of tokens to be transferred.
      */
-    error ERC1363TransferFromFailed(address from, address to, uint256 value);
+    error ERC1363TransferFromFailed(address sender, address receiver, uint256 value);
 
     /**
      * @dev Indicates a failure within the {approve} part of a approveAndCall operation.
+     * @param spender Address which will spend the funds.
+     * @param value Amount of tokens to be spent.
      */
     error ERC1363ApproveFailed(address spender, uint256 value);
 
@@ -73,7 +66,7 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 {
         if (!transfer(to, value)) {
             revert ERC1363TransferFailed(to, value);
         }
-        _checkOnTransferReceived(_msgSender(), to, value, data);
+        ERC1363Utils.checkOnERC1363TransferReceived(_msgSender(), _msgSender(), to, value, data);
         return true;
     }
 
@@ -105,7 +98,7 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 {
         if (!transferFrom(from, to, value)) {
             revert ERC1363TransferFromFailed(from, to, value);
         }
-        _checkOnTransferReceived(from, to, value, data);
+        ERC1363Utils.checkOnERC1363TransferReceived(_msgSender(), from, to, value, data);
         return true;
     }
 
@@ -132,67 +125,7 @@ abstract contract ERC1363 is ERC20, ERC165, IERC1363 {
         if (!approve(spender, value)) {
             revert ERC1363ApproveFailed(spender, value);
         }
-        _checkOnApprovalReceived(spender, value, data);
+        ERC1363Utils.checkOnERC1363ApprovalReceived(_msgSender(), spender, value, data);
         return true;
     }
-
-    /**
-     * @dev Performs a call to {IERC1363Receiver-onTransferReceived} on a target address.
-     *
-     * Requirements:
-     *
-     * - The target has code (i.e. is a contract).
-     * - The target `to` must implement the {IERC1363Receiver} interface.
-     * - The target must return the {IERC1363Receiver-onTransferReceived} selector to accept the transfer.
-     */
-    function _checkOnTransferReceived(address from, address to, uint256 value, bytes memory data) private {
-        if (to.code.length == 0) {
-            revert ERC1363InvalidReceiver(to);
-        }
-
-        try IERC1363Receiver(to).onTransferReceived(_msgSender(), from, value, data) returns (bytes4 retval) {
-            if (retval != IERC1363Receiver.onTransferReceived.selector) {
-                revert ERC1363InvalidReceiver(to);
-            }
-        } catch (bytes memory reason) {
-            if (reason.length == 0) {
-                revert ERC1363InvalidReceiver(to);
-            } else {
-                /// @solidity memory-safe-assembly
-                assembly {
-                    revert(add(32, reason), mload(reason))
-                }
-            }
-        }
-    }
-
-    /**
-     * @dev Performs a call to {IERC1363Spender-onApprovalReceived} on a target address.
-     *
-     * Requirements:
-     *
-     * - The target has code (i.e. is a contract).
-     * - The target `spender` must implement the {IERC1363Spender} interface.
-     * - The target must return the {IERC1363Spender-onApprovalReceived} selector to accept the approval.
-     */
-    function _checkOnApprovalReceived(address spender, uint256 value, bytes memory data) private {
-        if (spender.code.length == 0) {
-            revert ERC1363InvalidSpender(spender);
-        }
-
-        try IERC1363Spender(spender).onApprovalReceived(_msgSender(), value, data) returns (bytes4 retval) {
-            if (retval != IERC1363Spender.onApprovalReceived.selector) {
-                revert ERC1363InvalidSpender(spender);
-            }
-        } catch (bytes memory reason) {
-            if (reason.length == 0) {
-                revert ERC1363InvalidSpender(spender);
-            } else {
-                /// @solidity memory-safe-assembly
-                assembly {
-                    revert(add(32, reason), mload(reason))
-                }
-            }
-        }
-    }
 }

+ 119 - 0
contracts/token/ERC20/extensions/draft-ERC20TemporaryApproval.sol

@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.24;
+
+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";
+
+/**
+ * @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.
+ */
+abstract contract ERC20TemporaryApproval is ERC20, IERC7674 {
+    using SlotDerivation for bytes32;
+    using StorageSlot for bytes32;
+    using StorageSlot for StorageSlot.Uint256SlotType;
+
+    // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.ERC20_TEMPORARY_APPROVAL_STORAGE")) - 1)) & ~bytes32(uint256(0xff))
+    bytes32 private constant ERC20_TEMPORARY_APPROVAL_STORAGE =
+        0xea2d0e77a01400d0111492b1321103eed560d8fe44b9a7c2410407714583c400;
+
+    /**
+     * @dev {allowance} override that includes the temporary allowance when looking up the current allowance. If
+     * adding up the persistent and the temporary allowances result in an overflow, type(uint256).max is returned.
+     */
+    function allowance(address owner, address spender) public view virtual override(IERC20, ERC20) returns (uint256) {
+        (bool success, uint256 amount) = Math.tryAdd(
+            super.allowance(owner, spender),
+            _temporaryAllowance(owner, spender)
+        );
+        return success ? amount : type(uint256).max;
+    }
+
+    /**
+     * @dev Internal getter for the current temporary allowance that `spender` has over `owner` tokens.
+     */
+    function _temporaryAllowance(address owner, address spender) internal view virtual returns (uint256) {
+        return _temporaryAllowanceSlot(owner, spender).tload();
+    }
+
+    /**
+     * @dev Alternative to {approve} that sets a `value` amount of tokens as the temporary allowance of `spender` over
+     * the caller's tokens.
+     *
+     * Returns a boolean value indicating whether the operation succeeded.
+     *
+     * Requirements:
+     * - `spender` cannot be the zero address.
+     *
+     * Does NOT emit an {Approval} event.
+     */
+    function temporaryApprove(address spender, uint256 value) public virtual returns (bool) {
+        _temporaryApprove(_msgSender(), spender, value);
+        return true;
+    }
+
+    /**
+     * @dev Sets `value` as the temporary allowance of `spender` over the `owner` s tokens.
+     *
+     * This internal function is equivalent to `temporaryApprove`, and can be used to e.g. set automatic allowances
+     * for certain subsystems, etc.
+     *
+     * Requirements:
+     * - `owner` cannot be the zero address.
+     * - `spender` cannot be the zero address.
+     *
+     * Does NOT emit an {Approval} event.
+     */
+    function _temporaryApprove(address owner, address spender, uint256 value) internal virtual {
+        if (owner == address(0)) {
+            revert ERC20InvalidApprover(address(0));
+        }
+        if (spender == address(0)) {
+            revert ERC20InvalidSpender(address(0));
+        }
+        _temporaryAllowanceSlot(owner, spender).tstore(value);
+    }
+
+    /**
+     * @dev {_spendAllowance} override that consumes the temporary allowance (if any) before eventually falling back
+     * to consuming the persistent allowance.
+     * NOTE: This function skips calling `super._spendAllowance` if the temporary allowance
+     * is enough to cover the spending.
+     */
+    function _spendAllowance(address owner, address spender, uint256 value) internal virtual override {
+        // load transient allowance
+        uint256 currentTemporaryAllowance = _temporaryAllowance(owner, spender);
+
+        // Check and update (if needed) the temporary allowance + set remaining value
+        if (currentTemporaryAllowance > 0) {
+            // All value is covered by the infinite allowance. nothing left to spend, we can return early
+            if (currentTemporaryAllowance == type(uint256).max) {
+                return;
+            }
+            // check how much of the value is covered by the transient allowance
+            uint256 spendTemporaryAllowance = Math.min(currentTemporaryAllowance, value);
+            unchecked {
+                // decrease transient allowance accordingly
+                _temporaryApprove(owner, spender, currentTemporaryAllowance - spendTemporaryAllowance);
+                // update value necessary
+                value -= spendTemporaryAllowance;
+            }
+        }
+        // reduce any remaining value from the persistent allowance
+        if (value > 0) {
+            super._spendAllowance(owner, spender, value);
+        }
+    }
+
+    function _temporaryAllowanceSlot(
+        address owner,
+        address spender
+    ) private pure returns (StorageSlot.Uint256SlotType) {
+        return ERC20_TEMPORARY_APPROVAL_STORAGE.deriveMapping(owner).deriveMapping(spender).asUint256();
+    }
+}

+ 94 - 0
contracts/token/ERC20/utils/ERC1363Utils.sol

@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {IERC1363Receiver} from "../../../interfaces/IERC1363Receiver.sol";
+import {IERC1363Spender} from "../../../interfaces/IERC1363Spender.sol";
+
+/**
+ * @dev Library that provides common ERC-1363 utility functions.
+ *
+ * See https://eips.ethereum.org/EIPS/eip-1363[ERC-1363].
+ */
+library ERC1363Utils {
+    /**
+     * @dev Indicates a failure with the token `receiver`. Used in transfers.
+     * @param receiver Address to which tokens are being transferred.
+     */
+    error ERC1363InvalidReceiver(address receiver);
+
+    /**
+     * @dev Indicates a failure with the token `spender`. Used in approvals.
+     * @param spender Address that may be allowed to operate on tokens without being their owner.
+     */
+    error ERC1363InvalidSpender(address spender);
+
+    /**
+     * @dev Performs a call to {IERC1363Receiver-onTransferReceived} on a target address.
+     *
+     * Requirements:
+     *
+     * - The target has code (i.e. is a contract).
+     * - The target `to` must implement the {IERC1363Receiver} interface.
+     * - The target must return the {IERC1363Receiver-onTransferReceived} selector to accept the transfer.
+     */
+    function checkOnERC1363TransferReceived(
+        address operator,
+        address from,
+        address to,
+        uint256 value,
+        bytes memory data
+    ) internal {
+        if (to.code.length == 0) {
+            revert ERC1363InvalidReceiver(to);
+        }
+
+        try IERC1363Receiver(to).onTransferReceived(operator, from, value, data) returns (bytes4 retval) {
+            if (retval != IERC1363Receiver.onTransferReceived.selector) {
+                revert ERC1363InvalidReceiver(to);
+            }
+        } catch (bytes memory reason) {
+            if (reason.length == 0) {
+                revert ERC1363InvalidReceiver(to);
+            } else {
+                assembly ("memory-safe") {
+                    revert(add(32, reason), mload(reason))
+                }
+            }
+        }
+    }
+
+    /**
+     * @dev Performs a call to {IERC1363Spender-onApprovalReceived} on a target address.
+     *
+     * Requirements:
+     *
+     * - The target has code (i.e. is a contract).
+     * - The target `spender` must implement the {IERC1363Spender} interface.
+     * - The target must return the {IERC1363Spender-onApprovalReceived} selector to accept the approval.
+     */
+    function checkOnERC1363ApprovalReceived(
+        address operator,
+        address spender,
+        uint256 value,
+        bytes memory data
+    ) internal {
+        if (spender.code.length == 0) {
+            revert ERC1363InvalidSpender(spender);
+        }
+
+        try IERC1363Spender(spender).onApprovalReceived(operator, value, data) returns (bytes4 retval) {
+            if (retval != IERC1363Spender.onApprovalReceived.selector) {
+                revert ERC1363InvalidSpender(spender);
+            }
+        } catch (bytes memory reason) {
+            if (reason.length == 0) {
+                revert ERC1363InvalidSpender(spender);
+            } else {
+                assembly ("memory-safe") {
+                    revert(add(32, reason), mload(reason))
+                }
+            }
+        }
+    }
+}

+ 26 - 14
contracts/token/ERC20/utils/SafeERC20.sol

@@ -17,8 +17,6 @@ import {Address} from "../../../utils/Address.sol";
  * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
  */
 library SafeERC20 {
-    using Address for address;
-
     /**
      * @dev An operation with an ERC-20 token failed.
      */
@@ -142,14 +140,25 @@ library SafeERC20 {
      * on the return value: the return value is optional (but if data is returned, it must not be false).
      * @param token The token targeted by the call.
      * @param data The call data (encoded using abi.encode or one of its variants).
+     *
+     * This is a variant of {_callOptionalReturnBool} that reverts if call fails to meet the requirements.
      */
     function _callOptionalReturn(IERC20 token, bytes memory data) private {
-        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
-        // we're implementing it ourselves. We use {Address-functionCall} to perform this call, which verifies that
-        // the target address contains contract code and also asserts for success in the low-level call.
+        uint256 returnSize;
+        uint256 returnValue;
+        assembly ("memory-safe") {
+            let success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
+            // bubble errors
+            if iszero(success) {
+                let ptr := mload(0x40)
+                returndatacopy(ptr, 0, returndatasize())
+                revert(ptr, returndatasize())
+            }
+            returnSize := returndatasize()
+            returnValue := mload(0)
+        }
 
-        bytes memory returndata = address(token).functionCall(data);
-        if (returndata.length != 0 && !abi.decode(returndata, (bool))) {
+        if (returnSize == 0 ? address(token).code.length == 0 : returnValue != 1) {
             revert SafeERC20FailedOperation(address(token));
         }
     }
@@ -160,14 +169,17 @@ library SafeERC20 {
      * @param token The token targeted by the call.
      * @param data The call data (encoded using abi.encode or one of its variants).
      *
-     * This is a variant of {_callOptionalReturn} that silents catches all reverts and returns a bool instead.
+     * This is a variant of {_callOptionalReturn} that silently catches all reverts and returns a bool instead.
      */
     function _callOptionalReturnBool(IERC20 token, bytes memory data) private returns (bool) {
-        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
-        // we're implementing it ourselves. We cannot use {Address-functionCall} here since this should return false
-        // and not revert is the subcall reverts.
-
-        (bool success, bytes memory returndata) = address(token).call(data);
-        return success && (returndata.length == 0 || abi.decode(returndata, (bool))) && address(token).code.length > 0;
+        bool success;
+        uint256 returnSize;
+        uint256 returnValue;
+        assembly ("memory-safe") {
+            success := call(gas(), token, 0, add(data, 0x20), mload(data), 0, 0x20)
+            returnSize := returndatasize()
+            returnValue := mload(0)
+        }
+        return success && (returnSize == 0 ? address(token).code.length > 0 : returnValue == 1);
     }
 }

+ 3 - 2
contracts/token/ERC721/ERC721.sol

@@ -195,8 +195,9 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
 
     /**
      * @dev Checks if `spender` can operate on `tokenId`, assuming the provided `owner` is the actual owner.
-     * Reverts if `spender` does not have approval from the provided `owner` for the given token or for all its assets
-     * the `spender` for the specific `tokenId`.
+     * Reverts if:
+     * - `spender` does not have approval from `owner` for `tokenId`.
+     * - `spender` does not have approval to manage all of `owner`'s assets.
      *
      * WARNING: This function assumes that `owner` is the actual owner of `tokenId` and does not verify this
      * assumption.

+ 2 - 0
contracts/token/ERC721/README.adoc

@@ -65,3 +65,5 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel
 == Utilities
 
 {{ERC721Holder}}
+
+{{ERC721Utils}}

+ 1 - 2
contracts/token/ERC721/utils/ERC721Utils.sol

@@ -37,8 +37,7 @@ library ERC721Utils {
                     // non-IERC721Receiver implementer
                     revert IERC721Errors.ERC721InvalidReceiver(to);
                 } else {
-                    /// @solidity memory-safe-assembly
-                    assembly {
+                    assembly ("memory-safe") {
                         revert(add(32, reason), mload(reason))
                     }
                 }

+ 1 - 2
contracts/utils/Address.sol

@@ -139,8 +139,7 @@ library Address {
         // Look for revert reason and bubble it up if present
         if (returndata.length > 0) {
             // The easiest way to bubble the revert reason is using memory via assembly
-            /// @solidity memory-safe-assembly
-            assembly {
+            assembly ("memory-safe") {
                 let returndata_size := mload(returndata)
                 revert(add(32, returndata), returndata_size)
             }

+ 47 - 52
contracts/utils/Arrays.sol

@@ -4,6 +4,7 @@
 
 pragma solidity ^0.8.20;
 
+import {Comparators} from "./Comparators.sol";
 import {SlotDerivation} from "./SlotDerivation.sol";
 import {StorageSlot} from "./StorageSlot.sol";
 import {Math} from "./math/Math.sol";
@@ -16,7 +17,7 @@ library Arrays {
     using StorageSlot for bytes32;
 
     /**
-     * @dev Sort an array of bytes32 (in memory) following the provided comparator function.
+     * @dev Sort an array of uint256 (in memory) following the provided comparator function.
      *
      * This function does the sorting "in place", meaning that it overrides the input. The object is returned for
      * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.
@@ -25,20 +26,22 @@ library Arrays {
      * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful
      * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may
      * consume more gas than is available in a block, leading to potential DoS.
+     *
+     * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.
      */
     function sort(
-        bytes32[] memory array,
-        function(bytes32, bytes32) pure returns (bool) comp
-    ) internal pure returns (bytes32[] memory) {
+        uint256[] memory array,
+        function(uint256, uint256) pure returns (bool) comp
+    ) internal pure returns (uint256[] memory) {
         _quickSort(_begin(array), _end(array), comp);
         return array;
     }
 
     /**
-     * @dev Variant of {sort} that sorts an array of bytes32 in increasing order.
+     * @dev Variant of {sort} that sorts an array of uint256 in increasing order.
      */
-    function sort(bytes32[] memory array) internal pure returns (bytes32[] memory) {
-        sort(array, _defaultComp);
+    function sort(uint256[] memory array) internal pure returns (uint256[] memory) {
+        sort(array, Comparators.lt);
         return array;
     }
 
@@ -52,12 +55,14 @@ library Arrays {
      * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful
      * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may
      * consume more gas than is available in a block, leading to potential DoS.
+     *
+     * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.
      */
     function sort(
         address[] memory array,
         function(address, address) pure returns (bool) comp
     ) internal pure returns (address[] memory) {
-        sort(_castToBytes32Array(array), _castToBytes32Comp(comp));
+        sort(_castToUint256Array(array), _castToUint256Comp(comp));
         return array;
     }
 
@@ -65,12 +70,12 @@ library Arrays {
      * @dev Variant of {sort} that sorts an array of address in increasing order.
      */
     function sort(address[] memory array) internal pure returns (address[] memory) {
-        sort(_castToBytes32Array(array), _defaultComp);
+        sort(_castToUint256Array(array), Comparators.lt);
         return array;
     }
 
     /**
-     * @dev Sort an array of uint256 (in memory) following the provided comparator function.
+     * @dev Sort an array of bytes32 (in memory) following the provided comparator function.
      *
      * This function does the sorting "in place", meaning that it overrides the input. The object is returned for
      * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.
@@ -79,20 +84,22 @@ library Arrays {
      * array. Using it in view functions that are executed through `eth_call` is safe, but one should be very careful
      * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may
      * consume more gas than is available in a block, leading to potential DoS.
+     *
+     * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.
      */
     function sort(
-        uint256[] memory array,
-        function(uint256, uint256) pure returns (bool) comp
-    ) internal pure returns (uint256[] memory) {
-        sort(_castToBytes32Array(array), _castToBytes32Comp(comp));
+        bytes32[] memory array,
+        function(bytes32, bytes32) pure returns (bool) comp
+    ) internal pure returns (bytes32[] memory) {
+        sort(_castToUint256Array(array), _castToUint256Comp(comp));
         return array;
     }
 
     /**
-     * @dev Variant of {sort} that sorts an array of uint256 in increasing order.
+     * @dev Variant of {sort} that sorts an array of bytes32 in increasing order.
      */
-    function sort(uint256[] memory array) internal pure returns (uint256[] memory) {
-        sort(_castToBytes32Array(array), _defaultComp);
+    function sort(bytes32[] memory array) internal pure returns (bytes32[] memory) {
+        sort(_castToUint256Array(array), Comparators.lt);
         return array;
     }
 
@@ -105,12 +112,12 @@ library Arrays {
      * IMPORTANT: Memory locations between `begin` and `end` are not validated/zeroed. This function should
      * be used only if the limits are within a memory array.
      */
-    function _quickSort(uint256 begin, uint256 end, function(bytes32, bytes32) pure returns (bool) comp) private pure {
+    function _quickSort(uint256 begin, uint256 end, function(uint256, uint256) pure returns (bool) comp) private pure {
         unchecked {
             if (end - begin < 0x40) return;
 
             // Use first element as pivot
-            bytes32 pivot = _mload(begin);
+            uint256 pivot = _mload(begin);
             // Position where the pivot should be at the end of the loop
             uint256 pos = begin;
 
@@ -132,9 +139,8 @@ library Arrays {
     /**
      * @dev Pointer to the memory location of the first element of `array`.
      */
-    function _begin(bytes32[] memory array) private pure returns (uint256 ptr) {
-        /// @solidity memory-safe-assembly
-        assembly {
+    function _begin(uint256[] memory array) private pure returns (uint256 ptr) {
+        assembly ("memory-safe") {
             ptr := add(array, 0x20)
         }
     }
@@ -143,16 +149,16 @@ library Arrays {
      * @dev Pointer to the memory location of the first memory word (32bytes) after `array`. This is the memory word
      * that comes just after the last element of the array.
      */
-    function _end(bytes32[] memory array) private pure returns (uint256 ptr) {
+    function _end(uint256[] memory array) private pure returns (uint256 ptr) {
         unchecked {
             return _begin(array) + array.length * 0x20;
         }
     }
 
     /**
-     * @dev Load memory word (as a bytes32) at location `ptr`.
+     * @dev Load memory word (as a uint256) at location `ptr`.
      */
-    function _mload(uint256 ptr) private pure returns (bytes32 value) {
+    function _mload(uint256 ptr) private pure returns (uint256 value) {
         assembly {
             value := mload(ptr)
         }
@@ -170,38 +176,33 @@ library Arrays {
         }
     }
 
-    /// @dev Comparator for sorting arrays in increasing order.
-    function _defaultComp(bytes32 a, bytes32 b) private pure returns (bool) {
-        return a < b;
-    }
-
     /// @dev Helper: low level cast address memory array to uint256 memory array
-    function _castToBytes32Array(address[] memory input) private pure returns (bytes32[] memory output) {
+    function _castToUint256Array(address[] memory input) private pure returns (uint256[] memory output) {
         assembly {
             output := input
         }
     }
 
-    /// @dev Helper: low level cast uint256 memory array to uint256 memory array
-    function _castToBytes32Array(uint256[] memory input) private pure returns (bytes32[] memory output) {
+    /// @dev Helper: low level cast bytes32 memory array to uint256 memory array
+    function _castToUint256Array(bytes32[] memory input) private pure returns (uint256[] memory output) {
         assembly {
             output := input
         }
     }
 
-    /// @dev Helper: low level cast address comp function to bytes32 comp function
-    function _castToBytes32Comp(
+    /// @dev Helper: low level cast address comp function to uint256 comp function
+    function _castToUint256Comp(
         function(address, address) pure returns (bool) input
-    ) private pure returns (function(bytes32, bytes32) pure returns (bool) output) {
+    ) private pure returns (function(uint256, uint256) pure returns (bool) output) {
         assembly {
             output := input
         }
     }
 
-    /// @dev Helper: low level cast uint256 comp function to bytes32 comp function
-    function _castToBytes32Comp(
-        function(uint256, uint256) pure returns (bool) input
-    ) private pure returns (function(bytes32, bytes32) pure returns (bool) output) {
+    /// @dev Helper: low level cast bytes32 comp function to uint256 comp function
+    function _castToUint256Comp(
+        function(bytes32, bytes32) pure returns (bool) input
+    ) private pure returns (function(uint256, uint256) pure returns (bool) output) {
         assembly {
             output := input
         }
@@ -381,8 +382,7 @@ library Arrays {
      */
     function unsafeAccess(address[] storage arr, uint256 pos) internal pure returns (StorageSlot.AddressSlot storage) {
         bytes32 slot;
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             slot := arr.slot
         }
         return slot.deriveArray().offset(pos).getAddressSlot();
@@ -395,8 +395,7 @@ library Arrays {
      */
     function unsafeAccess(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) {
         bytes32 slot;
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             slot := arr.slot
         }
         return slot.deriveArray().offset(pos).getBytes32Slot();
@@ -409,8 +408,7 @@ library Arrays {
      */
     function unsafeAccess(uint256[] storage arr, uint256 pos) internal pure returns (StorageSlot.Uint256Slot storage) {
         bytes32 slot;
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             slot := arr.slot
         }
         return slot.deriveArray().offset(pos).getUint256Slot();
@@ -455,8 +453,7 @@ library Arrays {
      * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
      */
     function unsafeSetLength(address[] storage array, uint256 len) internal {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             sstore(array.slot, len)
         }
     }
@@ -467,8 +464,7 @@ library Arrays {
      * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
      */
     function unsafeSetLength(bytes32[] storage array, uint256 len) internal {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             sstore(array.slot, len)
         }
     }
@@ -479,8 +475,7 @@ library Arrays {
      * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
      */
     function unsafeSetLength(uint256[] storage array, uint256 len) internal {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             sstore(array.slot, len)
         }
     }

+ 10 - 7
contracts/utils/Base64.sol

@@ -23,6 +23,7 @@ library Base64 {
 
     /**
      * @dev Converts a `bytes` to its Bytes64Url `string` representation.
+     * Output is not padded with `=` as specified in https://www.rfc-editor.org/rfc/rfc4648[rfc4648].
      */
     function encodeURL(bytes memory data) internal pure returns (string memory) {
         return _encode(data, _TABLE_URL, false);
@@ -40,20 +41,22 @@ library Base64 {
 
         // If padding is enabled, the final length should be `bytes` data length divided by 3 rounded up and then
         // multiplied by 4 so that it leaves room for padding the last chunk
-        // - `data.length + 2`  -> Round up
-        // - `/ 3`              -> Number of 3-bytes chunks
+        // - `data.length + 2`  -> Prepare for division rounding up
+        // - `/ 3`              -> Number of 3-bytes chunks (rounded up)
         // - `4 *`              -> 4 characters for each chunk
+        // This is equivalent to: 4 * Math.ceil(data.length / 3)
+        //
         // If padding is disabled, the final length should be `bytes` data length multiplied by 4/3 rounded up as
         // opposed to when padding is required to fill the last chunk.
-        // - `4 *`              -> 4 characters for each chunk
-        // - `data.length + 2`  -> Round up
-        // - `/ 3`              -> Number of 3-bytes chunks
+        // - `4 * data.length`  -> 4 characters for each chunk
+        // - ` + 2`             -> Prepare for division rounding up
+        // - `/ 3`              -> Number of 3-bytes chunks (rounded up)
+        // This is equivalent to: Math.ceil((4 * data.length) / 3)
         uint256 resultLength = withPadding ? 4 * ((data.length + 2) / 3) : (4 * data.length + 2) / 3;
 
         string memory result = new string(resultLength);
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             // Prepare the lookup table (skip the first "length" byte)
             let tablePtr := add(table, 1)
 

+ 13 - 0
contracts/utils/Comparators.sol

@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+library Comparators {
+    function lt(uint256 a, uint256 b) internal pure returns (bool) {
+        return a < b;
+    }
+
+    function gt(uint256 a, uint256 b) internal pure returns (bool) {
+        return a > b;
+    }
+}

+ 8 - 4
contracts/utils/Create2.sol

@@ -41,9 +41,14 @@ library Create2 {
         if (bytecode.length == 0) {
             revert Create2EmptyBytecode();
         }
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             addr := create2(amount, add(bytecode, 0x20), mload(bytecode), salt)
+            // if no address was created, and returndata is not empty, bubble revert
+            if and(iszero(addr), not(iszero(returndatasize()))) {
+                let p := mload(0x40)
+                returndatacopy(p, 0, returndatasize())
+                revert(p, returndatasize())
+            }
         }
         if (addr == address(0)) {
             revert Errors.FailedDeployment();
@@ -63,8 +68,7 @@ library Create2 {
      * `deployer`. If `deployer` is this contract's address, returns the same value as {computeAddress}.
      */
     function computeAddress(bytes32 salt, bytes32 bytecodeHash, address deployer) internal pure returns (address addr) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             let ptr := mload(0x40) // Get free memory pointer
 
             // |                   | ↓ ptr ...  ↓ ptr + 0x0B (start) ...  ↓ ptr + 0x20 ...  ↓ ptr + 0x40 ...   |

+ 5 - 0
contracts/utils/Errors.sol

@@ -23,4 +23,9 @@ library Errors {
      * @dev The deployment failed.
      */
     error FailedDeployment();
+
+    /**
+     * @dev A necessary precompile is missing.
+     */
+    error MissingPrecompile(address);
 }

+ 1120 - 20
contracts/utils/Packing.sol

@@ -1,40 +1,1140 @@
 // SPDX-License-Identifier: MIT
+// This file was procedurally generated from scripts/generate/templates/Packing.js.
 
 pragma solidity ^0.8.20;
 
 /**
- * @dev Helper library packing and unpacking multiple values into bytes32
+ * @dev Helper library packing and unpacking multiple values into bytesXX.
+ *
+ * Example usage:
+ *
+ * ```solidity
+ * library MyPacker {
+ *     type MyType is bytes32;
+ *
+ *     function _pack(address account, bytes4 selector, uint64 period) external pure returns (MyType) {
+ *         bytes12 subpack = Packing.pack_4_8(selector, bytes8(period));
+ *         bytes32 pack = Packing.pack_20_12(bytes20(account), subpack);
+ *         return MyType.wrap(pack);
+ *     }
+ *
+ *     function _unpack(MyType self) external pure returns (address, bytes4, uint64) {
+ *         bytes32 pack = MyType.unwrap(self);
+ *         return (
+ *             address(Packing.extract_32_20(pack, 0)),
+ *             Packing.extract_32_4(pack, 20),
+ *             uint64(Packing.extract_32_8(pack, 24))
+ *         );
+ *     }
+ * }
+ * ```
  */
+// solhint-disable func-name-mixedcase
 library Packing {
-    type Uint128x2 is bytes32;
+    error OutOfRangeAccess();
 
-    /// @dev Cast a bytes32 into a Uint128x2
-    function asUint128x2(bytes32 self) internal pure returns (Uint128x2) {
-        return Uint128x2.wrap(self);
+    function pack_1_1(bytes1 left, bytes1 right) internal pure returns (bytes2 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(248, not(0)))
+            right := and(right, shl(248, not(0)))
+            result := or(left, shr(8, right))
+        }
     }
 
-    /// @dev Cast a Uint128x2 into a bytes32
-    function asBytes32(Uint128x2 self) internal pure returns (bytes32) {
-        return Uint128x2.unwrap(self);
+    function pack_2_2(bytes2 left, bytes2 right) internal pure returns (bytes4 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(240, not(0)))
+            right := and(right, shl(240, not(0)))
+            result := or(left, shr(16, right))
+        }
     }
 
-    /// @dev Pack two uint128 into a Uint128x2
-    function pack(uint128 first128, uint128 second128) internal pure returns (Uint128x2) {
-        return Uint128x2.wrap(bytes32(bytes16(first128)) | bytes32(uint256(second128)));
+    function pack_2_4(bytes2 left, bytes4 right) internal pure returns (bytes6 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(240, not(0)))
+            right := and(right, shl(224, not(0)))
+            result := or(left, shr(16, right))
+        }
     }
 
-    /// @dev Split a Uint128x2 into two uint128
-    function split(Uint128x2 self) internal pure returns (uint128, uint128) {
-        return (first(self), second(self));
+    function pack_2_6(bytes2 left, bytes6 right) internal pure returns (bytes8 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(240, not(0)))
+            right := and(right, shl(208, not(0)))
+            result := or(left, shr(16, right))
+        }
     }
 
-    /// @dev Get the first element of a Uint128x2 counting from higher to lower bytes
-    function first(Uint128x2 self) internal pure returns (uint128) {
-        return uint128(bytes16(Uint128x2.unwrap(self)));
+    function pack_4_2(bytes4 left, bytes2 right) internal pure returns (bytes6 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(224, not(0)))
+            right := and(right, shl(240, not(0)))
+            result := or(left, shr(32, right))
+        }
     }
 
-    /// @dev Get the second element of a Uint128x2 counting from higher to lower bytes
-    function second(Uint128x2 self) internal pure returns (uint128) {
-        return uint128(uint256(Uint128x2.unwrap(self)));
+    function pack_4_4(bytes4 left, bytes4 right) internal pure returns (bytes8 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(224, not(0)))
+            right := and(right, shl(224, 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)))
+            right := and(right, shl(192, not(0)))
+            result := or(left, shr(32, right))
+        }
+    }
+
+    function pack_4_12(bytes4 left, bytes12 right) internal pure returns (bytes16 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(224, not(0)))
+            right := and(right, shl(160, not(0)))
+            result := or(left, shr(32, right))
+        }
+    }
+
+    function pack_4_16(bytes4 left, bytes16 right) internal pure returns (bytes20 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(224, not(0)))
+            right := and(right, shl(128, not(0)))
+            result := or(left, shr(32, right))
+        }
+    }
+
+    function pack_4_20(bytes4 left, bytes20 right) internal pure returns (bytes24 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(224, not(0)))
+            right := and(right, shl(96, not(0)))
+            result := or(left, shr(32, right))
+        }
+    }
+
+    function pack_4_24(bytes4 left, bytes24 right) internal pure returns (bytes28 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(224, not(0)))
+            right := and(right, shl(64, not(0)))
+            result := or(left, shr(32, right))
+        }
+    }
+
+    function pack_4_28(bytes4 left, bytes28 right) internal pure returns (bytes32 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(224, not(0)))
+            right := and(right, shl(32, not(0)))
+            result := or(left, shr(32, right))
+        }
+    }
+
+    function pack_6_2(bytes6 left, bytes2 right) internal pure returns (bytes8 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(208, not(0)))
+            right := and(right, shl(240, 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)))
+            right := and(right, shl(208, not(0)))
+            result := or(left, shr(48, right))
+        }
+    }
+
+    function pack_8_4(bytes8 left, bytes4 right) internal pure returns (bytes12 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(192, not(0)))
+            right := and(right, shl(224, not(0)))
+            result := or(left, shr(64, right))
+        }
+    }
+
+    function pack_8_8(bytes8 left, bytes8 right) internal pure returns (bytes16 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(192, not(0)))
+            right := and(right, shl(192, not(0)))
+            result := or(left, shr(64, right))
+        }
+    }
+
+    function pack_8_12(bytes8 left, bytes12 right) internal pure returns (bytes20 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(192, not(0)))
+            right := and(right, shl(160, not(0)))
+            result := or(left, shr(64, right))
+        }
+    }
+
+    function pack_8_16(bytes8 left, bytes16 right) internal pure returns (bytes24 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(192, not(0)))
+            right := and(right, shl(128, not(0)))
+            result := or(left, shr(64, right))
+        }
+    }
+
+    function pack_8_20(bytes8 left, bytes20 right) internal pure returns (bytes28 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(192, not(0)))
+            right := and(right, shl(96, not(0)))
+            result := or(left, shr(64, right))
+        }
+    }
+
+    function pack_8_24(bytes8 left, bytes24 right) internal pure returns (bytes32 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(192, not(0)))
+            right := and(right, shl(64, not(0)))
+            result := or(left, shr(64, right))
+        }
+    }
+
+    function pack_12_4(bytes12 left, bytes4 right) internal pure returns (bytes16 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(160, not(0)))
+            right := and(right, shl(224, not(0)))
+            result := or(left, shr(96, right))
+        }
+    }
+
+    function pack_12_8(bytes12 left, bytes8 right) internal pure returns (bytes20 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(160, not(0)))
+            right := and(right, shl(192, 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)))
+            right := and(right, shl(160, not(0)))
+            result := or(left, shr(96, right))
+        }
+    }
+
+    function pack_12_16(bytes12 left, bytes16 right) internal pure returns (bytes28 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(160, not(0)))
+            right := and(right, shl(128, not(0)))
+            result := or(left, shr(96, right))
+        }
+    }
+
+    function pack_12_20(bytes12 left, bytes20 right) internal pure returns (bytes32 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(160, not(0)))
+            right := and(right, shl(96, not(0)))
+            result := or(left, shr(96, right))
+        }
+    }
+
+    function pack_16_4(bytes16 left, bytes4 right) internal pure returns (bytes20 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(128, not(0)))
+            right := and(right, shl(224, 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)))
+            right := and(right, shl(192, not(0)))
+            result := or(left, shr(128, right))
+        }
+    }
+
+    function pack_16_12(bytes16 left, bytes12 right) internal pure returns (bytes28 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(128, not(0)))
+            right := and(right, shl(160, not(0)))
+            result := or(left, shr(128, right))
+        }
+    }
+
+    function pack_16_16(bytes16 left, bytes16 right) internal pure returns (bytes32 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(128, not(0)))
+            right := and(right, shl(128, not(0)))
+            result := or(left, shr(128, right))
+        }
+    }
+
+    function pack_20_4(bytes20 left, bytes4 right) internal pure returns (bytes24 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(96, not(0)))
+            right := and(right, shl(224, not(0)))
+            result := or(left, shr(160, right))
+        }
+    }
+
+    function pack_20_8(bytes20 left, bytes8 right) internal pure returns (bytes28 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(96, not(0)))
+            right := and(right, shl(192, not(0)))
+            result := or(left, shr(160, right))
+        }
+    }
+
+    function pack_20_12(bytes20 left, bytes12 right) internal pure returns (bytes32 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(96, not(0)))
+            right := and(right, shl(160, not(0)))
+            result := or(left, shr(160, right))
+        }
+    }
+
+    function pack_24_4(bytes24 left, bytes4 right) internal pure returns (bytes28 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(64, not(0)))
+            right := and(right, shl(224, not(0)))
+            result := or(left, shr(192, right))
+        }
+    }
+
+    function pack_24_8(bytes24 left, bytes8 right) internal pure returns (bytes32 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(64, not(0)))
+            right := and(right, shl(192, not(0)))
+            result := or(left, shr(192, right))
+        }
+    }
+
+    function pack_28_4(bytes28 left, bytes4 right) internal pure returns (bytes32 result) {
+        assembly ("memory-safe") {
+            left := and(left, shl(32, not(0)))
+            right := and(right, shl(224, not(0)))
+            result := or(left, shr(224, right))
+        }
+    }
+
+    function extract_2_1(bytes2 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 1) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_2_1(bytes2 self, bytes1 value, uint8 offset) internal pure returns (bytes2 result) {
+        bytes1 oldValue = extract_2_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_4_1(bytes4 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 3) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_4_1(bytes4 self, bytes1 value, uint8 offset) internal pure returns (bytes4 result) {
+        bytes1 oldValue = extract_4_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_4_2(bytes4 self, uint8 offset) internal pure returns (bytes2 result) {
+        if (offset > 2) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(240, not(0)))
+        }
+    }
+
+    function replace_4_2(bytes4 self, bytes2 value, uint8 offset) internal pure returns (bytes4 result) {
+        bytes2 oldValue = extract_4_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_6_1(bytes6 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 5) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_6_1(bytes6 self, bytes1 value, uint8 offset) internal pure returns (bytes6 result) {
+        bytes1 oldValue = extract_6_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_6_2(bytes6 self, uint8 offset) internal pure returns (bytes2 result) {
+        if (offset > 4) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(240, not(0)))
+        }
+    }
+
+    function replace_6_2(bytes6 self, bytes2 value, uint8 offset) internal pure returns (bytes6 result) {
+        bytes2 oldValue = extract_6_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_6_4(bytes6 self, uint8 offset) internal pure returns (bytes4 result) {
+        if (offset > 2) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(224, not(0)))
+        }
+    }
+
+    function replace_6_4(bytes6 self, bytes4 value, uint8 offset) internal pure returns (bytes6 result) {
+        bytes4 oldValue = extract_6_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_8_1(bytes8 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 7) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_8_1(bytes8 self, bytes1 value, uint8 offset) internal pure returns (bytes8 result) {
+        bytes1 oldValue = extract_8_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_8_2(bytes8 self, uint8 offset) internal pure returns (bytes2 result) {
+        if (offset > 6) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(240, not(0)))
+        }
+    }
+
+    function replace_8_2(bytes8 self, bytes2 value, uint8 offset) internal pure returns (bytes8 result) {
+        bytes2 oldValue = extract_8_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_8_4(bytes8 self, uint8 offset) internal pure returns (bytes4 result) {
+        if (offset > 4) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(224, not(0)))
+        }
+    }
+
+    function replace_8_4(bytes8 self, bytes4 value, uint8 offset) internal pure returns (bytes8 result) {
+        bytes4 oldValue = extract_8_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_8_6(bytes8 self, uint8 offset) internal pure returns (bytes6 result) {
+        if (offset > 2) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(208, not(0)))
+        }
+    }
+
+    function replace_8_6(bytes8 self, bytes6 value, uint8 offset) internal pure returns (bytes8 result) {
+        bytes6 oldValue = extract_8_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_12_1(bytes12 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 11) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_12_1(bytes12 self, bytes1 value, uint8 offset) internal pure returns (bytes12 result) {
+        bytes1 oldValue = extract_12_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_12_2(bytes12 self, uint8 offset) internal pure returns (bytes2 result) {
+        if (offset > 10) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(240, not(0)))
+        }
+    }
+
+    function replace_12_2(bytes12 self, bytes2 value, uint8 offset) internal pure returns (bytes12 result) {
+        bytes2 oldValue = extract_12_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_12_4(bytes12 self, uint8 offset) internal pure returns (bytes4 result) {
+        if (offset > 8) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(224, not(0)))
+        }
+    }
+
+    function replace_12_4(bytes12 self, bytes4 value, uint8 offset) internal pure returns (bytes12 result) {
+        bytes4 oldValue = extract_12_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_12_6(bytes12 self, uint8 offset) internal pure returns (bytes6 result) {
+        if (offset > 6) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(208, not(0)))
+        }
+    }
+
+    function replace_12_6(bytes12 self, bytes6 value, uint8 offset) internal pure returns (bytes12 result) {
+        bytes6 oldValue = extract_12_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_12_8(bytes12 self, uint8 offset) internal pure returns (bytes8 result) {
+        if (offset > 4) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(192, not(0)))
+        }
+    }
+
+    function replace_12_8(bytes12 self, bytes8 value, uint8 offset) internal pure returns (bytes12 result) {
+        bytes8 oldValue = extract_12_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_16_1(bytes16 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 15) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_16_1(bytes16 self, bytes1 value, uint8 offset) internal pure returns (bytes16 result) {
+        bytes1 oldValue = extract_16_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_16_2(bytes16 self, uint8 offset) internal pure returns (bytes2 result) {
+        if (offset > 14) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(240, not(0)))
+        }
+    }
+
+    function replace_16_2(bytes16 self, bytes2 value, uint8 offset) internal pure returns (bytes16 result) {
+        bytes2 oldValue = extract_16_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_16_4(bytes16 self, uint8 offset) internal pure returns (bytes4 result) {
+        if (offset > 12) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(224, not(0)))
+        }
+    }
+
+    function replace_16_4(bytes16 self, bytes4 value, uint8 offset) internal pure returns (bytes16 result) {
+        bytes4 oldValue = extract_16_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_16_6(bytes16 self, uint8 offset) internal pure returns (bytes6 result) {
+        if (offset > 10) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(208, not(0)))
+        }
+    }
+
+    function replace_16_6(bytes16 self, bytes6 value, uint8 offset) internal pure returns (bytes16 result) {
+        bytes6 oldValue = extract_16_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_16_8(bytes16 self, uint8 offset) internal pure returns (bytes8 result) {
+        if (offset > 8) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(192, not(0)))
+        }
+    }
+
+    function replace_16_8(bytes16 self, bytes8 value, uint8 offset) internal pure returns (bytes16 result) {
+        bytes8 oldValue = extract_16_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_16_12(bytes16 self, uint8 offset) internal pure returns (bytes12 result) {
+        if (offset > 4) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(160, not(0)))
+        }
+    }
+
+    function replace_16_12(bytes16 self, bytes12 value, uint8 offset) internal pure returns (bytes16 result) {
+        bytes12 oldValue = extract_16_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_20_1(bytes20 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 19) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_20_1(bytes20 self, bytes1 value, uint8 offset) internal pure returns (bytes20 result) {
+        bytes1 oldValue = extract_20_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_20_2(bytes20 self, uint8 offset) internal pure returns (bytes2 result) {
+        if (offset > 18) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(240, not(0)))
+        }
+    }
+
+    function replace_20_2(bytes20 self, bytes2 value, uint8 offset) internal pure returns (bytes20 result) {
+        bytes2 oldValue = extract_20_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_20_4(bytes20 self, uint8 offset) internal pure returns (bytes4 result) {
+        if (offset > 16) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(224, not(0)))
+        }
+    }
+
+    function replace_20_4(bytes20 self, bytes4 value, uint8 offset) internal pure returns (bytes20 result) {
+        bytes4 oldValue = extract_20_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_20_6(bytes20 self, uint8 offset) internal pure returns (bytes6 result) {
+        if (offset > 14) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(208, not(0)))
+        }
+    }
+
+    function replace_20_6(bytes20 self, bytes6 value, uint8 offset) internal pure returns (bytes20 result) {
+        bytes6 oldValue = extract_20_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_20_8(bytes20 self, uint8 offset) internal pure returns (bytes8 result) {
+        if (offset > 12) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(192, not(0)))
+        }
+    }
+
+    function replace_20_8(bytes20 self, bytes8 value, uint8 offset) internal pure returns (bytes20 result) {
+        bytes8 oldValue = extract_20_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_20_12(bytes20 self, uint8 offset) internal pure returns (bytes12 result) {
+        if (offset > 8) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(160, not(0)))
+        }
+    }
+
+    function replace_20_12(bytes20 self, bytes12 value, uint8 offset) internal pure returns (bytes20 result) {
+        bytes12 oldValue = extract_20_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_20_16(bytes20 self, uint8 offset) internal pure returns (bytes16 result) {
+        if (offset > 4) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(128, not(0)))
+        }
+    }
+
+    function replace_20_16(bytes20 self, bytes16 value, uint8 offset) internal pure returns (bytes20 result) {
+        bytes16 oldValue = extract_20_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_24_1(bytes24 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 23) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_24_1(bytes24 self, bytes1 value, uint8 offset) internal pure returns (bytes24 result) {
+        bytes1 oldValue = extract_24_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_24_2(bytes24 self, uint8 offset) internal pure returns (bytes2 result) {
+        if (offset > 22) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(240, not(0)))
+        }
+    }
+
+    function replace_24_2(bytes24 self, bytes2 value, uint8 offset) internal pure returns (bytes24 result) {
+        bytes2 oldValue = extract_24_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_24_4(bytes24 self, uint8 offset) internal pure returns (bytes4 result) {
+        if (offset > 20) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(224, not(0)))
+        }
+    }
+
+    function replace_24_4(bytes24 self, bytes4 value, uint8 offset) internal pure returns (bytes24 result) {
+        bytes4 oldValue = extract_24_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_24_6(bytes24 self, uint8 offset) internal pure returns (bytes6 result) {
+        if (offset > 18) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(208, not(0)))
+        }
+    }
+
+    function replace_24_6(bytes24 self, bytes6 value, uint8 offset) internal pure returns (bytes24 result) {
+        bytes6 oldValue = extract_24_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_24_8(bytes24 self, uint8 offset) internal pure returns (bytes8 result) {
+        if (offset > 16) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(192, not(0)))
+        }
+    }
+
+    function replace_24_8(bytes24 self, bytes8 value, uint8 offset) internal pure returns (bytes24 result) {
+        bytes8 oldValue = extract_24_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_24_12(bytes24 self, uint8 offset) internal pure returns (bytes12 result) {
+        if (offset > 12) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(160, not(0)))
+        }
+    }
+
+    function replace_24_12(bytes24 self, bytes12 value, uint8 offset) internal pure returns (bytes24 result) {
+        bytes12 oldValue = extract_24_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_24_16(bytes24 self, uint8 offset) internal pure returns (bytes16 result) {
+        if (offset > 8) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(128, not(0)))
+        }
+    }
+
+    function replace_24_16(bytes24 self, bytes16 value, uint8 offset) internal pure returns (bytes24 result) {
+        bytes16 oldValue = extract_24_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_24_20(bytes24 self, uint8 offset) internal pure returns (bytes20 result) {
+        if (offset > 4) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(96, not(0)))
+        }
+    }
+
+    function replace_24_20(bytes24 self, bytes20 value, uint8 offset) internal pure returns (bytes24 result) {
+        bytes20 oldValue = extract_24_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_28_1(bytes28 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 27) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_28_1(bytes28 self, bytes1 value, uint8 offset) internal pure returns (bytes28 result) {
+        bytes1 oldValue = extract_28_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_28_2(bytes28 self, uint8 offset) internal pure returns (bytes2 result) {
+        if (offset > 26) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(240, not(0)))
+        }
+    }
+
+    function replace_28_2(bytes28 self, bytes2 value, uint8 offset) internal pure returns (bytes28 result) {
+        bytes2 oldValue = extract_28_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_28_4(bytes28 self, uint8 offset) internal pure returns (bytes4 result) {
+        if (offset > 24) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(224, not(0)))
+        }
+    }
+
+    function replace_28_4(bytes28 self, bytes4 value, uint8 offset) internal pure returns (bytes28 result) {
+        bytes4 oldValue = extract_28_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_28_6(bytes28 self, uint8 offset) internal pure returns (bytes6 result) {
+        if (offset > 22) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(208, not(0)))
+        }
+    }
+
+    function replace_28_6(bytes28 self, bytes6 value, uint8 offset) internal pure returns (bytes28 result) {
+        bytes6 oldValue = extract_28_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_28_8(bytes28 self, uint8 offset) internal pure returns (bytes8 result) {
+        if (offset > 20) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(192, not(0)))
+        }
+    }
+
+    function replace_28_8(bytes28 self, bytes8 value, uint8 offset) internal pure returns (bytes28 result) {
+        bytes8 oldValue = extract_28_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_28_12(bytes28 self, uint8 offset) internal pure returns (bytes12 result) {
+        if (offset > 16) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(160, not(0)))
+        }
+    }
+
+    function replace_28_12(bytes28 self, bytes12 value, uint8 offset) internal pure returns (bytes28 result) {
+        bytes12 oldValue = extract_28_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_28_16(bytes28 self, uint8 offset) internal pure returns (bytes16 result) {
+        if (offset > 12) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(128, not(0)))
+        }
+    }
+
+    function replace_28_16(bytes28 self, bytes16 value, uint8 offset) internal pure returns (bytes28 result) {
+        bytes16 oldValue = extract_28_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_28_20(bytes28 self, uint8 offset) internal pure returns (bytes20 result) {
+        if (offset > 8) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(96, not(0)))
+        }
+    }
+
+    function replace_28_20(bytes28 self, bytes20 value, uint8 offset) internal pure returns (bytes28 result) {
+        bytes20 oldValue = extract_28_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_28_24(bytes28 self, uint8 offset) internal pure returns (bytes24 result) {
+        if (offset > 4) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(64, not(0)))
+        }
+    }
+
+    function replace_28_24(bytes28 self, bytes24 value, uint8 offset) internal pure returns (bytes28 result) {
+        bytes24 oldValue = extract_28_24(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(64, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_32_1(bytes32 self, uint8 offset) internal pure returns (bytes1 result) {
+        if (offset > 31) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(248, not(0)))
+        }
+    }
+
+    function replace_32_1(bytes32 self, bytes1 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes1 oldValue = extract_32_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_32_2(bytes32 self, uint8 offset) internal pure returns (bytes2 result) {
+        if (offset > 30) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(240, not(0)))
+        }
+    }
+
+    function replace_32_2(bytes32 self, bytes2 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes2 oldValue = extract_32_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_32_4(bytes32 self, uint8 offset) internal pure returns (bytes4 result) {
+        if (offset > 28) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(224, not(0)))
+        }
+    }
+
+    function replace_32_4(bytes32 self, bytes4 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes4 oldValue = extract_32_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_32_6(bytes32 self, uint8 offset) internal pure returns (bytes6 result) {
+        if (offset > 26) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(208, not(0)))
+        }
+    }
+
+    function replace_32_6(bytes32 self, bytes6 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes6 oldValue = extract_32_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_32_8(bytes32 self, uint8 offset) internal pure returns (bytes8 result) {
+        if (offset > 24) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(192, not(0)))
+        }
+    }
+
+    function replace_32_8(bytes32 self, bytes8 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes8 oldValue = extract_32_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_32_12(bytes32 self, uint8 offset) internal pure returns (bytes12 result) {
+        if (offset > 20) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(160, not(0)))
+        }
+    }
+
+    function replace_32_12(bytes32 self, bytes12 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes12 oldValue = extract_32_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_32_16(bytes32 self, uint8 offset) internal pure returns (bytes16 result) {
+        if (offset > 16) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(128, not(0)))
+        }
+    }
+
+    function replace_32_16(bytes32 self, bytes16 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes16 oldValue = extract_32_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_32_20(bytes32 self, uint8 offset) internal pure returns (bytes20 result) {
+        if (offset > 12) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(96, not(0)))
+        }
+    }
+
+    function replace_32_20(bytes32 self, bytes20 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes20 oldValue = extract_32_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_32_24(bytes32 self, uint8 offset) internal pure returns (bytes24 result) {
+        if (offset > 8) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(64, not(0)))
+        }
+    }
+
+    function replace_32_24(bytes32 self, bytes24 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes24 oldValue = extract_32_24(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(64, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
+    }
+
+    function extract_32_28(bytes32 self, uint8 offset) internal pure returns (bytes28 result) {
+        if (offset > 4) revert OutOfRangeAccess();
+        assembly ("memory-safe") {
+            result := and(shl(mul(8, offset), self), shl(32, not(0)))
+        }
+    }
+
+    function replace_32_28(bytes32 self, bytes28 value, uint8 offset) internal pure returns (bytes32 result) {
+        bytes28 oldValue = extract_32_28(self, offset);
+        assembly ("memory-safe") {
+            value := and(value, shl(32, not(0)))
+            result := xor(self, shr(mul(8, offset), xor(oldValue, value)))
+        }
     }
 }

+ 1 - 2
contracts/utils/Panic.sol

@@ -45,8 +45,7 @@ library Panic {
     /// @dev Reverts with a panic code. Recommended to use with
     /// the internal constants with predefined codes.
     function panic(uint256 code) internal pure {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             mstore(0x00, 0x4e487b71)
             mstore(0x20, code)
             revert(0x1c, 0x24)

+ 12 - 4
contracts/utils/README.adoc

@@ -8,6 +8,8 @@ Miscellaneous contracts and libraries containing utility functions you can use t
  * {Math}, {SignedMath}: Implementation of various arithmetic functions.
  * {SafeCast}: Checked downcasting functions to avoid silent truncation.
  * {ECDSA}, {MessageHashUtils}: Libraries for interacting with ECDSA signatures.
+ * {P256}: Library for verifying and recovering public keys from secp256r1 signatures.
+ * {RSA}: Library with RSA PKCS#1 v1.5 signature verification utilities.
  * {SignatureChecker}: A library helper to support regular ECDSA from EOAs as well as ERC-1271 signatures for smart contracts.
  * {Hashes}: Commonly used hash functions.
  * {MerkleProof}: Functions for verifying https://en.wikipedia.org/wiki/Merkle_tree[Merkle Tree] proofs.
@@ -16,13 +18,14 @@ 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.
- * {ERC165, ERC165Checker}: Utilities for inspecting interfaces supported by contracts.
+ * {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`).
  * {EnumerableSet}: Like {EnumerableMap}, but for https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets]. Can be used to store privileged accounts, issued IDs, etc.
  * {DoubleEndedQueue}: An implementation of a https://en.wikipedia.org/wiki/Double-ended_queue[double ended queue] whose values can be removed added or remove from both sides. Useful for FIFO and LIFO structures.
  * {CircularBuffer}: A data structure to store the last N values pushed to it.
- * {Checkpoints}: A data structure to store values mapped to an strictly increasing key. Can be used for storing and accessing values over time.
+ * {Checkpoints}: A data structure to store values mapped to a strictly increasing key. Can be used for storing and accessing values over time.
+ * {Heap}: A library that implements a https://en.wikipedia.org/wiki/Binary_heap[binary heap] in storage.
  * {MerkleTree}: A library with https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] data structures and helper functions.
  * {Create2}: Wrapper around the https://blog.openzeppelin.com/getting-the-most-out-of-create2/[`CREATE2` EVM opcode] for safe use without having to deal with low-level assembly.
  * {Address}: Collection of functions for overloading Solidity's https://docs.soliditylang.org/en/latest/types.html#address[`address`] type.
@@ -32,10 +35,11 @@ Miscellaneous contracts and libraries containing utility functions you can use t
  * {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).
- * {Multicall}: Abstract contract with an utility to allow batching together multiple calls in a single transaction. Useful for allowing EOAs to perform multiple operations at once.
- * {Context}: An utility for abstracting the sender and calldata in the current execution context.
+ * {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.
 
 [NOTE]
 ====
@@ -100,6 +104,8 @@ Ethereum contracts have no native concept of an interface, so applications must
 
 {{Checkpoints}}
 
+{{Heap}}
+
 {{MerkleTree}}
 
 == Libraries
@@ -127,3 +133,5 @@ Ethereum contracts have no native concept of an interface, so applications must
 {{Packing}}
 
 {{Panic}}
+
+{{Comparators}}

+ 1 - 2
contracts/utils/ShortStrings.sol

@@ -64,8 +64,7 @@ library ShortStrings {
         uint256 len = byteLength(sstr);
         // using `new string(len)` would work locally but is not memory safe.
         string memory str = new string(32);
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             mstore(str, len)
             mstore(add(str, 0x20), sstr)
         }

+ 9 - 18
contracts/utils/SlotDerivation.sol

@@ -40,8 +40,7 @@ library SlotDerivation {
      * @dev Derive an ERC-7201 slot from a string (namespace).
      */
     function erc7201Slot(string memory namespace) internal pure returns (bytes32 slot) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             mstore(0x00, sub(keccak256(add(namespace, 0x20), mload(namespace)), 1))
             slot := and(keccak256(0x00, 0x20), not(0xff))
         }
@@ -60,8 +59,7 @@ library SlotDerivation {
      * @dev Derive the location of the first element in an array from the slot where the length is stored.
      */
     function deriveArray(bytes32 slot) internal pure returns (bytes32 result) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             mstore(0x00, slot)
             result := keccak256(0x00, 0x20)
         }
@@ -71,8 +69,7 @@ library SlotDerivation {
      * @dev Derive the location of a mapping element from the key.
      */
     function deriveMapping(bytes32 slot, address key) internal pure returns (bytes32 result) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             mstore(0x00, key)
             mstore(0x20, slot)
             result := keccak256(0x00, 0x40)
@@ -83,8 +80,7 @@ library SlotDerivation {
      * @dev Derive the location of a mapping element from the key.
      */
     function deriveMapping(bytes32 slot, bool key) internal pure returns (bytes32 result) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             mstore(0x00, key)
             mstore(0x20, slot)
             result := keccak256(0x00, 0x40)
@@ -95,8 +91,7 @@ library SlotDerivation {
      * @dev Derive the location of a mapping element from the key.
      */
     function deriveMapping(bytes32 slot, bytes32 key) internal pure returns (bytes32 result) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             mstore(0x00, key)
             mstore(0x20, slot)
             result := keccak256(0x00, 0x40)
@@ -107,8 +102,7 @@ library SlotDerivation {
      * @dev Derive the location of a mapping element from the key.
      */
     function deriveMapping(bytes32 slot, uint256 key) internal pure returns (bytes32 result) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             mstore(0x00, key)
             mstore(0x20, slot)
             result := keccak256(0x00, 0x40)
@@ -119,8 +113,7 @@ library SlotDerivation {
      * @dev Derive the location of a mapping element from the key.
      */
     function deriveMapping(bytes32 slot, int256 key) internal pure returns (bytes32 result) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             mstore(0x00, key)
             mstore(0x20, slot)
             result := keccak256(0x00, 0x40)
@@ -131,8 +124,7 @@ library SlotDerivation {
      * @dev Derive the location of a mapping element from the key.
      */
     function deriveMapping(bytes32 slot, string memory key) internal pure returns (bytes32 result) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             let length := mload(key)
             let begin := add(key, 0x20)
             let end := add(begin, length)
@@ -147,8 +139,7 @@ library SlotDerivation {
      * @dev Derive the location of a mapping element from the key.
      */
     function deriveMapping(bytes32 slot, bytes memory key) internal pure returns (bytes32 result) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             let length := mload(key)
             let begin := add(key, 0x20)
             let end := add(begin, length)

+ 25 - 44
contracts/utils/StorageSlot.sol

@@ -82,58 +82,52 @@ library StorageSlot {
      * @dev Returns an `AddressSlot` with member `value` located at `slot`.
      */
     function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             r.slot := slot
         }
     }
 
     /**
-     * @dev Returns an `BooleanSlot` with member `value` located at `slot`.
+     * @dev Returns a `BooleanSlot` with member `value` located at `slot`.
      */
     function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             r.slot := slot
         }
     }
 
     /**
-     * @dev Returns an `Bytes32Slot` with member `value` located at `slot`.
+     * @dev Returns a `Bytes32Slot` with member `value` located at `slot`.
      */
     function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             r.slot := slot
         }
     }
 
     /**
-     * @dev Returns an `Uint256Slot` with member `value` located at `slot`.
+     * @dev Returns a `Uint256Slot` with member `value` located at `slot`.
      */
     function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             r.slot := slot
         }
     }
 
     /**
-     * @dev Returns an `Int256Slot` with member `value` located at `slot`.
+     * @dev Returns a `Int256Slot` with member `value` located at `slot`.
      */
     function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             r.slot := slot
         }
     }
 
     /**
-     * @dev Returns an `StringSlot` with member `value` located at `slot`.
+     * @dev Returns a `StringSlot` with member `value` located at `slot`.
      */
     function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             r.slot := slot
         }
     }
@@ -142,18 +136,16 @@ library StorageSlot {
      * @dev Returns an `StringSlot` representation of the string storage pointer `store`.
      */
     function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             r.slot := store.slot
         }
     }
 
     /**
-     * @dev Returns an `BytesSlot` with member `value` located at `slot`.
+     * @dev Returns a `BytesSlot` with member `value` located at `slot`.
      */
     function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             r.slot := slot
         }
     }
@@ -162,8 +154,7 @@ library StorageSlot {
      * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`.
      */
     function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             r.slot := store.slot
         }
     }
@@ -232,8 +223,7 @@ library StorageSlot {
      * @dev Load the value held at location `slot` in transient storage.
      */
     function tload(AddressSlotType slot) internal view returns (address value) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             value := tload(slot)
         }
     }
@@ -242,8 +232,7 @@ library StorageSlot {
      * @dev Store `value` at location `slot` in transient storage.
      */
     function tstore(AddressSlotType slot, address value) internal {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             tstore(slot, value)
         }
     }
@@ -252,8 +241,7 @@ library StorageSlot {
      * @dev Load the value held at location `slot` in transient storage.
      */
     function tload(BooleanSlotType slot) internal view returns (bool value) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             value := tload(slot)
         }
     }
@@ -262,8 +250,7 @@ library StorageSlot {
      * @dev Store `value` at location `slot` in transient storage.
      */
     function tstore(BooleanSlotType slot, bool value) internal {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             tstore(slot, value)
         }
     }
@@ -272,8 +259,7 @@ library StorageSlot {
      * @dev Load the value held at location `slot` in transient storage.
      */
     function tload(Bytes32SlotType slot) internal view returns (bytes32 value) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             value := tload(slot)
         }
     }
@@ -282,8 +268,7 @@ library StorageSlot {
      * @dev Store `value` at location `slot` in transient storage.
      */
     function tstore(Bytes32SlotType slot, bytes32 value) internal {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             tstore(slot, value)
         }
     }
@@ -292,8 +277,7 @@ library StorageSlot {
      * @dev Load the value held at location `slot` in transient storage.
      */
     function tload(Uint256SlotType slot) internal view returns (uint256 value) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             value := tload(slot)
         }
     }
@@ -302,8 +286,7 @@ library StorageSlot {
      * @dev Store `value` at location `slot` in transient storage.
      */
     function tstore(Uint256SlotType slot, uint256 value) internal {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             tstore(slot, value)
         }
     }
@@ -312,8 +295,7 @@ library StorageSlot {
      * @dev Load the value held at location `slot` in transient storage.
      */
     function tload(Int256SlotType slot) internal view returns (int256 value) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             value := tload(slot)
         }
     }
@@ -322,8 +304,7 @@ library StorageSlot {
      * @dev Store `value` at location `slot` in transient storage.
      */
     function tstore(Int256SlotType slot, int256 value) internal {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             tstore(slot, value)
         }
     }

+ 26 - 4
contracts/utils/Strings.sol

@@ -26,14 +26,12 @@ library Strings {
             uint256 length = Math.log10(value) + 1;
             string memory buffer = new string(length);
             uint256 ptr;
-            /// @solidity memory-safe-assembly
-            assembly {
+            assembly ("memory-safe") {
                 ptr := add(buffer, add(32, length))
             }
             while (true) {
                 ptr--;
-                /// @solidity memory-safe-assembly
-                assembly {
+                assembly ("memory-safe") {
                     mstore8(ptr, byte(mod(value, 10), HEX_DIGITS))
                 }
                 value /= 10;
@@ -85,6 +83,30 @@ library Strings {
         return toHexString(uint256(uint160(addr)), ADDRESS_LENGTH);
     }
 
+    /**
+     * @dev Converts an `address` with fixed length of 20 bytes to its checksummed ASCII `string` hexadecimal
+     * representation, according to EIP-55.
+     */
+    function toChecksumHexString(address addr) internal pure returns (string memory) {
+        bytes memory buffer = bytes(toHexString(addr));
+
+        // hash the hex part of buffer (skip length + 2 bytes, length 40)
+        uint256 hashValue;
+        assembly ("memory-safe") {
+            hashValue := shr(96, keccak256(add(buffer, 0x22), 40))
+        }
+
+        for (uint256 i = 41; i > 1; --i) {
+            // possible values for buffer[i] are 48 (0) to 57 (9) and 97 (a) to 102 (f)
+            if (hashValue & 0xf > 7 && uint8(buffer[i]) > 96) {
+                // case shift by xoring with 0x20
+                buffer[i] ^= 0x20;
+            }
+            hashValue >>= 4;
+        }
+        return string(buffer);
+    }
+
     /**
      * @dev Returns true if the two strings are equal.
      */

+ 1 - 2
contracts/utils/cryptography/ECDSA.sol

@@ -60,8 +60,7 @@ library ECDSA {
             uint8 v;
             // ecrecover takes the signature parameters, and the only way to get them
             // currently is to use assembly.
-            /// @solidity memory-safe-assembly
-            assembly {
+            assembly ("memory-safe") {
                 r := mload(add(signature, 0x20))
                 s := mload(add(signature, 0x40))
                 v := byte(0, mload(add(signature, 0x60)))

+ 1 - 2
contracts/utils/cryptography/Hashes.sol

@@ -19,8 +19,7 @@ library Hashes {
      * @dev Implementation of keccak256(abi.encode(a, b)) that doesn't allocate or expand memory.
      */
     function _efficientKeccak256(bytes32 a, bytes32 b) private pure returns (bytes32 value) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             mstore(0x00, a)
             mstore(0x20, b)
             value := keccak256(0x00, 0x40)

+ 329 - 30
contracts/utils/cryptography/MerkleProof.sol

@@ -1,5 +1,6 @@
 // SPDX-License-Identifier: MIT
 // OpenZeppelin Contracts (last updated v5.0.0) (utils/cryptography/MerkleProof.sol)
+// This file was procedurally generated from scripts/generate/templates/MerkleProof.js.
 
 pragma solidity ^0.8.20;
 
@@ -18,6 +19,14 @@ import {Hashes} from "./Hashes.sol";
  * the Merkle tree could be reinterpreted as a leaf value.
  * OpenZeppelin's JavaScript library generates Merkle trees that are safe
  * against this attack out of the box.
+ *
+ * IMPORTANT: Consider memory side-effects when using custom hashing functions
+ * that access memory in an unsafe way.
+ *
+ * NOTE: This library supports proof verification for merkle trees built using
+ * custom _commutative_ hashing functions (i.e. `H(a, b) == H(b, a)`). Proving
+ * leaf inclusion in trees built using non-commutative hashing functions requires
+ * additional logic that is not supported by this library.
  */
 library MerkleProof {
     /**
@@ -30,13 +39,73 @@ library MerkleProof {
      * defined by `root`. For this, a `proof` must be provided, containing
      * sibling hashes on the branch from the leaf to the root of the tree. Each
      * pair of leaves and each pair of pre-images are assumed to be sorted.
+     *
+     * This version handles proofs in memory with the default hashing function.
      */
     function verify(bytes32[] memory proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
         return processProof(proof, leaf) == root;
     }
 
     /**
-     * @dev Calldata version of {verify}
+     * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
+     * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
+     * hash matches the root of the tree. When processing the proof, the pairs
+     * of leaves & pre-images are assumed to be sorted.
+     *
+     * This version handles proofs in memory with the default hashing function.
+     */
+    function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
+        bytes32 computedHash = leaf;
+        for (uint256 i = 0; i < proof.length; i++) {
+            computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]);
+        }
+        return computedHash;
+    }
+
+    /**
+     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
+     * defined by `root`. For this, a `proof` must be provided, containing
+     * sibling hashes on the branch from the leaf to the root of the tree. Each
+     * pair of leaves and each pair of pre-images are assumed to be sorted.
+     *
+     * This version handles proofs in memory with a custom hashing function.
+     */
+    function verify(
+        bytes32[] memory proof,
+        bytes32 root,
+        bytes32 leaf,
+        function(bytes32, bytes32) view returns (bytes32) hasher
+    ) internal view returns (bool) {
+        return processProof(proof, leaf, hasher) == root;
+    }
+
+    /**
+     * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
+     * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
+     * hash matches the root of the tree. When processing the proof, the pairs
+     * of leaves & pre-images are assumed to be sorted.
+     *
+     * This version handles proofs in memory with a custom hashing function.
+     */
+    function processProof(
+        bytes32[] memory proof,
+        bytes32 leaf,
+        function(bytes32, bytes32) view returns (bytes32) hasher
+    ) internal view returns (bytes32) {
+        bytes32 computedHash = leaf;
+        for (uint256 i = 0; i < proof.length; i++) {
+            computedHash = hasher(computedHash, proof[i]);
+        }
+        return computedHash;
+    }
+
+    /**
+     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
+     * defined by `root`. For this, a `proof` must be provided, containing
+     * sibling hashes on the branch from the leaf to the root of the tree. Each
+     * pair of leaves and each pair of pre-images are assumed to be sorted.
+     *
+     * This version handles proofs in calldata with the default hashing function.
      */
     function verifyCalldata(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
         return processProofCalldata(proof, leaf) == root;
@@ -46,9 +115,11 @@ library MerkleProof {
      * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
      * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
      * hash matches the root of the tree. When processing the proof, the pairs
-     * of leafs & pre-images are assumed to be sorted.
+     * of leaves & pre-images are assumed to be sorted.
+     *
+     * This version handles proofs in calldata with the default hashing function.
      */
-    function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
+    function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) {
         bytes32 computedHash = leaf;
         for (uint256 i = 0; i < proof.length; i++) {
             computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]);
@@ -57,12 +128,38 @@ library MerkleProof {
     }
 
     /**
-     * @dev Calldata version of {processProof}
+     * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
+     * defined by `root`. For this, a `proof` must be provided, containing
+     * sibling hashes on the branch from the leaf to the root of the tree. Each
+     * pair of leaves and each pair of pre-images are assumed to be sorted.
+     *
+     * This version handles proofs in calldata with a custom hashing function.
      */
-    function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) {
+    function verifyCalldata(
+        bytes32[] calldata proof,
+        bytes32 root,
+        bytes32 leaf,
+        function(bytes32, bytes32) view returns (bytes32) hasher
+    ) internal view returns (bool) {
+        return processProofCalldata(proof, leaf, hasher) == root;
+    }
+
+    /**
+     * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
+     * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
+     * hash matches the root of the tree. When processing the proof, the pairs
+     * of leaves & pre-images are assumed to be sorted.
+     *
+     * This version handles proofs in calldata with a custom hashing function.
+     */
+    function processProofCalldata(
+        bytes32[] calldata proof,
+        bytes32 leaf,
+        function(bytes32, bytes32) view returns (bytes32) hasher
+    ) internal view returns (bytes32) {
         bytes32 computedHash = leaf;
         for (uint256 i = 0; i < proof.length; i++) {
-            computedHash = Hashes.commutativeKeccak256(computedHash, proof[i]);
+            computedHash = hasher(computedHash, proof[i]);
         }
         return computedHash;
     }
@@ -71,7 +168,12 @@ library MerkleProof {
      * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
      * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
      *
+     * This version handles multiproofs in memory with the default hashing function.
+     *
      * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
+     *
+     * NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
+     * The `leaves` must be validated independently. See {processMultiProof}.
      */
     function multiProofVerify(
         bytes32[] memory proof,
@@ -83,9 +185,169 @@ library MerkleProof {
     }
 
     /**
-     * @dev Calldata version of {multiProofVerify}
+     * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
+     * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
+     * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
+     * respectively.
+     *
+     * This version handles multiproofs in memory with the default hashing function.
+     *
+     * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
+     * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
+     * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
+     *
+     * NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
+     * and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
+     * validating the leaves elsewhere.
+     */
+    function processMultiProof(
+        bytes32[] memory proof,
+        bool[] memory proofFlags,
+        bytes32[] memory leaves
+    ) internal pure returns (bytes32 merkleRoot) {
+        // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
+        // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
+        // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
+        // the Merkle tree.
+        uint256 leavesLen = leaves.length;
+        uint256 proofFlagsLen = proofFlags.length;
+
+        // Check proof validity.
+        if (leavesLen + proof.length != proofFlagsLen + 1) {
+            revert MerkleProofInvalidMultiproof();
+        }
+
+        // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
+        // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
+        bytes32[] memory hashes = new bytes32[](proofFlagsLen);
+        uint256 leafPos = 0;
+        uint256 hashPos = 0;
+        uint256 proofPos = 0;
+        // At each step, we compute the next hash using two values:
+        // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
+        //   get the next hash.
+        // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
+        //   `proof` array.
+        for (uint256 i = 0; i < proofFlagsLen; i++) {
+            bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
+            bytes32 b = proofFlags[i]
+                ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
+                : proof[proofPos++];
+            hashes[i] = Hashes.commutativeKeccak256(a, b);
+        }
+
+        if (proofFlagsLen > 0) {
+            if (proofPos != proof.length) {
+                revert MerkleProofInvalidMultiproof();
+            }
+            unchecked {
+                return hashes[proofFlagsLen - 1];
+            }
+        } else if (leavesLen > 0) {
+            return leaves[0];
+        } else {
+            return proof[0];
+        }
+    }
+
+    /**
+     * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
+     * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
+     *
+     * This version handles multiproofs in memory with a custom hashing function.
      *
      * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
+     *
+     * NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
+     * The `leaves` must be validated independently. See {processMultiProof}.
+     */
+    function multiProofVerify(
+        bytes32[] memory proof,
+        bool[] memory proofFlags,
+        bytes32 root,
+        bytes32[] memory leaves,
+        function(bytes32, bytes32) view returns (bytes32) hasher
+    ) internal view returns (bool) {
+        return processMultiProof(proof, proofFlags, leaves, hasher) == root;
+    }
+
+    /**
+     * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
+     * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
+     * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
+     * respectively.
+     *
+     * This version handles multiproofs in memory with a custom hashing function.
+     *
+     * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
+     * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
+     * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
+     *
+     * NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
+     * and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
+     * validating the leaves elsewhere.
+     */
+    function processMultiProof(
+        bytes32[] memory proof,
+        bool[] memory proofFlags,
+        bytes32[] memory leaves,
+        function(bytes32, bytes32) view returns (bytes32) hasher
+    ) internal view returns (bytes32 merkleRoot) {
+        // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
+        // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
+        // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
+        // the Merkle tree.
+        uint256 leavesLen = leaves.length;
+        uint256 proofFlagsLen = proofFlags.length;
+
+        // Check proof validity.
+        if (leavesLen + proof.length != proofFlagsLen + 1) {
+            revert MerkleProofInvalidMultiproof();
+        }
+
+        // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
+        // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
+        bytes32[] memory hashes = new bytes32[](proofFlagsLen);
+        uint256 leafPos = 0;
+        uint256 hashPos = 0;
+        uint256 proofPos = 0;
+        // At each step, we compute the next hash using two values:
+        // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
+        //   get the next hash.
+        // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
+        //   `proof` array.
+        for (uint256 i = 0; i < proofFlagsLen; i++) {
+            bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
+            bytes32 b = proofFlags[i]
+                ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
+                : proof[proofPos++];
+            hashes[i] = hasher(a, b);
+        }
+
+        if (proofFlagsLen > 0) {
+            if (proofPos != proof.length) {
+                revert MerkleProofInvalidMultiproof();
+            }
+            unchecked {
+                return hashes[proofFlagsLen - 1];
+            }
+        } else if (leavesLen > 0) {
+            return leaves[0];
+        } else {
+            return proof[0];
+        }
+    }
+
+    /**
+     * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
+     * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
+     *
+     * This version handles multiproofs in calldata with the default hashing function.
+     *
+     * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
+     *
+     * NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
+     * The `leaves` must be validated independently. See {processMultiProofCalldata}.
      */
     function multiProofVerifyCalldata(
         bytes32[] calldata proof,
@@ -102,13 +364,19 @@ library MerkleProof {
      * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
      * respectively.
      *
+     * This version handles multiproofs in calldata with the default hashing function.
+     *
      * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
      * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
      * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
+     *
+     * NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
+     * and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
+     * validating the leaves elsewhere.
      */
-    function processMultiProof(
-        bytes32[] memory proof,
-        bool[] memory proofFlags,
+    function processMultiProofCalldata(
+        bytes32[] calldata proof,
+        bool[] calldata proofFlags,
         bytes32[] memory leaves
     ) internal pure returns (bytes32 merkleRoot) {
         // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
@@ -116,17 +384,16 @@ library MerkleProof {
         // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
         // the Merkle tree.
         uint256 leavesLen = leaves.length;
-        uint256 proofLen = proof.length;
-        uint256 totalHashes = proofFlags.length;
+        uint256 proofFlagsLen = proofFlags.length;
 
         // Check proof validity.
-        if (leavesLen + proofLen != totalHashes + 1) {
+        if (leavesLen + proof.length != proofFlagsLen + 1) {
             revert MerkleProofInvalidMultiproof();
         }
 
         // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
         // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
-        bytes32[] memory hashes = new bytes32[](totalHashes);
+        bytes32[] memory hashes = new bytes32[](proofFlagsLen);
         uint256 leafPos = 0;
         uint256 hashPos = 0;
         uint256 proofPos = 0;
@@ -135,7 +402,7 @@ library MerkleProof {
         //   get the next hash.
         // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
         //   `proof` array.
-        for (uint256 i = 0; i < totalHashes; i++) {
+        for (uint256 i = 0; i < proofFlagsLen; i++) {
             bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
             bytes32 b = proofFlags[i]
                 ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
@@ -143,12 +410,12 @@ library MerkleProof {
             hashes[i] = Hashes.commutativeKeccak256(a, b);
         }
 
-        if (totalHashes > 0) {
-            if (proofPos != proofLen) {
+        if (proofFlagsLen > 0) {
+            if (proofPos != proof.length) {
                 revert MerkleProofInvalidMultiproof();
             }
             unchecked {
-                return hashes[totalHashes - 1];
+                return hashes[proofFlagsLen - 1];
             }
         } else if (leavesLen > 0) {
             return leaves[0];
@@ -158,31 +425,63 @@ library MerkleProof {
     }
 
     /**
-     * @dev Calldata version of {processMultiProof}.
+     * @dev Returns true if the `leaves` can be simultaneously proven to be a part of a Merkle tree defined by
+     * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
+     *
+     * This version handles multiproofs in calldata with a custom hashing function.
      *
      * CAUTION: Not all Merkle trees admit multiproofs. See {processMultiProof} for details.
+     *
+     * NOTE: Consider the case where `root == proof[0] && leaves.length == 0` as it will return `true`.
+     * The `leaves` must be validated independently. See {processMultiProofCalldata}.
+     */
+    function multiProofVerifyCalldata(
+        bytes32[] calldata proof,
+        bool[] calldata proofFlags,
+        bytes32 root,
+        bytes32[] memory leaves,
+        function(bytes32, bytes32) view returns (bytes32) hasher
+    ) internal view returns (bool) {
+        return processMultiProofCalldata(proof, proofFlags, leaves, hasher) == root;
+    }
+
+    /**
+     * @dev Returns the root of a tree reconstructed from `leaves` and sibling nodes in `proof`. The reconstruction
+     * proceeds by incrementally reconstructing all inner nodes by combining a leaf/inner node with either another
+     * leaf/inner node or a proof sibling node, depending on whether each `proofFlags` item is true or false
+     * respectively.
+     *
+     * This version handles multiproofs in calldata with a custom hashing function.
+     *
+     * CAUTION: Not all Merkle trees admit multiproofs. To use multiproofs, it is sufficient to ensure that: 1) the tree
+     * is complete (but not necessarily perfect), 2) the leaves to be proven are in the opposite order they are in the
+     * tree (i.e., as seen from right to left starting at the deepest layer and continuing at the next layer).
+     *
+     * NOTE: The _empty set_ (i.e. the case where `proof.length == 1 && leaves.length == 0`) is considered a no-op,
+     * and therefore a valid multiproof (i.e. it returns `proof[0]`). Consider disallowing this case if you're not
+     * validating the leaves elsewhere.
      */
     function processMultiProofCalldata(
         bytes32[] calldata proof,
         bool[] calldata proofFlags,
-        bytes32[] memory leaves
-    ) internal pure returns (bytes32 merkleRoot) {
+        bytes32[] memory leaves,
+        function(bytes32, bytes32) view returns (bytes32) hasher
+    ) internal view returns (bytes32 merkleRoot) {
         // This function rebuilds the root hash by traversing the tree up from the leaves. The root is rebuilt by
         // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
         // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
         // the Merkle tree.
         uint256 leavesLen = leaves.length;
-        uint256 proofLen = proof.length;
-        uint256 totalHashes = proofFlags.length;
+        uint256 proofFlagsLen = proofFlags.length;
 
         // Check proof validity.
-        if (leavesLen + proofLen != totalHashes + 1) {
+        if (leavesLen + proof.length != proofFlagsLen + 1) {
             revert MerkleProofInvalidMultiproof();
         }
 
         // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
         // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
-        bytes32[] memory hashes = new bytes32[](totalHashes);
+        bytes32[] memory hashes = new bytes32[](proofFlagsLen);
         uint256 leafPos = 0;
         uint256 hashPos = 0;
         uint256 proofPos = 0;
@@ -191,20 +490,20 @@ library MerkleProof {
         //   get the next hash.
         // - depending on the flag, either another value from the "main queue" (merging branches) or an element from the
         //   `proof` array.
-        for (uint256 i = 0; i < totalHashes; i++) {
+        for (uint256 i = 0; i < proofFlagsLen; i++) {
             bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
             bytes32 b = proofFlags[i]
                 ? (leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++])
                 : proof[proofPos++];
-            hashes[i] = Hashes.commutativeKeccak256(a, b);
+            hashes[i] = hasher(a, b);
         }
 
-        if (totalHashes > 0) {
-            if (proofPos != proofLen) {
+        if (proofFlagsLen > 0) {
+            if (proofPos != proof.length) {
                 revert MerkleProofInvalidMultiproof();
             }
             unchecked {
-                return hashes[totalHashes - 1];
+                return hashes[proofFlagsLen - 1];
             }
         } else if (leavesLen > 0) {
             return leaves[0];

+ 2 - 4
contracts/utils/cryptography/MessageHashUtils.sol

@@ -28,8 +28,7 @@ library MessageHashUtils {
      * See {ECDSA-recover}.
      */
     function toEthSignedMessageHash(bytes32 messageHash) internal pure returns (bytes32 digest) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             mstore(0x00, "\x19Ethereum Signed Message:\n32") // 32 is the bytes-length of messageHash
             mstore(0x1c, messageHash) // 0x1c (28) is the length of the prefix
             digest := keccak256(0x00, 0x3c) // 0x3c is the length of the prefix (0x1c) + messageHash (0x20)
@@ -74,8 +73,7 @@ library MessageHashUtils {
      * See {ECDSA-recover}.
      */
     function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32 digest) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             let ptr := mload(0x40)
             mstore(ptr, hex"19_01")
             mstore(add(ptr, 0x02), domainSeparator)

+ 317 - 0
contracts/utils/cryptography/P256.sol

@@ -0,0 +1,317 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {Math} from "../math/Math.sol";
+import {Errors} from "../Errors.sol";
+
+/**
+ * @dev Implementation of secp256r1 verification and recovery functions.
+ *
+ * The secp256r1 curve (also known as P256) is a NIST standard curve with wide support in modern devices
+ * 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.
+ */
+library P256 {
+    struct JPoint {
+        uint256 x;
+        uint256 y;
+        uint256 z;
+    }
+
+    /// @dev Generator (x component)
+    uint256 internal constant GX = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296;
+    /// @dev Generator (y component)
+    uint256 internal constant GY = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5;
+    /// @dev P (size of the field)
+    uint256 internal constant P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF;
+    /// @dev N (order of G)
+    uint256 internal constant N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551;
+    /// @dev A parameter of the weierstrass equation
+    uint256 internal constant A = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC;
+    /// @dev B parameter of the weierstrass equation
+    uint256 internal constant B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B;
+
+    /// @dev (P + 1) / 4. Useful to compute sqrt
+    uint256 private constant P1DIV4 = 0x3fffffffc0000000400000000000000000000000400000000000000000000000;
+
+    /// @dev N/2 for excluding higher order `s` values
+    uint256 private constant HALF_N = 0x7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8;
+
+    /**
+     * @dev Verifies a secp256r1 signature using the RIP-7212 precompile and falls back to the Solidity implementation
+     * if the precompile is not available. This version should work on all chains, but requires the deployment of more
+     * bytecode.
+     *
+     * @param h - hashed message
+     * @param r - signature half R
+     * @param s - signature half S
+     * @param qx - public key coordinate X
+     * @param qy - public key coordinate Y
+     *
+     * 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`.
+     */
+    function verify(bytes32 h, bytes32 r, bytes32 s, bytes32 qx, bytes32 qy) internal view returns (bool) {
+        (bool valid, bool supported) = _tryVerifyNative(h, r, s, qx, qy);
+        return supported ? valid : verifySolidity(h, r, s, qx, qy);
+    }
+
+    /**
+     * @dev Same as {verify}, but it will revert if the required precompile is not available.
+     *
+     * Make sure any logic (code or precompile) deployed at that address is the expected one,
+     * otherwise the returned value may be misinterpreted as a positive boolean.
+     */
+    function verifyNative(bytes32 h, bytes32 r, bytes32 s, bytes32 qx, bytes32 qy) internal view returns (bool) {
+        (bool valid, bool supported) = _tryVerifyNative(h, r, s, qx, qy);
+        if (supported) {
+            return valid;
+        } else {
+            revert Errors.MissingPrecompile(address(0x100));
+        }
+    }
+
+    /**
+     * @dev Same as {verify}, but it will return false if the required precompile is not available.
+     */
+    function _tryVerifyNative(
+        bytes32 h,
+        bytes32 r,
+        bytes32 s,
+        bytes32 qx,
+        bytes32 qy
+    ) private view returns (bool valid, bool supported) {
+        if (!_isProperSignature(r, s) || !isValidPublicKey(qx, qy)) {
+            return (false, true); // signature is invalid, and its not because the precompile is missing
+        }
+
+        (bool success, bytes memory returndata) = address(0x100).staticcall(abi.encode(h, r, s, qx, qy));
+        return (success && returndata.length == 0x20) ? (abi.decode(returndata, (bool)), true) : (false, false);
+    }
+
+    /**
+     * @dev Same as {verify}, but only the Solidity implementation is used.
+     */
+    function verifySolidity(bytes32 h, bytes32 r, bytes32 s, bytes32 qx, bytes32 qy) internal view returns (bool) {
+        if (!_isProperSignature(r, s) || !isValidPublicKey(qx, qy)) {
+            return false;
+        }
+
+        JPoint[16] memory points = _preComputeJacobianPoints(uint256(qx), uint256(qy));
+        uint256 w = Math.invModPrime(uint256(s), N);
+        uint256 u1 = mulmod(uint256(h), w, N);
+        uint256 u2 = mulmod(uint256(r), w, N);
+        (uint256 x, ) = _jMultShamir(points, u1, u2);
+        return ((x % N) == uint256(r));
+    }
+
+    /**
+     * @dev Public key recovery
+     *
+     * @param h - hashed message
+     * @param v - signature recovery param
+     * @param r - signature half R
+     * @param s - signature half S
+     *
+     * 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) {
+        if (!_isProperSignature(r, s) || v > 1) {
+            return (0, 0);
+        }
+
+        uint256 p = P; // cache P on the stack
+        uint256 rx = uint256(r);
+        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;
+
+        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));
+    }
+
+    /**
+     * @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.
+     */
+    function isValidPublicKey(bytes32 x, bytes32 y) internal pure returns (bool result) {
+        assembly ("memory-safe") {
+            let p := P
+            let lhs := mulmod(y, y, p) // y^2
+            let rhs := addmod(mulmod(addmod(mulmod(x, x, p), A, p), x, p), B, p) // ((x^2 + a) * x) + b = x^3 + ax + b
+            result := and(and(lt(x, p), lt(y, p)), eq(lhs, rhs)) // Should conform with the Weierstrass equation
+        }
+    }
+
+    /**
+     * @dev Checks if (r, s) is a proper signature.
+     * In particular, this checks that `s` is in the "lower-range", making the signature non-malleable.
+     */
+    function _isProperSignature(bytes32 r, bytes32 s) private pure returns (bool) {
+        return uint256(r) > 0 && uint256(r) < N && uint256(s) > 0 && uint256(s) <= HALF_N;
+    }
+
+    /**
+     * @dev Reduce from jacobian to affine coordinates
+     * @param jx - jacobian coordinate x
+     * @param jy - jacobian coordinate y
+     * @param jz - jacobian coordinate z
+     * @return ax - affine coordinate x
+     * @return ay - affine coordinate y
+     */
+    function _affineFromJacobian(uint256 jx, uint256 jy, uint256 jz) private view returns (uint256 ax, uint256 ay) {
+        if (jz == 0) return (0, 0);
+        uint256 p = P; // cache P on the stack
+        uint256 zinv = Math.invModPrime(jz, p);
+        assembly ("memory-safe") {
+            let zzinv := mulmod(zinv, zinv, p)
+            ax := mulmod(jx, zzinv, p)
+            ay := mulmod(jy, mulmod(zzinv, zinv, p), p)
+        }
+    }
+
+    /**
+     * @dev Point addition on the jacobian coordinates
+     * Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-1998-cmo-2
+     */
+    function _jAdd(
+        JPoint memory p1,
+        uint256 x2,
+        uint256 y2,
+        uint256 z2
+    ) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
+        assembly ("memory-safe") {
+            let p := P
+            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 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²
+
+            // 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)
+        }
+    }
+
+    /**
+     * @dev Point doubling on the jacobian coordinates
+     * Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-1998-cmo-2
+     */
+    function _jDouble(uint256 x, uint256 y, uint256 z) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
+        assembly ("memory-safe") {
+            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⁴
+
+            // x' = t = m²-2*s
+            rx := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p)
+            // y' = m*(s-t)-8*y⁴ = m*(s-x')-8*y⁴
+            ry := addmod(mulmod(m, addmod(s, sub(p, rx), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p)
+            // z' = 2*y*z
+            rz := mulmod(2, mulmod(y, z, p), p)
+        }
+    }
+
+    /**
+     * @dev Compute P·u1 + Q·u2 using the precomputed points for P and Q (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
+     */
+    function _jMultShamir(JPoint[16] memory points, uint256 u1, uint256 u2) private view returns (uint256, uint256) {
+        uint256 x = 0;
+        uint256 y = 0;
+        uint256 z = 0;
+        unchecked {
+            for (uint256 i = 0; i < 128; ++i) {
+                if (z > 0) {
+                    (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.
+                uint256 pos = ((u1 >> 252) & 0xc) | ((u2 >> 254) & 0x3);
+                if (pos > 0) {
+                    if (z == 0) {
+                        (x, y, z) = (points[pos].x, points[pos].y, points[pos].z);
+                    } else {
+                        (x, y, z) = _jAdd(points[pos], x, y, z);
+                    }
+                }
+                u1 <<= 2;
+                u2 <<= 2;
+            }
+        }
+        return _affineFromJacobian(x, y, z);
+    }
+
+    /**
+     * @dev Precompute a matrice of useful jacobian points associated with a given P. This can be seen as a 4x4 matrix
+     * that contains combination of P and G (generator) up to 3 times each. See the table below:
+     *
+     * ┌────┬─────────────────────┐
+     * │  i │  0    1     2     3 │
+     * ├────┼─────────────────────┤
+     * │  0 │  0    p    2p    3p │
+     * │  4 │  g  g+p  g+2p  g+3p │
+     * │  8 │ 2g 2g+p 2g+2p 2g+3p │
+     * │ 12 │ 3g 3g+p 3g+2p 3g+3p │
+     * └────┴─────────────────────┘
+     */
+    function _preComputeJacobianPoints(uint256 px, uint256 py) private pure returns (JPoint[16] memory points) {
+        points[0x00] = JPoint(0, 0, 0); // 0,0
+        points[0x01] = JPoint(px, py, 1); // 1,0 (p)
+        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[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[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)
+    }
+
+    function _jAddPoint(JPoint memory p1, JPoint memory p2) private pure returns (JPoint memory) {
+        (uint256 x, uint256 y, uint256 z) = _jAdd(p1, p2.x, p2.y, p2.z);
+        return JPoint(x, y, z);
+    }
+
+    function _jDoublePoint(JPoint memory p) private pure returns (JPoint memory) {
+        (uint256 x, uint256 y, uint256 z) = _jDouble(p.x, p.y, p.z);
+        return JPoint(x, y, z);
+    }
+}

+ 145 - 0
contracts/utils/cryptography/RSA.sol

@@ -0,0 +1,145 @@
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.20;
+
+import {Math} from "../math/Math.sol";
+
+/**
+ * @dev RSA PKCS#1 v1.5 signature verification implementation according to https://datatracker.ietf.org/doc/html/rfc8017[RFC8017].
+ *
+ * This library supports PKCS#1 v1.5 padding to avoid malleability via chosen plaintext attacks in practical implementations.
+ * 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]
+ */
+library RSA {
+    /**
+     * @dev Same as {pkcs1} but using SHA256 to calculate the digest of `data`.
+     */
+    function pkcs1Sha256(
+        bytes memory data,
+        bytes memory s,
+        bytes memory e,
+        bytes memory n
+    ) internal view returns (bool) {
+        return pkcs1(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].
+     *
+     * IMPORTANT: Although this function allows for it, using n of length 1024 bits is considered unsafe.
+     * Consider using at least 2048 bits.
+     *
+     * 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.
+     *
+     * @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) {
+        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 != s.length // signature must have the same length as the finite field
+            ) {
+                return false;
+            }
+
+            // Verify that s < n to ensure there's only one valid signature for a given message
+            for (uint256 i = 0; i < length; i += 0x20) {
+                uint256 p = Math.min(i, length - 0x20);
+                bytes32 sp = _unsafeReadBytes32(s, p);
+                bytes32 np = _unsafeReadBytes32(n, p);
+                if (sp < np) {
+                    // s < n in the upper bits (everything before is equal) → s < n globally: ok
+                    break;
+                } else if (sp > np || p == length - 0x20) {
+                    // s > n in the upper bits (everything before is equal) → s > n globally: fail
+                    // or
+                    // s = n and we are looking at the lower bits → s = n globally: fail
+                    return false;
+                }
+            }
+
+            // RSAVP1 https://datatracker.ietf.org/doc/html/rfc8017#section-5.2.2
+            // The previous check guarantees that n > 0. Therefore modExp cannot revert.
+            bytes memory buffer = Math.modExp(s, e, n);
+
+            // Check that buffer is well encoded:
+            // buffer ::= 0x00 | 0x01 | PS | 0x00 | DigestInfo
+            //
+            // With
+            // - PS is padding filled with 0xFF
+            // - DigestInfo ::= SEQUENCE {
+            //    digestAlgorithm AlgorithmIdentifier,
+            //      [optional algorithm parameters]
+            //    digest OCTET STRING
+            // }
+
+            // Get AlgorithmIdentifier from the DigestInfo, and set the config accordingly
+            // - params: includes 00 + first part of DigestInfo
+            // - mask: filter to check the params
+            // - offset: length of the suffix (including digest)
+            bytes32 params; // 0x00 | DigestInfo
+            bytes32 mask;
+            uint256 offset;
+
+            // Digest is expected at the end of the buffer. Therefore if NULL param is present,
+            // 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) {
+                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) {
+                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 = <implicit> | OCTET_STRING length (0x20) = 0420 (2 bytes)
+                params = 0x00302f300b060960864801650304020104200000000000000000000000000000;
+                mask = 0xffffffffffffffffffffffffffffffffffff0000000000000000000000000000; // (18 bytes)
+            } else {
+                // unknown
+                return false;
+            }
+
+            // Length is at least 0x40 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.
+            // This is required to ensure non-malleability. Not checking would allow an attacker to
+            // use the padding to manipulate the message in order to create a valid signature out of
+            // multiple valid signatures.
+            for (uint256 i = 2; i < paddingEnd; ++i) {
+                if (bytes1(_unsafeReadBytes32(buffer, i)) != 0xFF) {
+                    return false;
+                }
+            }
+
+            // All the other parameters are small enough to fit in a bytes32, so we can check them directly.
+            return
+                bytes2(0x0001) == bytes2(_unsafeReadBytes32(buffer, 0x00)) && // 00 | 01
+                // PS was checked in the loop
+                params == _unsafeReadBytes32(buffer, paddingEnd) & mask && // DigestInfo
+                // Optional parameters are not checked
+                digest == _unsafeReadBytes32(buffer, length - 0x20); // Digest
+        }
+    }
+
+    /// @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}.
+        assembly ("memory-safe") {
+            result := mload(add(add(array, 0x20), offset))
+        }
+    }
+}

+ 1 - 1
contracts/utils/introspection/ERC165Checker.sol

@@ -113,7 +113,7 @@ library ERC165Checker {
         bool success;
         uint256 returnSize;
         uint256 returnValue;
-        assembly {
+        assembly ("memory-safe") {
             success := staticcall(30000, account, add(encodedParams, 0x20), mload(encodedParams), 0x00, 0x20)
             returnSize := returndatasize()
             returnValue := mload(0x00)

+ 21 - 8
contracts/utils/math/Math.sol

@@ -144,7 +144,7 @@ library Math {
     function mulDiv(uint256 x, uint256 y, uint256 denominator) internal pure returns (uint256 result) {
         unchecked {
             // 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2²⁵⁶ and mod 2²⁵⁶ - 1, then use
-            // use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
+            // the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
             // variables such that product = prod1 * 2²⁵⁶ + prod0.
             uint256 prod0 = x * y; // Least significant 256 bits of the product
             uint256 prod1; // Most significant 256 bits of the product
@@ -232,13 +232,13 @@ library Math {
     /**
      * @dev Calculate the modular multiplicative inverse of a number in Z/nZ.
      *
-     * If n is a prime, then Z/nZ is a field. In that case all elements are inversible, expect 0.
+     * If n is a prime, then Z/nZ is a field. In that case all elements are inversible, except 0.
      * If n is not a prime, then Z/nZ is not a field, and some elements might not be inversible.
      *
      * If the input value is not inversible, 0 is returned.
      *
-     * NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Ferma's little theorem and get the
-     * inverse using `Math.modExp(a, n - 2, n)`.
+     * NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the
+     * inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}.
      */
     function invMod(uint256 a, uint256 n) internal pure returns (uint256) {
         unchecked {
@@ -288,6 +288,21 @@ library Math {
         }
     }
 
+    /**
+     * @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`.
+     *
+     * From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is
+     * prime, then `a**(p-1) ≡ 1 mod p`. As a consequence, we have `a * a**(p-2) ≡ 1 mod p`, which means that
+     * `a**(p-2)` is the modular multiplicative inverse of a in Fp.
+     *
+     * NOTE: this function does NOT check that `p` is a prime greater than `2`.
+     */
+    function invModPrime(uint256 a, uint256 p) internal view returns (uint256) {
+        unchecked {
+            return Math.modExp(a, p - 2, p);
+        }
+    }
+
     /**
      * @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m)
      *
@@ -321,8 +336,7 @@ library Math {
      */
     function tryModExp(uint256 b, uint256 e, uint256 m) internal view returns (bool success, uint256 result) {
         if (m == 0) return (false, 0);
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             let ptr := mload(0x40)
             // | Offset    | Content    | Content (Hex)                                                      |
             // |-----------|------------|--------------------------------------------------------------------|
@@ -372,8 +386,7 @@ library Math {
         // Encode call args in result and move the free memory pointer
         result = abi.encodePacked(b.length, e.length, mLen, b, e, m);
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             let dataPtr := add(result, 0x20)
             // Write result on top of args to avoid allocating extra memory.
             success := staticcall(gas(), 0x05, dataPtr, mload(result), dataPtr, mLen)

+ 1 - 2
contracts/utils/math/SafeCast.sol

@@ -1155,8 +1155,7 @@ library SafeCast {
      * @dev Cast a boolean (false or true) to a uint256 (0 or 1) with no jump.
      */
     function toUint(bool b) internal pure returns (uint256 u) {
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             u := iszero(iszero(b))
         }
     }

+ 2 - 2
contracts/utils/math/SignedMath.sol

@@ -58,10 +58,10 @@ library SignedMath {
             // Since `n` is a signed integer, the generated bytecode will use the SAR opcode to perform the right shift,
             // taking advantage of the most significant (or "sign" bit) in two's complement representation.
             // This opcode adds new most significant bits set to the value of the previous most significant bit. As a result,
-            // the mask will either be `bytes(0)` (if n is positive) or `~bytes32(0)` (if n is negative).
+            // the mask will either be `bytes32(0)` (if n is positive) or `~bytes32(0)` (if n is negative).
             int256 mask = n >> 255;
 
-            // A `bytes(0)` mask leaves the input unchanged, while a `~bytes32(0)` mask complements it.
+            // A `bytes32(0)` mask leaves the input unchanged, while a `~bytes32(0)` mask complements it.
             return uint256((n + mask) ^ mask);
         }
     }

+ 15 - 15
contracts/utils/structs/Checkpoints.sol

@@ -142,7 +142,7 @@ library Checkpoints {
 
             // Update or push new checkpoint
             if (lastKey == key) {
-                _unsafeAccess(self, pos - 1)._value = value;
+                last._value = value;
             } else {
                 self.push(Checkpoint224({_key: key, _value: value}));
             }
@@ -154,7 +154,7 @@ library Checkpoints {
     }
 
     /**
-     * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high`
+     * @dev Return the index of the first (oldest) checkpoint with key strictly bigger than the search key, or `high`
      * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive
      * `high`.
      *
@@ -178,9 +178,9 @@ library Checkpoints {
     }
 
     /**
-     * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or
-     * `high` if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and
-     * exclusive `high`.
+     * @dev Return the index of the first (oldest) checkpoint with key greater or equal than the search key, or `high`
+     * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive
+     * `high`.
      *
      * WARNING: `high` should not be greater than the array's length.
      */
@@ -337,7 +337,7 @@ library Checkpoints {
 
             // Update or push new checkpoint
             if (lastKey == key) {
-                _unsafeAccess(self, pos - 1)._value = value;
+                last._value = value;
             } else {
                 self.push(Checkpoint208({_key: key, _value: value}));
             }
@@ -349,7 +349,7 @@ library Checkpoints {
     }
 
     /**
-     * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high`
+     * @dev Return the index of the first (oldest) checkpoint with key strictly bigger than the search key, or `high`
      * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive
      * `high`.
      *
@@ -373,9 +373,9 @@ library Checkpoints {
     }
 
     /**
-     * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or
-     * `high` if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and
-     * exclusive `high`.
+     * @dev Return the index of the first (oldest) checkpoint with key greater or equal than the search key, or `high`
+     * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive
+     * `high`.
      *
      * WARNING: `high` should not be greater than the array's length.
      */
@@ -532,7 +532,7 @@ library Checkpoints {
 
             // Update or push new checkpoint
             if (lastKey == key) {
-                _unsafeAccess(self, pos - 1)._value = value;
+                last._value = value;
             } else {
                 self.push(Checkpoint160({_key: key, _value: value}));
             }
@@ -544,7 +544,7 @@ library Checkpoints {
     }
 
     /**
-     * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or `high`
+     * @dev Return the index of the first (oldest) checkpoint with key strictly bigger than the search key, or `high`
      * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive
      * `high`.
      *
@@ -568,9 +568,9 @@ library Checkpoints {
     }
 
     /**
-     * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or
-     * `high` if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and
-     * exclusive `high`.
+     * @dev Return the index of the first (oldest) checkpoint with key greater or equal than the search key, or `high`
+     * if there is none. `low` and `high` define a section where to do the search, with inclusive `low` and exclusive
+     * `high`.
      *
      * WARNING: `high` should not be greater than the array's length.
      */

+ 1 - 1
contracts/utils/structs/CircularBuffer.sol

@@ -91,7 +91,7 @@ library CircularBuffer {
     }
 
     /**
-     * @dev Length of the buffer. This is the maximum number of elements kepts in the buffer.
+     * @dev Length of the buffer. This is the maximum number of elements kept in the buffer.
      */
     function length(Bytes32CircularBuffer storage self) internal view returns (uint256) {
         return self._data.length;

+ 8 - 16
contracts/utils/structs/EnumerableMap.sol

@@ -245,8 +245,7 @@ library EnumerableMap {
         bytes32[] memory store = keys(map._inner);
         uint256[] memory result;
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             result := store
         }
 
@@ -339,8 +338,7 @@ library EnumerableMap {
         bytes32[] memory store = keys(map._inner);
         uint256[] memory result;
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             result := store
         }
 
@@ -433,8 +431,7 @@ library EnumerableMap {
         bytes32[] memory store = keys(map._inner);
         uint256[] memory result;
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             result := store
         }
 
@@ -527,8 +524,7 @@ library EnumerableMap {
         bytes32[] memory store = keys(map._inner);
         address[] memory result;
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             result := store
         }
 
@@ -621,8 +617,7 @@ library EnumerableMap {
         bytes32[] memory store = keys(map._inner);
         address[] memory result;
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             result := store
         }
 
@@ -715,8 +710,7 @@ library EnumerableMap {
         bytes32[] memory store = keys(map._inner);
         address[] memory result;
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             result := store
         }
 
@@ -809,8 +803,7 @@ library EnumerableMap {
         bytes32[] memory store = keys(map._inner);
         bytes32[] memory result;
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             result := store
         }
 
@@ -903,8 +896,7 @@ library EnumerableMap {
         bytes32[] memory store = keys(map._inner);
         bytes32[] memory result;
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             result := store
         }
 

+ 3 - 6
contracts/utils/structs/EnumerableSet.sol

@@ -220,8 +220,7 @@ library EnumerableSet {
         bytes32[] memory store = _values(set._inner);
         bytes32[] memory result;
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             result := store
         }
 
@@ -294,8 +293,7 @@ library EnumerableSet {
         bytes32[] memory store = _values(set._inner);
         address[] memory result;
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             result := store
         }
 
@@ -368,8 +366,7 @@ library EnumerableSet {
         bytes32[] memory store = _values(set._inner);
         uint256[] memory result;
 
-        /// @solidity memory-safe-assembly
-        assembly {
+        assembly ("memory-safe") {
             result := store
         }
 

+ 576 - 0
contracts/utils/structs/Heap.sol

@@ -0,0 +1,576 @@
+// SPDX-License-Identifier: MIT
+// This file was procedurally generated from scripts/generate/templates/Heap.js.
+
+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)
+ */
+library Heap {
+    using Math for *;
+    using SafeCast for *;
+
+    /**
+     * @dev Binary heap that supports values of type uint256.
+     *
+     * Each element of that structure uses 2 storage slots.
+     */
+    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
+    }
+
+    /**
+     * @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;
+    }
+
+    /**
+     * @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(Uint256Heap storage self) internal returns (uint256) {
+        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(
+        Uint256Heap storage self,
+        function(uint256, uint256) view returns (bool) comp
+    ) internal returns (uint256) {
+        unchecked {
+            uint64 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;
+
+            // 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(Uint256Heap storage self, uint256 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(
+        Uint256Heap storage self,
+        uint256 value,
+        function(uint256, uint256) view returns (bool) comp
+    ) internal {
+        uint64 size = length(self);
+        if (size == type(uint64).max) Panic.panic(Panic.RESOURCE_ERROR);
+
+        self.data.push(Uint256HeapNode({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(Uint256Heap storage self, uint256 newValue) internal returns (uint256) {
+        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(
+        Uint256Heap storage self,
+        uint256 newValue,
+        function(uint256, uint256) view returns (bool) comp
+    ) internal returns (uint256) {
+        uint64 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;
+
+        // 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(Uint256Heap storage self) internal view returns (uint64) {
+        return self.data.length.toUint64();
+    }
+
+    /**
+     * @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)
+        }
+    }
+
+    /**
+     * @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;
+    }
+
+    /**
+     * @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(
+        Uint256Heap storage self,
+        uint64 size,
+        uint64 pos,
+        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);
+            }
+        }
+    }
+
+    /**
+     * @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(
+        Uint256Heap storage self,
+        uint64 pos,
+        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;
+                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;
+            }
+
+            // 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)
+        }
+    }
+}

+ 35 - 9
contracts/utils/structs/MerkleTree.sol

@@ -17,7 +17,11 @@ import {Panic} from "../Panic.sol";
  *
  * * Depth: The number of levels in the tree, it also defines the maximum number of leaves as 2**depth.
  * * Zero value: The value that represents an empty leaf. Used to avoid regular zero values to be part of the tree.
- * * Hashing function: A cryptographic hash function used to produce internal nodes.
+ * * Hashing function: A cryptographic hash function used to produce internal nodes. Defaults to {Hashes-commutativeKeccak256}.
+ *
+ * NOTE: Building trees using non-commutative hashing functions (i.e. `H(a, b) != H(b, a)`) is supported. However,
+ * proving the inclusion of a leaf in such trees is not possible with the {MerkleProof} library since it only supports
+ * _commutative_ hashing functions.
  *
  * _Available since v5.1._
  */
@@ -27,9 +31,6 @@ library MerkleTree {
      *
      * The `sides` and `zero` arrays are set to have a length equal to the depth of the tree during setup.
      *
-     * The hashing function used during initialization to compute the `zeros` values (value of a node at a given depth
-     * for which the subtree is full of zero leaves). This function is kept in the structure for handling insertions.
-     *
      * Struct members have an underscore prefix indicating that they are "private" and should not be read or written to
      * directly. Use the functions provided below instead. Modifying the struct manually may violate assumptions and
      * lead to unexpected behavior.
@@ -44,7 +45,6 @@ library MerkleTree {
         uint256 _nextLeafIndex;
         bytes32[] _sides;
         bytes32[] _zeros;
-        function(bytes32, bytes32) view returns (bytes32) _fnHash;
     }
 
     /**
@@ -53,6 +53,9 @@ library MerkleTree {
      *
      * Calling this function on MerkleTree that was already setup and used will reset it to a blank state.
      *
+     * Once a tree is setup, any push to it must use the same hashing function. This means that values
+     * should be pushed to it using the default {xref-MerkleTree-push-struct-MerkleTree-Bytes32PushTree-bytes32-}[push] function.
+     *
      * 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.
      */
@@ -61,7 +64,10 @@ library MerkleTree {
     }
 
     /**
-     * @dev Same as {setup}, but allows to specify a custom hashing function.
+     * @dev Same as {xref-MerkleTree-setup-struct-MerkleTree-Bytes32PushTree-uint8-bytes32-}[setup], but allows to specify a custom hashing function.
+     *
+     * Once a tree is setup, any push to it must use the same hashing function. This means that values
+     * 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}.
@@ -85,7 +91,6 @@ library MerkleTree {
 
         // Set the first root
         self._nextLeafIndex = 0;
-        self._fnHash = fnHash;
 
         return currentZero;
     }
@@ -96,11 +101,32 @@ library MerkleTree {
      *
      * Hashing the leaf before calling this function is recommended as a protection against
      * second pre-image attacks.
+     *
+     * This variant uses {Hashes-commutativeKeccak256} to hash internal nodes. It should only be used on merkle trees
+     * that were setup using the same (default) hashing function (i.e. by calling
+     * {xref-MerkleTree-setup-struct-MerkleTree-Bytes32PushTree-uint8-bytes32-}[the default setup] function).
      */
     function push(Bytes32PushTree storage self, bytes32 leaf) internal returns (uint256 index, bytes32 newRoot) {
+        return push(self, leaf, Hashes.commutativeKeccak256);
+    }
+
+    /**
+     * @dev Insert a new leaf in the tree, and compute the new root. Returns the position of the inserted leaf in the
+     * tree, and the resulting root.
+     *
+     * Hashing the leaf before calling this function is recommended as a protection against
+     * second pre-image attacks.
+     *
+     * This variant uses a custom hashing function to hash internal nodes. It should only be called with the same
+     * function as the one used during the initial setup of the merkle tree.
+     */
+    function push(
+        Bytes32PushTree storage self,
+        bytes32 leaf,
+        function(bytes32, bytes32) view returns (bytes32) fnHash
+    ) internal returns (uint256 index, bytes32 newRoot) {
         // Cache read
         uint256 levels = self._zeros.length;
-        function(bytes32, bytes32) view returns (bytes32) fnHash = self._fnHash;
 
         // Get leaf index
         index = self._nextLeafIndex++;
@@ -123,7 +149,7 @@ library MerkleTree {
             }
 
             // Compute the current node hash by using the hash function
-            // with either the its sibling (side) or the zero value for that level.
+            // with either its sibling (side) or the zero value for that level.
             currentLevelHash = fnHash(
                 isLeft ? currentLevelHash : Arrays.unsafeAccess(self._sides, i).value,
                 isLeft ? Arrays.unsafeAccess(self._zeros, i).value : currentLevelHash

+ 1 - 1
docs/modules/ROOT/pages/erc4626.adoc

@@ -41,7 +41,7 @@ image::erc4626-mint.png[Minting shares]
 
 Symmetrically, if we focus on limiting our losses to a maximum of 0.5%, we need to get at least 200 shares. With the green exchange rate that requires just 20 tokens, but with the orange rate that requires 200000 tokens.
 
-We can clearly see that that the blue and green curves correspond to vaults that are safer than the yellow and orange curves.
+We can clearly see that the blue and green curves correspond to vaults that are safer than the yellow and orange curves.
 
 The idea of an inflation attack is that an attacker can donate assets to the vault to move the rate curve to the right, and make the vault unsafe.
 

+ 1 - 1
docs/modules/ROOT/pages/governance.adoc

@@ -20,7 +20,7 @@ The ERC-20 extension to keep track of votes and vote delegation is one such case
 
 === Governor & GovernorStorage
 
-An OpenZeppelin Governor contract is not interface-compatible with Compound's GovernorAlpha or Bravo. Even though events are fully compatible, proposal lifecycle functions (creation, execution, etc.) have different signatures that are meant to optimize storage use. Other functions from GovernorAlpha are Bravo are likewise not available. It’s possible to opt in some Bravo-like behavior by inheriting from the GovernorStorage module. This module provides proposal enumerability and alternate versions of the `queue`, `execute` and `cancel` function that only take the proposal id. This module reduces the calldata needed by some operations in exchange for an increased the storage footprint. This might be a good trade-off for some L2 chains. It also provides primitives for indexer-free frontends.
+An OpenZeppelin Governor contract is not interface-compatible with Compound's GovernorAlpha or Bravo. Even though events are fully compatible, proposal lifecycle functions (creation, execution, etc.) have different signatures that are meant to optimize storage use. Other functions from GovernorAlpha and Bravo are likewise not available. It’s possible to opt in some Bravo-like behavior by inheriting from the GovernorStorage module. This module provides proposal enumerability and alternate versions of the `queue`, `execute` and `cancel` function that only take the proposal id. This module reduces the calldata needed by some operations in exchange for an increased the storage footprint. This might be a good trade-off for some L2 chains. It also provides primitives for indexer-free frontends.
 
 Note that even with the use of this module, one important difference with Compound's GovernorBravo is the way that `proposalId`s are calculated. Governor uses the hash of the proposal parameters with the purpose of keeping its data off-chain by event indexing, while the original Bravo implementation uses sequential `proposalId`s.
 

+ 158 - 5
docs/modules/ROOT/pages/utilities.adoc

@@ -8,6 +8,10 @@ Here are some of the more popular ones.
 
 === Checking Signatures On-Chain
 
+At a high level, signatures are a set of cryptographic algorithms that allow for a _signer_ to prove himself owner of a _private key_ used to authorize a piece of information (generally a transaction or `UserOperation`). Natively, the EVM supports the Elliptic Curve Digital Signature Algorithm (https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm[ECDSA]) using the secp256k1 curve, however other signature algorithms such as P256 and RSA are supported.
+
+==== Ethereum Signatures (secp256k1)
+
 xref:api:utils.adoc#ECDSA[`ECDSA`] provides functions for recovering and managing Ethereum account ECDSA signatures. These are often generated via https://web3js.readthedocs.io/en/v1.7.3/web3-eth.html#sign[`web3.eth.sign`], and are a 65 byte array (of type `bytes` in Solidity) arranged the following way: `[[v (1)], [r (32)], [s (32)]]`.
 
 The data signer can be recovered with xref:api:utils.adoc#ECDSA-recover-bytes32-bytes-[`ECDSA.recover`], and its address compared to verify the signature. Most wallets will hash the data to sign and add the prefix `\x19Ethereum Signed Message:\n`, so when attempting to recover the signer of an Ethereum signed message hash, you'll want to use xref:api:utils.adoc#MessageHashUtils-toEthSignedMessageHash-bytes32-[`toEthSignedMessageHash`].
@@ -26,6 +30,73 @@ function _verify(bytes32 data, bytes memory signature, address account) internal
 
 WARNING: Getting signature verification right is not trivial: make sure you fully read and understand xref:api:utils.adoc#MessageHashUtils[`MessageHashUtils`]'s and xref:api:utils.adoc#ECDSA[`ECDSA`]'s documentation.
 
+==== 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.
+
+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.
+
+[source,solidity]
+----
+using P256 for bytes32;
+
+function _verify(
+    bytes32 data,
+    bytes32 r,
+    bytes32 s,
+    bytes32 qx,
+    bytes32 qy
+) internal pure returns (bool) {
+    return data.verify(data, r, s, qx, qy);
+}
+----
+
+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]
+----
+using P256 for bytes32;
+
+function _verify(
+    bytes32 data,
+    bytes32 r,
+    bytes32 s,
+    bytes32 qx,
+    bytes32 qy
+) internal pure returns (bool) {
+    // Will only call the precompile at address(0x100)
+    return data.verifyNative(data, r, s, qx, qy);
+}
+----
+
+IMPORTANT: The P256 library only allows for `s` values in the lower order of the curve (i.e. `s <= N/2`) to prevent malleability. In case your tooling produces signatures in both sides of the curve, consider flipping the `s` value to keep compatibility.
+
+==== 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]. 
+
+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.
+
+RSA signatures are known for being less efficient than elliptic curve signatures given the size of the keys, which are big compared to ECDSA keys with the same security level. Using plain RSA is considered unsafe, this is why the implementation uses the `EMSA-PKCS1-v1_5` encoding method from https://datatracker.ietf.org/doc/html/rfc8017[RFC8017] to include padding to the signature.
+
+To verify a signature using RSA, you can leverage the xref:api:utils.adoc#RSA[`RSA`] library that exposes a method for verifying RSA with the PKCS 1.5 standard:
+
+[source,solidity]
+----
+using RSA for bytes32;
+
+function _verify(
+    bytes32 data,
+    bytes memory signature,
+    bytes memory e,
+    bytes memory n
+) internal pure returns (bool) {
+    return data.pkcs1(signature, e, n);
+}
+----
+
+IMPORTANT: Always use keys of at least 2048 bits. Additionally, be aware that PKCS#1 v1.5 allows for replayability due to the possibility of arbitrary optional parameters. To prevent replay attacks, consider including an onchain nonce or unique identifier in the message.
+
 === 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.
@@ -118,19 +189,20 @@ Some use cases require more powerful data structures than arrays and mappings of
 - xref:api:utils.adoc#EnumerableSet[`EnumerableSet`]: A https://en.wikipedia.org/wiki/Set_(abstract_data_type)[set] with enumeration capabilities.
 - xref:api:utils.adoc#EnumerableMap[`EnumerableMap`]: A `mapping` variant with enumeration capabilities.
 - xref:api:utils.adoc#MerkleTree[`MerkleTree`]: An on-chain https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] with helper functions.
+- xref:api:utils.adoc#Heap.sol[`Heap`]: A 
 
 The `Enumerable*` structures are similar to mappings in that they store and remove elements in constant time and don't allow for repeated entries, but they also support _enumeration_, which means you can easily query all stored entries both on and off-chain.
 
 === Building a Merkle Tree
 
-Building an on-chain Merkle Tree allow developers to keep track of the history of roots in a decentralized manner. For these cases, the xref:api:utils.adoc#MerkleTree[`MerkleTree`] includes a predefined structure with functions to manipulate the tree (e.g. pushing values or resetting the tree).
+Building an on-chain Merkle Tree allows developers to keep track of the history of roots in a decentralized manner. For these cases, the xref:api:utils.adoc#MerkleTree[`MerkleTree`] includes a predefined structure with functions to manipulate the tree (e.g. pushing values or resetting the tree).
 
-The Merkle Tree does not keep track of the roots purposely, so that developers can choose their tracking mechanism. Setting up and using an Merkle Tree in Solidity is as simple as follows:
+The Merkle Tree does not keep track of the roots purposely, so that developers can choose their tracking mechanism. Setting up and using a Merkle Tree in Solidity is as simple as follows:
+
+NOTE: Functions are exposed without access control for demonstration purposes
 
 [source,solidity]
 ----
-// NOTE: Functions are exposed without access control for demonstration purposes
-
 using MerkleTree for MerkleTree.Bytes32PushTree;
 MerkleTree.Bytes32PushTree private _tree;
 
@@ -144,9 +216,90 @@ function push(bytes32 leaf) public /* onlyOwner */ {
 }
 ----
 
+The library also supports custom hashing functions, which can be passed as an extra parameter to the xref:api:utils.adoc#MerkleTree-push-struct-MerkleTree-Bytes32PushTree-bytes32-[`push`] and xref:api:utils.adoc#MerkleTree-setup-struct-MerkleTree-Bytes32PushTree-uint8-bytes32-[`setup`] functions.
+
+Using custom hashing functions is a sensitive operation. After setup, it requires to keep using the same hashing function for every new value pushed to the tree to avoid corrupting the tree. For this reason, it's a good practice to keep your hashing function static in your implementation contract as follows:
+
+[source,solidity]
+----
+using MerkleTree for MerkleTree.Bytes32PushTree;
+MerkleTree.Bytes32PushTree private _tree;
+
+function setup(uint8 _depth, bytes32 _zero) public /* onlyOwner */ {
+    root = _tree.setup(_depth, _zero, _hashFn);
+}
+
+function push(bytes32 leaf) public /* onlyOwner */ {
+    (uint256 leafIndex, bytes32 currentRoot) = _tree.push(leaf, _hashFn);
+    // Store the new root.
+}
+
+function _hashFn(bytes32 a, bytes32 b) internal view returns(bytes32) {
+    // Custom hash function implementation
+    // Kept as an internal implementation detail to 
+    // guarantee the same function is always used
+}
+---- 
+
+=== Using a Heap
+
+A https://en.wikipedia.org/wiki/Binary_heap[binary heap] is a data structure that always store the most important element at its peak and it can be used as a priority queue.
+
+To define what is most important in a heap, these frequently take comparator functions that tell the binary heap whether a value has more relevance than another.
+
+OpenZeppelin Contracts implements a Heap data structure with the properties of a binary heap. The heap uses the xref:api:utils.adoc#Comparators-lt-uint256-uint256-[`lt`] function by default but allows to customize its comparator.
+
+When using a custom comparator, it's recommended to wrap your function to avoid the possibility of mistakenly using a different comparator function:
+
+[source,solidity]
+----
+function pop(Uint256Heap storage self) internal returns (uint256) {
+    return pop(self, Comparators.gt);
+}
+
+function insert(Uint256Heap storage self, uint256 value) internal {
+    insert(self, value, Comparators.gt);
+}
+
+function replace(Uint256Heap storage self, uint256 newValue) internal returns (uint256) {
+    return replace(self, newValue, Comparators.gt);
+}
+----
+
+
 [[misc]]
 == Misc
 
+=== 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.
+
+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.
+
+The xref:api:utils.adoc#Packing[`Packing`] library is a set of utilities for packing values that fit in 32 bytes. The library includes 3 main functionalities:
+
+* Packing 2 `bytesXX` values
+* Extracting a packed `bytesXX` value from a `bytesYY`
+* Replacing a packed `bytesXX` value from a `bytesYY`
+
+With these primitives, one can build custom functions to create custom packed types. For example, suppose you need to pack an `address` of 20 bytes with a `bytes4` selector and an `uint64` time period:
+
+[source,solidity]
+----
+function _pack(address account, bytes4 selector, uint64 period) external pure returns (bytes32) {
+    bytes12 subpack = Packing.pack_4_8(selector, bytes8(period));
+    return Packing.pack_20_12(bytes20(account), subpack);
+}
+
+function _unpack(bytes32 pack) external pure returns (address, bytes4, uint64) {
+    return (
+        address(Packing.extract_32_20(pack, 0)),
+        Packing.extract_32_4(pack, 20),
+        uint64(Packing.extract_32_8(pack, 24))
+    );
+}
+----
+
 === Storage Slots
 
 Solidity allocates a storage pointer for each variable declared in a contract. However, there are cases when it's required to access storage pointers that can't be derived by using regular Solidity.
@@ -166,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 (UDVTs[https://docs.soliditylang.org/en/latest/types.html#user-defined-value-types]), which enables the same value types as in Solidity.
+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.
 
 [source,solidity]
 ----

+ 2 - 0
foundry.toml

@@ -1,6 +1,8 @@
 [profile.default]
 solc_version = '0.8.24'
 evm_version = 'cancun'
+optimizer = true
+optimizer-runs = 200
 src = 'contracts'
 out = 'out'
 libs = ['node_modules', 'lib']

+ 1 - 1
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.12
+halmos==0.1.14

+ 19 - 21
hardhat.config.js

@@ -1,12 +1,12 @@
 /// ENVVAR
-// - COMPILE_VERSION:   compiler version (default: 0.8.20)
-// - SRC:               contracts folder to compile (default: contracts)
-// - COMPILE_MODE:      production modes enables optimizations (default: development)
-// - IR:                enable IR compilation (default: false)
-// - COVERAGE:          enable coverage report
-// - ENABLE_GAS_REPORT: enable gas report
-// - COINMARKETCAP:     coinmarkercat api key for USD value in gas report
-// - CI:                output gas report to file instead of stdout
+// - COMPILER:      compiler version (default: 0.8.24)
+// - SRC:           contracts folder to compile (default: contracts)
+// - RUNS:          number of optimization runs (default: 200)
+// - IR:            enable IR compilation (default: false)
+// - COVERAGE:      enable coverage report (default: false)
+// - GAS:           enable gas report (default: false)
+// - COINMARKETCAP: coinmarketcap api key for USD value in gas report
+// - CI:            output gas report to file instead of stdout
 
 const fs = require('fs');
 const path = require('path');
@@ -25,11 +25,10 @@ const { argv } = require('yargs/yargs')()
       type: 'string',
       default: 'contracts',
     },
-    mode: {
-      alias: 'compileMode',
-      type: 'string',
-      choices: ['production', 'development'],
-      default: 'development',
+    runs: {
+      alias: 'optimizationRuns',
+      type: 'number',
+      default: 200,
     },
     ir: {
       alias: 'enableIR',
@@ -69,9 +68,6 @@ for (const f of fs.readdirSync(path.join(__dirname, 'hardhat'))) {
   require(path.join(__dirname, 'hardhat', f));
 }
 
-const withOptimizations = argv.gas || argv.coverage || argv.compileMode === 'production';
-const allowUnlimitedContractSize = argv.gas || argv.coverage || argv.compileMode === 'development';
-
 /**
  * @type import('hardhat/config').HardhatUserConfig
  */
@@ -80,11 +76,11 @@ module.exports = {
     version: argv.compiler,
     settings: {
       optimizer: {
-        enabled: withOptimizations,
-        runs: 200,
+        enabled: true,
+        runs: argv.runs,
       },
       evmVersion: argv.evm,
-      viaIR: withOptimizations && argv.ir,
+      viaIR: argv.ir,
       outputSelection: { '*': { '*': ['storageLayout'] } },
     },
   },
@@ -94,7 +90,7 @@ module.exports = {
       'initcode-size': 'off',
     },
     '*': {
-      'code-size': withOptimizations,
+      'code-size': true,
       'unused-param': !argv.coverage, // coverage causes unused-param warnings
       'transient-storage': false,
       default: 'error',
@@ -103,7 +99,9 @@ module.exports = {
   networks: {
     hardhat: {
       hardfork: argv.evm,
-      allowUnlimitedContractSize,
+      // Exposed contracts often exceed the maximum contract size. For normal contract,
+      // we rely on the `code-size` compiler warning, that will cause a compilation error.
+      allowUnlimitedContractSize: true,
       initialBaseFeePerGas: argv.coverage ? 0 : undefined,
     },
   },

+ 168 - 129
package-lock.json

@@ -17,14 +17,14 @@
         "@nomicfoundation/hardhat-ethers": "^3.0.4",
         "@nomicfoundation/hardhat-network-helpers": "^1.0.3",
         "@openzeppelin/docs-utils": "^0.1.5",
-        "@openzeppelin/merkle-tree": "^1.0.6",
+        "@openzeppelin/merkle-tree": "^1.0.7",
         "@openzeppelin/upgrade-safe-transpiler": "^0.3.32",
         "@openzeppelin/upgrades-core": "^1.20.6",
         "chai": "^4.2.0",
         "eslint": "^8.30.0",
         "eslint-config-prettier": "^9.0.0",
         "ethers": "^6.7.1",
-        "glob": "^10.3.5",
+        "glob": "^11.0.0",
         "graphlib": "^2.1.8",
         "hardhat": "^2.22.2",
         "hardhat-exposed": "^0.3.15",
@@ -35,7 +35,7 @@
         "p-limit": "^3.1.0",
         "prettier": "^3.0.0",
         "prettier-plugin-solidity": "^1.1.0",
-        "rimraf": "^5.0.1",
+        "rimraf": "^6.0.0",
         "semver": "^7.3.5",
         "solhint": "^5.0.0",
         "solhint-plugin-openzeppelin": "file:scripts/solhint-custom",
@@ -1990,70 +1990,15 @@
       }
     },
     "node_modules/@openzeppelin/merkle-tree": {
-      "version": "1.0.6",
-      "resolved": "https://registry.npmjs.org/@openzeppelin/merkle-tree/-/merkle-tree-1.0.6.tgz",
-      "integrity": "sha512-cGWOb2WBWbJhqvupzxjnKAwGLxxAEYPg51sk76yZ5nVe5D03mw7Vx5yo8llaIEqYhP5O39M8QlrNWclgLfKVrA==",
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/@openzeppelin/merkle-tree/-/merkle-tree-1.0.7.tgz",
+      "integrity": "sha512-i93t0YYv6ZxTCYU3CdO5Q+DXK0JH10A4dCBOMlzYbX+ujTXm+k1lXiEyVqmf94t3sqmv8sm/XT5zTa0+efnPgQ==",
       "dev": true,
       "dependencies": {
         "@ethersproject/abi": "^5.7.0",
-        "ethereum-cryptography": "^1.1.2"
-      }
-    },
-    "node_modules/@openzeppelin/merkle-tree/node_modules/@noble/hashes": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.2.0.tgz",
-      "integrity": "sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://paulmillr.com/funding/"
-        }
-      ]
-    },
-    "node_modules/@openzeppelin/merkle-tree/node_modules/@scure/bip32": {
-      "version": "1.1.5",
-      "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.1.5.tgz",
-      "integrity": "sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://paulmillr.com/funding/"
-        }
-      ],
-      "dependencies": {
-        "@noble/hashes": "~1.2.0",
-        "@noble/secp256k1": "~1.7.0",
-        "@scure/base": "~1.1.0"
-      }
-    },
-    "node_modules/@openzeppelin/merkle-tree/node_modules/@scure/bip39": {
-      "version": "1.1.1",
-      "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.1.tgz",
-      "integrity": "sha512-t+wDck2rVkh65Hmv280fYdVdY25J9YeEUIgn2LG1WM6gxFkGzcksoDiUkWVpVp3Oex9xGC68JU2dSbUfwZ2jPg==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://paulmillr.com/funding/"
-        }
-      ],
-      "dependencies": {
-        "@noble/hashes": "~1.2.0",
-        "@scure/base": "~1.1.0"
-      }
-    },
-    "node_modules/@openzeppelin/merkle-tree/node_modules/ethereum-cryptography": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-1.2.0.tgz",
-      "integrity": "sha512-6yFQC9b5ug6/17CQpCyE3k9eKBMdhyVjzUy1WkiuY/E4vj/SXDBbCw8QEIaXqf0Mf2SnY6RmpDcwlUmBSS0EJw==",
-      "dev": true,
-      "dependencies": {
-        "@noble/hashes": "1.2.0",
-        "@noble/secp256k1": "1.7.1",
-        "@scure/bip32": "1.1.5",
-        "@scure/bip39": "1.1.1"
+        "@ethersproject/bytes": "^5.7.0",
+        "@ethersproject/constants": "^5.7.0",
+        "@ethersproject/keccak256": "^5.7.0"
       }
     },
     "node_modules/@openzeppelin/upgrade-safe-transpiler": {
@@ -2987,9 +2932,9 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.6.8",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
-      "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz",
+      "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==",
       "dev": true,
       "dependencies": {
         "follow-redirects": "^1.15.6",
@@ -3171,12 +3116,12 @@
       }
     },
     "node_modules/braces": {
-      "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
-      "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
       "dev": true,
       "dependencies": {
-        "fill-range": "^7.0.1"
+        "fill-range": "^7.1.1"
       },
       "engines": {
         "node": ">=8"
@@ -4948,9 +4893,9 @@
       }
     },
     "node_modules/fill-range": {
-      "version": "7.0.1",
-      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
-      "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
       "dev": true,
       "dependencies": {
         "to-regex-range": "^5.0.1"
@@ -5281,22 +5226,24 @@
       }
     },
     "node_modules/glob": {
-      "version": "10.3.10",
-      "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz",
-      "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==",
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz",
+      "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "foreground-child": "^3.1.0",
-        "jackspeak": "^2.3.5",
-        "minimatch": "^9.0.1",
-        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0",
-        "path-scurry": "^1.10.1"
+        "jackspeak": "^4.0.1",
+        "minimatch": "^10.0.0",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^2.0.0"
       },
       "bin": {
         "glob": "dist/esm/bin.mjs"
       },
       "engines": {
-        "node": ">=16 || 14 >=14.17"
+        "node": "20 || >=22"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
@@ -5319,34 +5266,27 @@
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
       "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "balanced-match": "^1.0.0"
       }
     },
     "node_modules/glob/node_modules/minimatch": {
-      "version": "9.0.3",
-      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
-      "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
+      "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "brace-expansion": "^2.0.1"
       },
       "engines": {
-        "node": ">=16 || 14 >=14.17"
+        "node": "20 || >=22"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/glob/node_modules/minipass": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz",
-      "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==",
-      "dev": true,
-      "engines": {
-        "node": ">=16 || 14 >=14.17"
-      }
-    },
     "node_modules/global-modules": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
@@ -5709,6 +5649,16 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1"
       }
     },
+    "node_modules/hardhat-gas-reporter/node_modules/brace-expansion": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
+      "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
     "node_modules/hardhat-gas-reporter/node_modules/chalk": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -5770,6 +5720,27 @@
         "@scure/bip39": "1.2.2"
       }
     },
+    "node_modules/hardhat-gas-reporter/node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/hardhat-gas-reporter/node_modules/has-flag": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -5788,6 +5759,29 @@
         "node": ">=8"
       }
     },
+    "node_modules/hardhat-gas-reporter/node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
+    "node_modules/hardhat-gas-reporter/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/hardhat-gas-reporter/node_modules/markdown-table": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz",
@@ -5801,6 +5795,39 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/hardhat-gas-reporter/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/hardhat-gas-reporter/node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/hardhat-gas-reporter/node_modules/string-width": {
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -6789,15 +6816,16 @@
       }
     },
     "node_modules/jackspeak": {
-      "version": "2.3.6",
-      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
-      "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz",
+      "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==",
       "dev": true,
+      "license": "BlueOak-1.0.0",
       "dependencies": {
         "@isaacs/cliui": "^8.0.2"
       },
       "engines": {
-        "node": ">=14"
+        "node": "20 || >=22"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
@@ -7128,6 +7156,16 @@
       "integrity": "sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==",
       "dev": true
     },
+    "node_modules/lru-cache": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz",
+      "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
     "node_modules/make-error": {
       "version": "1.3.6",
       "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
@@ -7322,6 +7360,16 @@
         "node": ">= 6"
       }
     },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
     "node_modules/mixme": {
       "version": "0.5.9",
       "resolved": "https://registry.npmjs.org/mixme/-/mixme-0.5.9.tgz",
@@ -7995,6 +8043,13 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz",
+      "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0"
+    },
     "node_modules/parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -8059,39 +8114,22 @@
       "dev": true
     },
     "node_modules/path-scurry": {
-      "version": "1.10.1",
-      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz",
-      "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==",
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
+      "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
       "dev": true,
+      "license": "BlueOak-1.0.0",
       "dependencies": {
-        "lru-cache": "^9.1.1 || ^10.0.0",
-        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
       },
       "engines": {
-        "node": ">=16 || 14 >=14.17"
+        "node": "20 || >=22"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"
       }
     },
-    "node_modules/path-scurry/node_modules/lru-cache": {
-      "version": "10.0.1",
-      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz",
-      "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==",
-      "dev": true,
-      "engines": {
-        "node": "14 || >=16.14"
-      }
-    },
-    "node_modules/path-scurry/node_modules/minipass": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz",
-      "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==",
-      "dev": true,
-      "engines": {
-        "node": ">=16 || 14 >=14.17"
-      }
-    },
     "node_modules/path-type": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -8670,18 +8708,19 @@
       }
     },
     "node_modules/rimraf": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.1.tgz",
-      "integrity": "sha512-OfFZdwtd3lZ+XZzYP/6gTACubwFcHdLRqS9UX3UwpU2dnGQYkPFISRwvM3w9IiB2w7bW5qGo/uAwE4SmXXSKvg==",
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.0.tgz",
+      "integrity": "sha512-u+yqhM92LW+89cxUQK0SRyvXYQmyuKHx0jkx4W7KfwLGLqJnQM5031Uv1trE4gB9XEXBM/s6MxKlfW95IidqaA==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
-        "glob": "^10.2.5"
+        "glob": "^11.0.0"
       },
       "bin": {
-        "rimraf": "dist/cjs/src/bin.js"
+        "rimraf": "dist/esm/bin.mjs"
       },
       "engines": {
-        "node": ">=14"
+        "node": "20 || >=22"
       },
       "funding": {
         "url": "https://github.com/sponsors/isaacs"

+ 5 - 5
package.json

@@ -10,10 +10,10 @@
   "scripts": {
     "compile": "hardhat compile",
     "compile:harnesses": "env SRC=./certora/harnesses hardhat compile",
-    "coverage": "env COVERAGE=true hardhat coverage",
+    "coverage": "scripts/checks/coverage.sh",
     "docs": "npm run prepare-docs && oz-docs",
     "docs:watch": "oz-docs watch contracts docs/templates docs/config.js",
-    "prepare": "git config --local core.hooksPath .githooks",
+    "prepare": "scripts/prepare.sh",
     "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",
@@ -58,14 +58,14 @@
     "@nomicfoundation/hardhat-ethers": "^3.0.4",
     "@nomicfoundation/hardhat-network-helpers": "^1.0.3",
     "@openzeppelin/docs-utils": "^0.1.5",
-    "@openzeppelin/merkle-tree": "^1.0.6",
+    "@openzeppelin/merkle-tree": "^1.0.7",
     "@openzeppelin/upgrade-safe-transpiler": "^0.3.32",
     "@openzeppelin/upgrades-core": "^1.20.6",
     "chai": "^4.2.0",
     "eslint": "^8.30.0",
     "eslint-config-prettier": "^9.0.0",
     "ethers": "^6.7.1",
-    "glob": "^10.3.5",
+    "glob": "^11.0.0",
     "graphlib": "^2.1.8",
     "hardhat": "^2.22.2",
     "hardhat-exposed": "^0.3.15",
@@ -76,7 +76,7 @@
     "p-limit": "^3.1.0",
     "prettier": "^3.0.0",
     "prettier-plugin-solidity": "^1.1.0",
-    "rimraf": "^5.0.1",
+    "rimraf": "^6.0.0",
     "semver": "^7.3.5",
     "solhint": "^5.0.0",
     "solhint-plugin-openzeppelin": "file:scripts/solhint-custom",

+ 18 - 0
scripts/checks/coverage.sh

@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+export COVERAGE=true
+export FOUNDRY_FUZZ_RUNS=10
+
+# Hardhat coverage
+hardhat coverage
+
+if [ "${CI:-"false"}" == "true" ]; then
+  # Foundry coverage
+  forge coverage --report lcov --ir-minimum
+  # Remove zero hits
+  sed -i '/,0/d' lcov.info
+fi
+
+# Reports are then uploaded to Codecov automatically by workflow, and merged.

+ 9 - 4
scripts/generate/run.js

@@ -1,6 +1,6 @@
 #!/usr/bin/env node
 
-const cp = require('child_process');
+// const cp = require('child_process');
 const fs = require('fs');
 const path = require('path');
 const format = require('./format-lines');
@@ -23,22 +23,25 @@ function generateFromTemplate(file, template, outputPrefix = '') {
     ...(version ? [version + ` (${file})`] : []),
     `// This file was procedurally generated from ${input}.`,
     '',
-    require(template),
+    require(template).trimEnd(),
   );
 
   fs.writeFileSync(output, content);
-  cp.execFileSync('prettier', ['--write', output]);
+  // cp.execFileSync('prettier', ['--write', output]);
 }
 
 // Contracts
 for (const [file, template] of Object.entries({
+  'utils/cryptography/MerkleProof.sol': './templates/MerkleProof.js',
   'utils/math/SafeCast.sol': './templates/SafeCast.js',
+  'utils/structs/Checkpoints.sol': './templates/Checkpoints.js',
   'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js',
   'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js',
-  'utils/structs/Checkpoints.sol': './templates/Checkpoints.js',
+  'utils/structs/Heap.sol': './templates/Heap.js',
   'utils/SlotDerivation.sol': './templates/SlotDerivation.js',
   'utils/StorageSlot.sol': './templates/StorageSlot.js',
   'utils/Arrays.sol': './templates/Arrays.js',
+  'utils/Packing.sol': './templates/Packing.js',
   'mocks/StorageSlotMock.sol': './templates/StorageSlotMock.js',
 })) {
   generateFromTemplate(file, template, './contracts/');
@@ -47,6 +50,8 @@ 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',
 })) {
   generateFromTemplate(file, template, './test/');

+ 84 - 85
scripts/generate/templates/Arrays.js

@@ -5,6 +5,7 @@ const { TYPES } = require('./Arrays.opts');
 const header = `\
 pragma solidity ^0.8.20;
 
+import {Comparators} from "./Comparators.sol";
 import {SlotDerivation} from "./SlotDerivation.sol";
 import {StorageSlot} from "./StorageSlot.sol";
 import {Math} from "./math/Math.sol";
@@ -15,39 +16,41 @@ import {Math} from "./math/Math.sol";
 `;
 
 const sort = type => `\
-    /**
-     * @dev Sort an array of ${type} (in memory) following the provided comparator function.
-     *
-     * This function does the sorting "in place", meaning that it overrides the input. The object is returned for
-     * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.
-     *
-     * NOTE: this function's cost is \`O(n · log(n))\` in average and \`O(n²)\` in the worst case, with n the length of the
-     * array. Using it in view functions that are executed through \`eth_call\` is safe, but one should be very careful
-     * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may
-     * consume more gas than is available in a block, leading to potential DoS.
-     */
-    function sort(
-        ${type}[] memory array,
-        function(${type}, ${type}) pure returns (bool) comp
-    ) internal pure returns (${type}[] memory) {
-        ${
-          type === 'bytes32'
-            ? '_quickSort(_begin(array), _end(array), comp);'
-            : 'sort(_castToBytes32Array(array), _castToBytes32Comp(comp));'
-        }
-        return array;
+/**
+ * @dev Sort an array of ${type} (in memory) following the provided comparator function.
+ *
+ * This function does the sorting "in place", meaning that it overrides the input. The object is returned for
+ * convenience, but that returned value can be discarded safely if the caller has a memory pointer to the array.
+ *
+ * NOTE: this function's cost is \`O(n · log(n))\` in average and \`O(n²)\` in the worst case, with n the length of the
+ * array. Using it in view functions that are executed through \`eth_call\` is safe, but one should be very careful
+ * when executing this as part of a transaction. If the array being sorted is too large, the sort operation may
+ * consume more gas than is available in a block, leading to potential DoS.
+ *
+ * IMPORTANT: Consider memory side-effects when using custom comparator functions that access memory in an unsafe way.
+ */
+function sort(
+    ${type}[] memory array,
+    function(${type}, ${type}) pure returns (bool) comp
+) internal pure returns (${type}[] memory) {
+    ${
+      type === 'uint256'
+        ? '_quickSort(_begin(array), _end(array), comp);'
+        : 'sort(_castToUint256Array(array), _castToUint256Comp(comp));'
     }
+    return array;
+}
 
-    /**
-     * @dev Variant of {sort} that sorts an array of ${type} in increasing order.
-     */
-    function sort(${type}[] memory array) internal pure returns (${type}[] memory) {
-        ${type === 'bytes32' ? 'sort(array, _defaultComp);' : 'sort(_castToBytes32Array(array), _defaultComp);'}
-        return array;
-    }
+/**
+ * @dev Variant of {sort} that sorts an array of ${type} in increasing order.
+ */
+function sort(${type}[] memory array) internal pure returns (${type}[] memory) {
+    ${type === 'uint256' ? 'sort(array, Comparators.lt);' : 'sort(_castToUint256Array(array), Comparators.lt);'}
+    return array;
+}
 `;
 
-const quickSort = `
+const quickSort = `\
 /**
  * @dev Performs a quick sort of a segment of memory. The segment sorted starts at \`begin\` (inclusive), and stops
  * at end (exclusive). Sorting follows the \`comp\` comparator.
@@ -57,12 +60,12 @@ const quickSort = `
  * IMPORTANT: Memory locations between \`begin\` and \`end\` are not validated/zeroed. This function should
  * be used only if the limits are within a memory array.
  */
-function _quickSort(uint256 begin, uint256 end, function(bytes32, bytes32) pure returns (bool) comp) private pure {
+function _quickSort(uint256 begin, uint256 end, function(uint256, uint256) pure returns (bool) comp) private pure {
     unchecked {
         if (end - begin < 0x40) return;
 
         // Use first element as pivot
-        bytes32 pivot = _mload(begin);
+        uint256 pivot = _mload(begin);
         // Position where the pivot should be at the end of the loop
         uint256 pos = begin;
 
@@ -84,9 +87,8 @@ function _quickSort(uint256 begin, uint256 end, function(bytes32, bytes32) pure
 /**
  * @dev Pointer to the memory location of the first element of \`array\`.
  */
-function _begin(bytes32[] memory array) private pure returns (uint256 ptr) {
-    /// @solidity memory-safe-assembly
-    assembly {
+function _begin(uint256[] memory array) private pure returns (uint256 ptr) {
+    assembly ("memory-safe") {
         ptr := add(array, 0x20)
     }
 }
@@ -95,16 +97,16 @@ function _begin(bytes32[] memory array) private pure returns (uint256 ptr) {
  * @dev Pointer to the memory location of the first memory word (32bytes) after \`array\`. This is the memory word
  * that comes just after the last element of the array.
  */
-function _end(bytes32[] memory array) private pure returns (uint256 ptr) {
+function _end(uint256[] memory array) private pure returns (uint256 ptr) {
     unchecked {
         return _begin(array) + array.length * 0x20;
     }
 }
 
 /**
- * @dev Load memory word (as a bytes32) at location \`ptr\`.
+ * @dev Load memory word (as a uint256) at location \`ptr\`.
  */
-function _mload(uint256 ptr) private pure returns (bytes32 value) {
+function _mload(uint256 ptr) private pure returns (uint256 value) {
     assembly {
         value := mload(ptr)
     }
@@ -123,34 +125,27 @@ function _swap(uint256 ptr1, uint256 ptr2) private pure {
 }
 `;
 
-const defaultComparator = `
-    /// @dev Comparator for sorting arrays in increasing order.
-    function _defaultComp(bytes32 a, bytes32 b) private pure returns (bool) {
-        return a < b;
-    }
-`;
-
 const castArray = type => `\
-    /// @dev Helper: low level cast ${type} memory array to uint256 memory array
-    function _castToBytes32Array(${type}[] memory input) private pure returns (bytes32[] memory output) {
-        assembly {
-            output := input
-        }
+/// @dev Helper: low level cast ${type} memory array to uint256 memory array
+function _castToUint256Array(${type}[] memory input) private pure returns (uint256[] memory output) {
+    assembly {
+        output := input
     }
+}
 `;
 
 const castComparator = type => `\
-    /// @dev Helper: low level cast ${type} comp function to bytes32 comp function
-    function _castToBytes32Comp(
-        function(${type}, ${type}) pure returns (bool) input
-    ) private pure returns (function(bytes32, bytes32) pure returns (bool) output) {
-        assembly {
-            output := input
-        }
+/// @dev Helper: low level cast ${type} comp function to uint256 comp function
+function _castToUint256Comp(
+    function(${type}, ${type}) pure returns (bool) input
+) private pure returns (function(uint256, uint256) pure returns (bool) output) {
+    assembly {
+        output := input
     }
+}
 `;
 
-const search = `
+const search = `\
 /**
  * @dev Searches a sorted \`array\` and returns the first index that contains
  * a value greater or equal to \`element\`. If no such index exists (i.e. all
@@ -319,24 +314,24 @@ function upperBoundMemory(uint256[] memory array, uint256 element) internal pure
 }
 `;
 
-const unsafeAccessStorage = type => `
+const unsafeAccessStorage = type => `\
 /**
-* @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check.
-*
-* WARNING: Only use if you are certain \`pos\` is lower than the array length.
-*/
+ * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check.
+ *
+ * WARNING: Only use if you are certain \`pos\` is lower than the array length.
+ */
 function unsafeAccess(${type}[] storage arr, uint256 pos) internal pure returns (StorageSlot.${capitalize(
   type,
 )}Slot storage) {
     bytes32 slot;
-    /// @solidity memory-safe-assembly
-    assembly {
+    assembly ("memory-safe") {
         slot := arr.slot
     }
     return slot.deriveArray().offset(pos).get${capitalize(type)}Slot();
-}`;
+}
+`;
 
-const unsafeAccessMemory = type => `
+const unsafeAccessMemory = type => `\
 /**
  * @dev Access an array in an "unsafe" way. Skips solidity "index-out-of-range" check.
  *
@@ -349,37 +344,41 @@ function unsafeMemoryAccess(${type}[] memory arr, uint256 pos) internal pure ret
 }
 `;
 
-const unsafeSetLength = type => `
+const unsafeSetLength = type => `\
 /**
  * @dev Helper to set the length of an dynamic array. Directly writing to \`.length\` is forbidden.
  *
  * WARNING: this does not clear elements if length is reduced, of initialize elements if length is increased.
  */
 function unsafeSetLength(${type}[] storage array, uint256 len) internal {
-    /// @solidity memory-safe-assembly
-    assembly {
+    assembly ("memory-safe") {
         sstore(array.slot, len)
     }
-}`;
+}
+`;
 
 // GENERATE
 module.exports = format(
   header.trimEnd(),
   'library Arrays {',
-  'using SlotDerivation for bytes32;',
-  'using StorageSlot for bytes32;',
-  // sorting, comparator, helpers and internal
-  sort('bytes32'),
-  TYPES.filter(type => type !== 'bytes32').map(sort),
-  quickSort,
-  defaultComparator,
-  TYPES.filter(type => type !== 'bytes32').map(castArray),
-  TYPES.filter(type => type !== 'bytes32').map(castComparator),
-  // lookup
-  search,
-  // unsafe (direct) storage and memory access
-  TYPES.map(unsafeAccessStorage),
-  TYPES.map(unsafeAccessMemory),
-  TYPES.map(unsafeSetLength),
+  format(
+    [].concat(
+      'using SlotDerivation for bytes32;',
+      'using StorageSlot for bytes32;',
+      '',
+      // sorting, comparator, helpers and internal
+      sort('uint256'),
+      TYPES.filter(type => type !== 'uint256').map(sort),
+      quickSort,
+      TYPES.filter(type => type !== 'uint256').map(castArray),
+      TYPES.filter(type => type !== 'uint256').map(castComparator),
+      // lookup
+      search,
+      // unsafe (direct) storage and memory access
+      TYPES.map(unsafeAccessStorage),
+      TYPES.map(unsafeAccessMemory),
+      TYPES.map(unsafeSetLength),
+    ),
+  ).trimEnd(),
   '}',
 );

+ 23 - 36
scripts/generate/templates/Checkpoints.js

@@ -17,10 +17,10 @@ import {Math} from "../math/Math.sol";
 `;
 
 const errors = `\
-    /**
-     * @dev A value was attempted to be inserted on a past checkpoint.
-     */
-    error CheckpointUnorderedInsertion();
+/**
+ * @dev A value was attempted to be inserted on a past checkpoint.
+ */
+error CheckpointUnorderedInsertion();
 `;
 
 const template = opts => `\
@@ -37,15 +37,11 @@ struct ${opts.checkpointTypeName} {
  * @dev Pushes a (\`key\`, \`value\`) pair into a ${opts.historyTypeName} so that it is stored as the checkpoint.
  *
  * Returns previous value and new value.
- * 
+ *
  * 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}, ${opts.valueTypeName}) {
     return _insert(self.${opts.checkpointFieldName}, key, value);
 }
 
@@ -108,15 +104,7 @@ function latest(${opts.historyTypeName} storage self) internal view returns (${o
  * @dev Returns whether there is a checkpoint in the structure (i.e. it is not empty), and if so the key and value
  * in the most recent checkpoint.
  */
-function latestCheckpoint(${opts.historyTypeName} storage self)
-    internal
-    view
-    returns (
-        bool exists,
-        ${opts.keyTypeName} ${opts.keyFieldName},
-        ${opts.valueTypeName} ${opts.valueFieldName}
-    )
-{
+function latestCheckpoint(${opts.historyTypeName} storage self) internal view returns (bool exists, ${opts.keyTypeName} ${opts.keyFieldName}, ${opts.valueTypeName} ${opts.valueFieldName}) {
     uint256 pos = self.${opts.checkpointFieldName}.length;
     if (pos == 0) {
         return (false, 0, 0);
@@ -144,11 +132,7 @@ 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}, ${opts.valueTypeName}) {
     uint256 pos = self.length;
 
     if (pos > 0) {
@@ -163,7 +147,7 @@ function _insert(
 
         // Update or push new checkpoint
         if (lastKey == key) {
-            _unsafeAccess(self, pos - 1).${opts.valueFieldName} = value;
+            last.${opts.valueFieldName} = value;
         } else {
             self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
         }
@@ -175,7 +159,7 @@ function _insert(
 }
 
 /**
- * @dev Return the index of the last (most recent) checkpoint with key lower or equal than the search key, or \`high\`
+ * @dev Return the index of the first (oldest) checkpoint with key strictly bigger than the search key, or \`high\`
  * if there is none. \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive
  * \`high\`.
  *
@@ -199,9 +183,9 @@ function _upperBinaryLookup(
 }
 
 /**
- * @dev Return the index of the first (oldest) checkpoint with key is greater or equal than the search key, or
- * \`high\` if there is none. \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and
- * exclusive \`high\`.
+ * @dev Return the index of the first (oldest) checkpoint with key greater or equal than the search key, or \`high\`
+ * if there is none. \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive
+ * \`high\`.
  *
  * WARNING: \`high\` should not be greater than the array's length.
  */
@@ -225,11 +209,10 @@ function _lowerBinaryLookup(
 /**
  * @dev Access an element of the array without performing bounds check. The position is assumed to be within bounds.
  */
-function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos)
-    private
-    pure
-    returns (${opts.checkpointTypeName} storage result)
-{
+function _unsafeAccess(
+    ${opts.checkpointTypeName}[] storage self,
+    uint256 pos
+) private pure returns (${opts.checkpointTypeName} storage result) {
     assembly {
         mstore(0, self.slot)
         result.slot := add(keccak256(0, 0x20), pos)
@@ -242,7 +225,11 @@ function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos)
 module.exports = format(
   header.trimEnd(),
   'library Checkpoints {',
-  errors,
-  OPTS.flatMap(opts => template(opts)),
+  format(
+    [].concat(
+      errors,
+      OPTS.map(opts => template(opts)),
+    ),
+  ).trimEnd(),
   '}',
 );

+ 20 - 28
scripts/generate/templates/Checkpoints.t.js

@@ -22,18 +22,13 @@ uint8 internal constant _KEY_MAX_GAP = 64;
 Checkpoints.${opts.historyTypeName} internal _ckpts;
 
 // helpers
-function _bound${capitalize(opts.keyTypeName)}(
-    ${opts.keyTypeName} x,
-    ${opts.keyTypeName} min,
-    ${opts.keyTypeName} max
-) internal pure returns (${opts.keyTypeName}) {
+function _bound${capitalize(opts.keyTypeName)}(${opts.keyTypeName} x, ${opts.keyTypeName} min, ${
+  opts.keyTypeName
+} max) internal pure returns (${opts.keyTypeName}) {
     return SafeCast.to${capitalize(opts.keyTypeName)}(bound(uint256(x), uint256(min), uint256(max)));
 }
 
-function _prepareKeys(
-    ${opts.keyTypeName}[] memory keys,
-    ${opts.keyTypeName} maxSpread
-) internal pure {
+function _prepareKeys(${opts.keyTypeName}[] memory keys, ${opts.keyTypeName} maxSpread) internal pure {
     ${opts.keyTypeName} lastKey = 0;
     for (uint256 i = 0; i < keys.length; ++i) {
         ${opts.keyTypeName} key = _bound${capitalize(opts.keyTypeName)}(keys[i], lastKey, lastKey + maxSpread);
@@ -42,11 +37,7 @@ function _prepareKeys(
     }
 }
 
-function _assertLatestCheckpoint(
-    bool exist,
-    ${opts.keyTypeName} key,
-    ${opts.valueTypeName} value
-) internal {
+function _assertLatestCheckpoint(bool exist, ${opts.keyTypeName} key, ${opts.valueTypeName} value) internal {
     (bool _exist, ${opts.keyTypeName} _key, ${opts.valueTypeName} _value) = _ckpts.latestCheckpoint();
     assertEq(_exist, exist);
     assertEq(_key, key);
@@ -54,11 +45,9 @@ function _assertLatestCheckpoint(
 }
 
 // tests
-function testPush(
-    ${opts.keyTypeName}[] memory keys,
-    ${opts.valueTypeName}[] memory values,
-    ${opts.keyTypeName} pastKey
-) public {
+function testPush(${opts.keyTypeName}[] memory keys, ${opts.valueTypeName}[] memory values, ${
+  opts.keyTypeName
+} pastKey) public {
     vm.assume(values.length > 0 && values.length <= keys.length);
     _prepareKeys(keys, _KEY_MAX_GAP);
 
@@ -71,7 +60,7 @@ function testPush(
     for (uint256 i = 0; i < keys.length; ++i) {
         ${opts.keyTypeName} key = keys[i];
         ${opts.valueTypeName} value = values[i % values.length];
-        if (i > 0 && key == keys[i-1]) ++duplicates;
+        if (i > 0 && key == keys[i - 1]) ++duplicates;
 
         // push
         _ckpts.push(key, value);
@@ -95,14 +84,12 @@ function testPush(
 
 // used to test reverts
 function push(${opts.keyTypeName} key, ${opts.valueTypeName} value) external {
-  _ckpts.push(key, value);
+    _ckpts.push(key, value);
 }
 
-function testLookup(
-    ${opts.keyTypeName}[] memory keys,
-    ${opts.valueTypeName}[] memory values,
-    ${opts.keyTypeName} lookup
-) public {
+function testLookup(${opts.keyTypeName}[] memory keys, ${opts.valueTypeName}[] memory values, ${
+  opts.keyTypeName
+} lookup) public {
     vm.assume(values.length > 0 && values.length <= keys.length);
     _prepareKeys(keys, _KEY_MAX_GAP);
 
@@ -124,7 +111,7 @@ function testLookup(
             upper = value;
         }
         // find the first key that is not smaller than the lookup key
-        if (key >= lookup && (i == 0 || keys[i-1] < lookup)) {
+        if (key >= lookup && (i == 0 || keys[i - 1] < lookup)) {
             lowerKey = key;
         }
         if (key == lowerKey) {
@@ -142,5 +129,10 @@ function testLookup(
 // GENERATE
 module.exports = format(
   header,
-  ...OPTS.flatMap(opts => [`contract Checkpoints${opts.historyTypeName}Test is Test {`, [template(opts)], '}']),
+  ...OPTS.flatMap(opts => [
+    `contract Checkpoints${opts.historyTypeName}Test is Test {`,
+    [template(opts).trimEnd()],
+    '}',
+    '',
+  ]),
 );

Някои файлове не бяха показани, защото твърде много файлове са промени