Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.exponent.finance/llms.txt

Use this file to discover all available pages before exploring further.

VaultAssertion is a standalone assertion runner. It observes Strategy Vault, protocol, and caller-supplied state and returns a structured report you can use to decide whether to build, send, or skip a transaction. It is complementary to VaultTransactionBuilder — caller-controlled, and entirely outside the transaction pipeline.
VaultAssertion does not build, mutate, or send transactions. It does not run a monitoring loop. It does not replace Squads policies or onchain validation hooks. It also does not require any transaction-builder input — you decide what to do with the report.

Quick Start

The two most actionable conditions for most strategy flows are ClmmPrice (live CLMM price/APY bounds) and KaminoLtv (Kamino obligation LTV bounds). The example below runs both in a single check and uses the result to gate the rest of the flow.
import {
  VaultAssertion,
  VaultAssertionConditionKind,
} from "@exponent-labs/exponent-sdk";
import { Connection, PublicKey } from "@solana/web3.js";

const connection = new Connection("https://api.mainnet-beta.solana.com");
const vaultAddress = new PublicKey("...");      // strategy vault
const clmmMarket = new PublicKey("...");        // CLMM MarketThree
const kaminoObligation = new PublicKey("...");  // Kamino obligation

const assertion = await VaultAssertion.create({
  connection,
  vault: vaultAddress,                          // PublicKey or a loaded vault
});

const report = await assertion.check({
  conditions: [
    {
      kind: VaultAssertionConditionKind.ClmmPrice,
      market: clmmMarket,
      impliedApy: { blockAboveBps: 1_500 },     // block if implied APY ≥ 15%
    },
    {
      kind: VaultAssertionConditionKind.KaminoLtv,
      obligation: kaminoObligation,
      ltv: { warnAboveBps: 6_800, blockAboveBps: 7_200 },
    },
  ],
});

if (report.decision === "block") {
  return;   // skip — caller decides what to do next
}
VaultAssertion.create accepts:
ParameterTypeDescription
connectionConnectionSolana RPC connection
vaultPublicKey | VaultAssertionLoadedVaultStrategy vault address. Pass a loaded vault to skip an RPC fetch
ownerPublicKeyOptional. Expected owner — defaults from loaded vault state when present
pricesExponentPricesOptional. Pre-decoded prices snapshot
commitmentCommitmentOptional. Defaults to confirmed
envEnvironmentOptional. Defaults from the loaded vault environment or LOCAL_ENV
assertion.check accepts a VaultAssertionCheckPlan:
ParameterTypeDescription
namestringOptional report name
conditionsVaultAssertionCondition[]Conditions to evaluate
maxSlotDriftnumberOptional. Block-only guard for how many slots may pass during one run
Invalid configuration throws VaultAssertionConfigError before any RPC reads or condition evaluation. Missing or undecodable on-chain state becomes a warn or block entry inside the returned report instead.

Report Shape

type VaultAssertionReport = {
  name?: string;
  decision: "allow" | "warn" | "block";
  causes: VaultAssertionCause[];
  checks: VaultAssertionCheck[];
  timing: VaultAssertionTiming;
  commitment: string;
  checkedAt: string;
};
FieldDescription
decisionAggregate result. "block" if any condition blocks, otherwise "warn" if any warn, otherwise "allow"
causesCompact list of non-allow reasons — quick “why did this stop?” handling
checksFull audit trail for every evaluated user-supplied condition (including allows)
timingSlot freshness metadata for the run
commitmentCommitment level used for reads
checkedAtISO timestamp when the run started
checks only contain user-supplied conditions. Slot drift failures are surfaced through causes and timing; they do not create synthetic checks.
Use causes for control flow (if (report.decision === "block") or for (const cause of report.causes)). Use checks for logging or audit display — every condition you supplied has a corresponding entry.

Timing

