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:
- Direct Smart Contract Integration using Solidity.
- Offchain Integration using the Morpho SDK with TypeScript and Viem.
- 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.
Approach | Deposit Function | Withdrawal Function | Description |
---|---|---|---|
Asset-First | deposit(assets, ...) | withdraw(assets, ...) | You specify the exact amount of the underlying token (e.g., USDC, WETH) you want to deposit or withdraw. |
Shares-First | mint(shares, ...) | redeem(shares, ...) | You specify the exact number of vault shares you want to mint or redeem. |
- 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).
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.
// 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.
// 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 Snippet
Here is a complete example contract demonstrating these interactions.
// 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
.