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
maxOutcapacity and the destination market has sufficientmaxIncapacity. - You have the correct
marketIdfor all source markets and themarketParamsfor the destination market. - You know the
feerequired for the transaction, which can be read from thefee(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:
- Check Target Market Liquidity: See if the user's borrow can be fulfilled without reallocation.
- Query Available Liquidity: If not, use the Morpho API to query the
publicAllocatorSharedLiquidityfor the target market. This returns a list of all markets in all vaults that can reallocate funds to your target. - Simulate and Build: Select the necessary source markets from the API response and construct the
withdrawalsarray. 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 viemSetup
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.
