What We’re Building

A React (Next.js) app that connects Dynamic’s MPC wallets to Morpho’s lending protocol, allowing users to:
  • Deposit assets into Morpho vaults
  • Track yields and positions
  • Claim MORPHO token rewards
  • Manage deposits and withdrawals
In this guide, you’ll learn how to integrate Morpho lending vaults with Dynamic’s MPC embedded wallets for seamless yield earning. If you want to take a quick look at the final code, check out the GitHub repository.

Key Components

  • Dynamic MPC Wallets - Embedded, non-custodial wallets with seamless auth
  • Dynamic Transaction Simulation - Built-in transaction preview showing asset transfers, fees, and counterparties before execution
  • Morpho GraphQL API - Fetching vaults, vault data, positions, etc.
  • Morpho Protocol - Lending protocol with peer-to-peer matching
  • ERC-4626 Vaults - Standardized deposit/withdrawal interface
  • Rewards System - MORPHO token distribution for liquidity providers

Building the Application

Project Setup

Start by creating a new Dynamic project with React, Viem, Wagmi, and Ethereum support:
terminal
npx create-dynamic-app@latest morpho-dynamic-app --framework nextjs --library viem --wagmi true --chains ethereum --pm bun
cd morpho-dynamic-app
This command will automatically set up:
  • Next.js app
  • Dynamic SDK with Ethereum wallet connectors
  • Wagmi for blockchain interactions
  • Viem for transaction handling
  • React Query for data fetching
  • All necessary dependencies and configurations

Configure Dynamic Environment

Create a .env.local file with your Dynamic environment ID:
.env.local
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=your-environment-id-here
You can find your Environment ID in the Dynamic dashboard under Developer Settings → SDK & API Keys.

Enable Transaction Simulation

Dynamic provides built-in transaction simulation that enhances user experience by showing detailed transaction previews before execution. This feature displays:
  • Asset Transfers - Clear breakdown of tokens being sent and received
  • Network Fees - Estimated gas costs and total transaction cost
  • Counterparties - Contract addresses and their verification status
  • Transaction Details - Complete transaction information for user review
To enable transaction simulation:
  1. Go to your Dynamic dashboard
  2. Navigate to Developer SettingsEmbedded WalletsDynamic
  3. Enable “Show Confirmation UI” and “Transaction Simulation” toggles
When enabled, users will see a comprehensive transaction preview like this when depositing or withdrawing from Morpho vaults:
This ensures users can review all transaction details, including the exact amount of assets being deposited/withdrawn, before confirming the transaction.

Set Up Contract ABIs

Create the necessary ABI files in src/lib/ABIs/:
  • ERC20_ABI.ts - Standard ERC20 token interface
  • ERC4626_ABI.ts - ERC-4626 vault interface for deposits/withdrawals
  • REWARDS_ABI.ts - Morpho rewards distributor interface
  • MORPHO_MARKETS_ABI.ts - Core Morpho markets contract interface
You can find the ABIs here.

Configure Constants

Set up your contract addresses and network configuration in src/lib/constants.ts:
constants.ts
export const CONTRACTS = {
  REWARDS_DISTRIBUTOR: "0x3B14E5C73e0a56D607A8688098326fD4b4292135",
  MORPHO_MARKETS: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
} as const;

export const MARKET_PARAMS = {
  loanToken: "0x4200000000000000000000000000000000000006",
  collateralToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  oracle: "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70",
  irm: "0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC",
  lltv: BigInt("850000000000000000"),
} as const;

export const NETWORK = {
  CHAIN_ID: 8453,
  NAME: "Base",
} as const;

export const API = {
  MORPHO_GRAPHQL: "https://api.morpho.org/graphql",
} as const;

export const DECIMALS = {
  MORPHO: 18,
  WETH: 18,
} as const;
This constants file centralizes all configuration including contract addresses, network settings, API endpoints, and token decimals. The as const assertion ensures type safety by making the objects immutable. Vault addresses and token addresses are fetched dynamically from the Morpho GraphQL API.

Create Custom Hooks

Build the custom hooks for data management. Start with useVaultsList.ts:
useVaultsList.ts
import { useState, useEffect } from "react";
import { API, NETWORK } from "../constants";

