Instructions Reference
The entry points exposed by the keystone_finance program (ksUSD vault). Grouped by caller role.
v1 hedges on Phoenix Perps (USDC margin via Ember, CPI through the Rise SDK).
Source: the program entrypoints · IDL: the generated IDL
One-time setup (admin)
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
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)
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:
Buy jitoSOL via Jupiter — the unlevered spot leg.
Post
margin_usdcon Phoenix, via Ember.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
DeltaWithinBandunless|delta| / NAVclearsdelta_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, withcollateral_cappedset 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
lotswith 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
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.
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):
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