synapsecns/sanguine

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

Summary

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

import {CallerNotInbox, DisputeTimeoutNotOver, NotaryInDispute} from "../../contracts/libs/Errors.sol";
import {IAgentSecured} from "../../contracts/interfaces/IAgentSecured.sol";
import {InterfaceDestination} from "../../contracts/interfaces/InterfaceDestination.sol";
import {ISnapshotHub} from "../../contracts/interfaces/ISnapshotHub.sol";
import {DISPUTE_TIMEOUT_NOTARY, SNAPSHOT_TREE_HEIGHT} from "../../contracts/libs/Constants.sol";
import {MerkleMath} from "../../contracts/libs/merkle/MerkleMath.sol";

import {InterfaceSummit} from "../../contracts/Summit.sol";
import {Versioned} from "../../contracts/base/Version.sol";

import {AgentFlag, AgentStatus, Summit, SynapseTest} from "../utils/SynapseTest.t.sol";
import {State, RawAttestation, RawSnapshot, RawState, RawStateIndex} from "../utils/libs/SynapseStructs.t.sol";
import {Random} from "../utils/libs/Random.t.sol";
import {AgentSecuredTest} from "./hubs/ExecutionHub.t.sol";

// solhint-disable func-name-mixedcase
// solhint-disable no-empty-blocks
// solhint-disable code-complexity
contract SummitTest is AgentSecuredTest {
    struct SignedSnapshot {
        bytes snapshot;
        bytes signature;
    }

    uint256 internal constant STATES = 10;

    mapping(uint256 => mapping(uint256 => RawState)) internal guardStates;
    mapping(uint256 => SignedSnapshot) internal guardSnapshots;
    mapping(uint256 => RawAttestation) internal notaryAttestations;

    // Deploy Production version of Summit and mocks for everything else
    constructor() SynapseTest(DEPLOY_PROD_SUMMIT) {}

    function setUp() public virtual override {
        RawAttestation memory empty = RawAttestation({
            snapRoot: 0,
            dataHash: 0,
            _agentRoot: 0,
            _snapGasHash: 0,
            nonce: 0,
            blockNumber: uint40(block.number),
            timestamp: uint40(block.timestamp)
        });
        empty.setDataHash();
        notaryAttestations[0] = empty;
        super.setUp();
    }

    function test_setupCorrectly() public {
        // Check Agents
        // Summit should know about agents from all domains, including Guards
        for (uint256 d = 0; d < allDomains.length; ++d) {
            uint32 domain = allDomains[d];
            for (uint256 i = 0; i < domains[domain].agents.length; ++i) {
                address agent = domains[domain].agents[i];
                checkAgentStatus(agent, IAgentSecured(summit).agentStatus(agent), AgentFlag.Active);
            }
        }
        // Check version
        assertEq(Versioned(summit).version(), LATEST_VERSION, "!version");
        // Check attestation getter for zero nonce
        (bytes memory attPayload, bytes32 agentRoot, uint256[] memory snapGas) = ISnapshotHub(summit).getAttestation(0);
        assertEq(attPayload, notaryAttestations[0].formatAttestation(), "!attPayload");
        assertEq(agentRoot, 0, "!agentRoot");
        assertEq(snapGas.length, 0, "!snapGas");
    }

    function test_constructor_revert_chainIdOverflow() public {
        vm.chainId(2 ** 32);
        vm.expectRevert("SafeCast: value doesn't fit in 32 bits");
        new Summit({synapseDomain_: 1, agentManager_: address(2), inbox_: address(3)});
    }

    function test_cleanSetup(Random memory random) public override {
        uint32 domain = DOMAIN_SYNAPSE;
        vm.chainId(domain);
        address agentManager = random.nextAddress();
        address inbox_ = random.nextAddress();
        address caller = random.nextAddress();
        Summit cleanContract = new Summit(domain, agentManager, inbox_);
        vm.prank(caller);
        cleanContract.initialize();
        assertEq(cleanContract.owner(), caller, "!owner");
        assertEq(cleanContract.agentManager(), agentManager, "!agentManager");
        assertEq(cleanContract.inbox(), inbox_, "!inbox");
        assertEq(cleanContract.localDomain(), domain, "!localDomain");
    }

    function initializeLocalContract() public override {
        Summit(localContract()).initialize();
    }

    function test_acceptGuardSnapshot_revert_notInbox(address caller) public {
        vm.assume(caller != localInbox());
        vm.expectRevert(CallerNotInbox.selector);
        vm.prank(caller);
        InterfaceSummit(summit).acceptGuardSnapshot(0, 0, "");
    }

    function test_acceptNotarySnapshot_revert_notInbox(address caller) public {
        vm.assume(caller != localInbox());
        vm.expectRevert(CallerNotInbox.selector);
        vm.prank(caller);
        InterfaceSummit(summit).acceptNotarySnapshot(0, 0, 0, "");
    }

    function test_acceptNotarySnapshot_revert_blockTimestampOverflow() public {
        address notary = domains[DOMAIN_LOCAL].agent;
        address guard = domains[0].agent;
        Random memory random = Random("salt");
        RawSnapshot memory rawSnap = random.nextSnapshot();
        // Another Guard signs the snapshot
        (bytes memory snapPayload, bytes memory guardSignature) = signSnapshot(guard, rawSnap);
        bytes memory notarySig = signSnapshot(notary, snapPayload);
        inbox.submitSnapshot(snapPayload, guardSignature);
        vm.warp(2 ** 40);
        vm.expectRevert("SafeCast: value doesn't fit in 40 bits");
        inbox.submitSnapshot(snapPayload, notarySig);
    }

    function test_verifyAttestation_existingNonce(Random memory random, uint256 mask) public {
        test_notarySnapshots(random);
        // Restrict nonce to existing ones
        uint32 nonce = uint32(bound(random.nextUint32(), 0, DOMAIN_AGENTS));
        // Attestation is valid if and only if all four fields match
        (bool isValid, RawAttestation memory ra) = notaryAttestations[nonce].modifyAttestation(mask);
        verifyAttestation(random, ra, isValid);
    }

    function test_verifyAttestation_unknownNonce(Random memory random, RawAttestation memory ra) public {
        test_notarySnapshots(random);
        // Restrict nonce to non-existing ones
        ra.nonce = uint32(bound(ra.nonce, DOMAIN_AGENTS + 1, type(uint32).max));
        verifyAttestation(random, ra, false);
    }

    function verifyAttestation(Random memory random, RawAttestation memory ra, bool isValid) public {
        // Pick random domain expect for 0
        uint256 domainIndex = bound(random.nextUint256(), 1, allDomains.length - 1);
        uint32 domain = allDomains[domainIndex];
        // Pick random Notary
        uint256 notaryIndex = bound(random.nextUint256(), 0, DOMAIN_AGENTS - 1);
        address notary = domains[domain].agents[notaryIndex];
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);
        if (!isValid) {
            // Expect Events to be emitted
            vm.expectEmit(true, true, true, true);
            emit InvalidAttestation(attPayload, attSig);
            // TODO: check that anyone could make the call
            expectStatusUpdated(AgentFlag.Fraudulent, domain, notary);
            expectDisputeResolved(0, notary, address(0), address(this));
        }
        vm.recordLogs();
        assertEq(inbox.verifyAttestation(attPayload, attSig), isValid, "!returnValue");
        if (isValid) {
            assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't");
        }
        // Verify report on constructed attestation
        verifyAttestationReport(random, ra, isValid);
    }

    function verifyAttestationReport(Random memory random, RawAttestation memory ra, bool isAttestationValid) public {
        // Report is considered invalid, if reported attestation is valid
        bool isValid = !isAttestationValid;
        // Pick random Guard
        uint256 guardIndex = bound(random.nextUint256(), 0, DOMAIN_AGENTS - 1);
        address guard = domains[0].agents[guardIndex];
        (bytes memory attPayload, bytes memory arSig) = signAttestationReport(guard, ra);
        if (!isValid) {
            // Expect Events to be emitted
            vm.expectEmit(true, true, true, true);
            emit InvalidAttestationReport(attPayload, arSig);
            // TODO: check that anyone could make the call
            expectStatusUpdated(AgentFlag.Fraudulent, 0, guard);
            expectDisputeResolved(0, guard, address(0), address(this));
        }
        vm.recordLogs();
        assertEq(inbox.verifyAttestationReport(attPayload, arSig), isValid, "!returnValue");
        if (isValid) {
            assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't");
        }
    }

    function test_guardSnapshots(Random memory random) public {
        // Every Guard submits a snapshot with a random state for domains in [1 .. DOMAINS] range
        for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
            State[] memory states = new State[](STATES);
            for (uint32 j = 0; j < STATES; ++j) {
                // Use random non-zero nonce for every state
                uint32 nonce = uint32(bound(random.nextUint32(), 1, type(uint32).max));
                guardStates[i][j] = random.nextState({origin: j + 1, nonce: nonce});
                states[j] = guardStates[i][j].castToState();
            }
            address guard = domains[0].agents[i];
            (bytes memory snapPayload, bytes memory snapSig) = signSnapshot(guard, states);
            guardSnapshots[i] = SignedSnapshot(snapPayload, snapSig);
        }

        for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
            // Check that every State is saved
            for (uint256 j = 0; j < STATES; ++j) {
                vm.expectEmit(true, true, true, true);
                emit StateSaved(guardStates[i][j].formatState());
            }
            vm.expectEmit(true, true, true, true);
            emit SnapshotAccepted(0, domains[0].agents[i], guardSnapshots[i].snapshot, guardSnapshots[i].signature);
            (bytes memory attPayload, bytes32 agentRoot, uint256[] memory snapGas) =
                inbox.submitSnapshot(guardSnapshots[i].snapshot, guardSnapshots[i].signature);
            assertEq(attPayload, "", "Guard: non-empty attestation");
            assertEq(agentRoot, bytes32(0), "Guard: non-empty agent root");
            assertEq(snapGas.length, 0, "Guard: non-empty snap gas data");
            // Check latest Guard States
            for (uint32 j = 0; j < STATES; ++j) {
                assertEq(
                    ISnapshotHub(summit).getLatestAgentState(j + 1, domains[0].agents[i]),
                    guardStates[i][j].formatState(),
                    "!latestState: guard"
                );
            }
        }

        for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
            (bytes memory snapPayload, bytes memory snapSignature) = ISnapshotHub(summit).getGuardSnapshot(i);
            assertEq(snapPayload, guardSnapshots[i].snapshot, "!snapshot");
            assertEq(snapSignature, guardSnapshots[i].signature, "!signature");
        }

        // Check global latest state
        checkLatestState();
    }

    function test_notarySnapshots(Random memory random) public {
        // Every Guard submits a snapshot with a random state for domains in [1 .. DOMAINS] range
        test_guardSnapshots(random);

        bytes[] memory snapPayloads = new bytes[](DOMAIN_AGENTS);
        bytes[] memory snapSignatures = new bytes[](DOMAIN_AGENTS);

        // Every Notary submits a snapshot with a random Guard state for all domains
        for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
            // Set random timestamp and block height
            RawAttestation memory ra;
            ra.blockNumber = random.nextUint40();
            ra.timestamp = random.nextUint40();
            vm.roll(ra.blockNumber);
            vm.warp(ra.timestamp);

            RawSnapshot memory rs;
            rs.states = new RawState[](STATES);
            for (uint256 j = 0; j < STATES; ++j) {
                // Pick a random Guard to choose their state for domain (J+1)
                // To ensure that all Notary snapshots are different pick Guard
                // with the same index as Notary for the first state
                uint256 guardIndex = j == 0 ? i : random.nextUint256() % DOMAIN_AGENTS;
                rs.states[j] = guardStates[guardIndex][j];
            }

            // Calculate root and height using AttestationProofGenerator
            acceptSnapshot(rs);
            ra.snapRoot = getSnapshotRoot();
            ra._agentRoot = getAgentRoot();
            ra._snapGasHash = rs.snapGasHash();
            ra.setDataHash();
            // This is i-th submitted attestation so far, but attestation nonce starts from 1
            ra.nonce = i + 1;
            notaryAttestations[ra.nonce] = ra;
            bytes memory attestation = ra.formatAttestation();

            address notary = domains[DOMAIN_LOCAL].agents[i];
            (snapPayloads[i], snapSignatures[i]) = signSnapshot(notary, rs);
            // Nothing should be saved before Notary submitted their first snapshot
            (bytes memory attPayload, bytes32 agentRoot, uint256[] memory snapGas) =
                ISnapshotHub(summit).getLatestNotaryAttestation(notary);
            assertEq(attPayload, "");
            assertEq(agentRoot, bytes32(0));
            assertEq(snapGas.length, 0);

            vm.expectEmit(true, true, true, true);
            emit AttestationSaved(attestation);
            vm.expectEmit(true, true, true, true);
            emit SnapshotAccepted(DOMAIN_LOCAL, notary, snapPayloads[i], snapSignatures[i]);
            // Should pass to Destination: acceptAttestation(status, sigIndex, attestation, agentRoot, snapGas)
            vm.expectCall(
                destinationSynapse,
                abi.encodeWithSelector(
                    InterfaceDestination.acceptAttestation.selector,
                    agentIndex[notary],
                    type(uint256).max,
                    attestation,
                    ra._agentRoot,
                    rs.snapGas()
                )
            );

            (attPayload, agentRoot, snapGas) = inbox.submitSnapshot(snapPayloads[i], snapSignatures[i]);
            assertEq(attPayload, attestation, "Notary: incorrect attestation");
            assertEq(agentRoot, ra._agentRoot, "Notary: incorrect agent root");
            assertEq(keccak256(abi.encodePacked(snapGas)), ra._snapGasHash, "Notary: incorrect snap gas hash");
            // Check attestation getter
            (attPayload, agentRoot, snapGas) = ISnapshotHub(summit).getAttestation(ra.nonce);
            assertEq(attPayload, attestation, "!getAttestation");
            assertEq(agentRoot, ra._agentRoot, "!getAttestation: agent root");
            assertEq(keccak256(abi.encodePacked(snapGas)), ra._snapGasHash, "!getAttestation: gas hash");
            (attPayload, agentRoot, snapGas) = ISnapshotHub(summit).getLatestNotaryAttestation(notary);
            assertEq(attPayload, attestation, "!latestAttestation");
            assertEq(agentRoot, ra._agentRoot, "!latestAttestation: agent root");
            assertEq(keccak256(abi.encodePacked(snapGas)), ra._snapGasHash, "!latestAttestation: gas hash");

            // Check proofs for every State in the Notary snapshot
            for (uint8 j = 0; j < STATES; ++j) {
                bytes32[] memory snapProof = ISnapshotHub(summit).getSnapshotProof(ra.nonce, j);
                // Item to prove is State's "left sub-leaf"
                (bytes32 item,) = rs.states[j].castToState().subLeafs();
                // Item index is twice the state index (since it's a left child)
                assertEq(
                    MerkleMath.proofRoot(2 * j, item, snapProof, SNAPSHOT_TREE_HEIGHT), ra.snapRoot, "!getSnapshotProof"
                );
            }

            // Check latest Notary States
            for (uint32 j = 0; j < STATES; ++j) {
                assertEq(
                    ISnapshotHub(summit).getLatestAgentState(j + 1, notary),
                    rs.states[j].formatState(),
                    "!latestState: notary"
                );
            }
        }

        for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
            (bytes memory snapPayload, bytes memory snapSignature) = ISnapshotHub(summit).getNotarySnapshot(i);
            assertEq(snapPayload, snapPayloads[i], "!payload");
            assertEq(snapSignature, snapSignatures[i], "!signature");
        }

        for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
            address guard = domains[0].agents[i];
            // No Attestations should be saved for Guards
            (bytes memory attPayload, bytes32 agentRoot, uint256[] memory snapGas) =
                ISnapshotHub(summit).getLatestNotaryAttestation(guard);
            assertEq(attPayload, "");
            assertEq(agentRoot, bytes32(0));
            assertEq(snapGas.length, 0);
        }

        // Check global latest state
        checkLatestState();
    }

    function test_getLatestState_empty(uint32 domain) public {
        assertEq(InterfaceSummit(summit).getLatestState(domain), "");
    }

    function checkLatestState() public {
        // Check global latest state
        for (uint32 j = 0; j < STATES; ++j) {
            RawState memory latestState;
            for (uint32 i = 0; i < DOMAIN_AGENTS; ++i) {
                if (guardStates[i][j].nonce > latestState.nonce) {
                    latestState = guardStates[i][j];
                }
            }
            assertEq(InterfaceSummit(summit).getLatestState(j + 1), latestState.formatState(), "!getLatestState");
        }
    }

    // ══════════════════════════════════════════ TESTS: WHILE IN DISPUTE ══════════════════════════════════════════════

    function test_submitSnapshot_revert_notaryInDispute(RawState memory rs) public {
        (uint256 domainId, RawStateIndex memory rsi) = (1, RawStateIndex(0, 1));
        uint32 domain = allDomains[domainId];
        (address guard0, address guard1, address notary) =
            (domains[0].agents[0], domains[0].agents[1], domains[domain].agents[0]);
        // Put Notary 0 and Guard 0 in dispute
        openDispute({guard: guard0, notary: notary});
        // Make sure state nonce is non-zero
        if (rs.nonce == 0) rs.nonce = 1;
        // Create Guard 1 snapshot
        (bytes memory snapPayload, bytes memory guardSig) = createSignedSnapshot(guard1, rs, rsi);
        // Guard 1 submits snapshot
        inbox.submitSnapshot(snapPayload, guardSig);
        // Notary 0 signs the same snapshot
        bytes memory notarySig = signSnapshot(notary, snapPayload);
        vm.expectRevert(NotaryInDispute.selector);
        inbox.submitSnapshot(snapPayload, notarySig);
    }

    function test_submitSnapshot_success_guardInDispute(RawState memory rs) public {
        (uint256 domainId, RawStateIndex memory rsi) = (1, RawStateIndex(0, 1));
        uint32 domain = allDomains[domainId];
        (address guard, address notary) = (domains[0].agents[0], domains[domain].agents[0]);
        // Put Notary 0 and Guard 0 in dispute
        openDispute({guard: guard, notary: notary});
        // Make sure state nonce is non-zero
        if (rs.nonce == 0) rs.nonce = 1;
        (bytes memory snapPayload, bytes memory guardSig) = createSignedSnapshot(guard, rs, rsi);
        // Guard 0 submits snapshot - being in dispute does not interfere with future snapshots
        vm.expectEmit();
        emit SnapshotAccepted(0, guard, snapPayload, guardSig);
        inbox.submitSnapshot(snapPayload, guardSig);
    }

    // ═════════════════════════════════════════ TESTS: NOTARY WON DISPUTE ═════════════════════════════════════════════

    function prepareSubmitSnapshotDisputeTest() internal returns (bytes memory snapPayload, bytes memory notarySig) {
        address notary = domains[DOMAIN_LOCAL].agent;
        address reportGuard = domains[0].agent;
        address snapshotGuard = domains[0].agents[1];

        Random memory random = Random("salt");
        RawSnapshot memory rawSnap = random.nextSnapshot();
        // Another Guard signs the snapshot
        bytes memory guardSignature;
        (snapPayload, guardSignature) = signSnapshot(snapshotGuard, rawSnap);
        notarySig = signSnapshot(notary, snapPayload);
        inbox.submitSnapshot(snapPayload, guardSignature);
        openTestDispute({guardIndex: agentIndex[reportGuard], notaryIndex: agentIndex[notary]});
    }

    /// @dev Resolves test dispute above in favor of the Notary.
    function prepareNotaryWonDisputeTest() internal {
        address notary = domains[DOMAIN_LOCAL].agent;
        address guard = domains[0].agent;
        resolveTestDispute({slashedIndex: agentIndex[guard], rivalIndex: agentIndex[notary]});
    }

    function test_submitSnapshot_revert_notaryWonDisputeTimeout() public {
        (bytes memory snapPayload, bytes memory notarySig) = prepareSubmitSnapshotDisputeTest();
        skip(7 days);
        prepareNotaryWonDisputeTest();
        skip(DISPUTE_TIMEOUT_NOTARY - 1);
        vm.expectRevert(DisputeTimeoutNotOver.selector);
        inbox.submitSnapshot(snapPayload, notarySig);
    }

    function test_submitSnapshot_afterNotaryDisputeTimeout() public {
        address notary = domains[DOMAIN_LOCAL].agent;
        (bytes memory snapPayload, bytes memory notarySig) = prepareSubmitSnapshotDisputeTest();
        skip(7 days);
        prepareNotaryWonDisputeTest();
        skip(DISPUTE_TIMEOUT_NOTARY);
        inbox.submitSnapshot(snapPayload, notarySig);
        (bytes memory snapPayload_,) = ISnapshotHub(summit).getNotarySnapshot(0);
        assertEq(snapPayload_, snapPayload);
    }

    // ═════════════════════════════════════════════════ OVERRIDES ═════════════════════════════════════════════════════

    function localContract() public view override returns (address) {
        return summit;
    }

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