sherlock-audit / 2024-08-tokamak-network-judging

1 stars 0 forks source link

KingNFT - Users might permanently lose their fund while depositing from L1 -> L2 with long messages #51

Open sherlock-admin4 opened 3 weeks ago

sherlock-admin4 commented 3 weeks ago

KingNFT

Medium

Users might permanently lose their fund while depositing from L1 -> L2 with long messages

Summary

Current design of L1CrossDomainMessenger and OptimismPortal2 allows users to send message up to about 120K bytes from L1 to L2:

File: src\L1\L1CrossDomainMessenger.sol
151:     function sendNativeTokenMessage(
152:         address _target,
153:         uint256 _amount,
154:         bytes calldata _message,
155:         uint32 _minGasLimit
156:     )
157:         external
158:     {
159:         // Triggers a message to the other messenger. Note that the amount of gas provided to the
160:         // message is the amount of gas requested by the user PLUS the base gas value. We want to
161:         // guarantee the property that the call to the target contract will always have at least
162:         // the minimum gas limit specified by the user.
163:         _sendNativeTokenMessage(msg.sender, _target, _amount, _minGasLimit, _message);
164:     }

File: src\L1\OptimismPortal2.sol
513:     function _depositTransaction(
...
520:         bytes calldata _data,
...
522:     )
523:         internal
524:         metered(_gasLimit)
525:     {
...
545:         require(_data.length <= 120_000, "OptimismPortal: data too large");
...
559:     }

But the actual maximum length that can successfully be included in L2 blocks could be as low as about 45K. Those deposit transactions with data length between 45K ~ 120K might be included as Fully or Partially failed transactions by l2 blocks, due to OutOfGas error.

Root Cause

The issue arises due to the incorrect gas estimation for L2CrossDomainMessenger.relayMessage()(link). Those dynamic message length sensitive gas costs are not taken into consideration, which include: (1) Calldata Copy (2) Memory Allocation (3) Memory Copy (4) Keccak256

Let's take a close look at the decompiled code of L2CrossDomainMessenger.relayMessage() (refer: https://app.dedaub.com/decompile?md5=f23a0ccec5778d28e4819aea113abf3c). For version 1 message and the successful execution path, there are 3 Memory Allocations (L204, L207, L246), 2 Calldata Copies(L205, L247), 1 Memory Copy (L215~218), 5 Keccak256(L230, L237, L257, L258, L259).

