| description | Force-include a transaction on MegaETH using Foundry (cast or forge) — step-by-step guide for scripted and automated submissions. |
|---|
Force inclusion submits a transaction directly to the L1 OptimismPortal, bypassing the sequencer. This page covers the scripted path using Foundry. For the Etherscan UI path, see Force-include a transaction.
{% tabs %} {% tab title="Mainnet" %}
| Contract | Chain | Address |
|---|---|---|
| OptimismPortal | Ethereum (chain 1) | 0x7f82f57F0Dd546519324392e408b01fcC7D709e8 |
{% endtab %} {% tab title="Testnet (Sepolia)" %}
| Contract | Chain | Address |
|---|---|---|
| OptimismPortal | Ethereum Sepolia (chain 11155111) | 0xF68D900e1Cdec64a8f5Dc0Ee873A9E2879256b10 |
{% endtab %} {% endtabs %}
Your L1 wallet address is preserved as the sender on MegaETH — so the same private key that holds tokens on MegaETH signs the L1 transaction.
{% stepper %} {% step %}
export PRIVATE_KEY=0x...
export TOKEN=... # ERC-20 contract address on MegaETH
export RECIPIENT=... # address to receive the tokens
export AMOUNT=... # amount in wei
# Testnet
export PORTAL=0xF68D900e1Cdec64a8f5Dc0Ee873A9E2879256b10
export L1_RPC=https://ethereum-sepolia-rpc.publicnode.com
export L2_RPC=https://carrot.megaeth.com/rpc # see Connect page for all RPC options
# Mainnet
# export PORTAL=0x7f82f57F0Dd546519324392e408b01fcC7D709e8
# export L1_RPC=<your Ethereum mainnet RPC>
# export L2_RPC=https://mainnet.megaeth.com/rpc{% endstep %} {% step %}
SENDER=$(cast wallet address --private-key $PRIVATE_KEY)
cast call $TOKEN "balanceOf(address)(uint256)" $SENDER --rpc-url $L2_RPC{% endstep %} {% step %}
Always query the MegaETH L2 RPC — MegaETH's dual gas model (compute + storage) means standard Ethereum tooling underestimates. Add 20% headroom to account for storage gas variance.
SENDER=$(cast wallet address --private-key $PRIVATE_KEY)
GAS_EST=$(cast estimate $TOKEN \
"transfer(address,uint256)" $RECIPIENT $AMOUNT \
--from $SENDER \
--rpc-url $L2_RPC)
export GAS_LIMIT=$(( GAS_EST * 120 / 100 ))
echo "estimate: $GAS_EST limit: $GAS_LIMIT"For a standard ERC-20 transfer() on MegaETH the estimate is approximately 54,000 gas; a limit of 65,000 is sufficient.
{% hint style="warning" %}
The portal enforces a minimum _gasLimit based on calldata length (~26,500 for a transfer() call).
Values below the minimum revert with SmallGasLimit.
{% endhint %}
{% endstep %} {% step %}
The _data field passed to depositTransaction is the function call you want to execute on L2, serialized into bytes (ABI-encoded).
Use cast calldata to produce it:
CALLDATA=$(cast calldata "transfer(address,uint256)" $RECIPIENT $AMOUNT)
echo $CALLDATAExample output:
0xa9059cbb000000000000000000000000<recipient>0000000000000000000000000000000000000000000000000de0b6b3a7640000
The first 4 bytes (0xa9059cbb) are the transfer function selector.
The remaining 64 bytes are the ABI-encoded recipient address and amount.
For a plain ETH transfer with no contract call, set _data to 0x and skip this step.
{% endstep %} {% step %}
{% tabs %} {% tab title="cast" %}
cast send $PORTAL \
"depositTransaction(address,uint256,uint64,bool,bytes)" \
$TOKEN \
0 \
$GAS_LIMIT \
false \
$CALLDATA \
--value 0 \
--rpc-url $L1_RPC \
--private-key $PRIVATE_KEY{% endtab %} {% tab title="forge script" %}
Create script/ForceInclude.s.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
import {Script} from "forge-std/Script.sol";
interface IOptimismPortal {
function depositTransaction(
address _to, uint256 _value, uint64 _gasLimit,
bool _isCreation, bytes calldata _data
) external payable;
}
contract ForceInclude is Script {
function run() external {
address portal = vm.envAddress("PORTAL");
address token = vm.envAddress("TOKEN");
address recipient = vm.envAddress("RECIPIENT");
uint256 amount = vm.envUint("AMOUNT");
uint64 gasLimit = uint64(vm.envUint("GAS_LIMIT"));
bytes memory data = abi.encodeWithSignature(
"transfer(address,uint256)", recipient, amount
);
vm.startBroadcast();
IOptimismPortal(portal).depositTransaction(
token, 0, gasLimit, false, data
);
vm.stopBroadcast();
}
}forge script script/ForceInclude.s.sol \
--rpc-url $L1_RPC \
--broadcast \
--private-key $PRIVATE_KEY{% endtab %} {% endtabs %}
The call emits a TransactionDeposited event on L1.
Save the transaction hash to track the deposit.
{% endstep %} {% step %}
After 5–20 minutes, confirm the recipient's balance increased.
cast call $TOKEN "balanceOf(address)(uint256)" $RECIPIENT --rpc-url $L2_RPC{% endstep %} {% endstepper %}
depositTransactionemitsTransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData)on L1.- MegaETH watches for these events and converts each one into a deposited transaction (type
0x7E) to execute on L2. - The deposited transaction runs on L2 with your L1 wallet address as the sender (no address transformation is applied for regular wallets).
_gasLimitcaps L2 execution gas. If unused, it is not refunded, but no ETH is charged on L1 for it — only the standard Ethereum gas fee for callingdepositTransactionapplies.- Setting
_valueforwards ETH to_to; it must equalmsg.value.
For the formal deposit specification, see Deposits in the OP Stack spec.