# Introduction

**ksUSD converts SOL staking yield into dollar yield.** You deposit USDC and hold ksUSD, and its price drifts up as the vault earns.

The vault holds staked SOL (jitoSOL) and shorts SOL-PERP against it. The hedge is what makes the conversion possible: it cancels SOL's price moves, so what reaches you is the staking yield denominated in dollars rather than in a volatile asset. When perp funding is positive, the hedge pays you on top. When it stops paying, the vault parks in USDC lending.

Staking is the engine here, not funding — the jitoSOL leg on its own earns more than the vault's whole net APY. See [the whitepaper](/reference/whitepaper#i-what-it-converts) for the breakdown.

> **Pre-deployment.** Not yet on mainnet. v1 hedges on Phoenix Perps (Ellipsis Labs), posting USDC as margin via Ember with jitoSOL held unlevered as the spot leg. Backtested funding is a proxy, since Phoenix only launched Dec 2025.

* Deposit USDC and get ksUSD at the current share price.
* Hold it. There is no staking step and nothing to claim. Carry accrues into the price.
* Withdraw whenever you want. Small amounts pay out instantly, larger ones queue.

The vault picks its own mode from an on-chain funding signal and rebalances on a schedule. Nobody has to steer it. See [Strategy & Modes](/how-it-works/strategy-and-modes).

***

## How it earns

| Yield source              | Active when             | Mechanism                                                                                                                                                                                         |
| ------------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **jitoSOL staking yield** | Normal basis            | **Most of the return.** About 7% APR, embedded in the jitoSOL/SOL rate. This is the spot leg of the hedge.                                                                                        |
| Phoenix SOL-PERP funding  | Normal basis            | Upside, not the engine. The vault shorts the perp and collects funding when it clears the dynamic threshold. Phoenix has been paying near zero, so treat this as a bonus rather than a base case. |
| USDC lending carry        | Buffer, reserve, parked | Lent on the Kamino USDC reserve. Everything the vault holds runs here whenever hedging isn't worth it.                                                                                            |

**Funding** is a payment perp traders make to each other every few hours to keep the perp price near spot. When more people are long than short, the longs pay the shorts, so a short position gets paid to sit there. **Delta-neutral** means the vault holds staked SOL and shorts the same amount of SOL, so the two cancel and SOL's price stops mattering to your return — near zero, not exactly zero. See [Risk](/how-it-works/volatility-risk-management).

When the hedge stops paying for itself, the short closes, jitoSOL converts to USDC, and everything runs as USDC lending until it's worth running again.

***

## How to use it

```
deposit                  USDC → mint ksUSD at current share price
withdraw_instant         burn ksUSD → USDC from the vault's liquidity buffer
request_withdrawal       burn ksUSD now (locks redemption price), queued for payout
process_withdrawal       permissionless crank — fulfill queued requests in FIFO order
claim_wind_down          (wind-down only) burn ksUSD → pro-rata USDC
```

* The ksUSD share price drifts up over time.
* Performance fees apply only above the high-water mark, at the vault level.
* [Fee structure →](/reference/fees)

***

## Why Solana

The strategy needs four things in one place: an on-chain perp to hedge with, a high-yield staking token, deep USDC lending, and fees cheap enough to rebalance often. Solana has all four close enough together to compose.

Ethereum doesn't line up the same way. Gas makes frequent rebalancing expensive, and staking yield, funding, and lending live in separate places. The spot leg also earns less there: jitoSOL runs near 5.8% on Solana staking plus MEV, against roughly 3% for Ethereum staking tokens. Full detail is in the [whitepaper](/reference/whitepaper#iv-why-solana).

***

## Why on-chain carry, not RWA

RWA dollars earn whatever T-bills earn, so their yield rises and falls with interest rates. ksUSD earns from crypto funding, staking, and lending, which move on their own schedule.

So it isn't a higher-yielding RWA dollar. It's a different kind of dollar: uncorrelated to the Fed, and fully on-chain, so you can check the yield and the collateral yourself instead of trusting an off-chain custodian.

***

## Doc map

**Users:** [Deposit](/keystone-finance/deposit) · [Withdraw](/keystone-finance/withdraw) · [Check position](/keystone-finance/check-position)

**Builders:** [Quick start](/for-developers/quick-start) · [Instructions](/for-developers/instructions) · [Accounts](/for-developers/accounts) · [Events](/for-developers/events) · [Errors](/for-developers/errors)

**Operators:** [Keeper bot](/for-operators/keeper-bot) · [Monitoring](/for-operators/monitoring) · [Admin ops](/for-operators/admin-ops)

**Reference:** [Whitepaper](/reference/whitepaper) · [Historical simulation](/reference/historical-simulation) · [Fees](/reference/fees) · [NAV & pricing](/reference/nav-calculation) · [Security](/reference/security) · [Protocols](/reference/protocols)

***

## Program

| Network | Program                                        | Address                                        |
| ------- | ---------------------------------------------- | ---------------------------------------------- |
| Devnet  | `keystone_finance` (ksUSD vault)               | `E7tpCcxtvuTXLAckBGWb1AsndpLQ1Y9hQA3iGYSXz2vJ` |
| Mainnet | `E7tpCcxtvuTXLAckBGWb1AsndpLQ1Y9hQA3iGYSXz2vJ` | Pending audit / deploy                         |


# Glossary

Quick definitions for terms used across the ksUSD docs.

A few come up everywhere, so they're defined once here:

* **Carry** — income you collect for holding a position, rather than for correctly guessing a price move.
* **Funding** — a payment perp traders make to each other every few hours to keep the perp price near spot. When more traders are long than short, longs pay shorts, so a short position gets paid to sit there.
* **NAV** — net asset value, the total dollar value of everything the vault holds.
* **Delta-neutral** — holding an asset and shorting the same amount of it, so its price stops affecting your return.

***

## Product

| Term                    | Definition                                                                                                                                                                                                                                                                                                                       |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **ksUSD**               | A dollar backed by staked SOL. It converts SOL staking yield into dollar yield by holding jitoSOL and hedging the price risk away; funding and lending add to it. The price drifts up as that accrues.                                                                                                                           |
| **Vault**               | The single Anchor program and its account, which holds the assets, mints ksUSD, and runs the strategy.                                                                                                                                                                                                                           |
| **Coordinates carry**   | ksUSD gathers jitoSOL staking, Phoenix funding, and lending carry into one token, run by one vault.                                                                                                                                                                                                                              |
| **Gross effective NAV** | `cached_nav_usdc − queue_pending_usdc − reserve_fund_usdc`. What the high-water mark is measured against.                                                                                                                                                                                                                        |
| **Effective NAV**       | `gross_effective_nav_usdc − pending_perf_fees_usdc`. What holders actually own, and what deposits and withdrawals price against.                                                                                                                                                                                                 |
| **Share price**         | `effective_nav_usdc × 1e9 / total_shares` (scaled by 1e9). What 1 ksUSD is worth in USDC.                                                                                                                                                                                                                                        |
| **HWM**                 | High-water mark: the highest *gross* share price the vault has ever reached. Performance fees accrue only above it, and it never moves down.                                                                                                                                                                                     |
| **Accrued perf fees**   | `pending_perf_fees_usdc`. Performance fees already earned but not yet paid out. Set aside as they're earned, so the share price is always net of them.                                                                                                                                                                           |
| **Reserve fund**        | A pool of USDC built from the 5% skim on performance fees, lent on Kamino. It absorbs first losses **up to its size**, and it grows slowly — treat it as an operational buffer, not as depeg or exploit cover. Paying out is a discretionary admin action (`pay_from_reserve`), never automatic. Tracked as `reserve_fund_usdc`. |

## Modes (`PositionMode`)

| Term             | Definition                                                                                                                                                                                                                                                                                        |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Parked**       | No perp position. Everything sits in USDC, lent on Kamino via `lend_idle_usdc`. This is the default when funding is thin or negative. (The enum variant is named `Idle`.)                                                                                                                         |
| **Normal basis** | Hold jitoSOL outright for staking yield, and short an equal amount of SOL-PERP on Phoenix (1× notional) with USDC as margin. Runs when funding clears the threshold by the ±3% band. The threshold is usually near 0%, and the program floors it so the vault never shorts into negative funding. |
| **WindDown**     | The shutdown mode, set by `init_wind_down`. No new deposits or positions; everyone redeems their share via `claim_wind_down`.                                                                                                                                                                     |
| **Dwell**        | The minimum time the vault must stay in one mode before it can switch again. Default 12 hours.                                                                                                                                                                                                    |
| **Funding EMA**  | A running average of the funding rate that weights recent readings more heavily (`funding_apr_smoothed_bps`). Updated on every `settle` and compared against `funding_threshold_*_bps` to pick the mode.                                                                                          |

## Withdrawal

| Term                   | Definition                                                                                                                                                                    |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Liquidity buffer**   | Idle USDC the vault keeps on hand so withdrawals can pay out instantly. Default 10% of NAV.                                                                                   |
| **Instant withdrawal** | `withdraw_instant`. One transaction, paid straight out of the buffer.                                                                                                         |
| **Queued withdrawal**  | `request_withdrawal` burns your shares and locks the amount you're owed. Later, `process_withdrawal` pays it out and closes the request. Anyone can trigger that second step. |
| **Queue cursor**       | `queue_next_id` and `queue_processed_through` on the vault. Together they track which requests are still outstanding.                                                         |

## On-chain accounts

A **PDA** is an account the program controls directly, with no private key behind it. An **ATA** is the standard account a wallet holds a particular token in.

| Term                       | Definition                                                                                                                                                                  |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Vault PDA**              | Seeds `[b"vault"]`. Signs every token operation, and is the authority over the ksUSD mint and all vault-owned token accounts.                                               |
| **WithdrawalRequest**      | One account per queued request, at `[b"withdrawal_request", vault, request_id]`. Closed by `process_withdrawal`. Processed strictly in order via `queue_processed_through`. |
| **Reserve ATA**            | A vault-owned USDC account used only for the reserve fund (`vault.reserve_ata`). Fixed at `initialize`.                                                                     |
| **Phoenix trader account** | The Phoenix Perps trader and subaccount PDAs the vault owns, set up through the Rise SDK builder path. Created by `enable_phoenix`.                                         |
| **Kamino lending cTokens** | The receipts for USDC lent on Kamino: `vault_lend_collateral_ata` for the idle-USDC leg and `reserve_lend_collateral_ata` for the reserve. Both fixed by `enable_lending`.  |

## External protocols

"CPI" below means one Solana program calling another within a transaction, which is how the vault operates on these venues.

| Term                    | Definition                                                                                                                                                                                                                                                       |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Phoenix Perps**       | The on-chain perp exchange from Ellipsis Labs, with real two-sided funding. It's the v1 hedge venue. USDC margin goes in through **Ember**, and the vault calls it directly on-chain using the Rise SDK. Program `EtrnLzgbS7nMMy5fbD42kXiUzGg8XQzJ972Xtk1cjWih`. |
| **Ember**               | The program that turns USDC into Phoenix perp margin. Program `EMBERpYNE6ehWmXymZZS2skiFmCa9V5dp14e1iduM5qy`.                                                                                                                                                    |
| **Kamino KLend**        | The lending protocol the vault uses for USDC (`lend_idle_usdc`, `lend_reserve`).                                                                                                                                                                                 |
| **Marginfi**            | Held in reserve as a second USDC lending venue in v1.1. The vault has fields for it, but it isn't wired up yet.                                                                                                                                                  |
| **Jupiter V6**          | A swap aggregator. Every USDC ↔ jitoSOL swap routes through it.                                                                                                                                                                                                  |
| **Pyth (pull oracles)** | The price source for SOL/USD and jitoSOL/USD. Every read is checked for staleness (5 min) and confidence (2%). Set via `set_oracles`.                                                                                                                            |
| **Jito (jitoSOL)**      | A liquid staking token. Its yield comes from staking rewards plus MEV tips, both baked into the jitoSOL/SOL exchange rate.                                                                                                                                       |

## Off-chain

| Term        | Definition                                                                                                                                                                                                                                                                                           |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Keeper**  | The off-chain bot that runs the vault's routine instructions on a schedule. Its key is pinned as `authorized_keeper`, and it's the only signer that can open or close positions, move capital in and out of lending, or call `attest_nav`.                                                           |
| **Cranker** | Anyone who pays to run an instruction that needs no permission: `settle`, `process_withdrawal`, and `emergency_close` once tripped. One nuance on `settle` — anyone can call it to refresh NAV and the depeg guard, but only the keeper's call updates the funding signal that gates position entry. |
| **Admin**   | Whoever holds the admin key. Required for `set_pause`, `update_params`, `transfer_admin`, and `collect_fees`. This becomes a multisig before launch.                                                                                                                                                 |

## Backtest

| Term               | Definition                                                                                                                                                                                                                                                                                      |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Target APY**     | Regime-dependent, so read the drawdown first: **−0.3%** through the 2025–26 funding compression. Yield is **\~6% net** when funding is thin, as it is now, scaling toward \~11% when funding is rich. That figure is fully loaded — already net of fees, trading costs, and the margin haircut. |
| **Daily backtest** | Daily figures over a 24-month window, Jul 2024 – Jun 2026, covering the two modes v1 actually runs. Phoenix only launched in Dec 2025, so it uses historical SOL-perp funding as a stand-in. Method: [historical-simulation.md](/reference/historical-simulation).                              |
| **Funding proxy**  | Binance funding taken at face value, on the basis that Phoenix prices its own funding off CEX index feeds. It still flatters the funding leg against what Phoenix has actually paid — the staking leg is what carries the result.                                                               |


# What is ksUSD?

**ksUSD converts SOL staking yield into dollar yield.** Hold it and its price rises as the vault earns.

Solana staking pays well, but it pays in SOL — which is no use if you account in dollars. The vault holds staked SOL and hedges the price risk away, so what you keep is the yield without the asset. That hedge is the whole mechanism, and when perp funding is positive it pays you rather than costing you.

Under the hood it's deliberately small: one Anchor program, one vault account the program itself controls, and one token.

Most of the return is jitoSOL staking (about 7% APR). Phoenix SOL-PERP funding adds to it when the market pays, and USDC lending on Kamino (about 4–5% APR) is the floor when hedging isn't worth it. Full breakdown in the [Introduction](/#how-it-earns).

## Strategy in brief

There is one strategy, and it runs in one of two modes at a time.

**Normal basis.** The vault holds jitoSOL to earn staking yield, and shorts the same amount of SOL on Phoenix using USDC as margin. The long and the short cancel out, so SOL's price barely affects your return. This mode runs when funding clears the vault's threshold, which sits near 0%. The jitoSOL is held "unlevered", meaning it's owned outright with nothing borrowed against it.

**Parked.** When funding is thin or negative, the vault closes the short, sells the jitoSOL back to USDC, and lends everything on Kamino.

Parked always means USDC lending. It never holds tokenized T-bills.

[Strategy & Modes](/how-it-works/strategy-and-modes) has the full mechanics, the funding threshold, and the guardrails around switching.

***

## Related

* [Whitepaper](/reference/whitepaper) — full design, NAV math, security, risk disclosures
* [Deposit](/keystone-finance/deposit) · [Withdraw](/keystone-finance/withdraw) · [Check position & NAV](/keystone-finance/check-position)
* [Risk management](/how-it-works/volatility-risk-management) — drawdown guard, slippage bounds, dwell timers, depeg and staleness guards
* [Fee structure](/reference/fees) — 0% management, 20% performance above the high-water mark, 5% reserve skim


# Deposit

Send USDC to the ksUSD vault and get `ksUSD` back at the current share price. How many tokens you get depends on that price, so a deposit made later buys fewer tokens, each worth more.

> **New here?** Read [What is ksUSD?](/keystone-finance/reserve-asset) first.

***

## The instruction

```
deposit(usdc_amount: u64)
```

One transaction does all three steps, or none of them:

* Moves your USDC into the vault's USDC account
* Mints `ksUSD` to you at the current share price
* Updates the vault's record of what it holds

The accounts below use two Solana conventions. A **PDA** is an account the program controls directly, with no private key behind it. An **ATA** is the standard token account a wallet holds a given token in.

| Account              | Mutability | Purpose                                   |
| -------------------- | ---------- | ----------------------------------------- |
| `vault`              | mut        | Vault PDA (seeds: `[b"vault"]`)           |
| `vault_usdc_account` | mut        | Vault's USDC ATA — receives the deposit   |
| `ksusd_mint`         | mut        | ksUSD share mint — vault PDA is authority |
| `user_usdc_account`  | mut        | Depositor's USDC source ATA               |
| `user_ksusd_account` | mut        | Depositor's ksUSD destination ATA         |
| `user`               | signer     | Depositor                                 |
| `token_program`      | —          | SPL token program                         |

***

## Share-price math

```
effective_nav_usdc = cached_nav_usdc − queue_pending_usdc − reserve_fund_usdc
                                     − pending_perf_fees_usdc
share_price_1e9    = effective_nav_usdc × 1e9 / max(total_shares, 1)
shares_to_mint     = usdc_amount × 1e9 / share_price_1e9    // both sides 6 decimals
```

`deposit` calls `accrue_perf_fees` before it prices your mint. That advances the high-water mark to the current price first, so you're never charged a performance fee on gains the vault made before you arrived.

The first deposit into an empty vault mints shares 1:1 at $1.00, since the starting `share_price_1e9` is `1_000_000_000`. Every deposit after that mints fewer shares, because the share price has drifted up with the carry earned so far.

***

## Capacity

The vault ships with a **$250k deposit cap**. That isn't a demand estimate — it's roughly the largest short Phoenix's book can absorb, and it re-sizes as the venue grows.

It's a hard cap rather than a soft target because the vault only earns while its dollars are *in* the hedge. Whatever it can't short sits in USDC lending instead. A cap set above what the venue can hold doesn't add yield, then — it dilutes the part that does, until ksUSD is a Kamino deposit wearing a wrapper, carrying all of the operational risk for none of the carry.

So the cap tracks the venue:

```
max short = min( 15% of Phoenix SOL-PERP OI , 25% of the 30-day MEDIAN daily volume )
```

On current Phoenix numbers the volume limb binds, by roughly a factor of two, because exit is harder than entry — a clean unwind is about a quarter of a day's flow.

The word *median* is load-bearing. The 30-day mean runs well above the median, since a few spike days sit inside any window, and sizing off the mean would imply a short the venue couldn't absorb on an ordinary day.

### The ramp

| Phase        | Cap        | Opens when Phoenix has                                         |
| ------------ | ---------- | -------------------------------------------------------------- |
| **Launch**   | **$250k**  | roughly today's book                                           |
| Private beta | $1M        | a few times today's median daily volume                        |
| Public       | $5M → $25M | a far deeper book, or a second venue to split the short across |

Tiers open against the live book, not on a date. Drift went down in January 2025, so there's no second Solana perp venue wired into v1 to split across yet.

### Reading the cap live

`deposit_cap_usdc` is a `u64` on the `Vault` account. `0` pauses deposits outright, `u64::MAX` means uncapped, anything else is a hard ceiling.

```ts
const vault = await program.account.vault.fetch(vaultPda);

// Headroom is measured against CACHED nav, not effective nav: queued
// withdrawals and the reserve fund are netted out of TVL but still occupy
// cap room, so effective NAV would overstate what you can actually deposit.
const remaining = BN.max(
  vault.depositCapUsdc.sub(vault.cachedNavUsdc),
  new BN(0)
);
```

Admins move the cap with `update_params`. Recompute it against the live book first — `npm run phoenix:capacity` — rather than trusting a constant, since both inputs move week to week.

***

## Guardrails

A deposit can be blocked by any of three things:

