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

Depositing & Withdrawing from Vaults

Morpho Vault V2 follows the standard ERC‑4626 interface for deposit and withdrawal operations. That means deposit(), withdraw(), mint(), and redeem() behave like familiar tokenized vault flows for new Earn integrations on Morpho.

This guide covers two integration methods for Morpho Vault deposits and withdrawals. Because Morpho Vault V2 follows the ERC4626 standard, the patterns below apply whether you're building a smart contract, dApp, or backend service.

Integration methods:
  1. Smart Contract Integration using Solidity
  2. Offchain Integration using TypeScript
    1. SDK - the recommended path, using @morpho-org/morpho-sdk (handles approvals, Permit/Permit2, native wrapping, slippage, and Bundler3 routing for you)
    2. Direct Viem - hand-rolled writeContract calls when you need full control over the transaction

Key Concepts: Assets vs. Shares

When interacting with ERC4626 vaults, you have two approaches for deposits and withdrawals: an asset-first approach or a shares-first approach. Understanding the difference is key to a robust integration.

ApproachDeposit FunctionWithdrawal FunctionDescription
Asset-Firstdeposit(assets, ...)withdraw(assets, ...)You specify the exact amount of the underlying token (e.g., USDC, WETH) you want to deposit or withdraw.
Shares-Firstmint(shares, ...)redeem(shares, ...)You specify the exact number of vault shares you want to mint or redeem.
Best Practice:
  • For deposits, deposit() is the most common and intuitive function.
  • For full withdrawals, redeem() is recommended. Redeeming all of a user's shares ensures their balance goes to zero and avoids leaving behind small, unusable amounts of "dust."
  • For partial withdrawals where a user needs a specific amount of the underlying asset, withdraw() is appropriate.

Prerequisites

Before you begin, you will need:

  • The address of the Morpho Vault you want to interact with. You can find active vaults using the Morpho API.
  • An account with a balance of the vault's underlying asset (e.g., WETH for a WETH vault).

Vault Safety: Inflation Attack Protection

All ERC4626-compliant vaults, including Morpho Vaults, require a dead deposit to protect against share inflation attacks. This protection must be verified before your first interaction with a vault.

Verification Check:
// Check that the vault has adequate protection
const deadAddress = "0x000000000000000000000000000000000000dEaD";
const deadShares = await vault.balanceOf(deadAddress);
 
if (deadShares < 1_000_000_000n) {
  throw new Error("Vault lacks required inflation protection");
}
Key Points:
  • The dead deposit must be at least 1e9 shares for assets with more than 9 decimals, or 1e12 shares otherwise (approximately $1 equivalent).
  • This check should be performed during vault integration/whitelisting, not on every transaction
  • Properly protected vaults make inflation attacks economically unfeasible
  • Morpho Vault V2 integrations should ensure this protection is established by curators

For a detailed explanation of how this protection works, see Vault Mechanics: Inflation Attack Protection.

EIP-2612 Permit Support

Morpho Vault V2 supports EIP-2612 permit for vault receipt tokens, allowing gasless approvals via off-chain signatures.

Vault Receipt Tokens Permit

Morpho Vault V2 implements EIP-2612 on vault receipt tokens. This lets you approve someone to transfer your vault receipt tokens without a separate on-chain approve() transaction.

Use case: Approve a third party (e.g., a router contract) to transfer your vault receipt tokens using an off-chain signature.

Gasless Deposits via the Underlying Asset's Permit

If the underlying asset supports EIP-2612 (e.g., USDC, DAI), you can sign a permit off-chain for the asset, then call asset.permit() + vault.deposit() in sequence. This eliminates the need for a separate approve() transaction while keeping the Vault V2 deposit flow ERC4626-compatible.

Example: Gasless Deposit with EIP-2612 Permit (TypeScript)

The following example demonstrates a deposit into a Morpho Vault using the underlying asset's EIP-2612 permit. The flow is:

  1. Sign an EIP-712 permit message off-chain (gasless)
  2. Call asset.permit() on-chain to set the approval
  3. Call vault.deposit() to deposit the approved tokens
