Skip to content

Bytes library and CAIP2/CAIP10 helpers #5252

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

Merged
merged 51 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
b2eedbe
Strings: add toUint, toInt and hexToUint
Amxx Aug 28, 2024
efd2f30
codespell
Amxx Aug 28, 2024
bc42b25
Update contracts/utils/Strings.sol
Amxx Aug 29, 2024
07f4b44
Update .changeset/eighty-hounds-promise.md
Amxx Sep 2, 2024
40ba631
Update contracts/utils/Strings.sol
Amxx Sep 3, 2024
07ec518
Update Strings.sol
Amxx Sep 3, 2024
95fb0db
Apply suggestions from code review
Amxx Sep 3, 2024
f263819
Update contracts/utils/Strings.sol
Amxx Sep 3, 2024
f51fbe6
Update Strings.sol
Amxx Sep 3, 2024
52a301b
Fix value variable
cairoeth Sep 3, 2024
027859e
make return explicit
Amxx Sep 4, 2024
a91a999
branchless
Amxx Sep 4, 2024
86abf5a
Update contracts/utils/Strings.sol
Amxx Sep 5, 2024
6dca3cb
Update contracts/utils/Strings.sol
Amxx Sep 5, 2024
a7a6e9e
add try variants + use for governor proposal parsing
Amxx Sep 9, 2024
ec9a659
parseAddress
Amxx Sep 11, 2024
568dc7b
use string literal for 0x
Amxx Sep 17, 2024
0292c31
Apply suggestions from code review
Amxx Sep 17, 2024
aea4a14
add support for + prefix in parseInt
Amxx Sep 17, 2024
cf78a9f
Remove invalid "memory-safe" annotation.
Amxx Sep 17, 2024
26cec97
Merge branch 'master' into feature/parse-strings
Amxx Sep 18, 2024
3a7f904
Merge branch 'master' into feature/parse-strings
Amxx Oct 11, 2024
4d18729
Add Bytes.sol
Amxx Oct 11, 2024
c7a7c94
codespell
Amxx Oct 11, 2024
d6319e8
cleanup
Amxx Oct 11, 2024
b3bf461
Update .changeset/eighty-hounds-promise.md
Amxx Oct 11, 2024
2ab63b7
Update .changeset/rude-cougars-look.md
Amxx Oct 11, 2024
231b93b
optimization
Amxx Oct 11, 2024
24f1490
optimization
Amxx Oct 11, 2024
43f0dc1
testing
Amxx Oct 11, 2024
7b7c1fd
comment update
Amxx Oct 11, 2024
2abfa49
Update contracts/utils/Strings.sol
Amxx Oct 11, 2024
f433e6d
making unsafeReadBytesOffset private
Amxx Oct 11, 2024
27c7c0d
optimize
Amxx Oct 11, 2024
75e1e4c
Update contracts/utils/README.adoc
Amxx Oct 11, 2024
4f48757
Update contracts/governance/Governor.sol
Amxx Oct 11, 2024
1ec1e3f
rename parseHex to parseHexUint
Amxx Oct 11, 2024
53d72d7
fix tests
Amxx Oct 11, 2024
c5790f8
optimize
Amxx Oct 14, 2024
f6007b0
add CAIP2, CAIP10 and Bytes
Amxx Oct 11, 2024
b17f548
documentation
Amxx Oct 11, 2024
d679082
Bytes testing
Amxx Oct 11, 2024
117c10f
stateless + rename
Amxx Oct 11, 2024
54e1732
codespell
Amxx Oct 11, 2024
88ee5a4
use unchecked accesses
Amxx Oct 14, 2024
811d7c3
documentation
Amxx Oct 14, 2024
c54c431
Apply suggestions from code review
Amxx Oct 14, 2024
37f2363
Merge branch 'master' into features/caip-helpers
Amxx Oct 14, 2024
1acd441
Update Stateless.sol
Amxx Oct 14, 2024
c5d6081
refactor tests
Amxx Oct 14, 2024
170a48c
Apply suggestions from code review
Amxx Oct 14, 2024
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/eighty-hounds-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`Strings`: Add `parseUint`, `parseInt`, `parseHexUint` and `parseAddress` to parse strings into numbers and addresses. Also provide variants of these functions that parse substrings, and `tryXxx` variants that do not revert on invalid input.
5 changes: 5 additions & 0 deletions .changeset/healthy-books-shout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`CAIP2` and `CAIP10`: Add libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.
5 changes: 5 additions & 0 deletions .changeset/proud-planes-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`Bytes`: Add a library of common operation that operate on `bytes` objects.
82 changes: 27 additions & 55 deletions contracts/governance/Governor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {DoubleEndedQueue} from "../utils/structs/DoubleEndedQueue.sol";
import {Address} from "../utils/Address.sol";
import {Context} from "../utils/Context.sol";
import {Nonces} from "../utils/Nonces.sol";
import {Strings} from "../utils/Strings.sol";
import {IGovernor, IERC6372} from "./IGovernor.sol";