export interface Vault {
  id: string;
  address: string;
  name: string;
  symbol: string;
  asset: string;
  apy: string;
  netApy: string;
  tvl: string;
  description: string;
  whitelisted: boolean;
  totalSupply: string;
  sharePrice: string;
  rewards: Reward[];
}

export interface Reward {
  asset: string;
  supplyApr: string;
  yearlySupplyTokens: string;
}

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

  useEffect(() => {
    const fetchVaults = async () => {
      try {
        setLoading(true);
        const response = await fetch(API.MORPHO_GRAPHQL, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            query: `query GetVaults($chainId: Int!) {
              vaults(where: {chainId: $chainId}) {
                items {
                  id
                  address
                  name
                  symbol
                  whitelisted
                  asset {
                    address
                    symbol
                    decimals
                  }
                  state {
                    totalAssets
                    totalAssetsUsd
                    totalSupply
                    avgNetApy
                    allTimeApy
                    apy
                    netApy
                    netApyWithoutRewards
                    sharePrice
                    sharePriceUsd
                    rewards {
                      asset {
                        address
                        symbol
                      }
                      supplyApr
                      yearlySupplyTokens
                    }
                  }
                }
              }
            }`,
            variables: { chainId: NETWORK.CHAIN_ID },
          }),
        });

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();

        if (data.errors) {
          throw new Error(data.errors[0].message);
        }

        // Transform the API response to our Vault interface
        const transformedVaults = data.data.vaults.items.map((vault: any) => ({
          id: vault.id,
          address: vault.address,
          name: vault.name || `${vault.asset?.symbol || "Unknown"} Vault`,
          symbol: vault.symbol,
          asset: vault.asset?.symbol || "Unknown",
          apy: vault.state?.apy
            ? `${(vault.state.apy * 100).toFixed(2)}%`
            : "N/A",
          netApy: vault.state?.netApy
            ? `${(vault.state.netApy * 100).toFixed(2)}%`
            : "N/A",
          tvl: vault.state?.totalAssetsUsd
            ? `$${(vault.state.totalAssetsUsd / 1e6).toFixed(1)}M`
            : "N/A",
          description: `Earn yield on ${
            vault.asset?.symbol || "Unknown"
          } deposits`,
          whitelisted: vault.whitelisted || false,
          totalSupply: vault.state?.totalSupply
            ? (Number(vault.state.totalSupply) / 1e18).toLocaleString()
            : "N/A",
          sharePrice: vault.state?.sharePriceUsd
            ? `$${vault.state.sharePriceUsd.toFixed(6)}`
            : "N/A",
          rewards: (vault.state?.rewards || []).map((reward: any) => ({
            asset: reward.asset?.symbol || "Unknown",
            supplyApr: reward.supplyApr
              ? `${(reward.supplyApr * 100).toFixed(2)}%`
              : "N/A",
            yearlySupplyTokens: reward.yearlySupplyTokens || "N/A",
          })),
        }));

        setVaults(transformedVaults);
        setError(null);
      } catch (err) {
        setError(err instanceof Error ? err.message : "Failed to fetch vaults");
      } finally {
        setLoading(false);
      }
    };

    fetchVaults();
  }, [sortBy]);

  return { vaults, loading, error };
}
This hook makes a GraphQL request to the Morpho GraphQL API to fetch all vaults dynamically. Each vault includes its asset information (address, symbol, decimals) which is used throughout the application.

Build Vault Detail Hook

Create useVaultDetail.ts for fetching individual vault information:
useVaultDetail.ts
import { useState, useEffect } from "react";
import { API, NETWORK } from "../constants";

export interface VaultDetail {
  id: string;
  address: string;
  name: string;
  symbol: string;
  asset: string;
  assetAddress: string;
  assetDecimals: number;
  apy: string;
  netApy: string;
  tvl: string;
  totalAssets: string;
  totalAssetsUsd: number;
  description: string;
  whitelisted: boolean;
  totalSupply: string;
  sharePrice: string;
  rewards: Reward[];
}

export interface Reward {
  asset: string;
  supplyApr: string;
  yearlySupplyTokens: string;
}

