synapsecns/sanguine

View on GitHub
packages/contracts-core/contracts/inbox/StatementInbox.sol

Summary

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

// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════
import {Attestation, AttestationLib} from "../libs/memory/Attestation.sol";
import {
    AgentNotGuard,
    AgentNotNotary,
    IncorrectAgentDomain,
    IncorrectSnapshotProof,
    IncorrectSnapshotRoot,
    IncorrectState,
    IndexOutOfRange
} from "../libs/Errors.sol";
import {Receipt, ReceiptLib} from "../libs/memory/Receipt.sol";
import {Snapshot, SnapshotLib} from "../libs/memory/Snapshot.sol";
import {State, StateLib} from "../libs/memory/State.sol";
import {AgentStatus} from "../libs/Structures.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {MessagingBase} from "../base/MessagingBase.sol";
import {StatementInboxEvents} from "../events/StatementInboxEvents.sol";
import {IAgentManager} from "../interfaces/IAgentManager.sol";
import {IExecutionHub} from "../interfaces/IExecutionHub.sol";
import {IStateHub} from "../interfaces/IStateHub.sol";
import {IStatementInbox} from "../interfaces/IStatementInbox.sol";
// ═════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

/// @notice `StatementInbox` is the entry point for all agent-signed statements. It verifies the
/// agent signatures, and passes the unsigned statements to the contract to consume it via `acceptX` functions. Is is
/// also used to verify the agent-signed statements and initiate the agent slashing, should the statement be invalid.
/// `StatementInbox` is responsible for the following:
/// - Accepting State and Receipt Reports to initiate a dispute between Guard and Notary.
/// - Storing all the Guard Reports with the Guard signature leading to a dispute.
/// - Verifying State/State Reports referencing the local chain and slashing the signer if statement is invalid.
/// - Verifying Receipt/Receipt Reports referencing the local chain and slashing the signer if statement is invalid.
abstract contract StatementInbox is MessagingBase, StatementInboxEvents, IStatementInbox {
    using AttestationLib for bytes;
    using ReceiptLib for bytes;
    using StateLib for bytes;
    using SnapshotLib for bytes;

    struct StoredReport {
        uint256 sigIndex;
        bytes statementPayload;
    }

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

    address public agentManager;
    address public origin;
    address public destination;

    // TODO: optimize this
    bytes[] internal _storedSignatures;
    StoredReport[] internal _storedReports;

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

    // ════════════════════════════════════════════════ INITIALIZER ════════════════════════════════════════════════════

    /// @dev Initializes the contract:
    /// - Sets up `msg.sender` as the owner of the contract.
    /// - Sets up `agentManager`, `origin`, and `destination`.
    // solhint-disable-next-line func-name-mixedcase
    function __StatementInbox_init(address agentManager_, address origin_, address destination_)
        internal
        onlyInitializing
    {
        agentManager = agentManager_;
        origin = origin_;
        destination = destination_;
        __Ownable2Step_init();
    }

    // ══════════════════════════════════════════ SUBMIT AGENT STATEMENTS ══════════════════════════════════════════════

    /// @inheritdoc IStatementInbox
    // solhint-disable-next-line ordering
    function submitStateReportWithSnapshot(
        uint8 stateIndex,
        bytes memory srSignature,
        bytes memory snapPayload,
        bytes memory snapSignature
    ) external returns (bool wasAccepted) {
        // This will revert if payload is not a snapshot
        Snapshot snapshot = snapPayload.castToSnapshot();
        // This will revert if the snapshot signer is not a known Notary
        (AgentStatus memory notaryStatus,) =
            _verifySnapshot({snapshot: snapshot, snapSignature: snapSignature, verifyNotary: true});
        // Notary needs to be Active/Unstaking
        notaryStatus.verifyActiveUnstaking();
        // Check if Notary is active on this chain
        _verifyNotaryDomain(notaryStatus.domain);
        // This will revert if state index is out of range
        State state = snapshot.state(stateIndex);
        // This will revert if the report signer is not an known Guard
        (AgentStatus memory guardStatus,) = _verifyStateReport(state, srSignature);
        // Check that Guard is active
        guardStatus.verifyActive();
        _saveReport(state.unwrap().clone(), srSignature);
        // This will revert if either actor is already in dispute
        IAgentManager(agentManager).openDispute(guardStatus.index, notaryStatus.index);
        return true;
    }

    /// @inheritdoc IStatementInbox
    function submitStateReportWithAttestation(
        uint8 stateIndex,
        bytes memory srSignature,
        bytes memory snapPayload,
        bytes memory attPayload,
        bytes memory attSignature
    ) external returns (bool wasAccepted) {
        // This will revert if payload is not a snapshot
        Snapshot snapshot = snapPayload.castToSnapshot();
        // This will revert if state index is out of range
        State state = snapshot.state(stateIndex);
        // This will revert if the report signer is not an known Guard
        (AgentStatus memory guardStatus,) = _verifyStateReport(state, srSignature);
        // Check that Guard is active
        guardStatus.verifyActive();
        // This will revert if payload is not an attestation
        Attestation att = attPayload.castToAttestation();
        // This will revert if signer is not an known Notary
        (AgentStatus memory notaryStatus,) = _verifyAttestation(att, attSignature);
        // Notary needs to be Active/Unstaking
        notaryStatus.verifyActiveUnstaking();
        // Check if Notary is active on this chain
        _verifyNotaryDomain(notaryStatus.domain);
        if (snapshot.calculateRoot() != att.snapRoot()) revert IncorrectSnapshotRoot();
        _saveReport(state.unwrap().clone(), srSignature);
        // This will revert if either actor is already in dispute
        IAgentManager(agentManager).openDispute(guardStatus.index, notaryStatus.index);
        return true;
    }

    /// @inheritdoc IStatementInbox
    function submitStateReportWithSnapshotProof(
        uint8 stateIndex,
        bytes memory statePayload,
        bytes memory srSignature,
        bytes32[] memory snapProof,
        bytes memory attPayload,
        bytes memory attSignature
    ) external returns (bool wasAccepted) {
        // This will revert if payload is not a state
        State state = statePayload.castToState();
        // This will revert if the report signer is not an known Guard
        (AgentStatus memory guardStatus,) = _verifyStateReport(state, srSignature);
        // Check that Guard is active
        guardStatus.verifyActive();
        // This will revert if payload is not an attestation
        Attestation att = attPayload.castToAttestation();
        // This will revert if signer is not a known Notary
        (AgentStatus memory notaryStatus,) = _verifyAttestation(att, attSignature);
        // Notary needs to be Active/Unstaking
        notaryStatus.verifyActiveUnstaking();
        // Check if Notary is active on this chain
        _verifyNotaryDomain(notaryStatus.domain);
        // This will revert if any of these is true:
        //  - Attestation root is not equal to Merkle Root derived from State and Snapshot Proof.
        //  - Snapshot Proof's first element does not match the State metadata.
        //  - Snapshot Proof length exceeds Snapshot tree Height.
        //  - State index is out of range.
        _verifySnapshotMerkle(att, stateIndex, state, snapProof);
        _saveReport(statePayload, srSignature);
        // This will revert if either actor is already in dispute
        IAgentManager(agentManager).openDispute(guardStatus.index, notaryStatus.index);
        return true;
    }

    // ══════════════════════════════════════════ VERIFY AGENT STATEMENTS ══════════════════════════════════════════════

    /// @inheritdoc IStatementInbox
    function verifyReceipt(bytes memory rcptPayload, bytes memory rcptSignature)
        external
        returns (bool isValidReceipt)
    {
        // This will revert if payload is not a receipt
        Receipt rcpt = rcptPayload.castToReceipt();
        // This will revert if the attestation signer is not a known Notary
        (AgentStatus memory status, address notary) = _verifyReceipt(rcpt, rcptSignature);
        // Notary needs to be Active/Unstaking
        status.verifyActiveUnstaking();
        isValidReceipt = IExecutionHub(destination).isValidReceipt(rcptPayload);
        if (!isValidReceipt) {
            emit InvalidReceipt(rcptPayload, rcptSignature);
            IAgentManager(agentManager).slashAgent(status.domain, notary, msg.sender);
        }
    }

    /// @inheritdoc IStatementInbox
    function verifyReceiptReport(bytes memory rcptPayload, bytes memory rrSignature)
        external
        returns (bool isValidReport)
    {
        // This will revert if payload is not a receipt
        Receipt rcpt = rcptPayload.castToReceipt();
        // This will revert if the report signer is not a known Guard
        (AgentStatus memory status, address guard) = _verifyReceiptReport(rcpt, rrSignature);
        // Guard needs to be Active/Unstaking
        status.verifyActiveUnstaking();
        // Report is valid IF AND ONLY IF the reported receipt in invalid
        isValidReport = !IExecutionHub(destination).isValidReceipt(rcptPayload);
        if (!isValidReport) {
            emit InvalidReceiptReport(rcptPayload, rrSignature);
            IAgentManager(agentManager).slashAgent(status.domain, guard, msg.sender);
        }
    }

    /// @inheritdoc IStatementInbox
    function verifyStateWithAttestation(
        uint8 stateIndex,
        bytes memory snapPayload,
        bytes memory attPayload,
        bytes memory attSignature
    ) external returns (bool isValidState) {
        // This will revert if payload is not an attestation
        Attestation att = attPayload.castToAttestation();
        // This will revert if the attestation signer is not a known Notary
        (AgentStatus memory status, address notary) = _verifyAttestation(att, attSignature);
        // Notary needs to be Active/Unstaking
        status.verifyActiveUnstaking();
        // This will revert if payload is not a snapshot
        Snapshot snapshot = snapPayload.castToSnapshot();
        if (snapshot.calculateRoot() != att.snapRoot()) revert IncorrectSnapshotRoot();
        // This will revert if state does not refer to this chain
        bytes memory statePayload = snapshot.state(stateIndex).unwrap().clone();
        isValidState = IStateHub(origin).isValidState(statePayload);
        if (!isValidState) {
            emit InvalidStateWithAttestation(stateIndex, statePayload, attPayload, attSignature);
            IAgentManager(agentManager).slashAgent(status.domain, notary, msg.sender);
        }
    }

    /// @inheritdoc IStatementInbox
    function verifyStateWithSnapshotProof(
        uint8 stateIndex,
        bytes memory statePayload,
        bytes32[] memory snapProof,
        bytes memory attPayload,
        bytes memory attSignature
    ) external returns (bool isValidState) {
        // This will revert if payload is not an attestation
        Attestation att = attPayload.castToAttestation();
        // This will revert if the attestation signer is not a known Notary
        (AgentStatus memory status, address notary) = _verifyAttestation(att, attSignature);
        // Notary needs to be Active/Unstaking
        status.verifyActiveUnstaking();
        // This will revert if payload is not a state
        State state = statePayload.castToState();
        // This will revert if any of these is true:
        //  - Attestation root is not equal to Merkle Root derived from State and Snapshot Proof.
        //  - Snapshot Proof's first element does not match the State metadata.
        //  - Snapshot Proof length exceeds Snapshot tree Height.
        //  - State index is out of range.
        _verifySnapshotMerkle(att, stateIndex, state, snapProof);
        // This will revert if state does not refer to this chain
        isValidState = IStateHub(origin).isValidState(statePayload);
        if (!isValidState) {
            emit InvalidStateWithAttestation(stateIndex, statePayload, attPayload, attSignature);
            IAgentManager(agentManager).slashAgent(status.domain, notary, msg.sender);
        }
    }

    /// @inheritdoc IStatementInbox
    function verifyStateWithSnapshot(uint8 stateIndex, bytes memory snapPayload, bytes memory snapSignature)
        external
        returns (bool isValidState)
    {
        // This will revert if payload is not a snapshot
        Snapshot snapshot = snapPayload.castToSnapshot();
        // This will revert if the snapshot signer is not a known Guard/Notary
        (AgentStatus memory status, address agent) =
            _verifySnapshot({snapshot: snapshot, snapSignature: snapSignature, verifyNotary: false});
        // Agent needs to be Active/Unstaking
        status.verifyActiveUnstaking();
        // This will revert if state does not refer to this chain
        isValidState = IStateHub(origin).isValidState(snapshot.state(stateIndex).unwrap().clone());
        if (!isValidState) {
            emit InvalidStateWithSnapshot(stateIndex, snapPayload, snapSignature);
            IAgentManager(agentManager).slashAgent(status.domain, agent, msg.sender);
        }
    }

    /// @inheritdoc IStatementInbox
    function verifyStateReport(bytes memory statePayload, bytes memory srSignature)
        external
        returns (bool isValidReport)
    {
        // This will revert if payload is not a state
        State state = statePayload.castToState();
        // This will revert if the report signer is not a known Guard
        (AgentStatus memory status, address guard) = _verifyStateReport(state, srSignature);
        // Guard needs to be Active/Unstaking
        status.verifyActiveUnstaking();
        // Report is valid IF AND ONLY IF the reported state in invalid
        // This will revert if the reported state does not refer to this chain
        isValidReport = !IStateHub(origin).isValidState(statePayload);
        if (!isValidReport) {
            emit InvalidStateReport(statePayload, srSignature);
            IAgentManager(agentManager).slashAgent(status.domain, guard, msg.sender);
        }
    }

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

    /// @inheritdoc IStatementInbox
    function getReportsAmount() external view returns (uint256) {
        return _storedReports.length;
    }

    /// @inheritdoc IStatementInbox
    function getGuardReport(uint256 index)
        external
        view
        returns (bytes memory statementPayload, bytes memory reportSignature)
    {
        if (index >= _storedReports.length) revert IndexOutOfRange();
        StoredReport memory storedReport = _storedReports[index];
        statementPayload = storedReport.statementPayload;
        reportSignature = _storedSignatures[storedReport.sigIndex];
    }

    /// @inheritdoc IStatementInbox
    function getStoredSignature(uint256 index) external view returns (bytes memory) {
        return _storedSignatures[index];
    }

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

    /// @dev Saves the statement reported by Guard as invalid and the Guard Report signature.
    function _saveReport(bytes memory statementPayload, bytes memory reportSignature) internal {
        uint256 sigIndex = _saveSignature(reportSignature);
        _storedReports.push(StoredReport(sigIndex, statementPayload));
    }

    /// @dev Saves the signature and returns its index.
    function _saveSignature(bytes memory signature) internal returns (uint256 sigIndex) {
        sigIndex = _storedSignatures.length;
        _storedSignatures.push(signature);
    }

    // ═══════════════════════════════════════════════ AGENT CHECKS ════════════════════════════════════════════════════

    /**
     * @dev Recovers a signer from a hashed message, and a EIP-191 signature for it.
     * Will revert, if the signer is not a known agent.
     * @dev Agent flag could be any of these: Active/Unstaking/Resting/Fraudulent/Slashed
     * Further checks need to be performed in a caller function.
     * @param hashedStatement   Hash of the statement that was signed by an Agent
     * @param signature         Agent signature for the hashed statement
     * @return status   Struct representing agent status:
     *                  - flag      Unknown/Active/Unstaking/Resting/Fraudulent/Slashed
     *                  - domain    Domain where agent is/was active
     *                  - index     Index of agent in the Agent Merkle Tree
     * @return agent    Agent that signed the statement
     */
    function _recoverAgent(bytes32 hashedStatement, bytes memory signature)
        internal
        view
        returns (AgentStatus memory status, address agent)
    {
        bytes32 ethSignedMsg = ECDSA.toEthSignedMessageHash(hashedStatement);
        agent = ECDSA.recover(ethSignedMsg, signature);
        status = IAgentManager(agentManager).agentStatus(agent);
        // Discard signature of unknown agents.
        // Further flag checks are supposed to be performed in a caller function.
        status.verifyKnown();
    }

    /// @dev Verifies that Notary signature is active on local domain.
    function _verifyNotaryDomain(uint32 notaryDomain) internal view {
        // Notary needs to be from the local domain (if contract is not deployed on Synapse Chain).
        // Or Notary could be from any domain (if contract is deployed on Synapse Chain).
        if (notaryDomain != localDomain && localDomain != synapseDomain) revert IncorrectAgentDomain();
    }

    // ════════════════════════════════════════ ATTESTATION RELATED CHECKS ═════════════════════════════════════════════

    /**
     * @dev Internal function to verify the signed attestation payload.
     * Reverts if any of these is true:
     *  - Attestation signer is not a known Notary.
     * @param att               Typed memory view over attestation payload
     * @param attSignature      Notary signature for the attestation
     * @return status           Struct representing agent status, see {_recoverAgent}
     * @return notary           Notary that signed the snapshot
     */
    function _verifyAttestation(Attestation att, bytes memory attSignature)
        internal
        view
        returns (AgentStatus memory status, address notary)
    {
        // This will revert if signer is not a known agent
        (status, notary) = _recoverAgent(att.hashValid(), attSignature);
        // Attestation signer needs to be a Notary, not a Guard
        if (status.domain == 0) revert AgentNotNotary();
    }

    /**
     * @dev Internal function to verify the signed attestation report payload.
     * Reverts if any of these is true:
     *  - Report signer is not a known Guard.
     * @param att               Typed memory view over attestation payload that Guard reports as invalid
     * @param arSignature       Guard signature for the "invalid attestation" report
     * @return status           Struct representing guard status, see {_recoverAgent}
     * @return guard            Guard that signed the report
     */
    function _verifyAttestationReport(Attestation att, bytes memory arSignature)
        internal
        view
        returns (AgentStatus memory status, address guard)
    {
        // This will revert if signer is not a known agent
        (status, guard) = _recoverAgent(att.hashInvalid(), arSignature);
        // Report signer needs to be a Guard, not a Notary
        if (status.domain != 0) revert AgentNotGuard();
    }

    // ══════════════════════════════════════════ RECEIPT RELATED CHECKS ═══════════════════════════════════════════════

    /**
     * @dev Internal function to verify the signed receipt payload.
     * Reverts if any of these is true:
     *  - Receipt signer is not a known Notary.
     * @param rcpt              Typed memory view over receipt payload
     * @param rcptSignature     Notary signature for the receipt
     * @return status           Struct representing agent status, see {_recoverAgent}
     * @return notary           Notary that signed the snapshot
     */
    function _verifyReceipt(Receipt rcpt, bytes memory rcptSignature)
        internal
        view
        returns (AgentStatus memory status, address notary)
    {
        // This will revert if signer is not a known agent
        (status, notary) = _recoverAgent(rcpt.hashValid(), rcptSignature);
        // Receipt signer needs to be a Notary, not a Guard
        if (status.domain == 0) revert AgentNotNotary();
    }

    /**
     * @dev Internal function to verify the signed receipt report payload.
     * Reverts if any of these is true:
     * - Report signer is not a known Guard.
     * @param rcpt              Typed memory view over receipt payload that Guard reports as invalid
     * @param rrSignature       Guard signature for the "invalid receipt" report
     * @return status           Struct representing guard status, see {_recoverAgent}
     * @return guard            Guard that signed the report
     */
    function _verifyReceiptReport(Receipt rcpt, bytes memory rrSignature)
        internal
        view
        returns (AgentStatus memory status, address guard)
    {
        // This will revert if signer is not a known agent
        (status, guard) = _recoverAgent(rcpt.hashInvalid(), rrSignature);
        // Report signer needs to be a Guard, not a Notary
        if (status.domain != 0) revert AgentNotGuard();
    }

    // ═══════════════════════════════════════ STATE/SNAPSHOT RELATED CHECKS ═══════════════════════════════════════════

    /**
     * @dev Internal function to verify the signed snapshot report payload.
     * Reverts if any of these is true:
     *  - Report signer is not a known Guard.
     * @param state             Typed memory view over state payload that Guard reports as invalid
     * @param srSignature       Guard signature for the report
     * @return status           Struct representing guard status, see {_recoverAgent}
     * @return guard            Guard that signed the report
     */
    function _verifyStateReport(State state, bytes memory srSignature)
        internal
        view
        returns (AgentStatus memory status, address guard)
    {
        // This will revert if signer is not a known agent
        (status, guard) = _recoverAgent(state.hashInvalid(), srSignature);
        // Report signer needs to be a Guard, not a Notary
        if (status.domain != 0) revert AgentNotGuard();
    }

    /**
     * @dev Internal function to verify the signed snapshot payload.
     * Reverts if any of these is true:
     *  - Snapshot signer is not a known Agent.
     *  - Snapshot signer is not a Notary (if verifyNotary is true).
     * @param snapshot          Typed memory view over snapshot payload
     * @param snapSignature     Agent signature for the snapshot
     * @param verifyNotary      If true, snapshot signer needs to be a Notary, not a Guard
     * @return status           Struct representing agent status, see {_recoverAgent}
     * @return agent            Agent that signed the snapshot
     */
    function _verifySnapshot(Snapshot snapshot, bytes memory snapSignature, bool verifyNotary)
        internal
        view
        returns (AgentStatus memory status, address agent)
    {
        // This will revert if signer is not a known agent
        (status, agent) = _recoverAgent(snapshot.hashValid(), snapSignature);
        // If requested, snapshot signer needs to be a Notary, not a Guard
        if (verifyNotary && status.domain == 0) revert AgentNotNotary();
    }

    // ═══════════════════════════════════════════ MERKLE RELATED CHECKS ═══════════════════════════════════════════════

    /**
     * @dev Internal function to verify that snapshot roots match.
     * Reverts if any of these is true:
     *  - Attestation root is not equal to Merkle Root derived from State and Snapshot Proof.
     *  - Snapshot Proof's first element does not match the State metadata.
     *  - Snapshot Proof length exceeds Snapshot tree Height.
     *  - State index is out of range.
     * @param att               Typed memory view over Attestation
     * @param stateIndex        Index of state in the snapshot
     * @param state             Typed memory view over the provided state payload
     * @param snapProof         Raw payload with snapshot data
     */
    function _verifySnapshotMerkle(Attestation att, uint8 stateIndex, State state, bytes32[] memory snapProof)
        internal
        pure
    {
        // Snapshot proof first element should match State metadata (aka "right sub-leaf")
        (, bytes32 rightSubLeaf) = state.subLeafs();
        if (snapProof[0] != rightSubLeaf) revert IncorrectSnapshotProof();
        // Reconstruct Snapshot Merkle Root using the snapshot proof
        // This will revert if:
        //  - State index is out of range.
        //  - Snapshot Proof length exceeds Snapshot tree Height.
        bytes32 snapshotRoot = SnapshotLib.proofSnapRoot(state.root(), state.origin(), snapProof, stateIndex);
        // Snapshot root should match the attestation root
        if (att.snapRoot() != snapshotRoot) revert IncorrectSnapshotRoot();
    }
}