Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Morpho vaults pool deposited assets into lending markets where borrowers pay interest. That interest flows back to depositors automatically. This guide walks through integrating Morpho vaults into a Next.js app with Dynamic embedded wallets. For the final code, see the GitHub repository.

How it works

When a user deposits, they receive vault tokens representing their share of the pool. As borrowers pay interest, each vault token becomes redeemable for more of the underlying asset. When the user withdraws, they get back their original deposit plus accrued yield. No claiming or compounding is required. Example: A user deposits 1,000 USDC when each vault token is worth 1.00 USDC. When the token value rises to 1.05 USDC, those tokens are worth 1,050 USDC. APY is variable and adjusts based on borrower demand and the curator’s allocation strategy.

Setup

Project setup

Follow the JS SDK Quickstart to initialize a Next.js app with Dynamic. Scaffold a Next.js app with create-next-app and mirror the provider wiring from the quickstart or the GitHub repository linked above.
In the Dynamic dashboard, enable Ethereum under Chains & Networks, enable Embedded wallets under Wallets, and add your app’s origin under Security → Allowed Origins.

Install dependencies

npm install @dynamic-labs-sdk/client @dynamic-labs-sdk/evm viem

Environment variables

.env.local
NEXT_PUBLIC_DYNAMIC_ENV_ID=your-environment-id-here
Your environment ID is in the Dynamic dashboard under Developer Settings → SDK & API Keys.

Initialize Dynamic

Create src/lib/dynamic.ts:
src/lib/dynamic.ts
import { createDynamicClient } from "@dynamic-labs-sdk/client";
import { addEvmExtension } from "@dynamic-labs-sdk/evm";

export const dynamicClient = createDynamicClient({
  environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!,
  metadata: { name: "Morpho Lending" },
});

addEvmExtension();

Configure providers

Create src/lib/providers.tsx with a custom wallet context that tracks the active chain ID:
src/lib/providers.tsx
"use client";

import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react";
import { getWalletAccounts, onEvent, isSignedIn, logout, detectOAuthRedirect, completeSocialAuthentication } from "@dynamic-labs-sdk/client";
import { createWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas";
import { isEvmWalletAccount, type EvmWalletAccount } from "@dynamic-labs-sdk/evm";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { dynamicClient } from "./dynamic";

interface WalletContextValue {
  evmAccount: EvmWalletAccount | null;
  loggedIn: boolean;
  chainId: number;
  setChainId: (id: number) => void;
  ensureEvmWallet: () => Promise<void>;
  disconnect: () => Promise<void>;
}

const WalletContext = createContext<WalletContextValue>({
  evmAccount: null,
  loggedIn: false,
  chainId: 8453, // Base default
  setChainId: () => {},
  ensureEvmWallet: async () => {},
  disconnect: async () => {},
});

export function useWallet() { return useContext(WalletContext); }

const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, refetchOnWindowFocus: false } } });

export default function Providers({ children }: { children: ReactNode }) {
  const [evmAccount, setEvmAccount] = useState<EvmWalletAccount | null>(null);
  const [loggedIn, setLoggedIn] = useState(false);
  const [chainId, setChainId] = useState<number>(8453);

  const refresh = useCallback(() => {
    const accounts = getWalletAccounts(dynamicClient);
    setEvmAccount(accounts.find(isEvmWalletAccount) ?? null);
    setLoggedIn(isSignedIn(dynamicClient));
  }, []);

  const disconnect = useCallback(async () => {
    await logout(dynamicClient);
    setEvmAccount(null);
    setLoggedIn(false);
  }, []);

  const ensureEvmWallet = useCallback(async () => {
    try {
      const accounts = getWalletAccounts(dynamicClient);
      if (!accounts.some(isEvmWalletAccount) && isSignedIn(dynamicClient)) {
        await createWaasWalletAccounts({ chains: ["EVM"] }, dynamicClient);
      }
    } catch {}
    refresh();
  }, [refresh]);

  useEffect(() => {
    const handleOAuthRedirect = async () => {
      if (typeof window === "undefined") return;
      try {
        const url = new URL(window.location.href);
        const isOAuth = await detectOAuthRedirect({ url }, dynamicClient);
        if (isOAuth) {
          await completeSocialAuthentication({ url }, dynamicClient);
          await ensureEvmWallet();
          window.history.replaceState({}, "", window.location.pathname);
          return;
        }
      } catch {}
      refresh();
    };
    handleOAuthRedirect();
    const unsubWallets = onEvent({ event: "walletAccountsChanged", listener: () => ensureEvmWallet() }, dynamicClient);
    const unsubLogout = onEvent({ event: "logout", listener: () => { setEvmAccount(null); setLoggedIn(false); } }, dynamicClient);
    return () => { unsubWallets(); unsubLogout(); };
  }, [refresh, ensureEvmWallet]);

  return (
    <WalletContext.Provider value={{ evmAccount, loggedIn, chainId, setChainId, ensureEvmWallet, disconnect }}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </WalletContext.Provider>
  );
}

Configure networks and constants