export function useVaultDetail(vaultId: string) {
  const [vault, setVault] = useState<VaultDetail | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    const fetchVaultDetail = async () => {
      try {
        setLoading(true);
        const response = await fetch(API.MORPHO_GRAPHQL, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            query: `query GetVaultDetail($chainId: Int!, $vaultId: String!) {
              vaults(where: {chainId: $chainId, address: $vaultId}) {
                items {
                  id
                  address
                  name
                  symbol
                  whitelisted
                  asset {
                    address
                    symbol
                    decimals
                  }
                  state {
                    totalAssets
                    totalAssetsUsd
                    totalSupply
                    avgNetApy
                    allTimeApy
                    apy
                    netApy
                    netApyWithoutRewards
                    sharePrice
                    sharePriceUsd
                    rewards {
                      asset {
                        address
                        symbol
                      }
                      supplyApr
                      yearlySupplyTokens
                    }
                  }
                }
              }
            }`,
            variables: { chainId: NETWORK.CHAIN_ID, vaultId },
          }),
        });

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();

        if (data.errors) {
          throw new Error(data.errors[0].message);
        }

        if (data.data.vaults.items.length === 0) {
          throw new Error("Vault not found");
        }

        const vaultData = data.data.vaults.items[0];

        // Transform the API response to our VaultDetail interface
        const transformedVault: VaultDetail = {
          id: vaultData.id,
          address: vaultData.address,
          name:
            vaultData.name || `${vaultData.asset?.symbol || "Unknown"} Vault`,
          symbol: vaultData.symbol,
          asset: vaultData.asset?.symbol || "Unknown",
          assetAddress: vaultData.asset?.address || "",
          assetDecimals: vaultData.asset?.decimals || 18,
          apy: vaultData.state?.apy
            ? `${(vaultData.state.apy * 100).toFixed(2)}%`
            : "N/A",
          netApy: vaultData.state?.netApy
            ? `${(vaultData.state.netApy * 100).toFixed(2)}%`
            : "N/A",
          tvl: vaultData.state?.totalAssetsUsd
            ? `$${(vaultData.state.totalAssetsUsd / 1e6).toFixed(1)}M`
            : "N/A",
          totalAssets: vaultData.state?.totalAssets
            ? (Number(vaultData.state.totalAssets) / 1e18).toLocaleString()
            : "N/A",
          totalAssetsUsd: vaultData.state?.totalAssetsUsd || 0,
          description: `Earn yield on ${
            vaultData.asset?.symbol || "Unknown"
          } deposits`,
          whitelisted: vaultData.whitelisted || false,
          totalSupply: vaultData.state?.totalSupply
            ? (Number(vaultData.state.totalSupply) / 1e18).toLocaleString()
            : "N/A",
          sharePrice: vaultData.state?.sharePriceUsd
            ? `$${vaultData.state.sharePriceUsd.toFixed(6)}`
            : "N/A",
          rewards: (vaultData.state?.rewards || []).map((reward: any) => ({
            asset: reward.asset?.symbol || "Unknown",
            supplyApr: reward.supplyApr
              ? `${(reward.supplyApr * 100).toFixed(2)}%`
              : "N/A",
            yearlySupplyTokens: reward.yearlySupplyTokens || "N/A",
          })),
        };

        setVault(transformedVault);
        setError(null);
      } catch (err) {
        setError(
          err instanceof Error ? err.message : "Failed to fetch vault detail"
        );
      } finally {
        setLoading(false);
      }
    };

    if (vaultId) {
      fetchVaultDetail();
    }
  }, [vaultId]);

  return { vault, loading, error };
}
This hook fetches detailed information for a specific vault, including asset details, APY, TVL, and user position data. We use this in the vault detail page where user can deposit, withdraw, and see their position.

Build Vault Operations Hook

