> For the complete documentation index, see [llms.txt](https://docs.keystonefi.xyz/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.keystonefi.xyz/build-on-it/quick-start/instructions.md).

# Instructions Reference

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

{% hint style="warning" %}
v1 hedges on Phoenix Perps (USDC margin via Ember, CPI through the Rise SDK).
{% endhint %}

> Source: [the program entrypoints](https://github.com/kamwithak/keystone-contracts/blob/main/programs/keystone-finance/src/lib.rs) · IDL: the generated IDL

***

## One-time setup (admin)

| Instruction                     | Caller           | Description                                                                                                                                                                                                        |
| ------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `initialize(params)`            | Admin (deployer) | Creates the vault PDA at seeds `[b"vault"]` and the ksUSD share mint. Sets all risk parameters. Idempotent guard via PDA `init`.                                                                                   |
| `enable_phoenix(perp_asset_id)` | Admin            | One-time: pins the vault's Phoenix trader, market, global config, canonical mint and canonical ATA. `perp_asset_id` is the SOL-PERP id in the `PerpAssetMap` (0). Required before any perp position can be opened. |
| `enable_lending()`              | Admin            | One-time: pins the Kamino USDC reserve and both cToken ATAs (idle-USDC leg and reserve leg). Required before any `lend_*` call.                                                                                    |
| `set_oracles()`                 | Admin            | Pins the Pyth SOL/USD and jitoSOL/USD accounts. Re-callable to rotate feeds.                                                                                                                                       |

### `InitializeParams` defaults

| Field                             | Default                    | Meaning                                                                                           |
| --------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------- |
| `liquidity_buffer_bps`            | 1\_000 (10%)               | Idle USDC kept on-vault for instant withdrawals                                                   |
| `funding_threshold_normal_bps`    | 0 (safety floor)           | On-chain floor — never short into negative funding; keeper applies the dynamic threshold above it |
| `perf_fee_bps`                    | 2\_000 (20%)               | Performance fee above HWM                                                                         |
| `min_dwell_seconds`               | 43\_200 (12 h)             | Minimum time between mode switches                                                                |
| `max_swap_slippage_bps`           | 50 (0.5%)                  | Jupiter swap slippage cap                                                                         |
| `emergency_close_dd_bps`          | 500 (5%)                   | NAV drawdown from peak that trips `emergency_close`                                               |
| `deposit_cap_usdc`                | 500\_000\_000\_000 ($500k) | Cached-NAV cap on new deposits. `u64::MAX` = uncapped, `0` = deposits paused                      |
| `min_request_shares`              | 1\_000\_000 (1 ksUSD)      | Minimum size for a queued withdrawal request                                                      |
| `max_pending_queue_usdc`          | `u64::MAX` (uncapped)      | Ceiling on total USDC owed to the withdrawal queue                                                |
| `max_nav_change_bps_per_hour`     | 5\_000 (50%/hr)            | Bound on a single `attest_nav` delta                                                              |
| `funding_max_staleness_seconds`   | 21\_600 (6 h)              | Opens revert if the funding signal is older than this                                             |
| `consecutive_dd_settles_required` | 2                          | Bad settles needed in a row before drawdown trips                                                 |
| `authorized_keeper`               | admin key                  | Gates strategy instructions. `Pubkey::default()` = fully permissionless                           |
| `lst_depeg_bps`                   | 500 (5%)                   | jitoSOL/SOL deviation that auto-pauses on `settle`                                                |

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

***

## User flow (anyone)

| Instruction                  | Description                                                                                                                                                   |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `deposit(usdc_amount)`       | Transfer USDC to the vault, mint ksUSD at current share price. → [Deposit](/start-here/deposit.md)                                                            |
| `withdraw_instant(shares)`   | Burn ksUSD, receive USDC from the liquidity buffer. Reverts with `InsufficientLiquidityBuffer` if not enough idle USDC. → [Withdraw](/start-here/withdraw.md) |
| `request_withdrawal(shares)` | Burn ksUSD immediately, lock redemption price, create a queued `WithdrawalRequest` PDA.                                                                       |
| `process_withdrawal()`       | Permissionless crank — fulfill a queued request, close the PDA, refund rent to the original requester.                                                        |
| `claim_wind_down(shares)`    | Wind-down only. Burn ksUSD for a pro-rata share of the vault's idle USDC. Reverts unless `position_mode == WindDown`.                                         |

***

## Strategy (keeper-cranked, permissionless)

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

### `open_position`

```
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`. Splits the USDC and builds both legs in one transaction:

1. Buy jitoSOL via Jupiter — the unlevered spot leg.
2. Post `margin_usdc` on Phoenix, via Ember.
3. Short SOL-PERP delta-neutral against the spot leg.

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

### `close_position`

```
close_position(arena_account_count, min_base_lots_to_fill, min_quote_lots_to_fill,
               min_usdc_out, jupiter_swap_data)
```

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

### `settle`

```
settle(funding_apr_bps_now)
```

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

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

### `emergency_close`

```
emergency_close(arena_account_count, min_base_lots_to_fill, min_quote_lots_to_fill,
                jupiter_swap_data)
```

* Force closes on a drawdown breach, or while the vault is paused.
* Auto-pauses the vault on exit.
* Permissionless once tripped.

### `attest_nav`

```
attest_nav(new_nav_usdc)
```

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

### `lend_idle_usdc` / `unlend_usdc`

```
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`

```
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.

### `rebalance_hedge`

```
rebalance_hedge(arena_account_count, min_base_lots_to_fill, min_quote_lots_to_fill)
```

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

* Reverts with `DeltaWithinBand` unless `|delta| / NAV` clears `delta_band_bps`.
* Under-hedged sells more (not reduce-only); over-hedged buys back (reduce-only, so an overstated size cannot flip the book long).
* Increases are capped at the resulting **total** — not the increment — by `max_position_base_lots`.
* They are clamped again to what posted collateral supports at `min_margin_bps` — clamped rather than rejected, with `collateral_capped` set on the event.

### `reduce_position`

```
reduce_position(lots, arena_account_count, min_base_lots_to_fill, min_quote_lots_to_fill,
                min_usdc_out, jupiter_swap_data, destination)
```

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

`destination` picks where the proceeds go:

* `Buffer` — proceeds and proportional margin land in the vault USDC ATA, funding the withdrawal queue.
* `Margin` — margin stays on the venue and spot proceeds are posted on top; notional falls while collateral rises.

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

* It may only route to `Margin` — defend, never drain.
* It may only close the slice that restores health to `min_margin_bps`, not the whole book.
* It must supply a real Jupiter route, because a `lots` with no route would shrink the short while the whole spot leg stayed, leaving the vault net long.

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

## 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** to the admin. No shares are minted, and the share price doesn't move. Requires Parked and not paused. → [Fees](/start-here/fees.md) |
| `reset_peak()`              | Re-anchor `peak_share_price_1e9` for the drawdown guard. Does not touch the HWM.                                                                                                 |
| `init_wind_down()`          | Terminal. Blocks deposits and new positions; holders redeem via `claim_wind_down`.                                                                                               |

***

## Account-context groups for `remaining_accounts`

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

| Instruction       | Groups                                                                                   |
| ----------------- | ---------------------------------------------------------------------------------------- |
| `open_position`   | Jupiter swap → Ember margin deposit → Phoenix place\_perp\_order                         |
| `add_margin`      | Ember margin deposit only — arena slices only, no Jupiter group                          |
| `rebalance_hedge` | Phoenix place\_perp\_order only — arena slices only, no Jupiter group                    |
| `reduce_position` | place\_perp\_order (reduce-only) → Ember margin move → Jupiter swap                      |
| `close_position`  | place\_perp\_order (reduce-only) → settle funding → Ember margin withdraw → Jupiter swap |
| `settle`          | Phoenix funding / margin re-eval accounts (when position open)                           |
| `emergency_close` | place\_perp\_order → Ember margin withdraw → Jupiter swap                                |

See [Keeper bot](/run-it/keeper-bot.md) for the off-chain account-assembly conventions.

***

## Errors

See [Errors](/build-on-it/quick-start/errors.md) 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.