/**
Expand Down Expand Up @@ -760,67 +761,25 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
address proposer,
string memory description
) internal view virtual returns (bool) {
uint256 len = bytes(description).length;

// Length is too short to contain a valid proposer suffix
if (len < 52) {
return true;
}

// Extract what would be the `#proposer=0x` marker beginning the suffix
bytes12 marker;
assembly ("memory-safe") {
// - Start of the string contents in memory = description + 32
// - First character of the marker = len - 52
// - Length of "#proposer=0x0000000000000000000000000000000000000000" = 52
// - We read the memory word starting at the first character of the marker:
// - (description + 32) + (len - 52) = description + (len - 20)
// - Note: Solidity will ignore anything past the first 12 bytes
marker := mload(add(description, sub(len, 20)))
}

// If the marker is not found, there is no proposer suffix to check
if (marker != bytes12("#proposer=0x")) {
return true;
}
unchecked {
uint256 length = bytes(description).length;

// Parse the 40 characters following the marker as uint160
uint160 recovered = 0;
for (uint256 i = len - 40; i < len; ++i) {
(bool isHex, uint8 value) = _tryHexToUint(bytes(description)[i]);
// If any of the characters is not a hex digit, ignore the suffix entirely
if (!isHex) {
// Length is too short to contain a valid proposer suffix
if (length < 52) {
return true;
}
recovered = (recovered << 4) | value;
}

return recovered == uint160(proposer);
}
// Extract what would be the `#proposer=` marker beginning the suffix
bytes10 marker = bytes10(_unsafeReadBytesOffset(bytes(description), length - 52));

/**
* @dev Try to parse a character from a string as a hex value. Returns `(true, value)` if the char is in
* `[0-9a-fA-F]` and `(false, 0)` otherwise. Value is guaranteed to be in the range `0 <= value < 16`
*/
function _tryHexToUint(bytes1 char) private pure returns (bool isHex, uint8 value) {
uint8 c = uint8(char);
unchecked {
// Case 0-9
if (47 < c && c < 58) {
return (true, c - 48);
}
// Case A-F
else if (64 < c && c < 71) {
return (true, c - 55);
}
// Case a-f
else if (96 < c && c < 103) {
return (true, c - 87);
}
// Else: not a hex char
else {
return (false, 0);
// If the marker is not found, there is no proposer suffix to check
if (marker != bytes10("#proposer=")) {
return true;
}

// Check that the last 42 characters (after the marker) are a properly formatted address.
(bool success, address recovered) = Strings.tryParseAddress(description, length - 42, length);
return !success || recovered == proposer;
}
}

Expand Down Expand Up @@ -849,4 +808,17 @@ abstract contract Governor is Context, ERC165, EIP712, Nonces, IGovernor, IERC72
* @inheritdoc IGovernor
*/
function quorum(uint256 timepoint) public view virtual returns (uint256);

/**
* @dev Reads a bytes32 from a bytes array without bounds checking.
*
* NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the
* assembly block as such would prevent some optimizations.
*/
function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) {
// This is not memory safe in the general case, but all calls to this private function are within bounds.
assembly ("memory-safe") {
value := mload(add(buffer, add(0x20, offset)))
}
}
}
2 changes: 2 additions & 0 deletions contracts/mocks/Stateless.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {Arrays} from "../utils/Arrays.sol";
import {AuthorityUtils} from "../access/manager/AuthorityUtils.sol";
import {Base64} from "../utils/Base64.sol";
import {BitMaps} from "../utils/structs/BitMaps.sol";
import {CAIP2} from "../utils/CAIP2.sol";
import {CAIP10} from "../utils/CAIP10.sol";
import {Checkpoints} from "../utils/structs/Checkpoints.sol";
import {CircularBuffer} from "../utils/structs/CircularBuffer.sol";
import {Clones} from "../proxy/Clones.sol";
Expand Down
115 changes: 115 additions & 0 deletions contracts/utils/Bytes.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.20;

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

