For the complete documentation index, see llms.txt. This page is also available as Markdown.

Instructions Reference

The entry points exposed by the keystone_finance program (ksUSD vault). Grouped by caller role.

Source: the program entrypoints · IDL: the generated IDL


One-time setup (admin)

Instruction
Caller
Description

initialize(params)

Admin (deployer)

Creates the vault PDA at seeds [b"vault"] and the ksUSD share mint. Sets all risk parameters. Idempotent guard via PDA init.

enable_phoenix(perp_asset_id)

Admin

One-time: pins the vault's Phoenix trader, market, global config, canonical mint and canonical ATA. perp_asset_id is the SOL-PERP id in the PerpAssetMap (0). Required before any perp position can be opened.

enable_lending()

Admin

One-time: pins the Kamino USDC reserve and both cToken ATAs (idle-USDC leg and reserve leg). Required before any lend_* call.

set_oracles()

Admin

Pins the Pyth SOL/USD and jitoSOL/USD accounts. Re-callable to rotate feeds.

InitializeParams defaults

Field
Default
Meaning

liquidity_buffer_bps

1_000 (10%)

Idle USDC kept on-vault for instant withdrawals

funding_threshold_normal_bps

0 (safety floor)

On-chain floor — never short into negative funding; keeper applies the dynamic threshold above it

perf_fee_bps

2_000 (20%)

Performance fee above HWM

reserve_skim_bps

500 (5%)

Of accrued perf fees, routed to reserve fund

min_dwell_seconds

43_200 (12 h)

Minimum time between mode switches

max_swap_slippage_bps

50 (0.5%)

Jupiter swap slippage cap

emergency_close_dd_bps

500 (5%)

NAV drawdown from peak that trips emergency_close

deposit_cap_usdc

500_000_000_000 ($500k)

Cached-NAV cap on new deposits. u64::MAX = uncapped, 0 = deposits paused

min_request_shares

1_000_000 (1 ksUSD)

Minimum size for a queued withdrawal request

max_pending_queue_usdc

u64::MAX (uncapped)

Ceiling on total USDC owed to the withdrawal queue

max_nav_change_bps_per_hour

5_000 (50%/hr)

Bound on a single attest_nav delta

funding_max_staleness_seconds

21_600 (6 h)

Opens revert if the funding signal is older than this

consecutive_dd_settles_required

2

Bad settles needed in a row before drawdown trips

authorized_keeper

admin key

Gates strategy instructions. Pubkey::default() = fully permissionless

lst_depeg_bps

500 (5%)

jitoSOL/SOL deviation that auto-pauses on settle

phoenix_perp_asset_id, the Phoenix accounts, the Kamino reserve, and the oracles are not set here. They're pinned separately by enable_phoenix, enable_lending, and set_oracles.


User flow (anyone)

Instruction
Description

deposit(usdc_amount)

Transfer USDC to the vault, mint ksUSD at current share price. → Deposit

withdraw_instant(shares)

Burn ksUSD, receive USDC from the liquidity buffer. Reverts with InsufficientLiquidityBuffer if not enough idle USDC. → Withdraw

request_withdrawal(shares)

Burn ksUSD immediately, lock redemption price, create a queued WithdrawalRequest PDA.

process_withdrawal()

Permissionless crank — fulfill a queued request, close the PDA, refund rent to the original requester.

claim_wind_down(shares)

Wind-down only. Burn ksUSD for a pro-rata share of the vault's idle USDC. Reverts unless position_mode == WindDown.


Strategy (keeper-cranked, permissionless)

All strategy instructions take a 3- to 5-group remaining_accounts layout. The keeper builds these off-chain (in the v1 target, with the Rise SDK + Jupiter quote API) and passes the per-group counts as u8 arguments so the handler can split safely.

open_position

Normal basis open. Requires Idle. Splits the USDC and builds both legs in one transaction:

  1. Buy jitoSOL via Jupiter — the unlevered spot leg.

  2. Post margin_usdc on Phoenix, via Ember.

  3. Short SOL-PERP delta-neutral against the spot leg.

margin_usdc must be at least min_margin_bps of usdc_amount, which is what bounds leverage.

close_position

Normal basis close. Reduce-only long order → settle funding → withdraw USDC margin → sell jitoSOL→USDC.

settle

Refresh NAV and peak share price, settle funding if a position is open, log drawdown if the guard tripped.

Permissionless — but the funding_apr_bps_now EMA update only applies when the authorized keeper signs, so a stranger can keep NAV fresh without steering the regime signal.

emergency_close

  • Force closes on a drawdown breach, or while the vault is paused.

  • Auto-pauses the vault on exit.

  • Permissionless once tripped.

attest_nav

Refresh cached_nav_usdc to include unrealized perp PnL. The delta is bounded by max_nav_change_bps_per_hour; over that it reverts with NavChangeExceedsCap. Calls accrue_perf_fees.

lend_idle_usdc / unlend_usdc

Move idle USDC into and out of the Kamino USDC reserve. Lending reverts with LendingBreachesBuffer if it would eat the liquidity buffer.

add_margin

Move USDC margin to the Phoenix trader without touching the position — the defence against segregated-collateral liquidation (see below). Respects the liquidity buffer and a ceiling of effective_nav / 2.

