From be5c60ac21b99284837e4db54424f70fe4a9acfa Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 15 May 2026 15:38:21 -0600 Subject: [PATCH 1/2] Add `values(start, end)` slice accessor to `DoubleEndedQueue` Mirrors the paginated `values` accessor in `EnumerableSet`. Clamps `end` to `length(deque)` and `start` to `end` (both with `Math.min`), then reads the slice through the wraparound-aware `_begin` offset. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/noisy-dragons-paint.md | 5 +++ contracts/utils/structs/DoubleEndedQueue.sol | 26 ++++++++++++++ test/utils/structs/DoubleEndedQueue.test.js | 36 ++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 .changeset/noisy-dragons-paint.md diff --git a/.changeset/noisy-dragons-paint.md b/.changeset/noisy-dragons-paint.md new file mode 100644 index 00000000000..92413abe841 --- /dev/null +++ b/.changeset/noisy-dragons-paint.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`DoubleEndedQueue`: Add `values(deque, start, end)` to return a slice of the queue as an array, mirroring the paginated `values` accessor in `EnumerableSet`. Out-of-bound values for `start` and `end` are clamped to the queue length. diff --git a/contracts/utils/structs/DoubleEndedQueue.sol b/contracts/utils/structs/DoubleEndedQueue.sol index 1d431d2edbe..2fc247a182b 100644 --- a/contracts/utils/structs/DoubleEndedQueue.sol +++ b/contracts/utils/structs/DoubleEndedQueue.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.20; +import {Math} from "../math/Math.sol"; import {Panic} from "../Panic.sol"; /** @@ -209,6 +210,31 @@ library DoubleEndedQueue { } } + /** + * @dev Return a slice of the queue in an array, with the first item at `start` (inclusive) and the last item at + * `end` (exclusive). Out-of-bound values for `start` and `end` are clamped to the queue length. + * + * WARNING: This operation will copy a portion of the storage to memory, which can be quite expensive. This is + * designed to mostly be used by view accessors that are queried without any gas fees. Developers should keep in + * mind that this function has an unbounded cost, and using it as part of a state-changing function may render the + * function uncallable if the queue grows to a point where copying to memory consumes too much gas to fit in a + * block. + */ + function values(Bytes32Deque storage deque, uint256 start, uint256 end) internal view returns (bytes32[] memory) { + unchecked { + end = Math.min(end, length(deque)); + start = Math.min(start, end); + + uint256 len = end - start; + bytes32[] memory result = new bytes32[](len); + uint128 begin = deque._begin; + for (uint256 i = 0; i < len; ++i) { + result[i] = deque._data[begin + uint128(start + i)]; + } + return result; + } + } + /** * @dev Resets the queue back to being empty. * diff --git a/test/utils/structs/DoubleEndedQueue.test.js b/test/utils/structs/DoubleEndedQueue.test.js index 6f8235ccdd3..ba235616473 100644 --- a/test/utils/structs/DoubleEndedQueue.test.js +++ b/test/utils/structs/DoubleEndedQueue.test.js @@ -54,6 +54,12 @@ describe('DoubleEndedQueue', function () { await expect(this.getContent()).to.eventually.have.ordered.members([bytesA, bytesB]); }); + + it('values returns empty array', async function () { + await expect(this.mock.$values(0, 0, 0)).to.eventually.deep.equal([]); + await expect(this.mock.$values(0, 0, 10)).to.eventually.deep.equal([]); + await expect(this.mock.$values(0, 5, 10)).to.eventually.deep.equal([]); + }); }); describe('when not empty', function () { @@ -140,5 +146,35 @@ describe('DoubleEndedQueue', function () { await expect(this.mock.$empty(0)).to.eventually.be.true; await expect(this.getContent()).to.eventually.have.ordered.members([]); }); + + describe('values', function () { + it('returns the full content for [0, length)', async function () { + await expect(this.mock.$values(0, 0, this.content.length)).to.eventually.deep.equal(this.content); + }); + + it('paginates across all begin/end combinations', async function () { + for (const begin of [0, 1, 2, 3, 4]) + for (const end of [0, 1, 2, 3, 4]) { + await expect(this.mock.$values(0, begin, end)).to.eventually.deep.equal(this.content.slice(begin, end)); + } + }); + + it('clamps end to length', async function () { + await expect(this.mock.$values(0, 0, ethers.MaxUint256)).to.eventually.deep.equal(this.content); + await expect(this.mock.$values(0, 1, ethers.MaxUint256)).to.eventually.deep.equal(this.content.slice(1)); + }); + + it('clamps start to end', async function () { + await expect(this.mock.$values(0, ethers.MaxUint256, ethers.MaxUint256)).to.eventually.deep.equal([]); + await expect(this.mock.$values(0, 2, 1)).to.eventually.deep.equal([]); + }); + + it('reflects pushFront/pushBack ordering (wraparound indices)', async function () { + await this.mock.$pushFront(0, bytesD); + const expected = [bytesD, ...this.content]; + await expect(this.mock.$values(0, 0, expected.length)).to.eventually.deep.equal(expected); + await expect(this.mock.$values(0, 1, 3)).to.eventually.deep.equal(expected.slice(1, 3)); + }); + }); }); }); From 508c781066d11bed5c7bf4ddc204958f96787ca8 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Mon, 18 May 2026 10:18:49 +0200 Subject: [PATCH 2/2] minor change --- contracts/utils/structs/DoubleEndedQueue.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/utils/structs/DoubleEndedQueue.sol b/contracts/utils/structs/DoubleEndedQueue.sol index 2fc247a182b..83163fd718b 100644 --- a/contracts/utils/structs/DoubleEndedQueue.sol +++ b/contracts/utils/structs/DoubleEndedQueue.sol @@ -227,9 +227,10 @@ library DoubleEndedQueue { uint256 len = end - start; bytes32[] memory result = new bytes32[](len); - uint128 begin = deque._begin; - for (uint256 i = 0; i < len; ++i) { - result[i] = deque._data[begin + uint128(start + i)]; + + uint128 offset = deque._begin + uint128(start); + for (uint128 i = 0; i < len; ++i) { + result[i] = deque._data[offset + i]; } return result; }