File: https://app.dedaub.com/decompile?md5=f23a0ccec5778d28e4819aea113abf3c#decompiled
175: function relayMessage(uint256 _nonce, address _sender, address _target, uint256 _value, uint256 _minGasLimit, bytes _message) public payable {
176:     require(msg.data.length - 4 >= 192);
177:     require(_message <= uint64.max);
178:     require(4 + _message + 31 < msg.data.length);
179:     require(_message.length <= uint64.max);
180:     require(4 + _message + _message.length + 32 <= msg.data.length);
181:     require(_nonce >> 240 < 2, Error('CrossDomainMessenger: only version 0 or 1 messages are supported at this time'));
182:     if (!(0 - uint16(_nonce >> 240))) {
183:         v0 = new bytes[](_message.length);
184:         CALLDATACOPY(v0.data, _message.data, _message.length);
185:         v0[_message.length] = 0;
186:         v1 = new uint256[](32 + ((0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 & 31 + v0.length) + (36 + v1 + 128)) - MEM[64] - 32);
187:         MEM[36 + v1 + 32] = _sender;
188:         MEM[36 + v1 + 64] = 128;
189:         MEM[36 + v1 + 128] = v0.length;
190:         v2 = 0;
191:         while (v2 < v0.length) {
192:             MEM[32 + (v2 + (36 + v1 + 128))] = v0[v2];
193:             v2 += 32;
194:         }
195:         if (v2 > v0.length) {
196:             MEM[36 + v1 + 128 + v0.length + 32] = 0;
197:         }
198:         MEM[36 + v1 + 96] = _nonce;
199:         MEM[v1.data] = 0xcbd4ece900000000000000000000000000000000000000000000000000000000 | uint224(_target);
200:         v3 = v1.length;
201:         v4 = v1.data;
202:         require(!_successfulMessages[keccak256(v1)], Error('CrossDomainMessenger: legacy withdrawal already relayed'));
203:     }
204:     v5 = new bytes[](_message.length);
205:     CALLDATACOPY(v5.data, _message.data, _message.length);
206:     v5[_message.length] = 0;
207:     v6 = new uint256[](32 + ((0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0 & 31 + v5.length) + (36 + v6 + 192)) - MEM[64] - 32);
208:     MEM[36 + v6 + 32] = _sender;
209:     MEM[36 + v6 + 64] = _target;
210:     MEM[36 + v6 + 96] = _value;
211:     MEM[36 + v6 + 128] = _minGasLimit;
212:     MEM[36 + v6 + 160] = 192;
213:     MEM[36 + v6 + 192] = v5.length;
214:     v7 = 0;
215:     while (v7 < v5.length) {
216:         MEM[32 + (v7 + (36 + v6 + 192))] = v5[v7];
217:         v7 += 32;
218:     }
219:     if (v7 > v5.length) {
220:         MEM[36 + v6 + 192 + v5.length + 32] = 0;
221:     }
222:     MEM[v6.data] = 0xd764ad0b00000000000000000000000000000000000000000000000000000000 | uint224(_nonce);
223:     v8 = v6.length;
224:     v9 = v6.data;
225:     if (_oTHER_MESSENGER != address(0xffffffffffffffffffffffffeeeeffffffffffffffffffffffffffffffffeeef + msg.sender)) {
226:         require(!msg.value, Error('CrossDomainMessenger: value must be zero unless message is from a system address'));
227:         require(_failedMessages[keccak256(v6)], Error('CrossDomainMessenger: message cannot be replayed'));
228:     } else {
229:         require(msg.value == _value, Panic(1)); // low-level assert failed
230:         require(!_failedMessages[keccak256(v6)], Panic(1)); // low-level assert failed
231:     }
232:     v10 = v11 = this == _target;
233:     if (this != _target) {
234:         v10 = 0x4200000000000000000000000000000000000016 == _target;
235:     }
236:     require(!v10, Error('CrossDomainMessenger: cannot send message to blocked system address'));
237:     require(!_successfulMessages[keccak256(v6)], Error('CrossDomainMessenger: message has already been relayed'));
238:     v12 = 0x1b1a(40000, 5000);
239:     v13 = v14 = msg.gas * 63 < (_minGasLimit << 6) + (40000 + uint64(v12)) * 63;
240:     if (!v14) {
241:         v13 = 57005 != address(_xDomainMessageSender);
242:     }
243:     if (!v13) {
244:         _xDomainMessageSender = _sender | bytes12(_xDomainMessageSender);
245:         require(msg.gas >= 40000, Panic(17)); // arithmetic overflow or underflow
246:         v15 = new bytes[](_message.length);
247:         CALLDATACOPY(v16.data, _message.data, _message.length);
248:         v15[_message.length] = 0;
249:         v17 = v15.length;
250:         v18 = _target.call(v16.data).value(_value).gas(msg.gas - 40000);
251:         _xDomainMessageSender = 0xdead | bytes12(_xDomainMessageSender);
252:         if (!v18) {
253:             _failedMessages[keccak256(v6)] = 1;
254:             emit FailedRelayedMessage(keccak256(v6));
255:             require(tx.origin - 1, Error('CrossDomainMessenger: failed to relay message'));
256:         } else {
257:             require(!_successfulMessages[keccak256(v6)], Panic(1)); // low-level assert failed
258:             _successfulMessages[keccak256(v6)] = 1;
259:             emit RelayedMessage(keccak256(v6));
260:         }
261:     } else {
262:         _failedMessages[keccak256(v6)] = 1;
263:         emit FailedRelayedMessage(keccak256(v6));
264:         require(tx.origin - 1, Error('CrossDomainMessenger: failed to relay message'));
265:     }
266: }

