On this page

Bridge an ERC-20

The same EVM flow handles a canonical token, a return to its original chain, and a representation-to-representation transfer.

1. Identify the source token

const identity = await sourceClient.readContract({
  address: omnisea,
  abi: bridgeAbi,
  functionName: "oftToOriginal",
  args: [token],
});

const isRepresentation = identity[3];
  • Canonical token: use quoteSendOriginal and sendOriginal.
  • Omniasset representation: use quoteSendOFT and sendOFT.

2. Check the destination

For a non-original destination, call representationFor with the canonical chain ID, EVM address type 1, and left-padded original token address. A zero result means the first transfer must deploy the representation.

const isFirstTransfer = representation === zeroAddress;

Use false when returning to the original chain, where Omnisea unlocks backing instead of deploying a token.

3. Build options

function u128(value: bigint) {
  return value.toString(16).padStart(32, "0");
}

function lzReceiveOptions(gas: bigint) {
  return `0x000301002101${u128(gas)}${u128(0n)}` as const;
}

const receiveGas = isFirstTransfer
  ? await sourceClient.readContract({ address: omnisea, abi: bridgeAbi, functionName: "minCreationGas" })
  : await sourceClient.readContract({ address: omnisea, abi: bridgeAbi, functionName: "minTransferGas" });
const options = lzReceiveOptions(receiveGas);

Read Executor options before adding destination composition.

4. Approve a canonical token

sendOriginal transfers directly from the sender to OmniseaVault, so approve the core entrypoint. sendOFT burns through the bridge and needs no ERC-20 allowance.

await walletClient.writeContract({
  address: token,
  abi: erc20Abi,
  functionName: "approve",
  args: [omnisea, amount],
});

5. Quote and send

const compose = {
  composer: zeroAddress,
  gasLimit: 0n,
  message: "0x",
} as const;

const args = [dstEid, token, amount, recipient, isFirstTransfer, compose, options] as const;
const quoteFunction = isRepresentation ? "quoteSendOFT" : "quoteSendOriginal";
const sendFunction = isRepresentation ? "sendOFT" : "sendOriginal";

const totalFee = await sourceClient.readContract({
  address: omnisea,
  abi: bridgeAbi,
  functionName: quoteFunction,
  args,
});

const hash = await walletClient.writeContract({
  address: omnisea,
  abi: bridgeAbi,
  functionName: sendFunction,
  args,
  value: totalFee,
});

The native value must equal the live quote exactly. If the LayerZero fee moves before submission, re-quote and retry in a bounded loop.

6. Track and recover

BridgeMessageSent includes the LayerZero GUID. Track the source transaction through LayerZero Scan or the Omnisea interface. If the main destination execution is cached as failed, anyone can retry it or pay to restore the asset to the source sender. A failed optional composer is retried separately and never repeats the bridge credit.

Experimental Beta is Live-Learn more about the Pilot