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/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/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 {} 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/Memory.sol b/contracts/utils/Memory.sol new file mode 100644 index 00000000000..abb6f100bc6 --- /dev/null +++ b/contracts/utils/Memory.sol @@ -0,0 +1,40 @@ +// 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 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/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; } } diff --git a/docs/modules/ROOT/pages/utilities.adoc b/docs/modules/ROOT/pages/utilities.adoc index b8afec4eabd..84ede5a4b5e 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 +} +---- + +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]. diff --git a/test/utils/Memory.t.sol b/test/utils/Memory.t.sol new file mode 100644 index 00000000000..99cb75fb095 --- /dev/null +++ b/test/utils/Memory.t.sol @@ -0,0 +1,15 @@ +// 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.setFreePointer(ptr.asPointer()); + assertEq(Memory.getFreePointer().asBytes32(), ptr); + } +} 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); + }); + }); +});