Now, let's analyze each part of these dynamic gas costs:

  1. Calldata Copy costs 3 gas per word (1 word = 32 bytes) refer:https://github.com/ethereum-optimism/op-geth/blob/5f7ebba8a124ae87225f81a1a9c827f8a534f2b7/core/vm/jump_table.go#L453

  2. Memory Allocation gas contains both linear cost and quadratic cost, which can be calculated by

    length  = total memory allocation size in bytes
    words =  (length * 3 + 31) / 32
    linearCost = words * 3
    quadCost = words * words / 512;
    totalCost = linearCost + quadCost 

    refer: https://github.com/ethereum-optimism/op-geth/blob/5f7ebba8a124ae87225f81a1a9c827f8a534f2b7/core/vm/gas_table.go#L30

  3. Memory Copy cost is a bit complicated, we need to parse the yul of L215~218 of the above decompiled code, which is

    
    File: https://app.dedaub.com/decompile?md5=f23a0ccec5778d28e4819aea113abf3c#yul
    703:                         for {
    704:                             let _249 := lt(_246, _245)
    705:                             let _250 := 0x20
    706:                             _195 := add(_250, _247)
    707:                         }
    708:                         not(iszero(_249))
    709:                         { }
    710:                         {
    711:                             _246 := _195
    712:                             _249 := lt(_246, _245)
    713:                             let _251 := not(iszero(_249))
    714:                             _247 := _195
    715:                             _250 := 0x20
    716:                             let _252 := mload(add(_250, add(_240, _247)))
    717:                             mstore(add(_250, add(_247, _244)), _252)
    718:                             _195 := add(_250, _247)
    719:                             _246 := _195
    720:                             _247 := _195
    721:                             _248 := _195
    722:                         }