Create useVaultOperations.ts for deposit/withdraw functionality:
useVaultOperations.ts
import { useState } from "react";
import { parseUnits } from "viem";
import { useReadContract, useWriteContract } from "wagmi";
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 [amount, setAmount] = useState("");
  const [txStatus, setTxStatus] = useState("");

  // Read asset balance
  const { data: assetBalance } = useReadContract({
    address: vaultInfo?.asset.address as `0x${string}`,
    abi: ERC20_ABI,
    functionName: "balanceOf",
    args: address ? [address] : undefined,
    query: { enabled: !!address && !!vaultInfo?.asset.address },
  });

  // Read vault share balance
  const { data: vaultBalance } = useReadContract({
    address: vaultInfo?.address as `0x${string}`,
    abi: ERC4626_ABI,
    functionName: "balanceOf",
    args: address ? [address] : undefined,
    query: { enabled: !!address && !!vaultInfo?.address },
  });

  // Read user's deposited assets (convert shares to assets)
  const { data: depositedAssets } = useReadContract({
    address: vaultInfo?.address as `0x${string}`,
    abi: ERC4626_ABI,
    functionName: "convertToAssets",
    args: vaultBalance ? [vaultBalance] : undefined,
    query: { enabled: !!vaultInfo?.address && !!vaultBalance },
  });

  // Read allowance
  const { data: allowance } = useReadContract({
    address: vaultInfo?.asset.address as `0x${string}`,
    abi: ERC20_ABI,
    functionName: "allowance",
    args:
      address && vaultInfo?.asset.address && vaultInfo?.address
        ? [address, vaultInfo.address]
        : undefined,
    query: {
      enabled: !!address && !!vaultInfo?.asset.address && !!vaultInfo?.address,
    },
  });

  const { writeContract: writeApprove, isPending: isApprovePending } =
    useWriteContract();
  const { writeContract: writeDeposit, isPending: isDepositPending } =
    useWriteContract();
  const { writeContract: writeWithdraw, isPending: isWithdrawPending } =
    useWriteContract();

  const handleApprove = async () => {
    if (!vaultInfo?.asset.address || !vaultInfo?.address) return;

    try {
      await writeApprove({
        address: vaultInfo.asset.address as `0x${string}`,
        abi: ERC20_ABI,
        functionName: "approve",
        args: [vaultInfo.address, parseUnits(amount, vaultInfo.asset.decimals)],
      });
    } catch (error) {
      console.error("Approval failed:", error);
      throw error;
    }
  };

  const handleDeposit = async (amount: string) => {
    if (!vaultInfo?.address || !address) return;

    try {
      await writeDeposit({
        address: vaultInfo.address as `0x${string}`,
        abi: ERC4626_ABI,
        functionName: "deposit",
        args: [parseUnits(amount, vaultInfo.asset.decimals), address],
      });
    } catch (error) {
      console.error("Deposit failed:", error);
      throw error;
    }
  };

  const handleWithdraw = async (amount: string) => {
    if (!vaultInfo?.address || !address) return;

    try {
      await writeWithdraw({
        address: vaultInfo.address as `0x${string}`,
        abi: ERC4626_ABI,
        functionName: "withdraw",
        args: [parseUnits(amount, vaultInfo.asset.decimals), address, address],
      });
    } catch (error) {
      console.error("Withdraw failed:", error);
      throw error;
    }
  };

  const needsApproval =
    (allowance !== undefined &&
      vaultInfo?.asset.decimals &&
      parseUnits(amount || "0", vaultInfo.asset.decimals) >
        (allowance as bigint)) ||
    false;

  return {
    assetBalance: assetBalance
      ? formatUnits(assetBalance, vaultInfo?.asset.decimals || 18)
      : "0",
    vaultBalance: vaultBalance
      ? formatUnits(vaultBalance, vaultInfo?.asset.decimals || 18)
      : "0",
    allowance: allowance
      ? formatUnits(allowance, vaultInfo?.asset.decimals || 18)
      : "0",
    amount,
    setAmount,
    handleApprove,
    handleDeposit,
    handleWithdraw,
    isApprovePending,
    isDepositPending,
    isWithdrawPending,
    txStatus,
  };
}
This hook accepts a VaultInfo object that contains the dynamic vault and asset information. It uses the asset’s decimals from the vault data, making it work with any token type. The convertToAssets function is used to calculate the actual deposited assets from vault shares. It returns a couple of very important values and functions that we use in the vault detail page such as handleApprove, handleDeposit, handleWithdraw, allowance, assetBalance, and vaultBalance. When users interact with these functions, Dynamic’s transaction simulation will automatically show a detailed preview of the transaction, including the exact amount of assets being deposited or withdrawn, estimated gas fees, and the vault contract address they’re interacting with. This provides transparency and security for all DeFi operations.

Build Vault List Page

Create the main vault discovery page in src/app/earn/page.tsx:
page.tsx
"use client";

import { useAccount } from "wagmi";
import { DynamicWidget } from "@dynamic-labs/sdk-react-core";
import { useVaultsList } from "../../lib/hooks/useVaultsList";

