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.
The whole idea, in one line
share price = the dollars the vault holds for shareholders ÷ ksUSD in circulationThat dollar figure is the vault's NAV — net asset value. Divide it by the token count and you have what one token is worth.
Two examples, and they are the only two operations there are:
Deposit. Put in $100 while the price is $1.03 → you get
100 ÷ 1.03 = 97.09ksUSD.Redeem. Hand back 97.09 ksUSD while the price is $1.05 → you get
97.09 × 1.05 = $101.94.
Everything below is that one division, written precisely enough to implement.
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_sharesThe 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)Rust helpers:
Vault::share_price_1e9()andVault::gross_share_price_1e9()
Reading the 1e9 scale
A Solana account stores integers, not decimals, so the price is carried as dollars × 1,000,000,000. To read one, move the decimal point nine places left:
Stored share_price_1e9
Dollars per ksUSD
1_000_000_000
$1.000000
1_030_000_000
$1.030000
1_002_500_000
$1.002500
The scale is 1e9 rather than the token's own 1e6 so that a day of carry — a few hundredths of a cent per token — doesn't round away to nothing. It's wide enough to price accurately without pushing the arithmetic past a u128 intermediate.
Why three things come out of NAV
The vault holds some money that isn't shareholders'. Each subtraction takes one of those out before the division, so the price reflects only what holders actually own.
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:
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:
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:
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
A vault holding $1M against 1M ksUSD, which then earns $30,000 of carry. The left column is what a holder sees; the right is what's stored on-chain.
Start — NAV
$1,000,000
cached_nav_usdc = 1_000_000_000_000
Start — supply
1,000,000 ksUSD
total_shares = 1_000_000_000_000
Start — price
$1.0000
share_price_1e9 = 1_000_000_000
After $30k carry — NAV
$1,030,000
cached_nav_usdc = 1_030_000_000_000
After carry — supply
unchanged
total_shares = 1_000_000_000_000
After carry — price
$1.0300
share_price_1e9 = 1_030_000_000
Nobody's balance moved when the vault earned. The price did the work.
Now a new depositor sends 100 USDC. They get $100 ÷ $1.03 = 97.087… ksUSD:
Vault after: total_shares ≈ 1_000_097_087_378, cached_nav_usdc = 1_030_100_000_000, and the price is still $1.0300 — $100 of new money bought exactly $100 of shares. The truncation rounds against the depositor by under one base unit, a millionth of a token, which is what stops a mint from nudging the price down for everyone else.
So the new holder has ~97.087 ksUSD worth the same ~$100 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,
depositcallsaccrue_perf_feesbefore 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 — HWM math and reserve skim
Check position & NAV — TypeScript snippets to query share price live
Security model — oracle, slippage, drawdown guard
Last updated