Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

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, and shared-liquidity reallocations for you.

If you are integrating Morpho into a wallet, dApp, partner app, agent, or backend service, this is the SDK you want.

When to use the Morpho SDK vs Blue SDK

Use caseUse
Build production deposit / withdraw / borrow / repay transactions in an appMorpho SDK (this page)
Need automatic ERC-20 approvals, Permit/Permit2, Morpho setAuthorization, native wrappingMorpho SDK
Want a single, consistent abstraction layer across VaultV2 and Morpho Blue marketsMorpho SDK
Want bundler3 / GeneralAdapter1 calldata composed and slippage-protected for youMorpho SDK
Need raw entity classes (Market, Position, Vault) for offchain computationsBlue SDK
Need viem-based fetchers for low-level on-chain readsBlue SDK Viem
Building a liquidation bot, simulation pipeline, or custom calldata composerBlue SDK + Blue SDK Viem (and friends)

The Morpho SDK depends on @morpho-org/blue-sdk, @morpho-org/blue-sdk-viem, @morpho-org/bundler-sdk-viem, and @morpho-org/simulation-sdk under the hood - so you can always reach for the lower-level primitives when you need them, without reaching past the Morpho SDK for the common path.

Installation

pnpm add @morpho-org/morpho-sdk viem
# or
yarn add @morpho-org/morpho-sdk viem
# or
npm install @morpho-org/morpho-sdk viem

viem is a peer dependency (^2.0.0) and must be installed alongside the SDK.

Local development against the SDK

If you need to debug or extend the SDK locally, clone the repository and link it into your app:

# In a clone of github.com/morpho-org/morpho-sdk
git clone https://github.com/morpho-org/morpho-sdk.git
cd morpho-sdk
pnpm install
pnpm run build:link
 
# In your application
pnpm link @morpho-org/morpho-sdk

See the SDK README and CONTRIBUTING.md for the full development setup.

Setup

import "dotenv/config";
import { type Address, createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import { MorphoClient } from "@morpho-org/morpho-sdk";
 
// `MorphoClient` wraps a viem client and is the entry point for all SDK actions.
//
// Builder = signer: `userAddress` MUST equal the connected viem account, and the
// SAME client must sign the transaction. The SDK enforces this via
// `validateUserAddress` (throws `MissingClientPropertyError` / `AddressMismatchError`).
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
 
const walletClient = createWalletClient({
  account,
  chain: mainnet,
  transport: http(process.env.RPC_URL_MAINNET),
});
 
const morpho = new MorphoClient(walletClient, {
  // Enables Permit / Permit2 in `getRequirements()` to skip the extra approval tx.
  supportSignature: true,
  // Use deployless reads for chains where the SDK's helpers are not yet deployed.
  supportDeployless: false,
  // Optional metadata stamped into bundler calldata for attribution / tracing.
  metadata: { origin: "my-app" },
});
 
const USER_ADDRESS = account.address as Address;

MorphoClient is the entry point. It wraps a viem client and exposes three entity factories:

// Three entity factories - chainId is mandatory (validated against the viem client).
const vaultV2 = morpho.vaultV2(
  "0xVaultV2Address0000000000000000000000000000" as Address,
  mainnet.id,
);
 
const market = morpho.marketV1(
  {
    loanToken: "0xLoanToken00000000000000000000000000000000" as Address,
    collateralToken: "0xCollateralToken00000000000000000000000000" as Address,
    oracle: "0xOracle0000000000000000000000000000000000" as Address,
    irm: "0xIrm00000000000000000000000000000000000000" as Address,
    lltv: 945000000000000000n, // 94.5%
  },
  mainnet.id,
);

Client options

OptionDefaultEffect
supportSignaturefalseWhen true, getRequirements() returns Permit / Permit2 sign requests where supported, so you can skip the extra ERC-20 approve transaction.
supportDeployless-Use deployless reads on chains where the SDK helper contracts have not yet been deployed. Falls back to standard reads otherwise.
metadata-{ origin, timestamp? }, optionally embedded as bundler calldata metadata for attribution / tracing.

The getRequirements flow

Every action that touches a user's tokens or positions returns the same shape:

  • buildTx(requirementSignature?) - returns the final, deep-frozen viem Transaction ({ to, value, data, action }).
  • 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 pass back into buildTx(signature). Enabled with MorphoClient({ supportSignature: true }).
  • A Morpho authorization transaction - morpho.setAuthorization(generalAdapter1, true). Required once per user for borrow, supplyCollateralBorrow, and repayWithdrawCollateral. The SDK only returns it if it is missing.

Typical pattern:

// Every action that touches user tokens or positions returns:
//   - buildTx(requirementSignature?) - produces the final viem `Transaction`
//   - getRequirements()             - pre-flight: ERC-20 approvals, Permit/Permit2,
//                                     Morpho `setAuthorization` for GeneralAdapter1
//
// Typical flow:
//   1. const { buildTx, getRequirements } = vault.deposit({ ... })
//   2. const requirements = await getRequirements()
//   3. For each requirement: send the approval / authorization tx,
//      OR ask the user to sign the permit and capture `RequirementSignature`.
//   4. const tx = buildTx(signature?)
//   5. Send `tx` with the SAME viem client used to build it.
const accrualVault = await vaultV2.getData();
 
const deposit = vaultV2.deposit({
  amount: 1_000000000000000000n,
  userAddress: USER_ADDRESS,
  accrualVault,
});
 
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, isRequirementAuthorization, isRequirementSignature) so you can dispatch each requirement to the right UX (send tx vs. sign permit).

