Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion finance/betting-market/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Betting Market

A parimutuel (pooled) betting market. An admin creates an event, adds its possible outcomes, and opens it to bets; bettors stake a token on the outcome they expect to win until betting closes. All stakes share one pool, and when the admin settles the event, losing stakes (minus a protocol fee) are split among winners in proportion to their stake.
A parimutuel (pooled) betting market. An admin creates an event, adds its possible outcomes, and opens it to bets; bettors stake a token on the outcome they expect to win until betting closes. All stakes share one pool, and when the admin settles the event, losing stakes (minus a program fee) are split among winners in proportion to their stake.

[⚓ Anchor](./anchor)

Expand Down
4 changes: 2 additions & 2 deletions finance/betting-market/anchor-v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A parimutuel (pooled) betting market on Solana. An admin creates an **event**, a
**outcomes**, and opens it to bets; bettors then stake a token on the outcome they think will win,
until the event's betting close time. Every stake across
every outcome goes into one pool. When the admin settles the event to the winning outcome, the
losing stakes - minus a protocol fee - are split among the winners in proportion to their stake.
losing stakes - minus a program fee - are split among the winners in proportion to their stake.

This is the pooled model used by Solana prediction-market platforms such as Hedgehog Markets,
where odds are set by the crowd's stakes rather than by an order book or a fixed-odds bookmaker.
Expand Down Expand Up @@ -155,7 +155,7 @@ anchor test

### How does a prediction market work on Solana?

This example uses the parimutuel (pooled) model: an admin sets up an event with `initialize_event` and `add_outcome` and opens it with `open_betting`, and bettors stake tokens on an outcome with `place_bet` until betting closes. Every stake goes into one pool; after `settle_event` names the winning outcome, winners call `claim_winnings` to split the losing stakes, minus a protocol fee, in proportion to their own stake.
This example uses the parimutuel (pooled) model: an admin sets up an event with `initialize_event` and `add_outcome` and opens it with `open_betting`, and bettors stake tokens on an outcome with `place_bet` until betting closes. Every stake goes into one pool; after `settle_event` names the winning outcome, winners call `claim_winnings` to split the losing stakes, minus a program fee, in proportion to their own stake.

### How are the odds set?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub struct Config {
pub admin: Pubkey,
pub token_mint: Pubkey,
pub fee_recipient: Pubkey,
// Protocol fee, in basis points, that new events copy into their own
// Program fee, in basis points, that new events copy into their own
// `fee_bps` at creation. Settlement charges the event's copy, so changing
// this value only affects events created afterwards.
pub default_fee_bps: u16,
Expand Down
4 changes: 2 additions & 2 deletions finance/betting-market/anchor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A parimutuel (pooled) betting market on Solana. An admin creates an **event**, a
**outcomes**, and opens it to bets; bettors then stake a token on the outcome they think will win,
until the event's betting close time. Every stake across
every outcome goes into one pool. When the admin settles the event to the winning outcome, the
losing stakes - minus a protocol fee - are split among the winners in proportion to their stake.
losing stakes - minus a program fee - are split among the winners in proportion to their stake.

This is the pooled model used by Solana prediction-market platforms such as Hedgehog Markets,
where odds are set by the crowd's stakes rather than by an order book or a fixed-odds bookmaker.
Expand Down Expand Up @@ -155,7 +155,7 @@ anchor test

### How does a prediction market work on Solana?

This example uses the parimutuel (pooled) model: an admin sets up an event with `initialize_event` and `add_outcome` and opens it with `open_betting`, and bettors stake tokens on an outcome with `place_bet` until betting closes. Every stake goes into one pool; after `settle_event` names the winning outcome, winners call `claim_winnings` to split the losing stakes, minus a protocol fee, in proportion to their own stake.
This example uses the parimutuel (pooled) model: an admin sets up an event with `initialize_event` and `add_outcome` and opens it with `open_betting`, and bettors stake tokens on an outcome with `place_bet` until betting closes. Every stake goes into one pool; after `settle_event` names the winning outcome, winners call `claim_winnings` to split the losing stakes, minus a program fee, in proportion to their own stake.

