# Yield on Aave

Build a DeFi application that lets users supply USDC into Aave V3 lending pools and withdraw their yield — all through Openfort embedded wallets on Base.

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

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

<HoverCardLink
  title="View Sample Code"
  subtitle="GitHub Repository"
  description="Complete source code for the Aave DeFi application with frontend, backend, and smart contract integrations."
  href="https://github.com/openfort-xyz/recipes-hub/tree/main/aave"
  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/aave openfort-aave
cd openfort-aave
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**

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 environment variables at startup. If any required variable is missing, you'll see an error modal explaining exactly what's needed.
:::

### Understand the provider setup

The app nests Wagmi, React Query, Aave, and Openfort providers. The Openfort SDK integrates as a wagmi connector, so all wallet interactions go through standard wagmi hooks:

```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>
      <AaveProvider client={aaveClient}>
        <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>
      </AaveProvider>
    </OpenfortWagmiBridge>
  </WagmiProvider>
</QueryClientProvider>
```

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

### Explore the Aave supply flow

The core DeFi logic lives in the `useAaveOperations` hook. When a user clicks "Supply 0.1 USDC", the hook:

1. Passes an **execution-plan handler** to `useSupply` that routes each step of the plan to the wallet
2. Calls `supply()` keyed off the reserve id; the SDK figures out whether an approval (or permit) is needed first
3. Sends each step — approval/permit and the deposit — through the Openfort wallet

This recipe uses the Aave **v4** SDK (hub/spoke). The supply request takes a `reserve` id (no separate market/currency/chainId), and execution runs through a plan handler:

```typescript
// src/hooks/useAaveOperations.ts
const [supply] = useSupply((plan) => {
  switch (plan.__typename) {
    case "TransactionRequest":
      return sendTransaction(plan);
    case "Erc20Approval":
      // Approve Aave to spend USDC — by permit signature when supported, else a tx
      return plan.bySignature ? signTypedData(plan.bySignature) : sendTransaction(plan.byTransaction);
    case "PreContractActionRequired":
      return sendTransaction(plan.transaction);
  }
});

const result = await supply({
  reserve: usdcReserve.id,
  amount: { erc20: { value: bigDecimal(0.1) } }, // 0.1 USDC
  sender: evmAddress(walletClient.account.address),
});
```

The plan handler transparently covers both first-time users (who need an approval or permit) and returning users (who can supply directly). `sendTransaction`/`signTypedData` come from `@aave/react/viem`.

### 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 pool** to deposit into Aave
3. Watch your supply balance and APY update in real-time
4. Click **Withdraw all from pool** to redeem your USDC plus earned yield

::::

## How it works

The app reads on-chain data from two sources and writes transactions through the Openfort embedded wallet:

**USDC wallet balance** is read using wagmi's `useReadContract` to call `balanceOf` on the Base USDC contract (`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`):

```typescript
// src/App.tsx
const { data: usdcBalance, refetch: refetchUsdcBalance } = useReadContract({
  address: USDC_CONTRACT_ADDRESS,
  abi: usdcAbi,
  functionName: 'balanceOf',
  args: address ? [address] : undefined,
  query: { enabled: !!address },
});
```

**Aave supply position and APY** are fetched via the `@aave/client` SDK, which queries Aave V3 markets and the user's supply positions:

```typescript
// src/hooks/useAaveSupplies.ts
const result = await fetchUserSupplies(aaveClient, {
  markets: markets.map((market) => ({
    chainId: market.chain.chainId,
    address: market.address,
  })),
  user,
});
```

**Withdrawals** use the same plan pattern as supply but with `{ max: true }` to redeem the full position:

```typescript
// src/hooks/useAaveOperations.ts
const withdrawResult = await withdraw({
  market: evmAddress(usdcReserve.marketAddress),
  amount: {
    erc20: {
      currency: evmAddress(usdcReserve.currencyAddress),
      value: { max: true },
    },
  },
  sender: evmAddress(walletClient.account.address),
  chainId: usdcReserve.chainId,
});
```
