Skip to main content
Version: Endpoint V2

OVault EVM Implementation

Deploy omnichain ERC-4626 vaults that enable users to deposit assets from any chain and receive shares on a preferred network through a single transaction.

OVault Comparison OVault Comparison

Prerequisites

Before implementing OVault, you should understand:

  1. OFT Standard: How Omnichain Fungible Tokens work and what the typical deployment looks like
  2. Composer Pattern: Understanding of composeMsg encoding and cross-chain message workflows
  3. ERC-4626 Vaults: How the tokenized vault standard interface works for deposit/redeem operations

OVault uses a hub-and-spoke model where:

  • Hub Chain: Hosts the ERC4626 vault, the VaultComposer, and ShareOFTAdapter (lockbox model)
  • Spoke Chains: Host AssetOFTs and ShareOFTs that connect to the hub

Mental Model

Think of OVault as two separate OFT meshes (asset + share) connected by an ERC4626 vault and Composer contract on a hub chain. Users send assets or shares to the composer with special instructions, and the composer orchestrates vault operations and delivers the output token.

Installation

Below, you can find instructions for installing the OVault contracts:

OVault in a new project

To start using LayerZero OVault contracts in a new project, use the LayerZero CLI tool, create-lz-oapp. The CLI tool allows developers to create any omnichain application in <4 minutes! Get started by running the following from your command line:

LZ_ENABLE_OVAULT_EXAMPLE=1 npx create-lz-oapp@latest --example ovault-evm

OVault Contracts

Your OVault implementation requires four core contracts. Here's how to properly inherit from the base contracts:

Asset OFT (All Chains)

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol";

/**
* @title MyAssetOFT
* @notice ERC20 representation of the vault's asset token on a spoke chain for cross-chain functionality
* @dev This contract represents the vault's underlying asset on spoke chains. It inherits from
* LayerZero's OFT (Omnichain Fungible Token) to enable seamless cross-chain transfers of the
* vault's asset tokens between the hub chain and spoke chains.
*
* The asset OFT acts as a bridgeable ERC20 representation of the vault's collateral asset, allowing
* users to move their assets across supported chains while maintaining fungibility.
*/
contract MyAssetOFT is OFT {
/**
* @notice Constructs the Asset OFT contract
* @dev Initializes the OFT with LayerZero endpoint and sets up ownership
* @param _name The name of the asset token
* @param _symbol The symbol of the asset token
* @param _lzEndpoint The address of the LayerZero endpoint on this chain
* @param _delegate The address that will have owner privileges
*/
constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {
// NOTE: Uncomment the line below if you need to mint initial supply
// This can be useful for testing or if the asset needs initial liquidity
// _mint(msg.sender, 1 ether);
}
}

MyAssetOFT is a standard ERC20 token that will be the asset inside the ERC4626 vault.

If your intended vault asset is already an OFT (e.g., USDT0), you do not need to deploy this contract. The asset token must be deployed on at least the hub chain.

Vault + Share Adapter (Hub Chain)

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ERC20 } from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { ERC4626 } from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";

import { OFTAdapter } from "@layerzerolabs/oft-evm/contracts/OFTAdapter.sol";

/**
* @title MyERC4626
* @notice ERC4626 tokenized vault implementation for cross-chain vault operations
* @dev SECURITY CONSIDERATIONS:
* - Donation/inflation attacks on empty or low-liquidity vaults
* - Share price manipulation via large donations before first deposit
* - Slippage during deposit/redeem operations in low-liquidity conditions
* - First depositor advantage scenarios
*
* See OpenZeppelin ERC4626 documentation for full risk analysis:
* https://docs.openzeppelin.com/contracts/4.x/erc4626#inflation-attack
*
* MITIGATIONS:
* - OpenZeppelin v4.9+ includes virtual assets/shares to mitigate inflation attacks
* - Deployers should consider initial deposits to prevent manipulation
*/
contract MyERC4626 is ERC4626 {
/**
* @notice Creates a new ERC4626 vault
* @dev Initializes the vault with virtual assets/shares protection against inflation attacks
* @param _name The name of the vault token
* @param _symbol The symbol of the vault token
* @param _asset The underlying asset that the vault accepts
*/
constructor(string memory _name, string memory _symbol, IERC20 _asset) ERC20(_name, _symbol) ERC4626(_asset) {}
}

