From f75a32b80b08ce7764b7b7446a14a3d9fa304499 Mon Sep 17 00:00:00 2001 From: ChiTimesChi Date: Mon, 30 Oct 2023 12:21:19 +0000 Subject: [PATCH 1/4] Publish - @synapsecns/contracts-core@1.0.22 --- packages/contracts-core/CHANGELOG.md | 8 ++++++++ packages/contracts-core/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/contracts-core/CHANGELOG.md b/packages/contracts-core/CHANGELOG.md index 4a762160e9..ac7e6aa6ef 100644 --- a/packages/contracts-core/CHANGELOG.md +++ b/packages/contracts-core/CHANGELOG.md @@ -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/contracts-core@1.0.21...@synapsecns/contracts-core@1.0.22) (2023-10-30) + +**Note:** Version bump only for package @synapsecns/contracts-core + + + + + ## [1.0.21](https://github.com/synapsecns/sanguine/compare/@synapsecns/contracts-core@1.0.20...@synapsecns/contracts-core@1.0.21) (2023-10-27) **Note:** Version bump only for package @synapsecns/contracts-core diff --git a/packages/contracts-core/package.json b/packages/contracts-core/package.json index bbb5fe4c03..dc0fde9dfb 100644 --- a/packages/contracts-core/package.json +++ b/packages/contracts-core/package.json @@ -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", From 47aed7a7f4ab21d9f5897d1b4d47c5a6d22432a1 Mon Sep 17 00:00:00 2001 From: Moses <103143573+Defi-Moses@users.noreply.github.com> Date: Tue, 31 Oct 2023 17:50:47 +0000 Subject: [PATCH 2/4] Adds sUSD and all other CCTP enabled assets and Fallback (#1524) * Adds SUSD and all other CCTP enables assets as well as fallback for decimals and images * added error catching for missing decimals --- packages/explorer-ui/assets/icons/susd.svg | 1 + .../components/misc/IconAndAmount.tsx | 8 +++++++- packages/explorer-ui/constants/tokens/basic.ts | 18 ++++++++++++++++++ .../explorer-ui/utils/addressToDecimals.ts | 13 +++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 packages/explorer-ui/assets/icons/susd.svg create mode 100644 packages/explorer-ui/utils/addressToDecimals.ts diff --git a/packages/explorer-ui/assets/icons/susd.svg b/packages/explorer-ui/assets/icons/susd.svg new file mode 100644 index 0000000000..8794225fcd --- /dev/null +++ b/packages/explorer-ui/assets/icons/susd.svg @@ -0,0 +1 @@ + diff --git a/packages/explorer-ui/components/misc/IconAndAmount.tsx b/packages/explorer-ui/components/misc/IconAndAmount.tsx index cd0f29fa93..5da344d429 100644 --- a/packages/explorer-ui/components/misc/IconAndAmount.tsx +++ b/packages/explorer-ui/components/misc/IconAndAmount.tsx @@ -3,6 +3,7 @@ import { formatAmount } from '@utils/formatAmount' import { AssetImage } from '@components/misc/AssetImage' import { addressToSymbol } from '@utils/addressToSymbol' import { TOKEN_HASH_MAP } from '@constants/tokens/basic' +import { addressToDecimals } from '@utils/addressToDecimals' export function IconAndAmount({ formattedValue, @@ -24,14 +25,19 @@ export function IconAndAmount({ styledCoinClass = t && `${getCoinTextColor(t)} ${textSize}` } + let amount let showToken if (tokenSymbol) { const displaySymbol = addressToSymbol({ tokenAddress, chainId }) showToken =
{displaySymbol}
+ amount = formattedValue } else { const displaySymbol = addressToSymbol({ tokenAddress, chainId }) showToken = displaySymbol ?
{displaySymbol}
: -- + const dec = 10**addressToDecimals({tokenAddress, chainId}) + amount = formattedValue / (dec/10**6) } + return (
@@ -41,7 +47,7 @@ export function IconAndAmount({ className={`${iconSize} inline mr-1 rounded-lg hover:opacity-[0.8] transition-all ease-in-out`} />
- {formatAmount(formattedValue)} + {formatAmount(amount)}
{showToken} diff --git a/packages/explorer-ui/constants/tokens/basic.ts b/packages/explorer-ui/constants/tokens/basic.ts index 08d51ef58b..d76d5e1123 100644 --- a/packages/explorer-ui/constants/tokens/basic.ts +++ b/packages/explorer-ui/constants/tokens/basic.ts @@ -22,6 +22,7 @@ import pepeLogo from '@assets/icons/pepe.webp' import maticLogo from '@assets/icons/matic.svg' import crvusdLogo from '@assets/icons/crvusd.svg' import ftmLogo from '@assets/icons/ftm.svg' +import susdLogo from '@assets/icons/susd.svg' import { ChainId } from '@constants/networks' import { Token } from '@utils/classes/Token' import { @@ -277,6 +278,7 @@ export const DAI = new Token({ [ChainId.BOBA]: '0xf74195Bb8a5cf652411867c5C2C5b8C2a402be35', [ChainId.KLAYTN]: '0x078dB7827a5531359f6CB63f62CFA20183c4F10c', [ChainId.BASE]: '0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb', + [ChainId.OPTIMISM]: '0xda10009cbd5d07dd0cecc66161fc93d7c9000da1', }, decimals: 18, symbol: 'DAI', @@ -285,6 +287,17 @@ export const DAI = new Token({ swapableType: 'USD', }) +export const SUSD = new Token({ + addresses: { + [ChainId.OPTIMISM]: '0x8c6f28f2F1A3C87F0f938b96d27520d9751ec8d9', + }, + decimals: 18, + symbol: 'sUSD', + name: 'Synth USD', + logo: susdLogo, + swapableType: 'USD', +}) + export const CRVUSD = new Token({ addresses: { [ChainId.BASE]: '0x417ac0e078398c154edfadd9ef675d30be60af93', @@ -535,6 +548,7 @@ export const FRAX = new Token({ [ChainId.MOONBEAM]: '0xDd47A348AB60c61Ad6B60cA8C31ea5e00eBfAB4F', [ChainId.HARMONY]: '0x1852F70512298d56e9c8FDd905e02581E04ddb2a', [ChainId.DOGECHAIN]: '0x10D70831f9C3c11c5fe683b2f1Be334503880DB6', + [ChainId.ARBITRUM]: '0x17fc002b466eec40dae837fc4be5c67993ddbd6f', }, decimals: 18, symbol: 'FRAX', @@ -1050,11 +1064,13 @@ export const BASIC_TOKENS_BY_CHAIN = { AGEUR, UNIDX, PEPE, + FRAX, ], [ChainId.AVALANCHE]: [ USDC, USDT, CCTP_USDC, + // Note that this is Dai.e on Avalanche DAI, WETHE, NETH, @@ -1132,6 +1148,8 @@ export const BASIC_TOKENS_BY_CHAIN = { USDT, CCTP_USDC, UNIDX, + SUSD, + DAI, ], [ChainId.TERRA]: [UST], [ChainId.CRONOS]: [ diff --git a/packages/explorer-ui/utils/addressToDecimals.ts b/packages/explorer-ui/utils/addressToDecimals.ts new file mode 100644 index 0000000000..cb10ab7c13 --- /dev/null +++ b/packages/explorer-ui/utils/addressToDecimals.ts @@ -0,0 +1,13 @@ +import { TOKEN_HASH_MAP } from '@constants/tokens/basic' + +export function addressToDecimals({ tokenAddress, chainId }) { + let decimals = + tokenAddress && + chainId && + TOKEN_HASH_MAP[chainId][tokenAddress.toLowerCase()]?.decimals[chainId] + + if (decimals === undefined) { + decimals = 18 + } + return decimals +} From 4e12b934ba403aed10a8188603782a3552f18ca2 Mon Sep 17 00:00:00 2001 From: aureliusbtc Date: Tue, 31 Oct 2023 17:55:05 +0000 Subject: [PATCH 3/4] Publish - @synapsecns/explorer-ui@0.1.24 --- packages/explorer-ui/CHANGELOG.md | 8 ++++++++ packages/explorer-ui/package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/explorer-ui/CHANGELOG.md b/packages/explorer-ui/CHANGELOG.md index 6b032464c1..39abc367aa 100644 --- a/packages/explorer-ui/CHANGELOG.md +++ b/packages/explorer-ui/CHANGELOG.md @@ -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. +## [0.1.24](https://github.com/synapsecns/sanguine/compare/@synapsecns/explorer-ui@0.1.23...@synapsecns/explorer-ui@0.1.24) (2023-10-31) + +**Note:** Version bump only for package @synapsecns/explorer-ui + + + + + ## [0.1.23](https://github.com/synapsecns/sanguine/compare/@synapsecns/explorer-ui@0.1.22...@synapsecns/explorer-ui@0.1.23) (2023-10-27) **Note:** Version bump only for package @synapsecns/explorer-ui diff --git a/packages/explorer-ui/package.json b/packages/explorer-ui/package.json index 5df9eca8db..7c3f935fb1 100644 --- a/packages/explorer-ui/package.json +++ b/packages/explorer-ui/package.json @@ -1,6 +1,6 @@ { "name": "@synapsecns/explorer-ui", - "version": "0.1.23", + "version": "0.1.24", "private": true, "engines": { "node": ">=16.0.0" From 00e4cffd9db294025a1d062503e0d267821e0879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=CF=87=C2=B2?= <88190723+ChiTimesChi@users.noreply.github.com> Date: Wed, 1 Nov 2023 00:31:58 +0300 Subject: [PATCH 4/4] Fix: ToB-05 (destination chain agent root) (#1445) * Initial draft for `setAgentRootWhenStuck` * Add tests for the expected behavior * Add "fresh data" check * Introduce `onlyWhenStuck` modifier * Move `resolveStuckDispute` to BondingManager * Adjust tests * Use consistent naming * Add interface for the two-step proposing of agent root * Define new events, errors and constants * Add tests with the expected behavior * Add tests/note wrt cancelling proposed agent root * Implement two-step proposal * Remove single-step root proposal * Update the interface: separate func for cancelling * Modify events, add new error * Add more tests to cover expected behavior * Don't allow empty root in proposeAgentRoot * Implement cancelling * Add agent root checks in tests * Fix: cache the cancelled root * Remote the extra lib --- .../contracts/events/AgentManagerEvents.sol | 18 ++ .../contracts/interfaces/IAgentManager.sol | 13 - .../interfaces/InterfaceBondingManager.sol | 13 + .../interfaces/InterfaceLightManager.sol | 49 +++- .../contracts/libs/Constants.sol | 2 + .../contracts-core/contracts/libs/Errors.sol | 7 +- .../contracts/manager/AgentManager.sol | 28 +-- .../contracts/manager/BondingManager.sol | 15 +- .../contracts/manager/LightManager.sol | 59 ++++- .../test/suite/manager/AgentManager.t.sol | 75 ------ .../test/suite/manager/BondingManager.t.sol | 77 +++++- .../test/suite/manager/LightManager.t.sol | 232 ++++++++++++++++++ 12 files changed, 475 insertions(+), 113 deletions(-) diff --git a/packages/contracts-core/contracts/events/AgentManagerEvents.sol b/packages/contracts-core/contracts/events/AgentManagerEvents.sol index 3fdaaabf58..abb40b5452 100644 --- a/packages/contracts-core/contracts/events/AgentManagerEvents.sol +++ b/packages/contracts-core/contracts/events/AgentManagerEvents.sol @@ -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. diff --git a/packages/contracts-core/contracts/interfaces/IAgentManager.sol b/packages/contracts-core/contracts/interfaces/IAgentManager.sol index 6fe81c0045..b3322e5dfc 100644 --- a/packages/contracts-core/contracts/interfaces/IAgentManager.sol +++ b/packages/contracts-core/contracts/interfaces/IAgentManager.sol @@ -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: diff --git a/packages/contracts-core/contracts/interfaces/InterfaceBondingManager.sol b/packages/contracts-core/contracts/interfaces/InterfaceBondingManager.sol index f168998127..6624efc093 100644 --- a/packages/contracts-core/contracts/interfaces/InterfaceBondingManager.sol +++ b/packages/contracts-core/contracts/interfaces/InterfaceBondingManager.sol @@ -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 ═══════════════════════════════════════════════════════ /** diff --git a/packages/contracts-core/contracts/interfaces/InterfaceLightManager.sol b/packages/contracts-core/contracts/interfaces/InterfaceLightManager.sol index 06f6f51e48..6f2f19d8e9 100644 --- a/packages/contracts-core/contracts/interfaces/InterfaceLightManager.sol +++ b/packages/contracts-core/contracts/interfaces/InterfaceLightManager.sol @@ -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. @@ -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_); } diff --git a/packages/contracts-core/contracts/libs/Constants.sol b/packages/contracts-core/contracts/libs/Constants.sol index 8eba0ea593..f5039df183 100644 --- a/packages/contracts-core/contracts/libs/Constants.sol +++ b/packages/contracts-core/contracts/libs/Constants.sol @@ -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 without fresh data from Notaries before contract owner can resolve stuck disputes manually uint256 constant FRESH_DATA_TIMEOUT = 4 hours; diff --git a/packages/contracts-core/contracts/libs/Errors.sol b/packages/contracts-core/contracts/libs/Errors.sol index 02f3ab8ef7..0b9b2d3b85 100644 --- a/packages/contracts-core/contracts/libs/Errors.sol +++ b/packages/contracts-core/contracts/libs/Errors.sol @@ -14,6 +14,7 @@ error IncorrectAttestation(); error IncorrectAgentDomain(); error IncorrectAgentIndex(); error IncorrectAgentProof(); +error IncorrectAgentRoot(); error IncorrectDataHash(); error IncorrectDestinationDomain(); error IncorrectOriginDomain(); @@ -74,9 +75,13 @@ error AgentNotFraudulent(); error AgentNotUnstaking(); error AgentUnknown(); +error AgentRootNotProposed(); +error AgentRootTimeoutNotOver(); + +error NotStuck(); + error DisputeAlreadyResolved(); error DisputeNotOpened(); -error DisputeNotStuck(); error GuardInDispute(); error NotaryInDispute(); diff --git a/packages/contracts-core/contracts/manager/AgentManager.sol b/packages/contracts-core/contracts/manager/AgentManager.sol index d027737170..96cc1d98f1 100644 --- a/packages/contracts-core/contracts/manager/AgentManager.sol +++ b/packages/contracts-core/contracts/manager/AgentManager.sol @@ -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 ══════════════════════════════ @@ -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 @@ -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(); diff --git a/packages/contracts-core/contracts/manager/BondingManager.sol b/packages/contracts-core/contracts/manager/BondingManager.sol index 72805a8579..ae005f73ca 100644 --- a/packages/contracts-core/contracts/manager/BondingManager.sol +++ b/packages/contracts-core/contracts/manager/BondingManager.sol @@ -7,6 +7,8 @@ import { AgentCantBeAdded, CallerNotDestination, CallerNotSummit, + DisputeAlreadyResolved, + DisputeNotOpened, IncorrectAgentDomain, IncorrectOriginDomain, IndexOutOfRange, @@ -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"; @@ -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 diff --git a/packages/contracts-core/contracts/manager/LightManager.sol b/packages/contracts-core/contracts/manager/LightManager.sol index 2bb20a6fd9..cd408cc85f 100644 --- a/packages/contracts-core/contracts/manager/LightManager.sol +++ b/packages/contracts-core/contracts/manager/LightManager.sol @@ -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"; @@ -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"; @@ -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; @@ -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 @@ -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 { diff --git a/packages/contracts-core/test/suite/manager/AgentManager.t.sol b/packages/contracts-core/test/suite/manager/AgentManager.t.sol index 056cc8f9a3..7816080275 100644 --- a/packages/contracts-core/test/suite/manager/AgentManager.t.sol +++ b/packages/contracts-core/test/suite/manager/AgentManager.t.sol @@ -2,12 +2,6 @@ pragma solidity 0.8.17; import {FRESH_DATA_TIMEOUT} from "../../../contracts/libs/Constants.sol"; -import { - DisputeAlreadyResolved, - DisputeNotOpened, - DisputeNotStuck, - IncorrectAgentDomain -} from "../../../contracts/libs/Errors.sol"; import {AgentFlag, AgentStatus, DisputeFlag} from "../../../contracts/libs/Structures.sol"; import {InterfaceDestination} from "../../../contracts/interfaces/InterfaceDestination.sol"; @@ -34,75 +28,6 @@ abstract contract AgentManagerTest is MessagingBaseTest { assertEq(testedAM().agentRoot(), getAgentRoot()); } - // ═══════════════════════════════════════ TESTS: RESOLVE STUCK DISPUTES ═══════════════════════════════════════════ - - function test_resolveStuckDispute(Random memory random, uint256 timePassed) public { - address guard = randomGuard(random); - address notary = randomNotary(random); - openDispute(guard, notary); - timePassed = FRESH_DATA_TIMEOUT + (timePassed % 1 days); - mockSnapRootTime(timePassed); - address slashedAgent = random.nextUint256() % 2 == 0 ? guard : notary; - address rival = slashedAgent == guard ? notary : guard; - expectStatusUpdated(AgentFlag.Fraudulent, agentDomain[slashedAgent], slashedAgent); - expectDisputeResolved(1, slashedAgent, rival, address(0)); - testedAM().resolveStuckDispute(agentDomain[slashedAgent], slashedAgent); - checkDisputeStatus(slashedAgent, DisputeFlag.Slashed, rival, address(0), 1); - checkDisputeStatus(rival, DisputeFlag.None, address(0), address(0), 0); - } - - function test_resolveStuckDispute_revert_callerNotOwner(address caller) public { - vm.assume(caller != testedAM().owner()); - expectRevertNotOwner(); - vm.prank(caller); - testedAM().resolveStuckDispute(0, address(0)); - } - - function test_resolveStuckDispute_revert_timeoutNotPassed(Random memory random, uint256 timePassed) public { - address guard = randomGuard(random); - address notary = randomNotary(random); - openDispute(guard, notary); - timePassed = timePassed % FRESH_DATA_TIMEOUT; - mockSnapRootTime(timePassed); - address slashedAgent = random.nextUint256() % 2 == 0 ? guard : notary; - vm.expectRevert(DisputeNotStuck.selector); - testedAM().resolveStuckDispute(agentDomain[slashedAgent], slashedAgent); - } - - function test_resolveStuckDispute_revert_agentNotDispute(Random memory random) public { - address guard0 = getGuard(0); - address notary = randomNotary(random); - openDispute(guard0, notary); - address guard1 = getGuard(1); - mockSnapRootTime(FRESH_DATA_TIMEOUT); - vm.expectRevert(DisputeNotOpened.selector); - testedAM().resolveStuckDispute(agentDomain[guard1], guard1); - } - - function test_resolveStuckDispute_revert_alreadyResolved(Random memory random) public { - address guard = randomGuard(random); - address notary = randomNotary(random); - openDispute(guard, notary); - address slashedByInbox = random.nextUint256() % 2 == 0 ? guard : notary; - vm.prank(localInbox()); - testedAM().slashAgent(agentDomain[slashedByInbox], slashedByInbox, address(0)); - address slashedByOwner = random.nextUint256() % 2 == 0 ? guard : notary; - mockSnapRootTime(FRESH_DATA_TIMEOUT); - vm.expectRevert(slashedByInbox == slashedByOwner ? DisputeAlreadyResolved.selector : DisputeNotOpened.selector); - testedAM().resolveStuckDispute(agentDomain[slashedByOwner], slashedByOwner); - } - - function test_resolveStuckDispute_revert_incorrectDomain(Random memory random, uint32 incorrectDomain) public { - address guard = randomGuard(random); - address notary = randomNotary(random); - openDispute(guard, notary); - address slashedAgent = random.nextUint256() % 2 == 0 ? guard : notary; - vm.assume(incorrectDomain != agentDomain[slashedAgent]); - mockSnapRootTime(FRESH_DATA_TIMEOUT); - vm.expectRevert(IncorrectAgentDomain.selector); - testedAM().resolveStuckDispute(incorrectDomain, slashedAgent); - } - function mockSnapRootTime(uint256 timePassed) public { // Force destStatus() to return (timestamp, timestamp, 1) as (snapRootTime, agentRootTime, notaryIndex) vm.mockCall( diff --git a/packages/contracts-core/test/suite/manager/BondingManager.t.sol b/packages/contracts-core/test/suite/manager/BondingManager.t.sol index ec79e84fe8..e27486b816 100644 --- a/packages/contracts-core/test/suite/manager/BondingManager.t.sol +++ b/packages/contracts-core/test/suite/manager/BondingManager.t.sol @@ -2,19 +2,23 @@ pragma solidity 0.8.17; import {InterfaceOrigin} from "../../../contracts/interfaces/InterfaceOrigin.sol"; -import {AGENT_TREE_HEIGHT} from "../../../contracts/libs/Constants.sol"; +import {AGENT_TREE_HEIGHT, FRESH_DATA_TIMEOUT} from "../../../contracts/libs/Constants.sol"; import { AgentCantBeAdded, AgentNotActive, AgentNotUnstaking, CallerNotSummit, + DisputeAlreadyResolved, + DisputeNotOpened, + IncorrectAgentDomain, MustBeSynapseDomain, IncorrectOriginDomain, + NotStuck, SlashAgentOptimisticPeriod, SynapseDomainForbidden } from "../../../contracts/libs/Errors.sol"; import {MerkleMath} from "../../../contracts/libs/merkle/MerkleMath.sol"; -import {AgentFlag} from "../../../contracts/libs/Structures.sol"; +import {AgentFlag, DisputeFlag} from "../../../contracts/libs/Structures.sol"; import {AgentManagerTest} from "./AgentManager.t.sol"; import {BondingManager, BondingManagerHarness, SynapseTest} from "../../utils/SynapseTest.t.sol"; @@ -72,6 +76,75 @@ contract BondingManagerTest is AgentManagerTest { assertEq(bondingManager.version(), LATEST_VERSION, "!version"); } + // ═══════════════════════════════════════ TESTS: RESOLVE STUCK DISPUTES ═══════════════════════════════════════════ + + function test_resolveDisputeWhenStuck(Random memory random, uint256 timePassed) public { + address guard = randomGuard(random); + address notary = randomNotary(random); + openDispute(guard, notary); + timePassed = FRESH_DATA_TIMEOUT + (timePassed % 1 days); + mockSnapRootTime(timePassed); + address slashedAgent = random.nextUint256() % 2 == 0 ? guard : notary; + address rival = slashedAgent == guard ? notary : guard; + expectStatusUpdated(AgentFlag.Fraudulent, agentDomain[slashedAgent], slashedAgent); + expectDisputeResolved(1, slashedAgent, rival, address(0)); + bondingManager.resolveDisputeWhenStuck(agentDomain[slashedAgent], slashedAgent); + checkDisputeStatus(slashedAgent, DisputeFlag.Slashed, rival, address(0), 1); + checkDisputeStatus(rival, DisputeFlag.None, address(0), address(0), 0); + } + + function test_resolveDisputeWhenStuck_revert_callerNotOwner(address caller) public { + vm.assume(caller != testedAM().owner()); + expectRevertNotOwner(); + vm.prank(caller); + bondingManager.resolveDisputeWhenStuck(0, address(0)); + } + + function test_resolveDisputeWhenStuck_revert_notStuck(Random memory random, uint256 timePassed) public { + address guard = randomGuard(random); + address notary = randomNotary(random); + openDispute(guard, notary); + timePassed = timePassed % FRESH_DATA_TIMEOUT; + mockSnapRootTime(timePassed); + address slashedAgent = random.nextUint256() % 2 == 0 ? guard : notary; + vm.expectRevert(NotStuck.selector); + bondingManager.resolveDisputeWhenStuck(agentDomain[slashedAgent], slashedAgent); + } + + function test_resolveDisputeWhenStuck_revert_agentNotDispute(Random memory random) public { + address guard0 = getGuard(0); + address notary = randomNotary(random); + openDispute(guard0, notary); + address guard1 = getGuard(1); + mockSnapRootTime(FRESH_DATA_TIMEOUT); + vm.expectRevert(DisputeNotOpened.selector); + bondingManager.resolveDisputeWhenStuck(agentDomain[guard1], guard1); + } + + function test_resolveDisputeWhenStuck_revert_alreadyResolved(Random memory random) public { + address guard = randomGuard(random); + address notary = randomNotary(random); + openDispute(guard, notary); + address slashedByInbox = random.nextUint256() % 2 == 0 ? guard : notary; + vm.prank(localInbox()); + bondingManager.slashAgent(agentDomain[slashedByInbox], slashedByInbox, address(0)); + address slashedByOwner = random.nextUint256() % 2 == 0 ? guard : notary; + mockSnapRootTime(FRESH_DATA_TIMEOUT); + vm.expectRevert(slashedByInbox == slashedByOwner ? DisputeAlreadyResolved.selector : DisputeNotOpened.selector); + bondingManager.resolveDisputeWhenStuck(agentDomain[slashedByOwner], slashedByOwner); + } + + function test_resolveDisputeWhenStuck_revert_incorrectDomain(Random memory random, uint32 incorrectDomain) public { + address guard = randomGuard(random); + address notary = randomNotary(random); + openDispute(guard, notary); + address slashedAgent = random.nextUint256() % 2 == 0 ? guard : notary; + vm.assume(incorrectDomain != agentDomain[slashedAgent]); + mockSnapRootTime(FRESH_DATA_TIMEOUT); + vm.expectRevert(IncorrectAgentDomain.selector); + bondingManager.resolveDisputeWhenStuck(incorrectDomain, slashedAgent); + } + // ══════════════════════════════════ TESTS: UNAUTHORIZED ACCESS (NOT OWNER) ═══════════════════════════════════════ function test_addAgent_revert_notOwner(address caller) public { diff --git a/packages/contracts-core/test/suite/manager/LightManager.t.sol b/packages/contracts-core/test/suite/manager/LightManager.t.sol index f43f523b52..12280d393c 100644 --- a/packages/contracts-core/test/suite/manager/LightManager.t.sol +++ b/packages/contracts-core/test/suite/manager/LightManager.t.sol @@ -1,10 +1,15 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.17; +import {AGENT_ROOT_PROPOSAL_TIMEOUT, FRESH_DATA_TIMEOUT} from "../../../contracts/libs/Constants.sol"; import { + AgentRootNotProposed, + AgentRootTimeoutNotOver, CallerNotDestination, IncorrectAgentProof, + IncorrectAgentRoot, MustBeSynapseDomain, + NotStuck, SynapseDomainForbidden, WithdrawTipsOptimisticPeriod } from "../../../contracts/libs/Errors.sol"; @@ -103,6 +108,8 @@ contract LightManagerTest is AgentManagerTest { checkAgentStatus(agent, lightManager.agentStatus(agent), AgentFlag.Slashed); } + // ═══════════════════════════════════════════ TESTS: SET AGENT ROOT ═══════════════════════════════════════════════ + function test_setAgentRoot(bytes32 root) public { bool isDifferent = root != lightManager.agentRoot(); if (isDifferent) { @@ -122,6 +129,231 @@ contract LightManagerTest is AgentManagerTest { test_setAgentRoot(lightManager.agentRoot()); } + // ════════════════════════════════════ TESTS: SET AGENT ROOT (WHEN STUCK) ═════════════════════════════════════════ + + function checkProposedAgentData(bytes32 expectedAgentRoot, uint256 expectedProposedAt) public { + (bytes32 agentRoot, uint256 proposedAt) = lightManager.proposedAgentRootData(); + assertEq(agentRoot, expectedAgentRoot, "!agentRoot"); + assertEq(proposedAt, expectedProposedAt, "!proposedAt"); + } + + function test_proposeAgentRootWhenStuck_revert_notOwner(address caller) public { + vm.assume(caller != lightManager.owner()); + expectRevertNotOwner(); + vm.prank(caller); + lightManager.proposeAgentRootWhenStuck("root"); + } + + function test_resolveProposedAgentRoot_revert_notOwner(address caller) public { + vm.assume(caller != lightManager.owner()); + expectRevertNotOwner(); + vm.prank(caller); + lightManager.resolveProposedAgentRoot(); + } + + function test_cancelProposedAgentRoot_revert_notOwner(address caller) public { + vm.assume(caller != lightManager.owner()); + expectRevertNotOwner(); + vm.prank(caller); + lightManager.cancelProposedAgentRoot(); + } + + function test_proposedAgentRootDataEmpty() public { + checkProposedAgentData({expectedAgentRoot: 0, expectedProposedAt: 0}); + } + + function test_proposeAgentRootWhenStuck() public { + bytes32 oldRoot = lightManager.agentRoot(); + vm.warp(1234); + mockSnapRootTime(FRESH_DATA_TIMEOUT); + bytes32 expectedAgentRoot = keccak256("mock root"); + uint256 expectedProposedAt = block.timestamp; + vm.expectEmit(address(lightManager)); + emit AgentRootProposed(expectedAgentRoot); + lightManager.proposeAgentRootWhenStuck(expectedAgentRoot); + checkProposedAgentData(expectedAgentRoot, expectedProposedAt); + assertEq(lightManager.agentRoot(), oldRoot); + } + + function test_proposeAgentRootWhenStuck_proposedTwice() public { + bytes32 oldRoot = lightManager.agentRoot(); + mockSnapRootTime(FRESH_DATA_TIMEOUT); + lightManager.proposeAgentRootWhenStuck("first root"); + skip(1 hours); + bytes32 expectedAgentRoot = keccak256("second root"); + uint256 expectedProposedAt = block.timestamp; + vm.expectEmit(address(lightManager)); + emit AgentRootProposed(expectedAgentRoot); + lightManager.proposeAgentRootWhenStuck(expectedAgentRoot); + checkProposedAgentData(expectedAgentRoot, expectedProposedAt); + assertEq(lightManager.agentRoot(), oldRoot); + } + + function test_proposeAgentRootWhenStuck_proposedTwice_revert_chainUnstuck() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + lightManager.proposeAgentRootWhenStuck("first root"); + skip(1 hours); + mockSnapRootTime(0); + vm.expectRevert(NotStuck.selector); + lightManager.proposeAgentRootWhenStuck("second root"); + } + + function test_proposeAgentRootWhenStuck_revert_emptyRoot() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + vm.expectRevert(IncorrectAgentRoot.selector); + lightManager.proposeAgentRootWhenStuck(0); + } + + function test_proposeAgentRootWhenStuck_revert_notStuck() public { + bytes32 newRoot = keccak256("mock root"); + mockSnapRootTime(FRESH_DATA_TIMEOUT - 1); + vm.expectRevert(NotStuck.selector); + lightManager.proposeAgentRootWhenStuck(newRoot); + } + + function test_cancelProposedAgentRoot() public { + bytes32 oldRoot = lightManager.agentRoot(); + mockSnapRootTime(FRESH_DATA_TIMEOUT); + bytes32 root = "mock root"; + lightManager.proposeAgentRootWhenStuck(root); + skip(1 hours); + // This should cancel the proposed agent root and the timestamp + vm.expectEmit(address(lightManager)); + emit ProposedAgentRootCancelled(root); + lightManager.cancelProposedAgentRoot(); + checkProposedAgentData(0, 0); + assertEq(lightManager.agentRoot(), oldRoot); + } + + function test_cancelProposedAgentRoot_chainUnstuck() public { + bytes32 oldRoot = lightManager.agentRoot(); + mockSnapRootTime(FRESH_DATA_TIMEOUT); + bytes32 newRoot = keccak256("mock root"); + lightManager.proposeAgentRootWhenStuck(newRoot); + skip(1 hours); + mockSnapRootTime(0); + // This should cancel the proposed agent root and the timestamp + vm.expectEmit(address(lightManager)); + emit ProposedAgentRootCancelled(newRoot); + lightManager.cancelProposedAgentRoot(); + checkProposedAgentData(0, 0); + assertEq(lightManager.agentRoot(), oldRoot); + } + + function test_cancelProposedAgentRoot_revert_notProposed() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + vm.expectRevert(AgentRootNotProposed.selector); + lightManager.cancelProposedAgentRoot(); + } + + function test_cancelProposedAgentRoot_revert_alreadyResolved() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + lightManager.proposeAgentRootWhenStuck("mock root"); + skip(AGENT_ROOT_PROPOSAL_TIMEOUT); + lightManager.resolveProposedAgentRoot(); + skip(AGENT_ROOT_PROPOSAL_TIMEOUT); + vm.expectRevert(AgentRootNotProposed.selector); + lightManager.cancelProposedAgentRoot(); + } + + function test_resolveProposedAgentRoot() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + bytes32 newRoot = keccak256("mock root"); + lightManager.proposeAgentRootWhenStuck(newRoot); + skip(AGENT_ROOT_PROPOSAL_TIMEOUT); + // Should emit two events: one signaling the new root, and another one signaling the manual resolution + vm.expectEmit(address(lightManager)); + emit RootUpdated(newRoot); + vm.expectEmit(address(lightManager)); + emit ProposedAgentRootResolved(newRoot); + lightManager.resolveProposedAgentRoot(); + checkProposedAgentData(0, 0); + assertEq(lightManager.agentRoot(), newRoot); + } + + /// @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. + function test_resolveProposedAgentRoot_chainUnstuck() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + bytes32 newRoot = keccak256("mock root"); + lightManager.proposeAgentRootWhenStuck(newRoot); + skip(AGENT_ROOT_PROPOSAL_TIMEOUT); + mockSnapRootTime(0); + // Should emit two events: one signaling the new root, and another one signaling the manual resolution + vm.expectEmit(address(lightManager)); + emit RootUpdated(newRoot); + vm.expectEmit(address(lightManager)); + emit ProposedAgentRootResolved(newRoot); + lightManager.resolveProposedAgentRoot(); + checkProposedAgentData(0, 0); + assertEq(lightManager.agentRoot(), newRoot); + } + + function test_resolveProposedAgentRoot_revert_timeoutNotOver() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + bytes32 newRoot = keccak256("mock root"); + lightManager.proposeAgentRootWhenStuck(newRoot); + skip(AGENT_ROOT_PROPOSAL_TIMEOUT - 1); + vm.expectRevert(AgentRootTimeoutNotOver.selector); + lightManager.resolveProposedAgentRoot(); + } + + function test_resolveProposedAgentRoot_proposedTwice() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + lightManager.proposeAgentRootWhenStuck("first root"); + skip(1 hours); + bytes32 newRoot = keccak256("second root"); + lightManager.proposeAgentRootWhenStuck(newRoot); + skip(AGENT_ROOT_PROPOSAL_TIMEOUT); + // Should emit two events: one signaling the new root, and another one signaling the manual resolution + vm.expectEmit(address(lightManager)); + emit RootUpdated(newRoot); + vm.expectEmit(address(lightManager)); + emit ProposedAgentRootResolved(newRoot); + lightManager.resolveProposedAgentRoot(); + checkProposedAgentData(0, 0); + assertEq(lightManager.agentRoot(), newRoot); + } + + function test_resolveProposedAgentRoot_proposedTwice_revert_timeoutNotOver() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + lightManager.proposeAgentRootWhenStuck("first root"); + skip(1 hours); + bytes32 newRoot = keccak256("second root"); + lightManager.proposeAgentRootWhenStuck(newRoot); + skip(AGENT_ROOT_PROPOSAL_TIMEOUT - 1); + vm.expectRevert(AgentRootTimeoutNotOver.selector); + lightManager.resolveProposedAgentRoot(); + } + + function test_proposeAgentRootWhenStuck_proposedTwice_cancelled_revert_notProposed() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + lightManager.proposeAgentRootWhenStuck("first root"); + skip(1 hours); + lightManager.cancelProposedAgentRoot(); + skip(AGENT_ROOT_PROPOSAL_TIMEOUT); + vm.expectRevert(AgentRootNotProposed.selector); + lightManager.resolveProposedAgentRoot(); + } + + function test_resolveProposedAgentRoot_revert_notProposed() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + skip(AGENT_ROOT_PROPOSAL_TIMEOUT); + vm.expectRevert(AgentRootNotProposed.selector); + lightManager.resolveProposedAgentRoot(); + } + + function test_resolveProposedAgentRoot_revert_alreadyResolved() public { + mockSnapRootTime(FRESH_DATA_TIMEOUT); + lightManager.proposeAgentRootWhenStuck("mock root"); + skip(AGENT_ROOT_PROPOSAL_TIMEOUT); + lightManager.resolveProposedAgentRoot(); + skip(AGENT_ROOT_PROPOSAL_TIMEOUT); + vm.expectRevert(AgentRootNotProposed.selector); + lightManager.resolveProposedAgentRoot(); + } + // ═══════════════════════════════════════ TEST: UPDATE AGENTS (REVERTS) ═══════════════════════════════════════════ function test_addAgent_revert_invalidProof(uint256 domainId, uint256 agentId) public {