From 6614ddde35f20e5ce588399e3c46aa1a408c7c16 Mon Sep 17 00:00:00 2001 From: Wael Almattar Date: Fri, 4 Jul 2025 09:25:17 +0200 Subject: [PATCH 1/2] feat: add utility library FixedSizeMemoryStack --- contracts/utils/FixedSizeMemoryStack.sol | 102 +++++++++++++++++++ test/utils/FixedSizeMemoryStack.t.sol | 121 +++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 contracts/utils/FixedSizeMemoryStack.sol create mode 100644 test/utils/FixedSizeMemoryStack.t.sol diff --git a/contracts/utils/FixedSizeMemoryStack.sol b/contracts/utils/FixedSizeMemoryStack.sol new file mode 100644 index 00000000000..0196b27d44d --- /dev/null +++ b/contracts/utils/FixedSizeMemoryStack.sol @@ -0,0 +1,102 @@ +// 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; + unchecked { + 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(); + unchecked { + 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..1694d4cd8cb --- /dev/null +++ b/test/utils/FixedSizeMemoryStack.t.sol @@ -0,0 +1,121 @@ +// 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 testPeek() public { + FixedSizeMemoryStack.Stack memory stack = FixedSizeMemoryStack.init(10); + + vm.expectRevert(abi.encodeWithSelector(FixedSizeMemoryStack.StackUnderflow.selector)); + stack.peek(); + assertEq(stack._top, 0); + + 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); + } +} From 47db11b5e36512ff9c7cce79bb9a1930e50de115 Mon Sep 17 00:00:00 2001 From: Wael Almattar Date: Fri, 4 Jul 2025 09:36:33 +0200 Subject: [PATCH 2/2] docs: changeset --- .changeset/rude-eggs-smoke.md | 5 +++++ contracts/utils/FixedSizeMemoryStack.sol | 2 +- test/utils/FixedSizeMemoryStack.t.sol | 3 +-- 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 .changeset/rude-eggs-smoke.md 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 index 0196b27d44d..4c06fc62cae 100644 --- a/contracts/utils/FixedSizeMemoryStack.sol +++ b/contracts/utils/FixedSizeMemoryStack.sol @@ -1,4 +1,4 @@ -// SPDX‑License‑Identifier: MIT +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title FixedSizeMemoryStack diff --git a/test/utils/FixedSizeMemoryStack.t.sol b/test/utils/FixedSizeMemoryStack.t.sol index 1694d4cd8cb..77151292848 100644 --- a/test/utils/FixedSizeMemoryStack.t.sol +++ b/test/utils/FixedSizeMemoryStack.t.sol @@ -1,5 +1,4 @@ -// SPDX-License-Identifier: MIT - +// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import {Test} from "forge-std/Test.sol";