margin trade test

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-03-04 14:30:53 +01:00 committed by Christian Kamm
parent 425e22a086
commit dcacadbcbf
14 changed files with 422 additions and 44 deletions

1
.gitignore vendored
View File

@ -8,4 +8,5 @@ node_modules
.idea
programs/margin-trade/src/lib-expanded.rs
programs/mango-v4/src/lib-expanded.rs

10
Cargo.lock generated
View File

@ -1494,6 +1494,7 @@ dependencies = [
"fixed",
"fixed-macro",
"log",
"margin-trade",
"pyth-client",
"serde",
"solana-logger",
@ -1505,6 +1506,15 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "margin-trade"
version = "0.1.0"
dependencies = [
"anchor-lang",
"anchor-spl",
"solana-program",
]
[[package]]
name = "matches"
version = "0.1.9"

View File

@ -27,9 +27,9 @@ bytemuck = "^1.7.2"
fixed = { version = "=1.11.0", features = ["serde", "borsh"] }
fixed-macro = "^1.1.1"
pyth-client = {version = "0.5.0", features = ["no-entrypoint"]}
static_assertions = "1.1"
solana-program = "1.9.5"
serde = "^1.0"
solana-program = "1.9.5"
static_assertions = "1.1"
[dev-dependencies]
@ -43,3 +43,4 @@ log = "0.4.14"
env_logger = "0.9.0"
base64 = "0.13.0"
async-trait = "0.1.52"
margin-trade = { path = "../margin-trade", features = ["cpi"] }

View File

@ -1,9 +1,10 @@
use crate::error::MangoError;
use crate::state::{compute_health, MangoAccount, MangoGroup};
use crate::util::to_account_meta;
use crate::{group_seeds, Mango};
use crate::state::{compute_health, MangoAccount, MangoGroup, TokenBank};
use crate::{group_seeds, util, Mango};
use anchor_lang::prelude::*;
use anchor_spl::token::TokenAccount;
use solana_program::instruction::Instruction;
use std::cell::{Ref, RefMut};
#[derive(Accounts)]
pub struct MarginTrade<'info> {
@ -20,18 +21,44 @@ pub struct MarginTrade<'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<()> {
pub fn margin_trade<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, MarginTrade<'info>>,
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();
// remaining_accounts layout is expected as follows
// * active_len number of banks
// * active_len number of oracles
// * cpi_program
// * cpi_accounts
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..];
let cpi_program_id = *ctx.remaining_accounts[active_len * 2].key;
// prepare for cpi
let (cpi_ais, cpi_ams) = {
// we also need the group
let mut cpi_ais = [ctx.accounts.group.to_account_info()].to_vec();
// skip banks, oracles and cpi program from the remaining_accounts
let mut remaining_cpi_ais = ctx.remaining_accounts[active_len * 2 + 1..].to_vec();
cpi_ais.append(&mut remaining_cpi_ais);
let mut cpi_ams = cpi_ais.to_account_metas(Option::None);
// we want group to be the signer, so that loans can be taken from the token vaults
cpi_ams[0].is_signer = true;
(cpi_ais, cpi_ams)
};
// 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 {
for cpi_ai in &cpi_ais {
require!(
cpi_ai.key() != Mango::id(),
MangoError::InvalidMarginTradeTargetCpiProgram
@ -39,25 +66,85 @@ pub fn margin_trade(ctx: Context<MarginTrade>, cpi_data: Vec<u8>) -> Result<()>
}
// compute pre cpi health
let pre_cpi_health = compute_health(&mut account, &banks, &oracles).unwrap();
let pre_cpi_health = compute_health(&mut account, &banks, &oracles)?;
require!(pre_cpi_health > 0, MangoError::HealthMustBePositive);
msg!("pre_cpi_health {:?}", pre_cpi_health);
// prepare and invoke cpi
let cpi_ix = Instruction {
program_id: *ctx.remaining_accounts[active_len].key,
program_id: cpi_program_id,
data: cpi_data,
accounts: cpi_ais
.iter()
.skip(1)
.map(|cpi_ai| to_account_meta(cpi_ai))
.collect(),
accounts: cpi_ams,
};
let group_seeds = group_seeds!(group);
let pre_cpi_token_vault_amounts = get_pre_cpi_token_amounts(&ctx, &cpi_ais);
solana_program::program::invoke_signed(&cpi_ix, &cpi_ais, &[group_seeds])?;
adjust_for_post_cpi_token_amounts(
&ctx,
&cpi_ais,
pre_cpi_token_vault_amounts,
group,
&mut banks.to_vec(),
&mut account,
)?;
// compute post cpi health
let post_cpi_health = compute_health(&mut account, &banks, &oracles).unwrap();
let post_cpi_health = compute_health(&account, &banks, &oracles)?;
require!(post_cpi_health > 0, MangoError::HealthMustBePositive);
msg!("post_cpi_health {:?}", post_cpi_health);
Ok(())
}
fn get_pre_cpi_token_amounts(ctx: &Context<MarginTrade>, cpi_ais: &Vec<AccountInfo>) -> Vec<u64> {
let mut mango_vault_token_account_amounts = vec![];
for maybe_token_account in cpi_ais
.iter()
.filter(|ai| ai.owner == &TokenAccount::owner())
{
let maybe_mango_vault_token_account =
Account::<TokenAccount>::try_from(maybe_token_account).unwrap();
if maybe_mango_vault_token_account.owner == ctx.accounts.group.key() {
mango_vault_token_account_amounts.push(maybe_mango_vault_token_account.amount)
}
}
mango_vault_token_account_amounts
}
/// withdraws from bank, on users behalf, if he hasn't returned back entire loan amount
fn adjust_for_post_cpi_token_amounts(
ctx: &Context<MarginTrade>,
cpi_ais: &Vec<AccountInfo>,
pre_cpi_token_vault_amounts: Vec<u64>,
group: Ref<MangoGroup>,
banks: &mut Vec<AccountInfo>,
account: &mut RefMut<MangoAccount>,
) -> Result<()> {
let x = cpi_ais
.iter()
.filter(|ai| ai.owner == &TokenAccount::owner());
for (maybe_token_account, (pre_cpi_token_vault_amount, bank_ai)) in
util::zip!(x, pre_cpi_token_vault_amounts.iter(), banks.iter())
{
let maybe_mango_vault_token_account =
Account::<TokenAccount>::try_from(maybe_token_account).unwrap();
if maybe_mango_vault_token_account.owner == ctx.accounts.group.key() {
let still_loaned_amount =
pre_cpi_token_vault_amount - maybe_mango_vault_token_account.amount;
if still_loaned_amount <= 0 {
continue;
}
let token_index = group
.tokens
.index_for_mint(&maybe_mango_vault_token_account.mint)?;
let mut position = *account.indexed_positions.get_mut_or_create(token_index)?;
let bank_loader = AccountLoader::<'_, TokenBank>::try_from(bank_ai)?;
let mut bank = bank_loader.load_mut()?;
// todo: this doesnt work since bank is not mut in the tests atm
bank.withdraw(&mut position, still_loaned_amount);
}
}
Ok(())
}

