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.
Prerequisites
Before implementing OVault, you should understand:
- OFT Standard: How Omnichain Fungible Tokens work and what the typical deployment looks like
- Composer Pattern: Understanding of
composeMsg
encoding and cross-chain message workflows - 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, theVaultComposer
, andShareOFTAdapter
(lockbox model) - Spoke Chains: Host
AssetOFTs
andShareOFTs
that connect to the hub
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 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.
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
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.
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 indeployConfig.asset.chains
- Deploy the vault system (
ERC4626
,ShareOFTAdapter
,Composer
) on the hub chain - Deploy
ShareOFTs
to all spoke chains indeployConfig.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 (assets
→shares
) - ShareOFT caller → Triggers
redeem
operation (shares
→assets
)
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 incomposeMsg
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
- Start Simple: Deploy a basic vault first, add yield strategies later
- Test Thoroughly: Each operation type has different gas requirements
- Monitor Closely: Set up alerts for failed compose messages
- Plan Recovery: Document procedures for each failure scenario
- 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 refundSlippageExceeded
- 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:
- LZ Executor calls
lzReceive()
- tokens credited to composer ✓ - OFT calls
endpoint.sendCompose()
- composeMsg stored ✓ - LZ Executor calls
lzCompose()
on VaultComposerSync ✓ - Try-catch around
handleCompose()
catchesInsufficientMsgValue
revert - 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:
- LZ Executor calls
lzReceive()
- tokens credited to composer ✓ - OFT calls
endpoint.sendCompose()
- composeMsg stored ✓ - LZ Executor calls
lzCompose()
on VaultComposerSync ✓ - Try-catch around
handleCompose()
executes vault operation ✓ - Vault returns
actualAmount
(shares or assets) - Slippage check:
actualAmount >= minAmountLD
FAILS SlippageExceeded
revert caught by try-catch- 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 stalepreviewDeposit/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
- Switch wallet to hub chain network
- Call composer refund function
- Original tokens returned to source chain
- 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
- LayerZero Scan: Track cross-chain message status and identify failure points
- Hub Chain Explorer: Check composer transaction details and vault interaction logs
- Vault State Queries:
vault.previewDeposit(amount)
- Estimate shares receivedvault.previewRedeem(shares)
- Estimate assets receivedvault.maxDeposit(user)
- Check deposit limitsvault.maxRedeem(user)
- Check redemption limits
- Composer State: Check if refunds are available for failed operations