mc/kill flash loan 1 & 2 and rename flash loan 3 to flash loan (#131)

* remove flash loan 1 & 2

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>

* rename flash loan 3 to flash loan

* fix test

Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
microwavedcola1 2022-08-01 16:55:17 +02:00 committed by GitHub
parent c516e45d08
commit 5c3b2c1189
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 556 additions and 2372 deletions

View File

@ -1,397 +1,361 @@
use crate::accounts_zerocopy::*;
use crate::error::MangoError;
use crate::logs::{MarginTradeLog, TokenBalanceLog};
use crate::error::*;
use crate::group_seeds;
use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog};
use crate::state::MangoAccount;
use crate::state::{
compute_health, new_fixed_order_account_retriever, AccountLoaderDynamic, AccountRetriever,
Bank, Group, HealthType, MangoAccount, MangoAccountRefMut,
compute_health, compute_health_from_fixed_accounts, new_fixed_order_account_retriever,
AccountLoaderDynamic, AccountRetriever, Bank, Group, HealthType, TokenIndex,
};
use crate::{group_seeds, Mango};
use crate::util::checked_math as cm;
use anchor_lang::prelude::*;
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
use anchor_spl::token::{self, Token, TokenAccount};
use fixed::types::I80F48;
use solana_program::instruction::Instruction;
use std::cell::Ref;
use std::collections::HashMap;
/// The flash loan instruction
/// Sets up mango vaults for flash loan
///
/// In addition to these accounts, there must be a sequence of remaining_accounts:
/// 1. health_accounts: accounts needed for health checking
/// 2. per cpi
/// 2.a. target_program_id: the target program account
/// 2.b. target_accounts: the accounts to pass to the target program
///
/// Every vault address listed in 3. must also have the matching bank and oracle appear in 1.
///
/// Every vault that is to be withdrawn from must appear in the `withdraws` instruction argument.
/// The corresponding bank may be used as an authority for vault withdrawals.
/// In addition to these accounts, there must be remaining_accounts:
/// 1. N banks (writable)
/// 2. N vaults (writable), matching the banks
/// 3. N token accounts (writable), in the same order as the vaults,
/// the loaned funds are transfered into these
#[derive(Accounts)]
pub struct FlashLoan<'info> {
pub struct FlashLoanBegin<'info> {
pub group: AccountLoader<'info, Group>,
pub token_program: Program<'info, Token>,
#[account(mut, has_one = group, has_one = owner)]
/// Instructions Sysvar for instruction introspection
#[account(address = tx_instructions::ID)]
pub instructions: UncheckedAccount<'info>,
}
/// Finalizes a flash loan
///
/// In addition to these accounts, there must be remaining_accounts:
/// 1. health accounts, and every bank that also appeared in FlashLoanBegin must be writable
/// 2. N vaults (writable), matching what was in FlashLoanBegin
/// 3. N token accounts (writable), matching what was in FlashLoanBegin;
/// the `owner` must have authority to transfer tokens out of them
#[derive(Accounts)]
pub struct FlashLoanEnd<'info> {
#[account(mut, has_one = owner)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
pub token_program: Program<'info, Token>,
}
#[derive(Debug)]
struct AllowedVault {
/// index of the vault in cpi_ais
vault_cpi_ai_index: usize,
/// index of the bank in health_ais
bank_health_ai_index: usize,
/// raw index into account.tokens
raw_token_index: usize,
/// vault amount before cpi
pre_amount: u64,
/// requested withdraw amount
withdraw_amount: u64,
/// amount of withdraw request that is a loan
loan_amount: I80F48,
}
#[derive(AnchorDeserialize, AnchorSerialize, Clone, Copy, Debug)]
pub struct FlashLoanWithdraw {
/// Account index of the vault to withdraw from in the target_accounts section.
/// Index is counted after health accounts.
pub index: u8,
/// Requested withdraw amount.
pub amount: u64,
}
#[derive(AnchorDeserialize, AnchorSerialize, Debug)]
pub struct CpiData {
pub account_start: u8,
pub data: Vec<u8>,
}
/// - `withdraws` is a list of FlashLoanWithdraw requests.
/// - `cpi_datas` is a list of bytes per cpi to call the target_program_id with.
/// - `cpi_account_starts` is a list of index into the remaining accounts per cpi to call the target_program_id with.
pub fn flash_loan<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan<'info>>,
withdraws: Vec<FlashLoanWithdraw>,
cpi_datas: Vec<CpiData>,
/// The `loan_amounts` argument lists the amount to be loaned from each bank/vault and
/// the order matches the order of bank accounts.
pub fn flash_loan_begin<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoanBegin<'info>>,
loan_amounts: Vec<u64>,
) -> Result<()> {
require!(!cpi_datas.is_empty(), MangoError::SomeError);
let num_of_cpis = cpi_datas.len();
let num_health_accounts = cpi_datas.get(0).unwrap().account_start as usize;
let num_loans = loan_amounts.len();
require_eq!(ctx.remaining_accounts.len(), 3 * num_loans);
let banks = &ctx.remaining_accounts[..num_loans];
let vaults = &ctx.remaining_accounts[num_loans..2 * num_loans];
let token_accounts = &ctx.remaining_accounts[2 * num_loans..];
let group = ctx.accounts.group.load()?;
let group_seeds = group_seeds!(group);
let seeds = [&group_seeds[..]];
// Check that the banks and vaults correspond
for (((bank_ai, vault_ai), token_account_ai), amount) in banks
.iter()
.zip(vaults.iter())
.zip(token_accounts.iter())
.zip(loan_amounts.iter())
{
let mut bank = bank_ai.load_mut::<Bank>()?;
require_keys_eq!(bank.group, ctx.accounts.group.key());
require_keys_eq!(bank.vault, *vault_ai.key);
let token_account = Account::<TokenAccount>::try_from(token_account_ai)?;
bank.flash_loan_approved_amount = *amount;
bank.flash_loan_vault_initial = token_account.amount;
// Transfer the loaned funds
if *amount > 0 {
// Provide a readable error message in case the vault doesn't have enough tokens
if token_account.amount < *amount {
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
format!(
"bank vault {} does not have enough tokens, need {} but have {}",
vault_ai.key, amount, token_account.amount
)
});
}
let transfer_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: vault_ai.clone(),
to: token_account_ai.clone(),
authority: ctx.accounts.group.to_account_info(),
},
)
.with_signer(&seeds);
token::transfer(transfer_ctx, *amount)?;
}
}
// Check if the other instructions in the transactions are compatible
{
let ixs = ctx.accounts.instructions.as_ref();
let current_index = tx_instructions::load_current_index_checked(ixs)? as usize;
// Forbid FlashLoanBegin to be called from CPI (it does not have to be the first instruction)
let current_ix = tx_instructions::load_instruction_at_checked(current_index, ixs)?;
require_msg!(
current_ix.program_id == *ctx.program_id,
"FlashLoanBegin must be a top-level instruction"
);
// The only other mango instruction that must appear before the end of the tx is
// the FlashLoanEnd instruction. No other mango instructions are allowed.
let mut index = current_index + 1;
let mut found_end = false;
loop {
let ix = match tx_instructions::load_instruction_at_checked(index, ixs) {
Ok(ix) => ix,
Err(ProgramError::InvalidArgument) => break, // past the last instruction
Err(e) => return Err(e.into()),
};
// Check that the mango program key is not used
if ix.program_id == crate::id() {
// must be the last mango ix -- this could possibly be relaxed, but right now
// we need to guard against multiple FlashLoanEnds
require_msg!(
!found_end,
"the transaction must not contain a Mango instruction after FlashLoanEnd"
);
found_end = true;
// must be the FlashLoanEnd instruction
require!(
ix.data[0..8] == [178, 170, 2, 78, 240, 23, 190, 178],
MangoError::SomeError
);
// check that the same vaults are passed
let begin_accounts = &ctx.remaining_accounts[num_loans..];
let end_accounts = &ix.accounts[ix.accounts.len() - 2 * num_loans..];
for (begin_account, end_account) in begin_accounts.iter().zip(end_accounts.iter()) {
require_msg!(*begin_account.key == end_account.pubkey, "the trailing accounts passed to FlashLoanBegin and End must match, found {} on begin and {} on end", begin_account.key, end_account.pubkey);
}
} else {
// ensure no one can cpi into mango either
for meta in ix.accounts.iter() {
require_msg!(meta.pubkey != crate::id(), "instructions between FlashLoanBegin and End may not use the Mango program account");
}
}
index += 1;
}
require_msg!(
found_end,
"found no FlashLoanEnd instruction in transaction"
);
}
Ok(())
}
struct TokenVaultChange {
token_index: TokenIndex,
bank_index: usize,
raw_token_index: usize,
amount: I80F48,
}
pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoanEnd<'info>>,
) -> Result<()> {
let mut account = ctx.accounts.account.load_mut()?;
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
// Go over the banks passed as health accounts and:
// - Ensure that all banks that are passed in have activated positions.
// This is necessary because maybe the user wants to margin trade on a token
// that the account hasn't used before.
// - Collect the addresses of all banks to potentially sign for in cpi_ais.
// - Collect the addresses of all bank vaults.
// Note: This depends on the particular health account ordering.
let health_ais = &ctx.remaining_accounts[0..num_health_accounts];
let mut allowed_banks = HashMap::<&Pubkey, Ref<Bank>>::new();
// vault pubkey -> (bank_account_index, raw_token_index)
let mut allowed_vaults = HashMap::<Pubkey, (usize, usize)>::new();
for (i, ai) in health_ais.iter().enumerate() {
match ai.load::<Bank>() {
Ok(bank) => {
require!(bank.group == account.fixed.group, MangoError::SomeError);
let (_, raw_token_index, _) = account.token_get_mut_or_create(bank.token_index)?;
allowed_vaults.insert(bank.vault, (i, raw_token_index));
allowed_banks.insert(ai.key, bank);
// Find index at which vaults start
let vaults_index = ctx
.remaining_accounts
.iter()
.position(|ai| {
let maybe_token_account = Account::<TokenAccount>::try_from(ai);
if maybe_token_account.is_err() {
return false;
}
Err(Error::AnchorError(error))
if error.error_code_number == ErrorCode::AccountDiscriminatorMismatch as u32
|| error.error_code_number == ErrorCode::AccountOwnedByWrongProgram as u32 =>
{
break;
}
Err(error) => return Err(error),
maybe_token_account.unwrap().owner == account.fixed.group
})
.ok_or_else(|| error_msg!("expected at least one vault token account to be passed"))?;
let vaults_len = (ctx.remaining_accounts.len() - vaults_index) / 2;
require_eq!(ctx.remaining_accounts.len(), vaults_index + 2 * vaults_len);
// First initialize to the remaining delegated amount
let health_ais = &ctx.remaining_accounts[..vaults_index];
let vaults = &ctx.remaining_accounts[vaults_index..vaults_index + vaults_len];
let token_accounts = &ctx.remaining_accounts[vaults_index + vaults_len..];
let mut vaults_with_banks = vec![false; vaults.len()];
// Loop over the banks, finding matching vaults
// TODO: must be moved into health.rs, because it assumes something about the health accounts structure
let mut changes = vec![];
for (i, bank_ai) in health_ais.iter().enumerate() {
// iterate until the first non-bank
let bank = match bank_ai.load::<Bank>() {
Ok(b) => b,
Err(_) => break,
};
// find a vault -- if there's none, skip
let (vault_index, vault_ai) = match vaults
.iter()
.enumerate()
.find(|(_, vault_ai)| vault_ai.key == &bank.vault)
{
Some(v) => v,
None => continue,
};
vaults_with_banks[vault_index] = true;
let token_account_ai = &token_accounts[vault_index];
let token_account = Account::<TokenAccount>::try_from(&token_account_ai)?;
// Ensure this bank/vault combination was mentioned in the Begin instruction:
// The Begin instruction only checks that End ends with the same vault accounts -
// but there could be an extra vault account in End, or a different bank could be
// used for the same vault.
require_neq!(bank.flash_loan_vault_initial, u64::MAX);
// Create the token position now, so we can compute the pre-health with fixed order health accounts
let (_, raw_token_index, _) = account.token_get_mut_or_create(bank.token_index)?;
// Transfer any excess over the inital balance of the token account back
// into the vault. Compute the total change in the vault balance.
let mut change = -I80F48::from(bank.flash_loan_approved_amount);
if token_account.amount > bank.flash_loan_vault_initial {
let transfer_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: token_account_ai.clone(),
to: vault_ai.clone(),
authority: ctx.accounts.owner.to_account_info(),
},
);
let repay = token_account.amount - bank.flash_loan_vault_initial;
token::transfer(transfer_ctx, repay)?;
let repay = I80F48::from(repay);
change = cm!(change + repay);
}
changes.push(TokenVaultChange {
token_index: bank.token_index,
bank_index: i,
raw_token_index,
amount: change,
});
}
// all vaults must have had matching banks
for (i, has_bank) in vaults_with_banks.iter().enumerate() {
require_msg!(
has_bank,
"missing bank for vault index {}, address {}",
i,
vaults[i].key
);
}
// Check pre-cpi health
// NOTE: This health check isn't strictly necessary. It will be, later, when
// we want to have reduce_only or be able to move an account out of bankruptcy.
{
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
let pre_cpi_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
require!(pre_cpi_health >= 0, MangoError::HealthMustBePositive);
msg!("pre_cpi_health {:?}", pre_cpi_health);
}
let all_cpi_ais = &ctx.remaining_accounts[num_health_accounts..];
let mut all_cpi_ams = all_cpi_ais
.iter()
.flat_map(|item| item.to_account_metas(None))
.collect::<Vec<_>>();
require!(
all_cpi_ais.len() == all_cpi_ams.len(),
MangoError::SomeError
);
// Check that each group-owned token account is the vault of one of the allowed banks,
// and track its balance.
let mut used_vaults = all_cpi_ais
.iter()
.enumerate()
.filter_map(|(i, ai)| {
if ai.owner != &TokenAccount::owner() {
return None;
}
// Skip mints and other accounts that may be owned by the spl_token program
let maybe_token_account = Account::<TokenAccount>::try_from(ai);
if maybe_token_account.is_err() {
return None;
}
let token_account = maybe_token_account.unwrap();
if token_account.owner != ctx.accounts.group.key() {
return None;
}
// Every group-owned token account must be a vault of one of the banks.
if let Some(&(bank_index, raw_token_index)) = allowed_vaults.get(&ai.key) {
return Some(Ok((
ai.key,
AllowedVault {
vault_cpi_ai_index: i,
bank_health_ai_index: bank_index,
raw_token_index,
pre_amount: token_account.amount,
// these two are updated later
withdraw_amount: 0,
loan_amount: I80F48::ZERO,
},
)));
}
// This is to protect users, because if their cpi program sends deposits to a vault
// and they forgot to pass in the bank for the vault, their account would not be credited.
Some(Err(error!(MangoError::SomeError)))
})
.collect::<Result<HashMap<_, _>>>()?;
// Store the indexed value before the margin trade for logging purposes
let mut pre_indexed_positions = Vec::new();
for (_, info) in used_vaults.iter() {
let position = account.token_get_raw(info.raw_token_index);
pre_indexed_positions.push(position.indexed_position.to_bits());
}
// Find banks for used vaults in cpi_ais and collect signer seeds for them.
// Also update withdraw_amount and loan_amount.
let mut bank_signer_data = Vec::with_capacity(used_vaults.len());
for (ai, am) in all_cpi_ais.iter().zip(all_cpi_ams.iter_mut()) {
if ai.owner != &Mango::id() {
continue;
}
if let Some(bank) = allowed_banks.get(ai.key) {
if let Some(vault_info) = used_vaults.get_mut(&bank.vault) {
let withdraw_amount = withdraws
.iter()
.find_map(|&withdraw| {
(withdraw.index as usize == vault_info.vault_cpi_ai_index)
.then(|| withdraw.amount)
})
// Even if we don't withdraw from a vault we still need to track it:
// Possibly the invoked program will deposit funds into it.
.unwrap_or(0);
require!(
withdraw_amount <= vault_info.pre_amount,
MangoError::SomeError
);
vault_info.withdraw_amount = withdraw_amount;
// if there are withdraws: figure out loan amount, mark as signer
if withdraw_amount > 0 {
let token_account = account.token_get_mut_raw(vault_info.raw_token_index);
let native_position = token_account.native(&bank);
vault_info.loan_amount = if native_position > 0 {
(I80F48::from(withdraw_amount) - native_position).max(I80F48::ZERO)
} else {
I80F48::from(withdraw_amount)
};
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.bank_num.to_le_bytes(),
[bank.bump],
));
}
}
}
}
// Approve bank delegates for withdrawals
let group_seeds = group_seeds!(group);
let seeds = [&group_seeds[..]];
for (_, vault_info) in used_vaults.iter() {
if vault_info.withdraw_amount > 0 {
let approve_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Approve {
to: all_cpi_ais[vault_info.vault_cpi_ai_index].clone(),
delegate: health_ais[vault_info.bank_health_ai_index].clone(),
authority: ctx.accounts.group.to_account_info(),
},
)
.with_signer(&seeds);
token::approve(approve_ctx, vault_info.withdraw_amount)?;
}
}
msg!("withdraws {:#?}", withdraws);
msg!("cpi_datas {:#?}", cpi_datas);
msg!("allowed_vaults {:#?}", allowed_vaults);
msg!("used_vaults {:#?}", used_vaults);
// get rid of Ref<> to avoid limiting the cpi call
drop(allowed_banks);
drop(group);
drop(account);
// prepare signer seeds and invoke cpi
let group_key = ctx.accounts.group.key();
let signers = bank_signer_data
.iter()
.map(|(token_index, bank_num, bump)| {
[
group_key.as_ref(),
b"Bank".as_ref(),
&token_index[..],
&bank_num[..],
&bump[..],
]
})
.collect::<Vec<_>>();
let signers_ref = signers.iter().map(|v| &v[..]).collect::<Vec<_>>();
for (cpi_index, cpi_data) in cpi_datas.iter().enumerate() {
let cpi_account_start = cpi_data.account_start as usize;
let cpi_program_id = *ctx.remaining_accounts[cpi_account_start].key;
require_keys_neq!(cpi_program_id, crate::id(), MangoError::SomeError);
let all_cpi_ais_end_index = if cpi_index == num_of_cpis - 1 {
all_cpi_ams.len()
} else {
cpi_datas[cpi_index + 1].account_start as usize - num_health_accounts
};
let all_cpi_ais_start_index = cpi_account_start - num_health_accounts + 1;
let cpi_ais = &all_cpi_ais[all_cpi_ais_start_index..all_cpi_ais_end_index];
let cpi_ams = &all_cpi_ams[all_cpi_ais_start_index..all_cpi_ais_end_index];
let cpi_ix = Instruction {
program_id: cpi_program_id,
// todo future: optimise out these to_vecs
data: cpi_data.data.to_vec(),
accounts: cpi_ams.to_vec(),
};
solana_program::program::invoke_signed(&cpi_ix, &cpi_ais, &signers_ref)?;
}
// Revoke delegates for vaults
let group = ctx.accounts.group.load()?;
let group_seeds = group_seeds!(group);
for (_, vault_info) in used_vaults.iter() {
if vault_info.withdraw_amount > 0 {
let ix = token::spl_token::instruction::revoke(
&token::spl_token::ID,
all_cpi_ais[vault_info.vault_cpi_ai_index].key,
&ctx.accounts.group.key(),
&[],
)?;
solana_program::program::invoke_signed(
&ix,
&[
all_cpi_ais[vault_info.vault_cpi_ai_index].clone(),
ctx.accounts.group.to_account_info(),
],
&[group_seeds],
)?;
}
}
// Track vault changes and apply them to the user's token positions
let mut account = ctx.accounts.account.load_mut()?;
let inactive_tokens = adjust_for_post_cpi_vault_amounts(
health_ais,
all_cpi_ais,
&used_vaults,
&mut account.borrow_mut(),
)?;
// Check post-cpi health
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
let post_cpi_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
require!(post_cpi_health >= 0, MangoError::HealthMustBePositive);
msg!("post_cpi_health {:?}", post_cpi_health);
let pre_cpi_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
require!(pre_cpi_health >= 0, MangoError::HealthMustBePositive);
msg!("pre_cpi_health {:?}", pre_cpi_health);
// Token balances logging
let mut token_indexes = Vec::with_capacity(used_vaults.len());
let mut post_indexed_positions = Vec::with_capacity(used_vaults.len());
for (_, info) in used_vaults.iter() {
let position = account.token_get_raw(info.raw_token_index);
post_indexed_positions.push(position.indexed_position.to_bits());
token_indexes.push(position.token_index as u16);
let (bank, oracle_price) = retriever.bank_and_oracle(
&ctx.accounts.group.key(),
info.bank_health_ai_index,
position.token_index,
// Prices for logging
let mut prices = vec![];
for change in &changes {
let (_, oracle_price) = retriever.bank_and_oracle(
&account.fixed.group,
change.bank_index,
change.token_index,
)?;
prices.push(oracle_price);
}
// Drop retriever as mut bank below uses health_ais
drop(retriever);
// Apply the vault diffs to the bank positions
let mut deactivated_token_positions = vec![];
let mut token_loan_details = Vec::with_capacity(changes.len());
for (change, price) in changes.iter().zip(prices.iter()) {
let mut bank = health_ais[change.bank_index].load_mut::<Bank>()?;
let position = account.token_get_mut_raw(change.raw_token_index);
let native = position.native(&bank);
let approved_amount = I80F48::from(bank.flash_loan_approved_amount);
let loan = if native.is_positive() {
cm!(approved_amount - native).max(I80F48::ZERO)
} else {
approved_amount
};
let loan_origination_fee = cm!(loan * bank.loan_origination_fee_rate);
bank.collected_fees_native = cm!(bank.collected_fees_native + loan_origination_fee);
let is_active =
bank.change_without_fee(position, cm!(change.amount - loan_origination_fee))?;
if !is_active {
deactivated_token_positions.push(change.raw_token_index);
}
bank.flash_loan_approved_amount = 0;
bank.flash_loan_vault_initial = u64::MAX;
token_loan_details.push(FlashLoanTokenDetail {
token_index: position.token_index,
change_amount: change.amount.to_bits(),
loan: loan.to_bits(),
loan_origination_fee: loan_origination_fee.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: price.to_bits(),
});
emit!(TokenBalanceLog {
mango_account: ctx.accounts.account.key(),
token_index: bank.token_index as u16,
indexed_position: position.indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: oracle_price.to_bits(),
price: price.to_bits(),
});
}
emit!(MarginTradeLog {
emit!(FlashLoanLog {
mango_account: ctx.accounts.account.key(),
token_indexes,
pre_indexed_positions,
post_indexed_positions,
token_loan_details
});
// Deactivate inactive token accounts at the end
for raw_token_index in inactive_tokens {
// Check post-cpi health
let post_cpi_health =
compute_health_from_fixed_accounts(&account.borrow(), HealthType::Init, health_ais)?;
require!(post_cpi_health >= 0, MangoError::HealthMustBePositive);
msg!("post_cpi_health {:?}", post_cpi_health);
// Deactivate inactive token accounts after health check
for raw_token_index in deactivated_token_positions {
account.token_deactivate(raw_token_index);
}
Ok(())
}
fn adjust_for_post_cpi_vault_amounts(
health_ais: &[AccountInfo],
cpi_ais: &[AccountInfo],
used_vaults: &HashMap<&Pubkey, AllowedVault>,
account: &mut MangoAccountRefMut,
) -> Result<Vec<usize>> {
let mut inactive_token_raw_indexes = Vec::with_capacity(used_vaults.len());
for (_, info) in used_vaults.iter() {
let vault = Account::<TokenAccount>::try_from(&cpi_ais[info.vault_cpi_ai_index]).unwrap();
let mut bank = health_ais[info.bank_health_ai_index].load_mut::<Bank>()?;
let position = account.token_get_mut_raw(info.raw_token_index);
let loan_origination_fee = info.loan_amount * bank.loan_origination_fee_rate;
bank.collected_fees_native += loan_origination_fee;
let is_active = bank.change_without_fee(
position,
I80F48::from(vault.amount) - I80F48::from(info.pre_amount) - loan_origination_fee,
)?;
if !is_active {
inactive_token_raw_indexes.push(info.raw_token_index);
}
}
Ok(inactive_token_raw_indexes)
}

View File

@ -1,328 +0,0 @@
use crate::accounts_zerocopy::*;
use crate::error::MangoError;
use crate::group_seeds;
use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog};
use crate::state::{
compute_health, compute_health_from_fixed_accounts, new_fixed_order_account_retriever,
AccountLoaderDynamic, AccountRetriever, Bank, Group, HealthType, MangoAccount, TokenIndex,
};
use crate::util::checked_math as cm;
use anchor_lang::prelude::*;
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
use anchor_spl::token::{self, Token, TokenAccount};
use fixed::types::I80F48;
/// Sets up mango vaults for flash loan
///
/// In addition to these accounts, there must be a sequence of remaining_accounts:
/// 1. N banks
/// 2. N vaults, matching the banks
#[derive(Accounts)]
pub struct FlashLoan2Begin<'info> {
pub group: AccountLoader<'info, Group>,
pub temporary_vault_authority: Signer<'info>,
pub token_program: Program<'info, Token>,
#[account(address = tx_instructions::ID)]
pub instructions: UncheckedAccount<'info>,
}
/// Finalizes a flash loan
///
/// In addition to these accounts, there must be a sequence of remaining_accounts:
/// 1. health accounts
/// 2. N vaults, matching what was in FlashLoan2Begin
#[derive(Accounts)]
pub struct FlashLoan2End<'info> {
pub group: AccountLoader<'info, Group>,
#[account(mut, has_one = group, has_one = owner)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
pub token_program: Program<'info, Token>,
}
pub fn flash_loan2_begin<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan2Begin<'info>>,
loan_amounts: Vec<u64>,
) -> Result<()> {
let num_loans = loan_amounts.len();
require_eq!(
ctx.remaining_accounts.len(),
2 * num_loans,
MangoError::SomeError
);
let banks = &ctx.remaining_accounts[..num_loans];
let vaults = &ctx.remaining_accounts[num_loans..];
let group = ctx.accounts.group.load()?;
let group_seeds = group_seeds!(group);
let seeds = [&group_seeds[..]];
// Check that the banks and vaults correspond
for ((bank_ai, vault_ai), amount) in banks.iter().zip(vaults.iter()).zip(loan_amounts.iter()) {
let mut bank = bank_ai.load_mut::<Bank>()?;
require_keys_eq!(bank.group, ctx.accounts.group.key());
require_keys_eq!(bank.vault, *vault_ai.key);
let token_account = Account::<TokenAccount>::try_from(vault_ai)?;
bank.flash_loan_approved_amount = *amount;
bank.flash_loan_vault_initial = token_account.amount;
// Approve the withdraw
if *amount > 0 {
let approve_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Approve {
to: vault_ai.clone(),
delegate: ctx.accounts.temporary_vault_authority.to_account_info(),
authority: ctx.accounts.group.to_account_info(),
},
)
.with_signer(&seeds);
token::approve(approve_ctx, *amount)?;
}
}
// Check if the other instructions in the transactions are compatible
{
let ixs = ctx.accounts.instructions.as_ref();
let current_index = tx_instructions::load_current_index_checked(ixs)? as usize;
// Forbid FlashLoan2Begin to be called from CPI (it does not have to be the first instruction)
let current_ix = tx_instructions::load_instruction_at_checked(current_index, ixs)?;
require_keys_eq!(
current_ix.program_id,
*ctx.program_id,
MangoError::SomeError
);
// The only other mango instruction that must appear before the end of the tx is
// the FlashLoan2End instruction. No other mango instructions are allowed.
let mut index = current_index + 1;
let mut found_end = false;
loop {
let ix = match tx_instructions::load_instruction_at_checked(index, ixs) {
Ok(ix) => ix,
Err(ProgramError::InvalidArgument) => break, // past the last instruction
Err(e) => Err(e)?,
};
// Check that the mango program key is not used
if ix.program_id == crate::id() {
// must be the last mango ix -- this could possibly be relaxed, but right now
// we need to guard against multiple FlashLoanEnds
require!(!found_end, MangoError::SomeError);
found_end = true;
// must be the FlashLoan2End instruction
require!(
&ix.data[0..8] == &[187, 107, 239, 212, 18, 21, 145, 171],
MangoError::SomeError
);
// check that the same vaults are passed
let end_vaults = &ix.accounts[ix.accounts.len() - num_loans..];
for (start_vault, end_vault) in vaults.iter().zip(end_vaults.iter()) {
require_keys_eq!(*start_vault.key, end_vault.pubkey);
}
} else {
// ensure no one can cpi into mango either
for meta in ix.accounts.iter() {
require_keys_neq!(meta.pubkey, crate::id());
}
}
index += 1;
}
require!(found_end, MangoError::SomeError);
}
Ok(())
}
struct TokenVaultChange {
bank_index: usize,
raw_token_index: usize,
amount: I80F48,
}
// Remaining accounts:
// 1. health
// 2. vaults (must be same as FlashLoanStart)
pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan2End<'info>>,
) -> Result<()> {
let group = ctx.accounts.group.load()?;
let group_seeds = group_seeds!(group);
let mut account = ctx.accounts.account.load_mut()?;
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
// Find index at which vaults start
let vaults_index = ctx
.remaining_accounts
.iter()
.position(|ai| {
let maybe_token_account = Account::<TokenAccount>::try_from(ai);
if maybe_token_account.is_err() {
return false;
}
maybe_token_account.unwrap().owner == account.fixed.group
})
.ok_or_else(|| error!(MangoError::SomeError))?;
// First initialize to the remaining delegated amount
let health_ais = &ctx.remaining_accounts[..vaults_index];
let vaults = &ctx.remaining_accounts[vaults_index..];
let mut vaults_with_banks = vec![false; vaults.len()];
// Loop over the banks, finding matching vaults
// TODO: must be moved into health.rs, because it assumes something about the health accounts structure
let mut changes = vec![];
for (i, bank_ai) in health_ais.iter().enumerate() {
// iterate until the first non-bank
let bank = match bank_ai.load::<Bank>() {
Ok(b) => b,
Err(_) => break,
};
// find a vault -- if there's none, skip
let (vault_index, vault_ai) = match vaults
.iter()
.enumerate()
.find(|(_, vault_ai)| vault_ai.key == &bank.vault)
{
Some(v) => v,
None => continue,
};
let vault = Account::<TokenAccount>::try_from(vault_ai)?;
vaults_with_banks[vault_index] = true;
// Ensure this bank/vault combination was mentioned in the Begin instruction:
// The Begin instruction only checks that End ends with the same vault accounts -
// but there could be an extra vault account in End, or a different bank could be
// used for the same vault.
require_neq!(bank.flash_loan_vault_initial, u64::MAX);
// Create the token position now, so we can compute the pre-health with fixed order health accounts
let (_, raw_token_index, _) = account.token_get_mut_or_create(bank.token_index)?;
// Revoke delegation
let ix = token::spl_token::instruction::revoke(
&token::spl_token::ID,
vault_ai.key,
&ctx.accounts.group.key(),
&[],
)?;
solana_program::program::invoke_signed(
&ix,
&[vault_ai.clone(), ctx.accounts.group.to_account_info()],
&[group_seeds],
)?;
// Track vault difference
let new_amount = I80F48::from(vault.amount);
let old_amount = I80F48::from(bank.flash_loan_vault_initial);
let change = cm!(new_amount - old_amount);
changes.push(TokenVaultChange {
bank_index: i,
raw_token_index,
amount: change,
});
}
// all vaults must have had matching banks
require!(vaults_with_banks.iter().all(|&b| b), MangoError::SomeError);
// Check pre-cpi health
// NOTE: This health check isn't strictly necessary. It will be, later, when
// we want to have reduce_only or be able to move an account out of bankruptcy.
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
let pre_cpi_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
require!(pre_cpi_health >= 0, MangoError::HealthMustBePositive);
msg!("pre_cpi_health {:?}", pre_cpi_health);
// Prices for logging
let mut prices = vec![];
for change in &changes {
let (_, oracle_price) = retriever.bank_and_oracle(
&account.fixed.group,
change.bank_index,
change.raw_token_index as TokenIndex,
)?;
prices.push(oracle_price);
}
// Drop retriever as mut bank below uses health_ais
drop(retriever);
// Apply the vault diffs to the bank positions
let mut deactivated_token_positions = vec![];
let mut token_loan_details = Vec::with_capacity(changes.len());
for (change, price) in changes.iter().zip(prices.iter()) {
let mut bank = health_ais[change.bank_index].load_mut::<Bank>()?;
let position = account.token_get_mut_raw(change.raw_token_index);
let native = position.native(&bank);
let approved_amount = I80F48::from(bank.flash_loan_approved_amount);
let loan = if native.is_positive() {
cm!(approved_amount - native).max(I80F48::ZERO)
} else {
approved_amount
};
let loan_origination_fee = cm!(loan * bank.loan_origination_fee_rate);
bank.collected_fees_native = cm!(bank.collected_fees_native + loan_origination_fee);
let is_active =
bank.change_without_fee(position, cm!(change.amount - loan_origination_fee))?;
if !is_active {
deactivated_token_positions.push(change.raw_token_index);
}
bank.flash_loan_approved_amount = 0;
bank.flash_loan_vault_initial = u64::MAX;
token_loan_details.push(FlashLoanTokenDetail {
token_index: position.token_index,
change_amount: change.amount.to_bits(),
loan: loan.to_bits(),
loan_origination_fee: loan_origination_fee.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: price.to_bits(),
});
emit!(TokenBalanceLog {
mango_account: ctx.accounts.account.key(),
token_index: bank.token_index as u16,
indexed_position: position.indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: price.to_bits(),
});
}
emit!(FlashLoanLog {
mango_account: ctx.accounts.account.key(),
token_loan_details
});
// Check post-cpi health
let post_cpi_health =
compute_health_from_fixed_accounts(&account.borrow(), HealthType::Init, health_ais)?;
require!(post_cpi_health >= 0, MangoError::HealthMustBePositive);
msg!("post_cpi_health {:?}", post_cpi_health);
// Deactivate inactive token accounts after health check
for raw_token_index in deactivated_token_positions {
account.token_deactivate(raw_token_index);
}
Ok(())
}