/**
* @title MyShareOFTAdapter
* @notice OFT adapter for vault shares enabling cross-chain transfers
* @dev The share token MUST be an OFT adapter (lockbox).
* @dev A mint-burn adapter would not work since it transforms `ShareERC20::totalSupply()`
*/
contract MyShareOFTAdapter is OFTAdapter {
/**
* @notice Creates a new OFT adapter for vault shares
* @dev Sets up cross-chain token transfer capabilities for vault shares
* @param _token The vault share token to adapt for cross-chain transfers
* @param _lzEndpoint The LayerZero endpoint for this chain
* @param _delegate The account with administrative privileges
*/
constructor(
address _token,
address _lzEndpoint,
address _delegate
) OFTAdapter(_token, _lzEndpoint, _delegate) Ownable(_delegate) {}
}

MyERC4626 is the standard tokenized vault contract. Given an asset for a valid ERC20 contract in the constructor, the vault will create a corresponding share token.

This share must then be transformed into an Omnichain Fungible Token using MyShareOFTAdapter.

Composer (Hub Chain)

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.22;

import { VaultComposerSync } from "@layerzerolabs/ovault-evm/contracts/VaultComposerSync.sol";

/**
* @title MyOVaultComposer
* @notice Cross-chain vault composer enabling omnichain vault operations via LayerZero
*/
contract MyOVaultComposer is VaultComposerSync {
/**
* @notice Creates a new cross-chain vault composer
* @dev Initializes the composer with vault and OFT contracts for omnichain operations
* @param _ovault The vault contract implementing ERC4626 for deposit/redeem operations
* @param _assetOFT The OFT contract for cross-chain asset transfers
* @param _shareOFT The OFT contract for cross-chain share transfers
*/
constructor(
address _ovault,
address _assetOFT,
address _shareOFT
) VaultComposerSync(_ovault, _assetOFT, _shareOFT) {}
}

VaultComposerSync is the orchestrator contract that enables cross-chain vault operations between the OFT standard and ERC-4626 vaults, automatically handling deposits and redemptions based on incoming token transfers.

The "Sync" in VaultComposerSync refers to synchronous vault operations - meaning the vault must support immediate, single-transaction deposits and redemptions without delays or waiting periods.

For Asynchronous Vaults

For asynchronous vaults that require multi-transaction redemptions, you will need to modify the MyOVaultComposer contract.

Share OFT (Spoke Chains)

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol";

/**
* @title MyShareOFT
* @notice ERC20 representation of the vault's share token on a spoke chain for cross-chain functionality
* @dev This contract represents the vault's share tokens on spoke chains. It inherits from
* LayerZero's OFT (Omnichain Fungible Token) to enable seamless cross-chain transfers of
* vault shares between the hub chain and spoke chains. This contract is designed to work
* with ERC4626-compliant vaults, enabling standardized cross-chain vault interactions.
*
* Share tokens represent ownership in the vault and can be redeemed for the underlying
* asset on the hub chain. The OFT mechanism ensures that shares maintain their value and can be freely
* moved across supported chains while preserving the vault's accounting integrity.
*/
contract MyShareOFT is OFT {
/**
* @notice Constructs the Share OFT contract
* @dev Initializes the OFT with LayerZero endpoint and sets up ownership
* @param _name The name of the share token
* @param _symbol The symbol of the share token
* @param _lzEndpoint The address of the LayerZero endpoint on this chain
* @param _delegate The address that will have owner privileges
*/
constructor(
string memory _name,
string memory _symbol,
address _lzEndpoint,
address _delegate
) OFT(_name, _symbol, _lzEndpoint, _delegate) Ownable(_delegate) {
// WARNING: Do NOT mint share tokens directly as this breaks the vault's share-to-asset ratio
// Share tokens should only be minted by the vault contract during deposits to maintain
// the correct relationship between shares and underlying assets
// _mint(msg.sender, 1 ether); // ONLY uncomment for testing UI/integration, never in production
}
}

MyShareOFT is the OFT representation of the share token from the ERC4626 vault on other spoke chains. By default, OFT inherit the base ERC20 token standard.