export default function EarnPage() {
  const { isConnected } = useAccount();
  const { vaults, loading, error } = useVaultsList();

  if (!isConnected) {
    return (
      <div className="container mx-auto p-8">
        <h1 className="text-3xl font-bold mb-8">Earn with Morpho</h1>
        <p className="text-gray-600 mb-6">
          Connect your wallet to start earning competitive yields on your
          assets.
        </p>
        <DynamicWidget />
      </div>
    );
  }

  if (loading) {
    return (
      <div className="container mx-auto p-8">
        <div className="flex items-center justify-center">
          <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
        </div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="container mx-auto p-8">
        <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
          Error loading vaults: {error}
        </div>
      </div>
    );
  }

  return (
    <div className="container mx-auto p-8">
      <div className="flex justify-between items-center mb-8">
        <div>
          <h1 className="text-3xl font-bold">Available Vaults</h1>
          <p className="text-gray-600 mt-2">
            Choose from {vaults.length} optimized lending vaults
          </p>
        </div>
        <DynamicWidget />
      </div>

      <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
        {vaults.map((vault) => (
          <div
            key={vault.id}
            className="border rounded-lg p-6 shadow-md hover:shadow-lg transition-shadow"
          >
            <div className="flex items-center justify-between mb-4">
              <h3 className="text-xl font-semibold">{vault.name}</h3>
              {vault.whitelisted && (
                <span className="bg-green-100 text-green-800 text-xs px-2 py-1 rounded">
                  Whitelisted
                </span>
              )}
            </div>

            <p className="text-gray-600 mb-4 text-sm">{vault.description}</p>

            <div className="space-y-2 mb-4">
              <div className="flex justify-between">
                <span className="text-sm text-gray-500">Asset:</span>
                <span className="font-medium">{vault.asset}</span>
              </div>
              <div className="flex justify-between">
                <span className="text-sm text-gray-500">Net APY:</span>
                <span className="font-medium text-green-600">
                  {vault.netApy}
                </span>
              </div>
              <div className="flex justify-between">
                <span className="text-sm text-gray-500">TVL:</span>
                <span className="font-medium">{vault.tvl}</span>
              </div>
              <div className="flex justify-between">
                <span className="text-sm text-gray-500">Share Price:</span>
                <span className="font-medium">{vault.sharePrice}</span>
              </div>
            </div>

            <a
              href={`/earn/${vault.address}`}
              className="block text-center bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded transition-colors"
            >
              View Details
            </a>
          </div>
        ))}
      </div>
    </div>
  );
}
This page displays all available vaults dynamically, showing the asset type for each vault.
Morpho Vaults List Page

Create Vault Detail Page

Build the individual vault page in src/app/earn/[vaultId]/page.tsx:
[vaultId]/page.tsx
"use client";

import { useParams } from "next/navigation";
import { useState } from "react";
import { formatUnits } from "viem";
import { useVaultDetail } from "../../../lib/hooks/useVaultDetail";
import { useVaultOperations } from "../../../lib/hooks/useVaultOperations";
import { useAccount } from "wagmi";