Builder = signer

userAddress MUST equal the connected account on the viem client 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 Blue SDK Viem fetchers, so you always pass fresh, accrued data into the builders:

EntityMethodReturns
vaultV2vault.getData(parameters?)AccrualVaultV2 - total assets, total supply, asset address, share/asset conversion, curated allocations, adapters used by forceWithdraw / forceRedeem.
marketV1market.getMarketData(parameters?)Market - total supply / borrow assets and shares, utilization, liquidity, oracle price, rate-at-target.
marketV1market.getPositionData(user, parameters?)AccrualPosition - borrowAssets, collateral, supplyShares, borrowShares, maxBorrowAssets, ltv, isHealthy, plus the parent Market.

These data objects are the canonical @morpho-org/blue-sdk entities. Refer to that section for the full type surface, accrual math, and helpers - the Morpho SDK simply re-uses them.

VaultV2

import { type Address, parseUnits } from "viem";
import { mainnet } from "viem/chains";
import { MorphoClient } 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 accrualVault = await vault.getData();

Deposit

// Routed through Bundler3 via GeneralAdapter1 - enforces `maxSharePrice`
// (ERC-4626 inflation-attack guard) and supports atomic native-token wrapping.
const { buildTx, getRequirements } = vault.deposit({
  amount: parseUnits("1", 18),
  userAddress,
  accrualVault,
  // slippageTolerance defaults to 0.03% (max 10%).
});
 
const requirements = await getRequirements();
// e.g. [erc20Approval tx for GeneralAdapter1] OR [Permit / Permit2 signature requirement]
 
const tx = 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 { buildTx: buildNativeDepositTx } = vault.deposit({
  nativeAmount: parseUnits("1", 18), // 1 ETH wrapped → WETH → deposited
  userAddress,
  accrualVault,
});
 
// Mixed (ERC-20 + native) is also supported in a single bundle:
const { buildTx: buildMixedDepositTx } = vault.deposit({
  amount: parseUnits("0.5", 18),       // already-held WETH
  nativeAmount: parseUnits("0.5", 18), // raw ETH wrapped at execution
  userAddress,
  accrualVault,
});

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 { buildTx: buildWithdrawTx } = vault.withdraw({
  amount: parseUnits("0.5", 18),
  userAddress,
});
const withdrawTx = buildWithdrawTx();

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 { buildTx: buildRedeemTx } = vault.redeem({
  shares: parseUnits("1", 18),
  userAddress,
});
const redeemTx = buildRedeemTx();

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 { buildTx: buildForceWithdrawTx } = vault.forceWithdraw({
  deallocations: [
    {
      adapter: "0xAdapter0000000000000000000000000000000000",
      amount: parseUnits("0.5", 18),
    },
  ],
  withdraw: { amount: parseUnits("0.5", 18) },
  userAddress, // penalty source AND withdraw recipient
});
// 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 { buildTx: buildForceRedeemTx } = vault.forceRedeem({
  deallocations: [
    {
      adapter: "0xAdapter0000000000000000000000000000000000",
      amount: parseUnits("1.01", 18), // small buffer over target assets
    },
  ],
  redeem: { shares: parseUnits("1", 18) },
  userAddress,
});

MarketV1 (Morpho Blue)

