Appearance
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_HASHDelivery 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 feeEndpoint IDs Reference
| Chain | Endpoint ID | Confirmations |
|---|---|---|
| Ethereum | 30101 | 15 blocks |
| Base | 30184 | 10 blocks |
| Arbitrum | 30110 | 20 blocks |
| Etherlink | 30292 | 5 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: 0xc3752bf55496509c38ae94fc94139a3a2e6e1de76f0db02800701197aac06493Result: 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:
Approval target is MinterBurner — not the OFT Adapter. Your router/facet must approve the MinterBurner contract on each chain. See the overview for addresses.
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.Token registration data — Contact the Stable Mint team for integration support and token registry configuration.