pragma solidity 0.8.15;

import { InvariantTest } from "forge-std/InvariantTest.sol";
import { StdUtils } from "forge-std/StdUtils.sol";
import { Vm } from "forge-std/Vm.sol";
import { OptimismPortal } from "../../L1/OptimismPortal.sol";
import { L1CrossDomainMessenger } from "../../L1/L1CrossDomainMessenger.sol";
import { Messenger_Initializer } from "../CommonTest.t.sol";
import { Types } from "../../libraries/Types.sol";
import { Predeploys } from "../../libraries/Predeploys.sol";
import { Encoding } from "../../libraries/Encoding.sol";
import { Hashing } from "../../libraries/Hashing.sol";

contract RelayActor is StdUtils {
    // Storage slot of the l2Sender
    uint256 constant senderSlotIndex = 50;

    uint256 public numHashes;
    bytes32[] public hashes;
    bool public reverted = false;

    OptimismPortal op;
    L1CrossDomainMessenger xdm;
    Vm vm;

    constructor(
        OptimismPortal _op,
        L1CrossDomainMessenger _xdm,
        Vm _vm
    ) {
        op = _op;
        xdm = _xdm;
        vm = _vm;
    }

    /**
     * Relays a message to the `L1CrossDomainMessenger` with a random `version`, `_minGasLimit`
     * and `_message`.
     */
    function relay(
        uint16 _version,
        uint32 _minGasLimit,
        bytes memory _message
    ) external {
        address target = address(0x04); // ID precompile
        address sender = Predeploys.L2_CROSS_DOMAIN_MESSENGER;

        // set the value of op.l2Sender() to be the L2 Cross Domain Messenger.
        vm.store(address(op), bytes32(senderSlotIndex), bytes32(abi.encode(sender)));

        // Restrict `_minGasLimit` to a number in the range of the block gas limit.
        _minGasLimit = uint32(bound(_minGasLimit, 0, block.gaslimit));

        // Restrict version to the range of [0, 1]
        _version = _version % 2;

        // Compute the cross domain message hash and store it in `hashes`.
        // The `relayMessage` function will always encode the message as a version 1
        // message after checking that the V0 hash has not already been relayed.
        bytes32 _hash = Hashing.hashCrossDomainMessageV1(
            Encoding.encodeVersionedNonce(0, _version),
            sender,
            target,
            0, // value
            _minGasLimit,
            _message
        );

        // Act as the optimism portal and call `relayMessage` on the `L1CrossDomainMessenger` with
        // the outer min gas limit.
        vm.startPrank(address(op));
        vm.expectCall(target, _message);
        try
            xdm.relayMessage{ gas: xdm.baseGas(_message, _minGasLimit) }(
                Encoding.encodeVersionedNonce(0, _version),
                sender,
                target,
                0, // value
                _minGasLimit,
                _message
            )
        {} catch {
            // If any of these calls revert, set `reverted` to true to fail the invariant test.
            // NOTE: This is to get around forge's invariant fuzzer ignoring reverted calls
            // to this function.
            reverted = true;
        }
        vm.stopPrank();

        hashes.push(_hash);
        numHashes += 1;
    }
}

contract XDM_MinGasLimits is Messenger_Initializer, InvariantTest {
    RelayActor actor;

    function setUp() public override {
        // Set up the `L1CrossDomainMessenger` and `OptimismPortal` contracts.
        super.setUp();

        // Deploy a relay actor
        actor = new RelayActor(op, L1Messenger, vm);

        // Target the `RelayActor` contract
        targetContract(address(actor));

        // Target the actor's `relay` function
        bytes4[] memory selectors = new bytes4[](1);
        selectors[0] = actor.relay.selector;
        targetSelector(FuzzSelector({ addr: address(actor), selectors: selectors }));
    }

    /**
     * @custom:invariant A call to `relayMessage` should never revert if at least the proper minimum
     * gas limits are supplied.
     *
     * There are two minimum gas limits here:
     *
     * - The outer min gas limit is for the call from the `OptimismPortal` to the
     * `L1CrossDomainMessenger`,  and it can be retrieved by calling the xdm's `baseGas` function
     * with the `message` and inner limit.
     *
     * - The inner min gas limit is for the call from the `L1CrossDomainMessenger` to the target
     * contract.
     */
    function invariant_minGasLimits() public {
        uint256 length = actor.numHashes();
        for (uint256 i = 0; i < length; ++i) {
            bytes32 hash = actor.hashes(i);
            // the message hash is in the successfulMessages mapping
            assertTrue(L1Messenger.successfulMessages(hash));
            // it is not in the received messages mapping
            assertFalse(L1Messenger.failedMessages(hash));
        }
        assertFalse(actor.reverted());
    }
}
