packages/contracts-core/contracts/libs/stack/Tips.sol
// 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);
}
}
}