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. Direct Smart Contract Integration using Solidity
  2. Offchain Integration using TypeScript and Viem

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 + Viem)

This method is ideal for dApp frontends or backend services that trigger transactions on behalf of users. We'll use TypeScript with Viem and the Morpho SDKs.

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 funds 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):