type VaultAssertionTiming = {
  referenceSlot: number;
  latestObservedSlot: number;
  slotDrift: number;
  maxSlotDrift?: number;
  exceededMaxSlotDrift: boolean;
};
check() captures referenceSlot once at the start of the run. Each condition records the most recent slot observed through RPC, and the report tracks the maximum drift across the run. maxSlotDrift is optional and disabled by default. When supplied, drift is checked before and after each condition. If drift exceeds the threshold, the report blocks and the remaining conditions are skipped.
referenceSlot is a freshness and audit baseline. It is not an atomic multi-account snapshot guarantee. Transaction blockhash freshness and confirmation remain outside VaultAssertion — the caller’s transaction builder/sender owns those.

Thresholds

Built-in conditions accept inclusive thresholds in two flavors: Basis-point thresholds — used for APY-style metrics:
{ warnAboveBps?: number; blockAboveBps?: number; warnBelowBps?: number; blockBelowBps?: number }
Raw numeric thresholds — used for prices, slot ages, and exchange rates:
{ warnAbove?: NumericValue; blockAbove?: NumericValue; warnBelow?: NumericValue; blockBelow?: NumericValue }
NumericValue accepts number | string | bigint | BN | Decimal. At least one bound must be specified per threshold. Any of the following throws VaultAssertionConfigError before evaluation:
  • empty threshold objects
  • unknown threshold fields, or wrong-unit fields for a metric (e.g., warnAboveBps on a price threshold)
  • negative values, or non-integer BPS values
  • block/warn ordering that makes the block threshold less severe than the warn threshold
When a threshold is breached, the corresponding check evidence includes breachedThresholds for inspection.

Built-In Conditions

VaultCoreState

Asserts strategy vault owner and status-flag invariants. With no inputs, it just records vault state in the audit trail.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.VaultCoreState,
  }],
});
ParameterTypeDescription
ownerPublicKeyOptional. Expected owner. Defaults to assertion-level owner
requireStatusFlagsnumberOptional. Status flag bits that must be set
forbidStatusFlagsnumberOptional. Status flag bits that must not be set

VaultPriceCoverage

Asserts that decoded ExponentPrices covers all required price IDs (or pairs) and is within a freshness bound.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.VaultPriceCoverage,
  }],
});
ParameterTypeDescription
pricesExponentPricesOptional. Pre-decoded prices snapshot. Otherwise fetched via the loaded vault fetcher
includeTrackedVaultPricesbooleanOptional. Default true. Include all price IDs tracked by the strategy vault
requiredPriceIdsNumericValue[]Optional. Explicit price IDs that must be present
requiredPricePairsVaultPricePair[]Optional. { priceMint, underlyingMint } pairs that must resolve
slotAgeNumberThresholdOptional. Freshness bound on each entry’s lastUpdatedSlot

KaminoLtv

Asserts a Kamino obligation’s LTV. The calculation is:
ltvBps = borrowedAssetsMarketValueSf / depositedValueSf * 10_000
borrowingDisabled always blocks. A stale obligation warns when LTV and slot-age thresholds are otherwise passing.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.KaminoLtv,
    obligation: new PublicKey("..."),
    ltv: { warnAboveBps: 6_800, blockAboveBps: 7_200 },
  }],
});
ParameterTypeDescription
obligationPublicKeyRequired. Kamino obligation account
ltvBpsThresholdRequired. LTV bound in basis points
lendingProgramIdPublicKeyOptional. Override the Kamino lending program ID
obligationSnapshotKaminoObligationSnapshotOptional. Pre-fetched snapshot — skips an RPC read
slotAgeNumberThresholdOptional. Freshness bound on the obligation’s lastUpdatedSlot

AmmPrice

Asserts price evidence on a legacy AMM (Market Two) market. The market is loaded through Market.load(...).
impliedApyBps = (exp(lastLnImpliedRate) - 1) * 10_000
ptPrice       = market.currentPtPriceInAsset
ptPriceInSy   = market.currentPtPriceInSy
At least one of impliedApy, ptPrice, or ptPriceInSy is required.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.AmmPrice,
    market: new PublicKey("..."),
    impliedApy: { blockBelowBps: 900 },
  }],
});
ParameterTypeDescription
marketPublicKeyRequired. Market Two PDA
impliedApyBpsThresholdOptional. Implied-APY bound in basis points
ptPriceNumberThresholdOptional. PT price (in asset)
ptPriceInSyNumberThresholdOptional. PT price denominated in SY
The loaded market’s vault.selfAddress is recorded as sourceVault evidence. It identifies the market’s underlying source — it is not a check that the strategy vault owns the market.

