synapsecns/sanguine

View on GitHub
packages/contracts-core/contracts/libs/stack/Tips.sol

Summary

Maintainability
Test Coverage
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {TIPS_GRANULARITY} from "../Constants.sol";
import {TipsOverflow, TipsValueTooLow} from "../Errors.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

/// Tips is encoded data with "tips paid for sending a base message".
/// Note: even though uint256 is also an underlying type for MemView, Tips is stored ON STACK.
type Tips is uint256;

using TipsLib for Tips global;

/// # Tips
/// Library for formatting _the tips part_ of _the base messages_.
///
/// ## How the tips are awarded
/// Tips are paid for sending a base message, and are split across all the agents that
/// made the message execution on destination chain possible.
/// ### Summit tips
/// Split between:
///     - Guard posting a snapshot with state ST_G for the origin chain.
///     - Notary posting a snapshot SN_N using ST_G. This creates attestation A.
///     - Notary posting a message receipt after it is executed on destination chain.
/// ### Attestation tips
/// Paid to:
///     - Notary posting attestation A to destination chain.
/// ### Execution tips
/// Paid to:
///     - First executor performing a valid execution attempt (correct proofs, optimistic period over),
///      using attestation A to prove message inclusion on origin chain, whether the recipient reverted or not.
/// ### Delivery tips.
/// Paid to:
///     - Executor who successfully executed the message on destination chain.
///
/// ## Tips encoding
/// - Tips occupy a single storage word, and thus are stored on stack instead of being stored in memory.
/// - The actual tip values should be determined by multiplying stored values by divided by TIPS_MULTIPLIER=2**32.
/// - Tips are packed into a single word of storage, while allowing real values up to ~8*10**28 for every tip category.
/// > The only downside is that the "real tip values" are now multiplies of ~4*10**9, which should be fine even for
/// the chains with the most expensive gas currency.
/// # Tips stack layout (from highest bits to lowest)
///
/// | Position   | Field          | Type   | Bytes | Description                                                |
/// | ---------- | -------------- | ------ | ----- | ---------------------------------------------------------- |
/// | (032..024] | summitTip      | uint64 | 8     | Tip for agents interacting with Summit contract            |
/// | (024..016] | attestationTip | uint64 | 8     | Tip for Notary posting attestation to Destination contract |
/// | (016..008] | executionTip   | uint64 | 8     | Tip for valid execution attempt on destination chain       |
/// | (008..000] | deliveryTip    | uint64 | 8     | Tip for successful message delivery on destination chain   |

