extract health, flesh out margin trade, todo - test

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-03-03 06:15:28 +01:00
parent 6702cdbba6
commit ce5f2027a1
9 changed files with 151 additions and 65 deletions

View File

@ -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

View File

@ -10,4 +10,6 @@ pub enum MangoError {
UnexpectedOracle,
#[msg("")]
UnknownOracleType,
#[msg("")]
InvalidMarginTradeTargetCpiProgram,
}

View File

@ -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<MarginTrade>, cpi_data: Vec<u8>) -> 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(())
}

View File

@ -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;

View File

@ -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<Withdraw>, 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);

View File

@ -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<Withdraw>, amount: u64, allow_borrow: bool) -> Result<()> {
instructions::withdraw(ctx, amount, allow_borrow)
}
pub fn margin_trade(ctx: Context<MarginTrade>, cpi_data: Vec<u8>) -> Result<()> {
instructions::margin_trade(ctx, cpi_data)
}
}
#[derive(Clone)]

View File

@ -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<MangoAccount>,
banks: &[AccountInfo],
oracles: &[AccountInfo],
) -> Result<I80F48> {
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<I80F48>; 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)
}

View File

@ -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;

View File

@ -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)
}
}