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.

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

Source chain Stargate pools

ChainLZ Endpoint IDStargate USDC Pool
Ethereum301010xc026395860Db2d07ee33e05fE50ed7bD583189C7
Arbitrum301100xe8CDF27AcD73a434D661C84887215F7598e7d0d3
Base301840x27a16dc786820B16E5c9028b75B99F6f604b5d26
Optimism301110xcE8CcA271Ebc0533920C83d39F417ED6A0abB7D0
Polygon301090x9Aa02D4Fae7F58b8E8f34c66E756cC734DAc7fe4
Avalanche301060x5634c4a5FEd09819E3c46D86A965Dd9447d86e47

Bridge to Tempo via Stargate

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 via Stargate

To bridge from Tempo back to another chain, call sendToken on the Stargate OFT contract on Tempo. The process is the same - quote, approve, send - but the source contract and destination EID are swapped.

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 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 { createWalletClient, createPublicClient, http, parseUnits, pad } from 'viem'
import { tempo } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
 
const account = privateKeyToAccount('0x...')
 
const walletClient = createWalletClient({
  account,
  chain: tempo,
  transport: http(),
})
 
// Stargate OFT for USDC.e on Tempo
const stargateOFT = '0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392' as const
// USDC.e on Tempo
const usdce = '0x20C000000000000000000000b9537d11c60E8b50' 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)
}
 
// 1. Quote the fee
const publicClient = createPublicClient({ chain: tempo, transport: http() })
 
const msgFee = await publicClient.readContract({
  address: stargateOFT,
  abi: stargateAbi, // same ABI as above
  functionName: 'quoteSend',
  args: [sendParam, false],
})
 
// 2. Approve token
await walletClient.writeContract({
  address: usdce,
  abi: erc20Abi,
  functionName: 'approve',
  args: [stargateOFT, amount],
})
 
// 3. Send the bridge transaction (no value - fee handled via EndpointDollar)
await walletClient.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.

This is transparent for end users:

  • Bridging to Tempo - fees are paid in native gas on the source chain (ETH, MATIC, AVAX, etc.) as normal.
  • Bridging from Tempo - LZEndpointDollar automatically deducts the messaging fee from a TIP-20 stablecoin. No msg.value is needed.
  • Developers don't need to interact with LZEndpointDollar directly. The OFT contracts on Tempo handle it internally.

Further reading