import { type Address, parseUnits } from "viem";
import { mainnet } from "viem/chains";
import { MorphoClient } from "@morpho-org/morpho-sdk";

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 = morpho.marketV1(
  {
    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 { buildTx: buildSupplyTx, getRequirements: getSupplyReqs } =
  market.supplyCollateral({
    amount: parseUnits("1", 18),
    userAddress,
    // nativeAmount: parseUnits("1", 18) // when collateral is wNative
  });
 
const supplyReqs = await getSupplyReqs();
const supplyTx = buildSupplyTx(/* 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 { buildTx: buildBorrowTx, getRequirements: getBorrowReqs } = market.borrow({
  amount: parseUnits("500", 6), // 500 USDC, for example
  userAddress,
  positionData,
});
 
const borrowReqs = await getBorrowReqs();
const borrowTx = buildBorrowTx();

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 { buildTx: buildSupplyBorrowTx, getRequirements: getSupplyBorrowReqs } =
  market.supplyCollateralBorrow({
    amount: parseUnits("1", 18),
    borrowAmount: parseUnits("500", 6),
    userAddress,
    positionData,
  });
 
const supplyBorrowReqs = await getSupplyBorrowReqs();
const supplyBorrowTx = buildSupplyBorrowTx(/* 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 { buildTx: buildPartialRepayTx } = market.repay({
  assets: parseUnits("100", 6),
  userAddress,
  positionData,
});
 
// Full repay by shares
const { buildTx: buildFullRepayTx, getRequirements: getRepayReqs } = market.repay(
  {
    shares: positionData.borrowShares,
    userAddress,
    positionData,
  },
);
 
const repayReqs = await getRepayReqs();
const fullRepayTx = buildFullRepayTx(/* 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 { buildTx: buildWithdrawCollateralTx } = market.withdrawCollateral({
  amount: parseUnits("0.25", 18),
  userAddress,
  positionData,
});
const withdrawCollateralTx = buildWithdrawCollateralTx();

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 { buildTx: buildRepayWithdrawTx, getRequirements: getRepayWithdrawReqs } =
  market.repayWithdrawCollateral({
    assets: parseUnits("100", 6), // or { shares: ... }
    withdrawAmount: parseUnits("0.25", 18),
    userAddress,
    positionData,
  });
 
const repayWithdrawReqs = await getRepayWithdrawReqs();
const repayWithdrawTx = buildRepayWithdrawTx(/* 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.

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 a SimulationState:

// 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 { buildTx: buildBorrowWithReallocTx } = market.borrow({
  amount: parseUnits("500", 6),
  userAddress,
  positionData,
  reallocations,
});
 
const borrowWithReallocTx = buildBorrowWithReallocTx();
// `borrowWithReallocTx.value` includes the sum of every PublicAllocator fee.
// Or let the SDK compute the optimal reallocation set for you:
//   1. `getReallocationData` - fetch the SimulationState for candidate vaults.
//   2. `getReallocations`    - derive the `VaultReallocation[]` from a target borrow size.
const reallocationData = await market.getReallocationData({
  vaultAddresses: [
    "0xVaultV1Address0000000000000000000000000000",
  ] as Address[],
  market: positionData.market,
  block: { number: 0n, timestamp: 0n }, // populate from your viem client
});
 
const computed = market.getReallocations({
  reallocationData,
  borrowAmount: parseUnits("500", 6),
});
 
const { buildTx: buildBorrowAutoTx } = market.borrow({
  amount: parseUnits("500", 6),
  userAddress,
  positionData,
  reallocations: computed,
});
 
const borrowAutoTx = buildBorrowAutoTx();

Reallocations work the same way for supplyCollateralBorrow. The transaction's value includes the sum of every PublicAllocator fee.

Errors and invariants

The SDK uses dedicated error classes (no generic Errors) so you can branch on failure modes deterministically. A few highlights:

ErrorWhen it triggers
MissingClientPropertyError("account")The viem client passed to MorphoClient has no connected account. Required for builder = signer enforcement.
AddressMismatchErroruserAddress does not match the connected account on the viem client.
ChainIdMismatchErrorThe viem client's chain id does not match the entity's chainId.
BorrowExceedsSafeLtvErrorRequested borrow would exceed collateralValue * (LLTV - buffer). Includes the maximum safe additional borrow amount.
MissingMarketPriceErrorOracle price unavailable for the market - health validation cannot run.
WithdrawMakesPositionUnhealthyErrorA withdrawCollateral / repayWithdrawCollateral would cross the LLTV buffer post-execution.
MutuallyExclusiveRepayAmountsErrorBoth assets and shares were provided to repay / repayWithdrawCollateral. Pass exactly one.
NativeAmountOnNonWNativeVaultErrornativeAmount was passed to a deposit on a vault whose asset is not the chain's wNative.
EmptyReallocationWithdrawalsError etc.Reallocation argument validation: empty list, target market included, unsorted withdrawals, negative fee, etc.

The full list lives in src/types/error.ts.

Key invariants

  • Builder = signer. The viem client used to build a transaction MUST be the one used to sign and send it.
  • Transaction objects are deep-frozen. They cannot be mutated after buildTx.
  • 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) and minSharePrice (borrows) are computed from fresh on-chain state and your slippage tolerance; the LLTV buffer additionally protects collateral operations.

Resources