Skip to content

Commit

Permalink
Add toUint, toInt and hexToUint to Strings (#5166)
Browse files Browse the repository at this point in the history
Co-authored-by: cairo <cairoeth@protonmail.com>
Co-authored-by: Ernesto García <ernestognw@gmail.com>
  • Loading branch information
3 people authored Oct 14, 2024
1 parent 72c152d commit bd58895
Show file tree
Hide file tree
Showing 6 changed files with 503 additions and 68 deletions.
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.
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: 1 addition & 1 deletion contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {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.
Expand Down
Loading

0 comments on commit bd58895

Please sign in to comment.