The example uses a per-chain network config to support Base, Ethereum mainnet, Arbitrum, Optimism, and Polygon. Create src/lib/networks.ts:
src/lib/networks.ts
export interface NetworkConfig {
  chainId: number;
  name: string;
  displayName: string;
  contracts: {
    rewardsDistributor: string;
    morphoMarkets: string;
  };
  marketParams?: {
    loanToken: string;
    collateralToken: string;
    oracle: string;
    irm: string;
    lltv: bigint;
  };
  api?: {
    morphoGraphql: string;
  };
  decimals?: {
    morpho: number;
    weth: number;
  };
}

export const SUPPORTED_NETWORKS: Record<number, NetworkConfig> = {
  8453: { // Base
    chainId: 8453,
    name: "base",
    displayName: "Base",
    contracts: {
      rewardsDistributor: "0x3B14E5C73e0a56D607A8688098326fD4b4292135",
      morphoMarkets: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
    },
    marketParams: {
      loanToken: "0x4200000000000000000000000000000000000006",   // WETH
      collateralToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
      oracle: "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70",
      irm: "0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC",
      lltv: BigInt("850000000000000000"),
    },
    api: { morphoGraphql: "https://api.morpho.org/graphql" },
    decimals: { morpho: 18, weth: 18 },
  },
  // ...other networks (mainnet, arbitrum, optimism, polygon)
};

export const DEFAULT_NETWORK = 8453;

Set up contract ABIs

Create the ABI files in src/lib/ABIs/:
  • ERC20_ABI.ts — standard token interface (balanceOf, approve, allowance)
  • ERC4626_ABI.ts — vault interface (deposit, withdraw, balanceOf, convertToAssets)
  • REWARDS_ABI.ts — Morpho rewards distributor (getUserRewardBalance, getRewardToken)
  • MORPHO_MARKETS_ABI.ts — core Morpho markets contract
You can find these files in the GitHub repository.

Fetching vault data

Vault list

Create src/lib/hooks/useVaultsList.ts to load all available vaults. The hook reads the active chain from useWallet() and queries the Morpho API:
src/lib/hooks/useVaultsList.ts
import { useState, useEffect } from "react";
import { useWallet } from "@/lib/providers";
import { getApiForChain } from "../constants";

export function useVaultsList(sortBy = "netApy-desc") {
  const { chainId } = useWallet();
  const [vaults, setVaults] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    async function fetchVaults() {
      try {
        setLoading(true);
        const api = getApiForChain(chainId);
        if (!api?.morphoGraphql) throw new Error("API not available for this network");

        const res = await fetch(api.morphoGraphql, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            query: `query GetVaults($chainId: Int!) {
              vaults(where: {chainId_in: [$chainId]}) {
                items {
                  id address name symbol whitelisted
                  asset { address symbol decimals }
                  state { totalAssetsUsd apy netApy totalSupply sharePriceUsd rewards { asset { symbol } supplyApr yearlySupplyTokens } }
                }
              }
            }`,
            variables: { chainId },
          }),
        });

        const json = await res.json();
        const items = json?.data?.vaults?.items || [];
        // format and set vaults...
        setVaults(items);
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to fetch vaults");
      } finally {
        setLoading(false);
      }
    }
    fetchVaults();
  }, [chainId, sortBy]);

  return { vaults, loading, error };
}

Deposit and withdraw

Deposits require a two-step flow: the user must first approve the vault to spend their tokens, then deposit. Use viem directly with createWalletClientForWalletAccount:
src/lib/hooks/useVaultOperations.ts
import { parseUnits, createPublicClient, http } from "viem";
import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem";
import { base } from "viem/chains";
import { useWallet } from "@/lib/providers";
import { ERC20_ABI, ERC4626_ABI } from "../ABIs";

interface VaultInfo {
  address: string;
  asset: { address: string; symbol: string; decimals: number };
}

export function useVaultOperations(address: string | undefined, vaultInfo: VaultInfo | null) {
  const { evmAccount } = useWallet();

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

  const getWalletClient = () => {
    if (!evmAccount) return null;
    return createWalletClientForWalletAccount({ walletAccount: evmAccount, chain: base });
  };

  const handleDeposit = async (e: React.FormEvent) => {
    if (!vaultInfo?.address || !address) return;
    const walletClient = getWalletClient();
    if (!walletClient) return;

    e.preventDefault();
    const { request } = await publicClient.simulateContract({
      address: vaultInfo.address as `0x${string}`,
      abi: ERC4626_ABI,
      functionName: "deposit",
      args: [parseUnits(amount, vaultInfo.asset.decimals), address as `0x${string}`],
      account: address as `0x${string}`,
    });
    await walletClient.writeContract(request);
  };

  // handleApprove and handleWithdraw follow the same pattern...

  return { handleDeposit /*, handleApprove, handleWithdraw, needsApproval */ };
}

Enable transaction simulation

Dynamic’s embedded wallets include built-in transaction previews. To enable, go to Developer Settings → Embedded Wallets → Dynamic in the dashboard and toggle on Show Confirmation UI and Transaction Simulation. Users will see the exact assets being transferred, estimated fees, and the vault contract before confirming.

Run the app

npm run dev
Add http://localhost:3000 to your allowed origins in the Dynamic dashboard under Developer Settings → CORS Origins.

Full source code

GitHub repository →

Additional resources