Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
9eb5f1c
Add memory utils
ernestognw Sep 4, 2024
2d397f4
Fix tests upgradeable
ernestognw Sep 4, 2024
2a0fb7e
Add docs
ernestognw Sep 5, 2024
a7e61c3
Make use of the library
ernestognw Sep 5, 2024
1aae8bb
Update docs/modules/ROOT/pages/utilities.adoc
ernestognw Oct 9, 2024
1b2679a
Merge branch 'master' into utils/memory
Amxx Mar 6, 2025
d514606
fix tests
Amxx Mar 6, 2025
14fa04e
Update contracts/utils/Memory.sol
ernestognw May 7, 2025
d0d55fc
Update contracts/utils/Memory.sol
arr00 May 7, 2025
7b3cb66
Add RLP library
ernestognw May 10, 2025
95149f8
Add TrieProof library
ernestognw May 11, 2025
ad5d4ac
up
ernestognw May 11, 2025
18540ef
Add docs
ernestognw May 11, 2025
163f27c
Workaround stack too deep
ernestognw May 24, 2025
c484289
Add Changesets
ernestognw May 29, 2025
e0d4790
Add more changesets
ernestognw Jun 7, 2025
a6f9053
Add FV and fuzz tests
ernestognw Jun 7, 2025
e2b5e4c
Merge branch 'master' into feature/rlp
ernestognw Jun 7, 2025
203d1a2
up
ernestognw Jun 7, 2025
48eabc1
docs
ernestognw Jun 7, 2025
63ced95
up pragma
ernestognw Jun 7, 2025
f342756
Add missing Bytes test
ernestognw Jun 7, 2025
23dba37
Add unit tests
ernestognw Jun 7, 2025
0cacca2
up pragma
ernestognw Jun 7, 2025
831e8ab
Move TrieProof
ernestognw Jun 7, 2025
5da111f
Fix countLeadingZeroes
ernestognw Jun 8, 2025
ba2293e
nits
ernestognw Jun 8, 2025
9409bc6
Improve
ernestognw Jun 8, 2025
e740dac
Fix
ernestognw Jun 8, 2025
0332ffe
Add Memory.sol library
ernestognw Jun 8, 2025
608e3cd
Merge branch 'master' into utils/memory
ernestognw Jun 8, 2025
ac92bb4
up
ernestognw Jun 8, 2025
6094bb7
Merge branch 'master' into utils/memory
ernestognw Jun 8, 2025
6bb96d5
WIP: Add more Memory functions
ernestognw Jun 8, 2025
860e5a8
up
ernestognw Jun 8, 2025
ecdb768
revert
ernestognw Jun 8, 2025
95907aa
Update docs
ernestognw Jun 8, 2025
124ccee
Nit
ernestognw Jun 8, 2025
c3237df
Finish fuzz tests and FV
ernestognw Jun 9, 2025
27f0a9b
up
ernestognw Jun 9, 2025
282ce39
up
ernestognw Jun 9, 2025
bdd2cf1
Add operations to Math.sol
ernestognw Jun 9, 2025
42c79f1
Add new equal, nibbles and countLeadingZeroes functions
ernestognw Jun 9, 2025
5754ab8
Rename countLeadingZeroes to clz
ernestognw Jun 9, 2025
44f0e14
up
ernestognw Jun 9, 2025
05c73bd
Pragma changes
ernestognw Jun 9, 2025
3a6fbf6
up
ernestognw Jun 9, 2025
e67e8b4
up
ernestognw Jul 4, 2025
3385718
Rename to in Math library and update corresponding tests for consis…
ernestognw Jul 9, 2025
40d7922
Update return types of reverseBits functions to match their respectiv…
ernestognw Jul 9, 2025
89860bc
Refactor reverseBits functions in to use fixed-size byte types
ernestognw Jul 9, 2025
9b58730
Test nits
ernestognw Jul 9, 2025
77ffa8c
Simplify
ernestognw Jul 9, 2025
ce91c80
up
ernestognw Jul 9, 2025
b3e3add
Move reverse functions to Bytes.sol
ernestognw Jul 9, 2025
2f3107c
Move Bytes.t.sol
ernestognw Jul 9, 2025
4383e01
Merge branch 'master' into feat/bytes-rlp
ernestognw Jul 9, 2025
5a44b11
up
ernestognw Jul 9, 2025
d6db2d7
Document
ernestognw Jul 9, 2025
3847050
Remove extra functions
ernestognw Jul 9, 2025
4fd1947
Update docs
ernestognw Jul 9, 2025
c4e0375
up
ernestognw Jul 9, 2025
acb14cb
Merge branch 'utils/memory' into feature/rlp
ernestognw Jul 9, 2025
2208006
Merge branch 'feat/math-reverse-bits' into feature/rlp
ernestognw Jul 9, 2025
13f4d8f
Merge branch 'feat/bytes-rlp' into feature/rlp
ernestognw Jul 9, 2025
502a520
Merge branch 'master' into feature/rlp
ernestognw Jul 12, 2025
aab9274
Merge branch 'master' into feature/rlp
Amxx Jul 15, 2025
aa26e48
up
Amxx Jul 15, 2025
948f0a1
Merge branch 'master' into feature/rlp
ernestognw Jul 31, 2025
d4bfb8b
Fix compilation
ernestognw Jul 31, 2025
138de7f
Remove dangling clz
ernestognw Jul 31, 2025
5efeb37
Make nibbles function private
ernestognw Jul 31, 2025
00ff228
Remove nibbles test
ernestognw Jul 31, 2025
c58c7fd
Remove TrieProof library
ernestognw Jul 31, 2025
d1aa944
Add some tests
ernestognw Aug 1, 2025
0925577
Merge branch 'master' into feature/rlp
ernestognw Aug 26, 2025
590b7d7
Use Bytes.concat
ernestognw Aug 26, 2025
8928039
Use Math.ternary
ernestognw Aug 27, 2025
bb027df
refactor encoding to reduce memory allocation
Amxx Sep 4, 2025
ec4a5c1
refactor tests to use ethers.encodeRlp as a reference
Amxx Sep 4, 2025
b75dc7c
fix comments
Amxx Sep 4, 2025
fc208c8
use mstore8 to avoid shift
Amxx Sep 4, 2025
fe0597c
decoding works
Amxx Sep 6, 2025
106764e
Add Memory.slice and refactor RLP read/decode
Amxx Sep 6, 2025
b55b1db
testing
Amxx Sep 6, 2025
e490d79
inspired by
Amxx Sep 6, 2025
bde8251
remove unecessary imports
Amxx Sep 6, 2025
8b6ea8c
read bool and string
Amxx Sep 7, 2025
dba642d
Bytes.Accumulator & RLP.encoder
Amxx Sep 8, 2025
5e3ce7c
reorder
Amxx Sep 8, 2025
4d39206
Merge branch 'master' into feature/rlp
Amxx Sep 10, 2025
902b222
document Bytes.accumulator
Amxx Sep 10, 2025
b301b06
pragma fix
Amxx Sep 10, 2025
1b112b2
mock stateless
Amxx Sep 10, 2025
9c0e924
slither
Amxx Sep 10, 2025
c48e623
Memory slice doc
Amxx Sep 10, 2025
b10ae8c
Update enums.js
Amxx Sep 10, 2025
d411318
avoid load error
Amxx Sep 10, 2025
df528af
fuzz test Memory slices
Amxx Sep 10, 2025
da61a5e
coverage
Amxx Sep 10, 2025
fadc8ac
Merge branch 'master' into feature/rlp
Amxx Sep 15, 2025
283ce7c
remove unused code
Amxx Sep 15, 2025
5561d52
Merge branch 'feature/rlp' of https://github.com/ernestognw/openzeppe…
Amxx Sep 15, 2025
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/lovely-cooks-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`RLP`: Add library for Ethereum's Recursive Length Prefix encoding/decoding.
1 change: 1 addition & 0 deletions contracts/mocks/Stateless.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {P256} from "../utils/cryptography/P256.sol";
import {Packing} from "../utils/Packing.sol";
import {Panic} from "../utils/Panic.sol";
import {RelayedCall} from "../utils/RelayedCall.sol";
import {RLP} from "../utils/RLP.sol";
import {RSA} from "../utils/cryptography/RSA.sol";
import {SafeCast} from "../utils/math/SafeCast.sol";
import {SafeERC20} from "../token/ERC20/utils/SafeERC20.sol";
Expand Down
2 changes: 1 addition & 1 deletion contracts/token/ERC20/extensions/ERC4626.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.4.0) (token/ERC20/extensions/ERC4626.sol)

