From 466c2ccd4c6c55b8e8b65092682dcea0d003ec60 Mon Sep 17 00:00:00 2001 From: Uddhav Date: Thu, 14 May 2026 11:17:28 -0700 Subject: [PATCH 1/5] docs(layerzero): document TempoOFTWrapper as recommended bridge-out flow LayerZero's docs document a TempoOFTWrapper contract (0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0) that bundles wrap, approve, and send into a single transaction, reducing bridge-out from Tempo from 5 txs + quote to 2 txs + quote. The wrapper is in production use (LZ docs, Boltz Exchange integration) but is currently undocumented in Tempo's docs, so users follow the manual flow unnecessarily. Changes: - Update 'Bridge from Tempo' intro to reference both flows and list the three accepted fee tokens (USDC.e, USDT0, pathUSD) per LZ docs - Add 'Recommended: TempoOFTWrapper' section with cast and viem examples using the verified sendOFT ABI - Document maxNativeFee semantics (reverts on re-quote overshoot) - Add warning about wrapper-as-msg.sender breaking compose-with-refund - Add tip about eth_call preflight to avoid dust - Move the existing 5-tx flow under 'Manual flow' as fallback for cases the wrapper can't handle References: - https://docs.layerzero.network/v2/developers/tempo/how-to/support-ofts-and-oapps - ABI verified against BoltzExchange/boltz-web-app integration Co-Authored-By: Uddhav <255779543+letstokenize@users.noreply.github.com> --- src/pages/guide/bridge-layerzero.mdx | 187 ++++++++++++++++++++++++++- 1 file changed, 186 insertions(+), 1 deletion(-) diff --git a/src/pages/guide/bridge-layerzero.mdx b/src/pages/guide/bridge-layerzero.mdx index af1cac87..9e5956a6 100644 --- a/src/pages/guide/bridge-layerzero.mdx +++ b/src/pages/guide/bridge-layerzero.mdx @@ -262,7 +262,192 @@ await walletClient.writeContract({ 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](#endpointdollar). 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. +Because Tempo has no native gas token, LayerZero messaging fees are paid in a whitelisted stablecoin via [LZEndpointDollar](#endpointdollar). The fee token must be `USDC.e`, `USDT0`, or `pathUSD`. There are two ways to send: the **TempoOFTWrapper** (recommended, 2 transactions) or the **manual flow** (5 transactions). Both produce identical on-chain results. + +### Recommended: TempoOFTWrapper + +The [`TempoOFTWrapper`](https://docs.layerzero.network/v2/developers/tempo/how-to/support-ofts-and-oapps) bundles wrapping, approvals, and sending into a single transaction. + +| Contract | Address | +|----------|---------| +| **TempoOFTWrapper** | [`0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0`](https://explore.tempo.xyz/address/0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0) | + +`sendOFT` atomically wraps the fee portion of your stablecoin into LZD, approves LZD to the OFT, and calls `send()`. If the re-quoted fee at execution time exceeds `maxNativeFee`, the entire transaction reverts. + +#### Using cast (Foundry) + +This example bridges USDC.e from Tempo to Base, paying fees in USDC.e. + +:::::steps + +### Quote the fee + +```bash +cast call 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \ + 'quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),bool)((uint256,uint256))' \ + "(30184,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ + false \ + --rpc-url https://rpc.tempo.xyz +``` + +Take the first returned number as `` (in stablecoin units, not ETH). + +### Approve the fee token to the wrapper + +When the bridge token and fee token are the same (both USDC.e here), approve ` + ` in a single call. If they differ (e.g., bridging EURC.e but paying fees in USDC.e), approve each separately. + +```bash +cast send 0x20C000000000000000000000b9537d11c60E8b50 \ + 'approve(address,uint256)' \ + 0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0 \ + $(( + )) \ + --rpc-url https://rpc.tempo.xyz \ + --private-key $PRIVATE_KEY +``` + +### Send via the wrapper + +```bash +cast send 0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0 \ + 'sendOFT(address,address,(uint32,bytes32,uint256,uint256,bytes,bytes,bytes),uint256)' \ + 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \ + 0x20C000000000000000000000b9537d11c60E8b50 \ + "(30184,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ + \ + --rpc-url https://rpc.tempo.xyz \ + --private-key $PRIVATE_KEY +``` + +The fourth argument is `maxNativeFee` - the maximum messaging fee you accept. The call reverts if the re-quoted fee at execution exceeds this value. + +### Verify transaction status + +```text +https://scan.layerzero-api.com/v1/messages/tx/ +``` + +::::: + +#### Using TypeScript (viem) + +```typescript +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() }) +const publicClient = createPublicClient({ chain: tempo, transport: http() }) + +// Stargate OFT for USDC.e on Tempo +const stargateOFT = '0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392' as const +// USDC.e on Tempo (bridge token + fee token) +const usdce = '0x20C000000000000000000000b9537d11c60E8b50' as const +// TempoOFTWrapper +const wrapper = '0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0' 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, +} + +const wrapperAbi = [ + { + name: 'sendOFT', + type: 'function', + stateMutability: 'nonpayable', + inputs: [ + { name: 'oft', type: 'address' }, + { name: 'feeToken', type: 'address' }, + { + 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: 'maxNativeFee', type: 'uint256' }, + ], + outputs: [ + { + name: 'msgReceipt', + type: 'tuple', + components: [ + { name: 'guid', type: 'bytes32' }, + { name: 'nonce', type: 'uint64' }, + { + name: 'fee', + type: 'tuple', + components: [ + { name: 'nativeFee', type: 'uint256' }, + { name: 'lzTokenFee', type: 'uint256' }, + ], + }, + ], + }, + { + name: 'oftReceipt', + type: 'tuple', + components: [ + { name: 'amountSentLD', type: 'uint256' }, + { name: 'amountReceivedLD', type: 'uint256' }, + ], + }, + ], + }, +] as const + +// 1. Quote the fee +const msgFee = await publicClient.readContract({ + address: stargateOFT, + abi: stargateAbi, // same ABI as the manual flow + functionName: 'quoteSend', + args: [sendParam, false], +}) + +// 2. Approve USDC.e to the wrapper (bridge amount + messaging fee) +await walletClient.writeContract({ + address: usdce, + abi: erc20Abi, + functionName: 'approve', + args: [wrapper, amount + msgFee.nativeFee], +}) + +// 3. Send via wrapper (atomic wrap + approve + send) +await walletClient.writeContract({ + address: wrapper, + abi: wrapperAbi, + functionName: 'sendOFT', + args: [stargateOFT, usdce, sendParam, msgFee.nativeFee], +}) +``` + +:::warning +**Do not use the wrapper for compose messages where the composer refunds the original sender.** The wrapper becomes `msg.sender` for the OFT call, so refunds are sent to the wrapper address - not the user - and funds will be permanently lost. Use the manual flow below for those cases. +::: + +:::tip +You can preflight `sendOFT` with `eth_call` to obtain `oftReceipt.amountSentLD` before sending the live transaction, which is useful for avoiding dust on amounts near the Stargate minimum. +::: + +### Manual flow + +Use this flow if you cannot use the wrapper (e.g., compose messages with refund-to-sender semantics). It requires 5 transactions plus a quote and produces the same end state. #### Using cast (Foundry) From c24456ddf591893ff9c63b8d834c9c60b0ca2a4f Mon Sep 17 00:00:00 2001 From: Uddhav Date: Thu, 14 May 2026 11:19:47 -0700 Subject: [PATCH 2/5] docs(layerzero): surface TempoOFTWrapper in top-level contracts table The contracts table at the top of the page is the first place readers look for canonical addresses. Adding TempoOFTWrapper there (alongside EndpointV2 and LZEndpointDollar) ensures it is discoverable before readers scroll into the per-flow walkthroughs and avoids them defaulting to the manual 5-tx flow out of unawareness. Also: - Add a 'Purpose' column to the contracts table so each entry's role is clear at a glance, with anchor links to the relevant section. - Add a one-liner listing the accepted fee tokens (USDC.e, USDT0, pathUSD) under the contracts table, since this is a top-level fact every outbound integrator needs. Co-Authored-By: Uddhav <255779543+letstokenize@users.noreply.github.com> --- src/pages/guide/bridge-layerzero.mdx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pages/guide/bridge-layerzero.mdx b/src/pages/guide/bridge-layerzero.mdx index 9e5956a6..acd72362 100644 --- a/src/pages/guide/bridge-layerzero.mdx +++ b/src/pages/guide/bridge-layerzero.mdx @@ -32,13 +32,16 @@ See the full token list at [tokenlist.tempo.xyz](https://tokenlist.tempo.xyz/lis ## LayerZero contracts on Tempo -| Contract | Address | -|----------|---------| -| **EndpointV2** | [`0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C`](https://explore.tempo.xyz/address/0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C) | -| **LZEndpointDollar** | [`0x0cEb237E109eE22374a567c6b09F373C73FA4cBb`](https://explore.tempo.xyz/address/0x0cEb237E109eE22374a567c6b09F373C73FA4cBb) | +| Contract | Address | Purpose | +|----------|---------|---------| +| **EndpointV2** | [`0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C`](https://explore.tempo.xyz/address/0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C) | Standard LayerZero endpoint | +| **LZEndpointDollar** | [`0x0cEb237E109eE22374a567c6b09F373C73FA4cBb`](https://explore.tempo.xyz/address/0x0cEb237E109eE22374a567c6b09F373C73FA4cBb) | Routes messaging fees through a stablecoin (no `msg.value` on Tempo). See [EndpointDollar](#endpointdollar). | +| **TempoOFTWrapper** | [`0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0`](https://explore.tempo.xyz/address/0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0) | Bundles wrap, approve, and send into one transaction for outbound bridging. See [Bridge from Tempo](#bridge-from-tempo). | Tempo's LayerZero Endpoint ID is **`30410`**. +Accepted fee tokens for `LZEndpointDollar`: `USDC.e`, `USDT0`, `pathUSD` (whitelisted 6-decimal stablecoins). + ## Stargate tokens [Stargate](https://stargate.finance/) manages liquidity pools for USDC.e and EURC.e. Use the Stargate `sendToken()` interface for these tokens. From 99cb5118c3259aaaa88faca2593a8893171e719b Mon Sep 17 00:00:00 2001 From: Uddhav Date: Thu, 14 May 2026 11:24:45 -0700 Subject: [PATCH 3/5] docs(layerzero): collapse manual bridge-out flow into a brief reference For all standard bridging the TempoOFTWrapper is functionally equivalent to the 5-tx manual flow. The manual flow is only required for compose messages with refund-to-sender semantics, where the wrapper becoming msg.sender breaks the refund. Carrying the full 5-tx cast walkthrough plus the matching viem example in Tempo docs encouraged readers to copy the worse path. Replace it with a short pointer to LZ's direct-flow reference plus a one-line breakdown of the call sequence so the structure is still discoverable. Net change: ~190 lines removed from bridge-layerzero.mdx; the wrapper flow remains the only fully documented bridge-out path on Tempo docs. Co-Authored-By: Uddhav <255779543+letstokenize@users.noreply.github.com> --- src/pages/guide/bridge-layerzero.mdx | 206 +-------------------------- 1 file changed, 7 insertions(+), 199 deletions(-) diff --git a/src/pages/guide/bridge-layerzero.mdx b/src/pages/guide/bridge-layerzero.mdx index acd72362..7a3d772d 100644 --- a/src/pages/guide/bridge-layerzero.mdx +++ b/src/pages/guide/bridge-layerzero.mdx @@ -32,16 +32,13 @@ See the full token list at [tokenlist.tempo.xyz](https://tokenlist.tempo.xyz/lis ## LayerZero contracts on Tempo -| Contract | Address | Purpose | -|----------|---------|---------| -| **EndpointV2** | [`0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C`](https://explore.tempo.xyz/address/0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C) | Standard LayerZero endpoint | -| **LZEndpointDollar** | [`0x0cEb237E109eE22374a567c6b09F373C73FA4cBb`](https://explore.tempo.xyz/address/0x0cEb237E109eE22374a567c6b09F373C73FA4cBb) | Routes messaging fees through a stablecoin (no `msg.value` on Tempo). See [EndpointDollar](#endpointdollar). | -| **TempoOFTWrapper** | [`0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0`](https://explore.tempo.xyz/address/0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0) | Bundles wrap, approve, and send into one transaction for outbound bridging. See [Bridge from Tempo](#bridge-from-tempo). | +| Contract | Address | +|----------|---------| +| **EndpointV2** | [`0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C`](https://explore.tempo.xyz/address/0x20Bb7C2E2f4e5ca2B4c57060d1aE2615245dCc9C) | +| **LZEndpointDollar** | [`0x0cEb237E109eE22374a567c6b09F373C73FA4cBb`](https://explore.tempo.xyz/address/0x0cEb237E109eE22374a567c6b09F373C73FA4cBb) | Tempo's LayerZero Endpoint ID is **`30410`**. -Accepted fee tokens for `LZEndpointDollar`: `USDC.e`, `USDT0`, `pathUSD` (whitelisted 6-decimal stablecoins). - ## Stargate tokens [Stargate](https://stargate.finance/) manages liquidity pools for USDC.e and EURC.e. Use the Stargate `sendToken()` interface for these tokens. @@ -448,200 +445,11 @@ await walletClient.writeContract({ You can preflight `sendOFT` with `eth_call` to obtain `oftReceipt.amountSentLD` before sending the live transaction, which is useful for avoiding dust on amounts near the Stargate minimum. ::: -### Manual flow - -Use this flow if you cannot use the wrapper (e.g., compose messages with refund-to-sender semantics). It requires 5 transactions plus a quote and produces the same end state. - -#### Using cast (Foundry) - -This example bridges USDC.e from Tempo to Base. - -:::::steps - -### Quote the fee - -```bash -cast call 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \ - 'quoteSend((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),bool)((uint256,uint256))' \ - "(30184,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ - false \ - --rpc-url https://rpc.tempo.xyz -``` - -Take the first returned number as `` (in stablecoin units, not ETH). - -### Approve USDC.e to the LZD wrapper - -Approve the `LZEndpointDollar` wrapper contract to spend `` of your USDC.e. This is the amount needed to cover the LayerZero messaging fee. - -```bash -cast send 0x20C000000000000000000000b9537d11c60E8b50 \ - "approve(address,uint256)" \ - 0x0cEb237E109eE22374a567c6b09F373C73FA4cBb \ - \ - --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. - -```bash -cast send 0x0cEb237E109eE22374a567c6b09F373C73FA4cBb \ - "wrap(address,address,uint256)" \ - 0x20C000000000000000000000b9537d11c60E8b50 \ - \ - \ - --rpc-url https://rpc.tempo.xyz \ - --private-key $PRIVATE_KEY -``` +### Manual flow (advanced) -### Approve LZD to Stargate +The wrapper covers all standard bridging. The only case that requires calling the OFT directly is **compose messages where the destination composer refunds the original sender** — the wrapper becomes `msg.sender`, so refunds would go to the wrapper and be lost. -Approve the Stargate OFT contract to spend your LZD so it can pay the messaging fee when sending. - -```bash -cast send 0x0cEb237E109eE22374a567c6b09F373C73FA4cBb \ - "approve(address,uint256)" \ - 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \ - \ - --rpc-url https://rpc.tempo.xyz \ - --private-key $PRIVATE_KEY -``` - -### Approve token on Tempo - -```bash -cast send 0x20C000000000000000000000b9537d11c60E8b50 \ - 'approve(address,uint256)' \ - 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \ - \ - --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](#endpointdollar). - -```bash -cast send 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \ - 'sendToken((uint32,bytes32,uint256,uint256,bytes,bytes,bytes),(uint256,uint256),address)' \ - "(30184,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ - "(,0)" \ - \ - --rpc-url https://rpc.tempo.xyz \ - --private-key $PRIVATE_KEY -``` - -### Verify transaction status - -```text -https://scan.layerzero-api.com/v1/messages/tx/ -``` - -::::: - -#### Using TypeScript (viem) - -```typescript -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 -// 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 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 USDC.e to LZD wrapper (for the messaging fee) -await walletClient.writeContract({ - address: usdce, - abi: erc20Abi, - functionName: 'approve', - args: [lzd, msgFee.nativeFee], -}) - -// 3. Wrap USDC.e into LZD -await walletClient.writeContract({ - address: lzd, - abi: wrapAbi, - functionName: 'wrap', - args: [usdce, account.address, msgFee.nativeFee], -}) - -// 4. Approve LZD to Stargate (for the messaging fee) -await walletClient.writeContract({ - address: lzd, - abi: erc20Abi, - functionName: 'approve', - args: [stargateOFT, msgFee.nativeFee], -}) - -// 5. Approve USDC.e to Stargate (for the bridge amount) -await walletClient.writeContract({ - address: usdce, - abi: erc20Abi, - functionName: 'approve', - args: [stargateOFT, amount], -}) - -// 6. Send the bridge transaction (no value - fee handled via EndpointDollar) -await walletClient.writeContract({ - address: stargateOFT, - abi: stargateAbi, - functionName: 'sendToken', - args: [sendParam, msgFee, account.address], -}) -``` +If you need that, follow the LayerZero direct-flow reference: [Direct flow (5 transactions) in LZ docs](https://docs.layerzero.network/v2/developers/tempo/how-to/support-ofts-and-oapps#direct-flow-1-view-call-5-transactions). The sequence is `quote` → `approve(USDC.e → LZD)` → `wrap` → `approve(LZD → OFT)` → `approve(USDC.e → OFT)` → `send{value: 0}`. ### Bus vs. Taxi mode From dfad5e6cf8c5f91e437db922ef1229e4780f558c Mon Sep 17 00:00:00 2001 From: Uddhav Date: Thu, 14 May 2026 11:35:01 -0700 Subject: [PATCH 4/5] docs(layerzero): simplify Bridge from Tempo section The previous draft of this section was top-heavy with framing. Trim it to just what an integrator needs to copy and run: - Drop the 'Recommended:' subheading and the parallel-flow framing. TempoOFTWrapper is the only flow worth documenting in Tempo's docs; the manual escape hatch is one warning callout pointing to LZ docs. - One-line intro: address, what it does, accepted fee tokens. - Cast walkthrough: 3 short steps (quote, approve, send) with the delivery-tracking URL folded into the send step. - viem example: ~50 lines down from ~140. Use parseAbi for the wrapper signature instead of expanding the full nested ABI by hand. - Drop the eth_call dust-preflight tip (advanced UX detail, not needed for the canonical example). Co-Authored-By: Uddhav <255779543+letstokenize@users.noreply.github.com> --- src/pages/guide/bridge-layerzero.mdx | 140 +++++---------------------- 1 file changed, 25 insertions(+), 115 deletions(-) diff --git a/src/pages/guide/bridge-layerzero.mdx b/src/pages/guide/bridge-layerzero.mdx index 7a3d772d..549f704f 100644 --- a/src/pages/guide/bridge-layerzero.mdx +++ b/src/pages/guide/bridge-layerzero.mdx @@ -260,23 +260,11 @@ await walletClient.writeContract({ ## 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 whitelisted stablecoin via [LZEndpointDollar](#endpointdollar). The fee token must be `USDC.e`, `USDT0`, or `pathUSD`. There are two ways to send: the **TempoOFTWrapper** (recommended, 2 transactions) or the **manual flow** (5 transactions). Both produce identical on-chain results. - -### Recommended: TempoOFTWrapper - -The [`TempoOFTWrapper`](https://docs.layerzero.network/v2/developers/tempo/how-to/support-ofts-and-oapps) bundles wrapping, approvals, and sending into a single transaction. - -| Contract | Address | -|----------|---------| -| **TempoOFTWrapper** | [`0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0`](https://explore.tempo.xyz/address/0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0) | - -`sendOFT` atomically wraps the fee portion of your stablecoin into LZD, approves LZD to the OFT, and calls `send()`. If the re-quoted fee at execution time exceeds `maxNativeFee`, the entire transaction reverts. +Use [`TempoOFTWrapper`](https://explore.tempo.xyz/address/0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0) (`0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0`). It pulls your stablecoin, wraps the fee portion into LZD, and calls `send()` on the OFT in a single transaction. Pay fees in `USDC.e`, `USDT0`, or `pathUSD`. #### Using cast (Foundry) -This example bridges USDC.e from Tempo to Base, paying fees in USDC.e. +This example bridges USDC.e from Tempo to Base. :::::steps @@ -290,22 +278,21 @@ cast call 0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392 \ --rpc-url https://rpc.tempo.xyz ``` -Take the first returned number as `` (in stablecoin units, not ETH). - -### Approve the fee token to the wrapper +The first returned number is `` in stablecoin units. -When the bridge token and fee token are the same (both USDC.e here), approve ` + ` in a single call. If they differ (e.g., bridging EURC.e but paying fees in USDC.e), approve each separately. +### Approve USDC.e to the wrapper ```bash cast send 0x20C000000000000000000000b9537d11c60E8b50 \ 'approve(address,uint256)' \ 0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0 \ $(( + )) \ - --rpc-url https://rpc.tempo.xyz \ - --private-key $PRIVATE_KEY + --rpc-url https://rpc.tempo.xyz --private-key $PRIVATE_KEY ``` -### Send via the wrapper +One approval covers both the bridge amount and the fee since they share the same token. Use a max approval if you bridge frequently. + +### Send ```bash cast send 0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0 \ @@ -314,42 +301,30 @@ cast send 0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0 \ 0x20C000000000000000000000b9537d11c60E8b50 \ "(30184,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ \ - --rpc-url https://rpc.tempo.xyz \ - --private-key $PRIVATE_KEY + --rpc-url https://rpc.tempo.xyz --private-key $PRIVATE_KEY ``` -The fourth argument is `maxNativeFee` - the maximum messaging fee you accept. The call reverts if the re-quoted fee at execution exceeds this value. - -### Verify transaction status - -```text -https://scan.layerzero-api.com/v1/messages/tx/ -``` +`` here is `maxNativeFee` — the call reverts if the fee at execution exceeds this. Track delivery at `https://scan.layerzero-api.com/v1/messages/tx/`. ::::: #### Using TypeScript (viem) ```typescript -import { createWalletClient, createPublicClient, http, parseUnits, pad } from 'viem' +import { createWalletClient, createPublicClient, http, parseUnits, pad, parseAbi } from 'viem' import { tempo } from 'viem/chains' import { privateKeyToAccount } from 'viem/accounts' const account = privateKeyToAccount('0x...') - const walletClient = createWalletClient({ account, chain: tempo, transport: http() }) const publicClient = createPublicClient({ chain: tempo, transport: http() }) -// Stargate OFT for USDC.e on Tempo -const stargateOFT = '0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392' as const -// USDC.e on Tempo (bridge token + fee token) +const oft = '0x8c76e2F6C5ceDA9AA7772e7efF30280226c44392' as const // Stargate USDC.e const usdce = '0x20C000000000000000000000b9537d11c60E8b50' as const -// TempoOFTWrapper const wrapper = '0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0' as const -const amount = parseUnits('1', 6) // 1 USDC.e -const minAmount = parseUnits('0.99', 6) // 1% slippage tolerance - +const amount = parseUnits('1', 6) +const minAmount = parseUnits('0.99', 6) const sendParam = { dstEid: 30184, // Base to: pad(account.address), @@ -360,97 +335,32 @@ const sendParam = { oftCmd: '0x' as const, } -const wrapperAbi = [ - { - name: 'sendOFT', - type: 'function', - stateMutability: 'nonpayable', - inputs: [ - { name: 'oft', type: 'address' }, - { name: 'feeToken', type: 'address' }, - { - 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: 'maxNativeFee', type: 'uint256' }, - ], - outputs: [ - { - name: 'msgReceipt', - type: 'tuple', - components: [ - { name: 'guid', type: 'bytes32' }, - { name: 'nonce', type: 'uint64' }, - { - name: 'fee', - type: 'tuple', - components: [ - { name: 'nativeFee', type: 'uint256' }, - { name: 'lzTokenFee', type: 'uint256' }, - ], - }, - ], - }, - { - name: 'oftReceipt', - type: 'tuple', - components: [ - { name: 'amountSentLD', type: 'uint256' }, - { name: 'amountReceivedLD', type: 'uint256' }, - ], - }, - ], - }, -] as const +const wrapperAbi = parseAbi([ + 'function sendOFT(address oft, address feeToken, (uint32 dstEid, bytes32 to, uint256 amountLD, uint256 minAmountLD, bytes extraOptions, bytes composeMsg, bytes oftCmd) sendParam, uint256 maxNativeFee)', +]) -// 1. Quote the fee +// 1. Quote const msgFee = await publicClient.readContract({ - address: stargateOFT, - abi: stargateAbi, // same ABI as the manual flow - functionName: 'quoteSend', - args: [sendParam, false], + address: oft, abi: stargateAbi, functionName: 'quoteSend', args: [sendParam, false], }) -// 2. Approve USDC.e to the wrapper (bridge amount + messaging fee) +// 2. Approve USDC.e to wrapper (bridge amount + fee) await walletClient.writeContract({ - address: usdce, - abi: erc20Abi, - functionName: 'approve', + address: usdce, abi: erc20Abi, functionName: 'approve', args: [wrapper, amount + msgFee.nativeFee], }) -// 3. Send via wrapper (atomic wrap + approve + send) +// 3. Send (wrap + approve + send happen atomically inside) await walletClient.writeContract({ - address: wrapper, - abi: wrapperAbi, - functionName: 'sendOFT', - args: [stargateOFT, usdce, sendParam, msgFee.nativeFee], + address: wrapper, abi: wrapperAbi, functionName: 'sendOFT', + args: [oft, usdce, sendParam, msgFee.nativeFee], }) ``` :::warning -**Do not use the wrapper for compose messages where the composer refunds the original sender.** The wrapper becomes `msg.sender` for the OFT call, so refunds are sent to the wrapper address - not the user - and funds will be permanently lost. Use the manual flow below for those cases. -::: - -:::tip -You can preflight `sendOFT` with `eth_call` to obtain `oftReceipt.amountSentLD` before sending the live transaction, which is useful for avoiding dust on amounts near the Stargate minimum. +**Do not use the wrapper for compose messages that refund `msg.sender`.** Refunds go to the wrapper, not your wallet, and are lost. For that case call the OFT directly — see [LayerZero's direct flow](https://docs.layerzero.network/v2/developers/tempo/how-to/support-ofts-and-oapps#direct-flow-1-view-call-5-transactions). ::: -### Manual flow (advanced) - -The wrapper covers all standard bridging. The only case that requires calling the OFT directly is **compose messages where the destination composer refunds the original sender** — the wrapper becomes `msg.sender`, so refunds would go to the wrapper and be lost. - -If you need that, follow the LayerZero direct-flow reference: [Direct flow (5 transactions) in LZ docs](https://docs.layerzero.network/v2/developers/tempo/how-to/support-ofts-and-oapps#direct-flow-1-view-call-5-transactions). The sequence is `quote` → `approve(USDC.e → LZD)` → `wrap` → `approve(LZD → OFT)` → `approve(USDC.e → OFT)` → `send{value: 0}`. - ### Bus vs. Taxi mode Stargate offers two delivery modes: From e1d00cbf761f61d48234a3f8b1798d8838b7f9d2 Mon Sep 17 00:00:00 2001 From: Uddhav Date: Thu, 14 May 2026 12:28:52 -0700 Subject: [PATCH 5/5] docs(layerzero): restore Verify step and split flags onto separate lines - Restore the 'Verify transaction status' step in the cast walkthrough. Folding it into the Send step's prose hid the LZ scan URL behind a paragraph; integrators expect a numbered step they can copy. - Put --rpc-url and --private-key on separate lines in the approve and send blocks. Matches the formatting used everywhere else on the page (Bridge to Tempo + manual flow examples). Co-Authored-By: Uddhav <255779543+letstokenize@users.noreply.github.com> --- src/pages/guide/bridge-layerzero.mdx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pages/guide/bridge-layerzero.mdx b/src/pages/guide/bridge-layerzero.mdx index 549f704f..c47d876b 100644 --- a/src/pages/guide/bridge-layerzero.mdx +++ b/src/pages/guide/bridge-layerzero.mdx @@ -287,7 +287,8 @@ cast send 0x20C000000000000000000000b9537d11c60E8b50 \ 'approve(address,uint256)' \ 0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0 \ $(( + )) \ - --rpc-url https://rpc.tempo.xyz --private-key $PRIVATE_KEY + --rpc-url https://rpc.tempo.xyz \ + --private-key $PRIVATE_KEY ``` One approval covers both the bridge amount and the fee since they share the same token. Use a max approval if you bridge frequently. @@ -301,10 +302,19 @@ cast send 0xbb95daF376cd63F258d7c37a4eFe57c10055E8E0 \ 0x20C000000000000000000000b9537d11c60E8b50 \ "(30184,$(cast abi-encode 'f(address)' ),,,0x,0x,0x)" \ \ - --rpc-url https://rpc.tempo.xyz --private-key $PRIVATE_KEY + --rpc-url https://rpc.tempo.xyz \ + --private-key $PRIVATE_KEY ``` -`` here is `maxNativeFee` — the call reverts if the fee at execution exceeds this. Track delivery at `https://scan.layerzero-api.com/v1/messages/tx/`. +`` here is `maxNativeFee` — the call reverts if the fee at execution exceeds this. + +### Verify transaction status + +Track delivery to the destination chain via the LayerZero scan API: + +```text +https://scan.layerzero-api.com/v1/messages/tx/ +``` :::::