// SPDX-License-Identifier: MIT pragma solidity 0.8.17; // ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════ import {Attestation, AttestationLib} from "../libs/memory/Attestation.sol"; import { CallerNotDestination, IncorrectAgentDomain, IncorrectSnapshotRoot, IncorrectTipsProof, MustBeSynapseDomain } from "../libs/Errors.sol"; import {ChainGas} from "../libs/stack/GasData.sol"; import {MerkleMath} from "../libs/merkle/MerkleMath.sol"; import {Receipt, ReceiptLib} from "../libs/memory/Receipt.sol"; import {Snapshot, SnapshotLib} from "../libs/memory/Snapshot.sol"; import {AgentStatus} from "../libs/Structures.sol"; import {Tips, TipsLib} from "../libs/stack/Tips.sol"; // ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════ import {StatementInbox} from "./StatementInbox.sol"; import {MessagingBase} from "../base/MessagingBase.sol"; import {InboxEvents} from "../events/InboxEvents.sol"; import {IAgentManager} from "../interfaces/IAgentManager.sol"; import {InterfaceDestination} from "../interfaces/InterfaceDestination.sol"; import {IExecutionHub} from "../interfaces/IExecutionHub.sol"; import {InterfaceInbox} from "../interfaces/InterfaceInbox.sol"; import {ISnapshotHub} from "../interfaces/ISnapshotHub.sol"; import {InterfaceSummit} from "../interfaces/InterfaceSummit.sol"; /// @notice `Inbox` is the child of `StatementInbox` contract, that is used on Synapse Chain. /// In addition to the functionality of `StatementInbox`, it also: /// - Accepts Guard and Notary Snapshots and passes them to `Summit` contract. /// - Accepts Notary-signed Receipts and passes them to `Summit` contract. /// - Accepts Receipt Reports to initiate a dispute between Guard and Notary. /// - Verifies Attestations and Attestation Reports, and slashes the signer if they are invalid. contract Inbox is StatementInbox, InboxEvents, InterfaceInbox { using AttestationLib for bytes; using ReceiptLib for bytes; using SnapshotLib for bytes; // Struct to get around stack too deep error. TODO: revisit this struct ReceiptInfo { AgentStatus rcptNotaryStatus; address notary; uint32 attNonce; AgentStatus attNotaryStatus; } // ══════════════════════════════════════════════════ STORAGE ══════════════════════════════════════════════════════ // The address of the Summit contract. address public summit; // ═════════════════════════════════════════ CONSTRUCTOR & INITIALIZER ═════════════════════════════════════════════ constructor(uint32 synapseDomain_) MessagingBase("0.0.3", synapseDomain_) { if (localDomain != synapseDomain) revert MustBeSynapseDomain(); } /// @notice Initializes `Inbox` contract: /// - Sets `msg.sender` as the owner of the contract /// - Sets `agentManager`, `origin`, `destination` and `summit` addresses function initialize(address agentManager_, address origin_, address destination_, address summit_) external initializer { __StatementInbox_init(agentManager_, origin_, destination_); summit = summit_; } // ══════════════════════════════════════════ SUBMIT AGENT STATEMENTS ══════════════════════════════════════════════ /// @inheritdoc InterfaceInbox function submitSnapshot(bytes memory snapPayload, bytes memory snapSignature) external returns (bytes memory attPayload, bytes32 agentRoot_, uint256[] memory snapGas) { // This will revert if payload is not a snapshot Snapshot snapshot = snapPayload.castToSnapshot(); // This will revert if the signer is not a known Guard/Notary (AgentStatus memory status, address agent) = _verifySnapshot({snapshot: snapshot, snapSignature: snapSignature, verifyNotary: false}); // Check that Agent is active status.verifyActive(); // Store Agent signature for the Snapshot uint256 sigIndex = _saveSignature(snapSignature); if (status.domain == 0) { // Guard that is in Dispute could still submit new snapshots, so we don't check that InterfaceSummit(summit).acceptGuardSnapshot({ guardIndex: status.index, sigIndex: sigIndex, snapPayload: snapPayload }); } else { // Get current agentRoot from AgentManager agentRoot_ = IAgentManager(agentManager).agentRoot(); // This will revert if Notary is in Dispute attPayload = InterfaceSummit(summit).acceptNotarySnapshot({ notaryIndex: status.index, sigIndex: sigIndex, agentRoot: agentRoot_, snapPayload: snapPayload }); ChainGas[] memory snapGas_ = snapshot.snapGas(); // Pass created attestation to Destination to enable executing messages coming to Synapse Chain InterfaceDestination(destination).acceptAttestation( status.index, type(uint256).max, attPayload, agentRoot_, snapGas_ ); // Use assembly to cast ChainGas[] to uint256[] without copying. Highest bits are left zeroed. // solhint-disable-next-line no-inline-assembly assembly { snapGas := snapGas_ } } emit SnapshotAccepted(status.domain, agent, snapPayload, snapSignature); } /// @inheritdoc InterfaceInbox function submitReceipt( bytes memory rcptPayload, bytes memory rcptSignature, uint256 paddedTips, bytes32 headerHash, bytes32 bodyHash ) external returns (bool wasAccepted) { // Struct to get around stack too deep error. ReceiptInfo memory info; // This will revert if payload is not a receipt Receipt rcpt = rcptPayload.castToReceipt(); // This will revert if the receipt signer is not a known Notary (info.rcptNotaryStatus, info.notary) = _verifyReceipt(rcpt, rcptSignature); // Receipt Notary needs to be Active info.rcptNotaryStatus.verifyActive(); info.attNonce = IExecutionHub(destination).getAttestationNonce(rcpt.snapshotRoot()); if (info.attNonce == 0) revert IncorrectSnapshotRoot(); // Attestation Notary domain needs to match the destination domain info.attNotaryStatus = IAgentManager(agentManager).agentStatus(rcpt.attNotary()); if (info.attNotaryStatus.domain != rcpt.destination()) revert IncorrectAgentDomain(); // Check that the correct tip values for the message were provided _verifyReceiptTips(rcpt.messageHash(), paddedTips, headerHash, bodyHash); // Store Notary signature for the Receipt uint256 sigIndex = _saveSignature(rcptSignature); // This will revert if Receipt Notary is in Dispute wasAccepted = InterfaceSummit(summit).acceptReceipt({ rcptNotaryIndex: info.rcptNotaryStatus.index, attNotaryIndex: info.attNotaryStatus.index, sigIndex: sigIndex, attNonce: info.attNonce, paddedTips: paddedTips, rcptPayload: rcptPayload }); if (wasAccepted) { emit ReceiptAccepted(info.rcptNotaryStatus.domain, info.notary, rcptPayload, rcptSignature); } } /// @inheritdoc InterfaceInbox function submitReceiptReport(bytes memory rcptPayload, bytes memory rcptSignature, bytes memory rrSignature) external returns (bool wasAccepted) { // This will revert if payload is not a receipt Receipt rcpt = rcptPayload.castToReceipt(); // This will revert if the receipt signer is not a known Guard (AgentStatus memory guardStatus,) = _verifyReceiptReport(rcpt, rrSignature); // Guard needs to be Active guardStatus.verifyActive(); // This will revert if report signer is not a known Notary (AgentStatus memory notaryStatus,) = _verifyReceipt(rcpt, rcptSignature); // Notary needs to be Active/Unstaking notaryStatus.verifyActiveUnstaking(); _saveReport(rcptPayload, rrSignature); // This will revert if either actor is already in dispute IAgentManager(agentManager).openDispute(guardStatus.index, notaryStatus.index); return true; } /// @inheritdoc InterfaceInbox function passReceipt(uint32 attNotaryIndex, uint32 attNonce, uint256 paddedTips, bytes memory rcptPayload) external returns (bool wasAccepted) { // Only Destination can pass receipts if (msg.sender != destination) revert CallerNotDestination(); return InterfaceSummit(summit).acceptReceipt({ rcptNotaryIndex: attNotaryIndex, attNotaryIndex: attNotaryIndex, sigIndex: type(uint256).max, attNonce: attNonce, paddedTips: paddedTips, rcptPayload: rcptPayload }); } // ══════════════════════════════════════════ VERIFY AGENT STATEMENTS ══════════════════════════════════════════════ /// @inheritdoc InterfaceInbox function verifyAttestation(bytes memory attPayload, bytes memory attSignature) external returns (bool isValidAttestation) { // 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(); isValidAttestation = ISnapshotHub(summit).isValidAttestation(attPayload); if (!isValidAttestation) { emit InvalidAttestation(attPayload, attSignature); IAgentManager(agentManager).slashAgent(status.domain, notary, msg.sender); } } /// @inheritdoc InterfaceInbox function verifyAttestationReport(bytes memory attPayload, bytes memory arSignature) external returns (bool isValidReport) { // This will revert if payload is not an attestation Attestation att = attPayload.castToAttestation(); // This will revert if the report signer is not a known Guard (AgentStatus memory status, address guard) = _verifyAttestationReport(att, arSignature); // Guard needs to be Active/Unstaking status.verifyActiveUnstaking(); // Report is valid IF AND ONLY IF the reported attestation in invalid isValidReport = !ISnapshotHub(summit).isValidAttestation(attPayload); if (!isValidReport) { emit InvalidAttestationReport(attPayload, arSignature); IAgentManager(agentManager).slashAgent(status.domain, guard, msg.sender); } } // ══════════════════════════════════════════════ INTERNAL VIEWS ═══════════════════════════════════════════════════ /// @dev Verifies that tips proof matches the message hash. function _verifyReceiptTips(bytes32 msgHash, uint256 paddedTips, bytes32 headerHash, bytes32 bodyHash) internal pure { Tips tips = TipsLib.wrapPadded(paddedTips); // full message leaf is (header, baseMessage), while base message leaf is (tips, remainingBody). if (MerkleMath.getParent(headerHash, MerkleMath.getParent(tips.leaf(), bodyHash)) != msgHash) { revert IncorrectTipsProof(); } } }