Skip to main content

How to bridge from parent chain to child chain

This guide explains how to programmatically send messages and bridge assets from a parent chain (like Ethereum) to an Arbitrum child chain. For conceptual information about the messaging protocol, see Parent to child chain messaging.

Prerequisites

  • A parent chain wallet with funds (ETH or the chain's native token)
  • Access to the parent chain's Inbox contract address
  • Familiarity with smart contract interactions

Bridging ETH to a child chain

To bridge ETH from a parent chain to a child chain, use the depositEth method on the Inbox contract:

function depositEth(address destAddr) external payable override returns (uint256)

Example: Depositing ETH

const inbox = new ethers.Contract(inboxAddress, inboxABI, parentSigner);
const tx = await inbox.depositEth(destinationAddress, {
value: ethers.utils.parseEther('0.1'), // Amount to deposit
});
await tx.wait();
warning

Depositing ETH directly via depositEth to a contract on a child chain will not invoke that contract's fallback function. If you need to trigger a fallback function, use retryable tickets instead.

How ETH deposits work

When you deposit ETH, the funds are held in the Arbitrum Bridge contract on the parent chain. The bridge then credits the deposited amount to your address on the child chain:

Depositing Ether

Address aliasing for contract depositors

When you deposit ETH from the parent chain, the destination address on the child chain depends on the caller type:

  • EOA caller: The deposited ETH appears at the same address on the child chain
  • Contract caller: The ETH goes to the contract's aliased address on the child chain
  • 7702-enabled account: Similar to contracts, uses the aliased address

The alias is calculated as:

Child_Alias = Parent_Contract_Address + 0x1111000000000000000000000000000000001111

To recover the original parent chain address in your child chain contract, use the AddressAliasHelper library:

modifier onlyFromMyL1Contract() {
require(
AddressAliasHelper.undoL1ToL2Alias(msg.sender) == myL1ContractAddress,
"ONLY_COUNTERPART_CONTRACT"
);
_;
}

Sending transactions via the Delayed Inbox

The Delayed Inbox allows you to send arbitrary messages from the parent chain to the child chain, bypassing the Sequencer if needed.

Sending signed messages

Signed messages prove EOA ownership and execute with the signer's address on the child chain (no aliasing).

Method 1: sendL2Message

More flexible, can be called by EOAs or contracts:

function sendL2Message(
bytes calldata messageData
) external returns (uint256)

Method 2: sendL2MessageFromOrigin

Cheaper gas costs, only callable by EOAs:

function sendL2MessageFromOrigin(
bytes calldata messageData
) external returns (uint256)

Example use case: Withdraw Ether tutorial

Sending unsigned messages

Unsigned messages are automatically aliased for security. The Delayed Inbox provides four methods for unsigned messages, divided by sender type (EOA vs contract) and funding source (parent chain vs child chain balance):

From EOAs (with nonce for replay protection)

sendL1FundedUnsignedTransaction - Transfers value from parent to child chain:

function sendL1FundedUnsignedTransaction(
uint256 gasLimit,
uint256 maxFeePerGas,
uint256 nonce,
address to,
bytes calldata data
) external payable returns (uint256)

sendUnsignedTransaction - Uses child chain balance (no L1 funds transferred):

function sendUnsignedTransaction(
uint256 gasLimit,
uint256 maxFeePerGas,
uint256 nonce,
address to,
uint256 value,
bytes calldata data
) external returns (uint256)

From Contracts (standard Ethereum replay protection)

sendContractTransaction - Uses contract's existing child chain balance:

function sendContractTransaction(
uint256 gasLimit,
uint256 maxFeePerGas,
address to,
uint256 value,
bytes calldata data
) external returns (uint256)

sendL1FundedContractTransaction - Transfers additional funds from parent to child:

function sendL1FundedContractTransaction(
uint256 gasLimit,
uint256 maxFeePerGas,
address to,
bytes calldata data
) external payable returns (uint256)

Creating retryable tickets

Retryable tickets are Arbitrum's canonical mechanism for reliable cross-chain message delivery. They automatically retry failed executions.

Key retryable ticket parameters

Understanding these parameters is crucial for successful retryable ticket creation:

  • l1CallValue (msg.value): Total ETH sent with the transaction from parent chain. This funds the ticket submission, gas, and call value.

  • to: The destination child chain address that will receive the retryable ticket execution.

  • l2CallValue: The amount of ETH to be sent as callvalue when executing the retryable on the child chain. This is supplied within the l1CallValue deposit.

  • maxSubmissionCost: Maximum ETH to pay for submitting the ticket. This amount is:

    • Supplied within the deposit (l1CallValue)
    • Later deducted from sender's child chain balance
    • Directly proportional to retryable data size and parent chain basefee
  • excessFeeRefundAddress: Where to refund unused gas and submission costs:

    • Formula: (gasLimit × maxFeePerGas - execution cost) + (maxSubmissionCost - submission cost)
    • Important: If auto-redeem fails, excess deposit goes to the alias of the L1 sender, not this address
  • callValueRefundAddress: The child chain address to credit the l2CallValue if the ticket times out or gets canceled. This address is also the "beneficiary" with permission to cancel the ticket.

  • gasLimit: Maximum gas for child chain execution of the ticket. Used for the automatic redemption attempt.

  • maxFeePerGas: Gas price bid for child chain execution, supplied within the deposit (l1CallValue).

  • data: Calldata to send to the destination address on the child chain.

Creating a retryable ticket

function createRetryableTicket(
address to,
uint256 l2CallValue,
uint256 maxSubmissionCost,
address excessFeeRefundAddress,
address callValueRefundAddress,
uint256 gasLimit,
uint256 maxFeePerGas,
bytes calldata data
) external payable returns (uint256)

Example using the Arbitrum SDK

import { L1ToL2MessageGasEstimator } from '@arbitrum/sdk';

// Estimate gas for the retryable ticket
const l1ToL2MessageGasEstimator = new L1ToL2MessageGasEstimator(l2Provider);
const retryableGasParams = await l1ToL2MessageGasEstimator.estimateAll(
{
from: senderAddress,
to: destinationAddress,
l2CallValue: ethers.utils.parseEther('0.01'),
excessFeeRefundAddress: refundAddress,
callValueRefundAddress: refundAddress,
data: calldata,
},
await l1Provider.getBaseFeePerGas(),
l1Provider,
);

// Create the retryable ticket
const inbox = new ethers.Contract(inboxAddress, inboxABI, l1Signer);
const tx = await inbox.createRetryableTicket(
destinationAddress,
ethers.utils.parseEther('0.01'), // l2CallValue
retryableGasParams.maxSubmissionCost,
refundAddress,
refundAddress,
retryableGasParams.gasLimit,
retryableGasParams.maxFeePerGas,
calldata,
{
value: retryableGasParams.deposit,
},
);
await tx.wait();

Redeeming retryable tickets

Retryable tickets can auto-redeem if sufficient gas is provided. If the initial redemption fails, you can manually redeem using the ArbRetryableTx precompile:

ArbRetryableTx(address(110)).redeem(ticketId);

Next steps