diff --git a/.changeset/dull-students-eat.md b/.changeset/dull-students-eat.md new file mode 100644 index 00000000000..94c4fc21ef2 --- /dev/null +++ b/.changeset/dull-students-eat.md @@ -0,0 +1,5 @@ +--- +'openzeppelin-solidity': minor +--- + +`Memory`: Add library with utilities to manipulate memory diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 97e79085bfb..fd0cf398eb0 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -49,6 +49,7 @@ import {SignatureChecker} from "../utils/cryptography/SignatureChecker.sol"; import {SignedMath} from "../utils/math/SignedMath.sol"; import {StorageSlot} from "../utils/StorageSlot.sol"; import {Strings} from "../utils/Strings.sol"; +import {Memory} from "../utils/Memory.sol"; import {Time} from "../utils/types/Time.sol"; contract Dummy1234 {} diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol new file mode 100644 index 00000000000..f09e1c8d626 --- /dev/null +++ b/contracts/utils/Memory.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/** + * @dev Utilities to manipulate memory. + * + * Memory is a contiguous and dynamic byte array in which Solidity stores non-primitive types. + * This library provides functions to manipulate pointers to this dynamic array. + * + * WARNING: When manipulating memory, make sure to follow the Solidity documentation + * guidelines for https://docs.soliditylang.org/en/v0.8.20/assembly.html#memory-safety[Memory Safety]. + */ +library Memory { + type Pointer is bytes32; + + /// @dev Returns a `Pointer` to the current free `Pointer`. + function getFreeMemoryPointer() internal pure returns (Pointer ptr) { + assembly ("memory-safe") { + ptr := mload(0x40) + } + } + + /** + * @dev Sets the free `Pointer` to a specific value. + * + * WARNING: Everything after the pointer may be overwritten. + **/ + function setFreeMemoryPointer(Pointer ptr) internal pure { + assembly ("memory-safe") { + mstore(0x40, ptr) + } + } + + /// @dev `Pointer` to `bytes32`. Expects a pointer to a properly ABI-encoded `bytes` object. + function asBytes32(Pointer ptr) internal pure returns (bytes32) { + return Pointer.unwrap(ptr); + } + + /// @dev `bytes32` to `Pointer`. Expects a pointer to a properly ABI-encoded `bytes` object. + function asPointer(bytes32 value) internal pure returns (Pointer) { + return Pointer.wrap(value); + } +} diff --git a/contracts/utils/README.adoc b/contracts/utils/README.adoc index 8640e56fa51..a536e3b51f8 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -38,6 +38,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Panic}: A library to revert with https://docs.soliditylang.org/en/v0.8.20/control-structures.html#panic-via-assert-and-error-via-require[Solidity panic codes]. * {Comparators}: A library that contains comparator functions to use with the {Heap} library. * {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers. + * {Memory}: A utility library to manipulate memory. * {InteroperableAddress}: Library for formatting and parsing ERC-7930 interoperable addresses. * {Blockhash}: A library for accessing historical block hashes beyond the standard 256 block limit utilizing EIP-2935's historical blockhash functionality. * {Time}: A library that provides helpers for manipulating time-related objects, including a `Delay` type. @@ -135,6 +136,8 @@ Ethereum contracts have no native concept of an interface, so applications must {{CAIP10}} +{{Memory}} + {{InteroperableAddress}} {{Blockhash}} diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index a248a0a324e..f0114726f2f 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -263,7 +263,7 @@ Some use cases require more powerful data structures than arrays and mappings of - xref:api:utils.adoc#EnumerableSet[`EnumerableSet`]: A https://en.wikipedia.org/wiki/Set_(abstract_data_type)[set] with enumeration capabilities. - xref:api:utils.adoc#EnumerableMap[`EnumerableMap`]: A `mapping` variant with enumeration capabilities. - xref:api:utils.adoc#MerkleTree[`MerkleTree`]: An on-chain https://wikipedia.org/wiki/Merkle_Tree[Merkle Tree] with helper functions. -- xref:api:utils.adoc#Heap.sol[`Heap`]: A +- xref:api:utils.adoc#Heap.sol[`Heap`]: A https://en.wikipedia.org/wiki/Binary_heap[binary heap] to store elements with priority defined by a compartor function. The `Enumerable*` structures are similar to mappings in that they store and remove elements in constant time and don't allow for repeated entries, but they also support _enumeration_, which means you can easily query all stored entries both on and off-chain. @@ -461,6 +461,38 @@ await instance.multicall([ ]); ---- +=== Memory + +The xref:api:utils.adoc#Memory[`Memory`] library provides functions for advanced use cases that require granular memory management. A common use case is to avoid unnecessary memory expansion costs when performing repeated operations that allocate memory in a loop. Consider the following example: + +[source,solidity] +---- +function processMultipleItems(uint256[] memory items) internal { + for (uint256 i = 0; i < items.length; i++) { + bytes memory tempData = abi.encode(items[i], block.timestamp); + // Process tempData... + } +} +---- + +Note that each iteration allocates new memory for `tempData`, causing the memory to expand continuously. This can be optimized by resetting the memory pointer between iterations: + +[source,solidity] +---- +function processMultipleItems(uint256[] memory items) internal { + Memory.Pointer ptr = Memory.getFreeMemoryPointer(); // Cache pointer + for (uint256 i = 0; i < items.length; i++) { + bytes memory tempData = abi.encode(items[i], block.timestamp); + // Process tempData... + Memory.setFreeMemoryPointer(ptr); // Reset pointer for reuse + } +} +---- + +This way, memory allocated for `tempData` in each iteration is reused, significantly reducing memory expansion costs when processing many items. + +IMPORTANT: Only use these functions after carefully confirming they're necessary. By default, Solidity handles memory safely. Using this library without understanding memory layout and safety may be dangerous. See the https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_memory.html[memory layout] and https://docs.soliditylang.org/en/v0.8.20/assembly.html#memory-safety[memory safety] documentation for details. + === Historical Block Hashes xref:api:utils.adoc#Blockhash[`Blockhash`] provides L2 protocol developers with extended access to historical block hashes beyond Ethereum's native 256-block limit. By leveraging https://eips.ethereum.org/EIPS/eip-2935[EIP-2935]'s history storage contract, the library enables access to block hashes up to 8,191 blocks in the past, making it invaluable for L2 fraud proofs and state verification systems. diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol new file mode 100644 index 00000000000..8ed9b4c43bc --- /dev/null +++ b/test/utils/Memory.t.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Memory} from "@openzeppelin/contracts/utils/Memory.sol"; + +contract MemoryTest is Test { + using Memory for *; + + // - first 0x80 bytes are reserved (scratch + FMP + zero) + uint256 constant START_PTR = 0x80; + // - moving the free memory pointer to far causes OOG errors + uint256 constant END_PTR = type(uint24).max; + + function testGetsetFreeMemoryPointer(uint256 seed) public pure { + bytes32 ptr = bytes32(bound(seed, START_PTR, END_PTR)); + ptr.asPointer().setFreeMemoryPointer(); + assertEq(Memory.getFreeMemoryPointer().asBytes32(), ptr); + } +} diff --git a/test/utils/Memory.test.js b/test/utils/Memory.test.js new file mode 100644 index 00000000000..9c1570aba0a --- /dev/null +++ b/test/utils/Memory.test.js @@ -0,0 +1,39 @@ +const { ethers } = require('hardhat'); +const { expect } = require('chai'); +const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); + +async function fixture() { + const mock = await ethers.deployContract('$Memory'); + return { mock }; +} + +describe('Memory', function () { + beforeEach(async function () { + Object.assign(this, await loadFixture(fixture)); + }); + + describe('free pointer', function () { + it('sets free memory pointer', async function () { + const ptr = ethers.toBeHex(0xa0, 32); + await expect(this.mock.$setFreeMemoryPointer(ptr)).to.not.be.reverted; + }); + + it('gets free memory pointer', async function () { + await expect(this.mock.$getFreeMemoryPointer()).to.eventually.equal( + ethers.toBeHex(0x80, 32), // Default pointer + ); + }); + }); + + describe('pointer conversions', function () { + it('asBytes32', async function () { + const ptr = ethers.toBeHex('0x1234', 32); + await expect(this.mock.$asBytes32(ptr)).to.eventually.equal(ptr); + }); + + it('asPointer', async function () { + const ptr = ethers.toBeHex('0x1234', 32); + await expect(this.mock.$asPointer(ptr)).to.eventually.equal(ptr); + }); + }); +});