Skip to content

Add Kamino/Solend-style lending program example#44

Merged
mikemaccana merged 6 commits into
mainfrom
claude/busy-pasteur-eMVnc
Jun 18, 2026
Merged

Add Kamino/Solend-style lending program example#44
mikemaccana merged 6 commits into
mainfrom
claude/busy-pasteur-eMVnc

Conversation

@mikemaccana

Copy link
Copy Markdown
Collaborator

What

A new finance/lending/anchor example: a Kamino/Solend-style borrow/lend program built with the techniques the most-used Solana lending protocols share.

How it works

  • Reserves (one per asset) own a program-controlled liquidity vault and a share-token mint. Suppliers deposit_reserve_liquidity to mint share tokens; the share/liquidity exchange rate rises as interest accrues, so redeem_reserve_collateral returns more than was deposited. available_liquidity (not the vault's raw balance) is the source of truth, which defeats the empty-pool share-inflation attack.
  • Interest uses a kinked utilization curve (min/optimal/max borrow rate around optimal_utilization) compounded through a cumulative borrow-rate index advanced by refresh_reserve. Each borrow stores scaled debt (principal ÷ index at borrow time), so every obligation's debt grows automatically.
  • Obligations hold posted share-token collateral and borrows. refresh_obligation recomputes borrowed_value, allowed_borrow_value (Σ collateral × LTV) and unhealthy_borrow_value (Σ collateral × liquidation threshold) from oracle prices. borrow_obligation_liquidity / withdraw_obligation_collateral are gated by the LTV limit; liquidate_obligation repays an unhealthy obligation's debt (capped by the close factor) and seizes collateral plus a bonus.
  • Oracle: PriceFeed mirrors a Switchboard On-Demand pull feed (mantissa + exponent + slot); a set_price handler writes it directly for deterministic LiteSVM tests, with the production Switchboard mapping documented in the README. Freshness is checked in slots.
  • Math: integer-only u128 (no floats, no fixed-point crates), ratios scaled by FIXED_POINT_SCALE (10^18), every conversion rounding in the protocol's favour.

A worked directional-short example (supply USDC, borrow + sell NVDAx, pay a variable rate, buy back, repay) is in the README.

Tests

Rust + LiteSVM, run with cargo test (Anchor's anchor test). 18 tests across 6 files cover supply/redeem, borrow/repay, withdraw, interest accrual, liquidation with bonus and close-factor cap, the inflation guard, stale reserve/price rejection, and rounding/health-boundary edges. anchor build (or cargo build-sbf) must run first so the tests can load the compiled .so.

https://claude.ai/code/session_01RwE8f8ahP5S6SDNTsXmpj9


Generated by Claude Code

claude added 6 commits June 2, 2026 21:33
A finance/lending Anchor example implementing the core techniques of the
most-used Solana lending protocols:

- Per-asset reserves with a program-owned liquidity vault and a share-token
  mint; the share/liquidity exchange rate rises as interest accrues.
- Utilization-based kinked interest-rate curve compounded through a cumulative
  borrow-rate index, with per-obligation scaled debt.
- Per-borrower obligations: post share-token collateral, borrow against it up
  to a loan-to-value limit, repay, withdraw.
- Oracle-priced health and close-factor-capped liquidation with a seize bonus.
- Switchboard-On-Demand-shaped price feed with a set_price test writer.
- Integer-only u128 math (no floats/fixed-point), rounding always in the
  protocol's favour; available_liquidity as source of truth defeats the
  empty-pool share-inflation attack.

Rust + LiteSVM tests cover supply/redeem, borrow/repay, withdraw, interest
accrual, liquidation, the inflation guard, stale reserve/price rejection, and
rounding edges (18 tests).

https://claude.ai/code/session_01RwE8f8ahP5S6SDNTsXmpj9
`anchor build`'s IDL generation compiles a generated test under the
idl-build feature, where the `#[constant]` macro mis-evaluates the 1e18
u128 literal as i32 ("literal out of range for i32"), failing CI. These
values don't need to be in the IDL, so they're plain `pub const`s now.

https://claude.ai/code/session_01RwE8f8ahP5S6SDNTsXmpj9
A Quasar (zero-copy, no_std) port of finance/lending, mirroring the shipped
Quasar escrow/vault examples' fixed-size-account idiom:

- Isolated single-collateral / single-borrow obligations (fixed-size accounts)
  rather than the Anchor version's Vec-based multi-asset obligation. Quasar does
  support Vec<T, N> and CtxWithRemaining, but the DeFi examples favour fixed-size
  positions, so this follows that idiom.
- Interest accrues inline per instruction instead of via a separate refresh.
- Keeps every core technique: share-token deposits, a kinked-curve cumulative
  interest index, oracle-priced health, and close-factor liquidation with a bonus.
- Integer-only u128 math scaled by 1e18, rounding in the protocol's favour.

quasar-svm tests cover supply/redeem, borrow up to the LTV limit (and rejection
beyond), repay, interest accrual lifting share value, and liquidation of an
unhealthy position with the healthy path rejected.

https://claude.ai/code/session_01RwE8f8ahP5S6SDNTsXmpj9
- Price feed PDAs are now seeded [b"price_feed", authority, mint]: a signer
  can only write the feed derived from their own key, removing the
  first-caller-claims race on the previous per-mint feed. Reserves trust
  exactly the feed their market owner registered.
- Lending markets are now isolation boundaries: every obligation handler
  (deposit/withdraw collateral, borrow, repay, liquidate, refresh) rejects
  reserves whose lending_market differs from the obligation's
  (MarketMismatch).
- Liquidation reads the close factor from the repay reserve (a property of
  the debt) and the bonus from the collateral reserve (a property of the
  seized asset), and rejects repayments whose seizure would exceed posted
  collateral (LiquidationTooLarge) instead of silently capping, which made
  the liquidator pay full price for less collateral.
- Withdraw health checks round the removed borrow power up at every step,
  so independent flooring can never let a withdraw pass that an exact
  recompute would reject.
- Documented share_mint_supply drift from direct token-program burns
  (protocol-favourable), the transfer-fee mint limitation, instant config
  changes, and the audit expectation; moved reserve_signer_seeds from
  math.rs to state/reserve.rs.
- New tests: cross-market reserve rejection, foreign-signer feed write
  rejection, over-seizing liquidation rejection (21 tests total).

https://claude.ai/code/session_01RwE8f8ahP5S6SDNTsXmpj9
Applies the same fixes as the Anchor version:

- Price feed PDAs seeded [b"price_feed", authority, mint]; init_reserve
  binds each reserve to the feed written by its market owner, removing the
  first-caller-claims race and the claim branch in set_price.
- deposit_obligation_collateral now requires the reserve to belong to the
  obligation's lending market (the one handler that was missing the check;
  the others already enforce it via has_one or stored-key equality).
- Liquidation reads the close factor from the borrow reserve (a property
  of the debt) and rejects repayments whose seizure would exceed posted
  collateral (LiquidationTooLarge) instead of silently capping.

https://claude.ai/code/session_01RwE8f8ahP5S6SDNTsXmpj9
Borrowers owe the full interest, but suppliers no longer receive all of it:
each accrual now keeps `reserve_factor_bps` of the freshly accrued interest in
the reserve's `accumulated_protocol_fees`, and the market owner withdraws it
with a new `collect_protocol_fees` handler. The fees are carved out of
`total_liquidity` (utilization still uses gross liquidity), so they never lift
the supplier exchange rate, and collection is capped by the vault's available
liquidity. This spread between the borrow and supply rates is the protocol's
revenue — the missing profit motive for the market owner.

Applied to both the Anchor and Quasar versions, with tests asserting fees
accrue at the configured factor and that the owner collects them, plus README
and CHANGELOG updates.

https://claude.ai/code/session_01RwE8f8ahP5S6SDNTsXmpj9
@mikemaccana mikemaccana merged commit 3ca8f0a into main Jun 18, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants