Invest in a Tranche

Each PRISM vault is partitioned into three tranches with different risk profiles. Investors choose a tranche, deposit USDC, and receive tranche tokens (pPRIME, pCORE, or pALPHA) at the current NAV. Yield accrues by lifting NAV; credit losses absorb in reverse priority by lowering the junior tranche’s NAV first.

Tranches at a Glance

TrancheTokenTarget APYLoss orderCapital profile
PrimepPRIME5%Absorbs lastConservative — paid first, protected by junior capital
CorepCORE8%Absorbs secondBalanced — meaningful yield with a buffer below
AlphapALPHA15%Absorbs firstLevered — first-loss capital, highest yield potential
Choose by risk tolerance, not nominal yield. A 15% APY target on Alpha only realizes if the underlying credit performs; if it doesn’t, Alpha takes the entire loss before any other tranche is touched.

The Reserve Invariant

Before writing any code, internalize the one invariant that drives every deposit and withdrawal:
vault_usdc_reserve == prime.total_assets + core.total_assets + alpha.total_assets
Deposits push USDC into the reserve and credit the chosen tranche. Withdrawals pull USDC out and decrement that tranche. Yield events increase one or more tranche balances. Loss events move USDC out of the reserve into a loss bucket account so the invariant always holds. This means every deposit you make grows exactly one tranche’s total_assets, and your tranche tokens represent a proportional claim on that tranche’s slice of the reserve.

Pre-flight Checks

Before invoking deposit, verify four conditions client-side. The on-chain handler enforces all of them — your transaction reverts with a typed PrismError if any fails — but failing fast on the client gives a better UX:
RequirementHow to verify
Vault state is Activevault.state enum equals { active: {} }
Protocol not pausedconfig.paused === false
Tranche not wipedtranche.navPerShareQ > 0n (or tranche.totalSupply === 0n for first deposit)
User has enough USDCuserUsdcAta.amount >= amount
import { getConfigPda, getTranchePda, getVaultPda, TrancheKind } from 'prismprotocol-sdk';

const [config] = getConfigPda();
const [vault] = getVaultPda(0);
const [tranchePda] = getTranchePda(vault, TrancheKind.Prime);

const [configAcc, vaultAcc, trancheAcc] = await Promise.all([
  core.account.globalConfig.fetch(config),
  core.account.vault.fetch(vault),
  core.account.tranche.fetch(tranchePda),
]);

if (configAcc.paused) throw new Error('Protocol is paused');
if (!('active' in vaultAcc.state)) throw new Error('Vault is not Active');

const supply = BigInt(trancheAcc.totalSupply.toString());
const navQ = BigInt(trancheAcc.navPerShareQ.toString());
if (supply > 0n && navQ === 0n) {
  throw new Error('Tranche has been wiped — deposits blocked');
}

Full Deposit Example

import { BN } from '@coral-xyz/anchor';
import {
  ASSOCIATED_TOKEN_PROGRAM_ID,
  TOKEN_PROGRAM_ID,
  getAssociatedTokenAddress,
} from '@solana/spl-token';
import { SystemProgram } from '@solana/web3.js';
import {
  TrancheKind,
  USDC_MINT,
  buildPrograms,
  getConfigPda,
  getTrancheMintPda,
  getTranchePda,
  getVaultPda,
  getVaultReservePda,
} from 'prismprotocol-sdk';

const VAULT_ID = 0;
const KIND = TrancheKind.Prime;
const AMOUNT = new BN(1_000 * 1_000_000); // 1,000 USDC, 6 decimals

const { core } = buildPrograms(connection, signer);

const [config]        = getConfigPda();
const [vault]         = getVaultPda(VAULT_ID);
const [tranche]       = getTranchePda(vault, KIND);
const [trancheMint]   = getTrancheMintPda(vault, KIND);
const [vaultReserve]  = getVaultReservePda(vault);

const userUsdcAta    = await getAssociatedTokenAddress(USDC_MINT, signer.publicKey);
const userTrancheAta = await getAssociatedTokenAddress(trancheMint, signer.publicKey);