pragma solidity ^0.8.20;
pragma solidity ^0.8.24;

import {IERC20, IERC20Metadata, ERC20} from "../ERC20.sol";
import {SafeERC20} from "../utils/SafeERC20.sol";
Expand Down
84 changes: 84 additions & 0 deletions contracts/utils/Bytes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
pragma solidity ^0.8.24;

import {Math} from "./math/Math.sol";
import {Memory} from "./Memory.sol";

/**
* @dev Bytes operations.
Expand Down Expand Up @@ -244,4 +245,87 @@ library Bytes {
value := mload(add(add(buffer, 0x20), offset))
}
}

/**
* @dev Bytes accumulator: a linked list of `bytes`.
*
* Note: This is a memory structure that SHOULD not be put in storage.
*/
struct Accumulator {
Memory.Pointer head;
Memory.Pointer tail;
}

/// @dev Item (list node) in a bytes accumulator
struct AccumulatorEntry {
Memory.Pointer next;
bytes data;
}

/// @dev Create a new (empty) accumulator
function accumulator() internal pure returns (Accumulator memory self) {
self.head = Memory.asPointer(0x00);
self.tail = Memory.asPointer(0x00);
}

/// @dev Add a bytes buffer to (the end of) an Accumulator
function push(Accumulator memory self, bytes memory data) internal pure returns (Accumulator memory) {
Memory.Pointer ptr = _asPtr(AccumulatorEntry({next: Memory.asPointer(0x00), data: data}));

if (Memory.asBytes32(self.head) == 0x00) {
self.head = ptr;
self.tail = ptr;
} else {
_asAccumulatorEntry(self.tail).next = ptr;
self.tail = ptr;
}

return self;
}

