nora

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.
chainRefLayerZero eid
eip155:130101
eip155:845330184
eip155:13730109
eip155:1115511140161
eip155:8453240245
eip155:8000240267

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

  1. quoteSend(sendParam, false) returns the fee. nativeFee is in the source chain's native token (ETH on Ethereum and Base, POL on Polygon).
  2. send(sendParam, fee, refundAddress) with msg.value exactly equal to nativeFee: more or less reverts with NotEnoughNative and nothing is burned. It burns amountLD from the caller and emits OFTSent. refundAddress receives the difference only when the fee recomputed at send time is lower than the one you passed; use your own wallet.

SendParam fields:

FieldValue
dstEiddestination eid from the table
torecipient 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
amountLDamount in token units, 6 decimals (5000000 = 5 BRS)
minAmountLDsame as amountLD; BRS has 6 shared decimals, so nothing is rounded away
extraOptions0x; the contract already enforces the destination gas
composeMsg0x
oftCmd0x

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/offramp burns only from a wallet authorized on that chain's whitelister. Register the wallet with the destination chainId in 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, send reverts and nothing is burned; a pathway pause also makes quoteSend revert, a token pause does not, so a successful quote is not proof the send will go through. On the destination side, your send succeeds 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's lzReceive for it, delivers the credit. Nothing is lost, but the balance is out of reach in between.

See also

On this page