Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Memory utility library #5189

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dull-students-eat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`Memory`: Add library with utilities to manipulate memory
3 changes: 3 additions & 0 deletions contracts/access/manager/AuthorityUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
pragma solidity ^0.8.20;

import {IAuthority} from "./IAuthority.sol";
import {Memory} from "../../utils/Memory.sol";

library AuthorityUtils {
/**
Expand All @@ -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))
);
Expand All @@ -27,6 +29,7 @@ library AuthorityUtils {
immediate = abi.decode(data, (bool));
}
}
Memory.setFreePointer(ptr);
return (immediate, delay);
}
}
1 change: 1 addition & 0 deletions contracts/mocks/Stateless.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
3 changes: 3 additions & 0 deletions contracts/token/ERC20/extensions/ERC4626.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, ())
);
Expand All @@ -93,6 +95,7 @@ abstract contract ERC4626 is ERC20, IERC4626 {
return (true, uint8(returnedDecimals));
}
}
Memory.setFreePointer(ptr);
return (false, 0);
}

Expand Down
7 changes: 7 additions & 0 deletions contracts/token/ERC20/utils/SafeERC20.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,15 +33,19 @@ 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);
}

/**
* @dev Transfer `value` amount of `token` from `from` to `to`, spending the approval given by `from` to the
* 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);
}

/**
Expand Down Expand Up @@ -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);
}

/**
Expand Down
40 changes: 40 additions & 0 deletions contracts/utils/Memory.sol
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we call that type type FMP is bytes32; ?

Copy link
Member Author

@ernestognw ernestognw Sep 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be better with FreePointer as FMP is not obvious at first.

EDIT: Acutally I do prefer just Pointer, since it's not always the free memory pointer. Anyone can get a point to wherever they want with asPointer

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would we rename the getFreePointer and setFreePointer to getFreeMemoryPointer and setFreeMemoryPointer ?


/// @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);
}
}
1 change: 1 addition & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
====
Expand Down
6 changes: 5 additions & 1 deletion contracts/utils/cryptography/SignatureChecker.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
36 changes: 35 additions & 1 deletion docs/modules/ROOT/pages/utilities.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
ernestognw marked this conversation as resolved.
Show resolved Hide resolved

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].
15 changes: 15 additions & 0 deletions test/utils/Memory.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
41 changes: 41 additions & 0 deletions test/utils/Memory.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
});