synapsecns/sanguine

View on GitHub
packages/contracts-core/contracts/hubs/StateHub.sol

Summary

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

// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════
import {IncorrectOriginDomain} from "../libs/Errors.sol";
import {GasData, GasDataLib} from "../libs/stack/GasData.sol";
import {HistoricalTree} from "../libs/merkle/MerkleTree.sol";
import {State, StateLib} from "../libs/memory/State.sol";
import {ChainContext} from "../libs/ChainContext.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {AgentSecured} from "../base/AgentSecured.sol";
import {StateHubEvents} from "../events/StateHubEvents.sol";
import {IStateHub} from "../interfaces/IStateHub.sol";
// ═════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";

/// @notice `StateHub` is a parent contract for `Origin`. It is responsible for the following:
/// - Keeping track of the historical Origin Merkle Tree containing all the message hashes.
/// - Keeping track of the historical Origin States, as well as verifying their validity.
abstract contract StateHub is AgentSecured, StateHubEvents, IStateHub {
    using SafeCast for uint256;
    using StateLib for bytes;

    struct OriginState {
        uint40 blockNumber;
        uint40 timestamp;
        GasData gasData;
    }
    // Bits left for tight packing: 80

    // ══════════════════════════════════════════════════ STORAGE ══════════════════════════════════════════════════════

    /// @dev Historical Merkle Tree
    /// Note: Takes two storage slots
    HistoricalTree private _tree;

    /// @dev All historical contract States
    OriginState[] private _originStates;

    /// @dev gap for upgrade safety
    uint256[47] private __GAP; // solhint-disable-line var-name-mixedcase

    // ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════

    /// @inheritdoc IStateHub
    function isValidState(bytes memory statePayload) external view returns (bool isValid) {
        // This will revert if payload is not a formatted state
        State state = statePayload.castToState();
        return _isValidState(state);
    }

    /// @inheritdoc IStateHub
    function statesAmount() external view returns (uint256) {
        return _originStates.length;
    }

    /// @inheritdoc IStateHub
    function suggestLatestState() external view returns (bytes memory stateData) {
        // This never underflows, assuming the contract was initialized
        return suggestState(_nextNonce() - 1);
    }

    /// @inheritdoc IStateHub
    function suggestState(uint32 nonce) public view returns (bytes memory stateData) {
        // This will revert if nonce is out of range
        bytes32 root = _tree.root(nonce);
        return _formatOriginState(_originStates[nonce], root, localDomain, nonce);
    }

    // ══════════════════════════════════════════════ INTERNAL LOGIC ═══════════════════════════════════════════════════

    /// @dev Initializes the saved states list by inserting a state for an empty Merkle Tree.
    function _initializeStates() internal {
        // This should only be called once, when the contract is initialized
        // This will revert if _tree.roots is non-empty
        bytes32 savedRoot = _tree.initializeRoots();
        // Save root for empty merkle _tree with block number and timestamp of initialization
        _saveState(savedRoot, _toOriginState());
    }

    /// @dev Inserts leaf into the Merkle Tree and saves the updated origin State.
    function _insertAndSave(bytes32 leaf) internal {
        bytes32 newRoot = _tree.insert(leaf);
        _saveState(newRoot, _toOriginState());
    }

    /// @dev Saves an updated state of the Origin contract
    function _saveState(bytes32 root, OriginState memory state) internal {
        uint32 nonce = _nextNonce();
        _originStates.push(state);
        // Emit event with raw state data
        emit StateSaved(_formatOriginState(state, root, localDomain, nonce));
    }

    // ══════════════════════════════════════════════ INTERNAL VIEWS ═══════════════════════════════════════════════════

    /// @dev Returns nonce of the next sent message: the amount of saved States so far.
    /// This always equals to "total amount of sent messages" plus 1.
    function _nextNonce() internal view returns (uint32) {
        // TODO: consider using more than 32 bits for origin nonces
        return _originStates.length.toUint32();
    }

    /// @dev Checks if a state is valid, i.e. if it matches the historical one.
    /// Reverts, if state refers to another Origin contract.
    function _isValidState(State state) internal view returns (bool) {
        // Check if state refers to this contract
        if (state.origin() != localDomain) revert IncorrectOriginDomain();
        // Check if nonce exists
        uint32 nonce = state.nonce();
        if (nonce >= _originStates.length) return false;
        // Check if state root matches the historical one
        if (state.root() != _tree.root(nonce)) return false;
        // Check if state metadata matches the historical one
        return _areEqual(state, _originStates[nonce]);
    }

    // ═════════════════════════════════════════════ STRUCT FORMATTING ═════════════════════════════════════════════════

    /// @dev Returns a formatted payload for a stored OriginState.
    function _formatOriginState(OriginState memory originState, bytes32 root, uint32 origin, uint32 nonce)
        internal
        pure
        returns (bytes memory)
    {
        return StateLib.formatState({
            root_: root,
            origin_: origin,
            nonce_: nonce,
            blockNumber_: originState.blockNumber,
            timestamp_: originState.timestamp,
            gasData_: originState.gasData
        });
    }

    /// @dev Child contract should implement the logic for getting the current gas data from the gas oracle
    /// to be saved as part of the Origin State.
    // solhint-disable-next-line ordering
    function _fetchGasData() internal view virtual returns (GasData);

    /// @dev Returns a OriginState struct to save in the contract.
    function _toOriginState() internal view returns (OriginState memory originState) {
        originState.blockNumber = ChainContext.blockNumber();
        originState.timestamp = ChainContext.blockTimestamp();
        originState.gasData = _fetchGasData();
    }

    /// @dev Checks that a state and its Origin representation are equal.
    function _areEqual(State state, OriginState memory originState) internal pure returns (bool) {
        return state.blockNumber() == originState.blockNumber && state.timestamp() == originState.timestamp;
    }
}