library TipsLib {
    using SafeCast for uint256;

    /// @dev Amount of bits to shift to summitTip field
    uint256 private constant SHIFT_SUMMIT_TIP = 24 * 8;
    /// @dev Amount of bits to shift to attestationTip field
    uint256 private constant SHIFT_ATTESTATION_TIP = 16 * 8;
    /// @dev Amount of bits to shift to executionTip field
    uint256 private constant SHIFT_EXECUTION_TIP = 8 * 8;

    // ═══════════════════════════════════════════════════ TIPS ════════════════════════════════════════════════════════

    /// @notice Returns encoded tips with the given fields
    /// @param summitTip_        Tip for agents interacting with Summit contract, divided by TIPS_MULTIPLIER
    /// @param attestationTip_   Tip for Notary posting attestation to Destination contract, divided by TIPS_MULTIPLIER
    /// @param executionTip_     Tip for valid execution attempt on destination chain, divided by TIPS_MULTIPLIER
    /// @param deliveryTip_      Tip for successful message delivery on destination chain, divided by TIPS_MULTIPLIER
    function encodeTips(uint64 summitTip_, uint64 attestationTip_, uint64 executionTip_, uint64 deliveryTip_)
        internal
        pure
        returns (Tips)
    {
        // forgefmt: disable-next-item
        return Tips.wrap(
            uint256(summitTip_) << SHIFT_SUMMIT_TIP |
            uint256(attestationTip_) << SHIFT_ATTESTATION_TIP |
            uint256(executionTip_) << SHIFT_EXECUTION_TIP |
            uint256(deliveryTip_)
        );
    }

    /// @notice Convenience function to encode tips with uint256 values.
    function encodeTips256(uint256 summitTip_, uint256 attestationTip_, uint256 executionTip_, uint256 deliveryTip_)
        internal
        pure
        returns (Tips)
    {
        // In practice, the tips amounts are not supposed to be higher than 2**96, and with 32 bits of granularity
        // using uint64 is enough to store the values. However, we still check for overflow just in case.
        // TODO: consider using Number type to store the tips values.
        return encodeTips({
            summitTip_: (summitTip_ >> TIPS_GRANULARITY).toUint64(),
            attestationTip_: (attestationTip_ >> TIPS_GRANULARITY).toUint64(),
            executionTip_: (executionTip_ >> TIPS_GRANULARITY).toUint64(),
            deliveryTip_: (deliveryTip_ >> TIPS_GRANULARITY).toUint64()
        });
    }

    /// @notice Wraps the padded encoded tips into a Tips-typed value.
    /// @dev There is no actual padding here, as the underlying type is already uint256,
    /// but we include this function for consistency and to be future-proof, if tips will eventually use anything
    /// smaller than uint256.
    function wrapPadded(uint256 paddedTips) internal pure returns (Tips) {
        return Tips.wrap(paddedTips);
    }

    /**
     * @notice Returns a formatted Tips payload specifying empty tips.
     * @return Formatted tips
     */
    function emptyTips() internal pure returns (Tips) {
        return Tips.wrap(0);
    }

    /// @notice Returns tips's hash: a leaf to be inserted in the "Message mini-Merkle tree".
    function leaf(Tips tips) internal pure returns (bytes32 hashedTips) {
        // solhint-disable-next-line no-inline-assembly
        assembly {
            // Store tips in scratch space
            mstore(0, tips)
            // Compute hash of tips padded to 32 bytes
            hashedTips := keccak256(0, 32)
        }
    }

    // ═══════════════════════════════════════════════ TIPS SLICING ════════════════════════════════════════════════════

    /// @notice Returns summitTip field
    function summitTip(Tips tips) internal pure returns (uint64) {
        // Casting to uint64 will truncate the highest bits, which is the behavior we want
        return uint64(Tips.unwrap(tips) >> SHIFT_SUMMIT_TIP);
    }

    /// @notice Returns attestationTip field
    function attestationTip(Tips tips) internal pure returns (uint64) {
        // Casting to uint64 will truncate the highest bits, which is the behavior we want
        return uint64(Tips.unwrap(tips) >> SHIFT_ATTESTATION_TIP);
    }

    /// @notice Returns executionTip field
    function executionTip(Tips tips) internal pure returns (uint64) {
        // Casting to uint64 will truncate the highest bits, which is the behavior we want
        return uint64(Tips.unwrap(tips) >> SHIFT_EXECUTION_TIP);
    }

    /// @notice Returns deliveryTip field
    function deliveryTip(Tips tips) internal pure returns (uint64) {
        // Casting to uint64 will truncate the highest bits, which is the behavior we want
        return uint64(Tips.unwrap(tips));
    }

    // ════════════════════════════════════════════════ TIPS VALUE ═════════════════════════════════════════════════════

    /// @notice Returns total value of the tips payload.
    /// This is the sum of the encoded values, scaled up by TIPS_MULTIPLIER
    function value(Tips tips) internal pure returns (uint256 value_) {
        value_ = uint256(tips.summitTip()) + tips.attestationTip() + tips.executionTip() + tips.deliveryTip();
        value_ <<= TIPS_GRANULARITY;
    }

    /// @notice Increases the delivery tip to match the new value.
    function matchValue(Tips tips, uint256 newValue) internal pure returns (Tips newTips) {
        uint256 oldValue = tips.value();
        if (newValue < oldValue) revert TipsValueTooLow();
        // We want to increase the delivery tip, while keeping the other tips the same
        unchecked {
            uint256 delta = (newValue - oldValue) >> TIPS_GRANULARITY;
            // `delta` fits into uint224, as TIPS_GRANULARITY is 32, so this never overflows uint256.
            // In practice, this will never overflow uint64 as well, but we still check it just in case.
            if (delta + tips.deliveryTip() > type(uint64).max) revert TipsOverflow();
            // Delivery tips occupy lowest 8 bytes, so we can just add delta to the tips value
            // to effectively increase the delivery tip (knowing that delta fits into uint64).
            newTips = Tips.wrap(Tips.unwrap(tips) + delta);
        }
    }
}