synapsecns/sanguine

View on GitHub
packages/contracts-core/contracts/Destination.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 {ByteString} from "./libs/memory/ByteString.sol";
import {AGENT_ROOT_OPTIMISTIC_PERIOD} from "./libs/Constants.sol";
import {IndexOutOfRange, DisputeTimeoutNotOver, NotaryInDispute, OutdatedNonce} from "./libs/Errors.sol";
import {ChainGas, GasData} from "./libs/stack/GasData.sol";
import {AgentStatus, DestinationStatus} from "./libs/Structures.sol";
import {ChainContext} from "./libs/ChainContext.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {AgentSecured} from "./base/AgentSecured.sol";
import {DestinationEvents} from "./events/DestinationEvents.sol";
import {IAgentManager} from "./interfaces/IAgentManager.sol";
import {InterfaceDestination} from "./interfaces/InterfaceDestination.sol";
import {InterfaceLightManager} from "./interfaces/InterfaceLightManager.sol";
import {IStatementInbox} from "./interfaces/IStatementInbox.sol";
import {ExecutionHub} from "./hubs/ExecutionHub.sol";

/// @notice `Destination` contract is used for receiving messages from other chains. It relies on
/// Notary-signed statements to get the truthful states of the remote chains. These states are then
/// used to verify the validity of the messages sent from the remote chains.
/// `Destination` is responsible for the following:
/// - Accepting the Attestations from the local Inbox contract.
/// - Using these Attestations to execute the messages (see parent `ExecutionHub`).
/// - Passing the Agent Merkle Roots from the Attestations to the local LightManager contract,
///   if deployed on a non-Synapse chain.
/// - Keeping track of the remote domains GasData submitted by Notaries, that could be later consumed
///   by the local `GasOracle` contract.
contract Destination is ExecutionHub, DestinationEvents, InterfaceDestination {
    using AttestationLib for bytes;
    using ByteString for bytes;

    // TODO: this could be further optimized in terms of storage
    struct StoredAttData {
        bytes32 agentRoot;
        bytes32 dataHash;
    }

    struct StoredGasData {
        GasData gasData;
        uint32 notaryIndex;
        uint40 submittedAt;
    }

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

    /// @dev Invariant: this is either current LightManager root,
    /// or the pending root to be passed to LightManager once its optimistic period is over.
    bytes32 internal _nextAgentRoot;

    /// @inheritdoc InterfaceDestination
    DestinationStatus public destStatus;

    /// @inheritdoc InterfaceDestination
    mapping(uint32 => uint32) public lastAttestationNonce;

    /// @dev Stored lookup data for all accepted Notary Attestations
    StoredAttData[] internal _storedAttestations;

    /// @dev Remote domains GasData submitted by Notaries
    mapping(uint32 => StoredGasData) internal _storedGasData;

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

    constructor(uint32 synapseDomain_, address agentManager_, address inbox_)
        AgentSecured("0.0.3", synapseDomain_, agentManager_, inbox_)
    {} // solhint-disable-line no-empty-blocks

    /// @notice Initializes Destination contract:
    /// - msg.sender is set as contract owner
    function initialize(bytes32 agentRoot) external initializer {
        // Initialize Ownable: msg.sender is set as "owner"
        __Ownable2Step_init();
        // Initialize ReeentrancyGuard
        __ReentrancyGuard_init();
        // Set Agent Merkle Root in Light Manager
        if (localDomain != synapseDomain) {
            _nextAgentRoot = agentRoot;
            InterfaceLightManager(address(agentManager)).setAgentRoot(agentRoot);
            destStatus.agentRootTime = ChainContext.blockTimestamp();
        }
        // No need to do anything on Synapse Chain, as the agent root is set in BondingManager
    }

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

    /// @inheritdoc InterfaceDestination
    function acceptAttestation(
        uint32 notaryIndex,
        uint256 sigIndex,
        bytes memory attPayload,
        bytes32 agentRoot,
        ChainGas[] memory snapGas
    ) external onlyInbox returns (bool wasAccepted) {
        // Check that we can trust the Notary data: they are not in dispute, and the dispute timeout is over (if any)
        if (_notaryDisputeExists(notaryIndex)) revert NotaryInDispute();
        if (_notaryDisputeTimeout(notaryIndex)) revert DisputeTimeoutNotOver();
        // First, try passing current agent merkle root
        // This will revert if payload is not an attestation
        Attestation att = attPayload.castToAttestation();
        // Check that this Notary hasn't used a more fresh nonce
        uint32 attNonce = att.nonce();
        if (attNonce <= lastAttestationNonce[notaryIndex]) revert OutdatedNonce();
        lastAttestationNonce[notaryIndex] = attNonce;
        // This will revert if snapshot root has been previously submitted
        _saveAttestation(att, notaryIndex, sigIndex);
        _storedAttestations.push(StoredAttData({agentRoot: agentRoot, dataHash: att.dataHash()}));
        // Save Agent Root if required, and update the Destination's Status
        bool rootPending = passAgentRoot();
        destStatus = _saveAgentRoot(rootPending, agentRoot, notaryIndex);
        _saveGasData(snapGas, notaryIndex);
        return true;
    }

    // ═══════════════════════════════════════════ AGENT ROOT QUARANTINE ═══════════════════════════════════════════════

    /// @inheritdoc InterfaceDestination
    function passAgentRoot() public returns (bool rootPending) {
        // Agent root is not passed on Synapse Chain, as it could be accessed via BondingManager
        if (localDomain == synapseDomain) return false;
        bytes32 oldRoot = IAgentManager(agentManager).agentRoot();
        bytes32 newRoot = _nextAgentRoot;
        // Check if agent root differs from the current one in LightManager
        if (oldRoot == newRoot) return false;
        DestinationStatus memory status = destStatus;
        // Invariant: Notary who supplied `newRoot` was registered as active against `oldRoot`
        // So we just need to check the Dispute status of the Notary
        if (_notaryDisputeExists(status.notaryIndex)) {
            // Remove the pending agent merkle root, as its signer is in dispute
            _nextAgentRoot = oldRoot;
            return false;
        }
        // If Notary recently won a Dispute, we can optimistically assume that their passed root is valid.
        // However, we need to wait until the Dispute timeout is over, before passing the new root to LightManager.
        if (_notaryDisputeTimeout(status.notaryIndex)) {
            // We didn't pass anything, but there is a pending root
            return true;
        }
        // Check if agent root optimistic period is over
        if (status.agentRootTime + AGENT_ROOT_OPTIMISTIC_PERIOD > block.timestamp) {
            // We didn't pass anything, but there is a pending root
            return true;
        }
        // `newRoot` signer was not disputed, and the root optimistic period is over.
        // Finally, pass the Agent Merkle Root to LightManager
        InterfaceLightManager(address(agentManager)).setAgentRoot(newRoot);
        return false;
    }

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

    /// @inheritdoc InterfaceDestination
    // solhint-disable-next-line ordering
    function attestationsAmount() external view returns (uint256) {
        return _roots.length;
    }

    /// @inheritdoc InterfaceDestination
    function getAttestation(uint256 index) external view returns (bytes memory attPayload, bytes memory attSignature) {
        if (index >= _roots.length) revert IndexOutOfRange();
        bytes32 snapRoot = _roots[index];
        SnapRootData memory rootData = _rootData[snapRoot];
        StoredAttData memory storedAtt = _storedAttestations[index];
        attPayload = AttestationLib.formatAttestation({
            snapRoot_: snapRoot,
            dataHash_: storedAtt.dataHash,
            nonce_: rootData.attNonce,
            blockNumber_: rootData.attBN,
            timestamp_: rootData.attTS
        });
        // Attestation signatures are not required on Synapse Chain, as the attestations could be accessed via Summit.
        if (localDomain != synapseDomain) {
            attSignature = IStatementInbox(inbox).getStoredSignature(rootData.sigIndex);
        }
    }

    /// @inheritdoc InterfaceDestination
    function getGasData(uint32 domain) external view returns (GasData gasData, uint256 dataMaturity) {
        StoredGasData memory storedGasData = _storedGasData[domain];
        // Form the data to return only if it exists and we can trust it:
        // - There is stored gas data for the domain
        // - Notary who provided the data is not in dispute
        // - Notary who provided the data is not in post-dispute timeout period
        // forgefmt: disable-next-item
        if (
            storedGasData.submittedAt != 0 &&
            !_notaryDisputeExists(storedGasData.notaryIndex) &&
            !_notaryDisputeTimeout(storedGasData.notaryIndex)
        ) {
            gasData = storedGasData.gasData;
            dataMaturity = block.timestamp - storedGasData.submittedAt;
        }
        // Return empty values if there is no data for the domain, or the notary who provided the data can't be trusted.
    }

    /// @inheritdoc InterfaceDestination
    function nextAgentRoot() external view returns (bytes32) {
        // Return current agent root on Synapse Chain for consistency
        return localDomain == synapseDomain ? IAgentManager(agentManager).agentRoot() : _nextAgentRoot;
    }

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

    /// @dev Saves Agent Merkle Root from the accepted attestation, if there is
    /// no pending root to be passed to LightManager.
    /// Returns the updated "last snapshot root / last agent root" status struct.
    function _saveAgentRoot(bool rootPending, bytes32 agentRoot, uint32 notaryIndex)
        internal
        returns (DestinationStatus memory status)
    {
        status = destStatus;
        // Update the timestamp for the latest snapshot root
        status.snapRootTime = ChainContext.blockTimestamp();
        // No need to save agent roots on Synapse Chain, as they could be accessed via BondingManager
        // Don't update agent root, if there is already a pending one
        // Update the data for latest agent root only if it differs from the saved one
        if (localDomain != synapseDomain && !rootPending && _nextAgentRoot != agentRoot) {
            status.agentRootTime = ChainContext.blockTimestamp();
            status.notaryIndex = notaryIndex;
            _nextAgentRoot = agentRoot;
            emit AgentRootAccepted(agentRoot);
        }
    }

    /// @dev Saves updated values from the snapshot's gas data list.
    function _saveGasData(ChainGas[] memory snapGas, uint32 notaryIndex) internal {
        uint256 statesAmount = snapGas.length;
        for (uint256 i = 0; i < statesAmount; i++) {
            ChainGas chainGas = snapGas[i];
            uint32 domain = chainGas.domain();
            // Don't save gas data for the local domain
            if (domain == localDomain) continue;
            StoredGasData memory storedGasData = _storedGasData[domain];
            // Check that the gas data is not already saved
            GasData gasData = chainGas.gasData();
            if (GasData.unwrap(gasData) == GasData.unwrap(storedGasData.gasData)) continue;
            // Save the gas data
            _storedGasData[domain] =
                StoredGasData({gasData: gasData, notaryIndex: notaryIndex, submittedAt: ChainContext.blockTimestamp()});
        }
    }
}