Skip to content

Bridge USDSM

This guide shows how to bridge USDSM between chains programmatically. The process requires two transactions: an ERC-20 approval and a bridge send.

Prerequisites

  • USDSM tokens on the source chain
  • Native gas token (ETH/XTZ) on the source chain for transaction fees + LayerZero messaging fee
  • The destination chain's Endpoint ID

Quick Start (ethers.js v6)

Step 1: Set up contracts

javascript
import { ethers } from 'ethers';

// Connect to source chain (example: Ethereum)
const provider = new ethers.JsonRpcProvider('YOUR_RPC_URL');
const signer = new ethers.Wallet('YOUR_PRIVATE_KEY', provider);

// Contract addresses (Ethereum — see overview for other chains)
const USDSM_ADDRESS = '0x399B29975CBE313C56269cD5097F5AE097Fa2741';
const ADAPTER_ADDRESS = '0x0aFa4BE6fA5ebAf5fFEe3AB9F88EAA3e16c37aDE';
const MINTER_BURNER_ADDRESS = '0x6A02e153bCbF9e6BE45C0b49F7F0ABc82859ebD5';

// Minimal ABIs
const ERC20_ABI = ['function approve(address, uint256) returns (bool)'];
const ADAPTER_ABI = [
  'function quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes), bool) view returns ((uint256,uint256))',
  'function send((uint32,bytes32,uint256,uint256,bytes,bytes,bytes), (uint256,uint256), address) payable returns ((bytes32,uint64,(uint256,uint256))'
];

const usdsm = new ethers.Contract(USDSM_ADDRESS, ERC20_ABI, signer);
const adapter = new ethers.Contract(ADAPTER_ADDRESS, ADAPTER_ABI, signer);

Step 2: Build send parameters

javascript
// Destination: Arbitrum (EID 30110)
const DST_EID = 30110;
const amount = ethers.parseEther('100'); // 100 USDSM

// Encode recipient as bytes32 (pad address to 32 bytes)
const recipientBytes32 = ethers.zeroPadValue(signer.address, 32);

// Enforced options: 120k gas for lzReceive
const extraOptions = '0x0003010011010000000000000000000000000001d4c0';

const sendParam = {
  dstEid: DST_EID,
  to: recipientBytes32,
  amountLD: amount,
  minAmountLD: amount,       // Set slippage tolerance if needed
  extraOptions: extraOptions,
  composeMsg: '0x',          // No composed message
  oftCmd: '0x',              // No OFT command
};

Step 3: Quote the messaging fee

javascript
const [nativeFee, lzTokenFee] = await adapter.quoteSend(
  [sendParam.dstEid, sendParam.to, sendParam.amountLD, sendParam.minAmountLD,
   sendParam.extraOptions, sendParam.composeMsg, sendParam.oftCmd],
  false // don't pay in LZ token
);

console.log(`LayerZero fee: ${ethers.formatEther(nativeFee)} ETH`);

Step 4: Approve and send

javascript
// IMPORTANT: Approve the MinterBurner, NOT the adapter
const approveTx = await usdsm.approve(MINTER_BURNER_ADDRESS, amount);
await approveTx.wait();
console.log('Approved MinterBurner');

// Send the bridge transaction
const fee = { nativeFee, lzTokenFee: 0n };
const sendTx = await adapter.send(
  [sendParam.dstEid, sendParam.to, sendParam.amountLD, sendParam.minAmountLD,
   sendParam.extraOptions, sendParam.composeMsg, sendParam.oftCmd],
  fee,
  signer.address, // refund address for excess gas
  { value: nativeFee }
);
const receipt = await sendTx.wait();
console.log(`Bridge TX: ${receipt.hash}`);

Step 5: Track delivery

Monitor your transaction on LayerZero Scan:

https://layerzeroscan.com/tx/YOUR_TX_HASH

Delivery typically takes 1–5 minutes depending on the source chain's block confirmations.

Using viem

