Browse Source

Merge branch 'master' into renovate/npm-undici-vulnerability

Hadrien Croubois 7 months ago
parent
commit
fa666ea94f
35 changed files with 989 additions and 241 deletions
  1. 5 0
      .changeset/blue-nails-give.md
  2. 5 0
      .changeset/fair-pumpkins-compete.md
  3. 5 0
      .changeset/fast-coats-try.md
  4. 5 0
      .changeset/good-cameras-rush.md
  5. 5 0
      .changeset/gorgeous-apes-jam.md
  6. 5 0
      .changeset/quiet-shrimps-kiss.md
  7. 5 0
      .changeset/sixty-tips-wink.md
  8. 3 0
      FUNDING.json
  9. 9 9
      contracts/access/manager/AuthorityUtils.sol
  10. 1 1
      contracts/governance/TimelockController.sol
  11. 11 1
      contracts/proxy/utils/Initializable.sol
  12. 15 0
      contracts/utils/cryptography/MessageHashUtils.sol
  13. 109 37
      contracts/utils/math/Math.sol
  14. 95 0
      contracts/utils/structs/EnumerableMap.sol
  15. 64 0
      contracts/utils/structs/EnumerableSet.sol
  16. 0 1
      hardhat.config.js
  17. 15 0
      hardhat/common-contracts.js
  18. 1 1
      lib/erc4626-tests
  19. 1 1
      lib/forge-std
  20. 1 1
      lib/halmos-cheatcodes
  21. 5 1
      scripts/checks/coverage.sh
  22. 25 0
      scripts/generate/templates/EnumerableMap.js
  23. 44 0
      scripts/generate/templates/EnumerableSet.js
  24. 2 2
      scripts/set-max-old-space-size.sh
  25. 7 5
      test/helpers/enums.js
  26. 12 0
      test/helpers/precompiles.js
  27. 33 0
      test/utils/cryptography/MessageHashUtils.t.sol
  28. 34 5
      test/utils/cryptography/MessageHashUtils.test.js
  29. 21 7
      test/utils/cryptography/SignatureChecker.test.js
  30. 60 22
      test/utils/math/Math.t.sol
  31. 298 147
      test/utils/math/Math.test.js
  32. 43 0
      test/utils/structs/EnumerableMap.behavior.js
  33. 1 0
      test/utils/structs/EnumerableMap.test.js
  34. 43 0
      test/utils/structs/EnumerableSet.behavior.js
  35. 1 0
      test/utils/structs/EnumerableSet.test.js

+ 5 - 0
.changeset/blue-nails-give.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Math`: Add `add512`, `mul512` and `mulShr`.

+ 5 - 0
.changeset/fair-pumpkins-compete.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Math`: Add saturating arithmetic operations `saturatingAdd`, `saturatingSub` and `saturatingMul`.

+ 5 - 0
.changeset/fast-coats-try.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`Initializable`: Add `_initializableStorageSlot` function that returns a pointer to the storage struct. The function allows customizing with a custom storage slot with an `override`.

+ 5 - 0
.changeset/good-cameras-rush.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`EnumerableMap`: Add `clear` function to EnumerableMaps which deletes all entries in the map.

+ 5 - 0
.changeset/gorgeous-apes-jam.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`TimelockController`: Receive function is now virtual.

+ 5 - 0
.changeset/quiet-shrimps-kiss.md

@@ -0,0 +1,5 @@
+---
+"openzeppelin-solidity": patch
+---
+
+`MessageHashUtils`: Add `toDataWithIntendedValidatorHash(address, bytes32)`.

+ 5 - 0
.changeset/sixty-tips-wink.md

@@ -0,0 +1,5 @@
+---
+'openzeppelin-solidity': minor
+---
+
+`EnumerableSet`: Add `clear` function to EnumerableSets which deletes all values in the set.

+ 3 - 0
FUNDING.json

@@ -3,5 +3,8 @@
     "ethereum": {
       "ownedBy": "0xAeb37910f93486C85A1F8F994b67E8187554d664"
     }
+  },
+  "opRetro": {
+    "projectId": "0x939241afa4c4b9e1dda6b8250baa8f04fa8b0debce738cfd324c0b18f9926d25"
   }
 }

+ 9 - 9
contracts/access/manager/AuthorityUtils.sol

