> ## Documentation Index
> Fetch the complete documentation index at: https://docs.layerzero.network/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrating lzAsset Transfers

> Overview of Integrating lzAsset Transfers on LayerZero V2. Learn the architecture, features, and how to get started building. LayerZero enables secure...

lzAsset contracts implement the standard **IOFT interface** for [Omnichain Fungible Tokens (OFTs)](../oft/quickstart). However, transferring between the **Native Mesh** (issuer-controlled) and the **lzAsset Mesh** (LayerZero-managed) requires routing through a **Hub Chain** using the `MultiHopComposer`.

For architecture and concepts, see [lzAsset Managed Service](/v2/concepts/applications/lzasset).

## The IOFT Interface

All lzAsset token contracts (on both Native and lzAsset chains) implement the standard [`IOFT` interface](https://github.com/LayerZero-Labs/devtools/blob/main/packages/oft-evm/contracts/interfaces/IOFT.sol). You interact with them using the standard `send()` and `quoteSend()` methods, identical to any other OFT deployment.

```solidity wrap theme={null}
// lzAsset tokens use the standard IOFT interface
import { IOFT, SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";

// Quote and send work exactly like any OFT
MessagingFee memory fee = IOFT(lzAssetToken).quoteSend(sendParam, false);
IOFT(lzAssetToken).send{value: fee.nativeFee}(sendParam, fee, refundAddress);
```

## Transfer Types

There are two types of transfers in the lzAsset ecosystem:

1. **Direct Transfers** (Within the same mesh)

   * **Within the lzAsset mesh**: Standard OFT transfer.
   * **Native to Native**: Standard OFT transfer (if multiple native chains exist).

2. **Cross-Mesh Transfers** (Between meshes)
   * **Native → lzAsset**: Must route through the Hub Chain.
   * **lzAsset → Native**: Must route through the Hub Chain.

### Direct Transfers (Standard)

For transfers within the same mesh (e.g., lzAsset chain A to lzAsset chain B), simply construct a standard `SendParam` with the destination endpoint ID and recipient address. No special composition is required.

```solidity wrap theme={null}
// Standard OFT Transfer (Direct)
SendParam memory sendParam = SendParam({
    dstEid: dstEid, // Destination lzAsset chain
    to: bytes32(uint256(uint160(recipient))),
    amountLD: amount,
    minAmountLD: amount,
    extraOptions: options, // Standard gas options
    composeMsg: "",
    oftCmd: ""
});
```

## Cross-Mesh Transfers (Multi-Hop)

To move assets between the Native Mesh and the lzAsset Mesh, the transfer must be routed through the **MultiHopComposer** on the Hub Chain. This is a **two-hop** process:

1. **Hop 1**: Source Chain → Hub Chain (Composer)
2. **Hop 2**: Hub Chain (Composer) → Destination Chain

You initiate this entire flow from the source chain by encoding the instructions for the second hop into the `composeMsg` of the first hop.

### Constructing the Send

You must construct two `SendParam` structs: one for the final destination (Hop 2) and one for the Hub (Hop 1).

#### 1. Prepare the Second Hop (Hub → Destination)

First, define where the tokens should go *after* they reach the Hub.

```solidity wrap theme={null}
// The parameters for the transfer from Hub to Final Destination
SendParam memory nextHopParam = SendParam({
    dstEid: finalDstEid, // The final destination chain ID
    to: bytes32(uint256(uint160(finalRecipient))),
    amountLD: amount,    // The amount to forward
    minAmountLD: 0,      // Set to 0 (slippage handled by Composer)
    extraOptions: nextHopOptions, // Options for delivery to final recipient
    composeMsg: "",      // Usually empty unless chaining further
    oftCmd: ""           // Standard transfer
});

// Encode this struct to be passed as the compose message
bytes memory composeMsg = abi.encode(nextHopParam);
```

#### 2. Prepare the First Hop (Source → Hub)

Next, wrap the second hop inside the parameters for the first hop. The destination is the **Hub Chain**, and the recipient is the **MultiHopComposer** contract.

**Critical Requirements**:

* `dstEid`: Must be the Hub Chain ID.
* `to`: Must be the address of the `MultiHopComposer` on the Hub.
* `composeMsg`: Must be the encoded `nextHopParam` from Step 1.
* **Gas & Value**: You must pay for the second hop's gas and fee *on the source chain*.

```solidity wrap theme={null}
// Calculate the fee required for the second hop (native fee on Hub)
// This implies quoting the OFT lockbox on the Hub chain off-chain (see Fee Estimation below)
// You must call IOFT.quoteSend() on the Hub chain and use the returned MessagingFee.nativeFee
uint128 gasForLzReceive = 100_000; // Gas for Hub OFT's _lzReceive (credits tokens + calls sendCompose)
uint128 gasForCompose = 500_000;  // Gas to execute lzCompose on Hub (MultiHopComposer logic)
uint128 msgValueForNextHop = quoteFromHub.nativeFee;

// Add execution options for the Hub - BOTH lzReceive AND lzCompose are required
bytes memory firstHopOptions = OptionsBuilder.newOptions()
    .addExecutorLzReceiveOption(gasForLzReceive, 0)
    .addExecutorLzComposeOption(0, gasForCompose, msgValueForNextHop);

// The parameters for the transfer from Source to Hub
SendParam memory sendParam = SendParam({
    dstEid: hubEid, // The Hub Chain ID
    to: bytes32(uint256(uint160(composerAddress))), // The MultiHopComposer address
    amountLD: amount,
    minAmountLD: amount,
    extraOptions: firstHopOptions,
    composeMsg: composeMsg, // The encoded nextHopParam
    oftCmd: "" // Must use Taxi mode for lzCompose
});
```

### Full Implementation Example

Here is a complete Solidity example of how to format and send a cross-mesh lzAsset transfer.

```solidity wrap theme={null}
import { OptionsBuilder } from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";
import { IOFT, SendParam, MessagingFee } from "@layerzerolabs/oft-evm/contracts/interfaces/IOFT.sol";

contract LzAssetSender {
    using OptionsBuilder for bytes;

    // The lzAsset token contract on this chain
    IOFT public lzAssetToken;
    // The address of the MultiHopComposer on the Hub Chain
    address public composerOnHub;
    // The Endpoint ID of the Hub Chain
    uint32 public hubEid;

    constructor(address _token, address _composer, uint32 _hubEid) {
        lzAssetToken = IOFT(_token);
        composerOnHub = _composer;
        hubEid = _hubEid;
    }

    function sendCrossMesh(
        uint32 _finalDstEid,
        address _finalRecipient,
        uint256 _amount,
        uint128 _nextHopNativeFee
    ) external payable {
        // 1. PREPARE SECOND HOP (Hub -> Destination)
        // Options for the final delivery (Executor gas on destination)
        bytes memory nextHopOptions = OptionsBuilder.newOptions()
            .addExecutorLzReceiveOption(200000, 0);

        SendParam memory nextHopParam = SendParam({
            dstEid: _finalDstEid,
            to: bytes32(uint256(uint160(_finalRecipient))),
            amountLD: _amount,
            minAmountLD: 0, // Composer handles amount adjustment
            extraOptions: nextHopOptions,
            composeMsg: "",
            oftCmd: ""
        });

        // 2. PREPARE FIRST HOP (Source -> Hub)
        // Encode the second hop instructions as the composeMsg
        // The OFT will wrap this with (nonce, srcEid, amountLD, composeFrom) before delivery
        // The MultiHopComposer extracts it via OFTComposeMsgCodec.composeMsg()
        bytes memory composeMsg = abi.encode(nextHopParam);

        // Options for the Hub: BOTH lzReceive AND lzCompose gas are required
        // - lzReceive: Hub OFT credits tokens and calls endpoint.sendCompose()
        // - lzCompose: MultiHopComposer executes and calls OFT.send() for second hop
        bytes memory firstHopOptions = OptionsBuilder.newOptions()
            .addExecutorLzReceiveOption(65_000, 0)
            .addExecutorLzComposeOption(0, 500_000, _nextHopNativeFee);

        SendParam memory sendParam = SendParam({
            dstEid: hubEid,
            to: bytes32(uint256(uint160(composerOnHub))),
            amountLD: _amount,
            minAmountLD: _amount, // Normal slippage for first hop
            extraOptions: firstHopOptions,
            composeMsg: composeMsg,
            oftCmd: ""
        });

        // 3. QUOTE AND SEND
        MessagingFee memory fee = lzAssetToken.quoteSend(sendParam, false);

        // Ensure enough value was sent
        require(msg.value >= fee.nativeFee, "Insufficient fee");

        // Send
        lzAssetToken.send{value: fee.nativeFee}(
            sendParam,
            fee,
            msg.sender // Refund address
        );
    }
}
```

<Info>
  ### composeMsg Encoding

  When you call `send()` with a `composeMsg`, the OFT automatically wraps your data with additional context before delivering it to the composer:

  * `nonce` - Transaction tracking
  * `srcEid` - Source chain endpoint ID
  * `amountLD` - Amount transferred
  * `composeFrom` - Original sender address (bytes32)
  * Your `composeMsg` payload

  The `MultiHopComposer` uses `OFTComposeMsgCodec.composeMsg(_message)` to extract your encoded `SendParam` from the full message.
</Info>

## Fee Estimation

Estimating the fee for a multi-hop transaction requires a two-step process because the source chain cannot directly quote the cost of the second hop (Hub → Destination).

### Step 1: Quote the Second Hop (Hub → Destination)

You must call `IOFT.quoteSend()` on the **Hub Chain's OFT contract** (the lockbox or adapter) to get the exact fee for the transfer from Hub to final destination.

This quote must be obtained **off-chain** before constructing your source transaction:

```javascript wrap theme={null}
import {ethers} from 'ethers';

// Connect to Hub Chain
const hubProvider = new ethers.JsonRpcProvider(HUB_RPC_URL);
const hubOFT = new ethers.Contract(HUB_OFT_ADDRESS, IOFT_ABI, hubProvider);

// Construct the nextHopParam (same as what you'll encode in composeMsg)
const nextHopParam = {
  dstEid: FINAL_DESTINATION_EID,
  to: ethers.zeroPadValue(recipientAddress, 32),
  amountLD: amountToSend,
  minAmountLD: 0,
  extraOptions: nextHopOptions, // lzReceive gas for final destination
  composeMsg: '0x',
  oftCmd: '0x',
};

// Quote the second hop on the Hub chain
const nextHopQuote = await hubOFT.quoteSend(nextHopParam, false);
const nextHopNativeFee = nextHopQuote.nativeFee; // Use this in Step 2
```

### Step 2: Pack Quote into First Hop Options

Once you have the `nativeFee` for the second hop, include it in the options for the first hop (Source → Hub):

1. **Add `lzReceiveOption`**: Gas for the Hub OFT's `_lzReceive` (credits tokens + calls `sendCompose`)
2. **Add `lzComposeOption`**: Gas for `MultiHopComposer.lzCompose()` execution, plus the `nativeFee` as `msg.value`
3. **Quote First Hop**: Call `quoteSend()` on the source chain. The returned fee includes everything needed for the full journey.

```javascript wrap theme={null}
// Build first hop options with the quoted second hop fee
const firstHopOptions = OptionsBuilder.newOptions()
  .addExecutorLzReceiveOption(65000, 0) // Hub _lzReceive gas
  .addExecutorLzComposeOption(0, 500000, nextHopNativeFee); // Compose gas + second hop fee

// Now quote the first hop on source chain
const sourceOFT = new ethers.Contract(SOURCE_OFT_ADDRESS, IOFT_ABI, sourceProvider);
const firstHopQuote = await sourceOFT.quoteSend(sendParam, false);

// firstHopQuote.nativeFee is your total msg.value for the entire cross-mesh transfer
```

<Tip>
  ### Fee Flow

  The `msg.value` passed via `addExecutorLzComposeOption` is delivered to the `MultiHopComposer` on the Hub. The composer then uses this value to pay for the `IOFT.send()` call that initiates the second hop.
</Tip>

<Note>
  ### Finding Addresses

  Always verify the correct `MultiHopComposer` address for your specific lzAsset token pair. Some deployments may use different Hub chains or composer instances.
</Note>