/// @dev Add a bytes buffer to (the beginning of) an Accumulator
function shift(Accumulator memory self, bytes memory data) internal pure returns (Accumulator memory) {
Memory.Pointer ptr = _asPtr(AccumulatorEntry({next: self.head, data: data}));

if (Memory.asBytes32(self.head) == 0x00) {
self.head = ptr;
self.tail = ptr;
} else {
self.head = ptr;
}

return self;
}

/// @dev Flatten all the bytes entries in an Accumulator into a single buffer
function flatten(Accumulator memory self) internal pure returns (bytes memory result) {
assembly ("memory-safe") {
result := mload(0x40)
let ptr := add(result, 0x20)
for {
let it := mload(self)
} iszero(iszero(it)) {
it := mload(it)
} {
let buffer := mload(add(it, 0x20))
let length := mload(buffer)
mcopy(ptr, add(buffer, 0x20), length)
ptr := add(ptr, length)
}
mstore(result, sub(ptr, add(result, 0x20)))
mstore(0x40, ptr)
}
Comment on lines +302 to +317
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: free memory pointer not 32‑byte aligned in flatten().

FMP must be rounded up to the next 32‑byte boundary. Writing mstore(0x40, ptr) leaves it potentially unaligned and can corrupt subsequent allocations.

Apply this diff:

     function flatten(Accumulator memory self) internal pure returns (bytes memory result) {
         assembly ("memory-safe") {
             result := mload(0x40)
-            let ptr := add(result, 0x20)
+            let dataStart := add(result, 0x20)
+            let ptr := dataStart
             for {
                 let it := mload(self)
             } iszero(iszero(it)) {
                 it := mload(it)
             } {
                 let buffer := mload(add(it, 0x20))
                 let length := mload(buffer)
                 mcopy(ptr, add(buffer, 0x20), length)
                 ptr := add(ptr, length)
             }
-            mstore(result, sub(ptr, add(result, 0x20)))
-            mstore(0x40, ptr)
+            let total := sub(ptr, dataStart)
+            mstore(result, total)
+            // advance FMP to the next 32-byte boundary after the copied data
+            let newFmp := and(add(ptr, 31), not(31))
+            mstore(0x40, newFmp)
         }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assembly ("memory-safe") {
result := mload(0x40)
let ptr := add(result, 0x20)
for {
let it := mload(self)
} iszero(iszero(it)) {
it := mload(it)
} {
let buffer := mload(add(it, 0x20))
let length := mload(buffer)
mcopy(ptr, add(buffer, 0x20), length)
ptr := add(ptr, length)
}
mstore(result, sub(ptr, add(result, 0x20)))
mstore(0x40, ptr)
}
function flatten(Accumulator memory self) internal pure returns (bytes memory result) {
assembly ("memory-safe") {
result := mload(0x40)
let dataStart := add(result, 0x20)
let ptr := dataStart
for { let it := mload(self) } iszero(iszero(it)) { it := mload(it) } {
let buffer := mload(add(it, 0x20))
let length := mload(buffer)
mcopy(ptr, add(buffer, 0x20), length)
ptr := add(ptr, length)
}
let total := sub(ptr, dataStart)
mstore(result, total)
// advance FMP to the next 32-byte boundary after the copied data
let newFmp := and(add(ptr, 31), not(31))
mstore(0x40, newFmp)
}
}
🤖 Prompt for AI Agents
In contracts/utils/Bytes.sol around lines 302 to 317 the free memory pointer
stored with mstore(0x40, ptr) may be left unaligned after writing flattened
bytes; update the final free-memory store to round ptr up to the next 32-byte
boundary before storing it (i.e., compute alignedPtr = (ptr + 31) & ~31 and
mstore(0x40, alignedPtr)) so the FMP remains 32-byte aligned and subsequent
allocations are safe.

}

function _asPtr(AccumulatorEntry memory item) private pure returns (Memory.Pointer ptr) {
assembly ("memory-safe") {
ptr := item
}
}

function _asAccumulatorEntry(Memory.Pointer ptr) private pure returns (AccumulatorEntry memory item) {
assembly ("memory-safe") {
item := ptr
}
}
}
71 changes: 70 additions & 1 deletion contracts/utils/Memory.sol
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;
pragma solidity ^0.8.24;

