Skip to content

Commit

Permalink
Merge branch 'master' into fix/tob/11-dispute-collusion
Browse files Browse the repository at this point in the history
  • Loading branch information
ChiTimesChi committed Oct 31, 2023
2 parents 34d666a + 00e4cff commit fb00b44
Show file tree
Hide file tree
Showing 20 changed files with 532 additions and 116 deletions.
8 changes: 8 additions & 0 deletions packages/contracts-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## [1.0.22](https://github.com/synapsecns/sanguine/compare/@synapsecns/[email protected]...@synapsecns/[email protected]) (2023-10-30)

**Note:** Version bump only for package @synapsecns/contracts-core





## [1.0.21](https://github.com/synapsecns/sanguine/compare/@synapsecns/[email protected]...@synapsecns/[email protected]) (2023-10-27)

**Note:** Version bump only for package @synapsecns/contracts-core
Expand Down
18 changes: 18 additions & 0 deletions packages/contracts-core/contracts/events/AgentManagerEvents.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ abstract contract AgentManagerEvents {
*/
event RootUpdated(bytes32 newRoot);

/**
* @notice Emitted after the contract owner proposes a new agent root to resolve the stuck chain.
* @param newRoot New agent merkle root that was proposed
*/
event AgentRootProposed(bytes32 newRoot);

/**
* @notice Emitted after the contract owner cancels the previously proposed agent root.
* @param proposedRoot Agent merkle root that was proposed
*/
event ProposedAgentRootCancelled(bytes32 proposedRoot);

/**
* @notice Emitted after the contract owner resolves the previously proposed agent root.
* @param proposedRoot New agent merkle root that was resolved
*/
event ProposedAgentRootResolved(bytes32 proposedRoot);

/**
* @notice Emitted whenever a status of the agent is updated.
* @dev Only Active/Unstaking/Resting/Slashed flags could be stored in the Agent Merkle Tree.
Expand Down
13 changes: 0 additions & 13 deletions packages/contracts-core/contracts/interfaces/IAgentManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,6 @@ interface IAgentManager {
*/
function openDispute(uint32 guardIndex, uint32 notaryIndex) external;

/**
* @notice Allows contract owner to resolve a stuck Dispute.
* This could only be called if no fresh data has been submitted by the Notaries to the Inbox,
* which is required for the Dispute to be resolved naturally.
* > Will revert if any of these is true:
* > - Caller is not contract owner.
* > - Domain doesn't match the saved agent domain.
* > - `slashedAgent` is not in Dispute.
* > - Less than `FRESH_DATA_TIMEOUT` has passed since the last Notary submission to the Inbox.
* @param slashedAgent Agent that is being slashed
*/
function resolveStuckDispute(uint32 domain, address slashedAgent) external;

/**
* @notice Allows Inbox to slash an agent, if their fraud was proven.
* > Will revert if any of these is true:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ interface InterfaceBondingManager {
*/
function withdrawTips(address recipient, uint32 origin, uint256 amount) external;

/**
* @notice Allows contract owner to resolve a stuck Dispute.
* This could only be called if no fresh data has been submitted by the Notaries to the Inbox,
* which is required for the Dispute to be resolved naturally.
* > Will revert if any of these is true:
* > - Caller is not contract owner.
* > - Domain doesn't match the saved agent domain.
* > - `slashedAgent` is not in Dispute.
* > - Less than `FRESH_DATA_TIMEOUT` has passed since the last Notary submission to the Inbox.
* @param slashedAgent Agent that is being slashed
*/
function resolveDisputeWhenStuck(uint32 domain, address slashedAgent) external;

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

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,45 @@ interface InterfaceLightManager {
* @notice Updates the root of Agent Merkle Tree that the Light Manager is tracking.
* Could be only called by a local Destination contract, which is supposed to
* verify the attested Agent Merkle Roots.
* @param agentRoot New Agent Merkle Root
* @param agentRoot_ New Agent Merkle Root
*/
function setAgentRoot(bytes32 agentRoot) external;
function setAgentRoot(bytes32 agentRoot_) external;

/**
* @notice Allows contract owner to set the agent root to resolve the "stuck" chain
* by proposing the new agent root. The contract owner will be able to resolve the proposed
* agent root after a certain period of time.
* Note: this function could be called multiple times, each time the timer will be reset.
* This could only be called if no fresh data has been submitted by the Notaries to the Inbox,
* indicating that the chain is stuck for one of the reasons:
* - All active Notaries are in Dispute.
* - No active Notaries exist under the current agent root.
* @dev Will revert if any of the following conditions is met:
* - Caller is not the contract owner.
* - Agent root is empty.
* - The chain is not in a stuck state (has recently received a fresh data from the Notaries).
* @param agentRoot_ New Agent Merkle Root that is proposed to be set
*/
function proposeAgentRootWhenStuck(bytes32 agentRoot_) external;

/**
* @notice Allows contract owner to cancel the previously proposed agent root.
* @dev Will revert if any of the following conditions is met:
* - Caller is not the contract owner.
* - No agent root was proposed.
*/
function cancelProposedAgentRoot() external;

/**
* @notice Allows contract owner to resolve the previously proposed agent root.
* This will update the agent root, allowing the agents to update their status, effectively
* resolving the "stuck" chain.
* @dev Will revert if any of the following conditions is met:
* - Caller is not the contract owner.
* - No agent root was proposed.
* - Not enough time has passed since the agent root was proposed.
*/
function resolveProposedAgentRoot() external;

/**
* @notice Withdraws locked base message tips from local Origin to the recipient.
Expand All @@ -32,4 +68,13 @@ interface InterfaceLightManager {
function remoteWithdrawTips(uint32 msgOrigin, uint256 proofMaturity, address recipient, uint256 amount)
external
returns (bytes4 magicValue);

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

/**
* @notice Returns the latest proposed agent root and the timestamp when it was proposed.
* @dev Will return zero values if no agent root was proposed, or if the proposed agent root
* was already resolved.
*/
function proposedAgentRootData() external view returns (bytes32 agentRoot_, uint256 proposedAt_);
}
2 changes: 2 additions & 0 deletions packages/contracts-core/contracts/libs/Constants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ bytes32 constant STATE_INVALID_SALT = keccak256("STATE_INVALID_SALT");
// ═════════════════════════════════ PROTOCOL ══════════════════════════════════
/// @dev Optimistic period for new agent roots in LightManager
uint32 constant AGENT_ROOT_OPTIMISTIC_PERIOD = 1 days;
/// @dev Timeout between the agent root could be proposed and resolved in LightManager
uint32 constant AGENT_ROOT_PROPOSAL_TIMEOUT = 12 hours;
uint32 constant BONDING_OPTIMISTIC_PERIOD = 1 days;
/// @dev Amount of time that the Notary will not be considered active after they won a dispute
uint32 constant DISPUTE_TIMEOUT_NOTARY = 12 hours;
Expand Down
7 changes: 6 additions & 1 deletion packages/contracts-core/contracts/libs/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ error IncorrectAttestation();
error IncorrectAgentDomain();
error IncorrectAgentIndex();
error IncorrectAgentProof();
error IncorrectAgentRoot();
error IncorrectDataHash();
error IncorrectDestinationDomain();
error IncorrectOriginDomain();
Expand Down Expand Up @@ -74,9 +75,13 @@ error AgentNotFraudulent();
error AgentNotUnstaking();
error AgentUnknown();

error AgentRootNotProposed();
error AgentRootTimeoutNotOver();

error NotStuck();

error DisputeAlreadyResolved();
error DisputeNotOpened();
error DisputeNotStuck();
error DisputeTimeoutNotOver();
error GuardInDispute();
error NotaryInDispute();
Expand Down
28 changes: 10 additions & 18 deletions packages/contracts-core/contracts/manager/AgentManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ import {FRESH_DATA_TIMEOUT} from "../libs/Constants.sol";
import {
CallerNotInbox,
DisputeAlreadyResolved,
DisputeNotOpened,
DisputeNotStuck,
IncorrectAgentDomain,
IndexOutOfRange,
GuardInDispute,
NotaryInDispute
NotaryInDispute,
NotStuck
} from "../libs/Errors.sol";
import {AgentFlag, AgentStatus, DisputeFlag} from "../libs/Structures.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
Expand Down Expand Up @@ -65,6 +64,13 @@ abstract contract AgentManager is MessagingBase, AgentManagerEvents, IAgentManag
_;
}

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
Expand All @@ -74,24 +80,10 @@ abstract contract AgentManager is MessagingBase, AgentManagerEvents, IAgentManag
inbox = inbox_;
}

