synapsecns/sanguine

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

Summary

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

import {DISPUTE_TIMEOUT_NOTARY} from "../../../contracts/libs/Constants.sol";
import {
    AlreadyExecuted,
    AlreadyFailed,
    DisputeTimeoutNotOver,
    GasLimitTooLow,
    GasSuppliedTooLow,
    IncorrectDestinationDomain,
    IncorrectOriginDomain,
    IncorrectMagicValue,
    IncorrectSnapshotRoot,
    MessageOptimisticPeriod,
    NotaryInDispute
} from "../../../contracts/libs/Errors.sol";
import {AgentFlag} from "../../../contracts/libs/Structures.sol";
import {IExecutionHub} from "../../../contracts/interfaces/IExecutionHub.sol";
import {IStatementInbox} from "../../../contracts/interfaces/IStatementInbox.sol";
import {SNAPSHOT_MAX_STATES} from "../../../contracts/libs/memory/Snapshot.sol";
import {MessageStatus} from "../../../contracts/libs/Structures.sol";

import {ReentrantApp} from "../../harnesses/client/ReentrantApp.t.sol";
import {RevertingApp} from "../../harnesses/client/RevertingApp.t.sol";
import {MessageRecipientMock} from "../../mocks/client/MessageRecipientMock.t.sol";

import {fakeSnapshot} from "../../utils/libs/FakeIt.t.sol";
import {Random} from "../../utils/libs/Random.t.sol";
import {
    ReceiptLib,
    MessageFlag,
    RawAttestation,
    RawBaseMessage,
    RawCallData,
    RawExecReceipt,
    RawHeader,
    RawMessage,
    RawSnapshot,
    RawState,
    RawStateIndex,
    RawTips
} from "../../utils/libs/SynapseStructs.t.sol";
import {AgentSecuredTest} from "../base/AgentSecured.t.sol";

