Morpho SDK
Introduction
The Morpho SDK (@morpho-org/morpho-sdk) is the default and recommended SDK for building applications on top of Morpho.
It is the abstraction layer that simplifies the Morpho protocol: it produces ready-to-send viem transactions for VaultV2 and MarketV1 (Morpho Blue) on every EVM-compatible chain Morpho is deployed on, while taking care of the Bundler3 / GeneralAdapter1 plumbing, slippage protection, ERC-20 approvals, Permit / Permit2 signatures, Morpho authorizations, native-token wrapping, Blue-to-Blue refinance, and shared-liquidity reallocations for you.
The SDK also supports refinance: atomically moving a borrower's collateral and debt from one Morpho Blue market to another market that shares the same loan token and collateral token, optionally using shared liquidity through the PublicAllocator.
If you are integrating Morpho into a wallet, dApp, partner app, agent, or backend service, this is the SDK you want.
Source: morpho-org/sdks · packages/morpho-sdk · Package: @morpho-org/morpho-sdk on npm
When to use the Morpho SDK vs the lower-level primitives
| Use case | Use |
|---|---|
| Build production deposit / withdraw / borrow / repay / refinance transactions in an app | Morpho SDK (this page) |
Need automatic ERC-20 approvals, Permit/Permit2, Morpho setAuthorization, native wrapping | Morpho SDK |
| Want a single, consistent abstraction layer across VaultV2 and Morpho Blue markets | Morpho SDK |
| Want bundler3 / GeneralAdapter1 calldata composed and slippage-protected for you | Morpho SDK |
Need raw entity classes (Market, Position, Vault) for offchain computations | @morpho-org/morpho-sdk/entities subpath export |
| Need viem-based fetchers for low-level on-chain reads | @morpho-org/morpho-sdk/fetch subpath export |
| Building a liquidation bot, simulation pipeline, or custom calldata composer | Morpho SDK subpath exports (/entities, /fetch, /bundler) and friends |
The Morpho SDK ships its lower-level primitives as subpath exports - @morpho-org/morpho-sdk/entities, /fetch, /abis, /addresses, /constants, and /bundler - so you can always reach for them when you need them, without reaching past the Morpho SDK for the common path.
Installation
pnpm add @morpho-org/morpho-sdk@5.0.0 viem
# or
yarn add @morpho-org/morpho-sdk@5.0.0 viem
# or
npm install @morpho-org/morpho-sdk@5.0.0 viemviem is a peer dependency (^2.0.0) and must be installed alongside the SDK.
Setup
import "dotenv/config";
import { type Address, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import { morphoViemExtension } from "@morpho-org/morpho-sdk";
// Private-key account: this is the connected account the SDK enforces as the signer.
const account = privateKeyToAccount(process.env.PRIVATE_KEY as Address);
// This is the account the builders must be told about (see the invariant below).
const USER_ADDRESS = account.address;
// Extend a viem wallet client with the Morpho namespace. `client.morpho` exposes the
// stateless entity factories: `vaultV1`, `vaultV2`, and `blue`.
const client = createWalletClient({
account,
chain: mainnet,
transport: http(process.env.RPC_URL),
}).extend(
morphoViemExtension({
// Collect EIP-712 permit / permit2 signatures instead of only on-chain ERC-20
// approvals; defaults to `false` (classic approvals only).
supportSignature: true,
// Analytics metadata stamped onto every transaction the namespace builds.
// `timestamp` is optional; `origin` is required when `metadata` is passed.
metadata: { origin: "my-app", timestamp: true },
// Allow entity fetchers to use deployless multicall for on-chain reads.
supportDeployless: true,
}),
);
// Builder = signer invariant:
// The `userAddress` you pass to any builder MUST equal the account connected to THIS
// extended client, and the SAME client MUST sign and send the resulting transaction.
// It is enforced by `validateUserAddress`, which throws `MissingClientPropertyError`
// when the client has no connected account, or `AddressMismatchError` when the
// connected account does not match `userAddress`.Extend your viem client with morphoViemExtension() to add the client.morpho namespace. That namespace exposes three entity factories:
import { MarketParams } from "@morpho-org/morpho-sdk/entities";
// Three entity factories - chainId is mandatory (validated against the viem client).
const vaultV2 = client.morpho.vaultV2(
"0xVaultV2Address0000000000000000000000000000" as Address,
mainnet.id,
);
// `blue()` derives and validates the market id from these params, so construct a
// `MarketParams` instance - a raw object literal has no id and throws MarketIdMismatchError.
const market = client.morpho.blue(
new MarketParams({
loanToken: "0xLoanToken00000000000000000000000000000000" as Address,
collateralToken: "0xCollateralToken00000000000000000000000000" as Address,
oracle: "0xOracle0000000000000000000000000000000000" as Address,
irm: "0xIrm00000000000000000000000000000000000000" as Address,
lltv: 945000000000000000n, // 94.5%
}),
mainnet.id,
);Extension options
Pass these options to morphoViemExtension(...):
| Option | Default | Effect |
|---|---|---|
supportSignature | false | Lets the SDK collect EIP-712 signatures for Permit / Permit2 requirements; when false, requirements are classic ERC-20 approval transactions only. |
supportDeployless | undefined | Forwarded to the entity fetchers to allow deployless multicall reads (deployless on getData / getMarketData); unset falls back to viem's default. |
metadata | undefined | An origin string (with optional timestamp flag) appended as hex analytics bytes to the calldata of every transaction the namespace builds; unset appends nothing. |
The getRequirements flow
Every action that touches a user's tokens or positions returns the same shape:
buildTx(signatures?)- returns the final, deep-frozenviemTransaction({ to, value, data, action }). Collected requirement signatures are passed as an array.getRequirements()- returns the on-chain pre-requisites that must be satisfied first.
A requirement is one of:
- An ERC-20 approval transaction the user must send first (approving the bundler - or Morpho - to pull tokens).
- A Permit / Permit2 signature request - an off-chain signature that you collect and pass back into
buildTx([signature])as an array. Enabled withmorphoViemExtension({ supportSignature: true }). - A Morpho authorization transaction -
morpho.setAuthorization(generalAdapter1, true). Required once per user forborrow,supplyCollateralBorrow,repayWithdrawCollateral, andrefinance. The SDK only returns it if it is missing.
Typical pattern:
// Every action that touches user tokens or positions returns:
// - buildTx(signatures?) - produces the final viem `Transaction`
// - getRequirements() - pre-flight: ERC-20 approvals, Permit/Permit2,
// Morpho `setAuthorization` for GeneralAdapter1
//
// Typical flow:
// 1. const deposit = vault.deposit({ ... })
// 2. const requirements = await deposit.getRequirements()
// 3. For each requirement: send the approval / authorization tx,
// OR ask the user to sign the permit and capture `RequirementSignature`.
// 4. const tx = deposit.buildTx(signatures?)
// 5. Send `tx` with the SAME viem client used to build it.
const vaultData = await vaultV2.getData();
const deposit = vaultV2.deposit({
amount: 1_000000000000000000n,
userAddress: USER_ADDRESS,
vaultData,
});
const requirements = await deposit.getRequirements();
// requirements: (ERC20Approval tx | Permit/Permit2 Requirement)[]
const tx = deposit.buildTx(/* [requirementSignature] */);
// `tx` is a deep-frozen `{ to, value, data, action }`.The SDK exports type guards (isRequirementApproval, isRequirementBlueAuthorization, isRequirementSignature) so you can dispatch each requirement to the right UX (send tx vs. sign permit).
Builder = signer
userAddressMUST equal the connected account on theviemclient used to build the transaction, and the SAME client MUST sign and send it.
Enforced at build time by validateUserAddress, which throws MissingClientPropertyError (no connected account) or AddressMismatchError (account mismatch).
This invariant exists because some bundles - particularly repayWithdrawCollateral - mix explicit onBehalf = userAddress (repay) with implicit msg.sender (transfer-from + withdraw). Splitting the builder and the signer would atomically repay one user's debt while withdrawing another user's collateral.
This is a build-time guard against accidental mixed-account bundles for honest integrators, not a defense against a malicious builder. The signer remains responsible for reviewing what they sign. See BUNDLER3.md for the deeper Bundler3 / GeneralAdapter1 context.
Fetching state
Each entity exposes thin wrappers over the canonical viem fetchers (re-exported from @morpho-org/morpho-sdk/fetch), so you always pass fresh, accrued data into the builders:
| Entity | Method | Returns |
|---|---|---|
vaultV2 | vault.getData(parameters?) | AccrualVaultV2 - total assets, total supply, asset address, share/asset conversion, curated allocations, adapters used by forceWithdraw / forceRedeem. |
blue | market.getMarketData(parameters?) | Market - total supply / borrow assets and shares, utilization, liquidity, oracle price, rate-at-target. |
blue | market.getPositionData(user, parameters?) | AccrualPosition - borrowAssets, collateral, supplyShares, borrowShares, maxBorrowAssets, ltv, isHealthy, plus the parent Market. |
These data objects are the canonical entity classes re-exported from @morpho-org/morpho-sdk/entities. Refer to that module for the full type surface, accrual math, and helpers.
VaultV2
import { type Address, parseUnits } from "viem";
import { mainnet } from "viem/chains";
import { morphoViemExtension } from "@morpho-org/morpho-sdk";Fetch vault state
// Fetches on-chain vault state with accrued interest.
// Returns an `AccrualVaultV2` with: address, asset, totalAssets, totalSupply,
// share/asset conversion helpers (`toShares`, `toAssets`), curated allocations,
// and adapters used by `forceWithdraw` / `forceRedeem`.
const vaultData = await vault.getData();Deposit
// Routed through Bundler3 via GeneralAdapter1 - enforces `maxSharePrice`
// (ERC-4626 inflation-attack guard) and supports atomic native-token wrapping.
const deposit = vault.deposit({
amount: parseUnits("1", 18),
userAddress,
vaultData,
// slippageTolerance defaults to 0.03% (max 10%).
});
const requirements = await deposit.getRequirements();
// e.g. [erc20Approval tx for GeneralAdapter1] OR [Permit / Permit2 signature requirement]
const depositTx = deposit.buildTx(/* [requirementSignature] */);Routed through Bundler3 via GeneralAdapter1 - the SDK enforces a maxSharePrice derived from accrualVault and slippageTolerance (default 0.03%, max 10%), preventing ERC-4626 inflation-attack share-price drift.
Deposit with native-token wrapping
// For vaults whose underlying asset is the chain's wNative token (WETH on mainnet),
// you can deposit native ETH that the bundler atomically wraps before depositing.
const nativeDeposit = vault.deposit({
nativeAmount: parseUnits("1", 18), // 1 ETH wrapped → WETH → deposited
userAddress,
vaultData,
});
const nativeDepositTx = nativeDeposit.buildTx();
// Mixed (ERC-20 + native) is also supported in a single bundle:
const mixedDeposit = vault.deposit({
amount: parseUnits("0.5", 18), // already-held WETH
nativeAmount: parseUnits("0.5", 18), // raw ETH wrapped at execution
userAddress,
vaultData,
});
const mixedDepositTx = mixedDeposit.buildTx();The bundler atomically transfers the native token, wraps it to the chain's wNative (e.g. WETH), and deposits alongside any ERC-20 amount. The transaction's value is set to nativeAmount. Throws NativeAmountOnNonWNativeVaultError if the vault asset is not the chain's wNative.
Withdraw
// Direct vault call - no bundler overhead, no Morpho authorization required.
const withdraw = vault.withdraw({
amount: parseUnits("0.5", 18),
userAddress,
});
const withdrawTx = withdraw.buildTx();Redeem
// Direct vault call - same as withdraw but specifies an exact share amount,
// so it is immune to share-price drift between quoting and execution.
const redeem = vault.redeem({
shares: parseUnits("1", 18),
userAddress,
});
const redeemTx = redeem.buildTx();Force withdraw / force redeem
For VaultV2, when the vault's idle liquidity is not sufficient to satisfy a withdrawal, you can pull liquidity back from specific markets / adapters first by encoding a forceDeallocate chain followed by a single withdraw (or redeem) - all inside the vault's native multicall.
// Encodes N `forceDeallocate` calls + 1 `withdraw` in a single VaultV2 multicall.
// Use this when the vault's idle liquidity is insufficient and you need to
// pull liquidity back from specific markets/adapters before withdrawing.
const forceWithdraw = vault.forceWithdraw({
deallocations: [
{
adapter: "0xAdapter0000000000000000000000000000000000",
amount: parseUnits("0.5", 18),
},
],
withdraw: { amount: parseUnits("0.5", 18) },
userAddress, // penalty source AND withdraw recipient
});
const forceWithdrawTx = forceWithdraw.buildTx();// Share-based counterpart to forceWithdraw. The deallocated total must be
// >= the asset-equivalent of the redeemed shares; apply a buffer for share-price drift.
const forceRedeem = vault.forceRedeem({
deallocations: [
{
adapter: "0xAdapter0000000000000000000000000000000000",
amount: parseUnits("1.01", 18), // small buffer over target assets
},
],
redeem: { shares: parseUnits("1", 18) },
userAddress,
});
const forceRedeemTx = forceRedeem.buildTx();MarketV1 (Morpho Blue)
import { type Address, parseUnits } from "viem";
import { mainnet } from "viem/chains";
import { morphoViemExtension } from "@morpho-org/morpho-sdk";
import { MarketParams } from "@morpho-org/morpho-sdk/entities";Build a market entity & fetch data
// Construct a Morpho Blue (MarketV1) entity from its `MarketParams`.
// The unique market id is derivable from these five fields.
const market = client.morpho.blue(
new MarketParams({
loanToken: "0xLoanToken00000000000000000000000000000000",
collateralToken: "0xCollateralToken00000000000000000000000000",
oracle: "0xOracle0000000000000000000000000000000000",
irm: "0xIrm00000000000000000000000000000000000000",
lltv: 860000000000000000n, // 86%
}),
mainnet.id,
);
// On-chain reads with accrued interest:
const marketData = await market.getMarketData();
const positionData = await market.getPositionData(userAddress);
// positionData: AccrualPosition with health metrics
// { borrowAssets, collateral, supplyShares, borrowShares,
// maxBorrowAssets, ltv, isHealthy, market, ... }Supply collateral
// Routed through Bundler3 via GeneralAdapter1.
// `getRequirements()` returns the ERC-20 approval (or Permit / Permit2 signature)
// for the collateral token to GeneralAdapter1.
const supplyCollateral = market.supplyCollateral({
amount: parseUnits("1", 18),
userAddress,
// nativeAmount: parseUnits("1", 18) // when collateral is wNative
});
const supplyReqs = await supplyCollateral.getRequirements();
const supplyTx = supplyCollateral.buildTx(/* [requirementSignature] */);Borrow
// Routed through Bundler3 via `morphoBorrow`. Requires GeneralAdapter1 to be
// authorized on Morpho - `getRequirements()` returns the `setAuthorization`
// transaction if it has not been done yet.
//
// Always pass a **fresh** `positionData` - stale data may cause unexpected
// health-check failures or unexpected slippage protection.
const borrow = market.borrow({
amount: parseUnits("500", 6), // 500 USDC, for example
userAddress,
positionData,
});
const borrowReqs = await borrow.getRequirements();
const borrowTx = borrow.buildTx();The SDK validates an LLTV buffer (default 0.5%, max 10%) against your positionData before signing, so you cannot accidentally build a transaction that lands on the wrong side of the liquidation threshold the moment it includes. getRequirements() returns setAuthorization(generalAdapter1, true) if it has not been done yet for the user.
Supply collateral & borrow (atomic)
// Atomic supply-then-borrow in a single bundle. Validates an LLTV buffer (0.5%
// by default, max 10%) so a fresh position is not instantly liquidatable.
//
// `getRequirements()` returns IN PARALLEL:
// - ERC-20 approval / Permit / Permit2 for the collateral token to GeneralAdapter1
// - `morpho.setAuthorization(generalAdapter1, true)` if not yet authorized
const supplyCollateralBorrow = market.supplyCollateralBorrow({
amount: parseUnits("1", 18),
borrowAmount: parseUnits("500", 6),
userAddress,
positionData,
});
const supplyBorrowReqs = await supplyCollateralBorrow.getRequirements();
const supplyBorrowTx = supplyCollateralBorrow.buildTx(/* [requirementSignature] */);Repay
Two modes - exactly one of assets or shares:
assets- partial repay by exact asset amount.shares- full repay by exact share count, immune to interest accrued between quote and inclusion (recommended for "close position" flows).
// Two modes - exactly one of `assets` / `shares`:
// - `assets`: partial repay by exact asset amount.
// - `shares`: full repay by exact share count, immune to interest accrued
// between quote and inclusion (recommended for "close position").
//
// Repay does NOT require Morpho authorization - only an ERC-20 approval (or
// permit) on the loan token to GeneralAdapter1.
// Partial repay by assets
const partialRepay = market.repay({
assets: parseUnits("100", 6),
userAddress,
positionData,
});
const partialRepayTx = partialRepay.buildTx(/* [requirementSignature] */);
// Full repay by shares
const fullRepay = market.repay({
shares: positionData.borrowShares,
userAddress,
positionData,
});
const repayReqs = await fullRepay.getRequirements();
const fullRepayTx = fullRepay.buildTx(/* [requirementSignature] */);repay does not require Morpho authorization - only an ERC-20 approval (or permit) on the loan token to GeneralAdapter1.
Withdraw collateral
// Direct call to `morpho.withdrawCollateral()` - no bundler, no GeneralAdapter1.
// `msg.sender` MUST be `onBehalf`, so this is a single self-signed transaction.
// The SDK validates the resulting position health using the LLTV buffer.
const withdrawCollateral = market.withdrawCollateral({
amount: parseUnits("0.25", 18),
userAddress,
positionData,
});
const withdrawCollateralTx = withdrawCollateral.buildTx();Direct call to morpho.withdrawCollateral() - no bundler, no GeneralAdapter1 authorization needed. The SDK validates position health after the withdrawal against the LLTV buffer.
Repay & withdraw collateral (atomic)
// Atomic repay → withdraw collateral via Bundler3.
// Bundle order is critical: repay FIRST, then withdraw.
//
// `getRequirements()` returns IN PARALLEL:
// - ERC-20 approval / Permit / Permit2 for the loan token (for repay)
// - `morpho.setAuthorization(generalAdapter1, true)` if not yet authorized (for withdraw)
//
// The SDK simulates the repay before validating that the resulting position
// can sustain the requested collateral withdrawal.
const repayWithdrawCollateral = market.repayWithdrawCollateral({
assets: parseUnits("100", 6), // or { shares: ... }
withdrawAmount: parseUnits("0.25", 18),
userAddress,
positionData,
});
const repayWithdrawReqs = await repayWithdrawCollateral.getRequirements();
const repayWithdrawTx = repayWithdrawCollateral.buildTx(/* [requirementSignature] */);Bundle order is critical and is encoded for you: repay first, then withdraw. The SDK simulates the repay before validating that the resulting position can sustain the requested collateral withdrawal.
Refinance to another Morpho Blue market
refinance atomically migrates a borrower position from a source Morpho Blue market to a target Morpho Blue market that uses the same loanToken and collateralToken.
The target market may differ by oracle, IRM, or LLTV. The SDK validates both markets and builds one Bundler3 transaction that:
- supplies collateral on the target market,
- borrows on the target market inside the callback,
- repays the source market,
- withdraws source collateral to settle the deferred collateral transfer.
No user-side prefunding is required for the migrated collateral. getRequirements() only returns the Morpho authorization transaction for GeneralAdapter1 when the user has not authorized it yet.
const sourceMarketParams = new MarketParams({
loanToken: "0xLoanToken00000000000000000000000000000000",
collateralToken: "0xCollateralToken00000000000000000000000000",
oracle: "0xSourceOracle0000000000000000000000000000",
irm: "0xIrm00000000000000000000000000000000000000",
lltv: 860000000000000000n,
});
const targetMarketParams = new MarketParams({
loanToken: "0xLoanToken00000000000000000000000000000000",
collateralToken: "0xCollateralToken00000000000000000000000000",
oracle: "0xTargetOracle0000000000000000000000000000",
irm: "0xIrm00000000000000000000000000000000000000",
lltv: 915000000000000000n,
});
const sourceMarket = client.morpho.blue(sourceMarketParams, mainnet.id);
const targetMarket = client.morpho.blue(targetMarketParams, mainnet.id);
const sourcePosition = await sourceMarket.getPositionData(userAddress);
const targetPosition = await targetMarket.getPositionData(userAddress);
const refinance = sourceMarket.refinance({
userAddress,
positionData: sourcePosition,
target: {
marketParams: targetMarketParams,
positionData: targetPosition,
},
collateralAmount: parseUnits("5", 18),
borrowAssets: parseUnits("5", 18),
});
const refinanceRequirements = await refinance.getRequirements();
for (const requirement of refinanceRequirements) {
await client.sendTransaction(requirement);
}
const refinanceTx = refinance.buildTx();
await client.sendTransaction(refinanceTx);refinance supports three modes:
- Assets mode - pass
borrowAssetsto migrate an exact debt amount. - Shares mode - pass
borrowSharesto migrate an exact source borrow-share amount. This is the preferred full-close mode because it is immune to interest accrual between quote and inclusion. - Collateral-only mode - omit both
borrowAssetsandborrowSharesto move collateral only.
// Assets mode: migrate an exact debt amount.
const assetsModeRefinance = sourceMarket.refinance({
userAddress,
positionData: sourcePosition,
target: {
marketParams: targetMarketParams,
positionData: targetPosition,
},
collateralAmount: parseUnits("5", 18),
borrowAssets: parseUnits("2", 18),
});
// Shares mode: full-close source debt.
const sharesModeRefinance = sourceMarket.refinance({
userAddress,
positionData: sourcePosition,
target: {
marketParams: targetMarketParams,
positionData: targetPosition,
},
collateralAmount: sourcePosition.collateral,
borrowShares: sourcePosition.borrowShares,
});
// Collateral-only mode.
const collateralOnlyRefinance = sourceMarket.refinance({
userAddress,
positionData: sourcePosition,
target: {
marketParams: targetMarketParams,
positionData: targetPosition,
},
collateralAmount: parseUnits("5", 18),
});refinance requires Morpho authorization for GeneralAdapter1. It does not need ERC-20 approvals because the migrated collateral and debt are moved through Morpho callbacks, not pulled from the user wallet.
Shared liquidity (reallocations)
When the borrowed market lacks liquidity but a MetaMorpho vault you trust has spare capacity in another market, you can reallocate liquidity via the PublicAllocator. The SDK encodes a reallocateTo() call per VaultReallocation and prepends them to the bundle, before morphoBorrow.
You can either pass an explicit reallocations array, or let the SDK compute one for you from ReallocationData:
// Pass an explicit `reallocations` array when the borrowed market lacks
// liquidity but a MetaMorpho vault you trust has spare capacity in another
// market it allocates to. The SDK encodes a `PublicAllocator.reallocateTo()`
// call for each entry and prepends it to the bundle, BEFORE `morphoBorrow`.
const reallocations: VaultReallocation[] = [
{
vault: "0xVaultV1Address0000000000000000000000000000",
fee: 0n, // PublicAllocator fee in native token (often 0)
withdrawals: [
{
marketParams: {
loanToken: "0xLoanToken00000000000000000000000000000000",
collateralToken:
"0xSourceCollateralToken000000000000000000000",
oracle: "0xSourceOracle0000000000000000000000000000",
irm: "0xIrm00000000000000000000000000000000000000",
lltv: 860000000000000000n,
},
amount: parseUnits("2000", 6),
},
],
},
];
const borrowWithRealloc = market.borrow({
amount: parseUnits("500", 6),
userAddress,
positionData,
reallocations,
});
const borrowWithReallocTx = borrowWithRealloc.buildTx();
// `borrowWithReallocTx.value` includes the sum of every PublicAllocator fee.// Or let the SDK compute the optimal reallocation set for you:
// 1. `getReallocationData` - fetch ReallocationData for candidate vaults.
// 2. `getReallocations` - derive the `VaultReallocation[]` from a target borrow size.
const reallocationData = await market.getReallocationData({
vaultAddresses: [
"0xVaultV1Address0000000000000000000000000000",
] as Address[],
block: { number: 0n, timestamp: 0n }, // populate from your viem client
});
const computed = market.getReallocations({
reallocationData,
operation: "borrow",
amount: parseUnits("500", 6),
});
const borrowAuto = market.borrow({
amount: parseUnits("500", 6),
userAddress,
positionData,
reallocations: computed,
});
const borrowAutoTx = borrowAuto.buildTx();Reallocations work the same way for supplyCollateralBorrow. For refinance, reallocations target the target market because the callback borrows there before repaying the source market. Pass them as targetReallocations. The transaction's value includes the sum of every PublicAllocator fee.
const block = await client.getBlock();
const targetReallocationData = await targetMarket.getReallocationData({
vaultAddresses: [
"0xVaultV1Address0000000000000000000000000000",
] as Address[],
block: {
number: block.number,
timestamp: BigInt(block.timestamp),
},
});
const targetReallocations = targetMarket.getReallocations({
reallocationData: targetReallocationData,
operation: "borrow",
amount: parseUnits("5", 18),
});
const refinanceWithReallocations = sourceMarket.refinance({
userAddress,
positionData: sourcePosition,
target: {
marketParams: targetMarketParams,
positionData: targetPosition,
},
collateralAmount: parseUnits("5", 18),
borrowAssets: parseUnits("5", 18),
targetReallocations,
});
const refinanceWithReallocationsTx = refinanceWithReallocations.buildTx();
// `refinanceWithReallocationsTx.value` includes PublicAllocator fees from `targetReallocations`.Errors and invariants
The SDK uses dedicated error classes (no generic Errors) so you can branch on failure modes deterministically. A few highlights:
| Error | When it triggers |
|---|---|
MissingClientPropertyError | The viem client used to build a transaction is missing a required property such as a connected account.address. |
AddressMismatchError | The client's account address does not match the userAddress the call was built for (the builder-equals-signer guard). |
ChainIdMismatchError | The client's chain id does not match the chain id the entity was constructed against. |
MarketIdMismatchError | A MarketParams's id does not match the id derived from its loanToken, collateralToken, oracle, irm, and lltv. |
VaultAddressMismatchError | The vault entity's address does not match the vault address embedded in the call's arguments. |
AccrualPositionUserMismatchError | The positionData passed to a market action belongs to a different user than the call's userAddress. |
MissingAccrualPositionError | A Morpho accrual position the call must read is absent from the supplied data. |
BundlerErrors.MissingSignature | A Bundler3 action that needs an off-chain signature is encoded before its permit/permit2/authorization signature is attached. |
BundlerErrors.UnexpectedAction | A Bundler3 action is encoded that is unsupported on the requested chain. |
BundlerErrors.UnexpectedSignature | A Morpho authorization signature names a forbidden authorized account (for example Bundler3 itself) instead of GeneralAdapter1. |
AmbiguousRequirementSignaturesError | buildTx receives more than one requirement signature of the same kind (permit or authorization). |
UnexpectedRequirementSignatureError | buildTx receives a signature of a kind the operation does not consume. |
InvalidSignatureError | EIP-712 signature verification fails because the signed data does not match the expected signer address. |
Permit2ExpirationMissingError | A permit2 requirement signature omits the required args.expiration field. |
DepositAmountMismatchError | A deposit's amount differs from the amount the supplied permit/permit2 signature was issued for. |
DepositAssetMismatchError | A deposit's asset differs from the asset the supplied permit/permit2 signature was issued for. |
ApprovalAmountLessThanSpendAmountError | An ERC-20 approval amount is smaller than the spend amount it must cover. |
UnsupportedErc20ApprovalSpenderError | A requirement encoder targets a spender that is not supported on the chain (not GeneralAdapter1, Permit2, or the Midnight adapters). |
NegativeSlippageToleranceError | A supplied slippage tolerance is negative. |
ExcessiveSlippageToleranceError | A supplied slippage tolerance exceeds the 10% MAX_SLIPPAGE_TOLERANCE. |
NonPositiveAssetAmountError | An asset amount that must be positive is zero or negative. |
NonPositiveSharesAmountError | A shares amount that must be positive is zero or negative. |
NonPositiveMaxSharePriceError | A vault deposit's maxSharePrice slippage bound is zero or negative. |
NegativeMinSharePriceError | A vault redeem's minSharePrice slippage bound is negative. |
ZeroDepositAmountError | A vault deposit resolves both amount and nativeAmount to zero. |
NativeAmountOnNonWNativeVaultError | A vault deposit uses nativeAmount but the vault asset is not the chain's wNative token. |
ChainWNativeMissingError | A nativeAmount is supplied but the chain has no configured wNative address. |
NegativeNativeAmountError | A supplied nativeAmount is negative. |
EmptyDeallocationsError | A VaultV2 forceWithdraw or forceRedeem call has no deallocations to perform. |
VaultAssetMismatchError | A vault migration's source vault asset differs from the target vault asset. |
NonPositiveBorrowAmountError | A market borrow amount is zero or negative. |
ZeroCollateralAmountError | A market collateral supply resolves both amount and nativeAmount to zero. |
NativeAmountOnNonWNativeAssetError | An action uses nativeAmount but the target asset is not the chain's wNative token. |
BorrowExceedsSafeLtvError | A borrow exceeds the LLTV-buffered safe maximum for the position. |
MissingMarketPriceError | The market's oracle price is unavailable, so position health cannot be validated. |
NonPositiveMinBorrowSharePriceError | A market borrow's minSharePrice slippage bound is negative. |
NegativeSupplyAmountError | A market loan-asset supply amount is negative. |
NegativeSupplyMaxSharePriceError | A market loan-asset supply's maxSharePrice slippage bound is negative. |
ZeroSupplyAmountError | A market loan-asset supply resolves both amount and nativeAmount to zero. |
NonPositiveTransferAmountError | A market repay's transferAmount is zero or negative. |
TransferAmountNotEqualToAssetsError | A market repay in assets mode has transferAmount not equal to assets. |
MutuallyExclusiveRepayAmountsError | A market repay specifies both assets and shares as non-zero. |
NonPositiveRepayAmountError | A market repay has both assets and shares zero, or either negative. |
NonPositiveRepayMaxSharePriceError | A market repay's maxSharePrice slippage bound is zero or negative. |
RepayExceedsDebtError | A repay in assets mode exceeds the borrower's outstanding debt. |
RepaySharesExceedDebtError | A repay in shares mode supplies more shares than the borrower owes. |
ShareDivideByZeroError | A share-amount conversion would divide by zero because the market has no shares of the relevant kind. |
NonPositiveWithdrawCollateralAmountError | A market withdrawCollateral amount is zero or negative. |
WithdrawExceedsCollateralError | A collateral withdrawal exceeds the position's available collateral. |
WithdrawMakesPositionUnhealthyError | A collateral withdrawal would leave the position above the LLTV-buffered safe maximum. |
NonPositiveWithdrawAmountError | A market loan-asset withdraw has both assets and shares zero, or either negative. |
NegativeWithdrawMinSharePriceError | A market loan-asset withdraw's minSharePrice slippage bound is negative. |
MutuallyExclusiveWithdrawAmountsError | A loan-asset withdraw specifies both assets and shares as non-zero. |
WithdrawExceedsSupplyError | A loan-asset withdraw in assets mode exceeds the user's supplied assets in the market. |
WithdrawSharesExceedSupplyError | A loan-asset withdraw in shares mode exceeds the user's owned supply shares. |
NegativeReallocationFeeError | A reallocation's PublicAllocator fee is negative. |
EmptyReallocationWithdrawalsError | A reallocation entry has no withdrawals. |
NonPositiveReallocationAmountError | A reallocation withdrawal amount is zero or negative. |
ReallocationWithdrawalOnTargetMarketError | A reallocation withdrawal references the operation's own target market. |
UnsortedReallocationWithdrawalsError | Reallocation withdrawals within a vault are not strictly sorted by market id. |
MissingPublicAllocatorConfigError | A vault selected for reallocation has no configured PublicAllocator. |
DisabledReallocationMarketError | A reallocation attempts to use a market the vault has disabled. |
InsufficientSharedLiquidityError | The shared liquidity selected by computeReallocations cannot cover the operation's shortfall on the target market. |
ReallocationWithdrawExceedsMarketSupplyError | computeReallocations is asked to withdraw more than the target market's current totalSupplyAssets. |
UnknownReallocationMarketError | Reallocation state does not contain a requested market. |
UnknownReallocationVaultError | Reallocation state does not contain a requested vault. |
UnknownReallocationVaultMarketConfigError | Reallocation state does not contain a requested vault-market config. |
UnknownReallocationPositionError | Reallocation state does not contain a requested market position. |
NegativeBorrowSharesError | A refinance's borrowShares argument is negative. |
BorrowAmountAndSharesExclusiveError | A refinance specifies both borrowAssets and borrowShares as non-zero. |
NegativeMaxRepaySharePriceError | A refinance's maxRepaySharePrice slippage bound is negative. |
RefinanceSameMarketError | A refinance has identical source and target market ids. |
RefinanceTokenMismatchError | A refinance's source and target markets do not share the same loan and collateral tokens. |
RefinanceExceedsCollateralError | A refinance's collateralAmount exceeds the source position's available collateral. |
RefinanceExceedsBorrowSharesError | A refinance's borrowShares exceeds the source position's outstanding borrow shares. |
RefinanceExceedsBorrowAssetsError | A refinance's borrowAssets exceeds the source position's outstanding debt assets. |
RefinanceSharesMissingBorrowAssetsError | A refinance in shares mode omits the positive borrowAssets overshoot required for the target borrow leg. |
The full list lives in src/types/error.ts.
Key invariants
- Builder = signer. The
viemclient used to build a transaction MUST be the one used to sign and send it. Transactionobjects are deep-frozen. They cannot be mutated afterbuildTx.- No
any. Strict TypeScript across the entire surface, with discriminated unions for every action type. - Chain id is mandatory. Every entity is constructed against a specific chain id, validated against the viem client.
- Bundler3 is the default route for any operation that touches a user's tokens or position; direct calls are reserved for surfaces with no attack surface (vault
withdraw/redeem,morpho.withdrawCollateral). - Slippage protection is always on.
maxSharePrice(deposits, repay) andminSharePrice(borrows) are computed from fresh on-chain state and your slippage tolerance; the LLTV buffer additionally protects collateral operations. - Refinance markets must be compatible. Source and target markets must be different and must share
loanTokenandcollateralToken. - Refinance uses fresh source and target positions. Always pass fresh source and target
positionData; the SDK validates source residual health and target aggregate health with the LLTV buffer. - Refinance shares mode overshoots and sweeps. Shares mode overshoots the target borrow with slippage, repays source shares, then sweeps residual loan tokens back into target debt or user.
- Collateral-only refinance skips target health validation. It skips target health / oracle validation because it cannot worsen target debt health.
Resources
- Repository: morpho-org/sdks · packages/morpho-sdk
- Package:
@morpho-org/morpho-sdkon npm - Architecture:
ARCHITECTURE.md - Bundler3 / GeneralAdapter1 deep-dive:
BUNDLER3.md - Examples:
examples/example.ts - Contributing:
CONTRIBUTING.md - Security:
SECURITY.md - Lower-level primitives:
@morpho-org/morpho-sdk/entities·@morpho-org/morpho-sdk/fetch