View File

@ -1,8 +1,8 @@
pub use self::margin_trade::*;
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::*;

View File

@ -1,5 +1,8 @@
use fixed::types::I80F48;
#[macro_use]
pub mod util;
extern crate static_assertions;
use anchor_lang::prelude::*;
@ -10,7 +13,6 @@ pub mod address_lookup_table;
pub mod error;
pub mod instructions;
pub mod state;
pub mod util;
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
@ -65,7 +67,10 @@ pub mod mango_v4 {
instructions::withdraw(ctx, amount, allow_borrow)
}
pub fn margin_trade(ctx: Context<MarginTrade>, cpi_data: Vec<u8>) -> Result<()> {
pub fn margin_trade<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, MarginTrade<'info>>,
cpi_data: Vec<u8>,
) -> Result<()> {
instructions::margin_trade(ctx, cpi_data)
}
}

View File

@ -6,23 +6,16 @@ use pyth_client::load_price;
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), +))
)
}
use crate::util;
pub fn compute_health(
account: &mut RefMut<MangoAccount>,
account: &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!(
for (position, (bank_ai, oracle_ai)) in util::zip!(
account.indexed_positions.iter_active(),
banks.iter(),
oracles.iter()

View File

@ -1,10 +1,12 @@
use solana_program::account_info::AccountInfo;
use solana_program::instruction::AccountMeta;
#![macro_use]
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)
}
#[macro_export]
macro_rules! zip {
($x: expr) => ($x);
($x: expr, $($y: expr), +) => (
$x.zip(
zip!($($y), +))
)
}
pub(crate) use zip;

View File

@ -130,6 +130,89 @@ async fn derive_health_check_remaining_account_metas(
// ClientInstruction impl
//
pub struct MarginTradeInstruction<'keypair> {
pub account: Pubkey,
pub owner: &'keypair Keypair,
pub mango_token_vault: Pubkey,
pub mango_group: Pubkey,
pub margin_trade_program_id: Pubkey,
pub loan_token_account: Pubkey,
pub loan_token_account_owner: Pubkey,
pub margin_trade_program_ix_cpi_data: Vec<u8>,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for MarginTradeInstruction<'keypair> {
type Accounts = mango_v4::accounts::MarginTrade;
type Instruction = mango_v4::instruction::MarginTrade;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let instruction = Self::Instruction {
cpi_data: self.margin_trade_program_ix_cpi_data.clone(),
};
let account: MangoAccount = account_loader.load(&self.account).await.unwrap();
let lookup_table = account_loader
.load_bytes(&account.address_lookup_table)
.await
.unwrap();
let accounts = Self::Accounts {
group: account.group,
account: self.account,
owner: self.owner.pubkey(),
};
let banks_and_oracles = mango_v4::address_lookup_table::addresses(&lookup_table);
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.push(AccountMeta {
pubkey: banks_and_oracles[0],
// since user doesnt return all loan after margin trade, we want to mark withdrawal from the bank
is_writable: true,
is_signer: false,
});
instruction.accounts.push(AccountMeta {
pubkey: banks_and_oracles[1],
is_writable: false,
is_signer: false,
});
instruction.accounts.push(AccountMeta {
pubkey: self.margin_trade_program_id,
is_writable: false,
is_signer: false,
});
instruction.accounts.push(AccountMeta {
pubkey: self.mango_token_vault,
is_writable: true,
is_signer: false,
});
instruction.accounts.push(AccountMeta {
pubkey: self.loan_token_account,
is_writable: true,
is_signer: false,
});
instruction.accounts.push(AccountMeta {
pubkey: self.loan_token_account_owner,
is_writable: false,
is_signer: false,
});
instruction.accounts.push(AccountMeta {
pubkey: spl_token::ID,
is_writable: false,
is_signer: false,
});
(accounts, instruction)
}
fn signers(&self) -> Vec<&Keypair> {
vec![self.owner]
}
}
pub struct WithdrawInstruction<'keypair> {
pub amount: u64,
pub allow_borrow: bool,