ClmmPrice

Asserts live CLMM price evidence. The evaluator fetches the market account, validates its program owner against EXPONENTCLMM_PROGRAM_ID, decodes it, then fetches the linked ticks account and performs the same validation.
spot          = ticks.currentSpotPrice
impliedApyBps = (spot - 1) * 10_000
tau           = max(0, market.financials.expirationTs - nowUnix) / 31_536_000
ptPrice       = spot ** -tau
At least one of impliedApy, ptPrice, or spotPrice is required.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.ClmmPrice,
    market: new PublicKey("..."),
    impliedApy: { blockAboveBps: 1_500 },
  }],
});
ParameterTypeDescription
marketPublicKeyRequired. CLMM MarketThree PDA
impliedApyBpsThresholdOptional. Implied-APY bound
ptPriceNumberThresholdOptional. Derived PT price
spotPriceNumberThresholdOptional. Spot price from ticks

OrderbookPrice

Asserts top-of-book APY and derived prices on an Exponent Orderbook. The evaluator filters expired offers, offers with non-positive amount or APY, and (by default) the assertion owner’s own non-virtual offers. Virtual offers are kept by default.
ptPrice       = calcPriceInAssetsByAPYPt(selectedRate, secondsRemaining)
ytPrice       = 1 - ptPrice
impliedApyBps = (exp(selectedRate) - 1) * 10_000
When both sides exist and source === "mid", the mid is used; with one side only, the available side is used.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.OrderbookPrice,
    orderbook: new PublicKey("..."),
    impliedApy: { blockAboveBps: 1_050 },
  }],
});
ParameterTypeDescription
orderbookPublicKeyRequired. Orderbook PDA
impliedApyBpsThresholdOptional. Implied-APY bound
ptPriceNumberThresholdOptional. Derived PT price
ytPriceNumberThresholdOptional. Derived YT price
source"mid" | "bestBid" | "bestAsk"Optional. Default "mid"
ownerPublicKey | falseOptional. Defaults to the assertion owner. false disables own-offer filtering
includeVirtualOffersbooleanOptional. Default true
spreadBpsThresholdOptional. Auxiliary threshold on the bid/ask spread
Like AmmPrice, the loaded orderbook’s vault.selfAddress is recorded as sourceVault evidence. It is not a strategy-vault ownership check.

SyExchangeRate

Asserts a core vault’s SY/base exchange rate, loaded through CoreVault.load(...).
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.SyExchangeRate,
    coreVault: new PublicKey("..."),
    exchangeRate: { warnBelow: "1.03", blockBelow: "1.01" },
  }],
});
ParameterTypeDescription
coreVaultPublicKeyRequired. Exponent Core vault address
exchangeRateNumberThresholdRequired. Bound on currentSyExchangeRate

KaminoBorrowCapacity

Asserts that a Kamino obligation has the headroom to take on a new borrow at a target LTV. The evaluator loads the obligation, the collateral and borrow reserves, and computes the projected post-borrow LTV against maxLtvBps (with optional ltvBufferBps).
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.KaminoBorrowCapacity,
    market: new PublicKey("..."),
    collateralReserve: new PublicKey("..."),
    borrowReserve: new PublicKey("..."),
    requestedBorrowAtomic: 50_000_000n,
    maxLtvBps: 7_000,
  }],
});
ParameterTypeDescription
marketPublicKeyRequired. Kamino lending market
collateralReservePublicKeyRequired. Reserve holding the collateral side
borrowReservePublicKeyRequired. Reserve being borrowed against
requestedBorrowAtomicNumericValueRequired. Atomic amount of debt to take on
maxLtvBpsnumberRequired. Maximum projected LTV in basis points
ownerPublicKeyOptional. Obligation owner — defaults to the assertion owner
obligationPublicKeyOptional. Pre-derived obligation PDA. Otherwise derived from market + owner
projectedCollateralAtomicNumericValueOptional. Override the projected collateral atomic amount
minimumBorrowAtomicNumericValueOptional. Minimum borrow that still satisfies caller intent
ltvBufferBpsnumberOptional. Headroom buffer applied to maxLtvBps
resizeToCapacitybooleanOptional. Allow the evaluator to suggest a smaller borrow that still fits inside maxLtvBps

