From 9fc8a5a56ab2c7911d37a6aa8a6832589dbff74c Mon Sep 17 00:00:00 2001 From: microwavedcola1 <89031858+microwavedcola1@users.noreply.github.com> Date: Mon, 27 Jun 2022 11:27:17 +0200 Subject: [PATCH] multiple banks (#82) * multiple banks Signed-off-by: microwavedcola1 * Fixes from review Signed-off-by: microwavedcola1 * test for closing multiple banks for a registered token Signed-off-by: microwavedcola1 * fix deregister_token * update idl Signed-off-by: microwavedcola1 * Fixes from review Signed-off-by: microwavedcola1 Co-authored-by: Christian Kamm --- Cargo.toml | 2 +- client/src/client.rs | 45 ++- keeper/src/crank.rs | 54 ++- keeper/src/taker.rs | 17 +- liquidator/src/liquidate.rs | 7 +- .../mango-v4/src/instructions/flash_loan.rs | 9 +- programs/mango-v4/src/instructions/mod.rs | 2 + .../src/instructions/token_add_bank.rs | 118 ++++++ .../src/instructions/token_deregister.rs | 90 +++-- .../src/instructions/token_register.rs | 23 +- .../mango-v4/src/instructions/update_index.rs | 90 ++++- programs/mango-v4/src/lib.rs | 18 +- programs/mango-v4/src/state/bank.rs | 142 +++++-- programs/mango-v4/src/state/mint_info.rs | 23 +- programs/mango-v4/src/state/oracle.rs | 2 +- programs/mango-v4/src/state/oracle_config.rs | 1 + .../tests/program_test/mango_client.rs | 185 +++++++-- .../tests/program_test/mango_setup.rs | 17 + programs/mango-v4/tests/test_basic.rs | 22 +- programs/mango-v4/tests/test_update_index.rs | 10 +- ts/client/src/accounts/bank.ts | 19 +- ts/client/src/client.ts | 15 +- ts/client/src/mango_v4.ts | 382 +++++++++++++++++- 23 files changed, 1092 insertions(+), 201 deletions(-) create mode 100644 programs/mango-v4/src/instructions/token_add_bank.rs diff --git a/Cargo.toml b/Cargo.toml index 371aad536..f09a70deb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,4 @@ members = [ [patch.crates-io] # for gzip encoded responses -jsonrpc-core-client = { git = "https://github.com/ckamm/jsonrpc.git", branch = "ckamm/http-with-gzip" } +jsonrpc-core-client = { git = "https://github.com/ckamm/jsonrpc.git", branch = "ckamm/http-with-gzip" } \ No newline at end of file diff --git a/client/src/client.rs b/client/src/client.rs index 6bc0e1b1f..349dbce04 100644 --- a/client/src/client.rs +++ b/client/src/client.rs @@ -36,8 +36,8 @@ pub struct MangoClient { pub group: Pubkey, // TODO: future: this may not scale if there's thousands of mints, probably some function // wrapping getMultipleAccounts is needed (or bettew: we provide this data as a service) - pub banks_cache: HashMap, - pub banks_cache_by_token_index: HashMap, + pub banks_cache: HashMap>, + pub banks_cache_by_token_index: HashMap>, pub mint_infos_cache: HashMap, pub mint_infos_cache_by_token_index: HashMap, pub serum3_markets_cache: HashMap, @@ -160,8 +160,14 @@ impl MangoClient { encoding: None, })])?; for (k, v) in bank_tuples { - banks_cache.insert(v.name().to_owned(), (k, v)); - banks_cache_by_token_index.insert(v.token_index, (k, v)); + banks_cache + .entry(v.name().to_owned()) + .or_insert_with(|| Vec::new()) + .push((k, v)); + banks_cache_by_token_index + .entry(v.token_index) + .or_insert_with(|| Vec::new()) + .push((k, v)); } // mintinfo cache @@ -243,6 +249,13 @@ impl MangoClient { }) } + pub fn get_mint_info(&self, token_index: &TokenIndex) -> Pubkey { + self.mint_infos_cache_by_token_index + .get(token_index) + .unwrap() + .0 + } + pub fn client(&self) -> Client { Client::new_with_options( self.cluster.clone(), @@ -302,7 +315,7 @@ impl MangoClient { // let addresses = mango_v4::address_lookup_table::addresses(&lookup_table); // banks.push(addresses[mint_info.address_lookup_table_bank_index as usize]); // oracles.push(addresses[mint_info.address_lookup_table_oracle_index as usize]); - banks.push(mint_info.bank); + banks.push(mint_info.first_bank()); oracles.push(mint_info.oracle); } if let Some(affected_bank) = affected_bank { @@ -387,7 +400,7 @@ impl MangoClient { // let addresses = mango_v4::address_lookup_table::addresses(&lookup_table); // banks.push(addresses[mint_info.address_lookup_table_bank_index as usize]); // oracles.push(addresses[mint_info.address_lookup_table_oracle_index as usize]); - banks.push((mint_info.bank, writable_bank)); + banks.push((mint_info.first_bank(), writable_bank)); oracles.push(mint_info.oracle); } @@ -431,7 +444,7 @@ impl MangoClient { token_name: &str, amount: u64, ) -> Result { - let bank = self.banks_cache.get(token_name).unwrap(); + let bank = self.banks_cache.get(token_name).unwrap().get(0).unwrap(); let mint_info: MintInfo = self.mint_infos_cache.get(&bank.1.mint).unwrap().1; let health_check_metas = @@ -471,7 +484,7 @@ impl MangoClient { &self, token_name: &str, ) -> Result { - let bank = self.banks_cache.get(token_name).unwrap().1; + let bank = self.banks_cache.get(token_name).unwrap().get(0).unwrap().1; let data = self .program() @@ -652,10 +665,10 @@ impl MangoClient { group: self.group(), account: self.mango_account_cache.0, open_orders, - quote_bank: quote_info.bank, - quote_vault: quote_info.vault, - base_bank: base_info.bank, - base_vault: base_info.vault, + quote_bank: quote_info.first_bank(), + quote_vault: quote_info.first_vault(), + base_bank: base_info.first_bank(), + base_vault: base_info.first_vault(), serum_market: serum3_market.0, serum_program: serum3_market.1.serum_program, serum_market_external: serum3_market.1.serum_market_external, @@ -730,10 +743,10 @@ impl MangoClient { group: self.group(), account: self.mango_account_cache.0, open_orders, - quote_bank: quote_info.bank, - quote_vault: quote_info.vault, - base_bank: base_info.bank, - base_vault: base_info.vault, + quote_bank: quote_info.first_bank(), + quote_vault: quote_info.first_vault(), + base_bank: base_info.first_bank(), + base_vault: base_info.first_vault(), serum_market: serum3_market.0, serum_program: serum3_market.1.serum_program, serum_market_external: serum3_market.1.serum_market_external, diff --git a/keeper/src/crank.rs b/keeper/src/crank.rs index 7f41dedd8..69ddd69e0 100644 --- a/keeper/src/crank.rs +++ b/keeper/src/crank.rs @@ -4,7 +4,7 @@ use crate::MangoClient; use anchor_lang::__private::bytemuck::cast_ref; use futures::Future; -use mango_v4::state::{Bank, EventQueue, EventType, FillEvent, OutEvent, PerpMarket}; +use mango_v4::state::{EventQueue, EventType, FillEvent, OutEvent, PerpMarket, TokenIndex}; use solana_sdk::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, @@ -18,7 +18,12 @@ pub async fn runner( let handles1 = mango_client .banks_cache .values() - .map(|(pk, bank)| loop_update_index(mango_client.clone(), *pk, *bank)) + .map(|banks_for_a_token| { + loop_update_index( + mango_client.clone(), + banks_for_a_token.get(0).unwrap().1.token_index, + ) + }) .collect::>(); let handles2 = mango_client @@ -43,31 +48,54 @@ pub async fn runner( Ok(()) } -pub async fn loop_update_index(mango_client: Arc, pk: Pubkey, bank: Bank) { +pub async fn loop_update_index(mango_client: Arc, token_index: TokenIndex) { let mut interval = time::interval(Duration::from_secs(5)); loop { interval.tick().await; let client = mango_client.clone(); + let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> { + let mint_info = client.get_mint_info(&token_index); + let banks_for_a_token = client.banks_cache_by_token_index.get(&token_index).unwrap(); + let token_name = banks_for_a_token.get(0).unwrap().1.name(); + + let bank_pubkeys_for_a_token = banks_for_a_token + .into_iter() + .map(|bank| bank.0) + .collect::>(); + let sig_result = client .program() .request() - .instruction(Instruction { - program_id: mango_v4::id(), - accounts: anchor_lang::ToAccountMetas::to_account_metas( - &mango_v4::accounts::UpdateIndex { bank: pk }, - None, - ), - data: anchor_lang::InstructionData::data( - &mango_v4::instruction::UpdateIndex {}, - ), + .instruction({ + let mut ix = Instruction { + program_id: mango_v4::id(), + accounts: anchor_lang::ToAccountMetas::to_account_metas( + &mango_v4::accounts::UpdateIndex { mint_info }, + None, + ), + data: anchor_lang::InstructionData::data( + &mango_v4::instruction::UpdateIndex {}, + ), + }; + let mut foo = bank_pubkeys_for_a_token + .iter() + .map(|bank_pubkey| AccountMeta { + pubkey: *bank_pubkey, + is_signer: false, + is_writable: false, + }) + .collect::>(); + ix.accounts.append(&mut foo); + ix }) .send(); + if let Err(e) = sig_result { log::error!("{:?}", e) } else { - log::info!("update_index {} {:?}", bank.name(), sig_result.unwrap()) + log::info!("update_index {} {:?}", token_name, sig_result.unwrap()) } Ok(()) diff --git a/keeper/src/taker.rs b/keeper/src/taker.rs index b6a00c961..0c8faf35f 100644 --- a/keeper/src/taker.rs +++ b/keeper/src/taker.rs @@ -6,7 +6,10 @@ use std::{ use fixed::types::I80F48; use futures::Future; -use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; +use mango_v4::{ + instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}, + state::Bank, +}; use tokio::time; @@ -86,7 +89,13 @@ fn ensure_oo(mango_client: &Arc) -> Result<(), anyhow::Error> { fn ensure_deposit(mango_client: &Arc) -> Result<(), anyhow::Error> { let mango_account = mango_client.get_account()?.1; - for (_, bank) in mango_client.banks_cache.values() { + let banks: Vec = mango_client + .banks_cache + .values() + .map(|vec| vec.get(0).unwrap().1) + .collect::>(); + + for bank in banks { let mint = &mango_client.mint_infos_cache.get(&bank.mint).unwrap().2; let desired_balance = I80F48::from_num(10_000 * 10u64.pow(mint.decimals as u32)); @@ -94,9 +103,9 @@ fn ensure_deposit(mango_client: &Arc) -> Result<(), anyhow::Error> let deposit_native = match token_account_opt { Some(token_account) => { - let native = token_account.native(bank); + let native = token_account.native(&bank); - let ui = token_account.ui(bank, mint); + let ui = token_account.ui(&bank, mint); log::info!("Current balance {} {}", ui, bank.name()); if native < I80F48::ZERO { diff --git a/liquidator/src/liquidate.rs b/liquidator/src/liquidate.rs index 918543483..de8c6d3b4 100644 --- a/liquidator/src/liquidate.rs +++ b/liquidator/src/liquidate.rs @@ -53,9 +53,9 @@ pub fn compute_health_( .unwrap(); banks.push(( - mint_info.bank, + mint_info.first_bank(), chain_data - .account(&mint_info.bank) + .account(&mint_info.first_bank()) .expect("chain data is missing bank"), )); oracles.push(( @@ -150,7 +150,8 @@ pub fn process_accounts<'a>( let mint_info_pk = mint_infos.get(&token.token_index).expect("always Ok"); let mint_info = load_mango_account_from_chain::(chain_data, mint_info_pk)?; - let bank = load_mango_account_from_chain::(chain_data, &mint_info.bank)?; + let bank = + load_mango_account_from_chain::(chain_data, &mint_info.first_bank())?; let oracle = chain_data.account(&mint_info.oracle)?; let price = oracle_price( &KeyedAccountSharedData::new(mint_info.oracle, oracle.clone()), diff --git a/programs/mango-v4/src/instructions/flash_loan.rs b/programs/mango-v4/src/instructions/flash_loan.rs index c738aa500..006bd9e67 100644 --- a/programs/mango-v4/src/instructions/flash_loan.rs +++ b/programs/mango-v4/src/instructions/flash_loan.rs @@ -209,7 +209,11 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>( am.is_signer = true; // this is the data we'll need later to build the PDA account signer seeds - bank_signer_data.push((bank.token_index.to_le_bytes(), [bank.bump])); + bank_signer_data.push(( + bank.token_index.to_le_bytes(), + bank.bank_num.to_le_bytes(), + [bank.bump], + )); } } } @@ -247,11 +251,12 @@ pub fn flash_loan<'key, 'accounts, 'remaining, 'info>( let group_key = ctx.accounts.group.key(); let signers = bank_signer_data .iter() - .map(|(token_index, bump)| { + .map(|(token_index, bank_num, bump)| { [ group_key.as_ref(), b"Bank".as_ref(), &token_index[..], + &bank_num[..], &bump[..], ] }) diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index 79edbb7df..ae6d6a458 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -27,6 +27,7 @@ pub use serum3_place_order::*; pub use serum3_register_market::*; pub use serum3_settle_funds::*; pub use set_stub_oracle::*; +pub use token_add_bank::*; pub use token_deposit::*; pub use token_deregister::*; pub use token_register::*; @@ -62,6 +63,7 @@ mod serum3_place_order; mod serum3_register_market; mod serum3_settle_funds; mod set_stub_oracle; +mod token_add_bank; mod token_deposit; mod token_deregister; mod token_register; diff --git a/programs/mango-v4/src/instructions/token_add_bank.rs b/programs/mango-v4/src/instructions/token_add_bank.rs new file mode 100644 index 000000000..a0e916460 --- /dev/null +++ b/programs/mango-v4/src/instructions/token_add_bank.rs @@ -0,0 +1,118 @@ +use anchor_lang::prelude::*; +use anchor_spl::token::{Mint, Token, TokenAccount}; + +// TODO: ALTs are unavailable +//use crate::address_lookup_table; + +use crate::state::*; + +#[derive(Accounts)] +#[instruction(token_index: TokenIndex, bank_num: u64)] +pub struct TokenAddBank<'info> { + #[account( + has_one = admin, + )] + pub group: AccountLoader<'info, Group>, + pub admin: Signer<'info>, + + pub mint: Account<'info, Mint>, + + #[account( + constraint = existing_bank.load()?.token_index == token_index, + has_one = group, + has_one = mint, + )] + pub existing_bank: AccountLoader<'info, Bank>, + + #[account( + init, + // using the token_index in this seed guards against reusing it + seeds = [group.key().as_ref(), b"Bank".as_ref(), &token_index.to_le_bytes(), &bank_num.to_le_bytes()], + bump, + payer = payer, + space = 8 + std::mem::size_of::(), + )] + pub bank: AccountLoader<'info, Bank>, + + #[account( + init, + seeds = [group.key().as_ref(), b"Vault".as_ref(), &token_index.to_le_bytes(), &bank_num.to_le_bytes()], + bump, + token::authority = group, + token::mint = mint, + payer = payer + )] + pub vault: Account<'info, TokenAccount>, + + #[account( + mut, + seeds = [group.key().as_ref(), b"MintInfo".as_ref(), mint.key().as_ref()], + bump + )] + pub mint_info: AccountLoader<'info, MintInfo>, + + // Creating an address lookup table needs a recent valid slot as an + // input argument. That makes creating ALTs from governance instructions + // impossible. Hence the ALT that this instruction uses must be created + // externally and the admin is responsible for placing banks/oracles into + // sensible address lookup tables. + // constraint: must be created, have the admin authority and have free space + // TODO: ALTs are unavailable + //#[account(mut)] + //pub address_lookup_table: UncheckedAccount<'info>, // TODO: wrapper? + #[account(mut)] + pub payer: Signer<'info>, + + pub token_program: Program<'info, Token>, + pub system_program: Program<'info, System>, + + // TODO: ALTs are unavailable + //pub address_lookup_table_program: UncheckedAccount<'info>, // TODO: force address? + pub rent: Sysvar<'info, Rent>, +} + +// TODO: should this be "configure_mint", we pass an explicit index, and allow +// overwriting config as long as the mint account stays the same? +#[allow(clippy::too_many_arguments)] +pub fn token_add_bank( + ctx: Context, + _token_index: TokenIndex, + bank_num: u64, +) -> Result<()> { + // TODO: Error if mint is already configured (technically, init of vault will fail) + + let existing_bank = ctx.accounts.existing_bank.load()?; + let mut bank = ctx.accounts.bank.load_init()?; + *bank = Bank::from_existing_bank(&existing_bank, ctx.accounts.vault.key(), bank_num); + + // TODO: ALTs are unavailable + // let alt_previous_size = + // address_lookup_table::addresses(&ctx.accounts.address_lookup_table.try_borrow_data()?) + // .len(); + // let address_lookup_table = Pubkey::default(); + // let alt_previous_size = 0; + + let mut mint_info = ctx.accounts.mint_info.load_mut()?; + let free_slot = mint_info + .banks + .iter() + .position(|bank| bank == &Pubkey::default()) + .unwrap(); + require_eq!(bank_num as usize, free_slot); + mint_info.banks[free_slot] = ctx.accounts.bank.key(); + mint_info.vaults[free_slot] = ctx.accounts.vault.key(); + + // TODO: ALTs are unavailable + /* + address_lookup_table::extend( + ctx.accounts.address_lookup_table.to_account_info(), + // TODO: is using the admin as ALT authority a good idea? + ctx.accounts.admin.to_account_info(), + ctx.accounts.payer.to_account_info(), + &[], + vec![ctx.accounts.bank.key(), ctx.accounts.oracle.key()], + )?; + */ + + Ok(()) +} diff --git a/programs/mango-v4/src/instructions/token_deregister.rs b/programs/mango-v4/src/instructions/token_deregister.rs index 23afa43ee..d69fa2ee1 100644 --- a/programs/mango-v4/src/instructions/token_deregister.rs +++ b/programs/mango-v4/src/instructions/token_deregister.rs @@ -1,9 +1,11 @@ use anchor_lang::prelude::*; -use anchor_spl::token::{self, CloseAccount, Token, TokenAccount}; +use anchor_spl::token::{self, CloseAccount, Token}; -use crate::state::*; +use crate::{accounts_zerocopy::LoadZeroCopyRef, state::*}; +use anchor_lang::AccountsClose; #[derive(Accounts)] +#[instruction(token_index: TokenIndex)] pub struct TokenDeregister<'info> { #[account( constraint = group.load()?.testing == 1, @@ -12,23 +14,11 @@ pub struct TokenDeregister<'info> { pub group: AccountLoader<'info, Group>, pub admin: Signer<'info>, - // match bank to group - // match bank to vault - #[account( - mut, - has_one = group, - has_one = vault, - close = sol_destination - )] - pub bank: AccountLoader<'info, Bank>, - - #[account(mut)] - pub vault: Account<'info, TokenAccount>, - // match mint info to bank #[account( mut, - has_one = bank, + has_one = group, + constraint = mint_info.load()?.token_index == token_index, close = sol_destination )] pub mint_info: AccountLoader<'info, MintInfo>, @@ -41,21 +31,63 @@ pub struct TokenDeregister<'info> { } #[allow(clippy::too_many_arguments)] -pub fn token_deregister(ctx: Context) -> Result<()> { +pub fn token_deregister<'key, 'accounts, 'remaining, 'info>( + ctx: Context<'key, 'accounts, 'remaining, 'info, TokenDeregister<'info>>, + token_index: TokenIndex, +) -> Result<()> { + let mint_info = ctx.accounts.mint_info.load()?; + { + let total_banks = mint_info + .banks + .iter() + .filter(|bank| *bank != &Pubkey::default()) + .count(); + require_eq!(total_banks * 2, ctx.remaining_accounts.len()); + } + let group = ctx.accounts.group.load()?; let group_seeds = group_seeds!(group); - let cpi_accounts = CloseAccount { - account: ctx.accounts.vault.to_account_info(), - destination: ctx.accounts.sol_destination.to_account_info(), - authority: ctx.accounts.group.to_account_info(), - }; - let cpi_program = ctx.accounts.token_program.to_account_info(); - token::close_account(CpiContext::new_with_signer( - cpi_program, - cpi_accounts, - &[group_seeds], - ))?; - ctx.accounts.vault.exit(ctx.program_id)?; + + // todo: use itertools::chunks(2) + for i in (0..ctx.remaining_accounts.len()).step_by(2) { + let vault_ai = &ctx.remaining_accounts[i + 1]; + let bank_ai = &ctx.remaining_accounts[i]; + + require_eq!(bank_ai.key(), mint_info.banks[i / 2]); + require_eq!(vault_ai.key(), mint_info.vaults[i / 2]); + + // todo: these checks might be superfluous, after above 2 checks + { + let bank = bank_ai.load::()?; + require_keys_eq!(bank.group, ctx.accounts.group.key()); + require_eq!(bank.token_index, token_index); + require_keys_eq!(bank.vault, vault_ai.key()); + } + + // note: vault seems to need closing before bank, weird solana oddity + // todo: add test to see if we can even close more than one bank in same ix + // close vault + let cpi_accounts = CloseAccount { + account: vault_ai.to_account_info(), + destination: ctx.accounts.sol_destination.to_account_info(), + authority: ctx.accounts.group.to_account_info(), + }; + let cpi_program = ctx.accounts.token_program.to_account_info(); + token::close_account(CpiContext::new_with_signer( + cpi_program, + cpi_accounts, + &[group_seeds], + ))?; + vault_ai.exit(ctx.program_id)?; + } + + // Close banks in a second step, because the cpi calls above don't like when someone + // else touches sol_destination.lamports in between. + for i in (0..ctx.remaining_accounts.len()).step_by(2) { + let bank_ai = &ctx.remaining_accounts[i]; + let bank_al: AccountLoader = AccountLoader::try_from(bank_ai)?; + bank_al.close(ctx.accounts.sol_destination.to_account_info())?; + } Ok(()) } diff --git a/programs/mango-v4/src/instructions/token_register.rs b/programs/mango-v4/src/instructions/token_register.rs index 4b6299211..7513c1297 100644 --- a/programs/mango-v4/src/instructions/token_register.rs +++ b/programs/mango-v4/src/instructions/token_register.rs @@ -9,10 +9,10 @@ use crate::error::*; use crate::state::*; use crate::util::fill16_from_str; -const INDEX_START: I80F48 = I80F48!(1_000_000); +pub const INDEX_START: I80F48 = I80F48!(1_000_000); #[derive(Accounts)] -#[instruction(token_index: TokenIndex)] +#[instruction(token_index: TokenIndex, bank_num: u64)] pub struct TokenRegister<'info> { #[account( has_one = admin, @@ -25,7 +25,7 @@ pub struct TokenRegister<'info> { #[account( init, // using the token_index in this seed guards against reusing it - seeds = [group.key().as_ref(), b"Bank".as_ref(), &token_index.to_le_bytes()], + seeds = [group.key().as_ref(), b"Bank".as_ref(), &token_index.to_le_bytes(), &bank_num.to_le_bytes()], bump, payer = payer, space = 8 + std::mem::size_of::(), @@ -34,7 +34,7 @@ pub struct TokenRegister<'info> { #[account( init, - seeds = [group.key().as_ref(), b"Vault".as_ref(), &token_index.to_le_bytes()], + seeds = [group.key().as_ref(), b"Vault".as_ref(), &token_index.to_le_bytes(), &bank_num.to_le_bytes()], bump, token::authority = group, token::mint = mint, @@ -90,6 +90,7 @@ pub struct InterestRateParams { pub fn token_register( ctx: Context, token_index: TokenIndex, + bank_num: u64, name: String, oracle_config: OracleConfig, interest_rate_params: InterestRateParams, @@ -103,6 +104,8 @@ pub fn token_register( ) -> Result<()> { // TODO: Error if mint is already configured (technically, init of vault will fail) + require_eq!(bank_num, 0); + let mut bank = ctx.accounts.bank.load_init()?; *bank = Bank { name: fill16_from_str(name)?, @@ -115,6 +118,8 @@ pub fn token_register( borrow_index: INDEX_START, indexed_total_deposits: I80F48::ZERO, indexed_total_borrows: I80F48::ZERO, + indexed_deposits: I80F48::ZERO, + indexed_borrows: I80F48::ZERO, last_updated: Clock::get()?.unix_timestamp, // TODO: add a require! verifying relation between the parameters util0: I80F48::from_num(interest_rate_params.util0), @@ -133,8 +138,9 @@ pub fn token_register( dust: I80F48::ZERO, token_index, bump: *ctx.bumps.get("bank").ok_or(MangoError::SomeError)?, - reserved: Default::default(), mint_decimals: ctx.accounts.mint.decimals, + bank_num: 0, + reserved: Default::default(), }; // TODO: ALTs are unavailable @@ -147,8 +153,8 @@ pub fn token_register( *mint_info = MintInfo { group: ctx.accounts.group.key(), mint: ctx.accounts.mint.key(), - bank: ctx.accounts.bank.key(), - vault: ctx.accounts.vault.key(), + banks: Default::default(), + vaults: Default::default(), oracle: ctx.accounts.oracle.key(), address_lookup_table, token_index, @@ -157,6 +163,9 @@ pub fn token_register( reserved: Default::default(), }; + mint_info.banks[0] = ctx.accounts.bank.key(); + mint_info.vaults[0] = ctx.accounts.vault.key(); + // TODO: ALTs are unavailable /* address_lookup_table::extend( diff --git a/programs/mango-v4/src/instructions/update_index.rs b/programs/mango-v4/src/instructions/update_index.rs index 6c16a89ac..83901f9c3 100644 --- a/programs/mango-v4/src/instructions/update_index.rs +++ b/programs/mango-v4/src/instructions/update_index.rs @@ -1,20 +1,88 @@ use anchor_lang::prelude::*; -use crate::state::Bank; - +use crate::{ + accounts_zerocopy::{LoadMutZeroCopyRef, LoadZeroCopyRef}, + error::MangoError, + state::{Bank, MintInfo}, +}; +use checked_math as cm; +use fixed::types::I80F48; #[derive(Accounts)] pub struct UpdateIndex<'info> { - // TODO: should we support arbitrary number of banks with remaining accounts? - // ix - consumed 17641 of 101000 compute units, so we have a lot of compute - #[account(mut)] - pub bank: AccountLoader<'info, Bank>, + pub mint_info: AccountLoader<'info, MintInfo>, } -pub fn update_index(ctx: Context) -> Result<()> { - // TODO: should we enforce a minimum window between 2 update_index ix calls? - let now_ts = Clock::get()?.unix_timestamp; - let mut bank = ctx.accounts.bank.load_mut()?; - bank.update_index(now_ts)?; +pub fn update_index(ctx: Context) -> Result<()> { + let mint_info = ctx.accounts.mint_info.load()?; + + let total_banks = mint_info + .banks + .iter() + .filter(|bank| *bank != &Pubkey::default()) + .count(); + + require_eq!(total_banks, ctx.remaining_accounts.len()); + let all_banks = ctx.remaining_accounts; + check_banks(all_banks, &mint_info)?; + + let mut indexed_total_deposits = I80F48::ZERO; + let mut indexed_total_borrows = I80F48::ZERO; + for ai in all_banks.iter() { + let bank = ai.load::()?; + indexed_total_deposits = cm!(indexed_total_deposits + bank.indexed_deposits); + indexed_total_borrows = cm!(indexed_total_borrows + bank.indexed_borrows); + } + + let now_ts = Clock::get()?.unix_timestamp; + let (diff_ts, deposit_index, borrow_index) = { + let mut some_bank = all_banks[0].load_mut::()?; + + // TODO: should we enforce a minimum window between 2 update_index ix calls? + let diff_ts = I80F48::from_num(now_ts - some_bank.last_updated); + + let (deposit_index, borrow_index) = + some_bank.compute_index(indexed_total_deposits, indexed_total_borrows, diff_ts)?; + + (diff_ts, deposit_index, borrow_index) + }; + + msg!("indexed_total_deposits {}", indexed_total_deposits); + msg!("indexed_total_borrows {}", indexed_total_borrows); + msg!("diff_ts {}", diff_ts); + msg!("deposit_index {}", deposit_index); + msg!("borrow_index {}", borrow_index); + + for ai in all_banks.iter() { + let mut bank = ai.load_mut::()?; + + bank.indexed_total_deposits = indexed_total_deposits; + bank.indexed_total_borrows = indexed_total_borrows; + + bank.last_updated = now_ts; + bank.charge_loan_fee(diff_ts); + + bank.deposit_index = deposit_index; + bank.borrow_index = borrow_index; + } + + Ok(()) +} + +fn check_banks(all_banks: &[AccountInfo], mint_info: &MintInfo) -> Result<()> { + for (idx, ai) in all_banks.iter().enumerate() { + match ai.load::() { + Ok(bank) => { + if mint_info.token_index != bank.token_index + || mint_info.group != bank.group + // todo: just below check should be enough, above 2 checks are superfluous and defensive + || mint_info.banks[idx] != ai.key() + { + return Err(error!(MangoError::SomeError)); + } + } + Err(error) => return Err(error), + } + } Ok(()) } diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index cb571ff48..35a792f0b 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -42,6 +42,7 @@ pub mod mango_v4 { pub fn token_register( ctx: Context, token_index: TokenIndex, + bank_num: u64, name: String, oracle_config: OracleConfig, interest_rate_params: InterestRateParams, @@ -56,6 +57,7 @@ pub mod mango_v4 { instructions::token_register( ctx, token_index, + bank_num, name, oracle_config, interest_rate_params, @@ -69,8 +71,20 @@ pub mod mango_v4 { ) } - pub fn token_deregister(ctx: Context) -> Result<()> { - instructions::token_deregister(ctx) + #[allow(clippy::too_many_arguments)] + pub fn token_add_bank( + ctx: Context, + token_index: TokenIndex, + bank_num: u64, + ) -> Result<()> { + instructions::token_add_bank(ctx, token_index, bank_num) + } + + pub fn token_deregister<'key, 'accounts, 'remaining, 'info>( + ctx: Context<'key, 'accounts, 'remaining, 'info, TokenDeregister<'info>>, + token_index: TokenIndex, + ) -> Result<()> { + instructions::token_deregister(ctx, token_index) } pub fn update_index(ctx: Context) -> Result<()> { diff --git a/programs/mango-v4/src/state/bank.rs b/programs/mango-v4/src/state/bank.rs index 27395b46d..accdd95f6 100644 --- a/programs/mango-v4/src/state/bank.rs +++ b/programs/mango-v4/src/state/bank.rs @@ -5,6 +5,7 @@ use anchor_lang::prelude::*; use fixed::types::I80F48; use fixed_macro::types::I80F48; use static_assertions::const_assert_eq; + use std::mem::size_of; pub const DAY: I80F48 = I80F48!(86400); @@ -30,6 +31,10 @@ pub struct Bank { pub indexed_total_deposits: I80F48, pub indexed_total_borrows: I80F48, + /// deposits/borrows for this bank + pub indexed_deposits: I80F48, + pub indexed_borrows: I80F48, + pub last_updated: i64, pub util0: I80F48, pub rate0: I80F48, @@ -66,10 +71,15 @@ pub struct Bank { pub mint_decimals: u8, pub reserved: [u8; 4], + + pub bank_num: u64, // TODO: add space for an oracle which services interest rate for the bank's mint // interest rate tied to oracle might help reduce spreads between deposits and borrows } -const_assert_eq!(size_of::(), 16 + 32 * 4 + 8 + 16 * 19 + 2 + 1 + 1 + 4); +const_assert_eq!( + size_of::(), + 16 + 32 * 4 + 8 + 16 * 21 + 2 + 1 + 1 + 4 + 8 +); const_assert_eq!(size_of::() % 8, 0); impl std::fmt::Debug for Bank { @@ -80,10 +90,13 @@ impl std::fmt::Debug for Bank { .field("mint", &self.mint) .field("vault", &self.vault) .field("oracle", &self.oracle) + .field("oracle_config", &self.oracle_config) .field("deposit_index", &self.deposit_index) .field("borrow_index", &self.borrow_index) .field("indexed_total_deposits", &self.indexed_total_deposits) .field("indexed_total_borrows", &self.indexed_total_borrows) + .field("indexed_deposits", &self.indexed_deposits) + .field("indexed_borrows", &self.indexed_borrows) .field("last_updated", &self.last_updated) .field("util0", &self.util0) .field("rate0", &self.rate0) @@ -106,6 +119,43 @@ impl std::fmt::Debug for Bank { } impl Bank { + pub fn from_existing_bank(existing_bank: &Bank, vault: Pubkey, bank_num: u64) -> Self { + Self { + name: existing_bank.name, + group: existing_bank.group, + mint: existing_bank.mint, + vault: vault, + oracle: existing_bank.oracle, + oracle_config: existing_bank.oracle_config, + deposit_index: existing_bank.deposit_index, + borrow_index: existing_bank.borrow_index, + indexed_total_deposits: existing_bank.indexed_total_deposits, + indexed_total_borrows: existing_bank.indexed_total_borrows, + indexed_deposits: I80F48::ZERO, + indexed_borrows: I80F48::ZERO, + last_updated: existing_bank.last_updated, + util0: existing_bank.util0, + rate0: existing_bank.rate0, + util1: existing_bank.util1, + rate1: existing_bank.rate1, + max_rate: existing_bank.max_rate, + collected_fees_native: existing_bank.collected_fees_native, + loan_origination_fee_rate: existing_bank.loan_origination_fee_rate, + loan_fee_rate: existing_bank.loan_fee_rate, + maint_asset_weight: existing_bank.maint_asset_weight, + init_asset_weight: existing_bank.init_asset_weight, + maint_liab_weight: existing_bank.maint_liab_weight, + init_liab_weight: existing_bank.init_liab_weight, + liquidation_fee: existing_bank.liquidation_fee, + dust: I80F48::ZERO, + token_index: existing_bank.token_index, + bump: existing_bank.bump, + mint_decimals: existing_bank.mint_decimals, + reserved: Default::default(), + bank_num, + } + } + pub fn name(&self) -> &str { std::str::from_utf8(&self.name) .unwrap() @@ -120,6 +170,14 @@ impl Bank { self.deposit_index * self.indexed_total_deposits } + pub fn native_borrows(&self) -> I80F48 { + self.borrow_index * self.indexed_borrows + } + + pub fn native_deposits(&self) -> I80F48 { + self.deposit_index * self.indexed_deposits + } + /// Returns whether the position is active /// /// native_amount must be >= 0 @@ -139,21 +197,20 @@ impl Bank { let new_indexed_value = cm!(position.indexed_position + indexed_change); if new_indexed_value.is_negative() { // pay back borrows only, leaving a negative position - self.indexed_total_borrows = cm!(self.indexed_total_borrows - indexed_change); - position.indexed_position = new_indexed_value; + let indexed_change = cm!(native_amount / self.borrow_index + I80F48::DELTA); + self.indexed_borrows = cm!(self.indexed_borrows - indexed_change); + position.indexed_position = cm!(position.indexed_position + indexed_change); return Ok(true); } else if new_native_position < I80F48::ONE && !position.is_in_use() { // if there's less than one token deposited, zero the position self.dust = cm!(self.dust + new_native_position); - self.indexed_total_borrows = - cm!(self.indexed_total_borrows + position.indexed_position); + self.indexed_borrows = cm!(self.indexed_borrows + position.indexed_position); position.indexed_position = I80F48::ZERO; return Ok(false); } // pay back all borrows - self.indexed_total_borrows = - cm!(self.indexed_total_borrows + position.indexed_position); // position.value is negative + self.indexed_borrows = cm!(self.indexed_borrows + position.indexed_position); // position.value is negative position.indexed_position = I80F48::ZERO; // deposit the rest native_amount = cm!(native_amount + native_position); @@ -164,7 +221,7 @@ impl Bank { // we want to ensure that users can withdraw the same amount they have deposited, so // (amount/index + delta)*index >= amount is a better guarantee. let indexed_change = cm!(native_amount / self.deposit_index + I80F48::DELTA); - self.indexed_total_deposits = cm!(self.indexed_total_deposits + indexed_change); + self.indexed_deposits = cm!(self.indexed_deposits + indexed_change); position.indexed_position = cm!(position.indexed_position + indexed_change); Ok(true) @@ -212,22 +269,20 @@ impl Bank { if new_native_position < I80F48::ONE && !position.is_in_use() { // zero the account collecting the leftovers in `dust` self.dust = cm!(self.dust + new_native_position); - self.indexed_total_deposits = - cm!(self.indexed_total_deposits - position.indexed_position); + self.indexed_deposits = cm!(self.indexed_deposits - position.indexed_position); position.indexed_position = I80F48::ZERO; return Ok(false); } else { // withdraw some deposits leaving a positive balance let indexed_change = cm!(native_amount / self.deposit_index); - self.indexed_total_deposits = cm!(self.indexed_total_deposits - indexed_change); + self.indexed_deposits = cm!(self.indexed_deposits - indexed_change); position.indexed_position = cm!(position.indexed_position - indexed_change); return Ok(true); } } // withdraw all deposits - self.indexed_total_deposits = - cm!(self.indexed_total_deposits - position.indexed_position); + self.indexed_deposits = cm!(self.indexed_deposits - position.indexed_position); position.indexed_position = I80F48::ZERO; // borrow the rest native_amount = -new_native_position; @@ -239,7 +294,7 @@ impl Bank { // add to borrows let indexed_change = cm!(native_amount / self.borrow_index); - self.indexed_total_borrows = cm!(self.indexed_total_borrows + indexed_change); + self.indexed_borrows = cm!(self.indexed_borrows + indexed_change); position.indexed_position = cm!(position.indexed_position - indexed_change); Ok(true) @@ -256,7 +311,7 @@ impl Bank { self.collected_fees_native = cm!(self.collected_fees_native + loan_origination_fee); let indexed_change = cm!(loan_origination_fee / self.borrow_index); - self.indexed_total_borrows = cm!(self.indexed_total_borrows + indexed_change); + self.indexed_borrows = cm!(self.indexed_borrows + indexed_change); position.indexed_position = cm!(position.indexed_position - indexed_change); Ok(()) @@ -290,26 +345,27 @@ impl Bank { // Borrows continously expose insurance fund to risk, collect fees from borrowers pub fn charge_loan_fee(&mut self, diff_ts: I80F48) { - let native_total_borrows_old = self.native_total_borrows(); - self.indexed_total_borrows = - cm!((self.indexed_total_borrows - * (I80F48::ONE + self.loan_fee_rate * (diff_ts / YEAR)))); - self.collected_fees_native = cm!( - self.collected_fees_native + self.native_total_borrows() - native_total_borrows_old - ); + let native_borrows_old = self.native_borrows(); + self.indexed_borrows = + cm!((self.indexed_borrows * (I80F48::ONE + self.loan_fee_rate * (diff_ts / YEAR)))); + self.collected_fees_native = + cm!(self.collected_fees_native + self.native_borrows() - native_borrows_old); } - pub fn update_index(&mut self, now_ts: i64) -> Result<()> { - let diff_ts = I80F48::from_num(now_ts - self.last_updated); - self.last_updated = now_ts; + pub fn compute_index( + &mut self, + indexed_total_deposits: I80F48, + indexed_total_borrows: I80F48, + diff_ts: I80F48, + ) -> Result<(I80F48, I80F48)> { + // compute index based on utilization + let native_total_deposits = self.deposit_index * indexed_total_deposits; + let native_total_borrows = self.borrow_index * indexed_total_borrows; - self.charge_loan_fee(diff_ts); - - // Update index based on utilization - let utilization = if self.native_total_deposits() == I80F48::ZERO { + let utilization = if native_total_deposits == I80F48::ZERO { I80F48::ZERO } else { - cm!(self.native_total_borrows() / self.native_total_deposits()) + cm!(native_total_borrows / native_total_deposits) }; let interest_rate = self.compute_interest_rate(utilization); @@ -317,15 +373,20 @@ impl Bank { let borrow_interest: I80F48 = cm!(interest_rate * diff_ts); let deposit_interest = cm!(borrow_interest * utilization); + // msg!("utilization {}", utilization); + // msg!("interest_rate {}", interest_rate); + // msg!("borrow_interest {}", borrow_interest); + // msg!("deposit_interest {}", deposit_interest); + if borrow_interest <= I80F48::ZERO || deposit_interest <= I80F48::ZERO { - return Ok(()); + return Ok((self.deposit_index, self.borrow_index)); } - self.borrow_index = cm!((self.borrow_index * borrow_interest) / YEAR + self.borrow_index); - self.deposit_index = + let borrow_index = cm!((self.borrow_index * borrow_interest) / YEAR + self.borrow_index); + let deposit_index = cm!((self.deposit_index * deposit_interest) / YEAR + self.deposit_index); - Ok(()) + Ok((deposit_index, borrow_index)) } /// returns the current interest rate in APR @@ -375,6 +436,7 @@ macro_rules! bank_seeds { $bank.group.as_ref(), b"Bank".as_ref(), $bank.token_index.to_le_bytes(), + &bank.bank_num.to_le_bytes(), &[$bank.bump], ] }; @@ -449,9 +511,9 @@ mod tests { account.indexed_position = indexed(I80F48::from_num(start), &bank); if start >= 0.0 { - bank.indexed_total_deposits = account.indexed_position; + bank.indexed_deposits = account.indexed_position; } else { - bank.indexed_total_borrows = -account.indexed_position; + bank.indexed_borrows = -account.indexed_position; } // get the rounded start value @@ -483,11 +545,11 @@ mod tests { assert!((account.indexed_position - expected_indexed).abs() <= epsilon); if account.indexed_position.is_positive() { - assert_eq!(bank.indexed_total_deposits, account.indexed_position); - assert_eq!(bank.indexed_total_borrows, I80F48::ZERO); + assert_eq!(bank.indexed_deposits, account.indexed_position); + assert_eq!(bank.indexed_borrows, I80F48::ZERO); } else { - assert_eq!(bank.indexed_total_deposits, I80F48::ZERO); - assert_eq!(bank.indexed_total_borrows, -account.indexed_position); + assert_eq!(bank.indexed_deposits, I80F48::ZERO); + assert_eq!(bank.indexed_borrows, -account.indexed_position); } } } diff --git a/programs/mango-v4/src/state/mint_info.rs b/programs/mango-v4/src/state/mint_info.rs index 0335f9155..91f63c5d1 100644 --- a/programs/mango-v4/src/state/mint_info.rs +++ b/programs/mango-v4/src/state/mint_info.rs @@ -4,17 +4,20 @@ use std::mem::size_of; use super::TokenIndex; +pub const MAX_BANKS: usize = 6; + // This struct describes which address lookup table can be used to pass // the accounts that are relevant for this mint. The idea is that clients // can load this account to figure out which address maps to use when calling // instructions that need banks/oracles for all active positions. #[account(zero_copy)] +#[derive(Debug)] pub struct MintInfo { // TODO: none of these pubkeys are needed, remove? pub group: Pubkey, pub mint: Pubkey, - pub bank: Pubkey, - pub vault: Pubkey, + pub banks: [Pubkey; MAX_BANKS], + pub vaults: [Pubkey; MAX_BANKS], pub oracle: Pubkey, pub address_lookup_table: Pubkey, @@ -26,5 +29,19 @@ pub struct MintInfo { pub reserved: [u8; 4], } -const_assert_eq!(size_of::(), 6 * 32 + 2 + 2 + 4); +const_assert_eq!( + size_of::(), + MAX_BANKS * 2 * 32 + 4 * 32 + 2 + 2 + 4 +); const_assert_eq!(size_of::() % 8, 0); + +impl MintInfo { + // used for health purposes + pub fn first_bank(&self) -> Pubkey { + self.banks[0] + } + + pub fn first_vault(&self) -> Pubkey { + self.vaults[0] + } +} diff --git a/programs/mango-v4/src/state/oracle.rs b/programs/mango-v4/src/state/oracle.rs index 49031e64f..b32816aca 100644 --- a/programs/mango-v4/src/state/oracle.rs +++ b/programs/mango-v4/src/state/oracle.rs @@ -54,7 +54,7 @@ pub mod switchboard_v2_mainnet_oracle { } #[zero_copy] -#[derive(AnchorDeserialize, AnchorSerialize)] +#[derive(AnchorDeserialize, AnchorSerialize, Debug)] pub struct OracleConfig { pub conf_filter: I80F48, } diff --git a/programs/mango-v4/src/state/oracle_config.rs b/programs/mango-v4/src/state/oracle_config.rs index e69de29bb..8b1378917 100644 --- a/programs/mango-v4/src/state/oracle_config.rs +++ b/programs/mango-v4/src/state/oracle_config.rs @@ -0,0 +1 @@ + diff --git a/programs/mango-v4/tests/program_test/mango_client.rs b/programs/mango-v4/tests/program_test/mango_client.rs index ea121a89d..50db8b7b2 100644 --- a/programs/mango-v4/tests/program_test/mango_client.rs +++ b/programs/mango-v4/tests/program_test/mango_client.rs @@ -96,6 +96,7 @@ async fn get_mint_info_by_token_index( account.group.as_ref(), b"Bank".as_ref(), &token_index.to_le_bytes(), + &0u64.to_le_bytes(), ], &mango_v4::id(), ) @@ -153,7 +154,7 @@ async fn derive_health_check_remaining_account_metas( // let addresses = mango_v4::address_lookup_table::addresses(&lookup_table); // banks.push(addresses[mint_info.address_lookup_table_bank_index as usize]); // oracles.push(addresses[mint_info.address_lookup_table_oracle_index as usize]); - banks.push(mint_info.bank); + banks.push(mint_info.first_bank()); oracles.push(mint_info.oracle); } @@ -212,7 +213,7 @@ async fn derive_liquidation_remaining_account_metas( // writable_bank, // )); // oracles.push(addresses[mint_info.address_lookup_table_oracle_index as usize]); - banks.push((mint_info.bank, writable_bank)); + banks.push((mint_info.first_bank(), writable_bank)); oracles.push(mint_info.oracle); } @@ -409,7 +410,7 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> { let health_check_metas = derive_health_check_remaining_account_metas( &account_loader, &account, - Some(mint_info.bank), + Some(mint_info.first_bank()), false, None, ) @@ -419,8 +420,8 @@ impl<'keypair> ClientInstruction for TokenWithdrawInstruction<'keypair> { group: account.group, account: self.account, owner: self.owner.pubkey(), - bank: mint_info.bank, - vault: mint_info.vault, + bank: mint_info.first_bank(), + vault: mint_info.first_vault(), token_account: self.token_account, token_program: Token::id(), }; @@ -473,7 +474,7 @@ impl<'keypair> ClientInstruction for TokenDepositInstruction<'keypair> { let health_check_metas = derive_health_check_remaining_account_metas( &account_loader, &account, - Some(mint_info.bank), + Some(mint_info.first_bank()), false, None, ) @@ -482,8 +483,8 @@ impl<'keypair> ClientInstruction for TokenDepositInstruction<'keypair> { let accounts = Self::Accounts { group: account.group, account: self.account, - bank: mint_info.bank, - vault: mint_info.vault, + bank: mint_info.first_bank(), + vault: mint_info.first_vault(), token_account: self.token_account, token_authority: self.token_authority.pubkey(), token_program: Token::id(), @@ -534,6 +535,7 @@ impl<'keypair> ClientInstruction for TokenRegisterInstruction<'keypair> { let instruction = Self::Instruction { name: "some_ticker".to_string(), token_index: self.token_index, + bank_num: 0, oracle_config: OracleConfig { conf_filter: I80F48::from_num::(0.10), }, @@ -558,6 +560,7 @@ impl<'keypair> ClientInstruction for TokenRegisterInstruction<'keypair> { self.group.as_ref(), b"Bank".as_ref(), &self.token_index.to_le_bytes(), + &0u64.to_le_bytes(), ], &program_id, ) @@ -567,6 +570,7 @@ impl<'keypair> ClientInstruction for TokenRegisterInstruction<'keypair> { self.group.as_ref(), b"Vault".as_ref(), &self.token_index.to_le_bytes(), + &0u64.to_le_bytes(), ], &program_id, ) @@ -618,31 +622,46 @@ impl<'keypair> ClientInstruction for TokenRegisterInstruction<'keypair> { } } -pub struct TokenDeregisterInstruction<'keypair> { - pub admin: &'keypair Keypair, - pub payer: &'keypair Keypair, - pub group: Pubkey, - pub mint: Pubkey, +pub struct TokenAddBankInstruction<'keypair> { pub token_index: TokenIndex, - pub sol_destination: Pubkey, + pub bank_num: u64, + + pub group: Pubkey, + pub admin: &'keypair Keypair, + pub mint: Pubkey, + pub address_lookup_table: Pubkey, + pub payer: &'keypair Keypair, } #[async_trait::async_trait(?Send)] -impl<'keypair> ClientInstruction for TokenDeregisterInstruction<'keypair> { - type Accounts = mango_v4::accounts::TokenDeregister; - type Instruction = mango_v4::instruction::TokenDeregister; - +impl<'keypair> ClientInstruction for TokenAddBankInstruction<'keypair> { + type Accounts = mango_v4::accounts::TokenAddBank; + type Instruction = mango_v4::instruction::TokenAddBank; async fn to_instruction( &self, - _loader: impl ClientAccountLoader + 'async_trait, - ) -> (Self::Accounts, Instruction) { + _account_loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, instruction::Instruction) { let program_id = mango_v4::id(); - let instruction = Self::Instruction {}; + let instruction = Self::Instruction { + token_index: self.token_index, + bank_num: self.bank_num, + }; + let existing_bank = Pubkey::find_program_address( + &[ + self.group.as_ref(), + b"Bank".as_ref(), + &self.token_index.to_le_bytes(), + &0u64.to_le_bytes(), + ], + &program_id, + ) + .0; let bank = Pubkey::find_program_address( &[ self.group.as_ref(), b"Bank".as_ref(), &self.token_index.to_le_bytes(), + &self.bank_num.to_le_bytes(), ], &program_id, ) @@ -652,6 +671,7 @@ impl<'keypair> ClientInstruction for TokenDeregisterInstruction<'keypair> { self.group.as_ref(), b"Vault".as_ref(), &self.token_index.to_le_bytes(), + &self.bank_num.to_le_bytes(), ], &program_id, ) @@ -667,16 +687,90 @@ impl<'keypair> ClientInstruction for TokenDeregisterInstruction<'keypair> { .0; let accounts = Self::Accounts { - admin: self.admin.pubkey(), group: self.group, + admin: self.admin.pubkey(), + mint: self.mint, + existing_bank, bank, vault, mint_info, + // TODO: ALTs are unavailable + //address_lookup_table: self.address_lookup_table, + payer: self.payer.pubkey(), + token_program: Token::id(), + system_program: System::id(), + // TODO: ALTs are unavailable + //address_lookup_table_program: mango_v4::address_lookup_table::id(), + rent: sysvar::rent::Rent::id(), + }; + + let instruction = make_instruction(program_id, &accounts, instruction); + (accounts, instruction) + } + + fn signers(&self) -> Vec<&Keypair> { + vec![self.admin, self.payer] + } +} + +pub struct TokenDeregisterInstruction<'keypair> { + pub admin: &'keypair Keypair, + pub payer: &'keypair Keypair, + pub group: Pubkey, + pub mint_info: Pubkey, + pub banks: Vec, + pub vaults: Vec, + pub token_index: TokenIndex, + pub sol_destination: Pubkey, +} +#[async_trait::async_trait(?Send)] +impl<'keypair> ClientInstruction for TokenDeregisterInstruction<'keypair> { + type Accounts = mango_v4::accounts::TokenDeregister; + type Instruction = mango_v4::instruction::TokenDeregister; + + async fn to_instruction( + &self, + _loader: impl ClientAccountLoader + 'async_trait, + ) -> (Self::Accounts, Instruction) { + let program_id = mango_v4::id(); + let instruction = Self::Instruction { + token_index: self.token_index, + }; + + let accounts = Self::Accounts { + admin: self.admin.pubkey(), + group: self.group, + mint_info: self.mint_info, sol_destination: self.sol_destination, token_program: Token::id(), }; - let instruction = make_instruction(program_id, &accounts, instruction); + let mut instruction = make_instruction(program_id, &accounts, instruction); + + let mut ams = self + .banks + .iter() + .zip(self.vaults.iter()) + .filter(|(bank, _)| **bank != Pubkey::default()) + .map(|(bank, vault)| { + vec![ + AccountMeta { + pubkey: *bank, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: *vault, + is_signer: false, + is_writable: true, + }, + ] + }) + .flat_map(|vec| vec.into_iter()) + .collect::>(); + dbg!(ams.clone()); + instruction.accounts.append(&mut ams); + (accounts, instruction) } @@ -1267,10 +1361,10 @@ impl<'keypair> ClientInstruction for Serum3PlaceOrderInstruction<'keypair> { group: account.group, account: self.account, open_orders, - quote_bank: quote_info.bank, - quote_vault: quote_info.vault, - base_bank: base_info.bank, - base_vault: base_info.vault, + quote_bank: quote_info.first_bank(), + quote_vault: quote_info.first_vault(), + base_bank: base_info.first_bank(), + base_vault: base_info.first_vault(), serum_market: self.serum_market, serum_program: serum_market.serum_program, serum_market_external: serum_market.serum_market_external, @@ -1472,10 +1566,10 @@ impl<'keypair> ClientInstruction for Serum3SettleFundsInstruction<'keypair> { group: account.group, account: self.account, open_orders, - quote_bank: quote_info.bank, - quote_vault: quote_info.vault, - base_bank: base_info.bank, - base_vault: base_info.vault, + quote_bank: quote_info.first_bank(), + quote_vault: quote_info.first_vault(), + base_bank: base_info.first_bank(), + base_vault: base_info.first_vault(), serum_market: self.serum_market, serum_program: serum_market.serum_program, serum_market_external: serum_market.serum_market_external, @@ -1558,10 +1652,10 @@ impl ClientInstruction for Serum3LiqForceCancelOrdersInstruction { group: account.group, account: self.account, open_orders, - quote_bank: quote_info.bank, - quote_vault: quote_info.vault, - base_bank: base_info.bank, - base_vault: base_info.vault, + quote_bank: quote_info.first_bank(), + quote_vault: quote_info.first_vault(), + base_bank: base_info.first_bank(), + base_vault: base_info.first_vault(), serum_market: self.serum_market, serum_program: serum_market.serum_program, serum_market_external: serum_market.serum_market_external, @@ -2037,7 +2131,8 @@ impl ClientInstruction for BenchmarkInstruction { } } pub struct UpdateIndexInstruction { - pub bank: Pubkey, + pub mint_info: Pubkey, + pub banks: Vec, } #[async_trait::async_trait(?Send)] impl ClientInstruction for UpdateIndexInstruction { @@ -2049,9 +2144,23 @@ impl ClientInstruction for UpdateIndexInstruction { ) -> (Self::Accounts, instruction::Instruction) { let program_id = mango_v4::id(); let instruction = Self::Instruction {}; - let accounts = Self::Accounts { bank: self.bank }; + let accounts = Self::Accounts { + mint_info: self.mint_info, + }; + + let mut instruction = make_instruction(program_id, &accounts, instruction); + let mut bank_ams = self + .banks + .iter() + .filter(|bank| **bank != Pubkey::default()) + .map(|bank| AccountMeta { + pubkey: *bank, + is_signer: false, + is_writable: true, + }) + .collect::>(); + instruction.accounts.append(&mut bank_ams); - let instruction = make_instruction(program_id, &accounts, instruction); (accounts, instruction) } diff --git a/programs/mango-v4/tests/program_test/mango_setup.rs b/programs/mango-v4/tests/program_test/mango_setup.rs index cbd42ea7c..e980e41da 100644 --- a/programs/mango-v4/tests/program_test/mango_setup.rs +++ b/programs/mango-v4/tests/program_test/mango_setup.rs @@ -19,6 +19,7 @@ pub struct Token { pub oracle: Pubkey, pub bank: Pubkey, pub vault: Pubkey, + pub mint_info: Pubkey, } pub struct GroupWithTokens { @@ -93,8 +94,23 @@ impl<'a> GroupWithTokensConfig<'a> { ) .await .unwrap(); + let _ = send_tx( + solana, + TokenAddBankInstruction { + token_index, + bank_num: 1, + group, + admin, + mint: mint.pubkey, + address_lookup_table, + payer, + }, + ) + .await + .unwrap(); let bank = register_token_accounts.bank; let vault = register_token_accounts.vault; + let mint_info = register_token_accounts.mint_info; tokens.push(Token { index: token_index, @@ -102,6 +118,7 @@ impl<'a> GroupWithTokensConfig<'a> { oracle, bank, vault, + mint_info, }); } diff --git a/programs/mango-v4/tests/test_basic.rs b/programs/mango-v4/tests/test_basic.rs index 49e5ff117..9b3f07707 100644 --- a/programs/mango-v4/tests/test_basic.rs +++ b/programs/mango-v4/tests/test_basic.rs @@ -139,6 +139,18 @@ async fn test_basic() -> Result<(), TransportError> { // // withdraw whatever is remaining, can't close bank vault without this + send_tx( + solana, + UpdateIndexInstruction { + mint_info: tokens[0].mint_info, + banks: { + let mint_info: MintInfo = solana.get_account(tokens[0].mint_info).await; + mint_info.banks.to_vec() + }, + }, + ) + .await + .unwrap(); let bank_data: Bank = solana.get_account(bank).await; send_tx( solana, @@ -173,7 +185,15 @@ async fn test_basic() -> Result<(), TransportError> { admin, payer, group, - mint: bank_data.mint, + mint_info: tokens[0].mint_info, + banks: { + let mint_info: MintInfo = solana.get_account(tokens[0].mint_info).await; + mint_info.banks.to_vec() + }, + vaults: { + let mint_info: MintInfo = solana.get_account(tokens[0].mint_info).await; + mint_info.vaults.to_vec() + }, token_index: bank_data.token_index, sol_destination: payer.pubkey(), }, diff --git a/programs/mango-v4/tests/test_update_index.rs b/programs/mango-v4/tests/test_update_index.rs index 282bea933..0a71993c5 100644 --- a/programs/mango-v4/tests/test_update_index.rs +++ b/programs/mango-v4/tests/test_update_index.rs @@ -1,6 +1,6 @@ #![cfg(feature = "test-bpf")] -use mango_v4::state::Bank; +use mango_v4::state::{Bank, MintInfo}; use solana_program_test::*; use solana_sdk::{signature::Keypair, transport::TransportError}; @@ -103,13 +103,19 @@ async fn test_update_index() -> Result<(), TransportError> { send_tx( solana, UpdateIndexInstruction { - bank: tokens[0].bank, + mint_info: tokens[0].mint_info, + banks: { + let mint_info: MintInfo = solana.get_account(tokens[0].mint_info).await; + mint_info.banks.to_vec() + }, }, ) .await .unwrap(); let bank_after_update_index = solana.get_account::(tokens[0].bank).await; + dbg!(bank_after_update_index); + dbg!(bank_after_update_index); assert!(bank_before_update_index.deposit_index < bank_after_update_index.deposit_index); assert!(bank_before_update_index.borrow_index < bank_after_update_index.borrow_index); diff --git a/ts/client/src/accounts/bank.ts b/ts/client/src/accounts/bank.ts index e5c209e9a..b9ee7b2e1 100644 --- a/ts/client/src/accounts/bank.ts +++ b/ts/client/src/accounts/bank.ts @@ -217,8 +217,8 @@ export class MintInfo { publicKey: PublicKey, obj: { mint: PublicKey; - bank: PublicKey; - vault: PublicKey; + banks: PublicKey[]; + vaults: PublicKey[]; oracle: PublicKey; addressLookupTable: PublicKey; tokenIndex: number; @@ -230,8 +230,8 @@ export class MintInfo { return new MintInfo( publicKey, obj.mint, - obj.bank, - obj.vault, + obj.banks, + obj.vaults, obj.oracle, obj.tokenIndex, ); @@ -240,9 +240,16 @@ export class MintInfo { constructor( public publicKey: PublicKey, public mint: PublicKey, - public bank: PublicKey, - public vault: PublicKey, + public banks: PublicKey[], + public vaults: PublicKey[], public oracle: PublicKey, public tokenIndex: number, ) {} + + public firstBank() { + return this.banks[0]; + } + public firstVault() { + return this.vaults[0]; + } } diff --git a/ts/client/src/client.ts b/ts/client/src/client.ts index 45fba0402..797fc2077 100644 --- a/ts/client/src/client.ts +++ b/ts/client/src/client.ts @@ -155,6 +155,7 @@ export class MangoClient { return await this.program.methods .tokenRegister( tokenIndex, + new BN(0), name, { confFilter: { @@ -189,16 +190,20 @@ export class MangoClient { const adminPk = (this.program.provider as AnchorProvider).wallet.publicKey; return await this.program.methods - .tokenDeregister() + .tokenDeregister(bank.tokenIndex) .accounts({ group: group.publicKey, admin: adminPk, - bank: bank.publicKey, - vault: bank.vault, mintInfo: group.mintInfosMap.get(bank.tokenIndex)?.publicKey, solDestination: (this.program.provider as AnchorProvider).wallet .publicKey, }) + .remainingAccounts( + [bank.publicKey, bank.vault].map( + (pk) => + ({ pubkey: pk, isWritable: true, isSigner: false } as AccountMeta), + ), + ) .rpc(); } @@ -1425,7 +1430,9 @@ export class MangoClient { const mintInfos = [...new Set(tokenIndices)].map( (tokenIndex) => group.mintInfosMap.get(tokenIndex)!, ); - healthRemainingAccounts.push(...mintInfos.map((mintInfo) => mintInfo.bank)); + healthRemainingAccounts.push( + ...mintInfos.map((mintInfo) => mintInfo.firstBank()), + ); healthRemainingAccounts.push( ...mintInfos.map((mintInfo) => mintInfo.oracle), ); diff --git a/ts/client/src/mango_v4.ts b/ts/client/src/mango_v4.ts index bed329b4d..700c3997b 100644 --- a/ts/client/src/mango_v4.ts +++ b/ts/client/src/mango_v4.ts @@ -120,6 +120,11 @@ export type MangoV4 = { "kind": "arg", "type": "u16", "path": "token_index" + }, + { + "kind": "arg", + "type": "u64", + "path": "bank_num" } ] } @@ -144,6 +149,11 @@ export type MangoV4 = { "kind": "arg", "type": "u16", "path": "token_index" + }, + { + "kind": "arg", + "type": "u64", + "path": "bank_num" } ] } @@ -204,6 +214,10 @@ export type MangoV4 = { "name": "tokenIndex", "type": "u16" }, + { + "name": "bankNum", + "type": "u64" + }, { "name": "name", "type": "string" @@ -251,7 +265,7 @@ export type MangoV4 = { ] }, { - "name": "tokenDeregister", + "name": "tokenAddBank", "accounts": [ { "name": "group", @@ -263,16 +277,144 @@ export type MangoV4 = { "isMut": false, "isSigner": true }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "existingBank", + "isMut": false, + "isSigner": false + }, { "name": "bank", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "const", + "type": "string", + "value": "Bank" + }, + { + "kind": "arg", + "type": "u16", + "path": "token_index" + }, + { + "kind": "arg", + "type": "u64", + "path": "bank_num" + } + ] + } }, { "name": "vault", "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "const", + "type": "string", + "value": "Vault" + }, + { + "kind": "arg", + "type": "u16", + "path": "token_index" + }, + { + "kind": "arg", + "type": "u64", + "path": "bank_num" + } + ] + } + }, + { + "name": "mintInfo", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "const", + "type": "string", + "value": "MintInfo" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Mint", + "path": "mint" + } + ] + } + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, "isSigner": false }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "tokenIndex", + "type": "u16" + }, + { + "name": "bankNum", + "type": "u64" + } + ] + }, + { + "name": "tokenDeregister", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, { "name": "mintInfo", "isMut": true, @@ -289,14 +431,19 @@ export type MangoV4 = { "isSigner": false } ], - "args": [] + "args": [ + { + "name": "tokenIndex", + "type": "u16" + } + ] }, { "name": "updateIndex", "accounts": [ { - "name": "bank", - "isMut": true, + "name": "mintInfo", + "isMut": false, "isSigner": false } ], @@ -1968,6 +2115,18 @@ export type MangoV4 = { "defined": "I80F48" } }, + { + "name": "indexedDeposits", + "type": { + "defined": "I80F48" + } + }, + { + "name": "indexedBorrows", + "type": { + "defined": "I80F48" + } + }, { "name": "lastUpdated", "type": "i64" @@ -2076,6 +2235,10 @@ export type MangoV4 = { 4 ] } + }, + { + "name": "bankNum", + "type": "u64" } ] } @@ -2208,12 +2371,22 @@ export type MangoV4 = { "type": "publicKey" }, { - "name": "bank", - "type": "publicKey" + "name": "banks", + "type": { + "array": [ + "publicKey", + 6 + ] + } }, { - "name": "vault", - "type": "publicKey" + "name": "vaults", + "type": { + "array": [ + "publicKey", + 6 + ] + } }, { "name": "oracle", @@ -3412,6 +3585,11 @@ export const IDL: MangoV4 = { "kind": "arg", "type": "u16", "path": "token_index" + }, + { + "kind": "arg", + "type": "u64", + "path": "bank_num" } ] } @@ -3436,6 +3614,11 @@ export const IDL: MangoV4 = { "kind": "arg", "type": "u16", "path": "token_index" + }, + { + "kind": "arg", + "type": "u64", + "path": "bank_num" } ] } @@ -3496,6 +3679,10 @@ export const IDL: MangoV4 = { "name": "tokenIndex", "type": "u16" }, + { + "name": "bankNum", + "type": "u64" + }, { "name": "name", "type": "string" @@ -3543,7 +3730,7 @@ export const IDL: MangoV4 = { ] }, { - "name": "tokenDeregister", + "name": "tokenAddBank", "accounts": [ { "name": "group", @@ -3555,16 +3742,144 @@ export const IDL: MangoV4 = { "isMut": false, "isSigner": true }, + { + "name": "mint", + "isMut": false, + "isSigner": false + }, + { + "name": "existingBank", + "isMut": false, + "isSigner": false + }, { "name": "bank", "isMut": true, - "isSigner": false + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "const", + "type": "string", + "value": "Bank" + }, + { + "kind": "arg", + "type": "u16", + "path": "token_index" + }, + { + "kind": "arg", + "type": "u64", + "path": "bank_num" + } + ] + } }, { "name": "vault", "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "const", + "type": "string", + "value": "Vault" + }, + { + "kind": "arg", + "type": "u16", + "path": "token_index" + }, + { + "kind": "arg", + "type": "u64", + "path": "bank_num" + } + ] + } + }, + { + "name": "mintInfo", + "isMut": true, + "isSigner": false, + "pda": { + "seeds": [ + { + "kind": "account", + "type": "publicKey", + "path": "group" + }, + { + "kind": "const", + "type": "string", + "value": "MintInfo" + }, + { + "kind": "account", + "type": "publicKey", + "account": "Mint", + "path": "mint" + } + ] + } + }, + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "tokenProgram", + "isMut": false, "isSigner": false }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "tokenIndex", + "type": "u16" + }, + { + "name": "bankNum", + "type": "u64" + } + ] + }, + { + "name": "tokenDeregister", + "accounts": [ + { + "name": "group", + "isMut": false, + "isSigner": false + }, + { + "name": "admin", + "isMut": false, + "isSigner": true + }, { "name": "mintInfo", "isMut": true, @@ -3581,14 +3896,19 @@ export const IDL: MangoV4 = { "isSigner": false } ], - "args": [] + "args": [ + { + "name": "tokenIndex", + "type": "u16" + } + ] }, { "name": "updateIndex", "accounts": [ { - "name": "bank", - "isMut": true, + "name": "mintInfo", + "isMut": false, "isSigner": false } ], @@ -5260,6 +5580,18 @@ export const IDL: MangoV4 = { "defined": "I80F48" } }, + { + "name": "indexedDeposits", + "type": { + "defined": "I80F48" + } + }, + { + "name": "indexedBorrows", + "type": { + "defined": "I80F48" + } + }, { "name": "lastUpdated", "type": "i64" @@ -5368,6 +5700,10 @@ export const IDL: MangoV4 = { 4 ] } + }, + { + "name": "bankNum", + "type": "u64" } ] } @@ -5500,12 +5836,22 @@ export const IDL: MangoV4 = { "type": "publicKey" }, { - "name": "bank", - "type": "publicKey" + "name": "banks", + "type": { + "array": [ + "publicKey", + 6 + ] + } }, { - "name": "vault", - "type": "publicKey" + "name": "vaults", + "type": { + "array": [ + "publicKey", + 6 + ] + } }, { "name": "oracle",