synapsecns/sanguine

View on GitHub
packages/contracts-core/contracts/Summit.sol

Summary

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

// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════
import {AttestationLib} from "./libs/memory/Attestation.sol";
import {ByteString} from "./libs/memory/ByteString.sol";
import {BONDING_OPTIMISTIC_PERIOD, TIPS_GRANULARITY} from "./libs/Constants.sol";
import {
    MustBeSynapseDomain,
    DisputeTimeoutNotOver,
    NotaryInDispute,
    TipsClaimMoreThanEarned,
    TipsClaimZero
} from "./libs/Errors.sol";
import {Receipt, ReceiptLib} from "./libs/memory/Receipt.sol";
import {Snapshot, SnapshotLib} from "./libs/memory/Snapshot.sol";
import {AgentFlag, AgentStatus, DisputeFlag, MessageStatus} from "./libs/Structures.sol";
import {Tips, TipsLib} from "./libs/stack/Tips.sol";
import {ChainContext} from "./libs/ChainContext.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {AgentSecured} from "./base/AgentSecured.sol";
import {SummitEvents} from "./events/SummitEvents.sol";
import {IAgentManager} from "./interfaces/IAgentManager.sol";
import {InterfaceBondingManager} from "./interfaces/InterfaceBondingManager.sol";
import {InterfaceSummit} from "./interfaces/InterfaceSummit.sol";
import {SnapshotHub} from "./hubs/SnapshotHub.sol";
// ═════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════
import {DoubleEndedQueue} from "@openzeppelin/contracts/utils/structs/DoubleEndedQueue.sol";

