Skip to content

Depositing & Withdrawing from Vaults

This tutorial provides a comprehensive guide for developers on how to integrate the core functionalities of Morpho Vaults: depositing and withdrawing assets.

Because all Morpho Vaults are ERC4626 compliant, they follow a standard interface for tokenized vaults. This makes integration predictable and straightforward, whether you are building a smart contract, a dApp, or a backend script.

We will cover three primary methods of interaction:

  1. Direct Smart Contract Integration using Solidity.
  2. Offchain Integration using the Morpho SDK with TypeScript and Viem.
  3. Advanced Integration using Morpho Bundlers for atomic, multi-step actions.

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 (approximately $10 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.

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 (V1)
import { IMetaMorpho } from "@morpho-org/metamorpho/src/interfaces/IMetaMorpho.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 = IMetaMorpho(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 (V1)
import { IMetaMorpho } from "@morpho-org/metamorpho/src/interfaces/IMetaMorpho.sol";
 
// Asset-first approach: Withdraw a specific amount of the underlying token.
uint256 assetsToWithdraw = 1 ether;
uint256 sharesBurned = IMetaMorpho(vaultAddress).withdraw(assetsToWithdraw, address(this), address(this));

Full Example: Solidity Snippets

Here are complete example contracts demonstrating these interactions for both vault versions.

Morpho Vault V1 (MetaMorpho)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
 
import { IMetaMorpho } from "@morpho-org/metamorpho/src/interfaces/IMetaMorpho.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 
contract MetaMorphoInteraction {
    IMetaMorpho public immutable vault;
    IERC20 public immutable asset;
 
    constructor(address _vault) {
        vault = IMetaMorpho(_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/morpho-ts
import { createWalletClient, http, publicActions, parseUnits } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
import { IMetaMorpho_ABI } from "@morpho-org/morpho-ts";
 
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: IMetaMorpho_ABI,
  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: IMetaMorpho_ABI,
  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: IMetaMorpho_ABI,
    functionName: "redeem",
    args: [userShares, account.address, account.address], // (shares, receiver, owner)
  });
  const redeemTxHash = await client.writeContract(redeemRequest);
  console.log("Withdrawal successful:", redeemTxHash);
}

Method 3: Advanced Integration with Bundlers

For complex operations, such as depositing into a vault and performing another action in the same transaction, Morpho Bundlers are the recommended tool.

Bundlers abstract away the low-level details like approvals and transfers. You simply define your high-level intent, and the bundler constructs the atomic transaction for you.

For example, to deposit into a vault, you would define an input operation like this:

import { type InputBundlerOperation } from "@morpho-org/bundler-sdk-viem";
 
const vaultAddress = "0x...";
const userAddress = "0x...";
const depositAmount = 1000000000000000000n; // 1 WETH
 
const depositOperation: InputBundlerOperation = {
  type: "MetaMorpho_Deposit",
  sender: userAddress,
  address: vaultAddress,
  args: {
    assets: depositAmount,
    receiver: userAddress,
  },
};

The bundler SDK would then automatically handle the approve call before executing the deposit.

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.

For production applications, we recommend using Bundlers, which automatically handle slippage checks for you.

For direct integrations, here's an educational example of how to implement a basic slippage check (1% tolerance):

Vault V1 with slippage check
// Educational example: Deposit with 1% slippage protection
import { IMetaMorpho } from "@morpho-org/metamorpho/src/interfaces/IMetaMorpho.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 
contract VaultV1DepositWithSlippage {
    function depositWithSlippage(
        address vaultAddress,
        uint256 depositAmount
    ) external returns (uint256) {
        IMetaMorpho vault = IMetaMorpho(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;
    }
}