diff --git a/.changeset/rude-eggs-smoke.md b/.changeset/rude-eggs-smoke.md new file mode 100644 index 00000000000..7eddf4d5653 --- /dev/null +++ b/.changeset/rude-eggs-smoke.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +Added FixedSizeMemoryStack a gas‑efficient, in‑memory, fixed‑capacity stack diff --git a/contracts/utils/FixedSizeMemoryStack.sol b/contracts/utils/FixedSizeMemoryStack.sol new file mode 100644 index 00000000000..052c3ba0c4a --- /dev/null +++ b/contracts/utils/FixedSizeMemoryStack.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/// @title FixedSizeMemoryStack +/// @notice A gas‑efficient, in‑memory, fixed‑capacity stack (first in, last out) for arbitrary 32‑byte values. +/// @dev +/// * Designed for algorithmic helpers (sorting, DFS/BFS, expression evaluation, etc.). +/// * **Not** suitable for persisting data between external calls – memory is transient. +/// * Functions are `internal`+`pure` so that the optimizer can aggressively inline them. +/// * +/// * The structure is designed to perform the following operations with the corresponding complexities: +/// * * push (insert a value onto the stack): O(1) +/// * * pop (remove the top value from the stack): O(1) +/// * * peek (return the top value from the stack): O(1) +/// * * size (return the number of elements in the stack): O(1) +/// * * capacity (return the maximum number of elements the stack can hold): O(1) +/// * * isEmpty (return whether the stack is empty): O(1) +/// * * isFull (return whether the stack is at full capacity): O(1) +/// * * clear (remove all elements from the stack): O(1) +library FixedSizeMemoryStack { + /// @notice Pushed more items than the stack’s capacity. + /// @param capacity Maximum number of elements the stack can hold. + error StackOverflow(uint256 capacity); + + /// @notice Popped or peeked when the stack was empty. + error StackUnderflow(); + + /// @notice Attempted to create a stack with zero capacity. + error ZeroCapacity(); + + struct Stack { + bytes32[] _data; // pre‑allocated fixed‑length array + uint256 _top; // next insertion index + } + + /// @notice Initialise a new fixed‑size stack in memory. + /// @param maxSize The maximum number of elements the stack can hold (> 0). + /// @return stack A fully initialised, empty stack. + function init(uint256 maxSize) internal pure returns (Stack memory stack) { + if (maxSize == 0) revert ZeroCapacity(); + stack._data = new bytes32[](maxSize); + stack._top = 0; + } + + /// @notice Push a value onto the stack. + /// @param stack The stack to mutate (memory reference). + /// @param value The value to push. + function push(Stack memory stack, bytes32 value) internal pure { + uint256 t = stack._top; + if (t >= stack._data.length) revert StackOverflow(stack._data.length); + stack._data[t] = value; + stack._top = t + 1; + } + + /// @notice Pop the top value from the stack. + /// @param stack The stack to mutate (memory reference). + /// @return value The element removed from the stack. + function pop(Stack memory stack) internal pure returns (bytes32 value) { + uint256 t = stack._top; + if (t == 0) revert StackUnderflow(); + t -= 1; + value = stack._data[t]; + stack._top = t; + } + + /// @notice Return, but do **not** remove, the element on top of the stack. + function peek(Stack memory stack) internal pure returns (bytes32 value) { + uint256 t = stack._top; + if (t == 0) revert StackUnderflow(); + value = stack._data[t - 1]; + } + + /// @notice Current number of stored elements. + function size(Stack memory stack) internal pure returns (uint256) { + return stack._top; + } + + /// @notice Maximum number of elements the stack can hold. + function capacity(Stack memory stack) internal pure returns (uint256) { + return stack._data.length; + } + + /// @notice Whether the stack currently holds zero elements. + function isEmpty(Stack memory stack) internal pure returns (bool) { + return stack._top == 0; + } + + /// @notice Whether the stack is at full capacity. + function isFull(Stack memory stack) internal pure returns (bool) { + return stack._top == stack._data.length; + } + + /// @notice Reset the stack to empty **without** reallocating memory. + /// @dev Sets the logical size to zero; underlying array remains allocated. + function clear(Stack memory stack) internal pure { + stack._top = 0; + } +} diff --git a/test/utils/FixedSizeMemoryStack.t.sol b/test/utils/FixedSizeMemoryStack.t.sol new file mode 100644 index 00000000000..96bb29903d9 --- /dev/null +++ b/test/utils/FixedSizeMemoryStack.t.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {SymTest} from "halmos-cheatcodes/SymTest.sol"; +import {FixedSizeMemoryStack} from "../../contracts/utils/FixedSizeMemoryStack.sol"; + +contract FixedSizeMemoryStackTest is Test, SymTest { + using FixedSizeMemoryStack for FixedSizeMemoryStack.Stack; + + function testFuzzPush(bytes32 item) public pure { + FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10); + stack.push(item); + + assertEq(stack._top, 1); + assertEq(stack._data[0], item); + } + + /// forge-config: default.allow_internal_expect_revert = true + function testPop() public { + FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10); + + vm.expectRevert((FixedSizeMemoryStack.StackUnderflow.selector)); + stack.pop(); + + bytes32 item1 = svm.createBytes32("item1"); + bytes32 item2 = svm.createBytes32("item2"); + + stack.push(item1); + stack.push(item2); + + assertEq(stack.pop(), item2); + assertEq(stack._top, 1); + assertEq(stack.pop(), item1); + assertEq(stack._top, 0); + } + + /// forge-config: default.allow_internal_expect_revert = true + function testPeekStackUnderflow() public { + FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10); + + vm.expectRevert(abi.encodeWithSelector(FixedSizeMemoryStack.StackUnderflow.selector)); + stack.peek(); + assertEq(stack._top, 0); + } + + function testPeek() public { + FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10); + + stack.push(bytes32(uint256(1))); + stack.push(bytes32(uint256(2))); + + assertEq(stack.peek(), bytes32(uint256(2))); + assertEq(stack._top, 2); + } + + function testSize() public { + FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10); + + assertEq(stack.size(), 0); + + stack.push(bytes32(uint256(1))); + assertEq(stack.size(), 1); + + stack.push(bytes32(uint256(2))); + assertEq(stack.size(), 2); + + stack.pop(); + assertEq(stack.size(), 1); + + stack.pop(); + assertEq(stack.size(), 0); + } + + function testCapacity() public { + FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10); + + assertEq(stack.capacity(), 10); + } + + function testIsEmpty() public { + FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10); + + assertEq(stack.isEmpty(), true); + + stack.push(bytes32(uint256(1))); + assertEq(stack.isEmpty(), false); + + stack.pop(); + assertEq(stack.isEmpty(), true); + } + + function testIsFull() public { + FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10); + + assertEq(stack.isFull(), false); + + stack.push(bytes32(uint256(1))); + assertEq(stack.isFull(), false); + + for (uint256 i = 0; i < 9; i++) { + stack.push(bytes32(uint256(i))); + } + + assertEq(stack.isFull(), true); + + stack.pop(); + assertEq(stack.isFull(), false); + } + + function testClear() public { + FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10); + + stack.push(bytes32(uint256(1))); + stack.push(bytes32(uint256(2))); + + stack.clear(); + + assertEq(stack.size(), 0); + assertEq(stack.isEmpty(), true); + assertEq(stack.isFull(), false); + assertEq(stack._top, 0); + } +}