KaminoWithdrawCapacity

Asserts that a Kamino obligation has the headroom to withdraw collateral while still satisfying maxLtvBps. Symmetrical to KaminoBorrowCapacity on the withdraw side.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.KaminoWithdrawCapacity,
    market: new PublicKey("..."),
    collateralReserve: new PublicKey("..."),
    requestedWithdrawAtomic: 100_000_000n,
    maxLtvBps: 7_000,
  }],
});
ParameterTypeDescription
marketPublicKeyRequired. Kamino lending market
collateralReservePublicKeyRequired. Reserve holding the collateral being withdrawn
requestedWithdrawAtomicNumericValueRequired. Atomic collateral amount to withdraw
maxLtvBpsnumberRequired. Maximum post-withdraw LTV in basis points
ownerPublicKeyOptional. Obligation owner — defaults to the assertion owner
obligationPublicKeyOptional. Pre-derived obligation PDA
minimumWithdrawAtomicNumericValueOptional. Minimum withdraw that still satisfies caller intent
projectedDebtKaminoProjectedDebtOptional. Override projected debt as { borrowedValueSf } or { borrowReserve, borrowedAtomic }
ltvBufferBpsnumberOptional. Headroom buffer applied to maxLtvBps
resizeToCapacitybooleanOptional. Allow the evaluator to suggest a smaller withdraw that still fits inside maxLtvBps

ClmmCapacity

Asserts that a CLMM swap of amountInAtomic from inputMint to outputMint is executable against the live tick state.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.ClmmCapacity,
    market: new PublicKey("..."),
    inputMint: new PublicKey("..."),
    outputMint: new PublicKey("..."),
    amountInAtomic: 1_000_000_000n,
  }],
});
ParameterTypeDescription
marketPublicKeyRequired. CLMM MarketThree PDA
inputMint / outputMintPublicKeyRequired. Swap direction
amountInAtomicNumericValueRequired. Atomic input amount
minimumExecutableInputAtomicNumericValueOptional. Minimum input that still satisfies caller intent
resizeBelowMaximumbooleanOptional. Allow the evaluator to suggest a smaller input that fits inside live capacity
slippageBpsnumberOptional. Slippage budget applied to expected output
capacitySnapshotClmmCapacitySnapshotOptional. Pre-fetched { maxExecutableInputAtomic, expectedOutputAtomic? } — skips an RPC read

SyBacking

Asserts that an SY mint is sufficiently backed by its underlying. Useful pre-flight before stripping or redeeming SY-denominated positions.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.SyBacking,
    syMint: new PublicKey("..."),
    requiredUnderlyingAtomic: 1_000_000_000n,
  }],
});
ParameterTypeDescription
syMintPublicKeyRequired. SY mint to verify
requiredUnderlyingAtomicNumericValueOptional. Underlying backing the caller expects to be available
snapshotobjectOptional. Pre-supplied { sySupplyAtomic, underlyingBackingAtomic, exchangeRate, lastUpdatedSlot }
blockWhenInsufficientbooleanOptional. Default true — block (rather than warn) when backing is insufficient

PolicyCoverage

Asserts that the strategy vault’s policies cover a required list of programs. Useful for catching missing policies before a flow that depends on them.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.PolicyCoverage,
    requiredPrograms: [JUPITER_LEND_BORROW_PROGRAM_ID, KAMINO_LENDING_PROGRAM_ID],
  }],
});
ParameterTypeDescription
requiredProgramsPublicKey[]Required. Program IDs every vault policy set should cover
policySnapshotVaultPolicySnapshotOptional. Pre-supplied snapshot — otherwise read from loaded vault state
blockWhenMissingbooleanOptional. Default true — block (rather than warn) when a required program isn’t covered

