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
5 changes: 5 additions & 0 deletions finance/lending/anchor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ Initial lending program: a Kamino/Solend-style borrow/lend market.
- Reserve factor: the protocol keeps `reserve_factor_bps` of accrued interest as
fees the market owner withdraws with `collect_protocol_fees`; the fees are
carved out of `total_liquidity` so they never inflate the supplier exchange rate.
- LendingMarket is seeded by a `market_id` index (`["lending_market", market_id]`),
not by any individual; one owner can run several independent markets, and admin
handlers authorize via `has_one = owner`.
- Price feeds are seeded `["price_feed", market, mint]` (scoped to a market, not
to an individual); only the market owner may write one (`has_one = owner`).
14 changes: 9 additions & 5 deletions finance/lending/anchor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ crosses the liquidation threshold and a liquidator can close part of the positio
### Accounts

- **`LendingMarket`** — top-level config (owner, quote-currency mint). PDA seeds
`["lending_market", owner]`.
`["lending_market", owner, market_id]`, where `market_id` is a per-owner `u64`
index. Seeding by an index (not the owner alone) lets one owner run several
independent, risk-isolated markets — their market 0, 1, 2 … — with no
cross-owner collisions.
- **`Reserve`** — one per asset. Owns a program-controlled liquidity vault and a
share-token mint, and stores the interest-rate config, the cumulative borrow-
rate index, available liquidity, and scaled total debt. PDA seeds
Expand Down Expand Up @@ -104,10 +107,11 @@ round-trips.
`PriceFeed` mirrors a Switchboard On-Demand pull feed: a signed mantissa, an
exponent (`price = mantissa * 10^exponent`), and the slot the price was written.
Freshness is checked in **slots** (`MAX_PRICE_STALENESS_SLOTS`), not wall-clock
time. The feed PDA is seeded by `[b"price_feed", authority, mint]`, so a signer
can only ever write the feed derived from their own key — there is no shared
per-mint feed to claim first — and a reserve trusts exactly one feed: the
account its market owner passed to `init_reserve`.
time. The feed PDA is seeded by `[b"price_feed", market, mint]` — scoped to a
market, not to any individual — and only that market's `owner` may write it
(`set_price` checks `has_one = owner`). So prices can't be squatted, a reserve
trusts exactly its own market's feed for the mint, and isolated markets can
price the same asset independently.

The `set_price` handler writes the feed directly so the LiteSVM tests are
deterministic; in production a reserve points at the real Switchboard feed and the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use anchor_spl::token_interface::{
transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked,
};

use crate::constants::LENDING_MARKET_SEED;
use crate::errors::LendingError;
use crate::state::{reserve_signer_seeds, LendingMarket, Reserve};

Expand Down Expand Up @@ -51,11 +50,9 @@ pub fn handle_collect_protocol_fees(context: Context<CollectProtocolFees>) -> Re

#[derive(Accounts)]
pub struct CollectProtocolFees<'info> {
#[account(
has_one = owner,
seeds = [LENDING_MARKET_SEED, owner.key().as_ref()],
bump = lending_market.bump,
)]
// Identified by the reserve's `has_one = lending_market`; we only prove the
// signer owns it.
#[account(has_one = owner)]
pub lending_market: Account<'info, LendingMarket>,

#[account(mut)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,29 @@ use anchor_spl::token_interface::Mint;
use crate::constants::LENDING_MARKET_SEED;
use crate::state::LendingMarket;

pub fn handle_init_lending_market(context: Context<InitLendingMarket>) -> Result<()> {
pub fn handle_init_lending_market(
context: Context<InitLendingMarket>,
market_id: u64,
) -> Result<()> {
let market = &mut context.accounts.lending_market;
market.market_id = market_id;
market.owner = context.accounts.owner.key();
market.quote_currency_mint = context.accounts.quote_currency_mint.key();
market.bump = context.bumps.lending_market;
Ok(())
}

#[derive(Accounts)]
#[instruction(market_id: u64)]
pub struct InitLendingMarket<'info> {
// Seeded by `market_id` alone — the market is not identified by any
// individual's address. `owner` is stored as a field and used only for
// authorization (`has_one = owner`) on admin instructions.
#[account(
init,
payer = owner,
space = LendingMarket::DISCRIMINATOR.len() + LendingMarket::INIT_SPACE,
seeds = [LENDING_MARKET_SEED, owner.key().as_ref()],
seeds = [LENDING_MARKET_SEED, &market_id.to_le_bytes()],
bump,
)]
pub lending_market: Account<'info, LendingMarket>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anchor_lang::prelude::*;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};

