# Yield on Morpho

Build a web application that lets users supply USDC to a Morpho Blue vault on Base and withdraw with earned yield — all through Openfort embedded wallets.

:::tip
You'll build a web app where users authenticate with Openfort, view their USDC wallet balance and Morpho vault position with live APY, and deposit or withdraw USDC from the vault — with optional gas sponsorship.
:::

![Morpho recipe interface](https://www.openfort.io/images/blog/morph_d715e21926.png?updated_at=2025-09-19T12:00:10.277Z)

<HoverCardLink
  title="View Sample Code"
  subtitle="GitHub Repository"
  description="Complete source code for the Morpho Blue integration with frontend, backend, and vault interaction logic."
  href="https://github.com/openfort-xyz/recipes-hub/tree/main/morpho"
  img={{
  src: "/img/icons/github-icon.svg",
  alt: "GitHub Icon",
  className: "rounded-none",
}}
  color="#333"
  external
/>

## Getting started

::::steps

### Set up your project

Clone the recipe and install dependencies:

```bash
pnpx gitpick openfort-xyz/recipes-hub/tree/main/morpho openfort-morpho
cd openfort-morpho
pnpm install
```

### Configure your Openfort credentials

This recipe uses **passkey** wallet recovery (client-side WebAuthn), so it needs no backend — only your Shield publishable key. Get your credentials from the [Openfort Dashboard](https://dashboard.openfort.io):

1. Navigate to **Developers** → **API Keys** and copy your **Publishable Key** (starts with `pk_`)
2. Copy your **Shield Public Key** from the same page
3. Optionally, go to **gas sponsorships** → create a new gas sponsorship → copy the **gas sponsorship ID**
4. Optionally, get a [WalletConnect Project ID](https://cloud.reown.com/)

Create the environment file and fill in your values:

```bash
cp .env.example .env
```

```bash
VITE_OPENFORT_PUBLISHABLE_KEY=pk_...
VITE_OPENFORT_SHIELD_PUBLISHABLE_KEY=...
VITE_OPENFORT_FEE_SPONSORSHIP_ID=pol_...           # Optional — enables gas sponsorship
VITE_WALLET_CONNECT_PROJECT_ID=...               # Optional — defaults to "demo"
```

:::tip
The app validates required environment variables at startup. If any are missing, you'll see an error modal explaining exactly what's needed.
:::

### Understand the provider setup

The app configures Wagmi, React Query, and Openfort providers. The Openfort SDK integrates as a wagmi connector for the Base chain:

```tsx
// src/Providers.tsx
const config = createConfig(
  getDefaultConfig({
    appName: "Openfort Wallet App",
    walletConnectProjectId: import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID || "demo",
    chains: [base],
    ssr: false,
  })
);

<QueryClientProvider client={queryClient}>
  <WagmiProvider config={config}>
    <OpenfortWagmiBridge>
      <OpenfortProvider
        publishableKey={import.meta.env.VITE_OPENFORT_PUBLISHABLE_KEY}
        walletConfig={{
          shieldPublishableKey: import.meta.env.VITE_OPENFORT_SHIELD_PUBLISHABLE_KEY,
          ethereum: {
            ethereumFeeSponsorshipId: import.meta.env.VITE_OPENFORT_FEE_SPONSORSHIP_ID || undefined,
          },
        }}
        uiConfig={{
          walletRecovery: { defaultMethod: RecoveryMethod.PASSKEY },
        }}
      >
        {children}
      </OpenfortProvider>
    </OpenfortWagmiBridge>
  </WagmiProvider>
</QueryClientProvider>
```

The `getDefaultConfig` from `@openfort/react` creates a wagmi config with the Openfort embedded wallet as a connector. This means you use standard wagmi hooks like `useAccount()` and `useWalletClient()` — Openfort handles signing behind the scenes.

### Explore the vault deposit flow

The core vault logic lives in the `useVaultOperations` hook. When a user supplies USDC to the Morpho vault, two transactions are needed: an ERC-20 approval followed by a vault deposit.

```typescript
// src/hooks/useVaultOperations.ts
const VAULT_ADDRESS = "0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183";
const supplyAmount = 100000n; // 0.1 USDC (6 decimals)

// Step 1: Approve the vault to spend USDC
const approveHash = await walletClient.writeContract({
  address: USDC_CONTRACT_ADDRESS,
  abi: usdcAbi,
  functionName: 'approve',
  args: [VAULT_ADDRESS, supplyAmount],
});
await waitForTransaction(approveHash);

// Step 2: Deposit USDC into the Morpho vault
const depositHash = await walletClient.writeContract({
  address: VAULT_ADDRESS,
  abi: MINIMAL_VAULT_ABI,
  functionName: 'deposit',
  args: [supplyAmount, address],
});
await waitForTransaction(depositHash);
```

The Morpho vault follows the ERC-4626 tokenized vault standard — `deposit` accepts an asset amount and mints vault shares to the receiver. The approval step authorizes the vault contract to pull USDC from the user's wallet.

### Run the application

```bash
pnpm dev
```

Open [http://localhost:5173](http://localhost:5173). Click **Connect Wallet** to authenticate with Openfort, then:

1. Fund your embedded wallet with USDC on Base
2. Click **Supply 0.1 USDC** to deposit into the Morpho vault
3. Watch your vault balance and APY update
4. Click **Withdraw all** to redeem your shares for USDC plus earned yield

::::

## How it works

The app interacts with two contracts on Base: the USDC token (`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`) and the Morpho Blue USDC vault (`0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183`).

**Vault balance** is read in two steps — first getting the user's share balance, then converting shares to the underlying USDC amount:

```typescript
// src/hooks/useVaultOperations.ts
const userShares = await viemClient.readContract({
  address: VAULT_ADDRESS,
  abi: MINIMAL_VAULT_ABI,
  functionName: 'balanceOf',
  args: [address],
});

const underlyingAmount = await viemClient.readContract({
  address: VAULT_ADDRESS,
  abi: MINIMAL_VAULT_ABI,
  functionName: 'convertToAssets',
  args: [userShares],
});
```

**Vault APY** is fetched from Morpho's public GraphQL API, providing real-time yield data:

```typescript
// src/hooks/useVaultApy.ts
const GET_VAULT_APY = gql`
  query VaultApy($vaultAddress: String!, $chainId: Int!) {
    vaultByAddress(address: $vaultAddress, chainId: $chainId) {
      state { netApy }
    }
  }
`;

const client = new GraphQLClient("https://api.morpho.org/graphql");
const data = await client.request(GET_VAULT_APY, {
  vaultAddress: VAULT_ADDRESS,
  chainId: chainId,
});
const apyPercent = (Number(data.vaultByAddress.state.netApy) * 100).toFixed(2);
```

**Withdrawals** redeem all vault shares for the underlying USDC. The hook reads the user's current share balance and calls `redeem`:

```typescript
// src/hooks/useVaultOperations.ts
const userShares = await viemClient.readContract({
  address: VAULT_ADDRESS,
  abi: MINIMAL_VAULT_ABI,
  functionName: 'balanceOf',
  args: [address],
});

const redeemHash = await walletClient.writeContract({
  address: VAULT_ADDRESS,
  abi: MINIMAL_VAULT_ABI,
  functionName: 'redeem',
  args: [userShares, address, address],
  gas: 500000n,
});
```

After each transaction, the app polls both the wallet and vault balances until a change is detected, ensuring the UI stays in sync.