caution

You should NEVER implement _mint() in the constructor or externally in MyShareOFT. Since shares can be redeemed for assets on the hub chain, minting new supply breaks the conversion rate inside the ERC4626 vault.

Deployment and Wiring

After reviewing the above OVault related contracts, follow these steps to deploy and wire the necessary pathways.

Creates standard OFT contracts for the vault's underlying asset.

1. Network Configuration

Update hardhat.config.ts to include your desired networks:

const config: HardhatUserConfig = {
networks: {
base: {
eid: EndpointId.BASESEP_V2_TESTNET,
url: process.env.RPC_URL_BASESEP_TESTNET || 'https://base-sepolia.gateway.tenderly.co',
accounts,
},
arbitrum: {
eid: EndpointId.ARBSEP_V2_TESTNET,
url: process.env.RPC_URL_ARBSEP_TESTNET || 'https://arbitrum-sepolia.gateway.tenderly.co',
accounts,
},
optimism: {
eid: EndpointId.OPTSEP_V2_TESTNET,
url: process.env.RPC_URL_OPTSEP_TESTNET || 'https://optimism-sepolia.gateway.tenderly.co',
accounts,
},
},
// ... rest of config
};

2. Deployment Configuration

Configure your vault deployment in devtools/deployConfig.ts. This file controls which chains get which contracts and their deployment settings:

Note: If your asset is already an OFT, you do not need to deploy a separate mesh. The only requirement is that the asset OFT supports the hub chain you are deploying to.

Asset and share token networks don't need to perfectly overlap. Configure based on your requirements:

import {EndpointId} from '@layerzerolabs/lz-definitions';

export const DEPLOYMENT_CONFIG = {
// Vault chain configuration (where the ERC4626 vault lives)
vault: {
eid: EndpointId.ARBSEP_V2_TESTNET, // Your hub chain
contracts: {
vault: 'MyERC4626',
shareAdapter: 'MyShareOFTAdapter',
composer: 'MyOVaultComposer',
},
// IF YOU HAVE A PRE-DEPLOYED ASSET, SET THE ADDRESS HERE
assetAddress: undefined, // Set to '0x...' to use existing asset
},

// Asset OFT configuration (deployed on all chains)
asset: {
contract: 'MyAssetOFT',
metadata: {
name: 'MyAssetOFT',
symbol: 'ASSET',
},
chains: [
EndpointId.OPTSEP_V2_TESTNET,
EndpointId.BASESEP_V2_TESTNET,
EndpointId.ARBSEP_V2_TESTNET, // Include hub chain
],
},

// Share OFT configuration (only on spoke chains)
share: {
contract: 'MyShareOFT',
metadata: {
name: 'MyShareOFT',
symbol: 'SHARE',
},
chains: [
EndpointId.OPTSEP_V2_TESTNET,
EndpointId.BASESEP_V2_TESTNET,
// Do NOT include hub chain (it uses ShareOFTAdapter)
],
},
};

Key Configuration Points:

  • Hub Chain: Set vault.eid to your chosen hub chain's endpoint ID
  • Asset Chains: Include all chains where you want asset OFTs (including the hub chain)
  • Share Chains: Include only spoke chains (exclude hub, which uses the ShareOFTAdapter)
  • Pre-deployed Asset: Set vault.assetAddress if using an existing asset token

The deployment scripts automatically determine what to deploy based on:

  • Vault contracts (ERC4626, ShareOFTAdapter, Composer) deploy only on the hub chain
  • Asset OFTs deploy on chains listed in asset.chains (unless using pre-deployed asset)
  • Share OFTs deploy on chains listed in share.chains

Build

Compile your contracts:

pnpm compile
info

If you're deploying the asset OFT from scratch for testing purposes, you'll need to mint an initial supply. Uncomment the _mint line in the MyAssetOFT constructor to provide initial liquidity. This ensures you have tokens to test deposit and cross-chain transfer functionality.

caution

Do NOT mint share tokens directly in MyShareOFT. Share tokens must only be minted by the vault contract during deposits to maintain the correct share-to-asset ratio. Manually minting share tokens breaks the vault's accounting and can lead to incorrect redemption values. The mint line in MyShareOFT should only be uncommented for UI/integration testing, never in production.

