Skip to content

This tutorial assumes you are already familiar with Morpho’s core packages (such as blue-sdk and blue-sdk-viem) and have set up a Viem client for on-chain interactions. For working examples, check out the bundler-basic-app or the earn-basic-app repositories.

Introduction

The bundler-sdk-viem package extends Morpho’s simulation and interaction capabilities by converting simple user interactions (e.g., supply, borrow, repay, withdraw) into a bundled transaction. This bundle automatically includes all necessary ERC20 approvals, transfers, token wrapping/unwrapping, and even public liquidity reallocations. In short, it enables you to execute complex DeFi operations atomically, thereby reducing gas fees and minimizing the risk of intermediate state changes.

The bundler works by:

  • Simulating the full sequence of operations.
  • Populating a bundle with required ERC20 operations.
  • Optimizing the operations (merging duplicates, redirecting tokens when possible).
  • Encoding the bundle for submission using Viem.

Installation

Install the bundler package along with its required peer dependencies:

yarn add @morpho-org/bundler-sdk-viem @morpho-org/blue-sdk @morpho-org/morpho-ts viem

Setup

First, import the basic key functions and types from bundler-sdk-viem as well as from Morpho’s other packages:

import {
  populateBundle,
  finalizeBundle,
  encodeBundle,
  type BundlingOptions,
  type InputBundlerOperation,
} from "@morpho-org/bundler-sdk-viem";
 
import { MarketId, DEFAULT_SLIPPAGE_TOLERANCE } from "@morpho-org/blue-sdk";
import { SimulationState } from "@morpho-org/simulation-sdk";
import { WalletClient } from "viem";

Also, set up your Viem client and simulation state as needed for your application. As a reminder, an example has been implemented in this bundler-basic-app.

Core Concepts

Bundled Operations

The bundler-sdk-viem transforms input operations—which represent simple user actions—into a bundle of low-level on-chain operations. Here is the list of all the input operations:

Blue Operations:

  • Blue_SetAuthorization
  • Blue_Borrow
  • Blue_Repay
  • Blue_Supply
  • Blue_SupplyCollateral
  • Blue_Withdraw
  • Blue_WithdrawCollateral

Morpho Vaults Operations:

  • MetaMorpho_Deposit
  • MetaMorpho_Withdraw
  • MetaMorpho_PublicReallocate

For example, when a user wants to

  1. Supply assets,
  2. Supply collateral,
  3. and then borrow in one transaction,

you can pass three input operations:

  • Blue_Supply
  • Blue_SupplyCollateral
  • Blue_Borrow

The bundler automatically adds:

  • The required ERC20 approvals (via Erc20_Approve, Erc20_Permit, or Erc20_Permit2)
  • ERC20 transfers from the user to the bundler
  • Additional steps such as token wrapping or unwrapping

Bundling Options

The behavior of the bundler can be fine-tuned using the BundlingOptions interface. For example, you can provide:

  • A set of tokens for which to use a simple permit flow (withSimplePermit)
  • [Public allocator _ TODO LINK tuto] options that trigger liquidity reallocations if necessary
  • A callback to define custom operations for covering any required token amounts

Key Functions

1. populateBundle

This function is the entry point to convert an array of InputBundlerOperation into a bundle of low-level operations. It:

  • Simulates each input operation using the current SimulationState.
  • Returns an object containing the bundled operations along with the simulation steps.
const { operations, steps } = populateBundle(
  inputOperations,
  simulationState,
  bundlingOptions
);

2. finalizeBundle

After populating the bundle, finalizeBundle is used to:

  • Merge duplicate operations (e.g., multiple ERC20 approvals or transfers).
  • Optimize the operation sequence by redirecting tokens (e.g., from the bundler to the receiver).
  • Append any additional transfer operations to “skim” any remaining tokens.
const optimizedOperations = finalizeBundle(
  operations,
  simulationState,
  receiverAddress,
  unwrapTokensSet,
  unwrapSlippage
);

3. encodeBundle

Once the operations are optimized, they are encoded into a single transaction using encodeBundle. This function packages all the operations along with the requirements for signatures so that the transaction can be submitted on-chain.

Simulation & Error Handling

The bundler-sdk-viem integrates tightly with simulation-sdk. Functions such as simulateRequiredTokenAmounts, simulateBundlerOperations, and getSimulatedBundlerOperation are used to:

  • Simulate the effect of each bundled operation on your current state.
  • Determine how much of each token is required (especially for ERC20 transfers).
  • Handle errors by providing detailed simulation steps, which can be very useful during development and testing.

Always wrap your bundling logic in a try-catch block to capture and log errors.

Advanced Bundling Options

You can customize the bundling behavior via the BundlingOptions parameter:

const bundlingOptions: BundlingOptions = {
  withSimplePermit: new Set(["0xTokenAddress1", "0xTokenAddress2"]),
  publicAllocatorOptions: {
    enabled: true,
    supplyTargetUtilization: {
      [marketId]: 905000000000000000n, // For a specific market
    },
    defaultSupplyTargetUtilization: 905000000000000000n,
  },
  getRequirementOperations: (requiredTokenAmounts) => {
    // Optionally, add custom operations to handle token requirements.
    return [];
  },
};

These options allow you to:

  • Force simple permit flows on certain tokens
  • Trigger liquidity reallocation when the market’s utilization exceeds a target threshold
  • Inject custom logic to handle required token amounts

Sending the Transaction

Once the bundle is encoded and signed, the transaction is sent to the blockchain via your Viem wallet client. The setupBundle function iterates through the individual transactions (including any signature requirements) and sends them sequentially.