* **The vault is paused.** The admin can pause it with `set_pause` in an emergency.
* **The deposit cap is hit.** A deposit reverts with `DepositCapExceeded` if `cached_nav_usdc + usdc_amount > deposit_cap_usdc`. See [Capacity](#capacity) above.
* **The amount is zero.** `usdc_amount` must be greater than 0.

***

## TypeScript example

```ts
import { Program, BN } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import { TOKEN_PROGRAM_ID, getAssociatedTokenAddressSync } from "@solana/spl-token";

const PROGRAM_ID = new PublicKey("E7tpCcxtvuTXLAckBGWb1AsndpLQ1Y9hQA3iGYSXz2vJ");

const [vaultPda] = PublicKey.findProgramAddressSync(
  [Buffer.from("vault")],
  PROGRAM_ID
);

const vault = await program.account.vault.fetch(vaultPda);
const userUsdcAta  = getAssociatedTokenAddressSync(vault.usdcMint,  user.publicKey);
const userKsusdAta = getAssociatedTokenAddressSync(vault.ksusdMint, user.publicKey);
const vaultUsdcAta = getAssociatedTokenAddressSync(vault.usdcMint,  vaultPda, true);

const tx = await program.methods
  .deposit(new BN("100_000_000"))         // 100 USDC (6 decimals)
  .accountsStrict({
    vault: vaultPda,
    vaultUsdcAccount: vaultUsdcAta,
    ksusdMint: vault.ksusdMint,
    userUsdcAccount: userUsdcAta,
    userKsusdAccount: userKsusdAta,
    user: user.publicKey,
    tokenProgram: TOKEN_PROGRAM_ID,
  })
  .rpc();
```

***

## Events

`DepositEvent` is emitted on success:

```rust
pub struct DepositEvent {
    pub user: Pubkey,
    pub usdc_amount: u64,
    pub shares_minted: u64,
    pub share_price_1e9: u64,
    pub new_total_shares: u64,
    pub timestamp: i64,
}
```

***

## Related

* [Withdraw](/keystone-finance/withdraw) — instant vs. queued paths
* [Check position & NAV](/keystone-finance/check-position)
* [NAV & share pricing](/reference/nav-calculation)
* [Errors](/for-developers/errors)


# Withdraw

You hand back `ksUSD` and get USDC. There are two normal ways to do that, plus a third that only applies if the vault is ever shut down.

| Path                                                     | When to use                                  | Speed                                                                                                     |
| -------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| **Instant** (`withdraw_instant`)                         | Vault has enough idle USDC                   | One transaction                                                                                           |
| **Queued** (`request_withdrawal` + `process_withdrawal`) | Withdrawal larger than the liquidity buffer  | Burned immediately at locked price; USDC paid out (FIFO, strict) after the keeper unwinds enough position |
| **Wind-down** (`claim_wind_down`)                        | Active only when `position_mode == WindDown` | Pro-rata redemption against the vault's idle USDC; no queue, no FIFO                                      |

Instant is the path almost everyone uses. The vault keeps a liquidity buffer of idle USDC on hand, 10% of what it holds by default, which is enough to cover about 99% of real withdrawals on the spot.

***

## Instant withdrawal

```
withdraw_instant(shares: u64)
```

* Burns `shares` of `ksUSD`
* Pays out USDC from the vault's idle balance at the current share price

| Account              | Mutability | Purpose                                 |
| -------------------- | ---------- | --------------------------------------- |
| `vault`              | mut        | Vault PDA                               |
| `vault_usdc_account` | mut        | Vault USDC ATA — source of payout       |
| `ksusd_mint`         | mut        | ksUSD share mint                        |
| `user_ksusd_account` | mut        | User's ksUSD ATA (must hold ≥ `shares`) |
| `user_usdc_account`  | mut        | User's USDC destination                 |
| `user`               | signer     | Withdrawer                              |
| `token_program`      | —          | SPL token program                       |

Math:

```
usdc_owed = shares × share_price_1e9 / 1e9
```

If `vault_usdc_account.amount < usdc_owed`, this reverts with `InsufficientLiquidityBuffer`. Use the queued path instead.

***

## Queued withdrawal

This is what happens when the vault doesn't have enough idle USDC to pay you on the spot.

Your shares burn right away at the current share price, so the dollar amount you're owed is locked in at that moment and can't drift while you wait. The request is recorded in its own account. Once the keeper has unwound enough of the position to refill the buffer, anyone can trigger the payout, including you.

### Step 1 — request

```
request_withdrawal(shares: u64)
```

* Burns shares
* Creates a request PDA at `[b"withdrawal_request", vault, queue_next_id]`
* Locks `usdc_owed = shares × share_price_1e9 / 1e9` on the request record

| Account                            | Mutability  | Purpose                                           |
| ---------------------------------- | ----------- | ------------------------------------------------- |
| `vault`                            | mut         | Vault PDA                                         |
| `withdrawal_request`               | init        | New per-request PDA (payer = user)                |
| `ksusd_mint`                       | mut         | ksUSD share mint                                  |
| `user_ksusd_account`               | mut         | User's ksUSD ATA (burned from)                    |
| `user_usdc_account`                | —           | User's USDC destination (recorded on the request) |
| `user`                             | signer, mut | Requester (pays rent)                             |
| `token_program` / `system_program` | —           | —                                                 |

### Step 2 — process (permissionless crank)

```
process_withdrawal()
```

* Anyone can call this, including the person who queued the request.
* The queue is strictly first-in, first-out. The next request processed must be `queue_processed_through + 1`; anything else reverts with `WithdrawalNotNextInQueue`.
* The payout is capped at the live share price. So if the vault's value has fallen since you queued, you get your fair share of it and no more.
* The request account closes on payout, refunding its rent deposit to whoever opened it.

| Account              | Mutability | Purpose                                      |
| -------------------- | ---------- | -------------------------------------------- |
| `vault`              | mut        | Vault PDA                                    |
| `withdrawal_request` | mut        | The queued request (will be closed)          |
| `vault_usdc_account` | mut        | Vault USDC ATA — source of payout            |
| `user_usdc_account`  | mut        | Recorded destination from the request        |
| `recipient_for_rent` | mut        | Original requester (rent refund destination) |
| `cranker`            | signer     | Anyone can crank                             |
| `token_program`      | —          | —                                            |

***

## TypeScript — instant path

```ts
const tx = await program.methods
  .withdrawInstant(new BN("25_000_000"))    // 25 ksUSD
  .accountsStrict({
    vault: vaultPda,
    vaultUsdcAccount: vaultUsdcAta,
    ksusdMint: vault.ksusdMint,
    userKsusdAccount: userKsusdAta,
    userUsdcAccount: userUsdcAta,
    user: user.publicKey,
    tokenProgram: TOKEN_PROGRAM_ID,
  })
  .rpc();
```

## TypeScript — queued path

```ts
const nextId = vault.queueNextId.toString();
const [requestPda] = PublicKey.findProgramAddressSync(
  [
    Buffer.from("withdrawal_request"),
    vaultPda.toBuffer(),
    new BN(nextId).toArrayLike(Buffer, "le", 8),
  ],
  PROGRAM_ID
);

await program.methods
  .requestWithdrawal(new BN("100_000_000_000"))   // 100,000 ksUSD
  .accountsStrict({
    vault: vaultPda,
    withdrawalRequest: requestPda,
    ksusdMint: vault.ksusdMint,
    userKsusdAccount: userKsusdAta,
    userUsdcAccount: userUsdcAta,
    user: user.publicKey,
    tokenProgram: TOKEN_PROGRAM_ID,
    systemProgram: SystemProgram.programId,
  })
  .rpc();

// Later, after the keeper has unwound enough position:
await program.methods
  .processWithdrawal()
  .accountsStrict({
    vault: vaultPda,
    withdrawalRequest: requestPda,
    vaultUsdcAccount: vaultUsdcAta,
    userUsdcAccount: userUsdcAta,
    recipientForRent: user.publicKey,
    cranker: anyWallet.publicKey,
    tokenProgram: TOKEN_PROGRAM_ID,
  })
  .rpc();
```

***

## Wind-down path

This only applies if the vault is being shut down for good. It is not a normal operating mode.

Once the admin calls `init_wind_down`, the vault stops taking deposits and stops opening positions. Everyone then redeems with:

```
claim_wind_down(shares: u64)
```

* Pays out your share of the vault's idle USDC at the live `effective_nav` share price.
* No queue, no ordering, and no rent refund.
* Reverts unless `position_mode == WindDown`.

***

## Events

* `WithdrawEvent` — emitted by `withdraw_instant`
* `WithdrawalRequested` — emitted by `request_withdrawal`
* `WithdrawalProcessed` — emitted by `process_withdrawal`
* `WindDownClaimed` — emitted by `claim_wind_down`

***

## Related

* [Deposit](/keystone-finance/deposit) · [Check position & NAV](/keystone-finance/check-position)
* [Fees](/reference/fees) — no withdrawal fees on either path
* [Errors](/for-developers/errors)


# Check Position & NAV

How to read your `ksUSD` balance, the current share price, and the total value of what the vault holds (its NAV).

Your balance never changes on its own. What changes is the share price, so the same balance is worth more over time.

***

## Your position

```ts
import { PublicKey } from "@solana/web3.js";
import { getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token";

const [vaultPda] = PublicKey.findProgramAddressSync(
  [Buffer.from("vault")],
  PROGRAM_ID
);

const vault = await program.account.vault.fetch(vaultPda);
const userKsusdAta = getAssociatedTokenAddressSync(vault.ksusdMint, user.publicKey);
const ksusdAccount = await getAccount(connection, userKsusdAta);

const shares = BigInt(ksusdAccount.amount.toString());
console.log("Your ksUSD:", Number(shares) / 1e6);   // 6 decimals
```

***

## Current share price

```ts
// Mirrors the on-chain Vault::share_price_1e9() helper.
const cachedNav = BigInt(vault.cachedNavUsdc.toString());
const queuePending = BigInt(vault.queuePendingUsdc.toString());
const reserveFund = BigInt(vault.reserveFundUsdc.toString());
const pendingPerfFees = BigInt(vault.pendingPerfFeesUsdc.toString());
const totalShares = BigInt(vault.totalShares.toString());

// Gross basis: what the high-water mark is measured against.
const grossEffectiveNav = cachedNav - queuePending - reserveFund;
// Net basis: what deposits and withdrawals actually price against.
const effectiveNav = grossEffectiveNav - pendingPerfFees;

const sharePrice1e9 = totalShares === 0n
  ? 1_000_000_000n
  : (effectiveNav * 1_000_000_000n) / totalShares;                 // 1e9-scaled

const sharePriceUsdc = Number(sharePrice1e9) / 1e9;                // dollars per ksUSD
console.log("Share price:", sharePriceUsdc.toFixed(6));
```

The share price starts at `1.000000` and rises as carry accrues. It only goes down in a drawdown.

Performance fees apply only to new gains above the highest price the vault has previously reached, tracked as `hwm_share_price_1e9`. So recovering from a dip is free.

***

## Your USDC-equivalent value

```ts
const yourUsdc = (shares * BigInt(Math.round(sharePrice1e9))) / 1_000_000_000n;
console.log("Withdrawable USDC:", Number(yourUsdc) / 1e6);
```

***

## Vault-level NAV

```ts
console.log("Cached NAV (USDC):       ", Number(vault.cachedNavUsdc.toString()) / 1e6);
console.log("Effective NAV (USDC):    ", Number(effectiveNav) / 1e6);
console.log("Queue pending (USDC):    ", Number(vault.queuePendingUsdc.toString()) / 1e6);
console.log("Reserve fund (USDC):     ", Number(vault.reserveFundUsdc.toString()) / 1e6);
console.log("Accrued perf fees (USDC):", Number(vault.pendingPerfFeesUsdc.toString()) / 1e6);
console.log("Total shares (ksUSD):    ", Number(vault.totalShares.toString()) / 1e6);
console.log("Position mode:           ", vault.positionMode);   // Idle / Normal / WindDown
console.log("Liquidity buffer bps:    ", vault.liquidityBufferBps);
console.log("Last settle ts:          ", new Date(Number(vault.lastSettleTs) * 1000));
console.log("Last NAV attest ts:      ", new Date(Number(vault.lastNavAttestTs) * 1000));
console.log("Funding EMA (bps):       ", vault.fundingAprSmoothedBps);
console.log("Peak share price 1e9:    ", vault.peakSharePrice1e9.toString());
console.log("HWM share price 1e9:     ", vault.hwmSharePrice1e9.toString());
console.log("Paused:                  ", vault.paused);
```

`cached_nav_usdc` refreshes on every deposit, withdrawal, and fee collection, and whenever someone runs `settle` or `attest_nav`. In between it holds the last recorded snapshot, so any profit or loss sitting open on the perp venue shows up at the next refresh rather than in real time.

***

## Pending queued withdrawal (if any)

```ts
// Derive your most recent withdrawal request PDA.
const [requestPda] = PublicKey.findProgramAddressSync(
  [
    Buffer.from("withdrawal_request"),
    vaultPda.toBuffer(),
    new BN(myRequestId).toArrayLike(Buffer, "le", 8),  // recorded when you called request_withdrawal
  ],
  PROGRAM_ID
);

const req = await program.account.withdrawalRequest.fetchNullable(requestPda);
if (req) {
  console.log("USDC owed:    ", Number(req.usdcOwed.toString()) / 1e6);
  console.log("Requested at: ", new Date(Number(req.requestedTs) * 1000));
  console.log("Processed:    ", req.processed);
}
```

The request account closes once `process_withdrawal` runs, and its rent deposit comes back to you.

***

## Related

* [Deposit](/keystone-finance/deposit) · [Withdraw](/keystone-finance/withdraw)
* [NAV & share pricing](/reference/nav-calculation)
* [Account structure](/for-developers/accounts) — full `Vault` and `WithdrawalRequest` layouts
* [Events](/for-developers/events) — programmatic subscription to NAV-changing events


# Strategy & Modes

ksUSD runs one trade. The vault holds staked SOL and shorts the same amount of SOL on a perp market, so the two cancel out and SOL's price stops mattering. Traders call this a delta-neutral basis trade.

At any moment the trade is either on or off. A funding-rate signal decides which, and a keeper bot flips the switch. Either way the vault's net SOL exposure stays near zero, so holding ksUSD is not a bet on SOL's price.

<figure><img src="/files/EimaYeZz49FEhjtDgiPf" alt="Two modes: Normal basis shorts SOL-PERP at 1× and holds jitoSOL unlevered, earning funding plus staking; Parked holds no perp and lends the entire NAV as USDC on Kamino, earning lending yield."><figcaption><p>The protocol is always in exactly one of two states.</p></figcaption></figure>

***

## The two modes

**Normal basis, the trade is on.** The vault holds jitoSOL outright, with nothing borrowed against it. This is the spot leg, and it earns staking yield. Against it, the vault shorts an equal amount of SOL-PERP on Phoenix, posting USDC as margin through Ember. The long and the short offset, so SOL price moves largely cancel and net exposure sits near zero — not exactly zero, since jitoSOL can drift against SOL and delta moves between rebalances (see [Risk](/how-it-works/volatility-risk-management)). The vault collects both things at once: staking yield on the jitoSOL, and funding on the short. jitoSOL is never posted as perp collateral.

**Parked, the trade is off.** The vault closes the short, sells the jitoSOL back to USDC, and lends everything on Kamino. This is the default whenever funding doesn't pay enough to be worth it. The liquidity buffer stays in the vault's own USDC account the whole time, so instant withdrawals keep working.

***

## When does the vault turn the trade on?

Most basis products just ask "is funding positive?" That's the wrong question here, because the jitoSOL you hold while short is *also* earning staking yield. The real question is whether staking plus funding beats plain USDC lending, after costs.

Work that out and the break-even lands at slightly *negative* funding, around −1.3%. Staking on its own already out-earns USDC lending, so the short has room to cost a little and still leave the vault ahead.

That means the entry point isn't a fixed number. It's a formula:

```
threshold = LEND / (1 − m) − STAKE + fees + buffer
```

| Term     | Meaning                                                                 |
| -------- | ----------------------------------------------------------------------- |
| `LEND`   | USDC lending APY — what parking the cash earns                          |
| `STAKE`  | jitoSOL staking APY — what the spot leg earns                           |
| `m`      | margin fraction (\~9% of NAV, earns 0%)                                 |
| `fees`   | round-trip perp + swap cost                                             |
| `buffer` | risk cushion: perp exposure, liquidation on a rally, jitoSOL depeg tail |

Once fees and the risk cushion are in, that pulls the practical threshold back up near 0% funding. When USDC lending gets rich, parking competes harder and the threshold rises. When staking runs hot, it falls. The keeper recalculates it each cycle from live rates.

### Why there's a band, not a line

You can't run the vault right at the line. Funding is noisy and chops back and forth across zero. The vault picks its mode on a 7-day average, but it earns or pays funding day by day. Sit exactly on the line and it flips in and out constantly.

That isn't a rounding error. Dropping the band roughly quadruples the number of mode switches over the backtest window, and the fees on those switches eat most of the yield — enough on its own to be the difference between a working product and a pointless one.

So the vault uses a band:

* Enter the short only when funding is clearly above the threshold, about 3% past it.
* Hold the short until funding drops clearly below, about 3% under.
* In between, do nothing.

The ±3% band covers the round-trip cost of switching, so ordinary funding noise doesn't trigger a flip. It's enforced in two layers: the program holds a floor and will never open a short into negative funding, and the keeper computes the live threshold from lending-versus-staking rates and applies the band on top.

Without the band, a bare 0% line looks fine when funding sits well above zero, which is what you see in a bull market or in older venue data that ran hot. But it bleeds out when funding hovers near zero, which is exactly where Solana funding sits today. With the band, the same rule works in both regimes: it runs the short when funding clearly pays and parks in lending when it doesn't. On realistic CEX-anchored funding that nets about **6%** with a **−0.3%** max drawdown, scaling toward \~11% when funding runs rich.

The on-chain floor stays at 0 rather than chasing the −1.3% theoretical optimum. Shorting into negative funding to squeeze out a little more carry mostly just increases exposure to a jitoSOL depeg, for very little gain.

***

## Why Parked stays in USDC lending, not T-bills

Parked always means USDC lending, never a tokenized T-bill like USDY or OUSG:

* Tokenized securities carry permissioned transfer hooks, which break permissionless holding and redemption.
* Their non-US-person restrictions shrink the addressable market.
* They add a securities-law surface.

Anyone who wants T-bill exposure can redeem and buy a bill directly. ksUSD measures itself against USDC lending, not a T-bill rate.

***

## Mode-switch guardrails

The funding signal decides *whether* to switch. These parameters bound *how*.

The keeper only executes; it doesn't decide. It's a permissioned key (`authorized_keeper`) that cannot open a position the funding rule forbids. And the drawdown, depeg, and oracle-divergence guards can pause the vault without anyone's permission.

| Guard                             | Default                      | Effect                                                                                                                          |
| --------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Funding threshold                 | 0% floor + dynamic, ±3% band | Vault shorts only when funding clears the threshold by the band, holds until it drops below; the band + 7-day mean stop whipsaw |
| `min_dwell_seconds`               | 12 h                         | Minimum time in a mode before it can switch again                                                                               |
| `funding_max_staleness_seconds`   | 6 h                          | Opens revert with `FundingSignalStale` if `settle` is overdue                                                                   |
| `consecutive_dd_settles_required` | 2                            | A drawdown trip needs two consecutive bad settles                                                                               |
| `lst_depeg_bps`                   | 5%                           | `settle` auto-pauses the vault on a jitoSOL depeg                                                                               |
| `max_swap_slippage_bps`           | 0.5%                         | Jupiter swaps revert past this realized slippage                                                                                |

***

## Related

* [What is ksUSD?](/keystone-finance/reserve-asset) — overview and yield sources
* [Risk management](/how-it-works/volatility-risk-management) — drawdown guard, slippage bounds, dwell timers, depeg and staleness guards
* [Whitepaper](/reference/whitepaper) — full design, NAV math, security, risk disclosures


# Risk Management

> **Note.** v1 hedges on Phoenix Perps (USDC margin via Ember, jitoSOL held unlevered as the spot leg).

How the ksUSD vault handles volatility, slippage, and oracle stress.

**NAV** below is the total dollar value of everything the vault holds. A **settle** is a routine update anyone can trigger that refreshes the vault's numbers from the venues it uses.

***

## Built-in protections

| Layer                       | Mechanism                                                                                                                                                            | Default                               |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| **Liquidity buffer**        | Idle USDC kept on-vault for instant withdrawals                                                                                                                      | 10% of NAV                            |
| **Drawdown guard**          | `emergency_close` callable by anyone once `(peak − current) / peak ≥ emergency_close_dd_bps` AND `consecutive_dd_settles_observed ≥ consecutive_dd_settles_required` | 5% NAV drop, sustained 2 settles      |
| **Mode-switch dwell**       | Minimum time between mode changes to prevent ping-pong on noisy funding                                                                                              | 12 h                                  |
| **Funding EMA + staleness** | Smoothed funding on-chain; opens revert if stale                                                                                                                     | 6 h staleness                         |
| **Drawdown latency**        | Two consecutive sub-threshold settles required before drawdown trips                                                                                                 | `consecutive_dd_settles_required = 2` |
| **Slippage cap**            | Jupiter swaps revert if executed price worse than `max_swap_slippage_bps`                                                                                            | 0.5%                                  |
| **Oracle staleness**        | Pyth feed must have an update within the staleness window                                                                                                            | 5 min                                 |
| **Oracle confidence**       | Pyth `confidence / price` must be tight                                                                                                                              | < 2%                                  |
| **LST depeg check**         | `settle` auto-pauses if `jitoSOL / SOL < 1 − lst_depeg_bps`                                                                                                          | 5%                                    |
| **NAV change cap**          | `attest_nav` delta bounded per hour                                                                                                                                  | ±50%/hr                               |
| **HWM monotonicity**        | Performance fees can't be charged twice on the same gains                                                                                                            | Always                                |
| **Deposit cap**             | Total NAV cap to bound systemic exposure during early operation                                                                                                      | $250k at launch                       |
| **Reserve fund**            | Loss-absorbing buffer fed by 5% of perf fees, lent on Kamino                                                                                                         | Grows over time                       |
| **Wind-down path**          | Terminal admin instruction that blocks new state and switches users to pro-rata claim                                                                                | Manual                                |
| **Minimum margin**          | `open_position` rejects a position that posts less margin than this at the venue                                                                                     | ≥9% of deployed capital               |
| **Margin ceiling**          | `add_margin` refuses to post more than half of effective NAV to the venue                                                                                            | 50% of NAV                            |
| **Margin top-up**           | Keeper adds collateral when health falls, restoring it before liquidation                                                                                            | Automatic                             |

"EMA" above is an exponential moving average, a running average that weights recent readings more heavily. The vault uses one to smooth the funding rate so a single odd hour can't move the decision.

***

## On stress events

### Funding spikes (e.g. FTX-style)

In extreme conditions hourly funding can spike deeply negative. It hit −235% APR in Nov 2022. The smoothed funding rate and the dwell timer stop the vault from flipping on one bad sample, so it waits for a sustained signal.

In v1 the vault simply parks through deeply negative funding. It closes the short and holds USDC lending instead of chasing the move. Parking is the safer response and still earns.

### jitoSOL depeg

`settle` reads both Pyth feeds. If `jitoSOL/SOL` falls below `1 − lst_depeg_bps`, it reverts with `LstDepeg` and pauses the vault. Any existing position stays open, but nothing new can be opened. The admin then investigates and either unpauses or calls `emergency_close`.

### Oracle outage

The Pyth staleness and confidence checks make every strategy instruction that needs a SOL/USD price revert. Deposits and instant withdrawals don't read the oracle at all, so those keep working through a brief outage.

### Phoenix outage / market halt

Strategy instructions fail at the point where they call into Phoenix. Open positions keep accruing funding, since the venue settles that internally, and the next `settle` refreshes NAV. If a position has to be exited urgently, `emergency_close` can run as soon as the market can be read again.

### Sharp SOL rally

**The vault is not levered.** Its total exposure equals its NAV, and the short is sized 1:1 against the jitoSOL it holds — that 1:1 is what makes the book neutral in the first place. A SOL rally can't hurt the portfolio.

It can still hurt the *position*, because the two legs sit in different places. Only about 9% of NAV is posted at Phoenix as margin; the rest of what backs that short is the jitoSOL held here in the vault, which Phoenix cannot see. So on a rally the short's loss eats visible margin while the exactly offsetting jitoSOL gain sits somewhere outside Phoenix's accounting. From the venue's point of view the position looks thinly collateralised, and its liquidation engine acts on its own view — closing a position that was never economically at risk, and turning a paper wash into a real loss.

The drawdown guard cannot catch this. `emergency_close` triggers on NAV falling, and NAV *doesn't* fall in a hedged rally — the legs offset. The vault reads healthy right up to the moment Phoenix liquidates.

So the keeper watches it directly. The `margin-health` duty runs every tick, right after `settle`, and moves USDC into Phoenix margin when the cushion thins — funded by the leg that is gaining at that same moment. Every top-up is bounded twice: it can never draw on the withdrawal buffer, and total posted margin can never exceed half of effective NAV.

Margin only moves one way. There is no instruction that takes collateral back off a live position — surplus returns when the position closes, which the vault does on its own at the next mode flip. That costs a little yield while it sits, and buys the guarantee that nothing in the system can under-collateralise a live short.

This is an active defence rather than a static cushion, so **it depends on the keeper running.** If auto-margin is off or the keeper is down, the position should be closed rather than left to sit. Thresholds and the measured move sizes behind them: [keeper bot](/for-operators/keeper-bot#the-keeper-moves-margin-autonomously) and [instructions](/for-developers/instructions#why-margin-is-its-own-instruction).

### NAV reconciliation drift

If a keeper-reported NAV would move more than `max_nav_change_bps_per_hour` in one step, `attest_nav` reverts with `NavChangeExceedsCap`. The keeper then stages several smaller updates across consecutive hours. An outsized jump is a signal to investigate, not something to push through.

***

## What's *not* protected

* **Residual price risk. The hedge is near-zero, not zero.** The vault is delta-neutral by design, but four things stop the offset being exact:

  * **jitoSOL is not SOL.** The spot leg is jitoSOL and the short is SOL-PERP, so the two only cancel while the jitoSOL/SOL ratio behaves. A depeg is a direct loss; a 5% move trips the auto-pause, and the reserve fund is sized for the typical 50–150 bps case.
  * **Delta drifts between rebalances.** The short is sized when the position opens. Price moves after that leave the book slightly long or short until the keeper resizes it.
  * **Staking accrual is deliberately unhedged.** jitoSOL appreciates against SOL as staking accrues — that appreciation *is* the yield — so a short sized against SOL never covers it. The vault carries a small, intentional net long as a result.
  * **The perp can trade away from spot.** Mark and index diverge, especially under stress, and the position is marked against the perp.

  None of this is large in normal conditions, and the backtest's −0.3% max drawdown is measured through a real funding compression. But "no price risk" would be the wrong claim, and it is not the one made here.
* **Smart-contract bugs.** The program is unaudited until the planned pre-mainnet audit. Don't deploy significant capital until the results are published.
* **Counterparty insolvency.** A critical exploit or a compromised upgrade key at Phoenix, Ember, Kamino, Marginfi, Jupiter, or Jito would flow straight through into NAV losses.
* **Black-swan slippage.** The modeled max drawdown of **−0.3%** counts funding only, because the backtest doesn't simulate the price impact of trading the perp leg during rebalances. A realistic worst month under stress is **−2% to −5%**. The reserve fund cushions that but doesn't remove it.
* **Regulatory action.** A jurisdiction restricting access to Solana DeFi could squeeze redemption liquidity for affected users.

***

## Related

* [Security model](/reference/security)
* [NAV & share pricing](/reference/nav-calculation) · [Whitepaper — Risk](/reference/whitepaper#risk)


# Quick Start

Read the vault's state and call it from your own code.

The vault is a singleton, deployed and initialized by the Keystone admin. You don't stand one up yourself — you point a client at the existing program and read or transact against it.

***

## Prerequisites

Only Node.js 20+ is needed to build against the vault. The Rust and Anchor toolchain below is for compiling the program from source, which you only need if you're reviewing or modifying it.

| Tool       | Version                | Needed for                 |
| ---------- | ---------------------- | -------------------------- |
| Node.js    | 20+                    | Client integration         |
| Rust       | 1.79+ (rustup-managed) | Building the program       |
| Anchor CLI | 0.32.1+                | Building the program / IDL |
| Solana CLI | 1.18+                  | Building the program       |

```bash
git clone https://github.com/kamwithak/keystone-contracts.git
cd keystone-contracts
npm install
```

***

## Program ID

| Network | Program ID                                     |
| ------- | ---------------------------------------------- |
| Devnet  | `E7tpCcxtvuTXLAckBGWb1AsndpLQ1Y9hQA3iGYSXz2vJ` |
| Mainnet | *pending audit*                                |

* Single Anchor program: `keystone_finance`
* Implements the ksUSD vault end-to-end
* 27 instructions · 42 errors · 25 events · 2 account types

***

## Build

```bash
# Rust check (fast, no BPF toolchain required)
cargo check -p keystone-finance

# Generate / refresh IDL from source (uses idl-build feature)
PATH="$HOME/.cargo/bin:$PATH" anchor idl build \
  -o target/idl/keystone_finance.json
```

* IDL: `target/idl/keystone_finance.json` — regenerated to match latest source
* TS types: `target/types/keystone_finance.ts` — derived from the IDL

***

## What you can call

Anyone can call these, and they're the whole surface most integrations need:

| Instruction                                           | Does                                               |
| ----------------------------------------------------- | -------------------------------------------------- |
| `deposit(usdc_amount)`                                | USDC in, ksUSD out at the current share price      |
| `withdraw_instant(shares)`                            | ksUSD in, USDC out of the liquidity buffer         |
| `request_withdrawal(shares)` + `process_withdrawal()` | Queued path for withdrawals larger than the buffer |

Position management (`open_position`, `close_position`, `attest_nav`, the lending cranks) is gated to the vault's authorized keeper. `settle`, `process_withdrawal`, and `emergency_close` need no permission at all. Full list in the [instructions reference](/for-developers/instructions).

***

## TypeScript client

```ts
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Connection, PublicKey } from "@solana/web3.js";
import idl from "../target/idl/keystone_finance.json" with { type: "json" };

const PROGRAM_ID = new PublicKey("E7tpCcxtvuTXLAckBGWb1AsndpLQ1Y9hQA3iGYSXz2vJ");
const connection = new Connection("https://api.devnet.solana.com", "confirmed");

const provider = new anchor.AnchorProvider(connection, wallet, { commitment: "confirmed" });
anchor.setProvider(provider);

const program = new Program(idl as any, provider);

const [vaultPda] = PublicKey.findProgramAddressSync(
  [Buffer.from("vault")],
  PROGRAM_ID
);

const vault = await program.account.vault.fetch(vaultPda);
```

***

## Related

* [Instructions reference](/for-developers/instructions) — every entry point
* [Account structure](/for-developers/accounts) — `Vault` and `WithdrawalRequest` layouts
* [Events](/for-developers/events) · [Errors](/for-developers/errors)
* [Deposit](/keystone-finance/deposit) · [Withdraw](/keystone-finance/withdraw) · [Check position & NAV](/keystone-finance/check-position)


# 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: [`programs/keystone-finance/src/lib.rs`](https://github.com/kamwithak/keystone-contracts/tree/main/programs/keystone-finance/src/lib.rs) · IDL: `target/idl/keystone_finance.json`

***

## 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`                | 250\_000\_000\_000 ($250k) | 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](/keystone-finance/deposit)                                                            |
| `withdraw_instant(shares)`   | Burn ksUSD, receive USDC from the liquidity buffer. Reverts with `InsufficientLiquidityBuffer` if not enough idle USDC. → [Withdraw](/keystone-finance/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.

| Instruction                                                                                                                                                                                    | Description                                                                                                                                                                                                                                                                                                      |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `open_position(usdc_amount, margin_usdc, perp_short_base_lots, min_base_lots_to_fill, min_quote_lots_to_fill, min_jitosol_out, jupiter_swap_data, jupiter_account_count, arena_account_count)` | **Normal basis open.** Requires `Idle`. Split USDC → buy jitoSOL (unlevered spot leg) via Jupiter → post `margin_usdc` on 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(perp_close_account_count, settle_account_count, phoenix_withdraw_account_count, jupiter_swap_data)`                                                                            | **Normal basis close.** Reduce-only long order → settle funding → withdraw USDC margin → sell jitoSOL→USDC.                                                                                                                                                                                                      |
| `settle()`                                                                                                                                                                                     | Read SOL-PERP funding, settle funding if a position is open, refresh peak share price, log drawdown if guard tripped. Permissionless.                                                                                                                                                                            |
| `emergency_close(perp_close_account_count, phoenix_withdraw_account_count, jupiter_swap_data)`                                                                                                 | Force close on drawdown breach or while vault is paused. Auto-pauses vault on exit. Permissionless once tripped.                                                                                                                                                                                                 |
| `attest_nav(new_nav_usdc)`                                                                                                                                                                     | Refresh `cached_nav_usdc` to include unrealized perp PnL. Delta bounded by `max_nav_change_bps_per_hour`; over that it reverts with `NavChangeExceedsCap`. Calls `accrue_perf_fees`.                                                                                                                             |
| `lend_idle_usdc(usdc_amount)` / `unlend_usdc(collateral_amount)`                                                                                                                               | Move idle USDC into and out of the Kamino USDC reserve. Lending reverts with `LendingBreachesBuffer` if it would eat the liquidity buffer.                                                                                                                                                                       |
| `add_margin(usdc_amount, arena_account_count)`                                                                                                                                                 | 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. |
| `lend_reserve(usdc_amount)` / `unlend_reserve(collateral_amount)`                                                                                                                              | Same 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](/reference/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                          |
| `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-operators/keeper-bot) for the off-chain account-assembly conventions.

***

## Errors

See [Errors](/for-developers/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.


# Account Structure

> v1 hedges on **Phoenix Perps** (Ellipsis Labs) with USDC margin via the **Ember** program; jitoSOL is held unlevered as the spot leg. The perp-venue account set below follows the Rise SDK (TraderHeader, PermissionAccount, GlobalConfig, PerpAssetMap) and is finalized with the integration.

The ksUSD vault uses two on-chain account types: a single `Vault` PDA and one `WithdrawalRequest` PDA per queued withdrawal.

> Source: [`programs/keystone-finance/src/state/vault.rs`](https://github.com/kamwithak/keystone-contracts/blob/main/programs/keystone-finance/src/state/vault.rs)
>
> Anchor client name: `program.account.vault.fetch(vaultPda)` and `program.account.withdrawalRequest.fetch(requestPda)`.

***

## `Vault` (singleton)

PDA seed: `[b"vault"]` · authority for ksUSD mint, vault USDC ATA, vault jitoSOL ATA, reserve USDC ATA, Phoenix trader account, and lend / reserve cToken ATAs.

### Identity

| Field           | Type     | Meaning                                                                       |
| --------------- | -------- | ----------------------------------------------------------------------------- |
| `admin`         | `Pubkey` | Admin authority (transferable via two-step `transfer_admin` + `accept_admin`) |
| `pending_admin` | `Pubkey` | Pending admin during a transfer; `Pubkey::default()` = none pending           |
| `bump`          | `u8`     | Vault PDA bump                                                                |
| `usdc_mint`     | `Pubkey` | USDC mint (depositor token)                                                   |
| `jitosol_mint`  | `Pubkey` | jitoSOL mint (LST spot leg — held unlevered for staking)                      |
| `ksusd_mint`    | `Pubkey` | ksUSD share mint — vault PDA is mint + freeze authority                       |

### Phoenix integration

| Field                   | Type     | Meaning                                                                                                                                         |
| ----------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `phoenix_trader`        | `Pubkey` | The vault's Phoenix cross-margin trader PDA (authority = vault). Registered and capability-enabled off-chain via the Rise SDK, then pinned here |
| `phoenix_market`        | `Pubkey` | SOL-PERP orderbook (market) account                                                                                                             |
| `phoenix_global_config` | `Pubkey` | Phoenix GlobalConfig — constrained on every Phoenix CPI                                                                                         |
| `canonical_mint`        | `Pubkey` | Ember canonical collateral mint. USDC wraps into this 1:1 to become Phoenix margin                                                              |
| `phoenix_canonical_ata` | `Pubkey` | The vault's canonical-token ATA, i.e. its Phoenix margin token account                                                                          |
| `phoenix_perp_asset_id` | `u64`    | SOL-PERP asset id in the `PerpAssetMap` (0), used to read the signed position                                                                   |
| `phoenix_initialized`   | `bool`   | `true` after `enable_phoenix` succeeds                                                                                                          |

Trading capability itself is **not** a vault field. It lives on the Phoenix trader account as capability flags, granted by Ellipsis's `risk_authority`. A freshly registered trader can hold funds but can't post margin until that grant lands.

### Lending venues

| Field                          | Type     | Meaning                                                                 |
| ------------------------------ | -------- | ----------------------------------------------------------------------- |
| `kamino_lend_reserve`          | `Pubkey` | Pinned Kamino USDC reserve (set by `enable_lending`)                    |
| `marginfi_lend_bank`           | `Pubkey` | Marginfi USDC bank (v1.1 placeholder — not yet wired)                   |
| `vault_lend_collateral_ata`    | `Pubkey` | Vault's cToken ATA for Kamino lending                                   |
| `reserve_lend_collateral_ata`  | `Pubkey` | Reserve fund's cToken ATA — distinct so reserve yield tracks separately |
| `kamino_lending_initialized`   | `bool`   | `true` after `enable_lending` succeeds                                  |
| `marginfi_lending_initialized` | `bool`   | Reserved for v1.1                                                       |
| `usdc_lent_kamino`             | `u64`    | USDC base units deposited at Kamino from vault main ATA                 |
| `reserve_lent_kamino`          | `u64`    | USDC base units deposited at Kamino from reserve ATA                    |
| `usdc_lent_marginfi`           | `u64`    | Reserved for v1.1                                                       |
| `reserve_ata`                  | `Pubkey` | Reserve fund USDC ATA (vault-PDA-owned). Pinned at init.                |

### Oracles (set by `set_oracles`)

| Field                 | Type     | Meaning                              |
| --------------------- | -------- | ------------------------------------ |
| `sol_oracle`          | `Pubkey` | Pyth SOL/USD pull-oracle account     |
| `jitosol_oracle`      | `Pubkey` | Pyth jitoSOL/USD pull-oracle account |
| `oracles_initialized` | `bool`   | `true` after `set_oracles` succeeds  |

### NAV & share accounting (USDC base units, 6 decimals)

| Field                | Type  | Meaning                                                       |
| -------------------- | ----- | ------------------------------------------------------------- |
| `total_shares`       | `u64` | Total ksUSD outstanding (6 decimals)                          |
| `cached_nav_usdc`    | `u64` | Last computed NAV in USDC base units                          |
| `last_settle_ts`     | `i64` | Last `settle` crank timestamp                                 |
| `last_nav_attest_ts` | `i64` | Last `attest_nav` timestamp — anchors the per-hour change cap |

### Performance fee & reserve

| Field                    | Type  | Meaning                                                                                                                                                                                                                                                                                                 |
| ------------------------ | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hwm_share_price_1e9`    | `u64` | High-water mark, measured against the **gross** share price (1e9 scale). Only moves up. Advances on its own at `hurdle_apr_bps`/yr — see [Fees](/reference/fees)                                                                                                                                        |
| `hurdle_apr_bps`         | `u16` | Benchmark the vault must beat before any performance fee applies. Not a fixed constant — `init-v1.ts` measures the live Kamino USDC supply APY at deploy and writes it (4.22% → 422 as of 4 Aug 2026), and the keeper's `watchdog` warns when it drifts from the smoothed rate. `0` disables the hurdle |
| `last_fee_accrual_ts`    | `i64` | When `accrue_perf_fees` last ran. Drives the hurdle ratchet; `0` means "never initialised" and suppresses it                                                                                                                                                                                            |
| `peak_share_price_1e9`   | `u64` | Rolling peak share price (drawdown anchor). Reset only by `reset_peak`, never by fee collection                                                                                                                                                                                                         |
| `pending_perf_fees_usdc` | `u64` | Performance fees accrued but not yet collected. Crystallized by `accrue_perf_fees` on every price change, and **subtracted from `effective_nav_usdc`** so the share price is net of fees owed. Zeroed by `collect_fees`                                                                                 |
| `reserve_fund_usdc`      | `u64` | Reserve fund balance in USDC base units (`reserve_ata` balance + lent portion)                                                                                                                                                                                                                          |

### Position state

| Field                  | Type           | Meaning                                                                                                   |
| ---------------------- | -------------- | --------------------------------------------------------------------------------------------------------- |
| `position_mode`        | `PositionMode` | `Idle` / `Normal` / `WindDown`                                                                            |
| `position_base_amount` | `i64`          | Signed Phoenix position in **base lots** (negative = short SOL-PERP). SOL-PERP uses `baseLotDecimals = 2` |
| `usdc_margin_posted`   | `u64`          | USDC base units currently posted as Phoenix margin via Ember                                              |
| `last_mode_change_ts`  | `i64`          | Anchors the `min_dwell_seconds` guard                                                                     |

### Withdrawal queue cursors

| Field                     | Type  | Meaning                                                     |
| ------------------------- | ----- | ----------------------------------------------------------- |
| `queue_next_id`           | `u64` | Next request\_id to assign                                  |
| `queue_processed_through` | `u64` | Smallest request\_id not yet processed (strict FIFO cursor) |
| `queue_pending_usdc`      | `u64` | Total USDC owed across pending requests                     |

### Configurable parameters

| Field                             | Type  | Default                                                                                                                                                           |
| --------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `liquidity_buffer_bps`            | `u16` | 1\_000 (10%)                                                                                                                                                      |
| `funding_threshold_normal_bps`    | `i32` | 0 (safety floor — the keeper applies the tighter dynamic threshold above it)                                                                                      |
| `perf_fee_bps`                    | `u16` | 2\_000                                                                                                                                                            |
| `hurdle_apr_bps`                  | `u16` | measured live at init (Kamino USDC supply APY). `400` in `InitializeParams::default()` is a tests/localnet fallback only — mainnet init aborts rather than use it |
| `min_margin_bps`                  | `u16` | 900 (bounds leverage at \~10x)                                                                                                                                    |
| `reserve_skim_bps`                | `u16` | 500                                                                                                                                                               |
| `min_dwell_seconds`               | `u32` | 43\_200 (12 h)                                                                                                                                                    |
| `max_swap_slippage_bps`           | `u16` | 50                                                                                                                                                                |
| `emergency_close_dd_bps`          | `u16` | 500                                                                                                                                                               |
| `deposit_cap_usdc`                | `u64` | 250\_000\_000\_000 ($250k launch cap). `u64::MAX` = uncapped; `0` = paused.                                                                                       |
| `min_request_shares`              | `u64` | 1\_000\_000 (1 ksUSD)                                                                                                                                             |
| `max_pending_queue_usdc`          | `u64` | `u64::MAX` (disabled)                                                                                                                                             |
| `max_nav_change_bps_per_hour`     | `u16` | 1\_000 (±10%/hr on keeper attestations; deposits/withdrawals write `cached_nav_usdc` directly and are not capped by it)                                           |
| `funding_max_staleness_seconds`   | `u32` | 21\_600 (6 h)                                                                                                                                                     |
| `consecutive_dd_settles_required` | `u8`  | 2                                                                                                                                                                 |
| `consecutive_dd_settles_observed` | `u8`  | Running counter                                                                                                                                                   |
| `lst_depeg_bps`                   | `u16` | 500 (5%)                                                                                                                                                          |
| `max_oi_fraction_bps`             | `u16` | Cap on the short as a fraction of Phoenix SOL-PERP open interest. Breach reverts with `PerpShortExceedsOiCap`                                                     |
| `max_position_base_lots`          | `u64` | Absolute ceiling on position size, in base lots                                                                                                                   |

### Risk flags & keeper allowlist

| Field               | Type     | Meaning                                                                                                                                                                                                                    |
| ------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `paused`            | `bool`   | Halts deposits + new positions; instant withdraw stays open                                                                                                                                                                |
| `authorized_keeper` | `Pubkey` | Gates `settle` / strategy opens / closes / lending cranks. **Exact match required** — `Pubkey::default()` (the post-init state) blocks every keeper-gated ix until admin runs `update_params { authorized_keeper: <key> }` |

### Smoothed funding signal

| Field                      | Type  | Meaning                                                                |
| -------------------------- | ----- | ---------------------------------------------------------------------- |
| `funding_apr_smoothed_bps` | `i32` | EMA of SOL-PERP funding, updated on every `settle`                     |
| `funding_smooth_last_ts`   | `i64` | Last EMA update timestamp. `0` sentinel = first settle hasn't run yet. |

### Reserved

| Field       | Type       | Meaning               |
| ----------- | ---------- | --------------------- |
| `_reserved` | `[u8; 10]` | Forward compatibility |

***

## `PositionMode` (enum)

| Variant    | Value | Meaning                                                                                 |
| ---------- | ----- | --------------------------------------------------------------------------------------- |
| `Idle`     | 0     | **Parked** (v1 name). No active perp position; capital in vault USDC (lent on Kamino).  |
| `Normal`   | 1     | Long jitoSOL spot (unlevered) + short SOL-PERP on Phoenix at 1× notional, USDC margin.  |
| `WindDown` | 3     | Terminal. New positions / deposits blocked; users claim pro-rata via `claim_wind_down`. |

***

## `WithdrawalRequest`

PDA seed: `[b"withdrawal_request", vault.key(), request_id_le_bytes]` · created by `request_withdrawal`, closed by `process_withdrawal` (rent refunded to the original requester).

| Field               | Type     | Meaning                                                          |
| ------------------- | -------- | ---------------------------------------------------------------- |
| `vault`             | `Pubkey` | Vault this request belongs to                                    |
| `user`              | `Pubkey` | Original requester (rent refund recipient)                       |
| `user_usdc_account` | `Pubkey` | USDC destination — must match at process time                    |
| `usdc_owed`         | `u64`    | Upper bound USDC owed at burn time (FIFO at request)             |
| `shares_burned`     | `u64`    | Shares burned at request — used for the price-at-process haircut |
| `request_id`        | `u64`    | Sequential ID assigned at enqueue                                |
| `requested_ts`      | `i64`    | Timestamp of request                                             |
| `bump`              | `u8`     | PDA bump                                                         |

`process_withdrawal` price-at-process haircut:

* Re-prices `usdc_owed` against the current share price
* Takes the **lower** of locked vs. live
* Depositors can never extract more than their pro-rata share if NAV has fallen since they queued

***

## Derivation helpers (TypeScript)

```ts
const [vaultPda] = PublicKey.findProgramAddressSync(
  [Buffer.from("vault")],
  PROGRAM_ID
);

const [requestPda] = PublicKey.findProgramAddressSync(
  [
    Buffer.from("withdrawal_request"),
    vaultPda.toBuffer(),
    new BN(requestId).toArrayLike(Buffer, "le", 8),
  ],
  PROGRAM_ID
);

// Vault ATAs (PDA-owned)
const vaultUsdcAta    = getAssociatedTokenAddressSync(vault.usdcMint,    vaultPda, true);
const vaultJitosolAta = getAssociatedTokenAddressSync(vault.jitosolMint, vaultPda, true);
const reserveUsdcAta  = vault.reserveAta;             // pinned at init
```

***

## Related

* [Instructions](/for-developers/instructions) · [Events](/for-developers/events) · [Errors](/for-developers/errors)
* [Quick start](/for-developers/quick-start) — getting a Program instance set up


# Events Reference

The ksUSD vault emits an event for nearly every state-changing instruction. Subscribe via `program.addEventListener("EventName", cb)` or parse logs from confirmed transactions.

> Sources: `programs/keystone-finance/src/instructions/*.rs` (search for `#[event]`).

| Event                   | Emitted by           | Notable fields                                                                                                      |
| ----------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `DepositEvent`          | `deposit`            | `user`, `usdc_amount`, `shares_minted`, `share_price_1e9`, `new_total_shares`, `timestamp`                          |
| `WithdrawEvent`         | `withdraw_instant`   | `user`, `shares_burned`, `usdc_paid`, `share_price_1e9`, `new_total_shares`, `timestamp`                            |
| `WithdrawalRequested`   | `request_withdrawal` | `user`, `request_id`, `shares_burned`, `usdc_owed`, `timestamp`                                                     |
| `WithdrawalProcessed`   | `process_withdrawal` | `user`, `request_id`, `usdc_paid`, `cranker`, `timestamp`                                                           |
| `MarginAdded`           | `add_margin`         | amount, new posted total, the health ratio that triggered it                                                        |
| `PositionOpened`        | `open_position`      | `mode` (`Normal`), `usdc_in`, `jitosol_purchased`, `perp_base_amount`, `timestamp`                                  |
| `PositionClosed`        | `close_position`     | `usdc_out`, `realized_pnl_signed`, `timestamp`                                                                      |
| `OraclesSet`            | `set_oracles`        | `sol_oracle`, `jitosol_oracle`, `timestamp`                                                                         |
| `PhoenixEnabled`        | `enable_phoenix`     | `trader`, `market`, `canonical_ata`, `timestamp`                                                                    |
| `LendingEnabled`        | `enable_lending`     | `kamino_lend_reserve`, `vault_lend_collateral_ata`, `reserve_lend_collateral_ata`, `timestamp`                      |
| `IdleLent`              | `lend_idle_usdc`     | `usdc_amount`, `new_usdc_lent_kamino`, `timestamp`                                                                  |
| `IdleUnlent`            | `unlend_usdc`        | `collateral_amount`, `usdc_received`, `new_usdc_lent_kamino`, `timestamp`                                           |
| `ReserveLent`           | `lend_reserve`       | `usdc_amount`, `new_reserve_lent_kamino`, `timestamp`                                                               |
| `ReserveUnlent`         | `unlend_reserve`     | `collateral_amount`, `usdc_received`, `new_reserve_lent_kamino`, `timestamp`                                        |
| `NavAttested`           | `attest_nav`         | `old_nav_usdc`, `new_nav_usdc`, `change_bps`, `last_nav_attest_ts`, `timestamp`                                     |
| `WindDownInitiated`     | `init_wind_down`     | `cached_nav_usdc`, `total_shares`, `timestamp`                                                                      |
| `WindDownClaimed`       | `claim_wind_down`    | `user`, `shares_burned`, `usdc_paid`, `timestamp`                                                                   |
| `PauseToggled`          | `set_pause`          | `paused`, `timestamp`                                                                                               |
| `PeakReset`             | `reset_peak`         | `new_peak_share_price_1e9`, `timestamp`                                                                             |
| `ReservePaidOut`        | `pay_from_reserve`   | `amount_usdc`, `new_reserve_fund_usdc`, `new_cached_nav_usdc`, `timestamp`                                          |
| `FeesCollected`         | `collect_fees`       | `perf_fee_usdc`, `admin_share_usdc`, `reserve_share_usdc`, `new_hwm_share_price_1e9`, `timestamp`                   |
| `DepegAutoPaused`       | `settle`             | `timestamp` — emitted when a jitoSOL depeg auto-pauses the vault                                                    |
| `ParamsUpdated`         | `update_params`      | every configurable parameter plus `authorized_keeper`, `max_oi_fraction_bps`, `max_position_base_lots`, `timestamp` |
| `AdminTransferProposed` | `transfer_admin`     | `current_admin`, `pending_admin`, `timestamp`                                                                       |
| `AdminTransferred`      | `accept_admin`       | `previous_admin`, `new_admin`, `timestamp`                                                                          |

> `settle` and `emergency_close` log via `msg!` rather than `#[event]`; subscribe to program logs if you need them programmatically.

***

## Subscribing in TypeScript

```ts
const listener = program.addEventListener("DepositEvent", (event, _slot, _sig) => {
  console.log("deposit", {
    user: event.user.toBase58(),
    usdc: Number(event.usdcAmount.toString()) / 1e6,
    shares: Number(event.sharesMinted.toString()) / 1e6,
    price: Number(event.sharePrice1e9.toString()) / 1e9,
  });
});

// later
await program.removeEventListener(listener);
```

For historical events, parse transaction logs with `EventParser`:

```ts
import { EventParser } from "@coral-xyz/anchor";

const parser = new EventParser(program.programId, program.coder);
for (const event of parser.parseLogs(tx.meta.logMessages)) {
  console.log(event.name, event.data);
}
```

***

## Related

* [Instructions](/for-developers/instructions) · [Account structure](/for-developers/accounts) · [Errors](/for-developers/errors)


# Error Codes

The `KsusdError` enum lists every program-level error the ksUSD vault can return. Each maps to an Anchor exit code (starting at `0x1770`; the IDL has the exact codes).

> Source: [`programs/keystone-finance/src/errors.rs`](https://github.com/kamwithak/keystone-contracts/blob/main/programs/keystone-finance/src/errors.rs)

***

## General

| Variant         | Meaning                           | Common cause                                                                                                 |
| --------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `Unauthorized`  | Admin / keeper signature required | A non-admin called a privileged instruction, or a non-keeper called strategy when `authorized_keeper` is set |
| `Paused`        | Vault is paused                   | Admin paused; only `withdraw_instant` and `claim_wind_down` still work                                       |
| `MathOverflow`  | Math overflow                     | Bad input or share-price edge case                                                                           |
| `InvalidParams` | Invalid configuration             | Param outside allowed range (e.g. `perf_fee_bps > 5000`) or remaining-accounts split overflow                |

## Deposit / withdraw

| Variant                       | Meaning                                                                                       |
| ----------------------------- | --------------------------------------------------------------------------------------------- |
| `ZeroDeposit`                 | `usdc_amount == 0`                                                                            |
| `DepositCapExceeded`          | New deposit would push `cached_nav_usdc` past `deposit_cap_usdc`                              |
| `InsufficientShares`          | User's ksUSD ATA holds less than requested                                                    |
| `InsufficientLiquidityBuffer` | Vault doesn't have enough idle USDC for instant withdrawal — use `request_withdrawal` instead |
| `SlippageExceeded`            | Realized swap slippage exceeds `max_swap_slippage_bps`                                        |

## Position state

| Variant                  | Meaning                                                                                                                                 |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `PositionAlreadyOpen`    | Calling `open_position` while a position is already open                                                                                |
| `PositionNotOpen`        | Calling `add_margin` while Parked                                                                                                       |
| `MarginBelowMinimum`     | `margin_usdc` is under `min_margin_bps` of the amount deployed, i.e. the requested leverage exceeds the on-chain bound                  |
| `MarginExceedsCap`       | The resulting posted margin would exceed `effective_nav / 2`. Bounds how much of the vault a compromised keeper can move onto the venue |
| `PositionNotOpen`        | Calling `close_position` / `emergency_close` with no active position                                                                    |
| `FundingThresholdNotMet` | Keeper attempted to open a mode whose funding-rate threshold wasn't cleared                                                             |
| `MinDwellNotElapsed`     | Less than `min_dwell_seconds` since last mode change                                                                                    |

## External venues

| Variant                  | Meaning                                                                            |
| ------------------------ | ---------------------------------------------------------------------------------- |
| `PhoenixCpiFailed`       | The Phoenix CPI returned an error                                                  |
| `EmberCpiFailed`         | The Ember CPI returned an error — USDC↔canonical margin wrap/unwrap failed         |
| `KaminoCpiFailed`        | The Kamino KLend CPI returned an error                                             |
| `JupiterSwapFailed`      | The Jupiter V6 route CPI failed                                                    |
| `InvalidMint`            | Account constraint mismatched expected mint                                        |
| `PhoenixMarketNotActive` | Phoenix SOL-PERP is halted, in settlement, or delisted — the venue circuit breaker |
| `PerpShortExceedsOiCap`  | The short would exceed the configured fraction of Phoenix SOL-PERP open interest   |

## Oracle

| Variant                    | Meaning                                              |
| -------------------------- | ---------------------------------------------------- |
| `StaleOracle`              | Pyth price update older than the staleness threshold |
| `OracleConfidenceExceeded` | Pyth confidence interval > 2% of the price           |

## Risk

| Variant                 | Meaning                                                                                                             |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `EmergencyDrawdown`     | NAV dropped more than `emergency_close_dd_bps` from peak                                                            |
| `LstDepeg`              | jitoSOL price deviated beyond `lst_depeg_bps` — `settle` auto-pauses                                                |
| `DrawdownTriggerLatent` | Drawdown trigger requires N consecutive settles below threshold (set by `consecutive_dd_settles_required`)          |
| `FundingSignalStale`    | Smoothed funding signal older than `funding_max_staleness_seconds`; run `settle` first                              |
| `FundingRateInsane`     | Funding rate sanity check failed (absurd value reported by the perp venue — most likely an oracle / venue incident) |

## Withdrawal queue

| Variant                      | Meaning                                                                               |
| ---------------------------- | ------------------------------------------------------------------------------------- |
| `WithdrawalAlreadyProcessed` | `process_withdrawal` called on a request that's already settled                       |
| `WithdrawalUserMismatch`     | Recorded `user_usdc_account` doesn't match the passed destination                     |
| `WithdrawalNotNextInQueue`   | Queued requests must be processed strictly in FIFO order — pass the next `request_id` |
| `BelowMinRequestShares`      | `request_withdrawal` shares below `min_request_shares`                                |
| `QueueCapExceeded`           | Queue would exceed `max_pending_queue_usdc`                                           |

## NAV attestation

| Variant               | Meaning                                                            |
| --------------------- | ------------------------------------------------------------------ |
| `NavChangeExceedsCap` | `attest_nav` delta exceeds `max_nav_change_bps_per_hour`           |
| `NavStale`            | NAV stale — `settle` / `attest_nav` required before this operation |

## Lending

| Variant                 | Meaning                                                                 |
| ----------------------- | ----------------------------------------------------------------------- |
| `LendingNotEnabled`     | `lend_idle_usdc` / `lend_reserve` called without prior `enable_lending` |
| `LendingBreachesBuffer` | Lending more would drop idle USDC below `liquidity_buffer_target`       |

## Wind-down / admin transfer

| Variant           | Meaning                                                             |
| ----------------- | ------------------------------------------------------------------- |
| `WindDownActive`  | Vault is in wind-down — new deposits / positions / un-pause blocked |
| `NoPendingAdmin`  | `accept_admin` called when no pending admin is set                  |
| `NotPendingAdmin` | `accept_admin` signer doesn't match `pending_admin`                 |

## Bootstrap

| Variant                 | Meaning                                                                            |
| ----------------------- | ---------------------------------------------------------------------------------- |
| `BootstrapResidualUsdc` | Vault USDC ATA must be empty before the first deposit (prevents dilution griefing) |

***

## Reading errors from a client

```ts
try {
  await program.methods.withdrawInstant(new BN(amount)).accountsStrict({...}).rpc();
} catch (e: any) {
  // AnchorError exposes .error.errorCode.code (string) and .error.errorMessage
  if (e.error?.errorCode?.code === "InsufficientLiquidityBuffer") {
    // fall back to request_withdrawal
  }
  throw e;
}
```

In raw transaction logs the error appears as e.g. `AnchorError caused by account: ... InsufficientLiquidityBuffer.` followed by the exit code.

***

## Related

* [Instructions](/for-developers/instructions) · [Account structure](/for-developers/accounts) · [Events](/for-developers/events)


# Admin Operations

Admin-only instructions on the ksUSD vault. All signed by the `vault.admin` keypair (or successor after `transfer_admin` + `accept_admin`).

> Source: [`programs/keystone-finance/src/instructions/admin.rs`](https://github.com/kamwithak/keystone-contracts/blob/main/programs/keystone-finance/src/instructions/admin.rs) · [`init_wind_down.rs`](https://github.com/kamwithak/keystone-contracts/blob/main/programs/keystone-finance/src/instructions/init_wind_down.rs) · [`enable_lending.rs`](https://github.com/kamwithak/keystone-contracts/blob/main/programs/keystone-finance/src/instructions/enable_lending.rs) · [`set_oracles.rs`](https://github.com/kamwithak/keystone-contracts/blob/main/programs/keystone-finance/src/instructions/set_oracles.rs)

***

## One-time setup

| Instruction                           | When                                                                                                      |
| ------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `initialize(params)`                  | First deploy. Creates the vault PDA, ksUSD mint, reserve USDC ATA.                                        |
| `enable_phoenix(subaccount_id, name)` | After init, before opening any perp position.                                                             |
| `enable_lending()`                    | After init, before any `lend_idle_usdc` / `lend_reserve`. Pins Kamino USDC reserve + the two cToken ATAs. |
| `set_oracles()`                       | After init, before strategy paths that read SOL/USD or jitoSOL/USD.                                       |

***

## `set_pause(paused: bool)`

* Halts new deposits and new positions
* Instant withdrawals and `claim_wind_down` stay open by design — depositors should always be able to exit
* Cannot un-pause while `position_mode == WindDown` (reverts with `WindDownActive`)

**When to use:**

* Pre-audit incident response
* Investigating an oracle / venue anomaly
* During an admin handover (briefly, to flush in-flight state)

```ts
await program.methods.setPause(true).accountsStrict({
  vault: vaultPda,
  admin: adminWallet.publicKey,
}).rpc();
```

Emits `PauseToggled`.

***

## `update_params(args)`

* Adjust risk parameters in place
* Each field on `UpdateParamsArgs` is `Option<T>` — only the fields you pass get written

| Field                             | Bounds                                                                                                                             |
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `liquidity_buffer_bps`            | 0–5\_000 (≤ 50% of NAV)                                                                                                            |
| `funding_threshold_normal_bps`    | i32; expected positive                                                                                                             |
| `perf_fee_bps`                    | 0–5\_000 (≤ 50%)                                                                                                                   |
| `reserve_skim_bps`                | 0–10\_000 (of perf fee)                                                                                                            |
| `min_dwell_seconds`               | 0–`u32::MAX`                                                                                                                       |
| `max_swap_slippage_bps`           | 0–1\_000 (≤ 10%)                                                                                                                   |
| `emergency_close_dd_bps`          | 0–10\_000                                                                                                                          |
| `deposit_cap_usdc`                | `u64::MAX` = uncapped, `0` = pause new deposits, else USDC base units                                                              |
| `authorized_keeper`               | Pubkey; **exact match required**. `Pubkey::default()` (the post-init state) blocks all keeper-gated ix until admin sets a real key |
| `min_request_shares`              | u64                                                                                                                                |
| `max_pending_queue_usdc`          | `u64::MAX` = disabled                                                                                                              |
| `max_nav_change_bps_per_hour`     | 0–10\_000                                                                                                                          |
| `funding_max_staleness_seconds`   | u32                                                                                                                                |
| `consecutive_dd_settles_required` | ≥ 1                                                                                                                                |

* Invalid bounds → `InvalidParams`
* Mints, admin, oracles, Phoenix / Kamino refs are **immutable** through this instruction — set once at `initialize` / `enable_*` / `set_oracles`

```ts
await program.methods.updateParams({
  liquidityBufferBps: 800,            // tighten buffer to 8%
  emergencyCloseDdBps: 400,           // tighten drawdown guard to 4%
  // omit other fields to leave them unchanged
}).accountsStrict({
  vault: vaultPda,
  admin: adminWallet.publicKey,
}).rpc();
```

***

## `transfer_admin(new_admin)` + `accept_admin()`

Two-step admin handover:

* **Step 1** — records `new_admin` as a pending successor (signed by current admin)
* **Step 2** — finalizes (signed by the new admin)
* Prevents accidentally transferring to a dead key

**Target end-state:** a multisig (Squads / similar) once mainnet is live.

```ts
// Step 1 — current admin proposes
await program.methods.transferAdmin(squadsMultisig).accountsStrict({
  vault: vaultPda,
  admin: currentAdmin.publicKey,
}).rpc();

// Step 2 — new admin accepts
await program.methods.acceptAdmin().accountsStrict({
  vault: vaultPda,
  newAdmin: squadsMultisig,
}).rpc();
```

Errors: `NoPendingAdmin` (step 2 called without a pending), `NotPendingAdmin` (step 2 signer mismatch).

***

## `collect_fees()`

* Pays the performance fee in USDC (no dilutive share mint)
* Only callable when `position_mode == Idle` (Parked) — fees crystallize on realized gains
* See [Fees](/reference/fees) for the math
* Admin share → `admin_usdc_account`
* Reserve skim → `reserve_ata`
* HWM bumps to the post-fee share price (monotonic)

**When to call:**

* On a regular cadence (monthly is conventional; weekly is fine)
* Before any planned parameter change that affects share-price computation
* Before mainnet upgrade events (clean accounting boundary)

```ts
await program.methods.collectFees().accountsStrict({
  vault: vaultPda,
  vaultUsdcAccount: vaultUsdcAta,
  reserveUsdcAccount: vault.reserveAta,
  adminUsdcAccount: adminUsdcAta,
  admin: adminWallet.publicKey,
  tokenProgram: TOKEN_PROGRAM_ID,
}).rpc();
```

Idempotent when share price ≤ HWM (no transfer, no state change). Emits `FeesCollected`.

***

## `reset_peak()`

* Clears `peak_share_price_1e9` and `consecutive_dd_settles_observed` to current share price
* Use after legitimate recovery — prevents a stale peak from keeping the drawdown guard armed against routine volatility

```ts
await program.methods.resetPeak().accountsStrict({
  vault: vaultPda,
  admin: adminWallet.publicKey,
}).rpc();
```

Emits `PeakReset`.

***

## `pay_from_reserve(amount)`

* Moves USDC out of `reserve_ata` into the vault's main USDC ATA → socializes a reserve draw into NAV
* Caller must invoke `unlend_reserve` first if part of the reserve is currently lent at Kamino

```ts
await program.methods.payFromReserve(new BN(amount)).accountsStrict({
  vault: vaultPda,
  reserveUsdcAccount: vault.reserveAta,
  vaultUsdcAccount: vaultUsdcAta,
  admin: adminWallet.publicKey,
  tokenProgram: TOKEN_PROGRAM_ID,
}).rpc();
```

Emits `ReservePaidOut`.

***

## `init_wind_down()`

Terminal mode switch. Sets `position_mode = WindDown` and pauses the vault. After this:

* `deposit` reverts (`WindDownActive`)
* Strategy `open_*` reverts (`WindDownActive`)
* Existing positions must be closed via `close_position` / `emergency_close`
* Users redeem pro-rata via `claim_wind_down(shares)` against the vault's idle USDC

Cannot be undone via `set_pause(false)`.

```ts
await program.methods.initWindDown().accountsStrict({
  vault: vaultPda,
  admin: adminWallet.publicKey,
}).rpc();
```

Emits `WindDownInitiated`.

***

## Operational checklist

| Cadence                      | Action                                                                                                           |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| **Daily**                    | Verify `cached_nav_usdc` against on-chain Phoenix / Kamino reads (off-chain script).                             |
| **Weekly**                   | `collect_fees` if share price has crossed HWM. Refresh keeper bot keys if rotated.                               |
| **Monthly**                  | Review `peak_share_price_1e9` vs. live share price; `reset_peak` if a stale peak is dragging the drawdown guard. |
| **On incident**              | `set_pause(true)`, investigate, then either resolve and unpause or invoke `emergency_close`.                     |
| **On admin rotation**        | `transfer_admin` + `accept_admin` (two transactions).                                                            |
| **On legitimate retirement** | `init_wind_down`, close any open position, broadcast `claim_wind_down` instructions to depositors.               |

***

## Related

* [Keeper bot](/for-operators/keeper-bot) — non-admin strategy operations
* [Monitoring](/for-operators/monitoring) — what to watch
* [Errors](/for-developers/errors) — what reverts look like
* [Fees](/reference/fees) — `collect_fees` math


# Keeper Bot

> v1 hedges on Phoenix Perps (USDC margin via Ember). The keeper assembles the Phoenix and Ember account groups with the Rise SDK. Mode transitions run automatically off the on-chain funding signal; the keeper bot executes them and cranks settlement, lending, and withdrawals.

The keeper is an off-chain bot that cranks settlement, lending, mode transitions, and withdrawals on a schedule. Mode transitions are automatic: the bot opens or closes the basis when the on-chain smoothed funding signal and guardrails permit, with no manual intervention. The position, NAV-attestation, and lending instructions require the vault's `authorized_keeper` to sign — so the keeper is a permissioned *executor*, bounded by the on-chain rules, not a decision-maker. `settle`, `process_withdrawal`, and `emergency_close` are permissionless, so anyone can keep the vault safe and redeemable if the keeper goes offline.

> Reference implementation: [`scripts/keeper/`](https://github.com/kamwithak/keystone-contracts/tree/main/scripts/keeper/README.md) — `index.ts` is the loop, `duties/` are the individual cranks, shared plumbing is in [`scripts/lib/`](https://github.com/kamwithak/keystone-contracts/tree/main/scripts/lib/README.md). TypeScript + Anchor + Jupiter API. Run with `npm run keeper` (`KEEPER_DRY_RUN=1` to plan only, `KEEPER_SIMULATE=1` to build and simulate without sending).

***

## Cadence

| Instruction                       | Frequency                                                                                                | Trigger                                                                                                                     |
| --------------------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `settle`                          | Every \~1 hour                                                                                           | Refresh funding EMA, drawdown peak, LST depeg check, `settle_pnl` if open                                                   |
| `attest_nav(new_nav_usdc)`        | After `settle`                                                                                           | Snap NAV to live position value when on-chain reads can't capture unrealized PnL. Bounded by `max_nav_change_bps_per_hour`. |
| `open_position`                   | When the smoothed funding signal clears the threshold and dwell has elapsed                              | One-shot per mode entry                                                                                                     |
| `close_position`                  | When the signal falls back through the threshold                                                         | One-shot per mode exit                                                                                                      |
| `lend_idle_usdc` / `unlend_usdc`  | Whenever idle vault USDC exceeds the buffer target by > a margin (or buffer is short)                    | Keep USDC productive without breaching `liquidity_buffer_target`                                                            |
| `lend_reserve` / `unlend_reserve` | Same, for the reserve fund                                                                               | Reserve fund stays in Kamino unless an admin draw is queued                                                                 |
| `process_withdrawal`              | When the queue has unprocessed requests AND vault holds enough idle USDC for the **next** request (FIFO) | Permissionless — anyone can crank                                                                                           |
| `emergency_close`                 | When drawdown guard trips OR vault is paused with a position open                                        | Permissionless once tripped                                                                                                 |
| `add_margin`                      | Every tick while a position is open, when margin health drops below 2.5                                  | **The keeper moves funds on its own here** — see below                                                                      |

**No `increase_position` or `remove_margin` — they aren't in the program.** v1 ships what prevents a loss; growing a live short and pulling surplus margin back both only save the cost of a close+reopen, which the vault performs on its own at the next mode flip. Surplus margin still raises a `margin-surplus` warning so an operator can judge whether the drag is worth closing early, but no keeper action can reduce collateral on a live short.

***

## Signal pipeline (open / close decisions)

*The normal↔parked transition is decided by the on-chain funding signal; the keeper bot assembles and submits the switch automatically whenever the rules below permit. The pipeline is that funding logic.*

1. **Read funding.** `settle` parses the last funding rate + timestamp from the perp venue's SOL-PERP market and updates the on-chain EMA (`funding_apr_smoothed_bps`). Sanity-bounded — absurd values revert with `FundingRateInsane`.
2. **Stale check.** If `now - funding_smooth_last_ts > funding_max_staleness_seconds`, opens revert with `FundingSignalStale`. Run `settle` first.
3. **Threshold + dwell + drawdown latency.**
   * If `funding_apr_smoothed_bps ≥ funding_threshold_normal_bps` AND `now − last_mode_change_ts ≥ min_dwell_seconds` → call `open_position`.
   * Else if in the active mode but the signal decayed below threshold + hysteresis → call `close_position`.
   * Drawdown trips require `consecutive_dd_settles_observed ≥ consecutive_dd_settles_required` (default 2) before `emergency_close` can fire on a NAV breach — single-tick noise is rejected with `DrawdownTriggerLatent`.
4. **Build the transaction.** Assemble per-instruction `remaining_accounts` groups (see [Instructions](/for-developers/instructions)) and the Jupiter swap data via the Jupiter `/swap-instructions` endpoint.
5. **Submit.** Wrap with priority-fee compute budget instructions. Retry on slot-out / blockhash-not-found.

***

## Assembling `remaining_accounts`

* Strategy instructions pack multiple CPIs into one transaction
* Each group needs a precise account list — wrong order or missing accounts reverts the CPI
* Use the venue's official client / API to build each group:
* **Phoenix margin (Ember) / place\_perp\_order / settle funding** — use the **Rise SDK** (`github.com/Ellipsis-Labs/rise-public`) instruction builders and extract the `keys` array.
* **Kamino lending (`lend_idle_usdc` / `unlend_usdc` / `lend_reserve` / `unlend_reserve`)** — use the Kamino IDL and PDA derivation utilities to build the USDC reserve deposit/withdraw account groups.
* **Jupiter route\_swap** — call Jupiter's `/swap-instructions` HTTP endpoint with `userPublicKey = vaultPda`, then pass the returned `swapInstruction.data` as `jupiter_swap_data` and the returned `keys` as the relevant remaining-accounts group.

> Pass per-group counts as the u8 instruction args (`jupiter_account_count`, `phoenix_deposit_account_count`, etc.) so the on-chain handler can split safely.

***

## Process-withdrawal cranking

**Strict FIFO:**

* Next-in-line request\_id is `vault.queue_processed_through` itself (both counters start at 0, and `process_withdrawal` requires `request_id == queue_processed_through`)
* Any other request errors with `WithdrawalNotNextInQueue`
* Anyone can crank (including the original requester)
* Rent refunds to the original requester even if a different wallet pays the tx

**Liquidity shortfall:**

* Reverts with `InsufficientLiquidityBuffer` if vault USDC is short of the next `usdc_owed`
* Keeper should `unlend_usdc` first if Kamino USDC can cover the shortfall
* Otherwise wait for the next `close_*` to free up USDC

***

## Failure modes

| Symptom                                              | Cause                                                                  | Resolution                                                                                                       |
| ---------------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `MinDwellNotElapsed`                                 | Tried to switch modes too soon                                         | Wait for `last_mode_change_ts + min_dwell_seconds`                                                               |
| `FundingThresholdNotMet`                             | Smoothed funding hasn't crossed the threshold                          | Re-read `funding_apr_smoothed_bps`                                                                               |
| `FundingSignalStale`                                 | EMA older than `funding_max_staleness_seconds`                         | Run `settle` first                                                                                               |
| `SlippageExceeded`                                   | Jupiter route filled at worse price than expected                      | Refresh the quote, narrow the bound, retry                                                                       |
| `PhoenixCpiFailed`                                   | Phoenix returned an error — usually margin or oracle                   | Inspect program logs; may need to settle funding first                                                           |
| `InsufficientLiquidityBuffer` (`process_withdrawal`) | Not enough idle USDC for the next request                              | `unlend_usdc` to refill, or wait for next `close_*`                                                              |
| `LendingBreachesBuffer`                              | `lend_idle_usdc` would drop the buffer below `liquidity_buffer_target` | Reduce amount or run `unlend_usdc` instead                                                                       |
| `NavChangeExceedsCap`                                | `attest_nav` delta > `max_nav_change_bps_per_hour`                     | Stage with smaller deltas over multiple hours, or admin raises the cap if the position genuinely moved that much |
| `DrawdownTriggerLatent`                              | Drawdown observed but not yet across consecutive settles               | Wait one more settle cycle; this is by design                                                                    |
| `LstDepeg`                                           | jitoSOL/SOL deviation > `lst_depeg_bps`                                | `settle` auto-pauses; admin investigates and either unpauses or emergency-closes                                 |

***

## Idempotency

* All open / close instructions assert `position_mode` invariants on entry
* Duplicate calls during a retry storm revert cleanly without state damage
* `settle` is fully idempotent within an hour (no state change beyond the EMA refresh)

***

## Related

* [Admin operations](/for-operators/admin-ops)
* [Monitoring](/for-operators/monitoring)
* [Instructions reference](/for-developers/instructions) · [Errors](/for-developers/errors)

***

## The keeper moves margin autonomously

Unlike every other duty, `margin-health` can move vault funds without a human. That is deliberate: liquidation on a delta-neutral short can arrive in minutes, and an alert you read an hour later is not a defence.

The mechanism it defends against is unintuitive — the jitoSOL leg is in the vault and the margin is on Phoenix, and **Phoenix can only see its own side**. On a rally the short loses margin while the offsetting jitoSOL gain sits somewhere Phoenix cannot count, so a hedged position gets liquidated anyway. `emergency_close` will not save it, because NAV doesn't fall in a hedged rally.

Bounds on that autonomy:

* it can never draw on the withdrawal buffer
* total posted margin can never exceed **half of effective NAV** (enforced on-chain, not just in the keeper)
* there is no `remove_margin`, so no keeper action can reduce collateral on a live short
* `KEEPER_AUTO_MARGIN=0` reduces it to alert-only — but then you must be reachable within minutes

If the keeper is down, or auto-margin is off, the thin-margin policy (`min_margin_bps = 900`) is no longer justified — close the position rather than leave it sitting. Note the 10× this implies is the venue's view of the perp leg, not portfolio leverage: the vault's total exposure equals its NAV.


# Monitoring & Alerts

> v1 monitors the **Phoenix** position and Ember margin.

The ksUSD vault is one PDA — most monitoring reduces to watching that account plus a few external signals.

***

## Key metrics

| Metric                                  | Source                                                                   | Alert when                                                                                        |
| --------------------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| Share price (`share_price_1e9`)         | Derived from `effective_nav_usdc / total_shares`                         | Drops > 2% in 24h, or > `emergency_close_dd_bps` from `peak_share_price_1e9`                      |
| `cached_nav_usdc` vs. computed-live NAV | On-chain vs. off-chain reconciliation against Phoenix / Kamino positions | Discrepancy > 1%                                                                                  |
| Pyth SOL/USD staleness                  | Last-update timestamp on the Pyth feed                                   | > 5 min                                                                                           |
| Pyth confidence                         | `confidence / price`                                                     | > 1.5% (program reverts at 2%)                                                                    |
| jitoSOL/SOL depeg                       | Derived from `jitosol_oracle.price / sol_oracle.price`                   | > 80% of `lst_depeg_bps`                                                                          |
| `cached_nav_usdc` cap utilization       | `cached_nav_usdc / deposit_cap_usdc`                                     | > 90% (raise cap or pause new deposits)                                                           |
| `position_mode`                         | `vault.position_mode`                                                    | `Parked` (`Idle`) for > 24h while funding signal is active                                        |
| `position_base_amount`                  | `vault.position_base_amount`                                             | Drifts > 1% from expected (1× of the jitoSOL spot leg)                                            |
| `funding_apr_smoothed_bps` staleness    | `now - funding_smooth_last_ts`                                           | Approaching `funding_max_staleness_seconds`                                                       |
| `consecutive_dd_settles_observed`       | `vault.consecutive_dd_settles_observed`                                  | ≥ 1 (single-tick warning before lethal)                                                           |
| Perp-venue health factor                | Phoenix trader-account margin                                            | < 1.5                                                                                             |
| SOL-PERP funding rate                   | Phoenix `PerpAssetMap`                                                   | Sustained across `normal_thresh` past dwell                                                       |
| Pending withdrawals                     | `queue_next_id − queue_processed_through`                                | > 0 for more than one keeper cycle                                                                |
| Reserve fund                            | `reserve_fund_usdc` and `reserve_lent_kamino`                            | Reserve ATA empty while `reserve_fund_usdc > 0` (means `unlend_reserve` needed before any payout) |
| Lending balances                        | `usdc_lent_kamino`, `reserve_lent_kamino`                                | Sudden drop without a matching `unlend_*` event = Kamino-side anomaly                             |
| `vault.paused`                          | bool                                                                     | Unexpected `true` = investigate `LstDepeg` or admin action                                        |
| `vault.position_mode == WindDown`       | enum                                                                     | Terminal — only `claim_wind_down` should run                                                      |

***

## Reconciliation

Once per cycle, an off-chain script should compute the "true" NAV by summing:

1. Vault USDC ATA balance
2. `usdc_lent_kamino × kamino USDC reserve cToken exchange rate` (lent vault USDC, current value)
3. `reserve_fund_usdc` (already includes `reserve_lent_kamino` per the vault's accounting)
4. `(jitoSOL spot holdings) × Pyth jitoSOL price` — the unlevered spot leg (normal basis only)
5. Phoenix trader-account margin balance — posted USDC margin + settled funding + accrued PnL

Compare against `vault.cached_nav_usdc`. A discrepancy > 1% means:

* `settle` is overdue, **or**
* `attest_nav` is needed to capture unrealized PnL

***

## Emergency conditions

| Signal                                                                                | Response                                                                       |
| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Pyth SOL/USD stale > 10 min OR confidence > 2%                                        | Pause new deposits + positions; manual `emergency_close` if a position is open |
| jitoSOL depeg > `lst_depeg_bps`                                                       | `settle` auto-pauses; investigate and either unpause or emergency-close        |
| Phoenix outage / market halted                                                        | Same — pause and investigate                                                   |
| NAV drawdown ≥ `emergency_close_dd_bps` for `consecutive_dd_settles_required` settles | Anyone can call `emergency_close` permissionlessly; keeper should do it first  |
| `position_mode == WindDown`                                                           | Stop the keeper; broadcast `claim_wind_down` instructions to depositors        |

***

## Suggested external infra

* A serverless function (Cloudflare Workers / AWS Lambda) running every 5 min: fetch `vault` + Pyth + Phoenix trader account, compute the deltas, push to Grafana / Datadog / PagerDuty.
* A `program.addEventListener` consumer that subscribes to `DepositEvent`, `WithdrawEvent`, `WithdrawalRequested`, `WithdrawalProcessed`, `FeesCollected`, `PositionOpened`, `PositionClosed`, `NavAttested`, `IdleLent`, `IdleUnlent`, `ReserveLent`, `ReserveUnlent`, `WindDownInitiated`, and writes to a database for audit / reporting.
* A page in the operator dashboard tying `cached_nav_usdc` to the reconciliation script's live computation, with a visible delta and an "attest now" button that calls `attest_nav` when the delta crosses an internal threshold.

***

## Related

* [Keeper bot](/for-operators/keeper-bot) · [Admin operations](/for-operators/admin-ops)
* [Events](/for-developers/events) · [Errors](/for-developers/errors)


# Whitepaper

*Keystone Finance · Whitepaper v1.2 · 2026 · Kamran Choudhry*

***

ksUSD is a share in a vault that holds staked SOL and hedges the price risk away. Deposit USDC, hold the token, and the share price rises as carry accrues. Nothing is emitted, nothing sits off-chain, and every position can be checked on Solana.

One program, one vault, one share mint.

## I. What it converts

ksUSD converts SOL staking yield into dollar yield. The hedge is what makes that conversion possible, and when funding is positive the hedge pays you rather than costing you.

That is the whole product. It is worth saying against the usual framing, because almost every yield-bearing dollar on-chain is, underneath, a bet on perp funding, and this one isn't. Funding is the obvious thing to build on and the first thing to go when you need it. Ethena demonstrated both halves: sUSDe rode positive funding to around 15% through 2025, then settled back to 3.7% as funding compressed.¹ Build a dollar on funding and you inherit funding's cycle. Here funding is upside on a conversion that already works without it.

Fully hedged, per $100 of NAV per year:

| Leg                                            | Per year    |
| ---------------------------------------------- | ----------- |
| $90.9 of jitoSOL @ \~7% staking                | **+$6.36**  |
| $9.1 of USDC margin at Phoenix @ 0%            | $0.00       |
| Funding on the short — *Binance proxy, +3.76%* | +$3.42      |
| Funding on the short — ***measured Phoenix***  | **\~$0.00** |
| Perp fees + \~6 mode switches                  | −$0.30      |

The staking leg on its own earns more than the vault's entire net APY. Funding contributes about a third of the return in backtest, and close to nothing at the rate Phoenix has actually paid: over the past week its median hourly funding was zero, with 27% of hours positive.

A few things follow from that.

The correlation profile genuinely differs from both incumbents. RWA dollars move with the Fed. Ethena moves with the crypto leverage cycle. This moves with Solana network activity.

And the yield risk that matters is staking-rate compression, not thin funding. When funding is thin the vault parks in USDC lending and still earns. If Solana's staking rate falls there is nothing to park into; the ceiling simply comes down.

### Hedge integrity is the product

If the hedge is the mechanism that performs the conversion, then anything able to break the hedge breaks the product. It isn't a risk sitting beside the design; it is a hole in the design. A venue that can halt trading, or auto-deleverage a profitable short to cover someone else's loss, doesn't expose ksUSD to a bad day. It removes the leg that makes the yield a dollar yield while the vault is still holding the jitoSOL.

That is why the venue choice carries weight here rather than being a footnote. Phoenix settles on-chain. The vault opens, defends and closes its own hedge by calling the program directly, the position is visible on Solana, and no operator sits between the vault and its own short. None of that removes venue risk, and §VI is specific about what remains. It does put the mechanism somewhere it can be inspected, which is the minimum you should want from the thing the product depends on.

### What the conversion costs

Two things, stated rather than blended into a headline number.

Every dollar posted as margin is a dollar not earning staking yield. At v1 sizing about **9% of NAV sits at Phoenix as USDC margin earning nothing**, and about **91% is in jitoSOL** earning the staking rate. That drag is already inside every net figure here; it is the difference between jitoSOL's \~7% and the \~6.4% the staking leg actually contributes.

The obvious dismissal is that this is a hedged jitoSOL wrapper. The difference is what happens when funding inverts. A naive hedged LST keeps paying to hold its short and bleeds. ksUSD closes and parks in USDC lending, an exit that costs 20–40 bps, is written into the program, and is taken automatically on a signal nobody has to interpret. Parking is the part that isn't a wrapper.

## II. How it works

The vault has two modes and one rule for choosing between them.

| Mode             | Position                                                                     | Earns                              |
| ---------------- | ---------------------------------------------------------------------------- | ---------------------------------- |
| **Normal basis** | long jitoSOL spot (unlevered) + short SOL-PERP on Phoenix at 1×, USDC margin | jitoSOL staking + funding received |
| **Parked**       | all capital in USDC lending (Kamino)                                         | lending yield (\~4–5%)             |

<figure><img src="/files/TS0IbZEvvpJwuglwtmIC" alt="Smoothed perp funding decides the mode: when funding clears the dynamic threshold (near 0%) the vault moves to Normal basis (short plus staking); when funding is below it the vault moves to Parked (USDC lending). Transitions are automatic, driven by the funding signal."><figcaption><p>One on-chain signal, smoothed funding, decides the mode.</p></figcaption></figure>

The rule asks whether the hedge pays for itself, not whether funding is positive. Staking already carries the position, so the basis beats parked lending down to roughly −1.3% funding. The keeper computes that break-even live from the staking-versus-lending spread, and an on-chain floor of zero stops the program shorting into negative funding whatever the keeper concludes.

It applies as a ±3% band rather than a line. The vault opens only once funding clears the threshold by the band, and holds until funding falls clearly below it. In a thin regime funding crosses the line constantly, and paying the round-trip switch cost on every crossing is the largest avoidable leak in the design.

Transitions run automatically. The smoothed on-chain funding signal decides, a keeper bot executes, and a 7-day mean plus a 12-hour minimum hold filter out the noise. See [Strategy & Modes](/how-it-works/strategy-and-modes#when-does-the-vault-turn-the-trade-on).

### Near zero, not zero

Four things stop the legs cancelling exactly.

The spot leg is jitoSOL while the short is SOL-PERP, so they only offset while the jitoSOL/SOL ratio holds; a depeg is a straight loss. Delta drifts between rebalances. Staking accrual is left unhedged on purpose, since that appreciation is the yield, which leaves the vault carrying a small deliberate net long. And the perp can trade away from spot, with the position marked against the perp.

None of this is large in ordinary conditions. It is still enough that "no price risk" would be the wrong claim. See [Risk](/how-it-works/volatility-risk-management).

## III. The share, and what it costs

ksUSD is a **non-rebasing** share token. Your balance stays fixed and the share price rises as carry accrues. Redemption is against NAV, instant from the liquidity buffer and queued for larger size.

It is a share in a hedged carry vault, not a dollar-pegged stablecoin. No fixed yield, no RWA, no emissions, no view on where SOL goes. The architecture is small on purpose: one program, one vault, one rule set.

| Fee          | Rate                                   |
| ------------ | -------------------------------------- |
| Management   | **0%**                                 |
| Performance  | **20%, and only above a hurdle**       |
| Reserve skim | **5% of perf** → on-chain reserve fund |
| Withdrawal   | **0%**                                 |

The hurdle is the USDC lending rate, which is what the same capital would otherwise earn, so the performance fee applies only to what the vault adds on top. Without it the fee took a fifth of the parked leg's return and left a parked holder behind where they'd have been simply lending the USDC. Charging for that is hard to defend.

The rate isn't fixed in code. `hurdle_apr_bps` is read from Kamino's live supply APY at deploy and maintained against the smoothed rate afterwards. See [Fees](/reference/fees).

## IV. Why this only assembles on Solana

The design needs four things on one chain, close enough together to compose:

* **An on-chain perp.** The vault opens its hedge by calling into Phoenix Perps with USDC margin from its own program. Phoenix settles on-chain, and that direct call is the part that doesn't port.
* **Funding it receives**, rather than a pool borrow-fee it pays.
* **A high-yield LST and deep USDC lending.** jitoSOL held unlevered as the spot leg; Kamino for the liquidity buffer and parked NAV.
* **Fees low enough** that regular rebalancing stays economic.

On Ethereum these don't line up. Gas makes rebalancing expensive, the deepest perps sit off-chain or on separate domains, and LST yield, funding and lending live apart from each other.

The spot leg also earns more here. Solana staking plus MEV tips put jitoSOL near 7% against roughly 3% for Ethereum LSTs. Since the staking leg is where the return actually comes from, that gap matters more than the composability argument does.

## V. What the backtest shows

Daily resolution, July 2024 to June 2026, spanning the 2025–26 funding compression. The v1 set, normal plus parked, compounded 12.4% net, turning $100 into $112.37, and sat parked on a third of days.

| Variant                  | Net APY     | Gross APY | $100 net →  | Max DD                |
| ------------------------ | ----------- | --------- | ----------- | --------------------- |
| **Normal + parked (v1)** | **\~6%**    | \~7%      | **$112.37** | **−0.3%**             |
| USDC lending benchmark   | \~4%        | —         | —           | flat floor            |
| sUSDe benchmark          | \~5% recent | —         | —           | bleeds in low funding |

The drawdown is the number worth reading. Holding a 0.3% peak-to-trough loss through a funding collapse says something about the design. The yield magnitude says something about a regime nobody controls.

That 6% is fully loaded. It carries the performance fee, the margin-capital haircut where USDC posted as margin earns nothing, and trading costs. Against USDC lending it leaves a bit under 200 bps of edge in a thin-funding window, scaling toward 11% when funding runs rich.

Phoenix launched in December 2025, so the series uses daily Binance SOL-perp funding as a proxy, taken at face value with no multiplier applied — Phoenix anchors its own funding to CEX price feeds, so it tracks the same prices. Mode classification uses a 7-day mean with a ±3% hysteresis band. The perp leg is deliberately over-margined, well inside the venue limit, so about 9% of NAV sits at zero yield. Costs are 5 bps perp fee and 10 bps slippage per side, 20–40 bps per switch.

The proxy's weakness deserves stating. Even at face value it flatters the funding leg by a wide margin against what Phoenix has actually paid. The staking leg carries the result, which is the argument in §I, but it does mean the funding contribution should be read as a ceiling rather than a forecast. A single month under stress can also run worse than the modelled 0.3%. Detail: [historical-simulation.md](/reference/historical-simulation).

## VI. What can go wrong

Share price is NAV divided by shares, marked from on-chain balances and oracle-priced legs. Keeper attestation is bounded by a per-hour change cap and an oracle sanity band, so it cannot mark NAV past what the chain shows. A 10%-of-NAV buffer covers normal redemptions instantly. Larger ones burn at the locked price and settle against realized unwind proceeds through a permissionless crank, which means a queued payout in stressed markets can come in below marked NAV.

| Risk                           | Mitigation                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Smart-contract                 | Pre-mainnet audit scheduled; devnet only until then.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| **Hedge removed by the venue** | The one that matters most, per §I. A market halt, or auto-deleveraging that force-closes a profitable short to cover another trader's loss, ends the conversion while the vault still holds jitoSOL — leaving it long SOL until it can re-hedge or sell. On-chain settlement means the vault can see and act on this itself, and `emergency_close` can unwind the spot leg once the market is readable, but neither prevents an ADL. Sizing the short at a small fraction of open interest is the main defence, and it is why the deposit cap tracks the venue rather than demand. |
| Perp-venue dependency          | Phoenix is the only hedge venue, so an exploit, outage, or socialized loss hits ksUSD directly. Phoenix is in private beta, and trader onboarding needs an Ellipsis builder-access grant, which is a live external dependency. A second venue is the structural fix and is not in v1.                                                                                                                                                                                                                                                                                              |
| Counterparty                   | Phoenix, Ember, Kamino, Jupiter, Jito. No single one holds all NAV. The reserve fund absorbs first loss **up to its size, which is small** — see below.                                                                                                                                                                                                                                                                                                                                                                                                                            |
| Oracle                         | Pyth reads gated by 5-min staleness and 2% confidence; outages past that can still cause loss.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| Staking-rate compression       | The primary yield risk. A lower Solana staking rate lowers the ceiling, and parking does not help.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| Funding compression            | Secondary. The vault parks and earns \~4%; upside above that isn't guaranteed and is \~0 today.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| jitoSOL depeg                  | **Marked at market**, via the Pyth jitoSOL/USD feed — so a liquidity discount hits NAV and the share price immediately, not on sale. A discount of `lst_depeg_bps` against the Jito stake pool's redemption rate auto-pauses and arms `emergency_close`. Slashing is unhedgeable, and neither Jito nor any third party reimburses it.                                                                                                                                                                                                                                              |
| Liquidation                    | The short is over-margined and the keeper tops margin up actively. Both are needed: the jitoSOL sits in the vault while the margin sits at Phoenix, so the venue cannot see the collateral backing the short.                                                                                                                                                                                                                                                                                                                                                                      |
| Keeper outage                  | Buffer withdrawals stay open; rotation and queue processing pause until cranking resumes. Anyone can crank.                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |

Fallbacks are built in. Funding compression parks the vault. Perp-venue trouble triggers an oracle-divergence auto-pause and a permissionless `emergency_close`. A depeg auto-pauses. Wind-down turns the vault into a pro-rata USDC claim. Redemption works whether or not the team is present.

## VII. Position

|                | Yield source                          | Behaviour when funding dies      | Transparency             | Venues                                |
| -------------- | ------------------------------------- | -------------------------------- | ------------------------ | ------------------------------------- |
| **ksUSD**      | **Staking**, plus funding and lending | Parks at the lending floor       | Fully on-chain           | Phoenix, Kamino, Jito (Solana-native) |
| Ethena (sUSDe) | Delta-hedged perp funding             | Yield follows funding down       | Off-chain CEX, attested  | CEXs + custodians                     |
| MakerDAO / Sky | Stability fees + RWA/savings          | Unaffected; tracks rates instead | On-chain + off-chain RWA | RWA counterparties                    |
| Perena (USD\*) | Swap fees + stable yields             | Tracks volume instead            | On-chain                 | Solana AMM/DEX                        |

Ethena showed there is real demand for a yield-bearing dollar, and showed what happens when the one source of that yield compresses. The answer here isn't a better funding trade. It is a different engine, with funding as upside rather than foundation.

The longer goal is a dollar whose yield comes from the market it lives on, and can be checked against it.

*Simulated performance does not predict future results.*

¹ *Ethena figures are directional, per public dashboards from the backtest era.*

***

[app.keystonefi.xyz](https://app.keystonefi.xyz) · [docs.keystonefi.xyz](https://docs.keystonefi.xyz) · *Simulated figures only. Not investment advice.*


# Fee Structure

There's one fee schedule, and it applies to the vault as a whole rather than to individual holders.

Fees are paid in USDC out of the vault. They are not paid by minting new tokens, and nothing is ever taken out of your balance. What you see afterward is a share price that reflects the money that actually left.

***

## Rates

| Fee                     | Rate                                                                      | Mechanism                                                                                                                                                                                |
| ----------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Management              | **0%**                                                                    | Not charged. Capital does not pay rent.                                                                                                                                                  |
| Performance             | **20% above the HWM, itself above a hurdle set to the USDC lending rate** | Accrues only on gains that beat both the high-water mark and the benchmark, and is set aside as it's earned. The hurdle is `hurdle_apr_bps`, measured live at deploy — not a fixed rate. |
| Reserve skim            | **5% of accrued performance fees**                                        | Routed to the reserve fund instead of the admin.                                                                                                                                         |
| Rebalance / mode switch | **0%**                                                                    | No fee charged on internal position rotation.                                                                                                                                            |
| Withdrawal              | **0%**                                                                    | Neither instant nor queued path charges a fee.                                                                                                                                           |

***

## High-water mark (HWM)

The high-water mark is the highest **gross** share price the vault has ever reached, stored as `vault.hwm_share_price_1e9`. Performance fees accrue only while the gross price is above it.

The practical effect: if the vault dips and then recovers, you pay nothing on the recovery. You're only charged on genuinely new gains.

### The hurdle: the HWM ratchets at the benchmark rate

A plain high-water mark charges a fifth of *every* dollar of gain, including gains the vault didn't really add. The vault spends much of its life Parked, which is USDC lent on Kamino — something any holder can do directly in one click. Charging a performance fee on that leaves the holder behind where they'd have been without the vault at all, which is not a fee schedule anyone should defend.

So the HWM advances *on its own* at `hurdle_apr_bps` per year, whether or not the vault earns anything:

```
dt      = now − last_fee_accrual_ts
hwm    += hwm × hurdle_apr_bps × dt / (10_000 × SECONDS_PER_YEAR)
```

Only the excess over that ratchet is fee-bearing. A year that returns exactly the hurdle rate costs nothing; a year above it is charged only on the part above.

**`hurdle_apr_bps` is not a compiled-in rate.** `scripts/mainnet/init-v1.ts` reads the live Kamino USDC supply APY at deploy and writes that value. On mainnet a failed read aborts the deploy rather than falling back, because the fallback would be exactly the stale constant the hurdle exists to avoid. (The default in `InitializeParams` is a tests-and-localnet value, not the launch one.) After deploy it's adjustable via `update_params`, capped at 20%/yr, so it can track the benchmark as that moves. Setting it to `0` restores the plain HWM exactly.

The drift is not hypothetical — the rate moved by most of a point within days of the value first being chosen. Any rate printed in these docs is a dated snapshot; the live one is whatever `hurdle_apr_bps` currently reads on the vault account.

### What happens when lending spikes

Measuring the rate at deploy fixes the starting value, not the drift. USDC lending goes rich during liquidation cascades, and nothing updates the stored number on its own. That cuts both ways.

**A stale hurdle is worse than no hurdle.** If lending runs at triple the stored rate, the vault charges a performance fee on return the holder could have had by lending directly — and the overcharge scales with the spike. The hurdle has to be maintained; it is not set-and-forget.

**But chasing the spike is also wrong**, because the ratchet is one-way. Raise the hurdle, leave it a month, revert — the benchmark has already advanced and there is no un-ratchet, so a transient spike would suppress fees permanently.

Two mechanisms resolve that:

* **On-chain, the gap is capped.** The ratchet stops once the HWM sits `MAX_HURDLE_GAP_BPS` (5%) above the current gross share price. Over-reacting to a spike therefore costs a bounded amount rather than an unbounded one, which makes tracking the benchmark honestly a safe thing to do. It also stops a long flat stretch from putting the HWM so far overhead that every later recovery is fee-free forever.
* **Off-chain, the keeper watches the drift.** The `watchdog` duty compares `hurdle_apr_bps` against the *smoothed* lending APY rather than the spot rate, and warns once they diverge. Smoothing is the point: it fires on a rate that has actually moved, not on the first hour of a cascade.

**The operator rule:** raise the hurdle when the smoothed benchmark has held above it for a sustained stretch, not on a single spike. The cap means being late is cheap and being early is bounded.

There's a second-order effect too. A lending spike raises the opportunity cost of being hedged, so the keeper's dynamic threshold will already be pushing the vault toward Parked. The hurdle question and the regime question move together.

The implementation handles a few edge cases deliberately:

* **An empty vault does not ratchet.** A vault at zero shares for a year would otherwise hand the first depositor a hurdle debt to climb out of.
* **The ratchet won't run over intervals under an hour.** It rounds to whole units, so a short interval throws away most of its own increment — and because `settle` is permissionless, a fast crank could otherwise have quietly shrunk the hurdle. Short intervals are banked until an hour has accumulated, so the hurdle comes out the same however often anyone cranks.
* **A zero `last_fee_accrual_ts` does not ratchet.** The field is zero on a vault whose state predates it; without the guard `dt` would be the entire Unix epoch and the hurdle would swallow every fee the vault ever earns. The first accrual just sets the clock.

### Fees accrue continuously, not at collection

Fees are set aside as they're earned. `accrue_perf_fees` runs on every price change (`settle`, `attest_nav`) and again before any deposit mints shares:

```
gross_price      = gross_effective_nav_usdc × 1e9 / total_shares
if gross_price <= hwm_share_price_1e9:  nothing accrues
gain_per_share   = gross_price − hwm_share_price_1e9
fee              = gain_per_share × total_shares × perf_fee_bps / 1e13   // 1e9 price × 1e4 bps
pending_perf_fees_usdc += fee        (capped at gross_effective_nav_usdc)
hwm_share_price_1e9     = gross_price
```

Both of the consequences below were audit fixes:

* **`pending_perf_fees_usdc` is subtracted from `effective_nav_usdc`.** So the share price is always already net of fees owed, and collecting them later doesn't step the price down. An exiting holder also can't dodge a fee they've already accrued.
* **The HWM advances before new shares are minted.** A fresh depositor is never charged on appreciation that happened before they arrived. Accruing incrementally is what avoids that: recomputing `(price − HWM) × total_shares` once at collection time would apply a stale HWM to a grown share count and overcharge anyone who entered above it.

### What `collect_fees` does

By the time it runs, the amount is already determined. Collection just moves money.

1. Requires `position_mode == Idle` (Parked) and `!paused`, and only the admin can call it.
2. Calls `accrue_perf_fees` to catch up to the current price, then reads `pending_perf_fees_usdc`. If it's zero, the call is a no-op.
3. Splits it: `reserve_share = pending × reserve_skim_bps / 10_000`, and `admin_share = pending − reserve_share`.
4. Requires the vault's main USDC account to hold at least the full fee, or reverts with `InsufficientLiquidityBuffer`.
5. Transfers `admin_share` to the admin's USDC account, and `reserve_share` to the reserve ATA, adding it to `reserve_fund_usdc`.
6. Reduces `cached_nav_usdc` by **`admin_share` only**. The reserve share moved between two vault-owned accounts, so it never left the vault. Subtracting the full fee would double-count the skim and understate the share price.
7. Zeroes `pending_perf_fees_usdc` and lowers the HWM by exactly the per-share amount paid out (`perf_fee_usdc × 1e9 / total_shares`), updating the peak if the new price is a high.

   **Why "lower by the payout" and not "re-anchor to the current price".** Those agree when you collect at a high, but accrued fees survive a dip — collection is the only thing that zeroes them. Re-anchoring while the price was below the HWM would drop the bar to the bottom of the dip and charge the whole recovery a second time, breaking the guarantee above. Subtracting only the payout keeps the gap intact, so a recovery over ground you have already paid for stays free, and the hurdle's accumulated benchmark survives too.

Because step 7 removes exactly what step 6 accounted for, collection is **share-price-neutral**.

***

## Reserve fund

The reserve fund is an insurance buffer built into the program. If the vault takes a loss, this is what absorbs it first.

* It's funded by the 5% skim on performance fees.
* It's held as USDC in `reserve_ata`, a vault-owned account kept separate from the main one, and lent on Kamino via `lend_reserve` / `unlend_reserve` so it earns while it waits.
* It can only be spent through `pay_from_reserve(amount)`, which the admin has to sign and which is logged on-chain. If part of the reserve is lent out at the time, the admin has to call `unlend_reserve` first.
* The vault tracks it as `reserve_fund_usdc`, including whatever portion is currently lent.
* It's left out of `effective_nav_usdc`, so the reserve growing doesn't flatter the share price and spending it doesn't dilute anyone.

Nothing draws on the reserve automatically. The admin has to call `pay_from_reserve` deliberately, which keeps it from being spent on ordinary volatility.

***

## Who can call `collect_fees`

* Only the admin can call `collect_fees`
* The math is fully on-chain and reproducible from `cached_nav_usdc`, `queue_pending_usdc`, `reserve_fund_usdc`, `pending_perf_fees_usdc`, `total_shares`, and `hwm_share_price_1e9`
* `pending_perf_fees_usdc` is readable at any time, so the fee owed is visible before anyone collects it
* Anyone can check the fee delta before and after. Calling it needs the admin; auditing it needs nobody.

***

## Worked example

**Start:**

* Vault holds $1,000,000 NAV, 1,000,000 ksUSD outstanding
* Share price = $1.0000 · HWM = $1.0000

**Carry accrues** over a quarter. `cached_nav_usdc` grows to $1,030,000, and each `settle` along the way calls `accrue_perf_fees`. By the end:

* Gross share price: $1.0300, so gross gain per share is $0.0300
* Accrued fee: 0.0300 × 1,000,000 × 0.20 = **$6,000**, held in `pending_perf_fees_usdc`
* HWM has advanced to **$1.0300** (gross)
* Net share price: ($1,030,000 − $6,000) / 1,000,000 = **$1.0240**

Holders have been seeing $1.0240 all along, because the accrued fee was already carved out. Now the admin runs `close_position` to move the vault to Parked, then calls `collect_fees`:

* Reserve skim: $6,000 × 0.05 = **$300** → to `reserve_ata`, and `reserve_fund_usdc` += $300 (`lend_reserve` can then redeploy it on Kamino)
* Admin share: **$5,700** → admin's USDC account
* `cached_nav_usdc`: $1,030,000 − $5,700 = **$1,024,300** (admin share only; the $300 never left the vault)
* `pending_perf_fees_usdc` → 0, and `reserve_fund_usdc` is up $300
* Net share price: ($1,024,300 − $0 − $300) / 1,000,000 = **$1.0240**, unchanged
* HWM re-anchors to **$1.0240**

The share price doesn't move at collection, which is the whole design goal. The next fee only accrues above $1.0240, so recovering from a drawdown is free.

***

## Related

* [NAV & share pricing](/reference/nav-calculation) — share-price math, `effective_nav_usdc`
* [Admin operations](/for-operators/admin-ops) — `collect_fees`, `pay_from_reserve`
* [Security model](/reference/security) — admin role and HWM monotonicity guarantee
* [Whitepaper — Yield sources & fees](/reference/whitepaper#yield-sources--fees)


# NAV & Share Pricing

ksUSD prices itself the way sUSDe does. Your `ksUSD` balance never changes. What changes is the price of each token, which drifts up as carry accrues. So you don't watch for new tokens arriving; you watch the price.

NAV, below, is net asset value: the total dollar value of everything the vault holds.

***

## Share price

There are two NAV figures, and the difference between them is the accrued performance fee.

```
gross_effective_nav_usdc = cached_nav_usdc − queue_pending_usdc − reserve_fund_usdc
effective_nav_usdc       = gross_effective_nav_usdc − pending_perf_fees_usdc

share_price_1e9       = effective_nav_usdc × 1e9 / total_shares         (when total_shares > 0)
                      = 1_000_000_000 (= $1.00)                         (when total_shares = 0)
gross_share_price_1e9 = gross_effective_nav_usdc × 1e9 / total_shares
```

The **net** price (`share_price_1e9`) is what deposits and withdrawals use, so it's the one that matters to you. The **gross** price is what the high-water mark is measured against. Net is always ≤ gross.

* `cached_nav_usdc` — USDC base units (6 decimals)
* `total_shares` — ksUSD base units (6 decimals)
* The `1e9` scaling keeps enough precision without needing 128-bit math beyond the intermediate step
* Rust helpers: `Vault::share_price_1e9()` and `Vault::gross_share_price_1e9()`

### Why three things come out of NAV

| Subtraction              | Reason                                                                                                                                                                                                                                                                        |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `queue_pending_usdc`     | These shares are already burned and the vault owes real USDC against them. Leaving that money in the count would let new depositors mark the price up against a liability that's already locked.                                                                              |
| `reserve_fund_usdc`      | The reserve exists to absorb losses, not to pay holders. Keeping it out means the share price shows only what actually belongs to holders.                                                                                                                                    |
| `pending_perf_fees_usdc` | Performance fees are set aside as they're earned, not at collection time. Carving them out here means the share price is already net of fees owed, so collecting them later doesn't step the price down, and a holder who exits first can't dodge a fee they already accrued. |

***

## What `cached_nav_usdc` includes

The vault recalculates `cached_nav_usdc` any time its state changes. It adds up:

```
NAV = idle USDC in the vault's USDC ATA
    + reserve fund balance (whether parked or lent)
    + USDC lent on Kamino (idle-USDC leg)
    + (jitoSOL spot holdings × oracle jitoSOL price)    [normal basis — the unlevered spot leg]
    + perp-venue margin balance (settled funding + PnL) [normal basis — USDC margin on Phoenix]
```

Between updates, funding and profit still sitting open on the perp venue are not counted in `cached_nav_usdc`. Anyone can run `settle` to refresh the margin figures from the venue. For open profit and loss the keeper can also run `attest_nav(new_nav_usdc)`, which is capped per hour by `max_nav_change_bps_per_hour`.

***

## Oracle pricing

jitoSOL and SOL/USD prices come from Pyth. Every single read is checked twice before the vault will use it:

| Check      | Threshold                          | Effect                                               |
| ---------- | ---------------------------------- | ---------------------------------------------------- |
| Staleness  | < 5 minutes since last Pyth update | Stale → revert with `StaleOracle`                    |
| Confidence | conf / price < 2%                  | Out of band → revert with `OracleConfidenceExceeded` |

The thresholds are tight enough to catch an oracle outage or a manipulated feed, but loose enough not to trip on ordinary market activity.

***

## Drawdown peak

`vault.peak_share_price_1e9` holds the highest share price any `settle` has ever seen. The drawdown guard measures how far the price has fallen from that peak:

```
drawdown_bps = (peak - current) × 10_000 / peak
if drawdown_bps >= emergency_close_dd_bps:
    emergency_close is permissionless to crank
```

A single bad reading can't force an emergency exit. The condition has to hold across `consecutive_dd_settles_required` settles in a row, which defaults to 2, and firing early reverts with `DrawdownTriggerLatent`.

Only the admin can reset the peak, using `reset_peak`; collecting fees doesn't touch it. And note that the drawdown watches the live share price, not the post-fee high-water mark used for fees.

***

## Worked example

Initial state:

* `cached_nav_usdc = 1_000_000_000_000` ($1M)
* `total_shares = 1_000_000_000_000` (1M ksUSD, 6 decimals)
* `share_price_1e9 = 1_000_000_000` ($1.0000)

A keeper opens a normal-basis position, and over time funding earns $30,000 net. After the next `settle` refreshes NAV:

* `cached_nav_usdc = 1_030_000_000_000` ($1.03M)
* `total_shares = 1_000_000_000_000` (unchanged)
* `share_price_1e9 = 1_030_000_000` ($1.0300)

A new depositor sends 100 USDC:

* Shares minted: `100_000_000 × 1e9 / 1_030_000_000 = 97_087_378.6…`, truncated to **97,087,378** ksUSD base units
* Vault state after: `total_shares ≈ 1_000_097_087_378`, `cached_nav_usdc = 1_030_100_000_000`

They end up with about 97.087 ksUSD, worth the same \~100 USDC they put in. Fewer tokens than the earlier depositor got, each one worth more.

> This example ignores fees to keep the arithmetic clear. In the real path, `deposit` calls `accrue_perf_fees` before it prices the mint, so the share price is already net of fees owed and the new depositor is never charged on gains that happened before they arrived.

***

## Related

* [Fee structure](/reference/fees) — HWM math and reserve skim
* [Check position & NAV](/keystone-finance/check-position) — TypeScript snippets to query share price live
* [Security model](/reference/security) — oracle, slippage, drawdown guard


# Historical Simulation

*Daily resolution, Jul 2024 – Jun 2026 (24 months). Backtest re-cut 2026-08-05 for the fee hurdle; venue and lending rates measured the same day.*

> **Backtest.** Phoenix only launched in Dec 2025, so there isn't enough of its own funding history to test against. This runs on historical SOL-perp funding as a stand-in, using the two modes v1 actually has: normal basis and parked. Phoenix anchors its funding to CEX index feeds, so the stand-in uses **Binance funding at face value (×1.0)**, which tracks the same prices Phoenix does. An earlier version calibrated against Drift ran hot and produced a \~9% figure. The honest CEX-anchored number is \~6% net.

***

## Headline

Over 24 months that include the 2025–26 funding compression, the strategy's worst peak-to-trough fall was **−0.3%**, and $100 compounded to **$112.37**. It sat parked on a third of days, when funding wasn't paying enough to be worth it.

| Metric                                    | Value     |
| ----------------------------------------- | --------- |
| **Max drawdown**                          | **−0.3%** |
| Net APY (thin-funding regime, after fees) | **\~6%**  |
| Gross APY                                 | \~7%      |
| Parked share of days                      | 33%       |
| Parked floor / USDC lending benchmark     | \~4%      |

Read the drawdown first, not the yield. The design's whole claim is that it kept compounding straight through a funding collapse and never gave back more than a fraction of a percent from a peak.

The yield depends on the regime. About 6% net is what this risk profile pays when funding is thin, as it is now, and it scales toward \~11% when funding is rich. Phoenix pays roughly nothing today, so the vault sits near its lending floor and adds funding on top whenever there's funding to collect.

> These are backtest figures. The "\~0%" you may see in the app's regime widget is a live funding snapshot, not a backtest APY.

***

## What's already inside the number

The figure the backtest produces is the figure claimed. Nothing is adjusted on top of it afterwards.

Every cost sits inside the gross return: perp fees, slippage, mode-switch costs, and the margin haircut — the slice of NAV posted as USDC margin at Phoenix, which earns nothing rather than the basis. The only step from gross to net is the performance fee, charged above the high-water mark and above the lending-rate hurdle.

So \~6% is fully loaded, not a starting point to subtract from.

***

## Where the yield comes from

Most of it is staking, not funding. Fully hedged, per $100 of NAV per year:

| Leg                                            | Per year    |
| ---------------------------------------------- | ----------- |
| $90.9 of jitoSOL @ \~7% staking                | **+$6.36**  |
| $9.1 of USDC margin at Phoenix @ 0%            | $0.00       |
| Funding on the short — *Binance proxy, +3.76%* | +$3.42      |
| Funding on the short — ***measured Phoenix***  | **\~$0.00** |
| Perp fees + \~6 mode switches                  | −$0.30      |

**The staking leg alone out-earns the whole product's net APY.** Funding is the smaller half in the backtest, and close to nothing at Phoenix's actual rate — its median hourly funding over the last week was zero.

That's why the mode threshold is derived from the staking-versus-lending spread rather than from funding, and why the vault stays hedged even at slightly negative funding. The hedge takes price risk down to near zero; it is not the revenue.

It also means the Binance stand-in flatters the funding leg by a wide margin, so read that row as a ceiling rather than a forecast. And the yield risk that matters is staking-rate compression, not thin funding: thin funding parks the vault at the lending floor, whereas a lower Solana staking rate lowers the ceiling with nothing to park into.

***

## The fee hurdle

The performance fee applies only to return **above a hurdle** set to the USDC lending rate. Strategy, gross return and drawdown are all unchanged by it; the fee schedule is the only thing that moved.

It exists because of the parked leg. A third of the days here are spent lending USDC on Kamino, which any holder could do themselves in one click. A flat performance fee took a fifth of that return, leaving a parked holder behind where they'd have been without the vault at all. The hurdle removes that, and it tracks the lending rate rather than sitting at a fixed number.

Over this window it lifts net APY from **5.2% to 6.0%**, because the fees charged fall by close to 60%. Parked, you keep the lending rate. Hedged, the edge over lending it yourself is a bit under 200 bps.

```bash
npm run fund:backtest                    # with the hurdle (current)
HURDLE_APR_BPS=0 npm run fund:backtest   # the old flat-fee model
```

***

## Methodology

| Input               | Value                                                                                                                                                                                                                                               |
| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Window              | Jul 2024 – Jun 2026 (24 months, recent track record)                                                                                                                                                                                                |
| Funding source      | Daily Binance funding, taken at face value as a stand-in for Phoenix. Phoenix anchors its own funding to CEX index feeds (Binance, Coinbase, Hyperliquid, Bybit), so the stand-in references the same prices Phoenix does. No multiplier is applied |
| Mode classification | A 7-day rolling average with a ±3% band around the threshold to filter out flip-flopping. Normal basis when funding clears the threshold by that band, otherwise parked                                                                             |
| Margin haircut      | The short is deliberately over-margined, well inside the venue's limit, which leaves about 9% of NAV sitting as USDC margin earning nothing                                                                                                         |
| Parked behavior     | USDC at 4% lending APR                                                                                                                                                                                                                              |
| Costs               | 5 bps perp fee + 10 bps slippage per side; 20–40 bps mode-switch cost                                                                                                                                                                               |
| Fees                | 0% management, 20% performance above the HWM and above the lending-rate hurdle                                                                                                                                                                      |

**Reproduce:**

```bash
npm run fund:backtest    # pins --no-reverse --start=2024-07-01 --end=2026-06-30
```

`--no-reverse` is required. It restricts the run to the two modes v1 actually has, normal basis and parked. Without it the run also trades the dormant Reverse leg, which isn't in the program, and reports a materially different result. Data CSVs are committed under `scripts/simulations/data/`.

***

## Frictions not in the model

* **Price impact when rebalancing the perp leg.** The model uses a fixed slippage assumption. Real trades of real size depend on how much liquidity is there at the time, and under stress a month could run worse than the modeled −0.3%.
* **Proxy risk.** Using Binance funding at face value assumes Phoenix tracks CEX index feeds closely. Phoenix's own book may still behave differently in practice.
* **Reserve-fund drag** and the smoothing from hourly funding to daily figures. Neither is subtracted from the headline.

***

## Related

* [Whitepaper](/reference/whitepaper) — Performance section
* [Backtest data CSVs](https://github.com/kamwithak/keystone-contracts/tree/main/scripts/simulations/data/README.md)


# Utilization Metrics

How to work out where the vault's money is sitting at any given moment.

> Everything here comes from `vault.cached_nav_usdc` and the position fields. There are no view helpers to call; just read the `Vault` account directly.

***

## Buckets

Everything the vault holds falls into one of these:

| Bucket            | Source                                                                                     |
| ----------------- | ------------------------------------------------------------------------------------------ |
| Liquidity buffer  | Vault USDC ATA balance — sized to `liquidity_buffer_bps × cached_nav_usdc / 10_000` target |
| Reserve fund      | `reserve_fund_usdc` (productively lent on Kamino)                                          |
| Parked capital    | Kamino USDC reserve via `lend_idle_usdc`                                                   |
| jitoSOL spot leg  | jitoSOL held unlevered (normal basis) — `spot holdings × oracle jitoSOL price`             |
| Perp-venue margin | USDC margin on Phoenix via Ember (normal basis)                                            |
| Perp PnL          | Settled funding + mark-vs-entry PnL on the open position                                   |

***

## Derived ratios

```ts
const buffer        = vaultUsdcAtaBalance;          // off-chain query
const target        = vault.cachedNavUsdc.muln(vault.liquidityBufferBps).divn(10_000);
const bufferUtilBps = Number(buffer.mul(10_000n).div(target.toBigInt()));  // 10_000 = exactly on target

const queuePendingRatioBps = vault.queuePendingUsdc * 10_000n / vault.cachedNavUsdc;
const navPerShare1e9       = vault.cachedNavUsdc * 1_000_000_000n / vault.totalShares;
```

| Ratio                  | What it means                                                                                                                                                               |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bufferUtilBps`        | Whether the buffer holds enough to cover instant withdrawals. 10,000 means exactly on target. Below 9,000 it's underfunded and the keeper should move more capital into it. |
| `queuePendingRatioBps` | How much USDC is owed to queued withdrawals, as a share of NAV. If this stays above 10%, the keeper needs to unwind position to free up cash.                               |
| `navPerShare1e9`       | The live share price, scaled by 1e9. Drifts up with carry, and only ever starts at par on the very first deposit.                                                           |

***

## When to alert

| Condition                                                           | Interpretation                                                                |
| ------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| `bufferUtilBps < 5_000` for > 1h                                    | The buffer is under half its target, so instant withdrawals may start failing |
| `queuePendingRatioBps > 1_500` for > 6h                             | The queue is over 15% of NAV; the keeper should unwind position faster        |
| Perp margin or jitoSOL value drifts > 5% from what the mode implies | Something is wrong with margin, the oracle, or settlement. Investigate.       |

***

## Related

* [NAV & share pricing](/reference/nav-calculation) — full NAV formula
* [Monitoring](/for-operators/monitoring) — what to watch
* [Withdraw](/keystone-finance/withdraw) — instant vs. queued paths


# Protocol Integrations

> v1 hedges on **Phoenix Perps** (Ellipsis Labs) via on-chain CPI, USDC margin through **Ember**. The Phoenix/Ember CPI surface below is finalized with the integration.

The vault works with other Solana protocols by calling them directly from its own program, a pattern called CPI. Every one of those calls is signed by the vault itself, so no private key is involved.

For Phoenix, Keystone uses the **Rise SDK** (`github.com/Ellipsis-Labs/rise-public`) and reads Phoenix's account layouts from it. The other integrations build their instructions and account lists by hand.

***

## Venues

| Role                    | Program / Asset                                            | Address                                        |
| ----------------------- | ---------------------------------------------------------- | ---------------------------------------------- |
| Perp venue (mainnet)    | Phoenix Perps (Ellipsis Labs)                              | `EtrnLzgbS7nMMy5fbD42kXiUzGg8XQzJ972Xtk1cjWih` |
| Margin (USDC → Phoenix) | Ember                                                      | `EMBERpYNE6ehWmXymZZS2skiFmCa9V5dp14e1iduM5qy` |
| Spot leg                | jitoSOL (LST, held for staking — **not** posted as margin) | `J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn` |
| Lending / parked        | Kamino USDC reserve                                        | `D6q6wuQSrifJKZYpR1M8R4YawnLDtDsMmWM1NbBmgJ59` |
| Quote / settlement      | USDC                                                       | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |

***

## Dependencies

| Protocol                          | Purpose                                                                   | Where used                                                                          |
| --------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| **Phoenix Perps** (Ellipsis Labs) | SOL-PERP short; USDC margin (posted via Ember); real two-sided funding    | `open_position`, `close_position`, `settle`, `emergency_close`, `enable_phoenix`    |
| **Ember**                         | Routes USDC → Phoenix perp margin                                         | `open_position`, `close_position` (margin post/withdraw)                            |
| **Kamino KLend**                  | USDC lending for buffer + parked + reserve capital                        | `enable_lending`, `lend_idle_usdc`, `unlend_usdc`, `lend_reserve`, `unlend_reserve` |
| **Jupiter V6**                    | All token swaps (USDC ↔ jitoSOL); off-chain quote, on-chain execution     | Every open/close path that crosses tokens                                           |
| **Pyth (pull oracles)**           | SOL/USD + jitoSOL/USD for NAV math, drawdown guard, depeg check           | `settle`, `open_*`, `close_*`, `set_oracles`                                        |
| **Jito (jitoSOL)**                | Liquid staking; held **unlevered as the spot leg** (not posted as margin) | Implicit — staking yield is embedded in the jitoSOL/SOL exchange rate               |
| **Marginfi**                      | Reserved as a second USDC lending venue (v1.1)                            | Vault fields pinned at init; not yet CPI-wired                                      |

***

## Verified mainnet addresses

| Item                                  | Address                                        |
| ------------------------------------- | ---------------------------------------------- |
| Phoenix Perps Program                 | `EtrnLzgbS7nMMy5fbD42kXiUzGg8XQzJ972Xtk1cjWih` |
| Ember Program (USDC → Phoenix margin) | `EMBERpYNE6ehWmXymZZS2skiFmCa9V5dp14e1iduM5qy` |
| Phoenix SOL-PERP market               | resolved via `PerpAssetMap` (Rise SDK)         |
| Kamino KLend Program                  | `KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD`  |
| Kamino Main Market                    | `7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF` |
| Kamino USDC Reserve                   | `D6q6wuQSrifJKZYpR1M8R4YawnLDtDsMmWM1NbBmgJ59` |
| Kamino USDT Reserve                   | `H3t6qZ1JkguCNTi9uzVKqQ7dvt2cum4XiXWom6Gn5e5S` |
| Jupiter V6 Program                    | `JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4`  |
| jitoSOL mint                          | `J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn` |
| USDC mint                             | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |

***

## CPI surface (Rust)

| Module                          | Helpers                                                                                                                                                                                                                                                                                        |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `utils::phoenix_cpi` *(target)* | Onboard trader (Rise builder path), post/withdraw USDC margin via Ember, `place_perp_order`, `cancel_orders`, settle funding, `read_phoenix_funding_rate` — built on the `phoenix-rise` account views (`TraderHeader`, `TraderPositions`, `PerpAssetMap`, `GlobalConfig`, `PermissionAccount`) |
| `utils::kamino`                 | `deposit_reserve_liquidity`, `redeem_reserve_collateral`, `refresh_reserve`                                                                                                                                                                                                                    |
| `utils::jupiter_swap`           | `route_swap`, `require_min_received`                                                                                                                                                                                                                                                           |
| `utils::oracle`                 | Pyth pull-oracle staleness + confidence wrappers, jitoSOL/SOL depeg check                                                                                                                                                                                                                      |

* Every helper passes the vault's signer seeds (`[b"vault", &[bump]]`), which is how the vault authorizes the call.
* Phoenix account layouts come from the `phoenix-rise` crate, read in place without copying. Kamino's return data is checked with minimal Borsh decoding.
* Kamino's byte offsets are hard-coded against a layout verified on-chain. Phoenix uses the Rise definitions instead of hand-written offsets.

***

## Off-chain account assembly

Strategy instructions expect 3–5 groups of accounts in `remaining_accounts`, plus a count for each group. The keeper assembles those off-chain:

* **Phoenix** — the Rise SDK (`github.com/Ellipsis-Labs/rise-public`) provides the correct account ordering for trader onboarding, margin in and out through Ember, `place_perp_order`, and funding settlement.
* **Kamino** — the same idea. The `refresh_obligation` and collateral instructions follow a fixed pattern documented in the Kamino IDL.
* **Jupiter** — Jupiter's `/swap-instructions` endpoint returns both the swap data (passed as `jupiter_swap_data`) and the account list that goes with it (passed as the matching account group).

See [Keeper bot](/for-operators/keeper-bot) for the off-chain orchestration pattern.

***

## Related

* [Whitepaper](/reference/whitepaper) — single-page overview of the system
* [Instructions reference](/for-developers/instructions) — per-instruction account-context groups
* [Quick start](/for-developers/quick-start)


# 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

| Role          | Capabilities                                                                                                                                                                                                                                                                                                                                                        |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **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

| Protection               | Mechanism                                                                                                                                                                                                    |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 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_oi_fraction_bps` of Phoenix open interest (`PerpShortExceedsOiCap`) and by `max_position_base_lots` in absolute terms                                                           |
| 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:

1. Takes the number of accounts per group as `u8` arguments.
2. Splits `remaining_accounts` using `checked_add(...).ok_or(InvalidParams)`, so an offset that would overflow reverts instead of wrapping around.
3. 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 [`docs/PRE_MAINNET_AUDIT.md`](https://github.com/kamwithak/keystone-contracts/blob/main/docs/PRE_MAINNET_AUDIT.md).

**Don't deploy significant capital before the audit.**

***

## Reporting

Suspected vulnerabilities:

* Open a private GitHub security advisory on [github.com/kamwithak/keystone-contracts](https://github.com/kamwithak/keystone-contracts/security/advisories), **or**
* Contact the maintainer directly

A formal bug bounty stands up alongside the mainnet audit.

***

## Related

* [Whitepaper — Risk](/reference/whitepaper#risk)
* [Fees](/reference/fees) · [NAV & share pricing](/reference/nav-calculation)
* [Errors](/for-developers/errors) — every revert path enumerated


# Mainnet Checklist

> ksUSD is **not yet deployed to mainnet.** This page is the launch checklist that gates the mainnet program-deploy transaction. One program, one vault, one mint.

> **Pre-deployment (v1).** The perp venue is **Phoenix Perps (Ellipsis Labs)**, USDC margin via **Ember**, integrated by on-chain CPI (Rise SDK). Before it can post margin, the vault's trader PDA needs its `CAN_DEPOSIT`/`CAN_WITHDRAW` capabilities enabled — a self-serve step against Phoenix's public endpoint, claimed by `scripts/onboard/onboard.ts`. The exchange still reports itself gated, so treat it as unproven until the first send lands.

***

## Phase 1 — Engineering (in progress)

* [x] Single-program rewrite: one `keystone_finance` program, one vault, one share mint
* [ ] **Phoenix Perps CPI via the Rise SDK** — Ember USDC-margin path, trader onboarding, funding/position reads (`phoenix-rise` account views)
* [ ] **Ellipsis capability grant** on the vault's trader PDA — flags 6 → 63, via `scripts/onboard/onboard.ts` (one funded send, \~0.05 SOL)
* [x] Kamino KLend CPI surface (idle/reserve USDC lending)
* [x] Jupiter V6 swap CPI helper with explicit `min_*_out` slippage bounds
* [x] Hardened account-context offsets (`checked_add` on all slice math)
* [x] Funding EMA + staleness guard on-chain (`funding_apr_smoothed_bps`, `FundingSignalStale`)
* [x] Drawdown latency (`consecutive_dd_settles_required`, `DrawdownTriggerLatent`)
* [x] LST depeg auto-pause (`lst_depeg_bps`, `LstDepeg`)
* [x] Reserve fund segregation (`reserve_ata`, separate cToken ATA, excluded from `effective_nav_usdc`)
* [x] Wind-down terminal path (`init_wind_down`, `claim_wind_down`)
* [x] Devnet round-trip test scaffold (`scripts/devnet/test-roundtrip.ts`)
* [ ] Keeper bot end-to-end on devnet (open/close normal + settle + emergency\_close + lend cranks)
* [ ] Replay-protected admin transfer demo (`transfer_admin` + `accept_admin`)
* [ ] Devnet QA: ≥ 30 days continuous keeper operation with no manual intervention
* [ ] `scripts/mainnet/init-v1.ts` dry-run on mainnet RPC

***

## Phase 2 — Audit

* [ ] Audit firm engaged (Ottersec / Sec3 / Neodyme — pick one)
* [ ] All issues triaged; high / critical findings resolved
* [ ] Public audit report published in the repo and linked from the whitepaper
* [ ] Bug bounty stood up (Immunefi or equivalent)

***

## Phase 3 — Mainnet deployment

The program deploy itself is one script — `scripts/mainnet/deploy-mainnet.sh` — which builds, preflights, deploys, verifies the on-chain binary against the local build, writes the IDL, and records the deploy. Run `--check` first (no transactions), and `--rehearse` against devnet to walk the identical path.

```bash
npm run mainnet:deploy:check     # preflight only
npm run mainnet:deploy           # the real thing
npm run mainnet:deploy:verify    # re-verify on-chain binary vs local build
```

* [ ] Final program build under audit-locked commit; `anchor idl build` regenerated
* [ ] Admin multisig (Squads or equivalent) provisioned and tested on devnet
* [ ] `deploy-mainnet.sh --check` clean on the audit-locked commit
* [ ] Program deployed to mainnet (`--rotate-id` first if the launch ID must be isolated from devnet)
* [ ] On-chain binary hash verified against the local build (the script does this and fails loudly)
* [ ] On-chain IDL upgraded
* [ ] `deployments/mainnet-<utc>.json` committed as the deploy record
* [ ] `initialize` with conservative launch params:
  * `deposit_cap_usdc` = $250k (venue-derived; recompute with `npm run phoenix:capacity` on the day, raise after 30 days of clean operation)
  * `liquidity_buffer_bps` = 1\_000 (10%)
  * `emergency_close_dd_bps` = 500 (5% drawdown auto-trip)
  * `consecutive_dd_settles_required` = 2
  * `perf_fee_bps` = 2\_000 (20% above HWM)
  * `reserve_skim_bps` = 500 (5% of perf fee)
  * `lst_depeg_bps` = 500 (5%)
  * `max_nav_change_bps_per_hour` = 5\_000 (±50%/hr)
* [ ] `set_oracles` with Pyth pull-oracle pubkeys for SOL/USD + jitoSOL/USD
* [ ] `enable_phoenix` — onboard the vault PDA as a Phoenix trader (Rise builder path) and pin the Phoenix/Ember accounts
* [ ] `enable_lending` against Kamino USDC reserve
* [ ] `transfer_admin` → `accept_admin` to the multisig (two-step)
* [ ] Upgrade authority moved to the multisig (`deploy-mainnet.sh --transfer-authority <PK>`), IDL authority moved with it

***

## Phase 4 — Private beta

* [ ] Invite-only; raise `deposit_cap_usdc` $250k → $1M once the launch cap fills cleanly (gated by live OI, not demand)
* [ ] Keeper bot running with redundancy (≥ 2 independent operators)
* [ ] `lend_idle_usdc` running so the buffer's excess sits in Kamino productively
* [ ] Monitoring + alerting wired ([/for-operators/monitoring.md](/for-operators/monitoring))
* [ ] First `collect_fees` cycle after 30 days
* [ ] Public dashboard with live NAV, share price, position mode, queue depth, reserve fund

***

## Phase 5 — Public launch

* [ ] Raise `deposit_cap_usdc` to $5M, then $25M as TVL fills
* [ ] First composability integration (Kamino accepts ksUSD as collateral; Marginfi v1.1 second-venue)
* [ ] DAO / treasury wrappers announced (Solana-aligned partners)
* [ ] Public docs polish + integrator quickstarts

***

## Rollback / circuit-breaker plan

If any of the following occur in the first 90 days post-launch:

| Trigger                                        | Action                                                                                                           |
| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Audit reveals a missed critical issue          | `set_pause(true)` immediately; if non-recoverable, `init_wind_down` and coordinate redeploy under new program ID |
| NAV drawdown exceeds 3% within a week          | `emergency_close` + `set_pause(true)`; investigate before resuming                                               |
| Phoenix or Kamino announce a critical incident | `set_pause(true)` until the dependency is verified safe                                                          |
| Pyth feed instability for > 1h                 | `set_pause(true)`; instant withdrawals stay open                                                                 |
| LST depeg auto-trip                            | Vault is already paused by `settle`; admin investigates and either unpauses or emergency-closes                  |

> **In all cases**, instant withdrawals from the liquidity buffer remain available. Depositors can always exit during a pause.

***

## Related

* [Security model](/reference/security) · [Admin operations](/for-operators/admin-ops)
* [Pre-mainnet audit notes](https://github.com/kamwithak/keystone-contracts/blob/main/docs/PRE_MAINNET_AUDIT.md)
* [Whitepaper](/reference/whitepaper)


# Roadmap

**ksUSD in one line:** deposit USDC, hold one token, earn carry. Most of the yield is jitoSOL staking, with perp funding and USDC lending on top. No peg to defend, no position to manage.

This is the plain-English version of where we are and what's left before launch.

***

## How it earns

The vault holds two things at once. Staked SOL (jitoSOL), which earns a staking yield. And a short SOL position on a perp venue, which offsets most of SOL's price swings and often pays you to hold it. That payment is called funding.

The short is what turns a SOL-denominated yield into a dollar one, so you collect the staking yield with almost none of SOL's volatility, plus the funding when it's being paid. Staking is the bigger half by some distance. When funding stops paying, the vault parks: it drops the short and holds plain USDC lent on Kamino until funding is worth it again. An on-chain signal decides when to switch and a bot does the work. You never touch it.

In v1 the hedge runs on Phoenix Perps, with margin posted through Ember.

> In a 24-month backtest the worst drawdown was **−0.3%**, through the toughest funding stretch of the period. Yield depends on the regime: about **6% net** when funding is thin like today, up to \~11% when it's rich. Phoenix pays \~0% funding right now, so the vault sits near its \~4% lending floor. *Simulated, not a promise.*

***

## Where we are

Most of the plumbing is built and tested.

**Done.** The vault is one program, one vault, one token, and we've run the full deposit → park → earn → redeem cycle on a copy of mainnet against real protocol state. jitoSOL staking (Jito), USDC lending (Kamino), and USDC↔jitoSOL swaps (Jupiter) are all wired up.

**In progress: the hedge.** The code to trade on Phoenix is written and the live Phoenix program accepts it. Switching it on means enabling margin permissions on the vault's trading account, which we do ourselves through Phoenix's public API in a single transaction. Our script is built and tested up to that send. What's left is proving the full open-and-close round-trip.

Until the hedge turns on, the vault runs safely in parked mode (USDC lending), which is fully proven. Adding the hedge is an addition, not a rewrite.

***

## Getting to mainnet

Two things, running in parallel:

1. **A security audit** by a top Solana firm. This is the hard gate before real money, and good firms book months out, so we're shortlisting now.
2. **A seed raise** ($200–500K) to fund the audit and runway. A DAO treasury pre-committing capital de-risks it and seeds day-one deposits.

When those clear, we deploy with a **$250k launch cap** and open an invite-only private beta.

### Why $250k

$250k is about the most the venue can hold today. We run a permanent one-directional short, so the cap is whichever of two limits binds first:

`cap = min(15% of open interest, 25% of median daily volume)`

On current Phoenix numbers, volume binds — roughly twice as tight as open interest. A clean exit is about a quarter of a day's flow, and a typical Phoenix SOL day isn't deep enough to absorb much more than this.

Note that it uses the **median** day, not the average: a handful of spike days sit inside any 30-day window, and sizing off the mean would badly overstate what the book can actually take. And a **bigger cap would make the product worse.** If most of a large vault can't be hedged, the rest sits in lending and ksUSD becomes a Kamino deposit wearing a wrapper — all of the operational risk, none of the carry.

It also self-corrects: if our own size ever pushed funding below the line, the vault just parks, so it can't run itself into a loss.

*Measured live, never quoted from memory: `npx tsx scripts/phoenix/capacity.ts`.*

***

## After launch

Once there's a real track record, two things scale it.

**Composability.** Get ksUSD accepted as collateral, starting with Kamino, so holding it earns carry and unlocks borrowing. That's what makes deposits sticky. It's the sUSDe playbook.

**Distribution.** A USDC↔ksUSD pool and Jupiter routing for one-click in and out, plus yield-marketplace listings and DAO-treasury allocations.

The cap ramps $250k → $1M → $5M → $25M, each step gated by the same `min(15% OI, 25% of 30-day median volume)` rule, not by demand and not by a date.

**We grow with Phoenix.** That's the whole sizing policy. Each step up needs the venue's median daily volume to be correspondingly deeper, and the largest steps need a second venue to split the short across. Phoenix's SOL-PERP open interest has trended up over recent weeks but swings a lot week to week, so we treat it as a level to measure rather than a trend to extrapolate. We won't put a date on someone else's order book. The vault gets bigger exactly when the venue can hedge it.

***

## Timeline

| When                      | What                                                                                   |
| ------------------------- | -------------------------------------------------------------------------------------- |
| **Now**                   | Audit-firm shortlist · seed raise · anchor conversations · finish the hedge round-trip |
| **Q3 2026**               | Full hedge round-trip proven, mainnet-ready pending the audit                          |
| **Q4 2026 – Q1 2027**     | Audit done · mainnet deploy at the $250k cap · private beta, invite-only               |
| **H1 2027**               | Public launch · USDC↔ksUSD pool + Jupiter routing · first collateral listing           |
| **When the venue allows** | $1M, then $5M → $25M — opened by Phoenix's depth, not by a date                        |

Two clocks. The **dates** ride on the audit slot and the raise, not on the engineering. The **caps** don't ride on dates at all — they track Phoenix's open interest and volume, live. So we won't quote a TVL target for a given quarter: we can promise what we ship and how we size, not how deep someone else's book gets.

***

## Not in v1

* Multiple staking tokens (mSOL, jupSOL): later.
* A second lending venue (Marginfi): later. v1 is Kamino only.

***

## Related

* [Download the full roadmap (PDF)](https://www.keystonefi.xyz/roadmap.pdf)
* [Strategy & Modes](/how-it-works/strategy-and-modes) · [Whitepaper](/reference/whitepaper) · [Version history](/changelog/versions)


# Version History

* Tracks meaningful releases of the ksUSD vault program + surrounding docs / scripts
* Older entries cover the legacy multi-strategy architecture, retained for reference
* Canonical product: single-vault ksUSD design as of v2.0

***

## v2.1 — Jun 2026 · *current*

**Venue pivot to Phoenix Perps + two-mode scope.** Design changes for the v1 mainnet path (docs pivoted ahead of code; see the target-architecture banners).

### Venue

* Perp venue: **Phoenix Perps (Ellipsis Labs)** — program `EtrnLzgbS7nMMy5fbD42kXiUzGg8XQzJ972Xtk1cjWih`, USDC margin via Ember, on-chain CPI via the Rise SDK.
* USDC margin posted via the **Ember** program (`EMBERpYNE6ehWmXymZZS2skiFmCa9V5dp14e1iduM5qy`); integration is on-chain CPI using the **Rise SDK** (`github.com/Ellipsis-Labs/rise-public`).

### Architecture (Path B)

* jitoSOL is held **unlevered** as the spot leg for staking yield; **USDC** is the perp margin. No LST is ever posted as perp collateral.

### Modes

* v1 ships **two modes — normal basis + parked** — with transitions **automated on-chain**: the smoothed funding signal plus a keeper **dynamic threshold with a ±3% hysteresis band** (lending-vs-staking spread, floored at 0) decides the mode and the keeper bot executes the switch, no manual intervention. The band covers the round-trip switch cost and prevents whipsaw.

### Backtest

* Recut against the Phoenix venue: **−0.3% max drawdown** through the 2025–26 funding compression, **\~6% net** in a thin-funding regime, fully loaded. Full method and caveats: [`reference/historical-simulation.md`](/reference/historical-simulation).

***

## v2.0 — May 2026

**The ksUSD pivot.** Wholesale repositioning and code rewrite.

### Positioning

* Current canonical framing: *"Keystone Finance is the carry coordination layer for Solana."*
* Superseded: *"Keystone Finance coordinates carry across Solana's DeFi ecosystem."*
* Earlier framings retired: "on-chain analog of a multi-strategy hedge fund", "Layer 1 / Layer 2 architecture", "three risk profiles", "monetary layer", "productive reserve asset"

### Program

* Single Anchor program: `keystone_finance` (program ID `E7tpCcxtvuTXLAckBGWb1AsndpLQ1Y9hQA3iGYSXz2vJ` on devnet)
* Single state struct: `Vault` PDA at seeds `[b"vault"]` (replaces legacy `FundState` + `PortfolioState` hierarchy)
* Single share mint: `ksUSD` (6 decimals) — replaces `ksALPHA`, `ksNEUTRAL`, `ksDEFENSE`, `ksCORE`
* Two internal modes — normal basis and idle — chosen automatically by the SOL-PERP funding rate
* An on-chain perp venue (replaces Jupiter Perps in the legacy Neutral fund)
* Full entry-point list: [Instructions](/for-developers/instructions)

### Fee structure

* Management fee: **0%** (was 0.5% / yr on legacy Alpha & Neutral).
* Performance fee: **20% above HWM** (unchanged).
* Reserve skim: **5% of accrued perf fees** routed to the reserve fund (new).
* Withdrawal fee: **0%** on both instant and queued paths.

### Withdrawal queue

* Two-path withdrawal: `withdraw_instant` (from liquidity buffer) and `request_withdrawal` + `process_withdrawal` (queued). Replaces the legacy single `withdraw` path.

### Backtest

* Established the canonical method: daily resolution over a 24-month window, historical SOL-perp funding standing in for Phoenix, 7-day rolling mean with a ±3% hysteresis band for mode classification. Superseded in detail by the v2.1 recut; see [`reference/historical-simulation.md`](/reference/historical-simulation).

### Docs

* New whitepaper at `gitbook/reference/whitepaper.md` (v2.0) — single-vault design, full NAV math, security model, risk disclosures.
* Marketing site (`keystone-marketing/`) rebuilt around ksUSD with a single Performance chart sourced from the canonical backtest.
* Investor memo, PRD, and outreach summary at `docs/KSUSD_*.md` updated to v0.4.

### Breaking changes

* All legacy program IDs (`keystoneAlpha`, `keystoneDefense`, `keystoneCore`) are no longer deployed. The on-chain footprint is now one program.
* `ksCORE` / `ksALPHA` / `ksNEUTRAL` / `ksDEFENSE` mints are unsupported. Integrators should consume `ksUSD` only.
* `rebalance`, `migrate_portfolio`, and all multi-fund admin instructions are gone.

***

## Pre-pivot history (legacy)

Pre-v2.0 covered the multi-strategy design:

* **Keystone Alpha** — directional LST accumulation
* **Keystone Neutral** — basis carry on Jupiter Perps
* **Keystone Defense** — JupSOL baseline + dislocation deployment
* **Keystone Core** — portfolio orchestration with ksCORE

Detailed entries are retained in git history; active narrative is v2.0+.

**Key milestones:**

* **v1.x** — multi-strategy architecture in development; Jupiter Perps as the perp venue
* **v0.11.5** (Apr 2026) — last release of the multi-strategy design; portfolio rebalancing live on devnet
* **v0.10.0** — `deploy_capital` consolidated with auto-unpark; `unpark_capital` retired

For the historical narrative and per-version technical notes, see commits on `main` prior to the May 2026 pivot.


# Team

### Kamran Choudhry — Founder

I'm a full-stack engineer with 7+ years building in DeFi and Web3. I studied Computer Science at McGill (with a minor in Economics), and I've spent most of my career shipping trading and on-chain infrastructure.

Before Keystone, I built front-end and full-stack systems across the space:

* **Sei Labs** — full-stack engineering on DeFi infrastructure
* **Vertex Protocol** — built the trading app that grew the exchange from $0 to $100M+ peak TVL
* **Insrt Labs** — full-stack GameFi
* **Charged Particles** — front-end for NFT products

Along the way I've shipped things like a TypeScript SDK for an on-chain DEX (Monaco Protocol's Sei incubation) and 2D Pool, a peer-to-peer billiards game with real-money stakes on Solana.

I work across TypeScript, Rust, and Solana programs, with a focus on product engineering, DEX/AMM design, and smart-contract security. Keystone is where all of that comes together.

***

## Get in touch

* **Website** — [kamwithak.github.io](https://kamwithak.github.io/)
* **X / Twitter** — [@kamranwithak](https://x.com/kamranwithak)


