PerformanceBlog
Tempo MCP serverGive agents search and read tools for Tempo docs
Skip to content
LogoLogo

Bridge via LayerZero

LayerZero is the omnichain messaging protocol that powers token bridging on Tempo. Tokens are bridged using the OFT (Omnichain Fungible Token) standard - the source chain locks or burns tokens and the destination chain mints the bridged equivalent.

There are two flavors of OFT on Tempo:

  • Stargate - an application built on LayerZero that manages liquidity pools. Tokens like USDC.e and EURC.e use Stargate's sendToken() interface.
  • Standard OFT - token issuers (e.g. Tether for USDT0) deploy their own OFT adapters using LayerZero's send() interface directly.

Both use the same underlying LayerZero endpoint on Tempo.

USDC.e and native USDC

USDC.e is the bridged representation of USDC on Tempo. It is backed 1:1 by native USDC in Stargate liquidity infrastructure.

When USDC is bridged to Tempo through Stargate, native USDC is deposited into a Stargate pool and the equivalent amount of USDC.e is minted on Tempo. When USDC.e is bridged out, USDC.e is burned on Tempo and USDC is released through Stargate.

The zero-transfer-fee path is between Tempo and Ethereum:

RouteAsset receivedStargate transfer fee
Ethereum to TempoUSDC.e on Tempo0 bps
Tempo to EthereumNative USDC on Ethereum0 bps
Tempo to or from other chainsRoute-dependentQuote before execution

Routes between Tempo and other chains can carry standard Stargate route fees. If an integrator needs native USDC on another chain, the preferred settlement path is to bridge USDC.e from Tempo to native USDC on Ethereum, then move USDC onward using CCTP or another supported route. The LayerZero Value Transfer API can help discover and execute available routes.

Bridged tokens on Tempo

TokenAddressBridge
USDC.e (Bridged USDC)0x20C000000000000000000000b9537d11c60E8b50Stargate
EURC.e (Bridged EURC)0x20c0000000000000000000001621e21F71CF12fbStargate
USDT00x20c00000000000000000000014f22ca97301eb73OFT
frxUSD0x20c0000000000000000000003554d28269e0f3c2OFT
cUSD0x20c0000000000000000000000520792dccccccccOFT
stcUSD0x20c0000000000000000000008ee4fcff88888888OFT
GUSD0x20c0000000000000000000005c0bac7cef389a11OFT
rUSD0x20c0000000000000000000007f7ba549dd0251b9OFT
wsrUSD0x20c000000000000000000000aeed2ec36a54d0e5OFT

See the full token list at tokenlist.tempo.xyz.

LayerZero contracts on Tempo

ContractAddress
EndpointV20x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C
LZEndpointDollar0x0cEb237E109eE22374a567c6b09F373C73FA4cBb

Tempo's LayerZero Endpoint ID is 30410.

Stargate tokens

Stargate manages liquidity pools for USDC.e and EURC.e. Use the Stargate sendToken() interface for these tokens.

Stargate contracts on Tempo

TokenStargate OFT Contract
USDC.e0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392
EURC.e0x7753Dc8d4bd48Db599Da21E08b1Ab1D6FDFfdC71

These are the contracts users call to bridge tokens. They are not the authoritative contracts for Stargate v2 message security configuration. To inspect the live DVN setup for Stargate routes, resolve the chain's TokenMessaging OApp from Stargate metadata and read the LayerZero EndpointV2 config for that OApp. On Tempo, the Stargate v2 TokenMessaging OApp is 0x19Ff94Fe4C93D546e4DB3E1FB124D45366B0b9F5.

Source chain Stargate pools

ChainLZ Endpoint IDStargate USDC Pool
Ethereum301010xc026395860Db2d07ee33e05fE50ed7bD583189C7
Arbitrum301100xe8CDF27AcD73a434D661C84887215F7598e7d0d3
Base301840x27a16dc786820B16E5c9028b75B99F6f604b5d26
Optimism301110xcE8CcA271Ebc0533920C83d39F417ED6A0abB7D0
Polygon301090x9Aa02D4Fae7F58b8E8f34c66E756cC734DAc7fe4
Avalanche301060x5634c4a5FEd09819E3c46D86A965Dd9447d86e47

Bridge to Tempo

Using the Stargate app

  1. Go to stargate.finance
  2. Select your source chain and token (USDC or EURC)
  3. Set Tempo as the destination chain
  4. Enter the amount, approve, and send

