synapsecns/sanguine

View on GitHub
packages/contracts-core/contracts/base/AgentSecured.sol

Summary

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

// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════
import {DISPUTE_TIMEOUT_NOTARY} from "../libs/Constants.sol";
import {ChainContext} from "../libs/ChainContext.sol";
import {CallerNotAgentManager, CallerNotInbox} from "../libs/Errors.sol";
import {AgentStatus, DisputeFlag, DisputeStatus} from "../libs/Structures.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {IAgentManager} from "../interfaces/IAgentManager.sol";
import {IAgentSecured} from "../interfaces/IAgentSecured.sol";
import {MessagingBase} from "./MessagingBase.sol";

/**
 * @notice Base contract for messaging contracts that are secured by the agent manager.
 * `AgentSecured` relies on `AgentManager` to provide the following functionality:
 * - Keep track of agents and their statuses.
 * - Pass agent-signed statements that were verified by the agent manager.
 * - These statements are considered valid indefinitely, unless the agent is disputed.
 * - Disputes are opened and resolved by the agent manager.
 * > `AgentSecured` implementation should never use statements signed by agents that are disputed.
 */
abstract contract AgentSecured is MessagingBase, IAgentSecured {
    // ════════════════════════════════════════════════ IMMUTABLES ═════════════════════════════════════════════════════

    /// @inheritdoc IAgentSecured
    address public immutable agentManager;

    /// @inheritdoc IAgentSecured
    address public immutable inbox;

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

    // (agent index => their dispute status: flag, openedAt, resolvedAt)
    mapping(uint32 => DisputeStatus) internal _disputes;

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

    modifier onlyAgentManager() {
        if (msg.sender != agentManager) revert CallerNotAgentManager();
        _;
    }

    modifier onlyInbox() {
        if (msg.sender != inbox) revert CallerNotInbox();
        _;
    }

    constructor(string memory version_, uint32 synapseDomain_, address agentManager_, address inbox_)
        MessagingBase(version_, synapseDomain_)
    {
        agentManager = agentManager_;
        inbox = inbox_;
    }

    // ════════════════════════════════════════════ ONLY AGENT MANAGER ═════════════════════════════════════════════════

    /// @inheritdoc IAgentSecured
    function openDispute(uint32 guardIndex, uint32 notaryIndex) external onlyAgentManager {
        uint40 openedAt = ChainContext.blockTimestamp();
        DisputeStatus memory status = DisputeStatus({flag: DisputeFlag.Pending, openedAt: openedAt, resolvedAt: 0});
        _disputes[guardIndex] = status;
        _disputes[notaryIndex] = status;
    }

    /// @inheritdoc IAgentSecured
    function resolveDispute(uint32 slashedIndex, uint32 rivalIndex) external onlyAgentManager {
        // Update the dispute status of the slashed agent first.
        uint40 resolvedAt = ChainContext.blockTimestamp();
        _disputes[slashedIndex].flag = DisputeFlag.Slashed;
        _disputes[slashedIndex].resolvedAt = resolvedAt;
        // Mark the rival agent as not disputed, if there was an ongoing dispute.
        if (rivalIndex != 0) {
            _disputes[rivalIndex].flag = DisputeFlag.None;
            _disputes[rivalIndex].resolvedAt = resolvedAt;
        }
    }

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

    /// @inheritdoc IAgentSecured
    function agentStatus(address agent) external view returns (AgentStatus memory) {
        return _agentStatus(agent);
    }

    /// @inheritdoc IAgentSecured
    function getAgent(uint256 index) external view returns (address agent, AgentStatus memory status) {
        return _getAgent(index);
    }

    /// @inheritdoc IAgentSecured
    function latestDisputeStatus(uint32 agentIndex) external view returns (DisputeStatus memory) {
        return _disputes[agentIndex];
    }

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

    /// @dev Returns status of the given agent: (flag, domain, index).
    function _agentStatus(address agent) internal view returns (AgentStatus memory) {
        return IAgentManager(agentManager).agentStatus(agent);
    }

    /// @dev Returns agent and their status for a given agent index. Returns zero values for non existing indexes.
    function _getAgent(uint256 index) internal view returns (address agent, AgentStatus memory status) {
        return IAgentManager(agentManager).getAgent(index);
    }

    /// @dev Checks if a Dispute exists for the given Notary. This function returns true, if
    /// Notary is in ongoing Dispute, or if Dispute was resolved not in Notary's favor.
    /// In both cases we can't trust Notary's data.
    /// Note: Agent-Secured contracts can trust Notary data only if both `_notaryDisputeExists` and
    /// `_notaryDisputeTimeout` return false.
    function _notaryDisputeExists(uint32 notaryIndex) internal view returns (bool) {
        return _disputes[notaryIndex].flag != DisputeFlag.None;
    }

    /// @dev Checks if a Notary recently won a Dispute and is still in the "post-dispute" timeout period.
    /// In this period we still can't trust Notary's data, though we can optimistically assume that
    /// that the data will be correct after the timeout (assuming no new Disputes are opened).
    /// Note: Agent-Secured contracts can trust Notary data only if both `_notaryDisputeExists` and
    /// `_notaryDisputeTimeout` return false.
    function _notaryDisputeTimeout(uint32 notaryIndex) internal view returns (bool) {
        DisputeStatus memory status = _disputes[notaryIndex];
        // Exit early if Notary is in ongoing Dispute / slashed.
        if (status.flag != DisputeFlag.None) return false;
        // Check if Notary has been in any Dispute at all.
        if (status.openedAt == 0) return false;
        // Otherwise check if the Dispute timeout is still active.
        return block.timestamp < status.resolvedAt + DISPUTE_TIMEOUT_NOTARY;
    }
}