From ce5f2027a1d03b9ed4de942356543c05d2d13d30 Mon Sep 17 00:00:00 2001 From: microwavedcola1 Date: Thu, 3 Mar 2022 06:15:28 +0100 Subject: [PATCH] extract health, flesh out margin trade, todo - test Signed-off-by: microwavedcola1 --- README.md | 4 ++ programs/mango-v4/src/error.rs | 2 + .../mango-v4/src/instructions/margin_trade.rs | 63 +++++++++++++++++ programs/mango-v4/src/instructions/mod.rs | 2 + .../mango-v4/src/instructions/withdraw.rs | 54 +-------------- programs/mango-v4/src/lib.rs | 9 ++- programs/mango-v4/src/state/health.rs | 69 ++++++++++++++++--- programs/mango-v4/src/state/mod.rs | 3 +- programs/mango-v4/src/util.rs | 10 +++ 9 files changed, 151 insertions(+), 65 deletions(-) create mode 100644 programs/mango-v4/src/instructions/margin_trade.rs create mode 100644 programs/mango-v4/src/util.rs diff --git a/README.md b/README.md index 43051ff32..6eccded51 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,7 @@ programs └── tests # rust tests, TODO ``` +### How to open and manage pull requests +- when in doubt dont squash commits, specially when merge request is very large, specially if your branch contains unrelated commits +- use the why along with what for commit messages, code comments, makes it easy to understand the context +- add descriptions to your merge requests if they are non trivial, helps code reviewer watch out for things, understand the motivation for the merge request \ No newline at end of file diff --git a/programs/mango-v4/src/error.rs b/programs/mango-v4/src/error.rs index 90259c758..5f1cc9429 100644 --- a/programs/mango-v4/src/error.rs +++ b/programs/mango-v4/src/error.rs @@ -10,4 +10,6 @@ pub enum MangoError { UnexpectedOracle, #[msg("")] UnknownOracleType, + #[msg("")] + InvalidMarginTradeTargetCpiProgram, } diff --git a/programs/mango-v4/src/instructions/margin_trade.rs b/programs/mango-v4/src/instructions/margin_trade.rs new file mode 100644 index 000000000..5b20ad61f --- /dev/null +++ b/programs/mango-v4/src/instructions/margin_trade.rs @@ -0,0 +1,63 @@ +use crate::error::MangoError; +use crate::state::{compute_health, MangoAccount, MangoGroup}; +use crate::util::to_account_meta; +use crate::{group_seeds, Mango}; +use anchor_lang::prelude::*; +use solana_program::instruction::Instruction; + +#[derive(Accounts)] +pub struct MarginTrade<'info> { + pub group: AccountLoader<'info, MangoGroup>, + + #[account( + mut, + has_one = group, + has_one = owner, + )] + pub account: AccountLoader<'info, MangoAccount>, + + pub owner: Signer<'info>, +} + +/// reference https://github.com/blockworks-foundation/mango-v3/blob/mc/flash_loan/program/src/processor.rs#L5323 +pub fn margin_trade(ctx: Context, cpi_data: Vec) -> Result<()> { + let group = ctx.accounts.group.load()?; + let mut account = ctx.accounts.account.load_mut()?; + let active_len = account.indexed_positions.iter_active().count(); + let banks = &ctx.remaining_accounts[0..active_len]; + let oracles = &ctx.remaining_accounts[active_len..active_len * 2]; + let cpi_ais = &ctx.remaining_accounts[active_len * 2..]; + + // since we are using group signer seeds to invoke cpi, + // assert that none of the cpi accounts is the mango program to prevent that invoker doesn't + // abuse this ix to do unwanted changes + for cpi_ai in cpi_ais { + require!( + *ctx.remaining_accounts[active_len].key != Mango::id(), + MangoError::InvalidMarginTradeTargetCpiProgram + ); + } + + // compute pre cpi health + let pre_cpi_health = compute_health(&mut account, &banks, &oracles).unwrap(); + require!(pre_cpi_health > 0, MangoError::HealthMustBePositive); + + // prepare and invoke cpi + let cpi_ix = Instruction { + program_id: *ctx.remaining_accounts[active_len].key, + data: cpi_data, + accounts: cpi_ais + .iter() + .skip(1) + .map(|cpi_ai| to_account_meta(cpi_ai)) + .collect(), + }; + let group_seeds = group_seeds!(group); + solana_program::program::invoke_signed(&cpi_ix, &cpi_ais, &[group_seeds])?; + + // compute post cpi health + let post_cpi_health = compute_health(&mut account, &banks, &oracles).unwrap(); + require!(post_cpi_health > 0, MangoError::HealthMustBePositive); + + Ok(()) +} diff --git a/programs/mango-v4/src/instructions/mod.rs b/programs/mango-v4/src/instructions/mod.rs index d320ceda1..b11a4c435 100644 --- a/programs/mango-v4/src/instructions/mod.rs +++ b/programs/mango-v4/src/instructions/mod.rs @@ -2,6 +2,7 @@ pub use create_account::*; pub use create_group::*; pub use create_stub_oracle::*; pub use deposit::*; +pub use margin_trade::*; pub use register_token::*; pub use set_stub_oracle::*; pub use withdraw::*; @@ -10,6 +11,7 @@ mod create_account; mod create_group; mod create_stub_oracle; mod deposit; +mod margin_trade; mod register_token; mod set_stub_oracle; mod withdraw; diff --git a/programs/mango-v4/src/instructions/withdraw.rs b/programs/mango-v4/src/instructions/withdraw.rs index ac2391f19..454fa8e50 100644 --- a/programs/mango-v4/src/instructions/withdraw.rs +++ b/programs/mango-v4/src/instructions/withdraw.rs @@ -2,8 +2,6 @@ use anchor_lang::prelude::*; use anchor_spl::token; use anchor_spl::token::Token; use anchor_spl::token::TokenAccount; -use fixed::types::I80F48; -use pyth_client::load_price; use crate::error::*; use crate::state::*; @@ -49,14 +47,6 @@ impl<'info> Withdraw<'info> { } } -macro_rules! zip { - ($x: expr) => ($x); - ($x: expr, $($y: expr), +) => ( - $x.zip( - zip!($($y), +)) - ) -} - // TODO: It may make sense to have the token_index passed in from the outside. // That would save a lot of computation that needs to go into finding the // right index for the mint. @@ -114,48 +104,10 @@ pub fn withdraw(ctx: Context, amount: u64, allow_borrow: bool) -> Resu MangoError::SomeError ); - let mut assets = I80F48::ZERO; - let mut liabilities = I80F48::ZERO; // absolute value - for (position, (bank_ai, oracle_ai)) in zip!( - account.indexed_positions.iter_active(), - ctx.remaining_accounts.iter(), - ctx.remaining_accounts.iter().skip(active_len) - ) { - let bank_loader = AccountLoader::<'_, TokenBank>::try_from(bank_ai)?; - let bank = bank_loader.load()?; + let banks = &ctx.remaining_accounts[0..active_len]; + let oracles = &ctx.remaining_accounts[active_len..active_len * 2]; - // TODO: This assumes banks are passed in order - is that an ok assumption? - require!( - bank.token_index == position.token_index, - MangoError::SomeError - ); - - // converts the token value to the basis token value for health computations - // TODO: health basis token == USDC? - let oracle_data = &oracle_ai.try_borrow_data()?; - let oracle_type = determine_oracle_type(oracle_data)?; - require!(bank.oracle == oracle_ai.key(), MangoError::UnexpectedOracle); - - let price = match oracle_type { - OracleType::Stub => { - AccountLoader::<'_, StubOracle>::try_from(oracle_ai)? - .load()? - .price - } - OracleType::Pyth => { - let price_struct = load_price(&oracle_data).unwrap(); - I80F48::from_num(price_struct.agg.price) - } - }; - - let native_basis = position.native(&bank) * price; - if native_basis.is_positive() { - assets += bank.init_asset_weight * native_basis; - } else { - liabilities -= bank.init_liab_weight * native_basis; - } - } - let health = assets - liabilities; + let health = compute_health(&mut account, &banks, &oracles)?; msg!("health: {}", health); require!(health > 0, MangoError::SomeError); diff --git a/programs/mango-v4/src/lib.rs b/programs/mango-v4/src/lib.rs index e86dcdb2a..529829cb8 100644 --- a/programs/mango-v4/src/lib.rs +++ b/programs/mango-v4/src/lib.rs @@ -7,9 +7,10 @@ use anchor_lang::prelude::*; use instructions::*; pub mod address_lookup_table; -mod error; -mod instructions; +pub mod error; +pub mod instructions; pub mod state; +pub mod util; declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -67,6 +68,10 @@ pub mod mango_v4 { pub fn withdraw(ctx: Context, amount: u64, allow_borrow: bool) -> Result<()> { instructions::withdraw(ctx, amount, allow_borrow) } + + pub fn margin_trade(ctx: Context, cpi_data: Vec) -> Result<()> { + instructions::margin_trade(ctx, cpi_data) + } } #[derive(Clone)] diff --git a/programs/mango-v4/src/state/health.rs b/programs/mango-v4/src/state/health.rs index a308089d0..9d90db046 100644 --- a/programs/mango-v4/src/state/health.rs +++ b/programs/mango-v4/src/state/health.rs @@ -1,18 +1,65 @@ +use std::cell::RefMut; + +use anchor_lang::prelude::*; use fixed::types::I80F48; +use pyth_client::load_price; -pub struct UserActiveAssets { - pub spot: [bool; MAX_PAIRS], - pub perps: [bool; MAX_PAIRS], +use crate::error::MangoError; +use crate::state::{determine_oracle_type, MangoAccount, OracleType, StubOracle, TokenBank}; + +macro_rules! zip { + ($x: expr) => ($x); + ($x: expr, $($y: expr), +) => ( + $x.zip( + zip!($($y), +)) + ) } -pub struct HealthCache { - pub active_assets: UserActiveAssets, +pub fn compute_health( + account: &mut RefMut, + banks: &[AccountInfo], + oracles: &[AccountInfo], +) -> Result { + let mut assets = I80F48::ZERO; + let mut liabilities = I80F48::ZERO; // absolute value + for (position, (bank_ai, oracle_ai)) in zip!( + account.indexed_positions.iter_active(), + banks.iter(), + oracles.iter() + ) { + let bank_loader = AccountLoader::<'_, TokenBank>::try_from(bank_ai)?; + let bank = bank_loader.load()?; - /// Vec of length MAX_PAIRS containing worst case spot vals; unweighted - spot: Vec<(I80F48, I80F48)>, - perp: Vec<(I80F48, I80F48)>, - quote: I80F48, + // TODO: This assumes banks are passed in order - is that an ok assumption? + require!( + bank.token_index == position.token_index, + MangoError::SomeError + ); - /// This will be zero until update_health is called for the first time - health: [Option; 2], + // converts the token value to the basis token value for health computations + // TODO: health basis token == USDC? + let oracle_data = &oracle_ai.try_borrow_data()?; + let oracle_type = determine_oracle_type(oracle_data)?; + require!(bank.oracle == oracle_ai.key(), MangoError::UnexpectedOracle); + + let price = match oracle_type { + OracleType::Stub => { + AccountLoader::<'_, StubOracle>::try_from(oracle_ai)? + .load()? + .price + } + OracleType::Pyth => { + let price_struct = load_price(&oracle_data).unwrap(); + I80F48::from_num(price_struct.agg.price) + } + }; + + let native_basis = position.native(&bank) * price; + if native_basis.is_positive() { + assets += bank.init_asset_weight * native_basis; + } else { + liabilities -= bank.init_liab_weight * native_basis; + } + } + Ok(assets - liabilities) } diff --git a/programs/mango-v4/src/state/mod.rs b/programs/mango-v4/src/state/mod.rs index 257490e6b..6e7f9680d 100644 --- a/programs/mango-v4/src/state/mod.rs +++ b/programs/mango-v4/src/state/mod.rs @@ -1,3 +1,4 @@ +pub use health::*; pub use mango_account::*; pub use mango_group::*; pub use oracle::*; @@ -5,7 +6,7 @@ pub use token_bank::*; // mod advanced_orders; // mod cache; -// mod health; +mod health; mod mango_account; mod mango_group; mod oracle; diff --git a/programs/mango-v4/src/util.rs b/programs/mango-v4/src/util.rs new file mode 100644 index 000000000..9220b736d --- /dev/null +++ b/programs/mango-v4/src/util.rs @@ -0,0 +1,10 @@ +use solana_program::account_info::AccountInfo; +use solana_program::instruction::AccountMeta; + +pub fn to_account_meta(account_info: &AccountInfo) -> AccountMeta { + if account_info.is_writable { + AccountMeta::new(*account_info.key, account_info.is_signer) + } else { + AccountMeta::new_readonly(*account_info.key, account_info.is_signer) + } +}