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:
parent
c516e45d08
commit
5c3b2c1189
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
///
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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([
|
Loading…
Reference in New Issue