Skip to content

Depositing & Withdrawing from Vaults

Morpho Vaults V1 and Morpho Vaults V2 are functionally identical for deposit and withdrawal operations. Both implement the standard ERC‑4626 interface, meaning deposit(), withdraw(), mint(), and redeem() work the same way across all vault versions. Your integration code remains unchanged regardless of which vault version your users interact with.

This guide covers three integration methods for Morpho Vault deposits and withdrawals. Because all Morpho Vaults follow the ERC4626 standard, the patterns below apply universally—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
  • Both Morpho Vault V1 and V2 should have this protection established by curators

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

EIP-2612 Permit Support

Both Morpho Vault V1 and V2 support EIP-2612 permit for vault shares, allowing gasless approvals via off-chain signatures.

Vault Shares Permit (V1 & V2)

All Morpho Vault versions implement EIP-2612 on vault shares. This lets you approve someone to transfer your vault shares without a separate on-chain approve() transaction.

VersionImplementation
V1 / V1.1Inherits OpenZeppelin's ERC20Permit
V2Native EIP-2612 implementation

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

Gasless Deposits via the Underlying Asset's Permit (V1 & V2)

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. This flow works identically on both V1 and V2.

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 are some example contracts demonstrating these interactions for both vault versions.

Morpho Vault V2
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
import { IVaultV2 } from "@morpho-org/vault-v2/src/interfaces/IVaultV2.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 
contract VaultV2Interaction {
    IVaultV2 public immutable vault;
    IERC20 public immutable asset;
 
    constructor(address _vault) {
        vault = IVaultV2(_vault);
        asset = IERC20(vault.asset());
    }
 
    /// @notice Deposits a specified amount of assets into the vault.
    /// @param amount The amount of underlying assets to deposit.
    function deposit(uint256 amount) external {
        asset.approve(address(vault), amount);
        vault.deposit(amount, msg.sender);
    }
 
    /// @notice Withdraws all assets from the vault for the caller.
    function withdrawAll() external {
        uint256 shares = vault.balanceOf(msg.sender);
        if (shares > 0) {
            vault.redeem(shares, msg.sender, msg.sender);
        }
    }
}

Method 2: Offchain Integration (TypeScript SDK)

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 @morpho-org/blue-sdk @morpho-org/blue-sdk-viem @morpho-org/morpho-ts
import { createWalletClient, http, publicActions, parseUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import { metaMorphoAbi } from "@morpho-org/blue-sdk-viem";
 
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: metaMorphoAbi,
  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: metaMorphoAbi,
  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: metaMorphoAbi,
    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):

Vault V2 with slippage check
// Educational example: Deposit with 1% slippage protection
import { IVaultV2 } from "@morpho-org/vault-v2/src/interfaces/IVaultV2.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 
contract VaultV2DepositWithSlippage {
    function depositWithSlippage(
        address vaultAddress,
        uint256 depositAmount
    ) external returns (uint256) {
        IVaultV2 vault = IVaultV2(vaultAddress);
        IERC20 asset = IERC20(vault.asset());
 
        // 1. Preview expected shares
        uint256 expectedShares = vault.previewDeposit(depositAmount);
 
        // 2. Calculate minimum acceptable shares (1% slippage = 99% of expected)
        uint256 minShares = expectedShares * 99 / 100;
 
        // 3. Approve and execute deposit
        asset.approve(vaultAddress, depositAmount);
        uint256 sharesMinted = vault.deposit(depositAmount, msg.sender);
 
        // 4. Verify slippage tolerance
        require(sharesMinted >= minShares, "Slippage tolerance exceeded");
 
        return sharesMinted;
    }
}