App Integration
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.
Prerequisites Using SDKs
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
- Supply assets,
- Supply collateral,
- 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
Using Slippage Tolerance
When working with DeFi operations, including slippage tolerance is crucial for transaction success. Slippage occurs because between the time a transaction is simulated and when it's actually mined on-chain:
- Accrued interest might change the expected output amount
- Market states may be updated by other transactions
- Vault conditions might shift slightly
The DEFAULT_SLIPPAGE_TOLERANCE
parameter (imported from @morpho-org/blue-sdk
) covers these minor discrepancies to ensure your transactions succeed even when market conditions change slightly.
Important: Always include the slippage parameter in operations that involve asset conversions, including:
Blue_Supply
Blue_Borrow
Blue_Repay
Blue_Withdraw
MetaMorpho_Deposit
MetaMorpho_Withdraw
Assets vs Shares Parameters
Morpho operations often allow you to specify either an assets
or shares
parameter, depending on your use case, please read this section about Assets vs Shares
{
type: "Blue_Repay",
sender: userAddress,
address: morpho,
args: {
id: marketId,
shares: position.borrowShares, // Full repayment
onBehalf: userAddress,
slippage: DEFAULT_SLIPPAGE_TOLERANCE, // Always include slippage
},
}
Key Functions
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
);
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
);
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:
- Supply assets to a market
- Supply collateral
- 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.