Skip to main content
This guide covers integrating Vanna’s lending pools — depositing assets to earn yield, reading pool metrics, and redeeming vTokens.

Overview

Vanna has four lending pools:
PoolAssetSupply to earn
LendingPoolXLMXLMInterest from XLM borrowers
LendingPoolUSDCUSDCInterest from USDC borrowers
LendingPoolAquariusUSDCAquarius USDCInterest from AQ USDC borrowers
LendingPoolSoroswapUSDCSoroswap USDCInterest from SO USDC borrowers
Depositors receive vTokens (vXLM, vUSDC, etc.) that appreciate in value as interest accrues. Redeem them at any time for the underlying asset.

Prerequisites

Set up the Stellar SDK and server connection — see Getting Started.

Step 1: Read Pool State

Before depositing, read the pool’s current metrics:
import { scValToNative, nativeToScVal } from '@stellar/stellar-sdk';

const POOL_XLM = 'CBA4E4ZMXUKCDTNT7LDKSO3LGNGKHRCE4GUVPSRCAKU3TKAONUY7SVOB';
const VXLM    = 'CCQAAPNBYF6I7PRM2NZ4NRDYZUVJJANMX3RZ4ZLMQH6Z5WAUL2MHU2RZ';
const WAD     = 10n ** 18n;

async function getPoolStats() {
  const [liquidityScVal, borrowsScVal] = await Promise.all([
    readContract(POOL_XLM, 'get_total_liquidity_in_pool'),
    readContract(POOL_XLM, 'get_borrows'),
  ]);

  const liquidityWad = liquidityScVal as bigint;
  const borrowsWad   = borrowsScVal as bigint;
  const totalAssets  = liquidityWad + borrowsWad;

  const utilizationRate = borrowsWad * WAD / totalAssets;
  // utilizationRate / WAD = 0.0 to 1.0

  // Get vToken exchange rate
  const vtokenSupplyScVal = await readContract(VXLM, 'total_supply');
  const vtokenSupply = vtokenSupplyScVal as bigint;

  const exchangeRate = totalAssets * WAD / vtokenSupply;
  // exchangeRate / WAD = underlying per vToken

  return {
    liquidityXlm: Number(liquidityWad) / 1e18,
    borrowsXlm:   Number(borrowsWad) / 1e18,
    utilization:  Number(utilizationRate) / 1e18, // 0.0 to 1.0
    exchangeRate: Number(exchangeRate) / 1e18,
  };
}

Step 2: Get the Borrow APY

Query the Rate Model for the current borrow rate:
const RATE_MODEL = 'CCJAUPCU6EIFQK6GTAAYLW3Y4YETJAAUPAGBPFGQ2OUJPSW3WWHUCL2Z';
const SECS_PER_YEAR = 31_556_952n;

async function getBorrowAPY(liquidityWad: bigint, borrowsWad: bigint): Promise<number> {
  const ratePerSecWad = await readContract(
    RATE_MODEL,
    'get_borrow_rate_per_sec',
    [
      nativeToScVal(liquidityWad, { type: 'u256' }),
      nativeToScVal(borrowsWad,   { type: 'u256' }),
    ],
  ) as bigint;

  // Annual rate = rate_per_sec * SECS_PER_YEAR (in WAD)
  const annualRateWad = ratePerSecWad * SECS_PER_YEAR;
  return Number(annualRateWad) / 1e18; // 0.05 = 5% APY
}
The supply APY is approximately borrow_APY × utilization_rate (interest earned by LPs is the fraction of borrow interest from deployed capital).

Step 3: Read a User’s vToken Balance

async function getUserVtokenBalance(userAddress: string): Promise<bigint> {
  const balance = await readContract(
    VXLM,
    'balance',
    [nativeToScVal(userAddress, { type: 'address' })],
  ) as bigint;
  return balance;  // in i128, same scale as underlying
}

// Compute underlying value
async function getRedemptionValue(
  vtokenBalance: bigint,
  vtokenSupply: bigint,
  totalAssets: bigint,
): Promise<bigint> {
  return (vtokenBalance * totalAssets) / vtokenSupply;
}

Step 4: Deposit (Supply Liquidity)

async function depositXlm(
  userAddress: string,
  amountXlm: number,  // human-readable (e.g., 10 for 10 XLM)
): Promise<string> {
  const WAD = 10n ** 18n;
  const amountWad = BigInt(Math.floor(amountXlm * 1e18));  // convert to WAD

  return await invokeContract(
    userAddress,
    POOL_XLM,
    'deposit_xlm',
    [
      nativeToScVal(userAddress, { type: 'address' }),
      nativeToScVal(amountWad,   { type: 'u256' }),
    ],
  );
}
The LendingPool transfers XLM from the caller’s account. Freighter will prompt the user to authorize the native XLM transfer during the signing step — no separate approve() needed for XLM. For USDC pools, the approve() pattern is required.

Step 5: Redeem vTokens (Withdraw)

async function redeemVxlm(
  userAddress: string,
  vtokensToRedeem: bigint,  // in i128 (from balance() call)
): Promise<string> {
  return await invokeContract(
    userAddress,
    POOL_XLM,
    'redeem_vxlm',
    [
      nativeToScVal(userAddress,     { type: 'address' }),
      nativeToScVal(vtokensToRedeem, { type: 'u256' }),
    ],
  );
}
To withdraw all: fetch the user’s vToken balance and pass it as vtokensToRedeem.

Step 6: Fetch Deposit/Withdrawal History

Use Mercury to fetch historical events:
async function getEarnHistory(userAddress: string) {
  // Fetch deposit events where topic2 = user address
  const events = await fetchContractEvents(POOL_XLM, {
    account: userAddress,
    limit: 50,
  });

  return events.map(e => {
    const name = decodeScVal(e.topic1) as string;
    const data = decodeScVal(e.data) as {
      lender: string;
      amount: bigint;
      timestamp: bigint;
      asset_symbol: string;
    };
    return {
      type: name === 'deposit_event' ? 'deposit' : 'withdraw',
      amount: Number(data.amount) / 1e18,
      timestamp: Number(data.timestamp),
      asset: data.asset_symbol,
      txHash: e.tx,
    };
  });
}

Full Pool Data Component (React)

import { useQuery } from '@tanstack/react-query';

function usePoolData(poolAddress: string, vtokenAddress: string) {
  return useQuery({
    queryKey: ['pool-data', poolAddress],
    queryFn: async () => {
      const [liquidity, borrows, vtokenSupply] = await Promise.all([
        readContract(poolAddress, 'get_total_liquidity_in_pool') as Promise<bigint>,
        readContract(poolAddress, 'get_borrows') as Promise<bigint>,
        readContract(vtokenAddress, 'total_supply') as Promise<bigint>,
      ]);

      const totalAssets = liquidity + borrows;
      const utilizationRate = borrows * WAD / totalAssets;
      const exchangeRate = vtokenSupply > 0n ? totalAssets * WAD / vtokenSupply : WAD;

      return { liquidity, borrows, totalAssets, utilizationRate, exchangeRate };
    },
    staleTime: 5_000,  // 5 seconds — one ledger
  });
}

Error Handling

ErrorCauseFix
InsufficientBalanceCaller doesn’t have enough XLM/USDCCheck balance before calling
InsufficientPoolBalancePool can’t cover withdrawalPool is fully utilized; wait for repayments
Simulation failsContract panicDecode the error code from simResult.error