const sig = await core.methods
  .deposit(KIND, AMOUNT)
  .accounts({
    user: signer.publicKey,
    config,
    vault,
    tranche,
    trancheMint,
    userUsdcAta,
    vaultUsdcReserve: vaultReserve,
    userTrancheAta,
    tokenProgram: TOKEN_PROGRAM_ID,
    associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
    systemProgram: SystemProgram.programId,
  })
  .signers([signer])
  .rpc({ commitment: 'confirmed' });

console.log('deposit signature:', sig);
The init_if_needed constraint on the userTrancheAta account means a brand-new user — one who has never held this tranche — does not need a separate ATA-creation transaction. The deposit creates the ATA in the same instruction.

How Shares Are Computed

PRISM uses NAV-per-share accounting in Q64.64 fixed-point math. On deposit:
shares_minted = first deposit?      → usdc_amount             (1:1 at NAV = 1.0)
                otherwise            → usdc_amount × Q64_ONE / nav_per_share_q
The handler atomically:
  1. Validates pre-flight conditions (vault active, not paused, tranche not wiped).
  2. Computes shares_minted using the formula above.
  3. Transfers usdc_amount from your USDC ATA to the vault reserve.
  4. Mints shares_minted of the tranche token to your tranche ATA.
  5. Updates tranche.total_assets and tranche.total_supply.
  6. Recomputes tranche.nav_per_share_q from the updated assets and supply.
  7. Updates tranche.last_nav_update_ts to the current Solana clock.
NAV is unchanged after a deposit — the numerator and denominator increase proportionally. Yield events are what move NAV up; credit events are what move it down.

Computing Expected Shares Off-Chain

Use this when you want to show the user how many tranche tokens they will receive before signing:
import { Q64_ONE } from 'prismprotocol-sdk';

function expectedShares(usdcMicro: bigint, navQ: bigint, totalSupply: bigint): bigint {
  if (totalSupply === 0n) return usdcMicro;       // first deposit: 1:1
  if (navQ === 0n) throw new Error('Tranche wiped — deposits blocked');
  return (usdcMicro * Q64_ONE) / navQ;
}

const sharesOut = expectedShares(
  1_000_000_000n,                                  // 1,000 USDC
  BigInt(tranche.navPerShareQ.toString()),
  BigInt(tranche.totalSupply.toString()),
);

console.log(`You will receive ${sharesOut} ${TrancheKind[KIND]} micro-shares`);
The math is identical to the on-chain handler, so the displayed quantity matches the minted quantity exactly.

A Worked Example

StepTranche assetsSupplyNAVUser pPRIME
Pool empty00undefined0
User A deposits 1,000 USDC1,0001,0001.01,000
User B deposits 1,000 USDC2,0002,0001.01,000
Yield of 200 USDC accrues to Prime2,2002,0001.101,000 (still)
User C deposits 1,000 USDC at NAV 1.103,2002,909.09…1.10909.09
User A and B’s tokens are now worth more (NAV 1.10), but no new tokens were minted to them. User C entered at the higher NAV and got proportionally fewer tokens. NAV stays at 1.10 after C’s deposit.

Withdraw Flow

The mirror operation burns tranche tokens and pays out at current NAV:
const sharesAmount = new BN(1_000 * 1_000_000); // 1,000 micro-shares

await core.methods
  .withdraw(KIND, sharesAmount)
  .accounts({
    user: signer.publicKey,
    config,
    vault,
    tranche,
    trancheMint,
    vaultUsdcReserve: vaultReserve,
    userTrancheAta,
    userUsdcAta,
    tokenProgram: TOKEN_PROGRAM_ID,
  })
  .signers([signer])
  .rpc({ commitment: 'confirmed' });
Payout = shares × nav_per_share_q / Q64_ONE. After a credit event, NAV may be lower than at deposit — withdraw still settles at current NAV. After Alpha is wiped, an Alpha withdrawal returns 0 USDC; the burn still succeeds.

Computing Expected Payout

function expectedPayout(shares: bigint, navQ: bigint): bigint {
  return (shares * navQ) >> 64n;
}

const usdcOut = expectedPayout(
  1_000_000_000n,                                  // 1,000 micro-shares
  BigInt(tranche.navPerShareQ.toString()),
);