View File

@ -1,361 +0,0 @@
use crate::accounts_zerocopy::*;
use crate::error::*;
use crate::group_seeds;
use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog};
use crate::state::MangoAccount;
use crate::state::{
compute_health, compute_health_from_fixed_accounts, new_fixed_order_account_retriever,
AccountLoaderDynamic, AccountRetriever, Bank, Group, HealthType, TokenIndex,
};
use crate::util::checked_math as cm;
use anchor_lang::prelude::*;
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
use anchor_spl::token::{self, Token, TokenAccount};
use fixed::types::I80F48;
/// Sets up mango vaults for flash loan
///
/// In addition to these accounts, there must be remaining_accounts:
/// 1. N banks (writable)
/// 2. N vaults (writable), matching the banks
/// 3. N token accounts (writable), in the same order as the vaults,
/// the loaned funds are transfered into these
#[derive(Accounts)]
pub struct FlashLoan3Begin<'info> {
pub group: AccountLoader<'info, Group>,
pub token_program: Program<'info, Token>,
/// Instructions Sysvar for instruction introspection
#[account(address = tx_instructions::ID)]
pub instructions: UncheckedAccount<'info>,
}
/// Finalizes a flash loan
///
/// In addition to these accounts, there must be remaining_accounts:
/// 1. health accounts, and every bank that also appeared in FlashLoan3Begin must be writable
/// 2. N vaults (writable), matching what was in FlashLoan3Begin
/// 3. N token accounts (writable), matching what was in FlashLoan3Begin;
/// the `owner` must have authority to transfer tokens out of them
#[derive(Accounts)]
pub struct FlashLoan3End<'info> {
#[account(mut, has_one = owner)]
pub account: AccountLoaderDynamic<'info, MangoAccount>,
pub owner: Signer<'info>,
pub token_program: Program<'info, Token>,
}
/// The `loan_amounts` argument lists the amount to be loaned from each bank/vault and
/// the order matches the order of bank accounts.
pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan3Begin<'info>>,
loan_amounts: Vec<u64>,
) -> Result<()> {
let num_loans = loan_amounts.len();
require_eq!(ctx.remaining_accounts.len(), 3 * num_loans);
let banks = &ctx.remaining_accounts[..num_loans];
let vaults = &ctx.remaining_accounts[num_loans..2 * num_loans];
let token_accounts = &ctx.remaining_accounts[2 * num_loans..];
let group = ctx.accounts.group.load()?;
let group_seeds = group_seeds!(group);
let seeds = [&group_seeds[..]];
// Check that the banks and vaults correspond
for (((bank_ai, vault_ai), token_account_ai), amount) in banks
.iter()
.zip(vaults.iter())
.zip(token_accounts.iter())
.zip(loan_amounts.iter())
{
let mut bank = bank_ai.load_mut::<Bank>()?;
require_keys_eq!(bank.group, ctx.accounts.group.key());
require_keys_eq!(bank.vault, *vault_ai.key);
let token_account = Account::<TokenAccount>::try_from(token_account_ai)?;
bank.flash_loan_approved_amount = *amount;
bank.flash_loan_vault_initial = token_account.amount;
// Transfer the loaned funds
if *amount > 0 {
// Provide a readable error message in case the vault doesn't have enough tokens
if token_account.amount < *amount {
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
format!(
"bank vault {} does not have enough tokens, need {} but have {}",
vault_ai.key, amount, token_account.amount
)
});
}
let transfer_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: vault_ai.clone(),
to: token_account_ai.clone(),
authority: ctx.accounts.group.to_account_info(),
},
)
.with_signer(&seeds);
token::transfer(transfer_ctx, *amount)?;
}
}
// Check if the other instructions in the transactions are compatible
{
let ixs = ctx.accounts.instructions.as_ref();
let current_index = tx_instructions::load_current_index_checked(ixs)? as usize;
// Forbid FlashLoan3Begin to be called from CPI (it does not have to be the first instruction)
let current_ix = tx_instructions::load_instruction_at_checked(current_index, ixs)?;
require_msg!(
current_ix.program_id == *ctx.program_id,
"FlashLoan3Begin must be a top-level instruction"
);
// The only other mango instruction that must appear before the end of the tx is
// the FlashLoan3End instruction. No other mango instructions are allowed.
let mut index = current_index + 1;
let mut found_end = false;
loop {
let ix = match tx_instructions::load_instruction_at_checked(index, ixs) {
Ok(ix) => ix,
Err(ProgramError::InvalidArgument) => break, // past the last instruction
Err(e) => return Err(e.into()),
};
// Check that the mango program key is not used
if ix.program_id == crate::id() {
// must be the last mango ix -- this could possibly be relaxed, but right now
// we need to guard against multiple FlashLoanEnds
require_msg!(
!found_end,
"the transaction must not contain a Mango instruction after FlashLoan3End"
);
found_end = true;
// must be the FlashLoan3End instruction
require!(
ix.data[0..8] == [163, 231, 155, 56, 201, 68, 84, 148],
MangoError::SomeError
);
// check that the same vaults are passed
let begin_accounts = &ctx.remaining_accounts[num_loans..];
let end_accounts = &ix.accounts[ix.accounts.len() - 2 * num_loans..];
for (begin_account, end_account) in begin_accounts.iter().zip(end_accounts.iter()) {
require_msg!(*begin_account.key == end_account.pubkey, "the trailing accounts passed to FlashLoan3Begin and End must match, found {} on begin and {} on end", begin_account.key, end_account.pubkey);
}
} else {
// ensure no one can cpi into mango either
for meta in ix.accounts.iter() {
require_msg!(meta.pubkey != crate::id(), "instructions between FlashLoan3Begin and End may not use the Mango program account");
}
}
index += 1;
}
require_msg!(
found_end,
"found no FlashLoan3End instruction in transaction"
);
}
Ok(())
}
struct TokenVaultChange {
token_index: TokenIndex,
bank_index: usize,
raw_token_index: usize,
amount: I80F48,
}
pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan3End<'info>>,
) -> Result<()> {
let mut account = ctx.accounts.account.load_mut()?;
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
// Find index at which vaults start
let vaults_index = ctx
.remaining_accounts
.iter()
.position(|ai| {
let maybe_token_account = Account::<TokenAccount>::try_from(ai);
if maybe_token_account.is_err() {
return false;
}
maybe_token_account.unwrap().owner == account.fixed.group
})
.ok_or_else(|| error_msg!("expected at least one vault token account to be passed"))?;
let vaults_len = (ctx.remaining_accounts.len() - vaults_index) / 2;
require_eq!(ctx.remaining_accounts.len(), vaults_index + 2 * vaults_len);
// First initialize to the remaining delegated amount
let health_ais = &ctx.remaining_accounts[..vaults_index];
let vaults = &ctx.remaining_accounts[vaults_index..vaults_index + vaults_len];
let token_accounts = &ctx.remaining_accounts[vaults_index + vaults_len..];
let mut vaults_with_banks = vec![false; vaults.len()];
// Loop over the banks, finding matching vaults
// TODO: must be moved into health.rs, because it assumes something about the health accounts structure
let mut changes = vec![];
for (i, bank_ai) in health_ais.iter().enumerate() {
// iterate until the first non-bank
let bank = match bank_ai.load::<Bank>() {
Ok(b) => b,
Err(_) => break,
};
// find a vault -- if there's none, skip
let (vault_index, vault_ai) = match vaults
.iter()
.enumerate()
.find(|(_, vault_ai)| vault_ai.key == &bank.vault)
{
Some(v) => v,
None => continue,
};
vaults_with_banks[vault_index] = true;
let token_account_ai = &token_accounts[vault_index];
let token_account = Account::<TokenAccount>::try_from(&token_account_ai)?;
// Ensure this bank/vault combination was mentioned in the Begin instruction:
// The Begin instruction only checks that End ends with the same vault accounts -
// but there could be an extra vault account in End, or a different bank could be
// used for the same vault.
require_neq!(bank.flash_loan_vault_initial, u64::MAX);
// Create the token position now, so we can compute the pre-health with fixed order health accounts
let (_, raw_token_index, _) = account.token_get_mut_or_create(bank.token_index)?;
// Transfer any excess over the inital balance of the token account back
// into the vault. Compute the total change in the vault balance.
let mut change = -I80F48::from(bank.flash_loan_approved_amount);
if token_account.amount > bank.flash_loan_vault_initial {
let transfer_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::Transfer {
from: token_account_ai.clone(),
to: vault_ai.clone(),
authority: ctx.accounts.owner.to_account_info(),
},
);
let repay = token_account.amount - bank.flash_loan_vault_initial;
token::transfer(transfer_ctx, repay)?;
let repay = I80F48::from(repay);
change = cm!(change + repay);
}
changes.push(TokenVaultChange {
token_index: bank.token_index,
bank_index: i,
raw_token_index,
amount: change,
});
}
// all vaults must have had matching banks
for (i, has_bank) in vaults_with_banks.iter().enumerate() {
require_msg!(
has_bank,
"missing bank for vault index {}, address {}",
i,
vaults[i].key
);
}
// Check pre-cpi health
// NOTE: This health check isn't strictly necessary. It will be, later, when
// we want to have reduce_only or be able to move an account out of bankruptcy.
let retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
let pre_cpi_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
require!(pre_cpi_health >= 0, MangoError::HealthMustBePositive);
msg!("pre_cpi_health {:?}", pre_cpi_health);
// Prices for logging
let mut prices = vec![];
for change in &changes {
let (_, oracle_price) = retriever.bank_and_oracle(
&account.fixed.group,
change.bank_index,
change.token_index,
)?;
prices.push(oracle_price);
}
// Drop retriever as mut bank below uses health_ais
drop(retriever);
// Apply the vault diffs to the bank positions
let mut deactivated_token_positions = vec![];
let mut token_loan_details = Vec::with_capacity(changes.len());
for (change, price) in changes.iter().zip(prices.iter()) {
let mut bank = health_ais[change.bank_index].load_mut::<Bank>()?;
let position = account.token_get_mut_raw(change.raw_token_index);
let native = position.native(&bank);
let approved_amount = I80F48::from(bank.flash_loan_approved_amount);
let loan = if native.is_positive() {
cm!(approved_amount - native).max(I80F48::ZERO)
} else {
approved_amount
};
let loan_origination_fee = cm!(loan * bank.loan_origination_fee_rate);
bank.collected_fees_native = cm!(bank.collected_fees_native + loan_origination_fee);
let is_active =
bank.change_without_fee(position, cm!(change.amount - loan_origination_fee))?;
if !is_active {
deactivated_token_positions.push(change.raw_token_index);
}
bank.flash_loan_approved_amount = 0;
bank.flash_loan_vault_initial = u64::MAX;
token_loan_details.push(FlashLoanTokenDetail {
token_index: position.token_index,
change_amount: change.amount.to_bits(),
loan: loan.to_bits(),
loan_origination_fee: loan_origination_fee.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: price.to_bits(),
});
emit!(TokenBalanceLog {
mango_account: ctx.accounts.account.key(),
token_index: bank.token_index as u16,
indexed_position: position.indexed_position.to_bits(),
deposit_index: bank.deposit_index.to_bits(),
borrow_index: bank.borrow_index.to_bits(),
price: price.to_bits(),
});
}
emit!(FlashLoanLog {
mango_account: ctx.accounts.account.key(),
token_loan_details
});
// Check post-cpi health
let post_cpi_health =
compute_health_from_fixed_accounts(&account.borrow(), HealthType::Init, health_ais)?;
require!(post_cpi_health >= 0, MangoError::HealthMustBePositive);
msg!("post_cpi_health {:?}", post_cpi_health);
// Deactivate inactive token accounts after health check
for raw_token_index in deactivated_token_positions {
account.token_deactivate(raw_token_index);
}
Ok(())
}