import {
  createPublicClient,
  createWalletClient,
  http,
  type Address,
  type Hex,
  parseUnits,
} from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
 
// Example vault using USDC as underlying (USDC supports EIP-2612 permit)
const VAULT_ADDRESS = "0xBEEF01735c132Ada46AA9aA4c54623cAA92A64CB" as Address;
const USDC_ADDRESS = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" as Address;
 
const erc2612Abi = [
  {
    name: "permit",
    type: "function",
    inputs: [
      { name: "owner", type: "address" },
      { name: "spender", type: "address" },
      { name: "value", type: "uint256" },
      { name: "deadline", type: "uint256" },
      { name: "v", type: "uint8" },
      { name: "r", type: "bytes32" },
      { name: "s", type: "bytes32" },
    ],
    outputs: [],
    stateMutability: "nonpayable",
  },
  {
    name: "nonces",
    type: "function",
    inputs: [{ name: "owner", type: "address" }],
    outputs: [{ type: "uint256" }],
    stateMutability: "view",
  },
] as const;
 
const vaultAbi = [
  {
    name: "deposit",
    type: "function",
    inputs: [
      { name: "assets", type: "uint256" },
      { name: "receiver", type: "address" },
    ],
    outputs: [{ name: "shares", type: "uint256" }],
    stateMutability: "nonpayable",
  },
] as const;
 
function parseSignature(signature: Hex): { v: number; r: Hex; s: Hex } {
  const r = `0x${signature.slice(2, 66)}` as Hex;
  const s = `0x${signature.slice(66, 130)}` as Hex;
  const v = parseInt(signature.slice(130, 132), 16);
  return { v, r, s };
}
 
async function depositWithEIP2612Permit(
  publicClient: ReturnType<typeof createPublicClient>,
  walletClient: ReturnType<typeof createWalletClient>,
  vaultAddress: Address,
  assetAddress: Address,
  depositAmount: bigint,
  assetDomainConfig: { name: string; version: string },
) {
  const account = walletClient.account!;
  const chainId = await publicClient.getChainId();
 
  // 1. Get current nonce for the permit
  const nonce = await publicClient.readContract({
    address: assetAddress,
    abi: erc2612Abi,
    functionName: "nonces",
    args: [account.address],
  });
 
  // 2. Set deadline (1 hour from now)
  const deadline = BigInt(Math.floor(Date.now() / 1000) + 3600);
 
  // 3. Create EIP-712 typed data for the permit
  const typedData = {
    domain: {
      name: assetDomainConfig.name,
      version: assetDomainConfig.version,
      chainId,
      verifyingContract: assetAddress,
    },
    types: {
      Permit: [
        { name: "owner", type: "address" },
        { name: "spender", type: "address" },
        { name: "value", type: "uint256" },
        { name: "nonce", type: "uint256" },
        { name: "deadline", type: "uint256" },
      ],
    },
    primaryType: "Permit" as const,
    message: {
      owner: account.address,
      spender: vaultAddress,
      value: depositAmount,
      nonce,
      deadline,
    },
  };
 
  // 4. Sign the permit (GASLESS - off-chain signature)
  const signature = await walletClient.signTypedData(typedData);
  const { v, r, s } = parseSignature(signature);
 
  // 5. Execute permit on-chain (sets approval)
  const permitHash = await walletClient.writeContract({
    address: assetAddress,
    abi: erc2612Abi,
    functionName: "permit",
    args: [account.address, vaultAddress, depositAmount, deadline, v, r, s],
  });
  await publicClient.waitForTransactionReceipt({ hash: permitHash });
 
  // 6. Deposit into vault
  const depositHash = await walletClient.writeContract({
    address: vaultAddress,
    abi: vaultAbi,
    functionName: "deposit",
    args: [depositAmount, account.address],
  });
  await publicClient.waitForTransactionReceipt({ hash: depositHash });
 
  return { permitHash, depositHash };
}
 