### How are the odds set?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub struct Config {
pub admin: Address,
pub token_mint: Address,
pub fee_recipient: Address,
// Protocol fee, in basis points, that new events copy into their own
// Program fee, in basis points, that new events copy into their own
// `fee_bps` at creation. Settlement charges the event's copy, so changing
// this value only affects events created afterwards.
pub default_fee_bps: u16,
Expand Down
4 changes: 2 additions & 2 deletions finance/betting-market/quasar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
A parimutuel betting market on Solana, written with Quasar. An admin creates events (markets), adds the possible
outcomes, opens them to bets, and later settles or cancels each one. Bettors stake a fixed token on
the outcome they think will happen; when the event is settled, the winners split
the losing side's stakes in proportion to their own, after a protocol fee. This
the losing side's stakes in proportion to their own, after a program fee. This
is the same mechanism a racetrack tote board or a prediction market runs on.

This is a [Quasar](https://github.com/blueshift-gg/quasar) port of the Anchor
Expand Down Expand Up @@ -31,7 +31,7 @@ result is known the winners divide the pool.
naming the winning outcome. Bets are accepted only while `now <
betting_closes_at` and settlement only once `now >= betting_closes_at`, so
nobody can stake after the result could be known.
The protocol fee is charged only on the losing pool, so a winner can never
The program fee is charged only on the losing pool, so a winner can never
receive less than they staked. The fee moves to the fee recipient immediately;
the figures winners need are recorded on the event.
- A winner calls `claim_winnings` to withdraw their stake plus their share of
Expand Down
2 changes: 1 addition & 1 deletion finance/betting-market/quasar/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ declare_id!("7LyqAeLR3mK9dfj9LqxWzfKH61VVHzuNpkgW5Y32De74");

/// Parimutuel betting market. An admin creates events, adds outcomes, opens
/// them to bets, and settles or cancels them; bettors stake a fixed token on an outcome, and winners
/// share the losing pool (net of a protocol fee) pro-rata to their stake. See
/// share the losing pool (net of a program fee) pro-rata to their stake. See
/// README.md for the full walkthrough.
#[program]
mod quasar_betting_market {
Expand Down
2 changes: 1 addition & 1 deletion finance/betting-market/quasar/src/state/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub struct Config {
pub admin: Address,
pub token_mint: Address,
pub fee_recipient: Address,
/// Protocol fee, in basis points, that new events copy into their own
/// Program fee, in basis points, that new events copy into their own
/// `fee_bps` at creation. Settlement charges the event's copy, so changing
/// this value only affects events created afterwards.
pub default_fee_bps: u16,
Expand Down
26 changes: 13 additions & 13 deletions finance/lending/anchor-v1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
A Kamino/Solend-style borrow/lend program on Solana: suppliers earn interest on deposits,
borrowers post collateral and draw other assets against it, and liquidators keep
the market solvent. It demonstrates the techniques the most-used Solana lending
protocols share: share-token deposit accounting, a utilization-based interest
programs share: share-token deposit accounting, a utilization-based interest
index, oracle-priced obligation health, and close-factor-capped liquidation.

## Purpose
Expand Down Expand Up @@ -57,7 +57,7 @@ crosses the liquidation threshold and a liquidator can close part of the positio
Supplying liquidity mints share tokens; redeeming burns them. The exchange rate
is `total_liquidity / total_shares`, where `total_liquidity = available_liquidity
+ current_debt` and `total_shares` is the share supply plus `MINIMUM_SHARES`.
`available_liquidity` (not the vault's raw token balance) is the source of truth,
The program prices shares from `available_liquidity`, not the vault's raw token balance,
so a token donated directly to the vault cannot inflate the rate.

That alone does not close the empty-pool inflation attack, because
Expand Down Expand Up @@ -102,15 +102,15 @@ at the rates that applied to them.
The reserve still records `last_update_slot`, for a different job: handlers that
read the reserve's value require the refresh to have run in the current slot.

### Protocol fees (how the market earns)
### Program fees (how the market earns)

Borrowers owe the full interest, but suppliers don't receive all of it. On each
accrual the reserve keeps `config.reserve_factor_bps` of the freshly accrued
interest in `accumulated_protocol_fees`; only the remainder lifts the supplier
interest in `accumulated_program_fees`; only the remainder lifts the supplier
exchange rate. Those fees are carved out of `total_liquidity`, so they never
count as a supplier claim, and the market owner withdraws them with
**`collect_protocol_fees`** (paid out of the reserve's available liquidity).
This spread between the borrow rate and the supply rate is the protocol's revenue.
**`collect_program_fees`** (paid out of the reserve's available liquidity).
This spread between the borrow rate and the supply rate is the program's revenue.

### Obligation health

Expand All @@ -137,7 +137,7 @@ less, which would make the liquidator overpay.

All arithmetic is integer-only `u128`: no floats, no fixed-point crates. Ratios
(rates, the index, the exchange rate, obligation values) are scaled by
`FIXED_POINT_SCALE` (10^18). Every conversion rounds in the protocol's favour
`FIXED_POINT_SCALE` (10^18). Every conversion rounds in the program's favour
(user output floored, debt ceiled), so dust cannot be extracted by repeated
round-trips.

Expand Down Expand Up @@ -168,15 +168,15 @@ also reject results whose confidence interval is too wide.
Supplied liquidity sits in program-owned vault PDAs, and posted collateral sits in
per-obligation vault PDAs whose authority is the obligation PDA. The market owner
can update reserve risk parameters (`update_reserve_config`) and withdraw the
protocol's earned fees (`collect_protocol_fees`), but has no path to a supplier's
program's earned fees (`collect_program_fees`), but has no path to a supplier's
deposits or a borrower's collateral: there is no admin escape hatch over user funds.

### Known limits

- **Tokens with transfer fees are not supported.** The program uses
`token_interface`, so Token Extensions mints are accepted, but a transfer-fee
extension would make the vault receive less than the recorded deposit and the
accounting would overstate `available_liquidity`. Production protocols
accounting would overstate `available_liquidity`. Production lending programs
whitelist mints; a market owner here must only create reserves for tokens
without transfer fees.
- **Reserve config changes act immediately.** Lowering a reserve's
Expand All @@ -188,7 +188,7 @@ deposits or a borrower's collateral: there is no admin escape hatch over user fu
### Instruction handlers

Admin: `initialize_lending_market`, `initialize_reserve`, `update_reserve_config`, `set_price`,
`collect_protocol_fees`.
`collect_program_fees`.
Supply side: `refresh_reserve`, `deposit_reserve_liquidity`,
`redeem_reserve_collateral`. Borrow side: `initialize_obligation`, `refresh_obligation`,
`deposit_obligation_collateral`, `withdraw_obligation_collateral`,
Expand Down Expand Up @@ -218,15 +218,15 @@ move, the share-inflation guard, and rounding edges.

## FAQ

### How does a lending protocol work on Solana?
### How does a lending program work on Solana?

Suppliers deposit a token with `deposit_reserve_liquidity` and receive share tokens that grow in value as borrowers pay interest. Borrowers post those shares as collateral (`deposit_obligation_collateral`) and draw a different token with `borrow_obligation_liquidity`, up to a loan-to-value limit. When a position's collateral no longer covers its debt, anyone can call `liquidate_obligation` to repay part of the debt in exchange for discounted collateral.

### How does interest accrue without looping over every account?

Through a cumulative accumulation factor: `refresh_reserve` advances a per-reserve factor along a utilization-based rate curve, and each obligation stores the index value from its last interaction. The gap between the two is the interest owed, so no per-account accrual loop is needed. This is the same technique the most-used Solana lending protocols share.
Through a cumulative accumulation factor: `refresh_reserve` advances a per-reserve factor along a utilization-based rate curve, and each obligation stores the index value from its last interaction. The gap between the two is the interest owed, so no per-account accrual loop is needed. This is the same technique the most-used Solana lending programs share.

### How are prices fed into the protocol?
### How are prices fed into the program?

The admin `set_price` instruction handler stands in for an oracle feed in this example. `refresh_obligation` re-values collateral and debt at those prices before any borrow, withdraw, or liquidation is allowed, and stale reserves or prices are rejected.

Expand Down
2 changes: 1 addition & 1 deletion finance/lending/anchor-v1/programs/lending/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ pub enum LendingError {
MarketMismatch,
#[msg("Repay amount would seize more collateral than the obligation holds")]
LiquidationTooLarge,
#[msg("No protocol fees are available to collect")]
#[msg("No program fees are available to collect")]
NothingToCollect,
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ use anchor_spl::token_interface::{
use crate::errors::LendingError;
use crate::state::{reserve_signer_seeds, LendingMarket, Reserve};

/// Withdraw the protocol fees accrued in a reserve to the market owner. This is
/// Withdraw the program fees accrued in a reserve to the market owner. This is
/// how the owner earns: `reserve_factor_bps` of every interest accrual is set
/// aside in `accumulated_protocol_fees` (never credited to suppliers), and this
/// aside in `accumulated_program_fees` (never credited to suppliers), and this
/// handler pays it out, capped by the liquidity actually sitting in the vault.
pub fn handle_collect_protocol_fees(context: Context<CollectProtocolFees>) -> Result<()> {
pub fn handle_collect_program_fees(context: Context<CollectProgramFees>) -> Result<()> {
context.accounts.reserve.require_refreshed()?;

let reserve = &mut context.accounts.reserve;
// Fees are a claim on liquidity; only what is currently un-borrowed can be paid
// out right now. Any remainder stays owed until borrowers repay.
let amount = reserve
.accumulated_protocol_fees
.accumulated_program_fees
.min(reserve.available_liquidity);
require!(amount > 0, LendingError::NothingToCollect);

reserve.accumulated_protocol_fees = reserve
.accumulated_protocol_fees
reserve.accumulated_program_fees = reserve
.accumulated_program_fees
.checked_sub(amount)
.ok_or(LendingError::MathOverflow)?;
reserve.available_liquidity = reserve
Expand Down Expand Up @@ -51,7 +51,7 @@ pub fn handle_collect_protocol_fees(context: Context<CollectProtocolFees>) -> Re
}

#[derive(Accounts)]
pub struct CollectProtocolFees<'info> {
pub struct CollectProgramFees<'info> {
// Identified by the reserve's `has_one = lending_market`; we only prove the
// signer owns it.
#[account(has_one = owner)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub fn handle_initialize_reserve(
let clock = Clock::get()?;
reserve.last_update_slot = clock.slot;
reserve.last_accrual_timestamp = clock.unix_timestamp;
reserve.accumulated_protocol_fees = 0;
reserve.accumulated_program_fees = 0;
reserve.config = config;
reserve.bump = context.bumps.reserve;
Ok(())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
pub mod collect_protocol_fees;
pub mod collect_program_fees;
pub mod initialize_lending_market;
pub mod initialize_reserve;
pub mod set_price;
pub mod update_reserve_config;

pub use collect_protocol_fees::*;
pub use collect_program_fees::*;
pub use initialize_lending_market::*;
pub use initialize_reserve::*;
pub use set_price::*;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::state::{reserve_signer_seeds, Reserve};
/// Supply liquidity to a reserve and receive share tokens. The first deposit
/// mints share tokens 1:1, less the `MINIMUM_SHARES` withheld; later deposits
/// mint `liquidity_amount * total_shares / total_liquidity`, where
/// `total_shares` counts the withheld minimum, floored so the protocol keeps
/// `total_shares` counts the withheld minimum, floored so the program keeps
/// any rounding dust.
pub fn handle_deposit_reserve_liquidity(
context: Context<DepositReserveLiquidity>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::math::mul_div_floor;
use crate::state::{reserve_signer_seeds, Reserve};

/// Burn share tokens and withdraw the underlying liquidity they represent:
/// `share_amount * total_liquidity / total_shares`, floored so the protocol
/// `share_amount * total_liquidity / total_shares`, floored so the program
/// keeps any rounding dust. `total_shares` counts the `MINIMUM_SHARES` withheld
/// from the first deposit, as `deposit_reserve_liquidity` does, so their slice
/// of the pool never leaves. Capped by the reserve's available (un-borrowed)
Expand Down
4 changes: 2 additions & 2 deletions finance/lending/anchor-v1/programs/lending/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ pub mod lending {
instructions::handle_update_reserve_config(context, config)
}

pub fn collect_protocol_fees(context: Context<CollectProtocolFees>) -> Result<()> {
instructions::handle_collect_protocol_fees(context)
pub fn collect_program_fees(context: Context<CollectProgramFees>) -> Result<()> {
instructions::handle_collect_program_fees(context)
}

pub fn set_price(
Expand Down
2 changes: 1 addition & 1 deletion finance/lending/anchor-v1/programs/lending/src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::errors::LendingError;

/// Which way to break ties when a division truncates. Deposits/redeems and
/// collateral valuations round the user's favourable quantity DOWN; debt and
/// protocol-owed quantities round UP. The protocol never loses a base unit to
/// program-owed quantities round UP. The program never loses a base unit to
/// rounding, so dust cannot be extracted by repeated round-trips.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Rounding {
Expand Down
Loading
Loading