Exemple Implementation

Let's walk through a practical example that demonstrates how to bundle multiple Morpho actions into a single transaction. This example shows how to:

  1. Supply assets to a market
  2. Supply collateral
  3. Borrow assets

You can find a complete working implementation in our bundler-basic-app repository.

import { type Account, WalletClient, zeroAddress } from "viem";
import { parseAccount } from "viem/accounts";
 
import {
  type Address,
  addresses,
  ChainId,
  DEFAULT_SLIPPAGE_TOLERANCE,
  MarketId,
  MarketParams,
  UnknownMarketParamsError,
  getUnwrappedToken,
} from "@morpho-org/blue-sdk";
import {
  type BundlingOptions,
  type InputBundlerOperation,
  encodeBundle,
  finalizeBundle,
  populateBundle,
} from "@morpho-org/bundler-sdk-viem";
import "@morpho-org/blue-sdk-viem/lib/augment";
import { withSimplePermit } from "@morpho-org/morpho-test";
import {
  type SimulationState,
  SimulationState,
  isBlueOperation,
  isErc20Operation,
  isMetaMorphoOperation,
} from "@morpho-org/simulation-sdk";
 
export const setupBundle = async (
  client: WalletClient,
  startData: SimulationState,
  inputOperations: InputBundlerOperation[],
  {
    account: account_ = client.account,
    supportsSignature,
    unwrapTokens,
    unwrapSlippage,
    onBundleTx,
    ...options
  }: BundlingOptions & {
    account?: Address | Account;
    supportsSignature?: boolean;
    unwrapTokens?: Set<Address>;
    unwrapSlippage?: bigint;
    onBundleTx?: (data: SimulationState) => Promise<void> | void;
  } = {}
) => {
  if (!account_) throw new Error("Account is required");
  const account = parseAccount(account_);
 
  let { operations } = populateBundle(inputOperations, startData, {
    ...options,
    withSimplePermit: new Set([
      ...withSimplePermit[startData.chainId],
      ...(options?.withSimplePermit ?? []),
    ]),
    publicAllocatorOptions: {
      enabled: true,
      ...options.publicAllocatorOptions,
    },
  });
  operations = finalizeBundle(
    operations,
    startData,
    account.address,
    unwrapTokens,
    unwrapSlippage
  );
 
  const bundle = encodeBundle(operations, startData, supportsSignature);
 
  const tokens = new Set<Address>();
 
  operations.forEach((operation) => {
    const { address } = operation;
 
    if (
      isBlueOperation(operation) &&
      operation.type !== "Blue_SetAuthorization"
    ) {
      try {
        const marketParams = MarketParams.get(operation.args.id);
 
        if (marketParams.loanToken !== zeroAddress)
          tokens.add(marketParams.loanToken);
 
        if (marketParams.collateralToken !== zeroAddress)
          tokens.add(marketParams.collateralToken);
      } catch (error) {
        if (!(error instanceof UnknownMarketParamsError)) throw error;
      }
    }
 
    if (isMetaMorphoOperation(operation)) {
      tokens.add(address);
 
      const vault = startData.tryGetVault(address);
      if (vault) tokens.add(vault.asset);
    }
 
    if (isErc20Operation(operation)) {
      tokens.add(address);
 
      const unwrapped = getUnwrappedToken(address, startData.chainId);
      if (unwrapped != null) tokens.add(unwrapped);
    }
  });
 
  await onBundleTx?.(startData);
 
  // here EOA should sign tx, if it is a contract, this can be ignored/removed
  await Promise.all(
    bundle.requirements.signatures.map((requirement) =>
      requirement.sign(client, account)
    )
  );
 
  const txs = bundle.requirements.txs.map(({ tx }) => tx).concat([bundle.tx()]);
 
  for (const tx of txs) {
    await client.sendTransaction({ ...tx, account });
  }
 
  return { operations, bundle };
};
 
const { morpho } = addresses[ChainId.EthMainnet];
 
/**
 * Executes a series of Morpho operations: supply, supply collateral, and borrow
 * @param marketId - The ID of the market to interact with
 * @param client - The wallet client instance
 * @param simulationState - The current simulation state
 * @param amountSupply - Amount to supply as lending position
 * @param amountSupplyCollateral - Amount to supply as collateral
 * @param amountBorrow - Amount to borrow
 * @returns Array of transaction responses
 */
export const supplySupplyCollateralBorrow = async (
  marketId: MarketId,
  client: WalletClient,
  simulationState: SimulationState,
  amountSupply: bigint,
  amountSupplyCollateral: bigint,
  amountBorrow: bigint
) => {
  const user = client.account?.address;
  if (!user) throw new Error("User address is required");
  return setupBundle(client, simulationState, [
    {
      type: "Blue_Supply",
      sender: user,
      address: morpho,
      args: {
        id: marketId,
        assets: amountSupply,
        onBehalf: user,
        slippage: DEFAULT_SLIPPAGE_TOLERANCE,
      },
    },
    {
      type: "Blue_SupplyCollateral",
      sender: user,
      address: morpho,
      args: {
        id: marketId,
        assets: amountSupplyCollateral,
        onBehalf: user,
      },
    },
    {
      type: "Blue_Borrow",
      sender: user,
      address: morpho,
      args: {
        id: marketId,
        assets: amountBorrow,
        onBehalf: user,
        receiver: user,
        slippage: DEFAULT_SLIPPAGE_TOLERANCE,
      },
    },
  ]);
};

Main SDK Repository:

For more detailed source code and additional functions, visit the bundler-sdk-viem repository.