Bridging BRS between chains
Move BRS between Ethereum, Base and Polygon straight from the token contract, with no Nora API call. BRS is a LayerZero OFT, so any holder can bridge.
BRS on the EVM chains is a LayerZero v2 OFT (Omnichain Fungible Token). Bridging burns BRS on the source chain and mints the same amount on the destination, delivered by LayerZero. It is permissionless: the holder calls the token contract directly and pays the LayerZero fee in the source chain's native token. No Nora API call is in the path; Nora's controls on the token itself still apply, see Rules that still apply. Solana BRS is outside this mesh: there is no bridge between Solana and the EVM chains.
Use this page if you are building on BRS and want to move balances between chains. To turn BRL into BRS or back, use intents.
What you need
- The BRS contract address for your environment; it is the same on every chain of that environment, see Addresses.
- The LayerZero endpoint id (
eid) of the destination. It is not the EIP-155 chain id. - A wallet on the source chain holding BRS and enough native token for gas plus the LayerZero fee.
chainRef | LayerZero eid |
|---|---|
eip155:1 | 30101 |
eip155:8453 | 30184 |
eip155:137 | 30109 |
eip155:11155111 | 40161 |
eip155:84532 | 40245 |
eip155:80002 | 40267 |
Every pair inside an environment is wired in both directions: the three mainnets form one mesh, the three testnets another. A mainnet never bridges to a testnet.
The two calls
quoteSend(sendParam, false)returns the fee.nativeFeeis in the source chain's native token (ETH on Ethereum and Base, POL on Polygon).send(sendParam, fee, refundAddress)withmsg.valueexactly equal tonativeFee: more or less reverts withNotEnoughNativeand nothing is burned. It burnsamountLDfrom the caller and emitsOFTSent.refundAddressreceives the difference only when the fee recomputed at send time is lower than the one you passed; use your own wallet.
SendParam fields:
| Field | Value |
|---|---|
dstEid | destination eid from the table |
to | recipient address, left-padded to 32 bytes. The credit is a plain mint to that address on the destination, whether or not anything controls it there, so use an address you control on the destination chain: a Safe or smart account deployed only on the source chain would receive BRS nobody can move |
amountLD | amount in token units, 6 decimals (5000000 = 5 BRS) |
minAmountLD | same as amountLD; BRS has 6 shared decimals, so nothing is rounded away |
extraOptions | 0x; the contract already enforces the destination gas |
composeMsg | 0x |
oftCmd | 0x |
The quote is live. Quote and send in the same run. If the fee rises in
between, send reverts and nothing is burned; quote again.
Example (viem)
Bridges 5 BRS from Base Sepolia to Sepolia, to the same address.
import { createWalletClient, createPublicClient, http, pad, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { baseSepolia } from "viem/chains";
const BRS = "0x0000884179F857221EFC49C21BCb94af93CEB125"; // sandbox; production address on the Addresses page
const DST_EID = 40161; // Sepolia
const abi = parseAbi([
"struct SendParam { uint32 dstEid; bytes32 to; uint256 amountLD; uint256 minAmountLD; bytes extraOptions; bytes composeMsg; bytes oftCmd; }",
"struct MessagingFee { uint256 nativeFee; uint256 lzTokenFee; }",
"function quoteSend(SendParam s, bool payInLzToken) view returns (MessagingFee)",
"function send(SendParam s, MessagingFee fee, address refundAddress) payable",
]);
// Any viem Account works here: a hardware or custodial signer in production,
// a throwaway test key on Base Sepolia.
const account = privateKeyToAccount(process.env.HOLDER_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: baseSepolia, transport: http() });
const wallet = createWalletClient({ account, chain: baseSepolia, transport: http() });
const sendParam = {
dstEid: DST_EID,
to: pad(account.address),
amountLD: 5_000_000n,
minAmountLD: 5_000_000n,
extraOptions: "0x",
composeMsg: "0x",
oftCmd: "0x",
} as const;
const fee = await publicClient.readContract({ address: BRS, abi, functionName: "quoteSend", args: [sendParam, false] });
const hash = await wallet.writeContract({
address: BRS,
abi,
functionName: "send",
args: [sendParam, fee, account.address],
value: fee.nativeFee,
});
console.log("sent", hash);Delivery on the destination usually takes one to a few minutes. Track the
source transaction hash on LayerZero Scan
(layerzeroscan.com for mainnets,
testnet.layerzeroscan.com for
testnets), or poll balanceOf on the destination. The message id (guid)
is in the OFTSent event of the source receipt, and the OFTReceived
event on the destination carries the same guid.
Rules that still apply
- Burning through an intent on the destination needs a grant there. A
bridge-in credits any address, but
POST /v2/intents/offrampburns only from a wallet authorized on that chain's whitelister. Register the wallet with the destinationchainIdin the Wallets API before you rely on offramping bridged balance. - Freeze. A frozen holder cannot bridge out. A freeze on the destination does not stop an inbound credit: the BRS lands on the frozen address and stays there until it is unfrozen.
- Pauses, on either side. Nora can pause the token (
pause()) or one pathway (a LayerZero configuration change) during an incident. On the source side,sendreverts and nothing is burned; a pathway pause also makesquoteSendrevert, a token pause does not, so a successful quote is not proof the send will go through. On the destination side, yoursendsucceeds and the burn is final; the message stays verified and undelivered until the pause is lifted, then the LayerZero executor, or anyone calling the endpoint'slzReceivefor it, delivers the credit. Nothing is lost, but the balance is out of reach in between.
See also
- Addresses — BRS addresses per network and environment.
- Wallet whitelisting — the grant a wallet needs before it can burn on a chain.
- Offramp intent — turning bridged BRS back into BRL.
- LayerZero OFT docs — the standard behind
quoteSendandsend.