// SPDX-License-Identifier: MIT pragma solidity 0.8.17; // ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════ import {BONDING_OPTIMISTIC_PERIOD} from "../libs/Constants.sol"; import { AgentCantBeAdded, CallerNotDestination, CallerNotSummit, DisputeAlreadyResolved, DisputeNotOpened, IncorrectAgentDomain, IncorrectOriginDomain, IndexOutOfRange, MustBeSynapseDomain, SlashAgentOptimisticPeriod, SynapseDomainForbidden } from "../libs/Errors.sol"; import {DynamicTree, MerkleMath} from "../libs/merkle/MerkleTree.sol"; import {AgentFlag, AgentStatus, DisputeFlag} from "../libs/Structures.sol"; // ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════ import {AgentManager, IAgentManager} from "./AgentManager.sol"; import {MessagingBase} from "../base/MessagingBase.sol"; import {IAgentSecured} from "../interfaces/IAgentSecured.sol"; import {InterfaceBondingManager} from "../interfaces/InterfaceBondingManager.sol"; import {InterfaceLightManager} from "../interfaces/InterfaceLightManager.sol"; import {InterfaceOrigin} from "../interfaces/InterfaceOrigin.sol"; // ═════════════════════════════ EXTERNAL IMPORTS ══════════════════════════════ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol"; /// @notice BondingManager keeps track of all existing agents on the Synapse Chain. /// It utilizes a dynamic Merkle Tree to store the agent information. This enables passing only the /// latest merkle root of this tree (referenced as the Agent Merkle Root) to the remote chains, /// so that the agents could "register" themselves by proving their current status against this root. /// `BondingManager` is responsible for the following: /// - Keeping track of all existing agents, as well as their statuses. In the MVP version there is no token staking, /// which will be added in the future. Nonetheless, the agent statuses are still stored in the Merkle Tree, and /// the agent slashing is still possible, though with no reward/penalty for the reporter/reported. /// - Marking agents as "ready to be slashed" once their fraud is proven on the local or remote chain. Anyone could /// complete the slashing by providing the proof of the current agent status against the current Agent Merkle Root. /// - Sending Manager Message to remote `LightManager` to withdraw collected tips from the remote chain. /// - Accepting Manager Message from remote `LightManager` to slash agents on the Synapse Chain, when their fraud /// is proven on the remote chain. contract BondingManager is AgentManager, InterfaceBondingManager { using SafeCast for uint256; // ══════════════════════════════════════════════════ STORAGE ══════════════════════════════════════════════════════ // The address of the Summit contract. address public summit; // (agent => their status) mapping(address => AgentStatus) private _agentMap; // (domain => past and current agents for domain) mapping(uint32 => address[]) private _domainAgents; // A list of all agent accounts. First entry is address(0) to make agent indexes start from 1. address[] private _agents; // Merkle Tree for Agents. // leafs[0] = 0 // leafs[index > 0] = keccak(agentFlag, domain, _agents[index]) DynamicTree private _agentTree; // ═════════════════════════════════════════ CONSTRUCTOR & INITIALIZER ═════════════════════════════════════════════ constructor(uint32 synapseDomain_) MessagingBase("0.0.3", synapseDomain_) { if (localDomain != synapseDomain) revert MustBeSynapseDomain(); } function initialize(address origin_, address destination_, address inbox_, address summit_) external initializer { __AgentManager_init(origin_, destination_, inbox_); summit = summit_; __Ownable2Step_init(); // Insert a zero address to make indexes for Agents start from 1. // Zeroed index is supposed to be used as a sentinel value meaning "no agent". _agents.push(address(0)); } // ════════════════════════════════════════════ AGENTS LOGIC (MVP) ═════════════════════════════════════════════════ // TODO: remove these MVP functions once token staking is implemented /// @inheritdoc InterfaceBondingManager function addAgent(uint32 domain, address agent, bytes32[] memory proof) external onlyOwner { if (domain == synapseDomain) revert SynapseDomainForbidden(); // Check the STORED status of the added agent in the merkle tree AgentStatus memory status = _storedAgentStatus(agent); // Agent index in `_agents` uint32 index; // Leaf representing currently saved agent information in the tree bytes32 oldValue; if (status.flag == AgentFlag.Unknown) { // Unknown address could be added to any domain // New agent will need to be added to `_agents` list: could not have more than 2**32 agents // TODO: consider using more than 32 bits for agent indexes index = _agents.length.toUint32(); // Current leaf for index is bytes32(0), which is already assigned to `leaf` _agents.push(agent); _domainAgents[domain].push(agent); } else if (status.flag == AgentFlag.Resting && status.domain == domain) { // Resting agent could be only added back to the same domain // Agent is already in `_agents`, fetch the saved index index = status.index; // Generate the current leaf for the agent // oldValue includes the domain information, so we didn't had to check it above. // However, we are still doing this check to have a more appropriate revert string, // if a resting agent is requesting to be added to another domain. oldValue = _agentLeaf(AgentFlag.Resting, domain, agent); } else { // Any other flag indicates that agent could not be added revert AgentCantBeAdded(); } // This will revert if the proof for the old value is incorrect _updateLeaf(oldValue, proof, AgentStatus(AgentFlag.Active, domain, index), agent); } /// @inheritdoc InterfaceBondingManager function initiateUnstaking(uint32 domain, address agent, bytes32[] memory proof) external onlyOwner { // Check the CURRENT status of the unstaking agent AgentStatus memory status = agentStatus(agent); // Could only initiate the unstaking for the active agent for the domain status.verifyActive(); if (status.domain != domain) revert IncorrectAgentDomain(); // Leaf representing currently saved agent information in the tree. // oldValue includes the domain information, so we didn't had to check it above. // However, we are still doing this check to have a more appropriate revert string, // if an agent is initiating the unstaking, but specifies incorrect domain. bytes32 oldValue = _agentLeaf(AgentFlag.Active, domain, agent); // This will revert if the proof for the old value is incorrect _updateLeaf(oldValue, proof, AgentStatus(AgentFlag.Unstaking, domain, status.index), agent); } /// @inheritdoc InterfaceBondingManager function completeUnstaking(uint32 domain, address agent, bytes32[] memory proof) external onlyOwner { // Check the CURRENT status of the unstaking agent AgentStatus memory status = agentStatus(agent); // Could only complete the unstaking, if it was previously initiated // TODO: add more checks (time-based, possibly collecting info from other chains) status.verifyUnstaking(); if (status.domain != domain) revert IncorrectAgentDomain(); // Leaf representing currently saved agent information in the tree // oldValue includes the domain information, so we didn't had to check it above. // However, we are still doing this check to have a more appropriate revert string, // if an agent is completing the unstaking, but specifies incorrect domain. bytes32 oldValue = _agentLeaf(AgentFlag.Unstaking, domain, agent); // This will revert if the proof for the old value is incorrect _updateLeaf(oldValue, proof, AgentStatus(AgentFlag.Resting, domain, status.index), agent); } // ════════════════════════════════════════════════ ONLY OWNER ═════════════════════════════════════════════════════ /// @inheritdoc InterfaceBondingManager function resolveDisputeWhenStuck(uint32 domain, address slashedAgent) external onlyOwner onlyWhenStuck { AgentDispute memory slashedDispute = _agentDispute[_getIndex(slashedAgent)]; if (slashedDispute.flag == DisputeFlag.None) revert DisputeNotOpened(); if (slashedDispute.flag == DisputeFlag.Slashed) revert DisputeAlreadyResolved(); // This will revert if domain doesn't match the agent's domain. _slashAgent({domain: domain, agent: slashedAgent, prover: address(0)}); } // ══════════════════════════════════════════════ SLASHING LOGIC ═══════════════════════════════════════════════════ /// @inheritdoc InterfaceBondingManager function completeSlashing(uint32 domain, address agent, bytes32[] memory proof) external { // Check the CURRENT status of the unstaking agent AgentStatus memory status = agentStatus(agent); // Could only complete the slashing, if it was previously initiated status.verifyFraudulent(); if (status.domain != domain) revert IncorrectAgentDomain(); // Leaf representing currently saved agent information in the tree // oldValue includes the domain information, so we didn't had to check it above. // However, we are still doing this check to have a more appropriate revert string, // if anyone is completing the slashing, but specifies incorrect domain. bytes32 oldValue = _getLeaf(agent); // This will revert if the proof for the old value is incorrect _updateLeaf(oldValue, proof, AgentStatus(AgentFlag.Slashed, domain, status.index), agent); } /// @inheritdoc InterfaceBondingManager function remoteSlashAgent(uint32 msgOrigin, uint256 proofMaturity, uint32 domain, address agent, address prover) external returns (bytes4 magicValue) { // Only destination can pass Manager Messages if (msg.sender != destination) revert CallerNotDestination(); // Check that merkle proof is mature enough // TODO: separate constant for slashing optimistic period if (proofMaturity < BONDING_OPTIMISTIC_PERIOD) revert SlashAgentOptimisticPeriod(); // TODO: do we need to save domain where the agent was slashed? // Message needs to be sent from the remote chain if (msgOrigin == localDomain) revert IncorrectOriginDomain(); // Slash agent and notify local AgentSecured contracts _slashAgent(domain, agent, prover); // Magic value to return is selector of the called function return this.remoteSlashAgent.selector; } // ════════════════════════════════════════════════ TIPS LOGIC ═════════════════════════════════════════════════════ /// @inheritdoc InterfaceBondingManager function withdrawTips(address recipient, uint32 origin_, uint256 amount) external { // Only Summit can withdraw tips if (msg.sender != summit) revert CallerNotSummit(); if (origin_ == localDomain) { // Call local Origin to withdraw tips InterfaceOrigin(address(origin)).withdrawTips(recipient, amount); } else { // For remote chains: send a manager message to remote LightManager to handle the withdrawal // remoteWithdrawTips(msgOrigin, proofMaturity, recipient, amount) with the first two security args omitted InterfaceOrigin(origin).sendManagerMessage({ destination: origin_, optimisticPeriod: BONDING_OPTIMISTIC_PERIOD, payload: abi.encodeWithSelector(InterfaceLightManager.remoteWithdrawTips.selector, recipient, amount) }); } } // ═══════════════════════════════════════════════════ VIEWS ═══════════════════════════════════════════════════════ /// @inheritdoc IAgentManager function agentRoot() external view override returns (bytes32) { return _agentTree.root; } /// @inheritdoc InterfaceBondingManager function getActiveAgents(uint32 domain) external view returns (address[] memory agents) { uint256 amount = _domainAgents[domain].length; agents = new address[](amount); uint256 activeAgents = 0; for (uint256 i = 0; i < amount; ++i) { address agent = _domainAgents[domain][i]; if (agentStatus(agent).flag == AgentFlag.Active) { agents[activeAgents++] = agent; } } if (activeAgents != amount) { // Shrink the returned array by storing the required length in memory // solhint-disable-next-line no-inline-assembly assembly { mstore(agents, activeAgents) } } } /// @inheritdoc InterfaceBondingManager function agentLeaf(address agent) external view returns (bytes32 leaf) { return _getLeaf(agent); } /// @inheritdoc InterfaceBondingManager function leafsAmount() external view returns (uint256 amount) { return _agents.length; } /// @inheritdoc InterfaceBondingManager function getProof(address agent) external view returns (bytes32[] memory proof) { bytes32[] memory leafs = allLeafs(); // Use the STORED agent status from the merkle tree AgentStatus memory status = _storedAgentStatus(agent); // Use next available index for unknown agents uint256 index = status.flag == AgentFlag.Unknown ? _agents.length : status.index; return MerkleMath.calculateProof(leafs, index); } /// @inheritdoc InterfaceBondingManager function allLeafs() public view returns (bytes32[] memory leafs) { return getLeafs(0, _agents.length); } /// @inheritdoc InterfaceBondingManager function getLeafs(uint256 indexFrom, uint256 amount) public view returns (bytes32[] memory leafs) { uint256 amountTotal = _agents.length; if (indexFrom >= amountTotal) revert IndexOutOfRange(); if (indexFrom + amount > amountTotal) { amount = amountTotal - indexFrom; } leafs = new bytes32[](amount); for (uint256 i = 0; i < amount; ++i) { leafs[i] = _getLeaf(indexFrom + i); } } // ══════════════════════════════════════════════ INTERNAL LOGIC ═══════════════════════════════════════════════════ /// @dev Updates value in the Agent Merkle Tree to reflect the `newStatus`. /// Will revert, if supplied proof for the old value is incorrect. function _updateLeaf(bytes32 oldValue, bytes32[] memory proof, AgentStatus memory newStatus, address agent) internal { // New leaf value for the agent in the Agent Merkle Tree bytes32 newValue = _agentLeaf(newStatus.flag, newStatus.domain, agent); // This will revert if the proof for the old value is incorrect bytes32 newRoot = _agentTree.update(newStatus.index, oldValue, proof, newValue); _agentMap[agent] = newStatus; emit StatusUpdated(newStatus.flag, newStatus.domain, agent); emit RootUpdated(newRoot); } /// @dev Notify local AgentSecured contracts about the opened dispute. function _notifyDisputeOpened(uint32 guardIndex, uint32 notaryIndex) internal override { IAgentSecured(destination).openDispute(guardIndex, notaryIndex); IAgentSecured(summit).openDispute(guardIndex, notaryIndex); } /// @dev Notify local AgentSecured contracts about the resolved dispute. function _notifyDisputeResolved(uint32 slashedIndex, uint32 rivalIndex) internal override { IAgentSecured(destination).resolveDispute(slashedIndex, rivalIndex); IAgentSecured(summit).resolveDispute(slashedIndex, rivalIndex); } // ══════════════════════════════════════════════ INTERNAL VIEWS ═══════════════════════════════════════════════════ /// @dev Returns the status of the agent. function _storedAgentStatus(address agent) internal view override returns (AgentStatus memory) { return _agentMap[agent]; } /// @dev Returns agent address for the given index. Returns zero for non existing indexes. function _getAgent(uint256 index) internal view override returns (address agent) { if (index < _agents.length) { agent = _agents[index]; } } /// @dev Returns the index of the agent in the Agent Merkle Tree. Returns zero for non existing agents. function _getIndex(address agent) internal view override returns (uint256 index) { return _agentMap[agent].index; } /// @dev Returns the current leaf representing agent in the Agent Merkle Tree. function _getLeaf(address agent) internal view returns (bytes32 leaf) { // Get the agent status STORED in the merkle tree AgentStatus memory status = _storedAgentStatus(agent); if (status.flag != AgentFlag.Unknown) { return _agentLeaf(status.flag, status.domain, agent); } // Return empty leaf for unknown _agents } /// @dev Returns a leaf from the Agent Merkle Tree with a given index. function _getLeaf(uint256 index) internal view returns (bytes32 leaf) { if (index != 0) { return _getLeaf(_agents[index]); } // Return empty leaf for a zero index } }