A comprehensive technical guide for integrating Morpho lending vaults with Dynamic's MPC embedded wallets.
The lending flow consists of several modular components:
- Dynamic MPC Wallet - Provides embedded, non-custodial wallets
- Morpho Protocol - Decentralized lending protocol with optimized yields
- Morpho GraphQL API - Real-time vault data and user positions
- Custom Hooks - Encapsulated business logic for data fetching and operations
- UI Components - Reusable, focused components for different interface sections
- Constants & Configuration - Centralized configuration and styling
bun install
Key dependencies:
{
"@dynamic-labs/sdk-react-core": "^4.25.3",
"wagmi": "^2.16.0",
"viem": "^2.33.0",
"next": "15.4.1"
}
Key imports:
import { formatUnits, parseUnits } from "viem";
import { useAccount, useReadContract, useWriteContract } from "wagmi";
import { DynamicWidget } from "@dynamic-labs/sdk-react-core";
Users authenticate and receive an embedded wallet through Dynamic's MPC system:
import { DynamicWidget } from "@dynamic-labs/sdk-react-core";
<DynamicWidget />;
The wallet provides:
- Non-custodial security - Private keys split via MPC
- Seamless UX - No browser extensions required
- Balance tracking - Real-time token balances
The codebase follows a modular architecture:
src/
├── app/
│ ├── borrow/
│ │ └── page.tsx # Borrow page
│ ├── earn/
│ │ ├── page.tsx # Earn page (vaults list)
│ │ └── [vaultId]/
│ │ └── page.tsx # Individual vault page
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ └── globals.css # Global styles
├── components/
│ ├── Button.tsx
│ ├── Input.tsx
│ ├── MarketsBalanceDisplay.tsx
│ ├── MarketsForm.tsx
│ ├── MarketsModeSelector.tsx
│ ├── Navigation.tsx
│ └── index.ts
└── lib/
├── ABIs/
│ ├── ERC20_ABI.ts
│ ├── ERC4626_ABI.ts
│ ├── MORPHO_MARKETS_ABI.ts
│ ├── REWARDS_ABI.ts
│ └── index.ts
├── constants.ts # Contract addresses and configuration
├── hooks/
│ ├── useMarketsData.ts
│ ├── useMarketsOperations.ts
│ ├── useRewardsOperations.ts
│ ├── useVaultDetail.ts
│ ├── useVaultOperations.ts
│ ├── useVaultsList.ts
│ └── index.ts
├── providers.tsx
├── utils.ts
└── wagmi.ts
All contract addresses and configuration are centralized in src/lib/constants.ts
:
export const CONTRACTS = {
REWARDS_DISTRIBUTOR: "0x3B14E5C73e0a56D607A8688098326fD4b4292135",
MORPHO_MARKETS: "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb",
} as const;
export const MARKET_PARAMS = {
loanToken: "0x4200000000000000000000000000000000000006", // WETH
collateralToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC
oracle: "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70",
irm: "0x870aC11D48B15DB9a138Cf899d20F13F79Ba00BC",
lltv: BigInt("850000000000000000"), // 85%
} as const;
export const NETWORK = {
CHAIN_ID: 8453,
NAME: "Base",
} as const;
export const API = {
MORPHO_GRAPHQL: "https://api.morpho.org/graphql",
} as const;
The application uses custom hooks to encapsulate business logic and state management:
Fetches and manages markets data from the Morpho GraphQL API:
const marketsData = useMarketsData(address);
// Returns: { userAssets, userAssetsUsd, marketName, marketSymbol }
Manages vaults list data:
const vaultsList = useVaultsList();
// Returns: { vaults, loading, error }
Fetches detailed vault information:
const vaultDetail = useVaultDetail(vaultId);
// Returns: { vault, loading, error }
Handles all market operations (deposit, withdraw, approve):
const marketOps = useMarketsOperations(address);
// Returns: { amount, setAmount, handleDeposit, handleWithdraw, handleApprove, ... }
Handles vault-specific operations:
const vaultOps = useVaultOperations(address);
// Returns: { handleDeposit, handleWithdraw, handleApprove, ... }
Manages reward claiming operations:
const rewardsOps = useRewardsOperations();
// Returns: { handleClaimReward, isClaiming, claimError, ... }
Get the user's current position in the market:
const fetchUserPosition = async (userAddress: string) => {
try {
const res = await fetch("https://api.morpho.org/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `query GetAllUserPositions($chainId: Int!, $userAddress: String!) {
vaultPositions(where: {chainId_in: [$chainId], shares_gte: 0, userAddress_in: [$userAddress]}) {
items {
state {
assets
assetsUsd
}
vault {
address
name
symbol
}
}
}
}`,
variables: {
chainId: 8453, // Base chain ID
userAddress: userAddress,
},
}),
});
const json = await res.json();
const items = json?.data?.vaultPositions?.items;
const vaultItem = items?.find(
(item) =>
item.vault?.address?.toLowerCase() ===
MORPHO_MARKETS_ADDRESS.toLowerCase()
);
if (vaultItem && vaultItem.state) {
return {
assets: vaultItem.state.assets,
assetsUsd: vaultItem.state.assetsUsd,
vaultName: vaultItem.vault?.name,
vaultSymbol: vaultItem.vault?.symbol,
};
}
} catch (error) {
console.error("Failed to fetch user position:", error);
}
return null;
};
const { address, isConnected } = useAccount();
// Read USDC balance
const { data: usdcBalance } = useReadContract({
address: MARKET_PARAMS.collateralToken as `0x${string}`,
abi: ERC20_ABI,
functionName: "balanceOf",
args: address ? [address] : undefined,
query: { enabled: !!address },
});
// Read vault share balance
const { data: vaultBalance } = useReadContract({
address: CONTRACTS.MORPHO_MARKETS as `0x${string}`,
abi: ERC4626_ABI,
functionName: "balanceOf",
args: address ? [address] : undefined,
query: { enabled: !!address },
});
// Read allowance
const { data: allowance } = useReadContract({
address: MARKET_PARAMS.collateralToken as `0x${string}`,
abi: ERC20_ABI,
functionName: "allowance",
args: address ? [address, CONTRACTS.MORPHO_MARKETS] : undefined,
query: { enabled: !!address },
});
Before depositing, users must approve the vault to spend their tokens:
const {
writeContract: writeApprove,
isPending: isApproving,
error: approveError,
} = useWriteContract();
const handleApprove = async (amount: string) => {
try {
await writeApprove({
address: MARKET_PARAMS.collateralToken as `0x${string}`,
abi: ERC20_ABI,
functionName: "approve",
args: [CONTRACTS.MORPHO_MARKETS, parseUnits(amount, 6)], // USDC has 6 decimals
});
} catch (error) {
console.error("Approval failed:", error);
}
};
// Check if approval is needed
const needsApproval = (amount: string) => {
const parsedAmount = parseUnits(amount || "0", 6);
return allowance !== undefined && parsedAmount > (allowance as bigint);
};
Deposit assets into the Morpho market:
const {
writeContract: writeDeposit,
isPending: isDepositing,
error: depositError,
} = useWriteContract();
const handleDeposit = async (amount: string) => {
try {
await writeDeposit({
address: CONTRACTS.MORPHO_MARKETS as `0x${string}`,
abi: ERC4626_ABI,
functionName: "deposit",
args: [parseUnits(amount, 6), address], // assets, receiver
});
} catch (error) {
console.error("Deposit failed:", error);
}
};
Withdraw assets from the Morpho market:
const {
writeContract: writeWithdraw,
isPending: isWithdrawing,
error: withdrawError,
} = useWriteContract();
const handleWithdraw = async (amount: string) => {
try {
await writeWithdraw({
address: CONTRACTS.MORPHO_MARKETS as `0x${string}`,
abi: ERC4626_ABI,
functionName: "withdraw",
args: [
parseUnits(amount, 6), // assets
address, // receiver
address, // owner
],
});
} catch (error) {
console.error("Withdraw failed:", error);
}
};
Morpho markets distribute MORPHO tokens as rewards to users who provide liquidity. This integration includes:
const REWARDS_DISTRIBUTOR_ADDRESS = CONTRACTS.REWARDS_DISTRIBUTOR;
// Read user's reward balance
const { data: userRewardBalance } = useReadContract({
address: CONTRACTS.REWARDS_DISTRIBUTOR as `0x${string}`,
abi: REWARDS_ABI,
functionName: "getUserRewardBalance",
args: address ? [address, CONTRACTS.MORPHO_MARKETS] : undefined,
query: { enabled: !!address },
});
// Read reward token information
const { data: rewardTokenAddress } = useReadContract({
address: CONTRACTS.REWARDS_DISTRIBUTOR as `0x${string}`,
abi: REWARDS_ABI,
functionName: "getRewardToken",
args: [CONTRACTS.MORPHO_MARKETS],
});
const { data: rewardTokenSymbol } = useReadContract({
address: rewardTokenAddress as `0x${string}`,
abi: ERC20_ABI,
functionName: "symbol",
query: { enabled: !!rewardTokenAddress },
});
const {
writeContract: writeClaimReward,
isPending: isClaiming,
error: claimError,
} = useWriteContract();
const handleClaimReward = async () => {
try {
await writeClaimReward({
address: CONTRACTS.REWARDS_DISTRIBUTOR as `0x${string}`,
abi: REWARDS_ABI,
functionName: "claimReward",
args: [CONTRACTS.MORPHO_MARKETS],
});
} catch (error) {
console.error("Claim failed:", error);
}
};
The interface displays:
- Current reward balance in MORPHO tokens
- Claim button (only shown when rewards > 0)
- Real-time updates after successful claims
Base network (Chain ID: 8453):
- WETH Token:
0x4200000000000000000000000000000000000006
- USDC Token:
0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
- Morpho Markets:
0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb
- Rewards Distributor:
0x3B14E5C73e0a56D607A8688098326fD4b4292135
- Morpho GraphQL API:
https://api.morpho.org/graphql
- User connects via Dynamic MPC wallet
- Navigate to earn or borrow page
- Select deposit or withdraw mode
- Enter the amount to deposit/withdraw
- Check and handle token approvals (deposit only)
- Execute market transaction via ERC-4626 interface
- Display transaction status and updated balances
- Query GraphQL API for real-time position data
First, run the development server:
bun dev
Open http://localhost:3000 with your browser to see the result.
- ERC-4626 Compliance - Standard vault interface for deposits/withdrawals
- Real-time Data - GraphQL API integration for live market metrics
- Optimized Yields - Morpho's lending optimization algorithms
- Non-custodial - Dynamic MPC wallets maintain user control
- Multi-page Navigation - Separate earn and borrow interfaces
- Modular Components - Reusable UI components for different sections