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