With the following test script, we get memory copy costs is about ````130 gas per word````.
```solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import {Test, console} from "forge-std/Test.sol";

contract MemoryCopyGasTest is Test {
    function test() public {
        uint256 gasUsed;
        assembly {
            let _195 := 0x0
            let _246 := 0x0
            let _247 := 0x0
            let _248 := 0x0
            let _245 := 320 // copy 10 words = 320 bytes 
            let _240 := mload(0x40)
            let _244 := add(_240, 384)
            let _300 := gas()
            for {
                let _249 := lt(_246, _245)
                let _250 := 0x20
                _195 := add(_250, _247)
            }
            iszero(iszero(_249))
            { }
            {
                _246 := _195
                _249 := lt(_246, _245)
                let _251 := iszero(iszero(_249))
                _247 := _195
                _250 := 0x20
                let _252 := mload(add(_250, add(_240, _247)))
                mstore(add(_250, add(_247, _244)), _252)
                _195 := add(_250, _247)
                _246 := _195
                _247 := _195
                _248 := _195
            }
            let _301 := gas()
            gasUsed := sub(_300, _301)
        }
        assertEq(gasUsed, 1319);
    }
}
  1. Keccak256 cost 6 gas per word refer: https://github.com/ethereum-optimism/op-geth/blob/5f7ebba8a124ae87225f81a1a9c827f8a534f2b7/core/vm/jump_table.go#L403

To sum up, the total dynamic gas cost of message could be calculated approximately by the following formula:

    function _messageDynamicGasEstimation(uint256 length) internal pure returns(uint256) {
        // liner cost due to memory alloction, memory copy, calldata copy, keccack256
        // 3 memory alloction: 3 * 3 * length / 32
        // 2 calldata copy: 2 * 3 * length / 32
        // 1 memory copy: 1 * 130 * length / 32
        // 5 keccak256: 5 * 6 * length / 32
        // total: (9 + 6 + 130 + 30) * length / 32 = 175 * length / 32 ~= 6 * length
        uint256 linerCost = length * 6 ;

         // 3 memory alloctions quadratic cost
        uint256 words = (length * 3 + 31) / 32;
        uint256 quadCost = words * words / 512;
        return linerCost + quadCost;
    }

Internal pre-conditions

Users send long messages from L1 to L2

External pre-conditions

N/A

Attack Path

N/A

Impact

Once those deposit transactions with data length between 45k ~ 120k included as fully failed transaction by l2 block, carried funds would be permanently loss. And the partially failed transactions also introduce troubles to users, as they must manually retry the message on L2.

PoC

The following PoC shows cases with all 3 states of success, partially failed and fully failed.

// To run the test, put the file under path: 2024-08-tokamak-network\tokamak-thanos\packages\tokamak\contracts-bedrock\test\L2
// SPDX-License-Identifier: MIT
pragma solidity 0.8.15;

// Testing utilities
import { Bridge_Initializer } from "test/setup/Bridge_Initializer.sol";
import { Reverter, ConfigurableCaller } from "test/mocks/Callers.sol";
import { EIP1967Helper } from "test/mocks/EIP1967Helper.sol";

// Libraries
import { Hashing } from "src/libraries/Hashing.sol";
import { Encoding } from "src/libraries/Encoding.sol";
import { Types } from "src/libraries/Types.sol";
import { Constants } from "src/libraries/Constants.sol";

// Target contract dependencies
import { L2CrossDomainMessenger } from "src/L2/L2CrossDomainMessenger.sol";
import { L2ToL1MessagePasser } from "src/L2/L2ToL1MessagePasser.sol";
import { AddressAliasHelper } from "src/vendor/AddressAliasHelper.sol";
import { OptimismPortal } from "src/L1/OptimismPortal.sol";
import "forge-std/console2.sol";
import "forge-std/Test.sol";

library Burn {
    function gas(uint256 _amount) internal view {
        uint256 i = 0;
        uint256 initialGas = gasleft();
        while (initialGas - gasleft() < _amount) {
            ++i;
        }
    }
}

contract GasBurner {
    uint256 immutable GAS_TO_BURN;
    constructor(uint256 gas) {
        GAS_TO_BURN = gas - 500; // 500 for default codes added by solidity
    }

    receive() external payable {
        _burn();
    }

    fallback() external payable {
        _burn();
    }

    function _burn() internal view {
        Burn.gas(GAS_TO_BURN);
    }
}

contract L1ToL2TransactionsCanFailUnexpectedlyTest is Bridge_Initializer {
    address l2Target;
    uint32 l2TargetGasLimit;
    uint256 nativeTokenToSend;

    function setUp() public override  {
        super.setUp();
        l2NativeToken.faucet(1000 ether);
        assertEq(l2NativeToken.balanceOf(address(this)), 1000 ether);
        l2NativeToken.approve(address(l1CrossDomainMessenger), type(uint256).max);
    }
    function testMessage1KSuccess() public {
        nativeTokenToSend = 1 ether;
        _depolyL2Target(0); // EOA or empty account
        (
            uint256 l2TargetBalanceBefore,
            uint256 l2TargetBalanceAfter,
            uint256 l2MessengerBalanceBefore,
            uint256 l2MessengerBalanceAfter,
            bool successIncludedInL2Block
        ) = _testL1ToL2Message(1_000);
        assertEq(1 ether, l2TargetBalanceAfter - l2TargetBalanceBefore);
        assertEq(0 ether, l2MessengerBalanceAfter - l2MessengerBalanceBefore);
        assertEq(true, successIncludedInL2Block);
    }

    function testMessage10KSuccess() public {
        nativeTokenToSend = 1 ether;
        _depolyL2Target(0); // EOA or empty account
        (
            uint256 l2TargetBalanceBefore,
            uint256 l2TargetBalanceAfter,
            uint256 l2MessengerBalanceBefore,
            uint256 l2MessengerBalanceAfter,
            bool successIncludedInL2Block
        ) = _testL1ToL2Message(10_000);
        assertEq(1 ether, l2TargetBalanceAfter - l2TargetBalanceBefore);
        assertEq(0 ether, l2MessengerBalanceAfter - l2MessengerBalanceBefore);
        assertEq(true, successIncludedInL2Block);
    }

    function testMessage30KSuccess() public {
        nativeTokenToSend = 1 ether;
        _depolyL2Target(0); // EOA or empty account
        (
            uint256 l2TargetBalanceBefore,
            uint256 l2TargetBalanceAfter,
            uint256 l2MessengerBalanceBefore,
            uint256 l2MessengerBalanceAfter,
            bool successIncludedInL2Block
        ) = _testL1ToL2Message(30_000);
        assertEq(1 ether, l2TargetBalanceAfter - l2TargetBalanceBefore);
        assertEq(0 ether, l2MessengerBalanceAfter - l2MessengerBalanceBefore);
        assertEq(true, successIncludedInL2Block);
    }

    function testMessage45KSuccess() public {
        // using EOA or empty l2 target account, only support up to 45k bytes message
        nativeTokenToSend = 1 ether;
        _depolyL2Target(0); // EOA or empty account
        (
            uint256 l2TargetBalanceBefore,
            uint256 l2TargetBalanceAfter,
            uint256 l2MessengerBalanceBefore,
            uint256 l2MessengerBalanceAfter,
            bool successIncludedInL2Block
        ) = _testL1ToL2Message(45_000);
        assertEq(1 ether, l2TargetBalanceAfter - l2TargetBalanceBefore);
        assertEq(0 ether, l2MessengerBalanceAfter - l2MessengerBalanceBefore);
        assertEq(true, successIncludedInL2Block);
    }

    function testMessage46KFailedPartially() public {
        // Failed partially since 46k bytes message
        nativeTokenToSend = 1 ether;
        _depolyL2Target(0); // EOA or empty account
        (
            uint256 l2TargetBalanceBefore,
            uint256 l2TargetBalanceAfter,
            uint256 l2MessengerBalanceBefore,
            uint256 l2MessengerBalanceAfter,
            bool successIncludedInL2Block
        ) = _testL1ToL2Message(46_000);
        assertEq(0 ether, l2TargetBalanceAfter - l2TargetBalanceBefore);
        assertEq(1 ether, l2MessengerBalanceAfter - l2MessengerBalanceBefore);
        assertEq(true, successIncludedInL2Block);
    }

    function testMessage60KFailedPartially() public {
        nativeTokenToSend = 1 ether;
        _depolyL2Target(0); // EOA or empty account
        (
            uint256 l2TargetBalanceBefore,
            uint256 l2TargetBalanceAfter,
            uint256 l2MessengerBalanceBefore,
            uint256 l2MessengerBalanceAfter,
            bool successIncludedInL2Block
        ) = _testL1ToL2Message(60_000);
        assertEq(0 ether, l2TargetBalanceAfter - l2TargetBalanceBefore);
        assertEq(1 ether, l2MessengerBalanceAfter - l2MessengerBalanceBefore);
        assertEq(true, successIncludedInL2Block);
    }

    function testMessage61KFailedFully() public {
        // using EOA or empty l2 account, failed on 50k level
        nativeTokenToSend = 1 ether;
        _depolyL2Target(0);
        (
            uint256 l2TargetBalanceBefore,
            uint256 l2TargetBalanceAfter,
            uint256 l2MessengerBalanceBefore,
            uint256 l2MessengerBalanceAfter,
            bool successIncludedInL2Block
        ) = _testL1ToL2Message(61_000);
        assertEq(0 ether, l2TargetBalanceAfter - l2TargetBalanceBefore);
        assertEq(0 ether, l2MessengerBalanceAfter - l2MessengerBalanceBefore);
        assertEq(false, successIncludedInL2Block);
    }

    function testMessage70KFailedPartially() public {
        // l2 target contract need cost 50K gas
        _depolyL2Target({gasToBurn: 50_000});
        nativeTokenToSend = 1 ether;
        (
            uint256 l2TargetBalanceBefore,
            uint256 l2TargetBalanceAfter,
            uint256 l2MessengerBalanceBefore,
            uint256 l2MessengerBalanceAfter,
            bool successIncludedInL2Block
        ) = _testL1ToL2Message(70_000);
        assertEq(0 ether, l2TargetBalanceAfter - l2TargetBalanceBefore);
        assertEq(1 ether, l2MessengerBalanceAfter - l2MessengerBalanceBefore);
        assertEq(true, successIncludedInL2Block);
    }

    function testMessage90KFailedFully() public {
        _depolyL2Target({gasToBurn: 50_000});
        nativeTokenToSend = 1 ether;
        (
            uint256 l2TargetBalanceBefore,
            uint256 l2TargetBalanceAfter,
            uint256 l2MessengerBalanceBefore,
            uint256 l2MessengerBalanceAfter,
            bool successIncludedInL2Block
        ) = _testL1ToL2Message(90_000);
        assertEq(0 ether, l2TargetBalanceAfter - l2TargetBalanceBefore);
        assertEq(0 ether, l2MessengerBalanceAfter - l2MessengerBalanceBefore);
        assertEq(false, successIncludedInL2Block);
    }

    function testMessage110KFailedFully() public {
        _depolyL2Target({gasToBurn: 100_000});
        nativeTokenToSend = 1 ether;
        (
            uint256 l2TargetBalanceBefore,
            uint256 l2TargetBalanceAfter,
            uint256 l2MessengerBalanceBefore,
            uint256 l2MessengerBalanceAfter,
            bool successIncludedInL2Block
        ) = _testL1ToL2Message(110_000);
        assertEq(0 ether, l2TargetBalanceAfter - l2TargetBalanceBefore);
        assertEq(0 ether, l2MessengerBalanceAfter - l2MessengerBalanceBefore);
        assertEq(false, successIncludedInL2Block);
    }

    function testMessage119KFailedFully() public {
        _depolyL2Target({gasToBurn: 230_000});
        nativeTokenToSend = 1 ether;
        (

            uint256 l2TargetBalanceBefore,

            uint256 l2TargetBalanceAfter,
            uint256 l2MessengerBalanceBefore,
            uint256 l2MessengerBalanceAfter,
            bool successIncludedInL2Block
        ) = _testL1ToL2Message(119_000);
        assertEq(0 ether, l2TargetBalanceAfter - l2TargetBalanceBefore);
        assertEq(0 ether, l2MessengerBalanceAfter - l2MessengerBalanceBefore);
        assertEq(false, successIncludedInL2Block);
    }

    function _testL1ToL2Message(uint256 length) public returns (
        uint256 l2TargetBalanceBefore,
        uint256 l2TargetBalanceAfter,
        uint256 l2MessengerBalanceBefore,
        uint256 l2MessengerBalanceAfter,
        bool successIncludedInL2Block
    ) {
        bytes memory l2TargetData = _makeBytes(length);
        vm.recordLogs();
        l1CrossDomainMessenger.sendNativeTokenMessage(l2Target, nativeTokenToSend, l2TargetData,
            l2TargetGasLimit);
        (address from, address to, bytes memory opaqueData) = _getTransactionDepositedLog();
        assertEq(from, AddressAliasHelper.applyL1ToL2Alias(address(l1CrossDomainMessenger)));
        assertEq(to, address(l2CrossDomainMessenger));
        (uint256 mint, uint256 value, uint64 gasLimit, bytes memory data) =
            _decodeOpaqueData(opaqueData);
        assertEq(nativeTokenToSend, mint);
        assertEq(nativeTokenToSend, value);
        console2.log("initial gasLimit", gasLimit);
        bytes32 expectedDataHash = keccak256(
            abi.encodeWithSelector(
                l2CrossDomainMessenger.relayMessage.selector,
                l1CrossDomainMessenger.messageNonce() - 1,
                address(this),
                l2Target,
                nativeTokenToSend,
                l2TargetGasLimit,
                l2TargetData
            )
        );
        assertEq(expectedDataHash, keccak256(data));

        l2TargetBalanceBefore = l2Target.balance;
        l2MessengerBalanceBefore = address(l2CrossDomainMessenger).balance;
        successIncludedInL2Block = _mimicMintDepositTransactionOnL2(from, to, mint, value, gasLimit, data);
        l2TargetBalanceAfter = l2Target.balance;
        l2MessengerBalanceAfter = address(l2CrossDomainMessenger).balance;
    }

    function _mimicMintDepositTransactionOnL2 (
        address from, address to, uint256 mint, uint256 value, uint256 gasLimit, bytes memory data
    ) internal returns(bool success) {
        // refer: https://github.com/ethereum-optimism/op-geth/blob/5f7ebba8a124ae87225f81a1a9c827f8a534f2b7/core/state_transition.go#L414
        vm.deal(from, from.balance + mint);

        vm.startPrank(from);
        // 1. charge IntrinsicGas
        // refer: https://github.com/ethereum-optimism/op-geth/blob/5f7ebba8a124ae87225f81a1a9c827f8a534f2b7/core/state_transition.go#L71
        gasLimit -= 21000 + _dataIntrinsicGas(data);
        console2.log("gas remaining after IntrinsicGas:", gasLimit);
        assertTrue(gasleft() * 63 / 64 > gasLimit + 100_000); // ensure exact gasLimit passed to target
        uint256 dataStart;
        assembly {
            dataStart := add(data, 32)
        }
        uint256 dataLength = data.length;
        uint256 gasBefore = gasleft();
        assembly {
            success := call(gasLimit, to, value, dataStart, dataLength, 0, 0)
        }
        uint256 gasAfter = gasleft();
        console2.log("execution gas used:", gasBefore - gasAfter);
        uint256 dynamicCost = _messageDynamicGasEstimation(dataLength);
        console2.log("estimation of message dynamic gas needed:", dynamicCost);

        vm.stopPrank();
    }

    function _dataIntrinsicGas(bytes memory data) internal returns(uint256 gas) {
        for (uint256 i; i < data.length; ++i) {
            if (data[i] == bytes1(0)) {
                gas += 4;
            } else {
                gas += 16;
            }
        }
    }

    function _messageDynamicGasEstimation(uint256 length) internal pure returns(uint256) {
        // liner cost due to memory alloction, memory copy, calldata copy, keccack256
        // 3 memory alloction: 3 * 3 * length / 32
        // 2 calldata copy: 2 * 3 * length / 32
        // 1 memory copy: 1 * 130 * length / 32
        // 5 keccak256: 5 * 6 * length / 32
        // total: (9 + 6 + 130 + 30) * length / 32 = 175 * length / 32 ~= 6 * length
        uint256 linerCost = length * 6 ;

         //  memory alloctions (3)
        uint256 words = (length * 3 + 31) / 32;
        uint256 quadCost = words * words / 512;
        return linerCost + quadCost;
    }

    function _depolyL2Target(uint256 gasToBurn) internal {
        if (gasToBurn > 0) {
            l2Target = address(new GasBurner(gasToBurn));
            l2TargetGasLimit = uint32(gasToBurn);
        } else {
            l2Target = address(0x1234);
            assertEq(l2Target.code.length, 0); // EOA
            l2TargetGasLimit = 0;
        }
    }

    function _makeBytes(uint256 length) internal returns(bytes memory res) {
        uint256 words = (length + 31) / 32;
        bytes32 fill = 0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20;
        bytes32 lastFill = fill;
        uint256 remain = length % 32;
        if (remain != 0) {
            uint256 shift = (32 - remain) * 8;
            lastFill = (fill >> shift) << shift;
        }

        res = new bytes(words * 32);
        for (uint256 i; i < words; ++i) {
            assembly {
                 mstore(add(add(res, 32), mul(i, 32)), fill)
                 if eq(i, sub(words, 1)) {
                    mstore(add(add(res, 32), mul(i, 32)), lastFill)
                    mstore(res, length) // modify to actual length
                 }
            }
        }
        assertEq(length, res.length);
        bytes32 first;
        bytes32 last;
        assembly {
            first := mload(add(res, 32))
            last := mload(add(res, mul(words, 32)))
        }
        assertEq(fill, first);
        assertEq(lastFill, last);
    }

    function _getTransactionDepositedLog() internal returns
        (address from, address to, bytes memory opaqueData) {
        bytes32 targetTopic0 = keccak256("TransactionDeposited(address,address,uint256,bytes)");
        Vm.Log[] memory entries = vm.getRecordedLogs();
        for (uint256 i; i < entries.length; ++i) {
            if (entries[i].topics[0] == targetTopic0) {
                from = address(uint160(uint256(entries[i].topics[1])));
                to = address(uint160(uint256(entries[i].topics[2])));
                opaqueData = abi.decode(entries[i].data, (bytes));
                break;
            }
        }
    }

    // https://github.com/sherlock-audit/2024-08-tokamak-network/blob/6d4cf9ea730d5b52b622f0b3afd41a35d3eba8a2/tokamak-thanos/packages/tokamak/contracts-bedrock/src/L1/OptimismPortal2.sol#L554
    function _decodeOpaqueData(bytes memory opaqueData) internal pure returns(
        uint256 mint, uint256 value, uint64 gasLimit, bytes memory data
    ) {
        uint256 dataLength = opaqueData.length - 32 - 32 - 8 - 1;
        uint256 words = (dataLength + 31) / 32;
        uint256 allocSize = words * 32;
        data = new bytes(allocSize); // modify to actual size following
        assembly {
            let base := add(opaqueData, 32)
            mint := mload(base)
            value := mload(add(base, 32))
            gasLimit := mload(add(base, 40))
            mstore(data, dataLength) // set actual length
            // copy data
            let sourceBegin := add(base, 73)
            let targetBegin := add(data, 32)
            for { let i := 0 } lt(i, allocSize) { i := add(i, 32) } {
                let word := mload(add(sourceBegin, i))
                mstore(add(targetBegin, i), word)
            }
        }
    }
}

Mitigation

    function baseGas(bytes calldata _message, uint32 _minGasLimit) public pure returns (uint64) {
        return
        // Constant overhead
        RELAY_CONSTANT_OVERHEAD
        // Calldata overhead
        + (uint64(_message.length) * MIN_GAS_CALLDATA_OVERHEAD)
+       + _messageDynamicGasEstimation(_message.length)
        // Dynamic overhead (EIP-150)
        + ((_minGasLimit * MIN_GAS_DYNAMIC_OVERHEAD_NUMERATOR) / MIN_GAS_DYNAMIC_OVERHEAD_DENOMINATOR)
        // Gas reserved for the worst-case cost of 3/5 of the `CALL` opcode's dynamic gas
        // factors. (Conservative)
        + RELAY_CALL_OVERHEAD
        // Relay reserved gas (to ensure execution of `relayMessage` completes after the
        // subcontext finishes executing) (Conservative)
        + RELAY_RESERVED_GAS
        // Gas reserved for the execution between the `hasMinGas` check and the `CALL`
        // opcode. (Conservative)
        + RELAY_GAS_CHECK_BUFFER;
    }