Skip to content

Commit d4900d2

Browse files
authored
DEVREL-782 Feat: support native composer in ovault example (#1840)
1 parent 7761376 commit d4900d2

11 files changed

Lines changed: 332 additions & 87 deletions

File tree

.changeset/thin-lizards-lick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@layerzerolabs/ovault-evm-example": patch
3+
---
4+
5+
Adds native OFT support (NativeOFTAdapter, StargatePoolNative) to OVault composer and send tasks.

examples/ovault-evm/README.md

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Deploy **omnichain ERC-4626 vaults** that enable users to deposit assets from an
1616

1717
- [Prerequisite Knowledge](#prerequisite-knowledge)
1818
- [Introduction](#introduction)
19+
- [Composer Types](#composer-types)
1920
- [Requirements](#requirements)
2021
- [Scaffold this Example](#scaffold-this-example)
2122
- [Helper Tasks](#helper-tasks)
@@ -45,6 +46,49 @@ OVault extends the ERC-4626 tokenized vault standard with LayerZero's omnichain
4546

4647
OVault makes it extremely easy to move assets and shares between any supported chains, while also enabling cross-chain vault operations. Users can deposit assets from any chain to receive shares on any destination chain, redeem shares from any chain to receive assets on any destination chain, or simply transfer these tokens between chains - all through a unified interface.
4748

49+
**Stargate Integration**: OVault is fully compatible with Stargate OFTs (USDC, USDT, etc.) as vault assets, with automatic decimal handling and no configuration required.
50+
51+
## Composer Types
52+
53+
OVault supports two types of composers to handle different asset types:
54+
55+
### MyOVaultComposerERC20
56+
57+
Use this composer when your vault's underlying asset is a standard **ERC20 token**:
58+
59+
- Works with any ERC20-based OFT (e.g., Stargate USDC, USDT0, WBTC)
60+
- Direct integration with ERC-4626 vaults
61+
- Standard approval and transfer flow
62+
63+
### MyOVaultComposerNative
64+
65+
Use this composer when your vault's underlying asset is based on the **chain's native token** (e.g., ETH, HYPE):
66+
67+
- Required for native token OFTs like `NativeOFTAdapter` or `StargatePoolNative`
68+
- Automatically wraps native tokens (ETH) into WETH before depositing to the vault
69+
- Necessary because ERC-4626 vaults only support ERC20 tokens
70+
- Transparent to end users - they send native tokens and receive shares
71+
72+
> **Note**: This composer uses the WETH9 interface (`deposit()`/`withdraw()`) to convert between native and wrapped tokens. This works with standard implementations like ETH→WETH and HYPE→WHYPE. If your chain's wrapped native token uses a different interface, you must override the composer's `lzCompose()` function with the correct wrapping mechanism.
73+
74+
> **Issuing Your Own Native Asset**: If you plan on issuing a bridged version of the chain's native asset yourself (i.e., not `StargatePoolNative` or an already deployed `NativeOFTAdapter`), use a `NativeOFTAdapter` instead of a standard OFT. See the [native-oft-adapter example](https://github.com/LayerZero-Labs/devtools/tree/main/examples/native-oft-adapter) for details.
75+
76+
**Which Composer Should You Use?**
77+
78+
The composer type depends on your asset OFT's underlying token:
79+
80+
- If `assetOFT.token()` returns an ERC20 address → use `MyOVaultComposerERC20`
81+
- If `assetOFT.token()` returns `address(0)` (native token) → use `MyOVaultComposerNative`
82+
83+
**Customizing Token Initialization**
84+
85+
The parent `VaultComposerSync` contract provides overridable functions for custom token patterns:
86+
87+
- `_initializeAssetToken()`: Override for non-standard asset token configurations
88+
- `_initializeShareToken()`: Override for non-standard share token configurations
89+
90+
This is useful when your vault or OFT contracts don't follow the default patterns (e.g., custom ERC4626 vaults that accept ETH directly).
91+
4892
## Requirements
4993

5094
- **git**
@@ -117,6 +161,13 @@ Configure your vault deployment in `devtools/deployConfig.ts`. This file control
117161

118162
> **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.
119163
164+
> **Important - Composer Selection**: Choose the correct composer type based on your asset:
165+
>
166+
> - Use `MyOVaultComposerERC20` for standard ERC20 asset OFTs
167+
> - Use `MyOVaultComposerNative` for native token OFTs (e.g., `NativeOFTAdapter`, `StargatePoolNative`)
168+
>
169+
> See the [Composer Types](#composer-types) section for details.
170+
120171
```typescript
121172
import { EndpointId } from "@layerzerolabs/lz-definitions";
122173

@@ -134,7 +185,7 @@ export const DEPLOYMENT_CONFIG = {
134185
contracts: {
135186
vault: "MyERC4626",
136187
shareAdapter: "MyShareOFTAdapter",
137-
composer: "MyOVaultComposer",
188+
composer: "MyOVaultComposerERC20", // Use MyOVaultComposerNative for native token assets
138189
},
139190
// IF YOU HAVE EXISTING CONTRACTS, SET THE ADDRESSES HERE
140191
vaultAddress: undefined, // Set to '0x...' to use existing vault
@@ -143,10 +194,11 @@ export const DEPLOYMENT_CONFIG = {
143194
},
144195

145196
// Asset OFT configuration (deployed on specified chains OR use existing address)
197+
// NOTE: For native assets (ETH, HYPE), use 'MyAssetOFTNative' with 'MyOVaultComposerNative'
146198
asset: {
147-
contract: "MyAssetOFT",
199+
contract: "MyAssetOFTERC20",
148200
metadata: {
149-
name: "MyAssetOFT",
201+
name: "MyAssetOFTERC20",
150202
symbol: "ASSET",
151203
},
152204
deploymentEids: [_hubEid, ..._spokeEids],
@@ -229,7 +281,7 @@ Compile your contracts:
229281
pnpm compile`
230282
```
231283

232-
> **Testing Note**: 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.
284+
> **Testing Note**: 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 `MyAssetOFTERC20` constructor to provide initial liquidity. This ensures you have tokens to test deposit and cross-chain transfer functionality.
233285
>
234286
> **⚠️ Warning**: 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.
235287
@@ -274,17 +326,17 @@ import { OAppEnforcedOption } from "@layerzerolabs/toolbox-hardhat";
274326
275327
const optimismContract: OmniPointHardhat = {
276328
eid: EndpointId.OPTSEP_V2_TESTNET.valueOf(),
277-
contractName: "MyAssetOFT",
329+
contractName: "MyAssetOFTERC20",
278330
};
279331
280332
const arbitrumContract: OmniPointHardhat = {
281333
eid: EndpointId.ARBSEP_V2_TESTNET.valueOf(),
282-
contractName: "MyAssetOFT",
334+
contractName: "MyAssetOFTERC20",
283335
};
284336
285337
const baseContract: OmniPointHardhat = {
286338
eid: EndpointId.BASESEP_V2_TESTNET.valueOf(),
287-
contractName: "MyAssetOFT",
339+
contractName: "MyAssetOFTERC20",
288340
};
289341
290342
// Configure gas limits for message execution
@@ -650,6 +702,30 @@ npx hardhat lz:ovault:send \
650702
- `--lz-compose-value`: Value for lzCompose operation (in wei)
651703
- `--oft-address`: Override source OFT address
652704

705+
**Using Stargate Assets:**
706+
707+
The OVault system is fully compatible with Stargate OFTs (e.g., USDC.e) as the underlying vault asset. To use Stargate assets, specify the Stargate pool/OFT address using the `--oft-address` parameter:
708+
709+
```bash
710+
# Example: Deposit Stargate USDC from Ethereum to receive shares on Base
711+
npx hardhat lz:ovault:send \
712+
--src-eid 30101 \
713+
--dst-eid 30184 \
714+
--amount 100 \
715+
--to 0xYourRecipientAddress \
716+
--token-type asset \
717+
--oft-address 0xc026395860Db2d07ee33e05fE50ed7bD583189C7 # Stargate USDC pool on Ethereum
718+
```
719+
720+
**Key points for Stargate integration:**
721+
722+
- The `--oft-address` must point to the Stargate pool/OFT address on the source chain
723+
- Decimals are automatically detected (e.g., 6 decimals for USDC vs 18 for ETH)
724+
- No LayerZero config files needed - addresses are read from the deployed composer
725+
- Works with any Stargate asset that implements the IOFT interface
726+
- Find Stargate contract addresses in the [LayerZero OFT Ecosystem Docs](https://docs.layerzero.network/v2/deployments/oft-ecosystem-stargate-assets?stages=mainnet&issuers=Stargate)
727+
- Automatic slippage protection (0.5% default) for stablecoin transfers
728+
653729
**Gas Optimization:**
654730

655731
The task automatically optimizes gas limits based on operation type:

examples/ovault-evm/contracts/MyAssetOFT.sol renamed to examples/ovault-evm/contracts/MyAssetOFTERC20.sol

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
55
import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol";
66

77
/**
8-
* @title MyAssetOFT
8+
* @title MyAssetOFTERC20
99
* @notice ERC20 representation of the vault's asset token on a spoke chain for cross-chain functionality
1010
* @dev This contract represents the vault's underlying asset on spoke chains. It inherits from
1111
* LayerZero's OFT (Omnichain Fungible Token) to enable seamless cross-chain transfers of the
@@ -14,7 +14,7 @@ import { OFT } from "@layerzerolabs/oft-evm/contracts/OFT.sol";
1414
* The asset OFT acts as a bridgeable ERC20 representation of the vault's collateral asset, allowing
1515
* users to move their assets across supported chains while maintaining fungibility.
1616
*/
17-
contract MyAssetOFT is OFT {
17+
contract MyAssetOFTERC20 is OFT {
1818
/**
1919
* @notice Constructs the Asset OFT contract
2020
* @dev Initializes the OFT with LayerZero endpoint and sets up ownership
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.20;
3+
4+
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
5+
import { NativeOFTAdapter } from "@layerzerolabs/oft-evm/contracts/NativeOFTAdapter.sol";
6+
7+
/**
8+
* @title MyAssetOFTNative
9+
* @notice NativeOFTAdapter for issuing a bridged version of the chain's native asset (e.g., ETH, HYPE)
10+
*
11+
* @dev WARNING: Only use this if you plan on issuing a bridged version of the chain's native asset yourself.
12+
* Most integrations should use existing native asset OFTs like `StargatePoolNative`.
13+
*
14+
* @dev WARNING: ONLY 1 NativeOFTAdapter should exist for a given global mesh, unless you make a
15+
* non-default implementation, which needs to be done very carefully.
16+
*
17+
* @dev This contract adapts the chain's native currency to OFT functionality for cross-chain transfers.
18+
* When used with MyOVaultComposerNative, the composer handles wrapping/unwrapping via WETH9 interface.
19+
* - `token()` returns `address(0)` indicating native asset
20+
* - `approvalRequired()` returns `false` since native transfers don't need ERC20 approval
21+
*/
22+
contract MyAssetOFTNative is NativeOFTAdapter {
23+
/**
24+
* @notice Constructs the Native Asset OFT Adapter
25+
* @dev Initializes the NativeOFTAdapter with LayerZero endpoint and sets up ownership
26+
* @param _localDecimals The decimals of the native token on this chain (18 for ETH, 18 for HYPE)
27+
* @param _lzEndpoint The address of the LayerZero endpoint on this chain
28+
* @param _delegate The address that will have owner privileges
29+
*/
30+
constructor(
31+
uint8 _localDecimals,
32+
address _lzEndpoint,
33+
address _delegate
34+
) NativeOFTAdapter(_localDecimals, _lzEndpoint, _delegate) Ownable(_delegate) {}
35+
}

examples/ovault-evm/contracts/MyOVaultComposer.sol renamed to examples/ovault-evm/contracts/MyOVaultComposerERC20.sol

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import { VaultComposerSync } from "@layerzerolabs/ovault-evm/contracts/VaultComp
77
* @title MyOVaultComposer
88
* @notice Cross-chain vault composer enabling omnichain vault operations via LayerZero
99
*/
10-
contract MyOVaultComposer is VaultComposerSync {
10+
contract MyOVaultComposerERC20 is VaultComposerSync {
1111
/**
12-
* @notice Creates a new cross-chain vault composer
12+
* @notice Creates a new cross-chain vault composer where the vault asset is an ERC20 token
1313
* @dev Initializes the composer with vault and OFT contracts for omnichain operations
1414
* @param _vault The vault contract implementing ERC4626 for deposit/redeem operations
15-
* @param _assetOFT The OFT contract for cross-chain asset transfers
15+
* @param _assetOFT The OFT contract for cross-chain asset transfers of the vault asset
1616
* @param _shareOFT The OFT contract for cross-chain share transfers
1717
*/
1818
constructor(address _vault, address _assetOFT, address _shareOFT) VaultComposerSync(_vault, _assetOFT, _shareOFT) {}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.22;
3+
4+
import { VaultComposerSyncNative } from "@layerzerolabs/ovault-evm/contracts/VaultComposerSyncNative.sol";
5+
6+
/**
7+
* @title MyOVaultComposerNative
8+
* @notice Cross-chain vault composer enabling omnichain vault operations via LayerZero for native token assets
9+
*
10+
* @dev This composer uses the WETH9 interface (deposit/withdraw) to convert between native tokens and
11+
* wrapped native tokens. This works with standard implementations like ETH->WETH and HYPE->WHYPE.
12+
*
13+
* @dev IMPORTANT: OFT.token() != Vault.asset() in this scenario:
14+
* - The asset OFT returns address(0) for native tokens
15+
* - The vault asset is the wrapped native token (e.g., WETH)
16+
* - The composer handles the wrapping/unwrapping automatically
17+
*
18+
* @dev If your chain's wrapped native token does NOT follow the WETH9 interface, you must override
19+
* the lzCompose function to use the correct wrapping mechanism.
20+
*
21+
* @dev The parent contract provides overridable functions for custom token initialization:
22+
* - _initializeAssetToken(): Override for non-standard asset token patterns
23+
* - _initializeShareToken(): Override for non-standard share token patterns
24+
*/
25+
contract MyOVaultComposerNative is VaultComposerSyncNative {
26+
/**
27+
* @notice Creates a new cross-chain vault composer where the vault asset is the chain's native asset
28+
* @dev Initializes the composer with vault and OFT contracts for omnichain operations
29+
* @dev Requires the asset OFT to be a NativeOFTAdapter or StargatePoolNative contract (OFT.token() returns address(0))
30+
* @dev Requires the vault's underlying asset to be a WETH9-compatible wrapped native token
31+
* @param _vault The vault contract implementing ERC4626 for deposit/redeem operations (asset must be WETH)
32+
* @param _assetOFT The NativeOFTAdapter or StargatePoolNative contract for cross-chain native asset transfers
33+
* @param _shareOFT The OFT contract for cross-chain share transfers
34+
*/
35+
constructor(
36+
address _vault,
37+
address _assetOFT,
38+
address _shareOFT
39+
) VaultComposerSyncNative(_vault, _assetOFT, _shareOFT) {}
40+
}

examples/ovault-evm/deploy/MyOvault.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,17 +81,33 @@ const deploy: DeployFunction = async (hre) => {
8181

8282
if (DEPLOYMENT_CONFIG.vault.assetOFTAddress) {
8383
assetOFTAddress = DEPLOYMENT_CONFIG.vault.assetOFTAddress
84-
console.log(`Using existing asset address: ${assetOFTAddress}`)
84+
console.log(`Using existing asset OFT address: ${assetOFTAddress}`)
8585
} else {
8686
// Use deployed address or get from deployments
8787
assetOFTAddress =
8888
deployedContracts.assetOFT || (await hre.deployments.get(DEPLOYMENT_CONFIG.assetOFT.contract)).address
89-
console.log(`Using deployed asset address: ${assetOFTAddress}`)
90-
// Fetch underlying ERC20 token address from the OFT using the IOFT artifact
91-
const IOFTArtifact = await hre.artifacts.readArtifact('IOFT')
92-
const oftContract = await hre.ethers.getContractAt(IOFTArtifact.abi, assetOFTAddress)
93-
const assetTokenAddress = await oftContract.token()
94-
console.log(`Underlying ERC20 token address found from OFT deployment: ${assetTokenAddress}`)
89+
console.log(`Using deployed asset OFT address: ${assetOFTAddress}`)
90+
}
91+
92+
// Fetch underlying ERC20 token address from the OFT using the IOFT artifact
93+
const IOFTArtifact = await hre.artifacts.readArtifact('IOFT')
94+
const oftContract = await hre.ethers.getContractAt(IOFTArtifact.abi, assetOFTAddress)
95+
const oftTokenAddress = await oftContract.token()
96+
97+
// Handle native token OFTs (NativeOFTAdapter, StargatePoolNative) where token() returns address(0)
98+
let assetTokenAddress: string
99+
if (oftTokenAddress === hre.ethers.constants.AddressZero) {
100+
if (!DEPLOYMENT_CONFIG.vault.assetTokenAddress) {
101+
throw new Error(
102+
`Native asset OFT detected (token() returns address(0)), but vault.assetTokenAddress is not configured. ` +
103+
`For native assets, you must set vault.assetTokenAddress to the WETH address on this chain.`
104+
)
105+
}
106+
assetTokenAddress = DEPLOYMENT_CONFIG.vault.assetTokenAddress
107+
console.log(`Native asset OFT detected - using configured WETH address: ${assetTokenAddress}`)
108+
} else {
109+
assetTokenAddress = oftTokenAddress
110+
console.log(`Underlying ERC20 token address found from OFT: ${assetTokenAddress}`)
95111
}
96112

97113
// Get vault address (existing or deploy new)
@@ -107,7 +123,7 @@ const deploy: DeployFunction = async (hre) => {
107123
args: [
108124
DEPLOYMENT_CONFIG.shareOFT.metadata.name,
109125
DEPLOYMENT_CONFIG.shareOFT.metadata.symbol,
110-
assetOFTAddress,
126+
assetTokenAddress,
111127
],
112128
log: true,
113129
skipIfAlreadyDeployed: true,

examples/ovault-evm/devtools/deployConfig.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const DEPLOYMENT_CONFIG: DeploymentConfig = {
3737
contracts: {
3838
vault: 'MyERC4626',
3939
shareAdapter: 'MyShareOFTAdapter',
40-
composer: 'MyOVaultComposer',
40+
composer: 'MyOVaultComposerERC20',
4141
},
4242
// IF YOU HAVE EXISTING CONTRACTS, SET THE ADDRESSES HERE
4343
// This will skip deployment and use your existing hubEid contract deployments instead
@@ -47,6 +47,9 @@ export const DEPLOYMENT_CONFIG: DeploymentConfig = {
4747
assetOFTAddress: undefined, // Set to '0xdef...' to use existing asset OFT
4848
// This must be the address of the ShareOFTAdapter
4949
shareOFTAdapterAddress: undefined, // Set to '0xghi...' to use existing ShareOFTAdapter
50+
// Required for native token OFTs (NativeOFTAdapter, StargatePoolNative) where token() returns address(0)
51+
// Set this to the WETH address on the hub chain (e.g., '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' for Ethereum mainnet)
52+
assetTokenAddress: undefined,
5053
},
5154

5255
// Share OFT configuration (only on spoke chains)
@@ -60,10 +63,11 @@ export const DEPLOYMENT_CONFIG: DeploymentConfig = {
6063
},
6164

6265
// Asset OFT configuration (deployed on specified chains OR use existing address)
66+
// NOTE: For native assets (ETH, HYPE), use 'MyAssetOFTNative' with 'MyOVaultComposerNative'
6367
assetOFT: {
64-
contract: 'MyAssetOFT',
68+
contract: 'MyAssetOFTERC20',
6569
metadata: {
66-
name: 'MyAssetOFT',
70+
name: 'MyAssetOFTERC20',
6771
symbol: 'ASSET',
6872
},
6973
deploymentEids: [_hubEid, ..._spokeEids],

examples/ovault-evm/devtools/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface VaultConfig {
1717
vaultAddress?: string // Optional pre-deployed vault address
1818
assetOFTAddress?: string // Optional pre-deployed asset OFT address
1919
shareOFTAdapterAddress?: string // Optional pre-deployed ShareOFTAdapter address
20+
assetTokenAddress?: string // Optional: Required when asset OFT is native (token() returns address(0)), set to WETH address
2021
}
2122

2223
export interface DeploymentConfig {

0 commit comments

Comments
 (0)