View File

@ -78,7 +78,18 @@ pub struct TestContext {
}
impl TestContext {
pub async fn new() -> Self {
pub async fn new(
test_opt: Option<ProgramTest>,
margin_trade_program_id: Option<&Pubkey>,
margin_trade_token_account: Option<&Keypair>,
mtta_owner: Option<&Pubkey>,
) -> Self {
let mut test = if test_opt.is_some() {
test_opt.unwrap()
} else {
ProgramTest::new("mango_v4", mango_v4::id(), processor!(mango_v4::entry))
};
// We need to intercept logs to capture program log output
let log_filter = "solana_rbpf=trace,\
solana_runtime::message_processor=debug,\
@ -94,11 +105,8 @@ impl TestContext {
program_log: program_log_capture.clone(),
}));
let program_id = mango_v4::id();
let mut test = ProgramTest::new("mango_v4", program_id, processor!(mango_v4::entry));
// intentionally set to half the limit, to catch potential problems early
test.set_compute_max_units(100000);
test.set_compute_max_units(200000);
// Setup the environment
@ -156,6 +164,28 @@ impl TestContext {
}
let quote_index = mints.len() - 1;
// margin trade
if margin_trade_program_id.is_some() {
test.add_program(
"margin_trade",
*margin_trade_program_id.unwrap(),
std::option::Option::None,
);
test.add_packable_account(
margin_trade_token_account.unwrap().pubkey(),
u32::MAX as u64,
&Account {
mint: mints[0].pubkey,
owner: *mtta_owner.unwrap(),
amount: 10,
state: AccountState::Initialized,
is_native: COption::None,
..Account::default()
},
&spl_token::id(),
);
}
// Users
let num_users = 4;
let mut users = Vec::new();

View File