console.log(`You will receive ${Number(usdcOut) / 1_000_000} USDC`);

Insufficient Reserve

If the vault reserve does not have enough USDC to cover your withdrawal — for example, after capital has been disbursed to a borrower and not yet repaid — the transaction reverts with InsufficientLiquidity. Two recovery paths:
  1. Wait and retry. The reserve grows with every yield event and repayment.
  2. Exit through the AMM. Sell tranche tokens for USDC at market price (which may be at a discount or premium to NAV).
The PRISM frontend surfaces this fork automatically — when withdraw fails, an “AMM Exit” button appears.

Strategy Presets (Multi-Tranche Deposit)

Combine three deposits into a single transaction for one-click portfolio allocation:
import { Transaction } from '@solana/web3.js';

const PRESETS = {
  conservative: { prime: 70, core: 20, alpha: 10 },
  balanced:     { prime: 50, core: 30, alpha: 20 },
  aggressive:   { prime: 20, core: 30, alpha: 50 },
} as const;

async function applyPreset(
  preset: keyof typeof PRESETS,
  totalUsdcMicro: BN,
) {
  const allocation = PRESETS[preset];
  const total = totalUsdcMicro;

  const ixs = await Promise.all(
    [TrancheKind.Prime, TrancheKind.Core, TrancheKind.Alpha].map(async (kind) => {
      const pct = kind === TrancheKind.Prime ? allocation.prime
                : kind === TrancheKind.Core  ? allocation.core
                :                              allocation.alpha;
      const amount = total.muln(pct).divn(100);
      const [trancheMint] = getTrancheMintPda(vault, kind);
      const [tranchePda] = getTranchePda(vault, kind);
      const userTrancheAta = await getAssociatedTokenAddress(trancheMint, signer.publicKey);

      return core.methods
        .deposit(kind, amount)
        .accounts({
          user: signer.publicKey,
          config,
          vault,
          tranche: tranchePda,
          trancheMint,
          userUsdcAta,
          vaultUsdcReserve: vaultReserve,
          userTrancheAta,
          tokenProgram: TOKEN_PROGRAM_ID,
          associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
          systemProgram: SystemProgram.programId,
        })
        .instruction();
    }),
  );

  const tx = new Transaction().add(...ixs);
  return await provider.sendAndConfirm(tx);
}

await applyPreset('balanced', new BN(10_000 * 1_000_000));   // 10K USDC across 3 tranches
Three deposit instructions fit comfortably under Solana’s 1232-byte transaction limit. If you ever need more (e.g., depositing across multiple vaults in one tx), use Versioned Transactions with Address Lookup Tables.

Common Errors

The on-chain handler raises typed errors. Use the SDK’s decodeAnchorError helper to surface them in your UI:
ErrorMeaningWhat to do
VaultPausedProtocol is in emergency pauseWait for admin to unpause
VaultNotActiveVault is Defaulted or ResolvedCannot deposit — read-only
TrancheWipedNoDepositsAllowedNAV is 0 with non-zero supplyDeposit into a different tranche or wait for recovery
InsufficientLiquidity (withdraw)Reserve doesn’t cover the withdrawalWait or use AMM exit
Token error: 0x1User doesn’t have enough USDCCheck userUsdcAta.amount
Custom: BorrowerMismatchWrong signer for accrue_yieldUse the borrower account stored on the loan
import { decodeAnchorError } from 'prismprotocol-sdk';

try {
  await core.methods.deposit(KIND, AMOUNT).accounts({ /* ... */ }).rpc();
} catch (raw) {
  const decoded = decodeAnchorError(raw);
  switch (decoded?.error) {
    case 'VaultPaused':
      toast.warn('Protocol paused — try again later');
      break;
    case 'TrancheWipedNoDepositsAllowed':
      toast.error('This tranche was wiped by a credit event. Pick another tranche.');
      break;
    default:
      throw raw;
  }
}

What’s Next

Deployed Addresses

Reference for every program ID, PDA seed, and mint.

Test Suite

Run end-to-end deposit / yield / withdraw tests locally.