Security Model
ksUSD is one Anchor program, one vault account the program controls, and one token. There isn't much surface area to attack, which is deliberate.
Access control
Admin
initialize, enable_phoenix, enable_lending, set_oracles, set_pause, update_params, collect_fees, transfer_admin, accept_admin, reset_peak, pay_from_reserve, init_wind_down
Keeper
attest_nav, open_position, close_position, lend_idle_usdc, unlend_usdc, lend_reserve, unlend_reserve. Gated by exact match against authorized_keeper. Note the direction: Pubkey::default() (the post-initialize state) matches no signer, so these stay blocked until the admin pins a real key — it is not a permissionless fallback.
Any user
deposit, withdraw_instant, request_withdrawal, process_withdrawal, claim_wind_down (during wind-down), settle, emergency_close (once tripped). None of these consult the keeper key.
Vault PDA
Signs every token operation for the vault. It's derived from the program, so there's no private key anyone could steal.
Admin control moves in two steps, transfer_admin then accept_admin, so it can't be handed to a wrong or unreachable address by a single mistake. Once mainnet is live the admin becomes a multisig, Squads or similar.
Protections
Emergency pause
Admin halt of new deposits + new positions; instant withdrawals stay open
Wind-down
Terminal init_wind_down blocks new state; users redeem pro-rata via claim_wind_down
Deposit cap
cached_nav_usdc + usdc_amount > deposit_cap_usdc reverts; 0 fully pauses deposits
Bootstrap residual guard
First deposit reverts if vault USDC ATA is non-empty (BootstrapResidualUsdc) — prevents dilution griefing
Oracle validation
Pyth staleness (5 min) + confidence (2%) checks; revert on either
LST depeg auto-pause
settle reverts AND pauses the vault if jitoSOL/SOL deviates beyond lst_depeg_bps
Integer arithmetic
checked_add / checked_mul everywhere; 128-bit intermediates for NAV math; no floating point
Slippage protection
Jupiter swaps gated by max_swap_slippage_bps (default 0.5%) plus explicit min_*_out arguments
HWM monotonicity
Performance fees never charged twice on the same gains; HWM only goes up
Drawdown guard
emergency_close is callable by anyone once peak − current ≥ emergency_close_dd_bps AND observed across consecutive_dd_settles_required settles
Drawdown latency
Single-tick drawdown reverts with DrawdownTriggerLatent
Mode-switch dwell
min_dwell_seconds (default 12 h) between mode transitions — prevents ping-pong
Funding signal staleness
Opens revert with FundingSignalStale if the EMA hasn't been refreshed within funding_max_staleness_seconds
Funding signal integrity
settle is open to anyone, but only the authorized keeper's call updates the funding EMA. Otherwise any caller could steer the signal (or seed it outright on the first call) to force or block basis entry
Venue circuit breaker
Opens revert with PhoenixMarketNotActive if SOL-PERP is halted, settling, or delisted
Position size caps
The short is bounded by max_position_base_lots in absolute base lots (PerpShortExceedsSizeCap). There is no on-chain open-interest check — OI discipline is applied off-chain when that cap is set
Funding rate sanity
Venue-reported rates outside a hard sanity band revert with FundingRateInsane
NAV change cap
attest_nav deltas bounded by max_nav_change_bps_per_hour
Withdrawal FIFO
Strict FIFO order via WithdrawalNotNextInQueue guard; per-request price-at-process haircut so depositors never extract more than pro-rata
Reserve fund segregation
Reserve USDC sits in its own ATA, lent under a distinct cToken ATA; excluded from effective_nav_usdc
Account-context layering (strategy instructions)
open_position, close_position, and emergency_close each call several other programs in a single transaction: Phoenix, Ember, Kamino, and Jupiter. Since every one of those needs its own set of accounts, the handler has to divide up one long list. It does that in three steps:
Takes the number of accounts per group as
u8arguments.Splits
remaining_accountsusingchecked_add(...).ok_or(InvalidParams), so an offset that would overflow reverts instead of wrapping around.Hands each slice to the right helper, which then checks the accounts it was given.
The result is that the program never reads past the accounts it was actually handed, even if someone passes deliberately bad arguments. And each venue still validates its own accounts, so the vault isn't the only thing standing between a bad call and a bad outcome.
Audit status
The program is currently unaudited. An audit is planned before mainnet with a top-tier Solana firm (Ottersec, Sec3, or Neodyme). The scope is small enough to audit properly: one Anchor crate and one state struct.
Internal pre-audit notes are in the source.
Don't deploy significant capital before the audit.
Reporting
Suspected vulnerabilities:
Open a private GitHub security advisory on github.com/kamwithak/keystone-contracts, or
Contact the maintainer directly
A formal bug bounty stands up alongside the mainnet audit.
Related
Errors — every revert path enumerated
Last updated