diff --git a/Cargo.lock b/Cargo.lock index 63f0ca6..2fdaab9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -415,6 +415,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "backtrace" version = "0.3.69" @@ -1510,6 +1516,53 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" +[[package]] +name = "fixed" +version = "1.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc715d38bea7b5bf487fcd79bcf8c209f0b58014f3018a7a19c2b855f472048" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + +[[package]] +name = "fixed-macro" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0c48af8cb14e02868f449f8a2187bd78af7a08da201fdc78d518ecb1675bc" +dependencies = [ + "fixed", + "fixed-macro-impl", + "fixed-macro-types", +] + +[[package]] +name = "fixed-macro-impl" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c93086f471c0a1b9c5e300ea92f5cd990ac6d3f8edf27616ef624b8fa6402d4b" +dependencies = [ + "fixed", + "paste", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.99", +] + +[[package]] +name = "fixed-macro-types" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "044a61b034a2264a7f65aa0c3cd112a01b4d4ee58baace51fead3f21b993c7e4" +dependencies = [ + "fixed", + "fixed-macro-impl", +] + [[package]] name = "flate2" version = "1.0.22" @@ -1760,6 +1813,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -2916,6 +2979,30 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.99", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.78" @@ -4912,6 +4999,8 @@ dependencies = [ "coinbase-rs", "console 0.14.1", "fd-lock", + "fixed", + "fixed-macro", "ftx", "futures", "influxdb-client", @@ -4942,9 +5031,11 @@ dependencies = [ "spl-token 4.0.0", "spl-token-2022 2.0.1", "spl-token-lending", + "static_assertions", "strum", "thiserror", "tokio", + "uint", ] [[package]] @@ -5341,9 +5432,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "uint" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" dependencies = [ "byteorder", "crunchy", diff --git a/Cargo.toml b/Cargo.toml index 5026966..aa1bfa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,8 @@ clap = "2.33" coinbase-rs = { git = "https://github.com/mvines/coinbase-rs.git", rev = "f9647efa5386e24238a0b9baee3012e6d431271f" } console = "0.14.1" fd-lock = "3.0.0" +fixed = "1.12.0" +fixed-macro = "1.2.0" ftx = { git = "https://github.com/fabianboesiger/ftx", rev = "bb98235d356dd1a2becc5bdf32a4b738311ed434" } #ftx = { git = "https://github.com/mvines/ftx", rev = "22dea8cf63269645eb220c9ce5ffdd0b746a9ceb" } #ftx = { path = "../ftx" } @@ -57,8 +59,10 @@ spl-associated-token-account = "2.3.0" spl-token = "4.0.0" spl-token-2022 = "2.0.1" spl-token-lending = { git = "https://github.com/solana-labs/solana-program-library.git", rev = "1d1c2b178b8cf2ed3e28006c27b2ba5b3d039d67" } +static_assertions = "1.1.0" strum = { version = "0.23", features = ["derive"] } thiserror = "1.0" tokio = { version = "1", features = ["macros", "time"] } #tulipv2-sdk-common = "0.9.5" +uint = "0.9.5" diff --git a/src/bin/sys-lend.rs b/src/bin/sys-lend.rs index f1c2b2d..3fceaf8 100644 --- a/src/bin/sys-lend.rs +++ b/src/bin/sys-lend.rs @@ -1,6 +1,6 @@ use { clap::{value_t, value_t_or_exit, App, AppSettings, Arg, SubCommand}, - solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig}, + solana_account_decoder::UiAccountEncoding, solana_clap_utils::{self, input_parsers::*, input_validators::*}, solana_client::{ rpc_client::RpcClient, @@ -17,12 +17,14 @@ use { system_program, sysvar, transaction::Transaction, }, + std::collections::HashMap, sys::{ app_version, notifier::*, priority_fee::{apply_priority_fee, PriorityFee}, send_transaction_until_expired, token::*, + vendor::{kamino, marginfi_v2}, }, }; @@ -76,7 +78,7 @@ async fn main() -> Result<(), Box> { .value_name("POOL") .takes_value(true) .required(true) - .possible_values(&["kamino-main", "kamino-altcoins", "kamino-jup", "mfi"]) + .possible_values(&["kamino-main", "kamino-altcoins", "kamino-jlp", "mfi"]) .help("Lending pool"), ) .arg( @@ -87,6 +89,14 @@ async fn main() -> Result<(), Box> { .validator(is_valid_signer) .help("Account holding the deposit"), ) + .arg( + Arg::with_name("amount") + .value_name("AMOUNT") + .takes_value(true) + .validator(is_amount_or_all) + .required(true) + .help("The amount to deposit; accepts keyword ALL"), + ) .arg( Arg::with_name("token") .value_name("SOL or SPL Token") @@ -95,16 +105,6 @@ async fn main() -> Result<(), Box> { .validator(is_valid_token_or_sol) .default_value("USDC") .help("Token to deposit"), - ) - .arg( - Arg::with_name("retain") - .short("r") - .long("retain") - .value_name("UI_AMOUNT") - .takes_value(true) - .validator(is_parsable::) - .default_value("0.01") - .help("Amount of tokens to retain in the account"), ), ); @@ -136,32 +136,57 @@ async fn main() -> Result<(), Box> { let pool = value_t_or_exit!(matches, "pool", String); let token = MaybeToken::from(value_t!(matches, "token", Token).ok()); - let retain_amount = token.amount(value_t_or_exit!(matches, "retain", f64)); - let balance = token.balance(&rpc_client, &address)?; - let deposit_amount = balance.saturating_sub(retain_amount); + let token_balance = token.balance(&rpc_client, &address)?; + let deposit_amount = match matches.value_of("amount").unwrap() { + "ALL" => token_balance, + amount => token.amount(amount.parse::().unwrap()), + }; - if deposit_amount == 0 { + if deposit_amount > token_balance { + println!( + "Deposit amount of {} is greater than current balance of {}", + token.format_amount(deposit_amount), + token.format_amount(token_balance), + ); + println!( + "Deposit amount of {} is greater than current balance of {}", + token.format_amount(deposit_amount), + token.format_amount(token_balance), + ); println!( - "Current balance: {}\n\ - Retain amount: {}\n\ - \n\ - Nothing to deposit", - token.format_amount(balance), - token.format_amount(retain_amount) + "Deposit amount of {} is greater than current balance of {}", + token.format_amount(deposit_amount), + token.format_amount(token_balance), ); - return Ok(()); + println!( + "Deposit amount of {} is greater than current balance of {}", + token.format_amount(deposit_amount), + token.format_amount(token_balance), + ); + println!( + "Deposit amount of {} is greater than current balance of {}", + token.format_amount(deposit_amount), + token.format_amount(token_balance), + ); + /* + return Err(format!( + "Deposit amount of {} is greater than current balance of {}", + token.format_amount(deposit_amount), + token.format_amount(token_balance), + ).into()); + */ } println!( "Depositing {} into {}", token.format_amount(deposit_amount), - pool + pool, ); - let (mut instructions, required_compute_units) = if pool.starts_with("kamino-") { - kamino_deposit(&pool, address, token, deposit_amount)? + let (mut instructions, required_compute_units, apr) = if pool.starts_with("kamino-") { + kamino_deposit(&rpc_client, &pool, address, token, deposit_amount)? } else if pool == "mfi" { - mfi_deposit(address, token, deposit_amount)? + mfi_deposit(address, token, deposit_amount, false)? } else { unreachable!(); }; @@ -187,20 +212,23 @@ async fn main() -> Result<(), Box> { transaction.try_sign(&vec![signer], recent_blockhash)?; let signature = transaction.signatures[0]; - println!("Transaction signature: {signature}"); - - if !send_transaction_until_expired(&rpc_client, &transaction, last_valid_block_height) { - return Err("Deposit failed".into()); - } let msg = format!( - "Deposited {} from {} into {}", + "Depositing {} from {} into {} for {:.1}% APR via {}", token.format_amount(deposit_amount), address, - pool + pool, + apr * 100., + signature ); notifier.send(&msg).await; println!("{msg}"); + + if !send_transaction_until_expired(&rpc_client, &transaction, last_valid_block_height) { + let msg = format!("Deposit failed: {signature}"); + notifier.send(&msg).await; + return Err(msg.into()); + } } _ => unreachable!(), } @@ -212,7 +240,8 @@ fn mfi_deposit( address: Pubkey, token: MaybeToken, deposit_amount: u64, -) -> Result<(Vec, u32), Box> { + verbose: bool, +) -> Result<(Vec, u32, f64), Box> { const MFI_LEND_PROGRAM: Pubkey = pubkey!["MFv2hWf31Z9kbCa1snEPYctwafyhdvnV7FZnsebVacA"]; // Big mistake to require using `getProgramAccounts` to locate a MarginFi account for a wallet @@ -226,42 +255,7 @@ fn mfi_deposit( let marginfi_group = pubkey!["4qp6Fx6tnZkY5Wropq9wUYgtFxXKwE6viZxFHg3rdAG8"]; - let marginfi_accounts = rpc_client.get_program_accounts_with_config( - &MFI_LEND_PROGRAM, - RpcProgramAccountsConfig { - filters: Some(vec![ - RpcFilterType::DataSize(2312), - RpcFilterType::Memcmp(rpc_filter::Memcmp::new_raw_bytes( - 40, - address.to_bytes().to_vec(), - )), - RpcFilterType::Memcmp(rpc_filter::Memcmp::new_raw_bytes( - 8, - marginfi_group.to_bytes().to_vec(), - )), - ]), - account_config: RpcAccountInfoConfig { - encoding: Some(UiAccountEncoding::Base64), - data_slice: Some(UiDataSliceConfig { - offset: 0, - length: 0, - }), - commitment: None, - min_context_slot: None, - }, - ..RpcProgramAccountsConfig::default() - }, - )?; - - if marginfi_accounts.is_empty() { - return Err(format!("No MarginFi account found for {}", address).into()); - } - if marginfi_accounts.len() > 1 { - return Err(format!("Multiple MarginFi account found for {}", address).into()); - } - let marginfi_account = marginfi_accounts[0].0; - - let (bank, bank_liquidity_vault) = match token.token() { + let (bank_address, bank_liquidity_vault) = match token.token() { Some(Token::USDC) => Some(( pubkey!["2s37akK2eyBbp8DZgCm7RtsaEz8eJP3Nxd4urLHQv7yB"], pubkey!["7jaiZR5Sk8hdYN9MxTpczTcwbWpb5WEoxSANuUwveuat"], @@ -278,6 +272,97 @@ fn mfi_deposit( } .ok_or_else(|| format!("Depositing {token} into mfi is not supported"))?; + let mut user_accounts = rpc_client + .get_program_accounts_with_config( + &MFI_LEND_PROGRAM, + RpcProgramAccountsConfig { + filters: Some(vec![ + RpcFilterType::DataSize(2312), + RpcFilterType::Memcmp(rpc_filter::Memcmp::new_raw_bytes( + 40, + address.to_bytes().to_vec(), + )), + RpcFilterType::Memcmp(rpc_filter::Memcmp::new_raw_bytes( + 8, + marginfi_group.to_bytes().to_vec(), + )), + ]), + account_config: RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + ..RpcAccountInfoConfig::default() + }, + ..RpcProgramAccountsConfig::default() + }, + )? + .into_iter(); + + let (user_account_address, user_account_data) = user_accounts + .next() + .ok_or_else(|| format!("No MarginFi account found for {}", address))?; + + if user_accounts.next().is_some() { + return Err(format!("Multiple MarginFi account found for {}", address).into()); + } + + fn unsafe_load_bank( + rpc_client: &RpcClient, + address: &Pubkey, + ) -> Result> { + const LEN: usize = std::mem::size_of::(); + let account_data: [u8; LEN] = rpc_client.get_account_data(address)?[8..LEN + 8] + .try_into() + .unwrap(); + let reserve = unsafe { std::mem::transmute(account_data) }; + Ok(reserve) + } + + let bank = unsafe_load_bank(&rpc_client, &bank_address)?; + + let total_deposits = bank.get_asset_amount(bank.total_asset_shares.into()); + let total_borrow = bank.get_liability_amount(bank.total_liability_shares.into()); + let apr = bank + .config + .interest_rate_config + .calc_interest_rate(total_borrow / total_deposits) + .unwrap() + .0 + .to_num::(); + + if verbose { + let user_account = { + const LEN: usize = std::mem::size_of::(); + let data: [u8; LEN] = user_account_data.data[8..LEN + 8].try_into().unwrap(); + unsafe { std::mem::transmute::<[u8; LEN], marginfi_v2::MarginfiAccount>(data) } + }; + + if let Some(balance) = user_account.lending_account.get_balance(&bank_address) { + let deposit = bank.get_asset_amount(balance.asset_shares.into()); + println!( + "Current user deposits: {}", + token.format_amount(deposit.floor().to_num::()) + ); + + let liablilty = bank.get_liability_amount(balance.liability_shares.into()); + println!( + "Current user liablilty: {}", + token.format_amount(liablilty.floor().to_num::()) + ); + } + + println!( + "Deposit Limit: {}", + token.format_amount(bank.config.deposit_limit) + ); + println!( + "Pool deposits: {}", + token.format_amount(total_deposits.floor().to_num::()) + ); + println!( + "Pool liability: {}", + token.format_amount(total_borrow.floor().to_num::()) + ); + } + let marginfi_account_deposit_data = { let mut v = vec![0xab, 0x5e, 0xeb, 0x67, 0x52, 0x40, 0xd4, 0x8c]; v.extend(deposit_amount.to_le_bytes()); @@ -292,11 +377,11 @@ fn mfi_deposit( // Marginfi Group AccountMeta::new_readonly(marginfi_group, false), // Marginfi Account - AccountMeta::new(marginfi_account, false), + AccountMeta::new(user_account_address, false), // Signer AccountMeta::new(address, true), // Bank - AccountMeta::new(bank, false), + AccountMeta::new(bank_address, false), // Signer Token Account AccountMeta::new( spl_associated_token_account::get_associated_token_address(&address, &token.mint()), @@ -309,154 +394,77 @@ fn mfi_deposit( ], )]; - Ok((instructions, 50_000)) + Ok((instructions, 50_000, apr)) } fn kamino_deposit( + rpc_client: &RpcClient, pool: &str, address: Pubkey, token: MaybeToken, deposit_amount: u64, -) -> Result<(Vec, u32), Box> { +) -> Result<(Vec, u32, f64), Box> { const KAMINO_LEND_PROGRAM: Pubkey = pubkey!["KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD"]; const FARMS_PROGRAM: Pubkey = pubkey!["FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr"]; - struct KaminoMarket { - market_reserve: Vec, - active_reserve: usize, - reverse_refresh_obligation_reserve: bool, - lending_market_authority: Pubkey, - lending_market: Pubkey, - reserve_farm_state: Pubkey, - scope_prices: Pubkey, - reserve_liquidity_supply: Pubkey, - reserve_collateral_mint: Pubkey, - reserve_destination_deposit_collateral: Pubkey, - } + let market_reserve_map = match pool { + "kamino-main" => HashMap::from([ + ( + Some(Token::USDC), + pubkey!["D6q6wuQSrifJKZYpR1M8R4YawnLDtDsMmWM1NbBmgJ59"], + ), + ( + Some(Token::USDT), + pubkey!["H3t6qZ1JkguCNTi9uzVKqQ7dvt2cum4XiXWom6Gn5e5S"], + ), + ]), + "kamino-altcoins" => HashMap::from([( + Some(Token::USDC), + pubkey!["9TD2TSv4pENb8VwfbVYg25jvym7HN6iuAR6pFNSrKjqQ"], + )]), + "kamino-jlp" => HashMap::from([( + Some(Token::USDC), + pubkey!["Ga4rZytCpq1unD4DbEJ5bkHeUz9g3oh9AAFEi6vSauXp"], + )]), + _ => HashMap::default(), + }; - struct KaminoMarketReserve { - reserve: Pubkey, - pyth_oracle: Pubkey, + let market_reserve_address = *market_reserve_map + .get(&token.token()) + .ok_or_else(|| format!("Depositing {token} into {pool} is not supported"))?; + + fn unsafe_load_reserve( + rpc_client: &RpcClient, + address: &Pubkey, + ) -> Result> { + const LEN: usize = std::mem::size_of::(); + let account_data: [u8; LEN] = rpc_client.get_account_data(address)?[8..LEN + 8] + .try_into() + .unwrap(); + let reserve = unsafe { std::mem::transmute(account_data) }; + Ok(reserve) } - let market = match pool { - "kamino-main" => match token.token() { - Some(Token::USDC) => Some(KaminoMarket { - market_reserve: vec![ - KaminoMarketReserve { - reserve: pubkey!["H3t6qZ1JkguCNTi9uzVKqQ7dvt2cum4XiXWom6Gn5e5S"], - pyth_oracle: pubkey!["3vxLXJqLqF3JG5TCbYycbKWRBbCJQLxQmBGCkyqEEefL"], - }, - KaminoMarketReserve { - reserve: pubkey!["D6q6wuQSrifJKZYpR1M8R4YawnLDtDsMmWM1NbBmgJ59"], - pyth_oracle: pubkey!["Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD"], - }, - ], - active_reserve: 1, - reverse_refresh_obligation_reserve: true, - lending_market_authority: pubkey!["9DrvZvyWh1HuAoZxvYWMvkf2XCzryCpGgHqrMjyDWpmo"], - lending_market: pubkey!["7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF"], - reserve_farm_state: pubkey!["JAvnB9AKtgPsTEoKmn24Bq64UMoYcrtWtq42HHBdsPkh"], - scope_prices: pubkey!["3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C"], - reserve_liquidity_supply: pubkey!["Bgq7trRgVMeq33yt235zM2onQ4bRDBsY5EWiTetF4qw6"], - reserve_collateral_mint: pubkey!["B8V6WVjPxW1UGwVDfxH2d2r8SyT4cqn7dQRK6XneVa7D"], - reserve_destination_deposit_collateral: pubkey![ - "3DzjXRfxRm6iejfyyMynR4tScddaanrePJ1NJU2XnPPL" - ], - }), - Some(Token::USDT) => Some(KaminoMarket { - market_reserve: vec![ - KaminoMarketReserve { - reserve: pubkey!["D6q6wuQSrifJKZYpR1M8R4YawnLDtDsMmWM1NbBmgJ59"], - pyth_oracle: pubkey!["Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD"], - }, - KaminoMarketReserve { - reserve: pubkey!["H3t6qZ1JkguCNTi9uzVKqQ7dvt2cum4XiXWom6Gn5e5S"], - pyth_oracle: pubkey!["3vxLXJqLqF3JG5TCbYycbKWRBbCJQLxQmBGCkyqEEefL"], - }, - ], - active_reserve: 1, - reverse_refresh_obligation_reserve: false, - lending_market_authority: pubkey!["9DrvZvyWh1HuAoZxvYWMvkf2XCzryCpGgHqrMjyDWpmo"], - lending_market: pubkey!["7u3HeHxYDLhnCoErrtycNokbQYbWGzLs6JSDqGAv5PfF"], - reserve_farm_state: pubkey!["5pCqu9RFdL6QoN7KK4gKnAU6CjQFJot8nU7wpFK8Zwou"], - scope_prices: pubkey!["3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C"], - reserve_liquidity_supply: pubkey!["2Eff8Udy2G2gzNcf2619AnTx3xM4renEv4QrHKjS1o9N"], - reserve_collateral_mint: pubkey!["B8zf4kojJbwgCRKA7rLaLhRCZBGhgAJp8wPBVZZHMhSv"], - reserve_destination_deposit_collateral: pubkey![ - "CTCpzgNbPwWQSYamu4ZomgFuHf8DUGwq8hSYWVLurSJD" - ], - }), - _ => None, - }, - "kamino-altcoins" => { - if token.token() == Some(Token::USDC) { - Some(KaminoMarket { - market_reserve: vec![KaminoMarketReserve { - reserve: pubkey!["9TD2TSv4pENb8VwfbVYg25jvym7HN6iuAR6pFNSrKjqQ"], - pyth_oracle: pubkey!["Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD"], - }], - active_reserve: 0, - reverse_refresh_obligation_reserve: false, - lending_market_authority: pubkey![ - "81BgcfZuZf9bESLvw3zDkh7cZmMtDwTPgkCvYu7zx26o" - ], - lending_market: pubkey!["ByYiZxp8QrdN9qbdtaAiePN8AAr3qvTPppNJDpf5DVJ5"], - reserve_farm_state: pubkey!["23UsLhyeuZBCRJNVFkPrmMCfXuka8hQa8S6spXwTEHcc"], - scope_prices: pubkey!["3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C"], - reserve_liquidity_supply: pubkey![ - "HTyrXvSvBbD7WstvU3oqFTBZM1fPZJPxVRvwLAmCTDyJ" - ], - reserve_collateral_mint: pubkey![ - "A2mcvn3kQXwG9XPUPgjghXJDqvYHTpkCJE3wtKqU1VRn" - ], - reserve_destination_deposit_collateral: pubkey![ - "8bGWMt65Y7RV2DV5sxNRxFM5jsUhBMSo8u24pbRPjQLY" - ], - }) - } else { - None - } - } - "kamino-jup" => { - if token.token() == Some(Token::USDC) { - Some(KaminoMarket { - market_reserve: vec![KaminoMarketReserve { - reserve: pubkey!["Ga4rZytCpq1unD4DbEJ5bkHeUz9g3oh9AAFEi6vSauXp"], - pyth_oracle: pubkey!["Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD"], - }], - active_reserve: 0, - reverse_refresh_obligation_reserve: false, - lending_market_authority: pubkey![ - "B9spsrMK6pJicYtukaZzDyzsUQLgc3jbx5gHVwdDxb6y" - ], - lending_market: pubkey!["DxXdAyU3kCjnyggvHmY5nAwg5cRbbmdyX3npfDMjjMek"], - reserve_farm_state: pubkey!["EGDhupegCXLtonYDSY67c4dzw86S9eMxsntQ1yxWSoHv"], - scope_prices: pubkey!["3NJYftD5sjVfxSnUdZ1wVML8f3aC6mp1CXCL6L7TnU8C"], - reserve_liquidity_supply: pubkey![ - "GENey8es3EgGiNTM8H8gzA3vf98haQF8LHiYFyErjgrv" - ], - reserve_collateral_mint: pubkey![ - "32XLsweyeQwWgLKRVAzS72nxHGU1JmmNQQZ3C3q6fBjJ" - ], - reserve_destination_deposit_collateral: pubkey![ - "6WnymZBTAekuHf9DgsaDKJ397oEZ3qMApNMHg9qjqhgm" - ], - }) - } else { - None - } - } - _ => None, - } - .ok_or_else(|| format!("Depositing {token} into {pool} is not supported"))?; + let reserve = unsafe_load_reserve(rpc_client, &market_reserve_address)?; + let apr = reserve.current_supply_apr(); + + let lending_market = reserve.lending_market; + + let lending_market_authority = + Pubkey::find_program_address(&[b"lma", &lending_market.to_bytes()], &KAMINO_LEND_PROGRAM).0; + + let reserve_farm_state = reserve.farm_collateral; + let scope_prices = reserve.config.token_info.scope_configuration.price_feed; + let reserve_liquidity_supply = reserve.liquidity.supply_vault; + let reserve_collateral_mint = reserve.collateral.mint_pubkey; + let reserve_destination_deposit_collateral = reserve.collateral.supply_vault; let market_obligation = Pubkey::find_program_address( &[ &[0], &[0], &address.to_bytes(), - &market.lending_market.to_bytes(), + &lending_market.to_bytes(), &system_program::ID.to_bytes(), &system_program::ID.to_bytes(), ], @@ -464,36 +472,71 @@ fn kamino_deposit( ) .0; - let market_obligation_farm_user_state = Pubkey::find_program_address( - &[ - b"user", - &market.reserve_farm_state.to_bytes(), - &market_obligation.to_bytes(), - ], - &FARMS_PROGRAM, - ) - .0; + fn unsafe_load_obligation( + rpc_client: &RpcClient, + address: &Pubkey, + ) -> Result> { + const LEN: usize = std::mem::size_of::(); + let account_data: [u8; LEN] = rpc_client.get_account_data(address)?[8..LEN + 8] + .try_into() + .unwrap(); + let obligation = unsafe { std::mem::transmute(account_data) }; + Ok(obligation) + } + + let obligation = unsafe_load_obligation(rpc_client, &market_obligation)?; + + let obligation_market_reserves = obligation + .deposits + .iter() + .filter(|c| c.deposit_reserve != Pubkey::default()) + .map(|c| c.deposit_reserve) + .collect::>(); let mut instructions = vec![]; // Instruction: Kamino: Refresh Reserve - for market_reserve in &market.market_reserve { + + let mut refresh_reserves: Vec<(Pubkey, Pubkey)> = obligation_market_reserves + .iter() + .filter_map(|reserve_address| { + if *reserve_address != market_reserve_address { + let reserve = + unsafe_load_reserve(rpc_client, reserve_address).unwrap_or_else(|err| { + // TODO: propagate failure up instead of panic.. + panic!("unable to load reserve {reserve_address}: {err}") + }); + + Some(( + *reserve_address, + reserve.config.token_info.pyth_configuration.price, + )) + } else { + None + } + }) + .collect(); + + refresh_reserves.push(( + market_reserve_address, + reserve.config.token_info.pyth_configuration.price, + )); + for (refresh_reserve, pyth_oracle) in refresh_reserves.iter() { instructions.push(Instruction::new_with_bytes( KAMINO_LEND_PROGRAM, &[0x02, 0xda, 0x8a, 0xeb, 0x4f, 0xc9, 0x19, 0x66], vec![ // Reserve - AccountMeta::new(market_reserve.reserve, false), + AccountMeta::new(*refresh_reserve, false), // Lending Market - AccountMeta::new_readonly(market.lending_market, false), + AccountMeta::new_readonly(lending_market, false), // Pyth Oracle - AccountMeta::new_readonly(market_reserve.pyth_oracle, false), - // Switchboard Price Oracle + AccountMeta::new_readonly(*pyth_oracle, false), AccountMeta::new_readonly(KAMINO_LEND_PROGRAM, false), // Switchboard Twap Oracle AccountMeta::new_readonly(KAMINO_LEND_PROGRAM, false), // Scope Prices - AccountMeta::new_readonly(market.scope_prices, false), + AccountMeta::new_readonly(scope_prices, false), ], )); } @@ -501,20 +544,13 @@ fn kamino_deposit( // Instruction: Kamino: Refresh Obligation let mut refresh_obligation_account_metas = vec![ // Lending Market - AccountMeta::new_readonly(market.lending_market, false), + AccountMeta::new_readonly(lending_market, false), // Obligation AccountMeta::new(market_obligation, false), ]; - let market_reserve_iter: Vec<&KaminoMarketReserve> = - if market.reverse_refresh_obligation_reserve { - market.market_reserve.iter().rev().collect() - } else { - market.market_reserve.iter().collect() - }; - - for market_reserve in &market_reserve_iter { - refresh_obligation_account_metas.push(AccountMeta::new(market_reserve.reserve, false)); + for obligation_market_reserve in &obligation_market_reserves { + refresh_obligation_account_metas.push(AccountMeta::new(*obligation_market_reserve, false)); } instructions.push(Instruction::new_with_bytes( @@ -536,15 +572,26 @@ fn kamino_deposit( // Obligation AccountMeta::new(market_obligation, false), // Lending Market Authority - AccountMeta::new(market.lending_market_authority, false), + AccountMeta::new(lending_market_authority, false), // Reserve - AccountMeta::new(market.market_reserve[market.active_reserve].reserve, false), + AccountMeta::new(market_reserve_address, false), // Reserve Farm State - AccountMeta::new(market.reserve_farm_state, false), + AccountMeta::new(reserve_farm_state, false), // Obligation Farm User State - AccountMeta::new(market_obligation_farm_user_state, false), + AccountMeta::new( + Pubkey::find_program_address( + &[ + b"user", + &reserve_farm_state.to_bytes(), + &market_obligation.to_bytes(), + ], + &FARMS_PROGRAM, + ) + .0, + false, + ), // Lending Market - AccountMeta::new_readonly(market.lending_market, false), + AccountMeta::new_readonly(lending_market, false), // Farms Program AccountMeta::new_readonly(FARMS_PROGRAM, false), // Rent @@ -573,17 +620,17 @@ fn kamino_deposit( // Obligation AccountMeta::new(market_obligation, false), // Lending Market - AccountMeta::new_readonly(market.lending_market, false), + AccountMeta::new_readonly(lending_market, false), // Lending Market Authority - AccountMeta::new(market.lending_market_authority, false), + AccountMeta::new(lending_market_authority, false), // Reserve - AccountMeta::new(market.market_reserve[market.active_reserve].reserve, false), + AccountMeta::new(market_reserve_address, false), // Reserve Liquidity Supply - AccountMeta::new(market.reserve_liquidity_supply, false), + AccountMeta::new(reserve_liquidity_supply, false), // Reserve Collateral Mint - AccountMeta::new(market.reserve_collateral_mint, false), + AccountMeta::new(reserve_collateral_mint, false), // Reserve Destination Deposit Collateral - AccountMeta::new(market.reserve_destination_deposit_collateral, false), + AccountMeta::new(reserve_destination_deposit_collateral, false), // User Source Liquidity AccountMeta::new( spl_associated_token_account::get_associated_token_address(&address, &token.mint()), @@ -601,5 +648,5 @@ fn kamino_deposit( // Instruction: Kamino: Refresh Obligation Farms For Reserve instructions.push(kamino_refresh_obligation_farms_for_reserve); - Ok((instructions, 1_500_000)) + Ok((instructions, 500_000, apr)) } diff --git a/src/lib.rs b/src/lib.rs index cd0fe40..71436f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ pub mod metrics; pub mod notifier; pub mod priority_fee; pub mod token; +pub mod vendor; //pub mod tulip; pub fn app_version() -> String { diff --git a/src/vendor/kamino/borrow_rate_curve.rs b/src/vendor/kamino/borrow_rate_curve.rs new file mode 100644 index 0000000..6e93d15 --- /dev/null +++ b/src/vendor/kamino/borrow_rate_curve.rs @@ -0,0 +1,103 @@ +use super::fraction::{Fraction, FractionExtra}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(C, packed(1))] +pub struct BorrowRateCurve { + pub points: [CurvePoint; 11], +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct CurveSegment { + pub slope_nom: u32, + pub slope_denom: u32, + pub start_point: CurvePoint, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[repr(C, packed(1))] +pub struct CurvePoint { + pub utilization_rate_bps: u32, + pub borrow_rate_bps: u32, +} + +impl CurvePoint { + pub fn new(utilization_rate_bps: u32, borrow_rate_bps: u32) -> Self { + Self { + utilization_rate_bps, + borrow_rate_bps, + } + } +} + +impl CurveSegment { + pub fn from_points(start: CurvePoint, end: CurvePoint) -> Option { + let slope_nom = end.borrow_rate_bps.checked_sub(start.borrow_rate_bps)?; + if end.utilization_rate_bps <= start.utilization_rate_bps { + //msg!("Utilization rate must be ever growing in the curve"); + return None; + } + let slope_denom = end + .utilization_rate_bps + .checked_sub(start.utilization_rate_bps) + .unwrap(); + + Some(CurveSegment { + slope_nom, + slope_denom, + start_point: start, + }) + } + + pub(self) fn get_borrow_rate(&self, utilization_rate: Fraction) -> Option { + let start_utilization_rate = Fraction::from_bps(self.start_point.utilization_rate_bps); + + let coef = utilization_rate.checked_sub(start_utilization_rate)?; + + let nom = coef * u128::from(self.slope_nom); + let base_rate = nom / u128::from(self.slope_denom); + + let offset = Fraction::from_bps(self.start_point.borrow_rate_bps); + + Some(base_rate + offset) + } +} + +impl BorrowRateCurve { + pub fn get_borrow_rate(&self, utilization_rate: Fraction) -> Option { + let utilization_rate = if utilization_rate > Fraction::ONE { + /* + msg!( + "Warning: utilization rate is greater than 100% (scaled): {}", + utilization_rate.to_bits() + ); + */ + Fraction::ONE + } else { + utilization_rate + }; + + let utilization_rate_bps: u32 = utilization_rate.to_bps().unwrap(); + + let (start_pt, end_pt) = self + .points + .windows(2) + .map(|seg| { + let [first, second]: &[CurvePoint; 2] = seg.try_into().unwrap(); + (first, second) + }) + .find(|(first, second)| { + utilization_rate_bps >= first.utilization_rate_bps + && utilization_rate_bps <= second.utilization_rate_bps + }) + .unwrap(); + if utilization_rate_bps == start_pt.utilization_rate_bps { + return Some(Fraction::from_bps(start_pt.borrow_rate_bps)); + } else if utilization_rate_bps == end_pt.utilization_rate_bps { + return Some(Fraction::from_bps(end_pt.borrow_rate_bps)); + } + + let segment = CurveSegment::from_points(*start_pt, *end_pt)?; + + segment.get_borrow_rate(utilization_rate) + } +} diff --git a/src/vendor/kamino/fraction.rs b/src/vendor/kamino/fraction.rs new file mode 100644 index 0000000..e6219f8 --- /dev/null +++ b/src/vendor/kamino/fraction.rs @@ -0,0 +1,307 @@ +use fixed::traits::{FromFixed, ToFixed}; +pub use fixed::types::U68F60 as Fraction; +pub use fixed_macro::types::U68F60 as fraction; + +pub enum LendingError { + IntegerOverflow, +} + +#[allow(clippy::assign_op_pattern)] +#[allow(clippy::reversed_empty_ranges)] +mod uint_types { + use uint::construct_uint; + construct_uint! { + pub struct U256(4); + } + construct_uint! { + pub struct U128(2); + } +} + +pub use uint_types::{U128, U256}; + +pub fn pow_fraction(fraction: Fraction, power: u32) -> Option { + if power == 0 { + return Some(Fraction::ONE); + } + + let mut x = fraction; + let mut y = Fraction::ONE; + let mut n = power; + + while n > 1 { + if n % 2 == 1 { + y = x.checked_mul(y)?; + } + x = x.checked_mul(x)?; + n /= 2; + } + + x.checked_mul(y) +} + +pub trait FractionExtra { + fn to_percent(&self) -> Option; + fn to_bps(&self) -> Option; + fn from_percent(percent: Src) -> Self; + fn from_bps(bps: Src) -> Self; + fn checked_pow(&self, power: u32) -> Option + where + Self: std::marker::Sized; + + fn to_floor(&self) -> Dst; + fn to_ceil(&self) -> Dst; + fn to_round(&self) -> Dst; + + fn to_sf(&self) -> u128; + fn from_sf(sf: u128) -> Self; + + fn to_display(&self) -> FractionDisplay; +} + +impl FractionExtra for Fraction { + #[inline] + fn to_percent(&self) -> Option { + (self * 100).round().checked_to_num() + } + + #[inline] + fn to_bps(&self) -> Option { + (self * 10_000).round().checked_to_num() + } + + #[inline] + fn from_percent(percent: Src) -> Self { + let percent = Fraction::from_num(percent); + percent / 100 + } + + #[inline] + fn from_bps(bps: Src) -> Self { + let bps = Fraction::from_num(bps); + bps / 10_000 + } + + #[inline] + fn checked_pow(&self, power: u32) -> Option + where + Self: std::marker::Sized, + { + pow_fraction(*self, power) + } + + #[inline] + fn to_floor(&self) -> Dst { + self.floor().to_num() + } + + #[inline] + fn to_ceil(&self) -> Dst { + self.ceil().to_num() + } + + #[inline] + fn to_round(&self) -> Dst { + self.round().to_num() + } + + #[inline] + fn to_sf(&self) -> u128 { + self.to_bits() + } + + #[inline] + fn from_sf(sf: u128) -> Self { + Fraction::from_bits(sf) + } + + #[inline] + fn to_display(&self) -> FractionDisplay { + FractionDisplay(self) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, PartialOrd, Ord)] +pub struct BigFraction(pub U256); + +impl From for BigFraction +where + T: Into, +{ + fn from(fraction: T) -> Self { + let fraction: Fraction = fraction.into(); + let repr_fraction = fraction.to_bits(); + Self(U256::from(repr_fraction)) + } +} + +impl TryFrom for Fraction { + type Error = LendingError; + + fn try_from(value: BigFraction) -> Result { + let repr_faction: u128 = value + .0 + .try_into() + .map_err(|_| LendingError::IntegerOverflow)?; + Ok(Fraction::from_bits(repr_faction)) + } +} + +impl BigFraction { + pub fn from_bits(bits: [u64; 4]) -> Self { + Self(U256(bits)) + } + + pub fn from_num(num: T) -> Self + where + T: Into, + { + let value: U256 = num.into(); + let sf = value << Fraction::FRAC_NBITS; + Self(sf) + } +} + +use std::{ + fmt::Display, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign}, +}; + +impl Add for BigFraction { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl AddAssign for BigFraction { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } +} + +impl Sub for BigFraction { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl SubAssign for BigFraction { + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0; + } +} + +impl Mul for BigFraction { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + let extra_scaled = self.0 * rhs.0; + let res = extra_scaled >> Fraction::FRAC_NBITS; + Self(res) + } +} + +impl MulAssign for BigFraction { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl Div for BigFraction { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + let extra_scaled = self.0 << Fraction::FRAC_NBITS; + let res = extra_scaled / rhs.0; + Self(res) + } +} + +impl DivAssign for BigFraction { + fn div_assign(&mut self, rhs: Self) { + *self = *self / rhs; + } +} + +impl Mul for BigFraction +where + T: Into, +{ + type Output = Self; + + fn mul(self, rhs: T) -> Self::Output { + let rhs: U256 = rhs.into(); + Self(self.0 * rhs) + } +} + +impl MulAssign for BigFraction +where + T: Into, +{ + fn mul_assign(&mut self, rhs: T) { + let rhs: U256 = rhs.into(); + self.0 *= rhs; + } +} + +impl Div for BigFraction +where + T: Into, +{ + type Output = Self; + + fn div(self, rhs: T) -> Self::Output { + let rhs: U256 = rhs.into(); + Self(self.0 / rhs) + } +} + +impl DivAssign for BigFraction +where + T: Into, +{ + fn div_assign(&mut self, rhs: T) { + let rhs: U256 = rhs.into(); + self.0 /= rhs; + } +} + +impl From for U256 { + fn from(value: U128) -> Self { + Self([value.0[0], value.0[1], 0, 0]) + } +} + +impl TryFrom for U128 { + type Error = LendingError; + + fn try_from(value: U256) -> Result { + if value.0[2] != 0 || value.0[3] != 0 { + return Err(LendingError::IntegerOverflow); + } + Ok(Self([value.0[0], value.0[1]])) + } +} + +pub struct FractionDisplay<'a>(&'a Fraction); + +impl Display for FractionDisplay<'_> { + fn fmt(&self, formater: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let sf = self.0.to_bits(); + + const ROUND_COMP: u128 = (1 << Fraction::FRAC_NBITS) / (10_000 * 2); + let sf = sf + ROUND_COMP; + + let i = sf >> Fraction::FRAC_NBITS; + + const FRAC_MASK: u128 = (1 << Fraction::FRAC_NBITS) - 1; + let f_p = (sf & FRAC_MASK) as u64; + let f_p = ((f_p >> 30) * 10_000) >> 30; + write!(formater, "{i}.{f_p:0>4}") + } +} diff --git a/src/vendor/kamino/last_update.rs b/src/vendor/kamino/last_update.rs new file mode 100644 index 0000000..ea879d4 --- /dev/null +++ b/src/vendor/kamino/last_update.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct LastUpdate { + slot: u64, + stale: u8, + price_status: u8, + placeholder: [u8; 6], +} diff --git a/src/vendor/kamino/mod.rs b/src/vendor/kamino/mod.rs new file mode 100644 index 0000000..378b953 --- /dev/null +++ b/src/vendor/kamino/mod.rs @@ -0,0 +1,10 @@ +/// Kamino bits yanked from https://github.com/Kamino-Finance/klend/blob/master/programs/klend/src +mod borrow_rate_curve; +mod fraction; +mod last_update; +mod obligation; +mod reserve; +mod token_info; + +pub use obligation::Obligation; +pub use reserve::Reserve; diff --git a/src/vendor/kamino/obligation.rs b/src/vendor/kamino/obligation.rs new file mode 100644 index 0000000..0a4742b --- /dev/null +++ b/src/vendor/kamino/obligation.rs @@ -0,0 +1,60 @@ +use { + super::{last_update::LastUpdate, reserve::BigFractionBytes}, + solana_sdk::pubkey::Pubkey, +}; + +static_assertions::const_assert_eq!(3336, std::mem::size_of::()); +static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct Obligation { + pub tag: u64, + pub last_update: LastUpdate, + pub lending_market: Pubkey, + pub owner: Pubkey, + pub deposits: [ObligationCollateral; 8], + pub lowest_reserve_deposit_ltv: u64, + pub deposited_value_sf: u128, + + pub borrows: [ObligationLiquidity; 5], + pub borrow_factor_adjusted_debt_value_sf: u128, + pub borrowed_assets_market_value_sf: u128, + pub allowed_borrow_value_sf: u128, + pub unhealthy_borrow_value_sf: u128, + + pub deposits_asset_tiers: [u8; 8], + pub borrows_asset_tiers: [u8; 5], + + pub elevation_group: u8, + + pub num_of_obsolete_reserves: u8, + + pub has_debt: u8, + + pub referrer: Pubkey, + + pub padding_3: [u64; 128], +} + +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct ObligationCollateral { + pub deposit_reserve: Pubkey, + pub deposited_amount: u64, + pub market_value_sf: u128, + pub padding: [u64; 10], +} + +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct ObligationLiquidity { + pub borrow_reserve: Pubkey, + pub cumulative_borrow_rate_bsf: BigFractionBytes, + pub padding: u64, + pub borrowed_amount_sf: u128, + pub market_value_sf: u128, + pub borrow_factor_adjusted_market_value_sf: u128, + + pub padding2: [u64; 8], +} + diff --git a/src/vendor/kamino/reserve.rs b/src/vendor/kamino/reserve.rs new file mode 100644 index 0000000..c506d6a --- /dev/null +++ b/src/vendor/kamino/reserve.rs @@ -0,0 +1,167 @@ +pub use fixed::types::U68F60 as Fraction; +use { + super::{borrow_rate_curve::BorrowRateCurve, last_update::LastUpdate, token_info::TokenInfo}, + solana_sdk::pubkey::Pubkey, +}; + +#[derive(Default, Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct BigFractionBytes { + pub value: [u64; 4], + pub padding: [u64; 2], +} + +static_assertions::const_assert_eq!(8616, std::mem::size_of::()); +static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct Reserve { + pub version: u64, + + pub last_update: LastUpdate, + + pub lending_market: Pubkey, + + pub farm_collateral: Pubkey, + pub farm_debt: Pubkey, + + pub liquidity: ReserveLiquidity, + + pub reserve_liquidity_padding: [u64; 150], + + pub collateral: ReserveCollateral, + + pub reserve_collateral_padding: [u64; 150], + + pub config: ReserveConfig, + + pub config_padding: [u64; 150], + + pub padding: [u64; 240], +} + +impl Reserve { + pub fn current_supply_apr(&self) -> f64 { + let utilization_rate = self.liquidity.utilization_rate(); + let protocol_take_rate_pct = self.config.protocol_take_rate_pct as f64 / 100.; + + let current_borrow_rate = self + .config + .borrow_rate_curve + .get_borrow_rate(utilization_rate) + .unwrap_or(Fraction::ZERO); + + (utilization_rate + * current_borrow_rate + * (Fraction::ONE - Fraction::from_num(protocol_take_rate_pct))) + .checked_to_num() + .unwrap_or(0.) + } +} + +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct ReserveLiquidity { + pub mint_pubkey: Pubkey, + pub supply_vault: Pubkey, + pub fee_vault: Pubkey, + pub available_amount: u64, + pub borrowed_amount_sf: u128, + pub market_price_sf: u128, + pub market_price_last_updated_ts: u64, + pub mint_decimals: u64, + + pub deposit_limit_crossed_slot: u64, + pub borrow_limit_crossed_slot: u64, + + pub cumulative_borrow_rate_bsf: BigFractionBytes, + pub accumulated_protocol_fees_sf: u128, + pub accumulated_referrer_fees_sf: u128, + pub pending_referrer_fees_sf: u128, + pub absolute_referral_rate_sf: u128, + + pub padding2: [u64; 55], + pub padding3: [u128; 32], +} + +impl ReserveLiquidity { + pub fn total_supply(&self) -> Fraction { + Fraction::from(self.available_amount) + Fraction::from_bits(self.borrowed_amount_sf) + - Fraction::from_bits(self.accumulated_protocol_fees_sf) + - Fraction::from_bits(self.accumulated_referrer_fees_sf) + - Fraction::from_bits(self.pending_referrer_fees_sf) + } + + pub fn total_borrow(&self) -> Fraction { + Fraction::from_bits(self.borrowed_amount_sf) + } + + pub fn utilization_rate(&self) -> Fraction { + let total_supply = self.total_supply(); + if total_supply == Fraction::ZERO { + return Fraction::ZERO; + } + Fraction::from_bits(self.borrowed_amount_sf) / total_supply + } +} + +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct ReserveCollateral { + pub mint_pubkey: Pubkey, + pub mint_total_supply: u64, + pub supply_vault: Pubkey, + pub padding1: [u128; 32], + pub padding2: [u128; 32], +} + +static_assertions::const_assert_eq!(648, std::mem::size_of::()); +static_assertions::const_assert_eq!(0, std::mem::size_of::() % 8); +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct ReserveConfig { + pub status: u8, + pub asset_tier: u8, + pub reserved_0: [u8; 2], + pub multiplier_side_boost: [u8; 2], + pub multiplier_tag_boost: [u8; 8], + pub protocol_take_rate_pct: u8, + pub protocol_liquidation_fee_pct: u8, + pub loan_to_value_pct: u8, + pub liquidation_threshold_pct: u8, + pub min_liquidation_bonus_bps: u16, + pub max_liquidation_bonus_bps: u16, + pub bad_debt_liquidation_bonus_bps: u16, + pub deleveraging_margin_call_period_secs: u64, + pub deleveraging_threshold_slots_per_bps: u64, + pub fees: ReserveFees, + pub borrow_rate_curve: BorrowRateCurve, + pub borrow_factor_pct: u64, + + pub deposit_limit: u64, + pub borrow_limit: u64, + pub token_info: TokenInfo, + + pub deposit_withdrawal_cap: WithdrawalCaps, + pub debt_withdrawal_cap: WithdrawalCaps, + + pub elevation_groups: [u8; 20], + pub reserved_1: [u8; 4], +} + +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct WithdrawalCaps { + pub config_capacity: i64, + pub current_total: i64, + pub last_interval_start_timestamp: u64, + pub config_interval_length_seconds: u64, +} + +#[derive(Debug, Clone, Copy)] +#[repr(C, packed(1))] +pub struct ReserveFees { + pub borrow_fee_sf: u64, + pub flash_loan_fee_sf: u64, + pub padding: [u8; 8], +} diff --git a/src/vendor/kamino/token_info.rs b/src/vendor/kamino/token_info.rs new file mode 100644 index 0000000..0a019ba --- /dev/null +++ b/src/vendor/kamino/token_info.rs @@ -0,0 +1,51 @@ +use solana_sdk::pubkey::Pubkey; + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct TokenInfo { + pub name: [u8; 32], + + pub heuristic: PriceHeuristic, + + pub max_twap_divergence_bps: u64, + + pub max_age_price_seconds: u64, + pub max_age_twap_seconds: u64, + + pub scope_configuration: ScopeConfiguration, + + pub switchboard_configuration: SwitchboardConfiguration, + + pub pyth_configuration: PythConfiguration, + + pub _padding: [u64; 20], +} + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct PriceHeuristic { + pub lower: u64, + pub upper: u64, + pub exp: u64, +} + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct ScopeConfiguration { + pub price_feed: Pubkey, + pub price_chain: [u16; 4], + pub twap_chain: [u16; 4], +} + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct SwitchboardConfiguration { + pub price_aggregator: Pubkey, + pub twap_aggregator: Pubkey, +} + +#[derive(Debug, Clone, Copy)] +#[repr(C)] +pub struct PythConfiguration { + pub price: Pubkey, +} diff --git a/src/vendor/marginfi_v2.rs b/src/vendor/marginfi_v2.rs new file mode 100644 index 0000000..1f16be1 --- /dev/null +++ b/src/vendor/marginfi_v2.rs @@ -0,0 +1,370 @@ +/// MarginFi v2 bits yanked from https://github.com/mrgnlabs/marginfi-v2/tree/main/programs/marginfi/src +use { + fixed::types::I80F48, + solana_sdk::pubkey::Pubkey, + std::fmt::{Debug, Formatter}, +}; + +const MAX_ORACLE_KEYS: usize = 5; + +/// Value where total_asset_value_init_limit is considered inactive +const TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE: u64 = 0; + +macro_rules! assert_struct_size { + ($struct: ty, $size: expr) => { + static_assertions::const_assert_eq!(std::mem::size_of::<$struct>(), $size); + }; +} + +macro_rules! assert_struct_align { + ($struct: ty, $align: expr) => { + static_assertions::const_assert_eq!(std::mem::align_of::<$struct>(), $align); + }; +} + +assert_struct_size!(Bank, 1856); +assert_struct_align!(Bank, 8); +#[repr(C)] +#[cfg_attr( + any(feature = "test", feature = "client"), + derive(Debug, PartialEq, Eq, TypeLayout) +)] +#[derive(Default, Debug)] +pub struct Bank { + pub mint: Pubkey, + pub mint_decimals: u8, + + pub group: Pubkey, + + pub asset_share_value: WrappedI80F48, + pub liability_share_value: WrappedI80F48, + + pub liquidity_vault: Pubkey, + pub liquidity_vault_bump: u8, + pub liquidity_vault_authority_bump: u8, + + pub insurance_vault: Pubkey, + pub insurance_vault_bump: u8, + pub insurance_vault_authority_bump: u8, + pub collected_insurance_fees_outstanding: WrappedI80F48, + + pub fee_vault: Pubkey, + pub fee_vault_bump: u8, + pub fee_vault_authority_bump: u8, + pub collected_group_fees_outstanding: WrappedI80F48, + + pub total_liability_shares: WrappedI80F48, + pub total_asset_shares: WrappedI80F48, + + pub last_update: i64, + + pub config: BankConfig, + + /// Emissions Config Flags + /// + /// - EMISSIONS_FLAG_BORROW_ACTIVE: 1 + /// - EMISSIONS_FLAG_LENDING_ACTIVE: 2 + /// + pub emissions_flags: u64, + /// Emissions APR. + /// Number of emitted tokens (emissions_mint) per 1e(bank.mint_decimal) tokens (bank mint) (native amount) per 1 YEAR. + pub emissions_rate: u64, + pub emissions_remaining: WrappedI80F48, + pub emissions_mint: Pubkey, + + pub _padding_0: [[u64; 2]; 28], + pub _padding_1: [[u64; 2]; 32], // 16 * 2 * 32 = 1024B +} + +impl Bank { + pub fn get_asset_amount(&self, shares: I80F48) -> I80F48 { + shares + .checked_mul(self.asset_share_value.into()) + .expect("bad math") + } + pub fn get_liability_amount(&self, shares: I80F48) -> I80F48 { + shares + .checked_mul(self.liability_share_value.into()) + .expect("bad math") + } +} + +#[repr(C, align(8))] +#[cfg_attr( + any(feature = "test", feature = "client"), + derive(PartialEq, Eq, TypeLayout) +)] +#[derive(Default, Clone, Copy)] +pub struct WrappedI80F48 { + pub value: [u8; 16], +} + +impl Debug for WrappedI80F48 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", I80F48::from_le_bytes(self.value)) + } +} + +impl From for WrappedI80F48 { + fn from(i: I80F48) -> Self { + Self { + value: i.to_le_bytes(), + } + } +} + +impl From for I80F48 { + fn from(w: WrappedI80F48) -> Self { + Self::from_le_bytes(w.value) + } +} + +#[repr(u8)] +#[derive(Copy, Clone, Debug)] +pub enum BankOperationalState { + Paused, + Operational, + ReduceOnly, +} + +#[repr(C)] +#[derive(Default, Debug)] +pub struct InterestRateConfig { + // Curve Params + pub optimal_utilization_rate: WrappedI80F48, + pub plateau_interest_rate: WrappedI80F48, + pub max_interest_rate: WrappedI80F48, + + // Fees + pub insurance_fee_fixed_apr: WrappedI80F48, + pub insurance_ir_fee: WrappedI80F48, + pub protocol_fixed_fee_apr: WrappedI80F48, + pub protocol_ir_fee: WrappedI80F48, + + pub _padding: [[u64; 2]; 8], // 16 * 8 = 128 bytes +} + +/// Calculates the fee rate for a given base rate and fees specified. +/// The returned rate is only the fee rate without the base rate. +/// +/// Used for calculating the fees charged to the borrowers. +fn calc_fee_rate(base_rate: I80F48, rate_fees: I80F48, fixed_fees: I80F48) -> Option { + base_rate.checked_mul(rate_fees)?.checked_add(fixed_fees) +} + +impl InterestRateConfig { + /// Return interest rate charged to borrowers and to depositors. + /// Rate is denominated in APR (0-). + /// + /// Return (`lending_rate`, `borrowing_rate`, `group_fees_apr`, `insurance_fees_apr`) + pub fn calc_interest_rate( + &self, + utilization_ratio: I80F48, + ) -> Option<(I80F48, I80F48, I80F48, I80F48)> { + let protocol_ir_fee = I80F48::from(self.protocol_ir_fee); + let insurance_ir_fee = I80F48::from(self.insurance_ir_fee); + + let protocol_fixed_fee_apr = I80F48::from(self.protocol_fixed_fee_apr); + let insurance_fee_fixed_apr = I80F48::from(self.insurance_fee_fixed_apr); + + let rate_fee = protocol_ir_fee + insurance_ir_fee; + let total_fixed_fee_apr = protocol_fixed_fee_apr + insurance_fee_fixed_apr; + + let base_rate = self.interest_rate_curve(utilization_ratio)?; + + // Lending rate is adjusted for utilization ratio to symmetrize payments between borrowers and depositors. + let lending_rate = base_rate.checked_mul(utilization_ratio)?; + + // Borrowing rate is adjusted for fees. + // borrowing_rate = base_rate + base_rate * rate_fee + total_fixed_fee_apr + let borrowing_rate = base_rate + .checked_mul(I80F48::ONE.checked_add(rate_fee)?)? + .checked_add(total_fixed_fee_apr)?; + + let group_fees_apr = calc_fee_rate( + base_rate, + self.protocol_ir_fee.into(), + self.protocol_fixed_fee_apr.into(), + )?; + + let insurance_fees_apr = calc_fee_rate( + base_rate, + self.insurance_ir_fee.into(), + self.insurance_fee_fixed_apr.into(), + )?; + + assert!(lending_rate >= I80F48::ZERO); + assert!(borrowing_rate >= I80F48::ZERO); + assert!(group_fees_apr >= I80F48::ZERO); + assert!(insurance_fees_apr >= I80F48::ZERO); + + // TODO: Add liquidation discount check + + Some(( + lending_rate, + borrowing_rate, + group_fees_apr, + insurance_fees_apr, + )) + } + + /// Piecewise linear interest rate function. + /// The curves approaches the `plateau_interest_rate` as the utilization ratio approaches the `optimal_utilization_rate`, + /// once the utilization ratio exceeds the `optimal_utilization_rate`, the curve approaches the `max_interest_rate`. + /// + /// To be clear we don't particularly appreciate the piecewise linear nature of this "curve", but it is what it is. + fn interest_rate_curve(&self, ur: I80F48) -> Option { + let optimal_ur = self.optimal_utilization_rate.into(); + let plateau_ir = self.plateau_interest_rate.into(); + let max_ir: I80F48 = self.max_interest_rate.into(); + + if ur <= optimal_ur { + ur.checked_div(optimal_ur)?.checked_mul(plateau_ir) + } else { + (ur - optimal_ur) + .checked_div(I80F48::ONE - optimal_ur)? + .checked_mul(max_ir - plateau_ir)? + .checked_add(plateau_ir) + } + } +} + +#[repr(u8)] +#[cfg_attr(any(feature = "test", feature = "client"), derive(PartialEq, Eq))] +#[derive(Copy, Clone, Debug)] +pub enum OracleSetup { + None, + PythEma, + SwitchboardV2, +} + +#[repr(u64)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum RiskTier { + Collateral, + /// ## Isolated Risk + /// Assets in this trance can be borrowed only in isolation. + /// They can't be borrowed together with other assets. + /// + /// For example, if users has USDC, and wants to borrow XYZ which is isolated, + /// they can't borrow XYZ together with SOL, only XYZ alone. + Isolated, +} + +assert_struct_size!(BankConfig, 544); +assert_struct_align!(BankConfig, 8); +#[repr(C)] +#[derive(Debug)] +/// TODO: Convert weights to (u64, u64) to avoid precision loss (maybe?) +pub struct BankConfig { + pub asset_weight_init: WrappedI80F48, + pub asset_weight_maint: WrappedI80F48, + + pub liability_weight_init: WrappedI80F48, + pub liability_weight_maint: WrappedI80F48, + + pub deposit_limit: u64, + + pub interest_rate_config: InterestRateConfig, + pub operational_state: BankOperationalState, + + pub oracle_setup: OracleSetup, + pub oracle_keys: [Pubkey; MAX_ORACLE_KEYS], + + pub borrow_limit: u64, + + pub risk_tier: RiskTier, + + /// USD denominated limit for calculating asset value for initialization margin requirements. + /// Example, if total SOL deposits are equal to $1M and the limit it set to $500K, + /// then SOL assets will be discounted by 50%. + /// + /// In other words the max value of liabilities that can be backed by the asset is $500K. + /// This is useful for limiting the damage of orcale attacks. + /// + /// Value is UI USD value, for example value 100 -> $100 + pub total_asset_value_init_limit: u64, + + /// Time window in seconds for the oracle price feed to be considered live. + pub oracle_max_age: u16, + + pub _padding: [u16; 19], // 16 * 4 = 64 bytes +} + +impl Default for BankConfig { + fn default() -> Self { + Self { + asset_weight_init: I80F48::ZERO.into(), + asset_weight_maint: I80F48::ZERO.into(), + liability_weight_init: I80F48::ONE.into(), + liability_weight_maint: I80F48::ONE.into(), + deposit_limit: 0, + borrow_limit: 0, + interest_rate_config: Default::default(), + operational_state: BankOperationalState::Paused, + oracle_setup: OracleSetup::None, + oracle_keys: [Pubkey::default(); MAX_ORACLE_KEYS], + risk_tier: RiskTier::Isolated, + total_asset_value_init_limit: TOTAL_ASSET_VALUE_INIT_LIMIT_INACTIVE, + oracle_max_age: 0, + _padding: [0; 19], + } + } +} + +assert_struct_size!(MarginfiAccount, 2304); +assert_struct_align!(MarginfiAccount, 8); +#[repr(C)] +#[derive(Debug)] +pub struct MarginfiAccount { + pub group: Pubkey, // 32 + pub authority: Pubkey, // 32 + pub lending_account: LendingAccount, // 1728 + /// The flags that indicate the state of the account. + /// This is u64 bitfield, where each bit represents a flag. + /// + /// Flags: + /// - DISABLED_FLAG = 1 << 0 = 1 - This flag indicates that the account is disabled, + /// and no further actions can be taken on it. + pub account_flags: u64, // 8 + pub _padding: [u64; 63], // 8 * 63 = 512 +} + +const MAX_LENDING_ACCOUNT_BALANCES: usize = 16; + +assert_struct_size!(LendingAccount, 1728); +assert_struct_align!(LendingAccount, 8); +#[repr(C)] +#[derive(Debug)] +pub struct LendingAccount { + pub balances: [Balance; MAX_LENDING_ACCOUNT_BALANCES], // 104 * 16 = 1664 + pub _padding: [u64; 8], // 8 * 8 = 64 +} + +impl LendingAccount { + pub fn get_first_empty_balance(&self) -> Option { + self.balances.iter().position(|b| !b.active) + } +} + +impl LendingAccount { + pub fn get_balance(&self, bank_pk: &Pubkey) -> Option<&Balance> { + self.balances + .iter() + .find(|balance| balance.active && balance.bank_pk.eq(bank_pk)) + } +} + +assert_struct_size!(Balance, 104); +assert_struct_align!(Balance, 8); +#[repr(C)] +#[derive(Debug)] +pub struct Balance { + pub active: bool, + pub bank_pk: Pubkey, + pub asset_shares: WrappedI80F48, + pub liability_shares: WrappedI80F48, + pub emissions_outstanding: WrappedI80F48, + pub last_update: u64, + pub _padding: [u64; 1], +} diff --git a/src/vendor/mod.rs b/src/vendor/mod.rs new file mode 100644 index 0000000..086121a --- /dev/null +++ b/src/vendor/mod.rs @@ -0,0 +1,3 @@ +/// These projects don't provide a usable Rust SDK.. +pub mod kamino; +pub mod marginfi_v2;