Skip to content

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:

  1. Deposit USDC into the Vault contract, crediting the SolverNet Executor with newUSD.
  2. Approve the Staking contract to spend the Executor's received newUSD tokens.
  3. Stake the Executor's newUSD tokens in the Staking contract for the user (crediting the user with stkNewUSD).

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 the SolverNetExecutor contract on the destination chain. This Executor contract is the actual msg.sender for each call in the sequence. Therefore, any intermediate tokens generated (like newUSD 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 call approve on the newUSD token contract, granting the Staking contract permission to spend its (Executor's) tokens in the subsequent staking step.

  • Static Amounts: The amount used within the calls 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 of newUSD 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;
Staking Contract ABI (Example):
const stakingABI = [
  {
    inputs: [
      { internalType: 'address', name: 'beneficiary', type: 'address' },
      { internalType: 'uint256', name: 'amount', type: 'uint256' },
    ],
    name: 'stakeFor',
    outputs: [],
    stateMutability: 'nonpayable',
    type: 'function',
  },
] as const;
ERC20 ABI (for 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],
    },
  ],
});
Explanation:
  • calls: An array of objects, each defining a contract call to be executed sequentially on the destination chain by the SolverNet Executor.
  • Call 1: The Executor calls the vaultAddress's deposit function, receiving newUSD tokens itself.
  • Call 2: The Executor calls approve on the newUSDTokenAddress, granting the stakingContractAddress permission to spend the newUSD tokens it received in step 1.
  • Call 3: The Executor calls the stakingContractAddress's stakeFor function, staking its newUSD tokens on behalf of the userAddress. The user effectively receives stkNewUSD 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.