Skip to main content
DeFiYieldEmbedded Wallet

Overview

This guide shows how to integrate a Morpho vault with CDP embedded wallets so your users can deposit tokens to earn yield and withdraw them, without leaving your application. Morpho vaults are ERC-4626 tokenized wrappers that automatically allocate deposits across multiple Morpho Blue lending markets. This guide uses the Steakhouse USDC vault on Base.

What you’ll build

  • Authenticate users and create embedded wallets with CDP hooks
  • Deposit USDC into a Morpho vault to earn yield
  • Withdraw USDC from the vault
  • Read a user’s vault position

Prerequisites

  • A CDP project with your domain configured
  • Node.js 24+

Installation

Create a React project and install dependencies:
npm create vite@latest cdp-morpho-app -- --template react-ts
cd cdp-morpho-app
Install CDP packages and viem:
npm install @coinbase/cdp-react @coinbase/cdp-core @coinbase/cdp-hooks viem

Provider setup

Wrap your app with CDPReactProvider:
main.tsx
import { CDPReactProvider } from "@coinbase/cdp-react";

createRoot(document.getElementById("root")!).render(
  <CDPReactProvider
    config={{
      projectId: "YOUR_CDP_PROJECT_ID",
      // Use "smart" for gasless transactions via CDP Paymaster
      // Use "eoa" for standard EOA wallets
      ethereum: { createOnLogin: "smart" },
      appName: "Morpho Vault guide",
    }}
  >
    <App />
  </CDPReactProvider>
);

Configuration

Define the vault and token addresses, and a public client for reading onchain data:
morpho.ts
import {
  createPublicClient,
  http,
  encodeFunctionData,
  parseAbi,
  parseUnits,
  formatUnits,
  maxUint256,
  type Address,
} from "viem";
import { base } from "viem/chains";

// Steakhouse USDC vault on Base
const VAULT_ADDRESS: Address = "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183";
// USDC on Base
const USDC_ADDRESS: Address = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";

const publicClient = createPublicClient({
  chain: base,
  transport: http(),
});

Deposit

Before depositing, the user must approve the vault to spend their USDC. Then call deposit with the amount and receiver address.
morpho.ts
import { useEvmAddress, useSendEvmTransaction } from "@coinbase/cdp-hooks";

const { evmAddress } = useEvmAddress();
const { sendEvmTransaction } = useSendEvmTransaction();

// Step 1: Approve the vault to spend USDC (one-time)
const approveData = encodeFunctionData({
  abi: parseAbi(["function approve(address spender, uint256 amount) returns (bool)"]),
  functionName: "approve",
  args: [VAULT_ADDRESS, maxUint256],
});

await sendEvmTransaction({
  evmAccount: evmAddress,
  network: "base",
  transaction: {
    to: USDC_ADDRESS,
    data: approveData,
    chainId: 8453,
  },
});

// Step 2: Deposit USDC into the vault
const amount = parseUnits("100", 6); // 100 USDC

const depositData = encodeFunctionData({
  abi: parseAbi(["function deposit(uint256 assets, address receiver) returns (uint256 shares)"]),
  functionName: "deposit",
  args: [amount, evmAddress],
});

const { transactionHash } = await sendEvmTransaction({
  evmAccount: evmAddress,
  network: "base",
  transaction: {
    to: VAULT_ADDRESS,
    data: depositData,
    chainId: 8453,
  },
});

Withdraw

To withdraw, call withdraw with the amount of underlying USDC to receive:
morpho.ts
const amount = parseUnits("10", 6); // 10 USDC

const withdrawData = encodeFunctionData({
  abi: parseAbi([
    "function withdraw(uint256 assets, address receiver, address owner) returns (uint256 shares)",
  ]),
  functionName: "withdraw",
  args: [amount, evmAddress, evmAddress],
});

const { transactionHash } = await sendEvmTransaction({
  evmAccount: evmAddress,
  network: "base",
  transaction: {
    to: VAULT_ADDRESS,
    data: withdrawData,
    chainId: 8453,
  },
});
To withdraw the entire position, use redeem with all vault shares instead:
morpho.ts
const shares = await publicClient.readContract({
  address: VAULT_ADDRESS,
  abi: parseAbi(["function balanceOf(address owner) view returns (uint256)"]),
  functionName: "balanceOf",
  args: [evmAddress],
});

const redeemData = encodeFunctionData({
  abi: parseAbi([
    "function redeem(uint256 shares, address receiver, address owner) returns (uint256 assets)",
  ]),
  functionName: "redeem",
  args: [shares, evmAddress, evmAddress],
});

Check position

Read the user’s vault share balance and convert it to the underlying USDC value:
morpho.ts
async function getPosition(userAddress: Address) {
  const shares = await publicClient.readContract({
    address: VAULT_ADDRESS,
    abi: parseAbi(["function balanceOf(address owner) view returns (uint256)"]),
    functionName: "balanceOf",
    args: [userAddress],
  });

  const assets = await publicClient.readContract({
    address: VAULT_ADDRESS,
    abi: parseAbi(["function convertToAssets(uint256 shares) view returns (uint256)"]),
    functionName: "convertToAssets",
    args: [shares],
  });

  return {
    shares,
    assets,
    formatted: formatUnits(assets, 6),
  };
}

Using other vaults

This code works with any ERC-4626 Morpho vault. To use a different vault:
  1. Replace VAULT_ADDRESS with the target vault’s address
  2. Replace USDC_ADDRESS with the vault’s underlying token
  3. Update the parseUnits/formatUnits decimals to match the token.
Browse available vaults here.