synapsecns/sanguine

View on GitHub
packages/contracts-core/contracts/manager/AgentManager.sol

Summary

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

// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════
import {FRESH_DATA_TIMEOUT} from "../libs/Constants.sol";
import {
    CallerNotInbox,
    DisputeAlreadyResolved,
    IncorrectAgentDomain,
    IndexOutOfRange,
    GuardInDispute,
    NotaryInDispute,
    NotStuck
} from "../libs/Errors.sol";
import {AgentFlag, AgentStatus, DisputeFlag} from "../libs/Structures.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {MessagingBase} from "../base/MessagingBase.sol";
import {AgentManagerEvents} from "../events/AgentManagerEvents.sol";
import {IAgentManager} from "../interfaces/IAgentManager.sol";
import {InterfaceDestination} from "../interfaces/InterfaceDestination.sol";
import {IStatementInbox} from "../interfaces/IStatementInbox.sol";

/// @notice `AgentManager` is used to keep track of all the bonded agents and their statuses.
/// The exact logic of how the agent statuses are stored and updated is implemented in child contracts,
/// and depends on whether the contract is used on Synapse Chain or on other chains.
/// `AgentManager` is responsible for the following:
/// - Keeping track of all the bonded agents and their statuses.
/// - Keeping track of all the disputes between agents.
/// - Notifying `AgentSecured` contracts about the opened and resolved disputes.
/// - Notifying `AgentSecured` contracts about the slashed agents.
abstract contract AgentManager is MessagingBase, AgentManagerEvents, IAgentManager {
    struct AgentDispute {
        DisputeFlag flag;
        uint88 disputePtr;
        address fraudProver;
    }

    // TODO: do we want to store the dispute timestamp?
    struct OpenedDispute {
        uint32 guardIndex;
        uint32 notaryIndex;
        uint32 slashedIndex;
    }

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

    address public origin;

    address public destination;

    address public inbox;

    // (agent index => their dispute status)
    mapping(uint256 => AgentDispute) internal _agentDispute;

    // All disputes ever opened
    OpenedDispute[] internal _disputes;

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

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

    modifier onlyWhenStuck() {
        // Check if there has been no fresh data from the Notaries for a while.
        (uint40 snapRootTime,,) = InterfaceDestination(destination).destStatus();
        if (block.timestamp < FRESH_DATA_TIMEOUT + snapRootTime) revert NotStuck();
        _;
    }

    // ════════════════════════════════════════════════ INITIALIZER ════════════════════════════════════════════════════

    // solhint-disable-next-line func-name-mixedcase
    function __AgentManager_init(address origin_, address destination_, address inbox_) internal onlyInitializing {
        origin = origin_;
        destination = destination_;
        inbox = inbox_;
    }

    // ════════════════════════════════════════════════ ONLY INBOX ═════════════════════════════════════════════════════

    /// @inheritdoc IAgentManager
    // solhint-disable-next-line ordering
    function openDispute(uint32 guardIndex, uint32 notaryIndex) external onlyInbox {
        // Check that both agents are not in Dispute yet.
        if (_agentDispute[guardIndex].flag != DisputeFlag.None) revert GuardInDispute();
        if (_agentDispute[notaryIndex].flag != DisputeFlag.None) revert NotaryInDispute();
        _disputes.push(OpenedDispute(guardIndex, notaryIndex, 0));
        // Dispute is stored at length - 1, but we store the index + 1 to distinguish from "not in dispute".
        // TODO: check if we really need to use 88 bits for dispute indexes. Every dispute ends up in one of
        // the agents being slashed, so the number of disputes is limited by the number of agents (currently 2**32).
        // Thus we can do the unsafe cast to uint88.
        uint88 disputePtr = uint88(_disputes.length);
        _agentDispute[guardIndex] = AgentDispute(DisputeFlag.Pending, disputePtr, address(0));
        _agentDispute[notaryIndex] = AgentDispute(DisputeFlag.Pending, disputePtr, address(0));
        // Dispute index is length - 1. Note: report that initiated the dispute has the same index in `Inbox`.
        emit DisputeOpened({disputeIndex: disputePtr - 1, guardIndex: guardIndex, notaryIndex: notaryIndex});
        _notifyDisputeOpened(guardIndex, notaryIndex);
    }

    /// @inheritdoc IAgentManager
    function slashAgent(uint32 domain, address agent, address prover) external onlyInbox {
        _slashAgent(domain, agent, prover);
    }

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

    /// @inheritdoc IAgentManager
    function getAgent(uint256 index) external view returns (address agent, AgentStatus memory status) {
        agent = _getAgent(index);
        if (agent != address(0)) status = agentStatus(agent);
    }

    /// @inheritdoc IAgentManager
    function agentStatus(address agent) public view returns (AgentStatus memory status) {
        status = _storedAgentStatus(agent);
        // If agent was proven to commit fraud, but their slashing wasn't completed, return the Fraudulent flag.
        if (_agentDispute[_getIndex(agent)].flag == DisputeFlag.Slashed && status.flag != AgentFlag.Slashed) {
            status.flag = AgentFlag.Fraudulent;
        }
    }

    /// @inheritdoc IAgentManager
    function getDisputesAmount() external view returns (uint256) {
        return _disputes.length;
    }

    /// @inheritdoc IAgentManager
    function getDispute(uint256 index)
        external
        view
        returns (
            address guard,
            address notary,
            address slashedAgent,
            address fraudProver,
            bytes memory reportPayload,
            bytes memory reportSignature
        )
    {
        if (index >= _disputes.length) revert IndexOutOfRange();
        OpenedDispute memory dispute = _disputes[index];
        guard = _getAgent(dispute.guardIndex);
        notary = _getAgent(dispute.notaryIndex);
        if (dispute.slashedIndex > 0) {
            slashedAgent = _getAgent(dispute.slashedIndex);
            fraudProver = _agentDispute[dispute.slashedIndex].fraudProver;
        }
        (reportPayload, reportSignature) = IStatementInbox(inbox).getGuardReport(index);
    }

    /// @inheritdoc IAgentManager
    function disputeStatus(address agent)
        external
        view
        returns (DisputeFlag flag, address rival, address fraudProver, uint256 disputePtr)
    {
        uint256 agentIndex = _getIndex(agent);
        AgentDispute memory agentDispute = _agentDispute[agentIndex];
        flag = agentDispute.flag;
        fraudProver = agentDispute.fraudProver;
        disputePtr = agentDispute.disputePtr;
        if (disputePtr > 0) {
            OpenedDispute memory dispute = _disputes[disputePtr - 1];
            rival = _getAgent(dispute.guardIndex == agentIndex ? dispute.notaryIndex : dispute.guardIndex);
        }
    }

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

    /// @dev Hook that is called after agent was slashed in AgentManager and AgentSecured contracts were notified.
    // solhint-disable-next-line no-empty-blocks
    function _afterAgentSlashed(uint32 domain, address agent, address prover) internal virtual {}

    /// @dev Child contract should implement the logic for notifying AgentSecured contracts about the opened dispute.
    function _notifyDisputeOpened(uint32 guardIndex, uint32 notaryIndex) internal virtual;

    /// @dev Child contract should implement the logic for notifying AgentSecured contracts about the resolved dispute.
    function _notifyDisputeResolved(uint32 slashedIndex, uint32 rivalIndex) internal virtual;

    /// @dev Slashes the Agent and notifies the local Destination and Origin contracts about the slashed agent.
    /// Should be called when the agent fraud was confirmed.
    function _slashAgent(uint32 domain, address agent, address prover) internal {
        // Check that agent is Active/Unstaking and that the domains match
        AgentStatus memory status = _storedAgentStatus(agent);
        status.verifyActiveUnstaking();
        if (status.domain != domain) revert IncorrectAgentDomain();
        // The "stored" agent status is not updated yet, however agentStatus() will return AgentFlag.Fraudulent
        emit StatusUpdated(AgentFlag.Fraudulent, domain, agent);
        // This will revert if the agent has been slashed earlier
        _resolveDispute(status.index, prover);
        // Call "after slash" hook - this allows Bonding/Light Manager to add custom "after slash" logic
        _afterAgentSlashed(domain, agent, prover);
    }

    /// @dev Resolves a Dispute between a slashed Agent and their Rival (if there was one).
    function _resolveDispute(uint32 slashedIndex, address prover) internal {
        AgentDispute memory agentDispute = _agentDispute[slashedIndex];
        if (agentDispute.flag == DisputeFlag.Slashed) revert DisputeAlreadyResolved();
        agentDispute.flag = DisputeFlag.Slashed;
        agentDispute.fraudProver = prover;
        _agentDispute[slashedIndex] = agentDispute;
        // Check if there was a opened dispute with the slashed agent
        uint32 rivalIndex = 0;
        if (agentDispute.disputePtr != 0) {
            uint256 disputeIndex = agentDispute.disputePtr - 1;
            OpenedDispute memory dispute = _disputes[disputeIndex];
            _disputes[disputeIndex].slashedIndex = slashedIndex;
            // Clear the dispute status for the rival
            rivalIndex = dispute.notaryIndex == slashedIndex ? dispute.guardIndex : dispute.notaryIndex;
            delete _agentDispute[rivalIndex];
            emit DisputeResolved(disputeIndex, slashedIndex, rivalIndex, prover);
        }
        _notifyDisputeResolved(slashedIndex, rivalIndex);
    }

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

    /// @dev Generates leaf to be saved in the Agent Merkle Tree
    function _agentLeaf(AgentFlag flag, uint32 domain, address agent) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(flag, domain, agent));
    }

    /// @dev Returns the last known status for the agent from the Agent Merkle Tree.
    /// Note: the actual agent status (returned by `agentStatus()`) may differ, if agent fraud was proven.
    function _storedAgentStatus(address agent) internal view virtual returns (AgentStatus memory);

    /// @dev Returns agent address for the given index. Returns zero for non existing indexes.
    function _getAgent(uint256 index) internal view virtual returns (address);

    /// @dev Returns the index of the agent in the Agent Merkle Tree. Returns zero for non existing agents.
    function _getIndex(address agent) internal view virtual returns (uint256);
}