NestDevelopers
Borrow nUSD

Calculate accrued stability fees

Calculate projected debt from the vault timestamp and the current on-chain stability-fee APR.

The vault only stores fees checkpointed by a previous mint, repay, explicit fee accrual, or withdrawal. To show current debt, add the fee accumulated since lastAccrualTs.

stored debt = principalDebt + accruedFee
pending fee = stored debt × APR bps × elapsed seconds / (10,000 × 31,536,000)
projected debt = stored debt + pending fee

The contract floors division, but a positive accrual smaller than one nUSD base unit becomes one base unit. The fee is calculated on stored total debt, including previously accrued fees.

On-chain query

read-current-debt.ts
import { PublicKey } from "@solana/web3.js";
import {
  borrowVaultPda,
  marketBySymbol,
  type NestClient,
} from "../nest-client";

const BPS = 10_000n;
const SECONDS_PER_YEAR = 31_536_000n;

export function projectStabilityFee(input: {
  principalDebtNusd6: bigint;
  accruedFeeNusd6: bigint;
  lastAccrualTs: bigint;
  stabilityFeeAprBps: bigint;
  nowTs: bigint;
}) {
  const storedDebtNusd6 = input.principalDebtNusd6 + input.accruedFeeNusd6;
  if (
    storedDebtNusd6 === 0n
    || input.stabilityFeeAprBps === 0n
    || input.nowTs <= input.lastAccrualTs
  ) {
    return { storedDebtNusd6, pendingFeeNusd6: 0n, projectedDebtNusd6: storedDebtNusd6 };
  }

  const elapsed = input.nowTs - input.lastAccrualTs;
  const numerator = storedDebtNusd6 * input.stabilityFeeAprBps * elapsed;
  let pendingFeeNusd6 = numerator / (BPS * SECONDS_PER_YEAR);
  if (pendingFeeNusd6 === 0n && numerator > 0n) pendingFeeNusd6 = 1n;

  return {
    storedDebtNusd6,
    pendingFeeNusd6,
    projectedDebtNusd6: storedDebtNusd6 + pendingFeeNusd6,
  };
}

async function confirmedChainTime(client: NestClient) {
  const slot = await client.connection.getSlot("confirmed");
  const timestamp = await client.connection.getBlockTime(slot);
  if (timestamp === null) throw new Error(`No block time available for slot ${slot}`);
  return BigInt(timestamp);
}

export async function readCurrentDebt(client: NestClient, symbol: string) {
  const market = marketBySymbol(client, symbol);
  const coreProgramId = new PublicKey(client.deployment.programs.nestCore);
  const protocolKey = new PublicKey(client.deployment.accounts.protocol);
  const configKey = new PublicKey(market.collateralConfig);
  const vaultKey = borrowVaultPda(coreProgramId, client.wallet.publicKey, configKey);

  const [protocol, vault, nowTs] = await Promise.all([
    client.core.account.protocol.fetch(protocolKey),
    client.core.account.vault.fetch(vaultKey),
    confirmedChainTime(client),
  ]);

  const projected = projectStabilityFee({
    principalDebtNusd6: BigInt(vault.principalDebt.toString()),
    accruedFeeNusd6: BigInt(vault.accruedFee.toString()),
    lastAccrualTs: BigInt(vault.lastAccrualTs.toString()),
    stabilityFeeAprBps: BigInt(protocol.stabilityFeeAprBps.toString()),
    nowTs,
  });

  return {
    vault: vaultKey,
    principalDebtNusd6: BigInt(vault.principalDebt.toString()),
    checkpointedFeeNusd6: BigInt(vault.accruedFee.toString()),
    stabilityFeeAprBps: BigInt(protocol.stabilityFeeAprBps.toString()),
    ...projected,
  };
}

Projected and settled debt

This query is a preview. The transaction's Clock timestamp is authoritative, so the final fee can be slightly higher when the transaction executes. For a full repayment, pass an amount above the projection; the contract transfers only the amount actually owed and leaves the excess nUSD in the wallet.

Do not hardcode the APR

Read stabilityFeeAprBps from the on-chain Protocol account. Governance can update configuration, and an old frontend constant will produce the wrong debt.

On this page