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