/// @notice `Summit` contract is the cornerstone of the Synapse messaging protocol. This is where the
/// states of all the remote chains (provided collectively by the Guards and Notaries) are stored. This is
/// also the place where the tips are distributed among the off-chain actors.
/// `Summit` is responsible for the following:
/// - Accepting Guard and Notary snapshots from the local `Inbox` contract, and storing the states from these
///   snapshots (see parent contract `SnapshotHub`).
/// - Accepting Notary Receipts from the local `Inbox` contract, and using them to distribute tips among the
///   off-chain actors that participated in the message lifecycle.
contract Summit is SnapshotHub, SummitEvents, InterfaceSummit {
    using AttestationLib for bytes;
    using ByteString for bytes;
    using DoubleEndedQueue for DoubleEndedQueue.Bytes32Deque;
    using ReceiptLib for bytes;
    using SnapshotLib for bytes;

    // TODO: write docs, pack values
    struct SummitReceipt {
        uint32 origin;
        uint32 destination;
        uint32 attNonce;
        uint8 stateIndex;
        uint32 attNotaryIndex;
        address firstExecutor;
        address finalExecutor;
    }

    struct ReceiptStatus {
        MessageStatus status;
        bool pending;
        bool tipsAwarded;
        uint32 receiptNotaryIndex;
        uint40 submittedAt;
    }

    struct ReceiptTips {
        uint64 summitTip;
        uint64 attestationTip;
        uint64 executionTip;
        uint64 deliveryTip;
    }

    /// @notice Struct for storing the actor tips for a given origin domain.
    /// @param earned   Total amount of tips earned by the actor, denominated in domain's wei
    /// @param claimed  Total amount of tips claimed by the actor, denominated in domain's wei
    struct ActorTips {
        uint128 earned;
        uint128 claimed;
    }

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

    // (message hash => receipt data)
    mapping(bytes32 => SummitReceipt) private _receipts;

    // (message hash => receipt status)
    mapping(bytes32 => ReceiptStatus) private _receiptStatus;

    // (message hash => receipt tips)
    mapping(bytes32 => ReceiptTips) private _receiptTips;

    // Quarantine queue for message hashes
    DoubleEndedQueue.Bytes32Deque private _receiptQueue;

    /// @inheritdoc InterfaceSummit
    mapping(address => mapping(uint32 => ActorTips)) public actorTips;

    // ═════════════════════════════════════════ CONSTRUCTOR & INITIALIZER ═════════════════════════════════════════════

    constructor(uint32 synapseDomain_, address agentManager_, address inbox_)
        AgentSecured("0.0.3", synapseDomain_, agentManager_, inbox_)
    {
        if (localDomain != synapseDomain) revert MustBeSynapseDomain();
    }

    function initialize() external initializer {
        // Initialize Ownable: msg.sender is set as "owner"
        __Ownable2Step_init();
        _initializeAttestations();
    }

    // ═════════════════════════════════════════════ ACCEPT STATEMENTS ═════════════════════════════════════════════════

    /// @inheritdoc InterfaceSummit
    function acceptReceipt(
        uint32 rcptNotaryIndex,
        uint32 attNotaryIndex,
        uint256 sigIndex,
        uint32 attNonce,
        uint256 paddedTips,
        bytes memory rcptPayload
    ) external onlyInbox returns (bool wasAccepted) {
        // Check that we can trust the receipt Notary data: they are not in dispute, and the dispute timeout is over.
        if (_notaryDisputeExists(rcptNotaryIndex)) revert NotaryInDispute();
        if (_notaryDisputeTimeout(rcptNotaryIndex)) revert DisputeTimeoutNotOver();
        // This will revert if payload is not a receipt body
        return _saveReceipt({
            rcpt: rcptPayload.castToReceipt(),
            tips: TipsLib.wrapPadded(paddedTips),
            rcptNotaryIndex: rcptNotaryIndex,
            attNotaryIndex: attNotaryIndex,
            sigIndex: sigIndex,
            attNonce: attNonce
        });
    }

    /// @inheritdoc InterfaceSummit
    function acceptGuardSnapshot(uint32 guardIndex, uint256 sigIndex, bytes memory snapPayload) external onlyInbox {
        // Note: we don't check if Guard is in Dispute,
        // as the Guards could continue to submit snapshots after submitting a report.
        // This will revert if payload is not a snapshot
        _acceptGuardSnapshot(snapPayload.castToSnapshot(), guardIndex, sigIndex);
    }

    /// @inheritdoc InterfaceSummit
    function acceptNotarySnapshot(uint32 notaryIndex, uint256 sigIndex, bytes32 agentRoot, bytes memory snapPayload)
        external
        onlyInbox
        returns (bytes memory attPayload)
    {
        // Check that we can trust the snapshot Notary data: they are not in dispute, and the dispute timeout is over.
        if (_notaryDisputeExists(notaryIndex)) revert NotaryInDispute();
        if (_notaryDisputeTimeout(notaryIndex)) revert DisputeTimeoutNotOver();
        // This will revert if payload is not a snapshot
        return _acceptNotarySnapshot(snapPayload.castToSnapshot(), agentRoot, notaryIndex, sigIndex);
    }

    // ════════════════════════════════════════════════ TIPS LOGIC ═════════════════════════════════════════════════════

    /// @inheritdoc InterfaceSummit
    function distributeTips() public returns (bool queuePopped) {
        // Check message that is first in the "quarantine queue"
        if (_receiptQueue.empty()) return false;
        bytes32 messageHash = _receiptQueue.front();
        ReceiptStatus memory rcptStatus = _receiptStatus[messageHash];
        // Check if optimistic period for the receipt is over
        if (block.timestamp < uint256(rcptStatus.submittedAt) + BONDING_OPTIMISTIC_PERIOD) return false;
        // Fetch Notary who signed the receipt. If they are Slashed or in Dispute, exit early.
        if (_checkNotaryDisputed(messageHash, rcptStatus.receiptNotaryIndex)) return true;
        SummitReceipt memory summitRcpt = _receipts[messageHash];
        // Fetch Notary who signed the statement with snapshot root. If they are Slashed or in Dispute, exit early.
        if (_checkNotaryDisputed(messageHash, summitRcpt.attNotaryIndex)) return true;
        // At this point Receipt is optimistically verified to be correct, as well as the receipt's attestation
        // Meaning we can go ahead and distribute the tip values among the tipped actors.
        _awardTips(rcptStatus.receiptNotaryIndex, summitRcpt.attNotaryIndex, messageHash, summitRcpt, rcptStatus);
        // Save new receipt status
        rcptStatus.pending = false;
        rcptStatus.tipsAwarded = true;
        _receiptStatus[messageHash] = rcptStatus;
        // Remove the receipt from the queue
        _receiptQueue.popFront();
        return true;
    }

    /// @inheritdoc InterfaceSummit
    // solhint-disable-next-line ordering
    function withdrawTips(uint32 origin, uint256 amount) external {
        if (amount == 0) revert TipsClaimZero();
        ActorTips memory tips = actorTips[msg.sender][origin];
        if (tips.earned < amount + tips.claimed) revert TipsClaimMoreThanEarned();
        // Guaranteed to fit into uint128, as the sum is lower than `earned`
        actorTips[msg.sender][origin].claimed = uint128(tips.claimed + amount);
        InterfaceBondingManager(address(agentManager)).withdrawTips(msg.sender, origin, amount);
        emit TipWithdrawalInitiated(msg.sender, origin, amount);
    }

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

    /// @inheritdoc InterfaceSummit
    // solhint-disable-next-line ordering
    function receiptQueueLength() external view returns (uint256) {
        return _receiptQueue.length();
    }

    /// @inheritdoc InterfaceSummit
    function getLatestState(uint32 origin) external view returns (bytes memory statePayload) {
        // Get a list of currently active guards
        address[] memory guards = InterfaceBondingManager(address(agentManager)).getActiveAgents(0);
        SummitState memory latestState;
        for (uint256 i = 0; i < guards.length; ++i) {
            SummitState memory state = _latestState(origin, _agentStatus(guards[i]).index);
            if (state.nonce > latestState.nonce) latestState = state;
        }
        // Check if we found anything
        if (latestState.nonce != 0) {
            statePayload = _formatSummitState(latestState);
        }
    }

    // ═══════════════════════════════════════════ INTERNAL LOGIC: QUEUE ═══════════════════════════════════════════════

    /// @dev Checks if the given Notary has been disputed.
    /// - Notary was slashed => receipt is invalided and deleted
    /// - Notary is in Dispute => receipt handling is postponed
    function _checkNotaryDisputed(bytes32 messageHash, uint32 notaryIndex) internal returns (bool queuePopped) {
        // TODO: add timeout for Notaries that just won the dispute.
        DisputeFlag flag = _disputes[notaryIndex].flag;
        if (flag == DisputeFlag.Slashed) {
            // Notary has been slashed, so we can't trust their statement.
            // Honest Notaries are incentivized to resubmit the Receipt or Attestation if it was in fact valid.
            _deleteFromQueue(messageHash);
            queuePopped = true;
        } else if (flag == DisputeFlag.Pending || _notaryDisputeTimeout(notaryIndex)) {
            // Notary is in the ongoing Dispute, or has recently won one. We postpone the receipt handling.
            // To keep the tips flow going we add the receipt to the back of the queue,
            // hoping that by the next interaction the dispute will have been resolved.
            _moveToBack();
            queuePopped = true;
        }
    }

    /// @dev Deletes all stored receipt data and removes it from the queue.
    function _deleteFromQueue(bytes32 messageHash) internal {
        delete _receipts[messageHash];
        delete _receiptStatus[messageHash];
        delete _receiptTips[messageHash];
        _receiptQueue.popFront();
    }

    /// @dev Moves the front element of the queue to its back.
    function _moveToBack() internal {
        bytes32 popped = _receiptQueue.popFront();
        _receiptQueue.pushBack(popped);
    }

    /// @dev Saves the message from the receipt into the "quarantine queue". Once message leaves the queue,
    /// tips associated with the message are distributed across off-chain actors.
    function _saveReceipt(
        Receipt rcpt,
        Tips tips,
        uint32 rcptNotaryIndex,
        uint32 attNotaryIndex,
        uint256 sigIndex,
        uint32 attNonce
    ) internal returns (bool) {
        // TODO: save signature index
        // Check if tip values are non-zero
        if (tips.value() == 0) return false;
        // Check if there already exists receipt for the message
        bytes32 messageHash = rcpt.messageHash();
        ReceiptStatus memory savedRcpt = _receiptStatus[messageHash];
        // Don't save if receipt is already in the queue
        if (savedRcpt.pending) return false;
        // Get the status from the provided receipt
        MessageStatus msgStatus = rcpt.finalExecutor() == address(0) ? MessageStatus.Failed : MessageStatus.Success;
        // Don't save if we already have the receipt with at least this status
        if (savedRcpt.status >= msgStatus) return false;
        // Save information from the receipt
        _receipts[messageHash] = SummitReceipt({
            origin: rcpt.origin(),
            destination: rcpt.destination(),
            attNonce: attNonce,
            stateIndex: rcpt.stateIndex(),
            attNotaryIndex: attNotaryIndex,
            firstExecutor: rcpt.firstExecutor(),
            finalExecutor: rcpt.finalExecutor()
        });
        // Save receipt status: transfer tipsAwarded field (whether we paid tips for Failed Receipt before)
        _receiptStatus[messageHash] = ReceiptStatus({
            status: msgStatus,
            pending: true,
            tipsAwarded: savedRcpt.tipsAwarded,
            receiptNotaryIndex: rcptNotaryIndex,
            submittedAt: ChainContext.blockTimestamp()
        });
        // Save receipt tips
        _receiptTips[messageHash] = ReceiptTips({
            summitTip: tips.summitTip(),
            attestationTip: tips.attestationTip(),
            executionTip: tips.executionTip(),
            deliveryTip: tips.deliveryTip()
        });
        // Add message hash to the quarantine queue
        _receiptQueue.pushBack(messageHash);
        return true;
    }

    // ══════════════════════════════════════ INTERNAL LOGIC: TIPS ACCOUNTING ══════════════════════════════════════════

    /// @dev Awards tips to the agent/actors that participated in message lifecycle
    function _awardTips(
        uint32 rcptNotaryIndex,
        uint32 attNotaryIndex,
        bytes32 messageHash,
        SummitReceipt memory summitRcpt,
        ReceiptStatus memory rcptStatus
    ) internal {
        ReceiptTips memory tips = _receiptTips[messageHash];
        // Check if we awarded tips for this message earlier
        bool awardFirst = !rcptStatus.tipsAwarded;
        // Check if this is the final tips distribution
        bool awardFinal = rcptStatus.status == MessageStatus.Success;
        if (awardFirst) {
            // There has been a valid attempt to execute the message
            _awardSnapshotTip(summitRcpt.attNonce, summitRcpt.stateIndex, summitRcpt.origin, tips.summitTip);
            _awardAgentTip(attNotaryIndex, summitRcpt.origin, tips.attestationTip);
            _awardActorTip(summitRcpt.firstExecutor, summitRcpt.origin, tips.executionTip);
        }
        _awardReceiptTip(rcptNotaryIndex, awardFirst, awardFinal, summitRcpt.origin, tips.summitTip);
        if (awardFinal) {
            // Message has been executed successfully
            _awardActorTip(summitRcpt.finalExecutor, summitRcpt.origin, tips.deliveryTip);
        }
    }

    /// @dev Award tip to the bonded agent
    function _awardAgentTip(uint32 agentIndex, uint32 origin, uint64 tip) internal {
        (address agent, AgentStatus memory status) = _getAgent(agentIndex);
        // If agent has been slashed, their earned tips go to treasury
        if (status.flag == AgentFlag.Fraudulent || status.flag == AgentFlag.Slashed) {
            agent = address(0);
        }
        _awardActorTip(agent, origin, tip);
    }

    /// @dev Award tip to any actor whether bonded or unbonded
    function _awardActorTip(address actor, uint32 origin, uint64 tip) internal {
        // We need to do a shit here, as we operate with "scaled down" tips everywhere,
        // but Summit is supposed to store the "full tip value".
        // Tip fits into 64 bits, so it's safe to do a 32 bit shift without risk of overflow
        uint128 tipAwarded = uint128(tip) << uint128(TIPS_GRANULARITY);
        actorTips[actor][origin].earned += tipAwarded;
        emit TipAwarded(actor, origin, tipAwarded);
    }

    /// @dev Award tip for posting Receipt to Summit contract.
    function _awardReceiptTip(uint32 rcptNotaryIndex, bool awardFirst, bool awardFinal, uint32 origin, uint64 summitTip)
        internal
    {
        uint64 receiptTip = _receiptTip(summitTip);
        uint64 receiptTipAwarded;
        if (awardFirst && awardFinal) {
            receiptTipAwarded = receiptTip;
        } else if (awardFirst) {
            // Tip for posting Receipt with status >= MessageStatus.Failed
            receiptTipAwarded = receiptTip / 2;
        } else if (awardFinal) {
            // Tip for posting Receipt with status == MessageStatus.Success
            receiptTipAwarded = receiptTip - receiptTip / 2;
        }
        _awardAgentTip(rcptNotaryIndex, origin, receiptTipAwarded);
    }

    /// @dev Award tip for posting Snapshot to Summit contract.
    function _awardSnapshotTip(uint32 attNonce, uint8 stateIndex, uint32 origin, uint64 summitTip) internal {
        uint64 snapshotTip = _snapshotTip(summitTip);
        // Get the agents who submitted the given state for the attestation's snapshot
        (uint32 guardIndex, uint32 notaryIndex) = _stateAgents(attNonce, stateIndex);
        _awardAgentTip(guardIndex, origin, snapshotTip);
        _awardAgentTip(notaryIndex, origin, snapshotTip);
    }

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

    /// @dev Returns "snapshot part" of the summit tip.
    function _snapshotTip(uint64 summitTip) internal pure returns (uint64) {
        return summitTip / 3;
    }

    /// @dev Returns "receipt part" of the summit tip.
    function _receiptTip(uint64 summitTip) internal pure returns (uint64) {
        return summitTip - 2 * _snapshotTip(summitTip);
    }
}