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
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:
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
watchdogduty compareshurdle_apr_bpsagainst 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
settleis 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_tsdoes not ratchet. The field is zero on a vault whose state predates it; without the guarddtwould 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:
Both of the consequences below were audit fixes:
pending_perf_fees_usdcis subtracted fromeffective_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_sharesonce 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.
Requires
position_mode == Idle(Parked) and!paused, and only the admin can call it.Calls
accrue_perf_feesto catch up to the current price, then readspending_perf_fees_usdc. If it's zero, the call is a no-op.Splits it:
reserve_share = pending × reserve_skim_bps / 10_000, andadmin_share = pending − reserve_share.Requires the vault's main USDC account to hold at least the full fee, or reverts with
InsufficientLiquidityBuffer.Transfers
admin_shareto the admin's USDC account, andreserve_shareto the reserve ATA, adding it toreserve_fund_usdc.Reduces
cached_nav_usdcbyadmin_shareonly. 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.Zeroes
pending_perf_fees_usdcand 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 vialend_reserve/unlend_reserveso 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 callunlend_reservefirst.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_feesThe math is fully on-chain and reproducible from
cached_nav_usdc,queue_pending_usdc,reserve_fund_usdc,pending_perf_fees_usdc,total_shares, andhwm_share_price_1e9pending_perf_fees_usdcis readable at any time, so the fee owed is visible before anyone collects itAnyone 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_usdcHWM 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, andreserve_fund_usdc+= $300 (lend_reservecan 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, andreserve_fund_usdcis up $300Net 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 — share-price math,
effective_nav_usdcAdmin operations —
collect_fees,pay_from_reserveSecurity model — admin role and HWM monotonicity guarantee
Last updated