packages/contracts-core/contracts/hubs/StateHub.sol
// 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;
}
}