WithdrawalQueue

Asserts the state of the strategy vault’s withdrawal queue — useful for managers running fill/execute flows who want to refuse to act when there’s an unfilled or past-due withdrawal.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.WithdrawalQueue,
    blockWhenUnfilled: true,
    blockWhenPastDue: true,
  }],
});
ParameterTypeDescription
blockWhenUnfilledbooleanOptional. Block when there’s at least one unfilled withdrawal
blockWhenPastDuebooleanOptional. Block when there’s at least one past-due withdrawal
snapshotobjectOptional. Pre-supplied { accounts, lpLockedForWithdrawalAtomic?, pendingWithdrawalBacklogDueAt?, nowUnix? } — otherwise the evaluator fetches via the loaded vault fetcher

Custom Conditions

Custom conditions let you express app-specific policy that doesn’t belong as a generic SDK condition. They share the same audit-trail and evidence semantics as built-in conditions.
const report = await assertion.check({
  conditions: [{
    kind: VaultAssertionConditionKind.Custom,
    async evaluate(ctx) {
      const vaultUsdcTokenAccount = new PublicKey("...");
      const balance = await ctx.fetchTokenAmount(vaultUsdcTokenAccount);

      if (balance > 10_000_000n) {
        return ctx.block("Idle USDC exceeds cap", {
          observed: { balance },
          thresholds: { maxBalance: 10_000_000n },
          accounts: { vaultUsdcTokenAccount },
        });
      }

      return ctx.allow("Idle USDC within cap", {
        observed: { balance },
        accounts: { vaultUsdcTokenAccount },
      });
    },
  }],
});
The ctx (VaultAssertionContext) gives the evaluator first-class RPC access and report helpers:
Field / MethodDescription
connectionThe assertion’s Connection
vault / vaultAddressLoaded strategy vault data and its address
ownerEffective owner used by the assertion
referenceSlot / latestObservedSlotSlot baseline and the most recent slot observed in this run
commitmentThe configured commitment
slotAge(lastUpdatedSlot)Non-negative slot age relative to referenceSlot
fetchAccountInfo(address)Read and cache a raw account for the run
fetchTokenAmount(tokenAccount)Read an SPL token amount as bigint
fetchKaminoObligation(obligation, programId?)Fetch and decode a Kamino obligation snapshot
fetchDecodedKaminoObligation(obligation, programId?)Fetch and decode a raw Kamino Obligation account (lower-level than the snapshot helper)
fetchKaminoReserve(reserve, programId?)Fetch and decode a raw Kamino Reserve account
allow(message, evidence?)Build an allow result
warn(message, evidence?)Build a warn result
block(message, evidence?)Build a block result
Helpers cache reads within a single check() run. Evidence is normalized into JSON-safe report data — BigInt, BN, Decimal, PublicKey, Date, Buffer, Uint8Array, non-finite numbers, and cyclic objects are all handled.
Invalid return values from evaluate(...) (e.g., a thrown error or a malformed result) are converted into blocking evaluation_error entries rather than crashing the whole run.

Gating a Transaction

The expected integration is caller-controlled: assert first, decide second, build/send last.
import {
  VaultAssertion,
  VaultAssertionConditionKind,
  VaultTransactionBuilder,
} from "@exponent-labs/exponent-sdk";

const assertion = await VaultAssertion.create({ connection, vault });

const report = await assertion.check({
  conditions: [
    { kind: VaultAssertionConditionKind.VaultCoreState },
    {
      kind: VaultAssertionConditionKind.KaminoLtv,
      obligation: kaminoObligation,
      ltv: { warnAboveBps: 6_800, blockAboveBps: 7_200 },
    },
  ],
});

if (report.decision === "block") {
  return;   // skip the run; surface report.causes to your operator/log
}

const result = await VaultTransactionBuilder
  .create({ vault, connection, signer: manager.publicKey })
  .addActions([/* ... */])
  .build();

await result.send({ signers: [manager] });
report.causes and report.checks are JSON-safe — log them, ship them to a dashboard, or surface them to a manager UI. The decision belongs to the caller.