542 lines
21 KiB
Rust
542 lines
21 KiB
Rust
use crate::accounts_ix::*;
|
|
use crate::accounts_zerocopy::*;
|
|
use crate::error::*;
|
|
use crate::group_seeds;
|
|
use crate::health::*;
|
|
use crate::logs::{emit_stack, FlashLoanLogV3, FlashLoanTokenDetailV3, TokenBalanceLog};
|
|
use crate::state::*;
|
|
use crate::util::clock_now;
|
|
|
|
use anchor_lang::prelude::*;
|
|
use anchor_lang::solana_program::sysvar::instructions as tx_instructions;
|
|
use anchor_lang::Discriminator;
|
|
use anchor_spl::associated_token::AssociatedToken;
|
|
use anchor_spl::token::{self, TokenAccount};
|
|
use fixed::types::I80F48;
|
|
|
|
/// The `loan_amounts` argument lists the amount to be loaned from each bank/vault and
|
|
/// the order matches the order of bank accounts.
|
|
pub fn flash_loan_begin<'key, 'accounts, 'remaining, 'info>(
|
|
program_id: &Pubkey,
|
|
account_ai: &AccountLoader<'info, MangoAccountFixed>,
|
|
owner_pk: &Pubkey,
|
|
instructions_ai: &AccountInfo<'info>,
|
|
token_program_ai: &AccountInfo<'info>,
|
|
remaining_accounts: &[AccountInfo<'info>],
|
|
loan_amounts: Vec<u64>,
|
|
) -> Result<()> {
|
|
let num_loans = loan_amounts.len();
|
|
require_gt!(num_loans, 0);
|
|
|
|
// Loans of 0 are acceptable and common: Users often want to loan some of token A,
|
|
// nothing of token B, swap A to B and then deposit the gains.
|
|
|
|
let account = account_ai.load_full_mut()?;
|
|
|
|
// account constraint #1
|
|
require!(
|
|
account.fixed.is_owner_or_delegate(*owner_pk),
|
|
MangoError::SomeError
|
|
);
|
|
|
|
require_eq!(remaining_accounts.len(), 3 * num_loans + 1);
|
|
let banks = &remaining_accounts[..num_loans];
|
|
let vaults = &remaining_accounts[num_loans..2 * num_loans];
|
|
let token_accounts = &remaining_accounts[2 * num_loans..3 * num_loans];
|
|
let group_ai = &remaining_accounts[3 * num_loans];
|
|
|
|
let group_al = AccountLoader::<Group>::try_from(group_ai)?;
|
|
let group = group_al.load()?;
|
|
require!(
|
|
group.is_ix_enabled(IxGate::FlashLoan),
|
|
MangoError::IxIsDisabled
|
|
);
|
|
|
|
let group_seeds = group_seeds!(group);
|
|
let seeds = [&group_seeds[..]];
|
|
|
|
// This instruction does not currently deal with:
|
|
// - borrowing twice from the same bank
|
|
// - borrowing from two different banks for the same token
|
|
// Hence we collect all token_indexes and ensure each appears only once.
|
|
let mut seen_token_indexes = Vec::with_capacity(num_loans);
|
|
|
|
// 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, group_ai.key());
|
|
require_keys_eq!(bank.vault, *vault_ai.key);
|
|
|
|
require_msg!(
|
|
!seen_token_indexes.contains(&bank.token_index),
|
|
"each loan must be for a unique token_index"
|
|
);
|
|
seen_token_indexes.push(bank.token_index);
|
|
|
|
let vault = Account::<TokenAccount>::try_from(vault_ai)?;
|
|
let token_account = Account::<TokenAccount>::try_from(token_account_ai)?;
|
|
|
|
require_keys_eq!(token_account.mint, bank.mint);
|
|
|
|
// This check is likely unnecessary
|
|
require_keys_neq!(token_account.owner, group_ai.key());
|
|
|
|
require_eq!(bank.flash_loan_approved_amount, 0);
|
|
require_eq!(bank.flash_loan_token_account_initial, u64::MAX);
|
|
bank.flash_loan_approved_amount = *amount;
|
|
bank.flash_loan_token_account_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 vault.amount < *amount {
|
|
return err!(MangoError::InsufficentBankVaultFunds).with_context(|| {
|
|
format!(
|
|
"bank vault {} does not have enough tokens, need {} but have {}",
|
|
vault_ai.key, amount, vault.amount
|
|
)
|
|
});
|
|
}
|
|
|
|
let transfer_ctx = CpiContext::new(
|
|
token_program_ai.clone(),
|
|
token::Transfer {
|
|
from: vault_ai.clone(),
|
|
to: token_account_ai.clone(),
|
|
authority: group_ai.clone(),
|
|
},
|
|
)
|
|
.with_signer(&seeds);
|
|
token::transfer(transfer_ctx, *amount)?;
|
|
}
|
|
}
|
|
|
|
// Check if the other instructions in the transactions are compatible
|
|
{
|
|
let ixs = instructions_ai;
|
|
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!(
|
|
¤t_ix.program_id == 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()),
|
|
};
|
|
|
|
if account.fixed.is_delegate(*owner_pk) {
|
|
require_msg!(
|
|
ix.program_id == AssociatedToken::id()
|
|
|| ix.program_id == jupiter_mainnet_3::ID
|
|
|| ix.program_id == jupiter_mainnet_4::ID
|
|
|| ix.program_id == jupiter_mainnet_6::ID
|
|
|| ix.program_id == compute_budget::ID
|
|
|| ix.program_id == crate::id(),
|
|
"delegate is only allowed to pass in ixs to ATA or Jupiter v3/v4/v6 programs, passed ({})", ix.program_id
|
|
);
|
|
}
|
|
|
|
// Check that the mango program key is not used
|
|
if ix.program_id == crate::id() {
|
|
// must be the FlashLoanEnd instruction
|
|
require!(
|
|
ix.data[0..8] == crate::instruction::FlashLoanEndV2::discriminator(),
|
|
MangoError::SomeError
|
|
);
|
|
// the correct number of loans is passed to the End instruction
|
|
require_eq!(ix.data[8] as usize, num_loans);
|
|
|
|
require_msg!(
|
|
account_ai.key() == ix.accounts[0].pubkey,
|
|
"the mango account passed to FlashLoanBegin and End must match"
|
|
);
|
|
|
|
// check that the same vaults and token accounts are passed
|
|
let begin_accounts = &remaining_accounts[num_loans..];
|
|
let end_accounts = &ix.accounts[ix.accounts.len() - begin_accounts.len()..];
|
|
for (begin_account, end_account) in begin_accounts.iter().zip(end_accounts.iter()) {
|
|
require_msg!(*begin_account.key == end_account.pubkey, "the trailing vault, token and group accounts passed to FlashLoanBegin and End must match, found {} on begin and {} on end", begin_account.key, end_account.pubkey);
|
|
}
|
|
|
|
// No need to check any instructions after the end instruction.
|
|
// "Duplicate FlashLoanEnd" is guarded against the same way as "End without Begin":
|
|
// The End instruction requires at least one bank-vault pair and that bank
|
|
// must have flash_loan_token_account_initial set - which only happens in Begin.
|
|
found_end = true;
|
|
break;
|
|
} 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(())
|
|
}
|
|
|
|
pub fn flash_loan_swap_begin<'key, 'accounts, 'remaining, 'info>(
|
|
ctx: Context<'key, 'accounts, 'remaining, 'info, FlashLoanSwapBegin<'info>>,
|
|
loan_amount: u64,
|
|
) -> Result<()> {
|
|
// Create missing token accounts if needed. We do this here because
|
|
// it uses up fewer tx bytes than emitting the two create-idempotent instructions
|
|
// separately. Primarily because top-level ix program addresses can't be in
|
|
// an address lookup table.
|
|
|
|
// Remaining accounts are banks, vaults, token accounts, group
|
|
let rlen = ctx.remaining_accounts.len();
|
|
require_eq!(rlen, 2 + 2 + 2 + 1);
|
|
{
|
|
let input_account = &ctx.remaining_accounts[rlen - 3];
|
|
let ctx = CpiContext::new(
|
|
ctx.accounts.associated_token_program.to_account_info(),
|
|
anchor_spl::associated_token::Create {
|
|
payer: ctx.accounts.owner.to_account_info(),
|
|
associated_token: input_account.clone(),
|
|
authority: ctx.accounts.owner.to_account_info(),
|
|
mint: ctx.accounts.input_mint.to_account_info(),
|
|
system_program: ctx.accounts.system_program.to_account_info(),
|
|
token_program: ctx.accounts.token_program.to_account_info(),
|
|
},
|
|
);
|
|
anchor_spl::associated_token::create_idempotent(ctx)?;
|
|
}
|
|
{
|
|
let output_account = &ctx.remaining_accounts[rlen - 2];
|
|
let ctx = CpiContext::new(
|
|
ctx.accounts.associated_token_program.to_account_info(),
|
|
anchor_spl::associated_token::Create {
|
|
payer: ctx.accounts.owner.to_account_info(),
|
|
associated_token: output_account.clone(),
|
|
authority: ctx.accounts.owner.to_account_info(),
|
|
mint: ctx.accounts.output_mint.to_account_info(),
|
|
system_program: ctx.accounts.system_program.to_account_info(),
|
|
token_program: ctx.accounts.token_program.to_account_info(),
|
|
},
|
|
);
|
|
anchor_spl::associated_token::create_idempotent(ctx)?;
|
|
}
|
|
|
|
flash_loan_begin(
|
|
ctx.program_id,
|
|
&ctx.accounts.account,
|
|
ctx.accounts.owner.key,
|
|
&ctx.accounts.instructions,
|
|
&ctx.accounts.token_program,
|
|
ctx.remaining_accounts,
|
|
vec![loan_amount, 0],
|
|
)?;
|
|
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>>,
|
|
num_loans: u8,
|
|
flash_loan_type: FlashLoanType,
|
|
) -> Result<()> {
|
|
require_gt!(num_loans, 0);
|
|
|
|
// FlashLoanEnd can only be called in the same tx as a FlashLoanBegin because:
|
|
// - FlashLoanBegin checks for a matching FlashLoanEnd in the same tx
|
|
// - FlashLoanBegin sets flash_loan_token_account_initial on a bank, which is
|
|
// validated below. (and there must be at least one bank-vault-token account triple)
|
|
|
|
let mut account = ctx.accounts.account.load_full_mut()?;
|
|
|
|
// account constraint #1
|
|
require!(
|
|
account.fixed.is_owner_or_delegate(ctx.accounts.owner.key()),
|
|
MangoError::SomeError
|
|
);
|
|
|
|
let group = account.fixed.group;
|
|
|
|
let remaining_len = ctx.remaining_accounts.len();
|
|
let group_ai = &ctx.remaining_accounts[remaining_len - 1];
|
|
require_keys_eq!(group, group_ai.key());
|
|
|
|
// Find index at which vaults start
|
|
let vaults_len: usize = num_loans.into();
|
|
let vaults_index = remaining_len - 2 * vaults_len - 1;
|
|
|
|
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..vaults_index + 2 * vaults_len];
|
|
|
|
// Verify that each mentioned vault has a bank in the health accounts
|
|
let mut vaults_with_banks = vec![false; vaults.len()];
|
|
|
|
// Biggest flash_loan_swap_fee_rate over all involved banks
|
|
let mut max_swap_fee_rate = 0.0f32;
|
|
|
|
// 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,
|
|
};
|
|
require_keys_eq!(bank.group, group);
|
|
|
|
// 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)?;
|
|
|
|
// The token account could have been re-initialized for a different mint
|
|
require_keys_eq!(token_account.mint, bank.mint);
|
|
|
|
// 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.
|
|
// This check guarantees that FlashLoanBegin was called on this bank.
|
|
require_neq!(bank.flash_loan_token_account_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.ensure_token_position(bank.token_index)?;
|
|
|
|
// Transfer any excess over the initial 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_token_account_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_token_account_initial;
|
|
token::transfer(transfer_ctx, repay)?;
|
|
|
|
let repay = I80F48::from(repay);
|
|
change += repay;
|
|
}
|
|
|
|
max_swap_fee_rate = max_swap_fee_rate.max(bank.flash_loan_swap_fee_rate);
|
|
|
|
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_typed!(
|
|
has_bank,
|
|
MangoError::InvalidBank,
|
|
"missing bank for vault index {}, address {}",
|
|
i,
|
|
vaults[i].key
|
|
);
|
|
}
|
|
|
|
match flash_loan_type {
|
|
FlashLoanType::Unknown => {}
|
|
FlashLoanType::Swap | FlashLoanType::SwapWithoutFee => {
|
|
require_msg!(
|
|
changes.len() == 2,
|
|
"when flash_loan_type is Swap or SwapWithoutFee there must be exactly 2 token vault changes"
|
|
)
|
|
}
|
|
}
|
|
|
|
// Check health before balance adjustments
|
|
// The vault-to-bank matching above ensures that the banks for the affected tokens are available.
|
|
let (now_ts, now_slot) = clock_now();
|
|
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
|
health_ais,
|
|
&account.borrow(),
|
|
now_slot,
|
|
)?;
|
|
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
|
|
&account.borrow(),
|
|
&retriever,
|
|
now_ts,
|
|
)?;
|
|
|
|
let pre_init_health = account.check_health_pre(&health_cache)?;
|
|
|
|
// Prices for logging and net borrow checks
|
|
//
|
|
// This also verifies that all affected banks/oracles are available in health_cache:
|
|
// That is essential to avoid issues around withdrawing tokens when init health is negative
|
|
// (similar issue to token_withdraw)
|
|
let mut oracle_prices = vec![];
|
|
for change in &changes {
|
|
let (_, oracle_price) = retriever.bank_and_oracle(
|
|
&account.fixed.group,
|
|
change.bank_index,
|
|
change.token_index,
|
|
)?;
|
|
// Sanity check
|
|
health_cache.token_info_index(change.token_index)?;
|
|
|
|
oracle_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, oracle_price) in changes.iter().zip(oracle_prices.iter()) {
|
|
let mut bank = health_ais[change.bank_index].load_mut::<Bank>()?;
|
|
|
|
let position = account.token_position_mut_by_raw_index(change.raw_token_index);
|
|
let native = position.native(&bank);
|
|
|
|
let approved_amount_u64 = bank.flash_loan_approved_amount;
|
|
let approved_amount = I80F48::from(approved_amount_u64);
|
|
|
|
let loan = if native.is_positive() {
|
|
(approved_amount - native).max(I80F48::ZERO)
|
|
} else {
|
|
approved_amount
|
|
};
|
|
|
|
let loan_origination_fee = loan * bank.loan_origination_fee_rate;
|
|
bank.collected_fees_native += loan_origination_fee;
|
|
|
|
let swap_fee = if change.amount < 0 && flash_loan_type == FlashLoanType::Swap {
|
|
-change.amount * I80F48::from_num(max_swap_fee_rate)
|
|
} else {
|
|
I80F48::ZERO
|
|
};
|
|
bank.collected_fees_native += swap_fee;
|
|
|
|
let change_amount = change.amount - loan_origination_fee - swap_fee;
|
|
let native_after_change = native + change_amount;
|
|
if bank.are_deposits_reduce_only() {
|
|
require!(
|
|
native_after_change < 1 || native_after_change <= native,
|
|
MangoError::TokenInReduceOnlyMode
|
|
);
|
|
}
|
|
if bank.are_borrows_reduce_only() {
|
|
require!(
|
|
native_after_change >= native || native_after_change >= 0,
|
|
MangoError::TokenInReduceOnlyMode
|
|
);
|
|
}
|
|
|
|
let is_active = bank.change_without_fee(
|
|
position,
|
|
change_amount,
|
|
Clock::get()?.unix_timestamp.try_into().unwrap(),
|
|
)?;
|
|
if !is_active {
|
|
deactivated_token_positions.push(change.raw_token_index);
|
|
}
|
|
|
|
if change_amount < 0 && native_after_change < 0 {
|
|
bank.enforce_max_utilization_on_borrow()?;
|
|
bank.check_net_borrows(*oracle_price)?;
|
|
} else {
|
|
bank.enforce_borrows_lte_deposits()?;
|
|
}
|
|
|
|
if change_amount > 0 && native_after_change > 0 {
|
|
bank.check_deposit_and_oo_limit()?;
|
|
}
|
|
|
|
bank.flash_loan_approved_amount = 0;
|
|
bank.flash_loan_token_account_initial = u64::MAX;
|
|
|
|
token_loan_details.push(FlashLoanTokenDetailV3 {
|
|
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: oracle_price.to_bits(),
|
|
swap_fee: swap_fee.to_bits(),
|
|
approved_amount: approved_amount_u64,
|
|
});
|
|
|
|
emit_stack(TokenBalanceLog {
|
|
mango_group: group.key(),
|
|
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(),
|
|
});
|
|
}
|
|
|
|
emit_stack(FlashLoanLogV3 {
|
|
mango_group: group.key(),
|
|
mango_account: ctx.accounts.account.key(),
|
|
flash_loan_type,
|
|
token_loan_details,
|
|
});
|
|
|
|
// Check health after account position changes
|
|
let retriever = new_fixed_order_account_retriever_with_optional_banks(
|
|
health_ais,
|
|
&account.borrow(),
|
|
now_slot,
|
|
)?;
|
|
let health_cache = new_health_cache_skipping_missing_banks_and_bad_oracles(
|
|
&account.borrow(),
|
|
&retriever,
|
|
now_ts,
|
|
)?;
|
|
account.check_health_post(&health_cache, pre_init_health)?;
|
|
|
|
// Deactivate inactive token accounts after health check
|
|
for raw_token_index in deactivated_token_positions {
|
|
account.deactivate_token_position_and_log(raw_token_index, ctx.accounts.account.key());
|
|
}
|
|
|
|
Ok(())
|
|
}
|