Using cast (Foundry)

This example bridges USDC from Base to Tempo. Replace addresses for other tokens or source chains.

Get a quote

cast call 0x27a16dc786820B16E5c9028b75B99F6f604b5d26 \
  'quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),bool)((uint256,uint256))' \
  "(30410,$(cast abi-encode 'f(address)' <TEMPO_WALLET_ADDRESS>),<AMOUNT>,<MIN_AMOUNT>,0x,0x,0x)" \
  false \
  --rpc-url https://mainnet.base.org

Take the first returned number as <NATIVE_FEE>.

Approve token on source chain

cast send 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 \
  'approve(address,uint256)' \
  0x27a16dc786820B16E5c9028b75B99F6f604b5d26 \
  <AMOUNT> \
  --rpc-url https://mainnet.base.org \
  --private-key $PRIVATE_KEY

Send bridge transaction

cast send 0x27a16dc786820B16E5c9028b75B99F6f604b5d26 \
  'sendToken((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)' \
  "(30410,$(cast abi-encode 'f(address)' <TEMPO_WALLET_ADDRESS>),<AMOUNT>,<MIN_AMOUNT>,0x,0x,0x)" \
  "(<NATIVE_FEE>,0)" \
  <SOURCE_ADDRESS> \
  --value <NATIVE_FEE> \
  --rpc-url https://mainnet.base.org \
  --private-key $PRIVATE_KEY

Verify transaction status

https://scan.layerzero-api.com/v1/messages/tx/<SOURCE_TX_HASH>

Using TypeScript (viem)

import { createWalletClient, createPublicClient, http, parseUnits, pad } from 'viem'
import { base } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
 
const account = privateKeyToAccount('0x...')
 
const walletClient = createWalletClient({
  account,
  chain: base,
  transport: http(),
})
 
// Stargate pool on Base
const stargatePool = '0x27a16dc786820B16E5c9028b75B99F6f604b5d26' as const
// USDC on Base
const usdc = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' as const
 
const amount = parseUnits('1', 6) // 1 USDC
const minAmount = parseUnits('0.99', 6) // 1% slippage tolerance
 
const sendParam = {
  dstEid: 30410, // Tempo
  to: pad(account.address),
  amountLD: amount,
  minAmountLD: minAmount,
  extraOptions: '0x' as const,
  composeMsg: '0x' as const,
  oftCmd: '0x' as const, // taxi mode (immediate)
}
 
const stargateAbi = [
  {
    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' },
        ],
      },
    ],
  },
  {
    name: 'sendToken',
    type: 'function',
    stateMutability: 'payable',
    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: '_fee',
        type: 'tuple',
        components: [
          { name: 'nativeFee', type: 'uint256' },
          { name: 'lzTokenFee', type: 'uint256' },
        ],
      },
      { name: '_refundAddress', type: 'address' },
    ],
    outputs: [],
  },
] as const
 
const erc20Abi = [
  {
    name: 'approve',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'spender', type: 'address' },
      { name: 'amount', type: 'uint256' },
    ],
    outputs: [{ type: 'bool' }],
  },
] as const
 
// 1. Quote the fee
const publicClient = createPublicClient({ chain: base, transport: http() })
 
const msgFee = await publicClient.readContract({
  address: stargatePool,
  abi: stargateAbi,
  functionName: 'quoteSend',
  args: [sendParam, false],
})
 
// 2. Approve token
await walletClient.writeContract({
  address: usdc,
  abi: erc20Abi,
  functionName: 'approve',
  args: [stargatePool, amount],
})
 
// 3. Send the bridge transaction
await walletClient.writeContract({
  address: stargatePool,
  abi: stargateAbi,
  functionName: 'sendToken',
  args: [sendParam, msgFee, account.address],
  value: msgFee.nativeFee,
})

Bridge from Tempo

To bridge from Tempo back to another chain, call sendToken on the Stargate OFT contract on Tempo. The process is similar to bridging in - quote, approve, send - but includes additional steps to prepare the messaging fee.

Because Tempo has no native gas token, LayerZero messaging fees are paid in a TIP-20 stablecoin via LZEndpointDollar. Before sending a bridge transaction, you must wrap your USDC.e into an LZD (LayerZero Dollar) token that the endpoint can consume as a fee. This involves approving USDC.e to the LZD wrapper contract, wrapping it, and then approving the resulting LZD to the Stargate pool.

