Using the Cross-Chain Bridge¶
The native bridge moves BNB between BNB Smart Chain (BSC) and New L1 in both directions. BNB locked on one chain arrives as native BNB on the other, with no wrapped token. On devnet the bridge connects New L1 to BSC Testnet, and bridging tBNB from BSC Testnet is how you get BNB on the New L1 devnet.
Devnet stage
Use testnet BNB (tBNB) only. The bridge contracts have not been audited, and the devnet may be reset. The limits and delays on this page are devnet values and are deliberately small. Public networks will use different values.
Overview¶
You lock BNB in the source chain’s TokenHub. Once the source block is final, the New L1 validators and the guardians both attest the transfer, and a relayer delivers it to the destination chain, where TokenHub pays the recipient from its reserve. A transfer usually completes within a few seconds. The relayer pays the destination gas, so the recipient doesn’t need to hold BNB or do anything.
For the design, including the roles of validators, guardians, relayers, and the recovery council, see Cross-Chain Bridge.
Contracts¶
| Contract | Role | New L1 devnet (chain ID 1105) | BSC Testnet (chain ID 97) |
|---|---|---|---|
TokenHub |
Custody: locks outgoing BNB and pays out incoming BNB. This is the contract users call. | 0x0000000000000000000000000000000000001004 |
0xBfe025E8d71CeFeAA3921e89960A26F33B1594AF |
CrossChainHub |
Verifies certificates and records the outcome of each transfer | 0x0000000000000000000000000000000000002000 |
0x2E7004b6dF81650e48d74233923627b4308ad6C5 |
BridgeGovHub |
Bridge governance, pause, and freeze | 0x0000000000000000000000000000000000002007 |
0xEB13090dB3CdB2440bA490DA44AF3aE113Cad1a3 |
On New L1 these contracts are genesis predeploys (see System Contracts). On BSC they are upgradeable proxies named NewL1TokenHub, NewL1CrossChainHub, and NewL1BridgeGovHub. Both chains expose the same user-facing functions.
Bridge BNB¶
Before You Start¶
- Hold enough BNB on the source chain for the amount, the delivery fee, and gas for the source transaction. To get tBNB on BSC Testnet, use the BSC faucet.
- Decide on the recipient address on the destination chain. It doesn’t need to hold any BNB.
- Optionally, call
paused()andfrozen()on both chains’TokenHub. If the source chain is paused,transferOutreverts. If the destination is paused, delivery waits until it is unpaused.
Send a Transfer¶
Call transferOut(address recipient, uint128 deliveryFee) on the source chain’s TokenHub, with the amount plus the fee as the transaction value:
recipient: the address that receives the BNB on the destination chain.deliveryFee: in wei, no less thanminDeliveryFee()and no more thanmaxDeliveryFee().- Transaction value: the amount to deliver plus
deliveryFee. The recipient receives the value minusdeliveryFee.
The examples use Foundry’s cast to deliver 1 BNB with a 0.0003 BNB fee. Add your usual signing option, such as --account or --ledger.
RPC=https://bsc-testnet-dataseed.bnbchain.org
TOKEN_HUB=0xBfe025E8d71CeFeAA3921e89960A26F33B1594AF
RECIPIENT=0xYourAddressOnNewL1
# Current minimum delivery fee in wei (300000000000000 = 0.0003 BNB)
cast call $TOKEN_HUB "minDeliveryFee()(uint128)" --rpc-url $RPC
# Value = 1 BNB to deliver + 0.0003 BNB fee
cast send $TOKEN_HUB "transferOut(address,uint128)" $RECIPIENT 0.0003ether \
--value 1.0003ether --rpc-url $RPC
RPC=https://newl1-devnet-rpc.bnbchain.org/
TOKEN_HUB=0x0000000000000000000000000000000000001004
RECIPIENT=0xYourAddressOnBSC
# Current minimum delivery fee in wei (300000000000000 = 0.0003 BNB)
cast call $TOKEN_HUB "minDeliveryFee()(uint128)" --rpc-url $RPC
# Value = 1 BNB to deliver + 0.0003 BNB fee
cast send $TOKEN_HUB "transferOut(address,uint128)" $RECIPIENT 0.0003ether \
--value 1.0003ether --rpc-url $RPC
New L1 charges for the declared gas limit, so don’t pad the gas estimate.
transferOut reverts and locks nothing if any of its checks fail: a fee outside the bounds, an amount above the per-transfer cap, a paused or frozen bridge, a blacklisted sender or recipient, or the zero address as recipient.
Track a Transfer¶
-
Find the
messageId.transferOutemits it as the first indexed topic of theTransferLockedevent in your transaction receipt. On BscScan Testnet it appears in the transaction’s logs. Withcast, on either chain:cast receipt <TX_HASH> --rpc-url $RPC --json | jq -r '.logs[] | select(.topics[0] == "0x177324ae85361dbd3f63aab4130d0954c6bff770d4664ac9ea69a0d839802678") | .topics[1]' -
Check the destination. Call
terminalRecord(messageId)on the destination chain’sCrossChainHub. This example checks a transfer to New L1:cast call 0x0000000000000000000000000000000000002000 \ "terminalRecord(bytes32)(bool,(bool,bool,address,uint128,address,bool))" <MESSAGE_ID> \ --rpc-url https://newl1-devnet-rpc.bnbchain.org/The first value stays
falseuntil the destination records an outcome. After that it istrue, followed by(present, success, recipient, creditedAmount, deliveredBy, …).success = truemeans the transfer was delivered.
A delivered transfer is not always in the recipient’s balance yet. One delivery transaction can carry several transfers, so look for the destination TokenHub event with your messageId:
| Event | Meaning | What to do |
|---|---|---|
CreditDelivered |
The BNB was sent to the recipient. | Nothing. The transfer is complete. |
CreditLocked |
The amount is at or above the large-transfer threshold and is held until the lock ends. | Call matureLockedCredit after the lock ends. See Large Transfers. |
| Neither | Sending to the recipient failed, so the amount was recorded as a claim. | Call withdraw. See Claims. |
Don’t use ClaimCredited to detect a failed payout. It has no messageId, and each delivery also emits one for the executor’s share of the fee. To see the recipient’s claim, call claim(recipient).
Large Transfers¶
If the amount is at or above LARGE_TRANSFER_THRESHOLD() (10 BNB on devnet), the destination holds it for LARGE_TRANSFER_LOCK() seconds (10 seconds on devnet) instead of paying it out right away. During the lock, governance can cancel a forged transfer before anyone is paid. Expect a much longer lock on public networks: the design requires at least 48 hours there.
Relayers don’t release locked transfers. After the lock ends, someone has to call matureLockedCredit(messageId) on the destination TokenHub. Anyone can call it, and the BNB always goes to the transfer’s recipient, never to the caller. If sending to the recipient fails, the amount becomes a claim instead.
# Seconds left on the lock; 0 means it can be released
cast call $TOKEN_HUB "lockRemaining(bytes32)(uint256)" <MESSAGE_ID> --rpc-url $RPC
# Send the BNB to the recipient
cast send $TOKEN_HUB "matureLockedCredit(bytes32)" <MESSAGE_ID> --rpc-url $RPC
Here $TOKEN_HUB and $RPC refer to the destination chain. The lock clock stops while the bridge is frozen.
Claims¶
The destination sends BNB to the recipient with a 10,000 gas limit. If the recipient is a contract that needs more gas than that to receive BNB, or that rejects it, the amount is recorded as a claim for the recipient instead. Refunds are also paid as claims.
To pay out a claim, call withdraw(account) on the TokenHub that holds it. Anyone can call it, and it always pays the account’s full claim to that account. claim(account) returns the current balance.
cast call $TOKEN_HUB "claim(address)(bool,(bool,uint128))" <ACCOUNT> --rpc-url $RPC
cast send $TOKEN_HUB "withdraw(address)" <ACCOUNT> --rpc-url $RPC
withdraw forwards all available gas. If the account can’t receive BNB at all, withdraw reverts and the claim stays where it is.
Fees¶
| Cost | Who pays | Amount |
|---|---|---|
| Delivery fee | Sender, as part of the transferOut value |
Between minDeliveryFee() and maxDeliveryFee(), both adjustable by governance. Devnet: 0.0003 BNB minimum, 1 BNB maximum |
| Source chain gas | Sender | Gas for the transferOut transaction |
| Destination delivery gas | Relayer | Nothing for the recipient |
The delivery fee is shared among the executor of the delivery and the signing validators and guardians (see Fees). The current relayers deliver every batch no matter what fee it carries, so paying more than the minimum doesn’t speed up delivery.
Releasing a large transfer and withdrawing a claim are separate destination-chain transactions, paid for by whoever sends them.
Limits and Parameters¶
Both chains currently use the values below. You can read the live values from the TokenHub on either chain.
| Parameter | Getter | Devnet value | Effect |
|---|---|---|---|
| Minimum delivery fee | minDeliveryFee() |
0.0003 BNB | transferOut reverts below it |
| Maximum delivery fee | maxDeliveryFee() |
1 BNB | transferOut reverts above it |
| Per-transfer cap | PER_TRANSFER_CAP() |
100 BNB | transferOut reverts if the amount, excluding the fee, is larger |
| Large-transfer threshold | LARGE_TRANSFER_THRESHOLD() |
10 BNB | Amounts at or above it are locked on the destination |
| Large-transfer lock | LARGE_TRANSFER_LOCK() |
10 seconds | How long a large transfer stays locked |
| Release rate limit | releaseLimitCapacity(), releaseCapacity() (currently available) |
200 BNB, refilling continuously over 30 seconds | Caps how much the destination releases, counting amount plus fee. Transfers beyond it wait |
| Recipient gas | PUSH_GAS_LIMIT() |
10,000 | Gas forwarded when BNB is sent to the recipient |
The rate limit only delays transfers. A transfer that doesn’t fit now is delivered once the limit refills. The rate limit never makes it fail or triggers a refund.
Before You Bridge¶
- Native BNB only. The bridge doesn’t carry BEP-20 tokens. Tokens sent to a bridge contract are not bridged, and there’s no way to recover them.
- Always call
transferOut. A plain BNB transfer toTokenHubreverts. On New L1, a reverted transaction still costs its declared gas. - Check the recipient. BNB arrives at the exact address you pass as
recipient, on the destination chain. An EOA key controls the same address on both chains. A smart-contract wallet, such as a multisig, may not exist at that address on the other chain, or the address may belong to someone else there. Only bridge to an address you control on the destination chain. - Contracts must accept BNB. A contract recipient needs a
receiveor payable fallback function. If receiving uses more than 10,000 gas, the BNB becomes a claim. If the contract can’t accept BNB at all, the funds can’t be paid out. The same applies to a contract that callstransferOut: a refund goes back to it as the sender. - Transfers can’t be cancelled. A locked transfer is either delivered or, through governance, refunded. Transfers don’t expire.
- Pauses and freezes. When a chain’s bridge is paused,
transferOutreverts on that chain and deliveries to that chain wait. Claims and expired large-transfer locks can still be withdrawn. A freeze, the emergency stop, also blocks withdrawals and large-transfer releases until it is cleared. In both cases, transfers that are already locked wait. They are never lost. - Blacklists. Each chain’s bridge governance can blacklist addresses. A blacklisted sender or recipient can’t use
transferOut. If the sender or recipient is blacklisted after the transfer was locked, the destination skips the transfer and relayers may stop retrying it. Removing the address from the blacklist doesn’t restart delivery: the transfer must be delivered again withexecuteSingleon the destinationCrossChainHub. Otherwise governance can refund the transfer. - Transfers are public. The sender, recipient, and amount are visible on both chains. Bridging doesn’t go through the shielded pool.
- BSC finality. Transfers from BSC wait until BSC fast finality finalizes the source block. If fast finality stalls, they wait until it recovers, even while BSC keeps producing blocks.
Failures and Refunds¶
The bridge never refunds on its own. No timeout, error, or amount of elapsed time turns a locked transfer into a refund. A slow transfer (paused, rate-limited, or waiting for finality) is delivered once the cause clears.
If a transfer can never be delivered, for example because its recipient is blacklisted, validators and guardians can declare it failed on the destination chain and release a refund on the source chain (see Governance). The original sender is credited with the amount plus half the delivery fee, as a claim on the source chain. To collect it, call withdraw(sender). A transfer can never be both delivered and refunded.
Interface¶
The functions and events for sending, tracking, and completing a transfer:
interface ITokenHub {
struct Claim { bool present; uint128 amount; }
function transferOut(address recipient, uint128 deliveryFee) external payable returns (bytes32 messageId);
function matureLockedCredit(bytes32 messageId) external returns (uint128 amount);
function withdraw(address account) external returns (uint128 amount);
function minDeliveryFee() external view returns (uint128);
function maxDeliveryFee() external view returns (uint128);
function lockRemaining(bytes32 messageId) external view returns (uint256); // reverts if nothing is locked
function claim(address account) external view returns (bool present, Claim memory record);
function paused() external view returns (bool);
function frozen() external view returns (bool);
event TransferLocked(
bytes32 indexed messageId, address indexed sender, address indexed recipient,
uint128 principal, uint128 deliveryFee, uint64 sourceNonce, bytes32 payloadHash
);
event CreditDelivered(bytes32 indexed messageId, address indexed recipient, uint128 amount);
event CreditLocked(bytes32 indexed messageId, address indexed recipient, uint128 amount, uint64 lockedAt);
event ClaimCredited(address indexed account, uint128 amount, uint128 newTotal);
}
interface ICrossChainHub {
struct TerminalRecord {
bool present;
bool success;
address recipient;
uint128 creditedAmount;
address deliveredBy;
bool countedTowardRange;
}
function terminalRecord(bytes32 messageId) external view returns (bool present, TerminalRecord memory record);
}
transferOutreturns themessageId, so a contract that bridges can store it without parsing logs.- A terminal record with
present = trueandsuccess = falsemeans governance declared the transfer failed, and a refund follows on the source chain. - Before treating a delivery as irreversible, wait until the destination block that contains it is finalized. On New L1, use the
finalizedblock tag (see Consensus).