synapsecns/sanguine

View on GitHub
packages/contracts-core/test/suite/manager/LightManager.t.sol

Summary

Maintainability
Test Coverage
// 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;
    }
}