Browse Source

Extend Checkpoints with new sizes and lookup mechanisms (#3589)

Hadrien Croubois 3 years ago
parent
commit
71aaca2d9d

+ 3 - 0
CHANGELOG.md

@@ -22,6 +22,9 @@
  * `VestingWallet`: add `releasable` getters. ([#3580](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3580))
  * `Create2`: optimize address computation by using assembly instead of `abi.encodePacked`. ([#3600](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3600))
  * `Clones`: optimized the assembly to use only the scratch space during deployments, and optimized `predictDeterministicAddress` to use lesser operations. ([#3640](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3640))
+ * `Checkpoints`: Use procedural generation to support multiple key/value lengths. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
+ * `Checkpoints`: Add new lookup mechanisms. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
+ * `Array`: Add `unsafeAccess` functions that allow reading and writing to an element in a storage array bypassing Solidity's "out-of-bounds" check. ([#3589](https://github.com/OpenZeppelin/openzeppelin-contracts/pull/3589))
 
 ### Breaking changes
 

+ 0 - 19
contracts/mocks/ArraysImpl.sol

@@ -1,19 +0,0 @@
-// SPDX-License-Identifier: MIT
-
-pragma solidity ^0.8.0;
-
-import "../utils/Arrays.sol";
-
-contract ArraysImpl {
-    using Arrays for uint256[];
-
-    uint256[] private _array;
-
-    constructor(uint256[] memory array) {
-        _array = array;
-    }
-
-    function findUpperBound(uint256 element) external view returns (uint256) {
-        return _array.findUpperBound(element);
-    }
-}

+ 51 - 0
contracts/mocks/ArraysMock.sol

@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: MIT
+
+pragma solidity ^0.8.0;
+
+import "../utils/Arrays.sol";
+
+contract Uint256ArraysMock {
+    using Arrays for uint256[];
+
+    uint256[] private _array;
+
+    constructor(uint256[] memory array) {
+        _array = array;
+    }
+
+    function findUpperBound(uint256 element) external view returns (uint256) {
+        return _array.findUpperBound(element);
+    }
+
+    function unsafeAccess(uint256 pos) external view returns (uint256) {
+        return _array.unsafeAccess(pos).value;
+    }
+}
+
+contract AddressArraysMock {
+    using Arrays for address[];
+
+    address[] private _array;
+
+    constructor(address[] memory array) {
+        _array = array;
+    }
+
+    function unsafeAccess(uint256 pos) external view returns (address) {
+        return _array.unsafeAccess(pos).value;
+    }
+}
+
+contract Bytes32ArraysMock {
+    using Arrays for bytes32[];
+
+    bytes32[] private _array;
+
+    constructor(bytes32[] memory array) {
+        _array = array;
+    }
+
+    function unsafeAccess(uint256 pos) external view returns (bytes32) {
+        return _array.unsafeAccess(pos).value;
+    }
+}

+ 0 - 27
contracts/mocks/CheckpointsImpl.sol

@@ -1,27 +0,0 @@
-// SPDX-License-Identifier: MIT
-
-pragma solidity ^0.8.0;
-
-import "../utils/Checkpoints.sol";
-
-contract CheckpointsImpl {
-    using Checkpoints for Checkpoints.History;
-
-    Checkpoints.History private _totalCheckpoints;
-
-    function latest() public view returns (uint256) {
-        return _totalCheckpoints.latest();
-    }
-
-    function getAtBlock(uint256 blockNumber) public view returns (uint256) {
-        return _totalCheckpoints.getAtBlock(blockNumber);
-    }
-
-    function push(uint256 value) public returns (uint256, uint256) {
-        return _totalCheckpoints.push(value);
-    }
-
-    function length() public view returns (uint256) {
-        return _totalCheckpoints._checkpoints.length;
-    }
-}

+ 92 - 0
contracts/mocks/CheckpointsMock.sol

@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: MIT
+// This file was procedurally generated from scripts/generate/templates/CheckpointsMock.js.
+
+pragma solidity ^0.8.0;
+
+import "../utils/Checkpoints.sol";
+
+contract CheckpointsMock {
+    using Checkpoints for Checkpoints.History;
+
+    Checkpoints.History private _totalCheckpoints;
+
+    function latest() public view returns (uint256) {
+        return _totalCheckpoints.latest();
+    }
+
+    function push(uint256 value) public returns (uint256, uint256) {
+        return _totalCheckpoints.push(value);
+    }
+
+    function getAtBlock(uint256 blockNumber) public view returns (uint256) {
+        return _totalCheckpoints.getAtBlock(blockNumber);
+    }
+
+    function getAtRecentBlock(uint256 blockNumber) public view returns (uint256) {
+        return _totalCheckpoints.getAtRecentBlock(blockNumber);
+    }
+
+    function length() public view returns (uint256) {
+        return _totalCheckpoints._checkpoints.length;
+    }
+}
+
+contract Checkpoints224Mock {
+    using Checkpoints for Checkpoints.Trace224;
+
+    Checkpoints.Trace224 private _totalCheckpoints;
+
+    function latest() public view returns (uint224) {
+        return _totalCheckpoints.latest();
+    }
+
+    function push(uint32 key, uint224 value) public returns (uint224, uint224) {
+        return _totalCheckpoints.push(key, value);
+    }
+
+    function lowerLookup(uint32 key) public view returns (uint224) {
+        return _totalCheckpoints.lowerLookup(key);
+    }
+
+    function upperLookup(uint32 key) public view returns (uint224) {
+        return _totalCheckpoints.upperLookup(key);
+    }
+
+    function upperLookupRecent(uint32 key) public view returns (uint224) {
+        return _totalCheckpoints.upperLookupRecent(key);
+    }
+
+    function length() public view returns (uint256) {
+        return _totalCheckpoints._checkpoints.length;
+    }
+}
+
+contract Checkpoints160Mock {
+    using Checkpoints for Checkpoints.Trace160;
+
+    Checkpoints.Trace160 private _totalCheckpoints;
+
+    function latest() public view returns (uint160) {
+        return _totalCheckpoints.latest();
+    }
+
+    function push(uint96 key, uint160 value) public returns (uint160, uint160) {
+        return _totalCheckpoints.push(key, value);
+    }
+
+    function lowerLookup(uint96 key) public view returns (uint160) {
+        return _totalCheckpoints.lowerLookup(key);
+    }
+
+    function upperLookup(uint96 key) public view returns (uint160) {
+        return _totalCheckpoints.upperLookup(key);
+    }
+
+    function upperLookupRecent(uint96 key) public view returns (uint224) {
+        return _totalCheckpoints.upperLookupRecent(key);
+    }
+
+    function length() public view returns (uint256) {
+        return _totalCheckpoints._checkpoints.length;
+    }
+}

+ 1 - 0
contracts/mocks/EnumerableMapMock.sol

@@ -1,4 +1,5 @@
 // SPDX-License-Identifier: MIT
+// This file was procedurally generated from scripts/generate/templates/EnumerableMapMock.js.
 
 pragma solidity ^0.8.0;
 

+ 1 - 0
contracts/mocks/EnumerableSetMock.sol

@@ -1,4 +1,5 @@
 // SPDX-License-Identifier: MIT
+// This file was procedurally generated from scripts/generate/templates/EnumerableSetMock.js.
 
 pragma solidity ^0.8.0;
 

+ 1 - 0
contracts/mocks/SafeCastMock.sol

@@ -1,4 +1,5 @@
 // SPDX-License-Identifier: MIT
+// This file was procedurally generated from scripts/generate/templates/SafeCastMock.js.
 
 pragma solidity ^0.8.0;
 

+ 50 - 2
contracts/utils/Arrays.sol

@@ -3,12 +3,15 @@
 
 pragma solidity ^0.8.0;
 
+import "./StorageSlot.sol";
 import "./math/Math.sol";
 
 /**
  * @dev Collection of functions related to array types.
  */
 library Arrays {
+    using StorageSlot for bytes32;
+
     /**
      * @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
@@ -31,7 +34,7 @@ library Arrays {
 
             // Note that mid will always be strictly less than high (i.e. it will be a valid array index)
             // because Math.average rounds down (it does integer division with truncation).
-            if (array[mid] > element) {
+            if (unsafeAccess(array, mid).value > element) {
                 high = mid;
             } else {
                 low = mid + 1;
@@ -39,10 +42,55 @@ library Arrays {
         }
 
         // At this point `low` is the exclusive upper bound. We will return the inclusive upper bound.
-        if (low > 0 && array[low - 1] == element) {
+        if (low > 0 && unsafeAccess(array, low - 1).value == element) {
             return low - 1;
         } else {
             return low;
         }
     }
+
+    /**
+     * @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(address[] storage arr, uint256 pos) internal pure returns (StorageSlot.AddressSlot storage) {
+        bytes32 slot;
+        /// @solidity memory-safe-assembly
+        assembly {
+            mstore(0, arr.slot)
+            slot := add(keccak256(0, 0x20), pos)
+        }
+        return slot.getAddressSlot();
+    }
+
+    /**
+     * @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(bytes32[] storage arr, uint256 pos) internal pure returns (StorageSlot.Bytes32Slot storage) {
+        bytes32 slot;
+        /// @solidity memory-safe-assembly
+        assembly {
+            mstore(0, arr.slot)
+            slot := add(keccak256(0, 0x20), pos)
+        }
+        return slot.getBytes32Slot();
+    }
+
+    /**
+     * @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(uint256[] storage arr, uint256 pos) internal pure returns (StorageSlot.Uint256Slot storage) {
+        bytes32 slot;
+        /// @solidity memory-safe-assembly
+        assembly {
+            mstore(0, arr.slot)
+            slot := add(keccak256(0, 0x20), pos)
+        }
+        return slot.getUint256Slot();
+    }
 }

+ 428 - 25
contracts/utils/Checkpoints.sol

@@ -1,5 +1,7 @@
 // SPDX-License-Identifier: MIT
 // OpenZeppelin Contracts (last updated v4.5.0) (utils/Checkpoints.sol)
+// This file was procedurally generated from scripts/generate/templates/Checkpoints.js.
+
 pragma solidity ^0.8.0;
 
 import "./math/Math.sol";
@@ -15,21 +17,21 @@ import "./math/SafeCast.sol";
  * _Available since v4.5._
  */
 library Checkpoints {
+    struct History {
+        Checkpoint[] _checkpoints;
+    }
+
     struct Checkpoint {
         uint32 _blockNumber;
         uint224 _value;
     }
 
-    struct History {
-        Checkpoint[] _checkpoints;
-    }
-
     /**
      * @dev Returns the value in the latest checkpoint, or zero if there are no checkpoints.
      */
     function latest(History storage self) internal view returns (uint256) {
         uint256 pos = self._checkpoints.length;
-        return pos == 0 ? 0 : self._checkpoints[pos - 1]._value;
+        return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
     }
 
     /**
@@ -38,18 +40,34 @@ library Checkpoints {
      */
     function getAtBlock(History storage self, uint256 blockNumber) internal view returns (uint256) {
         require(blockNumber < block.number, "Checkpoints: block not yet mined");
+        uint32 key = SafeCast.toUint32(blockNumber);
 
-        uint256 high = self._checkpoints.length;
-        uint256 low = 0;
-        while (low < high) {
-            uint256 mid = Math.average(low, high);
-            if (self._checkpoints[mid]._blockNumber > blockNumber) {
-                high = mid;
-            } else {
-                low = mid + 1;
-            }
+        uint256 length = self._checkpoints.length;
+        uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, length);
+        return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
+    }
+
+    /**
+     * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one
+     * before it is returned, or zero otherwise. Similarly to {upperLookup} but optimized for the case when the search
+     * key is known to be recent.
+     */
+    function getAtRecentBlock(History storage self, uint256 blockNumber) internal view returns (uint256) {
+        require(blockNumber < block.number, "Checkpoints: block not yet mined");
+        uint32 key = SafeCast.toUint32(blockNumber);
+
+        uint256 length = self._checkpoints.length;
+        uint256 offset = 1;
+
+        while (offset <= length && _unsafeAccess(self._checkpoints, length - offset)._blockNumber > key) {
+            offset <<= 1;
         }
-        return high == 0 ? 0 : self._checkpoints[high - 1]._value;
+
+        uint256 low = offset < length ? length - offset : 0;
+        uint256 high = length - (offset >> 1);
+        uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high);
+
+        return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
     }
 
     /**
@@ -58,16 +76,7 @@ library Checkpoints {
      * Returns previous value and new value.
      */
     function push(History storage self, uint256 value) internal returns (uint256, uint256) {
-        uint256 pos = self._checkpoints.length;
-        uint256 old = latest(self);
-        if (pos > 0 && self._checkpoints[pos - 1]._blockNumber == block.number) {
-            self._checkpoints[pos - 1]._value = SafeCast.toUint224(value);
-        } else {
-            self._checkpoints.push(
-                Checkpoint({_blockNumber: SafeCast.toUint32(block.number), _value: SafeCast.toUint224(value)})
-            );
-        }
-        return (old, value);
+        return _insert(self._checkpoints, SafeCast.toUint32(block.number), SafeCast.toUint224(value));
     }
 
     /**
@@ -83,4 +92,398 @@ library Checkpoints {
     ) internal returns (uint256, uint256) {
         return push(self, op(latest(self), delta));
     }
+
+    /**
+     * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint,
+     * or by updating the last one.
+     */
+    function _insert(
+        Checkpoint[] storage self,
+        uint32 key,
+        uint224 value
+    ) private returns (uint224, uint224) {
+        uint256 pos = self.length;
+
+        if (pos > 0) {
+            // Copying to memory is important here.
+            Checkpoint memory last = _unsafeAccess(self, pos - 1);
+
+            // Checkpoints keys must be increasing.
+            require(last._blockNumber <= key, "Checkpoint: invalid key");
+
+            // Update or push new checkpoint
+            if (last._blockNumber == key) {
+                _unsafeAccess(self, pos - 1)._value = value;
+            } else {
+                self.push(Checkpoint({_blockNumber: key, _value: value}));
+            }
+            return (last._value, value);
+        } else {
+            self.push(Checkpoint({_blockNumber: key, _value: value}));
+            return (0, value);
+        }
+    }
+
+    /**
+     * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or `high` if there is none.
+     * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`.
+     *
+     * WARNING: `high` should not be greater than the array's length.
+     */
+    function _upperBinaryLookup(
+        Checkpoint[] storage self,
+        uint32 key,
+        uint256 low,
+        uint256 high
+    ) private view returns (uint256) {
+        while (low < high) {
+            uint256 mid = Math.average(low, high);
+            if (_unsafeAccess(self, mid)._blockNumber > key) {
+                high = mid;
+            } else {
+                low = mid + 1;
+            }
+        }
+        return high;
+    }
+
+    /**
+     * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or `high` if there is none.
+     * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`.
+     *
+     * WARNING: `high` should not be greater than the array's length.
+     */
+    function _lowerBinaryLookup(
+        Checkpoint[] storage self,
+        uint32 key,
+        uint256 low,
+        uint256 high
+    ) private view returns (uint256) {
+        while (low < high) {
+            uint256 mid = Math.average(low, high);
+            if (_unsafeAccess(self, mid)._blockNumber < key) {
+                low = mid + 1;
+            } else {
+                high = mid;
+            }
+        }
+        return high;
+    }
+
+    function _unsafeAccess(Checkpoint[] storage self, uint256 pos) private view returns (Checkpoint storage result) {
+        assembly {
+            mstore(0, self.slot)
+            result.slot := add(keccak256(0, 0x20), pos)
+        }
+    }
+
+    struct Trace224 {
+        Checkpoint224[] _checkpoints;
+    }
+
+    struct Checkpoint224 {
+        uint32 _key;
+        uint224 _value;
+    }
+
+    /**
+     * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints.
+     */
+    function latest(Trace224 storage self) internal view returns (uint224) {
+        uint256 pos = self._checkpoints.length;
+        return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
+    }
+
+    /**
+     * @dev Pushes a (`key`, `value`) pair into a Trace224 so that it is stored as the checkpoint.
+     *
+     * Returns previous value and new value.
+     */
+    function push(
+        Trace224 storage self,
+        uint32 key,
+        uint224 value
+    ) internal returns (uint224, uint224) {
+        return _insert(self._checkpoints, key, value);
+    }
+
+    /**
+     * @dev Returns the value in the oldest checkpoint with key greater or equal than the search key, or zero if there is none.
+     */
+    function lowerLookup(Trace224 storage self, uint32 key) internal view returns (uint224) {
+        uint256 length = self._checkpoints.length;
+        uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, length);
+        return pos == length ? 0 : _unsafeAccess(self._checkpoints, pos)._value;
+    }
+
+    /**
+     * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key.
+     */
+    function upperLookup(Trace224 storage self, uint32 key) internal view returns (uint224) {
+        uint256 length = self._checkpoints.length;
+        uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, length);
+        return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
+    }
+
+    /**
+     * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key (similarly to
+     * {upperLookup}), optimized for the case when the search key is known to be recent.
+     */
+    function upperLookupRecent(Trace224 storage self, uint32 key) internal view returns (uint224) {
+        uint256 length = self._checkpoints.length;
+        uint256 offset = 1;
+
+        while (offset <= length && _unsafeAccess(self._checkpoints, length - offset)._key > key) {
+            offset <<= 1;
+        }
+
+        uint256 low = offset < length ? length - offset : 0;
+        uint256 high = length - (offset >> 1);
+        uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high);
+
+        return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
+    }
+
+    /**
+     * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint,
+     * or by updating the last one.
+     */
+    function _insert(
+        Checkpoint224[] storage self,
+        uint32 key,
+        uint224 value
+    ) private returns (uint224, uint224) {
+        uint256 pos = self.length;
+
+        if (pos > 0) {
+            // Copying to memory is important here.
+            Checkpoint224 memory last = _unsafeAccess(self, pos - 1);
+
+            // Checkpoints keys must be increasing.
+            require(last._key <= key, "Checkpoint: invalid key");
+
+            // Update or push new checkpoint
+            if (last._key == key) {
+                _unsafeAccess(self, pos - 1)._value = value;
+            } else {
+                self.push(Checkpoint224({_key: key, _value: value}));
+            }
+            return (last._value, value);
+        } else {
+            self.push(Checkpoint224({_key: key, _value: value}));
+            return (0, value);
+        }
+    }
+
+    /**
+     * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or `high` if there is none.
+     * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`.
+     *
+     * WARNING: `high` should not be greater than the array's length.
+     */
+    function _upperBinaryLookup(
+        Checkpoint224[] storage self,
+        uint32 key,
+        uint256 low,
+        uint256 high
+    ) private view returns (uint256) {
+        while (low < high) {
+            uint256 mid = Math.average(low, high);
+            if (_unsafeAccess(self, mid)._key > key) {
+                high = mid;
+            } else {
+                low = mid + 1;
+            }
+        }
+        return high;
+    }
+
+    /**
+     * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or `high` if there is none.
+     * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`.
+     *
+     * WARNING: `high` should not be greater than the array's length.
+     */
+    function _lowerBinaryLookup(
+        Checkpoint224[] storage self,
+        uint32 key,
+        uint256 low,
+        uint256 high
+    ) private view returns (uint256) {
+        while (low < high) {
+            uint256 mid = Math.average(low, high);
+            if (_unsafeAccess(self, mid)._key < key) {
+                low = mid + 1;
+            } else {
+                high = mid;
+            }
+        }
+        return high;
+    }
+
+    function _unsafeAccess(Checkpoint224[] storage self, uint256 pos)
+        private
+        view
+        returns (Checkpoint224 storage result)
+    {
+        assembly {
+            mstore(0, self.slot)
+            result.slot := add(keccak256(0, 0x20), pos)
+        }
+    }
+
+    struct Trace160 {
+        Checkpoint160[] _checkpoints;
+    }
+
+    struct Checkpoint160 {
+        uint96 _key;
+        uint160 _value;
+    }
+
+    /**
+     * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints.
+     */
+    function latest(Trace160 storage self) internal view returns (uint160) {
+        uint256 pos = self._checkpoints.length;
+        return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
+    }
+
+    /**
+     * @dev Pushes a (`key`, `value`) pair into a Trace160 so that it is stored as the checkpoint.
+     *
+     * Returns previous value and new value.
+     */
+    function push(
+        Trace160 storage self,
+        uint96 key,
+        uint160 value
+    ) internal returns (uint160, uint160) {
+        return _insert(self._checkpoints, key, value);
+    }
+
+    /**
+     * @dev Returns the value in the oldest checkpoint with key greater or equal than the search key, or zero if there is none.
+     */
+    function lowerLookup(Trace160 storage self, uint96 key) internal view returns (uint160) {
+        uint256 length = self._checkpoints.length;
+        uint256 pos = _lowerBinaryLookup(self._checkpoints, key, 0, length);
+        return pos == length ? 0 : _unsafeAccess(self._checkpoints, pos)._value;
+    }
+
+    /**
+     * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key.
+     */
+    function upperLookup(Trace160 storage self, uint96 key) internal view returns (uint160) {
+        uint256 length = self._checkpoints.length;
+        uint256 pos = _upperBinaryLookup(self._checkpoints, key, 0, length);
+        return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
+    }
+
+    /**
+     * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key (similarly to
+     * {upperLookup}), optimized for the case when the search key is known to be recent.
+     */
+    function upperLookupRecent(Trace160 storage self, uint96 key) internal view returns (uint160) {
+        uint256 length = self._checkpoints.length;
+        uint256 offset = 1;
+
+        while (offset <= length && _unsafeAccess(self._checkpoints, length - offset)._key > key) {
+            offset <<= 1;
+        }
+
+        uint256 low = offset < length ? length - offset : 0;
+        uint256 high = length - (offset >> 1);
+        uint256 pos = _upperBinaryLookup(self._checkpoints, key, low, high);
+
+        return pos == 0 ? 0 : _unsafeAccess(self._checkpoints, pos - 1)._value;
+    }
+
+    /**
+     * @dev Pushes a (`key`, `value`) pair into an ordered list of checkpoints, either by inserting a new checkpoint,
+     * or by updating the last one.
+     */
+    function _insert(
+        Checkpoint160[] storage self,
+        uint96 key,
+        uint160 value
+    ) private returns (uint160, uint160) {
+        uint256 pos = self.length;
+
+        if (pos > 0) {
+            // Copying to memory is important here.
+            Checkpoint160 memory last = _unsafeAccess(self, pos - 1);
+
+            // Checkpoints keys must be increasing.
+            require(last._key <= key, "Checkpoint: invalid key");
+
+            // Update or push new checkpoint
+            if (last._key == key) {
+                _unsafeAccess(self, pos - 1)._value = value;
+            } else {
+                self.push(Checkpoint160({_key: key, _value: value}));
+            }
+            return (last._value, value);
+        } else {
+            self.push(Checkpoint160({_key: key, _value: value}));
+            return (0, value);
+        }
+    }
+
+    /**
+     * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or `high` if there is none.
+     * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`.
+     *
+     * WARNING: `high` should not be greater than the array's length.
+     */
+    function _upperBinaryLookup(
+        Checkpoint160[] storage self,
+        uint96 key,
+        uint256 low,
+        uint256 high
+    ) private view returns (uint256) {
+        while (low < high) {
+            uint256 mid = Math.average(low, high);
+            if (_unsafeAccess(self, mid)._key > key) {
+                high = mid;
+            } else {
+                low = mid + 1;
+            }
+        }
+        return high;
+    }
+
+    /**
+     * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or `high` if there is none.
+     * `low` and `high` define a section where to do the search, with inclusive `low` and exclusive `high`.
+     *
+     * WARNING: `high` should not be greater than the array's length.
+     */
+    function _lowerBinaryLookup(
+        Checkpoint160[] storage self,
+        uint96 key,
+        uint256 low,
+        uint256 high
+    ) private view returns (uint256) {
+        while (low < high) {
+            uint256 mid = Math.average(low, high);
+            if (_unsafeAccess(self, mid)._key < key) {
+                low = mid + 1;
+            } else {
+                high = mid;
+            }
+        }
+        return high;
+    }
+
+    function _unsafeAccess(Checkpoint160[] storage self, uint256 pos)
+        private
+        view
+        returns (Checkpoint160 storage result)
+    {
+        assembly {
+            mstore(0, self.slot)
+            result.slot := add(keccak256(0, 0x20), pos)
+        }
+    }
 }

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

@@ -1,5 +1,6 @@
 // SPDX-License-Identifier: MIT
 // OpenZeppelin Contracts (last updated v4.7.0) (utils/math/SafeCast.sol)
+// This file was procedurally generated from scripts/generate/templates/SafeCast.js.
 
 pragma solidity ^0.8.0;
 

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

@@ -1,5 +1,6 @@
 // SPDX-License-Identifier: MIT
 // OpenZeppelin Contracts (last updated v4.7.0) (utils/structs/EnumerableMap.sol)
+// This file was procedurally generated from scripts/generate/templates/EnumerableMap.js.
 
 pragma solidity ^0.8.0;
 

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

@@ -1,5 +1,6 @@
 // SPDX-License-Identifier: MIT
 // OpenZeppelin Contracts (last updated v4.7.0) (utils/structs/EnumerableSet.sol)
+// This file was procedurally generated from scripts/generate/templates/EnumerableSet.js.
 
 pragma solidity ^0.8.0;
 

+ 21 - 11
scripts/generate/run.js

@@ -1,6 +1,8 @@
 #!/usr/bin/env node
 
+const cp = require('child_process');
 const fs = require('fs');
+const path = require('path');
 const format = require('./format-lines');
 
 function getVersion (path) {
@@ -15,22 +17,30 @@ function getVersion (path) {
 
 for (const [ file, template ] of Object.entries({
   // SafeCast
-  'utils/math/SafeCast.sol': './templates/SafeCast',
-  'mocks/SafeCastMock.sol': './templates/SafeCastMock',
+  'utils/math/SafeCast.sol': './templates/SafeCast.js',
+  'mocks/SafeCastMock.sol': './templates/SafeCastMock.js',
   // EnumerableSet
-  'utils/structs/EnumerableSet.sol': './templates/EnumerableSet',
-  'mocks/EnumerableSetMock.sol': './templates/EnumerableSetMock',
+  'utils/structs/EnumerableSet.sol': './templates/EnumerableSet.js',
+  'mocks/EnumerableSetMock.sol': './templates/EnumerableSetMock.js',
   // EnumerableMap
-  'utils/structs/EnumerableMap.sol': './templates/EnumerableMap',
-  'mocks/EnumerableMapMock.sol': './templates/EnumerableMapMock',
+  'utils/structs/EnumerableMap.sol': './templates/EnumerableMap.js',
+  'mocks/EnumerableMapMock.sol': './templates/EnumerableMapMock.js',
+  // Checkpoints
+  'utils/Checkpoints.sol': './templates/Checkpoints.js',
+  'mocks/CheckpointsMock.sol': './templates/CheckpointsMock.js',
 })) {
-  const path = `./contracts/${file}`;
-  const version = getVersion(path);
+  const script = path.relative(path.join(__dirname, '../..'), __filename);
+  const input = path.join(path.dirname(script), template);
+  const output = `./contracts/${file}`;
+  const version = getVersion(output);
   const content = format(
     '// SPDX-License-Identifier: MIT',
-    (version ? version + ` (${file})\n` : ''),
-    require(template).trimEnd(),
+    ...(version ? [ version + ` (${file})` ] : []),
+    `// This file was procedurally generated from ${input}.`,
+    '',
+    require(template),
   );
 
-  fs.writeFileSync(path, content);
+  fs.writeFileSync(output, content);
+  cp.execFileSync('prettier', ['--write', output]);
 }

+ 291 - 0
scripts/generate/templates/Checkpoints.js

@@ -0,0 +1,291 @@
+const format = require('../format-lines');
+
+const VALUE_SIZES = [ 224, 160 ];
+
+const header = `\
+pragma solidity ^0.8.0;
+
+import "./math/Math.sol";
+import "./math/SafeCast.sol";
+
+/**
+ * @dev This library defines the \`History\` struct, for checkpointing values as they change at different points in
+ * time, and later looking up past values by block number. See {Votes} as an example.
+ *
+ * To create a history of checkpoints define a variable type \`Checkpoints.History\` in your contract, and store a new
+ * checkpoint for the current transaction block using the {push} function.
+ *
+ * _Available since v4.5._
+ */
+`;
+
+const types = opts => `\
+struct ${opts.historyTypeName} {
+    ${opts.checkpointTypeName}[] ${opts.checkpointFieldName};
+}
+
+struct ${opts.checkpointTypeName} {
+    ${opts.keyTypeName} ${opts.keyFieldName};
+    ${opts.valueTypeName} ${opts.valueFieldName};
+}
+`;
+
+/* eslint-disable max-len */
+const operations = opts => `\
+/**
+ * @dev Returns the value in the most recent checkpoint, or zero if there are no checkpoints.
+ */
+function latest(${opts.historyTypeName} storage self) internal view returns (${opts.valueTypeName}) {
+    uint256 pos = self.${opts.checkpointFieldName}.length;
+    return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
+}
+
+/**
+ * @dev Pushes a (\`key\`, \`value\`) pair into a ${opts.historyTypeName} so that it is stored as the checkpoint.
+ *
+ * Returns previous value and new value.
+ */
+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);
+}
+
+/**
+ * @dev Returns the value in the oldest checkpoint with key greater or equal than the search key, or zero if there is none.
+ */
+function lowerLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
+    uint256 length = self.${opts.checkpointFieldName}.length;
+    uint256 pos = _lowerBinaryLookup(self.${opts.checkpointFieldName}, key, 0, length);
+    return pos == length ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos).${opts.valueFieldName};
+}
+
+/**
+ * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key.
+ */
+function upperLookup(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
+    uint256 length = self.${opts.checkpointFieldName}.length;
+    uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, length);
+    return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
+}
+
+/**
+ * @dev Returns the value in the most recent checkpoint with key lower or equal than the search key (similarly to
+ * {upperLookup}), optimized for the case when the search key is known to be recent.
+ */
+function upperLookupRecent(${opts.historyTypeName} storage self, ${opts.keyTypeName} key) internal view returns (${opts.valueTypeName}) {
+    uint256 length = self.${opts.checkpointFieldName}.length;
+    uint256 offset = 1;
+
+    while (offset <= length && _unsafeAccess(self.${opts.checkpointFieldName}, length - offset).${opts.keyFieldName} > key) {
+        offset <<= 1;
+    }
+
+    uint256 low = offset < length ? length - offset : 0;
+    uint256 high = length - (offset >> 1);
+    uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, low, high);
+
+    return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
+}
+`;
+
+const legacyOperations = opts => `\
+/**
+ * @dev Returns the value in the latest checkpoint, or zero if there are no checkpoints.
+ */
+function latest(${opts.historyTypeName} storage self) internal view returns (uint256) {
+    uint256 pos = self.${opts.checkpointFieldName}.length;
+    return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
+}
+
+/**
+ * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one
+ * before it is returned, or zero otherwise.
+ */
+function getAtBlock(${opts.historyTypeName} storage self, uint256 blockNumber) internal view returns (uint256) {
+    require(blockNumber < block.number, "Checkpoints: block not yet mined");
+    uint32 key = SafeCast.toUint32(blockNumber);
+
+    uint256 length = self.${opts.checkpointFieldName}.length;
+    uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, 0, length);
+    return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
+}
+
+/**
+ * @dev Returns the value at a given block number. If a checkpoint is not available at that block, the closest one
+ * before it is returned, or zero otherwise. Similarly to {upperLookup} but optimized for the case when the search
+ * key is known to be recent.
+ */
+function getAtRecentBlock(${opts.historyTypeName} storage self, uint256 blockNumber) internal view returns (uint256) {
+    require(blockNumber < block.number, "Checkpoints: block not yet mined");
+    uint32 key = SafeCast.toUint32(blockNumber);
+
+    uint256 length = self.${opts.checkpointFieldName}.length;
+    uint256 offset = 1;
+
+    while (offset <= length && _unsafeAccess(self.${opts.checkpointFieldName}, length - offset).${opts.keyFieldName} > key) {
+        offset <<= 1;
+    }
+
+    uint256 low = offset < length ? length - offset : 0;
+    uint256 high = length - (offset >> 1);
+    uint256 pos = _upperBinaryLookup(self.${opts.checkpointFieldName}, key, low, high);
+
+    return pos == 0 ? 0 : _unsafeAccess(self.${opts.checkpointFieldName}, pos - 1).${opts.valueFieldName};
+}
+
+/**
+ * @dev Pushes a value onto a History so that it is stored as the checkpoint for the current block.
+ *
+ * Returns previous value and new value.
+ */
+function push(${opts.historyTypeName} storage self, uint256 value) internal returns (uint256, uint256) {
+    return _insert(self.${opts.checkpointFieldName}, SafeCast.toUint32(block.number), SafeCast.toUint224(value));
+}
+
+/**
+ * @dev Pushes a value onto a History, by updating the latest value using binary operation \`op\`. The new value will
+ * be set to \`op(latest, delta)\`.
+ *
+ * Returns previous value and new value.
+ */
+function push(
+    ${opts.historyTypeName} storage self,
+    function(uint256, uint256) view returns (uint256) op,
+    uint256 delta
+) internal returns (uint256, uint256) {
+    return push(self, op(latest(self), delta));
+}
+`;
+
+const helpers = opts => `\
+/**
+ * @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}) {
+    uint256 pos = self.length;
+
+    if (pos > 0) {
+        // Copying to memory is important here.
+        ${opts.checkpointTypeName} memory last = _unsafeAccess(self, pos - 1);
+
+        // Checkpoints keys must be increasing.
+        require(last.${opts.keyFieldName} <= key, "Checkpoint: invalid key");
+
+        // Update or push new checkpoint
+        if (last.${opts.keyFieldName} == key) {
+            _unsafeAccess(self, pos - 1).${opts.valueFieldName} = value;
+        } else {
+            self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
+        }
+        return (last.${opts.valueFieldName}, value);
+    } else {
+        self.push(${opts.checkpointTypeName}({${opts.keyFieldName}: key, ${opts.valueFieldName}: value}));
+        return (0, value);
+    }
+}
+
+/**
+ * @dev Return the index of the oldest checkpoint whose key is greater than the search key, or \`high\` if there is none.
+ * \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`.
+ *
+ * WARNING: \`high\` should not be greater than the array's length.
+ */
+function _upperBinaryLookup(
+    ${opts.checkpointTypeName}[] storage self,
+    ${opts.keyTypeName} key,
+    uint256 low,
+    uint256 high
+) private view returns (uint256) {
+    while (low < high) {
+        uint256 mid = Math.average(low, high);
+        if (_unsafeAccess(self, mid).${opts.keyFieldName} > key) {
+            high = mid;
+        } else {
+            low = mid + 1;
+        }
+    }
+    return high;
+}
+
+/**
+ * @dev Return the index of the oldest checkpoint whose key is greater or equal than the search key, or \`high\` if there is none.
+ * \`low\` and \`high\` define a section where to do the search, with inclusive \`low\` and exclusive \`high\`.
+ *
+ * WARNING: \`high\` should not be greater than the array's length.
+ */
+function _lowerBinaryLookup(
+    ${opts.checkpointTypeName}[] storage self,
+    ${opts.keyTypeName} key,
+    uint256 low,
+    uint256 high
+) private view returns (uint256) {
+    while (low < high) {
+        uint256 mid = Math.average(low, high);
+        if (_unsafeAccess(self, mid).${opts.keyFieldName} < key) {
+            low = mid + 1;
+        } else {
+            high = mid;
+        }
+    }
+    return high;
+}
+
+function _unsafeAccess(${opts.checkpointTypeName}[] storage self, uint256 pos)
+    private
+    view
+    returns (${opts.checkpointTypeName} storage result)
+{
+    assembly {
+        mstore(0, self.slot)
+        result.slot := add(keccak256(0, 0x20), pos)
+    }
+}
+`;
+/* eslint-enable max-len */
+
+// OPTIONS
+const defaultOpts = (size) => ({
+  historyTypeName: `Trace${size}`,
+  checkpointTypeName: `Checkpoint${size}`,
+  checkpointFieldName: '_checkpoints',
+  keyTypeName: `uint${256 - size}`,
+  keyFieldName: '_key',
+  valueTypeName: `uint${size}`,
+  valueFieldName: '_value',
+});
+
+const OPTS = VALUE_SIZES.map(size => defaultOpts(size));
+
+const LEGACY_OPTS = {
+  ...defaultOpts(224),
+  historyTypeName: 'History',
+  checkpointTypeName: 'Checkpoint',
+  keyFieldName: '_blockNumber',
+};
+
+// GENERATE
+module.exports = format(
+  header.trimEnd(),
+  'library Checkpoints {',
+  [
+    // Legacy types & functions
+    types(LEGACY_OPTS),
+    legacyOperations(LEGACY_OPTS),
+    helpers(LEGACY_OPTS),
+    // New flavors
+    ...OPTS.flatMap(opts => [
+      types(opts),
+      operations(opts),
+      helpers(opts),
+    ]),
+  ],
+  '}',
+);

+ 76 - 0
scripts/generate/templates/CheckpointsMock.js

@@ -0,0 +1,76 @@
+const format = require('../format-lines');
+
+const VALUE_SIZES = [ 224, 160 ];
+
+const header = `\
+pragma solidity ^0.8.0;
+
+import "../utils/Checkpoints.sol";
+`;
+
+const legacy = () => `\
+contract CheckpointsMock {
+    using Checkpoints for Checkpoints.History;
+
+    Checkpoints.History private _totalCheckpoints;
+
+    function latest() public view returns (uint256) {
+        return _totalCheckpoints.latest();
+    }
+
+    function push(uint256 value) public returns (uint256, uint256) {
+        return _totalCheckpoints.push(value);
+    }
+
+    function getAtBlock(uint256 blockNumber) public view returns (uint256) {
+        return _totalCheckpoints.getAtBlock(blockNumber);
+    }
+
+    function getAtRecentBlock(uint256 blockNumber) public view returns (uint256) {
+        return _totalCheckpoints.getAtRecentBlock(blockNumber);
+    }
+
+    function length() public view returns (uint256) {
+        return _totalCheckpoints._checkpoints.length;
+    }
+}
+`;
+
+const checkpoint = length => `\
+contract Checkpoints${length}Mock {
+    using Checkpoints for Checkpoints.Trace${length};
+
+    Checkpoints.Trace${length} private _totalCheckpoints;
+
+    function latest() public view returns (uint${length}) {
+        return _totalCheckpoints.latest();
+    }
+
+    function push(uint${256 - length} key, uint${length} value) public returns (uint${length}, uint${length}) {
+        return _totalCheckpoints.push(key, value);
+    }
+
+    function lowerLookup(uint${256 - length} key) public view returns (uint${length}) {
+        return _totalCheckpoints.lowerLookup(key);
+    }
+
+    function upperLookup(uint${256 - length} key) public view returns (uint${length}) {
+        return _totalCheckpoints.upperLookup(key);
+    }
+
+    function upperLookupRecent(uint${256 - length} key) public view returns (uint224) {
+        return _totalCheckpoints.upperLookupRecent(key);
+    }
+
+    function length() public view returns (uint256) {
+        return _totalCheckpoints._checkpoints.length;
+    }
+}
+`;
+
+// GENERATE
+module.exports = format(
+  header,
+  legacy(),
+  ...VALUE_SIZES.map(checkpoint),
+);

+ 3 - 3
scripts/generate/templates/SafeCast.js

@@ -159,10 +159,10 @@ module.exports = format(
   header.trimEnd(),
   'library SafeCast {',
   [
-    ...LENGTHS.map(size => toUintDownCast(size)),
+    ...LENGTHS.map(toUintDownCast),
     toUint(256),
-    ...LENGTHS.map(size => toIntDownCast(size)),
-    toInt(256).trimEnd(),
+    ...LENGTHS.map(toIntDownCast),
+    toInt(256),
   ],
   '}',
 );

+ 2 - 2
scripts/generate/templates/SafeCastMock.js

@@ -42,9 +42,9 @@ module.exports = format(
     'using SafeCast for int256;',
     '',
     toUint(256),
-    ...LENGTHS.map(size => toUintDownCast(size)),
+    ...LENGTHS.map(toUintDownCast),
     toInt(256),
-    ...LENGTHS.map(size => toIntDownCast(size)),
+    ...LENGTHS.map(toIntDownCast),
   ].flatMap(fn => fn.split('\n')).slice(0, -1),
   '}',
 );

+ 23 - 5
test/utils/Arrays.test.js

@@ -2,7 +2,9 @@ require('@openzeppelin/test-helpers');
 
 const { expect } = require('chai');
 
-const ArraysImpl = artifacts.require('ArraysImpl');
+const AddressArraysMock = artifacts.require('AddressArraysMock');
+const Bytes32ArraysMock = artifacts.require('Bytes32ArraysMock');
+const Uint256ArraysMock = artifacts.require('Uint256ArraysMock');
 
 contract('Arrays', function (accounts) {
   describe('findUpperBound', function () {
@@ -10,7 +12,7 @@ contract('Arrays', function (accounts) {
       const EVEN_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
 
       beforeEach(async function () {
-        this.arrays = await ArraysImpl.new(EVEN_ELEMENTS_ARRAY);
+        this.arrays = await Uint256ArraysMock.new(EVEN_ELEMENTS_ARRAY);
       });
 
       it('returns correct index for the basic case', async function () {
@@ -38,7 +40,7 @@ contract('Arrays', function (accounts) {
       const ODD_ELEMENTS_ARRAY = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21];
 
       beforeEach(async function () {
-        this.arrays = await ArraysImpl.new(ODD_ELEMENTS_ARRAY);
+        this.arrays = await Uint256ArraysMock.new(ODD_ELEMENTS_ARRAY);
       });
 
       it('returns correct index for the basic case', async function () {
@@ -66,7 +68,7 @@ contract('Arrays', function (accounts) {
       const WITH_GAP_ARRAY = [11, 12, 13, 14, 15, 20, 21, 22, 23, 24];
 
       beforeEach(async function () {
-        this.arrays = await ArraysImpl.new(WITH_GAP_ARRAY);
+        this.arrays = await Uint256ArraysMock.new(WITH_GAP_ARRAY);
       });
 
       it('returns index of first element in next filled range', async function () {
@@ -76,7 +78,7 @@ contract('Arrays', function (accounts) {
 
     context('Empty array', function () {
       beforeEach(async function () {
-        this.arrays = await ArraysImpl.new([]);
+        this.arrays = await Uint256ArraysMock.new([]);
       });
 
       it('always returns 0 for empty array', async function () {
@@ -84,4 +86,20 @@ contract('Arrays', function (accounts) {
       });
     });
   });
+
+  describe('unsafeAccess', function () {
+    for (const { type, artifact, elements } of [
+      { type: 'address', artifact: AddressArraysMock, elements: Array(10).fill().map(() => web3.utils.randomHex(20)) },
+      { type: 'bytes32', artifact: Bytes32ArraysMock, elements: Array(10).fill().map(() => web3.utils.randomHex(32)) },
+      { type: 'uint256', artifact: Uint256ArraysMock, elements: Array(10).fill().map(() => web3.utils.randomHex(32)) },
+    ]) {
+      it(type, async function () {
+        const contract = await artifact.new(elements);
+
+        for (const i in elements) {
+          expect(await contract.unsafeAccess(i)).to.be.bignumber.equal(elements[i]);
+        }
+      });
+    }
+  });
 });

+ 137 - 53
test/utils/Checkpoints.test.js

@@ -4,71 +4,155 @@ const { expect } = require('chai');
 
 const { batchInBlock } = require('../helpers/txpool');
 
-const CheckpointsImpl = artifacts.require('CheckpointsImpl');
+const CheckpointsMock = artifacts.require('CheckpointsMock');
+
+const first = (array) => array.length ? array[0] : undefined;
+const last = (array) => array.length ? array[array.length - 1] : undefined;
 
 contract('Checkpoints', function (accounts) {
-  beforeEach(async function () {
-    this.checkpoint = await CheckpointsImpl.new();
-  });
+  describe('History checkpoints', function () {
+    beforeEach(async function () {
+      this.checkpoint = await CheckpointsMock.new();
+    });
 
-  describe('without checkpoints', function () {
-    it('returns zero as latest value', async function () {
-      expect(await this.checkpoint.latest()).to.be.bignumber.equal('0');
+    describe('without checkpoints', function () {
+      it('returns zero as latest value', async function () {
+        expect(await this.checkpoint.latest()).to.be.bignumber.equal('0');
+      });
+
+      it('returns zero as past value', async function () {
+        await time.advanceBlock();
+        expect(await this.checkpoint.getAtBlock(await web3.eth.getBlockNumber() - 1)).to.be.bignumber.equal('0');
+        expect(await this.checkpoint.getAtRecentBlock(await web3.eth.getBlockNumber() - 1)).to.be.bignumber.equal('0');
+      });
     });
 
-    it('returns zero as past value', async function () {
-      await time.advanceBlock();
-      expect(await this.checkpoint.getAtBlock(await web3.eth.getBlockNumber() - 1)).to.be.bignumber.equal('0');
+    describe('with checkpoints', function () {
+      beforeEach('pushing checkpoints', async function () {
+        this.tx1 = await this.checkpoint.push(1);
+        this.tx2 = await this.checkpoint.push(2);
+        await time.advanceBlock();
+        this.tx3 = await this.checkpoint.push(3);
+        await time.advanceBlock();
+        await time.advanceBlock();
+      });
+
+      it('returns latest value', async function () {
+        expect(await this.checkpoint.latest()).to.be.bignumber.equal('3');
+      });
+
+      for (const fn of [ 'getAtBlock(uint256)', 'getAtRecentBlock(uint256)' ]) {
+        describe(`lookup: ${fn}`, function () {
+          it('returns past values', async function () {
+            expect(await this.checkpoint.methods[fn](this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
+            expect(await this.checkpoint.methods[fn](this.tx1.receipt.blockNumber)).to.be.bignumber.equal('1');
+            expect(await this.checkpoint.methods[fn](this.tx2.receipt.blockNumber)).to.be.bignumber.equal('2');
+            // Block with no new checkpoints
+            expect(await this.checkpoint.methods[fn](this.tx2.receipt.blockNumber + 1)).to.be.bignumber.equal('2');
+            expect(await this.checkpoint.methods[fn](this.tx3.receipt.blockNumber)).to.be.bignumber.equal('3');
+            expect(await this.checkpoint.methods[fn](this.tx3.receipt.blockNumber + 1)).to.be.bignumber.equal('3');
+          });
+          it('reverts if block number >= current block', async function () {
+            await expectRevert(
+              this.checkpoint.methods[fn](await web3.eth.getBlockNumber()),
+              'Checkpoints: block not yet mined',
+            );
+
+            await expectRevert(
+              this.checkpoint.methods[fn](await web3.eth.getBlockNumber() + 1),
+              'Checkpoints: block not yet mined',
+            );
+          });
+        });
+      }
+
+      it('multiple checkpoints in the same block', async function () {
+        const lengthBefore = await this.checkpoint.length();
+
+        await batchInBlock([
+          () => this.checkpoint.push(8, { gas: 100000 }),
+          () => this.checkpoint.push(9, { gas: 100000 }),
+          () => this.checkpoint.push(10, { gas: 100000 }),
+        ]);
+
+        expect(await this.checkpoint.length()).to.be.bignumber.equal(lengthBefore.addn(1));
+        expect(await this.checkpoint.latest()).to.be.bignumber.equal('10');
+      });
     });
   });
 
-  describe('with checkpoints', function () {
-    beforeEach('pushing checkpoints', async function () {
-      this.tx1 = await this.checkpoint.push(1);
-      this.tx2 = await this.checkpoint.push(2);
-      await time.advanceBlock();
-      this.tx3 = await this.checkpoint.push(3);
-      await time.advanceBlock();
-      await time.advanceBlock();
-    });
+  for (const length of [160, 224]) {
+    describe(`Trace${length}`, function () {
+      beforeEach(async function () {
+        this.contract = await artifacts.require(`Checkpoints${length}Mock`).new();
+      });
 
-    it('returns latest value', async function () {
-      expect(await this.checkpoint.latest()).to.be.bignumber.equal('3');
-    });
+      describe('without checkpoints', function () {
+        it('returns zero as latest value', async function () {
+          expect(await this.contract.latest()).to.be.bignumber.equal('0');
+        });
 
-    it('returns past values', async function () {
-      expect(await this.checkpoint.getAtBlock(this.tx1.receipt.blockNumber - 1)).to.be.bignumber.equal('0');
-      expect(await this.checkpoint.getAtBlock(this.tx1.receipt.blockNumber)).to.be.bignumber.equal('1');
-      expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber)).to.be.bignumber.equal('2');
-      // Block with no new checkpoints
-      expect(await this.checkpoint.getAtBlock(this.tx2.receipt.blockNumber + 1)).to.be.bignumber.equal('2');
-      expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber)).to.be.bignumber.equal('3');
-      expect(await this.checkpoint.getAtBlock(this.tx3.receipt.blockNumber + 1)).to.be.bignumber.equal('3');
-    });
+        it('lookup returns 0', async function () {
+          expect(await this.contract.lowerLookup(0)).to.be.bignumber.equal('0');
+          expect(await this.contract.upperLookup(0)).to.be.bignumber.equal('0');
+          expect(await this.contract.upperLookupRecent(0)).to.be.bignumber.equal('0');
+        });
+      });
 
-    it('reverts if block number >= current block', async function () {
-      await expectRevert(
-        this.checkpoint.getAtBlock(await web3.eth.getBlockNumber()),
-        'Checkpoints: block not yet mined',
-      );
+      describe('with checkpoints', function () {
+        beforeEach('pushing checkpoints', async function () {
+          this.checkpoints = [
+            { key: 2, value: '17' },
+            { key: 3, value: '42' },
+            { key: 5, value: '101' },
+            { key: 7, value: '23' },
+            { key: 11, value: '99' },
+          ];
+          for (const { key, value } of this.checkpoints) {
+            await this.contract.push(key, value);
+          }
+        });
 
-      await expectRevert(
-        this.checkpoint.getAtBlock(await web3.eth.getBlockNumber() + 1),
-        'Checkpoints: block not yet mined',
-      );
-    });
+        it('returns latest value', async function () {
+          expect(await this.contract.latest())
+            .to.be.bignumber.equal(last(this.checkpoints).value);
+        });
+
+        it('cannot push values in the past', async function () {
+          await expectRevert(this.contract.push(last(this.checkpoints).key - 1, '0'), 'Checkpoint: invalid key');
+        });
+
+        it('can update last value', async function () {
+          const newValue = '42';
+
+          // check length before the update
+          expect(await this.contract.length()).to.be.bignumber.equal(this.checkpoints.length.toString());
+
+          // update last key
+          await this.contract.push(last(this.checkpoints).key, newValue);
+          expect(await this.contract.latest()).to.be.bignumber.equal(newValue);
 
-    it('multiple checkpoints in the same block', async function () {
-      const lengthBefore = await this.checkpoint.length();
-      await batchInBlock([
-        () => this.checkpoint.push(8, { gas: 100000 }),
-        () => this.checkpoint.push(9, { gas: 100000 }),
-        () => this.checkpoint.push(10, { gas: 100000 }),
-      ]);
-      const lengthAfter = await this.checkpoint.length();
-
-      expect(lengthAfter.toNumber()).to.be.equal(lengthBefore.toNumber() + 1);
-      expect(await this.checkpoint.latest()).to.be.bignumber.equal('10');
+          // check that length did not change
+          expect(await this.contract.length()).to.be.bignumber.equal(this.checkpoints.length.toString());
+        });
+
+        it('lower lookup', async function () {
+          for (let i = 0; i < 14; ++i) {
+            const value = first(this.checkpoints.filter(x => i <= x.key))?.value || '0';
+
+            expect(await this.contract.lowerLookup(i)).to.be.bignumber.equal(value);
+          }
+        });
+
+        it('upper lookup', async function () {
+          for (let i = 0; i < 14; ++i) {
+            const value = last(this.checkpoints.filter(x => i >= x.key))?.value || '0';
+
+            expect(await this.contract.upperLookup(i)).to.be.bignumber.equal(value);
+            expect(await this.contract.upperLookupRecent(i)).to.be.bignumber.equal(value);
+          }
+        });
+      });
     });
-  });
+  }
 });