typescript
import { createPublicClient, createWalletClient, http, parseEther, padHex } from 'viem';
import { mainnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';

const account = privateKeyToAccount('0xYOUR_PRIVATE_KEY');
const publicClient = createPublicClient({ chain: mainnet, transport: http() });
const walletClient = createWalletClient({ account, chain: mainnet, transport: http() });

const USDSM = '0x399B29975CBE313C56269cD5097F5AE097Fa2741';
const ADAPTER = '0x0aFa4BE6fA5ebAf5fFEe3AB9F88EAA3e16c37aDE';
const MINTER_BURNER = '0x6A02e153bCbF9e6BE45C0b49F7F0ABc82859ebD5';

const amount = parseEther('100');
const dstEid = 30110; // Arbitrum

// 1. Approve MinterBurner
await walletClient.writeContract({
  address: USDSM,
  abi: [{ name: 'approve', type: 'function', inputs: [{ type: 'address' }, { type: 'uint256' }], outputs: [{ type: 'bool' }] }],
  functionName: 'approve',
  args: [MINTER_BURNER, amount],
});

// 2. Quote fee
const adapterAbi = [
  {
    name: 'quoteSend',
    type: 'function',
    stateMutability: 'view',
    inputs: [
      { name: '_sendParam', type: 'tuple', components: [
        { name: 'dstEid', type: 'uint32' },
        { name: 'to', type: 'bytes32' },
        { name: 'amountLD', type: 'uint256' },
        { name: 'minAmountLD', type: 'uint256' },
        { name: 'extraOptions', type: 'bytes' },
        { name: 'composeMsg', type: 'bytes' },
        { name: 'oftCmd', type: 'bytes' },
      ]},
      { name: '_payInLzToken', type: 'bool' },
    ],
    outputs: [
      { name: 'msgFee', type: 'tuple', components: [
        { name: 'nativeFee', type: 'uint256' },
        { name: 'lzTokenFee', type: 'uint256' },
      ]},
    ],
  },
];

const sendParam = {
  dstEid,
  to: padHex(account.address, { size: 32 }),
  amountLD: amount,
  minAmountLD: amount,
  extraOptions: '0x0003010011010000000000000000000000000001d4c0',
  composeMsg: '0x',
  oftCmd: '0x',
};

const { nativeFee } = await publicClient.readContract({
  address: ADAPTER,
  abi: adapterAbi,
  functionName: 'quoteSend',
  args: [sendParam, false],
});

// 3. Send bridge tx
// ... call adapter.send() with the quoted fee

Endpoint IDs Reference

ChainEndpoint IDConfirmations
Ethereum3010115 blocks
Base3018410 blocks
Arbitrum3011020 blocks
Etherlink302925 blocks

Higher confirmation counts mean longer wait before the message is attested by DVNs, but greater finality assurance.

Worked Example: Bridge 10 USDSM from Ethereum to Arbitrum

This is a real mainnet bridge transaction. Here's exactly what was called:

javascript
import { ethers } from 'ethers';

const provider = new ethers.JsonRpcProvider('https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY');
const signer = new ethers.Wallet('YOUR_KEY', provider);

// Ethereum mainnet addresses
const USDSM = '0x399B29975CBE313C56269cD5097F5AE097Fa2741';
const ADAPTER = '0x0aFa4BE6fA5ebAf5fFEe3AB9F88EAA3e16c37aDE';
const MINTER_BURNER = '0x6A02e153bCbF9e6BE45C0b49F7F0ABc82859ebD5';

const amount = ethers.parseEther('10'); // 10 USDSM
const DST_EID = 30110; // Arbitrum

const sendParam = {
  dstEid: DST_EID,
  to: ethers.zeroPadValue(signer.address, 32),
  amountLD: amount,
  minAmountLD: amount,
  extraOptions: '0x0003010011010000000000000000000000000001d4c0', // 120k gas
  composeMsg: '0x',
  oftCmd: '0x',
};

// 1. Quote the LZ fee
const adapter = new ethers.Contract(ADAPTER, ADAPTER_ABI, signer);
const { nativeFee } = await adapter.quoteSend(sendParam, false);
// Result: 0.000117 ETH

// 2. Approve MinterBurner (NOT the adapter)
const usdsm = new ethers.Contract(USDSM, ERC20_ABI, signer);
await (await usdsm.approve(MINTER_BURNER, amount)).wait();

// 3. Send
const tx = await adapter.send(
  sendParam,
  { nativeFee, lzTokenFee: 0n },
  signer.address,
  { value: nativeFee }
);
// TX: 0xc3752bf55496509c38ae94fc94139a3a2e6e1de76f0db02800701197aac06493

Result: 10 USDSM burned on Ethereum, 10 USDSM minted on Arbitrum. Delivered in ~3 minutes.

Track on LayerZero Scan: View transaction

Partner Integration

If you're integrating USDSM bridging into a bridge aggregator or dApp:

  1. Approval target is MinterBurner — not the OFT Adapter. Your router/facet must approve the MinterBurner contract on each chain. See the overview for addresses.

  2. Standard OFT interface — The adapter.send() call, parameters, fee quoting, and options encoding are all standard LayerZero V2 OFT. The only non-standard aspect is the approval target.

  3. Token registration data — Contact the Stable Mint team for integration support and token registry configuration.

Copyright © 2025 Stable mint Ltd. All rights reserved.