// 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"; import {AgentFlag, AgentStatus} from "../../../contracts/libs/Structures.sol"; import {InterfaceOrigin} from "../../../contracts/interfaces/InterfaceOrigin.sol"; import {AgentManagerTest} from "./AgentManager.t.sol"; import {AgentFlag, AgentStatus, LightManager, LightManagerHarness, SynapseTest} from "../../utils/SynapseTest.t.sol"; import {Random} from "../../utils/libs/Random.t.sol"; // solhint-disable func-name-mixedcase // solhint-disable no-empty-blocks // solhint-disable ordering contract LightManagerTest is AgentManagerTest { // Deploy mocks for every messaging contract constructor() SynapseTest(0) {} function test_cleanSetup(Random memory random) public override { uint32 domain = random.nextUint32(); vm.assume(domain != DOMAIN_SYNAPSE); vm.chainId(domain); address caller = random.nextAddress(); address origin_ = random.nextAddress(); address destination_ = random.nextAddress(); address inbox_ = random.nextAddress(); LightManager cleanContract = new LightManager(DOMAIN_SYNAPSE); vm.prank(caller); cleanContract.initialize(origin_, destination_, inbox_); assertEq(cleanContract.localDomain(), domain); assertEq(cleanContract.owner(), caller); assertEq(cleanContract.origin(), origin_); assertEq(cleanContract.destination(), destination_); assertEq(cleanContract.inbox(), inbox_); } function initializeLocalContract() public override { LightManager(localContract()).initialize(address(0), address(0), address(0)); } function test_constructor_revert_chainIdOverflow() public { vm.chainId(2 ** 32); vm.expectRevert("SafeCast: value doesn't fit in 32 bits"); new LightManager({synapseDomain_: 1}); } // ═══════════════════════════════════════════════ TESTS: SETUP ════════════════════════════════════════════════════ function test_constructor_revert_onSynapseChain() public { // Should not be able to deploy on Synapse Chain vm.chainId(DOMAIN_SYNAPSE); vm.expectRevert(SynapseDomainForbidden.selector); new LightManagerHarness(DOMAIN_SYNAPSE); } function test_setup() public override { super.test_setup(); assertEq(lightManager.version(), LATEST_VERSION, "!version"); } // ══════════════════════════════════ TESTS: UNAUTHORIZED ACCESS (NOT OWNER) ═══════════════════════════════════════ function test_setAgentRoot_revert_notDestination(address caller) public { vm.assume(caller != destination); vm.expectRevert(CallerNotDestination.selector); vm.prank(caller); lightManager.setAgentRoot(bytes32(uint256(1))); } // ═════════════════════════════════════════ TESTS: ADD/REMOVE AGENTS ══════════════════════════════════════════════ function test_addAgent_new(address caller, uint32 domain, address agent) public { // Should not be an already added agent vm.assume(agent != address(0)); vm.assume(lightManager.agentStatus(agent).flag == AgentFlag.Unknown); bytes32 root = addNewAgent(domain, agent); test_setAgentRoot(root); bytes32[] memory proof = getAgentProof(agent); expectStatusUpdated(AgentFlag.Active, domain, agent); // Anyone could add agents in Light Manager vm.prank(caller); lightManager.updateAgentStatus(agent, getAgentStatus(agent), proof); checkAgentStatus(agent, lightManager.agentStatus(agent), AgentFlag.Active); } function test_updateAgentStatus_slashed(address caller, uint256 domainId, uint256 agentId) public { (uint32 domain, address agent) = getAgent(domainId, agentId); // Set flag to Slashed in the Merkle Tree bytes32 root = updateAgent(AgentFlag.Slashed, agent); test_setAgentRoot(root); bytes32[] memory proof = getAgentProof(agent); expectStatusUpdated(AgentFlag.Slashed, domain, agent); expectDisputeResolved(0, agent, address(0), caller); vm.prank(caller); lightManager.updateAgentStatus(agent, getAgentStatus(agent), proof); checkAgentStatus(agent, lightManager.agentStatus(agent), AgentFlag.Slashed); } // ═══════════════════════════════════════════ TESTS: SET AGENT ROOT ═══════════════════════════════════════════════ function test_setAgentRoot(bytes32 root) public { bool isDifferent = root != lightManager.agentRoot(); if (isDifferent) { vm.expectEmit(); emit RootUpdated(root); } vm.recordLogs(); vm.prank(destination); lightManager.setAgentRoot(root); if (!isDifferent) { assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't have"); } assertEq(lightManager.agentRoot(), root, "!agentRoot"); } function test_setAgentRoot_equal() public { 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 { (, address agent) = getAgent(domainId, agentId); bytes32[] memory proof = getAgentProof(agent); AgentStatus memory status = getAgentStatus(agent); // This succeeds, but doesn't do anything, as agent was already added lightManager.updateAgentStatus(agent, status, proof); // Change agent root, so old proofs are no longer valid test_setAgentRoot(bytes32(0)); assertEq(uint8(lightManager.agentStatus(agent).flag), uint8(AgentFlag.Unknown)); vm.expectRevert(IncorrectAgentProof.selector); lightManager.updateAgentStatus(agent, status, proof); } // ════════════════════════════════════════════ TEST: WITHDRAW TIPS ════════════════════════════════════════════════ function test_remoteWithdrawTips(address actor, uint256 amount, uint32 proofMaturity) public { proofMaturity = uint32(bound(proofMaturity, BONDING_OPTIMISTIC_PERIOD, type(uint32).max)); skip(proofMaturity); bytes memory msgPayload = managerMsgPayload(DOMAIN_SYNAPSE, remoteWithdrawTipsCalldata(actor, amount)); bytes memory expectedCall = abi.encodeWithSelector(InterfaceOrigin.withdrawTips.selector, actor, amount); vm.expectCall(origin, expectedCall); managerMsgPrank(msgPayload); } function test_remoteWithdrawTips_revert_notDestination(address caller) public { vm.assume(caller != destination); skip(BONDING_OPTIMISTIC_PERIOD); vm.expectRevert(CallerNotDestination.selector); vm.prank(caller); lightManager.remoteWithdrawTips(DOMAIN_SYNAPSE, BONDING_OPTIMISTIC_PERIOD, address(0), 0); } function test_remoteWithdrawTips_revert_notSynapseChain(uint32 msgOrigin) public { vm.assume(msgOrigin != DOMAIN_SYNAPSE); skip(BONDING_OPTIMISTIC_PERIOD); bytes memory msgPayload = managerMsgPayload(msgOrigin, remoteWithdrawTipsCalldata(address(0), 0)); vm.expectRevert(MustBeSynapseDomain.selector); managerMsgPrank(msgPayload); } function test_remoteWithdrawTips_revert_optimisticPeriodNotOver(uint32 proofMaturity) public { proofMaturity = proofMaturity % BONDING_OPTIMISTIC_PERIOD; skipPeriod(proofMaturity); bytes memory msgPayload = managerMsgPayload(DOMAIN_SYNAPSE, remoteWithdrawTipsCalldata(address(0), 0)); vm.expectRevert(WithdrawTipsOptimisticPeriod.selector); managerMsgPrank(msgPayload); } function test_remoteWithdrawTips_revert_optimisticPeriodMinus1Second() public { test_remoteWithdrawTips_revert_optimisticPeriodNotOver(BONDING_OPTIMISTIC_PERIOD - 1); } // ══════════════════════════════════════════════════ HELPERS ══════════════════════════════════════════════════════ /// @notice Returns local domain for the tested contract function localDomain() public pure override returns (uint32) { return DOMAIN_LOCAL; } }