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::accounts_zerocopy::*;
|
||||||
use crate::error::MangoError;
|
use crate::error::*;
|
||||||
use crate::logs::{MarginTradeLog, TokenBalanceLog};
|
use crate::group_seeds;
|
||||||
|
use crate::logs::{FlashLoanLog, FlashLoanTokenDetail, TokenBalanceLog};
|
||||||
|
use crate::state::MangoAccount;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
compute_health, new_fixed_order_account_retriever, AccountLoaderDynamic, AccountRetriever,
|
compute_health, compute_health_from_fixed_accounts, new_fixed_order_account_retriever,
|
||||||
Bank, Group, HealthType, MangoAccount, MangoAccountRefMut,
|
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::prelude::*;
|
||||||
|
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
|
||||||
use anchor_spl::token::{self, Token, TokenAccount};
|
use anchor_spl::token::{self, Token, TokenAccount};
|
||||||
use fixed::types::I80F48;
|
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:
|
/// In addition to these accounts, there must be remaining_accounts:
|
||||||
/// 1. health_accounts: accounts needed for health checking
|
/// 1. N banks (writable)
|
||||||
/// 2. per cpi
|
/// 2. N vaults (writable), matching the banks
|
||||||
/// 2.a. target_program_id: the target program account
|
/// 3. N token accounts (writable), in the same order as the vaults,
|
||||||
/// 2.b. target_accounts: the accounts to pass to the target program
|
/// the loaned funds are transfered into these
|
||||||
///
|
|
||||||
/// 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.
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
pub struct FlashLoan<'info> {
|
pub struct FlashLoanBegin<'info> {
|
||||||
pub group: AccountLoader<'info, Group>,
|
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 account: AccountLoaderDynamic<'info, MangoAccount>,
|
||||||
pub owner: Signer<'info>,
|
pub owner: Signer<'info>,
|
||||||
|
|
||||||
pub token_program: Program<'info, Token>,
|
pub token_program: Program<'info, Token>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
/// The `loan_amounts` argument lists the amount to be loaned from each bank/vault and
|
||||||
struct AllowedVault {
|
/// the order matches the order of bank accounts.
|
||||||
/// index of the vault in cpi_ais
|
pub fn flash_loan_begin<'key, 'accounts, 'remaining, 'info>(
|
||||||
vault_cpi_ai_index: usize,
|
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoanBegin<'info>>,
|
||||||
/// index of the bank in health_ais
|
loan_amounts: Vec<u64>,
|
||||||
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>,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
require!(!cpi_datas.is_empty(), MangoError::SomeError);
|
let num_loans = loan_amounts.len();
|
||||||
let num_of_cpis = cpi_datas.len();
|
require_eq!(ctx.remaining_accounts.len(), 3 * num_loans);
|
||||||
let num_health_accounts = cpi_datas.get(0).unwrap().account_start as usize;
|
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 = 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()?;
|
let mut account = ctx.accounts.account.load_mut()?;
|
||||||
|
|
||||||
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
require!(!account.fixed.is_bankrupt(), MangoError::IsBankrupt);
|
||||||
|
|
||||||
// Go over the banks passed as health accounts and:
|
// Find index at which vaults start
|
||||||
// - Ensure that all banks that are passed in have activated positions.
|
let vaults_index = ctx
|
||||||
// This is necessary because maybe the user wants to margin trade on a token
|
.remaining_accounts
|
||||||
// that the account hasn't used before.
|
.iter()
|
||||||
// - Collect the addresses of all banks to potentially sign for in cpi_ais.
|
.position(|ai| {
|
||||||
// - Collect the addresses of all bank vaults.
|
let maybe_token_account = Account::<TokenAccount>::try_from(ai);
|
||||||
// Note: This depends on the particular health account ordering.
|
if maybe_token_account.is_err() {
|
||||||
let health_ais = &ctx.remaining_accounts[0..num_health_accounts];
|
return false;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
Err(Error::AnchorError(error))
|
|
||||||
if error.error_code_number == ErrorCode::AccountDiscriminatorMismatch as u32
|
maybe_token_account.unwrap().owner == account.fixed.group
|
||||||
|| error.error_code_number == ErrorCode::AccountOwnedByWrongProgram as u32 =>
|
})
|
||||||
{
|
.ok_or_else(|| error_msg!("expected at least one vault token account to be passed"))?;
|
||||||
break;
|
let vaults_len = (ctx.remaining_accounts.len() - vaults_index) / 2;
|
||||||
}
|
require_eq!(ctx.remaining_accounts.len(), vaults_index + 2 * vaults_len);
|
||||||
Err(error) => return Err(error),
|
|
||||||
|
// 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
|
// Check pre-cpi health
|
||||||
// NOTE: This health check isn't strictly necessary. It will be, later, when
|
// 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.
|
// 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 retriever = new_fixed_order_account_retriever(health_ais, &account.borrow())?;
|
||||||
let post_cpi_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
|
let pre_cpi_health = compute_health(&account.borrow(), HealthType::Init, &retriever)?;
|
||||||
require!(post_cpi_health >= 0, MangoError::HealthMustBePositive);
|
require!(pre_cpi_health >= 0, MangoError::HealthMustBePositive);
|
||||||
msg!("post_cpi_health {:?}", post_cpi_health);
|
msg!("pre_cpi_health {:?}", pre_cpi_health);
|
||||||
|
|
||||||
// Token balances logging
|
// Prices for logging
|
||||||
let mut token_indexes = Vec::with_capacity(used_vaults.len());
|
let mut prices = vec![];
|
||||||
let mut post_indexed_positions = Vec::with_capacity(used_vaults.len());
|
for change in &changes {
|
||||||
for (_, info) in used_vaults.iter() {
|
let (_, oracle_price) = retriever.bank_and_oracle(
|
||||||
let position = account.token_get_raw(info.raw_token_index);
|
&account.fixed.group,
|
||||||
post_indexed_positions.push(position.indexed_position.to_bits());
|
change.bank_index,
|
||||||
token_indexes.push(position.token_index as u16);
|
change.token_index,
|
||||||
|
|
||||||
let (bank, oracle_price) = retriever.bank_and_oracle(
|
|
||||||
&ctx.accounts.group.key(),
|
|
||||||
info.bank_health_ai_index,
|
|
||||||
position.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 {
|
emit!(TokenBalanceLog {
|
||||||
mango_account: ctx.accounts.account.key(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
token_index: bank.token_index as u16,
|
token_index: bank.token_index as u16,
|
||||||
indexed_position: position.indexed_position.to_bits(),
|
indexed_position: position.indexed_position.to_bits(),
|
||||||
deposit_index: bank.deposit_index.to_bits(),
|
deposit_index: bank.deposit_index.to_bits(),
|
||||||
borrow_index: bank.borrow_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(),
|
mango_account: ctx.accounts.account.key(),
|
||||||
token_indexes,
|
token_loan_details
|
||||||
pre_indexed_positions,
|
|
||||||
post_indexed_positions,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Deactivate inactive token accounts at the end
|
// Check post-cpi health
|
||||||
for raw_token_index in inactive_tokens {
|
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);
|
account.token_deactivate(raw_token_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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 benchmark::*;
|
||||||
pub use compute_account_data::*;
|
pub use compute_account_data::*;
|
||||||
pub use flash_loan::*;
|
pub use flash_loan::*;
|
||||||
pub use flash_loan2::*;
|
|
||||||
pub use flash_loan3::*;
|
|
||||||
pub use group_close::*;
|
pub use group_close::*;
|
||||||
pub use group_create::*;
|
pub use group_create::*;
|
||||||
pub use group_edit::*;
|
pub use group_edit::*;
|
||||||
|
@ -49,8 +47,6 @@ mod account_expand;
|
||||||
mod benchmark;
|
mod benchmark;
|
||||||
mod compute_account_data;
|
mod compute_account_data;
|
||||||
mod flash_loan;
|
mod flash_loan;
|
||||||
mod flash_loan2;
|
|
||||||
mod flash_loan3;
|
|
||||||
mod group_close;
|
mod group_close;
|
||||||
mod group_create;
|
mod group_create;
|
||||||
mod group_edit;
|
mod group_edit;
|
||||||
|
|
|
@ -193,39 +193,18 @@ pub mod mango_v4 {
|
||||||
instructions::token_withdraw(ctx, amount, allow_borrow)
|
instructions::token_withdraw(ctx, amount, allow_borrow)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn flash_loan<'key, 'accounts, 'remaining, 'info>(
|
pub fn flash_loan_begin<'key, 'accounts, 'remaining, 'info>(
|
||||||
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan<'info>>,
|
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoanBegin<'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>>,
|
|
||||||
loan_amounts: Vec<u64>,
|
loan_amounts: Vec<u64>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
instructions::flash_loan2_begin(ctx, loan_amounts)
|
instructions::flash_loan_begin(ctx, loan_amounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn flash_loan2_end<'key, 'accounts, 'remaining, 'info>(
|
// NOTE: keep disc synced in flash_loan.rs
|
||||||
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoan2End<'info>>,
|
pub fn flash_loan_end<'key, 'accounts, 'remaining, 'info>(
|
||||||
|
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoanEnd<'info>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
instructions::flash_loan2_end(ctx)
|
instructions::flash_loan_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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
|
|
|
@ -6,8 +6,7 @@ use anchor_spl::token::{Token, TokenAccount};
|
||||||
use fixed::types::I80F48;
|
use fixed::types::I80F48;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use mango_v4::instructions::{
|
use mango_v4::instructions::{
|
||||||
CpiData, FlashLoanWithdraw, InterestRateParams, Serum3OrderType, Serum3SelfTradeBehavior,
|
InterestRateParams, Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side,
|
||||||
Serum3Side,
|
|
||||||
};
|
};
|
||||||
use mango_v4::state::{MangoAccount, MangoAccountValue};
|
use mango_v4::state::{MangoAccount, MangoAccountValue};
|
||||||
use solana_program::instruction::Instruction;
|
use solana_program::instruction::Instruction;
|
||||||
|
@ -344,203 +343,7 @@ pub async fn account_position_f64(solana: &SolanaCookie, account: Pubkey, bank:
|
||||||
// ClientInstruction impl
|
// ClientInstruction impl
|
||||||
//
|
//
|
||||||
|
|
||||||
pub struct FlashLoanInstruction<'keypair> {
|
pub struct FlashLoanBeginInstruction {
|
||||||
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 group: Pubkey,
|
pub group: Pubkey,
|
||||||
pub mango_token_bank: Pubkey,
|
pub mango_token_bank: Pubkey,
|
||||||
pub mango_token_vault: Pubkey,
|
pub mango_token_vault: Pubkey,
|
||||||
|
@ -548,9 +351,9 @@ pub struct FlashLoan3BeginInstruction {
|
||||||
pub withdraw_amount: u64,
|
pub withdraw_amount: u64,
|
||||||
}
|
}
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl ClientInstruction for FlashLoan3BeginInstruction {
|
impl ClientInstruction for FlashLoanBeginInstruction {
|
||||||
type Accounts = mango_v4::accounts::FlashLoan3Begin;
|
type Accounts = mango_v4::accounts::FlashLoanBegin;
|
||||||
type Instruction = mango_v4::instruction::FlashLoan3Begin;
|
type Instruction = mango_v4::instruction::FlashLoanBegin;
|
||||||
async fn to_instruction(
|
async fn to_instruction(
|
||||||
&self,
|
&self,
|
||||||
_account_loader: impl ClientAccountLoader + 'async_trait,
|
_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 account: Pubkey,
|
||||||
pub owner: &'keypair Keypair,
|
pub owner: &'keypair Keypair,
|
||||||
pub mango_token_bank: Pubkey,
|
pub mango_token_bank: Pubkey,
|
||||||
|
@ -600,9 +403,9 @@ pub struct FlashLoan3EndInstruction<'keypair> {
|
||||||
pub target_token_account: Pubkey,
|
pub target_token_account: Pubkey,
|
||||||
}
|
}
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl<'keypair> ClientInstruction for FlashLoan3EndInstruction<'keypair> {
|
impl<'keypair> ClientInstruction for FlashLoanEndInstruction<'keypair> {
|
||||||
type Accounts = mango_v4::accounts::FlashLoan3End;
|
type Accounts = mango_v4::accounts::FlashLoanEnd;
|
||||||
type Instruction = mango_v4::instruction::FlashLoan3End;
|
type Instruction = mango_v4::instruction::FlashLoanEnd;
|
||||||
async fn to_instruction(
|
async fn to_instruction(
|
||||||
&self,
|
&self,
|
||||||
account_loader: impl ClientAccountLoader + 'async_trait,
|
account_loader: impl ClientAccountLoader + 'async_trait,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
#![cfg(feature = "test-bpf")]
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
use anchor_lang::InstructionData;
|
|
||||||
use solana_program_test::*;
|
use solana_program_test::*;
|
||||||
use solana_sdk::signature::Keypair;
|
use solana_sdk::signature::Keypair;
|
||||||
use solana_sdk::signature::Signer;
|
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
|
// 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.
|
// that they work in principle. It should be split up / renamed.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_margin_trade1() -> Result<(), BanksClientError> {
|
async fn test_margin_trade() -> 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> {
|
|
||||||
let builder = TestContextBuilder::new();
|
let builder = TestContextBuilder::new();
|
||||||
let context = builder.start_default().await;
|
let context = builder.start_default().await;
|
||||||
let solana = &context.solana.clone();
|
let solana = &context.solana.clone();
|
||||||
|
@ -732,7 +148,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
|
||||||
let deposit_amount = 1;
|
let deposit_amount = 1;
|
||||||
let send_flash_loan_tx = |solana, withdraw_amount, deposit_amount| async move {
|
let send_flash_loan_tx = |solana, withdraw_amount, deposit_amount| async move {
|
||||||
let mut tx = ClientTransaction::new(solana);
|
let mut tx = ClientTransaction::new(solana);
|
||||||
tx.add_instruction(FlashLoan3BeginInstruction {
|
tx.add_instruction(FlashLoanBeginInstruction {
|
||||||
group,
|
group,
|
||||||
mango_token_bank: bank,
|
mango_token_bank: bank,
|
||||||
mango_token_vault: vault,
|
mango_token_vault: vault,
|
||||||
|
@ -767,7 +183,7 @@ async fn test_margin_trade3() -> Result<(), BanksClientError> {
|
||||||
);
|
);
|
||||||
tx.add_signer(&payer);
|
tx.add_signer(&payer);
|
||||||
}
|
}
|
||||||
tx.add_instruction(FlashLoan3EndInstruction {
|
tx.add_instruction(FlashLoanEndInstruction {
|
||||||
account,
|
account,
|
||||||
owner,
|
owner,
|
||||||
mango_token_bank: bank,
|
mango_token_bank: bank,
|
||||||
|
|
|
@ -47,12 +47,10 @@ import {
|
||||||
import { SERUM3_PROGRAM_ID } from './constants';
|
import { SERUM3_PROGRAM_ID } from './constants';
|
||||||
import { Id } from './ids';
|
import { Id } from './ids';
|
||||||
import { IDL, MangoV4 } from './mango_v4';
|
import { IDL, MangoV4 } from './mango_v4';
|
||||||
import { FlashLoanWithdraw } from './types';
|
|
||||||
import {
|
import {
|
||||||
getAssociatedTokenAddress,
|
getAssociatedTokenAddress,
|
||||||
I64_MAX_BN,
|
I64_MAX_BN,
|
||||||
toNativeDecimals,
|
toNativeDecimals,
|
||||||
toU64,
|
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { simulate } from './utils/anchor';
|
import { simulate } from './utils/anchor';
|
||||||
|
|
||||||
|
@ -1335,187 +1333,6 @@ export class MangoClient {
|
||||||
|
|
||||||
if (!inputBank || !outputBank) throw new Error('Invalid token');
|
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[] =
|
const healthRemainingAccounts: PublicKey[] =
|
||||||
this.buildHealthRemainingAccounts(group, mangoAccount, [
|
this.buildHealthRemainingAccounts(group, mangoAccount, [
|
||||||
inputBank,
|
inputBank,
|
||||||
|
@ -1621,7 +1438,7 @@ export class MangoClient {
|
||||||
};
|
};
|
||||||
|
|
||||||
const flashLoanEndIx = await this.program.methods
|
const flashLoanEndIx = await this.program.methods
|
||||||
.flashLoan3End()
|
.flashLoanEnd()
|
||||||
.accounts({
|
.accounts({
|
||||||
account: mangoAccount.publicKey,
|
account: mangoAccount.publicKey,
|
||||||
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||||
|
@ -1643,7 +1460,7 @@ export class MangoClient {
|
||||||
// userDefinedInstructions.push(flashLoanEndIx);
|
// userDefinedInstructions.push(flashLoanEndIx);
|
||||||
|
|
||||||
const flashLoanBeginIx = await this.program.methods
|
const flashLoanBeginIx = await this.program.methods
|
||||||
.flashLoan3Begin([
|
.flashLoanBegin([
|
||||||
toNativeDecimals(amountIn, inputBank.mintDecimals),
|
toNativeDecimals(amountIn, inputBank.mintDecimals),
|
||||||
new BN(
|
new BN(
|
||||||
0,
|
0,
|
||||||
|
|
|
@ -1012,109 +1012,7 @@ export type MangoV4 = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "flashLoan",
|
"name": "flashLoanBegin",
|
||||||
"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",
|
|
||||||
"accounts": [
|
"accounts": [
|
||||||
{
|
{
|
||||||
"name": "group",
|
"name": "group",
|
||||||
|
@ -1145,7 +1043,7 @@ export type MangoV4 = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "flashLoan3End",
|
"name": "flashLoanEnd",
|
||||||
"accounts": [
|
"accounts": [
|
||||||
{
|
{
|
||||||
"name": "account",
|
"name": "account",
|
||||||
|
@ -2808,7 +2706,7 @@ export type MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -2819,6 +2717,15 @@ export type MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "bankNum",
|
"name": "bankNum",
|
||||||
"type": "u64"
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
128
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -2887,7 +2794,7 @@ export type MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
8
|
128
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2943,7 +2850,7 @@ export type MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -2959,6 +2866,15 @@ export type MangoV4 = {
|
||||||
"name": "netSettled",
|
"name": "netSettled",
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
256
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "padding1",
|
"name": "padding1",
|
||||||
"type": "u32"
|
"type": "u32"
|
||||||
|
@ -3024,7 +2940,7 @@ export type MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "padding1",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -3071,13 +2987,22 @@ export type MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding2",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
6
|
6
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
128
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -3110,7 +3035,7 @@ export type MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
8
|
128
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3172,6 +3097,15 @@ export type MangoV4 = {
|
||||||
1024
|
1024
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
128
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -3194,7 +3128,7 @@ export type MangoV4 = {
|
||||||
{
|
{
|
||||||
"defined": "AnyEvent"
|
"defined": "AnyEvent"
|
||||||
},
|
},
|
||||||
512
|
488
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3222,7 +3156,7 @@ export type MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "padding1",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -3390,13 +3324,22 @@ export type MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding2",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
6
|
6
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
128
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -3419,7 +3362,7 @@ export type MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "padding1",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -3453,13 +3396,22 @@ export type MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding2",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
5
|
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",
|
"name": "InterestRateParams",
|
||||||
"type": {
|
"type": {
|
||||||
|
@ -3814,13 +3727,22 @@ export type MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
5
|
5
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
64
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -3860,13 +3782,22 @@ export type MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
2
|
2
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
64
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -3881,7 +3812,7 @@ export type MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -3946,6 +3877,15 @@ export type MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "takerQuoteLots",
|
"name": "takerQuoteLots",
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
64
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -3962,7 +3902,7 @@ export type MangoV4 = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved1",
|
"name": "padding1",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -3975,7 +3915,7 @@ export type MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved2",
|
"name": "padding2",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -3990,6 +3930,15 @@ export type MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "orderId",
|
"name": "orderId",
|
||||||
"type": "i128"
|
"type": "i128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
64
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -4022,7 +3971,7 @@ export type MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
84
|
92
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4063,7 +4012,7 @@ export type MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
199
|
207
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5929,109 +5878,7 @@ export const IDL: MangoV4 = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "flashLoan",
|
"name": "flashLoanBegin",
|
||||||
"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",
|
|
||||||
"accounts": [
|
"accounts": [
|
||||||
{
|
{
|
||||||
"name": "group",
|
"name": "group",
|
||||||
|
@ -6062,7 +5909,7 @@ export const IDL: MangoV4 = {
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "flashLoan3End",
|
"name": "flashLoanEnd",
|
||||||
"accounts": [
|
"accounts": [
|
||||||
{
|
{
|
||||||
"name": "account",
|
"name": "account",
|
||||||
|
@ -7725,7 +7572,7 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -7736,6 +7583,15 @@ export const IDL: MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "bankNum",
|
"name": "bankNum",
|
||||||
"type": "u64"
|
"type": "u64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
128
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -7804,7 +7660,7 @@ export const IDL: MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
8
|
128
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7860,7 +7716,7 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -7876,6 +7732,15 @@ export const IDL: MangoV4 = {
|
||||||
"name": "netSettled",
|
"name": "netSettled",
|
||||||
"type": "f32"
|
"type": "f32"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
256
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "padding1",
|
"name": "padding1",
|
||||||
"type": "u32"
|
"type": "u32"
|
||||||
|
@ -7941,7 +7806,7 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "padding1",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -7988,13 +7853,22 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding2",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
6
|
6
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
128
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -8027,7 +7901,7 @@ export const IDL: MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
8
|
128
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8089,6 +7963,15 @@ export const IDL: MangoV4 = {
|
||||||
1024
|
1024
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
128
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -8111,7 +7994,7 @@ export const IDL: MangoV4 = {
|
||||||
{
|
{
|
||||||
"defined": "AnyEvent"
|
"defined": "AnyEvent"
|
||||||
},
|
},
|
||||||
512
|
488
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8139,7 +8022,7 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "padding1",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -8307,13 +8190,22 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding2",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
6
|
6
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
128
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -8336,7 +8228,7 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "padding",
|
"name": "padding1",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -8370,13 +8262,22 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding2",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
5
|
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",
|
"name": "InterestRateParams",
|
||||||
"type": {
|
"type": {
|
||||||
|
@ -8731,13 +8593,22 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u8"
|
"type": "u8"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
5
|
5
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
64
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -8777,13 +8648,22 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
2
|
2
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
64
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -8798,7 +8678,7 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved",
|
"name": "padding",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -8863,6 +8743,15 @@ export const IDL: MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "takerQuoteLots",
|
"name": "takerQuoteLots",
|
||||||
"type": "i64"
|
"type": "i64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
64
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -8879,7 +8768,7 @@ export const IDL: MangoV4 = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved1",
|
"name": "padding1",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -8892,7 +8781,7 @@ export const IDL: MangoV4 = {
|
||||||
"type": "u16"
|
"type": "u16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "reserved2",
|
"name": "padding2",
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
|
@ -8907,6 +8796,15 @@ export const IDL: MangoV4 = {
|
||||||
{
|
{
|
||||||
"name": "orderId",
|
"name": "orderId",
|
||||||
"type": "i128"
|
"type": "i128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reserved",
|
||||||
|
"type": {
|
||||||
|
"array": [
|
||||||
|
"u8",
|
||||||
|
64
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -8939,7 +8837,7 @@ export const IDL: MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
84
|
92
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8980,7 +8878,7 @@ export const IDL: MangoV4 = {
|
||||||
"type": {
|
"type": {
|
||||||
"array": [
|
"array": [
|
||||||
"u8",
|
"u8",
|
||||||
199
|
207
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ const MANGO_MAINNET_PAYER_KEYPAIR =
|
||||||
'/Users/tylershipe/.config/solana/deploy.json';
|
'/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
|
// NOTE: we assume that ATA for source and target already exist for wallet
|
||||||
async function main() {
|
async function main() {
|
||||||
|
@ -142,7 +142,7 @@ async function main() {
|
||||||
);
|
);
|
||||||
// 1. build flash loan end ix
|
// 1. build flash loan end ix
|
||||||
const flashLoadnEndIx = await client.program.methods
|
const flashLoadnEndIx = await client.program.methods
|
||||||
.flashLoan3End()
|
.flashLoanEnd()
|
||||||
.accounts({
|
.accounts({
|
||||||
account: mangoAccount.publicKey,
|
account: mangoAccount.publicKey,
|
||||||
owner: (client.program.provider as AnchorProvider).wallet.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
|
// 2. build flash loan start ix, add end ix as a post ix
|
||||||
try {
|
try {
|
||||||
res = await client.program.methods
|
res = await client.program.methods
|
||||||
.flashLoan3Begin([
|
.flashLoanBegin([
|
||||||
new BN(sourceAmount),
|
new BN(sourceAmount),
|
||||||
new BN(
|
new BN(
|
||||||
0,
|
0,
|
||||||
|
@ -198,7 +198,7 @@ async function main() {
|
||||||
group: group.publicKey,
|
group: group.publicKey,
|
||||||
// for observing ixs in the entire tx,
|
// 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. 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,
|
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||||
})
|
})
|
||||||
.remainingAccounts([
|
.remainingAccounts([
|
Loading…
Reference in New Issue