Deploy

Deploy all vault contracts across all configured chains:

pnpm hardhat lz:deploy --tags ovault

This single command will:

  • Deploy AssetOFTs to all chains in deployConfig.asset.chains
  • Deploy the vault system (ERC4626, ShareOFTAdapter, Composer) on the hub chain
  • Deploy ShareOFTs to all spoke chains in deployConfig.share.chains

The deployment scripts automatically skip existing deployments, so you can safely run this command when expanding to new chains. Simply add the new chain endpoints to your deployConfig.ts and run the deploy command again.

Tip: To deploy to specific networks only, use the --networks flag:

pnpm hardhat lz:deploy --tags ovault --networks arbitrum,optimism

3. Wire Asset and Share Mesh

# Configure LayerZero connections
pnpm hardhat lz:oapp:wire --oapp-config layerzero.asset.config.ts
pnpm hardhat lz:oapp:wire --oapp-config layerzero.share.config.ts

This establishes the peer relationships between each OFT deployment, enabling cross-chain token transfers. See the OFT Wiring Step for more information.

Usage

OVault enables four main operation patterns. Each uses the standard OFT.send() interface with the composer handling vault operations automatically.

Deposit Assets → Receive Shares on Same Chain

Scenario: Deposit asset from Arbitrum, receive vault shares on Arbitrum

# Using the CLI task (recommended)
npx hardhat lz:ovault:send \
--src-eid 30110 --dst-eid 30110 \
--amount 100.0 --to 0xRecipient \
--token-type asset

Flow:

Deposit Assets → Receive Shares on Different Chain

Scenario: Deposit asset from Arbitrum, receive vault shares on Optimism

# Using the CLI task (recommended)
npx hardhat lz:ovault:send \
--src-eid 30110 --dst-eid 30111 \
--amount 100.0 --to 0xRecipient \
--token-type asset

Flow:

Deposit Assets → Receive Shares on Hub

Scenario: Deposit asset from Arbitrum, receive vault shares on the hub chain

npx hardhat lz:ovault:send \
--src-eid 30110 --dst-eid 30184 \
--amount 100.0 --to 0xRecipient \
--token-type asset

Flow:

Redeem Shares → Receive Assets on Different Chain

Scenario: Redeem vault shares from Optimism, receive asset on Arbitrum

npx hardhat lz:ovault:send \
--src-eid 30111 --dst-eid 30110 \
--amount 50.0 --to 0xRecipient \
--token-type share

Flow:

Redeem Shares → Receive Assets on Hub

Scenario: Redeem vault shares from Optimism, receive asset on the hub chain

npx hardhat lz:ovault:send \
--src-eid 30111 --dst-eid 30184 \
--amount 50.0 --to 0xRecipient \
--token-type share

Flow:

SDK Integration

For programmatic integration, use the official @layerzerolabs/lz-ovault-sdk which simplifies OVault operations by using viem to generate the necessary calldata for calling OFT.send() with the proper composeMsg for the hub composer.

The SDK's OVaultMessageBuilder.generateOVaultInputs() method handles all the complex message encoding and returns ready-to-use transaction parameters for viem wallet clients.

import {OVaultMessageBuilder, OVaultOperations} from '@layerzerolabs/lz-ovault-sdk';

const inputs = await OVaultMessageBuilder.generateOVaultInputs({
srcEid: 40245, // Base Sepolia
hubEid: 40231, // Arbitrum Sepolia
dstEid: 40245, // Base Sepolia
operation: OVaultOperations.DEPOSIT,
amount: 100000000000000000n,
slippage: 0.01, // 1%
// ... other required parameters
});

// Use inputs.contractAddress, inputs.abi, inputs.txArgs with VIEM

For complete usage examples, API reference, and advanced configuration, see the SDK repository.

For manual integration and advanced usage, see the Technical Reference section below.

Technical Reference

OVault operations follow a two-phase architecture where failures and slippage protection occur in distinct stages:

Two-Phase Operation Flow

Phase 1: Source → Hub (Standard OFT)

  • User calls OFT.send() targeting the hub composer
  • Standard LayerZero transfer with compose message
  • Reliable transfer with minimal failure modes