/**
* @dev Bytes operations.
*/
library Bytes {
/**
* @dev Forward search for `s` in `buffer`
* * If `s` is present in the buffer, returns the index of the first instance
* * If `s` is not present in the buffer, returns type(uint256).max
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
*/
function indexOf(bytes memory buffer, bytes1 s) internal pure returns (uint256) {
return indexOf(buffer, s, 0);
}

/**
* @dev Forward search for `s` in `buffer` starting at position `pos`
* * If `s` is present in the buffer (at or after `pos`), returns the index of the next instance
* * If `s` is not present in the buffer (at or after `pos`), returns the length of the buffer
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
*/
function indexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) {
unchecked {
uint256 length = buffer.length;
for (uint256 i = pos; i < length; ++i) {
if (bytes1(_unsafeReadBytesOffset(buffer, i)) == s) {
return i;
}
}
return type(uint256).max;
}
}

/**
* @dev Backward search for `s` in `buffer`
* * If `s` is present in the buffer, returns the index of the last instance
* * If `s` is not present in the buffer, returns type(uint256).max
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf
*/
function lastIndexOf(bytes memory buffer, bytes1 s) internal pure returns (uint256) {
return lastIndexOf(buffer, s, type(uint256).max);
}

/**
* @dev Backward search for `s` in `buffer` starting at position `pos`
* * If `s` is present in the buffer (at or before `pos`), returns the index of the previous instance
* * If `s` is not present in the buffer (at or before `pos`), returns the length of the buffer
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf
*/
function lastIndexOf(bytes memory buffer, bytes1 s, uint256 pos) internal pure returns (uint256) {
unchecked {
uint256 length = buffer.length;
// NOTE here we cannot do `i = Math.min(pos + 1, length)` because `pos + 1` could overflow
for (uint256 i = Math.min(pos, length - 1) + 1; i > 0; --i) {
if (bytes1(_unsafeReadBytesOffset(buffer, i - 1)) == s) {
return i - 1;
}
}
return type(uint256).max;
}
}

/**
* @dev Copies the content of `buffer`, from `start` (included) to the end of `buffer` into a new bytes object in
* memory.
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
*/
function slice(bytes memory buffer, uint256 start) internal pure returns (bytes memory) {
return slice(buffer, start, buffer.length);
}

/**
* @dev Copies the content of `buffer`, from `start` (included) to `end` (excluded) into a new bytes object in
* memory.
*
* NOTE: replicates the behavior of https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
*/
function slice(bytes memory buffer, uint256 start, uint256 end) internal pure returns (bytes memory) {
// sanitize
uint256 length = buffer.length;
end = Math.min(end, length);
start = Math.min(start, end);

// allocate and copy
bytes memory result = new bytes(end - start);
assembly ("memory-safe") {
mcopy(add(result, 0x20), add(buffer, add(start, 0x20)), sub(end, start))
}

return result;
}

/**
* @dev Reads a bytes32 from a bytes array without bounds checking.
*
* NOTE: making this function internal would mean it could be used with memory unsafe offset, and marking the
* assembly block as such would prevent some optimizations.
*/
function _unsafeReadBytesOffset(bytes memory buffer, uint256 offset) private pure returns (bytes32 value) {
// This is not memory safe in the general case, but all calls to this private function are within bounds.
assembly ("memory-safe") {
value := mload(add(buffer, add(0x20, offset)))
}
}
}
49 changes: 49 additions & 0 deletions contracts/utils/CAIP10.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {SafeCast} from "./math/SafeCast.sol";
import {Bytes} from "./Bytes.sol";
import {CAIP2} from "./CAIP2.sol";
import {Strings} from "./Strings.sol";

