NestDocs
Stake nUSD

Convert nUSD into snUSD

Calculate the current share rate and convert nUSD into snUSD with a minimum output.

Quote the conversion

stake-quote.ts
export function quoteStakeShares(input: {
  amountNusd6: bigint;
  totalShares: bigint;
  stakingVaultNusd6: bigint;
  reservedPendingClaimsNusd6: bigint;
  revenueVaultBalanceNusd6: bigint;
  revenueBaselineNusd6: bigint;
}) {
  if (input.amountNusd6 <= 0n) throw new Error("Stake amount must be positive");
  if (input.totalShares === 0n) return input.amountNusd6;

  if (input.reservedPendingClaimsNusd6 > input.stakingVaultNusd6) {
    throw new Error("Recorded pending claims exceed staking assets");
  }
  const redeemable = input.stakingVaultNusd6 - input.reservedPendingClaimsNusd6;
  const pendingRevenue = input.revenueVaultBalanceNusd6 > input.revenueBaselineNusd6
    ? input.revenueVaultBalanceNusd6 - input.revenueBaselineNusd6
    : 0n;
  const entryAssets = redeemable + pendingRevenue;
  if (entryAssets <= 0n) throw new Error("Staking pool has no entry assets");
  return input.amountNusd6 * input.totalShares / entryAssets;
}

export function minimumSharesOut(quote: bigint, slippageBps = 50n) {
  if (quote <= 0n || slippageBps < 0n || slippageBps >= 10_000n) {
    throw new Error("Invalid quote or slippage");
  }
  const minimum = quote * (10_000n - slippageBps) / 10_000n;
  return minimum > 0n ? minimum : 1n;
}

TypeScript example

nusd-to-snusd.ts
import { BN } from "@coral-xyz/anchor";
import { PublicKey, Transaction } from "@solana/web3.js";
import { getAccount } from "@solana/spl-token";
import {
  addAtaIfMissing,
  ata,
  simulateSignSend,
  toBaseUnits,
  type NestClient,
} from "../nest-client";
import { minimumSharesOut, quoteStakeShares } from "./stake-quote";

export async function stakeNusd(
  client: NestClient,
  userAmount: string,
  slippageBps = 50n,
) {
  const owner = client.wallet.publicKey;
  const amount = toBaseUnits(userAmount, 6);
  const stakingState = new PublicKey(client.deployment.accounts.stakingState);
  const protocol = new PublicKey(client.deployment.accounts.protocol);
  const nestCoreProgram = new PublicKey(client.deployment.programs.nestCore);
  const nusdMint = new PublicKey(client.deployment.mints.nusd);
  const snusdMint = new PublicKey(client.deployment.mints.snusd);
  const nusdTokenProgram = new PublicKey(client.deployment.tokenPrograms.nusd);
  const snusdTokenProgram = new PublicKey(client.deployment.tokenPrograms.snusd);
  const stakingNusdVault = new PublicKey(client.deployment.vaults.stakingNusdVault);
  const revenueNusdVault = new PublicKey(client.deployment.vaults.stakerRevenueNusdVault);
  const ownerNusdAccount = ata(nusdMint, owner, nusdTokenProgram);

  const [state, protocolAccount, revenueVault, ownerNusd] = await Promise.all([
    client.stake.account.stakingState.fetch(stakingState),
    client.core.account.protocol.fetch(protocol),
    getAccount(client.connection, revenueNusdVault, "confirmed", nusdTokenProgram),
    getAccount(client.connection, ownerNusdAccount, "confirmed", nusdTokenProgram),
  ]);
  if (state.paused) throw new Error("Staking is paused");
  if (BigInt(protocolAccount.badDebtNusd.toString()) !== 0n) {
    throw new Error("Staking is unavailable while bad debt is outstanding");
  }
  if (ownerNusd.amount < amount) throw new Error("Insufficient nUSD balance");

  const quote = quoteStakeShares({
    amountNusd6: amount,
    totalShares: BigInt(state.totalShares.toString()),
    stakingVaultNusd6: BigInt(state.stakingVaultNusd.toString()),
    reservedPendingClaimsNusd6: BigInt(state.reservedPendingClaims.toString()),
    revenueVaultBalanceNusd6: revenueVault.amount,
    revenueBaselineNusd6: BigInt(state.revenueBaselineNusd.toString()),
  });
  const minShares = minimumSharesOut(quote, slippageBps);

  const transaction = new Transaction();
  const ownerSnusdAccount = await addAtaIfMissing(
    client,
    transaction,
    snusdMint,
    owner,
    snusdTokenProgram,
  );
  const sharesBefore = await client.connection.getAccountInfo(ownerSnusdAccount, "confirmed")
    ? (await getAccount(
        client.connection,
        ownerSnusdAccount,
        "confirmed",
        snusdTokenProgram,
      )).amount
    : 0n;

  transaction.add(await client.stake.methods
    .stake(new BN(amount.toString()), new BN(minShares.toString()))
    .accountsStrict({
      stakingState,
      protocol,
      nestCoreProgram,
      nusdMint,
      snusdMint,
      ownerNusdAccount,
      stakingNusdVault,
      ownerSnusdAccount,
      revenueNusdVault,
      owner,
      nusdTokenProgram,
      snusdTokenProgram,
    })
    .instruction());

  const signature = await simulateSignSend(client, transaction);
  const [nusdAfter, snusdAfter] = await Promise.all([
    getAccount(client.connection, ownerNusdAccount, "confirmed", nusdTokenProgram),
    getAccount(client.connection, ownerSnusdAccount, "confirmed", snusdTokenProgram),
  ]);
  const receivedShares = snusdAfter.amount - sharesBefore;
  if (ownerNusd.amount - nusdAfter.amount !== amount || receivedShares < minShares) {
    throw new Error(`Stake verification failed for ${signature}`);
  }
  return { signature, paidNusd6: amount, receivedSnusd6: receivedShares };
}

The quote is not a guarantee. The stake instruction checkpoints protocol revenue before minting shares; minSharesOut protects the user if that state changes. The transaction transfers nUSD and mints snUSD atomically. There is no waiting period before the shares appear in the owner's token account.

On this page