synapsecns/sanguine

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

Summary

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

import {IAgentSecured} from "../../contracts/interfaces/IAgentSecured.sol";
import {InterfaceGasOracle} from "../../contracts/interfaces/InterfaceGasOracle.sol";
import {IStateHub} from "../../contracts/interfaces/IStateHub.sol";
import {
    EthTransferFailed,
    IncorrectDestinationDomain,
    InsufficientEthBalance,
    TipsValueTooLow
} from "../../contracts/libs/Errors.sol";
import {SNAPSHOT_MAX_STATES} from "../../contracts/libs/Constants.sol";
import {TipsLib} from "../../contracts/libs/stack/Tips.sol";

import {InterfaceOrigin} from "../../contracts/Origin.sol";
import {Versioned} from "../../contracts/base/Version.sol";

import {RevertingApp} from "../harnesses/client/RevertingApp.t.sol";
import {console, stdJson} from "forge-std/Script.sol";

import {fakeState, fakeSnapshot} from "../utils/libs/FakeIt.t.sol";
import {Random} from "../utils/libs/Random.t.sol";
import {
    MessageFlag,
    RawAttestation,
    RawBaseMessage,
    RawGasData,
    RawHeader,
    RawMessage,
    RawRequest,
    RawSnapshot,
    RawState,
    RawStateIndex,
    RawTips
} from "../utils/libs/SynapseStructs.t.sol";
import {AgentFlag, GasOracle, Origin, SynapseTest} from "../utils/SynapseTest.t.sol";
import {AgentSecuredTest} from "./base/AgentSecured.t.sol";

