packages/contracts-core/contracts/libs/memory/ByteString.sol
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import {MemView, MemViewLib} from "./MemView.sol";
import {UnformattedCallData, UnformattedCallDataPrefix, UnformattedSignature} from "../Errors.sol";
/// @dev CallData is a memory view over the payload to be used for an external call, i.e.
/// recipient.call(callData). Its length is always (4 + 32 * N) bytes:
/// - First 4 bytes represent the function selector.
/// - 32 * N bytes represent N words that function arguments occupy.
type CallData is uint256;
/// @dev Attach library functions to CallData
using ByteString for CallData global;
/// @dev Signature is a memory view over a "65 bytes" array representing a ECDSA signature.
type Signature is uint256;
/// @dev Attach library functions to Signature
using ByteString for Signature global;
library ByteString {
using MemViewLib for bytes;
/**
* @dev non-compact ECDSA signatures are enforced as of OZ 4.7.3
*
* Signature payload memory layout
* [000 .. 032) r bytes32 32 bytes
* [032 .. 064) s bytes32 32 bytes
* [064 .. 065) v uint8 1 byte
*/
uint256 internal constant SIGNATURE_LENGTH = 65;
uint256 private constant OFFSET_R = 0;
uint256 private constant OFFSET_S = 32;
uint256 private constant OFFSET_V = 64;
/**
* @dev Calldata memory layout
* [000 .. 004) selector bytes4 4 bytes
* Optional: N function arguments
* [004 .. 036) arg1 bytes32 32 bytes
* ..
* [AAA .. END) argN bytes32 32 bytes
*/
uint256 internal constant SELECTOR_LENGTH = 4;
uint256 private constant OFFSET_SELECTOR = 0;
uint256 private constant OFFSET_ARGUMENTS = SELECTOR_LENGTH;
// ═════════════════════════════════════════════════ SIGNATURE ═════════════════════════════════════════════════════
/**
* @notice Constructs the signature payload from the given values.
* @dev Using ByteString.formatSignature({r: r, s: s, v: v}) will make sure
* that params are given in the right order.
*/
function formatSignature(bytes32 r, bytes32 s, uint8 v) internal pure returns (bytes memory) {
return abi.encodePacked(r, s, v);
}
/**
* @notice Returns a Signature view over for the given payload.
* @dev Will revert if the payload is not a signature.
*/
function castToSignature(bytes memory payload) internal pure returns (Signature) {
return castToSignature(payload.ref());
}
/**
* @notice Casts a memory view to a Signature view.
* @dev Will revert if the memory view is not over a signature.
*/
function castToSignature(MemView memView) internal pure returns (Signature) {
if (!isSignature(memView)) revert UnformattedSignature();
return Signature.wrap(MemView.unwrap(memView));
}
/**
* @notice Checks that a byte string is a signature
*/
function isSignature(MemView memView) internal pure returns (bool) {
return memView.len() == SIGNATURE_LENGTH;
}
/// @notice Convenience shortcut for unwrapping a view.
function unwrap(Signature signature) internal pure returns (MemView) {
return MemView.wrap(Signature.unwrap(signature));
}
// ═════════════════════════════════════════════ SIGNATURE SLICING ═════════════════════════════════════════════════
/// @notice Unpacks signature payload into (r, s, v) parameters.
/// @dev Make sure to verify signature length with isSignature() beforehand.
function toRSV(Signature signature) internal pure returns (bytes32 r, bytes32 s, uint8 v) {
// Get the underlying memory view
MemView memView = unwrap(signature);
r = memView.index({index_: OFFSET_R, bytes_: 32});
s = memView.index({index_: OFFSET_S, bytes_: 32});
// Can be safely casted to uint8, since we index a single byte
v = uint8(memView.indexUint({index_: OFFSET_V, bytes_: 1}));
}
// ═════════════════════════════════════════════════ CALLDATA ══════════════════════════════════════════════════════
/**
* @notice Constructs the calldata with the modified arguments:
* the existing arguments are prepended with the arguments from the prefix.
* @dev Given:
* - `calldata = abi.encodeWithSelector(foo.selector, d, e);`
* - `prefix = abi.encode(a, b, c);`
* - `a`, `b`, `c` are arguments of static type (i.e. not dynamically sized ones)
* Then:
* - Function will return abi.encodeWithSelector(foo.selector, a, c, c, d, e)
* - Returned calldata will trigger `foo(a, b, c, d, e)` when used for a contract call.
* Note: for clarification as to what types are considered static, see
* https://docs.soliditylang.org/en/latest/abi-spec.html#formal-specification-of-the-encoding
* @param callData Calldata that needs to be modified
* @param prefix ABI-encoded arguments to use as the first arguments in the new calldata
* @return Modified calldata having prefix as the first arguments.
*/
function addPrefix(CallData callData, bytes memory prefix) internal view returns (bytes memory) {
// Prefix should occupy a whole amount of words in memory
if (!_fullWords(prefix.length)) revert UnformattedCallDataPrefix();
MemView[] memory views = new MemView[](3);
// Use payload's function selector
views[0] = abi.encodePacked(callData.callSelector()).ref();
// Use prefix as the first arguments
views[1] = prefix.ref();
// Use payload's remaining arguments
views[2] = callData.arguments();
return MemViewLib.join(views);
}
/**
* @notice Returns a CallData view over for the given payload.
* @dev Will revert if the memory view is not over a calldata.
*/
function castToCallData(bytes memory payload) internal pure returns (CallData) {
return castToCallData(payload.ref());
}
/**
* @notice Casts a memory view to a CallData view.
* @dev Will revert if the memory view is not over a calldata.
*/
function castToCallData(MemView memView) internal pure returns (CallData) {
if (!isCallData(memView)) revert UnformattedCallData();
return CallData.wrap(MemView.unwrap(memView));
}
/**
* @notice Checks that a byte string is a valid calldata, i.e.
* a function selector, followed by arbitrary amount of arguments.
*/
function isCallData(MemView memView) internal pure returns (bool) {
uint256 length = memView.len();
// Calldata should at least have a function selector
if (length < SELECTOR_LENGTH) return false;
// The remainder of the calldata should be exactly N memory words (N >= 0)
return _fullWords(length - SELECTOR_LENGTH);
}
/// @notice Convenience shortcut for unwrapping a view.
function unwrap(CallData callData) internal pure returns (MemView) {
return MemView.wrap(CallData.unwrap(callData));
}
/// @notice Returns callData's hash: a leaf to be inserted in the "Message mini-Merkle tree".
function leaf(CallData callData) internal pure returns (bytes32) {
return callData.unwrap().keccak();
}
// ═════════════════════════════════════════════ CALLDATA SLICING ══════════════════════════════════════════════════
/**
* @notice Returns amount of memory words (32 byte chunks) the function arguments
* occupy in the calldata.
* @dev This might differ from amount of arguments supplied, if any of the arguments
* occupies more than one memory slot. It is true, however, that argument part of the payload
* occupies exactly N words, even for dynamic types like `bytes`
*/
function argumentWords(CallData callData) internal pure returns (uint256) {
// Get the underlying memory view
MemView memView = unwrap(callData);
// Equivalent of (length - SELECTOR_LENGTH) / 32
return (memView.len() - SELECTOR_LENGTH) >> 5;
}
/// @notice Returns selector for the provided calldata.
function callSelector(CallData callData) internal pure returns (bytes4) {
// Get the underlying memory view
MemView memView = unwrap(callData);
return bytes4(memView.index({index_: OFFSET_SELECTOR, bytes_: SELECTOR_LENGTH}));
}
/// @notice Returns abi encoded arguments for the provided calldata.
function arguments(CallData callData) internal pure returns (MemView) {
// Get the underlying memory view
MemView memView = unwrap(callData);
return memView.sliceFrom({index_: OFFSET_ARGUMENTS});
}
// ══════════════════════════════════════════════ PRIVATE HELPERS ══════════════════════════════════════════════════
/// @dev Checks if length is full amount of memory words (32 bytes).
function _fullWords(uint256 length) internal pure returns (bool) {
// The equivalent of length % 32 == 0
return length & 31 == 0;
}
}