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 feeThe 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
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.