PAR docs

Integration

Everything you need to talk to PAR from a client: the account addresses, the instruction list and a full example. The program follows standard Anchor conventions, so the IDL in engine/target/idl/par.json works with @coral-xyz/anchor.

Derive every account

ts
import { PublicKey } from "@solana/web3.js"

const PROGRAM_ID = new PublicKey("2JYLMvPgC3FmHd6RMZtLAYtjBXP5rPrKaY8aWXsunLeG")
const JITOSOL_MINT = new PublicKey("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn")
const MSOL_MINT = new PublicKey("mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So")

const pda = (...seeds: (Buffer | Uint8Array)[]) => PublicKey.findProgramAddressSync(seeds, PROGRAM_ID)[0]
const u16le = (n: number) => { const b = Buffer.alloc(2); b.writeUInt16LE(n); return b }

export const protocol = pda(Buffer.from("protocol"))
export const parMint = pda(Buffer.from("par_mint"))
export const branch = (collateralMint: PublicKey) => pda(Buffer.from("branch"), collateralMint.toBuffer())
export const collateralVault = (b: PublicKey) => pda(Buffer.from("collateral_vault"), b.toBuffer())
export const poolVault = (b: PublicKey) => pda(Buffer.from("pool_vault"), b.toBuffer())
export const bucket = (b: PublicKey, index: number) => pda(Buffer.from("bucket"), b.toBuffer(), u16le(index))
export const trove = (b: PublicKey, owner: PublicKey) => pda(Buffer.from("trove"), b.toBuffer(), owner.toBuffer())
export const deposit = (b: PublicKey, owner: PublicKey) => pda(Buffer.from("deposit"), b.toBuffer(), owner.toBuffer())

Rates and buckets

ts
export const MIN_RATE_BPS = 50
export const RATE_STEP_BPS = 10
export const BUCKET_COUNT = 246

export const rateOf = (index: number) => MIN_RATE_BPS + RATE_STEP_BPS * index // bps a year
export const indexOf = (rateBps: number) => (rateBps - MIN_RATE_BPS) / RATE_STEP_BPS

Shared account groups

Most instructions take the same two groups.

Market (shared.rs):

AccountWritableNotes
protocolno["protocol"]
branchyes["branch", collateral_mint]
par_mintyes["par_mint"]
pool_vaultyes["pool_vault", branch]
collateral_vaultyes["collateral_vault", branch]
token_programnoSPL Token (collateral)
par_token_programnoToken-2022 (PAR)

PriceFeeds:

AccountNotes
pyth_sol_usd7UVimffxr9ow1uXYxsr4LHAcV58mLzhmwaeKvJ1pjLiE (Pyth sponsored SOL/USD feed account)
switchboard_sol_usdrLSjLWWtsBS19K1fJTCKp442xK18DBbNvTbebSXr46A (PAR's own Switchboard on-demand SOL/USD feed; its authority is the incinerator)
rate_sourceJito stake pool for jitoSOL, Marinade state for mSOL

Bring a fresh Switchboard price

The Switchboard account is a pull feed: nobody keeps it fresh, and the program refuses a price older than 120 seconds. Opening, borrowing, withdrawing collateral and an early rate change need it fresh, so put Switchboard's own update in front of your instruction, in the same transaction. Redeeming and liquidating only need it when Pyth is stale too.

ts
import * as sb from "@switchboard-xyz/on-demand"
import { CrossbarClient } from "@switchboard-xyz/common"

const sbProgram = await sb.AnchorUtils.loadProgramFromConnection(connection)
const feed = new sb.PullFeed(sbProgram, SWITCHBOARD_SOL_USD)
const [update] = await feed.fetchUpdateIx({
  crossbarClient: new CrossbarClient("https://crossbar.switchboardlabs.xyz"),
  numSignatures: 1,
  payer: wallet.publicKey,
})
// Switchboard's two instructions go first: its signature check reads its own position.
const ixs = [...update, ComputeBudgetProgram.setComputeUnitLimit({ units: 660_000 }), openTroveIx]

The update adds about 500 bytes and costs the payer about 0.0009 SOL (the oracle's fee), so send a version 0 transaction with PAR's address lookup table (GxF6u4W6yknpUDB6xpZePQXYDudPdsRt27QNKsx4pLKb); without it, opening a trove plus the update does not fit in 1,232 bytes.

Open a trove with Anchor

ts
import { AnchorProvider, BN, Program } from "@coral-xyz/anchor"
import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token"
import idl from "./par.json"

const program = new Program(idl as any, provider)
const b = branch(JITOSOL_MINT)
const index = indexOf(300) // 3.00% a year

await program.methods
  .openTrove(index, new BN(10_000_000_000), new BN(5_000_000_000)) // 10 jitoSOL (9 dp), 5,000 PAR (6 dp)
  .accounts({
    owner: wallet.publicKey,
    market: {
      protocol, branch: b, parMint, poolVault: poolVault(b), collateralVault: collateralVault(b),
      tokenProgram: TOKEN_PROGRAM_ID, parTokenProgram: TOKEN_2022_PROGRAM_ID,
    },
    feeds: { pythSolUsd: PYTH_SOL_USD, switchboardSolUsd: SWITCHBOARD_SOL_USD, rateSource: JITO_STAKE_POOL },
    bucket: bucket(b, index),
    trove: trove(b, wallet.publicKey),
    ownerCollateral: getAssociatedTokenAddressSync(JITOSOL_MINT, wallet.publicKey),
    ownerPar: getAssociatedTokenAddressSync(parMint, wallet.publicKey, false, TOKEN_2022_PROGRAM_ID),
  })
  .rpc()

Build a redemption

redeem reads its troves from the remaining accounts in the order [bucket, trove, trove, .., bucket, trove, ..], starting at the lowest non-empty bucket.

ts
// 1. read the branch bitmap: bit i set = bucket i has an active trove
const br = await program.account.branch.fetch(b)
const active: number[] = []
br.activeBuckets.forEach((word: BN, w: number) => {
  for (let i = 0; i < 64; i++) if (word.testn(i)) active.push(w * 64 + i)
})

// 2. for each bucket in ascending order, list its troves.
//    trovesIn() is your helper: getProgramAccounts on Trove filtered by branch and bucket.
//    enoughDebt() is your helper: stop once the listed troves cover the amount.
const remaining = []
for (const index of active) {
  remaining.push({ pubkey: bucket(b, index), isWritable: true, isSigner: false })
  for (const t of await trovesIn(b, index)) remaining.push({ pubkey: t, isWritable: true, isSigner: false })
  if (enoughDebt(remaining)) break
}

// 3. send with a fee ceiling in basis points
await program.methods.redeem(new BN(100_000_000), 100) // 100 PAR, at most 1.00% fee
  .accounts({ redeemer, market, feeds, redeemerPar, redeemerCollateral })
  .remainingAccounts(remaining)
  .rpc()

A bucket listed out of order ends the walk, and a trove that has already left its bucket is skipped, so a slightly stale list fails safe.

Liquidation bots

liquidate is permissionless and pays the caller 0.5% of the trove's collateral, capped at two tokens. A bot needs to:

  1. Fetch troves of a branch and the current prices.
  2. Find troves whose collateral value is under 120% of their debt (debt includes accrued bucket interest and pending redistribution).
  3. Send liquidate with the trove, its bucket and the caller's collateral token account.