synapsecns/sanguine

View on GitHub
packages/contracts-core/contracts/libs/memory/ByteString.sol

Summary

Maintainability
Test Coverage
// 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;
    }
}