import {Panic} from "./Panic.sol";

/**
* @dev Utilities to manipulate memory.
Expand Down Expand Up @@ -41,4 +43,71 @@ library Memory {
function asPointer(bytes32 value) internal pure returns (Pointer) {
return Pointer.wrap(value);
}

type Slice is bytes32;

/// @dev Get a slice representation of a bytes object in memory
function asSlice(bytes memory self) internal pure returns (Slice result) {
assembly ("memory-safe") {
result := or(shl(128, mload(self)), add(self, 0x20))
}
}

/// @dev Private helper: create a slice from raw values (length and pointer)
function _asSlice(uint256 len, Memory.Pointer ptr) private pure returns (Slice result) {
// TODO: Fail if len or ptr is larger than type(uint128).max ?
assembly ("memory-safe") {
result := or(shl(128, len), ptr)
}
}
Comment on lines +58 to +62
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add bounds validation for length and pointer values.

The TODO comment correctly identifies a potential issue. Values exceeding type(uint128).max would cause silent truncation when packed into the 128-bit fields.

Apply this fix to add validation:

 function _asSlice(uint256 len, Memory.Pointer ptr) private pure returns (Slice result) {
-    // TODO: Fail if len or ptr  is larger than type(uint128).max ?
+    require(len <= type(uint128).max, "Length exceeds uint128 max");
+    require(uint256(asBytes32(ptr)) <= type(uint128).max, "Pointer exceeds uint128 max");
     assembly ("memory-safe") {
         result := or(shl(128, len), ptr)
     }
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In contracts/utils/Memory.sol around lines 58-62, add explicit bounds checks
before the assembly packing so neither len nor ptr exceed type(uint128).max; if
either value is > type(uint128).max revert with a clear error (or custom error)
to avoid silent truncation, then safely cast to uint128 (or mask) and perform
the existing assembly pack. Ensure both checks run on the original uint256
inputs and use descriptive revert messages or a custom error name.


/// @dev Returns the memory location of a given slice (equiv to self.offset for calldata slices)
function _pointer(Slice self) private pure returns (Memory.Pointer result) {
assembly ("memory-safe") {
result := and(self, shr(128, not(0)))
}
}

/// @dev Returns the length of a given slice (equiv to self.length for calldata slices)
function length(Slice self) internal pure returns (uint256 result) {
assembly ("memory-safe") {
result := shr(128, self)
}
}

/// @dev Offset a memory slice (equivalent to self[start:] for calldata slices)
function slice(Slice self, uint256 offset) internal pure returns (Slice) {
if (offset > length(self)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
return _asSlice(length(self) - offset, asPointer(bytes32(uint256(asBytes32(_pointer(self))) + offset)));
}

/// @dev Offset and cut a Slice (equivalent to self[start:start+length] for calldata slices)
function slice(Slice self, uint256 offset, uint256 len) internal pure returns (Slice) {
if (offset + len > length(self)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
return _asSlice(len, asPointer(bytes32(uint256(asBytes32(_pointer(self))) + offset)));
}

/**
* @dev Read a bytes32 buffer from a given Slice at a specific offset
*
* Note:If offset > length(slice) - 32, part of the return value will be out of bound and should be ignored.
*/
function load(Slice self, uint256 offset) internal pure returns (bytes32 value) {
if (offset >= length(self)) Panic.panic(Panic.ARRAY_OUT_OF_BOUNDS);
assembly ("memory-safe") {
value := mload(add(and(self, shr(128, not(0))), offset))
}
}

/// @dev Extract the data corresponding to a Slice (allocate new memory)
function toBytes(Slice self) internal pure returns (bytes memory result) {
uint256 len = length(self);
Memory.Pointer ptr = _pointer(self);
assembly ("memory-safe") {
result := mload(0x40)
mstore(result, len)
mcopy(add(result, 0x20), ptr, len)
mstore(0x40, add(add(result, len), 0x20))
}
}
}
3 changes: 3 additions & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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].
* {RelayedCall}: A library for performing calls that use minimal and predictable relayers to hide the sender.
* {RLP}: Library for encoding and decoding data in Ethereum's Recursive Length Prefix format.
* {ShortStrings}: Library to encode (and decode) short strings into (or from) a single bytes32 slot for optimizing costs. Short strings are limited to 31 characters.
* {SlotDerivation}: Methods for deriving storage slot from ERC-7201 namespaces as well as from constructions such as mapping and arrays.
* {StorageSlot}: Methods for accessing specific storage slots formatted as common primitive types.
Expand Down Expand Up @@ -138,6 +139,8 @@ Ethereum contracts have no native concept of an interface, so applications must

{{RelayedCall}}

{{RLP}}

{{ShortStrings}}

{{SlotDerivation}}
Expand Down
Loading