// solhint-disable func-name-mixedcase
// solhint-disable no-empty-blocks
// solhint-disable ordering
abstract contract ExecutionHubTest is AgentSecuredTest {
    address internal recipient;
    address internal executor;
    address internal executorNew;

    uint8 internal cachedStateIndex;

    bytes internal msgPayload;
    bytes32 internal msgLeaf;

    function setUp() public virtual override {
        super.setUp();
        recipient = address(new MessageRecipientMock());
        executor = makeAddr("Executor");
        executorNew = makeAddr("Executor New");
    }

    /// @notice Prevents this contract from being included in the coverage report
    function testExecutionHub() external {}

    // ═══════════════════════════════════════ TESTS: EXECUTE BASE MESSAGES ════════════════════════════════════════════

    function test_execute_base(
        RawBaseMessage memory rbm,
        RawHeader memory rh,
        SnapshotMock memory sm,
        uint32 timePassed,
        uint64 gasLimit
    ) public {
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (bytes32 snapRoot, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is over
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        gasLimit = uint64(bound(gasLimit, rbm.request.gasLimit, 2_000_000));
        // receiveBaseMessage(origin, nonce, sender, proofMaturity, version, message)
        bytes memory expectedCall = abi.encodeWithSelector(
            MessageRecipientMock.receiveBaseMessage.selector,
            rh.origin,
            rh.nonce,
            rbm.sender,
            timePassed,
            rbm.request.version,
            rbm.content
        );
        // expectCall(address callee, uint256 msgValue, uint64 gas, bytes calldata data)
        vm.expectCall(recipient, 0, gasLimit, expectedCall);
        vm.expectEmit();
        emit Executed(rh.origin, msgLeaf, true);
        vm.prank(executor);
        msgLeaf = rm.castToMessage().leaf();
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, gasLimit);
        bytes memory rcptPayload =
            verify_messageStatus(msgLeaf, snapRoot, sm.rsi.stateIndex, MessageStatus.Success, executor, executor);
        verify_receipt_valid(rcptPayload);
    }

    function test_execute_base_recipientEOA(Random memory random) public {
        recipient = random.nextAddress();
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (bytes32 snapRoot, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is over
        uint32 timePassed = random.nextUint32();
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        vm.expectEmit();
        emit Executed(rh.origin, msgLeaf, false);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
        bytes memory rcptPayloadFirst =
            verify_messageStatus(msgLeaf, snapRoot, sm.rsi.stateIndex, MessageStatus.Failed, executor, address(0));
        verify_receipt_valid(rcptPayloadFirst);
    }

    function test_execute_base_recipientReverted_thenSuccess(Random memory random) public {
        recipient = address(new RevertingApp());
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (bytes32 snapRoot, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is over
        uint32 timePassed = random.nextUint32();
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        vm.expectEmit();
        emit Executed(rh.origin, msgLeaf, false);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
        bytes memory rcptPayloadFirst =
            verify_messageStatus(msgLeaf, snapRoot, sm.rsi.stateIndex, MessageStatus.Failed, executor, address(0));
        verify_receipt_valid(rcptPayloadFirst);
        // Retry the same failed message
        RevertingApp(payable(recipient)).toggleRevert(false);
        vm.expectEmit();
        emit Executed(rh.origin, msgLeaf, true);
        vm.prank(executorNew);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
        bytes memory rcptPayloadSecond =
            verify_messageStatus(msgLeaf, snapRoot, sm.rsi.stateIndex, MessageStatus.Success, executor, executorNew);
        // Both receipts (historical and current) should be valid
        verify_receipt_valid(rcptPayloadFirst);
        verify_receipt_valid(rcptPayloadSecond);
        cachedStateIndex = uint8(sm.rsi.stateIndex);
    }

    function test_execute_base_recipientReverted_twice(Random memory random) public {
        recipient = address(new RevertingApp());
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (bytes32 snapRoot, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is over
        uint32 timePassed = random.nextUint32();
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        vm.expectEmit();
        emit Executed(rh.origin, msgLeaf, false);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
        bytes memory rcptPayload =
            verify_messageStatus(msgLeaf, snapRoot, sm.rsi.stateIndex, MessageStatus.Failed, executor, address(0));
        verify_receipt_valid(rcptPayload);
        // Retry the same failed message
        vm.expectRevert(AlreadyFailed.selector);
        vm.prank(executorNew);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
    }

    function test_execute_base_revert_alreadyExecuted(Random memory random) public {
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is over
        uint32 timePassed = random.nextUint32();
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
        vm.expectRevert(AlreadyExecuted.selector);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
    }

    function test_execute_base_revert_notaryInDispute(Random memory random) public {
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = prepareExecution(sm);
        // initiate dispute
        openDispute({guard: domains[0].agent, notary: domains[DOMAIN_LOCAL].agent});
        // Make sure that optimistic period is over
        uint32 timePassed = random.nextUint32();
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        vm.expectRevert(NotaryInDispute.selector);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
        verify_messageStatusNone(msgLeaf);
    }

    function test_execute_base_revert_notaryWonDisputeTimeout() public {
        address notary = domains[DOMAIN_LOCAL].agent;
        address guard = domains[0].agent;
        Random memory random = Random("Random Salt 1");
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = prepareExecution(sm);
        // initiate dispute
        openTestDispute({guardIndex: agentIndex[guard], notaryIndex: agentIndex[notary]});
        // Make sure that optimistic period is over
        skip(rh.optimisticPeriod);
        resolveTestDispute({slashedIndex: agentIndex[guard], rivalIndex: agentIndex[notary]});
        skip(DISPUTE_TIMEOUT_NOTARY - 1);
        vm.expectRevert(DisputeTimeoutNotOver.selector);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
    }

    function test_execute_base_afterNotaryDisputeTimeout() public {
        address notary = domains[DOMAIN_LOCAL].agent;
        address guard = domains[0].agent;
        Random memory random = Random("Random Salt 2");
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (bytes32 snapRoot, bytes32[] memory snapProof) = prepareExecution(sm);
        // initiate dispute
        openTestDispute({guardIndex: agentIndex[guard], notaryIndex: agentIndex[notary]});
        // Make sure that optimistic period is over
        skip(rh.optimisticPeriod);
        resolveTestDispute({slashedIndex: agentIndex[guard], rivalIndex: agentIndex[notary]});
        skip(DISPUTE_TIMEOUT_NOTARY);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
        verify_messageStatus(msgLeaf, snapRoot, sm.rsi.stateIndex, MessageStatus.Success, executor, executor);
    }

    function test_execute_base_revert_snapRootUnknown(Random memory random) public {
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = createSnapshotProof(sm);
        // Make sure that optimistic period is over
        uint32 timePassed = random.nextUint32();
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        vm.expectRevert(IncorrectSnapshotRoot.selector);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
        verify_messageStatusNone(msgLeaf);
    }

    function test_execute_base_revert_optimisticPeriodNotOver(Random memory random) public {
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        vm.assume(rh.optimisticPeriod != 0);
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is NOT over
        uint32 timePassed = random.nextUint32() % rh.optimisticPeriod;
        skip(timePassed);
        vm.expectRevert(MessageOptimisticPeriod.selector);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
        verify_messageStatusNone(msgLeaf);
    }

    function test_execute_base_revert_optimisticPeriodMinus1Second() public {
        Random memory random = Random("random salt");
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        vm.assume(rh.optimisticPeriod != 0);
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is NOT over
        uint32 timePassed = rh.optimisticPeriod - 1;
        skip(timePassed);
        vm.expectRevert(MessageOptimisticPeriod.selector);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
    }

    function test_execute_base_revert_gasLimitTooLow(Random memory random) public {
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is over
        uint32 timePassed = random.nextUint32();
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        // Make sure gas limit is lower than requested
        uint64 gasLimit = random.nextUint64() % rbm.request.gasLimit;
        vm.expectRevert(GasLimitTooLow.selector);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, gasLimit);
        verify_messageStatusNone(msgLeaf);
    }

    function test_execute_base_revert_gasSuppliedTooLow(Random memory random) public {
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is over
        uint32 timePassed = random.nextUint32();
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        vm.expectRevert(GasSuppliedTooLow.selector);
        vm.prank(executor);
        // Limit amount of gas for the whole call
        testedEH().execute{gas: rbm.request.gasLimit + 20_000}(
            msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit
        );
        verify_messageStatusNone(msgLeaf);
    }

    function test_execute_base_revert_wrongDestination(Random memory random, uint32 destination_) public {
        vm.assume(destination_ != localDomain());
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, destination_);
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is over
        uint32 timePassed = random.nextUint32();
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        vm.expectRevert(IncorrectDestinationDomain.selector);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
        verify_messageStatusNone(msgLeaf);
    }

    function test_execute_base_revert_originSameDomain() public {
        RawBaseMessage memory rbm;
        rbm.sender = rbm.recipient = addressToBytes32(recipient);
        RawMessage memory rm;
        rm.header.flag = uint8(MessageFlag.Base);
        rm.header.origin = localDomain();
        rm.header.destination = localDomain();
        rm.header.nonce = 1;
        rm.header.optimisticPeriod = 1 seconds;
        rm.body = rbm.formatBaseMessage();
        msgPayload = rm.formatMessage();
        vm.expectRevert(IncorrectOriginDomain.selector);
        testedEH().execute(msgPayload, new bytes32[](0), new bytes32[](0), 0, 0);
    }

    function test_execute_base_revert_reentrancy(Random memory random) public {
        recipient = address(new ReentrantApp());
        // Create some simple data
        (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm) = createDataRevertTest(random);
        // Create messages and get origin proof
        RawMessage memory rm = createBaseMessages(rbm, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (bytes32 snapRoot, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is over
        uint32 timePassed = random.nextUint32();
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        ReentrantApp(recipient).prepare(msgPayload, originProof, snapProof, sm.rsi.stateIndex);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, rbm.request.gasLimit);
        // We allow message recipient to revert, which is what supposed to happen if they try to reenter
        verify_messageStatus(msgLeaf, snapRoot, sm.rsi.stateIndex, MessageStatus.Failed, executor, address(0));
    }

    // ══════════════════════════════════════ TESTS: EXECUTE MANAGER MESSAGES ══════════════════════════════════════════

    function test_execute_manager(RawHeader memory rh, SnapshotMock memory sm, uint32 timePassed, uint64 gasLimit)
        public
    {
        // Create messages and get origin proof
        RawMessage memory rm = createManagerMessages(lightManager.remoteMockFunc.selector, rh, localDomain());
        msgPayload = rm.formatMessage();
        msgLeaf = rm.castToMessage().leaf();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (bytes32 snapRoot, bytes32[] memory snapProof) = prepareExecution(sm);
        // Make sure that optimistic period is over
        timePassed = uint32(bound(timePassed, rh.optimisticPeriod, rh.optimisticPeriod + 1 days));
        skip(timePassed);
        // expectCall(address callee, bytes calldata data)
        // remoteMockFunc(msgOrigin, proofMaturity, data); data = rh.nonce
        vm.expectCall(
            localAgentManager(),
            abi.encodeWithSelector(bondingManager.remoteMockFunc.selector, rh.origin, timePassed, rh.nonce)
        );
        vm.expectEmit();
        emit Executed(rh.origin, msgLeaf, true);
        vm.prank(executor);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, gasLimit);
        verify_messageStatus(msgLeaf, snapRoot, sm.rsi.stateIndex, MessageStatus.Success, executor, executor);
    }

    function test_execute_manager_revert_incorrectMagicValue() public {
        // Use empty values - these would be filled later in test preparation
        RawHeader memory rh;
        SnapshotMock memory sm;
        // Create messages and get origin proof
        RawMessage memory rm = createManagerMessages(lightManager.sensitiveMockFunc.selector, rh, localDomain());
        msgPayload = rm.formatMessage();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = prepareExecution(sm);
        vm.expectRevert(IncorrectMagicValue.selector);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, 0);
    }

    function test_execute_manager_revert_noMagicValue() public {
        // Use empty values - these would be filled later in test preparation
        RawHeader memory rh;
        SnapshotMock memory sm;
        // Create messages and get origin proof
        RawMessage memory rm = createManagerMessages(lightManager.sensitiveMockFuncVoid.selector, rh, localDomain());
        msgPayload = rm.formatMessage();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = prepareExecution(sm);
        vm.expectRevert(IncorrectMagicValue.selector);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, 0);
    }

    function test_execute_manager_revert_magicMoreThan32Bytes() public {
        // Use empty values - these would be filled later in test preparation
        RawHeader memory rh;
        SnapshotMock memory sm;
        // Create messages and get origin proof
        RawMessage memory rm =
            createManagerMessages(lightManager.sensitiveMockFuncOver32Bytes.selector, rh, localDomain());
        msgPayload = rm.formatMessage();
        bytes32[] memory originProof = getLatestProof(rh.nonce - 1);
        // Create snapshot proof
        adjustSnapshot(sm);
        (, bytes32[] memory snapProof) = prepareExecution(sm);
        vm.expectRevert(IncorrectMagicValue.selector);
        testedEH().execute(msgPayload, originProof, snapProof, sm.rsi.stateIndex, 0);
    }

    // ══════════════════════════════════════════ TESTS: INVALID RECEIPTS ══════════════════════════════════════════════

    function test_verifyReceipt_invalid_msgStatusNone(RawExecReceipt memory re) public {
        vm.assume(testedEH().messageStatus(re.messageHash) == MessageStatus.None);
        vm.assume(re.origin != localDomain());
        re.destination = localDomain();
        verify_receipt_invalid(re);
    }

    function test_verifyReceipt_invalid_msgStatusSuccess(uint256 mask) public {
        test_execute_base_recipientReverted_thenSuccess(Random(bytes32(mask)));
        RawExecReceipt memory re = RawExecReceipt({
            origin: DOMAIN_REMOTE,
            destination: localDomain(),
            messageHash: getLeaf(0),
            snapshotRoot: getSnapshotRoot(),
            stateIndex: cachedStateIndex,
            attNotary: domains[DOMAIN_LOCAL].agent,
            firstExecutor: executor,
            finalExecutor: executorNew
        });
        // Check that data we start with is valid. Use require() to break the test execution early.
        require(testedEH().isValidReceipt(re.formatReceipt()), "Incorrect initial receipt data");
        RawExecReceipt memory mre = re.modifyReceipt(mask);
        verify_receipt_invalid(mre);
    }

    // ═════════════════════════════════════════════════ VERIFIERS ═════════════════════════════════════════════════════

    function verify_messageStatusNone(bytes32 messageHash) public {
        verify_messageStatus(messageHash, bytes32(0), 0, MessageStatus.None, address(0), address(0));
    }

    function verify_messageStatus(
        bytes32 messageHash,
        bytes32 snapRoot,
        uint8 stateIndex,
        MessageStatus flag,
        address firstExecutor,
        address finalExecutor
    ) public returns (bytes memory rcptPayload) {
        MessageStatus flag_ = testedEH().messageStatus(messageHash);
        assertEq(uint8(flag_), uint8(flag), "!flag");
        rcptPayload = testedEH().messageReceipt(messageHash);
        if (flag == MessageStatus.None) {
            assertEq(rcptPayload.length, 0, "!receipt: empty");
        } else {
            address attNotary = domains[DOMAIN_LOCAL].agent;
            assertEq(
                rcptPayload,
                ReceiptLib.formatReceipt(
                    DOMAIN_REMOTE,
                    localDomain(),
                    messageHash,
                    snapRoot,
                    uint8(stateIndex),
                    attNotary,
                    firstExecutor,
                    finalExecutor
                )
            );
        }
    }

    function verify_receipt_valid(bytes memory rcptPayload) public {
        assertTrue(testedEH().isValidReceipt(rcptPayload));
        address notary = domains[DOMAIN_LOCAL].agent;
        bytes memory rcptSignature = signReceipt(notary, rcptPayload);
        vm.recordLogs();
        assertTrue(IStatementInbox(localInbox()).verifyReceipt(rcptPayload, rcptSignature));
        assertEq(vm.getRecordedLogs().length, 0);
    }

    function verify_receipt_invalid(RawExecReceipt memory re) public {
        bytes memory rcptPayload = re.formatReceipt();
        assertFalse(testedEH().isValidReceipt(rcptPayload));
        address notary = domains[DOMAIN_LOCAL].agent;
        bytes memory rcptSignature = signReceipt(notary, rcptPayload);
        // TODO: check that anyone could make the call
        expectStatusUpdated(AgentFlag.Fraudulent, DOMAIN_LOCAL, notary);
        expectDisputeResolved(0, notary, address(0), address(this));
        // expectAgentSlashed(localDomain(), notary, address(this));
        assertFalse(IStatementInbox(localInbox()).verifyReceipt(rcptPayload, rcptSignature));
    }

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

    /// @notice Prepares execution of the created messages
    function prepareExecution(SnapshotMock memory sm)
        public
        virtual
        returns (bytes32 snapRoot, bytes32[] memory snapProof);

    function createBaseMessages(RawBaseMessage memory rbm, RawHeader memory rh, uint32 destination_)
        public
        returns (RawMessage memory rm)
    {
        adjustHeader(rh, destination_);
        rbm.recipient = addressToBytes32(recipient);
        // Set sensible limitations for tips/request
        rbm.tips.boundTips(2 ** 32);
        rbm.request.gasLimit = uint64(bound(rbm.request.gasLimit, 50_000, 200_000));
        rh.flag = uint8(MessageFlag.Base);
        rm = RawMessage(rh, rbm.formatBaseMessage());
        createMessages(rh.nonce, rm);
    }

    function createManagerMessages(bytes4 selector, RawHeader memory rh, uint32 destination_)
        public
        returns (RawMessage memory rm)
    {
        adjustHeader(rh, destination_);
        RawCallData memory rcd = RawCallData({selector: selector, args: abi.encode(rh.nonce)});
        rh.flag = uint8(MessageFlag.Manager);
        rm = RawMessage(rh, rcd.formatCallData());
        createMessages(rh.nonce, rm);
    }

    function createMessages(uint32 msgNonce, RawMessage memory rm) public {
        for (uint32 nonce = 1; nonce <= MESSAGES; ++nonce) {
            if (nonce == msgNonce) {
                insertMessage(rm.castToMessage().leaf());
            } else {
                insertMessage(keccak256(abi.encode("Mocked payload", nonce)));
            }
        }
    }

    /// @notice Creates some simple data for the revert test
    function createDataRevertTest(Random memory random)
        public
        pure
        returns (RawBaseMessage memory rbm, RawHeader memory rh, SnapshotMock memory sm)
    {
        rbm.sender = random.next();
        rbm.content = "Test content";
        rbm.tips = RawTips(1, 1, 1, 1);
        rh.nonce = 1;
        rh.optimisticPeriod = random.nextUint32();
        sm = SnapshotMock(random.nextState(), RawStateIndex(random.nextUint8(), random.nextUint256()));
        sm.rsi.boundStateIndex();
    }

    /// @notice Sets realistic values for the message header
    function adjustHeader(RawHeader memory rh, uint32 destination_) public view {
        rh.origin = DOMAIN_REMOTE;
        rh.nonce = uint32(bound(rh.nonce, 1, MESSAGES));
        rh.destination = destination_;
        rh.optimisticPeriod = rh.optimisticPeriod % 1 days;
    }

    function adjustState(RawState memory rs) public view {
        rs.origin = DOMAIN_REMOTE;
        rs.nonce = MESSAGES;
        rs.root = getRoot(rs.nonce);
    }

    function adjustSnapshot(SnapshotMock memory sm) public view {
        adjustState(sm.rs);
        sm.rsi.boundStateIndex();
    }

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

    /// @notice Returns tested contract as IExecutionHub
    function testedEH() public view returns (IExecutionHub) {
        return IExecutionHub(localContract());
    }
}