Borrow nUSD
Calculate position health
Calculate collateral value, LTV, health factor, mint capacity, withdrawal capacity, and liquidation price.
Nest values collateral using the oracle price minus its confidence interval. All risk calculations must use this conservative price.
safe price = max(priceE8 - confidenceE8, 0)
collateral value = collateral raw × safe price × 10^6 / (10^decimals × 10^8)Calculation module
const BPS = 10_000n;
const USD_SCALE = 1_000_000n;
const PRICE_SCALE = 100_000_000n;
export function ceilDiv(numerator: bigint, denominator: bigint) {
if (denominator <= 0n) throw new Error("denominator must be positive");
return (numerator + denominator - 1n) / denominator;
}
export function conservativeCollateralValueUsd6(input: {
collateralRaw: bigint;
collateralDecimals: number;
priceE8: bigint;
confidenceE8: bigint;
}) {
const safePriceE8 = input.priceE8 > input.confidenceE8
? input.priceE8 - input.confidenceE8
: 0n;
const tokenScale = 10n ** BigInt(input.collateralDecimals);
const valueUsd6 = input.collateralRaw * safePriceE8 * USD_SCALE
/ (tokenScale * PRICE_SCALE);
return { safePriceE8, valueUsd6 };
}
export function positionRisk(input: {
collateralValueUsd6: bigint;
projectedDebtNusd6: bigint;
borrowLtvBps: bigint;
liquidationThresholdBps: bigint;
}) {
const debt = input.projectedDebtNusd6;
const borrowLimitNusd6 = input.collateralValueUsd6 * input.borrowLtvBps / BPS;
const liquidationLimitNusd6 = input.collateralValueUsd6
* input.liquidationThresholdBps / BPS;
const currentLtvBps = input.collateralValueUsd6 === 0n
? null
: debt * BPS / input.collateralValueUsd6;
const healthFactorBps = debt === 0n
? null
: liquidationLimitNusd6 * BPS / debt;
return {
borrowLimitNusd6,
liquidationLimitNusd6,
currentLtvBps,
healthFactorBps,
availableToMintNusd6: borrowLimitNusd6 > debt ? borrowLimitNusd6 - debt : 0n,
liquidatable: debt > liquidationLimitNusd6,
};
}
export function liquidationSafePriceE8(input: {
collateralRaw: bigint;
collateralDecimals: number;
projectedDebtNusd6: bigint;
liquidationThresholdBps: bigint;
}) {
if (input.projectedDebtNusd6 === 0n) return null;
if (input.collateralRaw === 0n) return 0n;
const requiredValueUsd6 = ceilDiv(input.projectedDebtNusd6 * BPS, input.liquidationThresholdBps);
return ceilDiv(
requiredValueUsd6 * 10n ** BigInt(input.collateralDecimals) * PRICE_SCALE,
input.collateralRaw * USD_SCALE,
);
}
export function maximumWithdrawRaw(input: {
collateralRaw: bigint;
collateralValueUsd6: bigint;
projectedDebtNusd6: bigint;
borrowLtvBps: bigint;
}) {
if (input.projectedDebtNusd6 === 0n) return input.collateralRaw;
if (input.collateralRaw === 0n || input.collateralValueUsd6 === 0n) return 0n;
// The contract requires the remaining position to stay inside borrow LTV.
const requiredValueUsd6 = ceilDiv(input.projectedDebtNusd6 * BPS, input.borrowLtvBps);
if (requiredValueUsd6 >= input.collateralValueUsd6) return 0n;
const requiredCollateralRaw = ceilDiv(
input.collateralRaw * requiredValueUsd6,
input.collateralValueUsd6,
);
return input.collateralRaw > requiredCollateralRaw
? input.collateralRaw - requiredCollateralRaw
: 0n;
}Read the inputs on-chain
import { PublicKey } from "@solana/web3.js";
import { borrowVaultPda, marketBySymbol, oraclePda, type NestClient } from "../nest-client";
export async function readHealthInputs(client: NestClient, symbol: string) {
const market = marketBySymbol(client, symbol);
const coreProgram = new PublicKey(client.deployment.programs.nestCore);
const configKey = new PublicKey(market.collateralConfig);
const vaultKey = borrowVaultPda(coreProgram, client.wallet.publicKey, configKey);
const oracleKey = oraclePda(coreProgram, configKey);
const [config, vault, oracle] = await Promise.all([
client.core.account.collateralConfig.fetch(configKey),
client.core.account.vault.fetch(vaultKey),
client.core.account.oracleSnapshot.fetch(oracleKey),
]);
return {
config,
vault,
oracle,
collateralRaw: BigInt(vault.collateralRaw.toString()),
storedDebtNusd6: BigInt(vault.principalDebt.toString())
+ BigInt(vault.accruedFee.toString()),
priceE8: BigInt(oracle.xstockUsd.priceE8.toString()),
confidenceE8: BigInt(oracle.xstockUsd.confidenceE8.toString()),
};
}Project the fee since lastAccrualTs using Accrued stability fees
before calculating health or capacity.
Borrow LTV and liquidation threshold are different
New debt and collateral withdrawals must remain within borrowLtvBps.
liquidationThresholdBps determines when liquidation is allowed. Do not use
the higher liquidation threshold to quote a withdrawable amount.