@ -1,8 +1,12 @@
#![cfg(feature = "test-bpf")]
use anchor_lang::InstructionData;
use fixed::types::I80F48;
use solana_program::pubkey::Pubkey;
use solana_program_test::*;
use solana_sdk::signature::Signer;
use solana_sdk::{signature::Keypair, transport::TransportError};
use std::str::FromStr;
use mango_v4::state::*;
use program_test::*;
@ -13,7 +17,18 @@ mod program_test;
// that they work in principle. It should be split up / renamed.
#[tokio::test]
async fn test_basic() -> Result<(), TransportError> {
let context = TestContext::new().await;
let margin_trade_program_id =
Pubkey::from_str("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix").unwrap();
let margin_trade_token_account = Keypair::new();
let (mtta_owner, mtta_bump_seeds) =
Pubkey::find_program_address(&[b"margintrade"], &margin_trade_program_id);
let context = TestContext::new(
Option::None,
Some(&margin_trade_program_id),
Some(&margin_trade_token_account),
Some(&mtta_owner),
)
.await;
let solana = &context.solana.clone();
let admin = &Keypair::new();
@ -125,6 +140,35 @@ async fn test_basic() -> Result<(), TransportError> {
);
}
//
// TEST: Margin trade
//
{
send_tx(
solana,
MarginTradeInstruction {
account,
owner,
mango_token_vault: vault,
mango_group: group,
margin_trade_program_id,
loan_token_account: margin_trade_token_account.pubkey(),
loan_token_account_owner: mtta_owner,
margin_trade_program_ix_cpi_data: {
let ix = margin_trade::instruction::MarginTrade {
amount_from: 2,
amount_to: 1,
loan_token_account_owner_bump_seeds: mtta_bump_seeds,
};
ix.data()
},
},
)
.await
.unwrap();
}
let margin_trade_loan = 1;
//
// TEST: Withdraw funds
//
@ -145,7 +189,10 @@ async fn test_basic() -> Result<(), TransportError> {
.await
.unwrap();
assert_eq!(solana.token_account_balance(vault).await, withdraw_amount);
assert_eq!(
solana.token_account_balance(vault).await,
withdraw_amount - margin_trade_loan
);
assert_eq!(
solana.token_account_balance(payer_mint0_account).await,
start_balance + withdraw_amount

View File

@ -0,0 +1,23 @@
[package]
name = "margin-trade"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "margin_trade"
doctest = false
[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
test-bpf = []
[dependencies]
anchor-lang = { version = "0.22.0", features = [] }
anchor-spl = "0.22.0"
solana-program = "1.9.5"

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -0,0 +1,94 @@
use anchor_lang::prelude::*;
use anchor_spl::token;
use anchor_spl::token::{Token, TokenAccount, Transfer};
declare_id!("J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix");
#[program]
pub mod margin_trade {
use super::*;
pub fn margin_trade(
ctx: Context<MarginTradeCtx>,
amount_from: u64,
loan_token_account_owner_bump_seeds: u8,
amount_to: u64,
) -> Result<()> {
msg!(
"taking amount({}) loan from mango for mint {:?}",
amount_from,
ctx.accounts.mango_token_vault.mint
);
token::transfer(ctx.accounts.transfer_from_mango_vault_ctx(), amount_from)?;
msg!("TODO: do something with the loan");
msg!(
"transferring amount({}) loan back to mango for mint {:?}",
amount_to,
ctx.accounts.loan_token_account.mint
);
let seeds = &[
b"margintrade".as_ref(),
&[loan_token_account_owner_bump_seeds],
];
token::transfer(
ctx.accounts
.transfer_back_to_mango_vault_ctx()
.with_signer(&[seeds]),
amount_to,
)?;
Ok(())
}
}
#[derive(Clone)]
pub struct MarginTrade;
impl anchor_lang::Id for MarginTrade {
fn id() -> Pubkey {
ID
}
}
#[derive(Accounts)]
pub struct MarginTradeCtx<'info> {
pub mango_group: Signer<'info>,
#[account(mut)]
pub mango_token_vault: Account<'info, TokenAccount>,
#[account(mut)]
pub loan_token_account: Account<'info, TokenAccount>,
// todo: can we do better than UncheckedAccount?
pub loan_token_account_owner: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
}
impl<'info> MarginTradeCtx<'info> {
pub fn transfer_from_mango_vault_ctx(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = Transfer {
from: self.mango_token_vault.to_account_info(),
to: self.loan_token_account.to_account_info(),
authority: self.mango_group.to_account_info(),
};
CpiContext::new(program, accounts)
}
pub fn transfer_back_to_mango_vault_ctx(
&self,
) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
let program = self.token_program.to_account_info();
let accounts = Transfer {
from: self.loan_token_account.to_account_info(),
to: self.mango_token_vault.to_account_info(),
authority: self.loan_token_account_owner.to_account_info(),
};
CpiContext::new(program, accounts)
}
}