Multi-Step Deposits
The Omni SDK allows you to compose multiple contract interactions on the destination chain within a single cross-chain order. This powerful feature enables complex workflows, such as depositing funds into one protocol and then staking the resulting token in another, all atomically from the user's perspective.
This guide demonstrates how to configure a multi-step order using the useOrder
hook and the calls
parameter.
Example Scenario
Imagine a user wants to deposit USDC on a source chain (e.g., Ethereum) and have it automatically deposited into a stablecoin vault on a destination chain (e.g., Optimism). The vault issues a receipt token (let's call it newUSD
). The user then wants to stake this newUSD
in a separate staking contract on the same destination chain, receiving a final staked token (let's call it stkNewUSD
).
The required steps on the destination chain, executed by the SolverNet system, are:
- Deposit USDC into the Vault contract, crediting the SolverNet Executor with
newUSD
. - Approve the Staking contract to spend the Executor's received
newUSD
tokens. - Stake the Executor's
newUSD
tokens in the Staking contract for the user (crediting the user withstkNewUSD
).
We can bundle these destination chain actions into a single Omni SDK order.
Key Considerations
-
SolverNet Executor: The
calls
array you define in your order is executed sequentially by theSolverNetExecutor
contract on the destination chain. This Executor contract is the actualmsg.sender
for each call in the sequence. Therefore, any intermediate tokens generated (likenewUSD
in our example) are held by the Executor, and any necessary approvals must be performed by the Executor. -
Token Approvals: Since the Executor receives the intermediate
newUSD
token, it must callapprove
on thenewUSD
token contract, granting the Staking contract permission to spend its (Executor
's) tokens in the subsequent staking step. -
Static Amounts: The
amount
used within thecalls
sequence (e.g., for approvals or subsequent interactions like staking) must be statically defined when configuring the order. The system does not dynamically read the output amount from one call (like the amount ofnewUSD
received from the vault) to use as input for a subsequent call. You must know and specify the exact amounts required for each step beforehand. For many deposit-and-stake scenarios where the vault issues tokens 1:1, the amount for the approval and staking steps will match the initial deposit amount.
🛠️ Step-by-Step Configuration
1. Obtain a Quote Using useQuote
First, get a quote for the initial cross-chain transfer (USDC to USDC in this case, as the solver needs USDC on the destination to initiate the flow).
import { useQuote } from '@omni-network/react'
import { parseUnits } from 'viem'
const quote = useQuote({
srcChainId: 1, // Replace with actual source chain ID (e.g., Ethereum)
destChainId: 10, // Replace with actual destination chain ID (e.g., Optimism)
deposit: {
token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC on source
amount: parseUnits('1000', 6), // 1000 USDC
},
expense: {
token: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', // USDC on destination
// Amount specified here covers the initial USDC needed by the solver.
// We typically request the same amount if the vault exchange rate is 1:1.
amount: parseUnits('1000', 6),
},
mode: 'expense', // Calculate deposit based on desired expense
});
2. Define Contract ABIs
You'll need the ABIs for the contracts involved in the destination chain interactions:
Vault Contract ABI (Example):const vaultABI = [
{
inputs: [
{ internalType: 'address', name: 'recipient', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'deposit',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
] as const;
const stakingABI = [
{
inputs: [
{ internalType: 'address', name: 'beneficiary', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'stakeFor',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
] as const;
approve
):
const erc20ABI = [
{
inputs: [
{ internalType: 'address', name: 'spender', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'approve',
outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function',
},
] as const;
3. Configure useOrder
with Destination Calls
Pass the quote data and define the sequence of destination contract calls using the calls
array.
import { useOrder } from '@omni-network/react'
// --- Replace with actual values ---
const vaultAddress = '0x...'; // Address of the vault contract on destination
const newUSDTokenAddress = '0x...'; // Address of the vault's receipt token (newUSD)
const stakingContractAddress = '0x...'; // Address of the staking contract
// Use the correct Executor address for the destination chain (Mainnet/Testnet)
const executorAddress = '0x...'; // Address of the SolverNet Executor contract
const userAddress = '0x...'; // User's wallet address
const depositAmount = parseUnits('1000', 6); // The amount of USDC being deposited / newUSD being staked
// --- End Replace ---
const order = useOrder({
// Ensure quote data is available and valid
quote: quote.data,
calls: [
{
// 1. Deposit USDC into the Vault, crediting SolverNet Executor
target: vaultAddress,
abi: vaultABI,
functionName: 'deposit',
// Vault receives USDC, credits `executorAddress` with `newUSD`
args: [executorAddress, depositAmount],
},
{
// 2. SolverNet Executor approves Staking contract to spend its `newUSD`
target: newUSDTokenAddress,
abi: erc20ABI,
functionName: 'approve',
args: [stakingContractAddress, depositAmount],
},
{
// 3. SolverNet Executor stakes its `newUSD` for the user (receiving stkNewUSD implicitly)
target: stakingContractAddress,
abi: stakingABI,
functionName: 'stakeFor',
args: [userAddress, depositAmount],
},
],
});
calls
: An array of objects, each defining a contract call to be executed sequentially on the destination chain by theSolverNet Executor
.- Call 1: The Executor calls the
vaultAddress
'sdeposit
function, receivingnewUSD
tokens itself. - Call 2: The Executor calls
approve
on thenewUSDTokenAddress
, granting thestakingContractAddress
permission to spend thenewUSD
tokens it received in step 1. - Call 3: The Executor calls the
stakingContractAddress
'sstakeFor
function, staking itsnewUSD
tokens on behalf of theuserAddress
. The user effectively receivesstkNewUSD
as a result of this action.
4. Execute the Order
Once the order is configured and the user confirms, trigger the execution:
const handleExecute = async () => {
// Add checks: ensure order and order.data are defined and quote is valid
if (order.isReady && order.open) {
try {
const receipt = await order.open();
// Handle successful transaction
console.log('Order submitted:', receipt?.transactionHash);
} catch (error) {
// Handle error
console.error('Failed to submit order:', error);
}
}
};
Congrats! You completed a complex order in 1 action for your users.