HIGH severity: User can receive too few tokens when Comptroller is unpaused. When MTA tokens are burned on L1 to generate MTy tokens on L2, the message is passed using Optimism's Cross Domain Messenger contract.

If the ultimate call to `buyBackFromL1()` fails, the Cross Domain Messenger contains functionality to set the message as `failed` so it can be replayed:

This...

Vulnerable code:
xDomainMsgSender = _sender;
bool success = SafeCall.callWithMinGas(_target, _minGasLimit, _value, _message);
xDomainMsgSender = Constants.DEFAULT_L2_SENDER;

if (success) {
    successfulMessages[versionedHash] = true;
    emit RelayedMessage(versionedHash);
} else {
    failedMessages[versionedHash] = true;
    emit FailedRelayedMessage(versionedHash);
}

// `totalAmountClaimed` is of the `tokenToBurn` denomination.
uint256 totalAmountClaimed = claimedAmountOf[l1Depositor];

// The cumulative token amount burnt and claimed against on L2 should never be less than
// what's been burnt on L1. This indicates some serious issues.
assert(totalAmountClaimed <= totalAmountBurntOnL1);

// The difference of both these variables tell us the claimable token amount in `tokenToBurn`
// denomination.
uint256 burnTokenAmount = totalAmountBurntOnL1 - totalAmountClaimed;

if (burnTokenAmount == 0) {
    revert ExceedingClaimableAmount(l1Depositor, 0, 0);
}

// Store the new total amount of tokens burnt on L1 and claimed against on L2.
l1BurntAmountOf[l1Depositor] = totalAmountBurntOnL1;

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "src/L1Comptroller.sol";
import "src/L2Comptroller.sol";
import {IERC20Burnable} from "../src/interfaces/IERC20Burnable.sol";
import {ICrossDomainMessenger} from "../src/interfaces/ICrossDomainMessenger.sol";
import {IPoolLogic} from "../src/interfaces/IPoolLogic.sol";
import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC20Upgradeable.sol";

library AddressAliasHelper {
    uint160 constant offset = uint160(0x1111000000000000000000000000000000001111);

    function applyL1ToL2Alias(address l1Address) internal pure returns (address l2Address) {
        unchecked {
            l2Address = address(uint160(l1Address) + offset);
        }
    }
}

interface IMTy is IERC20Upgradeable {
    function totalSupply() external view returns (uint);
}

contract OutofSyncBrickedFundsTest is Test {
    L2Comptroller l2c = L2Comptroller(0x3509816328cf50Fed7631c2F5C9a18c75cd601F0);
    ICrossDomainMessenger l2xdm = ICrossDomainMessenger(0x4200000000000000000000000000000000000007);
    IMTy mty = IMTy(0x0F6eAe52ae1f94Bc759ed72B201A2fDb14891485);

    function testOutOfSyncBrickedFunds() public {
        vm.createSelectFork("INSERT_RPC_URL");

        // simulate a situation where L2Comptroller has no funds & is paused
        address user = makeAddr("user");
        uint bal = mty.balanceOf(address(l2c));
        vm.prank(address(l2c));
        mty.transfer(address(1), bal);
        address owner = l2c.owner();
        vm.prank(owner);
        l2c.pause();

        // send two txs, one for 1e18 totalBurned and one for 2e18 totalBurned
        address aliasedXDM = AddressAliasHelper.applyL1ToL2Alias(l2xdm.l1CrossDomainMessenger());
        uint nonce100 = uint(keccak256(abi.encode("nonce100")));
        uint nonce200 = uint(keccak256(abi.encode("nonce200")));

        vm.startPrank(aliasedXDM);
        l2xdm.relayMessage(
            0x3509816328cf50Fed7631c2F5C9a18c75cd601F0, // L1Comptroller
            0x3509816328cf50Fed7631c2F5C9a18c75cd601F0, // L2Comptroller
            abi.encodeWithSignature(
                "buyBackFromL1(address,address,uint256)",
                user,
                user,
                1e18
            ),
            nonce100
        );
        l2xdm.relayMessage(
            0x3509816328cf50Fed7631c2F5C9a18c75cd601F0, // L1Comptroller
            0x3509816328cf50Fed7631c2F5C9a18c75cd601F0, // L2Comptroller
            abi.encodeWithSignature(
                "buyBackFromL1(address,address,uint256)",
                user,
                user,
                2e18
            ),
            nonce200
        );
        vm.stopPrank();

        // unpause the L2Comp contract
        vm.prank(owner);
        l2c.unpause();

        // execute the 2e18 transaction first, and then the 1e18 transaction
        // in bedrock, anyone can call this, but on old OP system we need to prank aliased XDM
        // these will be saved as unclaimed on contract because there are no funds to pay
        vm.startPrank(aliasedXDM);
        l2xdm.relayMessage(
            0x3509816328cf50Fed7631c2F5C9a18c75cd601F0, // L1Comptroller
            0x3509816328cf50Fed7631c2F5C9a18c75cd601F0, // L2Comptroller
            abi.encodeWithSignature(
                "buyBackFromL1(address,address,uint256)",
                user,
                user,
                2e18
            ),
            nonce200
        );
        l2xdm.relayMessage(
            0x3509816328cf50Fed7631c2F5C9a18c75cd601F0, // L1Comptroller
            0x3509816328cf50Fed7631c2F5C9a18c75cd601F0, // L2Comptroller
            abi.encodeWithSignature(
                "buyBackFromL1(address,address,uint256)",
                user,
                user,
                1e18
            ),
            nonce100
        );
        vm.stopPrank();

        // add funds to the contract
        deal(address(mty), address(l2c), 10e18);

        // user calls claimAll
        vm.prank(user);
        l2c.claimAll(user);

        // even though the user should have 2e18 worth of MTy tokens
        // they actually only have ~1e18 worth
        // their `l1BurntAmountOf` is 1e18 as well
        assertApproxEqAbs(l2c.convertToTokenToBurn(mty.balanceOf(user)), 1e18, 100);
        assertEq(l2c.l1BurntAmountOf(user), 1e18);
    }
}