// solhint-disable func-name-mixedcase
// solhint-disable no-empty-blocks
// solhint-disable ordering
contract OriginTest is AgentSecuredTest {
    address public sender = makeAddr("Sender");
    address public recipient = makeAddr("Recipient");
    uint32 public period = 1 minutes;
    RawTips public tips = RawTips(0, 0, 0, 0);
    RawRequest public request = RawRequest({gasLimit: 100_000, gasDrop: 0, version: 0});

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

    function test_setupCorrectly() public {
        // Check Agents: currently all Agents are known in LightManager
        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(origin).agentStatus(agent), AgentFlag.Active);
            }
        }
        // Check version
        assertEq(Versioned(origin).version(), LATEST_VERSION, "!version");
    }

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

    function test_cleanSetup(Random memory random) public override {
        uint32 domain = uint32(block.chainid);
        address caller = random.nextAddress();
        address agentManager = random.nextAddress();
        address inbox_ = random.nextAddress();
        address gasOracle_ = address(new GasOracle(DOMAIN_SYNAPSE, random.nextAddress()));

        Origin cleanContract = new Origin(DOMAIN_SYNAPSE, agentManager, inbox_, gasOracle_);
        vm.prank(caller);
        cleanContract.initialize();
        assertEq(cleanContract.owner(), caller, "!owner");
        assertEq(cleanContract.localDomain(), domain, "!localDomain");
        assertEq(cleanContract.agentManager(), agentManager, "!agentManager");
        assertEq(cleanContract.inbox(), inbox_, "!inbox");
        assertEq(cleanContract.gasOracle(), gasOracle_, "!gasOracle");
        assertEq(cleanContract.statesAmount(), 1, "!statesAmount");
    }

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

    function test_sendBaseMessage_revert_blockTimestampOverflow() public {
        vm.warp(2 ** 40);
        vm.prank(sender);
        vm.expectRevert("SafeCast: value doesn't fit in 40 bits");
        InterfaceOrigin(origin).sendBaseMessage(
            DOMAIN_REMOTE, addressToBytes32(recipient), period, request.encodeRequest(), "test content"
        );
    }

    function test_sendBaseMessage_revert_blockNumberOverflow() public {
        vm.roll(2 ** 40);
        vm.prank(sender);
        vm.expectRevert("SafeCast: value doesn't fit in 40 bits");
        InterfaceOrigin(origin).sendBaseMessage(
            DOMAIN_REMOTE, addressToBytes32(recipient), period, request.encodeRequest(), "test content"
        );
    }

    function test_sendBaseMessage_revert_tipsTooLow(RawTips memory minTips, uint256 msgValue) public {
        minTips.boundTips(1 ** 32);
        minTips.floorTips(1);
        msgValue = msgValue % minTips.castToTips().value();
        // Force gasOracle.getMinimumTips(DOMAIN_REMOTE, *, *) to return minTips
        vm.mockCall(
            gasOracle,
            abi.encodeWithSelector(InterfaceGasOracle.getMinimumTips.selector, DOMAIN_REMOTE),
            abi.encode(minTips.encodeTips())
        );
        deal(sender, msgValue);
        vm.expectRevert(TipsValueTooLow.selector);
        vm.prank(sender);
        InterfaceOrigin(origin).sendBaseMessage{value: msgValue}(
            DOMAIN_REMOTE, addressToBytes32(recipient), period, request.encodeRequest(), "test content"
        );
    }

    function test_sendBaseMessage_revert_sameDestination() public {
        vm.expectRevert(IncorrectDestinationDomain.selector);
        vm.prank(sender);
        InterfaceOrigin(origin).sendBaseMessage(
            localDomain(), addressToBytes32(recipient), period, request.encodeRequest(), "test content"
        );
    }

    function test_sendManagementMessage_revert_sameDestination() public {
        vm.expectRevert(IncorrectDestinationDomain.selector);
        vm.prank(localAgentManager());
        InterfaceOrigin(origin).sendManagerMessage(localDomain(), period, "test payload");
    }

    function test_getMinimumTipsValue(
        uint32 destination_,
        uint256 paddedRequest,
        uint256 contentLength,
        RawTips memory minTips
    ) public {
        minTips.boundTips(1 ** 32);
        // Force gasOracle.getMinimumTips(destination_, *, *) to return minTips
        vm.mockCall(
            gasOracle,
            abi.encodeWithSelector(InterfaceGasOracle.getMinimumTips.selector, destination_),
            abi.encode(minTips.encodeTips())
        );
        vm.expectCall(
            address(gasOracle),
            abi.encodeWithSelector(
                InterfaceGasOracle.getMinimumTips.selector, destination_, paddedRequest, contentLength
            )
        );
        assertEq(
            InterfaceOrigin(origin).getMinimumTipsValue(destination_, paddedRequest, contentLength),
            minTips.castToTips().value(),
            "!getMinimumTipsValue"
        );
    }

    function test_sendMessages(RawGasData memory rgd) public {
        // Force gasOracle.getGasData() to return rgd
        vm.mockCall(
            gasOracle, abi.encodeWithSelector(InterfaceGasOracle.getGasData.selector), abi.encode(rgd.encodeGasData())
        );
        uint192 encodedRequest = request.encodeRequest();
        bytes memory content = "test content";
        bytes memory body = RawBaseMessage({
            tips: tips,
            sender: addressToBytes32(sender),
            recipient: addressToBytes32(recipient),
            request: request,
            content: content
        }).formatBaseMessage();
        bytes[] memory messages = new bytes[](MESSAGES);
        bytes32[] memory leafs = new bytes32[](MESSAGES);
        bytes32[] memory roots = new bytes32[](MESSAGES);
        for (uint32 i = 0; i < MESSAGES; ++i) {
            RawMessage memory rm = RawMessage(
                RawHeader({
                    flag: uint8(MessageFlag.Base),
                    origin: DOMAIN_LOCAL,
                    nonce: i + 1,
                    destination: DOMAIN_REMOTE,
                    optimisticPeriod: period
                }),
                body
            );
            messages[i] = rm.formatMessage();
            leafs[i] = rm.castToMessage().leaf();
            insertMessage(leafs[i]);
            roots[i] = getRoot(i + 1);
        }

        for (uint32 i = 0; i < MESSAGES; ++i) {
            // Expect Origin Events
            RawState memory rs = RawState({
                root: roots[i],
                origin: DOMAIN_LOCAL,
                nonce: i + 1,
                blockNumber: uint40(block.number),
                timestamp: uint40(block.timestamp),
                gasData: rgd
            });
            bytes memory state = rs.formatState();
            vm.expectEmit();
            emit StateSaved(state);
            vm.expectEmit();
            emit Sent(leafs[i], i + 1, DOMAIN_REMOTE, messages[i]);
            vm.expectCall(gasOracle, abi.encodeCall(InterfaceGasOracle.updateGasData, (DOMAIN_REMOTE)));
            vm.prank(sender);
            (uint32 messageNonce, bytes32 messageHash) = InterfaceOrigin(origin).sendBaseMessage(
                DOMAIN_REMOTE, addressToBytes32(recipient), period, encodedRequest, content
            );
            // Check return values
            assertEq(messageNonce, i + 1, "!messageNonce");
            assertEq(messageHash, leafs[i], "!messageHash");
            skipBlock();
        }
    }

    function test_states(RawGasData memory rgd) public {
        IStateHub hub = IStateHub(origin);
        // Check initial States
        assertEq(hub.statesAmount(), 1, "!initial statesAmount");
        // Initial state was saved "1 block ago"
        RawState memory rs;
        rs.origin = DOMAIN_LOCAL;
        rs.blockNumber = uint40(block.number - 1);
        rs.timestamp = uint40(block.timestamp - BLOCK_TIME);
        bytes memory state = rs.formatState();
        assertEq(hub.suggestState(0), state, "!state: 0");
        assertEq(hub.suggestState(0), hub.suggestLatestState(), "!latest state: 0");
        // Send some messages
        test_sendMessages(rgd);
        // Check saved States
        assertEq(hub.statesAmount(), MESSAGES + 1, "!statesAmount");
        assertEq(hub.suggestState(0), state, "!suggestState: 0");
        for (uint32 i = 0; i < MESSAGES; ++i) {
            rs.nonce += 1;
            rs.root = getRoot(rs.nonce);
            rs.blockNumber += 1;
            rs.timestamp += uint40(BLOCK_TIME);
            rs.gasData = rgd;
            state = rs.formatState();
            assertEq(hub.suggestState(i + 1), state, "!suggestState");
        }
        assertEq(hub.suggestLatestState(), state, "!suggestLatestState");
    }

    function test_verifySnapshot_valid(uint32 nonce, RawGasData memory rgd, RawStateIndex memory rsi) public {
        // Use empty mutation mask
        test_verifySnapshot_existingNonce(nonce, 0, rgd, rsi);
    }

    function test_verifySnapshot_existingNonce(
        uint32 nonce,
        uint256 mask,
        RawGasData memory rgd,
        RawStateIndex memory rsi
    ) public boundIndex(rsi) {
        (bool isValid, RawState memory rs) = _prepareExistingState(rgd, nonce, mask);
        _verifySnapshot(rs, isValid, rsi);
    }

    function test_verifySnapshot_unknownNonce(RawState memory rs, RawStateIndex memory rsi) public boundIndex(rsi) {
        // Restrict nonce to non-existing ones
        rs.nonce = uint32(bound(rs.nonce, MESSAGES + 1, type(uint32).max));
        rs.origin = DOMAIN_LOCAL;
        // Remaining fields are fuzzed
        _verifySnapshot(rs, false, rsi);
    }

    function test_verifyAttestation_valid(Random memory random, uint32 nonce) public {
        test_verifyAttestation_existingNonce(random, nonce, 0);
    }

    function test_verifyAttestation_existingNonce(Random memory random, uint32 nonce, uint256 mask) public {
        (bool isValid, RawState memory rs) = _prepareExistingState(random.nextGasData(), nonce, mask);
        _verifyAttestation(random, rs, isValid);
    }

    function test_verifyAttestation_unknownNonce(Random memory random, RawState memory rs) public {
        // Restrict nonce to non-existing ones
        rs.nonce = uint32(bound(rs.nonce, MESSAGES + 1, type(uint32).max));
        rs.origin = DOMAIN_LOCAL;
        // Remaining fields are fuzzed
        _verifyAttestation(random, rs, false);
    }

    function test_verifyAttestationWithProof_valid(Random memory random, uint32 nonce) public {
        // Use empty mutation mask
        test_verifyAttestationWithProof_existingNonce(random, nonce, 0);
    }

    function test_verifyAttestationWithProof_existingNonce(Random memory random, uint32 nonce, uint256 mask) public {
        (bool isValid, RawState memory rs) = _prepareExistingState(random.nextGasData(), nonce, mask);
        _verifyAttestationWithProof(random, rs, isValid);
    }

    function test_verifyAttestationWithProof_unknownNonce(Random memory random, RawState memory rs) public {
        // Restrict nonce to non-existing ones
        rs.nonce = uint32(bound(rs.nonce, MESSAGES + 1, type(uint32).max));
        rs.origin = DOMAIN_LOCAL;
        // Remaining fields are fuzzed
        _verifyAttestationWithProof(random, rs, false);
    }

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

    function _prepareExistingState(RawGasData memory rgd, uint32 nonce, uint256 mask)
        internal
        returns (bool isValid, RawState memory rs)
    {
        uint40 initialBN = uint40(block.number - 1);
        uint40 initialTS = uint40(block.timestamp - BLOCK_TIME);
        test_sendMessages(rgd);
        // State is valid if and only if all three fields match
        isValid = mask & 7 == 0;
        // Restrict nonce to existing ones
        nonce = uint32(bound(nonce, 0, MESSAGES));
        rs.origin = DOMAIN_LOCAL;
        rs.nonce = nonce;
        rs.root = getRoot(nonce) ^ bytes32(mask & 1);
        rs.blockNumber = (initialBN + nonce) ^ uint40(mask & 2);
        rs.timestamp = uint40(initialTS + nonce * BLOCK_TIME) ^ uint40(mask & 4);
    }

    function _prepareAttestation(Random memory random, RawState memory rawState)
        internal
        returns (
            uint32 domain,
            address notary,
            RawStateIndex memory rsi,
            bytes memory snapshot,
            RawAttestation memory ra
        )
    {
        // Pick random domain expect for 0
        uint256 domainIndex = bound(random.nextUint256(), 1, allDomains.length - 1);
        domain = allDomains[domainIndex];
        // Pick random Notary
        uint256 notaryIndex = bound(random.nextUint256(), 0, DOMAIN_AGENTS - 1);
        notary = domains[domain].agents[notaryIndex];
        // Fuzz the position of invalid state in the snapshot
        rsi = random.nextStateIndex();
        RawSnapshot memory rawSnap = fakeSnapshot(rawState, rsi);
        snapshot = rawSnap.formatSnapshot();
        // Use random metadata
        ra = random.nextAttestation(rawSnap, random.nextUint32());
        // Save snapshot for Snapshot Proof generation
        acceptSnapshot(rawSnap);
    }

    function _verifyAttestation(Random memory random, RawState memory rawState, bool isValid) internal {
        (uint32 domain, address notary, RawStateIndex memory rsi, bytes memory snapshot, RawAttestation memory ra) =
            _prepareAttestation(random, rawState);
        bytes memory state = rawState.formatState();
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);
        if (!isValid) {
            // Expect Events to be emitted
            vm.expectEmit(true, true, true, true);
            emit InvalidStateWithAttestation(rsi.stateIndex, state, 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(
            lightInbox.verifyStateWithAttestation(rsi.stateIndex, snapshot, attPayload, attSig), isValid, "!returnValue"
        );
        if (isValid) {
            assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't");
        }
    }

    function _verifyAttestationWithProof(Random memory random, RawState memory rawState, bool isValid) internal {
        (uint32 domain, address notary, RawStateIndex memory rsi,, RawAttestation memory ra) =
            _prepareAttestation(random, rawState);
        bytes32[] memory snapProof = genSnapshotProof(rsi.stateIndex);
        bytes memory state = rawState.formatState();
        (bytes memory attPayload, bytes memory attSig) = signAttestation(notary, ra);
        if (!isValid) {
            // Expect Events to be emitted
            vm.expectEmit(true, true, true, true);
            emit InvalidStateWithAttestation(rsi.stateIndex, state, 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(
            lightInbox.verifyStateWithSnapshotProof(rsi.stateIndex, state, snapProof, attPayload, attSig),
            isValid,
            "!returnValue"
        );
        if (isValid) {
            assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't");
        }
    }

    function _verifySnapshot(RawState memory rawState, bool isValid, RawStateIndex memory rsi) internal {
        address notary = domains[DOMAIN_REMOTE].agent;
        RawSnapshot memory rawSnap = fakeSnapshot(rawState, rsi);
        (bytes memory snapPayload, bytes memory snapSig) = signSnapshot(notary, rawSnap);
        vm.recordLogs();
        if (!isValid) {
            // Expect Events to be emitted
            vm.expectEmit(true, true, true, true);
            emit InvalidStateWithSnapshot(rsi.stateIndex, snapPayload, snapSig);
            // TODO: check that anyone could make the call
            expectStatusUpdated(AgentFlag.Fraudulent, DOMAIN_REMOTE, notary);
            expectDisputeResolved(0, notary, address(0), address(this));
        }
        assertEq(lightInbox.verifyStateWithSnapshot(rsi.stateIndex, snapPayload, snapSig), isValid, "!returnValue");
        if (isValid) {
            assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't");
        }
        _verifyStateReport(rawState, isValid);
    }

    function _verifyStateReport(RawState memory rawState, bool isStateValid) internal {
        // Report is valid only if reported state is invalid
        bool isValid = !isStateValid;
        address guard = domains[0].agent;
        (bytes memory statePayload, bytes memory srSig) = signStateReport(guard, rawState);
        if (!isValid) {
            // Expect Events to be emitted
            vm.expectEmit(true, true, true, true);
            emit InvalidStateReport(statePayload, srSig);
            // TODO: check that anyone could make the call
            expectStatusUpdated(AgentFlag.Fraudulent, 0, guard);
            expectDisputeResolved(0, guard, address(0), address(this));
        }
        vm.recordLogs();
        assertEq(lightInbox.verifyStateReport(statePayload, srSig), isValid, "!returnValue");
        if (isValid) {
            assertEq(vm.getRecordedLogs().length, 0, "Emitted logs when shouldn't");
        }
    }

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

    function test_withdrawTips(uint256 amount) public {
        vm.deal(origin, amount);
        vm.expectEmit(origin);
        emit TipWithdrawalCompleted(recipient, amount);
        vm.prank(address(lightManager));
        InterfaceOrigin(origin).withdrawTips(recipient, amount);
        assertEq(recipient.balance, amount);
    }

    function test_remoteWithdrawTips_revert_insufficientBalance(uint256 balance, uint256 amount) public {
        amount = bound(amount, 1, type(uint256).max);
        balance = balance % amount;
        vm.deal(origin, balance);
        vm.expectRevert(InsufficientEthBalance.selector);
        vm.prank(address(lightManager));
        InterfaceOrigin(origin).withdrawTips(recipient, amount);
    }

    function test_withdrawTips_revert_recipientReverted(uint256 amount) public {
        address revertingRecipient = address(new RevertingApp());
        vm.deal(origin, amount);
        vm.expectRevert(EthTransferFailed.selector);
        vm.prank(address(lightManager));
        InterfaceOrigin(origin).withdrawTips(revertingRecipient, amount);
    }

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

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

    /// @notice Returns address of the tested contract
    function localContract() public view override returns (address) {
        return localOrigin();
    }
}