Symbiotic wstETH Deposit
This example demonstrates depositing wstETH from an L2 (Base Sepolia) into a Symbiotic collateral vault (DC_wstEth) on L1 (Holesky) using Omni SolverNet.
Since the Symbiotic vault contract used here (0x23e...fc) includes an onBehalfOf parameter in its deposit function (deposit(address onBehalfOf, uint256 amount)), we can directly specify the user's address in the useOrder call without needing withExecAndTransfer.
Concepts
- Direct Call: Because the target Symbiotic vault supports depositing
onBehalfOfa user, we don't needwithExecAndTransfer. -
Order Configuration:
deposit: wstETH from the source L2.expense: wstETH required by the vault on L1, with thespenderset to the vault contract address.calls: Contains a single, direct call to the vault'sdepositfunction, passing the connected user's address asonBehalfOf.
Code
import { useQuote, useOrder } from '@omni-network/react' // Assuming hooks are available here
import { type Address, zeroAddress } from 'viem'
import { useAccount } from 'wagmi'
// --- Configuration (Adapt as needed) ---
// Symbiotic Vault (DC_wstEth) on Holesky
const SYMBIOTIC_VAULT_ADDRESS = "0x23e98253f372ee29910e22986fe75bb287b011fc" as const
// wstETH on Holesky
const WSTETH_L1_ADDRESS = "0x8d09a4502cc8cf1547ad300e066060d043f6982d" as const
// Mock wstETH on Base Sepolia (mintable)
const WSTETH_L2_ADDRESS = "0x6319df7c227e34B967C1903A08a698A3cC43492B" as const
// Simple ABI for deposit(address, uint256)
const vaultABI = [
{
"inputs": [
{ "internalType": "address", "name": "onBehalfOf", "type": "address" },
{ "internalType": "uint256", "name": "amount", "type": "uint256" }
],
"name": "deposit",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
] as const;
// -------------------------------------
type UseL2DepositParams = {
destChainId: number // e.g., holesky.id
srcChainId: number // e.g., baseSepolia.id
depositAmount: bigint // Amount of wstETH to deposit from L2
}
export function useL2Deposit(params: UseL2DepositParams) {
const { address: userAddress, isConnected } = useAccount();
// 1. Quote: How much L1 wstETH expense for the L2 wstETH deposit?
const quote = useQuote({
srcChainId: params.srcChainId,
destChainId: params.destChainId,
deposit: {
token: WSTETH_L2_ADDRESS,
amount: params.depositAmount
},
expense: {
token: WSTETH_L1_ADDRESS
},
mode: "expense",
enabled: isConnected && params.depositAmount > 0n,
});
const quotedDepositAmt = quote.data?.deposit.amount ?? 0n;
const quotedExpenseAmt = quote.data?.expense.amount ?? 0n;
// 2. Configure Order (Direct call, no wrapping needed)
const order = useOrder({
srcChainId: params.srcChainId,
destChainId: params.destChainId,
deposit: {
token: WSTETH_L2_ADDRESS,
amount: quotedDepositAmt
},
expense: {
token: WSTETH_L1_ADDRESS,
spender: SYMBIOTIC_VAULT_ADDRESS, // Vault contract spends the solver's L1 wstETH
amount: quotedExpenseAmt
},
calls: [
{
target: SYMBIOTIC_VAULT_ADDRESS,
abi: vaultABI,
functionName: 'deposit',
// Directly pass user address and quoted L1 amount
args: [userAddress ?? zeroAddress, quotedExpenseAmt]
}
],
validateEnabled: quote.isSuccess && isConnected && userAddress != null,
});
// Expose order controls and status
return {
mutation: order.txMutation,
waitOrder: order.waitForTx,
orderStatus: order.status,
openL2Deposit: order.open,
isReady: order.isReady && order.validation?.status === 'accepted',
validation: order.validation,
quote,
};
}Usage
import React, { useState, useMemo } from 'react';
import { useL2Deposit } from './useL2Deposit'; // Import the custom hook
import { parseEther, formatEther } from 'viem';
import { baseSepolia, holesky } from 'viem/chains';
import { useAccount } from 'wagmi';
function SymbioticDepositComponent() {
const [amountStr, setAmountStr] = useState('0.01');
const { isConnected } = useAccount();
const depositAmount = useMemo(() => {
try {
return parseEther(amountStr as `${number}`);
} catch {
return 0n;
}
}, [amountStr]);
const {
openL2Deposit,
orderStatus,
isReady,
validation,
quote
} = useL2Deposit({
srcChainId: baseSepolia.id,
destChainId: holesky.id,
depositAmount: depositAmount, // This is the wstETH amount from L2
});
const quotedDepositAmt = quote.data?.deposit.amount ?? 0n;
const quotedExpenseAmt = quote.data?.expense.amount ?? 0n;
const isLoading = orderStatus === 'opening' || orderStatus === 'open';
return (
<div style={{ border: '1px solid #ccc', padding: '1rem', marginTop: '1rem' }}>
<h2>Symbiotic Vault Deposit (Base Sepolia wstETH -> Holesky Vault)</h2>
<label>
Amount wstETH to Deposit (from Base Sepolia):
<input
type="text"
value={amountStr}
onChange={(e) => setAmountStr(e.target.value)}
disabled={isLoading || !isConnected}
/>
</label>
<p style={{fontSize: '0.8em'}}>Make sure you have Mock wstETH on Base Sepolia. Mint <a href="https://basescan.org/address/0x6319df7c227e34B967C1903A08a698A3cC43492B#writeContract#F1" target="_blank" rel="noopener noreferrer">here</a>.</p>
{!isConnected && <p>Connect wallet to proceed.</p>}
{isConnected && depositAmount > 0n && (
<>
{quote.isLoading && <p>Getting quote...</p>}
{quote.isSuccess && <p>Quote: Deposit {formatEther(quotedDepositAmt)} L2 wstETH to deposit {formatEther(quotedExpenseAmt)} L1 wstETH.</p>}
{quote.isError && <p style={{color: 'red'}}>Quote Error: {quote.error.message}</p>}
{validation?.status === 'pending' && <p>Validating...</p>}
{validation?.status === 'rejected' && <p style={{color: 'red'}}>Rejected: {validation.rejectDescription}</p>}
<button onClick={openL2Deposit} disabled={!isReady || isLoading} style={{marginTop: '1rem'}}>
{isLoading ? 'Processing...' : 'Deposit via Omni'}
</button>
<p style={{ marginTop: '1rem' }}>Status: <strong>{orderStatus}</strong></p>
{orderStatus === 'filled' && <p style={{color: 'green'}}>✅ Success!</p>}
</>
)}
</div>
);
}
export default SymbioticDepositComponent;