export default function VaultDetailPage() {
  const params = useParams();
  const vaultId = params.vaultId as string;
  const { address } = useAccount();
  const [mode, setMode] = useState<"deposit" | "withdraw">("deposit");

  const { vault, loading, error } = useVaultDetail(vaultId);

  // Create vault info object for operations
  const vaultInfo = vault
    ? {
        address: vault.address,
        asset: {
          address: vault.assetAddress,
          symbol: vault.asset,
          decimals: vault.assetDecimals,
        },
      }
    : null;

  const {
    assetBalance,
    vaultBalance,
    depositedAssets,
    amount,
    setAmount,
    handleApprove,
    handleDeposit,
    handleWithdraw,
    isApproving,
    isDepositing,
    isWithdrawing,
    needsApproval,
  } = useVaultOperations(address, vaultInfo);

  const isLoading = isApproving || isDepositing || isWithdrawing;

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();

    if (!amount || parseFloat(amount) <= 0) {
      alert("Please enter a valid amount");
      return;
    }

    try {
      if (needsApproval) {
        await handleApprove();
      } else if (mode === "deposit") {
        await handleDeposit(e);
      } else {
        await handleWithdraw(e);
      }

      // Reset form on success
      setAmount("");
    } catch (error) {
      console.error("Transaction failed:", error);
      alert("Transaction failed. Please try again.");
    }
  };

  const setMaxAmount = () => {
    if (!vault) return;
    if (mode === "deposit") {
      setAmount(
        assetBalance
          ? formatUnits(assetBalance as bigint, vault.assetDecimals)
          : "0"
      );
    } else {
      setAmount(
        vaultBalance
          ? formatUnits(vaultBalance as bigint, vault.assetDecimals)
          : "0"
      );
    }
  };

  if (loading) {
    return (
      <div className="container mx-auto p-8">
        <div className="flex items-center justify-center">
          <div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
        </div>
      </div>
    );
  }

  if (error || !vault) {
    return (
      <div className="container mx-auto p-8">
        <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
          Error loading vault: {error || "Vault not found"}
        </div>
      </div>
    );
  }

  return (
    <div className="container mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">{vault.name}</h1>

      <div className="grid md:grid-cols-2 gap-8">
        <div className="bg-white/10 backdrop-blur-md border border-white/20 p-6 rounded-lg">
          <h2 className="text-xl font-semibold mb-4 text-white">
            Your Position
          </h2>
          <div className="space-y-3">
            <div className="flex justify-between">
              <span className="text-gray-300">Deposited Assets:</span>
              <span className="font-medium text-white">
                {depositedAssets
                  ? formatUnits(depositedAssets as bigint, vault.assetDecimals)
                  : "0"}{" "}
                {vault.asset}
              </span>
            </div>
            <div className="flex justify-between">
              <span className="text-gray-300">USD Value:</span>
              <span className="font-medium text-white">
                {depositedAssets && vault.sharePrice
                  ? `$${(
                      parseFloat(
                        formatUnits(
                          depositedAssets as bigint,
                          vault.assetDecimals
                        )
                      ) * parseFloat(vault.sharePrice.replace("$", ""))
                    ).toFixed(2)}`
                  : "$0.00"}
              </span>
            </div>
            <div className="flex justify-between">
              <span className="text-gray-300">{vault.asset} Balance:</span>
              <span className="font-medium text-white">
                {assetBalance
                  ? formatUnits(assetBalance as bigint, vault.assetDecimals)
                  : "0"}{" "}
                {vault.asset}
              </span>
            </div>
            <div className="flex justify-between">
              <span className="text-gray-300">Vault Shares:</span>
              <span className="font-medium text-white">
                {vaultBalance
                  ? formatUnits(vaultBalance as bigint, vault.assetDecimals)
                  : "0"}
              </span>
            </div>
          </div>
        </div>

        <div className="bg-white/10 backdrop-blur-md border border-white/20 p-6 rounded-lg">
          <h2 className="text-xl font-semibold mb-4 text-white">
            {mode === "deposit"
              ? `Deposit ${vault.asset}`
              : `Withdraw ${vault.asset}`}
          </h2>

          <div className="flex gap-2 mb-6">
            <button
              onClick={() => setMode("deposit")}
              className={`px-4 py-2 rounded transition-colors ${
                mode === "deposit"
                  ? "bg-blue-600 text-white"
                  : "bg-white/10 text-gray-300 hover:bg-white/20 border border-white/20"
              }`}
            >
              Deposit
            </button>
            <button
              onClick={() => setMode("withdraw")}
              className={`px-4 py-2 rounded transition-colors ${
                mode === "withdraw"
                  ? "bg-blue-600 text-white"
                  : "bg-white/10 text-gray-300 hover:bg-white/20 border border-white/20"
              }`}
            >
              Withdraw
            </button>
          </div>

          <form onSubmit={handleSubmit} className="space-y-4">
            <div>
              <div className="flex justify-between items-center mb-2">
                <label className="block text-sm font-medium text-gray-300">
                  Amount ({vault.asset})
                </label>
                <button
                  type="button"
                  onClick={setMaxAmount}
                  className="text-sm text-blue-400 hover:text-blue-300"
                >
                  Max:{" "}
                  {mode === "deposit"
                    ? assetBalance
                      ? formatUnits(assetBalance as bigint, vault.assetDecimals)
                      : "0"
                    : vaultBalance
                    ? formatUnits(vaultBalance as bigint, vault.assetDecimals)
                    : "0"}
                </button>
              </div>
              <input
                type="number"
                value={amount}
                onChange={(e) => setAmount(e.target.value)}
                placeholder="0.00"
                step="0.000001"
                min="0"
                className="w-full p-3 border border-white/20 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white/5 text-white placeholder:text-gray-400"
                disabled={isLoading}
              />
            </div>

            <button
              type="submit"
              disabled={isLoading || !amount || parseFloat(amount) <= 0}
              className="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 disabled:text-gray-400 text-white py-3 rounded-lg transition-colors"
            >
              {isLoading ? (
                <span className="flex items-center justify-center">
                  <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
                  Processing...
                </span>
              ) : needsApproval ? (
                `Approve ${amount} ${vault.asset}`
              ) : mode === "deposit" ? (
                `Deposit ${vault.asset}`
              ) : (
                `Withdraw ${vault.asset}`
              )}
            </button>
          </form>
        </div>
      </div>
    </div>
  );
}
This vault detail page dynamically displays the asset type and uses the vault’s asset information for all operations, making it work with any token type. The deposited assets are correctly calculated using the convertToAssets function from the ERC4626 standard.
Morpho Vault Detail Page