Using cast (Foundry)

This example bridges USDC.e from Tempo to Base.

Quote the fee

cast call 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \
  'quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),bool)((uint256,uint256))' \
  "(30184,$(cast abi-encode 'f(address)' <DESTINATION_ADDRESS>),<AMOUNT>,<MIN_AMOUNT>,0x,0x,0x)" \
  false \
  --rpc-url https://rpc.tempo.xyz

Take the first returned number as <NATIVE_FEE> (in stablecoin units, not ETH).

Approve USDC.e to the LZD wrapper

Approve the LZEndpointDollar wrapper contract to spend <NATIVE_FEE> of your USDC.e. This is the amount needed to cover the LayerZero messaging fee.

cast send 0x20C000000000000000000000b9537d11c60E8b50 \
  "approve(address,uint256)" \
  0x0cEb237E109eE22374a567c6b09F373C73FA4cBb \
  <NATIVE_FEE> \
  --rpc-url https://rpc.tempo.xyz \
  --private-key $PRIVATE_KEY

Wrap USDC.e into LZD

Wrap your USDC.e into the LZD token so it can be used as a messaging fee by the LayerZero endpoint.

cast send 0x0cEb237E109eE22374a567c6b09F373C73FA4cBb \
  "wrap(address,address,uint256)" \
  0x20C000000000000000000000b9537d11c60E8b50 \
  <WALLET_ADDRESS> \
  <NATIVE_FEE> \
  --rpc-url https://rpc.tempo.xyz \
  --private-key $PRIVATE_KEY

Approve LZD to Stargate

Approve the Stargate OFT contract to spend your LZD so it can pay the messaging fee when sending.

cast send 0x0cEb237E109eE22374a567c6b09F373C73FA4cBb \
  "approve(address,uint256)" \
  0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \
  <NATIVE_FEE> \
  --rpc-url https://rpc.tempo.xyz \
  --private-key $PRIVATE_KEY

Approve token on Tempo

cast send 0x20C000000000000000000000b9537d11c60E8b50 \
  'approve(address,uint256)' \
  0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \
  <AMOUNT> \
  --rpc-url https://rpc.tempo.xyz \
  --private-key $PRIVATE_KEY

Send bridge transaction

No --value is needed on Tempo - the messaging fee is paid in a TIP-20 stablecoin via EndpointDollar.

cast send 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \
  'sendToken((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)' \
  "(30184,$(cast abi-encode 'f(address)' <DESTINATION_ADDRESS>),<AMOUNT>,<MIN_AMOUNT>,0x,0x,0x)" \
  "(<NATIVE_FEE>,0)" \
  <TEMPO_ADDRESS> \
  --rpc-url https://rpc.tempo.xyz \
  --private-key $PRIVATE_KEY

Verify transaction status

https://scan.layerzero-api.com/v1/messages/tx/<SOURCE_TX_HASH>

Using TypeScript (viem)

import { parseUnits, pad } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { createClient } from 'viem/tempo'
 
const account = privateKeyToAccount('0x...')
 
const client = createClient({
  account,
})
 
// Stargate OFT for USDC.e on Tempo
const stargateOFT = '0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392' as const
// USDC.e on Tempo
const usdce = '0x20C000000000000000000000b9537d11c60E8b50' as const
// LZEndpointDollar wrapper
const lzd = '0x0cEb237E109eE22374a567c6b09F373C73FA4cBb' as const
 
const amount = parseUnits('1', 6) // 1 USDC.e
const minAmount = parseUnits('0.99', 6) // 1% slippage tolerance
 
const sendParam = {
  dstEid: 30184, // Base
  to: pad(account.address),
  amountLD: amount,
  minAmountLD: minAmount,
  extraOptions: '0x' as const,
  composeMsg: '0x' as const,
  oftCmd: '0x' as const, // taxi mode (immediate)
}
 
const wrapAbi = [
  {
    name: 'wrap',
    type: 'function',
    stateMutability: 'nonpayable',
    inputs: [
      { name: 'token', type: 'address' },
      { name: 'to', type: 'address' },
      { name: 'amount', type: 'uint256' },
    ],
    outputs: [],
  },
] as const
 
