synapsecns/sanguine

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

Summary

Maintainability
Test Coverage
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import {InterfaceOrigin} from "../../../contracts/interfaces/InterfaceOrigin.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, DisputeFlag} from "../../../contracts/libs/Structures.sol";
import {AgentManagerTest} from "./AgentManager.t.sol";

import {BondingManager, BondingManagerHarness, 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 BondingManagerTest is AgentManagerTest {
    // Deploy mocks for everything except BondingManager and Inbox
    constructor() SynapseTest(0) {}

    // ═══════════════════════════════════════════════ TESTS: SETUP ════════════════════════════════════════════════════

    function test_cleanSetup(Random memory random) public override {
        uint32 domain = DOMAIN_SYNAPSE;
        vm.chainId(domain);
        address caller = random.nextAddress();
        address origin_ = random.nextAddress();
        address destination_ = random.nextAddress();
        address summit_ = random.nextAddress();
        address inbox_ = random.nextAddress();
        BondingManager cleanContract = new BondingManager(domain);
        vm.prank(caller);
        cleanContract.initialize(origin_, destination_, inbox_, summit_);
        assertEq(cleanContract.localDomain(), domain);
        assertEq(cleanContract.owner(), caller);
        assertEq(cleanContract.origin(), origin_);
        assertEq(cleanContract.destination(), destination_);
        assertEq(cleanContract.inbox(), inbox_);
        assertEq(cleanContract.summit(), summit_);
        assertEq(cleanContract.leafsAmount(), 1);
    }

    function initializeLocalContract() public override {
        BondingManager(localContract()).initialize(address(0), address(0), address(0), address(0));
    }

    function test_constructor_revert_notOnSynapseChain(uint32 domain) public {
        vm.assume(domain != DOMAIN_SYNAPSE);
        vm.expectRevert(MustBeSynapseDomain.selector);
        new BondingManager(DOMAIN_SYNAPSE);
    }

    function test_constructor_revert_chainIdOverflow() public {
        vm.chainId(2 ** 32);
        vm.expectRevert("SafeCast: value doesn't fit in 32 bits");
        new BondingManager(DOMAIN_SYNAPSE);
    }

    function test_setup() public override {
        super.test_setup();
        assertEq(bondingManager.summit(), localSummit(), "!summit");
        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 {
        vm.assume(caller != address(this));
        expectRevertNotOwner();
        vm.prank(caller);
        bondingManager.addAgent(1, address(1), new bytes32[](0));
    }

    function test_initiateUnstaking_revert_notOwner(address caller) public {
        vm.assume(caller != address(this));
        expectRevertNotOwner();
        vm.prank(caller);
        bondingManager.initiateUnstaking(1, address(1), new bytes32[](0));
    }

    function test_completeUnstaking_revert_notOwner(address caller) public {
        vm.assume(caller != address(this));
        expectRevertNotOwner();
        vm.prank(caller);
        bondingManager.completeUnstaking(1, address(1), new bytes32[](0));
    }

    // ═════════════════════════════════════════ TESTS: ADD/REMOVE AGENTS ══════════════════════════════════════════════

    function test_addAgent_fromScratch() public {
        // Deploy fresh instance of BondingManager
        vm.chainId(DOMAIN_SYNAPSE);
        bondingManager = new BondingManagerHarness(DOMAIN_SYNAPSE);
        bondingManager.initialize(originSynapse, destinationSynapse, address(inbox), summit);
        // Try to add all agents one by one
        for (uint256 d = 0; d < allDomains.length; ++d) {
            uint32 domain = allDomains[d];
            for (uint256 i = 0; i < DOMAIN_AGENTS; ++i) {
                address agent = domains[domain].agents[i];
                bytes32[] memory proof = bondingManager.getProof(agent);
                bondingManager.addAgent(domain, agent, proof);
                checkAgentStatus(agent, bondingManager.agentStatus(agent), AgentFlag.Active);
            }
        }
    }

    function test_addAgent_new(uint32 domain, address agent) public {
        // Notaries on Syn Chain could nto be added
        vm.assume(domain != DOMAIN_SYNAPSE);
        // Should not be an already added agent
        vm.assume(bondingManager.agentStatus(agent).flag == AgentFlag.Unknown);
        vm.assume(agent != address(0));
        bytes32[] memory proof = getZeroProof();
        bytes32 newRoot = addNewAgent(domain, agent);
        expectStatusUpdated(AgentFlag.Active, domain, agent);
        vm.expectEmit();
        emit RootUpdated(newRoot);
        bondingManager.addAgent(domain, agent, proof);
        checkAgentStatus(agent, bondingManager.agentStatus(agent), AgentFlag.Active);
        assertEq(bondingManager.agentRoot(), newRoot, "!agentRoot");
    }

    function test_addAgent_revert_synapseDomain(address agent) public {
        bytes32[] memory proof = getZeroProof();
        vm.expectRevert(SynapseDomainForbidden.selector);
        bondingManager.addAgent(DOMAIN_SYNAPSE, agent, proof);
    }

    function test_addAgent_resting(uint256 domainId, uint256 agentId) public {
        // Full lifecycle for a live agent:
        // Active -> Unstaking -> Resting -> Active
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        updateStatus(AgentFlag.Unstaking, domain, agent);
        updateStatus(AgentFlag.Resting, domain, agent);
        updateStatus(AgentFlag.Active, domain, agent);
    }

    function test_initiateUnstaking(uint256 domainId, uint256 agentId) public {
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        updateStatus(AgentFlag.Unstaking, domain, agent);
    }

    function test_completeUnstaking(uint256 domainId, uint256 agentId) public {
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        updateStatus(AgentFlag.Unstaking, domain, agent);
        updateStatus(AgentFlag.Resting, domain, agent);
    }

    function updateStatus(AgentFlag flag, uint32 domain, address agent) public {
        updateStatus(address(this), flag, domain, agent);
    }

    function updateStatus(address caller, AgentFlag flag, uint32 domain, address agent) public {
        bytes32[] memory proof = getAgentProof(agent);
        bytes32 newRoot = updateAgent(flag, agent);
        expectStatusUpdated(flag, domain, agent);
        vm.expectEmit();
        emit RootUpdated(newRoot);
        vm.prank(caller);
        updateStatusWithProof(flag, domain, agent, proof);
        assertEq(bondingManager.agentRoot(), newRoot, "!agentRoot");
        checkAgentStatus(agent, bondingManager.agentStatus(agent), flag);
    }

    function updateStatusWithProof(AgentFlag flag, uint32 domain, address agent, bytes32[] memory proof) public {
        if (flag == AgentFlag.Unstaking) {
            bondingManager.initiateUnstaking(domain, agent, proof);
        } else if (flag == AgentFlag.Resting) {
            bondingManager.completeUnstaking(domain, agent, proof);
        } else if (flag == AgentFlag.Active) {
            bondingManager.addAgent(domain, agent, proof);
        } else if (flag == AgentFlag.Slashed) {
            bondingManager.completeSlashing(domain, agent, proof);
        }
    }

    // ═══════════════════════════════════════ TEST: UPDATE AGENTS (REVERTS) ═══════════════════════════════════════════

    function test_addAgent_revert_active(uint256 domainId, uint256 agentId) public {
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        updateStatusWithRevert(AgentFlag.Active, domain, agent, AgentCantBeAdded.selector);
    }

    function test_addAgent_revert_unstaking(uint256 domainId, uint256 agentId) public {
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        updateStatus(AgentFlag.Unstaking, domain, agent);
        updateStatusWithRevert(AgentFlag.Active, domain, agent, AgentCantBeAdded.selector);
    }

    function test_initiateUnstaking_revert_unstaking(uint256 domainId, uint256 agentId) public {
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        updateStatus(AgentFlag.Unstaking, domain, agent);
        updateStatusWithRevert(AgentFlag.Unstaking, domain, agent, AgentNotActive.selector);
    }

    function test_initiateUnstaking_revert_resting(uint256 domainId, uint256 agentId) public {
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        updateStatus(AgentFlag.Unstaking, domain, agent);
        updateStatus(AgentFlag.Resting, domain, agent);
        updateStatusWithRevert(AgentFlag.Unstaking, domain, agent, AgentNotActive.selector);
    }

    function test_completeUnstaking_revert_active(uint256 domainId, uint256 agentId) public {
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        updateStatusWithRevert(AgentFlag.Resting, domain, agent, AgentNotUnstaking.selector);
    }

    function test_completeUnstaking_revert_resting(uint256 domainId, uint256 agentId) public {
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        updateStatus(AgentFlag.Unstaking, domain, agent);
        updateStatus(AgentFlag.Resting, domain, agent);
        updateStatusWithRevert(AgentFlag.Resting, domain, agent, AgentNotUnstaking.selector);
    }

    function updateStatusWithRevert(AgentFlag flag, uint32 domain, address agent, bytes4 err) public {
        bytes32[] memory proof = getAgentProof(agent);
        vm.expectRevert(err);
        updateStatusWithProof(flag, domain, agent, proof);
    }

    // ═══════════════════════════════════════════ TEST: SLASHING AGENTS ═══════════════════════════════════════════════

    // TODO: test_initiateSlashing

    function test_remoteSlashAgent(uint32 msgOrigin, uint256 domainId, uint256 agentId, address prover) public {
        // Needs to be a REMOTE call
        vm.assume(msgOrigin != DOMAIN_SYNAPSE);
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        skipBondingOptimisticPeriod();
        bytes memory msgPayload = managerMsgPayload(msgOrigin, remoteSlashAgentCalldata(domain, agent, prover));
        expectStatusUpdated(AgentFlag.Fraudulent, domain, agent);
        expectDisputeResolved(0, agent, address(0), prover);
        managerMsgPrank(msgPayload);
        assertEq(uint8(bondingManager.agentStatus(agent).flag), uint8(AgentFlag.Fraudulent));
        // (bool isSlashed, address prover_) = bondingManager.slashStatus(agent);
        // assertTrue(isSlashed);
        // assertEq(prover_, prover);
    }

    function test_remoteSlashAgent_revert_optimisticPeriodNotOver(uint32 proofMaturity) public {
        proofMaturity = proofMaturity % BONDING_OPTIMISTIC_PERIOD;
        skipPeriod(proofMaturity);
        bytes memory msgPayload = managerMsgPayload(DOMAIN_REMOTE, remoteSlashAgentCalldata(0, address(0), address(0)));
        vm.expectRevert(SlashAgentOptimisticPeriod.selector);
        managerMsgPrank(msgPayload);
    }

    function test_remoteSlashAgent_revert_optimisticPeriodMinus1Second() public {
        test_remoteSlashAgent_revert_optimisticPeriodNotOver(BONDING_OPTIMISTIC_PERIOD - 1);
    }

    function test_remoteSlashAgent_revert_sameOriginDomain() public {
        uint32 proofMaturity = BONDING_OPTIMISTIC_PERIOD;
        skip(proofMaturity);
        bytes memory msgPayload = managerMsgPayload(DOMAIN_SYNAPSE, remoteSlashAgentCalldata(0, address(0), address(0)));
        vm.expectRevert(IncorrectOriginDomain.selector);
        managerMsgPrank(msgPayload);
    }

    function test_completeSlashing_active(uint256 domainId, uint256 agentId, address slasher) public {
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        // Initiate slashing
        test_remoteSlashAgent(DOMAIN_REMOTE, domainId, agentId, address(1));
        updateStatus(slasher, AgentFlag.Slashed, domain, agent);
        checkAgentStatus(agent, bondingManager.agentStatus(agent), AgentFlag.Slashed);
    }

    function test_completeSlashing_unstaking(uint256 domainId, uint256 agentId, address slasher) public {
        (uint32 domain, address agent) = getAgent(domainId, agentId);
        updateStatus(AgentFlag.Unstaking, domain, agent);
        // Initiate slashing
        test_remoteSlashAgent(DOMAIN_REMOTE, domainId, agentId, address(1));
        updateStatus(slasher, AgentFlag.Slashed, domain, agent);
        checkAgentStatus(agent, bondingManager.agentStatus(agent), AgentFlag.Slashed);
    }

    // ════════════════════════════════════════════ TEST: WITHDRAW TIPS ════════════════════════════════════════════════

    function test_withdrawTips_local(address recipient, uint256 amount) public {
        bytes memory expectedCall = abi.encodeWithSelector(InterfaceOrigin.withdrawTips.selector, recipient, amount);
        vm.expectCall(originSynapse, expectedCall);
        vm.prank(summit);
        bondingManager.withdrawTips(recipient, DOMAIN_SYNAPSE, amount);
    }

    function test_withdrawTips_remote(address recipient, uint32 domain, uint256 amount) public {
        vm.assume(domain != DOMAIN_SYNAPSE);
        // remoteWithdrawTips(msgOrigin, proofMaturity, recipient, amount), but first two are omitted
        bytes memory payload = abi.encodeWithSelector(lightManager.remoteWithdrawTips.selector, recipient, amount);
        // sendManagerMessage(destination, optimisticPeriod, payload)
        bytes memory expectedCall = abi.encodeWithSelector(
            InterfaceOrigin.sendManagerMessage.selector, domain, BONDING_OPTIMISTIC_PERIOD, payload
        );
        vm.expectCall(address(originSynapse), expectedCall);
        vm.prank(summit);
        bondingManager.withdrawTips(recipient, domain, amount);
    }

    function test_withdrawTips_revert_notSummit(address caller) public {
        vm.assume(caller != summit);
        vm.expectRevert(CallerNotSummit.selector);
        vm.prank(caller);
        bondingManager.withdrawTips(address(0), 0, 0);
    }

    // ════════════════════════════════════════════════ TEST: VIEWS ════════════════════════════════════════════════════

    function test_agentLeaf_knownAgent(uint256 domainId, uint256 agentId) public {
        (, address agent) = getAgent(domainId, agentId);
        assertEq(bondingManager.agentLeaf(agent), getAgentLeaf(agentIndex[agent]));
    }

    function test_agentLeaf_unknownAgent(address agent) public {
        // Should not be an already added agent
        vm.assume(bondingManager.agentStatus(agent).flag == AgentFlag.Unknown);
        assertEq(bondingManager.agentLeaf(agent), bytes32(0));
    }

    function test_getActiveAgents() public {
        for (uint256 d = 0; d < allDomains.length; ++d) {
            uint32 domain = allDomains[d];
            address[] memory agents = bondingManager.getActiveAgents(domain);
            assertEq(agents.length, DOMAIN_AGENTS);
            for (uint256 i = 0; i < agents.length; ++i) {
                assertEq(agents[i], domains[domain].agents[i]);
            }
        }
    }

    function test_getActiveAgents_agentsRemoved() public {
        // Change status of four agents into Unstaking, Resting, Fraudulent and Slashed - one for each domain
        test_initiateUnstaking(0, 0);
        test_completeUnstaking(1, 1);
        test_remoteSlashAgent(DOMAIN_REMOTE, 2, 2, address(1));
        test_completeSlashing_active(3, 3, address(1));
        for (uint256 d = 0; d < allDomains.length; ++d) {
            uint32 domain = allDomains[d];
            address[] memory agents = bondingManager.getActiveAgents(domain);
            assertEq(agents.length, DOMAIN_AGENTS - 1);
            for (uint256 i = 0; i < agents.length; ++i) {
                // Agent with index `d` was removed
                assertEq(agents[i], domains[domain].agents[i < d ? i : i + 1]);
            }
        }
    }

    function test_getProof_knownAgent(uint256 domainId, uint256 agentId) public {
        (, address agent) = getAgent(domainId, agentId);
        bytes32[] memory proof = bondingManager.getProof(agent);
        uint256 index = agentIndex[agent];
        checkProof(index, proof);
    }

    function test_getProof_unknownAgent(address agent) public {
        // Should not be an already added agent
        vm.assume(bondingManager.agentStatus(agent).flag == AgentFlag.Unknown);
        bytes32[] memory proof = bondingManager.getProof(agent);
        // Use the next index
        uint256 index = totalAgents + 1;
        checkProof(index, proof);
    }

    function checkProof(uint256 index, bytes32[] memory proof) public {
        assertEq(MerkleMath.proofRoot(index, getAgentLeaf(index), proof, AGENT_TREE_HEIGHT), getAgentRoot());
    }

    function test_allLeafs() public {
        assertEq(bondingManager.leafsAmount(), totalAgents + 1, "!leafsAmount");
        bytes32[] memory leafs = bondingManager.allLeafs();
        for (uint256 i = 0; i < leafs.length; ++i) {
            assertEq(leafs[i], getAgentLeaf(i));
        }
    }

    function test_getLeafs(uint256 indexFrom, uint256 amount) public {
        uint256 totalLeafs = totalAgents + 1;
        indexFrom = indexFrom % totalLeafs;
        // Allow index overrun
        amount = amount % (totalLeafs + 10);
        bytes32[] memory leafs = bondingManager.getLeafs(indexFrom, amount);
        if (indexFrom + amount <= totalLeafs) {
            assertEq(leafs.length, amount);
        } else {
            assertEq(leafs.length, totalLeafs - indexFrom);
        }
        for (uint256 i = 0; i < leafs.length; ++i) {
            assertEq(leafs[i], getAgentLeaf(indexFrom + i));
        }
    }

    // ══════════════════════════════════════════════════ HELPERS ══════════════════════════════════════════════════════

    /// @notice Returns local domain for the tested contract
    function localDomain() public pure override returns (uint32) {
        return DOMAIN_SYNAPSE;
    }
}