Causing users lose fund if bridging long message from L2 to L1 due to uncontrolled out-of-gas error
If the amount of gas provided during finalizing withdrawal transactions passes the check in `callWithMinGas`, it is not guaranteed that the relaying message transaction does not go out of gas. This can happen if the bridged message from L2 to L1 is long enough to increase the gas consumption significantly so that the predicted `baseGas` is not accurate enough.\\nSo far so good.\\nAs a result, while the transaction `OptimismPortal.finalizeWithdrawalTransaction` sets the flag `finalizedWithdrawals[withdrawalHash]` as `true`, the flags `failedMessages[versionedHash]` and `successfulMessages[versionedHash]` are `false`. So, the users can not replay their message, and his fund is lost.\\nThe question is that is there any possibility that `L1CrossDomainMessenger` reverts due to OOG, even though the required gas is calculated in L2 in the function `baseGas`?\\nSo, the amount of gas available to `L1CrossDomainMessenger` will be: `(G - K1 - 51)*(63/64)` Please note this number is based on the estimation of gas consumption explained in the comment:\\n// Because EIP-150 ensures that, a maximum of 63/64ths of the remaining gas in the call // frame may be passed to a subcontext, we need to ensure that the gas will not be // truncated to hold this function's invariant: "If a call is performed by // `callWithMinGas`, it must receive at least the specified minimum gas limit." In // addition, exactly 51 gas is consumed between the below `GAS` opcode and the `CALL` // opcode, so it is factored in with some extra room for error.\\nIn the function `L1CrossDomainMessenger.relayMessage`, some gas will be consumed from line 299 to line 360. For simplicity, I call this amount of gas `K2 + HashingGas`, i.e. the consumed gas is separated for later explanation. In other words, the sum of consumed gas from line 299 to 303 and the consumed gas from line 326 to 360, is called `K2`, and the consumed gas from line 304 to line 325 is called `HashingGas`.\\nSo, the `gasLeft()` in line 361 will be: `(G - K1 - 51)*(63/64) - K2 - HashingGas`\\nTo pass the condition `gasleft() >= ((_minGas + 200) * 64) / 63` in `L1CrossDomainMessenger`, it is necessary to have: `(G - K1 - 51)*(63/64) - K2 - HashingGas >= ((_minGas + 200) * 64) / 63` Please note that, `_minGas` here is equal to `_minGasLimit` which is the amount of gas set by the user to be forwarded to the final receiver on L1. So, after simplification: `G >= [((_minGasLimit + 200) * 64) / 63 + K2 + HashingGas] *(64/63) + 51 + K1`\\nAll in all:\\nTo pass the gas check in OptimismPortal: `G >= ((_minGasLimit * (1016/1000) + messageLength * 16 + 200_000 + 200) * 64) / 63 + K1`\\nTo pass the gas check in L1CrossDomainMessenger: `G >= [((_minGasLimit + 200) * 64) / 63 + K2 + HashingGas] *(64/63) + 51 + K1`\\nIf, `G` is between these two numbers (bigger than the first one, and smaller than the second one), it will pass the check in `OptimismPortal`, but it will revert in `L1CrossDomainMessenger`, as a result it is possible to attack.\\nSince, K1 and K2 are almost equal to 50_000, after simplification:\\n`G >= (_minGasLimit * (1016/1000) + messageLength * 16 ) * (64 / 63) + 253_378`\\n`G >= (_minGasLimit * (64 / 63) + HashingGas) *(64/63) + 101_051`\\nSo it is necessary to satisfy the following condition to be able to attack (in that case it is possible that the attacker provides gas amount between the higher and lower bound to execute the attack): (_minGasLimit * (1016/1000) + messageLength * 16 ) * (64 / 63) + 253_378 < (_minGasLimit * (64 / 63) + HashingGas) *(64/63) + 101_051After simplification, we have:messageLength < (HashingGas - 150_000) / 16`\\nPlease note that the `HashingGas` is a function of `messageLength`. In other words, the consumed gas from Line 304 to 325 is a function of `messageLength`, the longer length the higher gas consumption, but the relation is not linear, it is exponential.**\\nSo, for version zero, the condition can be relaxed to: `messageLength < (HashingGas * 2 - 150_000) / 16`\\nThe calculation shows that if the `messageLength` is equal to 1 mb for version 0, the gas consumed during hashing will be around 23.5M gas (this satisfies the condition above). While, if the `messageLength` is equal to 512 kb for version 0, the gas consumed during hashing will be around 7.3M gas (this does not satisfy the condition above marginally).\\nA short summary of calculation is:\\nmessageLength= 128 kb, HashingGas for v1= 508_000, HahingGas for v0= 1_017_287, attack not possible messageLength= 256 kb, HashingGas for v1= 1_290_584, HahingGas for v0= 2_581_168, attack not possible messageLength= 512 kb, HashingGas for v1= 3_679_097, HahingGas for v0= 7_358_194, attack not possible messageLength= 684 kb, HashingGas for v1= 5_901_416, HahingGas for v0= 11_802_831, attack possible messageLength= 1024 kb, HashingGas for v1= 11_754_659, HahingGas for v0= 23_509_318, attack possible\\n\\nWhich can be calculated approximately by:\\n```\\nfunction checkGasV1(bytes calldata _message)\\n        public\\n        view\\n        returns (uint256, uint256)\\n    {\\n        uint256 gas1 = gasleft();\\n        bytes32 versionedHash = Hashing.hashCrossDomainMessageV1(\\n            0,\\n            address(this),\\n            address(this),\\n            0,\\n            0,\\n            _message\\n        );\\n        uint256 gas2 = gasleft();\\n        return (_message.length, (gas1 - gas2));\\n    }\\n```\\n\\n```\\nfunction checkGasV0(bytes calldata _message)\\n        public\\n        view\\n        returns (\\n            uint256,\\n            uint256,\\n            uint256\\n        )\\n    {\\n        uint256 gas1 = gasleft();\\n        bytes32 versionedHash1 = Hashing.hashCrossDomainMessageV0(\\n            address(this),\\n            address(this),\\n            _message,\\n            0\\n        );\\n        uint256 gas2 = gasleft();\\n        uint256 gas3 = gasleft();\\n        bytes32 versionedHash2 = Hashing.hashCrossDomainMessageV1(\\n            0,\\n            address(this),\\n            address(this),\\n            0,\\n            0,\\n            _message\\n        );\\n        uint256 gas4 = gasleft();\\n        return (_message.length, (gas1 - gas2), (gas3 - gas4));\\n    }\\n```\\n\\nIt means that if for example the `messageLength` is equal to 684 kb (mostly non-zero, only 42 kb zero), and the message is version 0, and for example the `_minGasLimit` is equal to 21000, an attacker can exploit the user's withdrawal transaction by providing a gas meeting the following condition: `(_minGasLimit * (1016/1000) + 684 * 1024 * 16 ) * (64 / 63) + 253_378 < G < (_minGasLimit * (64 / 63) + 11_802_831) *(64/63) + 101_051` After, replacing the numbers, the provided gas by the attacker should be: `11_659_592 < G < 12_112_900` So, by providing almost 12M gas, it will pass the check in `OptimismPortal`, but it will revert in `L1CrossDomainMessenger` due to OOG, as a result the user's transaction will not be allowed to be replayed.\\nPlease note that if there is a long time between request of withdrawal transaction on L2 and finalizing withdrawal transaction on L1, it is possible that the gas price is low enough on L1, so economically reasonable for the attacker to execute it.\\nIn Summary:\\nWhen calculating the `baseGas` on L2, only the `minGasLimit` and `message.length` are considered, and a hardcoded overhead is also added. While, the hashing mechanism (due to memory expansion) is exponentially related to the length of the message. It means that, the amount of gas usage during relaying the message can be increased to the level that is higher than calculated value in `baseGas`. So, if the length of the message is long enough (to increase the gas significantly due to memory expansion), it provides an attack surface so that the attacker provides the amount of gas that only pass the condition in `OptimismPortal`, but goes out of gas in `L1CrossDomainMessenger`.
If all the gas is consumed before reaching to L361, the vulnerability is available. So, it is recommended to include memory expansion effect when calculating `baseGas`.
Users will lose fund because it is set as finalized, but not set as failed. So, they can not replay it.
```\\nfunction checkGasV1(bytes calldata _message)\\n        public\\n        view\\n        returns (uint256, uint256)\\n    {\\n        uint256 gas1 = gasleft();\\n        bytes32 versionedHash = Hashing.hashCrossDomainMessageV1(\\n            0,\\n            address(this),\\n            address(this),\\n            0,\\n            0,\\n            _message\\n        );\\n        uint256 gas2 = gasleft();\\n        return (_message.length, (gas1 - gas2));\\n    }\\n```\\n