View File

@ -5,8 +5,6 @@ pub use account_expand::*;
pub use benchmark::*;
pub use compute_account_data::*;
pub use flash_loan::*;
pub use flash_loan2::*;
pub use flash_loan3::*;
pub use group_close::*;
pub use group_create::*;
pub use group_edit::*;
@ -49,8 +47,6 @@ mod account_expand;
mod benchmark;
mod compute_account_data;
mod flash_loan;
mod flash_loan2;
mod flash_loan3;
mod group_close;
mod group_create;
mod group_edit;

View File

@ -193,39 +193,18 @@ pub mod mango_v4 {
instructions::token_withdraw(ctx, amount, allow_borrow)
}
pub fn flash_loan<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan<'info>>,
withdraws: Vec<FlashLoanWithdraw>,
cpi_datas: Vec<CpiData>,
) -> Result<()> {
instructions::flash_loan(ctx, withdraws, cpi_datas)
}
pub fn flash_loan2_begin<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan2Begin<'info>>,
pub fn flash_loan_begin<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoanBegin<'info>>,
loan_amounts: Vec<u64>,
) -> Result<()> {
instructions::flash_loan2_begin(ctx, loan_amounts)
instructions::flash_loan_begin(ctx, loan_amounts)
}
pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan2End<'info>>,
// NOTE: keep disc synced in flash_loan.rs
pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoanEnd<'info>>,
) -> Result<()> {
instructions::flash_loan2_end(ctx)
}
pub fn flash_loan3_begin<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan3Begin<'info>>,
loan_amounts: Vec<u64>,
) -> Result<()> {
instructions::flash_loan3_begin(ctx, loan_amounts)
}
// NOTE: keep disc synced in flash_loan3.rs
pub fn flash_loan3_end<'key, 'accounts, 'remaining, 'info>(
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan3End<'info>>,
) -> Result<()> {
instructions::flash_loan3_end(ctx)
instructions::flash_loan_end(ctx)
}
///