use crate::constants::{
FIXED_POINT_SCALE, LENDING_MARKET_SEED, LIQUIDITY_VAULT_SEED, RESERVE_SEED, SHARE_MINT_SEED,
FIXED_POINT_SCALE, LIQUIDITY_VAULT_SEED, PRICE_FEED_SEED, RESERVE_SEED, SHARE_MINT_SEED,
};
use crate::state::{LendingMarket, PriceFeed, Reserve, ReserveConfig};

Expand All @@ -29,11 +29,9 @@ pub fn handle_init_reserve(context: Context<InitReserve>, config: ReserveConfig)

#[derive(Accounts)]
pub struct InitReserve<'info> {
#[account(
has_one = owner,
seeds = [LENDING_MARKET_SEED, owner.key().as_ref()],
bump = lending_market.bump,
)]
// The reserve PDA below is seeded by this market's address, so the market is
// pinned by that seed; we only need to prove the signer owns it.
#[account(has_one = owner)]
pub lending_market: Account<'info, LendingMarket>,

#[account(mut)]
Expand Down Expand Up @@ -70,7 +68,12 @@ pub struct InitReserve<'info> {
)]
pub share_mint: InterfaceAccount<'info, Mint>,

#[account(constraint = price_feed.mint == liquidity_mint.key() @ crate::errors::LendingError::InvalidConfig)]
// Bound by seeds to this market's feed for this mint — the reserve can only
// trust the price its own market publishes.
#[account(
seeds = [PRICE_FEED_SEED, lending_market.key().as_ref(), liquidity_mint.key().as_ref()],
bump = price_feed.bump,
)]
pub price_feed: Account<'info, PriceFeed>,

pub token_program: Interface<'info, TokenInterface>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,22 @@ use anchor_lang::prelude::*;
use anchor_spl::token_interface::Mint;

use crate::constants::PRICE_FEED_SEED;
use crate::state::PriceFeed;
use crate::state::{LendingMarket, PriceFeed};

/// Test stand-in for a Switchboard On-Demand feed: writes a price directly so
/// LiteSVM tests are deterministic. In production the reserve points at a real
/// Switchboard feed instead and this handler is unused.
///
/// The feed PDA is seeded by `[b"price_feed", authority, mint]`, so each
/// authority can only ever write its own feed — there is no shared per-mint
/// feed to race for. A reserve trusts exactly one feed account: the one the
/// market owner passed to `init_reserve`.
/// The feed PDA is seeded by `[b"price_feed", market, mint]` and writing it
/// requires the market's `owner` to sign, so a market's prices can only be set
/// by that market and never squatted by an outsider.
pub fn handle_set_price(
context: Context<SetPrice>,
price_mantissa: i128,
exponent: i32,
) -> Result<()> {
let feed = &mut context.accounts.price_feed;
feed.authority = context.accounts.authority.key();
feed.market = context.accounts.lending_market.key();
feed.mint = context.accounts.mint.key();
feed.bump = context.bumps.price_feed;
feed.price_mantissa = price_mantissa;
Expand All @@ -29,20 +28,22 @@ pub fn handle_set_price(

#[derive(Accounts)]
pub struct SetPrice<'info> {
// The authority is part of the seeds: a signer can only ever address (and
// therefore write) the feed derived from their own key.
// Only the market's owner may publish its prices.
#[account(has_one = owner)]
pub lending_market: Account<'info, LendingMarket>,

#[account(mut)]
pub owner: Signer<'info>,

#[account(
init_if_needed,
payer = authority,
payer = owner,
space = PriceFeed::DISCRIMINATOR.len() + PriceFeed::INIT_SPACE,
seeds = [PRICE_FEED_SEED, authority.key().as_ref(), mint.key().as_ref()],
seeds = [PRICE_FEED_SEED, lending_market.key().as_ref(), mint.key().as_ref()],
bump,
)]
pub price_feed: Account<'info, PriceFeed>,