// Example: Deposit 100 USDC using permit
async function main() {
  const publicClient = createPublicClient({
    chain: mainnet,
    transport: http(),
  });
 
  const account = privateKeyToAccount("0x..." as `0x${string}`);
  const walletClient = createWalletClient({
    chain: mainnet,
    transport: http(),
    account,
  });
 
  const depositAmount = parseUnits("100", 6); // 100 USDC
 
  await depositWithEIP2612Permit(
    publicClient,
    walletClient,
    VAULT_ADDRESS,
    USDC_ADDRESS,
    depositAmount,
    { name: "USD Coin", version: "2" }, // USDC domain config
  );
}

Method 1: Smart Contract Integration (Solidity)

This method is for developers building smart contracts that need to deposit into or withdraw from a Morpho Vault onchain.

Step 1: Approve the Vault

Before your contract can deposit tokens into a vault, it must first approve the vault contract to spend its tokens.

import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 
// Within your contract
address vaultAddress = 0x...; // Address of the Morpho Vault
address assetAddress = 0x...; // Address of the underlying asset (e.g., WETH)
uint256 depositAmount = 1 ether;
 
// Approve the vault to spend the asset
IERC20(assetAddress).approve(vaultAddress, depositAmount);

Step 2: Deposit or Mint Assets

Choose the function that best fits your needs.

deposit (V2)
import { IVaultV2 } from "@morpho-org/vault-v2/src/interfaces/IVaultV2.sol";
 
// Asset-first approach: Deposit a specific amount of the underlying token.
// The contract will calculate and mint the corresponding number of shares.
uint256 sharesMinted = IVaultV2(vaultAddress).deposit(depositAmount, address(this));

Step 3: Withdraw or Redeem Assets

Similarly, you can withdraw by specifying either the asset amount or the share amount.

withdraw (V2)
import { IVaultV2 } from "@morpho-org/vault-v2/src/interfaces/IVaultV2.sol";
 
// Asset-first approach: Withdraw a specific amount of the underlying token.
uint256 assetsToWithdraw = 1 ether;
uint256 sharesBurned = IVaultV2(vaultAddress).withdraw(assetsToWithdraw, address(this), address(this));

Full Example: Solidity Snippets

Here is an example contract demonstrating these interactions for Morpho Vault V2.

Method 2: Offchain Integration (TypeScript)

This method is ideal for dApp frontends or backend services that trigger transactions on behalf of users. There are two recommended paths:

  • 2.1 - Recommended: the Morpho SDK (@morpho-org/morpho-sdk) - a thin abstraction layer over the Morpho protocol that builds final, ready-to-send viem transactions and resolves all on-chain pre-requisites for you (ERC-20 approvals, Permit / Permit2, native-token wrapping). Use this for any new integration.
  • 2.2 - Direct Viem - hand-rolled writeContract calls. Use this when you need full control of the transaction (custom calldata, custom routing, mock environments, very specific edge cases).

Method 2.1: Morpho SDK (recommended)

For an end-to-end walkthrough of every action this SDK exposes (deposit, withdraw, redeem, force-withdraw, the getRequirements flow, the builder = signer invariant, error classes, etc.) see the Morpho SDK page.

Step 1: Install and set up the client

npm install @morpho-org/morpho-sdk viem
# or
pnpm add @morpho-org/morpho-sdk viem
# or
yarn add @morpho-org/morpho-sdk viem
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import { MorphoClient } from "@morpho-org/morpho-sdk";
 
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
 
const client = createWalletClient({
  account,
  chain: mainnet,
  transport: http(process.env.RPC_URL_MAINNET),
});
 
const morpho = new MorphoClient(client, {
  // Enables Permit / Permit2 in getRequirements() so users skip the extra approve tx.
  supportSignature: true,
});
 
const vault = morpho.vaultV2("0xVaultAddress...", mainnet.id);

Step 2: Deposit

import { parseUnits } from "viem";
 
const accrualVault = await vault.getData(); // fresh on-chain state with accrued interest
 
const { buildTx, getRequirements } = vault.deposit({
  amount: parseUnits("1.0", 18), // 1 underlying token
  userAddress: account.address,
  accrualVault,
});
 
