diff --git a/CHANGELOG.md b/CHANGELOG.md index 549165a1cc8..403579e4795 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/contracts/mocks/ArraysImpl.sol b/contracts/mocks/ArraysImpl.sol deleted file mode 100644 index f720524b808..00000000000 --- a/contracts/mocks/ArraysImpl.sol +++ /dev/null @@ -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); - } -} diff --git a/contracts/mocks/ArraysMock.sol b/contracts/mocks/ArraysMock.sol new file mode 100644 index 00000000000..2ea17a09fde --- /dev/null +++ b/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; + } +} diff --git a/contracts/mocks/CheckpointsImpl.sol b/contracts/mocks/CheckpointsImpl.sol deleted file mode 100644 index 14681ca40b9..00000000000 --- a/contracts/mocks/CheckpointsImpl.sol +++ /dev/null @@ -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; - } -} diff --git a/contracts/mocks/CheckpointsMock.sol b/contracts/mocks/CheckpointsMock.sol new file mode 100644 index 00000000000..591d2fc3029 --- /dev/null +++ b/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; + } +} diff --git a/contracts/mocks/EnumerableMapMock.sol b/contracts/mocks/EnumerableMapMock.sol index 96013957938..b60b1e6d97f 100644 --- a/contracts/mocks/EnumerableMapMock.sol +++ b/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; diff --git a/contracts/mocks/EnumerableSetMock.sol b/contracts/mocks/EnumerableSetMock.sol index 922ce46d250..f75f38af1ff 100644 --- a/contracts/mocks/EnumerableSetMock.sol +++ b/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; diff --git a/contracts/mocks/SafeCastMock.sol b/contracts/mocks/SafeCastMock.sol index 806ce12740f..12a0de6324c 100644 --- a/contracts/mocks/SafeCastMock.sol +++ b/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; diff --git a/contracts/utils/Arrays.sol b/contracts/utils/Arrays.sol index 0783614cd3e..a0c967e17c5 100644 --- a/contracts/utils/Arrays.sol +++ b/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(); + } } diff --git a/contracts/utils/Checkpoints.sol b/contracts/utils/Checkpoints.sol index 606098bcc27..793ea3bd160 100644 --- a/contracts/utils/Checkpoints.sol +++ b/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) + } + } } diff --git a/contracts/utils/math/SafeCast.sol b/contracts/utils/math/SafeCast.sol index a4e831faf1a..1412d6064fb 100644 --- a/contracts/utils/math/SafeCast.sol +++ b/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; diff --git a/contracts/utils/structs/EnumerableMap.sol b/contracts/utils/structs/EnumerableMap.sol index 5db9df44ef1..0ec40f9e7fd 100644 --- a/contracts/utils/structs/EnumerableMap.sol +++ b/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; diff --git a/contracts/utils/structs/EnumerableSet.sol b/contracts/utils/structs/EnumerableSet.sol index 1aff30c3907..1d8029e3ff4 100644 --- a/contracts/utils/structs/EnumerableSet.sol +++ b/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; diff --git a/scripts/generate/run.js b/scripts/generate/run.js index 1967520f499..60dd795d598 100755 --- a/scripts/generate/run.js +++ b/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]); } diff --git a/scripts/generate/templates/Checkpoints.js b/scripts/generate/templates/Checkpoints.js new file mode 100644 index 00000000000..e368299f377 --- /dev/null +++ b/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), + ]), + ], + '}', +); diff --git a/scripts/generate/templates/CheckpointsMock.js b/scripts/generate/templates/CheckpointsMock.js new file mode 100755 index 00000000000..6ce8e534b0b --- /dev/null +++ b/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), +); diff --git a/scripts/generate/templates/SafeCast.js b/scripts/generate/templates/SafeCast.js index 4792d410b2d..8cf174206bf 100755 --- a/scripts/generate/templates/SafeCast.js +++ b/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), ], '}', ); diff --git a/scripts/generate/templates/SafeCastMock.js b/scripts/generate/templates/SafeCastMock.js index 9bb64d2c766..196d9b4f100 100755 --- a/scripts/generate/templates/SafeCastMock.js +++ b/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), '}', ); diff --git a/test/utils/Arrays.test.js b/test/utils/Arrays.test.js index 67128fac26f..8c287ccfff7 100644 --- a/test/utils/Arrays.test.js +++ b/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]); + } + }); + } + }); }); diff --git a/test/utils/Checkpoints.test.js b/test/utils/Checkpoints.test.js index 9938dc35b04..af8a1a13bb9 100644 --- a/test/utils/Checkpoints.test.js +++ b/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); + } + }); + }); }); - }); + } });