#[account(mut)]
pub authority: Signer<'info>,

pub mint: InterfaceAccount<'info, Mint>,

pub system_program: Program<'info, System>,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use anchor_lang::prelude::*;

use crate::constants::LENDING_MARKET_SEED;
use crate::state::{LendingMarket, Reserve, ReserveConfig};

pub fn handle_update_reserve_config(
Expand All @@ -14,11 +13,9 @@ pub fn handle_update_reserve_config(

#[derive(Accounts)]
pub struct UpdateReserveConfig<'info> {
#[account(
has_one = owner,
seeds = [LENDING_MARKET_SEED, owner.key().as_ref()],
bump = lending_market.bump,
)]
// The market is identified by the reserve's `has_one = lending_market`; we
// only need to prove the signer owns it, not re-derive its address.
#[account(has_one = owner)]
pub lending_market: Account<'info, LendingMarket>,

pub owner: Signer<'info>,
Expand Down
7 changes: 5 additions & 2 deletions finance/lending/anchor/programs/lending/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ declare_id!("4bvT6A8S7ZVL6bSvK2KoL2nQ4F5H6AF9133kCYbMJj1t");
pub mod lending {
use super::*;

pub fn init_lending_market(context: Context<InitLendingMarket>) -> Result<()> {
instructions::handle_init_lending_market(context)
pub fn init_lending_market(
context: Context<InitLendingMarket>,
market_id: u64,
) -> Result<()> {
instructions::handle_init_lending_market(context, market_id)
}

pub fn init_reserve(context: Context<InitReserve>, config: ReserveConfig) -> Result<()> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ use anchor_lang::prelude::*;
#[account]
#[derive(InitSpace)]
pub struct LendingMarket {
/// Index this market's PDA is derived from (`["lending_market", market_id]`).
/// The market is identified by this id, not by any individual — `owner` below
/// is a stored field used only for authorization, never part of the address.
/// Distinct markets (0, 1, 2 …) give independent, risk-isolated pools.
pub market_id: u64,

pub owner: Pubkey,

/// The mint that obligation values are denominated in (for example USDC).
Expand Down
14 changes: 6 additions & 8 deletions finance/lending/anchor/programs/lending/src/state/price_feed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use crate::errors::LendingError;
use crate::math::price_mantissa_to_scaled;

/// A price for one token, denominated in the market's quote currency.
/// PDA seeds: `[b"price_feed", authority, mint]` — the writer is part of the
/// address, so no two authorities can contend for the same feed account, and a
/// reserve trusts exactly the feed its market owner selected at `init_reserve`.
/// PDA seeds: `[b"price_feed", market, mint]` — scoped to a market (not to any
/// individual), so each market prices its own assets and one market can never
/// write another's feed. Only the market's `owner` may write it (`set_price`).
///
/// The layout mirrors a Switchboard On-Demand pull feed: a signed mantissa plus
/// an exponent (`price = price_mantissa * 10^exponent`) and the slot the value
Expand All @@ -21,6 +21,9 @@ use crate::math::price_mantissa_to_scaled;
#[account]
#[derive(InitSpace)]
pub struct PriceFeed {
/// The lending market this feed serves; part of the PDA seeds.
pub market: Pubkey,

pub mint: Pubkey,

pub price_mantissa: i128,
Expand All @@ -29,11 +32,6 @@ pub struct PriceFeed {

pub last_updated_slot: u64,

/// The signer whose key is in this feed's PDA seeds; the only account that
/// can write it. In production this field does not exist — the feed is
/// owned and written by Switchboard.
pub authority: Pubkey,

pub bump: u8,
}

Expand Down
43 changes: 31 additions & 12 deletions finance/lending/anchor/programs/lending/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ impl Env {

let owner = create_wallet(&mut svm, 1_000_000_000_000).unwrap();
let quote_mint = create_token_mint(&mut svm, &owner, 6, None).unwrap();
let market = pda(&[LENDING_MARKET_SEED, owner.pubkey().as_ref()]);
// The market is seeded by its market_id index alone (no owner). Market 0.
let market_id: u64 = 0;
let market = pda(&[LENDING_MARKET_SEED, &market_id.to_le_bytes()]);

let instruction = Instruction {
program_id: lending::id(),
Expand All @@ -108,7 +110,7 @@ impl Env {
system_program: system_program::id(),
}
.to_account_metas(None),
data: lending::instruction::InitLendingMarket {}.data(),
data: lending::instruction::InitLendingMarket { market_id }.data(),
};
send(&mut svm, vec![instruction], &[&owner], &owner.pubkey()).unwrap();

Expand All @@ -124,7 +126,10 @@ impl Env {
pub fn init_market_for(&mut self, market_owner: &Keypair) -> Pubkey {
let env_owner = self.owner.insecure_clone();
let quote_mint = create_token_mint(&mut self.svm, &env_owner, 6, None).unwrap();
let market = pda(&[LENDING_MARKET_SEED, market_owner.pubkey().as_ref()]);
// A distinct id from the env's market 0, since the id is the market's
// global identifier (the owner is not part of the seed).
let market_id: u64 = 1;
let market = pda(&[LENDING_MARKET_SEED, &market_id.to_le_bytes()]);
let instruction = Instruction {
program_id: lending::id(),
accounts: lending::accounts::InitLendingMarket {
Expand All @@ -134,7 +139,7 @@ impl Env {
system_program: system_program::id(),
}
.to_account_metas(None),
data: lending::instruction::InitLendingMarket {}.data(),
data: lending::instruction::InitLendingMarket { market_id }.data(),
};
send(&mut self.svm, vec![instruction], &[market_owner], &market_owner.pubkey()).unwrap();
market
Expand All @@ -153,12 +158,12 @@ impl Env {
) -> ReserveHandle {
let env_owner = self.owner.insecure_clone();
let mint = create_token_mint(&mut self.svm, &env_owner, decimals, None).unwrap();
self.set_price(mint, price_mantissa);
self.set_price_for(market_owner, market, mint, price_mantissa);

let reserve = pda(&[RESERVE_SEED, market.as_ref(), mint.as_ref()]);
let share_mint = pda(&[SHARE_MINT_SEED, reserve.as_ref()]);
let liquidity_vault = pda(&[LIQUIDITY_VAULT_SEED, reserve.as_ref()]);
let price_feed = self.price_feed_address(mint);
let price_feed = self.price_feed_address(market, mint);

let instruction = Instruction {
program_id: lending::id(),
Expand Down Expand Up @@ -197,17 +202,32 @@ impl Env {

/// The feed PDA the market owner writes for `mint`: seeded by the owner's
/// key, so it is the feed `add_reserve` registers reserves against.
pub fn price_feed_address(&self, mint: Pubkey) -> Pubkey {
pda(&[PRICE_FEED_SEED, self.owner.pubkey().as_ref(), mint.as_ref()])
/// The feed PDA for a given market and mint (seeds `["price_feed", market, mint]`).
pub fn price_feed_address(&self, market: Pubkey, mint: Pubkey) -> Pubkey {
pda(&[PRICE_FEED_SEED, market.as_ref(), mint.as_ref()])
}

pub fn set_price(&mut self, mint: Pubkey, price_mantissa: i128) {
let price_feed = self.price_feed_address(mint);
let owner = self.owner.insecure_clone();
let market = self.market;
self.set_price_for(&owner, market, mint, price_mantissa);
}

/// Publish a price for `mint` in `market`, signed by that market's `owner`.
pub fn set_price_for(
&mut self,
owner: &Keypair,
market: Pubkey,
mint: Pubkey,
price_mantissa: i128,
) {
let price_feed = self.price_feed_address(market, mint);
let instruction = Instruction {
program_id: lending::id(),
accounts: lending::accounts::SetPrice {
lending_market: market,
owner: owner.pubkey(),
price_feed,
authority: self.owner.pubkey(),
mint,
system_program: system_program::id(),
}
Expand All @@ -218,8 +238,7 @@ impl Env {
}
.data(),
};
let owner = self.owner.insecure_clone();
send(&mut self.svm, vec![instruction], &[&owner], &owner.pubkey()).unwrap();
send(&mut self.svm, vec![instruction], &[owner], &owner.pubkey()).unwrap();
}

pub fn add_reserve(
Expand Down
Loading
Loading