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:
| Route | Asset received | Stargate transfer fee |
|---|---|---|
| Ethereum to Tempo | USDC.e on Tempo | 0 bps |
| Tempo to Ethereum | Native USDC on Ethereum | 0 bps |
| Tempo to or from other chains | Route-dependent | Quote 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
| Token | Address | Bridge |
|---|---|---|
| USDC.e (Bridged USDC) | 0x20C000000000000000000000b9537d11c60E8b50 | Stargate |
| EURC.e (Bridged EURC) | 0x20c0000000000000000000001621e21F71CF12fb | Stargate |
| USDT0 | 0x20c00000000000000000000014f22ca97301eb73 | OFT |
| frxUSD | 0x20c0000000000000000000003554d28269e0f3c2 | OFT |
| cUSD | 0x20c0000000000000000000000520792dcccccccc | OFT |
| stcUSD | 0x20c0000000000000000000008ee4fcff88888888 | OFT |
| GUSD | 0x20c0000000000000000000005c0bac7cef389a11 | OFT |
| rUSD | 0x20c0000000000000000000007f7ba549dd0251b9 | OFT |
| wsrUSD | 0x20c000000000000000000000aeed2ec36a54d0e5 | OFT |
See the full token list at tokenlist.tempo.xyz.
LayerZero contracts on Tempo
| Contract | Address |
|---|---|
| EndpointV2 | 0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C |
| LZEndpointDollar | 0x0cEb237E109eE22374a567c6b09F373C73FA4cBb |
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
| Token | Stargate OFT Contract |
|---|---|
| USDC.e | 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 |
| EURC.e | 0x7753Dc8d4bd48Db599Da21E08b1Ab1D6FDFfdC71 |
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
| Chain | LZ Endpoint ID | Stargate USDC Pool |
|---|---|---|
| Ethereum | 30101 | 0xc026395860Db2d07ee33e05fE50ed7bD583189C7 |
| Arbitrum | 30110 | 0xe8CDF27AcD73a434D661C84887215F7598e7d0d3 |
| Base | 30184 | 0x27a16dc786820B16E5c9028b75B99F6f604b5d26 |
| Optimism | 30111 | 0xcE8CcA271Ebc0533920C83d39F417ED6A0abB7D0 |
| Polygon | 30109 | 0x9Aa02D4Fae7F58b8E8f34c66E756cC734DAc7fe4 |
| Avalanche | 30106 | 0x5634c4a5FEd09819E3c46D86A965Dd9447d86e47 |
Bridge to Tempo
Using the Stargate app
- Go to stargate.finance
- Select your source chain and token (USDC or EURC)
- Set Tempo as the destination chain
- 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.orgTake 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_KEYSend 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_KEYUsing 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.xyzTake 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_KEYWrap 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_KEYApprove 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_KEYApprove token on Tempo
cast send 0x20C000000000000000000000b9537d11c60E8b50 \
'approve(address,uint256)' \
0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \
<AMOUNT> \
--rpc-url https://rpc.tempo.xyz \
--private-key $PRIVATE_KEYSend 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_KEYUsing 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:
| Mode | oftCmd | Delivery | Cost |
|---|---|---|---|
| Taxi | 0x (empty) | Immediate - message sent right away | Higher gas cost |
| Bus | 0x00 (1 byte) | Batched - waits for other passengers | Lower 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_KEYEndpointDollar
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
LZEndpointDollaris required. - Bridging from Tempo -
LZEndpointDollardeducts the messaging fee from an LZD token (a wrapped TIP-20 stablecoin) instead ofmsg.value. Before callingsendToken/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
- LayerZero V2 documentation
- Stargate documentation
- Bridges & Exchanges on Tempo
- Getting Funds on Tempo
Was this helpful?