Phase 2: Hub Operations + Output Routing

  • Composer executes vault operations (deposit/redeem)
  • Critical slippage point: Vault conversion rates may have changed
  • Output tokens routed to final destination (local or cross-chain)

Operation Detection

The composer automatically determines the vault operation based on which OFT sent the tokens:

  • AssetOFT caller → Triggers deposit operation (assetsshares)
  • ShareOFT caller → Triggers redeem operation (sharesassets)

Slippage Protection Strategy

Since the real slippage occurs during vault operations on the hub, the composeMsg contains the critical slippage parameters:

  • Phase 1 minAmountLD: Set for source token (not critical for vault rates)
  • Phase 2 minAmountLD: Set in composeMsg for vault output (critical protection)

1. Standard OFT Transfer Initiation

Users call the standard OFT interface with compose instructions:

// Standard OFT send with compose message
assetOFT.send(
SendParam({
dstEid: hubEid, // Always send to hub first
to: bytes32(composer), // VaultComposerSync address
amountLD: depositAmount,
minAmountLD: minDepositAmount, // Slippage protection
extraOptions: "...", // Gas for compose + second hop
composeMsg: composeMsg, // Second SendParam + minMsgValue
oftCmd: ""
}),
MessagingFee(msg.value, 0),
refundAddress
);

2a. Composer Message Reception

When tokens arrive at the hub via lzReceive(), the composer is triggered via lzCompose():

function lzCompose(
address _composeCaller, // Either ASSET_OFT or SHARE_OFT
bytes32 _guid,
bytes calldata _message, // Contains routing instructions
address _executor,
bytes calldata _extraData
) external payable

The compose message contains the Phase 2 routing instructions with critical slippage protection:

// Decoded in handleCompose() - this controls Phase 2 behavior
(SendParam memory sendParam, uint256 minMsgValue) = abi.decode(
_composeMsg,
(SendParam, uint256)
);

// SendParam for vault output routing:
// - dstEid: Target chain for output tokens
// - to: Final recipient address
// - amountLD: Updated by composer to actual vault output
// - minAmountLD: CRITICAL - protects against vault rate slippage
// - extraOptions: Gas settings for destination transfer
// - composeMsg: Empty (no nested compose)
// - oftCmd: Empty (no OFT commands)

2b. Operation Detection & Execution

The composer automatically determines the vault operation based on which OFT sent the tokens:

Asset Deposit Flow (AssetOFT → Composer):

function _depositAndSend() {
// 1. Deposit assets into vault
uint256 shareAmount = VAULT.deposit(_assetAmount, address(this));

// 2. Verify slippage protection
_assertSlippage(shareAmount, _sendParam.minAmountLD);

// 3. Route shares to final destination
_send(SHARE_OFT, shareAmount, _refundAddress);
}

Share Redemption Flow (ShareOFT → Composer):

function _redeemAndSend() {
// 1. Redeem shares from vault
uint256 assetAmount = VAULT.redeem(_shareAmount, address(this), address(this));

// 2. Verify slippage protection
_assertSlippage(assetAmount, _sendParam.minAmountLD);

// 3. Route assets to final destination
_send(ASSET_OFT, assetAmount, _refundAddress);
}

2c. Smart Output Routing

The _send() function handles both local and cross-chain delivery:

function _send(address _oft, SendParam memory _sendParam, address _refundAddress) {
if (_sendParam.dstEid == VAULT_EID) {
// Same chain: Direct ERC20 transfer (no LayerZero fees)
address erc20 = _oft == ASSET_OFT ? ASSET_ERC20 : SHARE_ERC20;
IERC20(erc20).safeTransfer(_sendParam.to.bytes32ToAddress(), _sendParam.amountLD);
} else {
// Cross-chain: Standard OFT send
IOFT(_oft).send{ value: msg.value }(_sendParam, MessagingFee(msg.value, 0), _refundAddress);
}
}

Key Implementation Tips

  1. Start Simple: Deploy a basic vault first, add yield strategies later
  2. Test Thoroughly: Each operation type has different gas requirements
  3. Monitor Closely: Set up alerts for failed compose messages
  4. Plan Recovery: Document procedures for each failure scenario
  5. Optimize Gas: Use the task's automatic optimization, adjust as needed

Troubleshooting