// 1. Resolve approvals / permits before sending the deposit
const requirements = await getRequirements();
let signature;
for (const req of requirements) {
  if ("sign" in req) {
    // Permit / Permit2: capture the off-chain signature
    signature = await req.sign(client, account.address);
  } else {
    // ERC-20 approval: send the approval transaction
    await client.sendTransaction(req);
  }
}
 
// 2. Build and send the deposit tx (same client that built it - "builder = signer")
const tx = buildTx(signature);
const depositTxHash = await client.sendTransaction(tx);
console.log("Deposit successful:", depositTxHash);

Step 3: Withdraw / redeem

// Redeem all of a user's shares (recommended for full exits - leaves no dust)
const userShares = accrualVault.toShares(parseUnits("1.0", 18));
 
const { buildTx: buildRedeemTx } = vault.redeem({
  shares: userShares,
  userAddress: account.address,
});
 
const redeemTxHash = await client.sendTransaction(buildRedeemTx());
console.log("Withdrawal successful:", redeemTxHash);

For partial withdrawals by exact asset amount, use vault.withdraw({ amount, userAddress }) instead of redeem.

Method 2.2: Direct Viem

This path is useful when you need full control over the transaction. We'll use TypeScript with Viem directly against the vault's ERC-4626 interface.

Step 1: Setup

First, install the necessary packages and set up your Viem client.

npm install viem
import { createWalletClient, http, parseAbi, publicActions, parseUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
 
const vaultAbi = parseAbi([
  "function deposit(uint256 assets, address receiver) returns (uint256 shares)",
  "function redeem(uint256 shares, address receiver, address owner) returns (uint256 assets)",
  "function balanceOf(address owner) view returns (uint256)",
]);
 
const vaultAddress = "0x..."; // The vault address
const assetAddress = "0x..."; // The vault's underlying asset address
const account = privateKeyToAccount("0x..."); // The user's account
 
const client = createWalletClient({
  account,
  chain: mainnet,
  transport: http(process.env.RPC_URL_MAINNET),
}).extend(publicActions);

Step 2: Approve the Vault

Before depositing, check the current allowance and approve if necessary.

// 1. Check current allowance
const allowance = await client.readContract({
  address: assetAddress,
  abi: IERC20_ABI, // A standard ERC20 ABI
  functionName: "allowance",
  args: [account.address, vaultAddress],
});
 
const amountToDeposit = parseUnits("1.0", 18); // Example: 1 WETH
 
// 2. Approve if allowance is insufficient
if (allowance < amountToDeposit) {
  const { request } = await client.simulateContract({
    address: assetAddress,
    abi: IERC20_ABI,
    functionName: "approve",
    args: [vaultAddress, amountToDeposit],
  });
  await client.writeContract(request);
}

Step 3: Deposit Assets

Call the deposit function to add assets to the vault.

const { request: depositRequest } = await client.simulateContract({
  address: vaultAddress,
  abi: vaultAbi,
  functionName: "deposit",
  args: [amountToDeposit, account.address], // (assets, receiver)
});
 
const depositTxHash = await client.writeContract(depositRequest);
console.log("Deposit successful:", depositTxHash);

Step 4: Withdraw Assets

To withdraw the full balance, first fetch the user's share balance, then call redeem.

// 1. Get the user's share balance
const userShares = await client.readContract({
  address: vaultAddress,
  abi: vaultAbi,
  functionName: "balanceOf",
  args: [account.address],
});
 
// 2. Redeem the shares for the underlying asset
if (userShares > 0n) {
  const { request: redeemRequest } = await client.simulateContract({
    address: vaultAddress,
    abi: vaultAbi,
    functionName: "redeem",
    args: [userShares, account.address, account.address], // (shares, receiver, owner)
  });
  const redeemTxHash = await client.writeContract(redeemRequest);
  console.log("Withdrawal successful:", redeemTxHash);
}

Additional Considerations

Slippage for User Experience

While not a security requirement when proper dead deposit protection is in place, implementing slippage tolerance can improve user experience in your application.

Here's an educational example of how to implement a basic slippage check (1% tolerance):