Skip to content

Triggering the Public Allocator

This tutorial explains how to trigger a reallocation using the Public Allocator to provide just-in-time liquidity for a borrow transaction.

Before you begin, make sure you understand the core concepts behind the Public Allocator and why it's a powerful tool for enhancing liquidity.

Review the Public Allocator Concept Page first.

The Core Action: reallocateTo

The central function of the Public Allocator is reallocateTo. This function pulls liquidity from a specified list of source markets within a vault and supplies it to a single destination market.

function reallocateTo(
    address vault,
    Withdrawal[] calldata withdrawals,
    MarketParams calldata supplyMarketParams
) external payable;
  • vault: The vault whose funds are being reallocated.
  • withdrawals: An array of structs, each specifying a source market and the amount to withdraw.
  • supplyMarketParams: The destination market where all withdrawn funds will be deposited.

Method 1: Manual Trigger via Etherscan (For Learning)

Manually triggering the reallocateTo function on a block explorer is a great way to understand its inputs.

1. Check Prerequisites

Before sending a transaction, verify:

  • The Public Allocator has the allocator role for the target vault.
  • The source markets have sufficient maxOut capacity and the destination market has sufficient maxIn capacity.
  • You have the correct marketId for all source markets and the marketParams for the destination market.
  • You know the fee required for the transaction, which can be read from the fee(vaultAddress) view function on the Public Allocator contract.

2. Structure the withdrawals Array

The withdrawals array must be sorted in ascending order by market ID.

3. Execute the Transaction

On a block explorer like Etherscan, navigate to the Public Allocator contract, connect your wallet, and call reallocateTo. You must send the required fee as the value of the transaction.

Example Input: This example reallocates 70 WETH from an Idle Market and 800 WETH from a wstETH market, supplying a total of 870 WETH to an rETH market.

// Payable Amount (Value): [Fee in ETH, e.g., 0.001]
{
  "vault": "0x38989bba00bdf8181f4082995b3deae96163ac5d",
  "withdrawals": [
    // Withdrawal from Idle Market (assuming it has the lower marketId)
    {
      "marketParams": { "loanToken": "0xc02...", "collateralToken": "0x000...", ... },
      "amount": "70000000000000000000"
    },
    // Withdrawal from wstETH/WETH market
    {
      "marketParams": { "loanToken": "0xc02...", "collateralToken": "0x7f3...", ... },
      "amount": "800000000000000000000"
    }
  ],
  "supplyMarketParams": {
    "loanToken": "0xc02...",
    "collateralToken": "0xae7...",
    ...
  }
}

After this transaction, the rETH market will have an additional 870 WETH of liquidity available to borrow.

Method 2: Programmatic Trigger (SDK & API)

For a dApp integration, you will programmatically construct and send this transaction. The recommended flow is to use the Morpho API to find available liquidity and the SDK to simulate and execute.

The general logic is:

  1. Check Target Market Liquidity: See if the user's borrow can be fulfilled without reallocation.
  2. Query Available Liquidity: If not, use the Morpho API to query the publicAllocatorSharedLiquidity for the target market. This returns a list of all markets in all vaults that can reallocate funds to your target.
  3. Simulate and Build: Select the necessary source markets from the API response and construct the withdrawals array. The Morpho SDKs are ideal for simulating the transaction to ensure it will succeed and to calculate the precise inputs.

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 onchain operations. You'll need the following input operation:

  • 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 onchain:

  • 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.

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 onchain.

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 enable public reallocation via the BundlingOptions parameter:

const bundlingOptions: BundlingOptions = {
  withSimplePermit: new Set(["0xTokenAddress1", "0xTokenAddress2"]),
  publicAllocatorOptions: {
    enabled: true,
    supplyTargetUtilization: {
      [marketId]: 905000000000000000n, // For a specific market
    },
    defaultSupplyTargetUtilization: 905000000000000000n,
  },
};

These options allow you to:

  • Force simple permit flows on certain tokens
  • Trigger liquidity reallocation when the market’s utilization exceeds a target threshold

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.

Example Implementation

Let's walk through a practical example that demonstrates how to bundle a borrow action with public reallocation enabled, if needed.

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 borrow Morpho operation, via the Bundler
 * @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 Borrow = 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_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.

For a detailed, working example of how to implement this logic using TypeScript, Viem, and the Morpho API, refer to the public-allocator-scripts repository.

View the Public Allocator Simulation Script on GitHub :::