OVault operations have only two possible final outcomes: Success or Failed (but Refunded). Understanding the failure flow helps determine appropriate recovery actions.

Refund Scenarios and Recovery

The VaultComposerSync uses a try-catch pattern around handleCompose() to ensure robust error handling:

try this.handleCompose{ value: msg.value }(/*...*/) {
emit Sent(_guid);
} catch (bytes memory _err) {
// Automatic refund for any handleCompose failures
_refund(_composeCaller, _message, amount, tx.origin);
emit Refunded(_guid);
}

Common scenarios caught by try-catch:

  • InsufficientMsgValue - insufficient gas for destination delivery → Auto refund
  • SlippageExceeded - vault output below minimum → Manual refund available
  • Vault operational errors (paused, insufficient liquidity) → Manual refund available

Transaction Revert: Gas or Fee Issues

What happens: OFT transfer fails on source chain before any tokens move

Common causes:

  • Insufficient native tokens for LayerZero fees
  • Invalid destination endpoint configuration
  • Gas estimation errors

User experience: Transaction reverts immediately, no tokens transferred

Recovery: User can retry immediately after fixing the issue

  • Use quoteSend() to get accurate fee estimation
  • Verify destination chain configuration
  • Ensure sufficient native tokens for cross-chain fees

Automatic Refund: Insufficient msg.value for Second Hop

What happens: LayerZero completes lzReceive and lzCompose successfully, but insufficient gas for destination delivery

Technical flow:

  1. LZ Executor calls lzReceive() - tokens credited to composer ✓
  2. OFT calls endpoint.sendCompose() - composeMsg stored ✓
  3. LZ Executor calls lzCompose() on VaultComposerSync ✓
  4. Try-catch around handleCompose() catches InsufficientMsgValue revert
  5. Automatic _refund() triggered back to source chain

Common causes:

  • Underestimated gas for second hop during quoteSend()
  • Gas price fluctuations between quote and execution
  • Complex destination chain operations requiring more gas

User experience:

  • Cross-chain transfer appears successful initially
  • Composer automatically triggers refund to source chain
  • Original tokens returned within minutes

Recovery: Automatic - no user action required

  • Monitor source chain for refunded tokens
  • Retry with higher gas estimate from quoteSend()

Manual Refund: Vault Operation Issues

What happens: LayerZero flow completes successfully, but vault operation fails slippage check

Technical flow:

  1. LZ Executor calls lzReceive() - tokens credited to composer ✓
  2. OFT calls endpoint.sendCompose() - composeMsg stored ✓
  3. LZ Executor calls lzCompose() on VaultComposerSync ✓
  4. Try-catch around handleCompose() executes vault operation ✓
  5. Vault returns actualAmount (shares or assets)
  6. Slippage check: actualAmount >= minAmountLD FAILS
  7. SlippageExceeded revert caught by try-catch
  8. Manual _refund() available (user must trigger)

Common causes:

  • Vault share/asset price changed during cross-chain transfer
  • Vault hit deposit/withdrawal limits between quote and execution
  • minAmountLD set too high based on stale previewDeposit/previewRedeem data

User experience:

  • Cross-chain transfer succeeds
  • Vault operation fails on hub with slippage error
  • Tokens held by composer awaiting user action

Recovery: User must manually trigger refund from hub chain

  1. Switch wallet to hub chain network
  2. Call composer refund function
  3. Original tokens returned to source chain
  4. Retry operation with adjusted slippage tolerance

Prevention:

  • Use wider slippage tolerance (2-5% for volatile vaults)
  • Check vault limits: vault.maxDeposit(), vault.maxRedeem()
  • Monitor vault state with vault.previewDeposit() before large operations
  • Account for time delays in cross-chain operations when setting minAmountLD

Debugging Tools

  1. LayerZero Scan: Track cross-chain message status and identify failure points
  2. Hub Chain Explorer: Check composer transaction details and vault interaction logs
  3. Vault State Queries:
    • vault.previewDeposit(amount) - Estimate shares received
    • vault.previewRedeem(shares) - Estimate assets received
    • vault.maxDeposit(user) - Check deposit limits
    • vault.maxRedeem(user) - Check redemption limits
  4. Composer State: Check if refunds are available for failed operations