Merge pull request #17 from blockworks-foundation/mc/margin_trade
wip: mc/margin trade
This commit is contained in:
commit
528baab867
|
@ -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
|
|
@ -10,4 +10,6 @@ pub enum MangoError {
|
|||
UnexpectedOracle,
|
||||
#[msg("")]
|
||||
UnknownOracleType,
|
||||
#[msg("")]
|
||||
InvalidMarginTradeTargetCpiProgram,
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue