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();
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:
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
ETHappears at the same address on the child chain - Contract caller: The
ETHgoes 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 ascallvaluewhen executing the retryable on the child chain. This is supplied within thel1CallValuedeposit. -
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
- Supplied within the deposit (
-
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
- Formula:
-
callValueRefundAddress: The child chain address to credit thel2CallValueif 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
- For protocol-level details, see Parent to child chain messaging
- For token bridging, see Token bridging overview
- For bridging tokens programmatically, see Bridge tokens programmatically