// 1. Quote the fee
const msgFee = await client.readContract({
  address: stargateOFT,
  abi: stargateAbi, // same ABI as above
  functionName: 'quoteSend',
  args: [sendParam, false],
})
 
// 2. Approve USDC.e to LZD wrapper (for the messaging fee)
await client.writeContract({
  address: usdce,
  abi: erc20Abi,
  functionName: 'approve',
  args: [lzd, msgFee.nativeFee],
})
 
// 3. Wrap USDC.e into LZD
await client.writeContract({
  address: lzd,
  abi: wrapAbi,
  functionName: 'wrap',
  args: [usdce, account.address, msgFee.nativeFee],
})
 
// 4. Approve LZD to Stargate (for the messaging fee)
await client.writeContract({
  address: lzd,
  abi: erc20Abi,
  functionName: 'approve',
  args: [stargateOFT, msgFee.nativeFee],
})
 
// 5. Approve USDC.e to Stargate (for the bridge amount)
await client.writeContract({
  address: usdce,
  abi: erc20Abi,
  functionName: 'approve',
  args: [stargateOFT, amount],
})
 
// 6. Send the bridge transaction (no value - fee handled via EndpointDollar)
await client.writeContract({
  address: stargateOFT,
  abi: stargateAbi,
  functionName: 'sendToken',
  args: [sendParam, msgFee, account.address],
})

Bus vs. Taxi mode

Stargate offers two delivery modes:

ModeoftCmdDeliveryCost
Taxi0x (empty)Immediate - message sent right awayHigher gas cost
Bus0x00 (1 byte)Batched - waits for other passengersLower gas cost

All examples above use taxi mode. To use bus mode, set oftCmd to 0x00:

# cast - bus mode
oftCmd=0x00
// viem - bus mode
const sendParam = {
  // ...
  oftCmd: '0x00' as const, // bus mode
}

Standard OFT tokens

Tokens like USDT0, frxUSD, cUSD, and others are bridged using the standard LayerZero OFT send() interface. Each token issuer deploys their own OFT adapter contract. The send() interface uses the same SendParam struct as Stargate but calls send() instead of sendToken().

To bridge a standard OFT token, you need the OFT adapter contract address on the source chain. Refer to the token issuer's documentation for their deployment addresses:

The flow is the same as Stargate - quote, approve, send - but you call send() on the OFT adapter instead of sendToken() on a Stargate pool:

# Quote
cast call <OFT_ADAPTER> \
  'quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),bool)((uint256,uint256))' \
  "(30410,$(cast abi-encode 'f(address)' <TEMPO_ADDRESS>),<AMOUNT>,<MIN_AMOUNT>,0x,0x,0x)" \
  false \
  --rpc-url <SOURCE_RPC>
 
# Approve
cast send <TOKEN_ADDRESS> \
  'approve(address,uint256)' \
  <OFT_ADAPTER> \
  <AMOUNT> \
  --rpc-url <SOURCE_RPC> \
  --private-key $PRIVATE_KEY
 
# Send
cast send <OFT_ADAPTER> \
  'send((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)' \
  "(30410,$(cast abi-encode 'f(address)' <TEMPO_ADDRESS>),<AMOUNT>,<MIN_AMOUNT>,0x,0x,0x)" \
  "(<NATIVE_FEE>,0)" \
  <REFUND_ADDRESS> \
  --value <NATIVE_FEE> \
  --rpc-url <SOURCE_RPC> \
  --private-key $PRIVATE_KEY

EndpointDollar

Tempo has no native gas token, so there is no msg.value. Standard LayerZero endpoints require msg.value to pay messaging fees, which doesn't work on Tempo.

LZEndpointDollar (0x0cEb237E109eE22374a567c6b09F373C73FA4cBb) is an adapter contract that routes LayerZero messaging fees through a TIP-20 stablecoin instead of msg.value. It wraps the standard EndpointV2 so that OFT contracts can function on Tempo without modification.

How fees flow:

  • Bridging to Tempo - fees are paid in native gas on the source chain (ETH, MATIC, AVAX, etc.) as normal. No interaction with LZEndpointDollar is required.
  • Bridging from Tempo - LZEndpointDollar deducts the messaging fee from an LZD token (a wrapped TIP-20 stablecoin) instead of msg.value. Before calling sendToken / send, you must wrap USDC.e into LZD and approve LZD to the OFT contract. See Bridge from Tempo for the exact steps.

Further reading