@@ -17,16 +17,16 @@ library AuthorityUtils {
         address target,
         bytes4 selector
     ) internal view returns (bool immediate, uint32 delay) {
-        (bool success, bytes memory data) = authority.staticcall(
-            abi.encodeCall(IAuthority.canCall, (caller, target, selector))
-        );
-        if (success) {
-            if (data.length >= 0x40) {
-                (immediate, delay) = abi.decode(data, (bool, uint32));
-            } else if (data.length >= 0x20) {
-                immediate = abi.decode(data, (bool));
+        bytes memory data = abi.encodeCall(IAuthority.canCall, (caller, target, selector));
+
+        assembly ("memory-safe") {
+            mstore(0x00, 0x00)
+            mstore(0x20, 0x00)
+
+            if staticcall(gas(), authority, add(data, 0x20), mload(data), 0x00, 0x40) {
+                immediate := mload(0x00)
+                delay := mload(0x20)
             }
         }
-        return (immediate, delay);
     }
 }

+ 1 - 1
contracts/governance/TimelockController.sol

@@ -152,7 +152,7 @@ contract TimelockController is AccessControl, ERC721Holder, ERC1155Holder {
     /**
      * @dev Contract might receive/hold ETH as part of the maintenance process.
      */
-    receive() external payable {}
+    receive() external payable virtual {}
 
     /**
      * @dev See {IERC165-supportsInterface}.

+ 11 - 1
contracts/proxy/utils/Initializable.sol

@@ -216,13 +216,23 @@ abstract contract Initializable {
         return _getInitializableStorage()._initializing;
     }
 
+    /**
+     * @dev Pointer to storage slot. Allows integrators to override it with a custom storage location.
+     *
+     * NOTE: Consider following the ERC-7201 formula to derive storage locations.
+     */
+    function _initializableStorageSlot() internal pure virtual returns (bytes32) {
+        return INITIALIZABLE_STORAGE;
+    }
+
     /**
      * @dev Returns a pointer to the storage namespace.
      */
     // solhint-disable-next-line var-name-mixedcase
     function _getInitializableStorage() private pure returns (InitializableStorage storage $) {
+        bytes32 slot = _initializableStorageSlot();
         assembly {
-            $.slot := INITIALIZABLE_STORAGE
+            $.slot := slot
         }
     }
 }

+ 15 - 0
contracts/utils/cryptography/MessageHashUtils.sol

@@ -63,6 +63,21 @@ library MessageHashUtils {
         return keccak256(abi.encodePacked(hex"19_00", validator, data));
     }
 
+    /**
+     * @dev Variant of {toDataWithIntendedValidatorHash-address-bytes} optimized for cases where `data` is a bytes32.
+     */
+    function toDataWithIntendedValidatorHash(
+        address validator,
+        bytes32 messageHash
+    ) internal pure returns (bytes32 digest) {
+        assembly ("memory-safe") {
+            mstore(0x00, hex"19_00")
+            mstore(0x02, shl(96, validator))
+            mstore(0x16, messageHash)
+            digest := keccak256(0x00, 0x36)
+        }
+    }
+
     /**
      * @dev Returns the keccak256 digest of an EIP-712 typed data (ERC-191 version `0x01`).
      *

+ 109 - 37
contracts/utils/math/Math.sol

@@ -17,14 +17,42 @@ library Math {
         Expand // Away from zero
     }
 
+    /**
+     * @dev Return the 512-bit addition of two uint256.
+     *
+     * The result is stored in two 256 variables such that sum = high * 2²⁵⁶ + low.
+     */
+    function add512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {
+        assembly ("memory-safe") {
+            low := add(a, b)
+            high := lt(low, a)
+        }
+    }
+
+    /**
+     * @dev Return the 512-bit multiplication of two uint256.
+     *
+     * The result is stored in two 256 variables such that product = high * 2²⁵⁶ + low.
+     */
+    function mul512(uint256 a, uint256 b) internal pure returns (uint256 high, uint256 low) {
+        // 512-bit multiply [high low] = x * y. Compute the product mod 2²⁵⁶ and mod 2²⁵⁶ - 1, then use
+        // the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
+        // variables such that product = high * 2²⁵⁶ + low.
+        assembly ("memory-safe") {
+            let mm := mulmod(a, b, not(0))
+            low := mul(a, b)
+            high := sub(sub(mm, low), lt(mm, low))
+        }
+    }
+
     /**
      * @dev Returns the addition of two unsigned integers, with an success flag (no overflow).
      */
     function tryAdd(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
         unchecked {
             uint256 c = a + b;
-            if (c < a) return (false, 0);
-            return (true, c);
+            success = c >= a;
+            result = c * SafeCast.toUint(success);
         }
     }
 
@@ -33,8 +61,9 @@ library Math {
      */
     function trySub(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
         unchecked {
-            if (b > a) return (false, 0);
-            return (true, a - b);
+            uint256 c = a - b;
+            success = c <= a;
+            result = c * SafeCast.toUint(success);
         }
     }
 
@@ -43,13 +72,14 @@ library Math {
      */
     function tryMul(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
         unchecked {
-            // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
-            // benefit is lost if 'b' is also tested.
-            // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
-            if (a == 0) return (true, 0);
             uint256 c = a * b;
-            if (c / a != b) return (false, 0);
-            return (true, c);
+            assembly ("memory-safe") {
+                // Only true when the multiplication doesn't overflow
+                // (c / a == b) || (a == 0)
+                success := or(eq(div(c, a), b), iszero(a))
+            }
+            // equivalent to: success ? c : 0
+            result = c * SafeCast.toUint(success);
         }
     }
 
@@ -58,8 +88,11 @@ library Math {
      */
     function tryDiv(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
         unchecked {
-            if (b == 0) return (false, 0);
-            return (true, a / b);
+            success = b > 0;
+            assembly ("memory-safe") {
+                // The `DIV` opcode returns zero when the denominator is 0.
+                result := div(a, b)
+            }
         }
     }
 
@@ -68,11 +101,38 @@ library Math {
      */
     function tryMod(uint256 a, uint256 b) internal pure returns (bool success, uint256 result) {
         unchecked {
-            if (b == 0) return (false, 0);
-            return (true, a % b);
+            success = b > 0;
+            assembly ("memory-safe") {
+                // The `MOD` opcode returns zero when the denominator is 0.
+                result := mod(a, b)
+            }
         }
     }
 
+    /**
+     * @dev Unsigned saturating addition, bounds to `2²⁵⁶ - 1` instead of overflowing.
+     */
+    function saturatingAdd(uint256 a, uint256 b) internal pure returns (uint256) {
+        (bool success, uint256 result) = tryAdd(a, b);
+        return ternary(success, result, type(uint256).max);
+    }
+
+    /**
+     * @dev Unsigned saturating subtraction, bounds to zero instead of overflowing.
+     */
+    function saturatingSub(uint256 a, uint256 b) internal pure returns (uint256) {
+        (, uint256 result) = trySub(a, b);
+        return result;
+    }
+
+    /**
+     * @dev Unsigned saturating multiplication, bounds to `2²⁵⁶ - 1` instead of overflowing.
+     */
+    function saturatingMul(uint256 a, uint256 b) internal pure returns (uint256) {
+        (bool success, uint256 result) = tryMul(a, b);
+        return ternary(success, result, type(uint256).max);
+    }
+
     /**
      * @dev Branchless ternary evaluation for `a ? b : c`. Gas costs are constant.
      *
@@ -143,26 +203,18 @@ 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
-            // 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
-            assembly {
-                let mm := mulmod(x, y, not(0))
-                prod1 := sub(sub(mm, prod0), lt(mm, prod0))
-            }
+            (uint256 high, uint256 low) = mul512(x, y);
 
             // Handle non-overflow cases, 256 by 256 division.
-            if (prod1 == 0) {
+            if (high == 0) {
                 // Solidity will revert if denominator == 0, unlike the div opcode on its own.
                 // The surrounding unchecked block does not change this fact.
                 // See https://docs.soliditylang.org/en/latest/control-structures.html#checked-or-unchecked-arithmetic.
-                return prod0 / denominator;
+                return low / denominator;
             }
 
             // Make sure the result is less than 2²⁵⁶. Also prevents denominator == 0.
-            if (denominator <= prod1) {
+            if (denominator <= high) {
                 Panic.panic(ternary(denominator == 0, Panic.DIVISION_BY_ZERO, Panic.UNDER_OVERFLOW));
             }
 
@@ -170,34 +222,34 @@ library Math {
             // 512 by 256 division.
             ///////////////////////////////////////////////
 
-            // Make division exact by subtracting the remainder from [prod1 prod0].
+            // Make division exact by subtracting the remainder from [high low].
             uint256 remainder;
-            assembly {
+            assembly ("memory-safe") {
                 // Compute remainder using mulmod.
                 remainder := mulmod(x, y, denominator)
 
                 // Subtract 256 bit number from 512 bit number.
-                prod1 := sub(prod1, gt(remainder, prod0))
-                prod0 := sub(prod0, remainder)
+                high := sub(high, gt(remainder, low))
+                low := sub(low, remainder)
             }
 
             // Factor powers of two out of denominator and compute largest power of two divisor of denominator.
             // Always >= 1. See https://cs.stackexchange.com/q/138556/92363.
 
             uint256 twos = denominator & (0 - denominator);
-            assembly {
+            assembly ("memory-safe") {
                 // Divide denominator by twos.
                 denominator := div(denominator, twos)
 
-                // Divide [prod1 prod0] by twos.
-                prod0 := div(prod0, twos)
+                // Divide [high low] by twos.
+                low := div(low, twos)
 
                 // Flip twos such that it is 2²⁵⁶ / twos. If twos is zero, then it becomes one.
                 twos := add(div(sub(0, twos), twos), 1)
             }
 
-            // Shift in bits from prod1 into prod0.
-            prod0 |= prod1 * twos;
+            // Shift in bits from high into low.
+            low |= high * twos;
 
             // Invert denominator mod 2²⁵⁶. Now that denominator is an odd number, it has an inverse modulo 2²⁵⁶ such
             // that denominator * inv ≡ 1 mod 2²⁵⁶. Compute the inverse by starting with a seed that is correct for
@@ -215,9 +267,9 @@ library Math {
 
             // Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
             // This will give us the correct result modulo 2²⁵⁶. Since the preconditions guarantee that the outcome is
-            // less than 2²⁵⁶, this is the final result. We don't need to compute the high bits of the result and prod1
+            // less than 2²⁵⁶, this is the final result. We don't need to compute the high bits of the result and high
             // is no longer required.
-            result = prod0 * inverse;
+            result = low * inverse;
             return result;
         }
     }
@@ -229,6 +281,26 @@ library Math {
         return mulDiv(x, y, denominator) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, denominator) > 0);
     }
 
+    /**
+     * @dev Calculates floor(x * y >> n) with full precision. Throws if result overflows a uint256.
+     */
+    function mulShr(uint256 x, uint256 y, uint8 n) internal pure returns (uint256 result) {
+        unchecked {
+            (uint256 high, uint256 low) = mul512(x, y);
+            if (high >= 1 << n) {
+                Panic.panic(Panic.UNDER_OVERFLOW);
+            }
+            return (high << (256 - n)) | (low >> n);
+        }
+    }
+
+    /**
+     * @dev Calculates x * y >> n with full precision, following the selected rounding direction.
+     */
+    function mulShr(uint256 x, uint256 y, uint8 n, Rounding rounding) internal pure returns (uint256) {
+        return mulShr(x, y, n) + SafeCast.toUint(unsignedRoundsUp(rounding) && mulmod(x, y, 1 << n) > 0);
+    }
+
     /**
      * @dev Calculate the modular multiplicative inverse of a number in Z/nZ.
      *

+ 95 - 0
contracts/utils/structs/EnumerableMap.sol

@@ -16,6 +16,7 @@ import {EnumerableSet} from "./EnumerableSet.sol";
  * - Entries are added, removed, and checked for existence in constant time
  * (O(1)).
  * - Entries are enumerated in O(n). No guarantees are made on the ordering.
+ * - Map can be cleared (all entries removed) in O(n).
  *
  * ```solidity
  * contract Example {
@@ -90,6 +91,20 @@ library EnumerableMap {
         return map._keys.remove(key);
     }
 
+    /**
+     * @dev Removes all the entries from a map. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(Bytes32ToBytes32Map storage map) internal {
+        uint256 len = length(map);
+        for (uint256 i = 0; i < len; ++i) {
+            delete map._values[map._keys.at(i)];
+        }
+        map._keys.clear();
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -185,6 +200,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(key));
     }
 
+    /**
+     * @dev Removes all the entries from a map. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(UintToUintMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -278,6 +303,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(key));
     }
 
+    /**
+     * @dev Removes all the entries from a map. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(UintToAddressMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -371,6 +406,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(key));
     }
 
+    /**
+     * @dev Removes all the entries from a map. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(UintToBytes32Map storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -464,6 +509,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(uint256(uint160(key))));
     }
 
+    /**
+     * @dev Removes all the entries from a map. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(AddressToUintMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -557,6 +612,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(uint256(uint160(key))));
     }
 
+    /**
+     * @dev Removes all the entries from a map. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(AddressToAddressMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -650,6 +715,16 @@ library EnumerableMap {
         return remove(map._inner, bytes32(uint256(uint160(key))));
     }
 
+    /**
+     * @dev Removes all the entries from a map. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(AddressToBytes32Map storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -743,6 +818,16 @@ library EnumerableMap {
         return remove(map._inner, key);
     }
 
+    /**
+     * @dev Removes all the entries from a map. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(Bytes32ToUintMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */
@@ -836,6 +921,16 @@ library EnumerableMap {
         return remove(map._inner, key);
     }
 
+    /**
+     * @dev Removes all the entries from a map. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(Bytes32ToAddressMap storage map) internal {
+        clear(map._inner);
+    }
+
     /**
      * @dev Returns true if the key is in the map. O(1).
      */

+ 64 - 0
contracts/utils/structs/EnumerableSet.sol

@@ -4,6 +4,7 @@
 
 pragma solidity ^0.8.20;
 
+import {Arrays} from "../Arrays.sol";
 import {Hashes} from "../cryptography/Hashes.sol";
 
 /**
@@ -16,6 +17,7 @@ import {Hashes} from "../cryptography/Hashes.sol";
  * - Elements are added, removed, and checked for existence in constant time
  * (O(1)).
  * - Elements are enumerated in O(n). No guarantees are made on the ordering.
+ * - Set can be cleared (all elements removed) in O(n).
  *
  * ```solidity
  * contract Example {
@@ -116,6 +118,20 @@ library EnumerableSet {
         }
     }
 
+    /**
+     * @dev Removes all the values from a set. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function _clear(Set storage set) private {
+        uint256 len = _length(set);
+        for (uint256 i = 0; i < len; ++i) {
+            delete set._positions[set._values[i]];
+        }
+        Arrays.unsafeSetLength(set._values, 0);
+    }
+
     /**
      * @dev Returns true if the value is in the set. O(1).
      */
@@ -182,6 +198,16 @@ library EnumerableSet {
         return _remove(set._inner, value);
     }
 
+    /**
+     * @dev Removes all the values from a set. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(Bytes32Set storage set) internal {
+        _clear(set._inner);
+    }
+
     /**
      * @dev Returns true if the value is in the set. O(1).
      */
@@ -255,6 +281,16 @@ library EnumerableSet {
         return _remove(set._inner, bytes32(uint256(uint160(value))));
     }
 
+    /**
+     * @dev Removes all the values from a set. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(AddressSet storage set) internal {
+        _clear(set._inner);
+    }
+
     /**
      * @dev Returns true if the value is in the set. O(1).
      */
@@ -328,6 +364,16 @@ library EnumerableSet {
         return _remove(set._inner, bytes32(value));
     }
 
+    /**
+     * @dev Removes all the values from a set. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(UintSet storage set) internal {
+        _clear(set._inner);
+    }
+
     /**
      * @dev Returns true if the value is in the set. O(1).
      */
@@ -442,6 +488,24 @@ library EnumerableSet {
         }
     }
 
+    /**
+     * @dev Removes all the values from a set. O(n).
+     *
+     * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+     * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
+     */
+    function clear(Bytes32x2Set storage self) internal {
+        bytes32[2][] storage v = self._values;
+
+        uint256 len = length(self);
+        for (uint256 i = 0; i < len; ++i) {
+            delete self._positions[_hash(v[i])];
+        }
+        assembly ("memory-safe") {
+            sstore(v.slot, 0)
+        }
+    }
+
     /**
      * @dev Returns true if the value is in the self. O(1).
      */

+ 0 - 1
hardhat.config.js

@@ -90,7 +90,6 @@ module.exports = {
       'initcode-size': 'off',
     },
     '*': {
-      'code-size': true,
       'unused-param': !argv.coverage, // coverage causes unused-param warnings
       'transient-storage': false,
       default: 'error',

+ 15 - 0
hardhat/common-contracts.js

@@ -6,6 +6,7 @@ const fs = require('fs');
 const path = require('path');
 
 const INSTANCES = {
+  // Entrypoint v0.7.0
   entrypoint: {
     address: '0x0000000071727De22E5E9d8BAf0edAc6f37da032',
     abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../test/bin/EntryPoint070.abi'), 'utf-8')),
@@ -16,6 +17,20 @@ const INSTANCES = {
     abi: JSON.parse(fs.readFileSync(path.resolve(__dirname, '../test/bin/SenderCreator070.abi'), 'utf-8')),
     bytecode: fs.readFileSync(path.resolve(__dirname, '../test/bin/SenderCreator070.bytecode'), 'hex'),
   },
+  // Arachnid's deterministic deployment proxy
+  // See: https://github.com/Arachnid/deterministic-deployment-proxy/tree/master
+  arachnidDeployer: {
+    address: '0x4e59b44847b379578588920cA78FbF26c0B4956C',
+    abi: [],
+    bytecode:
+      '0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3',
+  },
+  // Micah's deployer
+  micahDeployer: {
+    address: '0x7A0D94F55792C434d74a40883C6ed8545E406D12',
+    abi: [],
+    bytecode: '0x60003681823780368234f58015156014578182fd5b80825250506014600cf3',
+  },
 };
 
 task(TASK_TEST_SETUP_TEST_ENVIRONMENT).setAction((_, env, runSuper) =>

+ 1 - 1
lib/erc4626-tests

@@ -1 +1 @@
-Subproject commit 8b1d7c2ac248c33c3506b1bff8321758943c5e11
+Subproject commit 232ff9ba8194e406967f52ecc5cb52ed764209e9

+ 1 - 1
lib/forge-std

@@ -1 +1 @@
-Subproject commit 1eea5bae12ae557d589f9f0f0edae2faa47cb262
+Subproject commit 3b20d60d14b343ee4f908cb8079495c07f5e8981

+ 1 - 1
lib/halmos-cheatcodes

@@ -1 +1 @@
-Subproject commit c0d865508c0fee0a11b97732c5e90f9cad6b65a5
+Subproject commit 7328abe100445fc53885c21d0e713b95293cf14c

+ 5 - 1
scripts/checks/coverage.sh

@@ -14,7 +14,11 @@ if [ "${CI:-"false"}" == "true" ]; then
   # Foundry coverage
   forge coverage --report lcov --ir-minimum
   # Remove zero hits
-  sed -i '/,0/d' lcov.info
+  if [[ "$OSTYPE" == "darwin"* ]]; then
+    sed -i '' '/,0/d' lcov.info
+  else
+    sed -i '/,0/d' lcov.info
+  fi
 fi
 
 # Reports are then uploaded to Codecov automatically by workflow, and merged.

+ 25 - 0
scripts/generate/templates/EnumerableMap.js

@@ -17,6 +17,7 @@ import {EnumerableSet} from "./EnumerableSet.sol";
  * - Entries are added, removed, and checked for existence in constant time
  * (O(1)).
  * - Entries are enumerated in O(n). No guarantees are made on the ordering.
+ * - Map can be cleared (all entries removed) in O(n).
  *
  * \`\`\`solidity
  * contract Example {
@@ -91,6 +92,20 @@ function remove(Bytes32ToBytes32Map storage map, bytes32 key) internal returns (
     return map._keys.remove(key);
 }
 
+/**
+ * @dev Removes all the entries from a map. O(n).
+ *
+ * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+ * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block.
+ */
+function clear(Bytes32ToBytes32Map storage map) internal {
+    uint256 len = length(map);
+    for (uint256 i = 0; i < len; ++i) {
+        delete map._values[map._keys.at(i)];
+    }
+    map._keys.clear();
+}
+
 /**
  * @dev Returns true if the key is in the map. O(1).
  */
@@ -188,6 +203,16 @@ function remove(${name} storage map, ${keyType} key) internal returns (bool) {
     return remove(map._inner, ${toBytes32(keyType, 'key')});
 }
 
+/**
+ * @dev Removes all the entries from a map. O(n).
+ *
+ * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+ * function uncallable if the map grows to the point where clearing it consumes too much gas to fit in a block.
+ */
+function clear(${name} storage map) internal {
+    clear(map._inner);
+}
+
 /**
  * @dev Returns true if the key is in the map. O(1).
  */

+ 44 - 0
scripts/generate/templates/EnumerableSet.js

@@ -5,6 +5,7 @@ const { TYPES } = require('./EnumerableSet.opts');
 const header = `\
 pragma solidity ^0.8.20;
 
+import {Arrays} from "../Arrays.sol";
 import {Hashes} from "../cryptography/Hashes.sol";
 
 /**
@@ -17,6 +18,7 @@ import {Hashes} from "../cryptography/Hashes.sol";
  * - Elements are added, removed, and checked for existence in constant time
  * (O(1)).
  * - Elements are enumerated in O(n). No guarantees are made on the ordering.
+ * - Set can be cleared (all elements removed) in O(n).
  *
  * \`\`\`solidity
  * contract Example {
@@ -119,6 +121,20 @@ function _remove(Set storage set, bytes32 value) private returns (bool) {
     }
 }
 
+/**
+ * @dev Removes all the values from a set. O(n).
+ *
+ * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+ * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
+ */
+function _clear(Set storage set) private {
+    uint256 len = _length(set);
+    for (uint256 i = 0; i < len; ++i) {
+        delete set._positions[set._values[i]];
+    }
+    Arrays.unsafeSetLength(set._values, 0);
+}
+
 /**
  * @dev Returns true if the value is in the set. O(1).
  */
@@ -187,6 +203,16 @@ function remove(${name} storage set, ${type} value) internal returns (bool) {
     return _remove(set._inner, ${toBytes32(type, 'value')});
 }
 
+/**
+ * @dev Removes all the values from a set. O(n).
+ *
+ * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+ * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
+ */
+function clear(${name} storage set) internal {
+    _clear(set._inner);
+}
+
 /**
  * @dev Returns true if the value is in the set. O(1).
  */
@@ -303,6 +329,24 @@ function remove(${name} storage self, ${type} memory value) internal returns (bo
     }
 }
 
+/**
+ * @dev Removes all the values from a set. O(n).
+ *
+ * WARNING: Developers should keep in mind that this function has an unbounded cost and using it may render the
+ * function uncallable if the set grows to the point where clearing it consumes too much gas to fit in a block.
+ */
+function clear(${name} storage self) internal {
+    ${type}[] storage v = self._values;
+
+    uint256 len = length(self);
+    for (uint256 i = 0; i < len; ++i) {
+        delete self._positions[_hash(v[i])];
+    }
+    assembly ("memory-safe") {
+        sstore(v.slot, 0)
+    }
+}
+
 /**
  * @dev Returns true if the value is in the self. O(1).
  */

+ 2 - 2
scripts/set-max-old-space-size.sh

@@ -1,10 +1,10 @@
-#!/usr/bin/env bash
+#!/usr/bin/env sh
 
 # This script sets the node `--max-old-space-size` to 8192 if it is not set already.
 # All existing `NODE_OPTIONS` are retained as is.
 
 export NODE_OPTIONS="${NODE_OPTIONS:-}"
 
-if [[ $NODE_OPTIONS != *"--max-old-space-size"* ]]; then
+if [ "${NODE_OPTIONS##*--max-old-space-size*}" = "$NODE_OPTIONS" ]; then
   export NODE_OPTIONS="${NODE_OPTIONS} --max-old-space-size=8192"
 fi

+ 7 - 5
test/helpers/enums.js

@@ -1,12 +1,14 @@
-function Enum(...options) {
-  return Object.fromEntries(options.map((key, i) => [key, BigInt(i)]));
-}
+const { ethers } = require('ethers');
+
+const Enum = (...options) => Object.fromEntries(options.map((key, i) => [key, BigInt(i)]));
+const EnumTyped = (...options) => Object.fromEntries(options.map((key, i) => [key, ethers.Typed.uint8(i)]));
 
 module.exports = {
   Enum,
+  EnumTyped,
   ProposalState: Enum('Pending', 'Active', 'Canceled', 'Defeated', 'Succeeded', 'Queued', 'Expired', 'Executed'),
   VoteType: Object.assign(Enum('Against', 'For', 'Abstain'), { Parameters: 255n }),
-  Rounding: Enum('Floor', 'Ceil', 'Trunc', 'Expand'),
+  Rounding: EnumTyped('Floor', 'Ceil', 'Trunc', 'Expand'),
   OperationState: Enum('Unset', 'Waiting', 'Ready', 'Done'),
-  RevertType: Enum('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'),
+  RevertType: EnumTyped('None', 'RevertWithoutMessage', 'RevertWithMessage', 'RevertWithCustomError', 'Panic'),
 };

+ 12 - 0
test/helpers/precompiles.js

@@ -0,0 +1,12 @@
+module.exports = {
+  ecRecover: '0x0000000000000000000000000000000000000001',
+  SHA2_256: '0x0000000000000000000000000000000000000002',
+  RIPEMD_160: '0x0000000000000000000000000000000000000003',
+  identity: '0x0000000000000000000000000000000000000004',
+  modexp: '0x0000000000000000000000000000000000000005',
+  ecAdd: '0x0000000000000000000000000000000000000006',
+  ecMul: '0x0000000000000000000000000000000000000007',
+  ecPairing: '0x0000000000000000000000000000000000000008',
+  blake2f: '0x0000000000000000000000000000000000000009',
+  pointEvaluation: '0x000000000000000000000000000000000000000a',
+};

+ 33 - 0
test/utils/cryptography/MessageHashUtils.t.sol

@@ -0,0 +1,33 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.20;
+
+import {Test} from "forge-std/Test.sol";
+import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
+
+contract MessageHashUtilsTest is Test {
+    function testToDataWithIntendedValidatorHash(address validator, bytes memory data) external pure {
+        assertEq(
+            MessageHashUtils.toDataWithIntendedValidatorHash(validator, data),
+            MessageHashUtils.toDataWithIntendedValidatorHash(_dirty(validator), data)
+        );
+    }
+
+    function testToDataWithIntendedValidatorHash(address validator, bytes32 messageHash) external pure {
+        assertEq(
+            MessageHashUtils.toDataWithIntendedValidatorHash(validator, messageHash),
+            MessageHashUtils.toDataWithIntendedValidatorHash(_dirty(validator), messageHash)
+        );
+
+        assertEq(
+            MessageHashUtils.toDataWithIntendedValidatorHash(validator, messageHash),
+            MessageHashUtils.toDataWithIntendedValidatorHash(validator, abi.encodePacked(messageHash))
+        );
+    }
+
+    function _dirty(address input) private pure returns (address output) {
+        assembly ("memory-safe") {
+            output := or(input, shl(160, not(0)))
+        }
+    }
+}

+ 34 - 5
test/utils/cryptography/MessageHashUtils.test.js

@@ -19,14 +19,16 @@ describe('MessageHashUtils', function () {
       const message = ethers.randomBytes(32);
       const expectedHash = ethers.hashMessage(message);
 
-      expect(await this.mock.getFunction('$toEthSignedMessageHash(bytes32)')(message)).to.equal(expectedHash);
+      await expect(this.mock.getFunction('$toEthSignedMessageHash(bytes32)')(message)).to.eventually.equal(
+        expectedHash,
+      );
     });
 
     it('prefixes dynamic length data correctly', async function () {
       const message = ethers.randomBytes(128);
       const expectedHash = ethers.hashMessage(message);
 
-      expect(await this.mock.getFunction('$toEthSignedMessageHash(bytes)')(message)).to.equal(expectedHash);
+      await expect(this.mock.getFunction('$toEthSignedMessageHash(bytes)')(message)).to.eventually.equal(expectedHash);
     });
 
     it('version match for bytes32', async function () {
@@ -39,7 +41,20 @@ describe('MessageHashUtils', function () {
   });
 
   describe('toDataWithIntendedValidatorHash', function () {
-    it('returns the digest correctly', async function () {
+    it('returns the digest of `bytes32 messageHash` correctly', async function () {
+      const verifier = ethers.Wallet.createRandom().address;
+      const message = ethers.randomBytes(32);
+      const expectedHash = ethers.solidityPackedKeccak256(
+        ['string', 'address', 'bytes32'],
+        ['\x19\x00', verifier, message],
+      );
+
+      await expect(
+        this.mock.getFunction('$toDataWithIntendedValidatorHash(address,bytes32)')(verifier, message),
+      ).to.eventually.equal(expectedHash);
+    });
+
+    it('returns the digest of `bytes memory message` correctly', async function () {
       const verifier = ethers.Wallet.createRandom().address;
       const message = ethers.randomBytes(128);
       const expectedHash = ethers.solidityPackedKeccak256(
@@ -47,7 +62,21 @@ describe('MessageHashUtils', function () {
         ['\x19\x00', verifier, message],
       );
 
-      expect(await this.mock.$toDataWithIntendedValidatorHash(verifier, message)).to.equal(expectedHash);
+      await expect(
+        this.mock.getFunction('$toDataWithIntendedValidatorHash(address,bytes)')(verifier, message),
+      ).to.eventually.equal(expectedHash);
+    });
+
+    it('version match for bytes32', async function () {
+      const verifier = ethers.Wallet.createRandom().address;
+      const message = ethers.randomBytes(32);
+      const fixed = await this.mock.getFunction('$toDataWithIntendedValidatorHash(address,bytes)')(verifier, message);
+      const dynamic = await this.mock.getFunction('$toDataWithIntendedValidatorHash(address,bytes32)')(
+        verifier,
+        message,
+      );
+
+      expect(fixed).to.equal(dynamic);
     });
   });
 
@@ -62,7 +91,7 @@ describe('MessageHashUtils', function () {
       const structhash = ethers.randomBytes(32);
       const expectedHash = hashTypedData(domain, structhash);
 
-      expect(await this.mock.$toTypedDataHash(domainSeparator(domain), structhash)).to.equal(expectedHash);
+      await expect(this.mock.$toTypedDataHash(domainSeparator(domain), structhash)).to.eventually.equal(expectedHash);
     });
   });
 });

+ 21 - 7
test/utils/cryptography/SignatureChecker.test.js

@@ -2,6 +2,8 @@ const { ethers } = require('hardhat');
 const { expect } = require('chai');
 const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
 
+const precompile = require('../../helpers/precompiles');
+
 const TEST_MESSAGE = ethers.id('OpenZeppelin');
 const TEST_MESSAGE_HASH = ethers.hashMessage(TEST_MESSAGE);
 
@@ -25,15 +27,18 @@ describe('SignatureChecker (ERC1271)', function () {
 
   describe('EOA account', function () {
     it('with matching signer and signature', async function () {
-      expect(await this.mock.$isValidSignatureNow(this.signer, TEST_MESSAGE_HASH, this.signature)).to.be.true;
+      await expect(this.mock.$isValidSignatureNow(this.signer, TEST_MESSAGE_HASH, this.signature)).to.eventually.be
+        .true;
     });
 
     it('with invalid signer', async function () {
-      expect(await this.mock.$isValidSignatureNow(this.other, TEST_MESSAGE_HASH, this.signature)).to.be.false;
+      await expect(this.mock.$isValidSignatureNow(this.other, TEST_MESSAGE_HASH, this.signature)).to.eventually.be
+        .false;
     });
 
     it('with invalid signature', async function () {
-      expect(await this.mock.$isValidSignatureNow(this.signer, WRONG_MESSAGE_HASH, this.signature)).to.be.false;
+      await expect(this.mock.$isValidSignatureNow(this.signer, WRONG_MESSAGE_HASH, this.signature)).to.eventually.be
+        .false;
     });
   });
 
@@ -41,19 +46,28 @@ describe('SignatureChecker (ERC1271)', function () {
     for (const fn of ['isValidERC1271SignatureNow', 'isValidSignatureNow']) {
       describe(fn, function () {
         it('with matching signer and signature', async function () {
-          expect(await this.mock.getFunction(`$${fn}`)(this.wallet, TEST_MESSAGE_HASH, this.signature)).to.be.true;
+          await expect(this.mock.getFunction(`$${fn}`)(this.wallet, TEST_MESSAGE_HASH, this.signature)).to.eventually.be
+            .true;
         });
 
         it('with invalid signer', async function () {
-          expect(await this.mock.getFunction(`$${fn}`)(this.mock, TEST_MESSAGE_HASH, this.signature)).to.be.false;
+          await expect(this.mock.getFunction(`$${fn}`)(this.mock, TEST_MESSAGE_HASH, this.signature)).to.eventually.be
+            .false;
+        });
+
+        it('with identity precompile', async function () {
+          await expect(this.mock.getFunction(`$${fn}`)(precompile.identity, TEST_MESSAGE_HASH, this.signature)).to
+            .eventually.be.false;
         });
 
         it('with invalid signature', async function () {
-          expect(await this.mock.getFunction(`$${fn}`)(this.wallet, WRONG_MESSAGE_HASH, this.signature)).to.be.false;
+          await expect(this.mock.getFunction(`$${fn}`)(this.wallet, WRONG_MESSAGE_HASH, this.signature)).to.eventually
+            .be.false;
         });
 
         it('with malicious wallet', async function () {
-          expect(await this.mock.getFunction(`$${fn}`)(this.malicious, TEST_MESSAGE_HASH, this.signature)).to.be.false;
+          await expect(this.mock.getFunction(`$${fn}`)(this.malicious, TEST_MESSAGE_HASH, this.signature)).to.eventually
+            .be.false;
         });
       });
     }

+ 60 - 22
test/utils/math/Math.t.sol

@@ -11,6 +11,48 @@ contract MathTest is Test {
         assertEq(Math.ternary(f, a, b), f ? a : b);
     }
 
+    // ADD512 & MUL512
+    function testAdd512(uint256 a, uint256 b) public pure {
+        (uint256 high, uint256 low) = Math.add512(a, b);
+
+        // test against tryAdd
+        (bool success, uint256 result) = Math.tryAdd(a, b);
+        if (success) {
+            assertEq(high, 0);
+            assertEq(low, result);
+        } else {
+            assertEq(high, 1);
+        }
+
+        // test against unchecked
+        unchecked {
+            assertEq(low, a + b); // unchecked allow overflow
+        }
+    }
+
+    function testMul512(uint256 a, uint256 b) public pure {
+        (uint256 high, uint256 low) = Math.mul512(a, b);
+
+        // test against tryMul
+        (bool success, uint256 result) = Math.tryMul(a, b);
+        if (success) {
+            assertEq(high, 0);
+            assertEq(low, result);
+        } else {
+            assertGt(high, 0);
+        }
+
+        // test against unchecked
+        unchecked {
+            assertEq(low, a * b); // unchecked allow overflow
+        }
+
+        // test against alternative method
+        (uint256 _high, uint256 _low) = _mulKaratsuba(a, b);
+        assertEq(high, _high);
+        assertEq(low, _low);
+    }
+
     // MIN & MAX
     function testSymbolicMinMax(uint256 a, uint256 b) public pure {
         assertEq(Math.min(a, b), a < b ? a : b);
@@ -184,7 +226,7 @@ contract MathTest is Test {
     // MULDIV
     function testMulDiv(uint256 x, uint256 y, uint256 d) public pure {
         // Full precision for x * y
-        (uint256 xyHi, uint256 xyLo) = _mulHighLow(x, y);
+        (uint256 xyHi, uint256 xyLo) = Math.mul512(x, y);
 
         // Assume result won't overflow (see {testMulDivDomain})
         // This also checks that `d` is positive
@@ -194,9 +236,9 @@ contract MathTest is Test {
         uint256 q = Math.mulDiv(x, y, d);
 
         // Full precision for q * d
-        (uint256 qdHi, uint256 qdLo) = _mulHighLow(q, d);
+        (uint256 qdHi, uint256 qdLo) = Math.mul512(q, d);
         // Add remainder of x * y / d (computed as rem = (x * y % d))
-        (uint256 qdRemLo, uint256 c) = _addCarry(qdLo, mulmod(x, y, d));
+        (uint256 c, uint256 qdRemLo) = Math.add512(qdLo, mulmod(x, y, d));
         uint256 qdRemHi = qdHi + c;
 
         // Full precision check that x * y = q * d + rem
@@ -204,8 +246,9 @@ contract MathTest is Test {
         assertEq(xyLo, qdRemLo);
     }
 
+    /// forge-config: default.allow_internal_expect_revert = true
     function testMulDivDomain(uint256 x, uint256 y, uint256 d) public {
-        (uint256 xyHi, ) = _mulHighLow(x, y);
+        (uint256 xyHi, ) = Math.mul512(x, y);
 
         // Violate {testMulDiv} assumption (covers d is 0 and result overflow)
         vm.assume(xyHi >= d);
@@ -216,6 +259,7 @@ contract MathTest is Test {
     }
 
     // MOD EXP
+    /// forge-config: default.allow_internal_expect_revert = true
     function testModExp(uint256 b, uint256 e, uint256 m) public {
         if (m == 0) {
             vm.expectRevert(stdError.divisionError);
@@ -236,6 +280,7 @@ contract MathTest is Test {
         }
     }
 
+    /// forge-config: default.allow_internal_expect_revert = true
     function testModExpMemory(uint256 b, uint256 e, uint256 m) public {
         if (m == 0) {
             vm.expectRevert(stdError.divisionError);
@@ -263,26 +308,13 @@ contract MathTest is Test {
         }
     }
 
-    function _nativeModExp(uint256 b, uint256 e, uint256 m) private pure returns (uint256) {
-        if (m == 1) return 0;
-        uint256 r = 1;
-        while (e > 0) {
-            if (e % 2 > 0) {
-                r = mulmod(r, b, m);
-            }
-            b = mulmod(b, b, m);
-            e >>= 1;
-        }
-        return r;
-    }
-
     // Helpers
     function _asRounding(uint8 r) private pure returns (Math.Rounding) {
         vm.assume(r < uint8(type(Math.Rounding).max));
         return Math.Rounding(r);
     }
 
-    function _mulHighLow(uint256 x, uint256 y) private pure returns (uint256 high, uint256 low) {
+    function _mulKaratsuba(uint256 x, uint256 y) private pure returns (uint256 high, uint256 low) {
         (uint256 x0, uint256 x1) = (x & type(uint128).max, x >> 128);
         (uint256 y0, uint256 y1) = (y & type(uint128).max, y >> 128);
 
@@ -302,10 +334,16 @@ contract MathTest is Test {
         }
     }
 
-    function _addCarry(uint256 x, uint256 y) private pure returns (uint256 res, uint256 carry) {
-        unchecked {
-            res = x + y;
+    function _nativeModExp(uint256 b, uint256 e, uint256 m) private pure returns (uint256) {
+        if (m == 1) return 0;
+        uint256 r = 1;
+        while (e > 0) {
+            if (e % 2 > 0) {
+                r = mulmod(r, b, m);
+            }
+            b = mulmod(b, b, m);
+            e >>= 1;
         }
-        carry = res < x ? 1 : 0;
+        return r;
     }
 }

+ 298 - 147
test/utils/math/Math.test.js

@@ -16,10 +16,13 @@ const uint256 = value => ethers.Typed.uint256(value);
 bytes.zero = '0x';
 uint256.zero = 0n;
 
-async function testCommutative(fn, lhs, rhs, expected, ...extra) {
-  expect(await fn(lhs, rhs, ...extra)).to.deep.equal(expected);
-  expect(await fn(rhs, lhs, ...extra)).to.deep.equal(expected);
-}
+const testCommutative = (fn, lhs, rhs, expected, ...extra) =>
+  Promise.all([
+    expect(fn(lhs, rhs, ...extra)).to.eventually.deep.equal(expected),
+    expect(fn(rhs, lhs, ...extra)).to.eventually.deep.equal(expected),
+  ]);
+
+const splitHighLow = n => [n / (1n << 256n), n % (1n << 256n)];
 
 async function fixture() {
   const mock = await ethers.deployContract('$Math');
@@ -39,6 +42,24 @@ describe('Math', function () {
     Object.assign(this, await loadFixture(fixture));
   });
 
+  describe('add512', function () {
+    it('adds correctly without reverting', async function () {
+      const values = [0n, 1n, 17n, 42n, ethers.MaxUint256 - 1n, ethers.MaxUint256];
+      for (const [a, b] of product(values, values)) {
+        await expect(this.mock.$add512(a, b)).to.eventually.deep.equal(splitHighLow(a + b));
+      }
+    });
+  });
+
+  describe('mul512', function () {
+    it('multiplies correctly without reverting', async function () {
+      const values = [0n, 1n, 17n, 42n, ethers.MaxUint256 - 1n, ethers.MaxUint256];
+      for (const [a, b] of product(values, values)) {
+        await expect(this.mock.$mul512(a, b)).to.eventually.deep.equal(splitHighLow(a * b));
+      }
+    });
+  });
+
   describe('tryAdd', function () {
     it('adds correctly', async function () {
       const a = 5678n;
@@ -57,13 +78,13 @@ describe('Math', function () {
     it('subtracts correctly', async function () {
       const a = 5678n;
       const b = 1234n;
-      expect(await this.mock.$trySub(a, b)).to.deep.equal([true, a - b]);
+      await expect(this.mock.$trySub(a, b)).to.eventually.deep.equal([true, a - b]);
     });
 
     it('reverts if subtraction result would be negative', async function () {
       const a = 1234n;
       const b = 5678n;
-      expect(await this.mock.$trySub(a, b)).to.deep.equal([false, 0n]);
+      await expect(this.mock.$trySub(a, b)).to.eventually.deep.equal([false, 0n]);
     });
   });
 
@@ -91,25 +112,25 @@ describe('Math', function () {
     it('divides correctly', async function () {
       const a = 5678n;
       const b = 5678n;
-      expect(await this.mock.$tryDiv(a, b)).to.deep.equal([true, a / b]);
+      await expect(this.mock.$tryDiv(a, b)).to.eventually.deep.equal([true, a / b]);
     });
 
     it('divides zero correctly', async function () {
       const a = 0n;
       const b = 5678n;
-      expect(await this.mock.$tryDiv(a, b)).to.deep.equal([true, a / b]);
+      await expect(this.mock.$tryDiv(a, b)).to.eventually.deep.equal([true, a / b]);
     });
 
     it('returns complete number result on non-even division', async function () {
       const a = 7000n;
       const b = 5678n;
-      expect(await this.mock.$tryDiv(a, b)).to.deep.equal([true, a / b]);
+      await expect(this.mock.$tryDiv(a, b)).to.eventually.deep.equal([true, a / b]);
     });
 
     it('reverts on division by zero', async function () {
       const a = 5678n;
       const b = 0n;
-      expect(await this.mock.$tryDiv(a, b)).to.deep.equal([false, 0n]);
+      await expect(this.mock.$tryDiv(a, b)).to.eventually.deep.equal([false, 0n]);
     });
   });
 
@@ -118,32 +139,88 @@ describe('Math', function () {
       it('when the dividend is smaller than the divisor', async function () {
         const a = 284n;
         const b = 5678n;
-        expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]);
+        await expect(this.mock.$tryMod(a, b)).to.eventually.deep.equal([true, a % b]);
       });
 
       it('when the dividend is equal to the divisor', async function () {
         const a = 5678n;
         const b = 5678n;
-        expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]);
+        await expect(this.mock.$tryMod(a, b)).to.eventually.deep.equal([true, a % b]);
       });
 
       it('when the dividend is larger than the divisor', async function () {
         const a = 7000n;
         const b = 5678n;
-        expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]);
+        await expect(this.mock.$tryMod(a, b)).to.eventually.deep.equal([true, a % b]);
       });
 
       it('when the dividend is a multiple of the divisor', async function () {
         const a = 17034n; // 17034 == 5678 * 3
         const b = 5678n;
-        expect(await this.mock.$tryMod(a, b)).to.deep.equal([true, a % b]);
+        await expect(this.mock.$tryMod(a, b)).to.eventually.deep.equal([true, a % b]);
       });
     });
 
     it('reverts with a 0 divisor', async function () {
       const a = 5678n;
       const b = 0n;
-      expect(await this.mock.$tryMod(a, b)).to.deep.equal([false, 0n]);
+      await expect(this.mock.$tryMod(a, b)).to.eventually.deep.equal([false, 0n]);
+    });
+  });
+
+  describe('saturatingAdd', function () {
+    it('adds correctly', async function () {
+      const a = 5678n;
+      const b = 1234n;
+      await testCommutative(this.mock.$saturatingAdd, a, b, a + b);
+      await testCommutative(this.mock.$saturatingAdd, a, 0n, a);
+      await testCommutative(this.mock.$saturatingAdd, ethers.MaxUint256, 0n, ethers.MaxUint256);
+    });
+
+    it('bounds on addition overflow', async function () {
+      await testCommutative(this.mock.$saturatingAdd, ethers.MaxUint256, 1n, ethers.MaxUint256);
+      await expect(this.mock.$saturatingAdd(ethers.MaxUint256, ethers.MaxUint256)).to.eventually.equal(
+        ethers.MaxUint256,
+      );
+    });
+  });
+
+  describe('saturatingSub', function () {
+    it('subtracts correctly', async function () {
+      const a = 5678n;
+      const b = 1234n;
+      await expect(this.mock.$saturatingSub(a, b)).to.eventually.equal(a - b);
+      await expect(this.mock.$saturatingSub(a, a)).to.eventually.equal(0n);
+      await expect(this.mock.$saturatingSub(a, 0n)).to.eventually.equal(a);
+      await expect(this.mock.$saturatingSub(0n, a)).to.eventually.equal(0n);
+      await expect(this.mock.$saturatingSub(ethers.MaxUint256, 1n)).to.eventually.equal(ethers.MaxUint256 - 1n);
+    });
+
+    it('bounds on subtraction overflow', async function () {
+      await expect(this.mock.$saturatingSub(0n, 1n)).to.eventually.equal(0n);
+      await expect(this.mock.$saturatingSub(1n, 2n)).to.eventually.equal(0n);
+      await expect(this.mock.$saturatingSub(1n, ethers.MaxUint256)).to.eventually.equal(0n);
+      await expect(this.mock.$saturatingSub(ethers.MaxUint256 - 1n, ethers.MaxUint256)).to.eventually.equal(0n);
+    });
+  });
+
+  describe('saturatingMul', function () {
+    it('multiplies correctly', async function () {
+      const a = 1234n;
+      const b = 5678n;
+      await testCommutative(this.mock.$saturatingMul, a, b, a * b);
+    });
+
+    it('multiplies by zero correctly', async function () {
+      const a = 0n;
+      const b = 5678n;
+      await testCommutative(this.mock.$saturatingMul, a, b, 0n);
+    });
+
+    it('bounds on multiplication overflow', async function () {
+      const a = ethers.MaxUint256;
+      const b = 2n;
+      await testCommutative(this.mock.$saturatingMul, a, b, ethers.MaxUint256);
     });
   });
 
@@ -163,24 +240,24 @@ describe('Math', function () {
     it('is correctly calculated with two odd numbers', async function () {
       const a = 57417n;
       const b = 95431n;
-      expect(await this.mock.$average(a, b)).to.equal((a + b) / 2n);
+      await expect(this.mock.$average(a, b)).to.eventually.equal((a + b) / 2n);
     });
 
     it('is correctly calculated with two even numbers', async function () {
       const a = 42304n;
       const b = 84346n;
-      expect(await this.mock.$average(a, b)).to.equal((a + b) / 2n);
+      await expect(this.mock.$average(a, b)).to.eventually.equal((a + b) / 2n);
     });
 
     it('is correctly calculated with one even and one odd number', async function () {
       const a = 57417n;
       const b = 84346n;
-      expect(await this.mock.$average(a, b)).to.equal((a + b) / 2n);
+      await expect(this.mock.$average(a, b)).to.eventually.equal((a + b) / 2n);
     });
 
     it('is correctly calculated with two max uint256 numbers', async function () {
       const a = ethers.MaxUint256;
-      expect(await this.mock.$average(a, a)).to.equal(a);
+      await expect(this.mock.$average(a, a)).to.eventually.equal(a);
     });
   });
 
@@ -196,35 +273,35 @@ describe('Math', function () {
       const a = 0n;
       const b = 2n;
       const r = 0n;
-      expect(await this.mock.$ceilDiv(a, b)).to.equal(r);
+      await expect(this.mock.$ceilDiv(a, b)).to.eventually.equal(r);
     });
 
     it('does not round up on exact division', async function () {
       const a = 10n;
       const b = 5n;
       const r = 2n;
-      expect(await this.mock.$ceilDiv(a, b)).to.equal(r);
+      await expect(this.mock.$ceilDiv(a, b)).to.eventually.equal(r);
     });
 
     it('rounds up on division with remainders', async function () {
       const a = 42n;
       const b = 13n;
       const r = 4n;
-      expect(await this.mock.$ceilDiv(a, b)).to.equal(r);
+      await expect(this.mock.$ceilDiv(a, b)).to.eventually.equal(r);
     });
 
     it('does not overflow', async function () {
       const a = ethers.MaxUint256;
       const b = 2n;
       const r = 1n << 255n;
-      expect(await this.mock.$ceilDiv(a, b)).to.equal(r);
+      await expect(this.mock.$ceilDiv(a, b)).to.eventually.equal(r);
     });
 
     it('correctly computes max uint256 divided by 1', async function () {
       const a = ethers.MaxUint256;
       const b = 1n;
       const r = ethers.MaxUint256;
-      expect(await this.mock.$ceilDiv(a, b)).to.equal(r);
+      await expect(this.mock.$ceilDiv(a, b)).to.eventually.equal(r);
     });
   });
 
@@ -248,28 +325,30 @@ describe('Math', function () {
     describe('does round down', function () {
       it('small values', async function () {
         for (const rounding of RoundingDown) {
-          expect(await this.mock.$mulDiv(3n, 4n, 5n, rounding)).to.equal(2n);
-          expect(await this.mock.$mulDiv(3n, 5n, 5n, rounding)).to.equal(3n);
+          await expect(this.mock.$mulDiv(3n, 4n, 5n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$mulDiv(3n, 5n, 5n, rounding)).to.eventually.equal(3n);
         }
       });
 
       it('large values', async function () {
         for (const rounding of RoundingDown) {
-          expect(await this.mock.$mulDiv(42n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding)).to.equal(41n);
+          await expect(this.mock.$mulDiv(42n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding)).to.eventually.equal(
+            41n,
+          );
 
-          expect(await this.mock.$mulDiv(17n, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal(17n);
+          await expect(this.mock.$mulDiv(17n, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.eventually.equal(17n);
 
-          expect(
-            await this.mock.$mulDiv(ethers.MaxUint256 - 1n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
-          ).to.equal(ethers.MaxUint256 - 2n);
+          await expect(
+            this.mock.$mulDiv(ethers.MaxUint256 - 1n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
+          ).to.eventually.equal(ethers.MaxUint256 - 2n);
 
-          expect(
-            await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
-          ).to.equal(ethers.MaxUint256 - 1n);
+          await expect(
+            this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
+          ).to.eventually.equal(ethers.MaxUint256 - 1n);
 
-          expect(await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal(
-            ethers.MaxUint256,
-          );
+          await expect(
+            this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256, ethers.MaxUint256, rounding),
+          ).to.eventually.equal(ethers.MaxUint256);
         }
       });
     });
@@ -277,28 +356,91 @@ describe('Math', function () {
     describe('does round up', function () {
       it('small values', async function () {
         for (const rounding of RoundingUp) {
-          expect(await this.mock.$mulDiv(3n, 4n, 5n, rounding)).to.equal(3n);
-          expect(await this.mock.$mulDiv(3n, 5n, 5n, rounding)).to.equal(3n);
+          await expect(this.mock.$mulDiv(3n, 4n, 5n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$mulDiv(3n, 5n, 5n, rounding)).to.eventually.equal(3n);
         }
       });
 
       it('large values', async function () {
         for (const rounding of RoundingUp) {
-          expect(await this.mock.$mulDiv(42n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding)).to.equal(42n);
+          await expect(this.mock.$mulDiv(42n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding)).to.eventually.equal(
+            42n,
+          );
 
-          expect(await this.mock.$mulDiv(17n, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal(17n);
+          await expect(this.mock.$mulDiv(17n, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.eventually.equal(17n);
 
-          expect(
-            await this.mock.$mulDiv(ethers.MaxUint256 - 1n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
-          ).to.equal(ethers.MaxUint256 - 1n);
+          await expect(
+            this.mock.$mulDiv(ethers.MaxUint256 - 1n, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
+          ).to.eventually.equal(ethers.MaxUint256 - 1n);
 
-          expect(
-            await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
-          ).to.equal(ethers.MaxUint256 - 1n);
+          await expect(
+            this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256 - 1n, ethers.MaxUint256, rounding),
+          ).to.eventually.equal(ethers.MaxUint256 - 1n);
 
-          expect(await this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256, ethers.MaxUint256, rounding)).to.equal(
+          await expect(
+            this.mock.$mulDiv(ethers.MaxUint256, ethers.MaxUint256, ethers.MaxUint256, rounding),
+          ).to.eventually.equal(ethers.MaxUint256);
+        }
+      });
+    });
+  });
+
+  describe('mulShr', function () {
+    it('reverts with result higher than 2 ^ 256', async function () {
+      const a = 5n;
+      const b = ethers.MaxUint256;
+      const c = 1n;
+      await expect(this.mock.$mulShr(a, b, c, Rounding.Floor)).to.be.revertedWithPanic(
+        PANIC_CODES.ARITHMETIC_UNDER_OR_OVERFLOW,
+      );
+    });
+
+    describe('does round down', function () {
+      it('small values', async function () {
+        for (const rounding of RoundingDown) {
+          await expect(this.mock.$mulShr(3n, 5n, 1n, rounding)).to.eventually.equal(7n);
+          await expect(this.mock.$mulShr(3n, 5n, 2n, rounding)).to.eventually.equal(3n);
+        }
+      });
+
+      it('large values', async function () {
+        for (const rounding of RoundingDown) {
+          await expect(this.mock.$mulShr(42n, ethers.MaxUint256, 255n, rounding)).to.eventually.equal(83n);
+
+          await expect(this.mock.$mulShr(17n, ethers.MaxUint256, 255n, rounding)).to.eventually.equal(33n);
+
+          await expect(this.mock.$mulShr(ethers.MaxUint256, ethers.MaxInt256 + 1n, 255n, rounding)).to.eventually.equal(
+            ethers.MaxUint256,
+          );
+
+          await expect(this.mock.$mulShr(ethers.MaxUint256, ethers.MaxInt256, 255n, rounding)).to.eventually.equal(
+            ethers.MaxUint256 - 2n,
+          );
+        }
+      });
+    });
+
+    describe('does round up', function () {
+      it('small values', async function () {
+        for (const rounding of RoundingUp) {
+          await expect(this.mock.$mulShr(3n, 5n, 1n, rounding)).to.eventually.equal(8n);
+          await expect(this.mock.$mulShr(3n, 5n, 2n, rounding)).to.eventually.equal(4n);
+        }
+      });
+
+      it('large values', async function () {
+        for (const rounding of RoundingUp) {
+          await expect(this.mock.$mulShr(42n, ethers.MaxUint256, 255n, rounding)).to.eventually.equal(84n);
+
+          await expect(this.mock.$mulShr(17n, ethers.MaxUint256, 255n, rounding)).to.eventually.equal(34n);
+
+          await expect(this.mock.$mulShr(ethers.MaxUint256, ethers.MaxInt256 + 1n, 255n, rounding)).to.eventually.equal(
             ethers.MaxUint256,
           );
+
+          await expect(this.mock.$mulShr(ethers.MaxUint256, ethers.MaxInt256, 255n, rounding)).to.eventually.equal(
+            ethers.MaxUint256 - 1n,
+          );
         }
       });
     });
@@ -320,8 +462,8 @@ describe('Math', function () {
 
       describe(`using p=${p} which is ${p > 1 && factors.length > 1 ? 'not ' : ''}a prime`, function () {
         it('trying to inverse 0 returns 0', async function () {
-          expect(await this.mock.$invMod(0, p)).to.equal(0n);
-          expect(await this.mock.$invMod(p, p)).to.equal(0n); // p is 0 mod p
+          await expect(this.mock.$invMod(0, p)).to.eventually.equal(0n);
+          await expect(this.mock.$invMod(p, p)).to.eventually.equal(0n); // p is 0 mod p
         });
 
         if (p != 0) {
@@ -349,7 +491,7 @@ describe('Math', function () {
           const e = 200n;
           const m = 50n;
 
-          expect(await this.mock.$modExp(type(b), type(e), type(m))).to.equal(type(b ** e % m).value);
+          await expect(this.mock.$modExp(type(b), type(e), type(m))).to.eventually.equal(type(b ** e % m).value);
         });
 
         it('is correctly reverting when modulus is zero', async function () {
@@ -373,7 +515,9 @@ describe('Math', function () {
         it(`calculates b ** e % m (b=2**${log2b}+1) (e=2**${log2e}+1) (m=2**${log2m}+1)`, async function () {
           const mLength = ethers.dataLength(ethers.toBeHex(m));
 
-          expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal(bytes(modExp(b, e, m), mLength).value);
+          await expect(this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.eventually.equal(
+            bytes(modExp(b, e, m), mLength).value,
+          );
         });
       }
     });
@@ -387,7 +531,10 @@ describe('Math', function () {
           const e = 200n;
           const m = 50n;
 
-          expect(await this.mock.$tryModExp(type(b), type(e), type(m))).to.deep.equal([true, type(b ** e % m).value]);
+          await expect(this.mock.$tryModExp(type(b), type(e), type(m))).to.eventually.deep.equal([
+            true,
+            type(b ** e % m).value,
+          ]);
         });
 
         it('is correctly reverting when modulus is zero', async function () {
@@ -395,7 +542,7 @@ describe('Math', function () {
           const e = 200n;
           const m = 0n;
 
-          expect(await this.mock.$tryModExp(type(b), type(e), type(m))).to.deep.equal([false, type.zero]);
+          await expect(this.mock.$tryModExp(type(b), type(e), type(m))).to.eventually.deep.equal([false, type.zero]);
         });
       });
     }
@@ -409,7 +556,7 @@ describe('Math', function () {
         it(`calculates b ** e % m (b=2**${log2b}+1) (e=2**${log2e}+1) (m=2**${log2m}+1)`, async function () {
           const mLength = ethers.dataLength(ethers.toBeHex(m));
 
-          expect(await this.mock.$tryModExp(bytes(b), bytes(e), bytes(m))).to.deep.equal([
+          await expect(this.mock.$tryModExp(bytes(b), bytes(e), bytes(m))).to.eventually.deep.equal([
             true,
             bytes(modExp(b, e, m), mLength).value,
           ]);
@@ -421,35 +568,39 @@ describe('Math', function () {
   describe('sqrt', function () {
     it('rounds down', async function () {
       for (const rounding of RoundingDown) {
-        expect(await this.mock.$sqrt(0n, rounding)).to.equal(0n);
-        expect(await this.mock.$sqrt(1n, rounding)).to.equal(1n);
-        expect(await this.mock.$sqrt(2n, rounding)).to.equal(1n);
-        expect(await this.mock.$sqrt(3n, rounding)).to.equal(1n);
-        expect(await this.mock.$sqrt(4n, rounding)).to.equal(2n);
-        expect(await this.mock.$sqrt(144n, rounding)).to.equal(12n);
-        expect(await this.mock.$sqrt(999999n, rounding)).to.equal(999n);
-        expect(await this.mock.$sqrt(1000000n, rounding)).to.equal(1000n);
-        expect(await this.mock.$sqrt(1000001n, rounding)).to.equal(1000n);
-        expect(await this.mock.$sqrt(1002000n, rounding)).to.equal(1000n);
-        expect(await this.mock.$sqrt(1002001n, rounding)).to.equal(1001n);
-        expect(await this.mock.$sqrt(ethers.MaxUint256, rounding)).to.equal(340282366920938463463374607431768211455n);
+        await expect(this.mock.$sqrt(0n, rounding)).to.eventually.equal(0n);
+        await expect(this.mock.$sqrt(1n, rounding)).to.eventually.equal(1n);
+        await expect(this.mock.$sqrt(2n, rounding)).to.eventually.equal(1n);
+        await expect(this.mock.$sqrt(3n, rounding)).to.eventually.equal(1n);
+        await expect(this.mock.$sqrt(4n, rounding)).to.eventually.equal(2n);
+        await expect(this.mock.$sqrt(144n, rounding)).to.eventually.equal(12n);
+        await expect(this.mock.$sqrt(999999n, rounding)).to.eventually.equal(999n);
+        await expect(this.mock.$sqrt(1000000n, rounding)).to.eventually.equal(1000n);
+        await expect(this.mock.$sqrt(1000001n, rounding)).to.eventually.equal(1000n);
+        await expect(this.mock.$sqrt(1002000n, rounding)).to.eventually.equal(1000n);
+        await expect(this.mock.$sqrt(1002001n, rounding)).to.eventually.equal(1001n);
+        await expect(this.mock.$sqrt(ethers.MaxUint256, rounding)).to.eventually.equal(
+          340282366920938463463374607431768211455n,
+        );
       }
     });
 
     it('rounds up', async function () {
       for (const rounding of RoundingUp) {
-        expect(await this.mock.$sqrt(0n, rounding)).to.equal(0n);
-        expect(await this.mock.$sqrt(1n, rounding)).to.equal(1n);
-        expect(await this.mock.$sqrt(2n, rounding)).to.equal(2n);
-        expect(await this.mock.$sqrt(3n, rounding)).to.equal(2n);
-        expect(await this.mock.$sqrt(4n, rounding)).to.equal(2n);
-        expect(await this.mock.$sqrt(144n, rounding)).to.equal(12n);
-        expect(await this.mock.$sqrt(999999n, rounding)).to.equal(1000n);
-        expect(await this.mock.$sqrt(1000000n, rounding)).to.equal(1000n);
-        expect(await this.mock.$sqrt(1000001n, rounding)).to.equal(1001n);
-        expect(await this.mock.$sqrt(1002000n, rounding)).to.equal(1001n);
-        expect(await this.mock.$sqrt(1002001n, rounding)).to.equal(1001n);
-        expect(await this.mock.$sqrt(ethers.MaxUint256, rounding)).to.equal(340282366920938463463374607431768211456n);
+        await expect(this.mock.$sqrt(0n, rounding)).to.eventually.equal(0n);
+        await expect(this.mock.$sqrt(1n, rounding)).to.eventually.equal(1n);
+        await expect(this.mock.$sqrt(2n, rounding)).to.eventually.equal(2n);
+        await expect(this.mock.$sqrt(3n, rounding)).to.eventually.equal(2n);
+        await expect(this.mock.$sqrt(4n, rounding)).to.eventually.equal(2n);
+        await expect(this.mock.$sqrt(144n, rounding)).to.eventually.equal(12n);
+        await expect(this.mock.$sqrt(999999n, rounding)).to.eventually.equal(1000n);
+        await expect(this.mock.$sqrt(1000000n, rounding)).to.eventually.equal(1000n);
+        await expect(this.mock.$sqrt(1000001n, rounding)).to.eventually.equal(1001n);
+        await expect(this.mock.$sqrt(1002000n, rounding)).to.eventually.equal(1001n);
+        await expect(this.mock.$sqrt(1002001n, rounding)).to.eventually.equal(1001n);
+        await expect(this.mock.$sqrt(ethers.MaxUint256, rounding)).to.eventually.equal(
+          340282366920938463463374607431768211456n,
+        );
       }
     });
   });
@@ -458,33 +609,33 @@ describe('Math', function () {
     describe('log2', function () {
       it('rounds down', async function () {
         for (const rounding of RoundingDown) {
-          expect(await this.mock.$log2(0n, rounding)).to.equal(0n);
-          expect(await this.mock.$log2(1n, rounding)).to.equal(0n);
-          expect(await this.mock.$log2(2n, rounding)).to.equal(1n);
-          expect(await this.mock.$log2(3n, rounding)).to.equal(1n);
-          expect(await this.mock.$log2(4n, rounding)).to.equal(2n);
-          expect(await this.mock.$log2(5n, rounding)).to.equal(2n);
-          expect(await this.mock.$log2(6n, rounding)).to.equal(2n);
-          expect(await this.mock.$log2(7n, rounding)).to.equal(2n);
-          expect(await this.mock.$log2(8n, rounding)).to.equal(3n);
-          expect(await this.mock.$log2(9n, rounding)).to.equal(3n);
-          expect(await this.mock.$log2(ethers.MaxUint256, rounding)).to.equal(255n);
+          await expect(this.mock.$log2(0n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log2(1n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log2(2n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log2(3n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log2(4n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log2(5n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log2(6n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log2(7n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log2(8n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log2(9n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log2(ethers.MaxUint256, rounding)).to.eventually.equal(255n);
         }
       });
 
       it('rounds up', async function () {
         for (const rounding of RoundingUp) {
-          expect(await this.mock.$log2(0n, rounding)).to.equal(0n);
-          expect(await this.mock.$log2(1n, rounding)).to.equal(0n);
-          expect(await this.mock.$log2(2n, rounding)).to.equal(1n);
-          expect(await this.mock.$log2(3n, rounding)).to.equal(2n);
-          expect(await this.mock.$log2(4n, rounding)).to.equal(2n);
-          expect(await this.mock.$log2(5n, rounding)).to.equal(3n);
-          expect(await this.mock.$log2(6n, rounding)).to.equal(3n);
-          expect(await this.mock.$log2(7n, rounding)).to.equal(3n);
-          expect(await this.mock.$log2(8n, rounding)).to.equal(3n);
-          expect(await this.mock.$log2(9n, rounding)).to.equal(4n);
-          expect(await this.mock.$log2(ethers.MaxUint256, rounding)).to.equal(256n);
+          await expect(this.mock.$log2(0n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log2(1n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log2(2n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log2(3n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log2(4n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log2(5n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log2(6n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log2(7n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log2(8n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log2(9n, rounding)).to.eventually.equal(4n);
+          await expect(this.mock.$log2(ethers.MaxUint256, rounding)).to.eventually.equal(256n);
         }
       });
     });
@@ -492,37 +643,37 @@ describe('Math', function () {
     describe('log10', function () {
       it('rounds down', async function () {
         for (const rounding of RoundingDown) {
-          expect(await this.mock.$log10(0n, rounding)).to.equal(0n);
-          expect(await this.mock.$log10(1n, rounding)).to.equal(0n);
-          expect(await this.mock.$log10(2n, rounding)).to.equal(0n);
-          expect(await this.mock.$log10(9n, rounding)).to.equal(0n);
-          expect(await this.mock.$log10(10n, rounding)).to.equal(1n);
-          expect(await this.mock.$log10(11n, rounding)).to.equal(1n);
-          expect(await this.mock.$log10(99n, rounding)).to.equal(1n);
-          expect(await this.mock.$log10(100n, rounding)).to.equal(2n);
-          expect(await this.mock.$log10(101n, rounding)).to.equal(2n);
-          expect(await this.mock.$log10(999n, rounding)).to.equal(2n);
-          expect(await this.mock.$log10(1000n, rounding)).to.equal(3n);
-          expect(await this.mock.$log10(1001n, rounding)).to.equal(3n);
-          expect(await this.mock.$log10(ethers.MaxUint256, rounding)).to.equal(77n);
+          await expect(this.mock.$log10(0n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log10(1n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log10(2n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log10(9n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log10(10n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log10(11n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log10(99n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log10(100n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log10(101n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log10(999n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log10(1000n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log10(1001n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log10(ethers.MaxUint256, rounding)).to.eventually.equal(77n);
         }
       });
 
       it('rounds up', async function () {
         for (const rounding of RoundingUp) {
-          expect(await this.mock.$log10(0n, rounding)).to.equal(0n);
-          expect(await this.mock.$log10(1n, rounding)).to.equal(0n);
-          expect(await this.mock.$log10(2n, rounding)).to.equal(1n);
-          expect(await this.mock.$log10(9n, rounding)).to.equal(1n);
-          expect(await this.mock.$log10(10n, rounding)).to.equal(1n);
-          expect(await this.mock.$log10(11n, rounding)).to.equal(2n);
-          expect(await this.mock.$log10(99n, rounding)).to.equal(2n);
-          expect(await this.mock.$log10(100n, rounding)).to.equal(2n);
-          expect(await this.mock.$log10(101n, rounding)).to.equal(3n);
-          expect(await this.mock.$log10(999n, rounding)).to.equal(3n);
-          expect(await this.mock.$log10(1000n, rounding)).to.equal(3n);
-          expect(await this.mock.$log10(1001n, rounding)).to.equal(4n);
-          expect(await this.mock.$log10(ethers.MaxUint256, rounding)).to.equal(78n);
+          await expect(this.mock.$log10(0n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log10(1n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log10(2n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log10(9n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log10(10n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log10(11n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log10(99n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log10(100n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log10(101n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log10(999n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log10(1000n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log10(1001n, rounding)).to.eventually.equal(4n);
+          await expect(this.mock.$log10(ethers.MaxUint256, rounding)).to.eventually.equal(78n);
         }
       });
     });
@@ -530,31 +681,31 @@ describe('Math', function () {
     describe('log256', function () {
       it('rounds down', async function () {
         for (const rounding of RoundingDown) {
-          expect(await this.mock.$log256(0n, rounding)).to.equal(0n);
-          expect(await this.mock.$log256(1n, rounding)).to.equal(0n);
-          expect(await this.mock.$log256(2n, rounding)).to.equal(0n);
-          expect(await this.mock.$log256(255n, rounding)).to.equal(0n);
-          expect(await this.mock.$log256(256n, rounding)).to.equal(1n);
-          expect(await this.mock.$log256(257n, rounding)).to.equal(1n);
-          expect(await this.mock.$log256(65535n, rounding)).to.equal(1n);
-          expect(await this.mock.$log256(65536n, rounding)).to.equal(2n);
-          expect(await this.mock.$log256(65537n, rounding)).to.equal(2n);
-          expect(await this.mock.$log256(ethers.MaxUint256, rounding)).to.equal(31n);
+          await expect(this.mock.$log256(0n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log256(1n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log256(2n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log256(255n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log256(256n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log256(257n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log256(65535n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log256(65536n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log256(65537n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log256(ethers.MaxUint256, rounding)).to.eventually.equal(31n);
         }
       });
 
       it('rounds up', async function () {
         for (const rounding of RoundingUp) {
-          expect(await this.mock.$log256(0n, rounding)).to.equal(0n);
-          expect(await this.mock.$log256(1n, rounding)).to.equal(0n);
-          expect(await this.mock.$log256(2n, rounding)).to.equal(1n);
-          expect(await this.mock.$log256(255n, rounding)).to.equal(1n);
-          expect(await this.mock.$log256(256n, rounding)).to.equal(1n);
-          expect(await this.mock.$log256(257n, rounding)).to.equal(2n);
-          expect(await this.mock.$log256(65535n, rounding)).to.equal(2n);
-          expect(await this.mock.$log256(65536n, rounding)).to.equal(2n);
-          expect(await this.mock.$log256(65537n, rounding)).to.equal(3n);
-          expect(await this.mock.$log256(ethers.MaxUint256, rounding)).to.equal(32n);
+          await expect(this.mock.$log256(0n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log256(1n, rounding)).to.eventually.equal(0n);
+          await expect(this.mock.$log256(2n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log256(255n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log256(256n, rounding)).to.eventually.equal(1n);
+          await expect(this.mock.$log256(257n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log256(65535n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log256(65536n, rounding)).to.eventually.equal(2n);
+          await expect(this.mock.$log256(65537n, rounding)).to.eventually.equal(3n);
+          await expect(this.mock.$log256(ethers.MaxUint256, rounding)).to.eventually.equal(32n);
         }
       });
     });

+ 43 - 0
test/utils/structs/EnumerableMap.behavior.js

@@ -117,6 +117,49 @@ function shouldBehaveLikeMap() {
     });
   });
 
+  describe('clear', function () {
+    it('clears a single entry', async function () {
+      await this.methods.set(this.keyA, this.valueA);
+
+      await this.methods.clear();
+
+      expect(await this.methods.contains(this.keyA)).to.be.false;
+      await expectMembersMatch(this.methods, [], []);
+    });
+
+    it('clears multiple entries', async function () {
+      await this.methods.set(this.keyA, this.valueA);
+      await this.methods.set(this.keyB, this.valueB);
+      await this.methods.set(this.keyC, this.valueC);
+
+      await this.methods.clear();
+
+      expect(await this.methods.contains(this.keyA)).to.be.false;
+      expect(await this.methods.contains(this.keyB)).to.be.false;
+      expect(await this.methods.contains(this.keyC)).to.be.false;
+      await expectMembersMatch(this.methods, [], []);
+    });
+
+    it('does not revert on empty map', async function () {
+      await this.methods.clear();
+    });
+
+    it('clear then add entry', async function () {
+      await this.methods.set(this.keyA, this.valueA);
+      await this.methods.set(this.keyB, this.valueB);
+      await this.methods.set(this.keyC, this.valueC);
+
+      await this.methods.clear();
+
+      await this.methods.set(this.keyA, this.valueA);
+
+      expect(await this.methods.contains(this.keyA)).to.be.true;
+      expect(await this.methods.contains(this.keyB)).to.be.false;
+      expect(await this.methods.contains(this.keyC)).to.be.false;
+      await expectMembersMatch(this.methods, [this.keyA], [this.valueA]);
+    });
+  });
+
   describe('read', function () {
     beforeEach(async function () {
       await this.methods.set(this.keyA, this.valueA);

+ 1 - 0
test/utils/structs/EnumerableMap.test.js

@@ -26,6 +26,7 @@ async function fixture() {
             get: `$get_EnumerableMap_${name}(uint256,${keyType})`,
             tryGet: `$tryGet_EnumerableMap_${name}(uint256,${keyType})`,
             remove: `$remove_EnumerableMap_${name}(uint256,${keyType})`,
+            clear: `$clear_EnumerableMap_${name}(uint256)`,
             length: `$length_EnumerableMap_${name}(uint256)`,
             at: `$at_EnumerableMap_${name}(uint256,uint256)`,
             contains: `$contains_EnumerableMap_${name}(uint256,${keyType})`,

+ 43 - 0
test/utils/structs/EnumerableSet.behavior.js

@@ -109,6 +109,49 @@ function shouldBehaveLikeSet() {
       expect(await this.methods.contains(this.valueB)).to.be.false;
     });
   });
+
+  describe('clear', function () {
+    it('clears a single value', async function () {
+      await this.methods.add(this.valueA);
+
+      await this.methods.clear();
+
+      expect(await this.methods.contains(this.valueA)).to.be.false;
+      await expectMembersMatch(this.methods, []);
+    });
+
+    it('clears multiple values', async function () {
+      await this.methods.add(this.valueA);
+      await this.methods.add(this.valueB);
+      await this.methods.add(this.valueC);
+
+      await this.methods.clear();
+
+      expect(await this.methods.contains(this.valueA)).to.be.false;
+      expect(await this.methods.contains(this.valueB)).to.be.false;
+      expect(await this.methods.contains(this.valueC)).to.be.false;
+      await expectMembersMatch(this.methods, []);
+    });
+
+    it('does not revert on empty set', async function () {
+      await this.methods.clear();
+    });
+
+    it('clear then add value', async function () {
+      await this.methods.add(this.valueA);
+      await this.methods.add(this.valueB);
+      await this.methods.add(this.valueC);
+
+      await this.methods.clear();
+
+      await this.methods.add(this.valueA);
+
+      expect(await this.methods.contains(this.valueA)).to.be.true;
+      expect(await this.methods.contains(this.valueB)).to.be.false;
+      expect(await this.methods.contains(this.valueC)).to.be.false;
+      await expectMembersMatch(this.methods, [this.valueA]);
+    });
+  });
 }
 
 module.exports = {

+ 1 - 0
test/utils/structs/EnumerableSet.test.js

@@ -30,6 +30,7 @@ async function fixture() {
         methods: getMethods(mock, {
           add: `$add(uint256,${type})`,
           remove: `$remove(uint256,${type})`,
+          clear: `$clear_EnumerableSet_${name}(uint256)`,
           contains: `$contains(uint256,${type})`,
           length: `$length_EnumerableSet_${name}(uint256)`,
           at: `$at_EnumerableSet_${name}(uint256,uint256)`,