OVault EVM Implementation
Create an omnichain ERC-4626 vault that enable users to deposit assets or redeem shares on any blockchain 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
composeMsgencoding and cross-chain message workflows - ERC-4626 Vaults: How the tokenized vault standard interface works for
deposit/redeemoperations
An Omnichain Vault (OVault) takes a new or existing ERC4626 vault, and connects the underlying asset or share to many blockchain networks using the Omnichain Fungible Token (OFT) standard.
An OVault implementation requires five core contracts to be deployed:
- an
OFTasset - an
ERC4626vault - an
OFTAdapterto transform the vault's share into an omnichain token - a
VaultComposerSyncto orchestrate omnichain deposits and redemptions between the asset and share - an
OFTto represent the shares on spoke chains
You can review the implementation of these contracts under Contracts Overview.
Step 1. Project Installation
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
After running, select the directory for the scaffold project to be cloned into:
✔ Where do you want to start your project? … ./<YOUR_DIRECTORY>
? Which example would you like to use as a starting point? › - Use arrow-keys. Return to submit.
❯ OVault EVM
OApp
OFT
OFTAdapter
ONFT721
After the installer completes, copy and paste the .env.example in the project root, add your PRIVATE_KEY, RPC_URL you will be working with, and rename the file to .env:
cp .env.example .env
You can find the sample codebase in devtools/examples/ovault-evm.
Step 2. Network Configuration
Update hardhat.config.ts to include your desired networks. Modify your .env file or the URL directly to change network RPCs:
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
};
Step 3. Deployment Configuration
Configure your vault deployment in devtools/deployConfig.ts. This file controls which contracts to deploy and to what chains.
The deployConfig supports several modes, depending on what contracts already have been deployed on the hub chain. If your asset token is already an OFT, you do not need to deploy a new OFT contract mesh. Both Stargate Hydra assets (e.g., USDC.e) and standard OFTs (e.g., USDT0) can be used as the asset inside the ERC4626 vault. To see a list of existing OFT-compatible assets, review the LayerZero OFT API.
Pick the setup section that best aligns with your deployment needs:
- 3.1a Existing AssetOFT
- 3.1b Existing AssetOFT and Vault
- 3.1c Existing AssetOFT, Vault, and ShareOFTAdapter
For a completely fresh deployment of the AssetOFT, OVault, and ShareOFT:
3.1a Existing AssetOFT
The only requirement is that your assetOFT is deployed on the hub chain defined in the deployConfig file.
-
Update the
_hubEidand the_spokeEidsfor the networks you plan to deploy to accordingly. -
Add the
assetOFTAddresscontract for the_hubEidnetwork under yourvaultconfig. -
Add any changes necessary to your
VaultandShareOFTconfigcontractormetadata.
// Hub network where ERC4626 lives
const _hubEid = EndpointId.ARBSEP_V2_TESTNET;
// Spoke networks where ShareOFT lives (excluding hub)
const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET];
// ============================================
// Deployment Export
// ============================================
// devtools/deployConfig.ts
export const DEPLOYMENT_CONFIG: DeploymentConfig = {
vault: {
contracts: {
vault: 'MyERC4626',
shareAdapter: 'MyShareOFTAdapter',
composer: 'MyOVaultComposer',
},
deploymentEid: _hubEid,
vaultAddress: undefined, // Existing ERC4626 vault
assetOFTAddress: '<YOUR_ASSET_OFT_ADDRESS>', // Existing AssetOFT
shareOFTAdapterAddress: undefined, // Deploy ShareOFTAdapter
},
// Share OFT configuration (only on spoke chains)
shareOFT: {
contract: 'MyShareOFT',
metadata: {
name: 'MyShareOFT',
symbol: 'SHARE',
},
deploymentEids: _spokeEids,
},
// Asset OFT configuration (deployed on specified chains)
assetOFT: {
contract: 'MyAssetOFT',
metadata: {
name: 'MyAssetOFT',
symbol: 'ASSET',
},
deploymentEids: [_hubEid, ..._spokeEids],
},
} as const;
assetOFT and shareOFT networks do not need to perfectly overlap, as long as both contain deployments on the _hubEid. Configure based on your deployment requirements.
3.1b Existing AssetOFT and Vault
If your assetOFT and ERC4626 contracts are already deployed, you only need to deploy the ShareOFTAdapter and Composer.
-
Update the
_hubEidand the_spokeEidsfor the networks you plan to deploy to accordingly. -
Add the
vaultAddressandassetOFTAddressfor the_hubEidnetwork under yourvaultconfig. -
Add any changes necessary to your
ShareOFTconfigcontractormetadata.
// devtools/deployConfig.ts
// Hub network where ERC4626 lives
const _hubEid = EndpointId.ARBSEP_V2_TESTNET;
// Spoke networks where ShareOFT lives (excluding hub)
const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET];
// ============================================
// Deployment Export
// ============================================
export const DEPLOYMENT_CONFIG: DeploymentConfig = {
vault: {
contracts: {
vault: 'MyERC4626',
shareAdapter: 'MyShareOFTAdapter',
composer: 'MyOVaultComposer',
},
deploymentEid: _hubEid,
vaultAddress: '<YOUR_VAULT_ADDRESS>', // Existing ERC4626 vault
assetOFTAddress: '<YOUR_ASSET_OFT_ADDRESS>', // Existing AssetOFT token
shareOFTAdapterAddress: undefined, // Deploy ShareOFTAdapter
},
// Share OFT configuration (only on spoke chains)
shareOFT: {
contract: 'MyShareOFT',
metadata: {
name: 'MyShareOFT',
symbol: 'SHARE',
},
deploymentEids: _spokeEids,
},
// Asset OFT configuration (deployed on specified chains)
assetOFT: {
contract: 'MyAssetOFT',
metadata: {
name: 'MyAssetOFT',
symbol: 'ASSET',
},
deploymentEids: [_hubEid, ..._spokeEids],
},
} as const;
This configuration will skip deploying the AssetOFT and ERC4626 Vault contracts, deploying only the ShareOFTAdapter and Composer.
3.1c Existing AssetOFT, Vault, and ShareOFT
If your assetOFT, ERC4626, and ShareOFTAdapter have already been deployed, you only need to deploy the Composer.
-
Update the
_hubEidand the_spokeEidsfor the networks you plan to deploy to accordingly. -
Add the
vaultAddress,assetOFTAddress, andshareOFTAdapterAddressfor the_hubEidnetwork under yourvaultconfig. -
Add any changes necessary to your
composerconfig undervault.
The Vault, ShareOFT, and AssetOFT configs and deployments will be skipped.
// devtools/deployConfig.ts
// Hub network where ERC4626 lives
const _hubEid = EndpointId.ARBSEP_V2_TESTNET;
// Spoke networks where ShareOFT lives (excluding hub)
const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET];
// ============================================
// Deployment Export
// ============================================
export const DEPLOYMENT_CONFIG: DeploymentConfig = {
vault: {
contracts: {
vault: 'MyERC4626',
shareAdapter: 'MyShareOFTAdapter',
composer: 'MyOVaultComposer',
},
deploymentEid: _hubEid,
vaultAddress: '<YOUR_VAULT_ADDRESS>', // Existing ERC4626 vault
assetOFTAddress: '<YOUR_ASSET_OFT_ADDRESS>', // Existing AssetOFT
shareOFTAdapterAddress: <'YOUR_SHARE_OFT_ADAPTER_ADDRESS'>, // Existing ShareOFTAdapter
},
// Share OFT configuration (only on spoke chains)
shareOFT: {
contract: 'MyShareOFT',
metadata: {
name: 'MyShareOFT',
symbol: 'SHARE',
},
deploymentEids: _spokeEids,
},
// Asset OFT configuration (deployed on specified chains OR use existing address)
assetOFT: {
contract: 'MyAssetOFT',
metadata: {
name: 'MyAssetOFT',
symbol: 'ASSET',
},
deploymentEids: [_hubEid, ..._spokeEids],
},
} as const;
This configuration will skip deployment of the AssetOFT, ERC4626 vault, and ShareOFT contracts. Only the Composer will be deployed.
3.1d New AssetOFT, Vault, and ShareOFT
If you have no existing OVault contracts, this configuration will deploy AssetOFT, ERC4626 vault, ShareOFTAdapter, and the Composer.
const _hubEid = EndpointId.ARBSEP_V2_TESTNET;
const _spokeEids = [EndpointId.OPTSEP_V2_TESTNET, EndpointId.BASESEP_V2_TESTNET];
// ============================================
// Deployment Export
// ============================================
export const DEPLOYMENT_CONFIG: DeploymentConfig = {
vault: {
contracts: {
vault: 'MyERC4626',
shareAdapter: 'MyShareOFTAdapter',
composer: 'MyOVaultComposer',
},
deploymentEid: _hubEid,
vaultAddress: undefined,
assetOFTAddress: undefined,
shareOFTAdapterAddress: undefined,
},
// Share OFT configuration (only on spoke chains)
ShareOFT: {
contract: 'MyShareOFT',
metadata: {
name: 'MyShareOFT',
symbol: 'SHARE',
},
deploymentEids: _spokeEids,
},
// Asset OFT configuration (deployed on specified chains)
AssetOFT: {
contract: 'MyAssetOFT',
metadata: {
name: 'MyAssetOFT',
symbol: 'ASSET',
},
deploymentEids: [_hubEid, ..._spokeEids],
},
} as const;
This configuration will deploy all core OVault contracts for a full fresh setup.
3.2 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.
3.3 Deploy
Deploy all vault contracts across all configured chains:
pnpm hardhat lz:deploy --tags ovault
Based on your deployConfig.ts, this single command will begin deploying the defined contracts on your target _hubEid and _spokeEids.
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
--networksflag:pnpm hardhat lz:deploy --tags ovault --networks arbitrum,optimism
Step 4. Wiring New Mesh
This establishes the peer relationships between each OFT deployment, enabling cross-chain token transfers. See the OFT Wiring Step for more information.
Depending on your deployment configuration in Step 3, you will have to wire either your newly deployed ShareOFT, AssetOFT, or both.
4.1 Existing Asset
After modifying your layerzero.share.config.ts:
pnpm hardhat lz:oapp:wire --oapp-config layerzero.share.config.ts
4.2 Existing Asset & Share
No action needed.
4.3 New Asset & Share
After modifying your layerzero.asset.config.ts and layerzero.share.config.ts:
# 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
Step 5: Usage
OVault enables two main operation patterns: deposits and redemptions. Each uses the standard OFT.send() interface with the composer handling vault operations automatically.
The provided project scaffold demonstrates how to create send calls in devtools/examples/ovault-evm/tasks/sendOVaultComposer.ts.
Deposit Assets → Receive Shares
Scenario: Deposit asset from a _spokeEid, receive vault shares on the same _spokeEid
# 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:
Scenario: Deposit asset from a _spokeEid, receive vault shares on a different _spokeEid
# 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:
Scenario: Deposit asset from _spokeEid, receive vault shares on the _hubEid chain
npx hardhat lz:ovault:send \
--src-eid 30110 --dst-eid 30184 \
--amount 100.0 --to 0xRecipient \
--token-type asset
Flow: