From 82002436961987e8b78249287f1bd51f1086fc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=90or=C4=91e=20Mijovi=C4=87?= Date: Thu, 26 Mar 2026 10:51:39 +0100 Subject: [PATCH] Add PagedArray implementation. --- src/MonadTest.sol | 26 +- src/utils/PagedArray.sol | 248 +++++++++++++ src/utils/README.md | 45 +++ test/utils/PagedArray.t.sol | 716 ++++++++++++++++++++++++++++++++++++ 4 files changed, 1034 insertions(+), 1 deletion(-) create mode 100644 src/utils/PagedArray.sol create mode 100644 src/utils/README.md create mode 100644 test/utils/PagedArray.t.sol diff --git a/src/MonadTest.sol b/src/MonadTest.sol index 102b943..6f6320a 100644 --- a/src/MonadTest.sol +++ b/src/MonadTest.sol @@ -7,6 +7,30 @@ import {MonadStdConstants} from "./MonadStdConstants.sol"; import {IMonadStaking} from "./interfaces/IMonadStaking.sol"; import {IReserveBalance} from "./interfaces/IReserveBalance.sol"; +interface Vm { + function expectRevert(bytes4 selector) external; + function expectRevert(bytes calldata revertData) external; + function expectRevert() external; + function assume(bool condition) external pure; +} + /// @title MonadTest /// @notice Convenience aggregate import/base for Monad std utilities in tests. -abstract contract MonadTest is MonadBase {} +abstract contract MonadTest is MonadBase { + Vm internal constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code"))))); + + function assertEq(uint256 a, uint256 b) internal pure { + require(a == b, "assertEq(uint256) failed"); + } + + function assertEq(uint256 a, uint256 b, string memory message) internal pure { + require(a == b, message); + } + + function assertEq(uint256[] memory a, uint256[] memory b) internal pure { + require(a.length == b.length, "assertEq(uint256[]): length mismatch"); + for (uint256 i; i < a.length; i++) { + require(a[i] == b[i], "assertEq(uint256[]): element mismatch"); + } + } +} diff --git a/src/utils/PagedArray.sol b/src/utils/PagedArray.sol new file mode 100644 index 0000000..fd3f20e --- /dev/null +++ b/src/utils/PagedArray.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +/// @title PagedArray +/// @notice A storage-efficient dynamic array library that aligns data to MIP-8 +/// page boundaries, ensuring all elements within a page are warm after +/// the first SLOAD. +/// @dev MIP-8 defines a page as 128 contiguous EVM words (4096 bytes). A slot +/// belongs to a page via: +/// +/// page_index(slot) = slot >> 7 +/// offset(slot) = slot & 0x7F +/// +/// Once any slot on a page is accessed, all subsequent SLOAD/SSTORE on +/// that page are charged at warm cost. This library exploits that property +/// by aligning the array base to a 128-slot page boundary: +/// +/// base = and(keccak256(arr.slot), not(0x7f)) — clears low 7 bits +/// +/// base + 0 → length +/// base + 1 → arr[0] +/// base + 2 → arr[1] +/// ... +/// base + N → arr[N-1] +/// +/// Crucially, the length is stored at `base + 0` — on the same page as +/// the data — rather than at `arr.slot` (as a native Solidity array would). +/// This is intentional: length and data are almost always accessed +/// together (bounds checks, iteration, push, pop), so co-locating them on +/// the same page means the first SLOAD warms the entire page, making all +/// subsequent length and data reads warm at no extra cold cost. +/// A native `uint256[]` stores its length at `arr.slot` and data at +/// `keccak256(arr.slot)`, which land on different pages and incur two +/// separate cold SLOAD charges. +/// +/// All elements within the first page (indices 0–126) share a single cold +/// SLOAD charge. Beyond 127 elements the array spills naturally into +/// subsequent pages with no special handling required. +/// +/// The `Array` struct uses a dummy `_ptr` field solely to anchor a unique +/// storage slot. Its value is never read or written by this library. +/// This avoids collision with native `uint256[]` Solidity arrays, which +/// store their length at the declared slot and data at `keccak256(slot)`. +/// +/// @custom:mip https://github.com/monad-crypto/MIPs/blob/main/MIPS/MIP-8.md +library PagedArray { + /// @notice Thrown when pop() is called on an array with no elements. + error EmptyArray(); + + /// @notice Thrown when pop(n) is called with `n` greater than the current length. + /// @param requested Number of elements requested to pop. + /// @param available Number of elements currently in the array. + error InsufficientElements(uint256 requested, uint256 available); + + /// @notice Thrown when get() is called with an index that is out of bounds. + /// @param index The index that was requested. + /// @param length The current length of the array. + error IndexOutOfBounds(uint256 index, uint256 length); + + /// @notice Handle to a page-aware array in contract storage. + /// @dev The `_ptr` field is never read or written — it exists only to + /// reserve a unique storage slot that `arr.slot` resolves to. + struct Array { + uint256 _ptr; + } + + function dataLocationAndLen(Array storage arr) internal view returns (uint256 baseSlot, uint256 length) { + assembly { + mstore(0x00, arr.slot) + baseSlot := and(keccak256(0x00, 0x20), not(0x7f)) + length := sload(baseSlot) + } + } + + /// @notice Returns the number of elements in the array. + /// @param arr Storage pointer to the array. + /// @return arrayLen Current number of elements. + function len(Array storage arr) internal view returns (uint256 arrayLen) { + (, arrayLen) = dataLocationAndLen(arr); + } + + /// @notice Appends a value to the end of the array. + /// @dev Increments the length stored at the page-aligned base slot and + /// writes `value` to `baseSlot + newLength`. Data is 1-indexed so + /// that index 0 never aliases the length slot. + /// @param arr Storage pointer to the array. + /// @param value Value to append. + function push(Array storage arr, uint256 value) internal { + (uint256 baseSlot, uint256 arrayLen) = dataLocationAndLen(arr); + assembly { + sstore(add(baseSlot, add(arrayLen, 1)), value) + sstore(baseSlot, add(arrayLen, 1)) + } + } + + /// @notice Appends multiple elements to the end of the array. + /// @dev Elements are written sequentially starting at `baseSlot + arrayLen + 1`. + /// A single length update is deferred to after the loop, saving repeated + /// SSTOREs to the length slot. + /// @param arr Storage pointer to the array. + /// @param values Memory array of values to append. + function push(Array storage arr, uint256[] memory values) internal { + (uint256 baseSlot, uint256 arrayLen) = dataLocationAndLen(arr); + assembly { + let count := mload(values) + let ptr := add(values, 0x20) + let writeSlot := add(baseSlot, add(arrayLen, 1)) + for { let i := 0 } lt(i, count) { i := add(i, 1) } { + sstore(writeSlot, mload(ptr)) + writeSlot := add(writeSlot, 1) + ptr := add(ptr, 0x20) + } + sstore(baseSlot, add(arrayLen, count)) + } + } + + /// @notice Removes and returns the last element of the array. + /// @dev Clears the vacated storage slot to reclaim gas. Reverts with + /// `EmptyArray()` if the array is empty. + /// @param arr Storage pointer to the array. + /// @return poppedValue The value that was removed. + function pop(Array storage arr) internal returns (uint256 poppedValue) { + (uint256 baseSlot, uint256 arrayLen) = dataLocationAndLen(arr); + require(arrayLen > 0, EmptyArray()); + + assembly { + poppedValue := sload(add(baseSlot, arrayLen)) + sstore(add(baseSlot, arrayLen), 0) + sstore(baseSlot, sub(arrayLen, 1)) + } + } + + /// @notice Removes and returns the last `n` elements of the array in LIFO order. + /// @dev Clears each vacated slot to reclaim gas. Reverts if `n` exceeds the + /// current length. Elements are returned as a freshly allocated memory + /// array where index 0 is the last element of the storage array. + /// @param arr Storage pointer to the array. + /// @param n Number of elements to pop. + /// @return out Memory array of popped values in LIFO order. + function pop(Array storage arr, uint256 n) internal returns (uint256[] memory out) { + (uint256 baseSlot, uint256 arrayLen) = dataLocationAndLen(arr); + require(n <= arrayLen, InsufficientElements(n, arrayLen)); + out = new uint256[](n); + assembly { + let newLen := sub(arrayLen, n) + for { let i := 0 } lt(i, n) { i := add(i, 1) } { + let storageIdx := sub(arrayLen, i) // arrayLen, arrayLen-1, ... + let val := sload(add(baseSlot, storageIdx)) + mstore(add(out, shl(5, add(i, 1))), val) // out[i] = val + sstore(add(baseSlot, storageIdx), 0) + } + sstore(baseSlot, newLen) + } + } + + /// @notice Returns the element at `index` without modifying the array. + /// @dev Reverts with {IndexOutOfBounds} if `index` is out of bounds. + /// Elements are stored at `baseSlot + index + 1` (1-indexed). + /// @param arr Storage pointer to the array. + /// @param index Zero-based index of the element to retrieve. + /// @return value The element at the given index. + function get(Array storage arr, uint256 index) internal view returns (uint256 value) { + (uint256 baseSlot, uint256 arrayLen) = dataLocationAndLen(arr); + require(index < arrayLen, IndexOutOfBounds(index, arrayLen)); + assembly { + value := sload(add(baseSlot, add(index, 1))) + } + } + + /// @notice Overwrites elements starting at `start` with the given values. + /// @dev Reverts if the range [start, start + values.length) exceeds the + /// current length — this method only overwrites existing elements, + /// it does not extend the array. Use push() to append new elements. + /// @param arr Storage pointer to the array. + /// @param start Zero-based index of the first element to overwrite. + /// @param values Memory array of values to write. + function set(Array storage arr, uint256 start, uint256[] memory values) internal { + if (values.length == 0) { + return; + } + (uint256 baseSlot, uint256 arrayLen) = dataLocationAndLen(arr); + uint256 count = values.length; + require(start + count <= arrayLen, IndexOutOfBounds(start + count - 1, arrayLen)); + assembly { + let ptr := add(values, 0x20) + let writeSlot := add(baseSlot, add(start, 1)) + for { let i := 0 } lt(i, count) { i := add(i, 1) } { + sstore(writeSlot, mload(ptr)) + writeSlot := add(writeSlot, 1) + ptr := add(ptr, 0x20) + } + } + } + + /// @notice Copies the entire array from storage into a new memory array. + /// @dev Reads length once, then loads each element sequentially. Elements + /// within the same page are warm after the first SLOAD, so this is + /// cheap for arrays that fit within a single page (up to 126 elements). + /// @param arr Storage pointer to the array. + /// @return out A freshly allocated memory array containing all elements. + function toMemory(Array storage arr) internal view returns (uint256[] memory out) { + (uint256 baseSlot, uint256 arrayLen) = dataLocationAndLen(arr); + out = new uint256[](arrayLen); + assembly { + let ptr := add(out, 0x20) + for { let i := 0 } lt(i, arrayLen) { i := add(i, 1) } { + mstore(ptr, sload(add(baseSlot, add(i, 1)))) + ptr := add(ptr, 0x20) + } + } + } + + /// @notice Initializes the array from a memory array, replacing all existing contents. + /// @dev Reads the old length first, then clears any slots beyond the new length + /// to reclaim gas refunds. Slots within the new length are overwritten + /// directly without clearing first. Length is updated once at the end. + /// @param arr Storage pointer to the array. + /// @param values Memory array of values to initialize from. + function fromMemory(Array storage arr, uint256[] memory values) internal { + (uint256 baseSlot, uint256 oldLen) = dataLocationAndLen(arr); + uint256 newLen = values.length; + assembly { + // overwrite slots [1, newLen] with new values + let ptr := add(values, 0x20) + let writeSlot := add(baseSlot, 1) + for { let i := 0 } lt(i, newLen) { i := add(i, 1) } { + sstore(writeSlot, mload(ptr)) + writeSlot := add(writeSlot, 1) + ptr := add(ptr, 0x20) + } + + // clear stale slots (newLen, oldLen] + for { let i := add(newLen, 1) } iszero(gt(i, oldLen)) { i := add(i, 1) } { + sstore(add(baseSlot, i), 0) + } + + sstore(baseSlot, newLen) + } + } + + /// @notice Removes all elements from the array and clears their storage slots. + /// @dev Delegates to fromMemory with an empty array, reclaiming gas refunds + /// for all cleared slots. + /// @param arr Storage pointer to the array. + function clear(Array storage arr) internal { + fromMemory(arr, new uint256[](0)); + } +} diff --git a/src/utils/README.md b/src/utils/README.md new file mode 100644 index 0000000..59e2d5e --- /dev/null +++ b/src/utils/README.md @@ -0,0 +1,45 @@ +### MIP-8 Collections + +A set of data structures optimized for [MIP-8](https://github.com/monad-crypto/MIPs/blob/main/MIPS/MIP-8.md) page-aware storage. MIP-8 makes the page — 128 contiguous EVM slots — the atomic unit of storage I/O. Once any slot on a page is accessed, all subsequent reads and writes to that page within the transaction are warm. These collections align their storage layouts to page boundaries to exploit this property, co-locating related data so that a single cold SLOAD warms everything needed for common operations. + +#### `PagedArray` + +[`src/utils/PagedArray.sol`](src/utils/PagedArray.sol) is a dynamic array that stores its length and data on the same MIP-8 page, unlike a native Solidity `uint256[]` which scatters length and data across two separate pages. + +```solidity +import {PagedArray} from "monad-std/utils/PagedArray.sol"; + +contract Example { + using PagedArray for PagedArray.Array; + + PagedArray.Array private items; + + function add(uint256 value) external { items.push(value); } + function remove() external returns (uint256) { return items.pop(); } + function at(uint256 i) external view returns (uint256) { return items.get(i); } + function count() external view returns (uint256) { return items.len(); } +} +``` + +**API** + +| Function | Description | +|---|---| +| `push(value)` | Append a single element | +| `push(values[])` | Append multiple elements from memory | +| `pop()` | Remove and return the last element | +| `pop(n)` | Remove and return the last `n` elements | +| `get(index)` | Read element at index | +| `set(start, values[])` | Overwrite elements starting at index | +| `len()` | Return current length | +| `toMemory()` | Copy entire array to a memory array | +| `fromMemory(values[])` | Replace contents from a memory array | +| `clear()` | Remove all elements and reclaim storage | + +**Gas comparison** (Monad gas constants: 8,100 cold / 100 warm) + +| Operation | `uint256[]` | `PagedArray` | +|---|---|---| +| Read length + 1 element | 2 × 8,100 = 16,200 | 8,100 + 100 = 8,200 | +| Read length + 8 elements | 9 × 8,100 = 72,900 | 8,100 + 8 × 100 = 8,900 | +| Read length + 127 elements | 128 × 8,100 = 1,036,800 | 8,100 + 127 × 100 = 20,800 | \ No newline at end of file diff --git a/test/utils/PagedArray.t.sol b/test/utils/PagedArray.t.sol new file mode 100644 index 0000000..862c483 --- /dev/null +++ b/test/utils/PagedArray.t.sol @@ -0,0 +1,716 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.26; + +import {PagedArray} from "src/utils/PagedArray.sol"; +import {MonadTest} from "src/MonadTest.sol"; + +// --------------------------------------------------------------------------- +// Harness — wraps reverting calls so they happen at a lower call depth +// --------------------------------------------------------------------------- + +contract ArrayHarness { + using PagedArray for PagedArray.Array; + + PagedArray.Array public arr; + + function push(uint256 v) external { + arr.push(v); + } + + function pop() external returns (uint256) { + return arr.pop(); + } + + function popN(uint256 n) external returns (uint256[] memory) { + return arr.pop(n); + } + + function get(uint256 i) external view returns (uint256) { + return arr.get(i); + } + + function len() external view returns (uint256) { + return arr.len(); + } + + function set(uint256 start, uint256[] memory values) external { + arr.set(start, values); + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +contract PagedArrayTest is MonadTest { + using PagedArray for PagedArray.Array; + + ArrayHarness private h; + PagedArray.Array private arr; + PagedArray.Array private arr2; + + function setUp() public { + h = new ArrayHarness(); + } + + // ------------------------------------------------------------------------- + // len + // ------------------------------------------------------------------------- + + function test_len_initiallyZero() public view { + assertEq(arr.len(), 0); + } + + // ------------------------------------------------------------------------- + // push / len + // ------------------------------------------------------------------------- + + function test_push_incrementsLen() public { + arr.push(1); + assertEq(arr.len(), 1); + arr.push(2); + assertEq(arr.len(), 2); + } + + function test_push_firstElement() public { + arr.push(42); + assertEq(arr.get(0), 42); + } + + function test_push_multipleElements() public { + arr.push(10); + arr.push(20); + arr.push(30); + assertEq(arr.get(0), 10); + assertEq(arr.get(1), 20); + assertEq(arr.get(2), 30); + } + + function test_push_maxUint() public { + arr.push(type(uint256).max); + assertEq(arr.get(0), type(uint256).max); + } + + function test_push_zero() public { + arr.push(0); + assertEq(arr.len(), 1); + assertEq(arr.get(0), 0); + } + + // ------------------------------------------------------------------------- + // pop (single) + // ------------------------------------------------------------------------- + + function test_pop_returnsLastElement() public { + arr.push(10); + arr.push(20); + assertEq(arr.pop(), 20); + } + + function test_pop_decrementsLen() public { + arr.push(1); + arr.push(2); + arr.pop(); + assertEq(arr.len(), 1); + } + + function test_pop_clearsSlot() public { + arr.push(99); + arr.pop(); + arr.push(77); + assertEq(arr.get(0), 77); + } + + function test_pop_emptyReverts() public { + vm.expectRevert(PagedArray.EmptyArray.selector); + h.pop(); + } + + function test_pop_toEmpty() public { + arr.push(1); + arr.pop(); + assertEq(arr.len(), 0); + } + + function test_pop_toEmptyThenPushAgain() public { + arr.push(1); + arr.pop(); + arr.push(2); + assertEq(arr.len(), 1); + assertEq(arr.get(0), 2); + } + + // ------------------------------------------------------------------------- + // pop (n) + // ------------------------------------------------------------------------- + + function test_popN_returnsElementsInLIFOOrder() public { + arr.push(10); + arr.push(20); + arr.push(30); + uint256[] memory out = arr.pop(2); + assertEq(out.length, 2); + assertEq(out[0], 30); + assertEq(out[1], 20); + } + + function test_popN_decrementsLen() public { + arr.push(1); + arr.push(2); + arr.push(3); + arr.pop(2); + assertEq(arr.len(), 1); + } + + function test_popN_clearsSlots() public { + arr.push(1); + arr.push(2); + arr.pop(2); + arr.push(99); + assertEq(arr.get(0), 99); + assertEq(arr.len(), 1); + } + + function test_popN_zero() public { + arr.push(1); + uint256[] memory out = arr.pop(0); + assertEq(out.length, 0); + assertEq(arr.len(), 1); + } + + function test_popN_all() public { + arr.push(1); + arr.push(2); + arr.push(3); + uint256[] memory out = arr.pop(3); + assertEq(out.length, 3); + assertEq(arr.len(), 0); + } + + function test_popN_exceedsLenReverts() public { + h.push(2); + vm.expectRevert(abi.encodeWithSelector(PagedArray.InsufficientElements.selector, 3, 1)); + h.popN(3); + } + + function test_popN_emptyReverts() public { + vm.expectRevert(abi.encodeWithSelector(PagedArray.InsufficientElements.selector, 1, 0)); + h.popN(1); + } + + // ------------------------------------------------------------------------- + // get + // ------------------------------------------------------------------------- + + function test_get_outOfBoundsReverts() public { + h.push(1); + vm.expectRevert(abi.encodeWithSelector(PagedArray.IndexOutOfBounds.selector, 1, 1)); + h.get(1); + } + + function test_get_emptyReverts() public { + vm.expectRevert(abi.encodeWithSelector(PagedArray.IndexOutOfBounds.selector, 0, 0)); + h.get(0); + } + + // ------------------------------------------------------------------------- + // page boundary + // ------------------------------------------------------------------------- + + function test_crossPageBoundary_push() public { + for (uint256 i; i < 200; i++) { + arr.push(i); + } + assertEq(arr.len(), 200); + } + + function test_crossPageBoundary_get() public { + for (uint256 i; i < 200; i++) { + arr.push(i); + } + assertEq(arr.get(0), 0); + assertEq(arr.get(126), 126); + assertEq(arr.get(127), 127); + assertEq(arr.get(199), 199); + } + + function test_crossPageBoundary_pop() public { + for (uint256 i; i < 200; i++) { + arr.push(i); + } + assertEq(arr.pop(), 199); + assertEq(arr.len(), 199); + } + + function test_crossPageBoundary_popN() public { + for (uint256 i; i < 200; i++) { + arr.push(i); + } + uint256[] memory out = arr.pop(10); + assertEq(out.length, 10); + assertEq(out[0], 199); + assertEq(out[9], 190); + assertEq(arr.len(), 190); + } + + // ------------------------------------------------------------------------- + // no collision between two separate arrays + // ------------------------------------------------------------------------- + + function test_twoArraysNoCollision() public { + arr.push(1); + arr.push(2); + arr2.push(100); + arr2.push(200); + assertEq(arr.get(0), 1); + assertEq(arr.get(1), 2); + assertEq(arr2.get(0), 100); + assertEq(arr2.get(1), 200); + } + + // ------------------------------------------------------------------------- + // toMemory + // ------------------------------------------------------------------------- + + function test_toMemory_empty() public view { + assertEq(arr.toMemory().length, 0); + } + + function test_toMemory_singleElement() public { + arr.push(42); + uint256[] memory out = arr.toMemory(); + assertEq(out.length, 1); + assertEq(out[0], 42); + } + + function test_toMemory_multipleElements() public { + arr.push(10); + arr.push(20); + arr.push(30); + uint256[] memory out = arr.toMemory(); + assertEq(out[0], 10); + assertEq(out[1], 20); + assertEq(out[2], 30); + } + + function test_toMemory_doesNotMutateStorage() public { + arr.push(1); + arr.push(2); + arr.toMemory(); + assertEq(arr.len(), 2); + assertEq(arr.get(0), 1); + assertEq(arr.get(1), 2); + } + + function test_toMemory_isIndependentCopy() public { + arr.push(99); + uint256[] memory out = arr.toMemory(); + arr.pop(); + arr.push(77); + assertEq(out[0], 99); + } + + function test_toMemory_crossPageBoundary() public { + for (uint256 i; i < 200; i++) { + arr.push(i); + } + uint256[] memory out = arr.toMemory(); + assertEq(out.length, 200); + for (uint256 i; i < 200; i++) { + assertEq(out[i], i); + } + } + + // ------------------------------------------------------------------------- + // fuzz + // ------------------------------------------------------------------------- + + function testFuzz_pushThenGet(uint256[] calldata values) public { + vm.assume(values.length > 0 && values.length <= 500); + for (uint256 i; i < values.length; i++) { + arr.push(values[i]); + } + for (uint256 i; i < values.length; i++) { + assertEq(arr.get(i), values[i]); + } + } + + function testFuzz_pushThenPop(uint256[] calldata values) public { + vm.assume(values.length > 0 && values.length <= 500); + for (uint256 i; i < values.length; i++) { + arr.push(values[i]); + } + for (uint256 i = values.length; i > 0; i--) { + assertEq(arr.pop(), values[i - 1]); + } + assertEq(arr.len(), 0); + } + + function testFuzz_toMemory(uint256[] calldata values) public { + vm.assume(values.length <= 500); + for (uint256 i; i < values.length; i++) { + arr.push(values[i]); + } + uint256[] memory out = arr.toMemory(); + assertEq(out.length, values.length); + for (uint256 i; i < values.length; i++) { + assertEq(out[i], values[i]); + } + } + + function testFuzz_popN(uint256[] calldata values, uint256 n) public { + vm.assume(values.length > 0 && values.length <= 500); + vm.assume(n > 0 && n <= values.length); + for (uint256 i; i < values.length; i++) { + arr.push(values[i]); + } + uint256[] memory out = arr.pop(n); + assertEq(out.length, n); + assertEq(arr.len(), values.length - n); + for (uint256 i; i < n; i++) { + assertEq(out[i], values[values.length - 1 - i]); + } + } + + function test_pushN_crossPageBoundary() public { + uint256[] memory values = new uint256[](200); + for (uint256 i; i < 200; i++) { + values[i] = i; + } + arr.push(values); + assertEq(arr.len(), 200); + assertEq(arr.get(126), 126); + assertEq(arr.get(127), 127); + assertEq(arr.get(199), 199); + } + + function test_pushN_consistentWithSinglePush() public { + uint256[] memory values = new uint256[](3); + values[0] = 10; + values[1] = 20; + values[2] = 30; + arr.push(10); + arr.push(20); + arr.push(30); + arr2.push(values); + assertEq(arr.len(), arr2.len()); + for (uint256 i; i < 3; i++) { + assertEq(arr.get(i), arr2.get(i)); + } + } + + function testFuzz_pushN(uint256[] calldata values) public { + vm.assume(values.length <= 500); + uint256[] memory mem = new uint256[](values.length); + for (uint256 i; i < values.length; i++) { + mem[i] = values[i]; + } + arr.push(mem); + assertEq(arr.len(), values.length); + for (uint256 i; i < values.length; i++) { + assertEq(arr.get(i), values[i]); + } + } + + function testFuzz_pushN_consistentWithSinglePush(uint256[] calldata values) public { + vm.assume(values.length <= 500); + uint256[] memory mem = new uint256[](values.length); + for (uint256 i; i < values.length; i++) { + mem[i] = values[i]; + arr.push(values[i]); + } + arr2.push(mem); + assertEq(arr.len(), arr2.len()); + for (uint256 i; i < values.length; i++) { + assertEq(arr.get(i), arr2.get(i)); + } + } + + // ------------------------------------------------------------------------- + // set(start, values) + // ------------------------------------------------------------------------- + + function test_set_overwritesElements() public { + arr.push(1); + arr.push(2); + arr.push(3); + uint256[] memory values = new uint256[](2); + values[0] = 20; + values[1] = 30; + arr.set(1, values); + assertEq(arr.get(0), 1); + assertEq(arr.get(1), 20); + assertEq(arr.get(2), 30); + } + + function test_set_fromStart() public { + arr.push(1); + arr.push(2); + arr.push(3); + uint256[] memory values = new uint256[](3); + values[0] = 10; + values[1] = 20; + values[2] = 30; + arr.set(0, values); + assertEq(arr.get(0), 10); + assertEq(arr.get(1), 20); + assertEq(arr.get(2), 30); + } + + function test_set_singleElement() public { + arr.push(1); + arr.push(2); + arr.push(3); + uint256[] memory values = new uint256[](1); + values[0] = 99; + arr.set(1, values); + assertEq(arr.get(0), 1); + assertEq(arr.get(1), 99); + assertEq(arr.get(2), 3); + } + + function test_set_doesNotChangeLen() public { + arr.push(1); + arr.push(2); + arr.push(3); + uint256[] memory values = new uint256[](2); + values[0] = 10; + values[1] = 20; + arr.set(0, values); + assertEq(arr.len(), 3); + } + + function test_set_emptyValues() public { + arr.push(1); + arr.push(2); + arr.set(0, new uint256[](0)); + assertEq(arr.len(), 2); + assertEq(arr.get(0), 1); + assertEq(arr.get(1), 2); + } + + function test_set_crossPageBoundary() public { + for (uint256 i; i < 200; i++) { + arr.push(i); + } + uint256[] memory values = new uint256[](10); + for (uint256 i; i < 10; i++) { + values[i] = 999 + i; + } + arr.set(122, values); + for (uint256 i; i < 10; i++) { + assertEq(arr.get(122 + i), 999 + i); + } + assertEq(arr.get(121), 121); + assertEq(arr.get(132), 132); + } + + function test_set_outOfBoundsReverts() public { + h.push(1); + h.push(2); + uint256[] memory values = new uint256[](2); + values[0] = 10; + values[1] = 20; + vm.expectRevert(abi.encodeWithSelector(PagedArray.IndexOutOfBounds.selector, 2, 2)); + h.set(1, values); + } + + function test_set_emptyArrayReverts() public { + uint256[] memory values = new uint256[](1); + values[0] = 1; + vm.expectRevert(abi.encodeWithSelector(PagedArray.IndexOutOfBounds.selector, 0, 0)); + h.set(0, values); + } + + function testFuzz_set(uint256[] calldata initial, uint256[] calldata updates, uint256 start) public { + vm.assume(initial.length > 0 && initial.length <= 500); + vm.assume(updates.length > 0 && updates.length <= initial.length); + vm.assume(start <= initial.length - updates.length); + for (uint256 i; i < initial.length; i++) { + arr.push(initial[i]); + } + uint256[] memory mem = new uint256[](updates.length); + for (uint256 i; i < updates.length; i++) { + mem[i] = updates[i]; + } + arr.set(start, mem); + for (uint256 i; i < start; i++) { + assertEq(arr.get(i), initial[i]); + } + for (uint256 i; i < updates.length; i++) { + assertEq(arr.get(start + i), updates[i]); + } + for (uint256 i = start + updates.length; i < initial.length; i++) { + assertEq(arr.get(i), initial[i]); + } + assertEq(arr.len(), initial.length); + } + + // ------------------------------------------------------------------------- + // fromMemory + // ------------------------------------------------------------------------- + + function test_fromMemory_onEmptyArray() public { + uint256[] memory values = new uint256[](3); + values[0] = 10; + values[1] = 20; + values[2] = 30; + arr.fromMemory(values); + assertEq(arr.len(), 3); + assertEq(arr.get(0), 10); + assertEq(arr.get(1), 20); + assertEq(arr.get(2), 30); + } + + function test_fromMemory_replacesExistingElements() public { + arr.push(1); + arr.push(2); + arr.push(3); + uint256[] memory values = new uint256[](3); + values[0] = 10; + values[1] = 20; + values[2] = 30; + arr.fromMemory(values); + assertEq(arr.len(), 3); + assertEq(arr.get(0), 10); + assertEq(arr.get(1), 20); + assertEq(arr.get(2), 30); + } + + function test_fromMemory_shrinks_clearsStaleSlots() public { + arr.push(1); + arr.push(2); + arr.push(3); + uint256[] memory values = new uint256[](1); + values[0] = 99; + arr.fromMemory(values); + assertEq(arr.len(), 1); + assertEq(arr.get(0), 99); + arr.push(0); + arr.push(0); + assertEq(arr.get(1), 0); + assertEq(arr.get(2), 0); + } + + function test_fromMemory_grows() public { + arr.push(1); + uint256[] memory values = new uint256[](3); + values[0] = 10; + values[1] = 20; + values[2] = 30; + arr.fromMemory(values); + assertEq(arr.len(), 3); + assertEq(arr.get(0), 10); + assertEq(arr.get(1), 20); + assertEq(arr.get(2), 30); + } + + function test_fromMemory_empty_clearsAll() public { + arr.push(1); + arr.push(2); + arr.push(3); + arr.fromMemory(new uint256[](0)); + assertEq(arr.len(), 0); + arr.push(0); + arr.push(0); + arr.push(0); + assertEq(arr.get(0), 0); + assertEq(arr.get(1), 0); + assertEq(arr.get(2), 0); + } + + function test_fromMemory_crossPageBoundary() public { + for (uint256 i; i < 200; i++) { + arr.push(i); + } + uint256[] memory values = new uint256[](150); + for (uint256 i; i < 150; i++) { + values[i] = 999 + i; + } + arr.fromMemory(values); + assertEq(arr.len(), 150); + for (uint256 i; i < 150; i++) { + assertEq(arr.get(i), 999 + i); + } + arr.push(0); + assertEq(arr.get(150), 0); + } + + function testFuzz_fromMemory(uint256[] calldata initial, uint256[] calldata next) public { + vm.assume(initial.length <= 500 && next.length <= 500); + for (uint256 i; i < initial.length; i++) { + arr.push(initial[i]); + } + uint256[] memory mem = new uint256[](next.length); + for (uint256 i; i < next.length; i++) { + mem[i] = next[i]; + } + arr.fromMemory(mem); + assertEq(arr.len(), next.length); + for (uint256 i; i < next.length; i++) { + assertEq(arr.get(i), next[i]); + } + if (next.length < initial.length) { + uint256 staleCount = initial.length - next.length; + for (uint256 i; i < staleCount; i++) { + arr.push(0); + } + for (uint256 i; i < staleCount; i++) { + assertEq(arr.get(next.length + i), 0); + } + } + } + + // ------------------------------------------------------------------------- + // clear + // ------------------------------------------------------------------------- + + function test_clear_setsLenToZero() public { + arr.push(1); + arr.push(2); + arr.push(3); + arr.clear(); + assertEq(arr.len(), 0); + } + + function test_clear_clearsSlots() public { + arr.push(1); + arr.push(2); + arr.push(3); + arr.clear(); + arr.push(0); + arr.push(0); + arr.push(0); + assertEq(arr.get(0), 0); + assertEq(arr.get(1), 0); + assertEq(arr.get(2), 0); + } + + function test_clear_onEmptyArray() public { + arr.clear(); + assertEq(arr.len(), 0); + } + + function test_clear_thenPush() public { + arr.push(1); + arr.push(2); + arr.clear(); + arr.push(99); + assertEq(arr.len(), 1); + assertEq(arr.get(0), 99); + } + + function test_clear_crossPageBoundary() public { + for (uint256 i; i < 200; i++) { + arr.push(i); + } + arr.clear(); + assertEq(arr.len(), 0); + for (uint256 i; i < 200; i++) { + arr.push(0); + } + for (uint256 i; i < 200; i++) { + assertEq(arr.get(i), 0); + } + } +}