View File

@ -6,8 +6,7 @@ use anchor_spl::token::{Token, TokenAccount};
use fixed::types::I80F48;
use itertools::Itertools;
use mango_v4::instructions::{
CpiData, FlashLoanWithdraw, InterestRateParams, Serum3OrderType, Serum3SelfTradeBehavior,
Serum3Side,
InterestRateParams, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side,
};
use mango_v4::state::{MangoAccount, MangoAccountValue};
use solana_program::instruction::Instruction;
@ -344,203 +343,7 @@ pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank:
// ClientInstruction impl
//
pub struct FlashLoanInstruction<'keypair> {
pub account: Pubkey,
pub owner: &'keypair Keypair,
pub mango_token_bank: Pubkey,
pub mango_token_vault: Pubkey,
pub withdraw_amount: u64,
pub margin_trade_program_id: Pubkey,
pub deposit_account: Pubkey,
pub deposit_account_owner: Pubkey,
pub margin_trade_program_ix_cpi_data: Vec<u8>,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for FlashLoanInstruction<'keypair> {
type Accounts = mango_v4::accounts::FlashLoan;
type Instruction = mango_v4::instruction::FlashLoan;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
owner: self.owner.pubkey(),
token_program: Token::id(),
};
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
Some(self.mango_token_bank),
true,
None,
)
.await;
let instruction = Self::Instruction {
withdraws: vec![FlashLoanWithdraw {
index: 2,
amount: self.withdraw_amount,
}],
cpi_datas: vec![CpiData {
account_start: health_check_metas.len() as u8,
data: self.margin_trade_program_ix_cpi_data.clone(),
}],
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas.into_iter());
instruction.accounts.push(AccountMeta {
pubkey: self.margin_trade_program_id,
is_writable: false,
is_signer: false,
});
instruction.accounts.push(AccountMeta {
pubkey: self.mango_token_bank,
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.deposit_account,
is_writable: true,
is_signer: false,
});
instruction.accounts.push(AccountMeta {
pubkey: self.deposit_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 FlashLoan2BeginInstruction<'keypair> {
pub group: Pubkey,
pub mango_token_bank: Pubkey,
pub mango_token_vault: Pubkey,
pub withdraw_amount: u64,
pub temporary_vault_authority: &'keypair Keypair,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for FlashLoan2BeginInstruction<'keypair> {
type Accounts = mango_v4::accounts::FlashLoan2Begin;
type Instruction = mango_v4::instruction::FlashLoan2Begin;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
) -> (Self::Accounts, instruction::Instruction) {
let program_id = mango_v4::id();
let accounts = Self::Accounts {
group: self.group,
temporary_vault_authority: self.temporary_vault_authority.pubkey(),
token_program: Token::id(),
instructions: solana_program::sysvar::instructions::id(),
};
let instruction = Self::Instruction {
loan_amounts: vec![self.withdraw_amount],
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.push(AccountMeta {
pubkey: self.mango_token_bank,
is_writable: true,
is_signer: false,
});
instruction.accounts.push(AccountMeta {
pubkey: self.mango_token_vault,
is_writable: true,
is_signer: false,
});
(accounts, instruction)
}
fn signers(&self) -> Vec<&Keypair> {
vec![self.temporary_vault_authority]
}
}
pub struct FlashLoan2EndInstruction<'keypair> {
pub account: Pubkey,
pub owner: &'keypair Keypair,
pub mango_token_bank: Pubkey,
pub mango_token_vault: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for FlashLoan2EndInstruction<'keypair> {
type Accounts = mango_v4::accounts::FlashLoan2End;
type Instruction = mango_v4::instruction::FlashLoan2End;
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 {};
let account = account_loader
.load_mango_account(&self.account)
.await
.unwrap();
let health_check_metas = derive_health_check_remaining_account_metas(
&account_loader,
&account,
Some(self.mango_token_bank),
true,
None,
)
.await;
let accounts = Self::Accounts {
group: account.fixed.group,
account: self.account,
owner: self.owner.pubkey(),
token_program: Token::id(),
};
let mut instruction = make_instruction(program_id, &accounts, instruction);
instruction.accounts.extend(health_check_metas.into_iter());
instruction.accounts.push(AccountMeta {
pubkey: self.mango_token_vault,
is_writable: true,
is_signer: false,
});
(accounts, instruction)
}
fn signers(&self) -> Vec<&Keypair> {
vec![self.owner]
}
}
pub struct FlashLoan3BeginInstruction {
pub struct FlashLoanBeginInstruction {
pub group: Pubkey,
pub mango_token_bank: Pubkey,
pub mango_token_vault: Pubkey,
@ -548,9 +351,9 @@ pub struct FlashLoan3BeginInstruction {
pub withdraw_amount: u64,
}
#[async_trait::async_trait(?Send)]
impl ClientInstruction for FlashLoan3BeginInstruction {
type Accounts = mango_v4::accounts::FlashLoan3Begin;
type Instruction = mango_v4::instruction::FlashLoan3Begin;
impl ClientInstruction for FlashLoanBeginInstruction {
type Accounts = mango_v4::accounts::FlashLoanBegin;
type Instruction = mango_v4::instruction::FlashLoanBegin;
async fn to_instruction(
&self,
_account_loader: impl ClientAccountLoader + 'async_trait,
@ -592,7 +395,7 @@ impl ClientInstruction for FlashLoan3BeginInstruction {
}
}
pub struct FlashLoan3EndInstruction<'keypair> {
pub struct FlashLoanEndInstruction<'keypair> {
pub account: Pubkey,
pub owner: &'keypair Keypair,
pub mango_token_bank: Pubkey,
@ -600,9 +403,9 @@ pub struct FlashLoan3EndInstruction<'keypair> {
pub target_token_account: Pubkey,
}
#[async_trait::async_trait(?Send)]
impl<'keypair> ClientInstruction for FlashLoan3EndInstruction<'keypair> {
type Accounts = mango_v4::accounts::FlashLoan3End;
type Instruction = mango_v4::instruction::FlashLoan3End;
impl<'keypair> ClientInstruction for FlashLoanEndInstruction<'keypair> {
type Accounts = mango_v4::accounts::FlashLoanEnd;
type Instruction = mango_v4::instruction::FlashLoanEnd;
async fn to_instruction(
&self,
account_loader: impl ClientAccountLoader + 'async_trait,

View File

@ -1,6 +1,5 @@
#![cfg(feature = "test-bpf")]
use anchor_lang::InstructionData;
use solana_program_test::*;
use solana_sdk::signature::Keypair;
use solana_sdk::signature::Signer;
@ -13,590 +12,7 @@ mod program_test;
// This is an unspecific happy-case test that just runs a few instructions to check
// that they work in principle. It should be split up / renamed.
#[tokio::test]
async fn test_margin_trade1() -> Result<(), BanksClientError> {
let mut builder = TestContextBuilder::new();
builder.test().set_compute_max_units(170000);
let margin_trade = builder.add_margin_trade_program();
let context = builder.start_default().await;
let solana = &context.solana.clone();
let admin = &Keypair::new();
let owner = &context.users[0].key;
let payer = &context.users[1].key;
let mints = &context.mints[0..2];
let payer_mint0_account = context.users[1].token_accounts[0];
let payer_mint1_account = context.users[1].token_accounts[1];
let loan_origination_fee = 0.0005;
// higher resolution that the loan_origination_fee for one token
let balance_f64eq = |a: f64, b: f64| (a - b).abs() < 0.0001;
//
// SETUP: Create a group, account, register a token (mint0)
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
let bank = tokens[0].bank;
let vault = tokens[0].vault;
//
// provide some funds for tokens, so the test user can borrow
//
let provided_amount = 1000;
let provider_account = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
account_size: AccountSize::Large,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
send_tx(
solana,
TokenDepositInstruction {
amount: provided_amount,
account: provider_account,
token_account: payer_mint0_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: provided_amount,
account: provider_account,
token_account: payer_mint1_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
//
// create thes test user account
//
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
account_size: AccountSize::Large,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
//
// TEST: Deposit funds
//
let deposit_amount_initial = 100;
{
let start_balance = solana.token_account_balance(payer_mint0_account).await;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount_initial,
account,
token_account: payer_mint0_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
assert_eq!(
solana.token_account_balance(vault).await,
provided_amount + deposit_amount_initial
);
assert_eq!(
solana.token_account_balance(payer_mint0_account).await,
start_balance - deposit_amount_initial
);
assert_eq!(
account_position(solana, account, bank).await,
deposit_amount_initial as i64,
);
}
//
// TEST: Margin trade
//
let withdraw_amount = 2;
let deposit_amount = 1;
{
send_tx(
solana,
FlashLoanInstruction {
account,
owner,
mango_token_bank: bank,
mango_token_vault: vault,
withdraw_amount,
margin_trade_program_id: margin_trade.program,
deposit_account: margin_trade.token_account.pubkey(),
deposit_account_owner: margin_trade.token_account_owner,
margin_trade_program_ix_cpi_data: {
let ix = margin_trade::instruction::MarginTrade {
amount_from: withdraw_amount,
amount_to: deposit_amount,
deposit_account_owner_bump_seeds: margin_trade.token_account_bump,
};
ix.data()
},
},
)
.await
.unwrap();
}
assert_eq!(
solana.token_account_balance(vault).await,
provided_amount + deposit_amount_initial - withdraw_amount + deposit_amount
);
assert_eq!(
solana
.token_account_balance(margin_trade.token_account.pubkey())
.await,
withdraw_amount - deposit_amount
);
// no fee because user had positive balance
assert!(balance_f64eq(
account_position_f64(solana, account, bank).await,
(deposit_amount_initial - withdraw_amount + deposit_amount) as f64
));
//
// TEST: Bringing the balance to 0 deactivates the token
//
let deposit_amount_initial = account_position(solana, account, bank).await;
let margin_account_initial = solana
.token_account_balance(margin_trade.token_account.pubkey())
.await;
let withdraw_amount = deposit_amount_initial as u64;
let deposit_amount = 0;
{
send_tx(
solana,
FlashLoanInstruction {
account,
owner,
mango_token_bank: bank,
mango_token_vault: vault,
withdraw_amount,
margin_trade_program_id: margin_trade.program,
deposit_account: margin_trade.token_account.pubkey(),
deposit_account_owner: margin_trade.token_account_owner,
margin_trade_program_ix_cpi_data: {
let ix = margin_trade::instruction::MarginTrade {
amount_from: withdraw_amount,
amount_to: deposit_amount,
deposit_account_owner_bump_seeds: margin_trade.token_account_bump,
};
ix.data()
},
},
)
.await
.unwrap();
}
assert_eq!(solana.token_account_balance(vault).await, provided_amount);
assert_eq!(
solana
.token_account_balance(margin_trade.token_account.pubkey())
.await,
margin_account_initial + withdraw_amount
);
// Check that position is fully deactivated
let account_data = get_mango_account(solana, account).await;
assert_eq!(account_data.token_iter_active().count(), 0);
//
// TEST: Activating a token via margin trade
//
let margin_account_initial = solana
.token_account_balance(margin_trade.token_account.pubkey())
.await;
let withdraw_amount = 0;
let deposit_amount = margin_account_initial;
{
send_tx(
solana,
FlashLoanInstruction {
account,
owner,
mango_token_bank: bank,
mango_token_vault: vault,
withdraw_amount,
margin_trade_program_id: margin_trade.program,
deposit_account: margin_trade.token_account.pubkey(),
deposit_account_owner: margin_trade.token_account_owner,
margin_trade_program_ix_cpi_data: {
let ix = margin_trade::instruction::MarginTrade {
amount_from: withdraw_amount,
amount_to: deposit_amount,
deposit_account_owner_bump_seeds: margin_trade.token_account_bump,
};
ix.data()
},
},
)
.await
.unwrap();
}
assert_eq!(
solana.token_account_balance(vault).await,
provided_amount + deposit_amount
);
assert_eq!(
solana
.token_account_balance(margin_trade.token_account.pubkey())
.await,
0
);
assert!(balance_f64eq(
account_position_f64(solana, account, bank).await,
deposit_amount as f64
));
//
// TEST: Try loan fees by withdrawing more than the user balance
//
let deposit_amount_initial = account_position(solana, account, bank).await as u64;
let withdraw_amount = 500;
let deposit_amount = 450;
{
send_tx(
solana,
FlashLoanInstruction {
account,
owner,
mango_token_bank: bank,
mango_token_vault: vault,
withdraw_amount,
margin_trade_program_id: margin_trade.program,
deposit_account: margin_trade.token_account.pubkey(),
deposit_account_owner: margin_trade.token_account_owner,
margin_trade_program_ix_cpi_data: {
let ix = margin_trade::instruction::MarginTrade {
amount_from: withdraw_amount,
amount_to: deposit_amount,
deposit_account_owner_bump_seeds: margin_trade.token_account_bump,
};
ix.data()
},
},
)
.await
.unwrap();
}
assert_eq!(
solana.token_account_balance(vault).await,
provided_amount + deposit_amount_initial + deposit_amount - withdraw_amount
);
assert_eq!(
solana
.token_account_balance(margin_trade.token_account.pubkey())
.await,
withdraw_amount - deposit_amount
);
assert!(balance_f64eq(
account_position_f64(solana, account, bank).await,
(deposit_amount_initial + deposit_amount - withdraw_amount) as f64
- (withdraw_amount - deposit_amount_initial) as f64 * loan_origination_fee
));
Ok(())
}
// This is an unspecific happy-case test that just runs a few instructions to check
// that they work in principle. It should be split up / renamed.
#[tokio::test]
async fn test_margin_trade2() -> Result<(), BanksClientError> {
let builder = TestContextBuilder::new();
let context = builder.start_default().await;
let solana = &context.solana.clone();
let admin = &Keypair::new();
let owner = &context.users[0].key;
let payer = &context.users[1].key;
let mints = &context.mints[0..2];
let payer_mint0_account = context.users[1].token_accounts[0];
let payer_mint1_account = context.users[1].token_accounts[1];
let loan_origination_fee = 0.0005;
// higher resolution that the loan_origination_fee for one token
let balance_f64eq = |a: f64, b: f64| (a - b).abs() < 0.0001;
//
// SETUP: Create a group, account, register a token (mint0)
//
let mango_setup::GroupWithTokens { group, tokens, .. } = mango_setup::GroupWithTokensConfig {
admin,
payer,
mints,
}
.create(solana)
.await;
let bank = tokens[0].bank;
let vault = tokens[0].vault;
//
// provide some funds for tokens, so the test user can borrow
//
let provided_amount = 1000;
let provider_account = send_tx(
solana,
AccountCreateInstruction {
account_num: 1,
account_size: AccountSize::Large,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
send_tx(
solana,
TokenDepositInstruction {
amount: provided_amount,
account: provider_account,
token_account: payer_mint0_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
send_tx(
solana,
TokenDepositInstruction {
amount: provided_amount,
account: provider_account,
token_account: payer_mint1_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
//
// create thes test user account
//
let account = send_tx(
solana,
AccountCreateInstruction {
account_num: 0,
account_size: AccountSize::Large,
group,
owner,
payer,
},
)
.await
.unwrap()
.account;
//
// TEST: Deposit funds
//
let deposit_amount_initial = 100;
{
let start_balance = solana.token_account_balance(payer_mint0_account).await;
send_tx(
solana,
TokenDepositInstruction {
amount: deposit_amount_initial,
account,
token_account: payer_mint0_account,
token_authority: payer.clone(),
bank_index: 0,
},
)
.await
.unwrap();
assert_eq!(
solana.token_account_balance(vault).await,
provided_amount + deposit_amount_initial
);
assert_eq!(
solana.token_account_balance(payer_mint0_account).await,
start_balance - deposit_amount_initial
);
assert_eq!(
account_position(solana, account, bank).await,
deposit_amount_initial as i64,
);
}
//
// TEST: Margin trade
//
let margin_account = payer_mint0_account;
let margin_account_initial = solana.token_account_balance(margin_account).await;
let withdraw_amount = 2;
let deposit_amount = 1;
let send_flash_loan_tx = |solana, withdraw_amount, deposit_amount| async move {
let temporary_vault_authority = &Keypair::new();
let mut tx = ClientTransaction::new(solana);
tx.add_instruction(FlashLoan2BeginInstruction {
group,
temporary_vault_authority,
mango_token_bank: bank,
mango_token_vault: vault,
withdraw_amount,
})
.await;
if withdraw_amount > 0 {
tx.add_instruction_direct(
spl_token::instruction::transfer(
&spl_token::ID,
&vault,
&margin_account,
&temporary_vault_authority.pubkey(),
&[&temporary_vault_authority.pubkey()],
withdraw_amount,
)
.unwrap(),
);
}
if deposit_amount > 0 {
tx.add_instruction_direct(
spl_token::instruction::transfer(
&spl_token::ID,
&margin_account,
&vault,
&payer.pubkey(),
&[&payer.pubkey()],
deposit_amount,
)
.unwrap(),
);
tx.add_signer(&payer);
}
tx.add_instruction(FlashLoan2EndInstruction {
account,
owner,
mango_token_bank: bank,
mango_token_vault: vault,
})
.await;
tx.send().await.unwrap();
};
send_flash_loan_tx(solana, withdraw_amount, deposit_amount).await;
assert_eq!(
solana.token_account_balance(vault).await,
provided_amount + deposit_amount_initial - withdraw_amount + deposit_amount
);
assert_eq!(
solana.token_account_balance(margin_account).await,
margin_account_initial + withdraw_amount - deposit_amount
);
// no fee because user had positive balance
assert!(balance_f64eq(
account_position_f64(solana, account, bank).await,
(deposit_amount_initial - withdraw_amount + deposit_amount) as f64
));
//
// TEST: Bringing the balance to 0 deactivates the token
//
let deposit_amount_initial = account_position(solana, account, bank).await;
let margin_account_initial = solana.token_account_balance(margin_account).await;
let withdraw_amount = deposit_amount_initial as u64;
let deposit_amount = 0;
send_flash_loan_tx(solana, withdraw_amount, deposit_amount).await;
assert_eq!(solana.token_account_balance(vault).await, provided_amount);
assert_eq!(
solana.token_account_balance(margin_account).await,
margin_account_initial + withdraw_amount
);
// Check that position is fully deactivated
let account_data = get_mango_account(solana, account).await;
assert_eq!(account_data.token_iter_active().count(), 0);
//
// TEST: Activating a token via margin trade
//
let margin_account_initial = solana.token_account_balance(margin_account).await;
let withdraw_amount = 0;
let deposit_amount = 100;
send_flash_loan_tx(solana, withdraw_amount, deposit_amount).await;
assert_eq!(
solana.token_account_balance(vault).await,
provided_amount + deposit_amount
);
assert_eq!(
solana.token_account_balance(margin_account).await,
margin_account_initial - deposit_amount
);
assert!(balance_f64eq(
account_position_f64(solana, account, bank).await,
deposit_amount as f64
));
//
// TEST: Try loan fees by withdrawing more than the user balance
//
let margin_account_initial = solana.token_account_balance(margin_account).await;
let deposit_amount_initial = account_position(solana, account, bank).await as u64;
let withdraw_amount = 500;
let deposit_amount = 450;
println!("{}", deposit_amount_initial);
send_flash_loan_tx(solana, withdraw_amount, deposit_amount).await;
assert_eq!(
solana.token_account_balance(vault).await,
provided_amount + deposit_amount_initial + deposit_amount - withdraw_amount
);
assert_eq!(
solana.token_account_balance(margin_account).await,
margin_account_initial + withdraw_amount - deposit_amount
);
assert!(balance_f64eq(
account_position_f64(solana, account, bank).await,
(deposit_amount_initial + deposit_amount - withdraw_amount) as f64
- (withdraw_amount - deposit_amount_initial) as f64 * loan_origination_fee
));
Ok(())
}
// This is an unspecific happy-case test that just runs a few instructions to check
// that they work in principle. It should be split up / renamed.
#[tokio::test]
async fn test_margin_trade3() -> Result<(), BanksClientError> {
async fn test_margin_trade() -> Result<(), BanksClientError> {
let builder = TestContextBuilder::new();
let context = builder.start_default().await;
let solana = &context.solana.clone();
@ -732,7 +148,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
let deposit_amount = 1;
let send_flash_loan_tx = |solana, withdraw_amount, deposit_amount| async move {
let mut tx = ClientTransaction::new(solana);
tx.add_instruction(FlashLoan3BeginInstruction {
tx.add_instruction(FlashLoanBeginInstruction {
group,
mango_token_bank: bank,
mango_token_vault: vault,
@ -767,7 +183,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
);
tx.add_signer(&payer);
}
tx.add_instruction(FlashLoan3EndInstruction {
tx.add_instruction(FlashLoanEndInstruction {
account,
owner,
mango_token_bank: bank,

View File

@ -47,12 +47,10 @@ import {
import { SERUM3_PROGRAM_ID } from './constants';
import { Id } from './ids';
import { IDL, MangoV4 } from './mango_v4';
import { FlashLoanWithdraw } from './types';
import {
getAssociatedTokenAddress,
I64_MAX_BN,
toNativeDecimals,
toU64,
} from './utils';
import { simulate } from './utils/anchor';
@ -1335,187 +1333,6 @@ export class MangoClient {
if (!inputBank || !outputBank) throw new Error('Invalid token');
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(group, mangoAccount, [
inputBank,
outputBank,
]);
const parsedHealthAccounts = healthRemainingAccounts.map(
(pk) =>
({
pubkey: pk,
isWritable:
pk.equals(inputBank.publicKey) || pk.equals(outputBank.publicKey)
? true
: false,
isSigner: false,
} as AccountMeta),
);
/*
* Find or create associated token accounts
*/
let inputTokenAccountPk = await getAssociatedTokenAddress(
inputBank.mint,
mangoAccount.owner,
);
const inputTokenAccExists =
await this.program.provider.connection.getAccountInfo(
inputTokenAccountPk,
);
let preInstructions = [];
if (!inputTokenAccExists) {
preInstructions.push(
Token.createAssociatedTokenAccountInstruction(
mangoAccount.owner,
inputTokenAccountPk,
mangoAccount.owner,
inputBank.mint,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID,
),
);
}
let outputTokenAccountPk = await getAssociatedTokenAddress(
outputBank.mint,
mangoAccount.owner,
);
const outputTokenAccExists =
await this.program.provider.connection.getAccountInfo(
outputTokenAccountPk,
);
if (!outputTokenAccExists) {
preInstructions.push(
Token.createAssociatedTokenAccountInstruction(
mangoAccount.owner,
outputTokenAccountPk,
mangoAccount.owner,
outputBank.mint,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID,
),
);
}
/*
* Transfer input token to users wallet, then concat the passed in instructions
*/
const nativeInputAmount = toU64(
amountIn,
inputBank.mintDecimals,
).toNumber();
const instructions: TransactionInstruction[] = [];
const transferIx = Token.createTransferInstruction(
TOKEN_PROGRAM_ID,
inputBank.vault,
inputTokenAccountPk,
inputBank.publicKey,
[],
nativeInputAmount,
);
const inputBankKey = transferIx.keys[2];
transferIx.keys[2] = { ...inputBankKey, isWritable: true, isSigner: false };
instructions.push(transferIx);
instructions.concat(userDefinedInstructions);
const transferIx2 = Token.createTransferInstruction(
TOKEN_PROGRAM_ID,
outputTokenAccountPk,
outputBank.vault,
mangoAccount.owner,
[],
0, // todo: use this for testing, this should be the amount to transfer back
);
instructions.push(transferIx2);
/*
* Create object of amounts that will be withdrawn from bank vaults
*/
const targetRemainingAccounts = instructions
.map((ix) => [
{
pubkey: ix.programId,
isWritable: false,
isSigner: false,
} as AccountMeta,
...ix.keys,
])
.flat();
const vaultIndex = targetRemainingAccounts
.map((x) => x.pubkey.toString())
.lastIndexOf(inputBank.vault.toString());
const withdraws: FlashLoanWithdraw[] = [
{
index: vaultIndex,
amount: toU64(amountIn, inputBank.mintDecimals),
},
];
/*
* Build cpi data objects for instructions
*/
let cpiDatas = [];
for (const [index, ix] of instructions.entries()) {
if (index === 0) {
cpiDatas.push({
accountStart: new BN(parsedHealthAccounts.length),
data: ix.data,
});
} else {
cpiDatas.push({
accountStart: cpiDatas[index - 1].accountStart.add(
new BN(instructions[index - 1].keys.length + 1),
),
data: ix.data,
});
}
}
if (preInstructions.length) {
const tx = new Transaction();
for (const ix of preInstructions) {
tx.add(ix);
}
await this.program.provider.sendAndConfirm(tx);
}
return await this.program.methods
.flashLoan(withdraws, cpiDatas)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
})
.remainingAccounts([...parsedHealthAccounts, ...targetRemainingAccounts])
.rpc({ skipPreflight: true });
}
public async marginTrade3({
group,
mangoAccount,
inputToken,
amountIn,
outputToken,
userDefinedInstructions,
}: {
group: Group;
mangoAccount: MangoAccount;
inputToken: string;
amountIn: number;
outputToken: string;
userDefinedInstructions: TransactionInstruction[];
}): Promise<TransactionSignature> {
const inputBank = group.banksMap.get(inputToken);
const outputBank = group.banksMap.get(outputToken);
if (!inputBank || !outputBank) throw new Error('Invalid token');
const healthRemainingAccounts: PublicKey[] =
this.buildHealthRemainingAccounts(group, mangoAccount, [
inputBank,
@ -1621,7 +1438,7 @@ export class MangoClient {
};
const flashLoanEndIx = await this.program.methods
.flashLoan3End()
.flashLoanEnd()
.accounts({
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
@ -1643,7 +1460,7 @@ export class MangoClient {
// userDefinedInstructions.push(flashLoanEndIx);
const flashLoanBeginIx = await this.program.methods
.flashLoan3Begin([
.flashLoanBegin([
toNativeDecimals(amountIn, inputBank.mintDecimals),
new BN(
0,

View File

@ -1012,109 +1012,7 @@ export type MangoV4 = {
]
},
{
"name": "flashLoan",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "withdraws",
"type": {
"vec": {
"defined": "FlashLoanWithdraw"
}
}
},
{
"name": "cpiDatas",
"type": {
"vec": {
"defined": "CpiData"
}
}
}
]
},
{
"name": "flashLoan2Begin",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "temporaryVaultAuthority",
"isMut": false,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
},
{
"name": "instructions",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "loanAmounts",
"type": {
"vec": "u64"
}
}
]
},
{
"name": "flashLoan2End",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "flashLoan3Begin",
"name": "flashLoanBegin",
"accounts": [
{
"name": "group",
@ -1145,7 +1043,7 @@ export type MangoV4 = {
]
},
{
"name": "flashLoan3End",
"name": "flashLoanEnd",
"accounts": [
{
"name": "account",
@ -2808,7 +2706,7 @@ export type MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding",
"type": {
"array": [
"u8",
@ -2819,6 +2717,15 @@ export type MangoV4 = {
{
"name": "bankNum",
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
]
}
}
]
}
@ -2887,7 +2794,7 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
8
128
]
}
}
@ -2943,7 +2850,7 @@ export type MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding",
"type": {
"array": [
"u8",
@ -2959,6 +2866,15 @@ export type MangoV4 = {
"name": "netSettled",
"type": "f32"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
256
]
}
},
{
"name": "padding1",
"type": "u32"
@ -3024,7 +2940,7 @@ export type MangoV4 = {
"type": "u16"
},
{
"name": "padding",
"name": "padding1",
"type": {
"array": [
"u8",
@ -3071,13 +2987,22 @@ export type MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding2",
"type": {
"array": [
"u8",
6
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
]
}
}
]
}
@ -3110,7 +3035,7 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
8
128
]
}
}
@ -3172,6 +3097,15 @@ export type MangoV4 = {
1024
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
]
}
}
]
}
@ -3194,7 +3128,7 @@ export type MangoV4 = {
{
"defined": "AnyEvent"
},
512
488
]
}
}
@ -3222,7 +3156,7 @@ export type MangoV4 = {
"type": "u16"
},
{
"name": "padding",
"name": "padding1",
"type": {
"array": [
"u8",
@ -3390,13 +3324,22 @@ export type MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding2",
"type": {
"array": [
"u8",
6
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
]
}
}
]
}
@ -3419,7 +3362,7 @@ export type MangoV4 = {
"type": "u16"
},
{
"name": "padding",
"name": "padding1",
"type": {
"array": [
"u8",
@ -3453,13 +3396,22 @@ export type MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding2",
"type": {
"array": [
"u8",
5
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
]
}
}
]
}
@ -3526,45 +3478,6 @@ export type MangoV4 = {
]
}
},
{
"name": "FlashLoanWithdraw",
"type": {
"kind": "struct",
"fields": [
{
"name": "index",
"docs": [
"Account index of the vault to withdraw from in the target_accounts section.",
"Index is counted after health accounts."
],
"type": "u8"
},
{
"name": "amount",
"docs": [
"Requested withdraw amount."
],
"type": "u64"
}
]
}
},
{
"name": "CpiData",
"type": {
"kind": "struct",
"fields": [
{
"name": "accountStart",
"type": "u8"
},
{
"name": "data",
"type": "bytes"
}
]
}
},
{
"name": "InterestRateParams",
"type": {
@ -3814,13 +3727,22 @@ export type MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding",
"type": {
"array": [
"u8",
5
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
64
]
}
}
]
}
@ -3860,13 +3782,22 @@ export type MangoV4 = {
"type": "u16"
},
{
"name": "reserved",
"name": "padding",
"type": {
"array": [
"u8",
2
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
64
]
}
}
]
}
@ -3881,7 +3812,7 @@ export type MangoV4 = {
"type": "u16"
},
{
"name": "reserved",
"name": "padding",
"type": {
"array": [
"u8",
@ -3946,6 +3877,15 @@ export type MangoV4 = {
{
"name": "takerQuoteLots",
"type": "i64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
64
]
}
}
]
}
@ -3962,7 +3902,7 @@ export type MangoV4 = {
}
},
{
"name": "reserved1",
"name": "padding1",
"type": {
"array": [
"u8",
@ -3975,7 +3915,7 @@ export type MangoV4 = {
"type": "u16"
},
{
"name": "reserved2",
"name": "padding2",
"type": {
"array": [
"u8",
@ -3990,6 +3930,15 @@ export type MangoV4 = {
{
"name": "orderId",
"type": "i128"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
64
]
}
}
]
}
@ -4022,7 +3971,7 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
84
92
]
}
}
@ -4063,7 +4012,7 @@ export type MangoV4 = {
"type": {
"array": [
"u8",
199
207
]
}
}
@ -5929,109 +5878,7 @@ export const IDL: MangoV4 = {
]
},
{
"name": "flashLoan",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "withdraws",
"type": {
"vec": {
"defined": "FlashLoanWithdraw"
}
}
},
{
"name": "cpiDatas",
"type": {
"vec": {
"defined": "CpiData"
}
}
}
]
},
{
"name": "flashLoan2Begin",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "temporaryVaultAuthority",
"isMut": false,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
},
{
"name": "instructions",
"isMut": false,
"isSigner": false
}
],
"args": [
{
"name": "loanAmounts",
"type": {
"vec": "u64"
}
}
]
},
{
"name": "flashLoan2End",
"accounts": [
{
"name": "group",
"isMut": false,
"isSigner": false
},
{
"name": "account",
"isMut": true,
"isSigner": false
},
{
"name": "owner",
"isMut": false,
"isSigner": true
},
{
"name": "tokenProgram",
"isMut": false,
"isSigner": false
}
],
"args": []
},
{
"name": "flashLoan3Begin",
"name": "flashLoanBegin",
"accounts": [
{
"name": "group",
@ -6062,7 +5909,7 @@ export const IDL: MangoV4 = {
]
},
{
"name": "flashLoan3End",
"name": "flashLoanEnd",
"accounts": [
{
"name": "account",
@ -7725,7 +7572,7 @@ export const IDL: MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding",
"type": {
"array": [
"u8",
@ -7736,6 +7583,15 @@ export const IDL: MangoV4 = {
{
"name": "bankNum",
"type": "u64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
]
}
}
]
}
@ -7804,7 +7660,7 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
8
128
]
}
}
@ -7860,7 +7716,7 @@ export const IDL: MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding",
"type": {
"array": [
"u8",
@ -7876,6 +7732,15 @@ export const IDL: MangoV4 = {
"name": "netSettled",
"type": "f32"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
256
]
}
},
{
"name": "padding1",
"type": "u32"
@ -7941,7 +7806,7 @@ export const IDL: MangoV4 = {
"type": "u16"
},
{
"name": "padding",
"name": "padding1",
"type": {
"array": [
"u8",
@ -7988,13 +7853,22 @@ export const IDL: MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding2",
"type": {
"array": [
"u8",
6
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
]
}
}
]
}
@ -8027,7 +7901,7 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
8
128
]
}
}
@ -8089,6 +7963,15 @@ export const IDL: MangoV4 = {
1024
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
]
}
}
]
}
@ -8111,7 +7994,7 @@ export const IDL: MangoV4 = {
{
"defined": "AnyEvent"
},
512
488
]
}
}
@ -8139,7 +8022,7 @@ export const IDL: MangoV4 = {
"type": "u16"
},
{
"name": "padding",
"name": "padding1",
"type": {
"array": [
"u8",
@ -8307,13 +8190,22 @@ export const IDL: MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding2",
"type": {
"array": [
"u8",
6
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
]
}
}
]
}
@ -8336,7 +8228,7 @@ export const IDL: MangoV4 = {
"type": "u16"
},
{
"name": "padding",
"name": "padding1",
"type": {
"array": [
"u8",
@ -8370,13 +8262,22 @@ export const IDL: MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding2",
"type": {
"array": [
"u8",
5
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
128
]
}
}
]
}
@ -8443,45 +8344,6 @@ export const IDL: MangoV4 = {
]
}
},
{
"name": "FlashLoanWithdraw",
"type": {
"kind": "struct",
"fields": [
{
"name": "index",
"docs": [
"Account index of the vault to withdraw from in the target_accounts section.",
"Index is counted after health accounts."
],
"type": "u8"
},
{
"name": "amount",
"docs": [
"Requested withdraw amount."
],
"type": "u64"
}
]
}
},
{
"name": "CpiData",
"type": {
"kind": "struct",
"fields": [
{
"name": "accountStart",
"type": "u8"
},
{
"name": "data",
"type": "bytes"
}
]
}
},
{
"name": "InterestRateParams",
"type": {
@ -8731,13 +8593,22 @@ export const IDL: MangoV4 = {
"type": "u8"
},
{
"name": "reserved",
"name": "padding",
"type": {
"array": [
"u8",
5
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
64
]
}
}
]
}
@ -8777,13 +8648,22 @@ export const IDL: MangoV4 = {
"type": "u16"
},
{
"name": "reserved",
"name": "padding",
"type": {
"array": [
"u8",
2
]
}
},
{
"name": "reserved",
"type": {
"array": [
"u8",
64
]
}
}
]
}
@ -8798,7 +8678,7 @@ export const IDL: MangoV4 = {
"type": "u16"
},
{
"name": "reserved",
"name": "padding",
"type": {
"array": [
"u8",
@ -8863,6 +8743,15 @@ export const IDL: MangoV4 = {
{
"name": "takerQuoteLots",
"type": "i64"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
64
]
}
}
]
}
@ -8879,7 +8768,7 @@ export const IDL: MangoV4 = {
}
},
{
"name": "reserved1",
"name": "padding1",
"type": {
"array": [
"u8",
@ -8892,7 +8781,7 @@ export const IDL: MangoV4 = {
"type": "u16"
},
{
"name": "reserved2",
"name": "padding2",
"type": {
"array": [
"u8",
@ -8907,6 +8796,15 @@ export const IDL: MangoV4 = {
{
"name": "orderId",
"type": "i128"
},
{
"name": "reserved",
"type": {
"array": [
"u8",
64
]
}
}
]
}
@ -8939,7 +8837,7 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
84
92
]
}
}
@ -8980,7 +8878,7 @@ export const IDL: MangoV4 = {
"type": {
"array": [
"u8",
199
207
]
}
}

View File

@ -21,7 +21,7 @@ const MANGO_MAINNET_PAYER_KEYPAIR =
'/Users/tylershipe/.config/solana/deploy.json';
//
// example script which shows usage of flash loan 3 ix using a jupiter swap
// example script which shows usage of flash loan ix using a jupiter swap
//
// NOTE: we assume that ATA for source and target already exist for wallet
async function main() {
@ -142,7 +142,7 @@ async function main() {
);
// 1. build flash loan end ix
const flashLoadnEndIx = await client.program.methods
.flashLoan3End()
.flashLoanEnd()
.accounts({
account: mangoAccount.publicKey,
owner: (client.program.provider as AnchorProvider).wallet.publicKey,
@ -188,7 +188,7 @@ async function main() {
// 2. build flash loan start ix, add end ix as a post ix
try {
res = await client.program.methods
.flashLoan3Begin([
.flashLoanBegin([
new BN(sourceAmount),
new BN(
0,
@ -198,7 +198,7 @@ async function main() {
group: group.publicKey,
// for observing ixs in the entire tx,
// e.g. apart from flash loan start and end no other ix should target mango v4 program
// e.g. forbid FlashLoan3Begin been called from CPI
// e.g. forbid FlashLoanBegin been called from CPI
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
})
.remainingAccounts([