There is no counterpart: margin returns only via close_position, which withdraws it in full.

rebalance_hedge

Delta correction — moves the short only. Resizes the short to match the jitoSOL leg in SOL terms. It takes no size argument: the program reads the spot balance and the jitoSOL/SOL rate and computes the target itself.

  • Reverts with DeltaWithinBand unless |delta| / NAV clears delta_band_bps.

  • Under-hedged sells more (not reduce-only); over-hedged buys back (reduce-only, so an overstated size cannot flip the book long).

  • Increases are capped at the resulting total — not the increment — by max_position_base_lots.

  • They are clamped again to what posted collateral supports at min_margin_bps — clamped rather than rejected, with collateral_capped set on the event.

reduce_position

De-lever — moves both legs. Buys back lots of the short and sells the matching slice of jitoSOL, so net exposure is unchanged while the position gets smaller.

destination picks where the proceeds go:

  • Buffer — proceeds and proportional margin land in the vault USDC ATA, funding the withdrawal queue.

  • Margin — margin stays on the venue and spot proceeds are posted on top; notional falls while collateral rises.

Keeper-gated normally, and permissionless below hard_margin_floor_bps. A permissionless caller is bounded three ways:

  • It may only route to Margin — defend, never drain.

  • It may only close the slice that restores health to min_margin_bps, not the whole book.

  • It must supply a real Jupiter route, because a lots with no route would shrink the short while the whole spot leg stayed, leaving the vault net long.

The pause check is deliberately skipped on this path: a pause must not disable the margin defence. A full de-lever returns the vault to Idle and restarts the dwell clock.

lend_reserve / unlend_reserve

Same as the idle-USDC pair, but for the reserve fund, using its own cToken ATA so reserve yield stays separate from idle-USDC yield.


Admin

Instruction
Description

set_pause(paused)

Halt new deposits + new positions. Withdrawals from the liquidity buffer remain open.

update_params(args)

Adjust risk parameters within bounds. Cannot move admin or mints.

transfer_admin(new_admin)

Step 1 of the handover: records pending_admin. Does not change the admin.

accept_admin()

Step 2: the pending admin signs to take over. Completes the handover.

collect_fees()

Pay out pending_perf_fees_usdc in USDC — admin share to the admin, 5% skim to the reserve ATA. No shares are minted, and the share price doesn't move. Requires Parked and not paused. → Fees

pay_from_reserve(amount)

Spend the reserve fund to absorb a loss. Admin-signed and logged on-chain. unlend_reserve first if the reserve is lent out.

reset_peak()

Re-anchor peak_share_price_1e9 for the drawdown guard. Does not touch the HWM.

init_wind_down()

Terminal. Blocks deposits and new positions; holders redeem via claim_wind_down.


Account-context groups for remaining_accounts

Strategy instructions pack multiple downstream CPIs into one transaction. Each group's account list is documented in the handler source — link below per instruction. The Phoenix + Ember groups are assembled via the Rise SDK.

Instruction
Groups

open_position

Jupiter swap → Ember margin deposit → Phoenix place_perp_order

add_margin

Ember margin deposit only — arena slices only, no Jupiter group

rebalance_hedge

Phoenix place_perp_order only — arena slices only, no Jupiter group

reduce_position

place_perp_order (reduce-only) → Ember margin move → Jupiter swap

close_position

place_perp_order (reduce-only) → settle funding → Ember margin withdraw → Jupiter swap

settle

Phoenix funding / margin re-eval accounts (when position open)

emergency_close

place_perp_order → Ember margin withdraw → Jupiter swap

See Keeper bot for the off-chain account-assembly conventions.


Errors

See Errors for the full KsusdError enum and exit codes.


Why margin is its own instruction

The book is delta-neutral, but the two legs live in different places: the jitoSOL spot leg sits in the vault, the margin sits on Phoenix. Phoenix can only see its own side. On a rally the short's loss eats margin while the exactly offsetting jitoSOL gain sits somewhere Phoenix cannot count — so an economically hedged position gets liquidated, turning a paper wash into a realised loss.

emergency_close does not cover this. It triggers on NAV drawdown, and NAV doesn't fall in a hedged rally. The vault reads healthy right up to liquidation.

These multiples describe the perp leg only, not the vault. The vault is not levered — total exposure equals NAV, and the short is sized 1:1 against the jitoSOL. The multiple is what the position looks like from Phoenix's side, where only min_margin_bps of the deployed capital is visible as collateral. It is the right unit for reasoning about liquidation distance and the wrong one for describing the product.

Measured over 730 days of SOL (intraday high vs open, which is what liquidates a short):

Leverage
Liquidates at
Days breached
Frequency

3x

16.7%

2 / 730

~1x/yr

5x

10.0%

20 / 730

~10x/yr

10x (min_margin_bps = 900)

5.0%

139 / 730

~70x/yr

At that frequency a static buffer is not a defence. 10x is only acceptable because the keeper actively tops margin up — add_margin is what makes it so, and the margin-health keeper duty is what calls it. Without active defence the honest choice would be 3x, which at measured Phoenix funding clears lending by under 30bps and does not justify the operational risk.

Last updated