// ════════════════════════════════════════════════ ONLY OWNER ═════════════════════════════════════════════════════

/// @inheritdoc IAgentManager
// solhint-disable-next-line ordering
function resolveStuckDispute(uint32 domain, address slashedAgent) external onlyOwner {
AgentDispute memory slashedDispute = _agentDispute[_getIndex(slashedAgent)];
if (slashedDispute.flag == DisputeFlag.None) revert DisputeNotOpened();
if (slashedDispute.flag == DisputeFlag.Slashed) revert DisputeAlreadyResolved();
// 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 DisputeNotStuck();
// This will revert if domain doesn't match the agent's domain.
_slashAgent({domain: domain, agent: slashedAgent, prover: address(0)});
}

// ════════════════════════════════════════════════ 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();
Expand Down
15 changes: 14 additions & 1 deletion packages/contracts-core/contracts/manager/BondingManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
AgentCantBeAdded,
CallerNotDestination,
CallerNotSummit,
DisputeAlreadyResolved,
DisputeNotOpened,
IncorrectAgentDomain,
IncorrectOriginDomain,
IndexOutOfRange,
Expand All @@ -15,7 +17,7 @@ import {
SynapseDomainForbidden
} from "../libs/Errors.sol";
import {DynamicTree, MerkleMath} from "../libs/merkle/MerkleTree.sol";
import {AgentFlag, AgentStatus} from "../libs/Structures.sol";
import {AgentFlag, AgentStatus, DisputeFlag} from "../libs/Structures.sol";
// ═════════════════════════════ INTERNAL IMPORTS ══════════════════════════════
import {AgentManager, IAgentManager} from "./AgentManager.sol";
import {MessagingBase} from "../base/MessagingBase.sol";
Expand Down Expand Up @@ -147,6 +149,17 @@ contract BondingManager is AgentManager, InterfaceBondingManager {
_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
Expand Down
59 changes: 58 additions & 1 deletion packages/contracts-core/contracts/manager/LightManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@
pragma solidity 0.8.17;

// ══════════════════════════════ LIBRARY IMPORTS ══════════════════════════════
import {AGENT_TREE_HEIGHT, BONDING_OPTIMISTIC_PERIOD} from "../libs/Constants.sol";
import {
AGENT_ROOT_PROPOSAL_TIMEOUT,
AGENT_TREE_HEIGHT,
BONDING_OPTIMISTIC_PERIOD,
FRESH_DATA_TIMEOUT
} from "../libs/Constants.sol";
import {
AgentRootNotProposed,
AgentRootTimeoutNotOver,
IncorrectAgentIndex,
IncorrectAgentProof,
IncorrectAgentRoot,
CallerNotDestination,
MustBeSynapseDomain,
NotStuck,
SynapseDomainForbidden,
WithdrawTipsOptimisticPeriod
} from "../libs/Errors.sol";
Expand All @@ -18,6 +27,7 @@ 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 {InterfaceDestination} from "../interfaces/InterfaceDestination.sol";
import {InterfaceLightManager} from "../interfaces/InterfaceLightManager.sol";
import {InterfaceOrigin} from "../interfaces/InterfaceOrigin.sol";

Expand All @@ -33,6 +43,12 @@ contract LightManager is AgentManager, InterfaceLightManager {
/// @inheritdoc IAgentManager
bytes32 public agentRoot;

/// @dev Pending Agent Merkle Root that was proposed by the contract owner.
bytes32 internal _proposedAgentRoot;

/// @dev Timestamp when the Agent Merkle Root was proposed by the contract owner.
uint256 internal _agentRootProposedAt;

// (agentRoot => (agent => status))
mapping(bytes32 => mapping(address => AgentStatus)) private _agentMap;

Expand All @@ -53,6 +69,40 @@ contract LightManager is AgentManager, InterfaceLightManager {
__Ownable2Step_init();
}

// ════════════════════════════════════════════════ OWNER ONLY ═════════════════════════════════════════════════════

/// @inheritdoc InterfaceLightManager
function proposeAgentRootWhenStuck(bytes32 agentRoot_) external onlyOwner onlyWhenStuck {
if (agentRoot_ == 0) revert IncorrectAgentRoot();
// Update the proposed agent root, clear the timer if the root is empty
_proposedAgentRoot = agentRoot_;
_agentRootProposedAt = block.timestamp;
emit AgentRootProposed(agentRoot_);
}

/// @inheritdoc InterfaceLightManager
function cancelProposedAgentRoot() external onlyOwner {
bytes32 cancelledAgentRoot = _proposedAgentRoot;
if (cancelledAgentRoot == 0) revert AgentRootNotProposed();
_proposedAgentRoot = 0;
_agentRootProposedAt = 0;
emit ProposedAgentRootCancelled(cancelledAgentRoot);
}

/// @inheritdoc InterfaceLightManager
/// @dev Should proceed with the proposed root, even if new Notary data is available.
/// This is done to prevent rogue Notaries from going offline and then
/// indefinitely blocking the agent root resolution, thus `onlyWhenStuck` modifier is not used here.
function resolveProposedAgentRoot() external onlyOwner {
bytes32 newAgentRoot = _proposedAgentRoot;
if (newAgentRoot == 0) revert AgentRootNotProposed();
if (block.timestamp < _agentRootProposedAt + AGENT_ROOT_PROPOSAL_TIMEOUT) revert AgentRootTimeoutNotOver();
_setAgentRoot(newAgentRoot);
_proposedAgentRoot = 0;
_agentRootProposedAt = 0;
emit ProposedAgentRootResolved(newAgentRoot);
}

// ═══════════════════════════════════════════════ AGENTS LOGIC ════════════════════════════════════════════════════

/// @inheritdoc InterfaceLightManager
Expand Down Expand Up @@ -105,6 +155,13 @@ contract LightManager is AgentManager, InterfaceLightManager {
return this.remoteWithdrawTips.selector;
}

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

/// @inheritdoc InterfaceLightManager
function proposedAgentRootData() external view returns (bytes32 agentRoot_, uint256 proposedAt_) {
return (_proposedAgentRoot, _agentRootProposedAt);
}

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

function _afterAgentSlashed(uint32 domain, address agent, address prover) internal virtual override {
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@synapsecns/contracts-core",
"version": "1.0.21",
"version": "1.0.22",
"description": "",
"scripts": {
"build": "yarn build:contracts && yarn build:typescript && yarn build:go",
Expand Down
Loading

0 comments on commit fb00b44

Please sign in to comment.