From 9eb5f1cac83afe4d0000ea5d0a9aaf296a8c21d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 4 Sep 2024 11:13:57 -0600 Subject: [PATCH 01/19] Add memory utils --- .changeset/dull-students-eat.md | 5 ++++ contracts/utils/Memory.sol | 34 +++++++++++++++++++++++++++ contracts/utils/README.adoc | 1 + test/utils/Memory.t.sol | 16 +++++++++++++ test/utils/Memory.test.js | 41 +++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+) create mode 100644 .changeset/dull-students-eat.md create mode 100644 contracts/utils/Memory.sol create mode 100644 test/utils/Memory.t.sol create mode 100644 test/utils/Memory.test.js 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/utils/Memory.sol b/contracts/utils/Memory.sol new file mode 100644 index 00000000000..a0fc881e318 --- /dev/null +++ b/contracts/utils/Memory.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +/// @dev Memory utility library. +library Memory { + type Pointer is bytes32; + + /// @dev Returns a memory pointer to the current free memory pointer. + function getFreePointer() internal pure returns (Pointer ptr) { + assembly ("memory-safe") { + ptr := mload(0x40) + } + } + + /// @dev Sets the free memory pointer to a specific value. + /// + /// WARNING: Everything after the pointer may be overwritten. + function setFreePointer(Pointer ptr) internal pure { + assembly ("memory-safe") { + mstore(0x40, ptr) + } + } + + /// @dev Pointer to `bytes32`. + function asBytes32(Pointer ptr) internal pure returns (bytes32) { + return Pointer.unwrap(ptr); + } + + /// @dev `bytes32` to pointer. + 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 0ef3e5387c8..87af4fd4b7b 100644 --- a/contracts/utils/README.adoc +++ b/contracts/utils/README.adoc @@ -40,6 +40,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t * {Packing}: A library for packing and unpacking multiple values into bytes32 * {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 with the {Heap} library. + * {Memory}: A utility library to manipulate memory. [NOTE] ==== diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol new file mode 100644 index 00000000000..4cc60b88f9c --- /dev/null +++ b/test/utils/Memory.t.sol @@ -0,0 +1,16 @@ +// 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 *; + + function testSymbolicGetSetFreePointer(bytes32 ptr) public { + Memory.Pointer memoryPtr = ptr.asPointer(); + Memory.setFreePointer(memoryPtr); + assertEq(Memory.getFreePointer().asBytes32(), memoryPtr.asBytes32()); + } +} diff --git a/test/utils/Memory.test.js b/test/utils/Memory.test.js new file mode 100644 index 00000000000..5698728dcfd --- /dev/null +++ b/test/utils/Memory.test.js @@ -0,0 +1,41 @@ +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 memory pointer', async function () { + const ptr = '0x00000000000000000000000000000000000000000000000000000000000000a0'; + expect(await this.mock.$setFreePointer(ptr)).to.not.be.reverted; + }); + + it('gets memory pointer', async function () { + expect(await this.mock.$getFreePointer()).to.equal( + // Default pointer + '0x0000000000000000000000000000000000000000000000000000000000000080', + ); + }); + + it('asBytes32', async function () { + const ptr = ethers.toBeHex('0x1234', 32); + await this.mock.$setFreePointer(ptr); + expect(await this.mock.$asBytes32(ptr)).to.equal(ptr); + }); + + it('asPointer', async function () { + const ptr = ethers.toBeHex('0x1234', 32); + await this.mock.$setFreePointer(ptr); + expect(await this.mock.$asPointer(ptr)).to.equal(ptr); + }); + }); +}); From 2d397f467f202ccc1dc3c2b240a9f3353f13af83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 4 Sep 2024 11:43:04 -0600 Subject: [PATCH 02/19] Fix tests upgradeable --- contracts/mocks/Stateless.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/mocks/Stateless.sol b/contracts/mocks/Stateless.sol index 846c77d98e8..a96dd48cc87 100644 --- a/contracts/mocks/Stateless.sol +++ b/contracts/mocks/Stateless.sol @@ -37,6 +37,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 {} From 2a0fb7e5db92a563991ff7b447596b8191d22381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 5 Sep 2024 12:21:13 -0600 Subject: [PATCH 03/19] Add docs --- contracts/utils/Memory.sol | 8 +++++- docs/modules/ROOT/pages/utilities.adoc | 36 +++++++++++++++++++++++++- test/utils/Memory.t.sol | 5 ++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index a0fc881e318..abb6f100bc6 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -2,7 +2,13 @@ pragma solidity ^0.8.20; -/// @dev Memory utility library. +/// @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; diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index b8afec4eabd..d1cf470d60a 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -189,7 +189,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. @@ -386,3 +386,37 @@ await instance.multicall([ instance.interface.encodeFunctionData("bar") ]); ---- + +=== Memory + +The `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 iterating over a section of the code that allocates new memory. Consider the following example: + +[source,solidity] +---- +function callFoo(address target) internal { + bytes memory callData = abi.encodeWithSelector( + bytes4(keccak256("foo()")) + ) + (bool success, /* bytes memory returndata */) = target.call(callData); + require(success); +} +---- + +Note the function allocates memory for both the `callData` argument and for the returndata even if it's ignored. As such, it may be desirable to reset the free memory pointer after the end of the function. + +[source,solidity] +---- +function callFoo(address target) internal { + Memory.Pointer ptr = Memory.getFreePointer(); // Cache pointer + bytes memory callData = abi.encodeWithSelector( + bytes4(keccak256("foo()")) + ) + (bool success, /* bytes memory returndata */) = target.call(callData); + require(success); + Memory.setFreePointer(ptr); // Reset pointer +} +---- + +In this way, new memory will be allocated in the space where the `returndata` and `callData` used to be, potentially reducing memory expansion costs by shrinking the its size at the end of the transaction and resulting in gas savings. + +IMPORTANT: By default, Solidity handles memory safely. Using this library without understanding how memory works may be dangerous. Consider thoroughly reading the Solidity documentation about the https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_memory.html[memory layout] and how the language defines https://docs.soliditylang.org/en/v0.8.20/assembly.html#memory-safety[memory safety]. diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol index 4cc60b88f9c..99cb75fb095 100644 --- a/test/utils/Memory.t.sol +++ b/test/utils/Memory.t.sol @@ -9,8 +9,7 @@ contract MemoryTest is Test { using Memory for *; function testSymbolicGetSetFreePointer(bytes32 ptr) public { - Memory.Pointer memoryPtr = ptr.asPointer(); - Memory.setFreePointer(memoryPtr); - assertEq(Memory.getFreePointer().asBytes32(), memoryPtr.asBytes32()); + Memory.setFreePointer(ptr.asPointer()); + assertEq(Memory.getFreePointer().asBytes32(), ptr); } } From a7e61c3bd521822e7a8c932c5596c6f3cea8739e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Thu, 5 Sep 2024 12:32:19 -0600 Subject: [PATCH 04/19] Make use of the library --- contracts/access/manager/AuthorityUtils.sol | 3 +++ contracts/token/ERC20/extensions/ERC4626.sol | 3 +++ contracts/token/ERC20/utils/SafeERC20.sol | 7 +++++++ contracts/utils/cryptography/SignatureChecker.sol | 6 +++++- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/contracts/access/manager/AuthorityUtils.sol b/contracts/access/manager/AuthorityUtils.sol index fb3018ca805..4cc77123716 100644 --- a/contracts/access/manager/AuthorityUtils.sol +++ b/contracts/access/manager/AuthorityUtils.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.20; import {IAuthority} from "./IAuthority.sol"; +import {Memory} from "../../utils/Memory.sol"; library AuthorityUtils { /** @@ -17,6 +18,7 @@ library AuthorityUtils { address target, bytes4 selector ) internal view returns (bool immediate, uint32 delay) { + Memory.Pointer ptr = Memory.getFreePointer(); (bool success, bytes memory data) = authority.staticcall( abi.encodeCall(IAuthority.canCall, (caller, target, selector)) ); @@ -27,6 +29,7 @@ library AuthorityUtils { immediate = abi.decode(data, (bool)); } } + Memory.setFreePointer(ptr); return (immediate, delay); } } diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index c71b14ad48c..121d729bc96 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -7,6 +7,7 @@ import {IERC20, IERC20Metadata, ERC20} from "../ERC20.sol"; import {SafeERC20} from "../utils/SafeERC20.sol"; import {IERC4626} from "../../../interfaces/IERC4626.sol"; import {Math} from "../../../utils/math/Math.sol"; +import {Memory} from "../../../utils/Memory.sol"; /** * @dev Implementation of the ERC-4626 "Tokenized Vault Standard" as defined in @@ -84,6 +85,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { * @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way. */ function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool, uint8) { + Memory.Pointer ptr = Memory.getFreePointer(); (bool success, bytes memory encodedDecimals) = address(asset_).staticcall( abi.encodeCall(IERC20Metadata.decimals, ()) ); @@ -93,6 +95,7 @@ abstract contract ERC4626 is ERC20, IERC4626 { return (true, uint8(returnedDecimals)); } } + Memory.setFreePointer(ptr); return (false, 0); } diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index ed41fb042c9..4d06ded819d 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.20; import {IERC20} from "../IERC20.sol"; import {IERC1363} from "../../../interfaces/IERC1363.sol"; import {Address} from "../../../utils/Address.sol"; +import {Memory} from "../../../utils/Memory.sol"; /** * @title SafeERC20 @@ -32,7 +33,9 @@ library SafeERC20 { * non-reverting calls are assumed to be successful. */ function safeTransfer(IERC20 token, address to, uint256 value) internal { + Memory.Pointer ptr = Memory.getFreePointer(); _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value))); + Memory.setFreePointer(ptr); } /** @@ -40,7 +43,9 @@ library SafeERC20 { * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. */ function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { + Memory.Pointer ptr = Memory.getFreePointer(); _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); + Memory.setFreePointer(ptr); } /** @@ -72,12 +77,14 @@ library SafeERC20 { * to be set to zero before setting it to a non-zero value, such as USDT. */ function forceApprove(IERC20 token, address spender, uint256 value) internal { + Memory.Pointer ptr = Memory.getFreePointer(); bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value)); if (!_callOptionalReturnBool(token, approvalCall)) { _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0))); _callOptionalReturn(token, approvalCall); } + Memory.setFreePointer(ptr); } /** diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 9aaa2e0716c..16e038d2d87 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -5,6 +5,7 @@ pragma solidity ^0.8.20; import {ECDSA} from "./ECDSA.sol"; import {IERC1271} from "../../interfaces/IERC1271.sol"; +import {Memory} from "../Memory.sol"; /** * @dev Signature verification helper that can be used instead of `ECDSA.recover` to seamlessly support both ECDSA @@ -40,11 +41,14 @@ library SignatureChecker { bytes32 hash, bytes memory signature ) internal view returns (bool) { + Memory.Pointer ptr = Memory.getFreePointer(); (bool success, bytes memory result) = signer.staticcall( abi.encodeCall(IERC1271.isValidSignature, (hash, signature)) ); - return (success && + bool valid = (success && result.length >= 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); + Memory.setFreePointer(ptr); + return valid; } } From 1aae8bbd7e77f20600c4661a56a1d1d95ceb76b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Tue, 8 Oct 2024 23:48:04 -0600 Subject: [PATCH 05/19] Update docs/modules/ROOT/pages/utilities.adoc Co-authored-by: Hadrien Croubois --- docs/modules/ROOT/pages/utilities.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index d1cf470d60a..84ede5a4b5e 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -417,6 +417,6 @@ function callFoo(address target) internal { } ---- -In this way, new memory will be allocated in the space where the `returndata` and `callData` used to be, potentially reducing memory expansion costs by shrinking the its size at the end of the transaction and resulting in gas savings. +This way, memory is allocated to accommodate the `callData`, and the `returndata` is freed. This allows other memory operations to reuse that space, thus reducing the memory expansion costs of these operations. In particular, this allows many `callFoo` to be performed in a loop with limited memory expansion costs. IMPORTANT: By default, Solidity handles memory safely. Using this library without understanding how memory works may be dangerous. Consider thoroughly reading the Solidity documentation about the https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_memory.html[memory layout] and how the language defines https://docs.soliditylang.org/en/v0.8.20/assembly.html#memory-safety[memory safety]. From d514606a9cba7c1fc081427b5d7dd19017f26f9c Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Thu, 6 Mar 2025 17:14:59 +0100 Subject: [PATCH 06/19] fix tests --- test/utils/Memory.t.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol index 99cb75fb095..0affe3234c4 100644 --- a/test/utils/Memory.t.sol +++ b/test/utils/Memory.t.sol @@ -8,7 +8,11 @@ import {Memory} from "@openzeppelin/contracts/utils/Memory.sol"; contract MemoryTest is Test { using Memory for *; - function testSymbolicGetSetFreePointer(bytes32 ptr) public { + function testSymbolicGetSetFreePointer(uint256 seed) public pure { + // - first 0x80 bytes are reserved (scratch + FMP + zero) + // - moving the free memory pointer to far causes OOG errors + bytes32 ptr = bytes32(bound(seed, 0x80, type(uint24).max)); + Memory.setFreePointer(ptr.asPointer()); assertEq(Memory.getFreePointer().asBytes32(), ptr); } From 14fa04ef86a0c78af70fae316d7cee2cae71304c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ernesto=20Garc=C3=ADa?= Date: Wed, 7 May 2025 12:30:10 -0600 Subject: [PATCH 07/19] Update contracts/utils/Memory.sol Co-authored-by: Arr00 <13561405+arr00@users.noreply.github.com> --- contracts/utils/Memory.sol | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index abb6f100bc6..33842a6eb4d 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -2,13 +2,15 @@ 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]. +/** + * @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; From d0d55fcc356d813d5563687a229ee6710e12d5aa Mon Sep 17 00:00:00 2001 From: Arr00 <13561405+arr00@users.noreply.github.com> Date: Wed, 7 May 2025 14:32:03 -0400 Subject: [PATCH 08/19] Update contracts/utils/Memory.sol --- contracts/utils/Memory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index 33842a6eb4d..e5cc0e06cc8 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.20; /** - * @dev Utilities to manipulate memory. + * @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. From ac92bb41b6ccb71120b40601e63bb9f490d89ead Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 8 Jun 2025 13:12:54 -0600 Subject: [PATCH 09/19] up --- contracts/access/manager/AuthorityUtils.sol | 4 ---- contracts/token/ERC20/extensions/ERC4626.sol | 3 --- contracts/token/ERC20/utils/SafeERC20.sol | 7 ------- contracts/utils/cryptography/SignatureChecker.sol | 6 +----- 4 files changed, 1 insertion(+), 19 deletions(-) diff --git a/contracts/access/manager/AuthorityUtils.sol b/contracts/access/manager/AuthorityUtils.sol index 5aeed4f6285..8b0470968b9 100644 --- a/contracts/access/manager/AuthorityUtils.sol +++ b/contracts/access/manager/AuthorityUtils.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.20; import {IAuthority} from "./IAuthority.sol"; -import {Memory} from "../../utils/Memory.sol"; library AuthorityUtils { /** @@ -18,7 +17,6 @@ library AuthorityUtils { address target, bytes4 selector ) internal view returns (bool immediate, uint32 delay) { - Memory.Pointer ptr = Memory.getFreePointer(); bytes memory data = abi.encodeCall(IAuthority.canCall, (caller, target, selector)); assembly ("memory-safe") { @@ -34,7 +32,5 @@ library AuthorityUtils { delay := mul(delay, iszero(shr(32, delay))) } } - - Memory.setFreePointer(ptr); } } diff --git a/contracts/token/ERC20/extensions/ERC4626.sol b/contracts/token/ERC20/extensions/ERC4626.sol index d5b8bcb9888..6e6a57c305d 100644 --- a/contracts/token/ERC20/extensions/ERC4626.sol +++ b/contracts/token/ERC20/extensions/ERC4626.sol @@ -7,7 +7,6 @@ import {IERC20, IERC20Metadata, ERC20} from "../ERC20.sol"; import {SafeERC20} from "../utils/SafeERC20.sol"; import {IERC4626} from "../../../interfaces/IERC4626.sol"; import {Math} from "../../../utils/math/Math.sol"; -import {Memory} from "../../../utils/Memory.sol"; /** * @dev Implementation of the ERC-4626 "Tokenized Vault Standard" as defined in @@ -85,7 +84,6 @@ abstract contract ERC4626 is ERC20, IERC4626 { * @dev Attempts to fetch the asset decimals. A return value of false indicates that the attempt failed in some way. */ function _tryGetAssetDecimals(IERC20 asset_) private view returns (bool ok, uint8 assetDecimals) { - Memory.Pointer ptr = Memory.getFreePointer(); (bool success, bytes memory encodedDecimals) = address(asset_).staticcall( abi.encodeCall(IERC20Metadata.decimals, ()) ); @@ -95,7 +93,6 @@ abstract contract ERC4626 is ERC20, IERC4626 { return (true, uint8(returnedDecimals)); } } - Memory.setFreePointer(ptr); return (false, 0); } diff --git a/contracts/token/ERC20/utils/SafeERC20.sol b/contracts/token/ERC20/utils/SafeERC20.sol index bcd17bc0111..883e8d30c97 100644 --- a/contracts/token/ERC20/utils/SafeERC20.sol +++ b/contracts/token/ERC20/utils/SafeERC20.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.20; import {IERC20} from "../IERC20.sol"; import {IERC1363} from "../../../interfaces/IERC1363.sol"; -import {Memory} from "../../../utils/Memory.sol"; /** * @title SafeERC20 @@ -32,9 +31,7 @@ library SafeERC20 { * non-reverting calls are assumed to be successful. */ function safeTransfer(IERC20 token, address to, uint256 value) internal { - Memory.Pointer ptr = Memory.getFreePointer(); _callOptionalReturn(token, abi.encodeCall(token.transfer, (to, value))); - Memory.setFreePointer(ptr); } /** @@ -42,9 +39,7 @@ library SafeERC20 { * calling contract. If `token` returns no value, non-reverting calls are assumed to be successful. */ function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal { - Memory.Pointer ptr = Memory.getFreePointer(); _callOptionalReturn(token, abi.encodeCall(token.transferFrom, (from, to, value))); - Memory.setFreePointer(ptr); } /** @@ -104,14 +99,12 @@ library SafeERC20 { * set here. */ function forceApprove(IERC20 token, address spender, uint256 value) internal { - Memory.Pointer ptr = Memory.getFreePointer(); bytes memory approvalCall = abi.encodeCall(token.approve, (spender, value)); if (!_callOptionalReturnBool(token, approvalCall)) { _callOptionalReturn(token, abi.encodeCall(token.approve, (spender, 0))); _callOptionalReturn(token, approvalCall); } - Memory.setFreePointer(ptr); } /** diff --git a/contracts/utils/cryptography/SignatureChecker.sol b/contracts/utils/cryptography/SignatureChecker.sol index 1e8991a6bb9..261372f0c3d 100644 --- a/contracts/utils/cryptography/SignatureChecker.sol +++ b/contracts/utils/cryptography/SignatureChecker.sol @@ -5,7 +5,6 @@ pragma solidity ^0.8.24; import {ECDSA} from "./ECDSA.sol"; import {IERC1271} from "../../interfaces/IERC1271.sol"; -import {Memory} from "../Memory.sol"; import {IERC7913SignatureVerifier} from "../../interfaces/IERC7913.sol"; import {Bytes} from "../../utils/Bytes.sol"; @@ -51,15 +50,12 @@ library SignatureChecker { bytes32 hash, bytes memory signature ) internal view returns (bool) { - Memory.Pointer ptr = Memory.getFreePointer(); (bool success, bytes memory result) = signer.staticcall( abi.encodeCall(IERC1271.isValidSignature, (hash, signature)) ); - bool valid = (success && + return (success && result.length >= 32 && abi.decode(result, (bytes32)) == bytes32(IERC1271.isValidSignature.selector)); - Memory.setFreePointer(ptr); - return valid; } /** From 6bb96d5fcb850b240e4ae8a05473db943c379890 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 8 Jun 2025 15:35:11 -0600 Subject: [PATCH 10/19] WIP: Add more Memory functions --- contracts/utils/Bytes.sol | 7 ++++ contracts/utils/Memory.sol | 66 ++++++++++++++++++++++++++++++++++--- contracts/utils/Strings.sol | 3 +- test/utils/Bytes.t.sol | 12 +++++++ test/utils/Memory.t.sol | 43 +++++++++++++++++++++--- 5 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 test/utils/Bytes.t.sol diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index 1234b845513..f8c3fb2ebfa 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -99,6 +99,13 @@ library Bytes { return result; } + /** + * @dev Returns true if the two byte buffers are equal. + */ + function equal(bytes memory a, bytes memory b) internal pure returns (bool) { + return a.length == b.length && keccak256(a) == keccak256(b); + } + /** * @dev Reads a bytes32 from a bytes array without bounds checking. * diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index e5cc0e06cc8..2d8d76e85ce 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -14,14 +14,14 @@ pragma solidity ^0.8.20; library Memory { type Pointer is bytes32; - /// @dev Returns a memory pointer to the current free memory pointer. + /// @dev Returns a `Pointer` to the current free `Pointer`. function getFreePointer() internal pure returns (Pointer ptr) { assembly ("memory-safe") { ptr := mload(0x40) } } - /// @dev Sets the free memory pointer to a specific value. + /// @dev Sets the free `Pointer` to a specific value. /// /// WARNING: Everything after the pointer may be overwritten. function setFreePointer(Pointer ptr) internal pure { @@ -30,13 +30,71 @@ library Memory { } } - /// @dev Pointer to `bytes32`. + /// @dev Returns a `Pointer` to the content of a `bytes` buffer. Skips the length word. + function contentPointer(bytes memory buffer) internal pure returns (Pointer) { + return addOffset(asPointer(buffer), 32); + } + + /** + * @dev Copies `length` bytes from `srcPtr` to `destPtr`. Equivalent to https://www.evm.codes/?fork=cancun#5e[`mcopy`]. + * + * WARNING: Reading or writing beyond the allocated memory bounds of either pointer + * will result in undefined behavior and potential memory corruption. + */ + function copy(Pointer destPtr, Pointer srcPtr, uint256 length) internal pure { + assembly ("memory-safe") { + mcopy(destPtr, srcPtr, length) + } + } + + /// @dev Extracts a `bytes1` from a `Pointer`. `offset` starts from the most significant byte. + function extractByte(Pointer ptr, uint256 offset) internal pure returns (bytes1 v) { + bytes32 word = extractWord(ptr); + assembly ("memory-safe") { + v := byte(offset, word) + } + } + + /// @dev Extracts a `bytes32` from a `Pointer`. + function extractWord(Pointer ptr) internal pure returns (bytes32 v) { + assembly ("memory-safe") { + v := mload(ptr) + } + } + + /// @dev Adds an offset to a `Pointer`. + function addOffset(Pointer ptr, uint256 offset) internal pure returns (Pointer) { + return asPointer(bytes32(asUint256(ptr) + offset)); + } + + /// @dev `Pointer` to `bytes32`. function asBytes32(Pointer ptr) internal pure returns (bytes32) { return Pointer.unwrap(ptr); } - /// @dev `bytes32` to pointer. + /// @dev `Pointer` to `uint256`. + function asUint256(Pointer ptr) internal pure returns (uint256) { + return uint256(asBytes32(ptr)); + } + + /// @dev `bytes32` to `Pointer`. function asPointer(bytes32 value) internal pure returns (Pointer) { return Pointer.wrap(value); } + + /// @dev `bytes` to `Pointer`. + function asPointer(bytes memory value) internal pure returns (Pointer) { + bytes32 ptr; + assembly ("memory-safe") { + ptr := value + } + return asPointer(ptr); + } + + /// @dev `Pointer` to `bytes`. + function asBytes(Pointer ptr) internal pure returns (bytes memory b) { + assembly ("memory-safe") { + b := ptr + } + } } diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index 4cc597646f2..a865bfbc785 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -6,6 +6,7 @@ pragma solidity ^0.8.20; import {Math} from "./math/Math.sol"; import {SafeCast} from "./math/SafeCast.sol"; import {SignedMath} from "./math/SignedMath.sol"; +import {Bytes} from "./Bytes.sol"; /** * @dev String operations. @@ -132,7 +133,7 @@ library Strings { * @dev Returns true if the two strings are equal. */ function equal(string memory a, string memory b) internal pure returns (bool) { - return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); + return Bytes.equal(bytes(a), bytes(b)); } /** diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol new file mode 100644 index 00000000000..6b2d7b5cad3 --- /dev/null +++ b/test/utils/Bytes.t.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; + +contract BytesTest is Test { + function testSymbolicEqual(bytes memory a, bytes memory b) public pure { + assertEq(Bytes.equal(a, b), Bytes.equal(a, b)); + } +} diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol index 0affe3234c4..8964c164523 100644 --- a/test/utils/Memory.t.sol +++ b/test/utils/Memory.t.sol @@ -4,16 +4,49 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {Memory} from "@openzeppelin/contracts/utils/Memory.sol"; +import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; contract MemoryTest is Test { using Memory for *; - function testSymbolicGetSetFreePointer(uint256 seed) public pure { - // - first 0x80 bytes are reserved (scratch + FMP + zero) - // - moving the free memory pointer to far causes OOG errors - bytes32 ptr = bytes32(bound(seed, 0x80, type(uint24).max)); + // - 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; - Memory.setFreePointer(ptr.asPointer()); + function testGetSetFreePointer(uint256 seed) public pure { + bytes32 ptr = bytes32(bound(seed, START_PTR, END_PTR)); + ptr.asPointer().setFreePointer(); assertEq(Memory.getFreePointer().asBytes32(), ptr); } + + function testSymbolicContentPointer(uint256 seed) public pure { + Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); + assertEq(ptr.asBytes().contentPointer().asBytes32(), ptr.addOffset(32).asBytes32()); + } + + // function testCopy(bytes memory data, uint256 destSeed) public pure { + // uint256 upperPtr = data.asPointer().asUint256() + data.length; + // Memory.Pointer destPtr = bytes32(bound(destSeed, upperPtr, upperPtr + 100)).asPointer(); + // Memory.copy(data.asPointer(), destPtr, data.length + 32); + // for (uint256 i = 0; i < data.length; i++) { + // assertEq(data[i], destPtr.asBytes()[i]); + // } + // } + + function testExtractByte(uint256 seed, uint256 index) public pure { + index = bound(index, 0, 31); + Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); + assertEq(ptr.extractByte(index), bytes1(ptr.asBytes32() >> (256 - index * 8))); + } + + // function testExtractWord(uint256 seed) public pure { + // Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); + // assertEq(ptr.extractWord(), ptr.asBytes32()); + // } + + // function testAddOffset(uint256 seed, uint256 offset) public pure { + // Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); + // assertEq(ptr.addOffset(offset).asUint256(), ptr.asUint256() + offset); + // } } From 860e5a819701d49f4cbb8f13ca6306e1853e6939 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 8 Jun 2025 16:50:42 -0600 Subject: [PATCH 11/19] up --- contracts/utils/Memory.sol | 16 +++++--- test/utils/Memory.test.js | 78 +++++++++++++++++++++++++++++++------- 2 files changed, 74 insertions(+), 20 deletions(-) diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index 2d8d76e85ce..891754f94d1 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -47,7 +47,11 @@ library Memory { } } - /// @dev Extracts a `bytes1` from a `Pointer`. `offset` starts from the most significant byte. + /** + * @dev Extracts a `bytes1` from a `Pointer`. `offset` starts from the most significant byte. + * + * NOTE: Will return `0x00` if `offset` is larger or equal to `32`. + */ function extractByte(Pointer ptr, uint256 offset) internal pure returns (bytes1 v) { bytes32 word = extractWord(ptr); assembly ("memory-safe") { @@ -67,22 +71,22 @@ library Memory { return asPointer(bytes32(asUint256(ptr) + offset)); } - /// @dev `Pointer` to `bytes32`. + /// @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 `Pointer` to `uint256`. + /// @dev `Pointer` to `uint256`. Expects a pointer to a properly ABI-encoded `bytes` object. function asUint256(Pointer ptr) internal pure returns (uint256) { return uint256(asBytes32(ptr)); } - /// @dev `bytes32` to `Pointer`. + /// @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); } - /// @dev `bytes` to `Pointer`. + /// @dev Returns a `Pointer` to the `value`'s header (i.e. includes the length word). function asPointer(bytes memory value) internal pure returns (Pointer) { bytes32 ptr; assembly ("memory-safe") { @@ -91,7 +95,7 @@ library Memory { return asPointer(ptr); } - /// @dev `Pointer` to `bytes`. + /// @dev `Pointer` to `bytes`. Expects a pointer to a properly ABI-encoded `bytes` object. function asBytes(Pointer ptr) internal pure returns (bytes memory b) { assembly ("memory-safe") { b := ptr diff --git a/test/utils/Memory.test.js b/test/utils/Memory.test.js index 5698728dcfd..c6ae6ba2d76 100644 --- a/test/utils/Memory.test.js +++ b/test/utils/Memory.test.js @@ -14,28 +14,78 @@ describe('Memory', function () { }); describe('free pointer', function () { - it('sets memory pointer', async function () { - const ptr = '0x00000000000000000000000000000000000000000000000000000000000000a0'; - expect(await this.mock.$setFreePointer(ptr)).to.not.be.reverted; + it('sets free memory pointer', async function () { + const ptr = ethers.toBeHex(0xa0, 32); + await expect(this.mock.$setFreePointer(ptr)).to.not.be.reverted; }); - it('gets memory pointer', async function () { - expect(await this.mock.$getFreePointer()).to.equal( - // Default pointer - '0x0000000000000000000000000000000000000000000000000000000000000080', + it('gets free memory pointer', async function () { + await expect(this.mock.$getFreePointer()).to.eventually.equal( + ethers.toBeHex(0x80, 32), // Default pointer ); }); + }); - it('asBytes32', async function () { - const ptr = ethers.toBeHex('0x1234', 32); - await this.mock.$setFreePointer(ptr); - expect(await this.mock.$asBytes32(ptr)).to.equal(ptr); + it('extractWord extracts a word', async function () { + const ptr = await this.mock.$getFreePointer(); + await expect(this.mock.$extractWord(ptr)).to.eventually.equal(ethers.toBeHex(0, 32)); + }); + + it('extractByte extracts a byte', async function () { + const ptr = await this.mock.$getFreePointer(); + await expect(this.mock.$extractByte(ptr, 0)).to.eventually.equal(ethers.toBeHex(0, 1)); + }); + + it('contentPointer', async function () { + const data = ethers.toUtf8Bytes('hello world'); + const result = await this.mock.$contentPointer(data); + expect(result).to.equal(ethers.toBeHex(0xa0, 32)); // 0x80 is the default free pointer (length) + }); + + describe('addOffset', function () { + it('addOffset', async function () { + const basePtr = ethers.toBeHex(0x80, 32); + const offset = 32; + const expectedPtr = ethers.toBeHex(0xa0, 32); + + await expect(this.mock.$addOffset(basePtr, offset)).to.eventually.equal(expectedPtr); }); - it('asPointer', async function () { + it('addOffsetwraps around', async function () { + const basePtr = ethers.toBeHex(0x80, 32); + const offset = 256; + const expectedPtr = ethers.toBeHex(0x180, 32); + await expect(this.mock.$addOffset(basePtr, offset)).to.eventually.equal(expectedPtr); + }); + }); + + describe('pointer conversions', function () { + it('asBytes32 / asPointer', async function () { const ptr = ethers.toBeHex('0x1234', 32); - await this.mock.$setFreePointer(ptr); - expect(await this.mock.$asPointer(ptr)).to.equal(ptr); + await expect(this.mock.$asBytes32(ptr)).to.eventually.equal(ptr); + await expect(this.mock.$asPointer(ethers.Typed.bytes32(ptr))).to.eventually.equal(ptr); + }); + + it('asBytes / asPointer', async function () { + const ptr = await this.mock.$asPointer(ethers.Typed.bytes(ethers.toUtf8Bytes('hello world'))); + expect(ptr).to.equal(ethers.toBeHex(0x80, 32)); // Default free pointer + await expect(this.mock.$asBytes(ptr)).to.eventually.equal(ethers.toBeHex(0x20, 32)); + }); + + it('asUint256', async function () { + const value = 0x1234; + const ptr = ethers.toBeHex(value, 32); + await expect(this.mock.$asUint256(ptr)).to.eventually.equal(value); + }); + }); + + describe('memory operations', function () { + it('copy', async function () { + await expect(this.mock.$copy(ethers.toBeHex(0x80, 32), ethers.toBeHex(0xc0, 32), 32)).to.not.be.reverted; + }); + + it('copy with zero length', async function () { + await expect(this.mock.$copy(ethers.toBeHex(0x80, 32), ethers.toBeHex(0xc0, 32), 0)).to.not.be.reverted; }); }); }); From ecdb768fc852cc37608f4f9842cd1c9703bfa19a Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 8 Jun 2025 17:01:02 -0600 Subject: [PATCH 12/19] revert --- contracts/utils/Bytes.sol | 7 ------- contracts/utils/Strings.sol | 3 +-- test/utils/Bytes.t.sol | 12 ------------ 3 files changed, 1 insertion(+), 21 deletions(-) delete mode 100644 test/utils/Bytes.t.sol diff --git a/contracts/utils/Bytes.sol b/contracts/utils/Bytes.sol index f8c3fb2ebfa..1234b845513 100644 --- a/contracts/utils/Bytes.sol +++ b/contracts/utils/Bytes.sol @@ -99,13 +99,6 @@ library Bytes { return result; } - /** - * @dev Returns true if the two byte buffers are equal. - */ - function equal(bytes memory a, bytes memory b) internal pure returns (bool) { - return a.length == b.length && keccak256(a) == keccak256(b); - } - /** * @dev Reads a bytes32 from a bytes array without bounds checking. * diff --git a/contracts/utils/Strings.sol b/contracts/utils/Strings.sol index a865bfbc785..4cc597646f2 100644 --- a/contracts/utils/Strings.sol +++ b/contracts/utils/Strings.sol @@ -6,7 +6,6 @@ pragma solidity ^0.8.20; import {Math} from "./math/Math.sol"; import {SafeCast} from "./math/SafeCast.sol"; import {SignedMath} from "./math/SignedMath.sol"; -import {Bytes} from "./Bytes.sol"; /** * @dev String operations. @@ -133,7 +132,7 @@ library Strings { * @dev Returns true if the two strings are equal. */ function equal(string memory a, string memory b) internal pure returns (bool) { - return Bytes.equal(bytes(a), bytes(b)); + return bytes(a).length == bytes(b).length && keccak256(bytes(a)) == keccak256(bytes(b)); } /** diff --git a/test/utils/Bytes.t.sol b/test/utils/Bytes.t.sol deleted file mode 100644 index 6b2d7b5cad3..00000000000 --- a/test/utils/Bytes.t.sol +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.8.20; - -import {Test} from "forge-std/Test.sol"; -import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; - -contract BytesTest is Test { - function testSymbolicEqual(bytes memory a, bytes memory b) public pure { - assertEq(Bytes.equal(a, b), Bytes.equal(a, b)); - } -} From 95907aa286018d536920104790000c5f8ac6b478 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 8 Jun 2025 17:11:23 -0600 Subject: [PATCH 13/19] Update docs --- docs/modules/ROOT/pages/utilities.adoc | 49 +++++++++++++++++--------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 24c79276bc8..68358f8ec2a 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -463,37 +463,52 @@ await instance.multicall([ === Memory -The `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 iterating over a section of the code that allocates new memory. Consider the following example: +The `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 callFoo(address target) internal { - bytes memory callData = abi.encodeWithSelector( - bytes4(keccak256("foo()")) - ) - (bool success, /* bytes memory returndata */) = target.call(callData); - require(success); +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 the function allocates memory for both the `callData` argument and for the returndata even if it's ignored. As such, it may be desirable to reset the free memory pointer after the end of the function. +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 callFoo(address target) internal { +function processMultipleItems(uint256[] memory items) internal { Memory.Pointer ptr = Memory.getFreePointer(); // Cache pointer - bytes memory callData = abi.encodeWithSelector( - bytes4(keccak256("foo()")) - ) - (bool success, /* bytes memory returndata */) = target.call(callData); - require(success); - Memory.setFreePointer(ptr); // Reset pointer + for (uint256 i = 0; i < items.length; i++) { + bytes memory tempData = abi.encode(items[i], block.timestamp); + // Process tempData... + Memory.setFreePointer(ptr); // Reset pointer for reuse + } } ---- -This way, memory is allocated to accommodate the `callData`, and the `returndata` is freed. This allows other memory operations to reuse that space, thus reducing the memory expansion costs of these operations. In particular, this allows many `callFoo` to be performed in a loop with limited memory expansion costs. +This way, memory allocated for `tempData` in each iteration is reused, significantly reducing memory expansion costs when processing many items. -IMPORTANT: By default, Solidity handles memory safely. Using this library without understanding how memory works may be dangerous. Consider thoroughly reading the Solidity documentation about the https://docs.soliditylang.org/en/v0.8.20/internals/layout_in_memory.html[memory layout] and how the language defines https://docs.soliditylang.org/en/v0.8.20/assembly.html#memory-safety[memory safety]. +==== Copying memory buffers + +The `Memory` library provides a `copy` function that allows copying data between memory locations. This is useful when you need to extract a segment of data from a larger buffer or when you want to avoid unnecessary memory allocations. The following example demonstrates how to copy a segment of data from a source buffer: + +[source,solidity] +---- +function copyDataSegment(bytes memory source, uint256 offset, uint256 length) + internal pure returns (bytes memory result) { + + result = new bytes(length); + Memory.Pointer srcPtr = Memory.addOffset(Memory.contentPointer(source), offset); + Memory.Pointer destPtr = Memory.contentPointer(result); + + Memory.copy(destPtr, srcPtr, length); +} +---- + +IMPORTANT: Manual memory management increases gas costs and prevents compiler optimizations. Only use these functions after profiling confirms 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 From 124cceee184dc01c1d50301e7ef46a686696d988 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 8 Jun 2025 17:12:00 -0600 Subject: [PATCH 14/19] Nit --- docs/modules/ROOT/pages/utilities.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 68358f8ec2a..6d42ddc914d 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -463,7 +463,7 @@ await instance.multicall([ === Memory -The `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: +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] ---- From c3237dfbaa2cbb9855179d7fc689dbec4cbaa080 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 8 Jun 2025 18:07:37 -0600 Subject: [PATCH 15/19] Finish fuzz tests and FV --- test/utils/Memory.t.sol | 53 ++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol index 8964c164523..dcdc015ea28 100644 --- a/test/utils/Memory.t.sol +++ b/test/utils/Memory.t.sol @@ -4,7 +4,6 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; import {Memory} from "@openzeppelin/contracts/utils/Memory.sol"; -import {Bytes} from "@openzeppelin/contracts/utils/Bytes.sol"; contract MemoryTest is Test { using Memory for *; @@ -25,28 +24,44 @@ contract MemoryTest is Test { assertEq(ptr.asBytes().contentPointer().asBytes32(), ptr.addOffset(32).asBytes32()); } - // function testCopy(bytes memory data, uint256 destSeed) public pure { - // uint256 upperPtr = data.asPointer().asUint256() + data.length; - // Memory.Pointer destPtr = bytes32(bound(destSeed, upperPtr, upperPtr + 100)).asPointer(); - // Memory.copy(data.asPointer(), destPtr, data.length + 32); - // for (uint256 i = 0; i < data.length; i++) { - // assertEq(data[i], destPtr.asBytes()[i]); - // } - // } + function testCopy(bytes memory data, uint256 destSeed) public pure { + uint256 minDestPtr = Memory.getFreePointer().asUint256(); + Memory.Pointer destPtr = bytes32(bound(destSeed, minDestPtr, minDestPtr + END_PTR)).asPointer(); + destPtr.addOffset(data.length + 32).setFreePointer(); + destPtr.copy(data.asPointer(), data.length + 32); + bytes memory copiedData = destPtr.asBytes(); + assertEq(data.length, copiedData.length); + for (uint256 i = 0; i < data.length; i++) { + assertEq(data[i], copiedData[i]); + } + } - function testExtractByte(uint256 seed, uint256 index) public pure { + function testExtractByte(uint256 seed, uint256 index, bytes32 value) public pure { index = bound(index, 0, 31); Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); - assertEq(ptr.extractByte(index), bytes1(ptr.asBytes32() >> (256 - index * 8))); + + assembly ("memory-safe") { + mstore(ptr, value) + } + + bytes1 expected; + assembly ("memory-safe") { + expected := byte(index, value) + } + assertEq(ptr.extractByte(index), expected); } - // function testExtractWord(uint256 seed) public pure { - // Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); - // assertEq(ptr.extractWord(), ptr.asBytes32()); - // } + function testExtractWord(uint256 seed, bytes32 value) public pure { + Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); + assembly ("memory-safe") { + mstore(ptr, value) + } + assertEq(ptr.extractWord(), value); + } - // function testAddOffset(uint256 seed, uint256 offset) public pure { - // Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); - // assertEq(ptr.addOffset(offset).asUint256(), ptr.asUint256() + offset); - // } + function testSymbolicAddOffset(uint256 seed, uint256 offset) public pure { + offset = bound(offset, 0, type(uint256).max - END_PTR); + Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); + assertEq(ptr.addOffset(offset).asUint256(), ptr.asUint256() + offset); + } } From 27f0a9b2926df3170c208914a0c027abe5ef936d Mon Sep 17 00:00:00 2001 From: ernestognw Date: Sun, 8 Jun 2025 18:31:49 -0600 Subject: [PATCH 16/19] up --- contracts/utils/Memory.sol | 6 +++--- test/utils/Memory.t.sol | 8 ++++---- test/utils/Memory.test.js | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index 891754f94d1..84071f4d16b 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -52,15 +52,15 @@ library Memory { * * NOTE: Will return `0x00` if `offset` is larger or equal to `32`. */ - function extractByte(Pointer ptr, uint256 offset) internal pure returns (bytes1 v) { - bytes32 word = extractWord(ptr); + function loadByte(Pointer ptr, uint256 offset) internal pure returns (bytes1 v) { + bytes32 word = load(ptr); assembly ("memory-safe") { v := byte(offset, word) } } /// @dev Extracts a `bytes32` from a `Pointer`. - function extractWord(Pointer ptr) internal pure returns (bytes32 v) { + function load(Pointer ptr) internal pure returns (bytes32 v) { assembly ("memory-safe") { v := mload(ptr) } diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol index dcdc015ea28..3a663d2c95d 100644 --- a/test/utils/Memory.t.sol +++ b/test/utils/Memory.t.sol @@ -36,7 +36,7 @@ contract MemoryTest is Test { } } - function testExtractByte(uint256 seed, uint256 index, bytes32 value) public pure { + function testLoadByte(uint256 seed, uint256 index, bytes32 value) public pure { index = bound(index, 0, 31); Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); @@ -48,15 +48,15 @@ contract MemoryTest is Test { assembly ("memory-safe") { expected := byte(index, value) } - assertEq(ptr.extractByte(index), expected); + assertEq(ptr.loadByte(index), expected); } - function testExtractWord(uint256 seed, bytes32 value) public pure { + function testLoad(uint256 seed, bytes32 value) public pure { Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); assembly ("memory-safe") { mstore(ptr, value) } - assertEq(ptr.extractWord(), value); + assertEq(ptr.load(), value); } function testSymbolicAddOffset(uint256 seed, uint256 offset) public pure { diff --git a/test/utils/Memory.test.js b/test/utils/Memory.test.js index c6ae6ba2d76..7b675d40672 100644 --- a/test/utils/Memory.test.js +++ b/test/utils/Memory.test.js @@ -26,14 +26,14 @@ describe('Memory', function () { }); }); - it('extractWord extracts a word', async function () { + it('load extracts a word', async function () { const ptr = await this.mock.$getFreePointer(); - await expect(this.mock.$extractWord(ptr)).to.eventually.equal(ethers.toBeHex(0, 32)); + await expect(this.mock.$load(ptr)).to.eventually.equal(ethers.toBeHex(0, 32)); }); - it('extractByte extracts a byte', async function () { + it('loadByte extracts a byte', async function () { const ptr = await this.mock.$getFreePointer(); - await expect(this.mock.$extractByte(ptr, 0)).to.eventually.equal(ethers.toBeHex(0, 1)); + await expect(this.mock.$loadByte(ptr, 0)).to.eventually.equal(ethers.toBeHex(0, 1)); }); it('contentPointer', async function () { From e67e8b4fd020806f3c77ea68dab22020c0459e58 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Fri, 4 Jul 2025 15:59:36 -0600 Subject: [PATCH 17/19] up --- contracts/utils/Memory.sol | 4 ++-- docs/modules/ROOT/pages/utilities.adoc | 4 ++-- test/utils/Memory.t.sol | 10 +++++----- test/utils/Memory.test.js | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index 84071f4d16b..d787608dc63 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -15,7 +15,7 @@ library Memory { type Pointer is bytes32; /// @dev Returns a `Pointer` to the current free `Pointer`. - function getFreePointer() internal pure returns (Pointer ptr) { + function getFreeMemoryPointer() internal pure returns (Pointer ptr) { assembly ("memory-safe") { ptr := mload(0x40) } @@ -24,7 +24,7 @@ library Memory { /// @dev Sets the free `Pointer` to a specific value. /// /// WARNING: Everything after the pointer may be overwritten. - function setFreePointer(Pointer ptr) internal pure { + function setFreeMemoryPointer(Pointer ptr) internal pure { assembly ("memory-safe") { mstore(0x40, ptr) } diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index 6d42ddc914d..ee34c0c4c03 100644 --- a/docs/modules/ROOT/pages/utilities.adoc +++ b/docs/modules/ROOT/pages/utilities.adoc @@ -480,11 +480,11 @@ Note that each iteration allocates new memory for `tempData`, causing the memory [source,solidity] ---- function processMultipleItems(uint256[] memory items) internal { - Memory.Pointer ptr = Memory.getFreePointer(); // Cache pointer + 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.setFreePointer(ptr); // Reset pointer for reuse + Memory.setFreeMemoryPointer(ptr); // Reset pointer for reuse } } ---- diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol index 3a663d2c95d..016f328c41b 100644 --- a/test/utils/Memory.t.sol +++ b/test/utils/Memory.t.sol @@ -13,10 +13,10 @@ contract MemoryTest is Test { // - moving the free memory pointer to far causes OOG errors uint256 constant END_PTR = type(uint24).max; - function testGetSetFreePointer(uint256 seed) public pure { + function testGetsetFreeMemoryPointer(uint256 seed) public pure { bytes32 ptr = bytes32(bound(seed, START_PTR, END_PTR)); - ptr.asPointer().setFreePointer(); - assertEq(Memory.getFreePointer().asBytes32(), ptr); + ptr.asPointer().setFreeMemoryPointer(); + assertEq(Memory.getFreeMemoryPointer().asBytes32(), ptr); } function testSymbolicContentPointer(uint256 seed) public pure { @@ -25,9 +25,9 @@ contract MemoryTest is Test { } function testCopy(bytes memory data, uint256 destSeed) public pure { - uint256 minDestPtr = Memory.getFreePointer().asUint256(); + uint256 minDestPtr = Memory.getFreeMemoryPointer().asUint256(); Memory.Pointer destPtr = bytes32(bound(destSeed, minDestPtr, minDestPtr + END_PTR)).asPointer(); - destPtr.addOffset(data.length + 32).setFreePointer(); + destPtr.addOffset(data.length + 32).setFreeMemoryPointer(); destPtr.copy(data.asPointer(), data.length + 32); bytes memory copiedData = destPtr.asBytes(); assertEq(data.length, copiedData.length); diff --git a/test/utils/Memory.test.js b/test/utils/Memory.test.js index 7b675d40672..cd687e2f37c 100644 --- a/test/utils/Memory.test.js +++ b/test/utils/Memory.test.js @@ -16,23 +16,23 @@ describe('Memory', function () { describe('free pointer', function () { it('sets free memory pointer', async function () { const ptr = ethers.toBeHex(0xa0, 32); - await expect(this.mock.$setFreePointer(ptr)).to.not.be.reverted; + await expect(this.mock.$setFreeMemoryPointer(ptr)).to.not.be.reverted; }); it('gets free memory pointer', async function () { - await expect(this.mock.$getFreePointer()).to.eventually.equal( + await expect(this.mock.$getFreeMemoryPointer()).to.eventually.equal( ethers.toBeHex(0x80, 32), // Default pointer ); }); }); it('load extracts a word', async function () { - const ptr = await this.mock.$getFreePointer(); + const ptr = await this.mock.$getFreeMemoryPointer(); await expect(this.mock.$load(ptr)).to.eventually.equal(ethers.toBeHex(0, 32)); }); it('loadByte extracts a byte', async function () { - const ptr = await this.mock.$getFreePointer(); + const ptr = await this.mock.$getFreeMemoryPointer(); await expect(this.mock.$loadByte(ptr, 0)).to.eventually.equal(ethers.toBeHex(0, 1)); }); From 38470504c38f502bd783f7fa03621149dca2ec06 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 12:26:54 -0600 Subject: [PATCH 18/19] Remove extra functions --- contracts/utils/Memory.sol | 46 ----------------------------------- test/utils/Memory.t.sol | 46 ----------------------------------- test/utils/Memory.test.js | 49 -------------------------------------- 3 files changed, 141 deletions(-) diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index d787608dc63..0a4d902a0b6 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -30,57 +30,11 @@ library Memory { } } - /// @dev Returns a `Pointer` to the content of a `bytes` buffer. Skips the length word. - function contentPointer(bytes memory buffer) internal pure returns (Pointer) { - return addOffset(asPointer(buffer), 32); - } - - /** - * @dev Copies `length` bytes from `srcPtr` to `destPtr`. Equivalent to https://www.evm.codes/?fork=cancun#5e[`mcopy`]. - * - * WARNING: Reading or writing beyond the allocated memory bounds of either pointer - * will result in undefined behavior and potential memory corruption. - */ - function copy(Pointer destPtr, Pointer srcPtr, uint256 length) internal pure { - assembly ("memory-safe") { - mcopy(destPtr, srcPtr, length) - } - } - - /** - * @dev Extracts a `bytes1` from a `Pointer`. `offset` starts from the most significant byte. - * - * NOTE: Will return `0x00` if `offset` is larger or equal to `32`. - */ - function loadByte(Pointer ptr, uint256 offset) internal pure returns (bytes1 v) { - bytes32 word = load(ptr); - assembly ("memory-safe") { - v := byte(offset, word) - } - } - - /// @dev Extracts a `bytes32` from a `Pointer`. - function load(Pointer ptr) internal pure returns (bytes32 v) { - assembly ("memory-safe") { - v := mload(ptr) - } - } - - /// @dev Adds an offset to a `Pointer`. - function addOffset(Pointer ptr, uint256 offset) internal pure returns (Pointer) { - return asPointer(bytes32(asUint256(ptr) + offset)); - } - /// @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 `Pointer` to `uint256`. Expects a pointer to a properly ABI-encoded `bytes` object. - function asUint256(Pointer ptr) internal pure returns (uint256) { - return uint256(asBytes32(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/test/utils/Memory.t.sol b/test/utils/Memory.t.sol index 016f328c41b..8ed9b4c43bc 100644 --- a/test/utils/Memory.t.sol +++ b/test/utils/Memory.t.sol @@ -18,50 +18,4 @@ contract MemoryTest is Test { ptr.asPointer().setFreeMemoryPointer(); assertEq(Memory.getFreeMemoryPointer().asBytes32(), ptr); } - - function testSymbolicContentPointer(uint256 seed) public pure { - Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); - assertEq(ptr.asBytes().contentPointer().asBytes32(), ptr.addOffset(32).asBytes32()); - } - - function testCopy(bytes memory data, uint256 destSeed) public pure { - uint256 minDestPtr = Memory.getFreeMemoryPointer().asUint256(); - Memory.Pointer destPtr = bytes32(bound(destSeed, minDestPtr, minDestPtr + END_PTR)).asPointer(); - destPtr.addOffset(data.length + 32).setFreeMemoryPointer(); - destPtr.copy(data.asPointer(), data.length + 32); - bytes memory copiedData = destPtr.asBytes(); - assertEq(data.length, copiedData.length); - for (uint256 i = 0; i < data.length; i++) { - assertEq(data[i], copiedData[i]); - } - } - - function testLoadByte(uint256 seed, uint256 index, bytes32 value) public pure { - index = bound(index, 0, 31); - Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); - - assembly ("memory-safe") { - mstore(ptr, value) - } - - bytes1 expected; - assembly ("memory-safe") { - expected := byte(index, value) - } - assertEq(ptr.loadByte(index), expected); - } - - function testLoad(uint256 seed, bytes32 value) public pure { - Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); - assembly ("memory-safe") { - mstore(ptr, value) - } - assertEq(ptr.load(), value); - } - - function testSymbolicAddOffset(uint256 seed, uint256 offset) public pure { - offset = bound(offset, 0, type(uint256).max - END_PTR); - Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); - assertEq(ptr.addOffset(offset).asUint256(), ptr.asUint256() + offset); - } } diff --git a/test/utils/Memory.test.js b/test/utils/Memory.test.js index cd687e2f37c..6a1159c1493 100644 --- a/test/utils/Memory.test.js +++ b/test/utils/Memory.test.js @@ -26,39 +26,6 @@ describe('Memory', function () { }); }); - it('load extracts a word', async function () { - const ptr = await this.mock.$getFreeMemoryPointer(); - await expect(this.mock.$load(ptr)).to.eventually.equal(ethers.toBeHex(0, 32)); - }); - - it('loadByte extracts a byte', async function () { - const ptr = await this.mock.$getFreeMemoryPointer(); - await expect(this.mock.$loadByte(ptr, 0)).to.eventually.equal(ethers.toBeHex(0, 1)); - }); - - it('contentPointer', async function () { - const data = ethers.toUtf8Bytes('hello world'); - const result = await this.mock.$contentPointer(data); - expect(result).to.equal(ethers.toBeHex(0xa0, 32)); // 0x80 is the default free pointer (length) - }); - - describe('addOffset', function () { - it('addOffset', async function () { - const basePtr = ethers.toBeHex(0x80, 32); - const offset = 32; - const expectedPtr = ethers.toBeHex(0xa0, 32); - - await expect(this.mock.$addOffset(basePtr, offset)).to.eventually.equal(expectedPtr); - }); - - it('addOffsetwraps around', async function () { - const basePtr = ethers.toBeHex(0x80, 32); - const offset = 256; - const expectedPtr = ethers.toBeHex(0x180, 32); - await expect(this.mock.$addOffset(basePtr, offset)).to.eventually.equal(expectedPtr); - }); - }); - describe('pointer conversions', function () { it('asBytes32 / asPointer', async function () { const ptr = ethers.toBeHex('0x1234', 32); @@ -71,21 +38,5 @@ describe('Memory', function () { expect(ptr).to.equal(ethers.toBeHex(0x80, 32)); // Default free pointer await expect(this.mock.$asBytes(ptr)).to.eventually.equal(ethers.toBeHex(0x20, 32)); }); - - it('asUint256', async function () { - const value = 0x1234; - const ptr = ethers.toBeHex(value, 32); - await expect(this.mock.$asUint256(ptr)).to.eventually.equal(value); - }); - }); - - describe('memory operations', function () { - it('copy', async function () { - await expect(this.mock.$copy(ethers.toBeHex(0x80, 32), ethers.toBeHex(0xc0, 32), 32)).to.not.be.reverted; - }); - - it('copy with zero length', async function () { - await expect(this.mock.$copy(ethers.toBeHex(0x80, 32), ethers.toBeHex(0xc0, 32), 0)).to.not.be.reverted; - }); }); }); From 59f0aef2339eb1faf3e38638f92191e244dceace Mon Sep 17 00:00:00 2001 From: ernestognw Date: Wed, 9 Jul 2025 12:27:35 -0600 Subject: [PATCH 19/19] Revert "Remove extra functions" This reverts commit 38470504c38f502bd783f7fa03621149dca2ec06. --- contracts/utils/Memory.sol | 46 +++++++++++++++++++++++++++++++++++ test/utils/Memory.t.sol | 46 +++++++++++++++++++++++++++++++++++ test/utils/Memory.test.js | 49 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) diff --git a/contracts/utils/Memory.sol b/contracts/utils/Memory.sol index 0a4d902a0b6..d787608dc63 100644 --- a/contracts/utils/Memory.sol +++ b/contracts/utils/Memory.sol @@ -30,11 +30,57 @@ library Memory { } } + /// @dev Returns a `Pointer` to the content of a `bytes` buffer. Skips the length word. + function contentPointer(bytes memory buffer) internal pure returns (Pointer) { + return addOffset(asPointer(buffer), 32); + } + + /** + * @dev Copies `length` bytes from `srcPtr` to `destPtr`. Equivalent to https://www.evm.codes/?fork=cancun#5e[`mcopy`]. + * + * WARNING: Reading or writing beyond the allocated memory bounds of either pointer + * will result in undefined behavior and potential memory corruption. + */ + function copy(Pointer destPtr, Pointer srcPtr, uint256 length) internal pure { + assembly ("memory-safe") { + mcopy(destPtr, srcPtr, length) + } + } + + /** + * @dev Extracts a `bytes1` from a `Pointer`. `offset` starts from the most significant byte. + * + * NOTE: Will return `0x00` if `offset` is larger or equal to `32`. + */ + function loadByte(Pointer ptr, uint256 offset) internal pure returns (bytes1 v) { + bytes32 word = load(ptr); + assembly ("memory-safe") { + v := byte(offset, word) + } + } + + /// @dev Extracts a `bytes32` from a `Pointer`. + function load(Pointer ptr) internal pure returns (bytes32 v) { + assembly ("memory-safe") { + v := mload(ptr) + } + } + + /// @dev Adds an offset to a `Pointer`. + function addOffset(Pointer ptr, uint256 offset) internal pure returns (Pointer) { + return asPointer(bytes32(asUint256(ptr) + offset)); + } + /// @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 `Pointer` to `uint256`. Expects a pointer to a properly ABI-encoded `bytes` object. + function asUint256(Pointer ptr) internal pure returns (uint256) { + return uint256(asBytes32(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/test/utils/Memory.t.sol b/test/utils/Memory.t.sol index 8ed9b4c43bc..016f328c41b 100644 --- a/test/utils/Memory.t.sol +++ b/test/utils/Memory.t.sol @@ -18,4 +18,50 @@ contract MemoryTest is Test { ptr.asPointer().setFreeMemoryPointer(); assertEq(Memory.getFreeMemoryPointer().asBytes32(), ptr); } + + function testSymbolicContentPointer(uint256 seed) public pure { + Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); + assertEq(ptr.asBytes().contentPointer().asBytes32(), ptr.addOffset(32).asBytes32()); + } + + function testCopy(bytes memory data, uint256 destSeed) public pure { + uint256 minDestPtr = Memory.getFreeMemoryPointer().asUint256(); + Memory.Pointer destPtr = bytes32(bound(destSeed, minDestPtr, minDestPtr + END_PTR)).asPointer(); + destPtr.addOffset(data.length + 32).setFreeMemoryPointer(); + destPtr.copy(data.asPointer(), data.length + 32); + bytes memory copiedData = destPtr.asBytes(); + assertEq(data.length, copiedData.length); + for (uint256 i = 0; i < data.length; i++) { + assertEq(data[i], copiedData[i]); + } + } + + function testLoadByte(uint256 seed, uint256 index, bytes32 value) public pure { + index = bound(index, 0, 31); + Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); + + assembly ("memory-safe") { + mstore(ptr, value) + } + + bytes1 expected; + assembly ("memory-safe") { + expected := byte(index, value) + } + assertEq(ptr.loadByte(index), expected); + } + + function testLoad(uint256 seed, bytes32 value) public pure { + Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); + assembly ("memory-safe") { + mstore(ptr, value) + } + assertEq(ptr.load(), value); + } + + function testSymbolicAddOffset(uint256 seed, uint256 offset) public pure { + offset = bound(offset, 0, type(uint256).max - END_PTR); + Memory.Pointer ptr = bytes32(bound(seed, START_PTR, END_PTR)).asPointer(); + assertEq(ptr.addOffset(offset).asUint256(), ptr.asUint256() + offset); + } } diff --git a/test/utils/Memory.test.js b/test/utils/Memory.test.js index 6a1159c1493..cd687e2f37c 100644 --- a/test/utils/Memory.test.js +++ b/test/utils/Memory.test.js @@ -26,6 +26,39 @@ describe('Memory', function () { }); }); + it('load extracts a word', async function () { + const ptr = await this.mock.$getFreeMemoryPointer(); + await expect(this.mock.$load(ptr)).to.eventually.equal(ethers.toBeHex(0, 32)); + }); + + it('loadByte extracts a byte', async function () { + const ptr = await this.mock.$getFreeMemoryPointer(); + await expect(this.mock.$loadByte(ptr, 0)).to.eventually.equal(ethers.toBeHex(0, 1)); + }); + + it('contentPointer', async function () { + const data = ethers.toUtf8Bytes('hello world'); + const result = await this.mock.$contentPointer(data); + expect(result).to.equal(ethers.toBeHex(0xa0, 32)); // 0x80 is the default free pointer (length) + }); + + describe('addOffset', function () { + it('addOffset', async function () { + const basePtr = ethers.toBeHex(0x80, 32); + const offset = 32; + const expectedPtr = ethers.toBeHex(0xa0, 32); + + await expect(this.mock.$addOffset(basePtr, offset)).to.eventually.equal(expectedPtr); + }); + + it('addOffsetwraps around', async function () { + const basePtr = ethers.toBeHex(0x80, 32); + const offset = 256; + const expectedPtr = ethers.toBeHex(0x180, 32); + await expect(this.mock.$addOffset(basePtr, offset)).to.eventually.equal(expectedPtr); + }); + }); + describe('pointer conversions', function () { it('asBytes32 / asPointer', async function () { const ptr = ethers.toBeHex('0x1234', 32); @@ -38,5 +71,21 @@ describe('Memory', function () { expect(ptr).to.equal(ethers.toBeHex(0x80, 32)); // Default free pointer await expect(this.mock.$asBytes(ptr)).to.eventually.equal(ethers.toBeHex(0x20, 32)); }); + + it('asUint256', async function () { + const value = 0x1234; + const ptr = ethers.toBeHex(value, 32); + await expect(this.mock.$asUint256(ptr)).to.eventually.equal(value); + }); + }); + + describe('memory operations', function () { + it('copy', async function () { + await expect(this.mock.$copy(ethers.toBeHex(0x80, 32), ethers.toBeHex(0xc0, 32), 32)).to.not.be.reverted; + }); + + it('copy with zero length', async function () { + await expect(this.mock.$copy(ethers.toBeHex(0x80, 32), ethers.toBeHex(0xc0, 32), 0)).to.not.be.reverted; + }); }); });