/**
* @dev Helper library to format and parse CAIP-10 identifiers
*
* https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md[CAIP-10] defines account identifiers as:
* account_id: chain_id + ":" + account_address
* chain_id: [-a-z0-9]{3,8}:[-_a-zA-Z0-9]{1,32} (See {CAIP2})
* account_address: [-.%a-zA-Z0-9]{1,128}
*/
library CAIP10 {
using SafeCast for uint256;
using Strings for address;
using Bytes for bytes;

/// @dev Return the CAIP-10 identifier for an account on the current (local) chain.
function local(address account) internal view returns (string memory) {
return format(CAIP2.local(), account.toChecksumHexString());
}

/**
* @dev Return the CAIP-10 identifier for a given caip2 chain and account.
*
* NOTE: This function does not verify that the inputs are properly formatted.
*/
function format(string memory caip2, string memory account) internal pure returns (string memory) {
return string.concat(caip2, ":", account);
}

/**
* @dev Parse a CAIP-10 identifier into its components.
*
* NOTE: This function does not verify that the CAIP-10 input is properly formatted. The `caip2` return can be
* parsed using the {CAIP2} library.
*/
function parse(string memory caip10) internal pure returns (string memory caip2, string memory account) {
bytes memory buffer = bytes(caip10);

uint256 pos = buffer.lastIndexOf(":");
return (string(buffer.slice(0, pos)), string(buffer.slice(pos + 1)));
}
}
47 changes: 47 additions & 0 deletions contracts/utils/CAIP2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import {SafeCast} from "./math/SafeCast.sol";
import {Bytes} from "./Bytes.sol";
import {Strings} from "./Strings.sol";

/**
* @dev Helper library to format and parse CAIP-2 identifiers
*
* https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md[CAIP-2] defines chain identifiers as:
* chain_id: namespace + ":" + reference
* namespace: [-a-z0-9]{3,8}
* reference: [-_a-zA-Z0-9]{1,32}
*/
library CAIP2 {
using SafeCast for uint256;
using Strings for uint256;
using Bytes for bytes;

/// @dev Return the CAIP-2 identifier for the current (local) chain.
function local() internal view returns (string memory) {
return format("eip155", block.chainid.toString());
}

/**
* @dev Return the CAIP-2 identifier for a given namespace and reference.
*
* NOTE: This function does not verify that the inputs are properly formatted.
*/
function format(string memory namespace, string memory ref) internal pure returns (string memory) {
return string.concat(namespace, ":", ref);
}

/**
* @dev Parse a CAIP-2 identifier into its components.
*
* NOTE: This function does not verify that the CAIP-2 input is properly formatted.
*/
function parse(string memory caip2) internal pure returns (string memory namespace, string memory ref) {
bytes memory buffer = bytes(caip2);

uint256 pos = buffer.indexOf(":");
return (string(buffer.slice(0, pos)), string(buffer.slice(pos + 1)));
}
}
4 changes: 3 additions & 1 deletion contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,18 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {Address}: Collection of functions for overloading Solidity's https://docs.soliditylang.org/en/latest/types.html#address[`address`] type.
* {Arrays}: Collection of functions that operate on https://docs.soliditylang.org/en/latest/types.html#arrays[`arrays`].
* {Base64}: On-chain base64 and base64URL encoding according to https://datatracker.ietf.org/doc/html/rfc4648[RFC-4648].
* {Bytes}: Common operations on bytes objects.
* {Strings}: Common operations for strings formatting.
* {ShortString}: 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.
* {StorageSlot}: Methods for accessing specific storage slots formatted as common primitive types.
* {TransientSlot}: Primitives for reading from and writing to transient storage (only value types are currently supported).
* {Multicall}: Abstract contract with a utility to allow batching together multiple calls in a single transaction. Useful for allowing EOAs to perform multiple operations at once.
* {Context}: A utility for abstracting the sender and calldata in the current execution context.
* {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.
* {CAIP2}, {CAIP10}: Libraries for formatting and parsing CAIP-2 and CAIP-10 identifiers.

[NOTE]
====
Expand Down
Loading