Add Rewards Integration

Create the rewards hook in src/lib/hooks/useRewardsData.ts:
useRewardsData.ts
import { useState, useEffect } from "react";
import { formatUnits } from "viem";
import { useReadContract } from "wagmi";
import { CONTRACTS, DECIMALS } from "../constants";
import { ERC20_ABI, REWARDS_ABI } from "../ABIs";

interface RewardsData {
  rewardBalance: string | null;
  rewardToken: string | null;
  rewardTokenSymbol: string | null;
}

export function useRewardsData(
  address: string | undefined,
  vaultAddress?: string
): RewardsData {
  const [rewardBalance, setRewardBalance] = useState<string | null>(null);
  const [rewardToken, setRewardToken] = useState<string | null>(null);
  const [rewardTokenSymbol, setRewardTokenSymbol] = useState<string | null>(
    null
  );

  // Read reward balance
  const { data: userRewardBalance } = useReadContract({
    address: CONTRACTS.REWARDS_DISTRIBUTOR as `0x${string}`,
    abi: REWARDS_ABI,
    functionName: "getUserRewardBalance",
    args:
      address && vaultAddress
        ? [address as `0x${string}`, vaultAddress as `0x${string}`]
        : undefined,
    query: { enabled: !!address && !!vaultAddress },
  });

  // Read reward token address
  const { data: rewardTokenAddress } = useReadContract({
    address: CONTRACTS.REWARDS_DISTRIBUTOR as `0x${string}`,
    abi: REWARDS_ABI,
    functionName: "getRewardToken",
    args: vaultAddress ? [vaultAddress as `0x${string}`] : undefined,
    query: { enabled: !!vaultAddress },
  });

  // Read reward token symbol
  const { data: rewardTokenSymbolData } = useReadContract({
    address: rewardTokenAddress as `0x${string}`,
    abi: ERC20_ABI,
    functionName: "symbol",
    query: { enabled: !!rewardTokenAddress },
  });

  // Update reward data when contract data changes
  useEffect(() => {
    if (userRewardBalance !== undefined) {
      const formattedBalance = formatUnits(
        userRewardBalance as bigint,
        DECIMALS.MORPHO
      );
      setRewardBalance(formattedBalance);
    }
    if (rewardTokenAddress && typeof rewardTokenAddress === "string") {
      setRewardToken(rewardTokenAddress);
    }
    if (rewardTokenSymbolData && typeof rewardTokenSymbolData === "string") {
      setRewardTokenSymbol(rewardTokenSymbolData);
    }
  }, [userRewardBalance, rewardTokenAddress, rewardTokenSymbolData]);

  return {
    rewardBalance,
    rewardToken,
    rewardTokenSymbol,
  };
}
This hook manages MORPHO token rewards for vault depositors. It reads reward balances, fetches reward token information, and provides a function to claim accumulated rewards from the rewards distributor contract.

Run the Application

Start the development server:
terminal
bun dev
This command starts the development server using the package manager (bun). The app will be accessible at localhost:3000 where you can test the complete vault integration. The application will be available at http://localhost:3000.

Configure CORS

Add your local development URL to the CORS origins in your Dynamic dashboard under Developer Settings > CORS Origins to allow the application to communicate with Dynamic’s backend.

Conclusion

If you want to take a look at the full source code, check out the GitHub repository.

Additional Resources